程序運行截圖:
public class Node {
// 這僅僅是一個標識,用來標記這是哪一個節點
public int tag;
public Node next;
public Node(Node next, int tag) {
this.next = next;
this.tag = tag;
}
// 邏輯斷開 思路需要的tag
public boolean hasNext = true;
}
public class Algorithm3 {
/**
* [題目] 一個鏈表中包含環,請找出該鏈表的環的入口結點。
*/
public static void main(String[] args) {
Node node = createTestNode();
Node nodeBySlowFast = isCircleBySlowFast(node);
System.out.println("isCircleBySlowFast : "
+ ((nodeBySlowFast == null) ? "這不是一個環鏈表" : ("當前環鏈表的入口是: " + nodeBySlowFast.tag)));
node = createTestNode();
Node nodeByHashCode = isCircleByHashCode(node);
System.out.println("isCircleByHashCode : "
+ ((nodeByHashCode == null) ? "這不是一個環鏈表" : ("當前環鏈表的入口是: " + nodeByHashCode.tag)));
node = createTestNode();
Node nodeByHasNext = isCircleByHasNext(node);
System.out.println("isCircleByHasNext : "
+ ((nodeByHasNext == null) ? "這不是一個環鏈表" : ("當前環鏈表的入口是: " + nodeByHasNext.tag)));
}
public static Node createTestNode() {
Node node12 = new Node(null, 12);
Node node11 = new Node(node12, 11);
Node node10 = new Node(node11, 10);
Node node9 = new Node(node10, 9);
Node node8 = new Node(node9, 8);
Node node7 = new Node(node8, 7);
Node node6 = new Node(node7, 6);
Node node5 = new Node(node6, 5);
Node node4 = new Node(node5, 4);
Node node3 = new Node(node4, 3);
Node node2 = new Node(node3, 2);
// 設置環入口
node12.next = node5;
Node head = new Node(node2, 1);
return head;
}
/**
* 網上做法,其實也是最難的思路:
* 難度不在於編碼,而在於不藉助於除了 Node 之外的思路,尋找環的入口 !!
* 判斷是否是有環鏈表:一個快指針和一個慢指針,在環中,快指針肯定會反向追上慢指針。
* 尋找環入口:
* 兩個指針,一個從鏈表頭開始走,一個從環內快慢指針相交的節點走;
* 那麼當這兩個指針相交的時,此時相交的節點就是環入口。
* PS:這個尋找環入口的思路,並不容易,甚至於要理解也是有點困難的, 我自己也沒有想到方法,只能引用別人的博客中的思路。
*
* @param node
* @return
*/
public static Node isCircleBySlowFast(Node node) {
Node slow = node;
Node fast = node;
Node p = node;
while (slow != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
Node q = fast;
while(p != q) {
p = p.next;
q = q.next;
}
return p;
}
}
return null;
}
/**
* 另外的思路:
* 如果說 快慢指針的做法是較好的空間複雜度的算法,
* 其實我一直覺得可以使用一個字符串記錄 hashcode 來判斷是否重複
* PS:不過不知道其他語言有沒有類似 Java 這種 hashcode 的特性
*
* @param node
* @return
*/
public static Node isCircleByHashCode(Node node) {
String hashcodes = "";
String split = " || ";
Node temp = node;
while (temp != null) {
if (hashcodes.contains(split + temp.hashCode())) {
return temp;
}
hashcodes = hashcodes + split + temp.hashCode();
temp = temp.next;
}
return null;
}
/**
* 其實和 hashcode 是一樣的思路:
* 如果說 hashcode 侷限性是並不能確定所有的語言都有類似 hashcode 一樣的特性
* 那麼這個方法的思路,則是對於要操作的鏈表結構允許修改
* 我在節點類 Node 中添加了 hasNext 屬性,這是一種 邏輯斷開 的思路:
* 在我遍歷鏈表的過程中,使用兩個指針,因爲這是單鏈不是雙鏈,所以只能用兩個指針來處理
* 兩個指針分別是 slow 和fast,slow緊跟着fast,相差一個節點的距離
* slow 走過節點的時候,會將 hasNext 置爲 false,就是告訴你 鏈表已經斷開了
* 當fast 進入環後,遇到的第一個 邏輯斷開 的鏈表節點就是環的入口
*
* @param node
* @return
*/
public static Node isCircleByHasNext(Node node) {
Node slow = node;
Node fast = node;
while (fast != null && fast.next != null) {
if (!fast.next.hasNext) {
return fast.next;
}
slow = fast;
slow.hasNext = false;
fast = fast.next;
}
return null;
}
}
參考博客:判斷單鏈表中是否有環,找到環的入口節點,文章寫的很有條理,只是有些地方覺得描述的不夠詳細,其實我倒是希望能詳細些,於是有了下面的補充,其實就是在原作者的基礎上,寫下自己的理解。
圖解 isCircleBySlowFast 的思路
這個方法的思路在代碼中已經說過了,判斷是否存在環這不必贅述,難點在於怎麼找到環入口。從參考的博客中的思路來看,這完全就是數學做題的思維,將所有的已知條件全部列舉出來,將所有能找出的公式算出來,然後通過觀察公式之間的關係來解題。
需要知道: 單鏈表中的方向是固定的,也就是說在環內,說道距離,定然是逆時針來計算的,這一點很重要。
設兩個指針 slow / fast,鏈表起始點爲 h, 環入口爲t,環的長度爲 n
設 h->t 之間的距離爲 a, 當slow到t時,fast所處環中的位置記爲m1,且t->m1的距離記爲b
從上圖中可以看出此時slow前進的距離爲a。而fast的前進速度是slow的兩倍。環的長度之前說過記爲n。另外需要注意的是,當slow前進到t的時候,fast很可能已經在環內跑了好幾圈了,設fast已經跑了r圈,於是就有了下面的公式:
slow 前進的距離 = a;
fast 前進的距離 = 2 x slow 前進的距離 = 2a;
同時!! fast 前進距離 = 非環部分 + 環內 = 非環部 + (所跑圈數+t到m1的距離) = a + (nr+b);
最後: 2a = a + (nr+b) --> a = nr +b ;
設slow和fast環內相遇的節點爲m2
從上圖可以看出,m2到t的距離,已經標記爲b了,這是怎麼來的?難道是因爲t->m1的距離爲b,所以m2->t的距離就是b?完全不是這樣!!!
推導:
我們回到當slow到達t節點,fast處在m1位置時。此時fast與slow相距也就是 m1->t 的距離:環長 - t->m1,也就是 n - b。也就是說如果fast想追上slow,必須在前進速度的差值下填補這個距離,那麼需要多久能追上 slow 呢?
前進時間 = 距離差值 / 速度差值 = ( n -b ) / ( 2 -1 ) = n-b.
從上面可知,fast想要追上slow必須經過的時長爲: n-b。在這個時間裏,slow以1的速度,前進了n-b到m2節點。那麼m2->t的距離自然就是b了。
其實到了這裏,如果不是看到了上文中的博客,我是真的想不到將m2與t關聯在一起…臣妾真的做不到…
那麼我們把m2與t聯繫起來,如果從m2到t,需要前進的距離爲b,那麼不管從m2爲起始點,前進多少圈,只要在m2的位置前進b,都會到達t。也就是從m2前進:nx + b ,x爲前進了幾圈,都會抵達t。
還記得前面的公式嗎?a = nr +b。
如果從指針p、q分別從h和m2前進,當p到達t時,設前進了 nr+b,那麼q也前進了 nr+b,上面已經推導出從m2出發,前進任意 nx+b,都會到達t。換句話說,當p從h出發到達t的同時,q也到達了t!!!
所以,最後以代碼的形式表現上面推導的理論,就變成了分別從h和m2遍歷,當p、q遍歷到同一個節點時,該節點就是環入口。
感謝參考博客博主的文章。