K-way Merge
You already know how to merge two sorted lists in — it’s the merge step from merge sort. The interview twist is merging of them at once. Doing that with repeated two-way merges works, but a min-heap that always holds one candidate per list turns it into a single clean pass, and it’s the pattern behind an entire family of “smallest across k sequences” questions.
What you’ll learn
Section titled “What you’ll learn”- Why merging lists two-at-a-time is worse than it looks.
- The k-way merge template: a min-heap of
(value, list_index, element_index)tuples, one entry per list. - How the same heap-of-heads idea generalizes to a sorted matrix, not just a list of lists.
- Why the pattern costs , where is the total element count.
The cue
Section titled “The cue”When it is the wrong tool. If there is one unsorted array and you want its k smallest, that is
Top K or quickselect — there are no sorted
runs to exploit. If the matrix is fully sorted when flattened, one binary search is
and beats any merge. And if the question is “the kth smallest value” in a row/column-sorted
matrix with n large, binary search on the value is and beats the heap’s
once k approaches — LC 378 accepts both, and which one wins depends on k.
The tell that separates this from Top K: the input is already sorted in pieces, and the algorithm exists to avoid throwing that structure away.
The naive approach, and why it’s worse than it looks
Section titled “The naive approach, and why it’s worse than it looks”Merging sorted lists two at a time — merge list 1 into list 2, merge that into list 3, and so on — costs in the worst case: each of the merge passes touches close to all elements. As grows, that quadratic-ish blowup gets painful. The fix is to never compare more than candidates at once, no matter how many elements are still buried inside each list.
The pattern: one heap slot per list
Section titled “The pattern: one heap slot per list”Keep a min-heap with at most one entry per list — always its current
smallest unconsumed element. Popping the heap’s minimum gives you the next
overall smallest value; you then push that list’s next element back in to
replace it. The heap tuple carries (value, list_idx, elem_idx) so you
always know exactly which list to advance.
import heapq
def merge_k_sorted(lists):
result = []
heap = []
# Seed the heap with each list's first element (skip empty lists).
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst[0], i, 0))
while heap:
val, list_idx, elem_idx = heapq.heappop(heap)
result.append(val)
# Advance the list this value came from, if it has more elements.
if elem_idx + 1 < len(lists[list_idx]):
next_val = lists[list_idx][elem_idx + 1]
heapq.heappush(heap, (next_val, list_idx, elem_idx + 1))
return result
lists = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
print(merge_k_sorted(lists)) # expect [1, 2, 3, 4, 5, 6, 7, 8, 9]Visual intuition
Section titled “Visual intuition”The heap holds one slot per list, never the whole input. Watch the root: every pop is the global
minimum across all k lists, and the replacement comes only from the list that just gave one up:
heap is empty
Contrast the naive approach -- concatenate everything and sort -- at O(N log N). Keeping only k items in the heap is what turns log N into log k, and when k is 3 and N is a million that is the whole difference.
How it works
Section titled “How it works” graph TD
L0["List 0 head: 1"] --> H["min-heap: one (value, list_idx, elem_idx) per list"]
L1["List 1 head: 2"] --> H
L2["List 2 head: 3"] --> H
H --> P["pop the smallest -> append to result"]
P --> N["push that list's NEXT element back in"]
N --> H
The heap never holds more than items at once — one per list — no matter how large each individual list is. Every element from every list gets pushed and popped from the heap exactly once over the whole run.
Worked example: Kth Smallest Element in a Sorted Matrix
Section titled “Worked example: Kth Smallest Element in a Sorted Matrix”A matrix with sorted rows (and sorted columns) is just sorted lists in disguise — one list per row. Seed the heap with the first element of every row, then pop times, advancing along each row exactly like before.
import heapq
def kth_smallest_in_matrix(matrix, k):
n = len(matrix)
# Seed the heap with the first element of every row.
heap = [(matrix[row][0], row, 0) for row in range(n)]
heapq.heapify(heap)
val = None
for _ in range(k):
val, row, col = heapq.heappop(heap)
if col + 1 < n:
heapq.heappush(heap, (matrix[row][col + 1], row, col + 1))
return val
matrix = [
[1, 5, 9],
[10, 11, 13],
[12, 13, 15],
]
print(kth_smallest_in_matrix(matrix, 8)) # expect 13The -th pop off the heap is, by construction, the -th smallest value across the whole matrix — you never sort all elements to get there.
Dry run
Section titled “Dry run”merge_k_sorted([[1, 4, 7], [2, 5, 8], [3, 6, 9]])
Section titled “merge_k_sorted([[1, 4, 7], [2, 5, 8], [3, 6, 9]])”Heap contents are shown sorted for readability; only the root is guaranteed to be in place.
| Step | Pop | Result so far | Pushed | Heap after |
|---|---|---|---|---|
| seed | — | [] | — | (1,0,0) (2,1,0) (3,2,0) |
| 1 | (1,0,0) | [1] | (4,0,1) | (2,1,0) (3,2,0) (4,0,1) |
| 2 | (2,1,0) | [1,2] | (5,1,1) | (3,2,0) (4,0,1) (5,1,1) |
| 3 | (3,2,0) | [1,2,3] | (6,2,1) | (4,0,1) (5,1,1) (6,2,1) |
| 4 | (4,0,1) | [1,2,3,4] | (7,0,2) | (5,1,1) (6,2,1) (7,0,2) |
| 5 | (5,1,1) | [1,…,5] | (8,1,2) | (6,2,1) (7,0,2) (8,1,2) |
| 6 | (6,2,1) | [1,…,6] | (9,2,2) | (7,0,2) (8,1,2) (9,2,2) |
| 7 | (7,0,2) | [1,…,7] | — (list 0 exhausted) | (8,1,2) (9,2,2) |
| 8 | (8,1,2) | [1,…,8] | — | (9,2,2) |
| 9 | (9,2,2) | [1,…,9] | — | [] |
The heap never exceeded 3 entries — one per list — while nine elements passed through it. That is the whole point: space regardless of how long the lists are. Nine pops, six pushes, and every element entered and left exactly once.
Steps 7-9 show the heap shrinking. Once a list is exhausted its slot is not refilled, so k
effectively decreases and the remaining work gets cheaper. Nothing special-cases this; the
elem_idx + 1 < len(...) check is the only guard.
The tuple tiebreaker, and the crash without it
Section titled “The tuple tiebreaker, and the crash without it”kth_smallest_in_matrix on
1 5 9
10 11 13
12 13 15with k = 8. Watch pops 6 and 7:
| Pop | Value | From | Pushed | Heap after |
|---|---|---|---|---|
| 1 | 1 | row 0, col 0 | (5,0,1) | (5,0,1) (10,1,0) (12,2,0) |
| 2 | 5 | row 0, col 1 | (9,0,2) | (9,0,2) (10,1,0) (12,2,0) |
| 3 | 9 | row 0, col 2 | — | (10,1,0) (12,2,0) |
| 4 | 10 | row 1, col 0 | (11,1,1) | (11,1,1) (12,2,0) |
| 5 | 11 | row 1, col 1 | (13,1,2) | (12,2,0) (13,1,2) |
| 6 | 12 | row 2, col 0 | (13,2,1) | (13,1,2) (13,2,1) |
| 7 | 13 | row 1, col 2 | — | (13,2,1) |
| 8 | 13 | row 2, col 1 | (15,2,2) | (15,2,2) |
Answer 13, matching sorted(flattened)[7] on [1, 5, 9, 10, 11, 12, 13, 13, 15].
After step 6 the heap holds two entries with the identical value 13. Python compares tuples
left to right, so the value ties and the comparison falls through to list_idx: 1 < 2, so row 1’s
copy pops first. Both orders would give the same answer here, but the comparison has to resolve —
and that is the point.
Without the index, a tuple of (value, node) on tied values raises:
TypeError: '<' not supported between instances of 'Node' and 'Node'verified directly. This is why the tuple is (value, list_idx, elem_idx) and not just
(value, node). It bites on LC 23 specifically, where the payload is a ListNode — and only on
inputs with duplicate values, so it passes small tests and crashes on the real ones. Any always-
comparable tiebreaker works; the list index is the natural one, and it is what you need anyway to
know which list to advance.
The empty-list guard
Section titled “The empty-list guard”Seeding with a comprehension instead of the guarded loop:
| Input | Guarded loop | [(l[0], i, 0) for i, l in enumerate(lists)] |
|---|---|---|
[[], [1], []] | [1] | IndexError |
[[], [], []] | [] | IndexError |
[] | [] | [] (vacuously) |
LC 23’s constraints explicitly allow empty lists in the input, so if lst: is load-bearing, not
defensive style.
Why sequential pairwise merging is worse, counted
Section titled “Why sequential pairwise merging is worse, counted”Element-touch counts for k lists of 100 each — one “touch” is one element copied through one merge
pass:
k | N | Sequential pairwise | Balanced pairwise | Heap (pushes + pops) |
|---|---|---|---|---|
| 10 | 1,000 | 5,400 | 3,600 | 1,000 |
| 100 | 10,000 | 504,900 | 68,800 | 10,000 |
Sequential merging — fold list 1 into 2, that into 3, and so on — re-copies the growing
accumulator on every pass: , and at k = 100 that is 50x the heap’s work.
But balanced pairwise merging is genuinely competitive. Merge lists in pairs, then pairs of
pairs, tournament style: rounds, each touching all N elements, so — the
same asymptotic bound as the heap. Its 68,800 against the heap’s 10,000 is a constant factor (the
heap counts single pushes and pops, the merge counts copies), and it needs space rather than
.
So the honest answer to “can you do LC 23 without a heap?” is yes, divide-and-conquer pairwise merge, same — and the heap’s real advantages are space and that it works on streams of unknown length. Claiming the heap is asymptotically necessary is wrong, and it is a follow-up interviewers do ask.
Time and space complexity
Section titled “Time and space complexity”| Operation | Complexity |
|---|---|
| Seeding the heap with heads | |
| Each pop + push | |
| Total for elements across lists | |
| Space (heap holds at most entries) |
Compare that to sorting all elements directly: . When — a handful of long lists, or a matrix with far more entries than rows — the k-way merge is meaningfully cheaper.
When to use it
Section titled “When to use it”- You have already-sorted sequences (lists, arrays, or matrix rows) and need them combined into one sorted output, or need only the -th overall smallest/largest value without materializing the full merge.
- is small relative to the total element count — the heap’s cost per step is what makes this cheaper than one big sort.
- The “list” doesn’t have to be a Python list — linked lists, matrix rows,
or even ranges from
kdifferent iterators all work the same way, as long as each one is individually sorted and you can ask for “the next element after this one.”
The variant map
Section titled “The variant map”| Variant | What the heap entry carries | Canonical problem |
|---|---|---|
Merge k sorted lists | (value, list_idx, elem_idx) | 23 · 21 (k = 2) |
Merge k sorted linked lists | (value, list_idx, node) — the index is what makes ties comparable | 23 |
kth smallest in a row-sorted matrix | (value, row, col); pop k times | 378 |
kth smallest, k near | Abandon the heap — binary search the value, counting entries per row: | 378 |
k smallest pairs from two arrays | Frontier only: seed (a[i] + b[0], i, 0), push (i, j+1) on pop — never all pairs | 373 |
kth smallest in a multiplication table | No array exists to merge — binary search the value and count with division | 668 |
| Smallest range covering one element per list | Heap of heads plus a running maximum; the range is max - heap[0] | 632 |
kth smallest pair distance | Binary search the distance, count pairs with two pointers | 719 |
| Merge sorted iterators / files | heapq.merge(*iterables) — lazy, memory, the stdlib version of this page | external sort |
Sorted output, k inputs, no heap | Divide-and-conquer pairwise merge: also , but space | 23 |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 23 — Merge k Sorted Lists · Hard
Section titled “LC 23 — Merge k Sorted Lists · Hard”Problem. Merge k sorted linked lists into one sorted list and return its head.
Constraints. 0 <= k <= 10^4, 0 <= len(each list) <= 500, total nodes up to
10^4.
Examples. [[1,4,5],[1,3,4],[2,6]] gives [1,1,2,3,4,4,5,6] ·
[] gives [] · [[]] gives []
Editorial
The heap holds at most one node per list — the smallest unconsumed value from each. Popping the global minimum and pushing that node’s successor keeps the invariant.
Time for N total nodes across k lists. Space .
Two other approaches worth naming:
- Pairwise merging. Merge lists in rounds, halving the count each time: too, and it avoids the heap entirely. Many people find it easier to reason about.
- Concatenate and sort. — worse, and it ignores the sortedness you were given.
Follow-ups: “Do it without a heap?” — pairwise merging, using
LC 21 as the
subroutine. “Merge k sorted arrays?” — same heap, indices instead of nodes.
“Why and not ?” — the heap never holds more than k
entries.
LC 378 — Kth Smallest Element in a Sorted Matrix · Medium
Section titled “LC 378 — Kth Smallest Element in a Sorted Matrix · Medium”Problem. Given an n x n matrix whose rows and columns are each sorted
ascending, return the kth smallest element (in overall sorted order, counting
duplicates).
Constraints. 1 <= n <= 300, 1 <= k <= n^2.
Examples. matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8 gives 13 ·
matrix = [[-5]], k = 1 gives -5
Editorial
The matrix is n sorted lists, so this is a k-way merge stopped after k pops.
Time . Space .
Seeding only min(n, k) rows is a genuine optimisation: the first element of row k
is at least as large as the first elements of all earlier rows, so it cannot be among
the k smallest.
[[1,2],[1,3]] with k = 2 gives 1 — duplicates count separately, so the answer
is the second 1, not 2. That is a common misreading.
The stronger solution is binary search on the value range: pick a candidate value,
count how many matrix entries are at most it (via a staircase walk from the
bottom-left, ), and binary search until the count reaches k. That is
, which beats when k approaches , and
it uses space. Worth naming as the follow-up answer — it composes the
staircase walk with
binary search on the answer.
Follow-ups: “Beat ?” — the value-range binary search above. “Why
only min(n, k) rows?” — the bounding argument. “Kth largest?” — mirror it, or
ask for the th smallest.
LC 373 — Find K Pairs with Smallest Sums · Medium
Section titled “LC 373 — Find K Pairs with Smallest Sums · Medium”Problem. Given two sorted arrays and an integer k, return the k pairs
(u, v) with u from nums1 and v from nums2 that have the smallest sums.
Constraints. 1 <= len(nums1), len(nums2) <= 10^5, both sorted ascending,
1 <= k <= 10^4.
Examples. nums1 = [1,7,11], nums2 = [2,4,6], k = 3 gives
[[1,2],[1,4],[1,6]] · nums1 = [1,1,2], nums2 = [1,2,3], k = 2 gives
[[1,1],[1,1]]
Editorial
There can be pairs, so they must be generated lazily. The structure is a
k-way merge over conceptual rows: row i is the sorted sequence
nums1[i] + nums2[0], nums1[i] + nums2[1], ….
Time . Space .
The generation rule is what makes it correct: seed one entry per nums1 index, and
when popping (i, j) push only (i, j+1). Because each row is entered once at
j = 0 and advanced only along j, every pair is reachable by exactly one path —
so no duplicates and nothing missed. Pushing both (i+1, j) and (i, j+1) is the
common variant and it needs a visited set, because pairs become reachable two ways.
([1,1,2], [1,2,3], 2) giving [[1,1],[1,1]] shows duplicate values are distinct
pairs — both nums1[0] and nums1[1] pair with nums2[0].
Seeding only min(len(nums1), k) rows matters at the stated constraints: with
len(nums1) = 10^5 and k = 10^4, seeding everything would be ten times the
necessary work.
Follow-ups: “Why not push both directions?” — it works but needs deduplication;
the one-direction rule avoids it. “Kth smallest sum rather than the first k?” — pop
k times and return the last. “Three arrays?” — fold pairwise, or extend the heap
entry to three indices.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 373Find K Pairs with Smallest SumsmediumThe "lists" are implicit (all pairs `(nums1[i], nums2[j])`), generated and pruned lazily through the heap instead of built up front
- 378Kth Smallest Element in a Sorted MatrixmediumTreat each row as one of the $k$ sorted sequences
- 23Merge k Sorted ListshardThe exact template, applied to linked lists instead of Python lists
- 632Smallest Range Covering Elements from K ListshardExtend the pattern: track the current max alongside the heap's min to shrink the covering range as you advance
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
“Why does the tuple carry list_idx?” | Whether you have hit the crash | Two purposes. It tells you which list to advance, and it makes tied values comparable — without it, (value, node) on a duplicate raises TypeError: '<' not supported between instances of 'ListNode'. It only fails on inputs with duplicates, so it passes small tests |
| “What is the complexity, and in terms of what?” | Naming the right variables | time where N is the total element count and k the number of lists, plus space. Not — the heap is capped at one entry per list |
| “Can you do it without a heap?” | Whether you know the heap is not required | Yes — divide-and-conquer pairwise merge is also : rounds, each touching all N. The heap’s advantages are space instead of , and it works on streams of unknown length. Claiming the heap is asymptotically necessary is wrong |
| “Why is merging them one at a time worse?” | Cost analysis | Sequential folding re-copies the growing accumulator each pass: . Counted for k = 100 lists of 100, that is 504,900 element touches against the heap’s 10,000 |
| “The lists are now streams of unknown length” | Where the heap actually wins | Unchanged — the heap holds one entry per stream and asks each for its next value. This is what heapq.merge does, and it is how external merge sort works |
“k is now close to in LC 378” | Whether you re-check the bound | The heap is , so a large k makes it worse than binary search on the value: guess x, count entries in using the sorted rows, narrow. , independent of k |
“Now find the k smallest pairs from two arrays” | Whether you push the whole cross product | Seed one frontier — (a[i] + b[0], i, 0) — and on popping (i, j) push only (i, j + 1). . Pushing all pairs is the trap |
| “Give me the smallest range containing one element from each list” | Composing the pattern | Same heap of heads, plus a running maximum of everything currently in it. The candidate range is current_max - heap[0]; advance the list that just gave up its minimum. LC 632 |
| “Is there a stdlib shortcut?” | Fluency | heapq.merge(*iterables, key=…) — lazy, memory, arbitrary iterables. Say it exists, then hand-roll if the interviewer wants the mechanics |
| “What if one list is empty?” | Edge cases | The seeding loop must skip it. Seeding with a comprehension raises IndexError on [[], [1], []], and LC 23 explicitly permits empty lists in the input |
Self-check
Section titled “Self-check”-
Why does the heap entry carry `list_idx` in addition to the value?
Python compares tuples left to right, so on a tie it moves to the next component. With (value, node) that means comparing ListNode objects, which raises `TypeError: '<' not supported`. Verified directly. The vicious part is that it only fails when two lists offer the same value, so it passes small tests and crashes on real input. Stability is a side effect, not the reason.
pch.quizShowAnswer
B — Both to know which list to advance and to make tied values comparable -- without it, a tuple like (value, node) raises TypeError on duplicates — Python compares tuples left to right, so on a tie it moves to the next component. With (value, node) that means comparing ListNode objects, which raises `TypeError: '<' not supported`. Verified directly. The vicious part is that it only fails when two lists offer the same value, so it passes small tests and crashes on real input. Stability is a side effect, not the reason.
-
In the 3x3 matrix trace, the heap briefly holds (13,1,2) and (13,2,1) at once. What decides which pops first?
heapq is not stable -- there is no insertion-order guarantee at all. The tiebreak is whatever the next tuple component says, which is exactly why an always-comparable one must be there. Both orders happen to give the same answer for LC 378, but the comparison must *resolve* or the pop raises.
pch.quizShowAnswer
B — The tuple comparison falls through the tied value to list_idx, and 1 < 2, so row 1's copy pops first — heapq is not stable -- there is no insertion-order guarantee at all. The tiebreak is whatever the next tuple component says, which is exactly why an always-comparable one must be there. Both orders happen to give the same answer for LC 378, but the comparison must *resolve* or the pop raises.
-
What is the complexity of the k-way merge, and in terms of which variables?
The heap holds at most one entry per list, so each of the N push/pop pairs costs log k rather than log N. In the nine-element trace the heap never exceeded 3 entries. O(k) space is what lets the algorithm run on streams -- and it is the property that distinguishes it from every alternative.
pch.quizShowAnswer
B — O(N log k) time and O(k) space, where N is the total element count and k the number of lists — The heap holds at most one entry per list, so each of the N push/pop pairs costs log k rather than log N. In the nine-element trace the heap never exceeded 3 entries. O(k) space is what lets the algorithm run on streams -- and it is the property that distinguishes it from every alternative.
-
"Can you merge k sorted lists without a heap, in the same time bound?"
Merge in pairs, then pairs of pairs, tournament style. Counted for k = 100 lists of 100: sequential folding costs 504,900 element touches, balanced pairwise 68,800, the heap 10,000 pushes plus pops. The heap wins on constant factor and on space -- O(k) versus O(N) -- and it handles streams. But it is not asymptotically necessary, and claiming otherwise is a common overstatement. Concatenate-and-sort is O(N log N), a different bound.
pch.quizShowAnswer
B — Yes -- divide-and-conquer pairwise merging is also O(N log k): log k rounds, each touching all N elements — Merge in pairs, then pairs of pairs, tournament style. Counted for k = 100 lists of 100: sequential folding costs 504,900 element touches, balanced pairwise 68,800, the heap 10,000 pushes plus pops. The heap wins on constant factor and on space -- O(k) versus O(N) -- and it handles streams. But it is not asymptotically necessary, and claiming otherwise is a common overstatement. Concatenate-and-sort is O(N log N), a different bound.
-
Seeding the heap with `[(l[0], i, 0) for i, l in enumerate(lists)]` instead of a guarded loop. What breaks?
`l[0]` on an empty list raises immediately -- verified on [[], [1], []], which the guarded version correctly returns [1] for. LC 23's constraints allow empty lists in the input, so `if lst:` is load-bearing rather than defensive style. (The comprehension does still need an explicit heapify, but that is a separate issue.)
pch.quizShowAnswer
B — `IndexError` on any empty input list, which LC 23 explicitly permits — `l[0]` on an empty list raises immediately -- verified on [[], [1], []], which the guarded version correctly returns [1] for. LC 23's constraints allow empty lists in the input, so `if lst:` is load-bearing rather than defensive style. (The comprehension does still need an explicit heapify, but that is a separate issue.)
-
LC 373 asks for the k pairs with the smallest sums from two arrays of length n. What is the trap?
Seed only (a[i] + b[0], i, 0) for each i, and on popping (i, j) push just (i, j + 1). The heap stays O(k) and the total is O(k log k). The general principle is that a heap should hold the *frontier* of unexplored candidates, never the whole candidate set -- the same idea that makes Dijkstra and A* work.
pch.quizShowAnswer
B — Pushing all n^2 pairs into the heap -- O(n^2 log n^2) -- instead of seeding one frontier and expanding it — Seed only (a[i] + b[0], i, 0) for each i, and on popping (i, j) push just (i, j + 1). The heap stays O(k) and the total is O(k log k). The general principle is that a heap should hold the *frontier* of unexplored candidates, never the whole candidate set -- the same idea that makes Dijkstra and A* work.
-
For LC 378 (kth smallest in a row/column-sorted matrix), when is the heap the wrong choice?
The heap pops k times, so its cost scales with k. Binary search on the *value* guesses x, counts entries <= x in O(n) by walking each sorted row, and narrows -- O(n log R) regardless of k. LC 378 accepts both, and which wins depends on k relative to n^2. Noticing that the bound has a k in it is the whole point.
pch.quizShowAnswer
B — When k approaches n^2 -- the heap is O(k log n), so binary searching the value at O(n log R) becomes better and is independent of k — The heap pops k times, so its cost scales with k. Binary search on the *value* guesses x, counts entries <= x in O(n) by walking each sorted row, and narrows -- O(n log R) regardless of k. LC 378 accepts both, and which wins depends on k relative to n^2. Noticing that the bound has a k in it is the whole point.
-
The inputs become file streams too large to hold in memory. Does the algorithm change?
O(k) space and one forward pass per input are precisely the properties that make this the streaming algorithm. `heapq.merge(*iterables)` is the lazy stdlib version, accepting any iterables with an optional key. Pairwise merging is the wrong answer here -- it needs O(N) space to hold the intermediate results.
pch.quizShowAnswer
B — No -- the heap holds one entry per stream and asks each for its next value; this is exactly what heapq.merge and external merge sort do — O(k) space and one forward pass per input are precisely the properties that make this the streaming algorithm. `heapq.merge(*iterables)` is the lazy stdlib version, accepting any iterables with an optional key. Pairwise merging is the wrong answer here -- it needs O(N) space to hold the intermediate results.
Recall card
Section titled “Recall card”ksorted inputs -> a min-heap holding one entry per input. Pop the global minimum, then push only the next element from the list that just gave one up.- The entry is
(value, list_idx, elem_idx). The index tells you which list to advance and makes ties comparable — without it,(value, node)raisesTypeErroron duplicate values, so it passes small tests and fails real ones. - time, space —
Ntotal elements,klists. Each element is pushed and popped exactly once, and the heap is capped atk. - Skip empty lists when seeding. A comprehension raises
IndexError; LC 23 allows empty inputs. - A row-sorted matrix is
ksorted lists. Seed with each row’s head, popktimes. - Sequential pairwise folding is — 504,900 touches against 10,000 at
k = 100. But balanced pairwise merge is also ; the heap’s real edges are space and streaming, not the asymptotic bound. - The heap holds the frontier, never the whole candidate set. LC 373: seed one row of candidates and expand on pop — never all pairs.
- When
kapproaches , switch to binary search on the value (, independent ofk) — LC 378, 668, 719. heapq.merge(*iterables, key=…)is this algorithm in the stdlib: lazy, memory, any iterable. Name it, then hand-roll if asked.
- Merging sorted sequences two at a time costs up to — a single heap holding one candidate per sequence does it in instead.
- The heap tuple is
(value, list_idx, elem_idx)— the indices both identify where to advance next and break ties Python can compare safely. - Every element is pushed and popped from the heap exactly once over the whole run.
- The same idea covers linked lists, plain arrays, matrix rows, and even lazily generated pairs — anything you can ask “what’s your next smallest element?”
Next: Binary Search on Answer — turning binary search from an array lookup into a tool for optimization problems.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading