Partitioning (Quicksort)

Context: FIT2004_MOC · the engine inside Quick Sort and Quickselect — promoted out of Quick Sort because W4 examines the schemes themselves Parent Framework: Quick Sort

Quick Revision

  • 🎯 Objective: rearrange a range so ≤ pivot sits left and > pivot sits right ➔ the pivot lands at its final sorted index and is excluded from every later call.
  • 📦 Core Components: Out-of-place ➔ two temp lists, extra | Hoare’s ➔ in-place, swap per item | Lomuto’s ➔ in-place, more swaps, simpler | Dutch flag ➔ 3-way, retires the block.
  • ⚡ Key Constraint: the lecture’s three goals — in-place · fast · stable — are never all three at once; every scheme taught is unstable, and the reason differs per scheme.

📝 How It Works

1. The Shared Contract

  • The invariant being established ➔ left ≤ pivot, right > pivot — the equality goes left, and that single choice is what costs stability in every in-place scheme.
  • The payoff ➔ the pivot is provably final ⟹ both recursive calls skip it. Merge Sort has no equivalent free element; this is quicksort’s “trivial combine”.
  • Cost floor is identical ➔ every scheme touches all items once ⟹ comparisons per call. Schemes differ only in writes and space — so “which partition is better” is always a swap-count or memory question ➔ Sorting Problem.

2. Out-of-place Partitioning (the unit notes’ “naive 3-way partitioning”, §3.2)

  • Mechanism ➔ scan left→right appending each item to a left (), equal () and right () buffer, then concatenate and copy back.
  • Two buckets vs three ➔ the two-bucket form ( / ) is the one the lecture calls unstable; the naive 3-way form keeps the bucket, restoring stability and grouping duplicates so the recursion can skip them.
  • Space ➔ additional space beside the recursion stack ⟹ surrenders quicksort’s only structural advantage over Merge Sort.
  • Why the two-bucket form is NOT stable ➔ ≤ pivot sends every item equal to the pivot into the left buffer, i.e. in front of the pivot even when it started behind it ⟹ the pivot reorders its own duplicates. A third buffer repairs it — lecturer’s framing: anything can be made stable with more memory ➔ Sorting Problem §5.

3. Hoare’s In-place Partitioning (the version taught in this unit)

  • Setup ➔ swap the pivot to the front; set L_bad to the second slot and R_bad to the last.
  • Loop ➔ advance L_bad right to the first bad element ( pivot) · retreat R_bad left to the first bad element ( pivot) · swap that pair · repeat until the pointers cross.
  • Finish ➔ swap the parked pivot into R_bad ⟹ under this variant R_bad is the pivot’s final index, like Lomuto’s return value.
  • Invariants ➔ everything left of L_bad is · everything right of R_bad is · the band [L_bad … R_bad] is unprocessed and empties when the pointers cross.
  • Why it is the fast one ➔ each item is swapped at most once (only the pivot moves twice) because one swap repairs two misplaced items ⟹ roughly fewer writes than Lomuto’s.
  • Stability ➔ the pointer conditions can be tightened into a stable rule, but the final swap of the pivot from the front to R_bad jumps it over everything in between and destroys it ⟹ not stable.
  • Lecturer-flagged fragility ➔ the naive write-up has edge cases where the final pivot swap must not fire, plus a further special case needing an extra guard. Hoare’s is the scheme you get wrong; Lomuto’s is the one you get right.

4. Lomuto’s In-place Partitioning

  • Mechanism ➔ one left-to-right scan; boundary marks the end of the ≤ pivot run; every smaller item is swapped up to boundary; a final swap drops the pivot onto boundary.
  • Contract ➔ returns the pivot’s final index ⟹ recurse (start, j-1) and (j+1, end).
  • Verdict from the lecture ➔ in place · writes far more — every item below the pivot is swapped even when already on the correct side · still unstable ⟹ “worse than Hoare’s”.
  • Why it survives anyway ➔ easier to understand and implement, and free of Hoare’s edge cases — the FIT1008 version, and the one to write under time pressure.

5. Dutch National Flag — the In-place 3-way Partition

  • Where the idea came from ➔ chasing stability exposed the real waste: the block is already final, so re-sorting it is pure loss. Split into and both recursive calls skip the middle.
  • Pointers ➔ boundary1 (end of the prefix) · j (scan cursor) · boundary2 (start of the suffix); the loop runs while j <= boundary2.
  • Invariants ➔ [1 … boundary1-1] is · [boundary1 … j-1] is · [boundary2+1 … N] is · [j … boundary2] is unprocessed, empty exactly when the loop exits.
  • Returns two boundaries ➔ quicksort recurses on [start … boundary1-1] and [boundary2+1 … end] only.
  • Payoff ➔ with distinct keys the depth is , not ; the all-duplicates input drops from to .
  • Two lecturer-flagged traps ➔ the code changes depending on whether boundary1/boundary2 are inclusive or exclusive — commit to one and state it · and one specific case still fails without an extra if/else inside the loop.
  • Still not stable ➔ the branch swaps across long distances; a 3-way split fixes work, never order.

