Merge K Sorted Lists
Interview Prep
The problem
Given a list of k sorted linked lists, merge them into
one sorted linked list. Reuse the existing nodes.
class Node:
def __init__(self, val: int, next: 'Node | None' = None):
self.val = val
self.next = next Why "merge them pairwise in a loop" is wasteful
Concatenating list 0 with list 1, then merging that with list 2,
then list 3 — each merge revisits every node of the result so far.
If the lists are balanced in size, this is
O(k · N) where N is the total node count.
For k = N (lots of small lists), that's quadratic. Two
standard fixes get to O(N log k): a min-heap, or
divide-and-conquer pairwise merging.
Pattern: k-way merge with a min-heap
The classic external-sort primitive. At any moment, the smallest
unused element across all k lists is the head of one
of them. A min-heap of size k tracks those heads;
extract the min, append it to the output, push its successor. Each
of the N nodes participates in one push and one pop:
O(N log k).
A subtle gotcha: heaps compare tuples element-by-element. If two
heads have equal val, Python will try to compare the
Node objects, which aren't comparable. Use the list
index i (or any unique tiebreaker) as the second tuple
element.
Heap solution
import heapq
def merge_k(lists: list['Node | None']) -> 'Node | None':
"""Min-heap of (val, tiebreaker, node). O(N log k)."""
heap = []
for i, head in enumerate(lists):
if head:
heapq.heappush(heap, (head.val, i, head)) # i breaks ties on equal vals
dummy = Node(0)
tail = dummy
while heap:
val, i, node = heapq.heappop(heap)
tail.next = node
tail = node
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
tail.next = None
return dummy.next Trace
lists = [1->4->5, 1->3->4, 2->6]
heap initial: [(1, 0, A1), (1, 1, B1), (2, 2, C1)]
pop (1, 0): tail -> 1. push (4, 0, A2). heap: [(1,1,B1),(2,2,C1),(4,0,A2)]
pop (1, 1): tail -> 1. push (3, 1, B2). heap: [(2,2,C1),(3,1,B2),(4,0,A2)]
pop (2, 2): tail -> 2. push (6, 2, C2). heap: [(3,1,B2),(4,0,A2),(6,2,C2)]
pop (3, 1): tail -> 3. push (4, 1, B3). heap: [(4,0,A2),(4,1,B3),(6,2,C2)]
pop (4, 0): tail -> 4. push (5, 0, A3). heap: [(4,1,B3),(5,0,A3),(6,2,C2)]
pop (4, 1): tail -> 4. B has no next. heap: [(5,0,A3),(6,2,C2)]
pop (5, 0): tail -> 5. A has no next. heap: [(6,2,C2)]
pop (6, 2): tail -> 6. C has no next. heap: []
result: 1->1->2->3->4->4->5->6 Divide-and-conquer alternative
Pair up the lists, merge each pair (using the standard
two-list merge), then repeat on the halved set. After
log k rounds, one list remains. Each node is touched
in O(log k) rounds × O(1) per touch =
O(N log k). Same complexity as the heap, no heap
library required.
def merge_two(a, b):
dummy = Node(0); tail = dummy
while a and b:
if a.val <= b.val:
tail.next, a = a, a.next
else:
tail.next, b = b, b.next
tail = tail.next
tail.next = a or b
return dummy.next
def merge_k_divide(lists):
"""Pairwise merge in O(N log k) via divide-and-conquer."""
if not lists: return None
while len(lists) > 1:
merged = []
for i in range(0, len(lists), 2):
a = lists[i]
b = lists[i + 1] if i + 1 < len(lists) else None
merged.append(merge_two(a, b))
lists = merged
return lists[0] Complexity
- Time:
O(N log k)for either solution, whereNis the total node count. - Space:
O(k)for the heap,O(1)extra (besides recursion) for divide-and-conquer.
Variations worth knowing
- Merge k sorted arrays: identical algorithm,
heap holds
(value, list_index, position_in_list). Foundational for external sorting. - Smallest range covering all k lists: at each
step, the heap holds the current pointer into each list. Track
max in the window; the range is
(min, max). Advance the smallest, update. Famous Google interview question. - Find the kth smallest across k sorted arrays:
same heap; stop after popping
kelements. - Streaming top-k: the reverse — instead of merging
sorted lists, maintain a heap of size
kwhile streaming arbitrary values. Same data structure, different direction.