題目
有一個單向鏈表,鏈表中有可能出現“環”,就像下圖這樣。那么,如何用程序來判斷該鏈表是否為有環鏈表呢?
解決方案
方法1:
首先創建一個以節點ID為Key的HashSet集合,用來存儲曾經遍歷過的節點。然后同樣從頭節點開始,依次遍歷單鏈表中的每一個節點。每遍歷一個新節點,都用新節點和HashSet集合中存儲的節點進行比較,如果發現HashSet中存在與之相同的節點ID,則說明鏈表有環,如果HashSet中不存在與新節點相同的節點ID,就把這個新節點ID存入HashSet中,之后進入下一節點,繼續重復剛才的操作。
遍歷過5、3。

遍歷過5、3、7、2、6、8、1。
當再一次遍歷節點2時,查找HashSet,發現節點已存在。
由此可知,鏈表有環。
假設鏈表的節點數量為n,則該解法的時間復雜度是O(n)。由於使用了額外的存儲空間,所以算法的空間復雜度同樣是O(n)。
方法2:
首先創建兩個指針p1和p2(在Java里就是兩個對象引用),讓它們同時指向這個鏈表的頭節點。然后開始一個大循環,在循環體中,讓指針p1每次向后移動1個節點,讓指針p2每次向后移動2個節點,然后比較兩個指針指向的節點是否相同。如果相同,則可以判斷出鏈表有環,如果不同,則繼續下一次循環。
第1步,p1和p2都指向節點5。
第2步,p1指向節點3,p2指向節點7。
第3步,p1指向節點7,p2指向節點6。
第4步,p1指向節點2,p2指向節點1。
第5步,p1指向節點6,p2也指向節點6,p1和p2所指相同,說明鏈表有環。
分析:
此方法就類似於一個追及問題。在一個環形跑道上,兩個運動員從同一地點起跑,一個運動員速度快,另一個運動員速度慢。當兩人跑了一段時間后,速度快的運動員必然會再次追上並超過速度慢的運動員,原因很簡單,因為跑道是環形的。假設鏈表的節點數量為n,則該算法的時間復雜度為O(n)。除兩個指針外,沒有使用任何額外的存儲空間,所以空間復雜度是O(1)。
代碼:
package arithmetic.com.ty.binary; public class LinkedCycle { /** * 判斷是否有環 * * @param head 鏈表頭節點 */ public static boolean isCycle(Node head) { Node p1 = head; Node p2 = head; while (p2 != null && p2.next != null) { p1 = p1.next; p2 = p2.next.next; if (p1 == p2) { return true; } } return false; } /** * 鏈表節點 */ private static class Node { int data; Node next; Node(int data) { this.data = data; } } public static void main(String[] args) throws Exception { Node node1 = new Node(5); Node node2 = new Node(3); Node node3 = new Node(7); Node node4 = new Node(2); Node node5 = new Node(6); node1.next = node2; node2.next = node3; node3.next = node4; node4.next = node5; node5.next = node2; System.out.println(isCycle(node1)); } }
擴展問題
如果單向鏈表中存在環的情況,環長是多少?
思路:當兩個指針首次相遇,證明鏈表有環的時候,讓兩個指針從相遇點繼續循環前進,並統計前進的循環次數,直到兩個指針第2次相遇。此時,統計出來的前進次數就是環長。因為指針p1每次走1步,指針p2每次走2步,兩者的速度差是1步。當兩個指針再次相遇時,p2比p1多走了整整1圈。因此,環長 = 每一次速度差 × 前進次數 = 前進次數。
代碼:
/** * 計算環長 */ public static int cycleLenth(Node head) { Node p1 = head; Node p2 = head; int count = 0; int length = 0; while (count < 2 && p2 != null && p2.next != null) { if(count > 0) { length++; } p1 = p1.next; p2 = p2.next.next; if (p1 == p2) { count++; } } return length; }