天天看點

入門到進階:連結清單翻轉 | 算法必看系列七入門到進階:連結清單翻轉

檢視上文:連結清單的表示

入門到進階:連結清單翻轉

接下來我們會重點看一下連結清單的翻轉,連結清單的翻轉可以衍生出很多的變形,是面試中非常熱門的考點,基本上考連結清單必考翻轉!是以掌握連結清單的翻轉是必修課!

什麼是連結清單的翻轉:給定連結清單 head–>4—>3–>2–>1,将其翻轉成 head–>1–>2–>3–>4 ,由于翻轉連結清單是如此常見,如此重要,是以我們分别詳細講解下如何用遞歸和非遞歸這兩種方式來解題.

  • 遞歸翻轉

關于遞歸的文章之前寫了三篇,如果之前沒讀過的,強烈建議點選這裡檢視,總結了遞歸的常見解題套路,給出了遞歸解題的常見四步曲,如果看完對以下遞歸的解題套路會更加深刻,這裡不做贅述了,我們直接套遞歸的解題思路:

首先我們要檢視翻轉連結清單是否符合遞歸規律:問題可以分解成具有相同解決思路的子問題,子子問題…,直到最終的子問題再也無法分解。

要翻轉 head—>4—>3–>2–>1 連結清單,不考慮 head 結點,分析 4—>3–>2–>1,仔細觀察我們發現隻要先把 3–>2–>1 翻轉成 3<—-2<—-1,之後再把 3 指向 4 即可(如下圖示)

入門到進階:連結清單翻轉 | 算法必看系列七入門到進階:連結清單翻轉

圖:翻轉連結清單主要三步驟

隻要按以上步驟定義好這個翻轉函數的功能即可, 這樣由于子問題與最初的問題具有相同的解決思路,拆分後的子問題持續調用這個翻轉函數即可達到目的。

注意看上面的步驟1,問題的規模是不是縮小了(如下圖),從翻轉整個連結清單變成了隻翻轉部分連結清單!問題與子問題都是從某個結點開始翻轉,具有相同的解決思路,另外當縮小到隻翻轉一個結點時,顯然是終止條件,符合遞歸的條件!之後的翻轉 3–>2–>1, 2–>1 持續調用這個定義好的遞歸函數即可!

入門到進階:連結清單翻轉 | 算法必看系列七入門到進階:連結清單翻轉

既然符合遞歸的條件,那我們就可以套用

遞歸四步曲

來解題了(注意翻轉之後 head 的後繼節點變了,需要重新設定!别忘了這一步)

1、定義遞歸函數,明确函數的功能 根據以上分析,這個遞歸函數的功能顯然是翻轉某個節點開始的連結清單,然後傳回新的頭結點

/**
 * 翻轉結點 node 開始的連結清單
 */
public Node invertLinkedList(Node node) {
}           

2、尋找遞推公式 上文中已經詳細畫出了翻轉連結清單的步驟,簡單總結一下遞推步驟如下

  • 針對結點 node (值為 4), 先翻轉 node 之後的結點 invert(node->next) ,翻轉之後 4—>3—>2—>1 變成了 4—>3<—2<—1
  • 再把 node 節點的下個節點(3)指向 node,node 的後繼節點設定為空(避免形成環),此時變成了 4<—3<—2<—1
  • 傳回新的頭結點,因為此時新的頭節點從原來的 4 變成了 1,需要重新設定一下 head

3、将遞推公式代入第一步定義好的函數中,如下 (invertLinkedList)

/**
 * 遞歸翻轉結點 node 開始的連結清單
 */
public Node invertLinkedList(Node node) {
    if (node.next == null) {
        return node;
    }

    // 步驟 1: 先翻轉 node 之後的連結清單
    Node newHead = invertLinkedList(node.next);

    // 步驟 2: 再把原 node 節點後繼結點的後繼結點指向 node,node 的後繼節點設定為空(防止形成環)
    node.next.next = node;
    node.next = null;

    // 步驟 3: 傳回翻轉後的頭結點
    return newHead;
}

