算法題003 -- [判斷單鏈表中是否有環,找到環的入口節點] by java

程序運行截圖:
在這裏插入圖片描述

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遍歷到同一個節點時,該節點就是環入口。

感謝參考博客博主的文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章