☰
环形链表 II 详解:快慢指针寻找环入口的数学推导与实现
2026/10/18 10:46:26 网站建设 项目流程

环形链表 II 这道题,在链表相关的算法题里算是“绕不过去的一道坎”。LeetCode 第 142 题,要求我们不仅判断链表有没有环,还要精确找到“环的入口节点”。很多同学能看懂 141 题的判环写法,但到了这一题,快慢指针相遇之后的那套“再走一次”的操作,往往只能靠背结论。这篇文章我想把那么“结论”背后的为什么彻底拆开,从数学推导到代码实现,再到面试现场怎么讲清楚,一轮讲透。

这道题真正解决的是:在链表的尾节点指向了前面某个节点、形成闭环的场景下,我们要把一个链表的完整形态还原出来,定位出环起点。它适合三类人阅读:第一类是准备技术面试、正在刷题的开发者;第二类是理解了快慢指针判环、但对“求入口”的推导感到模糊的人;第三类是准备给他人讲题、需要把思路梳理清楚的读者。无论你是哪种,建议先弄明白一个核心点——这题的标准解法不是二分、不是动态规划,而是 Floyd 判圈算法,也就是快慢指针。

1. 题面拆解:为什么“找入口”比“判环”难一个量级

1.1 题目要求与输入输出特征

题目给的输入只有一个链表的头节点 head,我们要返回的是“链表开始入环的第一个节点”。这里有个容易被忽略的边界条件:如果链表没有环,返回 null。

举个最简单的例子。链表 A -> B -> C -> D -> B,这里 B 就是入环点,C、D 在环内,D 的 next 指向 B。这个链表里存在环,但环并不是从 head 开始的,而是从中间的 B 节点开始。入环点就是那个“环内第一个节点”,也就是说从 B 走下去会绕圈,但 B 本身是环的一部分。

再看一个容易混淆的例子。链表 head -> A -> B -> C,然后 C 指向 A。这个环的入口是 A,而不是 head。很多初学者会把“相遇点”当成入口,这是错误的。相遇点只是两个指针第一次碰面的位置,它取决于快指针的绕圈次数和环的大小,不一定等于入环点。

所以题目的核心有三个动作:确认链表中是否存在环、找到环的起点、处理无环情况。判断前后两个动作其实都可以复用思想,但中间的“找到起点”是最考验推导能力的一环。

1.2 从 141 题到 142 题的思路升级

141 题只需要返回布尔值,告诉我们有没有环。标准的做法是快慢指针同时从 head 出发,慢指针每次走一步,快指针每次走两步。如果链表中存在环,那么快指针一定会在环内“追上”慢指针,二者相遇。

但在 142 题里,仅仅判断是否相遇是不够的。我们需要在相遇之后继续利用某种几何关系,把入环点“算”出来。这里很多人会卡住:明明快慢指针已经相遇了,然后呢?为什么要把快指针重新放到 head,然后两个指针同时以相同速度前进?为什么它们再次相遇的位置就是入环点?

这背后是一个有趣且严谨的数学等式。只要把这个等式推导一遍,以后再遇到这题,就不需要背结论了。

提示:如果只是写题解,很多文章会直接给出代码。但面试官真正想听到的是“你为什么敢在相遇后让快指针回到 head”。推导过程才是得分点。

2. 先看最直白的方案:哈希表记录法

2.1 哈希表法的核心思路

在引入 Floyd 判圈法之前,我们先把哈希表法讲明白。它的思路非常直接:从 head 开始遍历链表,每经过一个节点,就把节点的引用存入一个哈希集合中。如果当前节点已经在集合里,说明这个节点就是环的入口,直接返回它;如果一直走到 null,说明无环,返回 null。

为什么哈希表法在这里是正确的?关键在于链表中每个节点都是内存中的独立对象,即使两个节点的 val 相同,它们在内存中的地址也不同,引用不相等。用集合存引用而不是存值,才能准确判重。

以链表 A -> B -> C -> D -> B 为例,遍历顺序是 A、B、C、D,第五次回到 B。当检查第五个节点 B 时,会发现 B 已经在集合中,于是返回 B。这个过程非常直观,不需要什么数学推导。

2.2 哈希表法的代码实现与复杂度

对应的代码大概是下面这个样子:

def detectCycle(head): visited = set() cur = head while cur: if cur in visited: return cur visited.add(cur) cur = cur.next return None

