【环形链表ll】找到有没有环已经很不容易了,还要让我找到环的入口?

82 阅读7分钟

142.环形链表II

力扣题目链接(opens new window)

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

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

image.png

思路

这道题目,不仅考察对链表的操作,而且还需要一些数学运算。

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

image.png

fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

动画如下:

image.png

如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

image.png

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

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

动画如下:

image.png

那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

本题的疑难点

1.为什么slow在第一圈未结束就被追上(即在slow走了x+y相遇),而不是 x+y +k(y+z) (k为假设slow走了k圈才被追上)

原因是慢指针进入入口时,快指针已经在圈里了,快指针与它的相对距离<1圈,因此追上它不需要一圈。 (相同时间下,慢指针走完一圈,快指针走的路程肯定大于一圈)

我们把环形链表拉直看看

链表.jpg 从图中可以看到,假设slow走了一圈(即图中第3个B点),fast走了一圈还要多,而slow和fast的初始距离小于1圈,因此在slow到达第3个B之前就会被追上(即一圈以内)。

2.为什么slow和fast一定能相遇?如果fast一次走3个节点呢?

我们先类比一下操场跑圈的场景,fast追上slow,就是对slow进行了“套圈”,只要fast的速度比slow快,不管快多少,fast一定能追上slow。

但是在操场上跑步,两者的移动是连续不断的,而链表是间断连续的,fast虽然也是必定能追上slow,但可能追上的点是在两个链表节点之间(这样是无意义的),不一定是正好在节点上相遇。

所以如果两者正好能在节点处相遇,就代表两者的相对速度要能被环的节点数整除。两者的相对速度是1,而1能被任何大于1的正整数整除。(3%1=0,4%1=0,5%1=0,....)

链表的环的节点数至少为3,以3为例,再来回答问题如果fast一次走3个节点呢?,此时相对速度等于3-1=2,3不能整除2,所以两者不能正好在节点处相遇。如果fast一次走3个节点,slow一次走一个,那么只能在环节点数为2的倍数的情况下能正好在节点处相遇。

这样就能理解fast一次走2个节点,slow一次走1个节点,他们永远能正好在环中的节点相遇的原因了。

3.环的入口怎么得来的

根据上面推导出来的公式 x = (n - 1) (y + z) + z

n代表fast走的圈数,其中n是大于等于1的,也就是fast追上slow,至少在环里走了一圈。

此时问题来了,为什么n>=1?为什么fast不能在完成一圈内就追上slow?

还是以操场跑步为例,slow到达入口时,fast已经进入环内,想要追上slow,必须先到slow的“后面”,才能追上slow。

再回到公式,这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

n>1的话就代表index2在环中走了(n-1)圈,最后返回index1,就是我们想要的结果。

image.png

代码

(版本一)快慢指针法
# 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:
        slow = head
        fast = head
        
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            
            # If there is a cycle, the slow and fast pointers will eventually meet
            if slow == fast:
                # Move one of the pointers back to the start of the list
                slow = head
                while slow != fast:
                    slow = slow.next
                    fast = fast.next
                return slow
        # If there is no cycle, return None
        return None