環形連結串列I、II(含程式碼以及證明)

2023-02-12 15:06:03

環形連結串列

解題思路

  1. 定義兩個指標,一個快指標,一個慢指標,快指標每次移動兩個節點,慢指標每次移動一個節點。
  2. 從頭節點開始,讓快慢指標同時移動,如果連結串列中有環,那麼快慢指標一定會在某個節點相遇。
  3. 如果快慢指標相遇了,說明連結串列中有環,返回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

解題思路

  1. 定義兩個指標,一個快指標fast,一個慢指標slow,都從頭節點head開始遍歷連結串列。
  2. 快指標每次走兩步,慢指標每次走一步,如果連結串列有環,那麼快慢指標一定會在環內相遇,否則快指標會先遍歷完連結串列(遇到null)。
  3. 如果快慢指標相遇,說明連結串列有環,此時將快指標重新指向頭節點head,慢指標保持在相遇節點,然後快慢指標都每次走一步,直到再次相遇,相遇的節點就是入環的第一個節點。即:從頭節點到入環節點的距離,等於從相遇節點繼續走到入環節點的距離。
  4. 如果快慢指標沒有相遇,說明連結串列無環,返回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;
    }