反轉鏈表系列問題

反轉鏈表系列問題

作者:Grey

原文地址:

博客園:反轉鏈表系列問題

CSDN:反轉鏈表系列問題

反轉單鏈表

題目描述見:LeetCode 206. Reverse Linked List

思路如下

對於任何一個節點 cur 來說,記錄一個前驅節點 pre (第一個節點的前驅節點是 null )

先用一個臨時節點 tmp 記錄 cur 的下一個節點,然後設置

cur.next = pre;
pre = cur;
cur = tmp;

以下是示例圖

假設原始鏈表如下

image

第一個節點的反轉流程如下

image

第二個節點的反轉流程如下

image

最後返回 pre 節點即爲反轉後的節點。

代碼如下

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode tmp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
}

時間複雜度\(O(N)\),空間複雜度\(O(1)\)

反轉鏈表也可以用遞歸方法來實現

定義遞歸函數 ListNode reverse(ListNode cur),這個遞歸函數的含義是

反轉以 cur 爲頭的鏈表,並把反轉後的頭節點返回。

這個遞歸函數的 base case 是,只有一個節點的時候,即

if (cur == null || cur.next == null) {
    return cur;
}

這種情況下,直接返回當前節點即可。

接下來是普遍情況:

image

當前來到 cur 節點,c,d,e 已經完成了反轉。

此時 cur 需要做如下操作:把 c , d, e 反轉後的頭節點獲取到,假設爲 pre , 在上圖中,pre 就是 e 節點,然後 cur 再做如下操作

cur.next.next = cur;
cur.next = null;

image

image

其中cur.next = null非常重要,只有這樣,才能防止出現環。完整代碼如下

class Solution {
    public ListNode reverseList(ListNode cur) {
        return reverse(cur);
    }

    // 反轉cur爲頭的鏈表,並把反轉後的頭節點返回
    public ListNode reverse(ListNode cur) {
        if (cur == null || cur.next == null) {
            return cur;
        }
        ListNode pre = reverse(cur.next);
        cur.next.next = cur;
        cur.next = null;
        return pre;
    }
}

時間複雜度\(O(N)\)

空間複雜度\(O(N)\)(遞歸棧佔用的空間)

反轉雙向鏈表

雙向鏈表和單鏈表的反轉類似,每個節點要多處理一次每個節點的前驅指針,

完整代碼如下

    public static DoubleNode reverseDoubleList(DoubleNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        DoubleNode pre = null;
        DoubleNode cur = head;
        while (cur != null) {
            DoubleNode tmp = cur.next;
            cur.next = pre;
            cur.last = tmp;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }

反轉單鏈表一部分

題目描述見:LeetCode 92. Reverse Linked List II

本題核心依然是反轉鏈表,只是增加了一些變量來記錄需要反轉鏈表的頭位置和結尾位置,不過需要注意的是,本題的鏈表開始位置是從 1 開始,所以,如果m = 1 && n != 1,說明反轉鏈表後需要返回新的頭部,只要m > 1,反轉鏈表一部分以後,返回原先的頭即可。

此外,本題的 follow up 提到

Could you do it in one pass?

就是遍歷一次鏈表能否解決問題,具體代碼如下

class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        if (head.next == null || left == right) {
            // 只有一個節點,怎麼反轉都一樣
            // 只要反轉一個節點,反轉前後鏈表還是一樣的
            return head;
        }
        if (left == 1) {
            // 需要換頭
            ListNode pre = null;
            ListNode end = head;
            ListNode cur = head;
            int gap = right - left + 1;
            while (gap != 0) {
                ListNode tmp = cur.next;
                cur.next = pre;
                pre = cur;
                cur = tmp;
                gap--;
            }
            end.next = cur;
            return pre;
        } else {
            ListNode pre = null;
            for (int i = 1; i < left; i++) {
                pre = pre == null ? head : pre.next;
            }
            ListNode end = pre;
            ListNode cur = pre == null ? head : pre.next;
            ListNode last = cur;
            int gap = right - left + 1;
            while (gap != 0) {
                ListNode tmp = cur.next;
                cur.next = pre;
                pre = cur;
                cur = tmp;
                gap--;
            }

            if (end != null) end.next = pre;
            if (last != null) last.next = cur;
            // 不需要換頭,返回原先的頭節點
            return head;
        }
    }
}

整個過程只遍歷一遍鏈表,時間複雜度\(O(N)\),空間複雜度\(O(1)\)

本題也可以利用遞歸方式解,主函數可以直接定義爲遞歸函數

public ListNode reverseBetween(ListNode head, int left, int right)

該函數表示反轉 leftright區間內的鏈表,並返回反轉後的最終鏈表的頭節點。

然後定義一個輔助函數(這個函數也是遞歸實現)

ListNode reverseN(ListNode head, int n)

該函數表示:反轉以 head 爲頭的鏈表的前 n 個節點,並返回反轉後的鏈表頭節點。

left == 1的條件下,主函數直接調用reverseN(head, right)即可。

即:

    public static ListNode reverseBetween(ListNode head, int left, int right) {
        if (left == 1) {
            // 相當於直接反轉前 right 個元素
            return reverseN(head, right);
        }
        // ...
    }

接下來是普遍情況:要實現 leftright 區間內的鏈表反轉,可以先考慮以head.next爲頭的對應區間的鏈表反轉

如下圖
image

然後將head.next 指向反轉後的節點即可

image

接下來是reverseN方法的實現,這個函數也是遞歸函數,表示反轉以head爲頭的前n個節點,並返回反轉後的頭節點,當n == 1時,返回head,接下來是普遍情況,反轉以head.next爲頭的前n - 1個節點,如下圖

image

反轉後的鏈表頭節點假設是last

image

這個last節點就是整個鏈表反轉前n個元素後的頭節點,然後將head節點進行最後一次反轉

head.next.next = head;

image

最後需要將heade節點連接起來,所以遞歸過程中,需要一直記錄反轉的部分鏈表的後繼節點,設置一個全局變量

static ListNode successor = null;

head指向successor節點

image

完成了前n個節點的反轉,完整代碼如下:

    // 遞歸解法
    public static ListNode reverseBetween(ListNode head, int left, int right) {
        if (left == 1) {
            return reverseN(head, right);
        }
        head.next = reverseBetween(head.next, left - 1, right - 1);
        return head;
    }

    // 反轉鏈表前N個節點
    public static ListNode reverseN(ListNode head, int n) {
        if (n == 1) {
            successor = head.next;
            return head;
        }
        ListNode last = reverseN(head.next, n - 1);
        head.next.next = head;
        head.next = successor;
        return last;
    }

    static ListNode successor = null;

更多

算法和數據結構學習筆記

算法和數據結構學習代碼

參考資料

算法和數據結構體系班-左程雲

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章