在链表操作中,快慢指针 (Fast & Slow Pointers) 是一种极其经典且优雅的技巧。它主要用于解决链表中的环路检测、中点寻找等问题。其背后的理论基础通常被称为 Floyd 判圈算法 (Floyd’s Cycle-Finding Algorithm),有时也形象地被称为「龟兔赛跑算法」。
本文将从原理、可视化演示到数学证明,全面解析该算法在不同场景下的应用。
0x01 核心原理
算法的核心思想非常简单:初始化两个指针 slow 和 fast 指向链表头部。
slow:每次移动一步 ($step = 1$)fast:每次移动两步 ($step = 2$)
如果链表中不存在环,fast 指针将率先到达链表尾部 (null);如果链表中存在环,由于 fast 的速度快于 slow,它最终会在环内「追上」 slow 指针。
复杂度分析
- 时间复杂度:$O(N)$。在没有环的情况下,遍历一次链表;在有环的情况下,两者入环后,
fast最多绕环一圈即可追上slow。 - 空间复杂度:$O(1)$。仅使用了两个指针,未使用额外的哈希表存储节点。
0x02 交互式算法演示
枯燥的文字不如直观的动画。下方的演示展示了一个带有环的链表(环入口为节点 3)。
- 阶段一 (Phase 1):点击「单步执行」,观察快慢指针如何移动并在环内相遇。
- 阶段二 (Phase 2):相遇后,算法如何利用「相遇点」和「新指针」找到环的精确入口。
0x03 典型应用场景
1. 判断链表是否有环
这是最基础的应用。只需判断 fast 和 slow 是否会重合。
代码逻辑
| |
2. 寻找环的入口节点
相遇只是第一步,更高级的应用是找到环的起始节点。这也是面试中的高频考点。
数学推导
假设链表头到环入口的距离为 $a$,环入口到相遇点的距离为 $b$,相遇点再回到环入口的距离为 $c$。也就是环的总长度 $L = b + c$。
当两者相遇时:
slow走过的路程:$S_{slow} = a + b$fast走过的路程:$S_{fast} = a + n(b + c) + b = a + nL + b$ ($n$ 为fast在环内绕的圈数,通常 $n \ge 1$)
利用速度关系: 由于
fast的速度是slow的 2 倍,所以 $S_{fast} = 2 \cdot S_{slow}$。 代入公式: $$ a + nL + b = 2(a + b) $$ $$ a + nL + b = 2a + 2b $$ 化简得: $$ a = (n - 1)L + c $$核心结论: 当 $n=1$ 时(通常情况),公式简化为 $a = c$。 这意味着:从「链表头」到「环入口」的距离,等于从「相遇点」继续向前走到「环入口」的距离。
算法实现
基于上述推导,我们只需在通过阶段一找到相遇点后:
- 保持
slow停在相遇点。 - 初始化一个新的指针(或直接复用
head)命名为finder,放在链表头部。 - 让
slow和finder同时移动,每次都只走一步。 - 它们再次相遇的节点,即为环的入口。
3. 寻找链表/环的长度
- 环的长度:在找到相遇点后,可以让
slow不动,用另一个指针绕环一圈回到slow,统计步数即可。 - 链表长度:如果存在环,链表长度通常定义为 $a + L$。
4. 寻找重复数 (数组应用)
有些数组问题可以转化为链表判圈问题。例如,数组中包含 $n+1$ 个整数,数值范围在 $[1, n]$ 之间,必定存在至少一个重复数。
我们可以将数组下标 i 视为节点,nums[i] 视为 next 指针指向的节点。由于存在重复数,意味着有多个下标指向同一个值(即多个节点指向同一个 next),这就形成了环。重复的那个数字,正是环的入口。
0x04 环的移除
在某些工程场景下(如内存回收、防止无限递归),我们不仅要检测环,还要从逻辑上打破环。
策略是:找到环的入口节点的前驱节点,将其 next 指向 null。
- 利用上述算法找到环入口。
- 由于我们已知入口节点,只需遍历环一圈,找到那个指向入口节点的节点(即尾节点)。
- 将尾节点的
next置空。
| |
0x05 总结
快慢指针是解决具有「循环」或「从末尾倒数」性质问题的利器。除了判圈,它也常用于:
- 寻找链表的中点(归并排序链表时常用)。
- 寻找链表倒数第 $k$ 个节点(等距双指针)。
掌握 $a=c$ 这一数学推导,是理解该工具上限的关键。