HARD Lab · Linked List · 56

23. Merge k Sorted Lists

Given k sorted linked lists, merge them into one sorted list. The key insight is that you can reduce k lists to one in O(log k) rounds by repeatedly merging pairs — the same divide-and-conquer that powers merge sort.

HardDivide & ConquerHeapMergeTypeScript

PROBLEM What we're solving

You receive an array of k sorted linked lists. Return one sorted linked list containing all nodes. Example: [[1,4,5], [1,3,4], [2,6]] [1,1,2,3,4,4,5,6]. You see this in database merge phases, external sort, and anywhere you must unify pre-sorted streams.

KEY IDEA Shrink k each round — not one node at a time

Insight → merging two sorted lists costs O(n). If you naively add one list at a time you do k merges totalling O(N·k). Instead, merge pairs in parallel: after one round you have ⌈k/2⌉ lists; after log₂k rounds you have 1. Total work: O(N log k) — the same asymptotic savings that make merge sort beat insertion sort.

RECIPE Pairwise merge rounds

  • 0 · Base case. Empty array → null. Single list → return it directly; no merging needed.
  • 1 · Round loop. While more than one list remains, collect results of merging pairs into a new array. An odd list out is carried unchanged.
  • 2 · mergeTwoLists. Classic two-pointer merge with a dummy head: compare front nodes, advance the smaller pointer, attach to tail. O(m+n).
  • 3 · Repeat. Replace the working array with the smaller merged array until length is 1. Return that element.
Classic confusion → people try to merge into the firstlist sequentially: list0 ← merge(list0, list1), then list0 ← merge(list0, list2), … This is O(N·k) because the growing result list is re-scanned every time. Pairwise merging avoids that by keeping each merge balanced.

COST Complexity & the heap alternative

Merge one-by-one (sequential)
O(N·k)
Each of k merges re-scans the growing result.
Divide & conquer / min-heap
O(N log k)
log k rounds; O(k) heap space.

Min-heap approach

Insert the head of every list into a min-heap keyed by node.val. Pop the minimum, attach it to the result, push that node's next (if any). Also O(N log k) — same asymptotic complexity but different constant factors. The heap approach is easier to extend to a streaming setting; the pairwise approach needs all lists up front and is usually simpler to implement correctly in an interview.

Pattern transfer → the same divide-and-conquer merging drives Sort List (LC 148), Count of Smaller Numbers After Self (merge-sort variant), and any external merge-sort pipeline. The min-heap approach transfers to Kth Smallest Element in a Sorted Matrix and Find K Pairs with Smallest Sums.

RUN IT Divide & conquer — merge pairs each round

step 0 / 8
STARTStart with 3 sorted lists. We will repeatedly merge pairs (divide & conquer) until one list remains.
[1→4→5]L0[1→3→4]L1[2→6]L2
State
0
round
3
lists remaining
pair being mergedmerged resultcarried (odd list)round counter
slowfast

TYPESCRIPT The solution, annotated

mergeKLists.ts
// Definition for singly-linked list.
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val = 0, next: ListNode | null = null) {
    this.val = val;
    this.next = next;
  }
}

// --- Approach 1: Divide & Conquer (matches the visualizer) ---
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
  if (lists.length === 0) return null;

  // Repeatedly halve the list of lists, merging pairs each round
  let current = lists.slice();
  while (current.length > 1) {
    const next: Array<ListNode | null> = [];
    for (let i = 0; i < current.length; i += 2) {
      if (i + 1 < current.length) {
        next.push(mergeTwoLists(current[i], current[i + 1]));
      } else {
        next.push(current[i]); // odd list — carry it over
      }
    }
    current = next;
  }
  return current[0];
}

function mergeTwoLists(
  a: ListNode | null,
  b: ListNode | null,
): ListNode | null {
  const dummy = new ListNode(0);
  let tail = dummy;
  while (a !== null && b !== null) {
    if (a.val <= b.val) { tail.next = a; a = a.next; }
    else                 { tail.next = b; b = b.next; }
    tail = tail.next!;
  }
  tail.next = a ?? b;
  return dummy.next;
}

