k-th Smallest Element | Partition-based | O(n) average
Standard Undergraduate
Section 1 -- Interactive Simulation
Speed:
0
Operations
0
Comparisons
0/0
Step
16
n
n: 16
QUICKSELECT(A, lo, hi, k):
if lo == hi: return A[lo]
p = PARTITION(A, lo, hi)
rank = p - lo + 1
if rank == k: return A[p]
elif k < rank: return QUICKSELECT(A, lo, p-1, k)
else: return QUICKSELECT(A, p+1, hi, k - rank)
Section 2 -- The Idea, Step by Step
Imagine a race with a hundred runners and someone asks, "who finished 3rd?" You don't need the full finishing order to answer that — you just need the third name. QuickSelect does exactly this for a list of numbers: it finds the $k$-th smallest value without bothering to sort the whole list.
The trick is partitioning. Pick one number as the pivot, then sweep through the list and push everything smaller to its left and everything larger to its right. Now the pivot sits in its final sorted spot, and you can read off its rank — how many values are at or before it. Take the list $[7, 2, 9, 4, 1]$ and look for the 2nd-smallest ($k=2$). Pick pivot $4$: the smaller pile is $\{2,1\}$ and the larger pile is $\{7,9\}$, so $4$ lands 3rd and its rank is $3$. Since $k=2$ is less than $3$, the answer must be hiding in the left pile, so we repeat the trick there and throw the rest of the list away.
Throwing half away is the whole point. Quicksort recurses into both halves and costs $O(n \log n)$; QuickSelect chases only the one half that can contain the answer. On a balanced split the work shrinks geometrically, $n + n/2 + n/4 + \dots = 2n$, so the average cost is just $O(n)$ — linear, faster than sorting.
The precise version. If a pivot settles at index $p$ within the range starting at $\text{lo}$, its rank is $\text{rank} = p - \text{lo} + 1$. The average-case recurrence $T(n) = T(n/2) + O(n)$ sums to $O(n)$. The catch is the worst case: if the pivot is always the smallest or largest value the split is useless and $T(n) = T(n-1) + O(n) = O(n^2)$. Choosing the pivot at random makes that essentially impossible, and the median-of-medians (BFPRT) rule guarantees $O(n)$ even in the worst case.
Try this with the controls above. (1) Trace the pseudo-code panel by hand on $[7,2,9,4,1]$ for $k=2$, following the three branches at rank == k, k < rank, and k > rank. (2) Switch the preset to Sorted, then Reverse — these are exactly the inputs that wreck a naive "always pick the first element" pivot and drag QuickSelect toward its $O(n^2)$ worst case, which is why real implementations randomise. (3) Type a custom list with repeats like 5,5,5,5,5 to picture the equal-keys case that three-way partitioning is built to handle.
Section 3 -- Complexity Analysis
Symbol
Meaning
n
Input size
T(n)
Time complexity function
h
Height or depth of structure
k
Key or rank parameter
W(n)
Worst-case complexity
A(n)
Average-case complexity
Step 1 -- Relation to Quicksort. QuickSelect reuses Quicksort PARTITION. Pivot lands at final sorted position p. Unlike Quicksort which recurses both halves, QuickSelect recurses into only ONE half based on k vs rank.
Step 2 -- Average Case. Expected balanced partition: T(n) = T(n/2) + n. Geometric series: n + n/2 + n/4 + ... = 2n. T(n) = O(n) average -- much better than sorting O(n log n).
Step 4 -- Median-of-Medians (BFPRT). Divide into groups of 5, find each median (6 comparisons), recursively select median of medians, use as pivot. Guarantees pivot between 30th-70th percentile. T(n)=T(n/5)+T(7n/10)+O(n) => O(n).
Step 5 -- Why groups of 5? Groups of 3: recurrence does not converge to O(n). Groups of 5: smallest odd size giving O(n) convergence with manageable constant. Median of 5 found in exactly 6 comparisons.
Step 6 -- std::nth_element. C++ std::nth_element uses introselect -- QuickSelect with Median-of-Medians fallback guaranteeing O(n) worst case. Returns k-th element with partial sort.
Section 4 -- FAQ
How is QuickSelect different from sorting? concept+
Sorting O(n log n) gives all order statistics. QuickSelect O(n) avg gives only one. Use it when only a single percentile is needed -- asymptotically faster and does not fully sort.
What is rank in QuickSelect? concept+
After PARTITION, pivot is at index p. Rank in [lo..hi] = p-lo+1 (how many elements in the current range are <= pivot). If rank==k: answer found. If krank: recurse right with k-rank.
How does std::nth_element use it? applied+
std::nth_element rearranges so element at nth is what it would be in sorted order. Elements before are <=, after are >=. Internally introselect. O(n) guaranteed.
What is BFPRT and why groups of 5? complex+
Blum-Floyd-Pratt-Rivest-Tarjan (1973). Groups of 5 => median of each (6 comparisons) => median of medians (T(n/5)) => partition. At least 30% elements on each side. Recurrence T(n)=T(n/5)+T(7n/10)+O(n) => O(n).
Can it find top-k elements? nonobv+
Yes. Find k-th smallest, then partition around it -- all elements <= k-th are top-k. Total O(n). For streaming top-k, use a min-heap of size k: O(n log k).
QuickSelect vs heap for top-k? deep+
Heap O(n log k): good for streaming and small k. QuickSelect O(n) avg: good when k is large (like median) and all elements available. For k=n/2, QuickSelect wins clearly.
Section 5 -- Common Misconceptions
Conceptual Misconceptions
X QuickSelect returns a sorted subarray.
O QuickSelect returns only the VALUE of the k-th smallest. Array is partially partitioned but not sorted.
X O(n) worst case without Median-of-Medians.
O Basic QuickSelect is O(n^2) worst case. Only BFPRT or introselect guarantees O(n) worst case.
X Calling QuickSelect k times for top-k.
O Call once to find k-th element O(n), then partition around it. Or use std::nth_element.
Implementation Errors
X Forgetting to adjust k when recursing right.
O If recursing right (rank < k), new k = k - rank. Left partition and pivot are excluded from the count.
X Using 1-indexed k without adjustment.
O Off-by-one errors are very common. Be consistent: if k=1 means smallest, rank=1 means pivot is smallest in current range.
X Using QuickSelect on linked lists.
O Partition requires random access O(1). For linked lists, copy to array first.