天天看點

連結清單翻轉的變形 | 算法必看系列八連結清單翻轉的變形實戰

檢視上文:連結清單翻轉的方法

連結清單翻轉的變形實戰

接下來我們來看看連結清單翻轉的變形:

變形題 1:給定一個連結清單的頭結點 head,以及兩個整數 from 和 to ,在連結清單上把第 from 個節點和第 to 個節點這一部分進行翻轉。例如:給定如下連結清單,from = 2, to = 4 head–>5–>4–>3–>2–>1 将其翻轉後,連結清單變成 head–>5—>2–>3–>4–>1

有了之前翻轉整個連結清單的解題思路,現在要翻轉部分連結清單就相對簡單多了,主要步驟如下:

1、根據 from 和 to 找到 from-1, from, to, to+1 四個結點(注意臨界條件,如果 from 從頭結點開始,則 from-1 結點為空, 翻轉後需要把 to 設定為頭結點的後繼結點, from 和 to 結點也可能超過尾結點,這兩種情況不符合條件不翻轉)。

2、對 from 到 to 的結點進行翻轉

3、将 from-1 節點指向 to 結點,将 from 結點指向 to + 1 結點

連結清單翻轉的變形 | 算法必看系列八連結清單翻轉的變形實戰

知道了以上的思路,代碼就簡單了,按上面的步驟1,2,3 實作,注釋也寫得很詳細,看以下代碼(對 from 到 to 結點的翻轉我們使用疊代翻轉,當然使用遞歸也是可以的,限于篇幅關系不展開,大家可以嘗試一下)。

/**
 * 疊代翻轉 from 到 to 的結點
 */
public void iterationInvertLinkedList(int fromIndex, int toIndex) throws Exception {

    Node fromPre = null;            // from-1結點
    Node from = null;               // from 結點
    Node to = null;                 // to 結點
    Node toNext = null;             // to+1 結點

    // 步驟 1:找到  from-1, from, to,  to+1 這四個結點
    Node tmp = head.next;
    int curIndex = 1;      // 頭結點的index為1
    while (tmp != null) {
        if (curIndex == fromIndex-1) {
            fromPre = tmp;
        } else if (curIndex == fromIndex) {
            from = tmp;
        } else if (curIndex == toIndex) {
            to = tmp;
        } else if (curIndex == toIndex+1) {
            toNext = tmp;
        }
        tmp = tmp.next;
        curIndex++;
    }

    if (from == null || to == null) {
        // from 或 to 都超過尾結點不翻轉
        throw new Exception("不符合條件");
    }

    // 步驟2:以下使用循環疊代法翻轉從 from 到 to 的結點
    Node pre = from;
    Node cur = pre.next;
    while (cur != toNext) {
        Node next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    
    // 步驟3:将 from-1 節點指向 to 結點(如果從 head 的後繼結點開始翻轉,則需要重新設定 head 的後繼結點),将 from 結點指向 to + 1 結點
    if (fromPre != null) {
        fromPre.next = to;
    } else {
        head.next = to;
    }
    from.next = toNext;
}           

變形題 2:給出一個連結清單,每 k 個節點一組進行翻轉,并傳回翻轉後的連結清單。k 是一個正整數,它的值小于或等于連結清單的長度。如果節點總數不是 k 的整數倍,那麼将最後剩餘節點保持原有順序。

示例 : 給定這個連結清單:head–>1->2->3->4->5 當 k = 2 時,應當傳回: head–>2->1->4->3->5 當 k = 3 時,應當傳回: head–>3->2->1->4->5 說明 :

  • 你的算法隻能使用常數的額外空間。
  • 你不能隻是單純的改變節點内部的值,而是需要實際的進行節點交換。

隻要我們能找到翻一組 k 個結點的方法,問題就解決了(之後隻要重複對 k 個結點一組的連結清單進行翻轉即可)。

接下來,我們以以下連結清單為例

連結清單翻轉的變形 | 算法必看系列八連結清單翻轉的變形實戰

來看看怎麼翻轉 3 個一組的連結清單(此例中 k = 3)

  • 首先,我們要記錄 3 個一組這一段連結清單的前繼結點,定義為 startKPre,然後再定義一個 step, 從這一段的頭結點 (1)開始周遊 2 次,找出這段連結清單的起始和終止結點,如下圖示
連結清單翻轉的變形 | 算法必看系列八連結清單翻轉的變形實戰
  • 找到 startK 和 endK 之後,根據之前的疊代翻轉法對 startK 和 endK 的這段連結清單進行翻轉
連結清單翻轉的變形 | 算法必看系列八連結清單翻轉的變形實戰
  • 然後将 startKPre 指向 endK,将 startK 指向 endKNext,即完成了對 k 個一組結點的翻轉。
連結清單翻轉的變形 | 算法必看系列八連結清單翻轉的變形實戰

知道了一組 k 個怎麼翻轉,之後隻要重複對 k 個結點一組的連結清單進行翻轉即可,對照圖示看如下代碼應該還是比較容易了解的

/**
 * 每 k 個一組翻轉連結清單
 * @param k
 */
public void iterationInvertLinkedListEveryK(int k) {
    Node tmp = head.next;
    int step = 0;               // 計數,用來找出首結點和尾結點

    Node startK = null;         // k個一組連結清單中的頭結點
    Node startKPre = head;      // k個一組連結清單頭結點的前置結點
    Node endK;                  // k個一組連結清單中的尾結點
    while (tmp != null) {
        // tmp 的下一個節點,因為由于翻轉,tmp 的後繼結點會變,要提前儲存
        Node tmpNext = tmp.next;
        if (step == 0) {
            // k 個一組連結清單區間的頭結點
            startK = tmp;
            step++;
        } else if (step == k-1) {
            // 此時找到了 k 個一組連結清單區間的尾結點(endK),對這段連結清單用疊代進行翻轉
            endK = tmp;
            Node pre = startK;
            Node cur = startK.next;
            if (cur == null) {
                break;
            }
            Node endKNext = endK.next;
            while (cur != endKNext) {
                Node next = cur.next;
                cur.next = pre;
                pre = cur;
                cur = next;
            }
            // 翻轉後此時 endK 和 startK 分别是是 k 個一組連結清單中的首尾結點
            startKPre.next = endK;
            startK.next = endKNext;

            // 目前的 k 個一組翻轉完了,開始下一個 k 個一組的翻轉
            startKPre = startK;
            step = 0;
        } else {
            step++;
        }
        tmp = tmpNext;
    }
}           

時間複雜度是多少呢,對連結清單從頭到尾循環了 n 次,同時每 k 個結點翻轉一次,可以認為總共翻轉了 n 次,是以時間複雜度是O(2n),去掉常數項,即為 O(n)。注:這題時間複雜度比較誤認為是O(k * n),實際上并不是每一次連結清單的循環都會翻轉連結清單,隻是在循環連結清單元素每 k 個結點的時候才會翻轉

變形題 3:變形 2 針對的是順序的 k 個一組翻轉,那如何逆序 k 個一組進行翻轉呢

例如:給定如下連結清單, head–>1–>2–>3–>4–>5 逆序 k 個一組翻轉後,連結清單變成

head–>1—>3–>2–>5–>4 (k = 2 時)

這道題是位元組跳動的面試題,确實夠變态的,順序 k 個一組翻轉都已經屬于 hard 級别了,逆序 k 個一組翻轉更是屬于 super hard 級别了,不過其實有了之前知識的鋪墊,應該不難,隻是稍微變形了一下,隻要對連結清單做如下變形即可

連結清單翻轉的變形 | 算法必看系列八連結清單翻轉的變形實戰

代碼的每一步其實都是用了我們之前實作好的函數,是以我們之前做的每一步都是有伏筆的哦!就是為了解決位元組跳動這道終極面試題!

/**
 * 逆序每 k 個一組翻轉連結清單
 * @param k
 */
public void reverseIterationInvertLinkedListEveryK(int k) {
    // 先翻轉連結清單
    iterationInvertLinkedList();
    // k 個一組翻轉連結清單
    iterationInvertLinkedListEveryK(k);
    // 再次翻轉連結清單
    iterationInvertLinkedList();
}           

由此可見,掌握基本的連結清單翻轉非常重要!難題多是在此基礎了做了相應的變形而已

總結

本文詳細講解了連結清單與數組的本質差別,相信大家對兩者的差別應該有了比較深刻的認識,尤其是程式局部性原理,相信大家看了應該會眼前一亮,之後通過對連結清單的翻轉由淺入深地介紹,相信之後的連結清單翻轉對大家應該不是什麼難事了,不過建議大家親自實作一遍文中的代碼哦,這樣印象會更深刻一些!

有時候看起來思路是這麼一回事,但真正操作起來還是會有不少坑,紙上得來終覺淺,絕知此事要躬行!

來源 | 五分鐘學算法

作者 | 程式員小吳

繼續閱讀