双指针技巧
一句话: 用两个指针的相对运动,把
直觉 — 两指针的单调运动
暴力两层循环之所以浪费,是因为它每次内层都从头扫一遍,把”已经知道不行”的组合又试了一次。双指针不一样: 每移动一步,都基于上一步学到的信息,永不回头。
为什么能不回头? 因为问题里有单调性:
- 对撞: 数组排好序 → 移动
left一定让sum变大,移动right一定让sum变小。 - 快慢: 两指针速度差恒定 → 相对位移随时间单调增长,该撞会撞,该追会追。
两个指针,各自只走 N 步,合起来
对照动画再读下面三种模式,印象会深一倍。
三种模式 — 对撞 / 快慢 / Floyd
| 模式 | 起点 | 运动 | 利用的单调性 | 典型题 |
|---|---|---|---|---|
| 对撞 | 两端 | 向中间逼近 | 数组有序,sum 随移动单调变 | 两数之和 II, 三数之和, 验证回文 |
| 快慢 | 同起点 / 错开 | 同向不同速 | 速度差恒定 → 相对位移单调 | 链表中点, 原地去重 |
| Floyd | 同起点 | 同向, fast 走 2 步 | 环内必追上(模 L 周期) | 141 环形链表, 142 找环入口 |
记忆抓手: 对撞看 sum, 快慢看距离差, Floyd 看周期。
为什么对撞可以”丢”一整列
数组有序时, 若 nums[left] + nums[right] < target, 那 left 配剩下任何比 right 小的数都还是更小, 一整列直接淘汰 → 只能 left++。反之 right--。每次淘汰一行或一列, 所以 N 步走完。
为什么”盛最多水”要移短板(LeetCode 11)
给定高度数组
height,第根竖线两端是 和 。选两根线和 x 轴围成容器,求最大盛水量。
容量取决于宽和短板:
两指针
关键引理: 设当前
(第一步: 高度
- 宽:
,严格变小。 - 高: 高度被短板
锁死, 再高也只能取到 ,更矮则更小,所以 。
含义: 短板 left++。对称地 right--。
移高的那根:宽变小、水位仍被短板卡住 → 纯亏;只有移短板才可能解锁更高水位。
循环不变量: 全局最优对始终在当前
这跟上面”丢一整列”同构: 那里丢的是有序数组的一行/一列,这里丢的是短板能配的所有线对。
为什么快慢能找中点
slow 走 1 步, fast 走 2 步。fast 到尾时, slow 正好在一半位置 — 距离差以 1 的速率单调增长, 二者的”时间差”就是答案。
模板 — Python 各一份
对撞: 两数之和 II
1 | |
快慢: 链表中点
1 | |
快慢: 原地去重(口诀: slow 指向”下一个要写的位置”)
1 | |
Floyd: 判环 + 找环入口
1 | |
Floyd 为什么 work — 把那行数学拆开
先把记号钉死(对照动画里的尾巴 + 环):
: 头到环入口的距离,也就是尾巴长度。 : 环入口到相遇点的距离,沿指针前进方向量。 : 环长。
第一阶段: 为什么撞得上,而且 slow 还没绕满一圈
两人都进环之后,slow 每步 1 格、fast 每步 2 格 —— fast 相对 slow 每轮多走 1 格。
把”fast 还要走多远才追上 slow”这段环上距离记成
: 这纯粹因为环长是 ,环上任意两点的距离都不可能 ,跟在哪进环无关。 - 每走一步
精确减 1: fast 每轮追近 1 格;又因为相对步长恰好是 1,fast 不会”跳过”slow,只会精确落在 那一格(所以一定相遇,不会擦肩)。
每步 D 减 1,且绝不越过 0 ⇒ 至多 L-1 步就在 D=0 处相遇。
合起来: 从
相遇时,各自走了多远?
- slow: 由上一节,它走不满一圈就被追上(那
步正好就是 ),所以总路程就是 。 - fast: 速度翻倍,路程是 slow 的两倍,即
。 - 但 fast 的路程还能换个角度数: 先花
走到入口,再在环里绕了 整圈又走 到相遇点,即 ( 是它多绕的圈数, )。
同一段路程的两种写法,画等号:
两边同时减掉
第二阶段: 为什么相遇后能定位入口?
所以模板第二阶段才那么写: 让一个指针回到 head,两个指针都改成每步 1 格、同时出发,走完
动画里绿色 slow 和红色 fast 在环里相遇的那一格,它到环入口的距离就是
经典题
热身 — 三种模式各刷一道, 形成肌肉记忆:
- 167. 两数之和 II — 对撞裸题
- 11. 盛最多水的容器 — 对撞 + 贪心(短板移动)
- 283. 移动零 — 快慢, slow 指”下一个非零位”
- 876. 链表中点 — 快慢标准
- 141. 环形链表 — Floyd 入门
进阶 — 都是”想清楚单调性在哪”的练习:
- 15. 三数之和 — 排序 + 固定一个 + 对撞, 注意去重
- 42. 接雨水 — 对撞 + 维护左右最大值, 短板侧确定贡献
- 142. 环形链表 II — Floyd 第二阶段, 套模板
- 287. 寻找重复数 — 数组当链表用, Floyd 找入口
- 75. 颜色分类 — 三指针荷兰国旗
思考题
对撞指针为什么要求数组有序? 无序时怎么办?
有序 → 单调性 → 每次移动都在确定地缩小搜索空间。无序时这条死。退路两条: 哈希表(
Floyd 中, slow 回到 head 后为什么两指针同速就能在入口相遇?
因为
原地去重要保留最多 2 个相同元素怎么改?
把比较对象往前看一位: nums[fast] != nums[slow - 1]。即 slow 看的是”上上次写入的值”,新来的只要和它不同就允许写。
1 | |
保留 nums[slow - (k-1)]。模式通用。
记忆法
一句话: 两指针朝一个方向走, 让目标单调变化, 永不回头。
三种模式 + 关键词:
| 模式 | 关键词 | 触发信号 |
|---|---|---|
| 对撞 | 有序 + sum | “两数和 / 容器 / 回文” |
| 快慢 | 速度差 | “中点 / 去重 / 移零” |
| Floyd | 周期 | “链表有没有环 / 找入口 / 重复数” |
看到题, 先问: 有单调性吗? 在哪一维? 答出来了, 双指针就是顺手的事。