给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next
指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null
。
为了表示给定链表中的环,我们使用整数 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 解释:链表中没有环。
题意:判断一个链表是不是存在环。如果不是则返回NULL,否则返回环的入口点。
首先排除遍历链表,如果链表中存在环,遍历会死循环,找不到尾。
1.巧妙解法判断链表中是否存在环:物理中的追击相遇问题。
具体操作:定义两个指针slow和fast。slow每次走1步,fast每次走2步,如果链表成环,slow和fast就一定会相遇,并且相遇点在环的内部。
图解:
设 L为头节点到环入口点之间的“距离”。
设C为环的长度。
初始状态:
slow进环状态:
从此刻开始分析:
设fast和slow的距离为N,每次fast走2步,slow走1步,它们之间的距离减少1,每次都判断一下是否相遇。
N的变化趋势为:
N
N-1
N-2
...
2
1
0
归纳为:fast和slow每走一次距离都减少1,并且每次都判断,那么一定不会错过。
原因:fast和slow的相对距离为N(N为整数),相对速度为1,又因为任何整数都是1的倍数,因此,相遇前需要走的步数 x = N/1 一定为整数,所以一定不会错过。
思考,如果slow走1步,fast走3步呢?一定会相遇吗?
答案:一定会相遇。
N的变化趋势第1种情况 N的变化趋势第2种情况
N N
N-2 N-2
... ...
4 5
2 3
0 1
-1
第1种情况:N是2的倍数,即N是偶数,相遇
第2种情况:N是奇数,N%2==1,此时第1次追赶会错过,此时 fast 和 slow 相距 C-1
1)C-1为偶数,第2次追赶相遇。
2) C-1为奇数,那么永远追不上。
此时就要探讨一个问题,N是奇数,C-1也为奇数,这种可能是否存在。
slow进环前路程
slow: L
fast: L+x*C+C-N
由fast = 3*slow 得:
3*L = L+x*C+C-N
2*L = (x+1)*C - N
若N为奇数,C-1也为奇数,右边:C为偶数->(x+1)*C为偶数->(x+1)*C-N为奇数。然而左边:2*L一定为偶数,故N为奇数,C-1也为奇数不成立。
所以
fast走3步,slow走1步,必定相遇。
总结:
1.当相对速度v为1时,因为所有的整数都是1的倍数,一定能追上,且是一圈之内,不会错过。
2.如果v不为1,需要判断slow刚进环时相对距离是不是v的倍数,如果是,则第一圈相遇,否则,还需要判断下一圈。
确定进环入口。方法1:一个指针slow(这里重复利用slow)从头指针开始走,一个指针从 slow 和 fast 相遇节点开始走,它们会在入口节点处相遇。
证明如下:
由分析可知:
slow走的路程: slow = L + X
fast 走的路程: fast = L + n*C + X
2*slow = fast
2*(L+X) = 2*(L+n*C+X)
化简得:L = (n-1)*C + (C-X)
其中n表示fast在环中走的圈数,且n>=1。
X表示入口节点与meetnode之间的距离。
关键:由于 L 代表从表头指针到入口节点的距离,又因为slow和meetnode速度相同且都为1,所以slow走L 和 meetnode走 (n-1)*C + (C-X)的时间相同。又 n>= 1,meetnode 至少走 C-X ,恰好为meetnode到入口节点的距离,其余的(n-1)*C, 只是在转圈,最后meetnode位置会定在 入口节点。
Q:为什么是 n*C 而不是 C?
A:因为 C 和 L 的长度未知。如果 L 比 C 大很多的话,可能 slow 在走到路口点时,fast在环内就转了很多圈了。
Q: 为什么slow连一圈都没走完就被追上了?
A: 因为 fast 的速度是 slow 的两倍,匀速且经过相同时间下,fast 路程是 slow 的两倍。极端情况如图所示:
假设slow进环时,slow 无限逼近于 fast,极限为 fast 和 slow 相对距离为C,相对速度为1,可算出时间为 C,即 slow 极限走1圈,fast走2圈。但由于这是散点运动,并不是数学和物理中连续的运动,因此取不到这个极限,那么slow就不可能走完1圈。
完整代码如下:
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode *slow,*fast;
slow = fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
//如果成环,找出相遇的节点
if(slow == fast)
{
struct ListNode* meetNode = fast;
slow = head;
//让slow从head开始走
//meetnode从当前相遇位置开始走
//他们一定会在环的入口节点相遇
while(slow != meetNode)
{
slow = slow->next;
meetNode = meetNode->next;
}
return slow;
}
}
return NULL;
}
确定进环入口。方法2:
不破坏链表结构解法:
记录meetnode节点,一个指针从表头走直到走到meetnode,统计长度count1;另一个指针从meeetnode的下一个开始走,统计长度count2。相当于将链表分成两个,记录他们的相交节点,这个相交节点就是环的入口节点。
图解如下(以下为断开的解法,题目中如果要求不破坏链表则不可取,图片仅供参考):
完整代码如下:
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow,*fast;
slow = fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
struct ListNode* meetNode = slow;
//两个统计链表的指针
struct ListNode *l1,*l2;
int len1,len2;
len1 = len2 = 1;
l1 = head;
l2 = meetNode->next;
while(l1 != meetNode)
{
l1 = l1->next;
len1++;
}
while(l2 != meetNode)
{
l2 = l2->next;
len2++;
}
//l1,l2重新回到各自表头
l1 = head;
l2 = meetNode->next;
int gap = abs(len1-len2);
if(len1>len2)
{
while(gap--)
{
l1 = l1->next;
}
}
else
{
while(gap--)
{
l2 = l2->next;
}
}
while(l1 != l2)
{
l1 = l1->next;
l2 = l2->next;
}
return l1;
}
}
return NULL;
}