算法描述
是否存在环路
结论:如果链表里有环,设有快慢指针从链表的头结点开始移动,快指针fast一次移动2步,慢指针slow一次移动一步。只要一直走下去,fast指针一定会和slow指针在环内相遇。反之,如果没环,那么快指针永远不会和慢指针相遇,快指针会走到末尾。
计算环的长度
方法:当快慢指针第一次相遇时,让快指针不动,慢指针再走一圈,等慢指针再次回到相遇点时,慢指针刚好走了环的长度。只要增加一个length变量,慢指针每走一步,length+1,等慢指针到相遇点时,length的值就是环的长度。
计算环的起点
方法:快慢指针第一次相遇后,让慢指针slow1留在相遇点。重新定义一个新的慢指针slow2,让它刚开始指向链表的头结点(链表的起点)。让两个慢指针slow1和slow2一起移动,每次移动一步。等两个慢指针刚相遇时,相遇点刚好是环的起点。
算法证明
证明快慢指针相遇和链表有环互为充要条件
首先,最简单的情况是,fast指针和slow指针移动速度相差1,也就是|v(fast)-v(slow)| = 1。我们可以这样理解,fast和slow指针的相对速度为1。假如有环,快指针会先进入环内,慢指针会后进入环内。当快慢指针都进入环后,从相对运动的角度考虑,可以理解为慢指针不动,快指针一次移动一位,那么快指针和慢指针的距离每次减少1,那么快指针迟早会追上"不动"的慢指针。
而对于|v(fast)-v(slow)| > 1的情况。假设快慢指针刚开始不从同一起点移动,可能会出现巧合的情况,快指针在环内永远没办法和慢指针相遇。 如下图所示:
快指针每次走4步,v(fast) = 4,而慢指针每次走1步,v(slow) = 1。当slow指针进入环的起点3时,快指针走4步到5,相当于比慢指针慢1步。之后快指针每次走4步,相当于绕一圈再多走1步,和慢指针每次走1步相当于同速。那样快慢指针永远没办法相遇。
可是,如果快慢指针刚开始从同一起点移动,假设快慢指针速度差距大于1。似乎里面存在复杂的数学问题,我没有举出快慢指针无法相遇的例子(如果懂的朋友希望可以在评论区指点我)。
总之,最好假设fast指针一次走2步,slow指针一次走1步,这样更好证明。
证明两个慢指针第二次相遇时,该结点就是环的起点
再看看找环的起点的方法
方法:快慢指针第一次相遇后,让慢指针slow1留在相遇点。重新定义一个新的慢指针slow2,让它刚开始指向链表的头结点(链表的起点)。让两个慢指针slow1和slow2一起移动,每次移动一步。等两个慢指针刚相遇时,相遇点刚好是环的起点。
需要2步。
1.先证明两个同速的慢指针必定会相遇。
假设快指针和慢指针第一次相遇时,慢指针需要走n步,快指针速度是慢指针的2倍,则快指针需要走2n步。
可以这样理解,从头结点开始移动的慢指针slow2,相当于刚开始的慢指针。当它走n步时,就刚好走到快慢指针第一次相遇的相遇点。
而原来的慢指针slow1是从相遇点开始走,你可以理解为,慢指针slow1相对于慢指针slow2,它先走了n步,走到相遇点,之后它们才一起走。等它再走n步时,相当于它一共走了2n步,一共走了快指针的步数。那么它一定会和快指针一样,再一次走到快慢指针的相遇点。
所以,slow1和slow2走n步后,2个慢指针slow1和slow2一定会在相遇点相遇。
2.再证明两个同速慢指针第一次相遇的结点是环的起点。
而由于2个慢指针slow1和slow2速度一样,那么它们一旦相遇,就会一直重合。那么倒推回去,它们必定是从环的起点就相遇,之后一直就重合,直到走到快慢指针的相遇点。
环的起点一定是2个慢指针首次相遇的结点,否则2个同速慢指针就永远不可能相遇。
从而证明了,只需要返回2个慢指针首次相遇的结点,即为环的起点。
代码实现
1.判断链表是否有环
bool hasCycle(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) // 如果快慢指针相遇,返回true
return true;
}
return false; // 如果跳出循环,说明fast到末尾,返回false
}
2.求链表的环的起点
struct ListNode *detectCycle(struct ListNode *head) {
// 如果链表是空表或者只有一个结点(且该节点的next不指向自身),直接返回false
if (head == NULL || head->next == NULL)
return false;
// 快慢指针初始都指向头结点
struct ListNode* fast = head;
struct ListNode* slow = head;
do
{
// 快指针每次移动2步,慢指针每次移动一步
fast = fast->next->next;
slow = slow->next;
// 如果fast能走到末尾,说明没环,返回false
if (fast == NULL || fast->next == NULL)
return false;
}
while (fast != slow); // 如果fast == slow,说明走到快慢指针相遇点
struct ListNode* newSlow = head; // 让新的慢指针指向链表头结点
// 假如链表头结点也是环的起点,两个慢指针一开始就相遇,程序不需要进入循环,直接返回该结点
// 假如链表头结点不是环的起点,两个慢指针首次相遇的结点,就是环的起点
while (slow != newSlow)
{
slow = slow->next;
newSlow = newSlow->next;
}
return slow; // 首次相遇的结点位置,就是环的起点
}