如何判斷鏈表有環
前天晚上臨睡覺前看到了公眾號腳本之家推送的一篇文章,文章內容是一道算法題,並給出了思路解釋,但沒有具體源碼實現,這讓我覺得少了點什么,於是,趁周末,我補齊了缺失的內容,好了,no code, no bb,我們開始吧。
題目描述:
有一個單向鏈表,鏈表中有可能出現“環”,就像下圖這樣。那么,如何用程序來判斷該鏈表是否為有環鏈表呢?(圖片來自公眾號)
方法1:
從頭開始遍歷整個單鏈表,每遍歷一個新節點,就把它與之前遍歷過的節點進行比較,如果值相同,那么就認為這兩個節點是一個節點,則證明鏈表有環,停止遍歷,否則繼續遍歷下一個節點,重復剛才的操作,直到遍歷結束。結合上圖來說,流程是這樣的:
① 得到 "3"節點,把它與第一個節點 “5”比較,值不相等,繼續遍歷下一個節點 “7”。(從第二個節點開始遍歷)
② 得到 “7”節點,把它依次與 “5”、“3”比較,值不相等,繼續遍歷下一個節點 “2”
③ 重復以上操作,直到遍歷完節點 “1”
④ 得到節點 “2”,把它依次與 “5”、“3”、“7”、“2”、“6”、“8”、“1”進行比較,當比較到節點 “2”時,值相等,遍歷結束,證明該鏈表有環。
假設鏈表的節點數量為n,則該解法的時間復雜度為O(n2),由於沒有創建額外的存儲空間,所以空間復雜度為O(1)
鏈表的實現比較簡單,我只寫了一個add方法,一個display方法:

