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);
}
}