这段代码逻辑清晰,看起来没什么问题。它的时间复杂度是 O(N),因为每个节点最多被访问一次;空间复杂度是 O(N),因为我们需要额外的集合来存放所有已访问的节点。

如果面试时你只写出这个版本,面试官通常会追问一句:能不能把空间复杂度优化到 O(1)?所以才有了快慢指针法。换句话说,哈希表法是一个用来验明思路的“地基”,而快慢指针法才是这个题目的精华。

2.3 哈希表法的局限性

哈希表法的优点是容易理解,几乎不可能写错。但它的缺陷也很明显:当链表长度达到百万级,额外使用一个哈希集合会占用可观的堆内存,这在嵌入式、底层系统或内存受限的场景下是无法接受的。

更重要的是,哈希表法没有充分挖掘链表“指针指向”这个特性。链表结构里天然包含了后继信息,我们完全可以利用数学上的追赶问题来求解,而不是简单地把所有节点都记下来。

3. Floyd 判圈法:快慢指针如何把环“跑”出来

3.1 快慢指针的基本设定

Floyd 判圈算法来自循环检测思想,它在链表题的出场率极高。标准做法:初始化两个指针 slow 和 fast,都指向 head。slow 每次移动一步,fast 每次移动两步,两个指针同时移动。

如果链表无环,那么 fast 会在某一刻到达 null 或者 fast.next 为 null,此时循环结束,返回 null。

如果链表有环,那么 fast 和 slow 最终会在环内的某个位置相遇。需要注意,相遇的位置一定在环内,不可能在环外。因为 slow 一旦进入环,它就在环内绕圈;fast 已经比 slow 早进入环,也一定在环内绕圈,二者最终会追上。

这里有个常见疑问:为什么快指针每次走两步,而不是三步、四步?从原理上说,只要两个指针速度不同,它们在环内就存在速度差,最终一定能够相遇。但选择“两倍速”有两个好处:

  • 最小且直观的速度差:慢指针走 1 步,快指针走 2 步,相对速度为 1 步/单位时间,最容易分析追及关系;
  • 数学推导简洁:后续求入环点时,两倍速的关系会产生极其干净的等式。

用生活化的例子来说,这就像操场跑步。慢者在前面跑,快者在后面追,只要跑道是闭合的,快者一定会追上慢者。关键在于追上时,快者已经比慢者多跑了一段“完整的环长”的整数倍。

3.2 第一次相遇的代码框架

我们先写一个只负责“判环”的版本,方便理解:

def hasCycle(head): if not head or not head.next: return False slow = head fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: return True return False

注意这里 while 条件写的是fast and fast.next,因为 fast 一次跳两步,只有确保 fast 和 fast.next 都不为空,才能安全地访问 fast.next.next。

在 142 题中,我们不是立刻返回 True,而是要在相遇之后继续操作,找出入口。于是代码结构会变成:

def detectCycle(head): if not head or not head.next: return None slow = head fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: fast = head while slow is not fast: slow = slow.next fast = fast.next return slow return None

这段代码前半段是标准的判环,后半段是关键:把 fast 重新指向 head,然后两个指针都以每次一步的速度前进。它们再次相遇的位置,就是环的入口。

4. 最关键的数学推导:为什么“再次相遇点 = 入环点”

4.1 参数定义与符号约定

为了把推导讲清楚,我们先定义几个符号。

假设环的入口节点为 P,头节点 head 到 P 的距离为 a。也就是说,从头节点出发,移动 a 步到达环的入口节点。

假设第一次相遇点(也就是 slow 和 fast 第一次碰面的节点)为 M。从 P 节点出发,顺时针移动 b 步到达 M。注意:这里的顺序是头节点 → 入口 → 相遇点。

从 M 继续顺时针移动 c 步,会再次回到入口节点 P。那么环的长度 L = b + c。

同时,我们用 n 表示从速度较快的那根指针开始进入环,到它与慢指针第一次相遇为止,快指针在环内绕的完整圈数。n 是一个大于等于 1 的整数。

用一张简单的示意图帮助理解:

head --a步--> P(入口) --b步--> M(相遇点) ^ | | c步 | +------------+

实际上环的结构可以看作三段:a 是入环前的直线部分,b 是入口到相遇点的那段弧,c 是相遇点回到入口的那段弧。

4.2 两倍速关系带来的关键等式

现在开始推时间。

