文章目录
- 题目1.判断链表中是否有环
- 1.1 思路分析(快慢指针)
- 1.2 思考:为什么快指针每次走两步,慢指针每次走一步两者一定可以相遇?
- 1.3 快指针一次走3步,走4步,...n步行吗?
- 题目2. 寻找入环点
- 2.1 思路1
- 2.2 代码实现
- 2.3 证明:为什么一个指针从相遇点开始走,一个指针从链表起始位置走,两者会在入环点相遇?
- 2.4 思路2(转换为链表相交问题)
- 2.5 代码实现
这篇文章,我们来看两道环形链表相关的题目。
题目1.判断链表中是否有环
链接: link
给你一个链表的头节点 head ,判断链表中是否有环。如果链表中存在环 ,则返回 true 。 否则,返回 false 。
1.1 思路分析(快慢指针)
什么是环形链表呢?
其实就是链表的尾结点的next不指向NULL,而是指向链表中的任意一个结点,也可以是自己。
我们来简化一下环形链表的图,其实就是长这样:
那这道题单纯的要判断链表中是否存在环其实很简单,思路就可以用我们熟悉的快慢指针:
定义两个指针fast和slow,初始都指向链表的第一个结点。然后快指针fast一次走两步,慢指针slow一次走一步。
如果链表中存在环,那他们就一定会相遇,即fast==slow
。
如果链表中没有环,那么fast指针一定会率先走到空,因为fast是一次走两步,所以要考虑链表的可能是奇数个也可能是偶数个。
如果是奇数个循环结束条件是fast->next==NULL
偶数个的话就是fast==NULL
时结束
那我们来写一下代码:
就过啦!
bool hasCycle(struct ListNode *head) {
struct ListNode* slow,*fast;
fast=slow=head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(slow==fast)
return true;
}
return false;
}
代码呢确实很简单,但是,还有一些问题值得我们来思考一下
1.2 思考:为什么快指针每次走两步,慢指针每次走一步两者一定可以相遇?
大家有没有想过为什么快指针每次走两步,慢指针每次走一步在带环的情况下两者一定可以相遇呢?
我们来一起证明一下:
慢指针slow一次走一步,我们假设慢指针进环的时候,fast和slow的距离是N
那此时fast和slow两个人都在环里,两者距离为N,而fast又比slow走得快,所以fast是不是就有可能追上slow。
那为什么fast一次走两步,slow一次走一步就一定可以追上呢?两者就一定会相遇呢?有没有可能会错过呢?
🆗,这样是一定可以相遇的:
此时两者都在环里,距离为N,fast一次走两步,slow一次走一步,所以它们的速度差是1。
也就是说,往后每走一次,两者的距离就缩小1
N,N-1,N-2,... ,3,2,1,0
那么N次之后,两者的距离就会缩小到0,此时两者就相遇了。
而且肯定在一圈之内就追上了,因为慢指针入环的时候,两者的距离肯定是小于环的周长的。
1.3 快指针一次走3步,走4步,…n步行吗?
那我们再来思考,上面我们证明了慢指针一次走一步,快指针一次走两步一定可以相遇。那么,快指针一次多走几步还可以吗?走3步,走4步,…n步行吗?
那这样的话能不能相遇就要看情况了,我们来分析一下,比如,我们以快指针每次走3步来分析一下(其它情况也类似):
慢指针slow呢还是一次走一步,那我们还是假设当slow走到入环点的时候,两者距离为N
slow进入环之后呢,fast就开始追击slow了。
那么此时fast一次走3步,slow一次走1步,即它们的速度差是2,也就是说,每追击一次,两者的距离缩小2
那此时它们还一定会相遇吗?
🆗,此时就要分情况看了:
如果N是偶数,那么N每次-2,最终一定可以减小到0,那就可以相遇。
如果N是奇数,每次-2,最终会减到...3,1,-1
那当它们的距离N变成-1的时候,意味着什么?
两者是不是错过了啊。fast直接跳到了slow前面距离为1的位置。
那此时两者的距离又变成了多少?
如果假设环的周长是C,那他们的距离就变成了C-1,然后fast重新开始追击slow,那这次能相遇吗?
是不是又取决于它们的新距离C-1是奇数还是偶数啊?
每次追击距离缩小2,如果C-1是偶数可以相遇,如果C-1是奇数那么将永远追不上了!
因为C-1是奇数的话,最终又会减到-1,减到-1的话它们的距离就还是C-1,C-1是奇数,最终又会减到-1,减到-1的话它们的距离就还是C-1…
就会一直循环下去,永远追不上!!!
而C-1到底是奇数还是偶数,我们不知道,这取决与环的大小。
那如果每次fast走的更多,走4步,5步,…n步也是一样的:
就看它们在对应的速度差下距离能不能缩小到0,slow入环时距离为N,假设速度差是gap,N每次减去gap,如果最终可以减到0,就可以相遇(即看N是不是gap的整数倍)。
如果最终不能减到0,那他们就会错过,假设错过之后距离为C-X,如果C-X是速度差gap的整数倍,那还可以相遇,如果不是,那就永远不能相遇。
所以:
如果快慢指针的速度差是1,那么一定可以追上相遇,如果大于1,就不一定了。
题目2. 寻找入环点
那么下面我们再来看一道环形链表的题目
链接: link
这道题呢,我们不仅要判断链表有没有环,还要返回入环的结点,如果链表无环,则返回 null。
2.1 思路1
这道题单要写代码的话呢其实很简单,有一个方法是这样的:
上面我们刚做了一道题不是判断链表是否带环嘛,用快慢指针如果最终可以相遇的话就是有环。
那现在要寻找入环点,就可以这样:
让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终就一定会在入环点相遇。
2.2 代码实现
那我们来写一下代码,看看能不能通过:
🆗,就过啦!
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode * fast=head;
struct ListNode * slow=head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(fast==slow)
{
struct ListNode *meet=fast;
struct ListNode *begin=head;
while(meet!=begin)
{
begin=begin->next;
meet=meet->next;
}
return meet;
}
}
return NULL;
}
但是,为什么一个指针从判环的相遇点开始走,一个指针从链表起始位置走,就一定会在入环点相遇呢?
那下面我们来证明一下这个结论
2.3 证明:为什么一个指针从相遇点开始走,一个指针从链表起始位置走,两者会在入环点相遇?
那我们依然还是来画图分析一下:
我们假设链表起点到入环点的距离为L,入环点到相遇点的距离为N,那相遇点在往前走到入环点的距离就是C-N。
那么快慢指针在相遇的时候,所走的路程:
慢指针slow:L+N
ps:慢指针在环内走的距离不会超过一圈的,上一题我们分析了,慢指针入环时两者的距离肯定小于N,一圈之内就追上了。
快指针fast:L+k*C+N
解释:快慢指针相遇时,快指针fast已经绕环走了k圈了,k至少为1。因为fast先进入环,而且速度快,所以一定先独自经过相遇点M,而最终两者又在M相遇。所以fast至少绕环走了一整圈再+N走到相遇点。
即k至少为1,至于具体的大小还取决于环的大小,环长C相对于L越小,k就越大。
然后:
又因为快指针的速度是慢指针的2倍,所以:
相遇时快指针的路程是慢指针的2倍,即
L+k*C+N=2*(L+N)
k*C=L+N
所以:
L=k*C-N
,即:
L=(k-1)*C+C-N
那我们再来看图:
L=(k-1)*C+C-N
,然后我们上面的思路不是让两个指针分别从起点和相遇点开始走嘛
begin从起点开始走,meet从相遇点开始走,两人同步走,每次都是走一步。
那begin走了L步走到入环点
meet就也走L步,L又等于(k-1)*C+C-N
,即meet先绕环走k-1圈(k>=1),那meet从入环点开始走的,不论走几圈,只要是整圈,还停下来就还是在相遇点这个位置嘛,然后还要走一个C-N,而我们看图C-N刚好就是相遇点距离入环点的距离。
所以meet走了L((k-1)*C+C-N
)步之后正好也走到了入环点。
那么就得以证明:
一个指针从判环的相遇点开始走,一个指针从链表起始位置走(每次都走一步),两者正好会在入环点相遇。
2.4 思路2(转换为链表相交问题)
那么这道题呢我们再来提供另外一种解法:
就是把它转换成链表相交的问题,我们前面写过这道题——链接: link
怎么做呢?
首先还需要找到快慢指针的相遇点,然后从相遇点把环形链表断开——变成单链表
然后就变成了相交链表找交点的问题
2.5 代码实现
我们来写一下代码:
相交链表找交点的代码我就不写了,我们直接拷贝之前写的:
然后我们只需:
提交
过啦!
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode *curA=headA;
struct ListNode *curB=headB;
int lenA=0;
int lenB=0;
//找尾,先判断是否相交,不相交直接返回NULL,相交再找
while(curA->next)
{
lenA++;
curA=curA->next;
}
//计算准确长度应该这样写:while(curA)
//这样while(curA->next)比实际长度小1,但是两个都小1,不影响差值
//而这样写循环结束cur就是尾,可直接判断是否相交
while(curB->next)
{
lenB++;
curB=curB->next;
}
//尾不相等,则不相交
if(curA!=curB)
return NULL;
//计算差值
int gap=abs(lenA-lenB);
//找出长的那一个
struct ListNode *longlist=headA;
struct ListNode *shortlist=headB;
if(lenB>lenA)
{
longlist=headB;
shortlist=headA;
}
//长表先走差值步
while(gap--)
{
longlist=longlist->next;
}
//再一起走,比较找交点
while(longlist!=shortlist)
{
longlist=longlist->next;
shortlist=shortlist->next;
}
return longlist;
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode * fast=head;
struct ListNode * slow=head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(fast==slow)
{
struct ListNode *otherhead=fast->next;
fast->next=NULL;
return getIntersectionNode(head,otherhead);
}
}
return NULL;
}