链表中环的入口结点 快慢指针法证明
题目
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围:
n <= 100000
,1 <= node.val <= 10000
。要求:空间复杂度 O(1),时间复杂度 O(n)。
例如,输入{1,2},{3,4,5}时,对应的环形链表如下图所示:
可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。
输入描述:
输入分为2段,第一段是入环前的链表部分,第二段是链表环的部分,后台会根据第二段是否为空将这两段组装成一个无环或者有环单链表
输出描述:
返回链表的环的入口结点即可,我们后台程序会打印这个结点对应的结点值;若没有,则返回对应编程语言的空结点即可。
算法描述
1)定义两个指针均指向头节点,分别为快慢指针;
2)两个指针同时遍历链表,慢指针逐个遍历,快指针隔一个遍历;
3)如任何一个指针指向空节点,则返回空节点;
4)否则回到2),重复2)至4),直到快慢指针指向同一节点;
5)将快指针拨回至头节点;
6)两个指针继续同时遍历链表,均逐个遍历;
7)直到快慢指针指向同一节点,返回指向的节点,此节点即为环的入口。
算法原理
可以将这道题目抽象为一种特殊的追及问题,如图所示,h为起点、e为环的入口、p为两个指针出发后第一次相遇的地方,L1、L2分别为非环区域的长度和环状区域的长度,S1、S2分别为相遇之前慢、快指针运动的长度,m为相遇时e与p的距离,其中长度、距离是节点数量的抽象表达。题目要求环的入口结点,抽象后就是求e点的位置,即L1的长度。
因为 快指针速度是慢指针速度的两倍
所以 S2 = 2 * S1
依题意与图示易得:
S1 = L1 + m
S2 = L1 + L2 + m
所以 S2 - L1 = S1 = L2
p距下一次到e还有:
Xpe = L2 - m
因为 S1 = L2, L1 + m = L2
所以 Xpe = L2 - m = L1
所以 此时将快指针放回h慢速运动,可以使两指针再e相遇
此算法只占用了快慢指针的额外内存空间,与输入规模无关,空间复杂度为O(1)。以慢指针为参照,慢指针走了L1 + L2 = n
个节点,由于两指针是同步运动的,无需考虑另一指针,时间复杂度为O(n),符合题目要求。
代码实现
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def EntryNodeOfLoop(self, pHead):
fast = pHead
slow = pHead
while True:
if fast.next is None or fast.next.next is None:
return None
slow = slow.next
fast = fast.next.next
if fast == slow:
break
if slow == pHead:
return pHead
fast = pHead
while True:
fast = fast.next
slow = slow.next
if fast == slow:
return slow
hash法与代码实现
此题中使用hash法有暴力法之嫌,并不高效。遍历节点,将节点hash存储,利用了节点和hash的唯一性,如节点第一次出现第二次,则说明此节点为环的入口。遍历节点造成的时间复杂度如双指针法,为O(n),但每一个节点都需要存储hash会带来额外的内存消耗,空间复杂度为O(n),不符合题目要求。
class Solution:
def EntryNodeOfLoop(self, pHead):
nodes = set()
node = pHead
while True :
if node is None:
return node
if node.val in nodes:
return node
nodes.add(x)
node = node.next