public static void main(String[] args) {
    LinkedList linkedList = new LinkedList();
    int[] arr = {4,3,2,1};
    for (int i = 0; i < arr.length; i++) {
        linkedList.addNode(arr[i]);
    }
    Node newHead = linkedList.invertLinkedList(linkedList.head.next);
    // 翻轉後别忘了設定頭結點的後繼結點!
    linkedList.head.next = newHead;
    linkedList.printList();      // 列印 1,2,3,4
}           

畫外音:翻轉後由于 head 的後繼結點變了,别忘了重新設定哦!

4、計算時間/空間複雜度 由于遞歸調用了 n 次 invertLinkedList 函數,是以時間複雜度顯然是 O(n), 空間複雜度呢,沒有用到額外的空間,但是由于遞歸調用了 n 次 invertLinkedList 函數,壓了 n 次棧,是以空間複雜度也是 O(n)。

遞歸一定要從函數的功能去了解,從函數的功能看,定義的遞歸函數清晰易懂,定義好了之後,由于問題與被拆分的子問題具有相同的解決思路,是以子問題隻要持續調用定義好的功能函數即可,切勿層層展開子問題,此乃遞歸常見的陷阱!仔細看函數的功能,其實就是按照下圖實作的。(對照着代碼看,是不是清晰易懂^_^)

入門到進階:連結清單翻轉 | 算法必看系列七入門到進階:連結清單翻轉
  • 非遞歸翻轉連結清單(疊代解法)

我們知道遞歸比較容易造成棧溢出,是以如果有其他時間/空間複雜度相近或更好的算法,應該優先選擇非遞歸的解法,那我們看看如何用疊代來翻轉連結清單,主要思路如下

入門到進階:連結清單翻轉 | 算法必看系列七入門到進階:連結清單翻轉

步驟 1:定義兩個節點:pre, cur ,其中 cur 是 pre 的後繼結點,如果是首次定義, 需要把 pre 指向 cur 的指針去掉,否則由于之後連結清單翻轉,cur 會指向 pre, 就進行了一個環(如下),這一點需要注意

入門到進階:連結清單翻轉 | 算法必看系列七入門到進階:連結清單翻轉

步驟2:知道了 cur 和 pre,翻轉就容易了,把 cur 指向 pre 即可,之後把 cur 設定為 pre ,cur 的後繼結點設定為 cur 一直往前重複此步驟即可,完整動圖如下

入門到進階:連結清單翻轉 | 算法必看系列七入門到進階:連結清單翻轉

注意:同遞歸翻轉一樣,疊代翻轉完了之後 head 的後繼結點從 4 變成了 1,記得重新設定一下。

知道了解題思路,實作代碼就容易多了,直接上代碼

/**
 * 疊代翻轉
 */
public void iterationInvertLinkedList() {
    // 步驟 1
    Node pre = head;
    Node cur = pre.getNext();
    pre.setNext(null);   // pre 是頭結點,避免翻轉連結清單後形成環

    // 步驟 2
    while (cur != null) {
        /**
         * 務必注意!!!:在 cur 指向 pre 之前一定要先保留 cur 的後繼結點,不然如果 cur 先指向 pre,之後就再也找不到後繼結點了
         */
        Node next = cur.getNext();
        cur.setNext(pre);
        pre = cur;
        cur = next;
    }
    // 此時 pre 指向的是原連結清單的尾結點,翻轉後即為連結清單 head 的後繼結點
    head.next = pre;
}           

用疊代的思路來做由于循環了 n 次,顯然時間複雜度為 O(n),另外由于沒有額外的空間使用,也未像遞歸那樣調用遞歸函數不斷壓棧,是以空間複雜度是 O(1),對比遞歸,顯然應該使用疊代的方式來處理!

花了這麼大的精力我們總算把翻轉連結清單給搞懂了,如果大家看了之後幾道翻轉連結清單的變形,會發現我們花了這麼大篇幅講解翻轉連結清單是值得的。

來源 | 五分鐘學算法

作者 | 程式員小吳

繼續閱讀