当第一次相遇发生时,我们假设 slow 走过的总距离是 S。slow 从头节点出发,直到相遇,这个过程中它最多只在环内走了一段弧 b,不会重复绕圈。为什么?因为在 slow 进入环之后,快指针已经领先它一截;慢指针可能还没走完一圈,两指针就已经相遇了。严谨地说,在第一次相遇发生前,slow 在环内走过的长度必然不超过一圈。如果等于一圈,它会回到入口 P,而这时 fast 也在环内绕了整圈,二者在入口处相遇,等价于 b=0 的情况,也是允许的。

所以 S = a + b。

快指针走过的距离是 F。由于 fast 的速度是 slow 的 2 倍,所以相同时间内 F = 2 * S = 2 * (a + b)。

另一方面,快指针的路径可以从结构上分析:它先走完 a 到达入口 P,然后进入环内绕圈。在第一次相遇前,fast 在环内绕了 n 圈整,再多走一段弧 b,最终到达相遇点 M。因此:

F = a + n * (b + c) + b

这里的 (b + c) 就是整圈环长 L。

把两个 F 的表达式联立:

2 * (a + b) = a + n * (b + c) + b

化简:

2a + 2b = a + b + n * (b + c) a + b = n * (b + c)

这个等式就是整个算法的核心。它的左边是 slow 走过的距离,右边是 n 倍的环长。它告诉我们:从头节点到相遇点的这段距离,恰好是环长的整数倍。

继续变形,目标是解出 a:

a = n * (b + c) - b a = n * (b + c) - b a = (n - 1) * (b + c) + c

这个式子非常漂亮。它表明:从头节点走到入口的步数 a,等于“从相遇点 M 走 c 步回到入口 P,然后再绕 (n-1) 整圈”的总步数。

4.3 推导结果与第二次遍历的对应关系

有了上面的式子,接下来就顺理成章了。

我们把 fast 指针重新放回 head,然后两个指针都每次走一步。当 slow 从 M 点出发,走 c 步,它会先回到入口 P;此时 fast 从 head 出发,走 c 步,它到了什么位置?

由于 a = c + (n-1) * L,所以 fast 走了 c 步之后,距离入口节点还差 (n-1) 圈。也就是说,fast 还没到入口。但我们已经知道,两个指针速度相同,起点不同,它们最终会在某个时刻相遇。现在从 c 步之后继续走:fast 还需要走 a - c 步,即 (n-1)*L 步,才能到达入口;而 slow 也在同时从入口 P 出发,绕 (n-1) 圈,最终也回到入口 P。

所以二者再次相遇的位置,正是入口 P。这段路程中,两个指针恰好都走了 a 步,构成一次精确会合。

用更通俗的话总结:第二次遍历时,一个指针从 head 出发,另一个从第一次相遇点出发,它们速度相同。因为距离 head 到入口的步数,与相遇点走回入口的步数加上若干个整圈相等,所以当第一个指针走到入口时,第二个指针也一定回到入口。于是两点相遇。

注意:这里的“走 c 步”是借用推导过程,实际上代码里让两个指针同时一步一步走即可,不需要人为控制步数。它们自行会相遇。

4.4 一个具体数字例子

为了让这个推导不那么抽象,来一个实例。

假设链表是 head(H) -> 1 -> 2 -> 3 -> 4 -> 2,其中节点 2 是环的入口 P。那么:

  • a = 2(H 到节点 1,再到节点 2,共 2 步)
  • 环长为 4(节点 2、3、4、2)
  • 假设 slow 与 fast 第一次相遇发生在节点 4,即 M = 节点 4
  • 那么从入口 2 到 M 节点 4,b = 2(2 走一步到 3,再走一步到 4)
  • 从 M 回入口,c = 2(4 走一步到 2,正好入口)

代入式子 a = (n-1)*L + c,得 2 = (n-1)*4 + 2,可以取 n=1,满足条件。第一次相遇时,fast 从 head 出发,走 a + b = 2 + 2 = 4 步就到 M;slow 走 2 步也到 M,而此时 fast 在环内只跑了 1 圈加 b 段,二者正好相遇。

第二次遍历时,fast 回到 head,slow 留在 M。两个指针同时走一步:slow 到 2(入口),fast 到 H.next(节点 1)。第二次走一步:slow 到 3,fast 到 2。第三次走一步:slow 到 4,fast 到 3。第四次走一步:slow 到 2(入口),fast 到 4。第五次走一步:slow 到 3,fast 到 2。第六次:slow 到 4,fast 到 3。第七次:slow 到 2,fast 到 4……这看起来没有相遇?等一下,我上面这段推演方向弄错了,需要重新纠正,避免误导。

