一句话总结
双指针就是用两个下标(指针)配合移动,一趟扫描代替暴力双重循环。它不改变数据结构本身,只是改变"遍历的策略",把时间复杂度从 O(n²) 降到 O(n) 或 O(n log n)。按移动方向分三种形态:
↔️
对撞指针
一左一右向中间靠拢。适用:有序数组两数之和、回文判断、盛最多水。
🐢🐰
快慢指针
同起点不同速度。适用:链表找中点、判环、删除倒数第 N 个。
➡️➡️
同向双指针(滑动窗口雏形)
同方向一前一后。适用:原地删除元素、移除重复项、合并有序数组。
一、对撞指针(左右相向而行)
1.1 标准套路
数组有序(或具有单调性)时,左指针 left 从头出发,右指针 right 从尾出发,根据当前组合与目标的差距决定谁移动:
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 两数之和 II | sum vs target | 小了 left++,大了 right-- |
| 125 验证回文串 | 两字符是否相等 | 相等则双双向中间移动 |
| 11 盛最多水的容器 | 面积 = min(h[l],h[r])×(r-l) | 移动较矮的一侧(否则不可能更优) |
| 611 有效三角形个数 | 固定最大边,内层对撞 | 先排序,倒序枚举最大边 |
| 977 有序数组的平方 | 比较两端平方的大小 | 大的填入结果尾部,对应指针移动 |
二、快慢指针(不同速度前进)
2.1 链表判环:Floyd 判圈法
慢指针每次走 1 步,快指针每次走 2 步。若有环,快指针终将"套圈"追上慢指针;若无环,快指针先到末尾。
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 原地删除:慢指针守住"结果区"
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 | 原地删除、去重、合并 |
⚠️ 面试常见坑:
① 对撞指针忘记先排序(两数之和在无序数组上直接对撞是错的);
② 盛水容器题里移动较高的一侧(应移动矮侧,因为面积受短板限制);
③ 链表快指针写成
④ 同向双指针更新答案的位置放在了 else 分支里,漏掉初始状态。
① 对撞指针忘记先排序(两数之和在无序数组上直接对撞是错的);
② 盛水容器题里移动较高的一侧(应移动矮侧,因为面积受短板限制);
③ 链表快指针写成
fast.next.next 前不检查 fast.next 是否为 None → 空指针异常;④ 同向双指针更新答案的位置放在了 else 分支里,漏掉初始状态。
与滑动窗口的关系:同向双指针中"两个指针夹住一段连续区间、并动态伸缩"的形态,就是下一讲的滑动窗口。可以把滑动窗口理解为同向双指针的特例与系统化。