skip to content

In a heap-based k-way merge, which two boundary conditions crash the naive loop?

level: middleimportance: should knowfreq 48%

answer

  1. what if a source starts out empty?
  2. every source runs out exactly once
  3. two array reads can go past the end
  4. the seed loop may insert fewer than k entries
  5. let the heap shrink instead of forcing a push

basics

~20 s

Seeding from a source that is empty, and refilling from a source that has just run out. Both read past the end of a sequence. Guard the seed loop with an emptiness check and the refill with a has-next check.

solid answer

~50 s

Two reads can go out of bounds. First, at initialisation: the seed loop takes each source's first element, and a source that arrived empty has none — skip it rather than inserting anything, and never assume the heap starts with exactly k entries. Second, mid-merge: after extracting an entry from source i, the refill step fetches source i's next element, and that source may have just been drained — push only if a successor exists, and let the heap shrink otherwise. Get both right and termination takes care of itself: the loop runs while the heap is non-empty, which happens exactly when every source has been drained and every element emitted. A third, quieter bug is counting emitted elements against a precomputed total instead of watching the heap, which desynchronises the moment any source is shorter than assumed.

code

pseudocode · 7 lines
pseudocode
h = new min-heap
for i in 0..k-1:
    insert(h, (src[i][0], i, 0))
while size(h) > 0:
    (v, i, p) = extract_min(h)
    emit(v)
    insert(h, (src[i][p+1], i, p+1))

go deeper

for a junior

Remember the two guards: skip empty sources when seeding the heap, and push a successor only when the source still has one. Know that the loop ends when the heap is empty.

for a middle

Explain when each guard fires — the exhaustion branch runs exactly once per source — and why the empty-source guard means the heap may start with fewer than k entries. Justify heap-emptiness as the termination condition.

for a senior

Show the failure modes in production terms: a silently truncated feed from a miscounted loop is worse than a crash, and a rescan-for-successor patch quietly restores the linear-in-k cost. Say how you would test the drain order.

for a principal

Own the standard the team codes against: guards over sentinels because sentinels smuggle in a key-range assumption, and structural termination over counters. Decide when a shared, tested merge primitive beats each team writing the loop again.

## The fragment The merge loop is short enough that candidates write it confidently and then get cut on the edges. The version below merges k timestamp-sorted log sources into one feed, and it is wrong in two specific places: ``` h = new min-heap for i in 0..k-1: insert(h, (src[i][0], i, 0)) while size(h) > 0: (v, i, p) = extract_min(h) emit(v) insert(h, (src[i][p+1], i, p+1)) ``` ## Bug one: the empty source at seeding `src[i][0]` assumes every source has a first element. In a fleet of 200 servers, at least one has been idle since the last rotation and its log is empty. Reading element 0 of an empty sequence is an out-of-bounds read. The fix is a guard in the seed loop: insert only if the source is non-empty. The consequence is worth saying out loud — **the heap does not necessarily start with k entries**. Any later logic that assumes "the heap holds exactly k until sources start draining" is now wrong too. Candidates who patch only the read and leave that assumption elsewhere have half-fixed it. ## Bug two: the exhausted source at refill `src[i][p+1]` assumes source i still has an element after position p. It will not, for every source, exactly once — at the moment that source's last element is emitted. Because that happens k times over the run, this is not an exotic edge case; it is guaranteed. The fix is a guard on the refill: push the successor only if `p+1 < length(src[i])`. Otherwise push nothing and let the heap shrink by one. The heap's size therefore decreases monotonically once sources begin to drain, which is exactly what makes the loop terminate. ## Why termination then needs no extra bookkeeping With both guards in place, `while size(h) > 0` is a complete and correct termination condition. The heap holds one entry per source that still has unemitted data; when it empties, every source has been drained and every element emitted. No counter, no sentinel, no per-source "done" flags. That matters because the common third bug is replacing the heap check with a precomputed count: "I know there are N lines total, so I will loop N times." It works only while your idea of N is exact. Any source shorter than assumed and the loop reads past the end of the heap; any source longer and you truncate the feed silently — the worse failure, because it produces plausible output. ## The sentinel alternative, and why it is a trap here A classic trick in older merge code is to pad each source with a sentinel value larger than any real key, so the refill never needs a guard. With a heap that backfires: sentinels are ordinary entries, they get extracted like anything else, and unless the emit step filters them they appear in the output. You have traded a boundary check for a filtering step plus a new correctness assumption — that no real key can exceed the sentinel. With timestamps, someone's clock skew eventually violates it. Guarding the refill is simpler and has no such assumption. ## The related-but-different mistake: losing the source identifier The fragment stores `(value, source index, position)`, which is what makes both the refill and the guard expressible. A version that heaps bare values compiles and even produces correct-looking output for a while — you can still extract minima — but there is no way to advance the right source, so people patch it by re-scanning all k sources for the emitted value. That is `O(k)` per element, quietly turning `O(N log k)` back into `O(N*k)`, and it breaks outright on duplicate timestamps, where the re-scan cannot tell which source's copy was just emitted. ## What an interviewer is listening for 1. You name **both** guards without being prompted, and you know the second one fires k times, not rarely. 2. You notice that the empty-source guard invalidates the "heap always has k entries" assumption. 3. You use heap-emptiness for termination rather than a counter. 4. You keep the source identifier in the entry and can say what breaks without it. A candidate who writes the loop correctly but cannot say *when* the exhaustion branch is taken has memorised it; a candidate who says "once per source, and the last one empties the heap" has understood it.

  • How often does the exhausted-source branch actually fire during a full merge?
    Exactly once per source — k times in all — because every source is drained precisely once. It is not a rare edge case, which is why the guard belongs in the main loop rather than in an error path. The last of those k firings leaves the heap empty and ends the merge.
  • What terminates the loop once both guards are in place?
    The heap becoming empty, and nothing else. The heap holds one entry for every source with unemitted data, so an empty heap means every source is drained. Using a precomputed element count instead is fragile: an over-estimate reads past the end of the heap, and an under-estimate silently truncates the feed, which is the more dangerous failure because the output still looks plausible.
  • Why not pad each source with a sentinel key larger than any real value to avoid the refill guard?
    Because a heap treats a sentinel as an ordinary entry: it gets extracted and, unless the emit step filters it, lands in the output. You have replaced one boundary check with a filter plus the assumption that no real key can ever exceed the sentinel — an assumption that clock skew or an unexpected key range eventually breaks. The guard is cheaper and assumption-free.

saying these in an interview costs you the question

  • Assumes every source has at least one element
  • Refills unconditionally after every extraction
  • Treats source exhaustion as a rare edge case
  • Loops a precomputed element count instead of checking the heap
  • Heaps bare values and rescans sources to find the successor

context