快慢指针算法学习记录

快慢指针

题目链接:

这两道题是链表专题中最经典的"快慢指针"应用,也是面试中出镜率极高的题目。141 只要求判断是否有环 ,142 进一步要求找到环的入口点。两题一脉相承,本文合在一起讲解。


第一部分:LeetCode 141. 环形链表 I

1. 题目描述

给你一个链表的头节点 head,判断链表中是否有环。如果链表中存在某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。返回 truefalse

复制代码
示例:head = [3,2,0,-4], pos = 1(尾节点连向下标为1的节点)
输出:true

2. 思路一:哈希表(先说,作为对比)

最直观的想法:遍历链表,把每个节点的地址 存入哈希表(unordered_set),如果遍历中遇到了已经存在哈希表中的地址,说明有环;如果走到了 nullptr,说明无环。

cpp 复制代码
class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head) {
            if (seen.count(head)) return true;
            seen.insert(head);
            head = head->next;
        }
        return false;
    }
};
  • 时间复杂度:O(N)
  • 空间复杂度:O(N) ------ 这是它的硬伤。面试时这个解法是"保底答案",说完之后面试官一定会追问:"能不能用 O(1) 空间?"------答案就是下面的快慢指针。

3. 思路二:快慢指针(Floyd 判环算法)

3.1 算法描述

快慢指针 :慢指针 slow 一次走一步,快指针 fast 一次走两步,两个指针从链表起始位置同时开始运行。

  • 如果链表带环 ,两个指针最终一定会在环中相遇
  • 如果链表不带环 ,快指针会率先走到链表的末尾(fastfast->nextnullptr)。

形象理解:这就像在环形操场上跑步,一个快一个慢------只要跑道是环形的,快的人一定会从后面追上(套圈)慢的人;如果跑道是直线,快的人跑完就结束了,两人永远不会"相遇"。

3.2 代码实现

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* slow = head;
        ListNode* fast = head;

        while (fast && fast->next)      // fast 能走两步才继续
        {
            slow = slow->next;          // 慢指针走一步
            fast = fast->next->next;    // 快指针走两步

            if (slow == fast)           // 相遇,说明有环
                return true;
        }
        return false;                   // fast 走到末尾,无环
    }
};

3.3 复杂度分析

  • 时间复杂度:O(N)。无环时 fast 走到末尾,约 N/2 次迭代;有环时,slow 入环后一圈之内必被追上,总迭代次数不超过 O(N)。
  • 空间复杂度O(1)。只用了两个指针。

3.4 代码细节说明

  1. 循环条件是 fast && fast->next,不是 slow 。因为快指针走得快、先"踩空"。只要 fastfast->next 都不为空,fast->next->next 就是安全的。slow 永远不会先越界,无需检查。
  2. 判断相等在移动之后。初始时 slow == fast == head,如果循环开始就判断会误判。把判断放在移动之后(或采用 do-while 结构)即可避开。
  3. 比较的是指针地址,不是节点值 。环的定义是"通过 next 能再次到达同一个节点",判断的是地址相等 slow == fast,不是 slow->val == fast->val

4. 面试必考追问:为什么一定能相遇?(数学证明)

这是面试官最爱挖的点。下面给出完整推理。

4.1 思考一:快指针走 2 步、慢指针走 1 步,有没有可能永远遇不上?

结论:不可能,一定相遇。

推理过程:

fast 比 slow 快,所以 fast 先进环 。当 slow 走完入环前的距离、即将进环的那一刻,fast 已经在环中某个位置了。设此时两者之间的距离为 N(沿前进方向,slow 落后 fast N 步,N 不超过环的周长)。

接下来进入"追逐阶段":

复制代码
每移动一次:
  slow 前进 1 步
  fast 前进 2 步
  两者距离缩小 1 步   (fast 每轮比 slow 多走 1 步)

距离变化:N → N-1 → N-2 → ... → 2 → 1 → 0(相遇)

距离每轮恰好缩小 1 ,必然在某一轮精确地缩小到 0,不存在"跨过"的可能。所以走 2 步 vs 走 1 步必定相遇。✅

4.2 思考二:快指针一次走 3 步、4 步......n 步,还行吗?

先说结论:理论上仍然可以相遇,但证明更复杂,且代码实现更繁琐,所以工程中统一用"1 步 vs 2 步"。

