快慢指针:核心思想与解题模板

快慢指针的本质是用两个指针的间距来记录位置信息,避免额外数组存储。

核心哲学

“与其存下来,不如走过去。”

当你需要找链表中某个特定位置的节点时,最直觉的做法是把所有节点存进数组,按下标取。但这要 O(n) 空间。

快慢指针的思路是:让一个指针先走几步,另一个指针后走,两个指针的间距就是你需要的偏移量。 当先走的指针到达末尾时,后走的指针恰好停在你想要的位置。


四种常见模式

模式一:找倒数第 k 个节点

fast 先走 k 步,然后 fast 和 slow 一起走,fast 到 null 时 slow 就是倒数第 k 个。

初始:     [1] [2] [3] [4] [5] → null
            ↑ slow
                ↑ fast(先走了 k=2 步)

结束:     [1] [2] [3] [4] [5] → null
                        ↑ slow      ↑ fast
            slow 在倒数第 k 个位置

如果要删除倒数第 k 个,fast 先走 k+1 步,这样 slow 停在被删节点的前一个(需要改 next 指针才能删除)。

为什么是 k+1 不是 k?

如果 fast 先走 k 步,fast 和 slow 一起走到 fast 为 null 时,slow 指向被删节点本身。但删除操作需要前一个节点的 next 指针,所以 fast 要多走一步,让 slow 停在被删节点的前一个。

经典题:10. One-Pass Removal of k-th Node from End

模式二:判断链表是否有环

fast 每次走两步,slow 每次走一步。如果有环,fast 一定会追上 slow(在环里转圈迟早相遇)。如果无环,fast 先到 null。

slow: 1 → 2 → 3 → 4 → 5 → 3(回到环入口)
fast: 1 → 3 → 5 → 3 → 5 → 3(追上 slow)

为什么 fast 一定能追上 slow?

假设环外长度 a,环长度 b。slow 进环时 fast 已经在环里了。fast 每步追近 slow 一格(fast 走 2 步,slow 走 1 步,相对速度 1),环长 b 步,最多 b 步内一定追上。

模式三:找链表中点

fast 每次走两步,slow 每次走一步。fast 到末尾时 slow 在中点。

slow: 1 → 2 → 3
fast: 1 → 3 → 5 → null
slow 停在中间节点

用于归并排序链表、判断回文链表。

细节:如果节点数是偶数,slow 停在前半段的最后一个节点(比如 5 个节点停在第 3 个,4 个节点停在第 2 个)。具体停在哪里取决于 fast 的终止条件(fast === null 还是 fast.next === null)。

模式四:找环的入口

先用模式二判断有环,相遇后让一个指针从 head 出发,另一个从相遇点出发,两个都每次走一步,再次相遇就是环入口。

数学证明:设环外长度 a,环入口到相遇点距离 b,相遇点到环入口距离 c。

  • slow 走了 a + b 步
  • fast 走了 a + b + n(b+c) 步(在环里转了 n 圈)
  • fast 是 slow 的两倍:2(a+b) = a + b + n(b+c)
  • 化简:a = (n-1)(b+c) + c
  • 即:a = n圈 + c,所以从 head 走 a 步 = 从相遇点走 n 圈 + c 步,两者在环入口相遇

解题模板

function fastSlowPointer(head: ListNode): ListNode | null {
    let fast = head
    let slow = head
 
    // fast 先走 k 步(模式一)
    for (let i = 0; i < k; i++) {
        if (!fast) return head  // k 越界
        fast = fast.next
    }
 
    // fast 和 slow 一起走
    while (fast) {
        fast = fast.next
        slow = slow.next
    }
 
    // slow 现在在目标位置
    return slow
}

复杂度

  • 时间:O(n),一次遍历。
  • 空间:O(1),只用两个指针。

这是快慢指针最大的优势:不额外存节点,只用两个指针的间距来定位。 和用数组存节点的方法相比,空间从 O(n) 降到 O(1)。


总结

遇到需要找链表中特定位置的问题,先问自己:

  1. 我要找的位置是倒数第几个?→ fast 先走 k 步。
  2. 要删除还是只读取?删除 → fast 先走 k+1 步(slow 要停在前一个)。
  3. 能不用数组?能 → 快慢指针,空间 O(1)。

想清楚这三点,再动笔写代码。