一.题目
在链表相关题目中,有一道非常经典的题目:带环链表(链接:141. 环形链表 - 力扣(LeetCode))。带环链表尾节点的next指针指向其他节点,因此遍历一个带环链表将是一个死循环,这是带环链表的基本特征,如示例所示。
二.解题
此题使用快慢指针能轻松解决。定义快指针fast和慢指针slow,快指针一次前进两步,慢指针一次前进一步,如此快指针一直领先于慢指针,率先进入环内,而后慢指针才进入环中,而后如果快指针追上了慢指针,那么就是带环链表。代码实现如下:可以看见是通过测试的
bool hasCycle(struct ListNode *head) {
//定义快慢指针
struct ListNode* fast = head,*slow = head;
//若不是带环则遇到尾节点或 NULL停止
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return true;
}
}
return false;
}
三.证明
然而这种写法真的合理吗,为什么快指针一定能追上慢指针,难道不存在一直错开的情况吗?
其实就上述代码而言,证明是很容易的。fast快指针一定快于slow慢指针进入环中,因此当slow刚好进入环中时,fast要么跟slow重合(特殊情况,此时直接就能判断带环了),要么有一定距离,令这距离为N,由于fast一次走两步,slow一次走一步,因此在环中每次移动fast距离slow的距离就会减1,N通过不断的减1终会减到0,此时fast和slow就重合了,因此这是一定能追上的。
四.拓展
上述证明只适用于fast走两步的情况,那么fast走三步,四步,五步,N步呢?我们改为fast一次走三步试一试,发现过关,改为四步试一试,仍然过关。
证明过程与上文相同吗?不一样,更为复杂,但证明过程是类似的,这里以走三步为例进行一次分析。
无论fast一次走几步,必然比slow快,更早进入环中,因此当slow进入环中时,fast必然和slow有一个距离N(N为0则直接判断为带环),fast每次走三步,速度差则为两步,因此N每次减2,能否减到0呢?若N为偶数,必然能减到0(此时fast和slow重合,则判断带环),若为奇数,那么最后会减到-1,即fast没有与slow重合,而是又超过了一步,那么此时fast要追上slow的距离则变为了C-1(C位环的节点数),此时又回到了距离的奇偶判断,若C-1为偶,则最后能为0,若C-1为奇,那么最后又会超过一步,永远无法重合。
那么总结一下,fast和slow一直无法重合的条件是:N为奇数并且C-1为奇数。即N为奇数并且C位偶数。 那么,这可能存在吗?使用简单的数学推导能得到:2L = (X+1)* C – N
能满足N为奇数并且C位偶数吗?这是不可能的,因为2L必然为偶数,而(X+1)*C也必然为偶数(一个数乘以偶数必然为偶数),N为奇数,偶数减奇数必然为奇数,这与2L为偶数相矛盾,因此不存在该情况,也就是说必然能追上。
五.相关题目
再来看一道与环形链表相关的题目:找到入环点(141. 环形链表 - 力扣(LeetCode))
进行简单的数学推理得到:L = X*(C-1)+ C - N,其中X*(C-1)就是绕圈而已,在图中清晰可见,这样我们就有了思路:让head从头开始遍历L,此时另一个指针从相遇点开始遍历,等到他们相遇时,此时就是第一个入环点。代码如下:
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* fast = head,*slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
struct ListNode* meet = fast;
while(fast != head)
{
head = head->next;
fast = fast->next;
}
return fast;
}
}
return NULL;
}