「Floyd 判圈算法」(又称龟兔赛跑算法)

目录

  • 算法描述
    • 是否存在环路
    • 计算环的长度
    • 计算环的起点
  • 算法证明
    • 证明快慢指针相遇和链表有环互为充要条件
    • 证明两个慢指针第二次相遇时,该结点就是环的起点
  • 代码实现
    • 1.判断链表是否有环
    • 2.求链表的环的起点



算法描述


是否存在环路

结论:如果链表里有环,设有快慢指针从链表的头结点开始移动,快指针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的情况。假设快慢指针刚开始不从同一起点移动,可能会出现巧合的情况,快指针在环内永远没办法和慢指针相遇。 如下图所示:
「Floyd 判圈算法」(又称龟兔赛跑算法)_第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;			// 首次相遇的结点位置,就是环的起点
}

你可能感兴趣的:(算法,链表,数据结构)