[Python-链表刷题]环形链表 II

142. 环形链表 II

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

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

        不允许修改 链表。

示例 1:

[Python-链表刷题]环形链表 II_第1张图片

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

示例 2:

[Python-链表刷题]环形链表 II_第2张图片

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

示例 3:

img

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

提示:

  • 链表中节点的数目范围在范围 [0, 104]

  • -105 <= Node.val <= 105

  • pos 的值为 -1 或者链表中的一个有效索引

解题思路

        该题其实可以拆分成两问,一是判断链表中有没有环,另一个是如果有环的话则需要找到入环口。

下面我们先来看一下第一个问题,如何判断链表中是否有环。

1.怎么判断链表中有环?

        我们可以把这个问题换一种情境来看,现在假设我要和一名跑步运动员赛跑。我们在相同的起跑线上开始跑步,那么跑步速度很慢的我肯定一开始就会被超过。而且如果我们是在一条直跑道比赛的话,那我们是不是很显然就没有再次相遇的机会了。如下:

[Python-链表刷题]环形链表 II_第3张图片

        那如果我们是在操场的跑道上比赛呢,操场的跑道是一个环形,那么即使我一开始被运动员超越了,运动员依然有可能绕我一圈再超越我一次,是不是就能够相遇了?

        同样的思路,我们可以在链表中设置一快一慢两个指针,一个指针遍历的快,另一个指针遍历的慢,那么如果链表中有环的话,有可能两个指针就能在环中相遇了。注意,我这里说的是有可能。因为在跑道上相遇是必然的事情,这是一个连续的过程,我和运动员肯定会在某一时刻在环中相遇的;但链表中指针的遍历却是离散化的,如果快指针一次走三个节点,而慢指针一次走一个节点,那么有可能两个指针永远没有相遇的时候,快指针只会一圈又一圈的跳过慢指针。

        我们如何确保两个指针一定会相遇呢?很简单,让快指针一次只走两个节点,慢指针一次走一个节点,这样是不是就相当于在环中,快指针正在以一个节点的相对速度向慢指针逼近?这样肯定就会有相遇的时刻了。

        第二个问题,如何找到入环处的节点呢?

2.如何找到入环口?

        上面的问题中,我们已经确定了快节点的速度为两个节点每次,慢节点的速度为一个节点每次。那么我们可以把两者走过的节点数通过速度联系起来了。下面我们来画一下路径图:

[Python-链表刷题]环形链表 II_第4张图片

        在上图,设x为链表头到入环处节点的距离,y为入环处节点到两节点相遇位置的距离,z为两节点相遇位置到入环处节点的距离。那么,我们容易得到第一个式子,当两个指针相遇时,快指针已经走了n圈再加上x与y之和;那第二个式子怎么来的呢?慢指针一定连一圈都没有走完就被追上了吗?我们画一个图来看看:

[Python-链表刷题]环形链表 II_第5张图片

        我们把圆环拆成直线来看,l代表一圈的长度,当慢指针进入环时,快指针在这个展开图中一定是位于慢指针之前的,这个比较容易想到,上面我们也得出了相同时间下,快指针走的距离是慢指针的两倍,因此如上图所示,快指针一定会在某个时刻与慢指针相遇,而此时慢指针走不完一圈。因此可以得出,慢指针走的距离为x+y。

        将快慢指针的距离式子联系起来,就可以得出上述结论,x=(n-1)(y+z)+z。

        这个式子什么意思呢?我们指到,y+z代表的是圆环一圈的长度,而n为相遇时快指针所走过的圈数,是大于等于1的,这个不加证明,只需要想象一个极端情况验证一下即可。x就是我们想要知道的链表头到入环处节点的距离,而z为相遇节点处到入环节点处的距离。

        这个式子就说明了一件事,链表头到入环处节点的长度等于从两个节点相遇位置开始绕n-1圈后再加上相遇节点处到入环节点处的长度。简单说就是,新设一个节点,从链表头节点开始,按一次一个节点的速度前进;同时,从两个指针相遇的位置,再新设一个节点按相同的速度前进,两者最终一定会相遇在入环处节点的位置上。因为两者走的距离是一样的。

        这个一定要好好理解。

3.代码

        理解了上面两个问题后,代码就容易的多了,我们只需要设置两个指针,进行遍历操作。遍历结束的标志是快指针或者快指针的下一跳指向None,因为这种情况下,链表一定没有环,环状的链表是不会有指向None的情况的。

        然后在遍历过程中,如果有快指针和慢指针相同的情况出现,那么就说明两者相遇了,就开始在链表头节点处和两者相遇处新设两个节点,开始以一次一个节点的速度前进,直到相遇,返回相遇处的节点即可。

        代码如下:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None
​
class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        fast = head
        slow = head
        #根据快指针能不能指到空来判断这是单向链表还是带有环的链表
        while fast and fast.next:
            fast = fast.next.next #快指针一次走两格
            slow = slow.next #慢指针一次走一格
            if fast == slow: #如果两个指针能够相遇,就说明是有环的
                #确定有环之后,就要在相遇的节点新设一个节点index1
                #在头节点新设一个节点index2
                #两者同步走,相遇处即为入环口
                index1 = fast
                index2 = head
                #遍历结束条件为两者相遇
                while index1 != index2:
                    index1 = index1.next
                    index2 = index2.next
                #相遇之后返回节点
                return index1 
        return None

感觉这个题好巧妙,后面再来多做做。

你可能感兴趣的:(链表,数据结构)