1. 程式人生 > 其它 >Floyd 判圈演算法

Floyd 判圈演算法

目錄

Floyd判圈演算法

以下是引用 wiki 的介紹:

Floyd判圈演算法(Floyd Cycle Detection Algorithm),又稱龜兔賽跑演算法(Tortoise and Hare Algorithm),是一個可以在有限狀態機迭代函式或者鍊表上判斷是否存在,求出該環的起點與長度的演算法。

​ 如果有限狀態機、迭代函式或者鍊表存在環,那麼一定存在一個起點可以到達某個環的某處(這個起點也可以在某個環上)。

初始狀態下,假設已知某個起點節點為節點S。現設兩個指標t和h,將它們均指向S。

​ 接著,同時讓t和h往前推進,但是二者的速度不同:t每前進1步,h前進2步。只要二者都可以前進而且沒有相遇,就如此保持二者的推進。當h無法前進,即到達某個沒有後繼的節點時,就可以確定從S出發不會遇到環。反之當t與h再次相遇時,就可以確定從S出發一定會進入某個環,設其為環C。

​ 如果確定了存在某個環,就可以求此環的起點與長度。

​ 上述演算法剛判斷出存在環C時,顯然t和h位於同一節點,設其為節點M。顯然,僅需令h不動,而t不斷推進,最終又會返回節點M,統計這一次t推進的步數,顯然這就是環C的長度。

​ 為了求出環C的起點,只要令h仍均位於節點M,而令t返回起點節點S,此時h與t之間距為環C長度的整數倍。隨後,同時讓t和h往前推進,且保持二者的速度相同:t每前進1步,h前進1步。持續該過程直至t與h再一次相遇,設此次相遇時位於同一節點P,則節點P即為從節點S出發所到達的環C的第一個節點,即環C的一個起點。


個人的理解:

​ 開始時,t與h都位於頭節點,每次h先移動2單位,之後判斷h是否與t在同一點內,若不在,則輪到t移動一單位;若在同一點,則說明存在一個環。否則如此往復,h一定會移動到連結串列的尾端,即無法再移動,這種情況便是不存在環的情況。

1、演示

下面進行演示。假設頭節點為0,第六號節點的子節點為第2號節點,所以有 2->3->4->5->6->3 成環。設 t 與 h 在環內相遇於 A 點

至於為什麼有環會相遇,這點很好理解。想象兩個不同速度的人在操場上跑步。即使慢的人領先與快的人,到最後快的人也會追上慢的人。

2、求環內交點

現在來說明一下,為什麼如果有環的話,為什麼快指標會在一圈之內追上慢指標。

已知 t(慢指標) 的速度為 1/s
	h(快指標) 的速度為 2/s

假設在 t 剛入環時,h 在環內的位置距離入環點的距離為 b,設 環長度為 L。
則此時 h 與 t之間的距離(追趕距離)為 L - b
而 h 與 t 的相對速度為:(2-1)/s,也就是每一個週期,h與t的距離都會縮短一個單位
所以,當 h 與 t 相遇時,他們所想好的時間為:(L-b)/(2-1) = (L-b)/s
由於t的速度為1/s 所以,t 與 h 相遇時,t所走的距離為(L-b)
上式中,僅當 b=0 時 t 需要走一圈之後才能與h相遇,但 b=0 這條件是不存在的
	若 b=0 這存在,這說明,t剛好追趕上h時是在入環口。
	但這很明顯是不可能的,起始時h先手走兩單位,之後才到t移動一單位。因此在不存在環的情況下,
	t 永遠追及不到 h(這也是判斷是否有環的思路)
所以,L-b 一定小於 L 即h在環內可以在一圈之內追及上t
/**
     *求環內交點
     * @param head 頭節點
     * @return 如果存在環,則返回環內快慢指標相交的節點;若無則返回null
     */
public static ListNode FindIntersection(ListNode head) {
    ListNode h = head, t = head;
    while (h != null ) {
        if (h.next != null) {
            h = h.next;
            t = t.next;
        }
        else {
            return null;
        }
        if (h.next != null) {
            h = h.next;
            //每次h跑完兩步就要檢查一下是否追上了t,這裡可能會有人有疑問,為什麼上面h剛跑完之後不檢查是否追上了t
            //這是因為,後面需要根據這個點來求入環點,而推導公式的基礎便是每次h行動2單位都是不可分割的。
            if (t == h) {
                return t;
            }
        }
    }
    return null;
}

3、求環入口

如上圖,設環外長度為a,h與t相交於環內的A點,其距離入環口的距離為b,一圈長度為 C

​ 由上面的現在來說明一下,為什麼如果有環的話,為什麼快指標會在一圈之內追上慢指標。可知,相遇時,t在環內走過的距離不會超過一圈

  • 則 t移動的距離為:Lt = a + C*0 + b
  • 則 h移動的距離為:Lh = a + C*n + b
  • 由於h移動的總距離為t的兩倍,則有 Lh-Lt = Lt = (a + C*0 +b ) - (a + C*n + b) = C*n