// --- Approach 2: Min-Heap (O(N log k)) ---
// import { MinHeap } from 'some-heap-library'; // or implement manually
//
// function mergeKListsHeap(lists: Array<ListNode | null>): ListNode | null {
//   const heap = new MinHeap<ListNode>((a, b) => a.val - b.val);
//   for (const head of lists) if (head) heap.push(head);
//   const dummy = new ListNode(0);
//   let tail = dummy;
//   while (!heap.isEmpty()) {
//     const node = heap.pop()!;
//     tail.next = node;
//     tail = tail.next;
//     if (node.next) heap.push(node.next);
//   }
//   return dummy.next;
// }

Reading it block by block

Lines 15–16 — guard clauses. Empty input returns null; a single list is already merged. These let the main loop assume ≥ 2 lists exist at the start of each iteration.
Lines 18–29 — divide-and-conquer round loop. current holds the working list of lists. Each iteration walks it in steps of 2, merging adjacent pairs into next. An odd-indexed tail element (i + 1 >= current.length) is carried forward without merging — it will find a partner in the next round.
Lines 32–43 — mergeTwoLists. Standard two-pointer sentinel-node merge. A dummy head eliminates the special-case for the first node. tail.next = a ?? b appends whichever list still has nodes. O(m + n) time, O(1) extra space.
Approach 2 sketch (lines 46–58) — min-heap. Seed the heap with all k heads, then greedily pop the minimum node and push its successor. Both approaches are O(N log k); the heap requires O(k) auxiliary space while pairwise is O(log k) stack frames or O(k) for the temporary arrays.
Complexity → O(N log k) time — N total nodes, log k rounds of merging. O(k) extra space for the temporary arrays each round (or O(log k) if done recursively). The mergeTwoLists inner loop looks like it might be O(N²) but each node is touched in exactly one merge per round, and there are log k rounds, giving O(N log k) total.

INTERVIEWFollow-ups they'll ask

  • "Can you do it with O(1) extra space?" Not trivially — the pairwise approach needs temporary arrays. A true in-place merge of linked lists is significantly more complex; in practice the O(k) space is accepted.
  • "What if lists arrive as a stream (online)?" Switch to the min-heap approach: maintain a heap of k current heads and emit the minimum as each new element arrives.
  • "Implement without recursion." The iterative pairwise loop is already non-recursive. If you wrote the recursive divide-and-conquer version, describe converting it to an iterative bottom-up pass (what the loop above does).
  • "Return the kth node of the merged list." Merge normally, then walk k−1 steps. Or use a heap and stop after k pops — O(k log k) without building the whole list.
  • "What if k is very large (millions of lists)?" The min-heap stays at O(k) memory but k itself becomes the bottleneck. Consider a tournament tree or chunked parallel merge.

MNEMONIC The one-liner

"Pair them up, merge, halve the count — log k rounds and you're done."

TRIGGERS When you see ___ → reach for ___

merge k sorted lists / streamspairwise divide & conquer or min-heap
O(N log k) over O(N·k)pair-merge rounds (like merge sort)
streaming / online merge of k sourcesmin-heap of size k
dummy head node in linked listsentinel for clean tail attachment

SKELETON The reusable shape

skeleton.ts
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
  if (lists.length === 0) return null;
  let current = lists.slice();
  while (current.length > 1) {
    const next: Array<ListNode | null> = [];
    for (let i = 0; i < current.length; i += 2) {
      next.push(i + 1 < current.length
        ? mergeTwoLists(current[i], current[i + 1])
        : current[i]);
    }
    current = next;
  }
  return current[0];
}

FLASHCARDS Tap to flip

Why is sequential merging O(N·k)?
Each of the k merges re-scans the entire growing result list, which grows by n each time.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the time complexity of the pairwise divide-and-conquer approach?
QUESTION 02
You merge k=4 lists sequentially (append list1 into list0, then list2, then list3). What is the complexity?
QUESTION 03
Trace: mergeKLists([[1,4,5],[1,3,4],[2,6]]). What is the output?
QUESTION 04
What is the purpose of the dummy (sentinel) head node in mergeTwoLists?
QUESTION 05
How many rounds of pairwise merging does k=8 lists require?
QUESTION 06
An odd list (no partner in the current round) is:
QUESTION 07
Which approach is naturally suited to a streaming / online setting where lists arrive one at a time?