Coding Interview Patterns Module 13 – Heaps and top-K
Merging k sorted lists with a heap
Merge k sorted lists through a heap that holds one value of each, then find the smallest range that covers every list, starting from the brute force.
What you will learn
- Merge k sorted lists in O(N log k) time with a heap that holds one entry per list
- Find the shortest window that covers a value of every list, and prove that the scan never misses it
- Order ties with the list number, so that the merge is stable and never compares payloads
- Merge files and other streams lazily with heapq.merge, reading one item ahead in each
- Compare the heap with scanning, pairwise merging and sorting by counted comparisons
Before you start
What you’ll learn · Before you start
What you will learn
- Merge k sorted lists in O(N log k) time with a heap that holds one entry per list
- Find the shortest window that covers a value of every list, and prove that the scan never misses it
- Order ties with the list number, so that the merge is stable and never compares payloads
- Merge files and other streams lazily with heapq.merge, reading one item ahead in each
- Compare the heap with scanning, pairwise merging and sorting by counted comparisons
Before you start
On this page
To merge k sorted lists, keep a min-heap that holds one entry from each list: the smallest value that list has not given out yet. Write out the top, then put the next value of the same list in its place. Each of the N values passes through the heap once, and the heap never holds more than k entries, so the merge costs O(N log k) time and O(k) memory, and it can read files too large for memory one value at a time. The same heap of fronts finds the smallest range that holds a value from every list. Both problems follow, with their brute force.
Two problems on k sorted lists
Problem A: one queue from k counters
A clinic’s k registration counters each log the minute of the day at which they registered each patient, in increasing order. The waiting-room screen needs every registration in one list, by minute; registrations in the same minute go in counter order, lowest first. Return the merged (minute, counter) pairs. Limits: 1 ≤ k ≤ 10,000 counters, N ≤ 1,000,000 registrations, minutes from 0 to 1,439.
The logs [[540, 545, 600], [541, 600], [], [530]] merge to 530@3, 540@0, 541@1, 545@0, 600@0 and 600@1, where
530@3 means minute 530 at counter 3.
Problem B: the shortest window that hears every sensor
Each of k sensors reports its reading times, in seconds, in increasing order. Find the shortest window [a, b], with a and b reading times, that contains a reading of every sensor; among equally short windows, return the earliest. Limits: 1 ≤ k ≤ 1,000 sensors with at least one reading each, N ≤ 200,000 readings, times from 0 to 10⁹.
[[3, 11, 19, 27], [8, 14, 25], [1, 17, 30]]gives [14, 19], with 19, 14 and 17 from sensors 0, 1 and 2. [25, 30] is as short but starts later.[[5], [5], [5]]gives [5, 5].
The brute force, and why it fails at these sizes
For problem A, scan the front of every log for the smallest, k looks per registration: 10¹⁰ looks at the limits. For problem B, try every reading as the start a, binary-search each sensor for its first reading at or after a, and stretch the window to the latest: up to N × k = 2 × 10⁸ searches.
# Both problems the slow way, with the work counted: scanning every log's front for the next registration, and
# trying every reading as the start of a window.
from bisect import bisect_left
def merge_by_scanning(logs):
"""Problem A: look at the front of every log, take the smallest (minute, counter), repeat."""
fronts = [0] * len(logs) # fronts[c]: the next unread position of log c
merged, looks = [], 0
while True:
best = None
for c, log in enumerate(logs):
looks += 1
if fronts[c] < len(log) and (best is None or log[fronts[c]] < logs[best][fronts[best]]):
best = c # strict <: on a tie the lower counter, seen first, stays
if best is None:
return merged, looks # every log is used up
merged.append((logs[best][fronts[best]], best))
fronts[best] += 1
def window_by_trying_starts(sensors):
"""Problem B: every reading as a start; each sensor's first reading at or after it decides the end."""
best, searches = None, 0
for start in sorted({t for readings in sensors for t in readings}):
end = start
for readings in sensors:
searches += 1
i = bisect_left(readings, start) # the first reading at or after start
if i == len(readings):
return best, searches # nothing from this sensor at or after start, nor after any later start
end = max(end, readings[i])
if best is None or end - start < best[1] - best[0]:
best = (start, end) # strict <: the earliest start wins a tie
return best, searches
def show_merge(merged):
return " ".join(f"{minute}@{c}" for minute, c in merged)
merged, looks = merge_by_scanning([[540, 545, 600], [541, 600], [], [530]])
print("A:", show_merge(merged), f"({looks} looks)")
window, searches = window_by_trying_starts([[3, 11, 19, 27], [8, 14, 25], [1, 17, 30]])
print("B:", list(window), f"({searches} searches)")
# The worst cases at the limits, counted from the loops above rather than run.
k, n = 10_000, 1_000_000
print(f"A at k = {k:,} logs and N = {n:,} entries: k * (N + 1) = {k * (n + 1):,} looks")
k, n = 1_000, 200_000
print(f"B at k = {k:,} sensors and N = {n:,} distinct readings: up to N * k = {n * k:,} binary searches") Output
A: 530@3 540@0 541@1 545@0 600@0 600@1 (28 looks) B: [14, 19] (26 searches) A at k = 10,000 logs and N = 1,000,000 entries: k * (N + 1) = 10,000,010,000 looks B at k = 1,000 sensors and N = 200,000 distinct readings: up to N * k = 200,000,000 binary searches
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Sorting the concatenated logs answers problem A in O(N log N) when everything fits in memory, but it needs every entry before it can return the first one.
What to notice
- In problem A, the next registration is always the smallest of the k unread fronts. The entries behind the fronts never compete.
- In problem B, a window that hears every sensor is set by k readings, one per sensor, from the smallest of them to the largest.
One front per list, for both problems
Text description of the diagram
The upper half shows problem A. Four counter logs are listed: counter 0 holds 540, 545 and 600; counter 1 holds 541 and 600; counter 2 has no entries; counter 3 holds 530. An arrow labelled "each log's next entry" leads to a min-heap that holds one front per log that still has one: (530, 3) on top, then (540, 0) and (541, 1). An arrow labelled "pop the smallest, push its successor" leads to the merged list: 530@3, 540@0, 541@1, 545@0, 600@0 and 600@1, where each entry names its minute and its counter, and the two entries at minute 600 come in counter order.
A dashed arrow labelled "the same heap of fronts" leads to the lower half, which shows problem B. Three sensors are listed with the reading each one holds at one moment: sensor 0 holds 19 of 3, 11, 19 and 27; sensor 1 holds 14 of 8, 14 and 25; sensor 2 holds 17 of 1, 17 and 30. An arrow labelled "spans" leads to the window [14, 19], which runs from the smallest held reading, 14, to the largest, 19, and so contains a reading of every sensor.
Both questions keep one candidate per list and keep asking for the smallest while one list moves on: the cue for a heap from the heaps overview.
Merging k logs, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- The invariant that makes the merge correct, and its cost
- The shortest covering window: always advance the earliest reading
- Why the scan never misses the shortest window: a proof
- A dry run on the three sensors
- The code in five languages
- Measured: the heap against scanning, pairwise merging and sorting
- Merging files and streams lazily with heapq.merge
- Edge cases and common bugs, each found by a drill
- Variants: linked lists, the j-th smallest, m of k sensors, common gaps
- Exercise: the shortest window that covers every list
- The shortest window that covers every list
- A practice set with sample answers
- Check yourself
- Interview questions
- Why does merging k sorted lists with a heap cost O(N log k) and not O(N log N), and when does the difference matter?
- A batch job must merge 2,000 sorted log files of about 5 GB each into one sorted file, on a machine with 16 GB of RAM. Describe the merge, how much memory it needs, and what limits its speed.
- For the smallest range that contains a value from each of k sorted lists, a scan holds one value per list and moves one list forward at each step. Which list must move, why does that never skip the answer, and what changes when values repeat?
- You must merge k sorted linked lists with N nodes in all. Compare merging them in pairs, round after round, with a heap of the k heads. Which would you write in an interview, and which in a service?
- Key takeaways
7 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 29 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- heapq: Heap queue algorithm (heapq.merge, heapreplace) (Python Software Foundation)
- listsort.txt: the design of the list sort, with its run detection (Python Software Foundation)
- Java SE 25 API: PriorityQueue (Oracle)
- Java SE 25 API: Comparator (Oracle)
- C++ working draft N4950: Class template priority_queue [priority.queue] (ISO/IEC JTC1/SC22/WG21 (HTML rendering of the working draft))
- C++ working draft N4950: Relational operators of tuple [tuple.rel] (ISO/IEC JTC1/SC22/WG21 (HTML rendering of the working draft))
- Package container/heap (The Go Authors)
- ECMAScript 2026 Language Specification: Number.MAX_SAFE_INTEGER (Ecma International)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress