Quick Sort Pivot Selection Strategies
How the pivot choice decides whether Quick Sort runs in O(n log n) or collapses to O(n²): random, median-of-three, dual-pivot and three-way partitioning.
On this page +
Quick Sort is the fastest comparison sort in practice and also the one with the most notorious failure mode. Both facts come from the same line of code: how it chooses the pivot. Choose well and every partition splits the array roughly in half, giving O(n log n). Choose badly and each partition peels off a single element, giving O(n²) — and the input that triggers it is the most common input imaginable, an already-sorted array.
This guide walks through every pivot strategy in use, shows exactly which input breaks each one, and explains what production implementations do about it. It is also one of the highest-yield interview topics on this site, because "how would you avoid Quick Sort's worst case?" is asked constantly.
Why the pivot decides everything #
Quick Sort's cost is the depth of its recursion multiplied by the O(n) work done at each level.
A pivot near the median splits n into two halves. Halving repeatedly reaches size 1 after about log₂n levels, so 1,000,000 elements need about 20 levels: 20 × n work, i.e. O(n log n).
A pivot that is the smallest or largest element splits n into a part of size 0 and a part of size n−1. Now you need n levels, giving n × n work: O(n²). For 1,000,000 elements that is roughly a trillion operations instead of 20 million — a fifty-thousand-fold difference produced by nothing but pivot choice.
Reassuringly, the split does not have to be good, only not-catastrophic. Even a consistent 90/10 split still yields O(n log n), just with a larger constant, because reducing the problem to 90% of its size repeatedly still takes a logarithmic number of steps. The pivot rule only needs to make the terrible case unlikely, not guarantee the perfect one.
Strategy 1 — first or last element (don't) #
The simplest rule, used in nearly every textbook because it makes the partition code short: take A[hi] as the pivot.
procedure partition(A, lo, hi):
pivot = A[hi] // last element
i = lo - 1
for j = lo to hi - 1:
if A[j] <= pivot:
i = i + 1
swap(A[i], A[j])
swap(A[i + 1], A[hi])
return i + 1Its failure is severe and its trigger is embarrassing. On an already-sorted array the last element is the maximum, so everything goes to the left partition and nothing to the right: O(n²). The same happens on a reverse-sorted array.
This matters because sorted or nearly-sorted input is extremely common — data straight out of a database with an ORDER BY, a list being re-sorted after a couple of appends, timestamps. The naive pivot is worst precisely where real data most often lives. Never ship it.
Strategy 2 — random pivot #
Pick a uniformly random index in the range and swap it to the end, then partition as usual:
procedure quickSort(A, lo, hi):
if lo < hi:
r = random integer in [lo, hi]
swap(A[r], A[hi]) // randomised pivot
p = partition(A, lo, hi)
quickSort(A, lo, p - 1)
quickSort(A, p + 1, hi)This is a genuine fix, and the reason is subtle but important: the worst case no longer belongs to any particular input. With a fixed rule, an adversary (or an unlucky data source) can hand you the one array that breaks it. With randomisation, no input is bad — only an unlucky sequence of coin flips is, and the probability of enough consecutive bad flips to matter is astronomically small. Expected time is O(n log n) for every input.
It is not free: generating random numbers costs something, and the branch is unpredictable. But it is the standard defence, and it is the answer interviewers are usually looking for.
Strategy 3 — median-of-three #
Look at the first, middle and last elements, take the median of those three, and use it as the pivot.
procedure medianOfThree(A, lo, hi):
mid = lo + (hi - lo) / 2
// order the three positions, then use the middle one
if A[lo] > A[mid]: swap(A[lo], A[mid])
if A[lo] > A[hi]: swap(A[lo], A[hi])
if A[mid] > A[hi]: swap(A[mid], A[hi])
return mid // A[mid] is the medianThis is the best practical heuristic without randomness, and it has a very useful property: on sorted input the middle element is the true median, so the pathological case for the naive pivot becomes the best case here. Since sorted-ish data is the common real-world hazard, median-of-three removes most of the risk for free, with no random number generator and better branch behaviour.
Its limitation is that it remains deterministic, so a determined adversary can still construct a killer input (there is a known "median-of-three killer" sequence). For sorting untrusted, attacker-controlled data, prefer randomisation or a depth limit. For ordinary data, median-of-three is excellent. Larger implementations extend the idea — sampling five or nine elements, or the "ninther" (median of three medians of three) — as arrays get bigger.
Strategy 4 — dual-pivot, and three-way partitioning #
Two refinements change the partition itself rather than the pivot choice.
Dual-pivot Quick Sort (Vladimir Yaroslavskiy, 2009) picks two pivots and splits into three regions: less than P1, between P1 and P2, and greater than P2. Fewer levels of recursion and better cache behaviour make it measurably faster on real hardware, which is why Java uses it for Arrays.sort() on primitive arrays.
Three-way partitioning (the Dutch national flag partition) solves a different problem: many duplicate keys. Standard two-way partitioning puts elements equal to the pivot on one side and then recurses over them again, so an array of all-identical values degenerates to O(n²). Three-way partitioning separates elements into less than, equal to and greater than, and never recurses into the equal block — because those elements are already in their final positions. On data with few distinct values this turns a quadratic disaster into something close to linear.
This is worth remembering, because duplicate-heavy data is very common: sorting a million records by a status field with eight possible values will hit it immediately. (Though as the decision guide notes, that particular shape is also Counting Sort's ideal case.)
What production actually does #
Real libraries combine defences rather than relying on one.
- C++
std::sortuses Introsort: median-of-three-style pivoting, plus a recursion-depth counter. If depth exceeds roughly 2·log n, it stops trusting the pivots entirely and switches that sub-problem to Heap Sort, which converts a possible O(n²) into a guaranteed O(n log n). - Java uses dual-pivot Quick Sort for primitives, with introspective checks for structured input, and stable Tim Sort for objects.
- Rust's
sort_unstableuses pattern-defeating quicksort (pdqsort), which detects patterns such as already-sorted or many-duplicate input and adapts, falling back to Heap Sort when it detects adversarial behaviour.
The shared design idea is the one worth taking away: do not try to pick a pivot that can never be bad — detect that things are going badly and change strategy. A cheap runtime check plus a fallback is more robust than any amount of cleverness in the pivot rule.
To see the effect directly, open the visualiser, run Quick Sort on a random array and watch the comparison counter, then run it on an already-sorted array. The difference in the counter is the pivot problem.
Frequently asked questions
What is the worst case input for Quick Sort? +
How do you avoid Quick Sort's worst case? +
Is median-of-three better than a random pivot? +
What is three-way partitioning and when do I need it? +
Why does Java use dual-pivot Quick Sort? +
See it in motion
Watch this algorithm and nine others run step by step, with live pseudocode and comparison counters.
Launch the visualiser
Software engineer at a stealth-stage startup, and previously a front-end engineer for around a year and a half.