【Hot100|25-LeetCode 142. 环形链表 II - 完整解法详解】


一、问题理解

问题描述

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

为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改链表。

示例

示例 1:

text

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

(图:节点 -4 的 next 指向节点 2,环入口为节点 2)

示例 2:

text

复制代码
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点,环入口为节点 1。

示例 3:

text

复制代码
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

要求

  • 时间复杂度: O(n)

  • 空间复杂度: 哈希表解法 O(n),快慢指针解法 O(1)

  • 不允许修改链表


二、核心思路:找到环的入口

基本思想

在 LeetCode 141 中,我们学习了如何用快慢指针判断链表是否有环。本题是它的进阶,需要找出环的入口节点。

核心思路:

  1. 判断是否有环:使用快慢指针,如果相遇则说明有环。

  2. 找到入口 :当快慢指针相遇后,将其中一个指针(比如 slow)移回链表头,然后两个指针以相同速度(每次一步)前进,再次相遇的位置即为环的入口。

数学证明

为什么这样能找到入口?

假设:

  • 链表头到环入口的距离为 a(节点数,从头节点出发,移动 a 步到达入口节点)

  • 环入口到快慢指针相遇点的距离为 b

  • 相遇点到环入口的距离(剩余环的长度)为 c,即环周长 L = b + c

当快慢指针相遇时:

  • 慢指针走过的距离:a + b

  • 快指针走过的距离:a + b + n * L,其中 n 是快指针在环内多绕的圈数(n ≥ 1)

由于快指针速度是慢指针的2倍,所以:

text

复制代码
2(a + b) = a + b + n * L
=> a + b = n * L
=> a = n * L - b = (n-1) * L + (L - b) = (n-1) * L + c

因此,从相遇点继续前进 c 步(即到环入口)相当于从头节点出发走 a 步,两者在入口相遇。

所以,当相遇后,将慢指针放回头节点,然后两个指针以相同速度移动,它们会在入口处相遇。


三、代码逐行解析

方法一:快慢指针法(最优解,O(1) 空间)

Python 解法

python

复制代码
# Definition for singly-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        # 1. 判断是否有环,并找到相遇点
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow == fast:
                # 有环,找入口
                slow = head
                while slow != fast:
                    slow = slow.next
                    fast = fast.next
                return slow
        # 无环
        return None
Java 解法

java

复制代码
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 1. 快慢指针找相遇点
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                // 有环,找入口
                slow = head;
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        return null;
    }
}

方法二:哈希表法(使用额外空间)

Python 解法

python

复制代码
class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        seen = set()
        curr = head
        while curr:
            if curr in seen:
                return curr
            seen.add(curr)
            curr = curr.next
        return None
Java 解法

java

复制代码
public class Solution {
    public ListNode detectCycle(ListNode head) {
        Set<ListNode> seen = new HashSet<>();
        ListNode curr = head;
        while (curr != null) {
            if (seen.contains(curr)) {
                return curr;
            }
            seen.add(curr);
            curr = curr.next;
        }
        return null;
    }
}

四、Java 与 Python 语法对比

操作 Java Python
链表节点定义 class ListNode { int val; ListNode next; ListNode(int x) { val = x; } } class ListNode: def __init__(self, x): self.val = x; self.next = None
空值判断 node == null node is None
集合/哈希表 Set<ListNode> set = new HashSet<>(); set.add(node); set.contains(node); set_ = set() set_.add(node) node in set_
循环条件 while (fast != null && fast.next != null) while fast and fast.next:
返回节点 return node; return node

五、实例演示

示例:head = [3,2,0,-4],环入口为节点 2(索引 1)

链表结构:3 → 2 → 0 → -4 → 2

快慢指针法过程

第一步:找相遇点

步骤 slow 位置 fast 位置 说明
初始 3 3 同时出发
1 2 0 slow 走一步到 2,fast 走两步到 0
2 0 2 slow 到 0,fast 到 2
3 -4 -4 slow 到 -4,fast 到 -4,相遇!此时 slow = fast = 节点 -4

第二步:找入口

  • slow 移回 head(节点 3)

  • fast 保持在相遇点(节点 -4)

  • 两者同步每次走一步:

    • 第1步:slow 到 2,fast 到 2(因为 -4 的 next 是 2)

    • 此时 slow == fast,返回该节点(节点 2),即为环入口。

哈希表法过程

遍历节点并存入集合,直到遇到重复节点:

  • 访问 3,加入集合

  • 访问 2,加入集合

  • 访问 0,加入集合

  • 访问 -4,加入集合

  • 再次访问 2,发现已在集合中 → 返回当前节点 2


六、关键细节解析

1. 为什么快慢指针相遇后,将慢指针移回头部再同步移动就能找到入口?

