资讯动态

数组题双指针技巧全解析:从快慢指针到滑动窗口

发布时间:2026/10/17 9:38:25 来源:尧图企业网站定制
数组题在Leetcode里的地位相当于篮球里的基础运球——谁都要练谁都能练但真正练明白的人不多。而双指针就是数组题里最能体现基本功的一组技巧。很多初学者刷到“移除元素”“有序数组的平方”这类题目时第一反应是暴力解能过但效率难看第二反应是看题解代码只有几行却看不懂为什么要这么写。这篇博文会以“代码随想录”的数组专题为线索把双指针这个套路彻底讲透什么时候用快慢指针、什么时候用左右对撞指针、边界条件怎么写才不会漏以及面试时怎么把思路说清楚。无论你是刚入门刷题的小白还是刷了几十道题但思路还比较零散的开发者这篇文章都适用。我会把“为什么这样解”放在“怎么解”前面讲再用几个Leetcode高频数组题移除元素、有效数组的平方、长度最小的子数组做逐一拆解每个题目都会给出Python实现、复杂度分析和发散思考。看完之后你可以直接照着思路上手练也能在面试里把双指针的来龙去脉讲明白。1. 双指针思路拆解为什么数组题里它这么能打1.1 双指针解决的到底是什么问题先看一个最常见的场景给你一个数组要求删除其中等于某个值的所有元素。最直觉的写法是找到目标元素后把后面所有元素整体往前挪一位然后删除尾部多余的元素。这个思路没错但它的问题在于每删除一个元素都涉及一轮O(n)的搬移最坏情况下时间复杂度是O(n²)。当数组长度达到十万量级时这个差距在在线评测系统里就是超时和不超时的区别。双指针的核心价值在于它把“找到目标元素”和“把后续元素搬移”这两个步骤合并成一次遍历。我们不再需要反复处理同一个元素因为每个元素只会被访问一次、最多赋值一次。这就是它能把暴力解法的平方复杂度降到线性复杂度的根本原因。用生活化的类比来解释想象有一条队伍要把队伍里所有戴帽子的人请出去剩下的队伍顺序保持不变。暴力做法是每发现一个戴帽子的人就让他后面所有人往前走一步——每次都要重新调整一次队形效率很低。双指针的做法是让一个人从前到后挨个检查快指针另外一个人在队伍里负责记录当前有效位置慢指针遇到不需要出列的人就把他安排到有效位置。整个队伍只需要走一遍就完成了筛选。1.2 快慢指针和左右对撞两种模式的适用边界双指针在数组题里并不是一种方法而是两大类方法的统称适用场景完全不同。第一种是快慢指针也叫同向双指针。两个指针都从数组头部出发只是移动速度不一样。快指针负责探索整个数组慢指针负责记录结果数组的当前位置。这种模式最适合“原地修改数组”的题目比如移除元素、删除重复项。它的核心思想是让快指针不受删除操作的影响只关心遍历慢指针则保证数组前k个位置始终是满足条件的结果。第二种是左右对撞指针两个指针分别从数组两端出发向中间移动直到相遇。这种模式适合数组本身有一定顺序比如有序需要利用“两端信息”来解决问题的场景。有序数组的平方、两数之和、反转字符串都属于这一类。它的核心洞察是最大或最小的答案往往出现在数组的两端而不是中间。实际做题时很多人的困惑不是不知道双指针这个概念而是拿到题之后判断不出该用哪种模式。这里给一个简单判断标准如果题目要求“原地修改数组”基本就是快慢指针如果题目给了一个有序数组或者数组两端的信息比中间更有价值优先想左右对撞。等做过五六道题之后再回头看这个判断会变成一种直觉。1.3 双指针的复杂度优势能有多大用表格对比双指针和暴力解法在几个典型数组题上的复杂度会比较直观题目场景暴力思路暴力复杂度双指针复杂度移除等于指定值的元素删除一次搬移一次O(n²)O(n)删除有序数组重复项逐个比较并搬移O(n²)O(n)有序数组平方后排序平方后整体排序O(nlogn)O(n)寻找长度最短的子数组枚举所有子数组O(n²)O(n)数据量小的时候O(n²)和O(n)的差别感受不出来但是当数组长度到10⁵甚至10⁶量级时前者可能跑几十秒后者是毫秒级。这也就是为什么Leetcode很多题目把数据范围设计得比较大目的就是逼你跳出暴力思路去寻找更优的解法。双指针作为把“循环套循环”压成“一次遍历”的典型工具含金量就在这里。2. 经典题目逐题拆解从暴力到双指针的完整思考过程2.1 移除元素Leetcode 27快慢指针的第一堂课先看题目给定一个数组nums和一个值val要求原地移除所有数值等于val的元素返回移除后数组的新长度。暴力解法很容易想到外层循环找等于val的位置找到后把后续所有元素往前移。但在写代码时很多人会发现一个细节问题——删除一个元素后当前下标元素被覆盖需要重新检查当前位置所以循环变量要减去1。这个写法不仅啰嗦而且边界条件一多就容易出错。快慢指针的解法很简洁def removeElement(nums, val): slow 0 for fast in range(len(nums)): if nums[fast] ! val: nums[slow] nums[fast] slow 1 return slow这段代码的逻辑是fast从头到尾遍历数组的每一个位置只要当前位置的值不等于val就把它写到slow指向的位置然后slow前进。如果等于valfast继续往前走slow原地等待。循环结束后数组前slow个位置就是移除val之后的结果slow的值就是新数组长度。关键在于理解为什么不需要真正“删除”元素。题目只要求返回新长度数组末尾多出来的旧值并不会影响判断——只要读数组的时候只读前slow个位置就行。这个思路在后续很多题目里都会遇到比如删除排序数组中的重复项解法几乎一模一样只是判断条件从“不等于val”变成了“不等于前一个元素”。我在实际教学和刷题过程中发现初学者最容易犯的错误是试图真的去“删除”数组元素甚至用pop操作这不仅破坏了数组的原本结构还让时间复杂度重新回到O(n²)。记住一个原则数组题里的“删除”往往是逻辑上的删除物理上只是用后续元素覆盖。2.2 删除有序数组中的重复项Leetcode 26同一个套路换个马甲这题是移除元素的直接变体给定一个排序数组要求原地删除重复出现的元素使每个元素只出现一次返回新长度。暴力思路是每发现一个重复元素就把后面的元素往前挪复杂度同样不理想。双指针的写法则和移除元素如出一辙def removeDuplicates(nums): if not nums: return 0 slow 1 for fast in range(1, len(nums)): if nums[fast] ! nums[fast - 1]: nums[slow] nums[fast] slow 1 return slow注意两个细节。第一个细节是slow从1开始因为数组第一个元素无论如何都会保留不需要比较第二个细节是判断条件用nums[fast] ! nums[fast-1]因为数组有序重复元素都是紧挨着的只需要跟它前一个元素比较不需要跟前面所有元素比较——这正是“有序数组”带来的便利。这个题目如果去掉“有序”条件判断重复的方式就要改成哈希表复杂度也会从O(n)变成O(n)时间加O(n)空间。所以以后做题时要养成习惯题目里每一个看似不起眼的限定条件都是在暗示你某种更优的解法。有序就是用来穿针引线的。2.3 有序数组的平方Leetcode 977左右对撞的入门题给定一个按非递减顺序排序的数组要求返回每个数字的平方组成的新数组同样按非递减顺序排序。这个题目最容易想到的解法是先算每个数的平方再调用排序函数。代码如下def sortedSquares(nums): res [] for num in nums: res.append(num * num) res.sort() return res复杂度是O(nlogn)空间O(n)。看起来已经不错了对不对但其实还有更巧妙的O(n)解法。关键洞察是数组本身有序但平方之后负数部分的平方会变得很大。最大的平方数一定出现在数组两端——要么是最左边的负数平方要么是最右边的正数平方。因此可以从两端往中间比较每次取出较大的那个从结果数组的末尾往前填入def sortedSquares(nums): n len(nums) res [0] * n left, right 0, n - 1 pos n - 1 while left right: if nums[left] * nums[left] nums[right] * nums[right]: res[pos] nums[left] * nums[left] left 1 else: res[pos] nums[right] * nums[right] right - 1 pos - 1 return res这个解法的核心是从“最大”开始排。因为平方之后最大的数肯定在两端的某一个所以每次比较两端平方的大小大的放结果数组的尾部然后再往中间挪动对应的指针。全部处理完后正好排成一个升序新数组。这里的边界条件是left right。如果你写成left right数组长度为奇数时中间元素会落掉。这个细节在面试里很容易被追问也是很多人在白板写代码时最容易翻车的地方。后面的第三章我会重点展开这类边界问题。2.4 长度最小的子数组Leetcode 209滑动窗口本质上也是双指针这个题稍有不同给定一个含有正整数的数组和一个正整数target找出该数组中满足其和大于等于target的长度最小的连续子数组并返回其长度。暴力解法是枚举每一个起点i再从i开始往后累加直到和大于等于target计算长度。时间复杂度O(n²)。代码写起来不算难但面试的时候如果只给出这个方案基本上等于是告诉面试官你没怎么练过。双指针在这里体现为“滑动窗口”其实就是用两个指针维护一个可伸缩的区间def minSubArrayLen(target, nums): n len(nums) left 0 cur_sum 0 ans float(inf) for right in range(n): cur_sum nums[right] while cur_sum target: ans min(ans, right - left 1) cur_sum - nums[left] left 1 return ans if ans ! float(inf) else 0这个解法的精妙之处在于right指针不断向右扩展窗口一旦窗口内元素之和满足大于等于target就尝试让left指针向右收缩窗口寻找更短的满足条件的区间。每个元素最多被加入一次、移出一次所以总时间消耗是O(n)。想象一下你手里有一个可变大小的购物框先不断往里面放东西一旦框里东西总价值够了就试着拿掉最早放进去的东西看看还能不能保持价值达标。如果还能达标就继续拿直到不能再拿为止。整个过程框的容量始终维持在刚好达标的最小状态。2.5 反转字符串与三数之和更多双指针的变式除了上面这几道双指针在字符串反转里也有非常经典的应用。给定一个字符数组原地反转。左右对撞指针的思路是左右指针分别指向首尾交换两个指针指向的元素然后分别向内移动。这种写法既不需要额外空间也很好理解。再往深处走三数之和这个面试高频题也用了双指针优化。最外层固定一个数剩下的两个数用左右指针在有序数组里寻找把三重循环O(n³)降成了O(n²)。这类题的本质是排序之后数组的左右两端信息帮助我们剪掉了大量无效的枚举。从刷题角度建议把这几道题按顺序练一遍27移除元素 → 26删除重复项 → 977有序数组的平方 → 209长度最小的子数组 → 再挑战三数之和。你会发现前面的题目像是阶梯每一步都在为后面做铺垫。等到你能够不看题解直接写出这几道题的双指针版本说明这个套路已经内化得差不多了。3. 实操细节与踩坑记录那些题解不会告诉你的边界问题3.1 循环不变量写代码前先想清楚每个指针的含义很多读题解的人会有一个困惑代码明明看懂了自己一写就错。问题往往出在“指针含义”没有定义清楚。写双指针代码之前先在心里确定三件事快指针或右指针代表什么慢指针或左指针代表什么循环结束的条件是什么以移除元素为例右指针fast的含义是“当前正在检查的元素位置”左指针slow的含义是“下一个可以写入的位置”。循环不变量是区间[0, slow)存放了所有已经检查完毕且不等于val的元素。每次迭代都保持这个性质不变循环结束之后自然得到正确答案。很多人觉得这种表述太学术实际上是效率最高的思路整理方式。先定义清楚再写循环体代码会有一种按图索骥的顺畅感不容易出现写一步想一步的混乱状态。3.2 边界条件对照表这几个坑几乎人人踩过我自己整理了一个双指针数组题的常见边界问题速查表刷题前可以对照一遍场景易错点正确写法左右对撞指针遍历数组长度为偶数或奇数时漏元素用left right不是left right快慢指针初始化slow的初始位置不对移除元素slow从0开始删除重复项slow从1开始快指针访问前一个元素fast0时越界循环从1开始先判断非空滑动窗口收缩cur_sum减到小于target时又加回去收缩用while循环直到不满足条件为止结果数组为空数组全是要删除的元素返回0或空数组前先判空这里特别提一下while循环里指针移动顺序的问题。在一次循环中是先处理数据再移动指针还是先移动指针再处理数据这两种写法结果完全不同。以移除元素为例正确的做法是先把nums[fast]赋值给nums[slow]然后slow加1。如果反过来slow先加1再赋值就会空出一个位置结果错乱。调试这类问题有个简单的技巧在循环里加一个print打印出每一次循环结束后fast、slow的位置以及整个数组的状态。不要觉得这是笨办法实际排查边界问题时逐步打印比盯着代码空想要快得多。3.3 原地修改的内存与性能权衡很多初学者会问为什么非要原地修改直接新建一个数组存结果不行吗答案是可以但要看题目要求。Leetcode很多题目明确写了“原地修改”意味着你只能在原数组上操作不能开辟额外空间。面试时如果忽略了这一点即便代码能通过测试用例也拿不到满分。但换个角度如果题目没有限制额外空间新建数组往往更容易理解和实现不一定非要原地。比如有序数组的平方新数组版本直接用双指针从后往前填逻辑一样清楚。面试里优先确保思路清晰在此基础上再优化空间复杂度。不要为了炫技而用复杂的原地写法结果代码漏洞百出。我个人经验是在本地练习时可以两种写法都实现一遍比较它们的差异。这个过程不仅能加深对数组内存布局的理解也能帮你从“会做题”过渡到“懂题目”。4. 常见问题排查与高频报错场景4.1 死循环你的循环条件或者指针移动规则写反了双指针最常见的运行时问题是死循环也就是循环永远不会结束。原因通常是某个分支里忘记移动指针了或者指针移动的方向不对。比如左右对撞时如果左边指针和右边指针指向相等的元素处理完交换后两个指针都必须向内移动漏掉任何一个都会出问题。排查思路很简单在循环出口处加一个计数变量如果循环次数超过了数组长度的两三倍还停不下来那基本可以断定是指针移动逻辑出了问题。另外建议在纸上模拟一轮运行把所有变量的变化过程写出来大多数逻辑错误都能现形。4.2 数组越界访问了不存在的下标数组越界经常出现在这几个位置对撞指针处理时left或right超出数组范围快慢指针中访问fast1或fast-1时没有检查边界滑动窗口减left时没有保证left不超过right。解法是写好边界检查再写业务逻辑。在while循环的开头先做条件判断把边界情况挡在门外。习惯之后写出来的代码会稳很多。4.3 结果数组的顺序不对你从哪个方向填结果没有想清楚有序数组的平方这道题里如果从结果数组头部开始填填出来的顺序是乱序的。正确做法是从末尾往前填因为每一次找到的都是当前剩余元素中平方最大的那个。这提醒我们一个关键点双指针找到的结果顺序未必是最终的顺序需要明确自己要的是升序还是降序然后再决定填充方向。4.4 面试表达先给暴力解再给双指针别一上来就秀最优解面试场景下我不建议直接写双指针的最优解。更稳妥的表达顺序是先说暴力解法说明复杂度是O(n²)然后引导出“可不可以利用数组的有序性或者双指针来优化”的思路。这样面试官能看清楚你的思考过程而不是觉得你背了题解。具体来说可以先说“最朴素的做法是枚举所有子数组复杂度O(n²)”然后停顿一下“不过这道题数组是正整数的存在一个单调性我可以使用滑动窗口把复杂度降到O(n)”。这种表达展示的是分析能力而分析能力正是面试真正考核的东西。5. 刷题路径与复盘方法从这一组题到建立自己的解题框架5.1 建议的刷题顺序和阶段目标如果有人问我双指针该从哪里入手我会建议按这样的顺序推进第一阶段Leetcode 27 移除元素把快慢指针的基本功练熟第二阶段Leetcode 26 删除有序数组重复项体会同一套路的变体第三阶段Leetcode 977 有序数组的平方掌握左右对撞的思维方式第四阶段Leetcode 209 长度最小的子数组理解滑动窗口的本质第五阶段Leetcode 344 反转字符串巩固对撞指针的手感第六阶段Leetcode 15 三数之和体验双指针在更高维度上的优化每个阶段之间不要贪快。以我个人的体验前面几道题即使看懂了题解也值得自己关掉答案亲手写一遍写完再看标准解法的差异。尤其是边界处理的部分自己写错一遍再纠正记忆会比单纯阅读深刻得多。5.2 建立自己的解题模板刷多了之后双指针题目其实可以归纳成几个固定模板。比如快慢指针的模板就是slow 0 for fast in range(len(nums)): if 条件: nums[slow] nums[fast] slow 1 return slow左右对撞的模板则是left, right 0, len(nums) - 1 while left right: if 判断条件: left 1 else: right - 1模板的意义在于降低思考负担但不要生搬硬套。模板是骨架具体的判断条件才是灵魂这个判断条件需要根据题意去设计。在复盘时不要把每道题当成孤立的题目而是把它归类到某个模板下同时记录这个模板这次的“变式”在哪里。坚持这样做十几道题之后你遇到新题时就能自动套用已有的框架而不是每次从零开始。5.3 复盘比刷题数量重要很多人刷题陷入“数量焦虑”一天刷十几道但刷完就忘。我的经验是每道题刷完之后花十分钟写下三件事这道题的暴力思路是什么双指针的优化点在哪里下次遇到什么特征的时候应该想到类似的做法这十分钟比多刷一道题值钱得多。这个过程看似简单坚持下来之后会发生一个显著变化看到新题不再慌张而是能迅速把它归入一个已知的思路框架中判断出该用哪些工具。刷题的本质其实是积累这种“条件反射”。从索引里单独拎出数组这一章来练性价比极高。它既是算法的基础也是双指针、二分查找、前缀和等多个重要技巧的载体。把这一组题吃透后面再学链表、字符串很多思路都是相通的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