← 返回博客列表

一、什么是快慢指针

快慢指针(Fast & Slow Pointers)是链表题中最经典的双指针技巧,又叫 Floyd's Tortoise and Hare(龟兔赛跑算法)。它设置两个指针同时从头节点出发,但移动速度不同:

一句话定义
快慢指针 = 两个同起点、不同速度的指针,靠"速度差"制造相遇或拉开距离,从而在一次遍历里解决环检测、找中点等问题。

它的典型应用几乎都集中在链表上:

应用 利用的特性 代表题目
环检测 有环则快指针必然追上慢指针 LeetCode 141 环形链表
找环入口 相遇后用数学关系定位入口 LeetCode 142 环形链表 II
找链表中点 快到尾时慢恰好走一半 LeetCode 876 链表的中间结点
倒数第 k 个 快先走 k 步拉开距离 LeetCode 19 删除倒数第 N 个
⚠ 适用前提
快慢指针本质是链表/顺序结构上的双指针。它依赖 next 指针的"单步前进",所以无法用于普通数组下标(数组没有环概念,找中点直接除以 2 即可)。判断一道题是否用快慢指针,先看操作对象是不是链表。

二、核心原理与图解

快慢指针的核心思想可以浓缩成两个直觉:"速度差导致追逐" 和 "速度比决定位置关系"。下面分四个场景图解。

2.1 速度差:线性链表上的追逐

在线性链表上,快指针每轮比慢指针多走 1 步。所以两者的距离每轮扩大 1。当快指针走到末尾(nullptr)时,慢指针恰好位于链表中部。这就是"找中点"的原理。

线性链表上快慢指针的追逐(1→2→3→4→5→∅) 初始: slow、fast 同在头节点 1 2 3 4 5 → ∅ S F 第 1 步: slow+1 到节点2,fast+2 到节点3 1 2 3 4 5 S F 第 2 步: slow+1 到节点3,fast+2 到节点5 1 2 3 4 5 S F 第 3 步: fast->next 为空 循环结束 slow 停在节点3=中点 绿=慢指针,红=快指针;fast 每轮多走 1 步,到尾时 slow 恰在中点
图 1:线性链表上快慢指针的追逐过程

2.2 环检测原理:为什么一定会相遇

如果链表有环,快指针永远不会走到 nullptr,而是会在环里无限转圈。关键问题是:快指针一定能在环里追上慢指针吗?

答案是一定。把目光聚焦到慢指针刚进入环的那一刻:此时快指针已经在环里某处。之后每走一轮,快指针比慢指针多走 1 步,也就是两者在环上的距离每轮缩小 1。距离从某个有限值开始,每轮减 1,最终必然减到 0——即相遇。

环形链表:快慢指针在环内相遇 1 head 2 环外段 a 3 入口E 4 5 相遇点M S F 相遇的本质: 慢指针进入环后, 快指针每轮多走 1 步, 环上间距每轮 -1, 必然在某轮减到 0 相遇。 若无环:fast 先到 ∅, 循环结束,返回无环。 有环时快指针在环内"套圈"慢指针;速度差恒为 1 保证不会跨过
图 2:环检测原理——快慢指针在环内必然相遇
为什么不会"跨过去"?
因为速度差恰好是 1。如果某轮快指针在慢指针后面 1 步,下一轮它就追上(距离 0);如果在前面,距离会从"环长 - k"逐轮减小,绝不会从 1 直接跳到 -1。这就是速度差为 1的精妙之处。

2.3 找链表中点:速度比决定位置

当快指针走到链表末尾时,慢指针走过的步数恰好是快指针的一半。因为快指针走的总步数 ≈ 链表长度,所以慢指针刚好停在链表中点。

找链表中点:fast 到尾时 slow 在中点 7 个节点(奇数): 1 2 3 4 5 6 7 → ∅ S F slow 在节点4 = 唯一中点 6 个节点(偶数): fast->next 为空时停止,slow 在节点4(靠右的中间节点) 奇数返回正中;偶数返回后半段的第一个(LeetCode 876 约定)
图 3:找链表中点——速度比 2:1 让慢指针停在中间

2.4 Floyd 判圈算法:两个阶段

找环入口的完整 Floyd 算法分两个阶段:

  1. 阶段一(相遇):快慢指针同速出发,慢走 1、快走 2,在环内某点 M 相遇。
  2. 阶段二(找入口):把一个指针重新放回头节点,另一个留在 M,两者都改为每次走 1 步,再次相遇的位置就是环入口 E。
