力扣hot100之142:环形链表Ⅱ
力扣hot100之142:环形链表Ⅱ

LeetCode 142. 环形链表 II

题目描述

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

不允许修改链表。


题目分析

这道题是 环形链表 的进阶版。

上一题“环形链表”只需要判断链表中是否有环,因此只要快慢指针能够相遇,就可以直接返回 true

而这道题更进一步,不仅要判断链表中有没有环,还要找到 环开始的位置,也就是入环点

看到这里,很多人第一反应可能是:

  • 遍历链表,把访问过的节点放进哈希表;

  • 如果某个节点第二次出现,那么这个节点就是入环点。

这个思路当然可以做,但是需要额外空间。

这题更经典的做法依然是 快慢指针,并且空间复杂度可以做到 O(1)


核心思路

整道题可以分成两个步骤:

第一步:判断链表中是否有环

定义两个指针:

  • slow 每次走一步

  • fast 每次走两步

如果链表中有环,那么这两个指针迟早会在环中相遇。

如果链表没有环,那么 fast 会先走到 null,这时直接返回 null 即可。

第二步:找到入环点

当快慢指针第一次相遇之后:

  • fast 回到链表头节点 head

  • 然后让 slowfast 每次都走一步

  • 当它们再次相遇时,相遇的位置就是 入环点

这一步是这道题最关键的地方。


为什么再次相遇的位置就是入环点?

这是这道题最经典的数学结论。

假设:

  • 从头节点到入环点的距离是 a

  • 入环点到第一次相遇点的距离是 b

  • 环的剩余长度是 c

那么环的总长度就是:

b + c

当快慢指针第一次相遇时:

  • 慢指针走过的路程:a + b

  • 快指针走过的路程:a + b + n(b + c)

    • 这里的 n 表示快指针比慢指针多转的环数

因为快指针速度是慢指针的 2 倍,所以:

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

整理一下:

a + b = n(b + c)

再变形:

a = n(b + c) - b

写成更容易理解的形式:

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

这说明什么?

说明从 头节点走到入环点的距离 a,恰好等于从 第一次相遇点继续往前走 c 步,再绕若干圈 到入环点的距离。

所以:

  • 一个指针从 head 出发

  • 一个指针从第一次相遇点出发

  • 两个指针每次都走一步

那么它们一定会在 入环点 相遇。


代码实现

/**
 * 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) {
        ListNode fast = head, slow = head;
        while (true) {
            if (fast == null || fast.next == null) return null;
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) break;
        }
        fast = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

代码讲解

1. 初始化快慢指针

ListNode fast = head, slow = head;

一开始两个指针都指向头节点。


2. 先判断链表中是否有环

while (true) {
    if (fast == null || fast.next == null) return null;
    fast = fast.next.next;
    slow = slow.next;
    if (fast == slow) break;
}

这里使用死循环配合返回和 break

  • 如果 fast == null 或者 fast.next == null,说明链表无环,直接返回 null

  • 否则让快慢指针继续前进

  • 如果两个指针相遇,说明链表有环,退出循环

这一段和上一题“环形链表”的判断逻辑基本一致。


3. 让一个指针回到头节点

fast = head;

相遇之后,把 fast 放回头节点。

此时:

  • slow 留在第一次相遇点

  • fast 从头节点重新出发


4. 两个指针同步前进

while (slow != fast) {
    slow = slow.next;
    fast = fast.next;
}

这时候两个指针都改为一次走一步。

根据上面的推导,它们会在入环点再次相遇。


5. 返回入环点

return fast;

因为相遇位置就是入环点,所以直接返回任意一个指针即可。


示例分析

假设链表如下:

3 -> 2 -> 0 -> -4
     ^         |
     |_________|

其中值为 2 的节点是入环点。

第一步:快慢指针第一次相遇

  • slow 每次走一步

  • fast 每次走两步

它们最终会在环中的某个位置相遇,但这个位置不一定是入环点。

比如可能会在值为 -4 的节点相遇。

第二步:寻找入环点

  • fast 放回头节点 3

  • slow 留在第一次相遇点 -4

  • 然后两个指针每次都走一步

最终它们会在值为 2 的节点再次相遇。

所以返回该节点即可。


为什么不能直接返回第一次相遇点?

因为 第一次相遇点只是在环内,不一定是入环点

快慢指针第一次相遇时,只能说明链表有环,但还不能直接说明当前节点就是环的起点。

因此必须进行第二轮同步移动,才能真正找到入环点。


复杂度分析

时间复杂度

O(n)

虽然看起来有两个阶段:

  1. 快慢指针判断是否有环

  2. 再次寻找入环点

但两个阶段加起来,整体遍历次数仍然是线性级别,所以时间复杂度为 O(n)

空间复杂度

O(1)

整个过程中只用了几个指针变量,没有使用额外数据结构。


和哈希表解法对比

哈希表解法

思路是把遍历过的节点存入集合中:

  • 如果某个节点第一次出现,就加入集合

  • 如果某个节点第二次出现,说明它就是入环点

这种做法的优点是思路直接,容易理解。

但缺点也很明显:

  • 需要额外的 O(n) 空间

快慢指针解法

优点:

  • 不需要额外空间

  • 是这道题最经典、最优雅的做法

缺点:

  • 数学推导稍微抽象一些

  • 第一次接触时不太容易一下想明白

面试中如果能写出快慢指针解法,并且解释清楚为什么第二次相遇是入环点,一般会非常加分。


总结

这道题的关键点有两个:

  1. 先用快慢指针判断链表是否有环

  2. 相遇后让一个指针回到头节点,两个指针同步前进,再次相遇的位置就是入环点

这类题目非常经典,建议和上一题“环形链表”放在一起记忆:

  • 141 题:判断有没有环

  • 142 题:找到环的入口

前者重点在于“快慢指针能否相遇”,后者重点在于“相遇后如何找到入环点”。

只要把这两个层次彻底理解清楚,链表中的环问题基本就掌握了。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