Topicsβ€ΊLinked Listβ€ΊMerge K Sorted Lists
πŸ“–

Merge K Sorted Lists

Linked List
~20 min read
5 practice problems

Overview

Merge k sorted lists means combining k sorted linked lists into one sorted list. Two main approaches: (1) Min-heap (priority queue): repeatedly take the smallest head among the k lists, append it to the result, and push the next node from that list into the heap. O(N log k) time where N is total nodes and k is the number of lists. (2) Divide and conquer (merge pairs): merge list 0 with 1, 2 with 3, etc., then merge the results in pairs until one list remains. O(N log k) as well. Both use the same Merge Two Sorted as a building block.

Key idea: The current minimum over all k heads is always at one of the heads; a min-heap lets you get that minimum and advance in O(log k). Alternatively, pairwise merging reduces k to 1 in O(log k) rounds, each round doing O(N) work.


When to Use

  • Merge k sorted lists β€” Exact problem.
  • External merge sort β€” K-way merge of sorted runs.

How It Works

Min-heap approach:

  1. Put the head of each non-empty list into a min-heap (compare by node value; you need to store the node or (value, list index) and be able to get the next node from that list).
  2. While the heap is not empty: pop the smallest node, append it to the result (dummy + tail), and if that node has a next, push next into the heap.
  3. Return dummy.next.

Divide and conquer:

  1. While there is more than one list: merge lists in pairs (list[i] with list[i+1]), store results, then replace the array with the merged results; repeat until one list remains.
  2. Return that list.

Example 1: Merge K Lists with Min-Heap

Problem: You are given an array of k linked lists, each sorted in ascending order. Merge all into one sorted list.

function mergeKLists(lists: (ListNode | null)[]): ListNode | null {
  const heap: ListNode[] = [];
  const cmp = (a: ListNode, b: ListNode) => a.val - b.val;
  for (const head of lists) {
    if (head !== null) heap.push(head);
  }
  if (heap.length === 0) return null;
  heap.sort(cmp);
  const dummy = new ListNode(-1);
  let tail = dummy;
  while (heap.length > 0) {
    const node = heap.shift()!;
    tail.next = node;
    tail = tail.next;
    if (node.next !== null) {
      heap.push(node.next);
      heap.sort(cmp);
    }
  }
  tail.next = null;
  return dummy.next;
}
  • Using array + sort each time gives O(N k) per extraction; a real min-heap gives O(log k). For a proper O(N log k) implementation use a priority queue that supports insert and extractMin in O(log size).

Example 2: Merge K Lists with Divide and Conquer

Problem: Same; use pairwise merge.

function mergeKLists(lists: (ListNode | null)[]): ListNode | null {
  if (lists.length === 0) return null;
  let arr = lists.filter((l) => l !== null);
  while (arr.length > 1) {
    const next: (ListNode | null)[] = [];
    for (let i = 0; i < arr.length; i += 2) {
      const a = arr[i];
      const b = i + 1 < arr.length ? arr[i + 1] : null;
      next.push(mergeTwo(a!, b));
    }
    arr = next;
  }
  return arr[0] ?? null;
}
function mergeTwo(l1: ListNode | null, l2: ListNode | null): ListNode | null {
  const dummy = new ListNode(-1);
  let tail = dummy;
  while (l1 && l2) {
    if (l1.val <= l2.val) { tail.next = l1; l1 = l1.next; }
    else { tail.next = l2; l2 = l2.next; }
    tail = tail.next;
  }
  tail.next = l1 ?? l2;
  return dummy.next;
}
  • Time: O(N log k). Space: O(1) for merge (stack O(log k) for recursion if implemented recursively).

Edge Cases

  • Empty array β€” Return null.
  • All lists null/empty β€” Return null.
  • Single list β€” Return that list.
  • One list long, others short β€” Heap stays small; pairwise merge also handles.

Common Mistakes

  1. Heap comparison β€” Compare by node value; when storing nodes, ensure you don't mutate the same node from two places.
  2. Pushing next β€” After appending a node to the result, push node.next (if not null) into the heap so that list continues to contribute.
  3. Tail.next = null β€” In heap approach, the last node appended might have a stale .next from its original list; set tail.next = null at the end to avoid cycles.

Time and Space Complexity

  • Time: O(N log k) for both heap and divide-and-conquer.
  • Space: O(k) for heap; O(1) extra for pairwise merge (excluding recursion).

Summary