这一篇开始,我们正式进入 “经典题实战”

前面几篇学的东西,这一篇都会用上:

  • vector

  • string

  • unordered_map

  • sort

  • 双指针

  • 滑动窗口

你会发现,前面那些基础不是白学的,到了做题这里,基本都会串起来。

这一篇我不只是给你代码,我更想带你建立一个习惯:

做题的时候,先看题型信号,再决定用什么方法。


1. 两数之和

1.1 题目大意

给定一个整数数组 nums 和一个目标值 target,请你在数组中找出和为目标值的那两个整数,并返回它们的下标。

比如:

nums = [2,7,11,15], target = 9

答案就是:

[0,1]

因为:

2 + 7 = 9

1.2 这题第一眼该想到什么

这题最关键的信号是:

  • 要找两个数

  • 和等于 target

  • 要求尽量快

  • 返回的是下标

这时候最优先想到的就是:

哈希表

因为我们可以一边遍历,一边看看“另一个数”有没有出现过。

比如当前是 x,那我就想找:

target - x

如果它已经在哈希表里出现了,那答案就出来了。


1.3 暴力做法

最容易想到的就是两层循环:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        for (int i = 0; i < nums.size(); i++) {
            for (int j = i + 1; j < nums.size(); j++) {
                if (nums[i] + nums[j] == target) {
                    return {i, j};
                }
            }
        }
        return {};
    }
};

这个方法能做出来,但是时间复杂度是:

O(n^2)

数据大了就不太行。


1.4 正确高频做法:哈希表

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> mp;

        for (int i = 0; i < nums.size(); i++) {
            int need = target - nums[i];

            if (mp.find(need) != mp.end()) {
                return {mp[need], i};
            }

            mp[nums[i]] = i;
        }

        return {};
    }
};

1.5 代码怎么理解

比如:

nums = [2,7,11,15], target = 9

一开始哈希表是空的。

第一步

遍历到 2

需要找:

9 - 2 = 7

哈希表里没有 7,那就把 2 放进去:

mp[2] = 0

第二步

遍历到 7

需要找:

9 - 7 = 2

发现哈希表里已经有 2 了,而且它的位置是 0。

所以答案就是:

{0, 1}

1.6 这题你要记住的核心

看到这种题,优先想:

  • 判存在

  • 查某个数是否出现过

  • 一边遍历一边找匹配

这就是哈希表的味道。


1.7 这题的模板意义

两数之和不仅是一道题,它还是很多题的起点。

你要记住这个模型:

哈希表模型

  • 遍历当前值 x

  • 想办法算出“我需要谁”

  • 去哈希表里查这个“谁”在不在

  • 在的话直接出答案

  • 不在就把当前值记录进去

后面很多题都是这个思路变形。


2. 移动零

2.1 题目大意

给定一个数组 nums,将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。

比如:

[0,1,0,3,12]

变成:

[1,3,12,0,0]

2.2 这题第一眼该想到什么

信号特别明显:

  • 原地修改数组

  • 保持相对顺序

  • 把有效元素往前挪

这就是典型的:

快慢指针


2.3 做法

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int slow = 0;

        for (int fast = 0; fast < nums.size(); fast++) {
            if (nums[fast] != 0) {
                swap(nums[slow], nums[fast]);
                slow++;
            }
        }
    }
};

2.4 代码怎么理解

你可以这样理解:

  • fast:负责往后扫,找非零元素

  • slow:负责记录“下一个非零元素该放哪”

也就是说:

fast 找值,slow 放值


比如:

[0,1,0,3,12]

一开始

  • slow = 0

fast = 0

  • nums[0] = 0

  • 不处理

fast = 1

  • nums[1] = 1

  • 不是 0

  • nums[slow] 交换

  • 交换后数组变成:

[1,0,0,3,12]

然后:

slow++

2.5 这题的本质

这题本质上不是“移动 0”,而是:

把所有非零元素稳定地放到前面。

剩下的自然就是 0 了。

这个想法特别重要。

以后碰到“删除元素”“保留满足条件的元素”这种题,也经常这么想。


2.6 这题要记住的核心

快慢指针常见模型

  • fast 扫描整个数组

  • slow 维护处理好的结果区间

  • 满足条件的元素,往前放


3. 盛最多水的容器

3.1 题目大意

给定一个长度为 n 的整数数组 height
n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。


3.2 暴力怎么想

枚举所有两根柱子:

  • 左边一根

  • 右边一根

  • 算面积

  • 取最大值

代码思路就是两层循环,复杂度:

O(n^2)

能写,但是不够优秀。


3.3 这题第一眼该想到什么

这题的特征是:

  • 从左右两边选

  • 两个位置一起决定答案

  • 可以从两端往中间逼近

这就是典型的:

左右指针


3.4 最关键的思路

面积公式:

面积 = (right - left) * min(height[left], height[right])

也就是说,面积由两部分决定:

  • 宽度

  • 较短的那根柱子

而真正限制装水量的,是:

短板

如果左边短,右边高,那你去动右边,其实不太有用。
因为宽度变小了,但短板可能还是左边。

所以更合理的是:

