Heaps: Top-K, K-way Merge and Two Heaps
A heap gives you the smallest (or largest) item in O(1) and updates in O(log n): perfect for top-k, merging k sorted lists and running medians.
- O(n log k)Top k of n items with a size-k heap
- O(log n)Push or pop: one swap per level
- O(k)Space for top k or a k-way merge
Show what to say
“When I need the k largest, smallest or most frequent, a merge of k sorted inputs, or a running median, I use a heap: peek is O(1), push and pop are O(log n), and nothing is fully sorted. For top k I keep a min-heap of size k, so it is O(n log k) with O(k) memory.”
Trade-offs
- Heap vs sorting for the kth largest: a size-k heap is O(n log k), O(k) memory, works on a stream; sorting is O(n log n).
- Heap vs quickselect: quickselect is O(n) on average but O(n²) worst case without a random pivot and reorders the input; the heap is steady.
- Build by pushing vs bottom-up heapify: O(n log n) against O(n), because half the nodes are leaves and sink nowhere.
Bring it up before they ask
Return a number from the comparator, not a boolean, and remember the k largest need a min-heap, because you evict the smallest you keep.
A binary heap keeps the best item at its root: peek in O(1), push and pop in O(log n). Most heap problems are top k (O(n log k)), k-way merge or two heaps for a running median.
When to reach for it
Tap the signals you can see in the problem.
RememberkkthMerge k sortedrunning
- Top k
The heap holdsthe best k so far
kth largesttop k frequentk closest points - K-way merge
The heap holdsone head per sorted input
merge k sorted listskth smallest in a sorted matrix - Two heaps
The heap holdsthe lower and upper halves
running mediansliding window median - Best first
The heap holdsevery pending item, cheapest first
task schedulingmeeting roomsDijkstra
The core idea: a tree that is only half sorted
Think of a tournament bracket. The champion sits at the top, and every winner beat the players below.
Read more · 92 words
Players on the same level are in no order, and that laziness keeps updates cheap. A heap is that bracket: every parent beats its children.
It has no gaps, so it fits in a plain array: the children of index i are 2i + 1 and 2i + 2. A million items is only 20 levels deep.
- push: add at the end, then sift up while better than the parent.
- pop: take the root, move the last item up, then sift down with the better child.
That is at most one swap per level:
Min-heap: push sifts up, pop sifts down
- size
- 0
An empty min-heap. Its rule: every parent is less than or equal to its children, so the smallest value always sits at the root, index 0.
const a = []; // the heap: a[0] is the minimum
function push(value) {
a.push(value);
let i = a.length - 1;
while (i > 0) {
const parent = (i - 1) >> 1;
if (a[parent] <= a[i]) break;
[a[i], a[parent]] = [a[parent], a[i]];
i = parent;
}
}
function pop() {
const top = a[0];
const last = a.pop();
if (a.length === 0) return top;
a[0] = last;
let i = 0;
while (true) {
const l = 2 * i + 1, r = 2 * i + 2;
let small = i;
if (l < a.length && a[l] < a[small]) small = l;
if (r < a.length && a[r] < a[small]) small = r;
if (small === i) break;
[a[i], a[small]] = [a[small], a[i]];
i = small;
}
return top;
}All steps as text
- An empty min-heap. Its rule: every parent is less than or equal to its children, so the smallest value always sits at the root, index 0.
- push(5): the heap is empty, so 5 becomes the root.
- push(3): append it as the last leaf, index 1. Only the path from this leaf up to the root can break the heap rule now.
- 3 is smaller than its parent 5, so they swap: 3 moves up to index 0.
- 3 reached the root: it is the new minimum. A push costs at most one swap per level, O(log n).
- push(8): append it as the last leaf, index 2. Only the path from this leaf up to the root can break the heap rule now.
- 8 is not smaller than its parent 3 (index 0), so the heap rule holds: stop.
- push(1): append it as the last leaf, index 3. Only the path from this leaf up to the root can break the heap rule now.
- 1 is smaller than its parent 5, so they swap: 1 moves up to index 1.
- 1 is smaller than its parent 3, so they swap: 1 moves up to index 0.
- 1 reached the root: it is the new minimum. A push costs at most one swap per level, O(log n).
- push(4): append it as the last leaf, index 4. Only the path from this leaf up to the root can break the heap rule now.
- 4 is not smaller than its parent 3 (index 1), so the heap rule holds: stop.
- pop(): the minimum is always at the root, so the answer is 1. The hard part is repairing the heap without it.
- Move the last leaf, 4, into the root's place. The tree stays complete, but 4 may be bigger than its children: sink it.
- The smaller child, 3, is smaller than 4: swap them so the smaller value moves up. 4 sinks to index 1.
- 4 is not bigger than its children (5), so the heap rule holds: stop.
- Return 1. The heap is valid again after at most one swap per level: O(log n).
- pop(): the minimum is always at the root, so the answer is 3. The hard part is repairing the heap without it.
- Move the last leaf, 5, into the root's place. The tree stays complete, but 5 may be bigger than its children: sink it.
- The smaller child, 4, is smaller than 5: swap them so the smaller value moves up. 5 sinks to index 1.
- 5 has no children at index 1, so it can't sink further: stop.
- Return 3. The heap is valid again after at most one swap per level: O(log n).
Play with it
Step through one swap at a time, then set Who sifts to You and make the swaps yourself.
Read more · 66 words
Step by step
Ask what you need the best of, repeatedly
The min or max many times: heap. Just once: scan.
Decide what goes into the heap
Numbers, pairs, list nodes.
Write the comparator and check its direction
The k largest need a min-heap.
Bound the size
Pop at k + 1 for top k.
Read the answer
peek()for the kth best, or pop all.
Heap and top k
Template 1: a binary heap with a comparator
One class, one comparator: a min-heap, a max-heap, or pairs ordered by any key.
Interviews expect you to write your own siftUp and siftDown.
// Template 1: a binary heap. JavaScript has no built-in priority queue, so write this once and reuse it.
// The heap lives in an array: the parent of index i is (i - 1) >> 1, its children are 2i + 1 and 2i + 2.
// compare(a, b) < 0 means a leaves first: (a, b) => a - b is a min-heap, (a, b) => b - a a max-heap.
export class Heap {
constructor(compare = (a, b) => a - b) {
this.items = [];
this.compare = compare;
}
size() {
return this.items.length;
}
peek() {
return this.items[0]; // undefined when empty
}
push(value) {
this.items.push(value); // add as the last leaf…
this.siftUp(this.items.length - 1); // …and float it up past larger parents
}
pop() {
const items = this.items;
if (items.length === 0) return undefined;
const top = items[0];
const last = items.pop();
if (items.length > 0) {
items[0] = last; // move the last leaf to the root…
this.siftDown(0); // …and sink it below smaller children
}
return top;
}
siftUp(i) {
const items = this.items;
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.compare(items[i], items[parent]) >= 0) break; // parent already comes first
[items[i], items[parent]] = [items[parent], items[i]];
i = parent;
}
}
siftDown(i) {
const items = this.items;
for (;;) {
const left = 2 * i + 1;
const right = left + 1;
let best = i; // whichever of i, left, right should come first
if (left < items.length && this.compare(items[left], items[best]) < 0) best = left;
if (right < items.length && this.compare(items[right], items[best]) < 0) best = right;
if (best === i) return;
[items[i], items[best]] = [items[best], items[i]];
i = best;
}
}
}new Heap((a, b) => a - b); // min-heap of numbers
new Heap((a, b) => b - a); // max-heap of numbers
new Heap((a, b) => a[1] - b[1]); // pairs, smallest second element first
new Heap((a, b) => a.val - b.val); // linked list nodes by valueTemplate 2: top k with a heap of size k
To keep the k largest, keep a min-heap of at most k.
Read more · 44 words
Its root is the weakest of the top k. Push each value; when the heap reaches k + 1, pop the root. At the end the root is the kth largest.
For the k smallest, use a max-heap. Try Kth Largest Element in a StreamEasy, Kth Largest Element in an ArrayMedium, Top K Frequent ElementsMedium and K Closest Points to OriginMedium.
Top k: a min-heap of size k keeps the k largest
- k
- 3
- size
- 0
Keep a min-heap that never holds more than k = 3 values. Its root is the smallest of the values kept: once the walk ends, that is the kth largest.
function kLargest(nums, k) {
const heap = new Heap((a, b) => a - b);
for (const x of nums) {
heap.push(x);
if (heap.size() > k) heap.pop();
}
return heap; // root = kth largest
}All steps as text
- Keep a min-heap that never holds more than k = 3 values. Its root is the smallest of the values kept: once the walk ends, that is the kth largest.
- Push 3. The heap holds 1 of at most 3 values, so nothing has to leave yet.
- Push 1. The heap holds 2 of at most 3 values, so nothing has to leave yet.
- Push 5. The heap holds 3 of at most 3 values, so nothing has to leave yet.
- Push 12. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
- Pop the root, 1: the 3 values left are all at least as big, so it is out for good. The new root, 3, is the 3rd largest so far.
- Push 2. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
- Pop the root, 2: the value we just pushed is the smallest, so it was never good enough for the top 3.
- Push 11. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
- Pop the root, 3: the 3 values left are all at least as big, so it is out for good. The new root, 5, is the 3rd largest so far.
- Push 7. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
- Pop the root, 5: the 3 values left are all at least as big, so it is out for good. The new root, 7, is the 3rd largest so far.
- Every value was pushed once and popped at most once, each O(log k). The heap holds the 3 largest values, 12, 11, 7, and its root 7 is the 3rd largest.
import { Heap } from './heap.js';
// Template 2: top k. To keep the k LARGEST values, keep a MIN-heap of size k: its top is the
// weakest of the current top k, the one to evict when something bigger arrives. O(n log k).
export function kLargest(nums, k) {
const heap = new Heap((a, b) => a - b);
for (const x of nums) {
heap.push(x);
if (heap.size() > k) heap.pop(); // drop the smallest: it is not in the top k
}
const out = [];
while (heap.size() > 0) out.push(heap.pop());
return out.reverse(); // largest first
}
// The mirror image: the k SMALLEST items by some key need a MAX-heap of size k.
export function kSmallestBy(items, k, key) {
const heap = new Heap((a, b) => key(b) - key(a));
for (const item of items) {
heap.push(item);
if (heap.size() > k) heap.pop(); // drop the largest key
}
const out = [];
while (heap.size() > 0) out.push(heap.pop());
return out.reverse(); // smallest key first
}K-way merge and two heaps
Template 3: k-way merge
The next output is always one of the k current heads. Keep them in a min-heap: pop the smallest, push its successor.
Practice: Merge k Sorted ListsHard.
K-way merge: a heap of the current heads
- k
- 3
- heapSize
- 0
Every array is sorted, so the next output value is always one of the 3 heads. A min-heap of heads finds the smallest in O(log k).
function mergeSorted(arrays) {
const heap = new Heap((a, b) => a.value - b.value);
arrays.forEach((arr, list) => {
if (arr.length) heap.push({ value: arr[0], list, index: 0 });
});
const out = [];
while (heap.size() > 0) {
const { value, list, index } = heap.pop();
out.push(value);
if (index + 1 < arrays[list].length)
heap.push({ value: arrays[list][index + 1], list, index: index + 1 });
}
return out;
}All steps as text
- Every array is sorted, so the next output value is always one of the 3 heads. A min-heap of heads finds the smallest in O(log k).
- Push the first value of every array. The heap holds one entry per array, never more.
- Pop 1 from array A: it is the smallest head, so it is the next output value.
- Array A's next value, 4, becomes its new head: push it. The heap again has one entry per unfinished array.
- Pop 2 from array B: it is the smallest head, so it is the next output value.
- Array B's next value, 3, becomes its new head: push it. The heap again has one entry per unfinished array.
- Pop 3 from array B: it is the smallest head, so it is the next output value.
- Array B's next value, 10, becomes its new head: push it. The heap again has one entry per unfinished array.
- Pop 4 from array A: it is the smallest head, so it is the next output value.
- Array A's next value, 9, becomes its new head: push it. The heap again has one entry per unfinished array.
- Pop 5 from array C: it is the smallest head, so it is the next output value.
- Array C's next value, 6, becomes its new head: push it. The heap again has one entry per unfinished array.
- Pop 6 from array C: it is the smallest head, so it is the next output value. Array C is now used up, so nothing replaces it.
- Pop 9 from array A: it is the smallest head, so it is the next output value. Array A is now used up, so nothing replaces it.
- Pop 10 from array B: it is the smallest head, so it is the next output value. Array B is now used up, so nothing replaces it.
- The heap is empty, so every value has been output in sorted order. Each of the 8 values was pushed and popped once: O(N log k).
import { Heap } from './heap.js';
// Template 3: k-way merge. The next output is always the smallest of the k current heads,
// so keep one entry per input in a min-heap. Each of the N items is pushed and popped once: O(N log k).
export function mergeSortedArrays(arrays) {
const heap = new Heap((a, b) => a.value - b.value);
arrays.forEach((arr, list) => {
if (arr.length > 0) heap.push({ value: arr[0], list, index: 0 });
});
const out = [];
while (heap.size() > 0) {
const { value, list, index } = heap.pop();
out.push(value);
const next = index + 1;
if (next < arrays[list].length) heap.push({ value: arrays[list][next], list, index: next }); // advance that input
}
return out;
}Template 4: two heaps for a running median
Keep the lower half in a max-heap and the upper half in a min-heap.
Read more · 49 words
Keep every low value at most every high value, and the halves equal in size (the lower may have one more).
The median is the lower top, or the average of both tops. To add a number, push it low, move the lower top up, and rebalance.
Practice: Find Median from Data StreamHard.
import { Heap } from './heap.js';
// Template 4: two heaps. `low` (a max-heap) holds the smaller half, `high` (a min-heap) the larger half,
// with low.size() === high.size() or one more. The median is then low's top, or the average of both tops.
export function runningMedians(nums) {
const low = new Heap((a, b) => b - a);
const high = new Heap((a, b) => a - b);
const medians = [];
for (const x of nums) {
low.push(x);
high.push(low.pop()); // keeps every low value <= every high value
if (high.size() > low.size()) low.push(high.pop()); // keeps the sizes balanced
medians.push(low.size() > high.size() ? low.peek() : (low.peek() + high.peek()) / 2);
}
return medians;
}Try it
// Edit the inputs and press Run. Heap and the helpers are the templates above.
const heap = new Heap((a, b) => a - b); // min-heap; (a, b) => b - a for a max-heap
[5, 3, 8, 1, 4].forEach((x) => heap.push(x));
console.log(heap.peek(), heap.size()); // 1 5
console.log(heap.pop(), heap.pop(), heap.pop()); // 1 3 4
console.log(kLargest([3, 1, 5, 12, 2, 11, 7], 3)); // [12, 11, 7]
const dist = ([x, y]) => x * x + y * y;
console.log(kSmallestBy([[3, 3], [1, -2], [-4, 0], [0, 1]], 2, dist)); // [[0, 1], [1, -2]]
console.log(mergeSortedArrays([[1, 4, 9], [2, 3, 10], [5, 6]])); // [1, 2, 3, 4, 5, 6, 9, 10]
console.log(runningMedians([6, 2, 9, 1])); // [6, 4, 6, 4]Complexity and JavaScript
Complexity
Read more · 133 words
| Operation or variant | Time | Extra space |
|---|---|---|
peek | O(1) | O(1) |
push, pop on n items | O(log n): one swap per level | O(1) |
| Build a heap from n items | O(n log n) by pushing, O(n) bottom-up | O(n) |
| Top k of n items | O(n log k) | O(k) |
| K-way merge of N items from k inputs | O(N log k) | O(k) |
| Running median | O(log n) per number, O(1) per median | O(n) |
JavaScript you'll use
JavaScript refresher: Heaps and priority queues Classes for data structures Sorting and comparatorsarr.sort() after every insert is O(n log n): don't fake a heap.
const parent = (i - 1) >> 1; // index of the parent: halve and round down
const left = 2 * i + 1, right = 2 * i + 2; // indices of the children
[a[i], a[j]] = [a[j], a[i]]; // swap two slots
const byCount = (a, b) => a[1] - b[1]; // comparators return numbers, never booleans
for (const entry of counts) heap.push(entry); // Map entries are [key, value] pairsInterview questions
Kth largest element: heap, sort or quickselect?
Sorting is O(n log n). A size-k heap is O(n log k) and works on a stream. Quickselect is O(n) on average.
Why can a heap be built in O(n)?
Sift down every non-leaf, last to first. Half the nodes are leaves.
How would you change an item's priority?
Keep a Map from item to index. Or push the new version and skip stale ones.
Running median when every number is between 0 and 100?
A count array of 101 buckets.
When is a heap the wrong tool?
Sorting once, arbitrary lookups or ranges, or k close to n.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Kth Largest Element in a StreamDesign a class that reports the kth largest value seen so far every time a new number arrives.Easy
- Kth Largest Element in an ArrayReturn the kth largest value in an unsorted array without fully sorting it.Medium
- Top K Frequent ElementsReturn the k values that appear most often in an array, in any order.Medium
- K Closest Points to OriginGiven points on a plane, return the k points nearest to the origin.Medium
- Task SchedulerFind the least time to finish all tasks when equal tasks must be separated by a cooldown.Medium
- Merge k Sorted ListsMerge k sorted linked lists into one sorted linked list.Hard
- Find Median from Data StreamDesign a class that accepts numbers one at a time and returns the median of everything so far in O(1).Hard
- Meeting Rooms IIFind the fewest meeting rooms needed so that every meeting gets a room for its whole duration.Medium
- Network Delay TimeA signal starts at one node of a weighted directed network. How long until every node has received it?Medium
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.
- Design a News Feed (Twitter or Instagram)The feed merges the pushed timeline with each celebrity's timeline, newest first. The page sorts; the heap pattern's k-way merge is how you do it without re-sorting everything.
- Design Uber (Ride Matching)Offering a ride to the nearest few drivers is a top-k problem over distance or ETA.
- Design a Real-Time LeaderboardThe top k of a score stream is the heap pattern; a sorted set is the same idea stored, with rank queries on top.
- Design a Web CrawlerThe frontier keeps a min-heap of the time each host may be fetched again; popping it gives the next host that is ready.