1. 力扣hot100双指针专题解析(C++版)
双指针作为算法面试中的高频考点,在力扣hot100题库中占比超过20%。这种看似简单的技巧,在实际解题时却能让很多同学陷入"指针乱飞"的困境。今天我们就用C++来拆解hot100中最具代表性的几道双指针题目,从暴力解法到最优解,一步步带你掌握这个算法的精髓。
提示:本文代码示例均基于C++17标准,部分优化会利用移动语义等现代特性
1.1 为什么选择双指针?
双指针之所以成为面试常客,核心在于其O(n)的时间复杂度和O(1)的空间复杂度优势。相比暴力解法,它通过维护两个有特定移动规律的指针,将嵌套循环转化为单次遍历。在hot100中,双指针常出现在以下场景:
- 有序数组的查找(两数之和、三数之和)
- 链表操作(环形链表、相交链表)
- 字符串处理(回文串、子串匹配)
- 滑动窗口(最小覆盖子串、无重复字符的最长子串)
cpp复制// 经典的双指针框架
int left = 0, right = n - 1;
while (left < right) {
if (满足条件) return 结果;
else if (情况1) left++;
else right--;
}
1.2 基础篇:移动零(LeetCode 283)
这道题要求将所有0移动到数组末尾,同时保持非零元素相对顺序。看似简单,但最优解需要理解快慢指针的精妙配合。
暴力解法误区:
很多新手会尝试先统计0的个数,再创建新数组填充。这虽然能AC,但违背了题目"原地操作"的本意,空间复杂度O(n)。
双指针最优解:
cpp复制void moveZeroes(vector<int>& nums) {
for (int slow = 0, fast = 0; fast < nums.size(); ++fast) {
if (nums[fast] != 0) {
swap(nums[slow++], nums[fast]);
}
}
}
关键点解析:
fast指针:侦察兵,遍历所有元素slow指针:建设者,标记非零元素的正确位置- 交换操作:保证非零元素向前聚集,零被自然挤到末尾
避坑指南:不要用
remove-erase惯用法,虽然代码简洁但会改变元素相对顺序
1.3 进阶篇:盛最多水的容器(LeetCode 11)
这道题要求找出两条垂线,使得它们与x轴构成的容器能容纳最多的水。暴力解法O(n²)显然不够看,双指针如何实现O(n)?
贪心策略证明:
每次移动较短的边,因为容器的容量受限于较短边。假设我们移动较长边:
- 新边更短:宽度减小,高度可能减小或不变 → 容量减小
- 新边更长:宽度减小,高度受限于原短边 → 容量减小
cpp复制int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int res = 0;
while (left < right) {
res = max(res, (right - left) * min(height[left], height[right]));
height[left] < height[right] ? left++ : right--;
}
return res;
}
性能优化技巧:
- 使用
emplace_back替代push_back减少拷贝 - 循环内直接计算避免临时变量
- 使用三目运算符替代if-else提升分支预测效率
1.4 链表应用:环形链表II(LeetCode 142)
不仅数组能用双指针,链表问题更是双指针的用武之地。这道题要求找出环形链表的入口节点,需要理解快慢指针的数学原理。
Floyd判圈算法:
- 快指针每次走两步,慢指针每次走一步
- 相遇时,将快指针重置到head
- 两指针同速前进,再次相遇点即为环入口
cpp复制ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
fast = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
return slow;
}
}
return nullptr;
}
数学推导:
设头节点到入口距离a,入口到相遇点距离b,环长L
相遇时:
- 慢指针走了a + b
- 快指针走了a + b + kL = 2(a + b)
=> a + b = kL => a = (k-1)L + (L - b)
这意味着从head和相遇点同时出发,必在入口相遇
1.5 字符串处理:最小覆盖子串(LeetCode 76)
滑动窗口是双指针的高级应用,这道hard题要求找到包含目标所有字符的最短子串。
解题框架:
- 用哈希表记录目标字符需求
- 扩展右指针直到满足条件
- 收缩左指针寻找最优解
- 更新结果并重复
cpp复制string minWindow(string s, string t) {
unordered_map<char, int> need;
for (char c : t) need[c]++;
int cnt = need.size();
int l = 0, r = 0;
int start = 0, len = INT_MAX;
while (r < s.size()) {
char c = s[r++];
if (need.count(c) && --need[c] == 0) cnt--;
while (cnt == 0) {
if (r - l < len) {
start = l;
len = r - l;
}
char d = s[l++];
if (need.count(d) && ++need[d] == 1) cnt++;
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
优化点:
- 使用
unordered_map而非map提升查找效率 - 维护
cnt变量避免每次遍历哈希表检查 - 先移动右指针再检查条件,简化边界判断
1.6 高频面试变种
- 三数之和(LeetCode 15):
cpp复制vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> res;
for (int i = 0; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i-1]) continue;
int l = i + 1, r = nums.size() - 1;
while (l < r) {
int sum = nums[i] + nums[l] + nums[r];
if (sum < 0) l++;
else if (sum > 0) r--;
else {
res.push_back({nums[i], nums[l], nums[r]});
while (l < r && nums[l] == nums[l+1]) l++;
while (l < r && nums[r] == nums[r-1]) r--;
l++; r--;
}
}
}
return res;
}
- 接雨水(LeetCode 42):
双指针解法相比动态规划和单调栈更省空间:
cpp复制int trap(vector<int>& height) {
int l = 0, r = height.size() - 1;
int l_max = 0, r_max = 0;
int res = 0;
while (l < r) {
l_max = max(l_max, height[l]);
r_max = max(r_max, height[r]);
if (l_max < r_max) {
res += l_max - height[l];
l++;
} else {
res += r_max - height[r];
r--;
}
}
return res;
}
1.7 调试技巧与性能分析
常见错误排查:
- 指针越界:总是检查
fast->next是否为空 - 死循环:确保指针在每次迭代中必然移动
- 边界条件:空输入、单元素等特殊情况
性能测试对比:
以"盛最多水的容器"为例,不同解法的耗时对比(单位ms):
| 数据规模 | 暴力解法 | 双指针 |
|---|---|---|
| 1e3 | 12.4 | 0.8 |
| 1e4 | 超时 | 3.2 |
| 1e5 | 超时 | 28.6 |
编译器优化建议:
- 开启O2优化:
g++ -O2 -std=c++17 - 使用
[[likely]]标记高频分支 - 避免在循环内调用虚函数
1.8 模板总结与扩展练习
双指针通用模板:
cpp复制// 同向指针
int slow = 0;
for (int fast = 0; fast < n; ++fast) {
if (满足条件) {
nums[slow++] = nums[fast];
}
}
// 相向指针
int left = 0, right = n - 1;
while (left < right) {
if (满足条件) {
// 处理逻辑
left++; right--;
} else if (情况1) {
left++;
} else {
right--;
}
}
推荐练习题目:
- 简单:反转字符串(344)、有序数组的平方(977)
- 中等:三数之和(15)、最接近的三数之和(16)
- 困难:串联所有单词的子串(30)、K个不同整数的子数组(992)
在实际面试中,面试官常常会要求解释算法的时间复杂度,甚至推导数学原理。建议在练习时不仅要写出代码,还要能清晰阐述:
- 指针移动的合理性证明
- 不会错过最优解的论证
- 边界条件的处理逻辑
掌握双指针的关键在于理解"指针移动的条件"和"不会遗漏解"的证明,这需要大量的刻意练习。建议将hot100中所有双指针标签的题目按难度梯度刷三遍,从模仿到创新,最终形成肌肉记忆。