下面按 PDF 的思路分两步证明"走 3 步"的情形。

Step 1:距离变化分析

slow 每次走 1 步,fast 每次走 3 步,每移动一次,距离缩小 2 步。设 slow 刚入环时两者距离为 N,环的周长为 C:

复制代码
距离变化:N → N-2 → N-4 → ...

情况 1:N 是偶数 → 距离依次减 2,第一轮就精确到 0,追上了。✅

情况 2:N 是奇数 → 距离减到 1 后,下一轮变成 -1,
       即 fast 越过了 slow 一个身位,相当于新距离为 C-1,进入新一轮追逐:
         a. C-1 是偶数 → 下一轮追上了 ✅
         b. C-1 是奇数 → 每一轮都错过,永远追不上 ❌

表面上,追不上的条件是:N 是奇数 且 C 是偶数。但 Step 2 会证明这个前提根本不可能出现。

Step 2:证明"N 为奇数、C 为偶数"的情况不存在

设:环的周长为 C,头节点到环入口的距离为 L,slow 走 1 步、fast 走 3 步。

注意一个关键事实:N 与 C 的奇偶关系不是从总路程方程推出来的 (总路程中 fast 已走的 N 与后来要补的 C−N 会相互抵消,式子里根本不出现 N),而是从 fast 此刻的位置推出来的:

slow 走到入口时共走了 L 步,同一时间段内 fast 走了 3L 步,fast 恰好领先 slow 2L 步。这 2L 步的领先折算到环上,就是 fast 在环中的位置 N:

复制代码
2L = q·C + N      (领先 2L 步 = 绕 q 整圈 + 环上偏移 N,q ≥ 0)

现在分析奇偶性:等式左边 2L 一定是偶数 ,所以右边 q·C + N 也必须是偶数。可能的组合只有:

  • 偶数 = 偶数 + 偶数
  • 偶数 = 奇数 + 奇数

若 C 是偶数,则 q·C 是偶数,N 必然也是偶数------"C 偶、N 奇"的组合在结构上根本不可能出现。对照 Step 1 的结论:

  • C 偶 ⇒ N 偶 → 对应 Step 1 的情况 1,第一轮就追上;
  • N 奇 ⇒ C 奇 → 对应 Step 1 的 2-a,下一轮追上。

也就是说,Step 1 中唯一"永远追不上"的前提(N 奇、C 偶)根本不存在。所以快指针走 3 步最终也一定相遇。✅

💡 补充:相遇时的总路程方程为 2(L + m) = x·C(m 为 slow 入环后走的距离,x 为 fast 多绕的整圈数)------其中 fast 追逐前已走的 N 与追逐中要补的 C−N 相互抵消,因此式中不出现 N。该方程与位置关系 2L = qC + N 相减,即得讲义中常见的 2L = (x−q)·C − N 形式,二者是等价的。

快指针走 4、5......n 步的证明方式同上(相对速度 k−1 与周长的奇偶/整除分析),结论都是必定相遇

💡 提示 :虽然已经证明快指针走几步都能相遇,但走 3 步以上在写代码时要额外处理"每步移动前检查空指针"的问题(如 PDF 示例代码中用一个内层循环逐位移动 fast,每次检查 fast->next),代码更繁琐、更易出错。因此涉及快慢指针的算法题,一律习惯使用"慢指针 1 步、快指针 2 步"的写法------相对速度为 1,逐格逼近,逻辑最干净。


第二部分:LeetCode 142. 环形链表 II

1. 题目描述

给定一个链表的头节点 head,如果链表中有环,返回开始入环的第一个节点 ;如果无环,返回 null

复制代码
示例:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的节点(值为 2 的那个节点)

2. 思路一:哈希表

和 141 题一样,遍历链表,第一个重复出现的节点地址就是环的入口。

cpp 复制代码
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head) {
            if (seen.count(head)) return head;   // 第一个重复的节点即入口
            seen.insert(head);
            head = head->next;
        }
        return nullptr;
    }
};
  • 时间 O(N),空间 O(N)。同样是保底答案,面试官会追问 O(1) 空间解法。

3. 思路二:快慢指针 + 数学结论

3.1 算法分两步

第一步 :先用 141 题的快慢指针判断是否有环,并记录相遇点 (无环则直接返回 null)。

第二步:利用下面的核心结论找入口。