通常如果 n=1,那么 c 应该等于 a。上述 a=2、c=2 满足。第二次遍历时,两个指针都每次一步,从不同位置出发。fast 从 head,slow 从 M。因为 a = c 且 n=1,fast 走 2 步到入口 2;slow 从 M 出发走 2 步,也到入口 2。它们会在第 2 步同时到达入口,正好相遇。所以下一次相遇就在节点 2 处。

但刚才我上面从 M 点出发的第一个步长方向应该按原链表 next 方向走:M=4 的 next 指向 2,所以 slow 第一步到 2;第二步到 3;第三步到 4……这就不对。这里出了问题,关键在于我的例子符号方向:我说“从 M 回入口,c=2”,通常“相遇点 M”是通过 next 方向继续前进回到入口,那么 M.next 就是指回入口。但我的示例里 4.next 是 2,所以 M 的 next 确实回到入口 2。走一步就到 2,c 应该等于 1 而不是 2。所以例子不匹配。

为了让推导自洽,我们应该让 b+c 构成环长。假设环入口是节点 1?这样重新举例:H -> 1 -> 2 -> 3 -> 1,其中 1 是入口。a = 1,环长 L = 3。如果 slow 和 fast 第一次相遇在节点 3:从入口 1 出发,走一步到 2,走两步到 3,所以 b=2;从 3 走一步回到 1,所以 c=1。代公式 a = c + (n-1)L = 1 + 0 = 1,n=1,成立。

第二次遍历中,fast 从 head 出发,slow 从 M(节点 3)出发,各走一步:fast 到 1,slow 从 3 的 next 到 1。两人立刻在入口 1 相遇。完美匹配。

所以一定要保证例子里 c 的步数与 next 方向一致。我重新设计例子时就别东扯西扯。用这个 H->1->2->3->1 例子就好:head 记为 H,节点 1 的值是1,也是入口。

这样例子简洁:a=1,b=2,c=1,L=3,n=1。第一次相遇点在节点 3;第二次两个指针各走 1 步在入口 1 相遇。

4.5 为什么快指针在第一次相遇前一定只比慢指针多跑 n 整圈

这个点经常被追问。设相遇前 fast 在环内绕了 n 圈整,这是根据速度关系自然得出的。因为 F = 2S,F - S = S,也就是相遇时快指针比慢指针多走的距离恰好等于慢指针的总距离。慢指针的总距离是 a+b,而 a+b 恰好是环长整数倍(已经推导过),所以快指针比慢指针多跑的距离就是整数圈。这样一来,n 的取值为正整数,且通常比实际绕圈数更有解释意义。

在一个环中,只要速度差大于零,快指针最终一定能赶上慢指针,这就是循环追及问题的核心。

5. 代码实现与边界条件处理

5.1 Python 完整实现

把上面的推导落实到代码,最终版本非常简短:

class ListNode: def __init__(self, x): self.val = x self.next = None def detectCycle(head): if not head or not head.next: return None slow = head fast = head # 第一阶段:找到第一次相遇点 while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: break else: return None # 第二阶段:重新定位入口 fast = head while slow is not fast: slow = slow.next fast = fast.next return slow

这里有个 Python 技巧:while...else结构。如果循环正常结束,说明 fast 或者 fast.next 变成了空,代表无环,执行return None;如果触发了 break,说明找到了相遇点,跳过 else 块。

第二阶段不需要判空,因为已经确认链表有环。两个指针一定会在进入环之前或进入环的瞬间相遇,不会出现某个指针为空的情况。

5.2 Java 与 C++ 版本参考

用 Java 写会稍显啰嗦,但整体结构一致:

public class Solution { public ListNode detectCycle(ListNode head) { if (head == null || head.next == null) { return null; } ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { break; } } if (fast == null || fast.next == null) { return null; } fast = head; while (slow != fast) { slow = slow.next; fast = fast.next; } return slow; } }

C++ 同理,注意判空和指针比较:

class Solution { public: ListNode *detectCycle(ListNode *head) { if (head == nullptr || head->next == nullptr) { return nullptr; } ListNode *slow = head; ListNode *fast = head; while (fast != nullptr && fast->next != nullptr) { slow = slow->next; fast = fast->next->next; if (slow == fast) { break; } } if (fast == nullptr || fast->next == nullptr) { return nullptr; } fast = head; while (slow != fast) { slow = slow->next; fast = fast->next; } return slow; } };

5.3 边界情况:单节点、双节点、无环链表的处理