Floyd 判圈算法的两个阶段 阶段一:追逐到相遇点 M head … E 入口 … M 相遇点 S F slow 走 1 步、fast 走 2 步 → 在 M 相遇 阶段二:从头出发再相遇于 E head … E 再次相遇=入口 … M P Q P 回 head、Q 留在 M,都走 1 步 → 相遇于 E(入口) 关键洞察 阶段一把"找入口" 变成"找相遇点"; 阶段二把"找入口" 变成"两个等速指针 再次相遇"。 数学保证两次相遇点 恰好都是环入口 E。
图 4:Floyd 判圈算法——相遇阶段与找入口阶段

2.5 复杂度分析

⏱ 时间复杂度:O(n)
环检测:慢指针进入环前最多走 n 步;进入环后,由于每轮距离缩小 1,最多再走"环长"步即相遇,合计 O(n)。
找中点:快指针最多走 n/2 轮即到尾,O(n)。

💾 空间复杂度:O(1)
只用两个(或三个)指针变量,不需要额外容器,这是快慢指针相对哈希表法的最大优势。

三、通用模板

快慢指针的代码极简,掌握下面两个模板就能覆盖绝大多数场景。

3.1 模板一:环检测(返回是否有环)

// 链表节点定义(所有题目通用)
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

// 模板一:判断链表是否有环
bool hasCycle(ListNode *head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {      // fast 能走才继续
        slow = slow->next;            // 慢走 1 步
        fast = fast->next->next;      // 快走 2 步
        if (slow == fast) return true; // 相遇 = 有环
    }
    return false;                      // fast 到 ∅ = 无环
}

3.2 模板二:找链表中点

// 模板二:返回链表的中间节点
// 偶数个节点时返回靠右的那个中间节点
ListNode* middleNode(ListNode *head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {       // fast 还能走 2 步就继续
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;                        // fast 到尾,slow 在中点
}
模板三要点
1. 循环条件恒为 fast && fast->next——保证 fast->next->next 不会解引用空指针
2. 先移动后判断——快慢指针先各走一步,再比较是否相遇(避免起点相同的误判)
3. 找中点奇偶差异——奇数返回正中;偶数时本模板返回靠右的中间节点(因为条件是 fast && fast->next)。若要返回靠左的,改用 fast->next && fast->next->next

四、示例一:环形链表(LeetCode 141)

题目(LeetCode 141):给定一个链表的头节点 head,判断链表中是否有环。如果链表中存在环,返回 true;否则返回 false。

4.1 思路

直接套用模板一:快慢指针同起点出发,慢走 1、快走 2。若快慢指针相遇则一定有环(参见图 2);若快指针走到 nullptr 则无环。

以链表 1→2→3→4→5,其中节点 5 的 next 指回节点 3(形成 3→4→5→3 的环)为例,追逐过程如下:

LeetCode 141 示例追逐:环 3→4→5→3 1 2 3 入口 4 5 追逐步骤: ① 起始:slow=1, fast=1 ② 1 轮:slow=2, fast=3 ③ 2 轮:slow=3, fast=5 ④ 3 轮:slow=4, fast=4 → 在节点 4 相遇,有环! S F 慢指针进入环后,快指针每轮靠近 1 步,3 轮后在节点 4 追上
图 5:LeetCode 141 环检测——快慢指针在节点 4 相遇

4.2 C++ 代码

#include <stdbool.h>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    bool hasCycle(ListNode *head) {
        if (!head) return false;          // 空链表必无环

        ListNode *slow = head, *fast = head;
        while (fast && fast->next) {
            slow = slow->next;          // 慢指针走 1 步
            fast = fast->next->next;    // 快指针走 2 步
            if (slow == fast) {
                return true;         // 相遇,存在环
            }
        }
        return false;                  // fast 走到末尾,无环
    }
};
💡 对比哈希表法
哈希表法把每个节点地址存入 unordered_set,遇到重复即有环。时间同样 O(n),但空间 O(n)。快慢指针法空间仅 O(1),是面试中更受青睐的写法。

五、示例二:环形链表 II(LeetCode 142)

题目(LeetCode 142):给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,返回 nullptr。

5.1 数学推导

