Intuition
At every step we want the smallest of the k current heads. A min-heap holding exactly those k heads gives us that in O(log k) per pop, so all N nodes go through at O(log k) cost each.
Explanation
Setup
A min-heap stores tuples so Python compares them lexicographically: first the value, then a tiebreaker, then the node itself. We need the tiebreaker because when two values are equal Python would otherwise try to compare ListNode objects directly, which raises a TypeError (no < defined on ListNode).
Steps
- Initialise
dummy = tail = ListNode(0)andheap = []. - Seed: for each non-empty list head
nodeat positioni, push(node.val, i, node)onto the heap. - While the heap is non-empty:
- Pop the smallest tuple — this is the next node in sorted order.
- Splice it onto the tail:
tail.next = node, thentail = tail.next. - If
node.nextexists, push(node.next.val, i, node.next)onto the heap. (We reuse the sameiso future ties from the same list still have a valid tiebreaker.)
- Return
dummy.next.
Walk-through
Three lists: A = [1, 4, 5] (i=0), B = [1, 3, 4] (i=1), C = [2, 6] (i=2).
Initial heap (top is smallest): [(1, 0, A1), (1, 1, B1), (2, 2, C1)].
| step | pop | append to result | push next from list i | heap top items |
|---|---|---|---|---|
| 1 | (1, 0, A1) | 1 | (4, 0, A4) | (1, 1, B1), (2, 2, C1), (4, 0, A4) |
| 2 | (1, 1, B1) | 1 | (3, 1, B3) | (2, 2, C1), (3, 1, B3), (4, 0, A4) |
| 3 | (2, 2, C1) | 2 | (6, 2, C6) | (3, 1, B3), (4, 0, A4), (6, 2, C6) |
| 4 | (3, 1, B3) | 3 | (4, 1, B4) | (4, 0, A4), (4, 1, B4), (6, 2, C6) |
| 5 | (4, 0, A4) | 4 | (5, 0, A5) | (4, 1, B4), (5, 0, A5), (6, 2, C6) |
| 6 | (4, 1, B4) | 4 | (B has no next) | (5, 0, A5), (6, 2, C6) |
| 7 | (5, 0, A5) | 5 | (A has no next) | (6, 2, C6) |
| 8 | (6, 2, C6) | 6 | (C has no next) | empty |
Result: 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6. ✓
Why we cap heap size at k
We only push the next node from a list after popping a node from that list. So at any moment the heap holds at most one node per list — at most k entries total. That's where the O(log k) per operation comes from.
Edge cases
- Some lists empty (
None): the seeding step skips them. The heap may start with fewer thankentries; that's fine. - All lists empty: the heap stays empty, the loop never runs, return
dummy.next = None. - Equal values across lists: the
itiebreaker keepsheapqfrom comparingListNodeobjects.
Complexity
- Time:
O(N log k)— each ofNnodes is pushed once and popped once, each operation isO(log k). - Space:
O(k)— the heap.
Implementation
import java.util.*;
class Solution {
static class ListNode {
int val;
ListNode next;
ListNode(int v) { val = v; }
}
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
ListNode[] nodes = new ListNode[lists.length];
for (int i = 0; i < lists.length; i++) {
if (lists[i] != null) {
nodes[i] = lists[i];
heap.offer(new int[]{lists[i].val, i});
}
}
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while (!heap.isEmpty()) {
int[] top = heap.poll();
int idx = top[1];
tail.next = nodes[idx];
tail = tail.next;
if (nodes[idx].next != null) {
nodes[idx] = nodes[idx].next;
heap.offer(new int[]{nodes[idx].val, idx});
}
}
return dummy.next;
}
}