目录
1、为什么循环的结束条件是fast或者fast->next指向NULL?
2、为什么要求快指针fast一次移动两个节点,慢指针slow一次移动一个节点?
快指针fast一次移动两个节点,慢指针slow一次移动一个节点,两者一定会相遇吗?
什么是环形链表?
环形链表是一种特殊类型的链表数据结构,其最后一个节点的"下一个"指针指向链表中的某个节点,形成一个闭环。换句话说,链表的最后一个节点连接到了链表中的某个中间节点,而不是通常情况下连接到空指针(null)。如图:
由于链表最后一个节点的下一个指针没有指向NULL,而是指向前面的某一个节点,所以我们不能再用 “current->next==NULL” 作为判断条件来遍历链表(这样会造成死循环),通常使用快慢指针来控制条件,下面的两个题目都是要借助快慢指针来实现。
(一)检测一个链表是否有环
先来说一下结论,如何判断一个链表是否有环:
1、首先,让快慢指针fast、slow指向链表的头节点Head;
2、让快指针fast一次向后移动两个节点,慢指针一次向后移动一个节点;
3、判断fast和slow是否移动到同一个节点上,如果移动到同一个节点上,就返回true;如果还没有移动到同一个节点上,进行步骤二。
4、上几个步骤通过循环完成,循环的结束条件是fast或者fast->next指向NULL。
代码如下:
struct ListNode* fast, *slow;
fast = slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
return true;
}
return false;
分析:
1、为什么循环的结束条件是fast或者fast->next指向NULL?
- 当一个链表中有环时,fast和fast->next永远不会指向NULL
- 当一个链表中没有环时,fast一定会移动到链表的结尾;又因为fast一次移动两个节点,所以有两种情况:①fast移动两次后,刚好指向NULL,结束循环;②fast移动一次后就已经指向NULL,此时再进行移动,就会出现对NULL的解引用
2、为什么要求快指针fast一次移动两个节点,慢指针slow一次移动一个节点?
-
快指针fast一次移动两个节点,慢指针slow一次移动一个节点,两者一定会相遇吗?
移动的过程中有一下三种状态:
1)快指针fast和慢指针slow都没有进入环的入口点
2)快指针fast进入环的入口点(或已在环内)、慢指针slow没有进入环的入口点
3)快指针fast和慢指针slow都在环内
为了方便画图,下面将抽象出一个模型来说明:
如果快指针fast和慢指针slow相遇,一定是在环内相遇,所以我们主要分析一下第三种状态。
此时fast和slow的距离设为N,当fast和slow每次移动时,两者的距离就会变成N-1(fast向前移动两个,slow向前移动一个,所以距离就近一个),移动一次两者的距离就-1,(N-1-1-1-1-1-1-1-1-……)直到N会被减小到零,两者就相遇了。
小结:
快指针fast一次移动两个节点,慢指针slow一次移动一个节点时,两者一定会相遇。
-
fast一次不能移动三个/四个节点吗?
可以用同样的分析方法,来分析一下fast一次移动三个/四个节点的情况。
1)fast一次移动三个节点的情况:
此时fast和slow的距离设为N,当fast和slow每次移动时,两者的距离就会变成N-2,所以N-2-2-2-2-……
不难发现,如果是每移动一次距离减2的话,可能会出现N不会减到0的情况,所以这里有以下两种情况:
① N为2的倍数时,
N可以被减到零,快慢指针会相遇;(在未减到0之前,fast一直在slow的后面,fast追slow)
② N不为2的倍数时(奇数)
N不会被减到0,N最后一次会被减到1,此时fast再向前移动2个节点、slow向前移动1个节点时,fast会领先slow一圈,这时fast会跑到slow的前面一个节点,两者的距离会变成C-1(设C为环的长度)(这里会由原先的fast追slow变为slow追fast);
然后会再进行距离减小:(C-1)-2-2-2-2-…… 到这里又有两种情况,(C-1)是否是2的倍数,如果是fast就可以和slow相遇;如果(C-1)不是2的倍数,此后便会重复上述过程,也就是说fast不会相遇。
小结:
所以fast一次移动三个节点,fast和slow不一定会相遇,它有两个影响点:
- 与环的长度
(C-1)是否是2的倍数
- 环入口点之前节点的个数有关
N是否是2的倍数-->在slow还没有到达环入口点前,fast会在环内一直循环,环入口点之前节点的个数不同,当slow到达环入口点时,fast的与slow的相对位置会不同,这样N就不同
2)fast一次移动四个节点的情况
移动四个节点的分析方法与三个的相同,下面我就CTRL+C和CTRL+V以下:
此时fast和slow的距离设为N,当fast和slow每次移动时,两者的距离就会变成N-3,所以N-3-3-3--……
不难发现,如果是每移动一次距离减3的话,可能会出现N不会减到0的情况,所以这里有以下两种情况:
① N为3的倍数时,
N可以被减到零,快慢指针会相遇;(在未减到0之前,fast一直在slow的后面,fast追slow)
② N不为3的倍数时
N不会被减到0,
N最后一次会被减到1,此时fast再向前移动3个节点、slow向前移动1个节点时,fast会领先slow一圈,这时fast会跑到slow的前面两个节点,两者的距离会变成C-2(设C为环的长度)(这里会由原先的fast追slow变为slow追fast);
然后会再进行距离减小:(C-2)-2-2-2-2-…… 到这里又有两种情况,(C-2)是否是2的倍数,如果是fast就可以和slow相遇;如果(C-2)不是2的倍数,此后便会重复上述过程,也就是说fast不会相遇。
N最后一次会被减到2,此时fast再向前移动3个节点、slow向前移动1个节点时,fast会领先slow一圈,这时fast会跑到slow的前面一个节点,两者的距离会变成C-1
然后会再进行距离减小:(C-1)-2-2-2-2-…… 到这里又有两种情况,(C-1)是否是2的倍数,如果是fast就可以和slow相遇;如果(C-1)不是2的倍数,此后便会重复上述过程,也就是说fast不会相遇。
小结:
所以fast一次移动三个节点,fast和slow不一定会相遇,它有两个影响点:
- 与环的长度
(C-1)是否是2的倍数
- 环入口点之前节点的个数有关
N是否是2的倍数-->在slow还没有到达环入口点前,fast会在环内一直循环,环入口点之前节点的个数不同,当slow到达环入口点时,fast的与slow的相对位置会不同,这样N就不同
(二)寻找环的入口点
这里还是先说明一下结论:
1、用两个指针cur1、cur2分别指向链表的头节点和快慢指针相遇的节点;
2、同时移动两个指针
3、当两个指针指向同一个节点时,该节点就是环的入口点
代码如下:
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* fast, *slow;
fast = slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
struct ListNode* meet = slow;
struct ListNode * cur = head;
while(meet != cur)
{
meet=meet->next;
cur = cur->next;
}
return meet;
}
}
return NULL;
}
分析:
(设环的长度为C)当慢指针slow到达相遇点时,slow走过的距离设为L+X,则快指针fast走过的距离为L+X+N*C (N是slow为到达环入口点时,fast所走过环的圈数, N大于等于1)
- slow走过的距离为什么是 L+X, 而不是L+X+圈数(即slow不会走环的一圈+X)?
最坏的情况是,如图
由小学数学可以知道,相同的起点,相同的时间,fast的速度是slow速度的二倍,所以fast走过的距离是slow的二倍,所以当slow回到起点时,fast刚好第二次回到起点,此时两者相遇,而slow也只是走了一圈。
由于走过前面 L 长度的节点后,fast和slow之间一定有距离,所以当slow到达环的入口点时,fast可能不在环的入口点,所以slow肯定不会走一圈的。
- fast走过的距离为什么是 L+X+N*C,并且N大于等于1?
这里我解释一下 “N*C,并且N大于等于1”,由于slow一次移动一个节点,fast一次移动两个节点,相同的时间下,fast的速度大于slow的速度,两者走过的距离肯定不相同,所以第一圈内,slow和fast不会相遇,只有当fast超过slow一圈或几圈后,两者才能相遇。
又因为fast走过的距离是slow走过距离的二倍,所以有 L+X+N*C = 2*(L+X)
进一步化简得,N*C = L+X -->> (n-1)*C + C = L + X
又因为fast再环上转了(n-1)圈,但是fast与slow的相对位置不变,所以得到下式:
C = L + X
L= C - X = N (N是相遇点与环的入口点的位置,顺时针)
由此可以得到上述结论。
当然,寻找环的入口点还可以用另一种方法:
让快慢指针相遇点与下一个节点断开,然后将问题转化为两个链表的相交,环的入口点其实就是两个链表的相交点。
代码如下:
struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB)
{
if(headA==NULL && headB==NULL)
{
return NULL;
}
struct ListNode* curA = headA, * curB = headB;
int count1 = 0, count2 = 0;
while (curA->next != NULL)
{
count1++;
curA = curA->next;
}
while (curB->next != NULL)
{
count2++;
curB = curB->next;
}
int gap = abs(count1 - count2);
struct ListNode* longlist = headA;
struct ListNode* shortlist = headB;
if (count1 < count2)
{
longlist = headB;
shortlist = headA;
}
if (curB == curA)
{
while (gap--)
{
longlist = longlist->next;
}
while (longlist != shortlist)
{
longlist = longlist->next;
shortlist = shortlist->next;
}
return longlist;
}
else
{
return NULL;
}
}
struct ListNode* getmeetnode(struct ListNode* head)
{
struct ListNode* fast, * slow;
fast = slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
{
return fast;
}
}
return NULL;
}
struct ListNode* detectCycle(struct ListNode* head)
{
// 找到相遇点
struct ListNode* meetnode = getmeetnode(head);
if(meetnode == NULL)
{
return NULL;
}
// 断开为两个链表
struct ListNode* cur1, *cur2;
cur1 = meetnode->next;
meetnode->next=NULL;
cur2 = head;
// 找到两个链表的交点
struct ListNode* IntersectionNode = getIntersectionNode(cur1, cur2);
return IntersectionNode;
}
本次内容到此结束了!如果你觉得这篇博客对你有帮助的话 ,希望你能够给我点个赞,鼓励一下我。感谢感谢……