数学证明已在第二部分给出。关键在于快慢指针相遇时,慢指针走的距离为 a+b,快指针走了 a+b+nL,且 2(a+b) = a+b+nL → a+b = nL → a = nL - b = (n-1)L + c。从头节点走 a 步与从相遇点走 c 步到达同一位置(环入口)。因此同步移动后会在入口相遇。

2. 快慢指针相遇时,快指针一定在环内多绕了至少一圈吗?

是的,因为快指针速度是慢指针的2倍,当慢指针进入环时,快指针已经在环内,之后快指针追上慢指针时,必然比慢指针多走至少一圈(n≥1)。

3. 如何处理无环情况?

在快慢指针过程中,如果 fastfast.nextnull,说明链表无环,直接返回 null

4. 哈希表法中,节点对象作为键是否可靠?

在 Python 和 Java 中,对象默认基于内存地址哈希,因此可以唯一标识节点,可靠。

5. 为什么不允许修改链表?

如果允许修改,可以在遍历时给节点加标记(如将 next 指向自身),但本题明确不允许修改,因此只能使用不破坏原结构的解法。


七、复杂度分析

方法一:快慢指针法

  • 时间复杂度: O(n)

    • 找相遇点:最多 O(n)

    • 找入口:最多 O(n)

  • 空间复杂度: O(1),只使用了常数个指针。

方法二:哈希表法

  • 时间复杂度: O(n),每个节点访问一次,集合查找 O(1)

  • 空间复杂度: O(n),存储所有节点。


八、其他解法

解法三:数学计算法(类似快慢指针,但更数学化)

有些解法通过计算环的长度来辅助,但本质上与快慢指针相同。

解法四:递归标记法(破坏链表,不推荐)

递归遍历时标记节点,但会修改链表,不符合要求。


九、常见问题与解答

Q1: 快慢指针相遇后,为什么将 slow 移回头部,而不是 fast 移回头部?

两者都可以,只要一个移到头部,另一个保持在相遇点,然后同步移动。结果是一样的。

Q2: 如果链表很长,快慢指针会溢出吗?

不会,指针只存储地址。

Q3: 环入口证明中,a 和 b 的定义是否包括入口节点?

a 是从头节点到环入口节点所需的步数(移动次数),b 是从入口节点到相遇点所需的步数。这些定义在数学推导中是自洽的,不影响结果。

Q4: 能否用哈希表找到入口后,再释放内存?

不需要,只返回节点即可。


十、相关题目

1. LeetCode 141. 环形链表

判断是否有环,是本题的基础。

2. LeetCode 160. 相交链表

两个无环链表找交点,也可用双指针法,思路有相似之处。

3. LeetCode 287. 寻找重复数

将数组视为链表,可以转化为环形链表 II 问题,用快慢指针找重复数(入口)。

4. LeetCode 202. 快乐数

判断循环,也可用快慢指针。


十一、总结

核心要点

  • 问题本质: 找到链表中环的入口节点。

  • 最优解法: 快慢指针 + 数学推导,时间复杂度 O(n),空间复杂度 O(1)。

  • 关键步骤:

    1. 用快慢指针找到相遇点。

    2. 将慢指针移回头部,然后与快指针同步移动,再次相遇即为入口。

算法步骤(快慢指针)

  1. 初始化 slow = fast = head

  2. 循环条件:fast != nullfast.next != null

    • slow 走一步,fast 走两步。

    • 如果 slow == fast,则存在环,跳出循环进入步骤3;否则继续。

  3. slow 指向 headfast 保持在相遇点。

  4. 同步移动(均每次一步),直到 slow == fast,该节点即为环入口。

  5. 如果步骤2循环正常结束,返回 null

复杂度对比

解法 时间复杂度 空间复杂度 是否修改链表
快慢指针 O(n) O(1)
哈希表 O(n) O(n)

扩展思考

快慢指针不仅能判断是否有环,还能精确定位环的入口,体现了数学在算法中的美妙应用。掌握这个推导过程,可以帮助我们解决更多类似问题,如寻找重复数等。

相关推荐
H Corey2 小时前
数据结构与算法:高效编程的核心
java·开发语言·数据结构·算法
SmartBrain2 小时前
Python 特性(第一部分):知识点讲解(含示例)
开发语言·人工智能·python·算法
墨雪不会编程3 小时前
C++之【list详解篇一】如何玩好链表
c++·链表·list
01二进制代码漫游日记3 小时前
自定义类型:联合和枚举(一)
c语言·开发语言·学习·算法
小学卷王3 小时前
复试day25
算法
样例过了就是过了3 小时前
LeetCode热题100 和为 K 的子数组
数据结构·算法·leetcode
二年级程序员3 小时前
单链表算法思路详解(下)
c语言·数据结构·算法
HAPPY酷3 小时前
C++ 成员指针(Pointer to Member)完全指南
java·c++·算法
Sunsets_Red3 小时前
浅谈随机化与模拟退火
java·c语言·c++·python·算法·c#·信息学竞赛