💡 结论 :让一个指针从链表起始位置 开始遍历,同时让另一个指针从判环时的相遇点 开始绕环运行,两个指针每次都只走一步 ,最终它们一定会在环的入口点相遇。

3.2 结论的证明(重点,面试必问)

符号设定(建议自己在纸上画一个带环链表对照):

复制代码
H ------------ L ------------ E ------------ X ------------ M
起点        入口点        相遇点
                ←--------- R-X ------------
                (M 沿环回到 E 的距离)

H:链表起始点
E:环的入口点
M:快慢指针的相遇点
L:H 到 E 的距离
X:E 到 M 的距离(沿环前进方向)
R:环的周长
则 M 到 E 的距离 = R - X

列方程:判环时,相遇点处两指针走过的路程:

复制代码
slow 的路程: L + X
fast 的路程: L + X + n·R

这里有两个要点需要说明:

  1. fast 可能已经在环里绕了 n 圈(n ≥ 1)。因为 fast 先进环,它可能已经在环中转了好几圈之后,才在 M 点与 slow 相遇。
  2. slow 进环之后,fast 一定在 slow 走完一圈之内追上它 。因为 slow 进环瞬间,两者的距离最多就是一个环长 R,而每移动一次距离缩小 1 步,所以 slow 最多走 R 步(一圈)就会被追上。因此 slow 的路程就是 L + X,不会多绕圈。

fast 的速度是 slow 的 2 倍,路程也是 2 倍关系:

复制代码
2(L + X) = L + X + nR

化简:
L + X = nR
L = nR - X
L = (n - 1)·R + (R - X)        (n = 1, 2, 3...,n 的大小取决于环的大小,环越小 n 越大)

解读最后一个式子

  • L ------ 一个指针从起点 H 走到入口 E 的距离;
  • (n-1)·R + (R-X) ------ 一个指针从相遇点 M 出发,先走 R-X 步正好到达入口 E,再多绕 n-1 整圈,还是在入口 E

极端情况最好理解:假设 n = 1,则 L = R - X,即"从 H 到 E 的距离"恰好等于"从 M 到 E 的距离"。

所以:一个指针从链表头出发,另一个指针从相遇点出发,都每次走一步,必然在入口点相遇(从相遇点出发的指针可能多绕了几圈,但相遇位置一定是入口)。✅

3.3 代码实现

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* slow = head;
        ListNode* fast = head;

        // 第一步:判环并找到相遇点
        while (fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;

            if (slow == fast)   // 相遇,确认有环
            {
                // 第二步:一个指针从头出发,一个从相遇点出发,同步走
                ListNode* p1 = head;
                ListNode* p2 = slow;   // 相遇点
                while (p1 != p2)
                {
                    p1 = p1->next;
                    p2 = p2->next;
                }
                return p1;      // 相遇处即环的入口
            }
        }
        return nullptr;         // 无环
    }
};

3.4 复杂度分析

  • 时间复杂度:O(N)。第一阶段判环 O(N),第二阶段两指针各走不超过 L + R 步,整体仍是 O(N)。
  • 空间复杂度O(1)。只用了常数个指针。

第三部分:面试官可能追问的问题汇总

这部分把两题相关的常见面试追问整理成问答,建议每条都能不看书复述。

Q1:为什么用快慢指针?哈希表不行吗?

哈希表可以,但空间复杂度是 O(N)。快慢指针把空间压到 O(1),时间不变。这是本系列题的核心考点------空间优化

Q2:为什么 slow 进环后,fast 一定能在一圈之内追上它?

slow 进环瞬间,fast 与 slow 的距离最多为环长 R。此后每轮距离恰好缩小 1(相对速度为 1),所以最多 R 轮内距离归零。slow 走 R 步刚好一圈,故一圈之内必被追上。

Q3:快指针走 2 步,有没有可能"跳过"慢指针而永远不相遇?

不会。相对速度为 1,距离按 N, N−1, N−2, ..., 1, 0 逐格缩小,不存在跨越。这正是选"1 步 vs 2 步"而不是其他步数组合的根本原因------相对速度 1 保证逐格逼近。(走 3 步虽然理论上也能相遇,但证明复杂且代码要逐位检查空指针,工程上不用。)

Q4:142 题中,为什么从 head 和相遇点同步出发会在入口相遇?

