前言
环形链表是链表的一种,特点是尾结点不指向NULL而是指向自身的任意一个结点,最后一个结点指向自己的也算。遍历环形链表会死循环。
现在我们通过难度循序渐进的两道题来深入了解一下环形链表相关的问题。
(采用C语言解答)
题目来源:leetcode
第一题
分析
这道题只要我们判断给的链表是否为环形链表就行了,只要返回布尔值;
示例可以帮助我们更好理解题意;
我们看看初始状态下给了什么,函数的返回值为bool类型,给了一个参数也就是要进行判断的链表头。
然后我们来想想怎么样才能判断一个链表是否是环形链表。
写个循环如果死循环就是环形链表肯定是不行的。其实我们只要创建两个指针,初始时都指向头结点,一个指针一次走一步,另一个一次走两步(快慢指针)。那么想象一下接下来会发生什么?
(相同颜色代表同时)
可以看到在环形链表中我们的快慢指针会相遇,而相遇就说明这是环形链表;
在非环形链表中快指针或快指针的下一个指针会走到空指针(如图我们应该在快指针的下一个指针走到空时就停止,否则再走一步的代码fast->next->next会造成对空指针的解引用,代码会报错)。也就是如果快指针或快指针的下一个指针走到空指针就说明是非环形链表。
代码参考
typedef struct ListNode ListNode;//为了方便
bool hasCycle(struct ListNode *head) {
ListNode* slow,*fast;
slow=fast=head;
while(fast&&fast->next)
{
slow=slow->next;//走一步
fast=fast->next->next;//走两步
if(slow==fast)
{
return true;
}
}
return false;//此时说明快指针或者快指针的下一个指针遇空了,也就说明非环形链表
}
证明
我们来更深入地证明以下为什么慢指针每次走一步,快指针每次走两步时二者一定会相遇;以及如果快指针每次走的不是两步而是三步或更多时,二者还会会相遇吗还是会错过,永不相遇。
我们来看看,慢指针每次走一步,快指针每次走两步时,二者间距离的变化趋势:
我们可以把走第一次时slow与fast的距离看为4,可以发现随着走的步数增加1,距离每次减少1,也就是说一定会有距离刚好见到1的情况,这也就说了快慢指针在慢指针每次走一步,快指针每次走两步时,如果是环形链表,一定会相遇。
那么快指针不是每次走两步时的情况又如何呢?
我们来看看,慢指针每次走一步,快指针每次走三步时,二者间距离的变化趋势:
这一种走法没法按上面的走法精确观察距离变化,但是我们可以看到每走一步,快慢指针的距离是减2的:
可以看到这种走法,走一步距离缩短了1。
现在我们假设这个环很大,s1和f1是入环时的情况,此时fast不知在环中绕了多少圈了,假设快慢指针此时距离为N,每走一步,距离就会减2:
前面是减1所以一定会距离变为0,但这里就不一定,可能有两种情况,最后一步距离减2到了0或者-1都有可能,我们需要探讨一下:
如果N为偶数,就会到0,快慢指针会相遇;如果N为奇数,就不会距离到0,快慢指针会错过,或者说快指针越过了慢指针而非相遇。也就是说,快慢指针“套圈”了。(就像跑一千米时跑得慢的人可能被跑得快的人套圈)。
总结两种情况:慢指针每次走一步,快指针每次走三步时,N为偶数时会相遇;N为奇数时会出现“套圈”。
但是套圈代表着之后还有机会可能相遇,所以还要接着探讨N为奇数的情况,现在我们假设环的长度为C。
我们从上面N每次减2来看,N为奇数时,距离会来到N-1的情况,这其实就是快慢指针距离为C-1。
所以如果N为奇数,C-1也为奇数,快慢指针就永远不会相遇。如果N为奇数,C-1为偶数,这一圈就会相遇了。
假设slow在进环前走的距离为L,进环时slow与fast的距离为N,环长为C。
那么slow在进环的时候,slow走过的距离为L,fast走的距离为L+xC+C-N。此外由于两个指针每次走时的步数差距,3slow=fast成立。
然后可以得到3L=L+xC+C-N,化简可以得到2L=(x+1)C-N。我们知道偶数乘任何数都是偶数,所以(x+1)C-N为偶数,分为两种情况,偶数-偶数为偶数,奇数-奇数为偶数。
在前面我们探讨了,N为奇数时快慢指针才会错过,所以只能是奇数-奇数情况,所以(x+1)C为奇数,所以C必须为奇数,那么C-1就一定为偶数,而前面我们说C-1要为奇数才能让快慢指针在套圈后无法相遇,显然做不到了。
所以N为奇数情况下,C不可能存在为偶数的情况。因此,套圈在下一圈也会相遇,也就是说慢指针每次走一步,快指针每次走三步时,快慢指针一定会相遇。
快指针每次走4步等推理过程也一样,,也会相遇。
但是我们一般用快指针走两步就行了。
第二题
分析
现在我们就不只是返回布尔值了, 而是要返回链表入环的第一个结点(无环返回NULL)。
注意相遇的结点不一定是入环的第一个结点。
这题光靠想是很困难的,而是要了解一个思想:
让⼀个指针从链表起始位置开始遍历链表,同时让⼀个指针从判环时相遇点的位置开始绕环运运行,两个指针都是每次均走⼀步,最终肯定会在入口点的位置相遇。
代码参考
证明
这个代码的通过证实了刚才所说的思想的正确性,那么现在问题是,为什么相遇点和头结点到入环结点的距离是相等的?和前面一题一样,我们来证明一下。
我们假设环的长度为R,相遇点到入环结点的距离为R-X,我们要证明的就是L=R-X。
同理,2×慢指针走的距离=快指针走的距离。慢指针走的距离为L+X,快指针走的为L+X+nR(不知道在相遇前绕了多少圈,为nR)。
2(L+X)=L+X+nR.
L+X=nR
L=nR-X
而绕了几圈无所谓,相当于R,所以L=R-X成立。
至此,本文结束^_^