Quick Sort

Context: FIT1008_MOC · a Divide and Conquer sort · solves the Sorting Problem · uses an Auxiliary Function (Recursion) · contrast with Merge Sort; falls back to Insertion Sort for tiny arrays FIT2004 emphasis: the three W3 design questions — how to partition efficiently (Lomuto vs Hoare vs 3-way), how to choose a good pivot (fixed → median-of-three → random → Median of Medians), and how to ensure the worst case never occurs · the same partition drives Quickselect

Quick Revision

  • 🎯 Objective: partition around a pivot, recurse both sides ➔ heavy-split / trivial-combine D&C sort, no merge.
  • 📦 Core Components: Partition ➔ smaller left, larger right | Pivot ➔ lands final | Recurse ➔ both sides.
  • ⚡ Key Constraint: in-place + smallest constant ➔ but on bad pivots; pivot choice is the whole game — and only a Median of Medians pivot converts “improbable” into “impossible”.

📝 Core

1. The Algorithm (Partition → Recurse)

  • Non-trivial split ➔ partition: smaller elements left of pivot, larger right.
  • Trivial combine ➔ pivot already final ➔ just sort each side (no merge).
  • In-placepartition’s returned boundary is final, excluded from both recursive calls.
  • Mirror of Merge Sort ➔ merge sort splits trivially and combines expensively; quicksort splits expensively and combines for free. Both pay per level — the difference is where.

2. Partitioning Efficiently

  • Lomuto (single pointer) ➔ one left-to-right scan, boundary marks the end of the “smaller” run, one swap per smaller element ⟹ simplest to write and to trace, but performs more swaps than necessary.
  • Hoare (two pointers) ➔ pointers walk inward from both ends and swap only out-of-place pairs ⟹ roughly fewer swaps; the catch is that it returns a split point, not the pivot’s final index, so the recursive calls are (start, j) and (j+1, end) — never j-1.
  • 3-way / Dutch national flag ➔ partition into , , and recurse only on the outer two ⟹ the all-duplicates input drops from to ; the standard fix when the key domain is small.
  • The efficiency question is about swaps, not comparisons ➔ all three schemes make comparisons per level; they differ in writes, which is what matters when records are large ➔ Sorting Problem.

3. Pivot Selection

  • Balanced split ➔ median-ish pivot ➔ levels × = .
  • Degenerate split ➔ fixed pivot on sorted input peels off one element ➔ depth , .
  • Balance need not be perfect ➔ even a fixed split gives depth ⟹ still ; only a split that is lopsided by a constant number of elements at every level reaches .
Pivot policyCost to chooseWorst caseTriggered byVerdict
First / last elementalready-sorted inputnever — sorted input is the common case, not a rare one
Middle elementorgan-pipe / crafted inputacceptable for coursework; still constructible
Median-of-threea crafted “killer” sequencethe practical library default
Random + RNG, probability an unlucky run onlyremoves the adversary, not the possibility
Median of Medians per levelnothingwhen a guarantee is contractually required

4. Ensuring the Worst Case Never Occurs

  • Randomisation is a probability claim ➔ it makes the input impossible to construct in advance, since the split no longer depends on the input’s arrangement; it does not bound any individual run.
  • Median of Medians is a guarantee ➔ a provable split at every level ⟹ -style balance ⟹ worst case, at a constant factor large enough that practice still prefers random pivots.
  • Introsort — the engineering answer ➔ run randomised quicksort but count the recursion depth; if it exceeds , abandon and finish with Heapsort worst case at quicksort’s average constant. (🔭 Beyond the lecture — not in the slides; named only because it is what real libraries ship.)
  • The exam framing ➔ “how do you ensure the worst case never occurs?” is asking you to distinguish expected from worst-case bounds, then name a mechanism that upgrades one to the other ➔ Algorithmic Complexity.

5. Bounding Stack Space

  • Risk ➔ naïve recursion stacks frames on a degenerate split — this is why quicksort’s auxiliary space is in the worst case, not .
  • Fix ➔ recurse smaller partition first, loop (tail-call) on larger ➔ depth space in every case, independent of pivot quality.
  • Not in-place either way ➔ in-place auxiliary, and live frames count; only Heapsort achieves among the sorts ➔ Sorting Problem.

6. Stability

  • Unstable as written ➔ partition swaps across long distances, throwing equal keys past one another — the same mechanism that makes selection sort unstable.
  • “Depends on the partition” ➔ an out-of-place partition that copies items into two buffers in input order is stable, at extra space per level; the in-place swap-based schemes are not.
  • Universal fallback ➔ index-tagging works here as on any comparison sort, at space and no change to the time bound ➔ Sorting Problem §6 owns the mechanism.

⚙️ Core Implementation

🔹 quick_sort + Lomuto partition

🔹 Hoare partition — fewer swaps, different contract

⚖️ Core Decision Matrix

(Domain A complexity table — Best / Average / Worst Time, Space, Stability.)

AlgorithmBestAverageWorstAuxiliary spaceStableTrait
Quick Sort smaller-first · naiveNoin-place, fastest in practice
Quick Sort Median of MediansNoworst case eliminated, large constant
Merge SortYesguaranteed + stable, scratch
HeapsortNoin-place + guaranteed
Quickselect iterativeone rank only, not a sort

When It Flips: quicksort's no-merge, cache-friendly, in-place partition gives the smallest constant among sorts — choose it unless a worst-case bound (Heapsort) or stability (Merge Sort) is required. A quicksort run mirrors the BST shape of inserting the same pivots. Switch to a 3-way partition once duplicate keys are common, and to Quickselect the moment only one rank is wanted rather than the whole order.

📊 Exam Execution Trace

Manual Execution Trace

partition([7,2,9,1,5]), pivot = array[mid] = 9 (parked at start); all others < 9 so boundary advances to the end:

Step / StateTrigger Oparray[k]< pivot(9)?boundaryArray Payload
0 (Init)park pivot0[9,2,7,1,5]
1scan2yes1[9,2,7,1,5]
2scan7yes2[9,2,7,1,5]
3scan1yes3[9,2,7,1,5]
4scan5yes4[9,2,7,1,5]
5place pivot4[5,2,7,1,9] (9 final)

Read the split: the pivot landed at index of , i.e. a partition — the degenerate case in miniature. One such level is harmless; of them in a row is the worst case.

Applied Exercise

Problem: Derive quicksort’s average and worst-case recurrences, then show that a fixed split is still . Derivation Proof / Hand-Calculation Walkthrough:

Final Extracted Output: expected , worst — and the boundary between them is constant-fraction vs constant-size splits, not “balanced vs unbalanced”. Any split that removes a fixed proportion keeps the depth logarithmic.

🧠 Active Recall