复述推导:相遇时 2(L+X) = L+X+nR,化简得 L = (n−1)R + (R−X)。从头走 L 步到入口,从相遇点走 R−X 步加 n−1 圈也正好到入口,两者步数相同、同步出发,故在入口相遇。这个公式建议能默写推导。

Q5:142 题中第二阶段,从相遇点出发的指针为什么要"绕 n−1 圈"?

因为 fast 在 slow 入环前可能已经在环中绕了 n 圈。环越小、入环前路径 L 越长,n 就越大。结论式中的 (n−1)R 正是补偿这部分------无论 n 是多少,最终相遇位置都是入口。

Q6:边界情况怎么处理?

  • 空链表 / 单节点无环while (fast && fast->next) 第一次就不成立,直接返回 false / nullptr,天然处理。
  • 单节点自环head->next == head):slow 走一步还在 head,fast 走两步也回到 head,相遇,正确判环。
  • 两个节点的环:同上,一两轮内即相遇。
  • 环入口就是头节点(L = 0):第二阶段 p1 从 head 出发不动也能在 head 处相遇......实际上 p1、p2 会各自走 R 步后在 head 相遇,结果正确。

Q7:判断环时比较 slow == fast 还是比较 val

比较指针地址。环是结构上的概念(next 指针指回了前面的节点),与节点存的值无关。链表中完全可能有重复的 val。

Q8:第二阶段找入口时,会不会出现 p2 走得太快错过入口?

不会。第二阶段两个指针都走一步,速度相同,是相对静止的追逐------距离固定为 L 与 R−X 的差值对应的环上偏移,由公式保证恰好在入口点汇合。

Q9:这两个算法叫什么名字?有出处吗?

Floyd 判圈算法(Floyd's Cycle-Finding Algorithm),也叫"龟兔赛跑算法"(Tortoise and Hare)。面试中说出这个名字会加分。

Q10:快慢指针还能解决哪些链表题?(举一反三)

同一个"快慢指针"思想还能解决:

题目 快慢指针的用法
链表的中间节点(LeetCode 876) fast 走 2 步、slow 走 1 步,fast 到尾时 slow 在中点
删除链表的倒数第 N 个结点(LeetCode 19) fast 先走 N 步,再与 slow 同步走,fast 到尾时 slow 在倒数第 N+1 个
回文链表(LeetCode 234) 先用快慢指针找中点,再反转后半段比较
相交链表(LeetCode 160) 可借用"是否成环/双指针同步"思想
环形链表 I / II(本文) 判环 + 找入口

总结

LeetCode 141 LeetCode 142
问题 是否有环 环的入口在哪
核心方法 快慢指针(Floyd 判圈) 先判环,再双指针同步找入口
关键结论 相对速度为 1,环中必相遇 L = (n−1)R + (R−X),头指针与相遇点指针在入口相遇
时间复杂度 O(N) O(N)
空间复杂度 O(1) O(1)
面试加分项 证明必相遇;讨论走 3 步的情形 默写公式推导;说明 n 的含义

记忆口诀:"一快一慢必相遇,一头一遇同步走,相逢之处即入口。"

相关推荐
sunshine22 girl1 小时前
Angular7,9,学习笔记四 父子组件传值,组件之间传值
笔记·学习
benchmark_cc1 小时前
1000只ETF的5分钟K线如何批量获取?QuantDash分页策略与高性能Python实践
开发语言·人工智能·爬虫·python·算法·quantdash·量化数据源
sel_91 小时前
【PEFT】参数高效微调(PEFT)技术详解:从原理到 LoRA/QLoRA 实战
人工智能·python·深度学习·算法·机器学习·参数高效微调
我星期八休息1 小时前
Linux I/O多路转接—epoll
java·linux·运维·服务器·开发语言·jvm·算法
蒸蒸yyyyzwd2 小时前
cpp选手秋招学习笔记 day18
笔记·学习
HugoStudio_SWAN2 小时前
C++ CMD 互动动画:按键触发爆炸效果
开发语言·c++·学习·程序人生
爱学习的小郑2 小时前
IC验证学习
学习
田径他爸来了2 小时前
嵌入式学习第32天(epoll)
学习
阳明山水2 小时前
Mamba与两阶段XGBoost的融合架构:面向间歇性需求的双路径预测框架
人工智能·深度学习·算法·机器学习·架构