这是 Floyd 算法的完整版。设(参见图 6):

Floyd 数学推导:a、b、c 三段距离 head … E a head→E … M … b E→M c M→E(绕回) 推导: slow 走:a + b fast 走:a + b + n(b+c) fast = 2·slow: 2(a+b) = a+b+n(b+c) ⇒ a = (n-1)(b+c) + c ⇒ a ≡ c (mod 环长) 即:从 head 走 a 步 = 从 M 走 c 步(+整圈) 两指针必在 E 相遇
图 6:LeetCode 142——a、b、c 距离关系与入口推导

推导过程:相遇时慢指针走了 a + b,快指针走了 a + b + n(b+c)(n 为快指针多绕的圈数,n ≥ 1)。由 fast = 2 × slow:

核心等式
2(a + b) = a + b + n(b + c)
⇒ a + b = n(b + c)
⇒ a = (n - 1)(b + c) + c

最后这个式子说明:从 head 走 a 步 = 从 M 出发走 c 步再绕 (n-1) 整圈,两者都会到达环入口 E。所以阶段二把一个指针放回 head、另一个留在 M,都改为每次走 1 步,它们必然在 E 相遇。

5.2 C++ 代码

#include <cstddef>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *slow = head, *fast = head;

        // 阶段一:快慢指针找相遇点
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
            if (slow == fast) {
                // 相遇,进入阶段二:找环入口
                ListNode *p = head;
                while (p != slow) {       // 两个都走 1 步
                    p = p->next;
                    slow = slow->next;
                }
                return p;               // 再次相遇 = 环入口
            }
        }
        return nullptr;                 // 无环
    }
};
⚠ 易错点
1. 阶段一的相遇判断必须在移动之后,否则起点相同的 slow/fast 会立刻"假相遇"。
2. 阶段二两个指针都改为走 1 步,千万别让其中一个还走 2 步。
3. 无环时 fast 会先到 nullptr,循环自然结束返回 nullptr,无需特判。

六、示例三:链表的中间结点(LeetCode 876)

题目(LeetCode 876):给定单链表的头节点 head,返回链表的中间节点。如果有两个中间节点,返回第二个中间节点。

6.1 思路

套用模板二。快指针走到末尾时,慢指针恰好在中间。对于偶数个节点,由于循环条件是 fast && fast->next,快指针会停在最后一个节点(fast->next 为空时停止),此时慢指针正好停在靠右的中间节点,符合题目要求。

LeetCode 876:偶数个节点返回靠右的中间节点 链表 1→2→3→4→5→6: 1 2 3 4 5 6 → ∅ S F fast 走到节点6(fast->next 为空停止),slow 停在节点4 = 第二个中间节点 追逐轨迹: 起始 slow=1, fast=1 → 第1轮 slow=2, fast=3 → 第2轮 slow=3, fast=5 → 第3轮 slow=4, fast=6 此时 fast->next 为空,循环结束,返回 slow = 节点4 偶数 6 个节点:中间是 3 和 4,题目要求返回第 2 个即节点 4
图 7:LeetCode 876——偶数节点链表的中点定位

6.2 C++ 代码

#include <cstddef>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode *slow = head, *fast = head;
        while (fast && fast->next) {     // fast 还能走 2 步就继续
            slow = slow->next;        // 慢走 1
            fast = fast->next->next;  // 快走 2
        }
        return slow;                  // fast 到尾,slow 即中点
    }
};
💡 延伸:归并排序链表
链表归并排序的第一步就是用快慢指针找中点,然后从中点切断、分别排序、再合并。这是快慢指针在非"找答案"场景下的重要应用——它把链表 O(1) 空间地一分为二。

七、总结与适用场景

快慢指针核心要点

维度 要点
本质 同起点、速度比 1:2 的两个指针,靠速度差制造相遇或定位
时间复杂度 O(n)——快指针最多遍历一次链表
空间复杂度 O(1)——仅需常数个指针变量
识别信号 题目是链表 + 问"是否有环/环入口/中点/倒数第 k 个"
关键不变量 循环条件恒为 fast && fast->next;速度差为 1 保证不跨过

典型题目清单

🎯 记忆口诀
"慢走一,快走二;有环必相遇,到尾在中点。找入口再放头,等速重逢即答案。"
前两句管环检测与找中点,后两句管找环入口。记住这四句,快慢指针的骨架就立住了。