Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Two clean O(N log k) solutions (N = total nodes, k = number of lists). (1) Min-heap: push the head of every list into a priority queue keyed by value; repeatedly pop the smallest, append it to a dummy-headed result, and push that node's next. (2) Divide-and-conquer: pair up the k lists and merge each pair with the standard two-list merge, halving the count each round until one list remains. Both beat the naïve 'merge one at a time' O(N·k) by replacing the linear scan for the minimum with either a heap or balanced pairwise merging.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can beat the naïve O(N·k) by recognising this as a k-way merge — and whether you can implement either a heap keyed correctly (comparator / tuple tiebreaker) or a clean divide-and-conquer with the two-list merge as a subroutine.
Where candidates slip
Merging lists one-by-one into an accumulator (O(N·k)), pushing whole ListNodes into a Python heap (TypeError when values tie), or forgetting to skip null/empty input lists.
How to stand out
Name both O(N log k) solutions and compare their trade-offs (heap = O(k) space, streaming-friendly; divide-and-conquer = O(log k) stack, cache-friendly). Derive the complexity out loud and reuse the two-list merge as a named helper.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewMerge k sorted listsHow to find the next smallest of k heads?Linear scanMin-heap of headsExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2 · Optimal
Min-Heap of List Heads (Optimal)
1Hint 1Explore hintHide hint
At every step the next node to output is the smallest among the current heads of the k lists. What data structure gives you the minimum of k items and lets you replace it fast?
2Hint 2Explore hintHide hint
Seed a min-heap with the head of each non-empty list. Pop the smallest, append it to a dummy-headed result, and if that node has a next, push the next into the heap.
3Hint 3Explore hintHide hint
The heap never holds more than k nodes, so each pop/push is O(log k); across all N nodes that's O(N log k). In Python push (val, index, node) so equal values don't compare ListNodes; in Java pass a comparator.
A · Core idea
The merge is driven by one repeated question: of the current front nodes of all k lists, which is smallest? A min-heap answers that in O(log k) and lets us swap in the next candidate just as cheaply.
Why a heap and not a linear scan. The naïve k-way merge scans all k heads to find the minimum each time it emits a node — O(k) per node, O(N·k) overall. A min-heap keeps the k candidate heads partially ordered so the minimum is always at the root (O(1) to read, O(log k) to remove). After we take the smallest node, the only new candidate is that node's successor in its own list, which we push in O(log k). So we replace an O(k) scan with an O(log k) heap op — that single substitution is the whole optimisation.
Why the heap size stays at k. At any instant the heap contains at most one node per list: the current head of each list that still has nodes. We pop one and push at most one (its next), so the size never grows beyond k. That's why the log factor is log k, not log N — a meaningfully tighter bound when k ≪ N.
Why the tiebreaker / comparator matters. When two heads share a value, the heap must still order its entries. In Java, ListNode isn't Comparable, so we must supply a comparator (a, b) -> Integer.compare(a.val, b.val). In Python, heapq compares tuples lexicographically; if we pushed (val, node) and two vals tied, it would fall through to comparing ListNode objects and throw TypeError. Pushing (val, index, node) with a unique index guarantees the comparison resolves on the integer index before it ever touches a node — a subtle but exam-critical detail.
Why the dummy head. As in every list-building problem, a sentinel removes the 'is this the first node?' branch. We append each popped node via curr.next = node; curr = node and return dummy.next. One more detail: the last node we append may still point into its old list, but because we always overwrite curr.next on the next append and the final node's old next was already consumed (or is null), the result is correctly terminated; defensively, you can set curr.next = null at the end.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
lists = [[1, 4, 5], [1, 3, 4], [2, 6]]Interactive diagram
Heap always surfaces the global minimum head
seedThe heap holds one live head per list; popping the root and pushing its successor keeps the size at ≤ k.
[1, 1, 2, 3, 4, 4, 5, 6]C · Code with teaching comments
D · Why it works and complexityO(N log k) timeO(k) spaceOpen the invariant and proofExplore the proofHide the proof
The heap converts 'minimum of k things, repeatedly, with cheap replacement' into a logarithmic operation — the defining use case for a priority queue. Recognising that the next output is always the minimum of the live heads is the entire leap; everything else is standard heap mechanics. The same shape appears in Smallest Range Covering Elements and Merge k sorted arrays/iterators.
ComplexityO(N log k) time, O(k) space — Each of the N nodes is pushed once and popped once. The heap size is bounded by k (one live head per list), so each operation costs O(log k). Total = N × O(log k) = O(N log k) time. The heap holds at most k entries → O(k) space (plus the O(1) dummy and pointers).
E · Edge cases and pitfalls7lists = [] — No heads pushed, heap empty, while loop skipped → return dummy.next = null.Explore edge casesHide edge cases
Edge cases handled
lists = []No heads pushed, heap empty, while loop skipped → return dummy.next = null.lists = [[]]The single head is null → skipped during seeding, heap stays empty → null result.lists = [[1], [], [0]]Push 1(L0) and 0(L2), skip the empty L1. Pop 0 then 1 → result [0, 1].
Pitfalls
- In Python, pushing (val, node) without an index — TypeError when two values tie and heapq compares ListNodes.
- In Java, forgetting the comparator — ClassCastException because ListNode isn't Comparable.
- Pushing null heads from empty lists — NullPointerException / wrong ordering.
- Not terminating the final node's next, leaving a stale link into an old list.
Approach 2 of 2
Divide and Conquer — Pairwise Merge
1Hint 1Explore hintHide hint
You already know how to merge TWO sorted lists in O(total) time. How few times do you need to apply that to collapse k lists into one?
2Hint 2Explore hintHide hint
Pair the lists up: merge lists[0]+lists[1], lists[2]+lists[3], and so on. That halves the count each round. After log k rounds only one list remains.
3Hint 3Explore hintHide hint
Implement it recursively by splitting the array in half, merging each half, then merging the two results — exactly merge sort's structure with sorted lists as the elements.
A · Core idea
Instead of a heap, we lean entirely on the two-list merge and apply it in a balanced binary-tree shape so that each node is merged through only log k levels.
Why pairing beats one-at-a-time. Merging lists sequentially into one accumulator re-walks the growing accumulator over and over: the first list's nodes get compared in every subsequent merge, giving O(N·k). Pairwise merging fixes the imbalance. In round one we merge k/2 independent pairs; in round two k/4 pairs of the results; and so on. A given node participates in one merge per round, and there are only log k rounds — so each node is touched log k times total, yielding O(N log k).
Why it's literally merge sort on lists. Classic merge sort splits an array in half, sorts each half, and merges. Here the 'elements' are already-sorted lists and the 'merge' is the two-sorted-list merge. We recurse on lists[lo..mid] and lists[mid+1..hi], then merge the two returned lists. The recursion tree has depth log k, and the merge work summed across any level is O(N) — the same accounting that gives merge sort its O(n log n).
Why the two-list merge is the heart of it. mergeTwo(a, b) walks both lists with a dummy head, always appending the smaller current node and advancing that list, then attaches whatever remains when one side empties. It runs in O(len(a) + len(b)) and O(1) extra space. Getting this primitive exactly right — the dummy head, the while a and b loop, and the curr.next = a or b tail attach — is what makes the whole divide-and-conquer correct.
Why the space profile differs from the heap. There's no auxiliary heap, so the only extra memory is the recursion stack of depth log k → O(log k). (An iterative version that repeatedly sweeps the array merging adjacent pairs uses O(1) extra.) That makes divide-and-conquer attractive when you want to avoid a heap's O(k) footprint, though both share the same time bound and reuse the same merge subroutine.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
lists = [L0=[1,4,5], L1=[1,3,4], L2=[2,6]]Interactive diagram
Balanced pairwise merge tree
splitk lists collapse in log k rounds; each round touches all N nodes once, giving O(N log k).
[1, 1, 2, 3, 4, 4, 5, 6]C · Code with teaching comments
Reference implementation previewpublic ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) { return null; } return merge(lists, 0, lists.length - 1);Explore the full codeHide the full code
D · Why it works and complexityO(N log k) timeO(log k) spaceOpen the invariant and proofExplore the proofHide the proof
Reframing 'merge k lists' as 'merge sort where the elements are sorted lists' instantly delivers the O(N log k) bound and reuses a primitive you already trust. Whenever you see 'combine k sorted things', think balanced pairwise merging or a heap — they're the two faces of k-way merging.
ComplexityO(N log k) time, O(log k) space — There are log k merge rounds. In each round every one of the N nodes is touched exactly once across all the pairwise merges, so each round is O(N). log k rounds × O(N) = O(N log k) time. Recursion depth is log k → O(log k) stack space (the iterative pairwise variant is O(1) extra).
E · Edge cases and pitfalls7lists = [] — Guard returns null immediately — no recursion on an empty range.Explore edge casesHide edge cases
Edge cases handled
lists = []Guard returns null immediately — no recursion on an empty range.lists = [[1,2,3]]Single list: lo == hi base case returns it unchanged with no merging.lists = [[], [1], []]mergeTwo treats null halves correctly — merging null with [1] just returns [1]; result [1].
Pitfalls
- Merging one list at a time into an accumulator — degrades to O(N·k).
- Recursing on an empty range without the lists.length == 0 guard — index errors.
- Using < instead of <= in mergeTwo loses stability (cosmetic here, but a habit worth keeping).
- Forgetting to attach the leftover tail (curr.next = a or b) — drops the rest of one list.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewimport java.util.PriorityQueue; public class ListNode { int val; ListNode next;Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Quadratic-ish one-at-a-time merge
Folding lists into a single accumulator re-traverses the growing result on every merge, so early nodes are compared k times — O(N·k). Pairwise merging or a heap brings it to O(N log k).
Explore the code comparisonHide the code comparison
Quadratic-ish one-at-a-time merge
Folding lists into a single accumulator re-traverses the growing result on every merge, so early nodes are compared k times — O(N·k). Pairwise merging or a heap brings it to O(N log k).
Don't write this
result = None
for lst in lists:
result = self._merge_two(result, lst) # re-walks the accumulator each time
return resultWrite this instead
if not lists:
return None
return self._merge(lists, 0, len(lists) - 1) # balanced pairwise merges2Pushing ListNodes into a Python heap
heapq compares tuple elements left-to-right; if it reaches a ListNode (because two values tie) it raises TypeError since ListNode defines no ordering. A unique index between val and node resolves ties first.
Explore the code comparisonHide the code comparison
Pushing ListNodes into a Python heap
heapq compares tuple elements left-to-right; if it reaches a ListNode (because two values tie) it raises TypeError since ListNode defines no ordering. A unique index between val and node resolves ties first.
Don't write this
heapq.heappush(heap, (node.val, node)) # TypeError on equal valsWrite this instead
heapq.heappush(heap, (node.val, i, node)) # index i breaks ties before node3Missing comparator on a Java PriorityQueue
PriorityQueue needs either Comparable elements or an explicit Comparator. ListNode is neither Comparable, so the default constructor throws ClassCastException at the first comparison.
Explore the code comparisonHide the code comparison
Missing comparator on a Java PriorityQueue
PriorityQueue needs either Comparable elements or an explicit Comparator. ListNode is neither Comparable, so the default constructor throws ClassCastException at the first comparison.
Don't write this
PriorityQueue<ListNode> pq = new PriorityQueue<>(); // ClassCastExceptionWrite this instead
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> Integer.compare(a.val, b.val));Revise
Carry the pattern into the interview
Pattern to remember
Min-heap over list heads OR divide-and-conquer pairwise merge
Code pattern to memorize
# Min-heap (O(N log k))
heap = [(node.val, i, node) for i, node in enumerate(lists) if node]
heapify(heap)
dummy = curr = ListNode(0)
while heap:
val, i, node = heappop(heap)
curr.next = node; curr = node
if node.next:
heappush(heap, (node.next.val, i, node.next))
return dummy.nextRules to remember
- 1Use a dummy head so appending the merged nodes never special-cases the first node.
- 2Heap holds at most k entries (one live head per list), so each push/pop is
O(log k)and total work isO(N log k). - 3In Python, push a tuple `(val, index, node)` — the
indextiebreaker stopsheapqfrom trying to compareListNodeobjects when values tie. - 4In Java, give
PriorityQueuean explicit comparator(a, b) -> a.val - b.val(orInteger.compare) sinceListNodeisn'tComparable. - 5Divide-and-conquer merges in pairs, halving k each round →
log krounds, each touching all N nodes →O(N log k). - 6Skip null lists when seeding the heap or the divide step — an empty input list contributes nothing and a null head must never be pushed/dereferenced.
- 7Both approaches reuse the two-list merge primitive (Merge Two Sorted Lists) — know it cold.
Use this pattern when
- Merge k already-sorted sequences (lists, streams, run files) into one sorted output
- k-way merge step of external merge sort
- Repeatedly needing the global minimum across k sorted sources → min-heap
- Combining results from k shards/partitions that are each locally sorted
Choose another pattern when
- Only two lists → just use the single two-pointer merge, no heap needed
- Lists are unsorted → sort first, or this k-way trick doesn't apply
- Total size is tiny → concatenate-and-sort O(N log N) is simpler and competitive
- You need O(1) extra space strictly → divide-and-conquer's recursion is O(log k) stack; heap is O(k)
Interview takeaway
Merge k Sorted Lists is the canonical k-way merge. The min-heap turns 'find the smallest of k heads' into an O(log k) operation; divide-and-conquer gets the same O(N log k) by merging in a balanced binary-tree shape. Both lean on the two-list merge — master that primitive and this hard problem becomes a composition exercise.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical k-way merge. The min-heap variant (O(k) space, streaming-friendly) and the divide-and-conquer variant (O(log k) stack, cache-friendly) both reach O(N log k) and both reduce to the two-list merge primitive. Recognise this shape in external merge sort, Smallest Range Covering Elements, and Kth Smallest in a Sorted Matrix.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Merge Two Sorted ListsThe two-list merge is the primitive both solutions here are built on — master it first.
- Merge Sorted ArrayComing soonTwo-way merge on arrays; merge from the back to reuse the destination array in O(1) space.
- Smallest Range Covering Elements from K ListsComing soonSame min-heap-of-k-heads engine, but track the current max to shrink a covering range.
- Kth Smallest Element in a Sorted MatrixComing soonEach row is a sorted list; a min-heap over row heads is the same k-way merge idea.