Top K elements with a heap
Find the k largest items by keeping a small min-heap of size k and throwing out its smallest item whenever it grows too big.
O(n log k)Space O(k)10 practice problemsStep-through pictureThe idea
Often you only need the best few items: the 10 slowest page loads, the 5 most used tags, the 3 closest stores. Sorting everything costs O(n log n), and it needs the whole list in memory at once.
Instead, keep a min-heap that never holds more than k items. Add each new item. If the heap now has k + 1 items, pop the smallest one. The items left are always the k largest seen so far, and the heap's top is the smallest of them: the k-th largest.
It feels backwards to use a min-heap to find the largest items, but it is the right tool: the item you need to throw out is the weakest of your current top k, and a min-heap keeps exactly that item at the top.
An everyday example
A talent show with only k chairs on stage
A talent show has 3 chairs on stage for the best acts so far. Each new act walks onto the stage. If there are now 4 acts, the weakest of the four leaves. You never compare the newcomer with everyone who has already left; you only need to know who the weakest on stage is. When the show ends, the three acts on stage are the top three.
| In the example | In the code |
|---|---|
| The chairs on stage | the heap, kept at k items |
| A new act walks on | heap.push(num) |
| Too many acts on stage | heap.size > k |
| The weakest act leaves | heap.pop() |
| The weakest act still on stage at the end | the heap's top: the k-th largest |
From the slow way to top k elements with a heap
- The obvious way to get the
klargest ofnnumbers is to sort them all and takekfrom the big end. That costsO(n log n)time and needs every number in memory at once. It also does far more work than asked: it puts thousands of losers in perfect order when only the winners matter. - Think about what you would keep while reading the numbers one by one. You only need the best
kseen so far. When a new number arrives, the only question is whether it beats the weakest of your currentk. If it does, the weakest drops out and the new one joins. If not, the new one is ignored. - So you need a group of about
kitems where the weakest is always ready to look at and remove, while new items keep joining. That is exactly a min-heap: its top is the smallest item, and push and pop costO(log k), because the heap never holds more thank + 1items. - The code does this in the simplest way: push every number, and if the heap now has
k + 1items, pop the smallest. A number too small to join is pushed and popped right away. After the last number, the heap holds theklargest, with the k-th largest on top. Push, trim back tok, repeat: that loop is the top K pattern.
How it works
- Make a min-heap with
new MinHeap((a, b) => a - b). - For each
num, push it into the heap. - If the heap has more than
kitems, pop the smallest. It can't be in the topk. - After the loop, the heap holds the
klargest values, smallest on top. - To return them from largest to smallest, pop them all into
resultand reverse it.
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 topK(nums, k) {const heap = new MinHeap((a, b) => a - b);for (const num of nums) {heap.push(num);if (heap.size > k) heap.pop();}const result = [];while (heap.size > 0) result.push(heap.pop());return result.reverse();}
How fast it is
| Time | O(n log k) |
|---|---|
| Extra memory | O(k) |
Each of the n items is pushed and maybe popped on a heap that never holds more than k + 1 items, so each operation costs O(log k).
- Best case
O(n)kis a small fixed number like 1 or 3: each push and pop touches only a couple of levels, so the run is one pass over the input. - Average case
O(n log k)Each of the
nnumbers costs one push and usually one pop on a heap of at mostk + 1items. - Worst case
O(n log k)Every push and pop travels the heap's full height, about
log₂ klevels. Whenkis close ton, this approachesO(n log n), the same as sorting.
Common mistakes
- Using a max-heap of all
nitems and poppingktimes. It works, but costsO(n)memory and loses the streaming benefit. - Using a max-heap with size
kfor the k largest. The top of a max-heap is the biggest item, which is the one you want to keep, not throw out. Use a min-heap for the k largest and a max-heap for the k smallest. - Forgetting a tie-break when scores can be equal. Two runs may pick different items. Order ties by a second key.
- Returning the heap's array directly. It is in heap order, not sorted order. Pop the items, or sort the final
k.
When to use it
Look for these clues in a problem:
- The question asks for the k largest, k smallest, k most frequent, or k closest items.
- It asks for the k-th largest or k-th smallest value.
- The data arrives as a stream and you can't keep or sort all of it.
kis much smaller thann, soO(n log k)beats sorting.
Check yourself
Answer each one in your head first, then open it.
Why use a min-heap to find the largest items?
The item you remove each time is the weakest of your current top k, which is the smallest. A min-heap keeps exactly that item on top.
In the trace, why does pushing 3 leave the group unchanged?
3 is smaller than all three members, so after the push it is the smallest of four and is the one popped.
How much memory does it use for a stream of a million numbers with k = 10?
A heap of at most 11 numbers. The stream itself is never stored.
What is on top of the heap after the loop?
The k-th largest number, the smallest member of the top k. In the trace that is 8.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 10 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go 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 rule: after each number is handled, the heap holds the largest k numbers seen so far, or all of them if fewer than k have been read. Before the first number the heap is empty, which is correct.
Each step keeps it. The new num is pushed, so the heap holds the old top k plus num. If that makes k + 1 items, the new top k is those items minus the smallest one, and the pop removes exactly that smallest one. If num is the smallest, it is the one removed, and nothing changes.
A dropped number can never be needed later. When it was popped, it was the smallest of k + 1 numbers, so at least k numbers were at least as big. Later steps only swap members for bigger ones, so those k stay at least as big, and the dropped number can't climb back into the top k.
After the last number, the rule says the heap holds the k largest of the whole input. Popping a min-heap gives them from smallest to largest, so reversing the list gives them largest first.
Going deeper
3 more ideas
Why the heap always holds the top k so far
Assume the heap holds the k largest values among the items seen so far. A new item arrives. If it belongs in the top k, the item that drops out is the smallest of those k + 1, which is exactly what pop removes. If it doesn't belong, it is itself the smallest and gets popped right away. Either way the rule still holds.
Ranking by something else
Change the compare function to rank by any score. For the k most frequent words, count each word with a Map, then push [word, count] pairs into a min-heap ordered by count. For the k closest points, push points into a max-heap ordered by distance and pop the farthest when the heap is too big. Add a tie-break (such as the word in alphabetical order) so the answer is unique.
Time and memory compared with sorting
Each item costs one push and maybe one pop on a heap of at most k + 1 items, so the time is O(n log k) and the memory is O(k). Sorting is O(n log n) with O(n) memory. For a million items and k = 10, log k is about 3, while log n is about 20.
Work it by hand
Every step in a table, with why
Find the 3 largest of 7 numbers. The heap is shown as its array, which is in heap order, not sorted order.
nums = [6, 2, 11, 5, 9, 3, 8], k = 3
| Step | num | After push | Popped | heap after | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | 6 | [6] | - | [6] | 1 item: keep it | The heap has room, so nothing is removed. |
| 2 | 2 | [2, 6] | - | [2, 6] | 2 items: keep both | 2 is smaller than 6, so it climbs to the top. |
| 3 | 11 | [2, 6, 11] | - | [2, 6, 11] | 3 items: the heap is full | Now there are exactly k items. From here on, every push makes 4 items and forces one pop. |
| 4 | 5 | [2, 5, 11, 6] | 2 | [5, 6, 11] | 4 > 3: drop the smallest | 2 is the weakest of the four, so it leaves. The top 3 so far are 5, 6, and 11. |
| 5 | 9 | [5, 6, 11, 9] | 5 | [6, 9, 11] | 5 drops out | 9 beats the weakest member, 5, so 5 leaves. |
| 6 | 3 | [3, 6, 11, 9] | 3 | [6, 9, 11] | 3 itself drops out | 3 is smaller than all three members. It rises to the top and is popped right away, so the group doesn't change. |
| 7 | 8 | [6, 8, 11, 9] | 6 | [8, 9, 11] | 6 drops out | 8 beats 6. The heap now holds the final top 3, with the third largest, 8, on top. |
Popping the heap gives 8, 9, 11, and reversing gives [11, 9, 8]. The heap never held more than 4 numbers, however long the input was.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Top K elements with a heap | O(n log k) | O(k) | k is much smaller than n, or the numbers arrive as a stream that you can't keep. |
| Sort everything | O(n log n) | O(n) | The input is small, or you need it sorted anyway. It is one line: [...nums].sort((a, b) => b - a).slice(0, k). |
| Quickselect | O(n) | O(1) | The whole array is in memory, you may reorder it, and you want the best average time. Its worst case is O(n²), and it can't work on a stream. |
| Counting buckets | O(n) | O(n) | The scores are small whole numbers, like counts from 1 to n. Put each item in a bucket by score and read the buckets from the top. |
Extra memory: O(k) for the heap, plus O(k) for the result. The input can be a stream that is never stored.
How the steps grow
- Top K elements with a heap, Sort everything
O(n log k) - Quickselect, Counting buckets
O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Top K elements with a heap | 33 | 9,966 | 20 million |
| Sort everything | 33 | 9,966 | 20 million |
| Quickselect | 10 | 1,000 | 1 million |
| Counting buckets | 10 | 1,000 | 1 million |
Inputs that need care
6 tricky inputs
kis 0:topK([4, 1, 9], 0)- After every push the size is
1 > 0, so every number is popped, and it returns[]. That is correct, but an early return is clearer. kbigger than the input:topK([4, 1], 5)- The heap never reaches
k + 1items, so nothing is popped, and it returns every number, largest first:[4, 1]. - Duplicates:
topK([7, 7, 7, 2], 2) - It returns
[7, 7], because equal values are separate entries. For the topkdifferent values, skip numbers you have already kept, using aSet. - Negative numbers:
topK([-3, -9, -1], 2) - Nothing changes:
(a, b) => a - bworks for any numbers, and it returns[-1, -3]. - Ties when ranking objects by a score
- Which tied object stays depends on the push order. Add a second key to the compare, such as the name, so the same input always gives the same answer.
- The k smallest instead of the k largest
- Use a max-heap,
(a, b) => b - a, and pop when the size goes overk. The rule to remember: the heap's top is always the item you would throw out next.
Variations and follow-ups
Twists interviewers add
Only the k-th largest
Return heap.peek() after the loop instead of popping everything. In a stream, that value is ready after every new number, so a dashboard can show the current k-th largest at any moment in O(1).
Skip numbers that can't join
Once the heap holds k items, compare num with heap.peek() first and skip it if it isn't bigger. Small numbers then cost one comparison instead of a push and a pop, and the answer is the same.
Top k by frequency
Count each item with a Map in one pass, then run the same loop over the [item, count] pairs with a compare on count. The total is O(n + m log k) for n items with m different values.
Heapify everything, then pop k
Build a max-heap of all n numbers in O(n) and pop k times, for O(n + k log n). It wins when k is close to n, but it needs every number in memory at once.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "k largest", "k smallest", "k most frequent", "k closest", "k-th largest", or "top k in a stream". If
kis much smaller thann, the heap is the expected answer. - Say the backwards-sounding part clearly: "for the k largest I keep a min-heap, because the item I need to throw out is the smallest of my current group". Interviewers listen for that sentence.
- Give the cost as
O(n log k)time andO(k)space, compare it with sorting'sO(n log n), and mention that the input can be a stream. - A common follow-up is "can you beat
O(n log k)?". Answer: quickselect finds the k-th largest inO(n)on average, and one more pass collects the bigger items. But it needs the whole array, changes it, and has anO(n²)worst case.
Where frontend code uses it
The same idea in real UI code
- A performance dashboard can show the 10 slowest requests out of a stream of thousands without keeping or sorting all of them.
- Trending tags or the most-used emoji are the top k items by count.
- A store locator shows the few closest stores to the user's location, ranked by distance.