Quick sort is a randomized divide-and-conquer sorting algorithm. Pick a pivot element, partition the array into elements less than and greater than the pivot, then recurse on each partition. Average , in-place, and consistently fast on real data — making it the most widely-used general-purpose sort.
The structure:
- Divide: pick a pivot . Partition the array into three groups: less than (L), equal to (E), greater than (G).
- Conquer (recur): sort L and G recursively.
- Combine: concatenate L, E, G — the array is now sorted.
The “equal” group (E) is just the pivot itself; you don’t need to recurse on it.
Implementation
There are two clean partition schemes — both correct, both with their own atomic notes.
- Lomuto partition — single boundary pointer, simpler invariants. Pivot ends at the returned index. About swaps per partition. The version most students see first.
- Hoare partition — two pointers walking inward. Returned index is not the pivot’s position. About swaps per partition. Handles duplicates much better. Tony Hoare’s original.
Both partition schemes are subroutines of quicksort: pick a pivot, partition, recurse on each side. The recursive set-up differs:
// With Lomuto: pivot is at p, exclude it from both sides
quicksort(arr, lo, p - 1);
quicksort(arr, p + 1, hi);
// With Hoare: returned index is a split point, not a position
quicksort_hoare(arr, lo, p); // includes p
quicksort_hoare(arr, p + 1, hi);Mixing up the recursion form is the classic Hoare-implementation bug. See Hoare partition for the worked-trace and Lomuto partition for the simpler version with full code.


Big O
- Best case: — pivot always lands in the middle, so each recursive call halves the problem. Recursion depth is , work per level is .
- Average case: — for random pivot choices on random data.
- Worst case: — pivot always lands at one extreme (e.g., always picking the first element on already-sorted input). Recursion depth becomes .
The worst case is a real concern. Mitigations:
- Randomize the pivot: pick a random index instead of always the first. Adversarial inputs become unlikely.
- Median of three: take the median of
arr[first],arr[mid],arr[last]as the pivot. Avoids worst-case behavior on common patterns (sorted, reverse-sorted). - Switch to heap sort on deep recursion: introsort uses quicksort but switches to heap sort once recursion depth exceeds about — guaranteed worst case.
Why it’s fast in practice
Despite the worst case, quicksort beats merge sort and heap sort on real workloads. Reasons:
- Cache friendly: in-place partition keeps data contiguous in memory.
- Few swaps: typical partition does fewer swaps than other sorts.
- Tail call elimination: the second recursive call can be done iteratively, saving stack frames.
Real-world sorting (C qsort, C++ std::sort, Python sort for primitives, Java Arrays.sort for primitives) is almost always quicksort or a variant.
In-place
Quicksort uses extra space (for the recursion stack). The array itself is sorted in place — no auxiliary array needed, unlike Merge sort which needs extra.
Stability
Quicksort is not stable — swaps during partition can change the relative order of equal elements. If stability is needed, Merge sort is the standard choice.
Comparison with other O(n log n) sorts
| Sort | Average | Worst | Space | Stable |
|---|---|---|---|---|
| Merge sort | Yes | |||
| Heap sort | No | |||
| Quick sort | No |
Quick sort dominates when memory matters and worst case isn’t a hard requirement. Merge sort wins for stability and guaranteed worst case. Heap sort is the choice for guaranteed worst case + space.
For divide-and-conquer with deterministic worst case, see Merge sort. For the simpler quadratic sorts, see Selection sort and Bubble sort.