C++刷 LeetCode Hot100 笔记(五)数组题第一轮实战:两数之和、移动零、盛最多水的容器、三数之和、无重复字符的最长子串
这一篇开始,我们正式进入 “经典题实战”。
前面几篇学的东西,这一篇都会用上:
-
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 这题的关键模板
你要记住这个流程:
滑动窗口四步走
-
右指针加入新元素
-
判断窗口是否合法
-
不合法就缩左边
-
合法后更新答案
这个模板非常通用。
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. 给自己记的几句话
可以直接记:
判存在、查匹配,先想哈希表
原地修改数组,先想快慢指针
左右两端一起决定答案,先想左右指针
连续子串求最长最短,先想滑动窗口
多个数求和,常常是排序后降维成双指针
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)