🎯 双指针算法详解

对撞指针 · 快慢指针 · 同向双指针:把 O(n²) 降为 O(n) 的利器

一句话总结

双指针就是用两个下标(指针)配合移动,一趟扫描代替暴力双重循环。它不改变数据结构本身,只是改变"遍历的策略",把时间复杂度从 O(n²) 降到 O(n) 或 O(n log n)。按移动方向分三种形态:

↔️

对撞指针

一左一右向中间靠拢。适用:有序数组两数之和、回文判断、盛最多水。

🐢🐰

快慢指针

同起点不同速度。适用:链表找中点、判环、删除倒数第 N 个。

➡️➡️

同向双指针(滑动窗口雏形)

同方向一前一后。适用:原地删除元素、移除重复项、合并有序数组。

一、对撞指针(左右相向而行)

1.1 标准套路

数组有序(或具有单调性)时,左指针 left 从头出发,右指针 right 从尾出发,根据当前组合与目标的差距决定谁移动:

有序数组找两数之和 = 20 2 7 11 15 19 23 28 left right 2 + 28 = 30 > 20 → right 左移 | 若和 < 目标 → left 右移 | 相等 → 找到答案

1.2 代码模板(LeetCode 167. 两数之和 II)

def twoSum(numbers, target): left, right = 0, len(numbers) - 1 while left < right: s = numbers[left] + numbers[right] if s == target: return [left + 1, right + 1] # 题目下标从 1 开始 elif s < target: left += 1 # 和太小,左指针右移增大 else: right -= 1 # 和太大,右指针左移减小 return [-1, -1]
为什么不会漏解?因为数组有序:如果 nums[left] + nums[right] > target,那么 nums[right] 与剩下的任何数配对都必然偏大,可以安全地永久排除 right——每一步都排除一个元素,最多 n 步,复杂度 O(n)。

1.3 经典题目清单

题目核心判断指针移动规则
167 两数之和 IIsum vs target小了 left++,大了 right--
125 验证回文串两字符是否相等相等则双双向中间移动
11 盛最多水的容器面积 = min(h[l],h[r])×(r-l)移动较矮的一侧(否则不可能更优)
611 有效三角形个数固定最大边,内层对撞先排序,倒序枚举最大边
977 有序数组的平方比较两端平方的大小大的填入结果尾部,对应指针移动

二、快慢指针(不同速度前进)

2.1 链表判环:Floyd 判圈法

慢指针每次走 1 步,快指针每次走 2 步。若有环,快指针终将"套圈"追上慢指针;若无环,快指针先到末尾。

A B 🐢 C D E 🐰 E 的 next 指回 C(成环) 🐢 每轮走 1 步:A→B→C→D→E→C→D... 🐰 每轮走 2 步:A→C→E→C→E...(在环里绕圈) 速度差 = 每轮缩短 1 步距离 → 兔子必然在环内某点追上乌龟 🎯 相遇后:把一个指针放回头部,两者同速前进,再次相遇处即环入口
def detectCycle(head): slow = fast = head while fast and fast.next: slow = slow.next # 🐢 1 步 fast = fast.next.next # 🐰 2 步 if slow is fast: # 相遇 → 有环 # 数学可证:从头走到环入口的距离 == 相遇点走回环入口的距离 slow = head while slow is not fast: slow = slow.next fast = fast.next return slow # 环的入口 return None # fast 到头 → 无环

2.2 快慢指针其他应用

场景指针规则结论
876 链表的中间结点slow 1 步、fast 2 步fast 到尾时,slow 恰在中点
19 删除倒数第 N 个节点fast 先走 N 步,再同速fast 到尾时 slow 在目标前一格
287 寻找重复数(数组判环)把 nums[i] 当作 next 指针数组隐式成环,同 Floyd 判圈
26/80 原地去重slow 指向已处理区末尾fast 扫描,遇新值则复制给 slow+1

三、同向双指针(一前一后)

3.1 原地删除:慢指针守住"结果区"

移除元素 val=3:[3, 1, 3, 2, 5, 3] → [1, 2, 5, ...] 1 2 5 3 1 3 slow(含此之前 = 有效结果) fast(扫描指针) nums[fast] != val → nums[++slow] = nums[fast],slow 右扩一格 灰格为已处理/废弃区域,最终返回 slow+1 作为新长度
def removeElement(nums, val): slow = -1 # 慢指针:结果区的最后一个有效位置 for fast in range(len(nums)): if nums[fast] != val: # fast 发现一个有效元素 slow += 1 nums[slow] = nums[fast] # 搬到结果区末尾 return slow + 1 # 新数组长度
同向双指针的通用心智模型:把数组想象成两段——[0..slow] 是已处理好的结果区,(slow..fast) 是废弃区,[fast..n) 是未探索区。fast 每发现一个该保留的元素,就把它"搬进"结果区。这个模型适用于原地去重、删除、分区(快排 partition)等一大类题。

四、三种形态对比与选择策略

维度对撞指针快慢指针同向双指针
移动方向相向而行同向不同速同向同速(角色不同)
前提条件通常需要有序链表/隐式环无特殊要求
终止条件left ≥ right 相遇fast 到达尾部/两针相遇fast 扫完全部
典型复杂度O(n)O(n)O(n)
代表题目两数之和、盛水容器、回文判环、找中点、倒数第 N原地删除、去重、合并
⚠️ 面试常见坑:
① 对撞指针忘记先排序(两数之和在无序数组上直接对撞是错的);
② 盛水容器题里移动较高的一侧(应移动矮侧,因为面积受短板限制);
③ 链表快指针写成 fast.next.next 前不检查 fast.next 是否为 None → 空指针异常;
④ 同向双指针更新答案的位置放在了 else 分支里,漏掉初始状态。
与滑动窗口的关系:同向双指针中"两个指针夹住一段连续区间、并动态伸缩"的形态,就是下一讲的滑动窗口。可以把滑动窗口理解为同向双指针的特例与系统化。