Analysing Quicksort

Quicksort is the algorithm that shouldn't work. Its worst case is a quadratic catastrophe, it can be toppled by an already-sorted array, and yet it is, in practice, the fastest general comparison sort we have — the one hiding inside your language's standard library. Understanding why is a masterclass in the whole toolkit of this course: recurrences, the difference between worst and average case, indicator random variables, and the almost magical effect of a single random choice.

We will pin down three numbers: the worst case (\Theta(n^2)), the best case (\Theta(n\log n)), and — the crown jewel — the expected case (\Theta(n\log n)), proved with an argument so clean it fits on a postcard. Then we see how a random pivot makes that expected bound hold on every input, promoting quicksort from a gamble on nice data to a robust randomised algorithm.

Recap: partition, then conquer

Quicksort is divide-and-conquer with all the work in the divide step. Pick a pivot, partition the array so everything smaller sits left of it and everything larger sits right, then recurse on the two sides. The pivot lands in its final sorted position, and — unlike merge sort — there is no combine step at all.

function quicksort(a: number[], lo = 0, hi = a.length - 1): void { if (lo >= hi) return; const p = partition(a, lo, hi); // p is the pivot's final index quicksort(a, lo, p - 1); // conquer the smaller elements quicksort(a, p + 1, hi); // conquer the larger elements }

A single partition over m elements does m-1 comparisons — each non-pivot element is compared once to the pivot. So the entire running time is governed by one quantity: the total number of pivot-versus-element comparisons across all recursive calls. Count those and you have analysed quicksort.

The two extremes: why the pivot is everything

Each partition splits an array of size m into pieces of size i and m-1-i, depending on the pivot's rank. The recursion tree's shape — and thus the total work — hangs entirely on that split.

The picture below is the balanced recursion tree. Read it level by level: every level partitions a collection of subarrays whose sizes sum to n, so each level costs \Theta(n); and because balanced splits halve the size, there are only \log_2 n levels. Width times height: n \times \log n.

The genius of the average-case argument is that you need nowhere near a perfect median for this picture to hold up to constants. Even a wildly lopsided 1:9 split every time still gives depth \log_{10/9} n = \Theta(\log n). Only the truly pathological, near-total imbalance produces n^2 — and that is astronomically unlikely with random pivots.

The postcard proof: expected comparisons via indicators

Here is the argument every algorithms course builds toward. Rename the elements by rank: let z_1 < z_2 < \dots < z_n be the sorted order. Define the indicator

X_{ij} = \mathbf{1}\big[\,z_i \text{ and } z_j \text{ are ever directly compared}\,\big], \qquad i < j.

Two elements are compared only when one of them is chosen as pivot while both are still in the same subarray — quicksort never compares two elements twice. The total number of comparisons is X = \sum_{i < j} X_{ij}, and by linearity of expectation, \mathbb{E}[X] = \sum_{i < j}\Pr[z_i,z_j\text{ compared}].

Now just sum. Substituting k = j - i:

\mathbb{E}[X] = \sum_{i=1}^{n-1}\sum_{j=i+1}^{n}\frac{2}{j-i+1} \;=\; \sum_{i=1}^{n-1}\sum_{k=1}^{n-i}\frac{2}{k+1} \;<\; \sum_{i=1}^{n} 2H_n \;=\; 2nH_n \approx 2n\ln n.

And there it is: \mathbb{E}[X] = 2n\ln n + \Theta(n) = \Theta(n\log n). The same harmonic number H_n from the hiring problem, doing the heavy lifting again. Three lines, no recurrence, no master theorem — just indicators and linearity.

Why randomising the pivot fixes everything

The proof above quietly assumed a random pivot (each element equally likely to be picked first). With a fixed pivot rule — "always take the first element" — the 2n\ln n bound is an average over random input orderings, and a sorted input destroys it. Randomising the pivot moves the randomness from the input (which the adversary controls) to the coins (which they don't):

This is the whole reason randomised quicksort is a Las Vegas algorithm — always correct, with a running time that is \Theta(n\log n) in expectation on any input at all. The single line "choose the pivot at random" converts a fragile average-case promise into a robust worst-case-input guarantee.

Both do \Theta(n\log n) comparisons, yet quicksort typically wins in wall-clock time. Two reasons, both about the machine rather than the maths. First, quicksort sorts in place with tiny constant overhead per comparison, while merge sort shuttles data through an auxiliary array, paying for extra memory traffic. Second, partitioning is exquisitely cache-friendly — it sweeps the array in two contiguous scans, exactly the access pattern modern memory hierarchies reward. The hidden constant in \Theta(\cdot), which asymptotic analysis deliberately discards, is where quicksort quietly banks its win. A reminder that \Theta is the beginning of a performance story, not the end.

A common muddle: students say randomised quicksort is \Theta(n\log n) "on average, so on a random input". That conflates two different randomisations. Deterministic quicksort's average is over random inputs — and a specific input (sorted) is genuinely, reliably terrible. Randomised quicksort's expectation is over the algorithm's own coins, and holds for a fixed, arbitrary input — there is no bad input, and re-running on the same array gives a fresh, independent draw. Also beware confusing "expected \Theta(n\log n)" with "\Theta(n\log n) with certainty": the worst case is still n^2; it is merely astronomically improbable. Expectation is a promise about the average of many runs, not a guarantee about any single one.