Partitioning & Quickselect
Rearrange an array around a pivot in linear time, then keep only the side that contains the target rank to find the k-th element in expected O(n).
Instinct Need the k-th smallest/largest, an unordered top-k, or values grouped into buckets in place - partition around a pivot and chase only one side.
Memory hack Pivot, partition, pick a side - quickselect is quicksort's lazy cousin that only chases the half holding k.
At a glance
When to use it
- You need the k-th smallest or largest element, not a full sort
- You want the top k items by some key in any order
- Values must be grouped into buckets in place in one pass
- You proposed sorting and are asked to do better
Key idea
Partition around a pivot so it lands at its final sorted index, then recurse into only the side that holds rank k. Discarding half each round gives expected O(n) instead of the O(n log n) of sorting everything.
Typical complexity
O(n) expected time (O(n^2) worst), O(1) space
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Sort Colors | Medium | Read solution | LeetCode ↗ |
| 2 | Kth Largest Element in an Array | Medium | Read solution | LeetCode ↗ |
| 3 | K Closest Points to Origin | Medium | Read solution | LeetCode ↗ |
| 4 | Top K Frequent Elements | Medium | Read solution | LeetCode ↗ |
The instinct
A partition step puts the pivot at the exact index it would occupy in sorted order: everything smaller to its left, everything larger to its right. If that index is the rank you want, you are done. If not, the answer is on one side, so you throw the other side away. Each round discards about half the remaining work, so the expected total is n + n/2 + n/4 + ... ≈ 2n - linear time without ever sorting. That is the engine behind Kth Largest Element in an Array.
Partitioning is also useful on its own. Sort Colors is a three-way partition around the value 1: zeros to the left, twos to the right, ones in between, in a single pass.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "k-th largest / smallest element", "can you do it without sorting?" | Quickselect finds a single rank in expected O(n). |
| "k closest / k most frequent, in any order" | Select rank k by the relevant key; everything left of it is the answer. |
| "sort 0s, 1s and 2s in one pass, in place" | Three-way (Dutch national flag) partition. |
| "move all X to the front / back, in place" | A two-pointer binary partition by predicate. |
| "find the median of an unsorted array" | Select rank n/2. |
| You proposed O(n log n) sorting and hear "can you do better?" | Selection beats full sorting when only one rank matters. |
| "negatives before positives", "evens before odds" | Partition by a yes/no predicate. |
Not this pattern if...
- Data arrives as a stream or you need the k-th largest after every insertion - keep a heap of size k.
- You need the k results sorted, or the original relative order preserved - sort, or use a heap.
- The array is already sorted - just index into it.
- The input must not be modified - quickselect rearranges it in place, so copy it or use a heap.
Memory hack
Pivot, partition, pick a side. Choose a random pivot, partition so it lands at its true sorted index p, then pick the side that contains your target index and forget the other one. Quicksort recurses into both sides; quickselect is lazy and recurses into one.
Analogy - finding the 10th tallest person in a crowd. Point at someone at random and say "shorter people step left, taller people step right". Count the right-hand group. If exactly 9 people are taller, the person you pointed at is your answer. If 30 are taller, send the left group home and repeat with the tall group. You never need to line everyone up by height.
Say it out loud:
- "Convert the request to an index: the k-th largest is index n - k in ascending order."
- "Random pivot, partition, and the pivot lands at p."
- "If p equals the target I'm done; if p is smaller go right, otherwise go left. Expected O(n), worst case O(n²), which the random pivot makes very unlikely."
How it works
Find the 2nd largest in [3, 2, 1, 5, 6, 4]: n = 6, so the target index is 6 - 2 = 4. For a readable trace the pivot is the last element (the template randomises it):
| Round | Range | Pivot | Array after partition | Pivot index p | Decision |
|---|---|---|---|---|---|
| 1 | [0, 5] | 4 | [3, 2, 1, 4, 6, 5] | 3 | 3 < 4, keep the right side |
| 2 | [4, 5] | 5 | [3, 2, 1, 4, 5, 6] | 4 | p == 4, answer is 5 |
The three-way partition keeps four regions with three pointers: [0, low) is "less than pivot", [low, i) is "equal", [i, high] is "not yet seen" and (high, end] is "greater". Each step shrinks the unseen region by one.
Java template
import java.util.concurrent.ThreadLocalRandom; class QuickselectTemplate { // k-th largest (1-based) = the element at index n - k in ascending order. static int kthLargest(int[] a, int k) { int target = a.length - k, lo = 0, hi = a.length - 1; while (true) { int p = partition(a, lo, hi); if (p == target) return a[p]; if (p < target) lo = p + 1; // answer lies to the right else hi = p - 1; // answer lies to the left } } // Lomuto partition with a random pivot; returns the pivot's final (sorted) index. static int partition(int[] a, int lo, int hi) { swap(a, lo + ThreadLocalRandom.current().nextInt(hi - lo + 1), hi); int pivot = a[hi], store = lo; for (int j = lo; j < hi; j++) { if (a[j] < pivot) swap(a, store++, j); // grow the "smaller than pivot" block } swap(a, store, hi); return store; } // Three-way (Dutch national flag) partition: < pivotValue | == pivotValue | > pivotValue. static void threeWay(int[] a, int pivotValue) { int low = 0, i = 0, high = a.length - 1; while (i <= high) { if (a[i] < pivotValue) swap(a, low++, i++); else if (a[i] > pivotValue) swap(a, i, high--); // don't advance i: the swapped-in value is unseen else i++; } } static void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } }
Sort Colors is simply threeWay(nums, 1). If the input may contain many equal values, use the three-way split inside quickselect too: when the target index falls in the "equal" block you can stop immediately.
Complexity
| Approach | Time | Extra space |
|---|---|---|
| Quickselect, random pivot | O(n) expected, O(n²) worst | O(1) iterative |
| Three-way partition (Sort Colors) | O(n), one pass | O(1) |
| Full sort, then index | O(n log n) | O(1) to O(n) |
| Min-heap of size k | O(n log k) | O(k) |
| Median of medians (deterministic) | O(n) worst case, large constant | O(log n) |
Variations
- Binary partition by predicate - two pointers, one write position; e.g. moving zeros or separating even and odd numbers.
- Three-way partition - low / mid / high pointers. Sort Colors.
- Selecting a rank - random-pivot quickselect. Kth Largest Element in an Array.
- Top-k by a derived key - select on distance or frequency, then take the left block; e.g. k closest points or top k frequent elements.
- Hoare partition - two pointers moving inward, fewer swaps, but it returns a split point rather than the pivot's final index.
- Counting instead of partitioning - when values come from a tiny range (just 0, 1, 2), two passes of counting also work.
Common mistakes
- Off-by-one when converting "k-th largest" to an index: it is
n - kin ascending order, notkorn - k - 1. - Advancing
iafter swapping withhighin the three-way partition - the value that just arrived has not been examined yet. - Recursing into both sides "to be safe" - that is quicksort, and the cost jumps to O(n log n).
- Treating Hoare's return value as the pivot's final position; only Lomuto (as written above) guarantees that.
- Forgetting that the input array is rearranged; copy it first if the caller still needs the original order.
A fixed pivot (always first or always last) degrades to O(n²) on sorted, reverse-sorted or all-equal input, and online judges deliberately include such tests. Always randomise the pivot, and prefer the three-way split when duplicates are likely.
All practice problems
2 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.