【LeetCode Cookbook(C++ 描述)】双指针技巧实战:数组问题高效解法

张开发
2026/5/2 5:42:36 15 分钟阅读

分享文章

【LeetCode Cookbook(C++ 描述)】双指针技巧实战:数组问题高效解法
1. 双指针技巧入门为什么它能高效解决数组问题第一次接触双指针技巧是在刷LeetCode第27题移除元素时。当时我用暴力解法两层循环硬怼虽然通过了测试但看到时间复杂度O(n²)的惨淡结果就知道肯定有更优解。直到发现快慢指针的解法才真正体会到算法优化的美妙。双指针的核心思想很简单用两个指针协同工作减少不必要的遍历。想象你在整理书架一本本检查并移除不需要的书。暴力解法相当于每找到一本要移除的书就把后面所有书往前挪一格而快慢指针则像两个人配合一个负责检查书籍是否需要保留另一个负责把保留的书放到新位置。这种技巧特别适合处理数组的原地修改问题。比如移除特定元素LeetCode 27去重LeetCode 26移动零LeetCode 283// 快慢指针移除元素的典型实现 int removeElement(vectorint nums, int val) { int slow 0; for (int fast 0; fast nums.size(); fast) { if (nums[fast] ! val) { nums[slow] nums[fast]; } } return slow; }这个解法之所以高效是因为它把时间复杂度从O(n²)降到了O(n)。快指针fast勇往直前探路慢指针slow稳扎稳打构建新数组每个元素只被处理一次。我在实际编码中发现理解快慢指针的关键是明确fast的职责遍历所有元素slow的定位指向下一个有效元素应该存放的位置2. 快慢指针的进阶应用从数组到链表掌握了基础用法后我发现快慢指针的适用场景比想象中更广。它不仅适用于数组还能巧妙解决链表问题。比如经典的链表去重问题LeetCode 83用快慢指针可以写出极其优雅的解法。记得有次面试被要求写一个检测链表环的算法。当时我灵机一动用快慢指针模拟龟兔赛跑慢指针每次走一步快指针每次走两步 如果有环快指针最终会追上慢指针。这个思路后来才知道是Floyd判圈算法。bool hasCycle(ListNode *head) { ListNode *slow head, *fast head; while (fast fast-next) { slow slow-next; fast fast-next-next; if (slow fast) return true; } return false; }快慢指针在链表问题中展现出独特优势空间效率O(1)的额外空间时间效率通常只需遍历1-2次链表思维简洁模拟现实中的追逐过程3. 左右指针数组中的双向奔赴如果说快慢指针是同向移动的好兄弟那么左右指针就是相向而行的有缘人。我第一次体会到左右指针的威力是在解两数之和IILeetCode 167时。传统暴力解法需要O(n²)时间而左右指针只需要O(n)vectorint twoSum(vectorint numbers, int target) { int left 0, right numbers.size() - 1; while (left right) { int sum numbers[left] numbers[right]; if (sum target) return {left1, right1}; else if (sum target) left; else right--; } return {-1, -1}; }左右指针特别适合处理有序数组的问题比如二分查找本身就是左右指针的经典应用反转数组LeetCode 344有序数组的平方LeetCode 977在实际编码中我发现左右指针有几种常见模式相向而行一个从头开始一个从尾开始向中间靠拢背向而行从中间开始向两端移动异步移动根据条件决定移动哪个指针4. 滑动窗口双指针的高级形态滑动窗口是我认为双指针技巧中最精妙的应用。第一次接触是在解长度最小的子数组LeetCode 209时暴力解法直接超时而滑动窗口解法优雅高效。滑动窗口的精髓在于用left和right指针维护一个窗口先扩展right直到满足条件然后收缩left寻找最优解像蚯蚓一样在数组上蠕动前进int minSubArrayLen(int target, vectorint nums) { int left 0, right 0, sum 0; int min_len INT_MAX; for (; right nums.size(); right) { sum nums[right]; while (sum target) { min_len min(min_len, right - left 1); sum - nums[left]; } } return min_len INT_MAX ? 0 : min_len; }滑动窗口适用于解决子数组/子串问题特别是需要满足某些条件的最短/最长子数组。比如最小覆盖子串LeetCode 76无重复字符的最长子串LeetCode 3字符串排列LeetCode 567在实际应用中我发现滑动窗口有几个易错点窗口收缩条件判断结果更新的时机边界条件处理特别是字符串结束时的处理5. 双指针的边界条件与调试技巧即使理解了双指针的原理实际编码时还是会遇到各种边界问题。记得有次做移除元素题目因为没处理好slow指针的移动导致最后一个元素总是处理错误。经过多次踩坑我总结出几个调试技巧打印指针位置在循环中加入调试输出观察指针移动cout fast: fast slow: slow endl;极端测试用例空数组、单元素数组、全相同元素数组可视化跟踪在纸上画出指针移动过程常见边界陷阱包括指针越界特别是right指针空输入处理元素全部相同或全部需要移除的情况更新结果的时机不对6. 双指针与其他算法的组合应用随着刷题经验增加我发现双指针经常与其他算法搭配使用产生112的效果。比如在三数之和LeetCode 15中双指针与排序的结合堪称经典。解题思路先排序固定一个数然后用左右指针在剩余数组中寻找两数之和vectorvectorint threeSum(vectorint nums) { sort(nums.begin(), nums.end()); vectorvectorint res; for (int i 0; i nums.size(); i) { if (i 0 nums[i] nums[i-1]) continue; int left i1, right nums.size()-1; while (left right) { int sum nums[i] nums[left] nums[right]; if (sum 0) { res.push_back({nums[i], nums[left], nums[right]}); while (left right nums[left] nums[left1]) left; while (left right nums[right] nums[right-1]) right--; left; right--; } else if (sum 0) left; else right--; } } return res; }其他常见组合模式双指针哈希表解决某些查找问题双指针贪心算法如容器盛水问题双指针二分查找优化搜索范围7. 从LeetCode到实际工程双指针的实用价值很多人觉得算法题只是面试需要其实双指针思想在实际工程中大有可为。我曾经优化过一个日志处理系统用滑动窗口算法将处理时间从小时级降到分钟级。典型应用场景包括大数据处理流式数据的时间窗口分析文件比对逐行比较两个大文件内存优化原地处理数据减少拷贝实时系统滑动窗口限流算法比如实现一个简单的文件差异检测void compareFiles(ifstream file1, ifstream file2) { string line1, line2; int ptr1 0, ptr2 0; while (getline(file1, line1) getline(file2, line2)) { if (line1 ! line2) { cout 差异行: file1# ptr1 vs file2# ptr2 endl; } ptr1; ptr2; } // 处理剩余行... }在工程实践中双指针的价值体现在空间效率减少临时存储时间效率降低时间复杂度代码简洁逻辑清晰易维护8. 双指针解题的思维训练法要真正掌握双指针光刷题还不够需要系统化的思维训练。我总结了一套三步法问题分析判断是否适合双指针是否涉及顺序遍历能否通过指针移动减少计算指针定义明确每个指针的语义快慢指针谁负责遍历谁负责写入左右指针移动条件是什么移动规则确定指针如何变化什么条件下移动移动步长是多少以颜色分类问题LeetCode 75为例void sortColors(vectorint nums) { int low 0, high nums.size() - 1; int curr 0; while (curr high) { if (nums[curr] 0) { swap(nums[curr], nums[low]); } else if (nums[curr] 2) { swap(nums[curr], nums[high--]); } else { curr; } } }训练时可以尝试先写暴力解法再优化画图辅助理解指针移动对每个问题至少思考两种不同的双指针解法9. 常见误区与性能优化即使是经验丰富的开发者使用双指针时也容易陷入一些误区。我曾经在一个项目中错误地使用了快慢指针导致处理特殊case时出现bug。常见误区包括指针移动条件错误该移动时没移动不该移动时移动了边界处理不当特别是数组开始和结束时的处理过度优化为了用双指针而用反而使代码复杂性能优化建议减少不必要的操作比如在滑动窗口中避免重复计算利用有序性排序后往往能简化双指针逻辑提前终止找到解后立即返回以盛最多水的容器LeetCode 11为例优化后的解法int maxArea(vectorint height) { int left 0, right height.size() - 1; int max_area 0; while (left right) { int area min(height[left], height[right]) * (right - left); max_area max(max_area, area); if (height[left] height[right]) { left; } else { right--; } } return max_area; }关键优化点每次移动较矮的那边因为高度取决于短板即时计算并更新最大面积单次遍历O(n)时间复杂度10. 从经典题目到举一反三最后我建议通过经典题目掌握双指针的核心模式然后举一反三。以下是必刷题目清单快慢指针系列删除排序数组中的重复项LeetCode 26移动零LeetCode 283寻找重复数LeetCode 287左右指针系列反转字符串LeetCode 344验证回文串LeetCode 125三数之和LeetCode 15滑动窗口系列无重复字符的最长子串LeetCode 3字符串的排列LeetCode 567最大连续1的个数IIILeetCode 1004每做完一道题可以问自己指针的初始位置合理吗移动条件是否覆盖所有情况能否进一步优化时间或空间比如做完无重复字符的最长子串后可以尝试变种题最多包含两个不同字符的最长子串至少包含K个重复字符的最长子串替换后的最长重复字符这种刻意练习能帮助建立对双指针的直觉遇到新问题时能快速识别适用场景。

更多文章