Quick Sort
Imagine organizing a crowd of people by height. You pick one person — the pivot — and ask everyone to stand left if they're shorter, right if they're taller. Now the pivot is in their exact final position. Repeat the process on the left crowd and the right crowd separately. That's quicksort: pick, partition, recurse.
It's fast in practice because partitioning is a single fast pass through contiguous memory (cache-friendly!), and on average the pivot lands near the middle, giving balanced recursion.
The Partition Step
Lomuto partition (simpler to understand): Pick the last element as pivot. Walk a pointer i from the left; whenever you find an element ≤ pivot, swap it with position i and advance i. Finally swap the pivot into position i. Easy to write, slightly more swaps.
Hoare partition (faster in practice): Two pointers start at opposite ends. Left pointer advances while it sees elements ≤ pivot; right pointer retreats while it sees elements ≥ pivot. When both stall, swap them and continue until the pointers cross. About 3× fewer swaps than Lomuto on random input.
Pivot Choice — Everything Depends on This
First or last element: Catastrophically bad on already-sorted or reverse-sorted input. Every partition produces one empty side → \(O(n^2)\) depth.
Random pivot: Pick a random index and swap it into position before partitioning. Randomness makes the \(O(n^2)\) worst case astronomically unlikely — the probability of consistently bad pivots is (2/n)^n.
Median-of-three: Look at first, middle, and last elements; use the median as pivot. Cheap, eliminates the sorted-input pathology, no random number needed.
Average Case \(O(n \log n)\) — Why It Works
With a random pivot, a pivot that lands in the middle 50% of values (which happens with probability 1/2) creates a split no worse than 25%-75%. The resulting recursion depth is still \(O(\log n)\) in expectation. More precisely, the expected total comparisons is about 2n ln n ≈ 1.39 n \(\log_2\)n — roughly 39% more comparisons than merge sort's n \(\log_2\)n, but with much better cache behavior in practice.
Introsort — The Real-World Best of Both
All major standard libraries use Introsort: start with quicksort, monitor recursion depth, and switch to heapsort if depth exceeds 2·log n. This gives \(O(n \log n)\) worst case while keeping quicksort's excellent average performance. For small sub-arrays (n ≤ ~32), they also switch to insertion sort. C++ std::sort, Java's Arrays.sort for primitives, and Rust's slice::sort_unstable all use Introsort variants.