移动更短的那一边

因为只有这样,才有机会让短板变高。


3.5 代码

class Solution {
public:
    int maxArea(vector<int>& height) {
        int left = 0;
        int right = height.size() - 1;
        int ans = 0;

        while (left < right) {
            int area = (right - left) * min(height[left], height[right]);
            ans = max(ans, area);

            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }

        return ans;
    }
};

3.6 这题要学到什么

这题最重要的不是把代码背下来,而是学会这种分析方式:

左右指针题常见思路

  • 当前答案由两端共同决定

  • 每次移动一个指针

  • 移动谁,是根据题目的性质决定的

  • 一般移动那个“限制答案”的位置


4. 三数之和

4.1 题目大意

给你一个整数数组 nums,判断是否存在三个元素 a、b、c,使得:

a + b + c = 0

找出所有不重复的三元组。

比如:

nums = [-1,0,1,2,-1,-4]

答案是:

[[-1,-1,2],[-1,0,1]]

4.2 这题第一眼该想到什么

这题是 Hot100 里非常经典的一道题。

它的信号是:

  • 要找三个数

  • 和等于某个值

  • 还要去重

这时候一个很经典的套路就是:

排序 + 固定一个数 + 双指针


4.3 为什么不能直接三层循环

三层循环当然能枚举所有情况,但复杂度是:

O(n^3)

太慢了。

所以我们得想办法降一维。


4.4 核心思路

先排序。

然后枚举第一个数 nums[i],接下来问题就变成:

在后面的区间里找两个数

使得:

nums[left] + nums[right] = -nums[i]

这就变成了熟悉的双指针问题。


4.5 代码

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ans;
        sort(nums.begin(), nums.end());

        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] > 0) break;

            if (i > 0 && nums[i] == nums[i - 1]) continue;

            int left = i + 1;
            int right = nums.size() - 1;

            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];

                if (sum < 0) {
                    left++;
                } else if (sum > 0) {
                    right--;
                } else {
                    ans.push_back({nums[i], nums[left], nums[right]});

                    while (left < right && nums[left] == nums[left + 1]) left++;
                    while (left < right && nums[right] == nums[right - 1]) right--;

                    left++;
                    right--;
                }
            }
        }

        return ans;
    }
};

4.6 这题最难的地方在哪

这题最容易卡人的地方有两个:

第一个:为什么要排序

因为排序之后,双指针才有意义。

你才能根据和的大小来决定:

  • 和太小,左指针右移

  • 和太大,右指针左移


第二个:怎么去重

这题最烦的就是重复答案。

去重分两层。

第一层:固定的第一个数去重
if (i > 0 && nums[i] == nums[i - 1]) continue;

意思是:

如果这次固定的数和前一次一样,那就跳过。


第二层:找到答案后,左右指针也去重
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;

意思是:

当前这个三元组已经收进答案了,那后面重复的值就别再重复算了。


4.7 这题的思维意义很大

这道题你不一定一次就完全吃透,但一定要建立这个意识:

很多复杂题,都是“降维”

  • 三数之和

  • 先固定一个

  • 剩下变成两数问题

  • 再用双指针解决

这是非常经典的套路。


5. 无重复字符的最长子串

5.1 题目大意

给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。

比如:

"abcabcbb"

答案是:

3

因为最长无重复子串是:

"abc"

5.2 这题第一眼该想到什么

信号特别明显:

  • 子串

  • 连续

  • 最长

  • 条件是“不重复”

这就是非常标准的:

滑动窗口


5.3 核心思路

维护一个窗口 [left, right],保证窗口内没有重复字符。

每次让 right 往右扩:

  • 把新字符放进窗口

  • 如果重复了,就不断移动 left

  • 直到窗口重新合法

  • 然后更新最大长度


5.4 代码

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> cnt;
        int left = 0;
        int ans = 0;

        for (int right = 0; right < s.size(); right++) {
            cnt[s[right]]++;

            while (cnt[s[right]] > 1) {
                cnt[s[left]]--;
                left++;
            }

            ans = max(ans, right - left + 1);
        }

        return ans;
    }
};

5.5 代码怎么理解

比如字符串:

"abcabcbb"

一开始窗口是空的。

right 走到 'a'

  • 放进去

  • 没重复

  • 更新答案

right 走到 'b'

  • 放进去

  • 没重复

  • 更新答案

right 走到 'c'

  • 放进去

  • 没重复

  • 更新答案

right 再走到 'a'

  • 这时候重复了

  • 就开始移动 left

  • 直到窗口里只有一个 a

这就是滑动窗口的核心。


5.6 这题的关键模板

你要记住这个流程:

滑动窗口四步走

  1. 右指针加入新元素

  2. 判断窗口是否合法

  3. 不合法就缩左边

  4. 合法后更新答案

这个模板非常通用。


6. 这五道题放在一起,要看出什么

这一篇不是机械记 5 道题,而是让你看出:

题目和方法之间的对应关系


6.1 两数之和

信号:

  • 查找某个值是否出现过

  • 判存在

  • 快速匹配

方法:

  • 哈希表


6.2 移动零

