K-way merge
Merge several sorted lists into one by keeping the current front item of each list in a min-heap and always taking the smallest.
O(N log k)Space O(k)5 practice problemsStep-through pictureThe idea
Merging two sorted lists is easy: compare their front items and take the smaller one. With k lists you would compare k front items every time, which is slow when k is big.
A min-heap does that comparison for you. Put the first item of each list in the heap, along with which list it came from (row) and its position in that list (col). Pop the smallest; it is the next item of the merged result. Then push the next item from the same list, so the heap again holds one front item per list.
The heap never holds more than k items, so each step costs O(log k), whatever the total number of items.
An everyday example
Several checkout lines merging into one door
Several lines of people stand in order of their ticket numbers, and everyone must go through one door in ticket order. A guard looks only at the person at the front of each line, waves through the lowest ticket, and then looks at the new front person of that line. The guard never has to look further back in any line.
| In the example | In the code |
|---|---|
| The front person of each line | one heap entry per list: { value, row, col } |
| Waving through the lowest ticket | heap.pop() and merged.push(value) |
| The next person steps up in that line | push lists[row][col + 1] |
| A line that is now empty | col + 1 is not less than lists[row].length |
From the slow way to k-way merge
- The obvious way is to join all the lists and sort the result:
O(N log N)forNitems in total. That throws away work that was already done, because every list is sorted. Merging two sorted lists takes onlyO(N): compare their front items and take the smaller. - With
klists, the same idea says: look at thekfront items and take the smallest. Scanningkfronts for every output item costsO(N × k), which is slow with hundreds of lists. The scan repeats almost the same work each time, because only one front changes after each take. - A group where you take the smallest and then add one new item is exactly what a min-heap does well. Keep one entry per list in a heap: the front
value, the list's indexrow, and the positioncol. Taking the smallest is a pop, and replacing it with the next item of the same list is a push, eachO(log k). rowandcolare what make it work: after a pop, you know which list to read next, and where. Put every list's first item in the heap, pop and output the smallest, push the next item from the same list, and repeat until the heap is empty: that loop is k-way merge.
How it works
- Make a min-heap ordered by
value. - Push the first item of every non-empty list as
{ value, row, col: 0 }. - While the heap is not empty, pop the smallest entry and add its
valuetomerged. - If its list has another item at
col + 1, push that item with the samerow. - When the heap is empty, every item of every list has been added in order.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function mergeSortedLists(lists) {const heap = new MinHeap((a, b) => a.value - b.value);lists.forEach((list, row) => {if (list.length > 0) heap.push({ value: list[0], row, col: 0 });});const merged = [];while (heap.size > 0) {const { value, row, col } = heap.pop();merged.push(value);if (col + 1 < lists[row].length) {heap.push({ value: lists[row][col + 1], row, col: col + 1 });}}return merged;}
How fast it is
| Time | O(N log k) |
|---|---|
| Extra memory | O(k) |
Each of the N items goes through a heap that holds at most k entries, one per list. The output list is not counted.
- Best case
O(N)There is only one list, or one non-empty list: the heap holds one entry, every push and pop is
O(1), and the merge is a plain copy. - Average case
O(N log k)Each item is pushed and popped once on a heap of up to
kentries. - Worst case
O(N log k)Many lists of similar length, so the heap stays full with
kentries for most of the run, and each pop sinks through alllog₂ klevels.
Common mistakes
- Pushing whole lists into the heap at the start. The heap then holds all
Nitems and you lose theO(log k)benefit. - Forgetting to store which list an item came from. After popping, you can't tell which list to take the next item from.
- Pushing empty lists' first items, which are
undefined. Skip empty lists. - Leaving ties to chance when the output order of equal values matters. Add
rowas a second compare key.
When to use it
Look for these clues in a problem:
- You get several sorted lists, feeds, or files and need one sorted result.
- You need the k-th smallest item across several sorted lists or a sorted grid.
- You need the smallest range or pairs that come from several sorted sources.
- The data is too large to combine and sort at once, but each part is sorted.
Check yourself
Answer each one in your head first, then open it.
Why does the heap never hold more than k entries?
It holds at most one entry per list: each pop removes a list's entry, and at most one push adds the next item from that same list.
Why must each entry remember row and col?
After a pop, the code must fetch the next item from the same list. Without row and col, it can't tell which list that was or where to read next.
In the trace, why does the heap shrink after 9 is popped?
9 was the last item of list 1, so col + 1 < lists[1].length is false and nothing is pushed.
How many heap operations does merging 10 lists of 1,000 items take?
Each of the 10,000 items is pushed once and popped once: 20,000 operations, each on a heap of at most 10 entries.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 5 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The loop keeps two rules. First, the heap holds exactly one entry for each list that still has unused items, and that entry is the list's first unused item. Second, merged is sorted, and every unused item is at least as big as the last item of merged.
Putting every non-empty list's first item in the heap makes the first rule true, and merged starts empty. Each loop pops the smallest entry. Because each list is sorted, a list's first unused item is its smallest unused item, so the smallest entry in the heap is the smallest unused item overall. Appending it keeps merged sorted, and pushing the next item from the same row keeps the first rule.
Each loop uses up exactly one item, so after N loops every item is in merged and the heap is empty. By the second rule, merged then holds all N items in sorted order.
Going deeper
3 more ideas
Why the smallest front item is the next one overall
Each list is sorted, so its front item is its smallest remaining item. The smallest remaining item overall must be the front of some list. The heap holds exactly the front of every list that still has items, so its top is the smallest remaining item overall.
Breaking ties
When two lists have the same front value, the heap may return either one first. If the output must be unique (for example, items from the first feed before items from the second when times match), compare row as a second key: (a, b) => a.value - b.value || a.row - b.row.
Time and memory
With N items in total across k lists, each item is pushed and popped once on a heap of at most k entries. That is O(N log k) time and O(k) extra memory besides the output. Concatenating and sorting would be O(N log N).
Work it by hand
Every step in a table, with why
Merge four sorted lists, one of them empty. Heap values are listed smallest first, but the heap keeps them in heap order.
lists = [[3, 8, 12], [1, 9], [], [4, 5, 14]]
| Step | Popped value | row, col | Pushed | Heap values | merged | Why |
|---|---|---|---|---|---|---|
| 1 | - | - | 3, 1, 4 | 1, 3, 4 | [] | Each non-empty list puts its first item in the heap. List 2 is empty, so it adds nothing. |
| 2 | 1 | 1, 0 | 9 from list 1 | 3, 4, 9 | [1] | 1 is the smallest front. List 1 has another item at col + 1, so 9 takes its place. |
| 3 | 3 | 0, 0 | 8 from list 0 | 4, 8, 9 | [1, 3] | List 0's front 3 comes out, and its next item 8 goes in. |
| 4 | 4 | 3, 0 | 5 from list 3 | 5, 8, 9 | [1, 3, 4] | List 3's front 4 comes out, and 5 replaces it. |
| 5 | 5 | 3, 1 | 14 from list 3 | 8, 9, 14 | [1, 3, 4, 5] | List 3 wins twice in a row. Its next item, 14, joins the heap. |
| 6 | 8 | 0, 1 | 12 from list 0 | 9, 12, 14 | [1, 3, 4, 5, 8] | List 0 gives 8, and its last item, 12, joins. |
| 7 | 9 | 1, 1 | nothing | 12, 14 | [1, 3, 4, 5, 8, 9] | 9 was the last item of list 1, so nothing is pushed and the heap shrinks to 2 entries. |
| 8 | 12 | 0, 2 | nothing | 14 | [1, 3, 4, 5, 8, 9, 12] | List 0 is now used up too. |
| 9 | 14 | 3, 2 | nothing | empty | [1, 3, 4, 5, 8, 9, 12, 14] | The last item. The heap is empty, so the loop ends. |
The result is [1, 3, 4, 5, 8, 9, 12, 14]. Each of the 8 items was pushed once and popped once, and the heap never held more than 3 entries, one per non-empty list.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| K-way merge | O(N log k) | O(k) | Many sorted lists, or streams too big to hold at once, that must become one sorted result. |
| Join and sort | O(N log N) | O(N) | The lists are small, or you can't trust them to be sorted. It is one line of code. |
| Scan all fronts each step | O(N · k) | O(1) | There are only 2 or 3 lists, so scanning the fronts costs less than heap bookkeeping. |
| Merge in pairs | O(N log k) | O(N) | You already have a two-list merge and don't mind building the arrays for each round. |
Extra memory: O(k) for the heap. The output merged takes O(N), which is the answer itself and isn't counted as extra.
How the steps grow
- K-way merge, Join and sort, Merge in pairs
O(N log k) - Scan all fronts each step
O(N · k)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| K-way merge | 33 | 9,966 | 20 million |
| Join and sort | 33 | 9,966 | 20 million |
| Scan all fronts each step | 100 | 1 million | 1 trillion |
| Merge in pairs | 33 | 9,966 | 20 million |
Inputs that need care
5 tricky inputs
- No lists:
lists = [] - The heap stays empty, the loop never runs, and it returns
[]. - Empty lists among full ones, as in the trace
- The
list.length > 0check skips them at the start. Without it, the heap gets an entry withvalue: undefined, and subtractingundefinedgivesNaN, which breaks the heap order. - One list:
lists = [[2, 6, 9]] - The heap holds one entry at a time, so the merge just copies the list.
- Equal values in different lists:
[[2, 7], [2, 3]] - It returns
[2, 2, 3, 7], but which2comes first isn't fixed. Adda.row - b.rowas a second key to always take the lowerrowfirst. - A list that isn't sorted:
[[5, 1], [3]] - The merge doesn't check. Every item still comes out, but not in order. Sort or check each list first if the input can't be trusted.
Variations and follow-ups
Twists interviewers add
Merging linked lists
With k sorted linked lists, put the head nodes in the heap, and after popping a node, push its next. The node itself plays the role of row and col, and no new arrays are needed: you just relink the nodes.
The k-th smallest across lists
Stop after k pops instead of emptying the heap. The cost is O(k log m) for m lists. The same works on a grid whose rows are sorted: treat each row as one list.
Smallest range that covers every list
Keep one entry per list and also track the largest value now in the heap. The range from the heap's top to that largest value contains one item from every list. Pop the top, push the next item from its list, update the largest, keep the narrowest range seen, and stop when any list runs out.
Merging in pairs
Merge the lists two at a time in rounds, like merge sort: k lists become k / 2, then k / 4. Each round touches all N items and there are log k rounds, so it is also O(N log k), without a heap.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "merge k sorted lists", "several sorted feeds", "k-th smallest in a sorted matrix", or "smallest range that includes an item from each list".
- Say what each heap entry stores and why: "the value, which list it came from, and its position, so after popping I know what to push next".
- Give the cost in terms of both sizes:
O(N log k)time forNitems inklists, andO(k)heap space. It beatsO(N log N)because the heap only ever holds one item per list. - Test by hand: an empty input, an empty list among full ones, a single list, and equal values in two lists.
- A common follow-up: "what if the lists are files too big for memory?". Answer: the same loop works as a stream. Read a file's next item only when its entry is popped, and write each output item as it comes out. This is how external merge sort combines sorted chunks.
Where frontend code uses it
The same idea in real UI code
- A unified inbox or activity feed merges items from several sources (comments, mentions, likes), each already sorted by time.
- A log viewer merges sorted log streams from several services into one timeline.
- Paginated results from several API shards can be merged page by page without loading everything at once.