142. 环形链表 II

121 阅读4分钟

题目

🔗题目链接:142. 环形链表 II - 力扣(LeetCode)

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 链表中节点的数目范围在范围 [0, 104] 内
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

进阶: 你是否可以使用 O(1) 空间解决此题?

思路

先确认链表是否有环,再想办法找到链表入环的第一个节点。

  1. 确认链表是否有环。

    使用快慢指针。快指针一次走两步,慢指针一次走一步,快慢指针相等(指向同一个节点)说明。快指针相对于慢指针每次移动一步,这样快指针在移动的过程中就不会跳过慢指针。

    在这里要注意,快指针一定是先入环慢指针后入环,然后在环内快指针追上慢指针(入环之前,快指针一定是在慢指针之前,且快指针跑得比慢指针快。所以在入环之前,快慢指针绝不可能相遇)

  2. 找到链表入环的第一个节点

    假设从头节点到环的入口节点的节点数是 x ,环入口节点到快慢指针相遇的节点数为 y ,从相遇节点再到环入口节点的节点数为 z 。如图所示:

    image.png

    fast 为快指针,slow为慢指针。

    那么相遇时,slow 走过的节点数为 x + y,fast 走过的节点数为 x + y + n (y + z) , n 为 fast 在环内走了 n 圈才与 slow 相遇,y + z 为一圈内节点的个数。

    假设 fast 和 slow 从开始到相遇的时间为 t,可以得到以下公式:

    82e0795bb54d1bc0a85df9be004c87f.jpg

    我们要求的是环的入口节点,所以我们要求出 x,那么公式就可以变为这样:

    x = n (y + z) - y

    x = ny + nz - y

    x = (n - 1)y + nz

    x = (n - 1)y + (n-1)z + z

    x = (n - 1)(y + z) + z

    n 代表 fast 在环内转的圈数, n >= 1。

    当 n = 1 时,fast 在环内转了一圈就和 slow 相遇了。此时 x = z。也就是在相遇处定义一个 index1,在头节点定义一个 index2,让他俩同时移动,且每次前进一个节点,那么他们(index1、index2)相遇的地方就是环入口节点。

    142.环形链表II(求入口).gif

    当 n > 1 时,就是 fast 在环内转了 n 圈才遇到 slow 。这种情况其实和 n = 1 时的效果是一样的。只不过是 index1(在相遇处定义的指针) 在环内转 (n - 1) 圈,然后遇到的 index2(在头节点定义的)。注:为什么是 index1 在环内转了 (n - 1) 圈,可以带入 n = 2 的情况模拟下。

代码

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function detectCycle(head: ListNode | null): ListNode | null {
    if (!head) return head;

    // 定义快慢指针
    let fast = head;
    let slow = head;

    while (fast && fast.next) {
        // fast 快指针一次走两步
        fast = fast.next.next;
        // slow 慢指针一次走一步
        slow = slow.next;

        // 当 fast === slow,也就是快慢指针相遇了,说明链表有环
        while (fast === slow) {
            // 相遇处定义一个 index1
            let index1 = fast;
            // 链表头定义一个 index2
            let index2 = head;

            // index1 与 index2 不相等,那就让他俩一直走下去
            while (index1 !== index2) {
                index1 = index1.next;
                index2 = index2.next;
            }

            return index1;
        }
    }

    return null;
};
  • 时间复杂度 O(n)

    快慢指针相遇前,指针走的次数小于链表长度(注意是次数,虽然 fast 一次走两个节点,但 fast 和 slow 走的次数是一样的。因为 slow 一次走一个节点,所以 slow 走的次数就是 slow 走的长度,而 slow 走的距离小于链表长度)

    快慢指针相遇后,两个 index 指针走的次数也小于链表长度。

    相遇前后走的两次长度都小于 n,所以走的从长度小于 2n。

  • 空间复杂度 O(1)

    没有申请额外的链表空间,只是定义了几个变量。