環形連結串列
解題思路
- 定義兩個指標,一個快指標,一個慢指標,快指標每次移動兩個節點,慢指標每次移動一個節點。
- 從頭節點開始,讓快慢指標同時移動,如果連結串列中有環,那麼快慢指標一定會在某個節點相遇。
- 如果快慢指標相遇了,說明連結串列中有環,返回true。如果快指標移動到了null,說明連結串列中沒有環,返回false。
思考1:為什麼快慢指標能判斷是否有環
- 如果連結串列中沒有環,那麼快指標會先於慢指標到達連結串列的尾部,也就是null,這時候我們就可以判斷連結串列中沒有環,返回false。
- 如果連結串列中有環,那麼快指標會在環中不斷地追趕慢指標,就像兩個人在環形跑道上跑步,速度快的人總會追上速度慢的人。這時候,快指標和慢指標一定會在某個節點相遇,這時候我們就可以判斷連結串列中有環,返回true。
- 為什麼快指標要每次移動兩個節點,慢指標要每次移動一個節點呢?這是為了保證快指標和慢指標的相對速度是一定的,如果快指標每次移動三個節點,慢指標每次移動一個節點,那麼快指標的速度就是慢指標的三倍,這樣可能會導致快指標在環中跳過慢指標,從而無法相遇。如果快指標每次移動一個節點,慢指標每次移動一個節點,那麼快指標和慢指標的速度就是一樣的,這樣也無法相遇。所以,快指標每次移動兩個節點,慢指標每次移動一個節點,是一種比較合理的選擇,可以保證快慢指標在環中一定會相遇。
程式碼實現
// 定義一個判斷連結串列是否有環的方法,接受一個頭節點作為引數
public boolean hasCycle(Node head) {
// 如果頭節點為空,直接返回false
if (head == null) {
return false;
}
// 定義快慢指標,初始化為頭節點
Node fast = head;
Node slow = head;
// 用一個迴圈來移動快慢指標
while (fast != null && fast.next != null) {
// 快指標每次移動兩個節點
fast = fast.next.next;
// 慢指標每次移動一個節點
slow = slow.next;
// 如果快慢指標相遇了,說明有環,返回true
if (fast == slow) {
return true;
}
}
// 如果快指標移動到了null,說明沒有環,返回false
return false;
}
環形連結串列II
解題思路
- 定義兩個指標,一個快指標fast,一個慢指標slow,都從頭節點head開始遍歷連結串列。
- 快指標每次走兩步,慢指標每次走一步,如果連結串列有環,那麼快慢指標一定會在環內相遇,否則快指標會先遍歷完連結串列(遇到null)。
- 如果快慢指標相遇,說明連結串列有環,此時將快指標重新指向頭節點head,慢指標保持在相遇節點,然後快慢指標都每次走一步,直到再次相遇,相遇的節點就是入環的第一個節點。即:從頭節點到入環節點的距離,等於從相遇節點繼續走到入環節點的距離。
- 如果快慢指標沒有相遇,說明連結串列無環,返回null。
舉個例子:
1 -> 2 -> 3 -> 4
^ |
| v
7 <- 6 <- 5
- 這個連結串列中有一個環,從節點2到節點7。環的長度是6,入口節點是節點2。
- 我們可以用快慢指標的方法,讓快指標從節點1開始,每次走兩步,慢指標從節點1開始,每次走一步,它們在節點7相遇,然後讓快指標從頭開始一步步走,慢指標保持在相遇節點開始一步步走,快慢指標再次在2節點相遇,所以2節點就是入環節點。
思考1:為什麼慢指標每次移動一步,快指標每次移動兩步,一定會在環中相遇而不是錯過?
假設連結串列的長度是L,環的長度是n,環的入口節點距離頭節點的距離是a,快慢指標在環中相遇的節點距離環的入口節點的距離是b,那麼有以下關係:
- 當快慢指標相遇時,慢指標走過的距離是a+b,快指標走過的距離是a+b+k*n,其中k是快指標在環中繞的圈數。
- 因為快指標的速度是慢指標的兩倍,所以快指標走過的距離是慢指標的兩倍,即2*(a+b) = a+b+kn,化簡得a+b = kn。
- 這個式子的意思是,當快慢指標相遇時,慢指標走過的距離剛好是環的長度的整數倍,也就是說,慢指標在環中走了k圈,快指標在環中走了k+1圈。
- 這樣,我們可以知道,快慢指標一定會在環中相遇,而不是錯過,因為快指標每次都會比慢指標多走一步,所以它們之間的距離每次都會縮小一步,直到相遇為止。
思考2:如何證明,從頭節點到入環節點的距離,等於從相遇節點繼續走到入環節點的距離?
假設從頭結點到環形入口節點的距離為x。 環形入口節點到fast指標與slow指標相遇節點距離為y。從相遇節點再到環形入口節點距離為z
- 當快慢指標首次相遇時,慢指標走過的路程為x + y,快指標走過的路程為x + y + n (y + z)。
- 由於快指標的速度是慢指標的兩倍,所以快指標走過的路程是慢指標的兩倍,即(x + y) * 2 = x + y + n (y + z)。
- 化簡得到x + y = n (y + z),即x = n (y + z) - y,再從n(y+z)中提出一個(y+z)來,得到x = (n - 1) (y + z) + z。而 y + z正好是環一圈的長度。
- 這個式子的意義是,從頭節點到入環節點的距離,等於快慢指標相遇後,一個指標繼續走n-1圈,再加上從相遇點到入環節點的距離。
- 當n=1時,即某一指標只走了一圈,那麼x = z,即從頭節點到入環節點的距離,等於從相遇節點繼續走到入環節點的距離。
- 因此,如果我們讓一個指標從頭節點開始,另一個指標從相遇節點開始,每次都走一步,那麼他們最終會在入環節點相遇,這樣就找到了入環節點。
程式碼實現
public ListNode detectCycle(ListNode head) {
//如果連結串列為空或只有一個節點,直接返回null
if (head == null || head.next == null) {
return null;
}
//定義快慢指標
ListNode fast = head;
ListNode slow = head;
//遍歷連結串列,直到快指標遇到null或快慢指標相遇
while (fast != null && fast.next != null) {
//快指標走兩步,慢指標走一步
fast = fast.next.next;
slow = slow.next;
//如果快慢指標相遇,說明有環
if (fast == slow) {
//將快指標重新指向頭節點
fast = head;
//快慢指標都每次走一步,直到再次相遇
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
//返回相遇的節點,即入環的第一個節點
return fast;
}
}
//如果快指標遇到null,說明無環,返回null
return null;
}