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 into one ➔ each step asks only “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 comparison ➔ Merge Sort’s merge compares a[ia] against b[ib] — per output item.
  • lists ⟹ a minimum query ➔ “smaller of two” becomes “minimum of ”, which a linear scan answers in ⟹ total. 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 .
  • Loop ➔ get_min emits the next output item in ; then add that list’s successor, keeping the heap at size .
  • Total ➔ items × (one extract + 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 makes the successor lookup .

3. The Invariant That Proves It

  • Invariant ➔ at 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 one item from a finite total of and refills only from a strictly advancing index.
  • 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 ➔ split any sequence of length into lists of length (each trivially sorted) and merge them with . The output is the sorted sequence ⟹ the merge algorithm is a comparison sort, and beating would break the floor ➔ Sorting Problem.
  • Read the shape, not just the result ➔ 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 ➔ LO1.
  • The bound is on the comparison model only ➔ a non-comparison merge over bounded integer keys is not covered, 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 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 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; 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: a reduction with auxiliary space unchanged at . That bound is what permits to exceed memory: only records are ever resident, and the lists can be read as disk streams because the algorithm never looks back — a strategy loading all items cannot merge data larger than memory at any complexity ➔ Algorithmic Complexity.

⚠️ Common Mistakes

  • 💡 Quoting for the heap version ➔ the heap never holds more than items, so each operation is — the whole point is that .
  • 💡 Losing the list id ➔ storing bare values makes the refill 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