力扣刷题#18-0142-环形链表II

力扣刷题#18-0142-环形链表 II

题目

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

不允许修改链表。

输入:head = [3, 2, 0, -4], pos = 1
输出:返回索引为 1 的链表节点(即值为 2 的节点)
解释:链表中有一个环,其尾部连接到第二个节点。
输入:head = [1], pos = -1
输出:null
解释:链表中没有环。

我的思路之旅:从"没什么思路"到恍然大悟

第一反应:懵了

刚看到这道题,我的反应是"没什么思路"。

上一题(#17 环形链表)只要求判断有没有环,我已经掌握了快慢指针——快指针每次走两步,慢指针每次走一步,如果它们相遇就有环。

但这题不一样:不但要知道有环,还要找到环的入口。我怎么知道快慢指针相遇后,环的入口在哪个位置?


方向一:快慢指针相遇后,回到入口?

直觉告诉我,快慢指针相遇的那个点,肯定和环入口有什么数学关系。

快指针跑的路程是慢指针的两倍——这个"两倍"里一定藏着信息。

但是我当时想不通:就算知道了这个"两倍关系",怎么把相遇点变成入口点呢?


方向二:自己画图找规律

我决定画一个图来理清思路。


手绘导图解析

我画了一个带环的链表,把所有关键位置标注出来:

图例说明:蓝色轨迹 = 慢指针移动路径,红色轨迹 = 快指针移动路径

图中变量含义(按我画的标注):

符号 含义
a 从 head 走到环入口 x₁ 的距离(步数)
x₁ 环的入口节点
x₂ 慢指针刚进入环时,快指针所在的位置(距 x₁ 恰好为 a)
b 从 x₂ 走到 x₃ 的距离(快指针走了 2b 步,慢指针从 x₁ 走了 b 步)
x₃ 快慢指针第一次相遇的节点

x₂ 不是随便一个中间点——它是慢指针踏进环的那一刻,快指针的"实时位置"。


慢指针的蓝色轨迹

慢指针每次走一步。

head ──a步──→ x₁(入口) ──b步──→ x₃(相遇点!)
         慢指针刚进环时,快指针在 x₂

走到相遇点 x₃ 时,慢指针共走了:

慢指针总路程 = a(head→x₁) + b(x₁→x₃) = a + b 步

快指针的红色轨迹

快指针每次走两步,速度是慢指针的 2 倍

关键时间点:慢指针刚踏入 x₁ 时

  • 慢指针走了 a 步到 x₁
  • 快指针走了 2a 步,其中 a 步到 x₁,另外 a 步在环里——所以快指针此刻正好在 x₂,且 x₁→x₂ = a。

之后,慢指针从 x₁ 出发、快指针从 x₂ 出发,继续在环里赛跑:

  • 慢指针走 b 步到达 x₃
  • 同一时间,快指针走 2b 步(2 倍速),也从 x₂ 到达 x₃

所以:

x₁→x₃(沿环)= b(慢走的)
x₂→x₃(沿环)= 2b(快走的)

走到 x₃ 时,快指针总共走了:

快指针总路程 = 2 × (a + b) = 2a + 2b 步

其中在环里的部分 = 2a + 2b - a(进环) = a + 2b 步。


推导演示(用具体数字)

a = 3,b = 2。链表共有:3 个前驱节点 + 一个长度为 5 的环。

head ──→ ① ──→ ② ──→ x₁(0) ──→ 1 ──→ x₃(2) ──→ x₂(3) ──→ 4 ──↺ 回到 x₁(0)
步数 慢指针位置 快指针位置(跳两步) 说明
0 head head 起点
1
2 1
3 x₁(0) x₂(3) ⚡ 慢进环!快刚好在 x₂(距 x₁ 为 3 = a)
4 1 x₁(0)
5 x₃(2) x₃(2) 🎯 快慢在 x₃ 相遇!

从进环到相遇:

  • 慢从 x₁ 走到 x₃:走了 b = 2 步 ✓
  • 快从 x₂ 走到 x₃:走了 2b = 4 步 ✓(x₂→4→x₁→1→x₃)

推导公式

快指针在环里的总路程 = a + 2b。这条路径从 x₁ 出发,经过 a 步到 x₂,再经 2b 步到 x₃,再加上可能绕的整圈 k:

a + 2b = (x₁→x₃) + k × C     (从 x₁ 到 x₃ 的最短距离 + k 个整圈)
a + 2b = b + kC               (因为 x₁→x₃ = b)
a + b = kC

所以环的总长:

C = (a + b) / k

关键的发现:x₃→x₁ = a

k = 1(最常见,快指针在环里多绕了 1 圈):

C = a + b

从相遇点 x₃ 沿环回到入口 x₁ 的距离:

x₃→x₁(沿环)= C - (x₁→x₃) = (a + b) - b = a

撞了!x₃ 绕回入口恰好也是 a 步!

路径 步数
head → x₁ a
x₃ → x₁(沿环方向) a

核心结论:head 到入口的距离,等于相遇点沿环回到入口的距离。

用例子验证:a=3,C=5,x₃→x₁ = 5 - 2 = 3 = a。✓

如果 k > 1(环更小,快多绕好几圈):从 x₃ 走 a 步仍然回到 x₁,只不过中间多绕了 (k-1) 圈——最终位置不变。推导:x₃→x₁ = C - b = (a+b)/k - b = a/k + b(1/k - 1)。虽然这个值不一定等于 a,但走 a 步绕 k 圈的环,模环长后落点仍然是 x₁。

这意味着什么?


两阶段算法:直觉打通

既然 x₃→x₁ = a(相遇点到入口的距离 = head到入口的距离),那就有了一个漂亮的解法:

阶段一:找相遇点(和 #17 一样)

快指针每次走两步,慢指针每次走一步。如果有环,它们必定在环上某点相遇。记录相遇点 meet(即图中的 x₃)。

阶段二:找入口(关键!)

把快指针拨回 head,慢指针留在相遇点 x₃
两个指针都改为每次走一步
它们会在环的入口 x₁ 处相遇!

为什么会相遇在入口?

因为 x₃→x₁ = a:

  • 从 head 出发,走 a 步 → 到入口 x₁
  • 从相遇点 x₃ 出发,沿环走 a 步 → 也到入口 x₁

两个指针同步、同速,一个从 head 出发,一个从相遇点 x₃ 出发——走了相同的 a 步后,同时到达 x₁。

操场类比

两个人在操场跑步,A 从起点(head)出发,B 从操场某处(相遇点 x₃)出发。已知起点到操场入口的距离 等于 B 所在位置沿跑道回到入口的距离。两人同速同向走相同的步数后,一定同时到达操场入口。


数学上为什么成立(无需担心 k > 1 的情况)

上面我推导了 k = 1(快指针在环里只多绕了 1 圈)时,x₃→x₁ = a。

那如果 k > 1(环很小,快指针多绕了好几圈)呢?

回顾核心等式:2a + b = k × C(C 是环总长)

当 k = 2 时:C = (2a+b)/2。x₃→x₁ = C - (a+b) = (2a+b)/2 - (a+b) = a/2 - b/2。这不等于 a。

但别慌!从 x₃ 出发走 a 步:

走 a 步在环上的位移 = a mod C

由于 2a+b = 2C(k=2),代入:

a = 2C - b - a
a = C + (C - a - b)
a = C + (x₃→x₁)
a mod C = x₃→x₁

所以从 x₃ 走 a 步(模环长)正好到达 x₁!多出来的一个整圈不影响"最终落在 x₁"这个事实。

所以无论快指针绕了多少圈,从 x₃ 出发走 a 步一定回到入口 x₁。阶段二永远正确。


我的 AC 代码

/**
 * 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, *fast = head;
        // 阶段一:找相遇点
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
            if (fast == slow) {  // 有环,进入阶段二
                // 阶段二:找入口
                fast = head;     // 快指针拨回头部
                while (fast != slow) {
                    fast = fast->next;
                    slow = slow->next;
                }
                return fast;     // 相遇点就是入口
            }
        }
        return nullptr;          // 无环
    }
};

执行结果:8ms,击败 61.67%;内存 11.04MB,击败 93.61%。

代码逐段解析

阶段一(第 13-14 行):

while (fast && fast->next) {
    slow = slow->next;
    fast = fast->next->next;

和 #17 快慢指针判环完全一致。while 条件确保 fastfast->next 都不为空时才向前走。

相遇判断(第 15 行):

if (fast == slow) {

注意是先走再判断。如果先判断再走,第一轮 slow 和 fast 都在 head,会误判为"已相遇"。

阶段二(第 17-21 行):

fast = head;
while (fast != slow) {
    fast = fast->next;
    slow = slow->next;
}
return fast;

fast 拨回 head,两指针同速(每次一步)前进,再次相遇时就是环入口。

无环处理(第 25 行):

return nullptr;

如果 while 因为 fastfast->next 为空而退出 → 链表无环 → 返回 nullptr


踩坑记录

坑 1:while 条件写错了对象

// ❌ 第一版
while (head->next && head) {
    slow = slow->next;
    fast = fast->next->next;
问题 说明
判断对象错了 应该判断 fast 是否为空,不是 headhead 永远指向链表头,永远不会变
短路求值顺序 head->next 写在 head 前面,空链表时先执行 head->next → 对空指针解引用 →
// ✅ 修复:判断 fast 和 fast->next
while (fast && fast->next) {

这和 #17 通过的那版条件完全一样。

坑 2:fast->next->next 缺少保护

fast = fast->next->next;  // 危险!

如果没有先检查 fast->next 是否为 nullptr,无环链表中 fast 走到尾节点时:

  • fast->nextnullptr
  • fast->next->next → 对空指针解引用 →

修复方式:保证这行代码在 while(fast && fast->next) 内部执行。


优化分析

是否可以优化?

优化项 收益 结论
删掉空链表特判 减 1 行 可删(while 条件已覆盖)
NULLnullptr 现代 C++ 规范 推荐统一
算法本身 已是理论最优 Floyd 判圈 = O(n) + O(1)

Floyd 判圈算法是这道题的已知最优解,时间 O(n)、空间 O(1),没有更优的算法可换。

8ms 的排名是 61.67%,不代表代码慢——这道题大部分提交都在 7-9ms 这个区间,被"挤"在同一档。


关键点总结

关键点 说明
两阶段法 ① 找相遇点 → ② 找入口
核心直觉 head→入口 = 相遇点→入口
while 对象 判断 fast && fast->next,不是 head
先走后判 slow/fast 各走一步,再判断 fast == slow
fast = head 阶段二前把快指针拨回头部
第二次相遇 两个指针都走一步,相遇点就是环入口

与 #17 环形链表的对比

维度 #17 环形链表 #18 环形链表 II
要求 判断是否有环 找到环的入口
阶段一 快慢指针判环 同 #17
阶段二 无(直接返回) 一指针回 head,同步走找入口
返回值 true / false ListNode*(入口节点 / nullptr)
核心洞察 快慢必然相遇 x₃→x₁ = a(head→入口 = 相遇点绕回入口)

复杂度分析

维度 说明
时间复杂度 O(n) 阶段一和阶段二各遍历不超过 n 步
空间复杂度 O(1) 只用了两个指针

本文档由 AI 辅助生成,作者提供问题,思路和代码,AI仅负责文本修饰,综合获得以上内容。

posted @ 2026-06-18 16:45  STA_running  阅读(8)  评论(0)    收藏  举报