6. -Partitioning — the Generalisation (applied W4)

  • The problem ➔ given pivots (not supplied sorted), rearrange so every item comes first, then everything above and at most , and so on — ordinary partitioning is .
  • Step 0 always ➔ sort the pivots, ; dominated because .
  • Naive: ➔ 2-way partition on , then partition the suffix on , and so on ➔ calls of up to .
  • Divide and conquer: ➔ partition the whole array on the middle pivot , then recurse left with the lower pivots and right with the upper ➔ levels (Divide and Conquer).
  • Why it is optimal — prove by reduction ➔ take all elements as pivots (); -partitioning then sorts the array, so beating would beat the comparison model’s floor ➔ .
  • The transferable move ➔ identical in shape to K-way Merge’s upgrade: a linear sweep over things becomes logarithmic in once a balanced structure is imposed — D&C here, a min-Heap there.

⚙️ Core Implementation

🔹 Out-of-place — stable only with a third buffer

🔹 Hoare’s — swap once per item, pivot parked at the front

🔹 Lomuto’s — one pointer, more writes

🔹 Dutch national flag — 3-way, returns two boundaries

🔹 -partitioning — D&C,

⚖️ Core Decision Matrix

SchemeIn-placeWrites per callExtra spaceReturnsStablePick it when
Out-of-placeNo copies + stackpivot’s final indexNo · Yes with a 3rd bufferstability is contractual and memory is free
Hoare’sYes swap per itempivot’s final index (this unit)Norecords are large ⟹ writes dominate
Lomuto’sYes1 swap per item , many redundantpivot’s final indexNowriting it correctly under exam time
Dutch flag (3-way)Yes1 swap per misplaced itemtwo boundariesNoduplicate keys are common ⟹

When It Flips: integer keys ⟹ the swap-count gap between Hoare's and Lomuto's is invisible, so write Lomuto's. Large records, or a stated write/IO cost ⟹ Hoare's write saving is the whole answer. Once the key domain is small (grades, flags, three sensor states) the argument stops being about swaps at all and the 3-way split changes the complexity class ➔ Quick Sort §2.

📊 Exam Execution Trace

Manual Execution Trace

Hoare’s on the lecture’s array [2, 8, 6, 4, 1, 7, 3, 5], pivot , 1-indexed:

StepL_bad stops atR_bad stops atActionArray payload
0 (park)——swap pivot to front[4, 8, 6, 2, 1, 7, 3, 5]
1 (8 ) (3 )swap[4, 3, 6, 2, 1, 7, 8, 5]
2 (6 ) (1 )swap[4, 3, 1, 2, 6, 7, 8, 5]
3 (6 ) (2 )crossed ⟹ exit[4, 3, 1, 2, 6, 7, 8, 5]
4 (finish)—swap pivot into R_bad[2, 3, 1, 4, 6, 7, 8, 5]

Read the result: sits at index — its final sorted position — after only swaps for items, each moved item moving exactly once. Lomuto’s on the same input writes every one of the three items below the pivot plus the two pivot swaps.

Manual Execution Trace (prep W4 — duplicates, both schemes)

[7, 1, 12, 9, 3, 3, 10, 6, 7, 14, 4], pivot (the first element).

Naive 3-way (out-of-place). One pass builds three buckets, then concatenates: = [1, 3, 3, 6, 4] · = [7, 7] · = [12, 9, 10, 14] ⟹ [1, 3, 3, 6, 4, 7, 7, 12, 9, 10, 14] — the pivot’s duplicates sit together, so quicksort recurses on the two outer blocks only.

Hoare’s, in-place. lo advances to a value ; hi retreats to a value ; swap; stop when they cross; finally swap the parked pivot with hi:

Steplo athi atActionArray
0——pivot already at the front[7, 1, 12, 9, 3, 3, 10, 6, 7, 14, 4]
(12) (4)swap[7, 1, 4, 9, 3, 3, 10, 6, 7, 14, 12]
(9) (7)swap[7, 1, 4, 7, 3, 3, 10, 6, 9, 14, 12]
(10) (6)swap[7, 1, 4, 7, 3, 3, 6, 10, 9, 14, 12]
crossed ⟹ exit[7, 1, 4, 7, 3, 3, 6, 10, 9, 14, 12]
—swap pivot into hi[6, 1, 4, 7, 3, 3, 7, 10, 9, 14, 12]

Read the result: the pivot lands at index in swaps. But the duplicate (originally index ) now sits at index — left of the pivot and not adjacent. Naive 3-way grouped the duplicates; Hoare’s scatters them, which is precisely the gap the Dutch national flag scheme closes.

  • Convention warning ➔ the prep sheet’s hi stops on while §3’s R_bad stops on ; both are correct schemes but they produce different arrays. State which stopping rule you use before the first swap.

⚠️ Common Mistakes

  • 💡 Assuming one universal Hoare contract ➔ this unit’s variant parks the pivot and swaps it to R_bad, so R_bad is the pivot’s final index ⟹ recurse (start, j-1) and (j+1, end). The textbook Hoare partition never parks the pivot and returns a split point that must be included on the left ⟹ (start, j) and (j+1, end). Copying the wrong call shape silently drops or duplicates an element — state which contract you are using before you recurse.
  • 💡 Claiming a partition is “swaps” ➔ it is comparisons; swaps are but the constant is the entire point of comparing schemes.
  • 💡 Calling the 3-way split a stability fix ➔ it was motivated by the stability discussion but delivers work reduction; the equal block is skipped, not preserved in order.

🧠 Active Recall