信号:

  • 原地修改数组

  • 保持顺序

  • 有效元素前移

方法:

  • 快慢指针


6.3 盛最多水的容器

信号:

  • 左右两边共同决定答案

  • 两端逼近

方法:

  • 左右指针


6.4 三数之和

信号:

  • 多个数求和

  • 需要去重

  • 可以先排序

方法:

  • 排序 + 固定一个数 + 双指针


6.5 无重复字符的最长子串

信号:

  • 连续子串

  • 最长

  • 动态维护条件

方法:

  • 滑动窗口 + 哈希表计数


7. 这一篇最该学会的,不是背答案

很多人刷题的时候有个问题:

  • 题做过

  • 代码也看懂了

  • 下次碰到类似题还是不会

原因通常不是因为你笨,而是因为你只记住了“这道题怎么写”,没有记住:

这道题为什么会想到这个方法

所以现在一定要养成这个习惯:

每做完一道题,都问自己三个问题。

问题 1:这题的信号是什么

比如:

  • 连续子串

  • 原地修改

  • 判存在

  • 排序后双指针


问题 2:为什么这种方法合适

比如:

  • 哈希表查找快

  • 双指针能降复杂度

  • 滑动窗口适合连续区间

  • 排序后才方便去重和移动指针


问题 3:这一题能抽象成什么模板

比如:

  • 两数匹配模板

  • 快慢指针模板

  • 左右指针模板

  • 滑动窗口模板

这个习惯一养成,你后面进步会快很多。


8. 这一篇的五道题,建议怎么复习

我建议别只是看一遍,然后就过去了。

可以这样复习。

第一遍

先把思路看懂,代码能看明白。

目标:

  • 知道这题用什么方法

  • 知道每个变量是干嘛的


第二遍

别看代码,自己试着默写。

目标:

  • 能把框架写出来

  • 哪怕细节有点错也没事


第三遍

重新回忆:

  • 这题属于什么题型

  • 为什么要用这个方法

  • 有没有模板感


第四遍

隔几天再写一次。

这时候你会发现,真正记住的东西才是你的。


9. 这一篇最容易犯的错误

9.1 两数之和

错误:

先把当前值放进哈希表,再查找。

这样有时候会把自己配给自己。

稳一点的顺序是:

  • 先查

  • 再放


9.2 移动零

错误:

只想着“把 0 往后移”,结果写得很乱。

更好的想法是:

  • 只管把非零元素放前面


9.3 盛最多水的容器

错误:

不知道为什么移动短板。

这个地方不要死记,要理解:

  • 宽度一定在缩小

  • 想让面积变大,就只能指望短板变高

  • 所以移动短板才有意义


9.4 三数之和

错误:

忘了去重。

这题不去重,答案会炸掉。


9.5 无重复字符的最长子串

错误:

窗口重复了以后,只缩一次左边。

其实不行,必须:

while (不合法)

一直缩到合法为止。


10. 这五道题对应的代码模板,建议直接背下来

10.1 哈希表判存在模板

unordered_map<int, int> mp;

for (int i = 0; i < nums.size(); i++) {
    int need = target - nums[i];
    if (mp.find(need) != mp.end()) {
        return {mp[need], i};
    }
    mp[nums[i]] = i;
}

10.2 快慢指针模板

int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
    if (满足条件) {
        swap(nums[slow], nums[fast]);
        slow++;
    }
}

10.3 左右指针模板

int left = 0;
int right = nums.size() - 1;

while (left < right) {
    if (条件满足) {
        // 更新答案
    }

    if (需要左移) {
        left++;
    } else {
        right--;
    }
}

10.4 排序 + 双指针模板

sort(nums.begin(), nums.end());

for (int i = 0; i < nums.size(); i++) {
    if (i > 0 && nums[i] == nums[i - 1]) continue;

    int left = i + 1;
    int right = nums.size() - 1;

    while (left < right) {
        int sum = nums[i] + nums[left] + nums[right];
        if (sum < target) left++;
        else if (sum > target) right--;
        else {
            // 记录答案
            // 去重
            left++;
            right--;
        }
    }
}

10.5 滑动窗口模板

unordered_map<char, int> cnt;
int left = 0;

for (int right = 0; right < s.size(); right++) {
    cnt[s[right]]++;

    while (窗口不合法) {
        cnt[s[left]]--;
        left++;
    }

    // 更新答案
}

11. 本篇总结

这五道题虽然看起来都不一样,但背后其实对应的是几个特别经典的模型:

  • 两数之和 -> 哈希表

  • 移动零 -> 快慢指针

  • 盛最多水的容器 -> 左右指针

  • 三数之和 -> 排序 + 双指针 + 去重

  • 无重复字符的最长子串 -> 滑动窗口

现在最重要的任务不是去追求做更多题,而是:

把这几个模型先练熟

因为后面很多题,都是这些模型换个皮。


12. 给自己记的几句话

可以直接记:

判存在、查匹配,先想哈希表

原地修改数组,先想快慢指针

左右两端一起决定答案,先想左右指针

连续子串求最长最短,先想滑动窗口

多个数求和,常常是排序后降维成双指针

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