Hoare's Quickselect (FIND, 1961): Expected $\Theta(N)$ Selection
Given an unsorted array $A[0 \dots n-1]$ and a target rank $k \in [0, n-1]$, the selection problem asks to rearrange $A$ (like C++'s std::nth_element) such that $A[k]$ holds the element that would appear at index $k$ if $A$ were sorted, with $A[0 \dots k-1] \le A[k] \le A[k+1 \dots n-1]$.
Randomized Quickselect picks a random pivot, partitions $A[lo \dots hi]$ around it to land at index $p$, and branches three ways:
- If $k == p$: Done! Return $A[p]$.
- If $k < p$: Recurse (or loop) only on the left subarray $A[lo \dots p-1]$.
- If $k > p$: Recurse (or loop) only on the right subarray $A[p+1 \dots hi]$.
Because a random pivot lands in the middle $50\%$ of the current subarray (ranks $[n/4, 3n/4]$) with probability $1/2$, every $2$ expected partitions shrink the active subarray to at most $\frac{3}{4}$ of its previous size. Summing the geometric series gives:
$$\mathbb{E}[T(n)] \le \sum_{j=0}^{\infty} 2 \cdot c n \left(\frac{3}{4}\right)^j = 8cn = \Theta(n)$$More precisely, using indicator random variables $X_{ij}$, Quickselect performs at most $2(n + 1 + (k+1)\ln\frac{n}{k} + (n-k)\ln\frac{n}{n-k}) \le (2 + 2\ln 2)n \approx 3.386n$ expected comparisons to find the median, and $2n + o(n)$ to find the minimum or maximum.
Can we guarantee a good pivot deterministically in $O(n)$ worst-case time? Suppose we partition $n$ elements into $\lceil n/5 \rceil$ groups of $5$, sort each group of $5$ in $O(1)$ comparisons (at most 6 comparisons per group), and recursively call bfprt_select on the $\lceil n/5 \rceil$ group medians to find their exact median $x$ (the Median of Medians).
How many elements are guaranteed to be $\le x$ (and symmetrically $\ge x$)?
- At least half of the $\lceil n/5 \rceil$ group medians are $\le x$ (that is, $\ge \frac{1}{2}\lceil n/5 \rceil$ groups).
- In every such group of $5$ (ignoring the one partial group and the pivot's own group), the group median and the $2$ smaller elements in its column of $5$ are all $\le x$—contributing $3$ elements per group!
- Therefore, at least $3\left(\left\lceil \frac{1}{2}\left\lceil \frac{n}{5} \right\rceil \right\rceil - 2\right) \ge \frac{3n}{10} - 6$ elements are $\le x$, and symmetrically at least $\frac{3n}{10} - 6$ elements are $\ge x$.
- Consequently, whichever side of $x$ we recurse into after partitioning contains at most $\frac{7n}{10} + 6$ elements!
This yields the celebrated BFPRT recurrence:
$$T(n) \le \underbrace{T\!\left(\left\lceil \frac{n}{5} \right\rceil\right)}_{\text{find median of medians}} + \underbrace{T\!\left(\frac{7n}{10} + 6\right)}_{\text{recurse on survivor side}} + \underbrace{\Theta(n)}_{\text{5-sorts + partition}}$$Because $\frac{1}{5} + \frac{7}{10} = \frac{9}{10} < 1$, the work decays geometrically at every level of the recursion tree ($1 + 0.9 + 0.81 + \dots = 10$), proving by induction (or Akra-Bazzi with $p = 1$) that $T(n) \le 10cn = \Theta(n)$ in the strict worst case!
Why Groups of $5$? Why Do Groups of $3$ Fail While Groups of $7$ Work?
Inspect the 5-column BFPRT Median-of-Medians grid: the quadrant of elements ≤ Median-of-Medians M (and ≥ M) contains at least 3n/10 elements, guaranteeing T(n) ≤ T(n/5) + T(7n/10) + O(n) = O(n).
Invariant & State
| Group Size $g$ | Median-of-Medians Call | Max Survivor Partition | Subproblem Fraction Sum | Worst-Case Complexity |
|---|---|---|---|---|
| $g = 3$ | $T(n / 3)$ | $T\!\left((1 - \frac{2}{6})n\right) = T(2n / 3)$ | $\frac{1}{3} + \frac{2}{3} = 1.00$ (No decay!) | $\Theta(n \log n)$ — Fails! |
| $g = 5$ (BFPRT Canonical) | $T(n / 5)$ | $T\!\left((1 - \frac{3}{10})n\right) = T(7n / 10)$ | $\frac{1}{5} + \frac{7}{10} = 0.90 < 1$ | $\Theta(n)$ — Smallest odd $g$ that works! |
| $g = 7$ | $T(n / 7)$ | $T\!\left((1 - \frac{4}{14})n\right) = T(5n / 7)$ | $\frac{1}{7} + \frac{5}{7} = \frac{6}{7} \approx 0.857 < 1$ | $\Theta(n)$ |
Introselect (std::nth_element) |
0 (Fast 3-sample / Ninther) | Switches to BFPRT / Heap on bad splits | Adaptive hybrid | $\Theta(n)$ worst-case + $\approx 3.4n$ average |
std::nth_element Uses Introselect Instead of Pure BFPRTAlthough BFPRT runs in strict worst-case $O(n)$ time, its constant factor is $\approx 16n\text{–}24n$ comparisons and requires recursive median-of-medians passes that touch memory multiple times—making pure BFPRT slower than sorting the entire array with std::sort up to $n \approx 10^6$! Consequently, C++ standard libraries (libstdc++ and libc++) implement David Musser's Introselect (or Alexandrescu's Adaptive Quickselect / Median of Ninthers): it runs standard Median-of-Three Hoare Quickselect in a tight iterative loop ($approx 3n$ comparisons) and only invokes BFPRT (or Heapselect) if the recursion depth/bad-split counter exceeds a logarithmic threshold.
Production C++20 Implementation: Iterative 3-Way Quickselect & In-Place BFPRT
#include <algorithm>
#include <concepts>
#include <cstddef>
#include <utility>
template <std::totally_ordered T>
void small_insertion_sort(T* a, std::size_t lo, std::size_t hi) noexcept {
for (std::size_t i = lo + 1; i <= hi; ++i) {
T v = std::move(a[i]);
std::size_t j = i;
while (j > lo && v < a[j - 1]) {
a[j] = std::move(a[j - 1]);
--j;
}
a[j] = std::move(v);
}
}
// Deterministic In-Place BFPRT (Median-of-Medians) with 3-Way Partitioning for Duplicates
template <std::totally_ordered T>
void bfprt_select(T* a, std::size_t lo, std::size_t hi, std::size_t k) noexcept {
while (hi - lo > 16) {
const std::size_t n = hi - lo + 1;
const std::size_t num_groups = n / 5;
// 1. Sort each group of 5 in-place and swap its median to the front [lo .. lo + num_groups - 1]
for (std::size_t g = 0; g < num_groups; ++g) {
const std::size_t g_lo = lo + g * 5;
small_insertion_sort(a, g_lo, g_lo + 4);
std::swap(a[lo + g], a[g_lo + 2]);
}
// 2. Recursively select the exact median of the group medians at a[lo + num_groups / 2]
const std::size_t mom_idx = lo + num_groups / 2;
bfprt_select(a, lo, lo + num_groups - 1, mom_idx);
const T pivot = a[mom_idx];
// 3. 3-Way Dutch National Flag Partition around pivot (handles all-equal keys in O(N)!)
std::size_t lt = lo, i = lo, gt = hi;
while (i <= gt) {
if (a[i] < pivot) std::swap(a[lt++], a[i++]);
else if (pivot < a[i]) std::swap(a[i], a[gt--]);
else ++i;
}
// 4. Tail-loop into whichever region contains target rank k
if (k < lt) {
hi = lt - 1;
} else if (k > gt) {
lo = gt + 1;
} else {
return; // k lies inside [lt .. gt], so a[k] == pivot is in its exact final spot!
}
}
small_insertion_sort(a, lo, hi);
}
In the classical textbook proof of BFPRT, all elements are assumed to be distinct. If an input array contains $n$ identical elements and you use a 2-way Lomuto partition inside BFPRT, the median-of-medians pivot $x$ is equal to every element, yet 2-way Lomuto places all $n - 1$ equal elements on one side—breaking the $\le 7n/10$ size bound and degrading BFPRT to $\Theta(n^2)$! Using 3-way Dutch National Flag partitioning inside BFPRT and Quickselect guarantees that all elements strictly $< x$ ($ge 3n/10$ minus duplicates) and strictly $> x$ are separated while all duplicates equal to $x$ are coalesced into $[lt \dots gt]$ in one linear pass.