1 //單向鏈表 2 public class SingleLinkedList { 3 private Node head;//標識頭節點 4 private int size;//標識鏈表中節點個數 5 6 public SingleLinkedList() { 7 this.size = 0; 8 this.head = null; 9 } 10 11 //node類 12 private class Node{ 13 private Object data;//每個節點的數據 14 private Node next;//指向下一個節點的鏈接 15 16 public Node(Object data) { 17 this.data = data; 18 } 19 } 20 21 /** 22 * 將節點插入鏈表 23 * @param data 帶插入的值 24 */ 25 public void add(Object data) { 26 Node temp = head; 27 if (size == 0) { 28 head = new Node(data); 29 size++; 30 return; 31 } 32 while (temp.next != null) { 33 temp = temp.next; 34 } 35 temp.next = new Node(data); 36 size++; 37 } 38 39 /** 40 * 從頭開始遍歷節點 41 */ 42 public void display() { 43 if (size > 0) { 44 Node node = head; 45 if (size == 1) { 46 System.out.println("[" + node.data + "]"); 47 return; 48 } 49 while (node != null) { 50 System.out.println(node.data); 51 node = node.next; 52 } 53 } else { 54 System.out.println("[]"); 55 } 56 } 57 }
方法1如下:
1 /** 2 * 根據索引得到鏈表的某個節點的值 3 * @param key 4 * @return 5 */ 6 public Object getNode(int key) { 7 8 if (key < 0 || key > size - 1) { 9 throw new ArrayIndexOutOfBoundsException("越界!"); 10 } else { 11 Node temp = head; 12 int count = 0; 13 while (temp != null) { 14 if (count == key) { 15 return temp.data; 16 } 17 temp = temp.next; 18 count++; 19 } 20 21 } 22 return null; 23 } 24 25 26 /** 27 * 從頭開始,依次與給定索引位置的節點的值進行比較,如果相同,則返回true,否則false 28 * @param key 29 * @return 30 */ 31 public boolean havaSameElement(int key) { 32 boolean flag = false; 33 int count = 0; 34 Node temp = head; 35 while (temp != null && count < key) { 36 if (temp.data == getNode(key)) { 37 flag = true; 38 return flag; 39 } 40 count++; 41 temp = temp.next; 42 } 43 return flag; 44 45 } 46 47 /** 48 * 方式1 49 * @return 50 */ 51 public boolean isAnnulate1() { 52 boolean flag = false; 53 for (int i = 1; i < size; i++) { 54 if (havaSameElement(i)) { 55 flag = true; 56 break; 57 } 58 } 59 return flag; 60 }
方法2:
這種方法用到了HashSet中add方法去重的特點,思路是這樣的:
① new一個HashSet,用來存儲之前遍歷過的節點值
②從頭節點head開始,依次遍歷鏈表中的節點,並把它add到集合中
③ 如果在集合中已經有一個相同的值,那么會返回false,這樣便證明鏈表有環,退出遍歷
方法2如下:
1 /** 2 * 方式2 3 * @return 4 */ 5 public boolean isAnnulate2() { 6 boolean flag = false; 7 Set<Object> set = new HashSet<>(); 8 Node temp = head; 9 while (temp != null) { 10 if (!set.add(temp.data)) { 11 flag = true; 12 break; 13 } 14 temp = temp.next; 15 } 16 return flag; 17 18 }
方法3:
定義兩個指針tortoise與rabbit,讓它們一開始均指向head頭節點,之后,tortoise每次向后移動一個節點,rabbit每次向后移動2個節點,只要這個鏈表是有環的,它們必定會在某一次移動完后相遇,什么?你問我為什么?我們來思考這樣一個問題,兩個人在運動場跑步,他們的起始位置都是一樣的,當開跑后,只有在兩種情況下,他們的位置會重合,第一就是他們的速度始終一致,第二就是跑得快的那個人套圈,如下圖所示:
我們假設兩位跑步的同學速度始終不變,即tortoise以V的速度進行移動,rabbit以2V的速度進行移動,在經過了相同的時間T后,他們相遇了,此時tortoise移動的距離為VT,而rabbit移動的距離為2VT,他們移動的距離差VT,即為這個鏈表中 “環”的周長,如上圖所示,節點A表示為環入口,節點B表示他們第一次相遇,我們將head頭節點至節點A的距離記為a,將節點A至他們第一次相遇的節點B的距離記為b,通過我們剛才的分析,不難得出,tortoise移動的距離VT = a + b,等量代換,他們移動的距離差也為 a+ b,所以鏈表中環的周長為 a + b,現在已知節點A至節點B的距離為b,那么節點B至節點A的距離便為a,至此,發現什么了?head頭節點到節點A的距離同樣為a,我們建立一個指針 start 指向頭節點,它與B節點處的tortoise同時以一個節點的速度進行移動,一段時間后,它們將在環入口相遇。我們不光能證明一個鏈表是否有環,還能找到環的入口。
方法3如下:
1 public Node getIntersect() { 2 Node temp = head; 3 Node tortoise = temp; 4 Node rabbit = temp; 5 while (rabbit != null && rabbit.next != null) { 6 tortoise = tortoise.next; 7 rabbit = rabbit.next.next; 8 if (tortoise == rabbit) { 9 return tortoise; 10 } 11 } 12 return null; 13 } 14 15 public Object isAnnulate3() { 16 Node temp = head; 17 if (temp == null) { 18 return null; 19 } 20 Node intersect = getIntersect(); 21 if (intersect == null) { 22 return null; 23 } 24 Node startNode = head; 25 while (startNode != intersect) { 26 startNode = startNode.next; 27 intersect = intersect.next; 28 } 29 return startNode.data; 30 31 }
我要說明的是,方法3中的代碼只是 “偽代碼”,它並不能真的證明鏈表有環,並返回環的入口節點值。至於為什么,我的理解是,因為單鏈表的結構特點,它並不會真的存在 “環”,我們這里說的環只是把它抽象出來,實際上,單鏈表中一個節點只保留有對它后面那個節點的引用,並沒有對它前面節點的引用,所以,並不存在真正的 “環”,不過,這種思路還是值得學習的。假設鏈表的節點數量為n,則該算法的時間復雜度為O(n),除指針外,沒有占用任何額外的存儲空間,所以空間復雜度為O(1)。
完整代碼如下:

