K-way Merge

Context: FIT2004_MOC · Merge Sort’s combine step generalised from lists to · the exam shape is “the naive version is — make it better”, and the answer is an ADT swap to a min-Heap (LO3) · the engine of external sorting

Quick Revision

  • 🎯 Objective: merge sorted lists holding items total into one sorted list ➔ the only question each step asks is “which head is smallest?”, so the cost of answering it is the algorithm.
  • 📦 Core Components: linear scan of heads per item, total | min-Heap of heads per item, total.
  • ⚡ Key Constraint: the heap holds one item per list, never the whole input ➔ auxiliary space is and independent of , which is exactly why this merges runs too large for memory.

📝 How It Works

1. Why -way Is Harder Than -way

  • Two lists ⟹ one comparisonMerge Sort’s merge compares a[ia] against b[ib], emits the smaller, advances that index — per output item, overall.
  • lists ⟹ a minimum query ➔ “smaller of two” becomes “minimum of ”, which a linear scan answers in total. The merge is no longer free; finding the minimum is the bottleneck.
  • Reframe as an ADT problem ➔ repeated extract-min with insert between extractions is the definition of a Priority Queue (ADT); swapping the linear scan for the right ADT is the entire optimisation.

2. The Min-Heap Solution

  • Seed ➔ push the head of each list as a triple ⟹ heap size exactly .
  • Loopget_min emits the next output item in ; then add that list’s successor, keeping the heap at size .
  • Total items each paying one extract and one insert ⟹ time, auxiliary beyond the output.
  • Carrying the list id is mandatory ➔ without it you know what the minimum was but not which list to refill from; the index field is what makes the successor lookup .

3. The Invariant That Proves It

  • Invariantat iteration , the heap root is the correct value for output[i], because it is every item remaining in every list.
  • Why it holds ➔ each list is sorted, so a list’s unconsumed head is that list’s own minimum; the heap contains all heads; the root is the minimum of the heads ⟹ the minimum of everything unconsumed ➔ Invariant.
  • Termination ➔ each iteration removes exactly one item from a finite total of and refills only from a strictly advancing index ⟹ the heap empties after iterations.
  • Stability comes free ➔ break heap ties by list id and the merge is stable, which matters when -way merge is the combine step of a stable sort.

4. Is Optimal — the Reduction

  • Claim ➔ no comparison-based -way merge can beat .
  • The reduction ➔ take any sequence of length , split it into lists of length (each trivially sorted), and merge them with . The output is the sorted sequence ⟹ the merge algorithm is a comparison sort.
  • The contradiction ➔ comparison sorting is ; at a merge faster than would therefore break that floor ➔ Sorting Problem.
  • Read the shape, not just the result ➔ this is a lower bound by reduction: you inherit a known bound by showing the new problem can simulate the old one. The same move proves optimality claims across the unit, so drill the argument, not the answer ➔ LO1.
  • The bound is on the comparison model only ➔ a non-comparison merge over bounded integer keys is not covered by it, exactly as Counting Sort escapes the sorting floor.

⚙️ Core Implementation

🔹 Heap-backed -way merge

⚖️ Core Decision Matrix

StrategyTimeAuxiliary spaceSelection rule
Linear scan of the heads indices tiny (2–4) — is not worth the heap’s constant
Min-Heap of headsthe default; also the only option when the lists stream from disk
Divide & conquer (recursive halving) scratchlists are already resident and you want Merge Sort’s 2-way merge reused unchanged
Concatenate, then sortnever for sorted input — it discards the sortedness you were given
Sequential accumulate (merge one at a time)never — re-copies the growing accumulator times

When It Flips: the heap wins once in the constants, i.e. from roughly . Note that is Merge Sort when : seeding singleton lists reproduces exactly — the two algorithms are the same recurrence read from opposite ends.

📊 Exam Execution Trace & Applied Exercises

Manual Execution Trace

: , , . Heap shown as sorted contents for readability; only the root is actually ordered.

StepHeap (size )Root extractedRefilled fromOutput so far
0 (Seed)[]
1[1]
2[1,2]
3[1,2,3]
4[1,2,3,4]
5[1,2,3,4,5]

Invariant check at step 3: the root is the other heads and , and each head is everything left in its own list ⟹ is the global minimum of all unconsumed items, so output[2] = 3 is final.

Applied Exercise

Problem: An external sort produced sorted runs totalling records. Quantify the gain from replacing the linear-scan minimum with a heap.

Final Extracted Output: against — a reduction here, with auxiliary space unchanged at . The bound is what permits to exceed memory: only records are ever resident.

⚠️ Common Mistakes

  • 💡 Quoting for the heap version ➔ the heap never holds more than items, so each operation is , not — the whole point is that .
  • 💡 Losing the list id ➔ storing bare values makes the refill step ambiguous; the triple is not optional bookkeeping, it is what keeps the refill .
  • 💡 Reaching for build_heap ➔ bottom-up construction needs all heads up front, which is fine at seed time but useless mid-merge; the refills are one-at-a-time add calls ➔ Heap.

🧠 Active Recall