【鏈表】LeetCode 148. 排序鏈表【中等】

給你鏈表的頭結點 head ,請將其按 升序 排列並返回 排序後的鏈表 。

示例1:

輸入:head = [4,2,1,3]
輸出:[1,2,3,4]

 

 示例2:

輸入:head = [-1,5,3,4,0]
輸出:[-1,0,3,4,5]

 

示例3:

輸入:head = []
輸出:[] 

提示:

  • 鏈表中節點的數目在範圍 [0, 5 * 104] 內
  • -105 <= Node.val <= 105

進階:你可以在 O(n log n) 時間複雜度和常數級空間複雜度下,對鏈表進行排序嗎?

【分析】

方法一:歸併排序(遞歸法)

題目要求時間空間複雜度分別爲O(nlogn)和O(1),根據時間複雜度我們自然想到二分法,從而聯想到歸併排序;

對數組做歸併排序的空間複雜度爲O(n),分別由新開闢數組O(n)和遞歸函數調用O(logn)組成,而根據鏈表特性:

  數組額外空間:鏈表可以通過修改引用來更改節點順序,無需像數組一樣開闢額外空間;

  遞歸額外空間:遞歸調用函數將帶來O(logn)的空間複雜度,因此若希望達到O(1)空間複雜度,則不能使用遞歸。

通過遞歸實現鏈表歸併排序,有以下兩個環節:

(1)分割cut環節:找到當前鏈表中點,並從中點將鏈表斷開(以便在下次遞歸cut時,鏈表片段擁有正確邊界)

  我們使用fast,slow快慢指針法,奇數個節點找到中點,偶數個節點找到中心左邊的節點;

  找到中點slow後,執行slow.next = None將鏈表切斷;

  遞歸分割時,輸入當前鏈表左端點head和中心點slow的下一個節點tmp(因爲鏈表是從slow切斷的);

  cut遞歸終止條件:當head.next == None時,說明只有一個節點了,直接返回此節點。

(2)合併merge環節:將兩個排序鏈表合併,轉化爲一個排序鏈表。

  雙指針法合併,建立輔助ListNode h作爲頭部

  設置兩指針left,right分別指向兩個鏈表頭部,比較兩個指針處節點值大小,由小到大加入合併鏈表頭部,指針交替前進,直至添加完兩個鏈表;

  返回輔助ListNode h作爲頭部的下個節點h.next;

  時間複雜度:O(l+r),l,r 分別代表兩個鏈表的長度。

當題目輸入的head == None時,直接返回None。

 

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def sortList(self, head: ListNode) -> ListNode:
        if not head or not head.next: return head # termination.
        # cut the LinkedList at the mid index.
        slow, fast = head, head.next
        while fast and fast.next:
            fast, slow = fast.next.next, slow.next
        mid, slow.next = slow.next, None # save and cut.
        # recursive for cutting.
        left, right = self.sortList(head), self.sortList(mid)
        # merge `left` and `right` linked list and return it.
        h = res = ListNode(0)
        while left and right:
            if left.val < right.val: h.next, left = left, left.next
            else: h.next, right = right, right.next
            h = h.next
        h.next = left if left else right
        return res.next

方法二:歸併排序(從底至頂直接合並)

對於非遞歸的歸併排序,需要使用迭代的方式替換cut環節:

  我們知道,cut環節本質上是通過二分法得到鏈表最小節點單元,再通過多輪合併得到排序結果。

  每一輪合併merge操作針對的單元都固定長度intv,例如:

  (1)第一輪合併時intv = 1,即將整個鏈表切分爲多個長度爲1的單元,並按順序兩兩排序合併,合併完成的已排序單元長度爲2;

  (2)第二輪合併時,intv = 2,即將整個鏈表分爲多個長度爲2的單元,並按順序兩兩排序合併,合併完成的已排序單元長度爲4;

  (3)以此類推,直到單元長度intv >= 鏈表長度,代表已經排序完成。

  根據以上理論,我們可以僅根據intv計算每個單元的邊界,並完成鏈表的每輪排序合併,例如:

  (1)當intv = 1時,將鏈表第1和第2節點排序合併,第3和第4節點排序合併,……。

  (2)當intv = 2時,將鏈表第1-2和第3-4節點排序合併,第5-6和第7-8節點排序合併,……。

  (3)當intv = 3時,將鏈表第1-4和第5-8節點排序合併,第9-12和第13-16節點排序合併,……。

此方法時間複雜度O(nlogn),空間複雜度O(1)。

 

模擬上述的多輪排序合併:

(1)統計鏈表長度length,用於通過判斷intv < length判定是否完成排序;

(2)額外聲明一個節點res,作爲頭部後面接整個鏈表,用於:

intv *= 2即切換到下一輪合併時,可通過res.next找到鏈表頭部h;

執行排序合併時,需要一個輔助節點作爲頭部,而res則作爲鏈表頭部排序合併時的輔助頭部pre;後面的合併排序可以將上次合併排序的尾部tail用做輔助節點。

(3)在每一輪intv下的合併流程:

  a)根據intv找到合併單元1和單元2的頭部h1,h2。由於鏈表長度可能不是2n,需要考慮邊界條件:

    在找h2的過程中,如果鏈表剩餘元素個數少於intv,則無需合併環節,直接break,執行下一輪合併;

    若h2存在,但以h2爲頭部的剩餘元素個數少於intv,也執行合併環節,h2單元的長度爲c2 = intv - i。

  b)合併長度爲c1, c2的h1, h2鏈表,其中:

    合併完成後,需要修改新的合併單元的尾部pre指針指向下一個合併單元頭部h(在尋找h1, h2環節中,h指針已經被移動到下一個單元頭部)。

    合併單元尾部同時也作爲下次合併的輔助頭部pre。

  c)當h等於None時,代表此輪intv合併完成,跳出。

(4)每輪合併完成後,將單元長度*2,切換到下一輪合併:intv *= 2。

代碼看: https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/

 

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