边界情况是这类题最容易写错的地方:

  • 空链表 head = null:直接返回 null;
  • 单节点无环:节点 next 为 null,返回 null;
  • 单节点自环:head.next = head,第一阶段 slow 和 fast 都会在第一步走到同一位置,慢指针走一步到 head,快指针走两步也到 head,相遇点就是入口 head;
  • 双节点环:1 -> 2 -> 1,入口是 1,第一次相遇点也是 1;
  • 长链无环:第一阶段正常遍历,fast 先到 null,返回 null。

在写代码时,最好把对 head 和 head.next 的空检查放在最前面,避免访问空指针的 next。

6. 手写链路推演:三种典型结构全步骤演示

6.1 结构一:无环链表

链表 H -> A -> B -> C -> null。slow 和 fast 都从 H 出发:

轮次slow 位置fast 位置状态
1HH初始
2AB未相遇
3Bnullfast 为空,无环

代码会执行return None。

6.2 结构二:环在链表尾部

链表 H -> A -> B -> C -> D -> B。入口是 B,环长是 3(B、C、D)。我们按次序推进:

轮次slow 位置fast 位置
1HH
2AC
3BB(已绕一圈?稍等需要计算)

这里不能简单罗列,我们还是严谨一点:初始都在 H,第一轮 slow 到 A,fast 到 C;第二轮 slow 到 B,fast 到 B?第二轮:slow 从 A 到 B,fast 从 C 到 D 再到 B?实际上 fast 一步跳两个节点,C.next 是 D,D.next 是 B,所以 fast 第二轮到 B。slow 第二轮到 B,二者正好在 B 相遇。此时 B 恰好就是入口,返回 B,非常顺利。

如果换成链表 H -> A -> B -> C -> D -> E -> C(入口 C,环长 3),初始在 H;第一轮 slow 到 A,fast 到 C?fast 从 H 两步跳到 C?这不可能,因为 head 到 A 是 1 步,到 B 是 2 步,到 C 是 3 步。fast 第一步从 H 跳到 B?等等,快指针一次跳两步,从 H 出发,第一步到达 B(H→A,A→B),不是 C。慢指针第一步到 A。第二轮 slow 到 B,fast 从 B 跳两步到 D(B→C,C→D)。第三轮 slow 到 C,fast 从 D 跳两步到 B(D→E,E→C?D.next=E,E.next=C,所以到 C;不对,D 到 C 是两步,也就是这一轮 fast 到达 C),所以相遇点就是 C,入口 C。

