142.环形链表II(快慢指针)

246 阅读4分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

每日刷题第28天 2021.1.24

环形链表

题目

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

示例

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

提示

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

解法

解题思路

  • 这道题主要考察两点:
    • 链表是否有环(也即 141.环形链表 )
    • 若链表有环,如何找到入环结点
  • 判断链表是否有环
  • 使用快慢指针,从头结点出发,fast 指针每次移动两个节点,slow 指针每次移动一个节点,若 fast 和 slow 在途中相遇 ,则说明这个链表有环。
  • 这里需要明确两点:
    • fast 和 slow 相遇,则一定在环中相遇
    • 若链表有环,则 fast 和 slow 一定会相遇
  • 接下来分析其原因:
    • fast 一定先入环,之后一直在环中循环,因此若相遇,一定是在环中。
    • slow 也入环后,实际上就成了 fast 追赶 slow,而 fast 每次追上一步,由于环没有终点,因此 fast 必定会追上 slow,两者相遇。
  • 若链表有环,如何找到入环结点
  • 这里需要一点简单的数学推导,假设头结点到入环结点之间的结点数为 a, 入环结点到 fast 与 slow 相遇结点之间的结点数为 b, 相遇结点再到入环结点之间的结点数为 c。则 fast 走过的结点数 lenFast = a + b + n(b + c)slow 走过的结点数 lenSlow = a + b,而 lenFast = 2 * lenSlow,简单的公式推导后可得 a = (n - 1) * (b + c) + c。这里用 a 表示是因为要求的是入环结点,而 a 正是头结点到入环结点之间的结点数。

回顾

  • a = (n - 1) * (b + c) + c 中我们可以得到什么呢?
  • 答案是:若两个指针分别从头结点和相遇结点同时出发,每次都向前移动一步,那么它们会在入环结点处相遇。 其中从相遇结点处出发的指针一共绕环走了n-1圈,显然 n 是大于等于 1 的。
  • 注意,之所以在相遇时 slow 走过的结点数为 a+b 而不是 a+m(b+c)+b,是因为 fast 每次追一步,即使在极端情况「slow 入环时 fast 在其前一个结点」下,fast 也会先于slow走完一圈前追到 slow
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/

/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
// 快慢指针
let fast = head;
let slow = head;
while (fast != null && fast.next != null) {
// 找到首次相遇点
fast = fast.next.next;
slow = slow.next;
if (slow == fast) {
// 第一次相遇就退出
break;
}
}

// 判断是否有环
if(!fast || !fast.next) return null;
// slow还是在相遇点
fast = head;
while (fast != null && slow != null) {
if (fast == slow) break;
fast = fast.next;
slow = slow.next;
}
return fast;

// 慢指针:a + s1
// 快指针:a + s1 + n * (s1 + s2)
// 快指针的速度是慢指针的2倍,那么路径应该也是慢指针的2倍
// 2 * (a + s1) = a + s1 + n * (s1 + s2)
// 2a + 2s1 = a + s1 + ns1 + ns2
// a = ns1 + ns2 - s1
// a = (n - 1)s1 + ns2
// a = (n - 1)(s1 + s2) + s2
// 因此:需要一个指针从开始节点,另一个指针从相遇点开始
};