Pattern #30
Quick Select
RecommendedPartition-based selection: kth smallest, top K in O(n) average.
When to use
Use when you need the kth smallest/largest or top K elements without fully sorting. O(n) average via partition; worst case O(n²) but randomized pivot makes it reliable.
Recognition cues
- Kth smallest / largest element
- Top K frequent / closest without full sort
- Median of unsorted array
- Partition around a pivot
Common pitfalls
- Worst case O(n²) with bad pivot (use randomization)
- Off-by-one on k vs 0-indexed vs 1-indexed
- Modifying the input array in place unexpectedly
90-second recognition drill
Which pattern fits best?
- Kth smallest / largest element
- Top K frequent / closest without full sort
- Median of unsorted array
Interactive
Mental model
A full worked walkthrough of the invariant. Pause, scrub the dots, or use ← →. Aim to narrate each step yourself.
k = 1 (0-indexed)
2nd smallest in [4,1,7,3,5]. Quick select: partition, recurse into one half.
How to think about it
Quick select is quicksort that only recurses into one half. Pick a pivot, partition the array so elements below the pivot are left and above are right. The pivot lands at its sorted position p. If p === k, you are done. If p < k, recurse right; if p > k, recurse left. Each step discards roughly half the array, giving O(n) average, far cheaper than the O(n log n) full sort.
The enemy is the worst case O(n²): a bad pivot (e.g. always the first element on an already-sorted input) shrinks the array by one each step. A randomized pivot makes this exponentially unlikely. For top-K where K is small relative to N, a heap (O(n log k)) can beat quick select, but quick select wins when K is a fraction of N or you need a single order statistic.
Template shapes
| Shape | Core move | Example |
|---|---|---|
| kth smallest | Partition, recurse into the half containing k | LC 215, LC 378 |
| Top K | Quick select to rank K, slice left partition | LC 347, LC 973 |
| Streaming top K | Heap (not quick select), select beats on the root | LC 703 |
| Median | Quick select to n/2 (or two selects for even n) | , |
Complexity baseline
O(n) average time, O(n²) worst case (mitigated by random pivot). O(1) extra space (in place). Recursion depth O(log n) average. For streaming top-K or tiny K, prefer a heap at O(n log k).
From template to problem
- Clarify k: 0-indexed or 1-indexed? Smallest or largest (flip comparison)?
- Choose a pivot strategy, randomized is the safe default.
- Partition into
[< pivot | pivot | > pivot]and compare pivot index to k. - Recurse into only the side that contains k; return when found.
- For top-K, quick select to the Kth element then slice, or use a heap if K is tiny.
Template
Same skeleton in TypeScript, Python, and C#. Adapt the invariant; keep the structure.
/** Quick select template: kth smallest (0-indexed) via randomized partition. */
export function quickSelect(nums: number[], k: number): number {
return select(nums, 0, nums.length - 1, k);
}
function select(nums: number[], lo: number, hi: number, k: number): number {
if (lo === hi) return nums[lo]!;
const pivotIdx = partition(nums, lo, hi);
if (k === pivotIdx) return nums[k]!;
if (k < pivotIdx) return select(nums, lo, pivotIdx - 1, k);
return select(nums, pivotIdx + 1, hi, k);
}
function partition(nums: number[], lo: number, hi: number): number {
const rand = lo + Math.floor(Math.random() * (hi - lo + 1));
[nums[rand], nums[hi]] = [nums[hi]!, nums[rand]!];
const pivot = nums[hi]!;
let i = lo;
for (let j = lo; j < hi; j++) {
if (nums[j]! < pivot) {
[nums[i], nums[j]] = [nums[j]!, nums[i]!];
i++;
}
}
[nums[i], nums[hi]] = [nums[hi]!, nums[i]!];
return i;
}/** Quick select template: kth smallest (0-indexed) via randomized partition. */
export function quickSelect(nums: number[], k: number): number {
return select(nums, 0, nums.length - 1, k);
}
function select(nums: number[], lo: number, hi: number, k: number): number {
if (lo === hi) return nums[lo]!;
const pivotIdx = partition(nums, lo, hi);
if (k === pivotIdx) return nums[k]!;
if (k < pivotIdx) return select(nums, lo, pivotIdx - 1, k);
return select(nums, pivotIdx + 1, hi, k);
}
function partition(nums: number[], lo: number, hi: number): number {
const rand = lo + Math.floor(Math.random() * (hi - lo + 1));
[nums[rand], nums[hi]] = [nums[hi]!, nums[rand]!];
const pivot = nums[hi]!;
let i = lo;
for (let j = lo; j < hi; j++) {
if (nums[j]! < pivot) {
[nums[i], nums[j]] = [nums[j]!, nums[i]!];
i++;
}
}
[nums[i], nums[hi]] = [nums[hi]!, nums[i]!];
return i;
}- 1#215 Kth Largest Element in an ArrayGuidemedium
- 2#347 Top K Frequent ElementsGuidemedium
- 3#378 Kth Smallest Element in a Sorted MatrixGuidemedium
- 4#973 K Closest Points to OriginGuidemedium
- 5#703 Kth Largest Element in a StreamGuideeasy
- 6#1985 Find the Kth Largest Integer in the ArrayGuidehard