Why this matters more than it seems
Merging two sorted lists is a warm-up exercise (it's the combine step of merge sort). Merging K sorted lists — where K itself can be large — is where a naive approach quietly blows up your complexity, and where recognizing "this is a k-way merge" saves you from writing something far slower than necessary. This pattern also has real-world weight beyond interviews: it's the second stage of every external-sort algorithm (databases, log processing) whenever data is too large to fit in memory and gets sorted in chunks that must later be merged.
The naive approach, and why it's worse than it looks
Given K sorted lists totaling N elements, the obvious move is: concatenate everything into one array, then sort it.
- Concatenation: O(N).
- Sorting: O(N log N).
- Total: O(N log N) — and it throws away the fact that each individual list was already sorted.
The heap-based approach
Keep a min-heap containing one "current head" element per list — at most K elements at any time. Repeatedly pop the smallest, emit it, and push the next element from whichever list that minimum came from.
import heapq
def merge_k_sorted(lists: list[list[int]]) -> list[int]:
heap = []
# Seed the heap with the first element of every non-empty list.
# Each entry is (value, list_index, element_index).
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst[0], i, 0))
result = []
while heap:
val, i, j = heapq.heappop(heap)
result.append(val)
if j + 1 < len(lists[i]):
heapq.heappush(heap, (lists[i][j + 1], i, j + 1))
return resultThe (value, list_index, element_index) triple is the standard shape for this pattern: the value drives the ordering, and the two indices let you find and push that list's next element once its current one is consumed. Including list_index as a tiebreaker also sidesteps a real bug: if two lists produce equal values, Python's heapq would otherwise try to compare the next tuple field — which breaks if that field isn't orderable (e.g., a linked-list node object). A monotonically distinct tiebreaker avoids that entirely.
Why this is O(N log k), not O(N log N)
The heap never holds more than K elements — one per list — no matter how large N is. Every one of the N elements is pushed and popped exactly once, and each of those operations costs O(log k) because that's the heap's size. Total: O(N log k). When K is small relative to N (say, merging 20 log files with a million lines total), this is a large win over O(N log N) — the same reasoning as the top-K pattern's O(n log k) versus O(n log n), just applied across lists instead of within one.
| Approach | Time | Space |
|---|---|---|
| Concatenate + sort | O(N log N) | O(N) |
| Pairwise merge, one list at a time | O(N·K) | O(N) |
| Pairwise merge, divide & conquer (merge lists two at a time, halving the count each round) | O(N log k) | O(N) (or O(1) extra for linked lists) |
| Min-heap of K current heads | O(N log k) | O(k) |
The divide-and-conquer alternative (repeatedly merging pairs of lists, the same idea as merge sort's combine step scaled up) achieves the same O(N log k) time and is worth knowing as a second valid approach — some interviewers will ask you to compare the two. Its main advantage is O(1) extra space when merging linked lists in place; the heap approach's advantage is a smaller constant-factor working set (O(k) instead of re-scanning merged output) and a more direct generalization to streaming sources where you don't have random access to "the next chunk" ahead of time.
Recognizing the pattern
The signal is almost always some version of: "you're given K sorted structures (arrays, linked lists, or streams) and need the merged/smallest/next result across all of them." Variants you'll see:
- Merge K sorted linked lists into one.
- Find the Kth smallest element across several sorted arrays (or a sorted matrix, which is really N sorted rows).
- Find pairs/triples with the smallest combined value, drawn from multiple sorted arrays — the heap holds candidate combinations instead of raw elements, but the "pop smallest, push its successor" mechanic is identical.
- Find the smallest range that includes at least one element from each of K lists — same heap of current heads, plus a running max to track the window.
In every case, ask yourself: "what is the current smallest unconsumed element across all sources, and what should replace it once I consume it?" If you can answer that, you have your heap and your push-on-pop rule.
Pitfalls and interview gotchas
- Comparing non-orderable tiebreakers. As above — always include an index (or other totally-ordered tiebreaker) in the tuple you push if the payload itself might tie or might not support
<. - Forgetting to check bounds before pushing the "next" element. After popping
(val, i, j), you must checkj + 1 < len(lists[i])before pushing — omitting this either crashes or silently pushes garbage. - Empty input lists. Skip empty lists when seeding the heap; pushing a sentinel or crashing on
lists[i][0]for an empty list is a common off-by-one. - Confusing K (number of lists) with N (total elements). The whole point of this pattern is that your heap size is bounded by K, not N — if your heap is holding all N elements at once, you've accidentally implemented "sort everything" with extra steps.
- Assuming the heap-of-heads approach generalizes to unsorted inputs. This pattern only works because each individual list is already sorted; if the inputs aren't sorted, you have to sort them first (or use a different technique entirely), which changes your complexity analysis.
- Linked lists specifically: remember you're pushing/comparing node values, not node references, and that once you've fully consumed a list, its next pointer being
None/nullis your "list exhausted" signal — don't forget to advance the list pointer even when you don't push a new heap entry.
How to state this in an interview
"Rather than concatenating and sorting, I'll keep a min-heap of size K holding the current head of each list. Each pop-and-push is O(log k), and every one of the N total elements goes through the heap exactly once, so this runs in O(N log k) time and O(k) space — better than the O(N log N) sort-everything approach when K is much smaller than N."
Further Resources (Optional)
- Wikipedia — K-way merge algorithmReference15m
- GeeksforGeeks — Merge K Sorted Arrays Using a Min-HeapArticle15m
- USACO Guide — Priority QueuesArticle25m
- Tech Interview Handbook — Heap cheatsheetArticle10m
- Wikipedia — External sorting (k-way merge at disk scale)Reference20m
- NeetCode — Merge k Sorted Lists (video walkthrough)Video12m
- Visualizing K-Way Merge: An Interactive Guide (tournament & loser trees)Article20m
Practice Problems
Interview relevancy:!!!Critical — must-have!!Important — highly recommended!Good to have~Niche — rarely asked
- Ugly Number IIMedium!2/525m
- Find K Pairs with Smallest SumsMedium!3/530m
- Merge k Sorted ListsHard!!!4/540m
- Smallest Range Covering Elements from K ListsHard!4/545m
- Design TwitterMedium!!4/540m
Optional Practice (Extra Reps)
For once you've cleared the main set above and want more reps on this pattern. These don't count toward the roadmap's progress stats — solve them purely for your own benefit.
- Super Ugly NumberMedium~2/525m
- Find the Kth Smallest Sum of a Matrix With Sorted RowsHard~4/540m
- K-th Smallest Prime FractionMedium~3/530m
- The Skyline ProblemHard~5/550m