所以可見 Lt 為環長的整數倍。

現在讓 t 指標回到頭節點,h 指標保持在 A 點,然後 兩指標同步的一起移動 a 單位(其中t指標的作用是貢獻 a 長度,使其與環內的已有長度 b 的 h 構成剛好一個環的長度)到達環入口。由於一開始 h 在環內距離入環點的距離為 b ,所以當 t 移動了 a 單位時,h在環內的長度為 b + a = Lt 所以此時 h 也剛好到達環入口(想一下,從一個地方進入操場跑一圈當然是會回到入口點的)。

所以當 t 與 h相交時便是環的入口。

public ListNode detectCycle(ListNode head) {
    if (head == null) {
        return null;
    }
    ListNode intersListNode = FindIntersection(head);
    if (intersListNode != null) {
        while (head != intersListNode) {
            head = head.next;
            intersListNode = intersListNode.next;
        }
        return head;
    } else {
        return null;
    }
}

4、求環長度

當找到環內交點時,令其中指標一個不動,而另一個指標每次移動一單位。最後再次相遇時所走過的距離便是環的長度。

/**
 *
 * @param intersListNode 迴圈圈內 快慢指標的交點
 * @return 一個圈的長度
 */
public static int LoopLength(ListNode head ,ListNode intersListNode) {
    int cnt = 0;
    //無環的情況
    if (intersListNode == null)
        return cnt;
    //無環外長的情況
    if (head == intersListNode) {
        cnt++;
        head = head.next;
        while (head != intersListNode && cnt++>0)
            head = head.next;

    }
    //有環外長的情況
    else {
        ListNode t = intersListNode.next;
        cnt++;
        while (t != intersListNode && cnt++>0)
            t = t.next;
    }
    return cnt;
}

5、程式碼彙總

import java.util.ArrayList;
import java.util.HashMap;

// Definition for singly-linked list.
class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
        next = null;
    }

}

public class Solution {

    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode intersListNode = FindIntersection(head);
        if (intersListNode != null) {
            while (head != intersListNode) {
                head = head.next;
                intersListNode = intersListNode.next;
            }
            return head;
        } else {
            return null;
        }
    }

    /**
     *
     * @param head 頭節點
     * @return 如果存在環,則返回環內快慢指標相交的節點;若無則返回null
     */
    public static ListNode FindIntersection(ListNode head) {
        ListNode h = head, t = head;
        while (h != null ) {
            if (h.next != null) {
                h = h.next;
                t = t.next;
            }
            else {
                return null;
            }
            if (h.next != null) {
                h = h.next;
                //每次h跑完兩步就要檢查一下是否追上了t,這裡可能會有人有疑問,為什麼上面h剛跑完之後不檢查是否追上了t
                //這是因為,後面需要根據這個點來求入環點,而推導公式的基礎便是每次h行動2單位都是不可分割的。
                if (t == h) {
                    return t;
                }
            }
        }
        return null;
    }

    /**
     *
     * @param intersListNode 迴圈圈內 快慢指標的交點
     * @return 一個圈的長度
     */
    public static int LoopLength(ListNode head ,ListNode intersListNode) {
        int cnt = 0;
        //無環的情況
        if (intersListNode == null)
            return cnt;
        //無環外長的情況
        if (head == intersListNode) {
            cnt++;
            head = head.next;
            while (head != intersListNode && cnt++>0)
                head = head.next;

        }
        //有環外長的情況
        else {
            ListNode t = intersListNode.next;
            cnt++;
            while (t != intersListNode && cnt++>0)
                t = t.next;
        }
        return cnt;
    }

    /**
     *
     * @param enm 連結串列的前後驅節點的關係
     * @param head 頭節點
     * @return 尾結點
     */
    public static ListNode init(int[][] enm, ListNode head) {
        HashMap<Integer, ListNode> map = new HashMap<Integer, ListNode>();
        ListNode t = head;
        map.put(0, head);
        for (int i = 0; i < enm.length; i++) {
            ListNode node;
            if (map.containsKey(enm[i][1])) {
                node = map.get(enm[i][1]);
            }
            else {
                node = new ListNode(enm[i][1]);
                map.put(enm[i][1], node);
            }
            t.next = node;
            t = node;
        }
        return t;
    }

    public static void main(String[] st) {
        ListNode head = new ListNode(0);
        Solution ss = new Solution();
        // int[][] enm = { { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 6 }, { 6, 3 } };
        int[][] enm = { { 0, 1 }, { 1, 0}};
        init(enm, head);
        System.out.println(LoopLength(head, FindIntersection(head)));
        System.out.println(ss.detectCycle(head).val);
    }
}