Quickselect
Quickselect finds the k-th smallest element by partitioning like quick sort, but recursing into only one side. That halves the expected work at every level and gives expected O(n) time.
Learning goals
- Trace quickselect and decide which side to continue on
- Explain why a random pivot gives expected linear time
- Describe its quadratic worst case and when it occurs
- Handle duplicate keys with three-way partitioning
Prerequisites
To find the 5th shortest person in a room, split everyone around one person. If the pivot has exactly four shorter people, they are the answer. Otherwise you only need to keep looking among one group, which is smaller each time.
Partition-based selection
Partition the array around a pivot. The pivot ends at index p, which is its final sorted position. If p == k - 1, the pivot is the answer. If p < k - 1, the answer lies to the right, so continue on [p+1..hi]. Otherwise it lies to the left.
Quick sort recurses on both sides; quickselect recurses on one. In the animation the discarded part of the array fades out, and you can change k to see it follow a different path. It is the fastest option when the whole array is in memory and you can reorder it.
1public static int quickSelect(int[] arr, int k) { // k-th smallest, 1-based2int lo = 0, hi = arr.length - 1, idx = k - 1;3while (lo <= hi) {4int p = partition(arr, lo, hi);5if (p == idx) return arr[p];6if (p < idx) lo = p + 1;7else hi = p - 1;8}9throw new IllegalArgumentException("k out of range");10}
Best
O(n)
Average
O(n)
Worst
O(n²)
Space
O(1)
Quickselect (k-th smallest)
Interactive · edit the inputint[] arr · length 8
The 3rd smallest value ends at sorted index 2. Partition once, then only follow the side that contains index 2.
1public static int quickSelect(int[] arr, int k) { // k-th smallest, 1-based2int lo = 0, hi = arr.length - 1, idx = k - 1;3while (lo <= hi) {4int p = partition(arr, lo, hi); // Lomuto, pivot = arr[hi]5if (p == idx) return arr[p];6if (p < idx) lo = p + 1; // answer is on the right7else hi = p - 1; // answer is on the left8}9return -1;10}
Live state
k
3
idx
2
lo
0
hi
7
Execution trace (dry run table)Show 35 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=3 idx=2 lo=0 hi=7 | The 3rd smallest value ends at sorted index 2. Partition once, then only follow the side that contains index 2. |
| 2 | L3 | lo=0 hi=7 idx=2 | Window [0..7] still contains index 2. |
| 3 | L4 | pivot=1 i=0 lo=0 hi=7 | Partition with pivot 1 (the last element). |
| 4 | L4 | pivot=1 i=0 j=0 | Not smaller than the pivot. |
| 5 | L4 | pivot=1 i=0 j=1 | Not smaller than the pivot. |
| 6 | L4 | pivot=1 i=0 j=2 | Not smaller than the pivot. |
| 7 | L4 | pivot=1 i=0 j=3 | Not smaller than the pivot. |
| 8 | L4 | pivot=1 i=0 j=4 | Not smaller than the pivot. |
| 9 | L4 | pivot=1 i=0 j=5 | Not smaller than the pivot. |
| 10 | L4 | pivot=1 i=0 j=6 | Not smaller than the pivot. |
| 11 | L4 | p=0 pivot=1 idx=2 | The pivot lands at its final index p = 0. |
| 12 | L5 | p=0 idx=2 | p < idx: the answer is to the right of the pivot, so discard [0..0]. |
| 13 | L6 | lo=1 hi=7 | lo = 1. |
| 14 | L3 | lo=1 hi=7 idx=2 | Window [1..7] still contains index 2. |
| 15 | L4 | pivot=7 i=1 lo=1 hi=7 | Partition with pivot 7 (the last element). |
| 16 | L4 | pivot=7 i=1 j=1 | Not smaller than the pivot. |
| 17 | L4 | pivot=7 i=1 j=2 | Smaller than the pivot: move it into the low region. |
| 18 | L4 | pivot=7 i=2 j=3 | Smaller than the pivot: move it into the low region. |
| 19 | L4 | pivot=7 i=3 j=4 | Not smaller than the pivot. |
| 20 | L4 | pivot=7 i=3 j=5 | Not smaller than the pivot. |
| 21 | L4 | pivot=7 i=3 j=6 | Not smaller than the pivot. |
| 22 | L4 | p=3 pivot=7 idx=2 | The pivot lands at its final index p = 3. |
| 23 | L5 | p=3 idx=2 | p > idx: the answer is to the left of the pivot, so discard [3..7]. |
| 24 | L7 | lo=1 hi=2 | hi = 2. |
| 25 | L3 | lo=1 hi=2 idx=2 | Window [1..2] still contains index 2. |
| 26 | L4 | pivot=3 i=1 lo=1 hi=2 | Partition with pivot 3 (the last element). |
| 27 | L4 | pivot=3 i=1 j=1 | Not smaller than the pivot. |
| 28 | L4 | p=1 pivot=3 idx=2 | The pivot lands at its final index p = 1. |
| 29 | L5 | p=1 idx=2 | p < idx: the answer is to the right of the pivot, so discard [1..1]. |
| 30 | L6 | lo=2 hi=2 | lo = 2. |
| 31 | L3 | lo=2 hi=2 idx=2 | Window [2..2] still contains index 2. |
| 32 | L4 | pivot=4 i=2 lo=2 hi=2 | Partition with pivot 4 (the last element). |
| 33 | L4 | p=2 pivot=4 idx=2 | The pivot lands at its final index p = 2. |
| 34 | L5 | p=2 idx=2 | p == idx, so arr[2] = 4 is the answer. The rest of the array never needed sorting. |
| 35 | L5 | return=4 | return 4 after 14 comparisons, which is less work than sorting the whole array. |
Randomized pivots
With a fixed pivot rule, an adversarial input (such as sorted data and a last-element pivot) forces bad splits. Picking the pivot at random makes the running time independent of the input order.
Swap a randomly chosen element to the end before partitioning. Median-of-three is a cheaper heuristic that works well on sorted and reverse sorted data but is not random.
1int r = lo + ThreadLocalRandom.current().nextInt(hi - lo + 1);2swap(arr, r, hi); // move the random pivot to the end3int p = partition(arr, lo, hi);
Expected linear time
Call a pivot good if it lands in the middle half of the sorted order (between the 25th and 75th percentile). That happens with probability 1/2. After a good pivot, at most 3/4 of the elements remain. In expectation two partitions are needed to get one good one.
So the expected work is E[T(n)] ≤ 2n + E[T(3n/4)]. Unrolling gives 2n · (1 + 3/4 + (3/4)² + …) = 2n / (1 - 3/4) = 8n = O(n). The partitioning cost shrinks geometrically, unlike quick sort, where both sides stay.
| Algorithm | Recurses on | Expected comparisons |
|---|---|---|
| Quick sort | Both sides | about 1.39 n log₂ n |
| Quickselect | One side | at most about 3.4 n (for the median) |
Worst-case behavior
If every pivot is the minimum or the maximum, each round removes only one element, giving n + (n - 1) + … + 1 = O(n²). With a random pivot this has vanishingly small probability. With a fixed pivot rule it is easy to trigger.
Introselect protects against this: it counts rounds and switches to the median-of-medians algorithm (next lesson) when progress is too slow, so the worst case becomes O(n). C++ std::nth_element does this.
Duplicate keys
Lomuto partitioning puts values equal to the pivot on the right side. When all elements are equal, every partition removes only one element and the algorithm is quadratic. Run the “All equal (quadratic)” preset to see it.
Use three-way partitioning (< pivot, = pivot, > pivot). If the target index falls inside the equal band, the answer is the pivot value and the search ends immediately. Otherwise continue on the strictly smaller or strictly larger part.
1// three-way quickselect step: [lo..lt-1] < pivot, [lt..gt] == pivot, [gt+1..hi] > pivot2if (idx < lt) hi = lt - 1;3else if (idx > gt) lo = gt + 1;4else return pivot; // idx is inside the equal band: done
Practice ladder
Now try it yourself
Easy
- Kth largest element in an array (convert to the k-th smallest)
- Find the median of an odd-length array
Medium
- Implement quickselect with a random pivot
- Top K frequent elements with quickselect on counts
- Kth smallest in a stream of 10 elements by hand
Advanced
- Three-way quickselect for arrays with many duplicates
- Prove quickselect's expected O(n) bound
- Introselect with a depth counter and fallback
Key takeaways
- Quickselect partitions once per round and keeps only the side containing the target index.
- Expected O(n) with a random pivot; worst case O(n²) with bad pivots or many duplicates.
- Three-way partitioning and an introselect fallback remove both weaknesses.