Coding Interview Patterns

Course Content

Coding Interview Patterns

20 sections · 146 lessons

Merge k Sorted Lists


This is the classic "one entry per source" heap problem, and it is rated hard mostly because of one detail: equal values. Get the tie-breaker right and the rest is a short loop.

The merged list, one smallest head at a time12345699nullfrom list 1from list 09 fromlist 09 fromlist 1Two heads tie at 9; the list index decides the order, so two nodes are never compared.
The next node is always the smallest current head, so a heap of k heads replaces walking the merged list again.

The problem

You are given k linked lists. Each list is already sorted in ascending order. Merge them all into one sorted linked list and return its head. Let N be the total number of nodes across all lists.

  • Lists 2 → 6 → 9, 1 → 4 → 9, 3 → 5 → 1 → 2 → 3 → 4 → 5 → 6 → 9 → 9.
  • An empty input, or k lists that are all empty → an empty list (None).

Constraints: 0 ≤ k ≤ 10⁴, each list has up to 500 nodes, N ≤ 10⁴, values between -10⁴ and 10⁴.

Clarifying questions

  • May I reuse the existing nodes? Yes; relinking them is expected and saves memory.
  • Can a list be empty (None)? Yes, any of them.
  • Are values unique? No, duplicates happen, within a list and across lists.

Approach 1: collect and sort, or merge one list at a time

The quickest correct answer copies every value out, sorts, and builds a new list: O(N log N) time and O(N) extra space. It ignores the fact that each list is already sorted.

A second simple answer reuses the two-list merge from the Linked Lists section and folds the lists in one at a time:

Python
from typing import Optionalclass ListNode:    def __init__(self, val: int, next: Optional["ListNode"] = None) -> None:        self.val = val        self.next = nextdef merge_two(a: Optional[ListNode], b: Optional[ListNode]) -> Optional[ListNode]:    """Standard merge of two sorted lists, reusing their nodes."""    dummy = ListNode(0)    tail = dummy    while a is not None and b is not None:        if a.val <= b.val:            tail.next, a = a, a.next        else:            tail.next, b = b, b.next        tail = tail.next    tail.next = a if a is not None else b    return dummy.nextdef merge_k_one_by_one(lists: list[Optional[ListNode]]) -> Optional[ListNode]:    """Fold the lists in one at a time: result = merge(result, next list)."""    result: Optional[ListNode] = None    for head in lists:        result = merge_two(result, head)    return result

Why it is slow: the growing result is walked again on every merge. If each list has N/k nodes, merge number i walks about i × N/k nodes. Summed over k merges that is about k × N / 2, so O(kN). With k = 10⁴ one-node lists, that is about 5 × 10⁷ node visits — too slow in Python, and almost all of it is walking the same early nodes again.

The key insight

At every moment, the next node of the output is the smallest of the k current heads. Nothing further down any list can be smaller, because every list is sorted. So you only ever need to compare k candidates, one per list.

"Give me the smallest of k changing candidates" is exactly a min-heap of size k. Pop the smallest head, attach it to the output, and push that node's successor — the new head of the same list. The heap never holds more than k entries, so each step costs O(log k) instead of walking a long list.

The one trap is ties. The heap entry must be a tuple whose first field is the value. If two heads have the same value, Python compares the next field. If that field is the node, it raises TypeError: '<' not supported between instances of 'ListNode'. So put a unique number — the list's index — between the value and the node.

Approach 2: a min-heap of heads

Python
import heapqfrom typing import Optionaldef merge_k_heap(lists: list[Optional[ListNode]]) -> Optional[ListNode]:    """Hold the current head of every list in a min-heap; always take the smallest."""    heap: list[tuple[int, int, ListNode]] = []    for index, head in enumerate(lists):        if head is not None:            heapq.heappush(heap, (head.val, index, head))   # index breaks ties    dummy = ListNode(0)    tail = dummy    while heap:        _, index, node = heapq.heappop(heap)        tail.next = node        tail = node        if node.next is not None:            heapq.heappush(heap, (node.next.val, index, node.next))    return dummy.next

Dry run on lists 0: 2 → 6 → 9, 1: 1 → 4 → 9, 2: 3 → 5. Heap entries are shown as (value, list).

Heap before popPopPush (successor)Output so far
(1,1) (2,0) (3,2)(1,1)(4,1)1
(2,0) (3,2) (4,1)(2,0)(6,0)1 2
(3,2) (4,1) (6,0)(3,2)(5,2)1 2 3
(4,1) (5,2) (6,0)(4,1)(9,1)1 2 3 4
(5,2) (6,0) (9,1)(5,2)— list 2 ends1 2 3 4 5
(6,0) (9,1)(6,0)(9,0)1 2 3 4 5 6
(9,0) (9,1)(9,0)— list 0 ends1 2 3 4 5 6 9
(9,1)(9,1)— list 1 ends1 2 3 4 5 6 9 9

The last two rows are the tie: both heads are 9, and the list index (0 before 1) decides the order without ever comparing nodes.

Complexity. Every one of the N nodes is pushed once and popped once, on a heap of at most k entries: O(N log k) time. Space O(k) for the heap; the output reuses the input nodes.

Approach 3: merge in pairs

A different way to reach O(N log k) without a heap: merge list 0 with 1, 2 with 3, and so on. That halves the number of lists. Repeat until one is left.

Python
def merge_k_pairs(lists: list[Optional[ListNode]]) -> Optional[ListNode]:    """Merge the lists in pairs, round after round, until one is left."""    if not lists:        return None    current = list(lists)    while len(current) > 1:        merged = []        for i in range(0, len(current), 2):            second = current[i + 1] if i + 1 < len(current) else None            merged.append(merge_two(current[i], second))        current = merged    return current[0]

Each round touches every node once, O(N), and there are log₂ k rounds, so the total is O(N log k) — the same as the heap. It needs O(k) space for the list of heads (or O(1) if you merge in place in the input array). The heap version is easier to extend to streams; the pairwise version has no tuple-tie trap.

Edge cases

  • lists is empty, or every list is None: the heap starts empty, the loop never runs, and dummy.next is None.
  • One list: it is returned node by node, unchanged.
  • Equal values across lists: handled by the index tie-breaker.
  • Very unequal lengths: a list that ends simply stops pushing; the others carry on.

Follow-ups

  • "The inputs are sorted arrays, not lists." Store (value, array index, position) and push position + 1 from the same array.
  • "The lists are huge files on disk." This is the external merge-sort step: the heap holds one buffered head per file, O(k) memory however large the files are.
  • "Find the smallest range that includes at least one number from each list." Keep one head per list in a min-heap and track the current maximum; the range is [heap root, current max]. Advance the list whose head was the minimum.