1 package judgeLinkedListCircle; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 7 /** 8 * 單向鏈表 9 * @author Cone 10 * @since 2019年7月27日 11 * 12 */ 13 public class SingleLinkedList { 14 private Node head;//標識頭節點 15 private int size;//標識鏈表中節點個數 16 17 public SingleLinkedList() { 18 this.size = 0; 19 this.head = null; 20 } 21 22 //node類 23 private class Node{ 24 private Object data;//每個節點的數據 25 private Node next;//指向下一個節點的鏈接 26 27 public Node(Object data) { 28 this.data = data; 29 } 30 } 31 32 /** 33 * 將節點插入鏈表 34 * @param data 帶插入的值 35 */ 36 public void add(Object data) { 37 Node temp = head; 38 if (size == 0) { 39 head = new Node(data); 40 size++; 41 return; 42 } 43 while (temp.next != null) { 44 temp = temp.next; 45 } 46 temp.next = new Node(data); 47 size++; 48 } 49 50 /** 51 * 從頭開始遍歷節點 52 */ 53 public void display() { 54 if (size > 0) { 55 Node node = head; 56 if (size == 1) { 57 System.out.println("[" + node.data + "]"); 58 return; 59 } 60 while (node != null) { 61 System.out.println(node.data); 62 node = node.next; 63 } 64 } else { 65 System.out.println("[]"); 66 } 67 } 68 69 /** 70 * 根據索引得到鏈表的某個節點的值 71 * @param key 72 * @return 73 */ 74 public Object getNode(int key) { 75 76 if (key < 0 || key > size - 1) { 77 throw new ArrayIndexOutOfBoundsException("越界!"); 78 } else { 79 Node temp = head; 80 int count = 0; 81 while (temp != null) { 82 if (count == key) { 83 return temp.data; 84 } 85 temp = temp.next; 86 count++; 87 } 88 89 } 90 return null; 91 } 92 93 94 /** 95 * 從頭開始,依次與給定索引位置的節點的值進行比較,如果相同,則返回true,否則false 96 * @param key 97 * @return 98 */ 99 public boolean havaSameElement(int key) { 100 boolean flag = false; 101 int count = 0; 102 Node temp = head; 103 while (temp != null && count < key) { 104 if (temp.data == getNode(key)) { 105 flag = true; 106 return flag; 107 } 108 count++; 109 temp = temp.next; 110 } 111 return flag; 112 113 } 114 115 /** 116 * 方式1 117 * @return 118 */ 119 public boolean isAnnulate1() { 120 boolean flag = false; 121 for (int i = 1; i < size; i++) { 122 if (havaSameElement(i)) { 123 flag = true; 124 break; 125 } 126 } 127 return flag; 128 } 129 130 131 /** 132 * 方式2 133 * @return 134 */ 135 public boolean isAnnulate2() { 136 boolean flag = false; 137 Set<Object> set = new HashSet<>(); 138 Node temp = head; 139 while (temp != null) { 140 if (!set.add(temp.data)) { 141 flag = true; 142 break; 143 } 144 temp = temp.next; 145 } 146 return flag; 147 148 } 149 150 public Node getIntersect() { 151 Node temp = head; 152 Node tortoise = temp; 153 Node rabbit = temp; 154 while (rabbit != null && rabbit.next != null) { 155 tortoise = tortoise.next; 156 rabbit = rabbit.next.next; 157 if (tortoise == rabbit) { 158 return tortoise; 159 } 160 } 161 return null; 162 } 163 164 /** 165 * 方式3 166 * @return 167 */ 168 public Object isAnnulate3() { 169 Node temp = head; 170 if (temp == null) { 171 return null; 172 } 173 Node intersect = getIntersect(); 174 if (intersect == null) { 175 return null; 176 } 177 Node startNode = head; 178 while (startNode != intersect) { 179 startNode = startNode.next; 180 intersect = intersect.next; 181 } 182 return startNode.data; 183 184 } 185 186 }
如有錯誤,歡迎指正。
代碼已上傳至github: