演算法圖鑑
Linked List · 05 / 05

Merge Lists合併串列

合併兩條有序串列,K 條時用堆積

用在:合併排序的核心、合併多個有序資料流

時間複雜度O(n+m)
空間複雜度O(1)
難度進階
前置知識Singly Linked List、Fast & Slow Pointers、Recursion

01為什麼需要它

合併多個已排序的日誌檔

每台伺服器的 log 各自依時間排好,要合成一份總的時間序。把全部倒進陣列再排序是 O(N log N),而且要先讀進記憶體。

為什麼用它每條都有序,只要反覆比較各條的「目前最前面那筆」、取最小的。兩條是 O(n + m),k 條用堆積是 O(N log k),而且可以串流處理。這是外部排序與 log 聚合系統的核心。

合併排序的最後一步

合併排序把資料切成兩半各自排好,最後要「把兩段有序的合成一段」。在串列上做這件事不用額外空間。

為什麼用它串列的合併只改指標、不搬資料,所以串列版合併排序是 O(n log n) 時間、O(log n) 空間,比陣列版省。LeetCode 148 Sort List 就是這題。

資料庫的 merge join

兩張表都依 join key 排好序,要找出 key 相同的配對。

為什麼用它同樣是雙指標同時往前走:誰小誰前進,相等就輸出。和合併串列是同一個骨架,只是「輸出」的動作不同。

看到這些關鍵字就想到它:兩條(或 k 條)已排序、合併、取最小的那個、合併排序、dummy + tail、多路歸併。

02核心概念

兩條已排序的串列要合成一條有序的,只需要一個觀察:整體最小的一定是兩條的頭之一。取走比較小的那個頭,剩下的仍然是兩條有序串列,重複同樣的事。這就是合併(merge)。每個節點只被比較一次、接上一次,O(n + m)。

寫法上用 dummy 節點當結果的起點、tail 指向結果的最後一個節點。每一輪把較小的頭接到 tail.next、tail 前進、那條串列的頭也前進。其中一條用完時,另一條剩下的部分本來就是接好的,直接把 tail.next 指過去,不用再一個一個接。不建新節點,只改指標,額外空間 O(1)。

合併 k 條時,每輪要在 k 個頭裡挑最小的。用最小堆積維護這 k 個頭,取出最小 O(log k)、把它的下一個放回去 O(log k),總共 O(N log k)。另一種寫法是兩兩合併、像錦標賽一樣分治,複雜度相同。

反過來,把合併當作零件就得到串列版合併排序:快慢指標找中點切半,遞迴排好兩半,再合併。這是串列排序的標準解,也把前面三篇(快慢指標、遞迴、合併)串在一起。

03演算法步驟

  1. 1dummytail = dummy。dummy 讓第一個節點的接法和後面的一樣。
  2. 2while a and b:比較 a.valb.val,把較小的接到 tail.next,那條串列的指標前進,tail = tail.next。相等時取 a,結果才是穩定的。
  3. 3迴圈結束後 tail.next = a or b,把還沒用完的那條整段接上。
  4. 4回傳 dummy.next,不是 dummy。
  5. 5k 條時把每條的頭放進最小堆積(Python 要加索引當 tie-breaker),每次 pop 最小的接上,再 push 它的 next。

04互動示範

逐步看兩條串列怎麼合併:每一步比較兩個頭,較小的接到結果尾端;一條用完後,另一條剩下的整段直接接上。

A
1
i
3
5
8
B
2
j
3
7
結果dummy
步驟 0/8準備一個 dummy 節點當結果的起點,tail 指著它。i、j 分別指向兩條串列的頭。

05程式碼

迭代版與遞迴版合併兩條、用堆積合併 k 條、以及把合併當零件的串列版合併排序。

import heapq

# 合併兩條有序串列:dummy + tail,每次接上比較小的那個。O(n + m)
def merge_two(a, b):
    dummy = tail = ListNode(0)
    while a and b:
        if a.val <= b.val:          # 相等時取 a,保持穩定
            tail.next, a = a, a.next
        else:
            tail.next, b = b, b.next
        tail = tail.next
    tail.next = a or b              # 剩下的那段直接接上
    return dummy.next


# 遞迴版:merge(a, b) = 較小的那個節點 + merge(剩下的)
def merge_two_rec(a, b):
    if not a: return b
    if not b: return a
    if a.val <= b.val:
        a.next = merge_two_rec(a.next, b)
        return a
    b.next = merge_two_rec(a, b.next)
    return b


# 合併 k 條:最小堆積存每條的頭,每次取最小的。O(N log k)
def merge_k(lists):
    heap = []
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))   # i 避免比較 node
    dummy = tail = ListNode(0)
    while heap:
        _, i, node = heapq.heappop(heap)
        tail.next = tail = node
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
    return dummy.next


# 串列版合併排序:快慢指標切半,遞迴排兩半,再合併。O(n log n)、O(log n) 堆疊
def sort_list(head):
    if not head or not head.next:
        return head
    slow, fast = head, head.next
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
    right, slow.next = slow.next, None      # 從中間切開
    return merge_two(sort_list(head), sort_list(right))

06練習題

  • LeetCode 21Merge Two Sorted ListsEasy
  • LeetCode 88Merge Sorted Array(陣列版,從後面往前填)Easy
  • LeetCode 148Sort List(串列版合併排序)Medium
  • LeetCode 23Merge k Sorted Lists(堆積)Hard
  • LeetCode 2Add Two Numbers(雙指標同時走的變形)Medium