Quickselect
Find the k-th smallest item without sorting everything, by partitioning around a pivot and keeping only the side that holds position k.
O(n) on averageSpace O(1)4 practice problemsStep-through pictureThe idea
Sometimes you don't need the whole list sorted. You only need one position of the sorted order: the median, the 3rd largest, the 95th percentile. Sorting first costs O(n log n). Quickselect does it in O(n) on average.
It uses quick sort's partition step. After partitioning around a pivot, the pivot sits at its final sorted index, store. If store is exactly k, that is the answer. If store is less than k, the answer is to the right; otherwise it is to the left. Unlike quick sort, quickselect only continues on one side.
Each round throws away the side that can't hold the answer. On average that is about half the list, so the work is n + n/2 + n/4 + ..., which adds up to about 2n.
An everyday example
Finding the 5th tallest without lining everyone up
A coach needs the 5th shortest player, not a full lineup. She picks one player and splits the group: shorter ones on the left, the rest on the right. If exactly four players are on the left, the chosen player is the 5th shortest: done. If six are on the left, the answer is among them, and the right group can go home. She repeats with the group that is left.
| In the example | In the code |
|---|---|
| The chosen player | pivot = items[high] |
| How many are shorter | store, the pivot's final index |
| Exactly four shorter: found | if (store === k) return items[k] |
| Sending one group home | low = store + 1 or high = store - 1 |
From the slow way to quickselect
- Suppose you need the median of a list, or its 3rd smallest item. The obvious way is to sort the whole list and read index
k. That costsO(n log n)and produces far more than you asked for: the full order of every item, when you wanted one position. - Quick sort's partition step already finds one position exactly. After partitioning around a pivot, the pivot sits at index
store, its final sorted place, with every smaller item to its left and the rest to its right. Ifstorehappens to equalk, the pivot is the answer and we're done. - If
storeis notk, the partition still tells us which side the answer is on. Whenstore < k, the answer is among the items to the right, so the left side and the pivot can be thrown away. Whenstore > k, it is on the left. Quick sort would now sort both sides; we only need one. - So keep a range
low..highthat must contain indexk. Partition it, comparestorewithk, and shrink the range to the side that holdsk. On average each round keeps about half, so the work is aboutn + n/2 + n/4 + ..., less than2npartition steps. Partition, then keep only the side withk: that loop is quickselect.
How it works
- Set
low = 0andhigh = items.length - 1. The answer's indexk(0-based) is inside this range. - Partition
items[low..high]aroundpivot = items[high], exactly as quick sort does. The pivot ends atstore. - If
store === k, returnitems[k]. - If
store < k, the answer is to the right of the pivot:low = store + 1. - Otherwise it is to the left:
high = store - 1. - When the range is a single item, that item is the answer.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function quickselect(items, k) {let low = 0;let high = items.length - 1;while (low < high) {const pivot = items[high];let store = low;for (let j = low; j < high; j++) {if (items[j] < pivot) {[items[store], items[j]] = [items[j], items[store]];store++;}}[items[store], items[high]] = [items[high], items[store]];if (store === k) return items[k];if (store < k) low = store + 1;else high = store - 1;}return items[k];}
How fast it is
| Time | O(n) on average |
|---|---|
| Extra memory | O(1) |
Each partition is linear in the range, and the range shrinks by about half each round on average, so the total is about 2n. It works in place with a loop, not recursion.
- Best case
O(n)The first pivot lands exactly at
k, likek = 2in[2, 9, 1, 7, 5]: pivot5has two smaller items, lands at index 2, and is returned after one partition of 4 comparisons. - Average case
O(n)Random data or random pivots. Each round keeps about half on average. Finding the median of 10,000 random numbers took about 33,000 comparisons, about
3.3n. - Worst case
O(n²)Each pivot is the biggest or smallest in its range: with the last item as pivot, an already sorted list and
k = 0. On 1,000 sorted items that is 999 rounds and 499,500 comparisons.
Common mistakes
- Mixing up 1-based and 0-based
k. "The 3rd smallest" is index2. - For the k-th largest, searching for index
k - 1instead ofn - k. - Forgetting that it rearranges the input array. Copy it first if the caller still needs the original order.
- Using the last item as the pivot on sorted input, which makes it
O(n²). Swap a random item intohighfirst.
When to use it
Look for these clues in a problem:
- The question asks for the k-th smallest or k-th largest item, a median, or a percentile.
- You need the
ksmallest items in any order, not sorted. - Sorting would work but the problem hints at better than
O(n log n).
Check yourself
Answer each one in your head first, then open it.
Why does quickselect average O(n) when quick sort averages O(n log n)?
Quick sort partitions both sides at every level, so each level costs O(n). Quickselect keeps only one side, so the costs shrink, n + n/2 + n/4 + ..., and add up to less than 2n.
What index do you pass for the 2nd largest of 7 items?
Index 5, which is n - k = 7 - 2. Sorted ascending, the largest is at 6 and the 2nd largest at 5.
After finding the median, which items are guaranteed to be to its left?
Every item smaller than the median, possibly with some equal ones. They are in no particular order, because quickselect never sorts the sides.
Why does the loop condition use low < high instead of low <= high?
When low === high, the range has one item, and the promise says it is the answer. Partitioning a one-item range would do nothing useful, so the loop stops and returns items[k].
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 4 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The loop keeps one promise: the item that belongs at sorted index k is somewhere in items[low..high], and every item before low is smaller than or equal to every item in the range, while every item after high is bigger than or equal to them. At the start the range is the whole list, so the promise holds.
Partitioning low..high puts the pivot at store, with exactly the smaller items of the range before it. Together with the items before low, that means exactly store items of the whole list belong before the pivot, so the pivot's sorted index is store. If store === k, it is the answer. If store < k, the answer is bigger than the pivot and everything left of it, so it is in store + 1..high. If store > k, it is in low..store - 1. Either way the promise still holds for the new range.
Each round leaves out at least the pivot, so the range shrinks by at least one and the loop ends. It ends either by finding store === k, or with low === high, a one-item range. By the promise, that single item is the answer, so returning items[k] is correct.
Going deeper
3 more ideas
Why throwing away a side is safe
After partitioning, every item before store is smaller than the pivot and every item after it is not. So the items before store are exactly the store smallest items (in some order), and the pivot is the next one. If k > store, the k-th smallest is not among the left items or the pivot, so only the right side matters. The same reasoning works the other way.
Average and worst case
With good pivots each round keeps about half, so the total is about n + n/2 + n/4 + ... = 2n: O(n). With the worst pivots (always the smallest or largest), each round removes only one item: O(n²). A random pivot makes the bad case extremely unlikely. There is also a guaranteed O(n) method (median of medians), but it is slower in practice.
k-th largest and the order of the rest
The k-th largest (1-based) is the item at sorted index n - k. After quickselect, the items before index k are the k smallest, but they are not sorted among themselves. If you need them sorted, sort just those k items afterwards.
Work it by hand
Every step in a table, with why
Find index k = 2, the 3rd smallest of 8 numbers. The range shrinks from both ends until it is a single index.
items = [15, 4, 22, 9, 30, 11, 6, 18], k = 2
| Step | low | high | pivot | store | items | What happens | Why |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 7 | 18 | 5 | [15, 4, 9, 11, 6, 18, 30, 22] | 5 > 2: high = 4 | Five items are smaller than 18, so it lands at index 5. The answer must be in 0..4; 30 and 22 are gone for good. |
| 2 | 0 | 4 | 6 | 1 | [4, 6, 9, 11, 15, 18, 30, 22] | 1 < 2: low = 2 | Only 4 is smaller than 6. The answer is to the right of index 1, so low moves up. |
| 3 | 2 | 4 | 15 | 4 | [4, 6, 9, 11, 15, 18, 30, 22] | 4 > 2: high = 3 | 9 and 11 are smaller than 15, so it stays at index 4. Drop it. |
| 4 | 2 | 3 | 11 | 3 | [4, 6, 9, 11, 15, 18, 30, 22] | 3 > 2: high = 2 | 9 is smaller than 11, so 11 stays at index 3. The range shrinks to 2..2. |
| 5 | 2 | 2 | - | - | [4, 6, 9, 11, 15, 18, 30, 22] | low < high is false: return items[2] | One index is left, and the promise says it holds the answer. The last line returns items[k]. |
It returns 9 after 4 partitions and 14 comparisons. Sorted, the list is [4, 6, 9, 11, 15, 18, 22, 30], and index 2 is indeed 9. Note that 30 and 22 were never put in order.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Quickselect | O(n) | O(1) | You need one order position, like a median or percentile, from an array you may rearrange. Linear on average. |
| Sort, then index | O(n log n) | O(n) | You will ask for many positions from the same data, or the list is small and simple code matters most. |
| Heap of size k | O(n log k) | O(k) | Data arrives as a stream, can't be rearranged, or k is small compared to n. |
| Counting sort | O(n + k) | O(k) | Values are small whole numbers, like scores from 0 to 100: count them and walk the counts to position k. |
Extra memory: O(1): a loop with low, high, store, and pivot, working inside the array. A recursive version would use stack space instead.
How the steps grow
- Quickselect
O(n) - Sort, then index, Heap of size k
O(n log n) - Counting sort
O(n + k)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Quickselect | 10 | 1,000 | 1 million |
| Sort, then index | 33 | 9,966 | 20 million |
| Heap of size k | 33 | 9,966 | 20 million |
| Counting sort | 20 | 2,000 | 2 million |
Inputs that need care
5 tricky inputs
- One item:
[42]withk = 0 low < highis0 < 0, false, so the loop never runs and it returnsitems[0], which is42.koutside the list:k = 3for 3 items, ork = -1- The promise is false from the start, and the code returns
undefined. Check0 <= k < items.lengthbefore calling. - All equal: 1,000 copies of
7, looking for the median items[j] < pivotis never true, so each round removes only the pivot: 501 rounds and about 375,000 comparisons. A three-way partition that groups equal items would answer in one round.- Already sorted input with the last item as pivot
- Each pivot is the biggest in its range, so the range shrinks by one per round:
O(n²). Swapping a random item intohighbefore each partition makes this extremely unlikely. - The caller still needs the original order
- Quickselect rearranges
itemsin place. Pass a copy,quickselect([...items], k), if the order matters elsewhere.
Variations and follow-ups
Twists interviewers add
The k smallest items
After quickselect returns, items[0..k-1] holds the k smallest items (in no particular order) and items[k] is the next one. So top-k lists, like the 10 cheapest products, take O(n) on average, plus O(k log k) if you want them sorted.
Random pivot
Swap a random item of low..high into high before partitioning. Then no fixed input is slow, and the expected number of comparisons is linear for every input.
Median of medians
Split the range into groups of 5, take each group's median, and use the median of those as the pivot. It guarantees a pivot that throws away at least about 30% each round, so the worst case is O(n). It is slower in practice, so it is mostly used as a fallback, as in introselect.
Heap of size k
Keep the k smallest items seen so far in a max-heap of size k, replacing its top when a smaller item arrives. It costs O(n log k) but works on a stream where you can't hold or rearrange the whole list.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "k-th largest", "k-th smallest", "median", "top k", or "closest k points" when the answer doesn't need to be sorted. Sorting works, but the follow-up will ask for better than
O(n log n). - State the conversion first: the k-th largest (1-based) is at sorted index
n - k, and the k-th smallest is atk - 1. Off-by-one errors here are the most common bug. - Explain it as "quick sort that only recurses into one side", and give the halving sum
n + n/2 + n/4 + ... < 2nfor the average case. - The usual follow-up is "what's the worst case, and how do you avoid it?". Answer:
O(n²)with consistently bad pivots; a random pivot makes it expectedO(n), and median of medians guaranteesO(n).
Where frontend code uses it
The same idea in real UI code
- Performance dashboards show median and 95th percentile load times. Quickselect finds these without sorting thousands of samples.
- Picking the
knearest items to a point, such as the closest map markers to the user, then sorting only thosekfor display. - Robust chart scaling: finding the 1st and 99th percentile values to ignore outliers when setting an axis range.