【LeetCode 刷题】寻找环形链表的入口节点:快慢指针 + 数学推导(O (1) 空间)
在上一篇「判断链表是否有环」的基础上,本题是进阶考点 ------不仅要判断链表是否存在环,还要找到环的第一个入口节点。常规解法(哈希表存储节点)会占用 O (n) 空间,而本文分享的「快慢指针 + 数学推导」解法,能在 O (n) 时间、O (1) 空间内精准找到环入口,是面试中最亮眼的最优解。
一、题目描述
环形链表 II 给定一个链表的头节点 head,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
核心要求
- 不允许修改给定的链表;
- 空间复杂度要求 O (1)(禁止使用哈希表等额外存储);
- 环的定义:链表中某个节点的
next指针指向链表中之前出现过的节点,形成环形结构。
链表节点定义
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
二、解题思路:快慢指针 + 数学推导(核心)
1. 两步核心逻辑
本题解法分为两个关键阶段,缺一不可:
阶段 1:快慢指针找「相遇点」
先用「快指针(步长 2)、慢指针(步长 1)」遍历链表:
- 若链表无环:快指针会先走到末尾(
null),直接返回null; - 若链表有环:快慢指针会在环内某个节点相遇(这是找到入口的前提)。
阶段 2:双指针找「环入口」
在快慢指针相遇后,新增两个指针:
index1:从链表头节点出发,每次走 1 步;index2:从快慢指针相遇点出发,每次走 1 步;两个指针相遇时,指向的节点就是环的入口节点(数学推导证明这一结论)。
2. 关键数学推导(面试高频考点)
要理解「为什么双指针能找到入口」,必须掌握以下推导(通俗版):
定义变量
a:链表头节点 → 环入口节点的距离;b:环入口节点 → 快慢指针相遇点的距离;c:相遇点 → 环入口节点的距离;L:环的总长度,L = b + c;n:快指针在环内绕的圈数(n ≥ 1)。
推导过程
- 慢指针走的总路程:
a + b(未绕环,只走了链表头到相遇点); - 快指针走的总路程:
a + b + n×L(走了 a+b 后,绕环 n 圈才和慢指针相遇); - 因为快指针速度是慢指针的 2 倍,所以:
2×(a + b) = a + b + n×L化简得:a = n×L - b又因为L = b + c,代入得:a = (n-1)×L + c。
结论
a = (n-1)×L + c 的核心含义:
- 从「链表头到环入口」的距离
a,等于从「相遇点绕环 (n-1) 圈后再走 c 的距离」; - 因此,
index1(走 a)和index2(走 (n-1)×L + c)最终会在环入口相遇((n-1)×L 是绕环整数圈,不影响相遇结果)。
3. 思路可视化(以 a=2, b=1, c=2, L=3 为例)
plaintext
链表结构:1 -> 2(入口)-> 3 -> 4 -> 2(环)
- a=2:头节点1 → 入口2的距离;
- b=1:入口2 → 相遇点3的距离;
- c=2:相遇点3 → 入口2的距离;
- L=3:环(2→3→4→2)的长度。
阶段1:快慢指针相遇
- 慢指针:1→2→3(路程a+b=3);
- 快指针:1→2→3→4→2→3(路程a+b+1×L=3+3=6,是慢指针2倍);
- 相遇点为3。
阶段2:双指针找入口
- index1:1→2(走a=2步);
- index2:3→4→2(走c=2步,绕环0圈);
- 相遇点为2(环入口)。
三、完整代码实现
public class Solution {
public ListNode detectCycle(ListNode head) {
// 初始化快慢指针,均指向链表头节点
ListNode fast = head;
ListNode slow = head;
// 阶段1:快慢指针找相遇点(无环则直接返回null)
while (fast != null && fast.next != null) {
// 快指针走2步,慢指针走1步
fast = fast.next.next;
slow = slow.next;
// 快慢指针相遇,进入阶段2:找环入口
if (fast == slow) {
// index1从链表头出发,index2从相遇点出发
ListNode index1 = head;
ListNode index2 = fast;
// 双指针每次走1步,相遇时即为环入口
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
// 返回环入口节点
return index1;
}
}
// 循环结束(快指针到末尾),说明无环
return null;
}
}