用公式验证:a=2(H→A→C,注意 head 到入口 C 是 3 步?从 H 出发,到 A 需要 1 步,到 B 需要 2 步,到 C 需要 3 步,所以 a=3。但是 slow 走到相遇点 C 需要 3 步,fast 速度是 slow 两倍,走了 6 步。环长 L=3,fast 进环前需要走 a 步,也就是从 H 到 C 走了 3 步,然后入环绕 1 圈又到 C,相遇。所以 n=1。第二次遍历时 fast 回 head,slow 留在 C,两边一步:fast 到 A,slow 到 D(C→D),没相遇;第二步:fast 到 B,slow 到 E;第三步:fast 到 C,slow 到 C,相遇,正是入口 C。完美。

这两个例子说明,相遇点不一定是入口,但第二次遍历一定会相遇在入口。

6.3 结构三:环在链表头部附近

链表 H -> A -> B -> A。入口是 A,环长是 2(A、B)。初始 H。第一轮:slow 到 A,fast 到 B。第二轮:slow 到 B,fast 从 B 跳一步到 A?不对,fast 每次两步:从 B 跳两步,B.next=A,A.next=B,所以 fast 回到 B;slow 到 B,二者在 B 相遇。相遇点 B,入口 A。然后 fast 回 head,两个指针同时走:第一步 fast 到 A,slow 从 B 走到 A,立刻相遇,返回 A。

这三个典型结构覆盖了无环、环在末端、环在头部附近三种场景,代码逻辑一致。

7. 常见错误与面试考点深度复盘

7.1 最容易犯的三种错误

错误一:判空条件写漏。在 while 中用fast.next.next时,如果 fast 本身是 null,程序会抛出空指针异常。所以必须写成while fast and fast.next,不能图省事。

错误二:初始指针设置不同。有些同学让 slow 先走一步、fast 先走两步再进入循环,这时要额外考虑初始位置的相遇判断,容易漏判。统一从 head 出发,使用前插式判断,代码更干净。

错误三:第一次相遇后直接返回相遇节点。这是这道题最常见的错误。相遇点不是入口,直接返回的话,如果数据恰好凑巧相同可能掩盖问题,但绝大多数情况下会得到错误答案。必须重新移动 fast 到 head,再走一次。

7.2 面试官追问清单

面试官在这道题上常见的追问包括:

  • 如果不能修改链表结构,你怎么做?(用上面的方法就是答案,它不修改链表结构)
  • 证明快慢指针一定会相遇吗?(追及问题,速度差存在且轨道闭合)
  • 如果快指针每次走三步,还能用同样的相遇推论求入口吗?(原则上仍能相遇,但不能推出 a = c + (n-1)L 这个简单等式,因此常规解法固定用两倍速,不要自行改动)
  • 如何求环的长度?(第一次相遇后,保持其中一个指针不动,另一个指针每次走一步,统计再次回到相遇点所需步数,就是环长)

在这些追问里,最需要重视的是数学推导要讲清。不要光说“这是模板”,而是把 a + b = n(b + c) 写出来,一边写一边解释含义。面试官真正看重的不是背诵能力,而是逻辑链是否完整。

7.3 与链表其他题目的横向关联

142 题的解法思想还能迁移到其他题目。

  • 141 题:只判环,不输出入口,是 142 的简化版。
  • 876 题“链表的中间节点”:快慢指针,快指针每次两步、慢指针每次一步,快指针到尾时慢指针恰在中点,属于同一思想的另一个应用。
  • 160 题“相交链表”:一种经典解法是把其中一个链表的尾部接到另一个链表的头部,转化成环形链表求入口,用的就是 142 的思想。

所以这道题不是孤立的“背结论题”。一旦吃透了 Floyd 判圈法,再去解决链表类的追及问题会顺手很多。

8. 复杂度对比与无额外空间的现实意义

8.1 两种方案的时间与空间对比

方案时间复杂度空间复杂度特点
哈希表法O(N)O(N)思路简单,代码短,但额外空间开销大
Floyd 判圈法O(N)O(1)需要推导数学关系,面试最优解

这里的时间复杂度需要说明:两个指针整体移动次数与链表长度成正比,如果链表很长且环很小,快指针可能需要在环内绕比较多圈才相遇,但总体仍在 O(N) 量级。具体到本题的朴素理解,就是从 head 到入环点加上环内有限次绕圈,不会超过线性次数太多。

Floyd 判圈法 O(1) 空间意味着它可以在不额外占用内存的情况下处理上亿节点的链表,在真实场景中比如内存管理、网络数据包检测、文件系统循环引用检测等方向,这种不增加存储开销的算法非常有价值。

8.2 实际工程中的抽象意义

链表环检测不只是面试题。在操作系统或数据库的日志记录、依赖关系解析、编译器符号表处理中,可能会遇到对象引用形成循环引用的问题。能判断是否存在环、找到环的起点,就可以进一步做循环清除或异常定位。

比如某个垃圾回收器在扫描对象引用图时需要识别循环引用,就可能采用类似的快慢指针思想。当然实际工程里情况更复杂,但核心的“标记-检测-定位”逻辑与 142 题一脉相承。

9. 这道题后续还能怎么扩展

我自己每年刷到这道题都会重新推一遍公式,不是因为记不住,而是每次推都能加深对“追及问题”的理解。

个人建议:不要满足于把代码 AC。可以尝试自己编不同结构的链表,比如入口在第一个节点、入口在中间、入口在最后一个节点、环长只有 1 等,手动按指针路径模拟一遍。这样再遇到变体题,比如“求环中包含的节点数量”或“判断两个链表是否相交”,你会更快找到办法。

扩展玩法是:把链表结构换成数组形式的“跳跃游戏”,给定一个数组,每个元素表示下一步跳到哪个下标,判断是否存在循环以及循环的入口。本质上就是 142 题的数组版,但它的解法依然可以用快慢指针思想,只不过下标访问代替了指针访问。能把链表题迁移到数组场景,说明你对 Floyd 判圈的掌握才真正到位。

如果后续想深入,建议一并刷完 141、876、160 三道题,形成一套“快慢指针四连”。这几道题放在一起,既能通过对比理解不同场景下的细节差异,也能为二叉树、图里的循环检测打下基础。

最后分享一个我实际用起来很顺的小办法:讲这道题的时候,别急着写代码,先在白板上画一条带环的链表,然后用笔模拟快慢指针的每一步。画完之后,公式推导自然就有了。真正理解之后,你根本不需要背答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询