給你鏈表的頭結點 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/