Pattern 13 of 24 · Trees · 35 min

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.

Spot it:k largest / smallest / closestmerge k sortedrunning medianalways process the cheapest nextJavaScript you'll use: Heaps and priority queues Classes for data structures Sorting and comparators
Say it in 30 seconds
  • 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.

0 of 0 sections ticked
30-second answer

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

Spot it in the problem0 of 3

Tap the signals you can see in the problem.

RememberkkthMerge k sortedrunning

Pick your variant
  • 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

heap as a tree
empty tree (null)
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
  1. 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.
  2. push(5): the heap is empty, so 5 becomes the root.
  3. 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.
  4. 3 is smaller than its parent 5, so they swap: 3 moves up to index 0.
  5. 3 reached the root: it is the new minimum. A push costs at most one swap per level, O(log n).
  6. 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.
  7. 8 is not smaller than its parent 3 (index 0), so the heap rule holds: stop.
  8. 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.
  9. 1 is smaller than its parent 5, so they swap: 1 moves up to index 1.
  10. 1 is smaller than its parent 3, so they swap: 1 moves up to index 0.
  11. 1 reached the root: it is the new minimum. A push costs at most one swap per level, O(log n).
  12. 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.
  13. 4 is not smaller than its parent 3 (index 1), so the heap rule holds: stop.
  14. pop(): the minimum is always at the root, so the answer is 1. The hard part is repairing the heap without it.
  15. Move the last leaf, 4, into the root's place. The tree stays complete, but 4 may be bigger than its children: sink it.
  16. The smaller child, 3, is smaller than 4: swap them so the smaller value moves up. 4 sinks to index 1.
  17. 4 is not bigger than its children (5), so the heap rule holds: stop.
  18. Return 1. The heap is valid again after at most one swap per level: O(log n).
  19. pop(): the minimum is always at the root, so the answer is 3. The hard part is repairing the heap without it.
  20. Move the last leaf, 5, into the root's place. The tree stays complete, but 5 may be bigger than its children: sink it.
  21. The smaller child, 4, is smaller than 5: swap them so the smaller value moves up. 5 sinks to index 1.
  22. 5 has no children at index 1, so it can't sink further: stop.
  23. 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

  1. Ask what you need the best of, repeatedly

    The min or max many times: heap. Just once: scan.

  2. Decide what goes into the heap

    Numbers, pairs, list nodes.

  3. Write the comparator and check its direction

    The k largest need a min-heap.

  4. Bound the size

    Pop at k + 1 for top k.

  5. Read the answer

    peek() for the kth best, or pop all.

Interactive

Top-k arena

Values stream in; a min-heap of size k keeps the largest k. Watch each swap on the tree and the array at once.

Held: 0 of 0 seenComparisons: 0 vs ≈0 re-sorting after every arrival

Press Step or Run to stream values in, or push your own. The heap keeps the 3 largest it has seen.

k, speed and your own values

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 value

Template 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

min-heap (at most k = 3)
empty tree (null)
nums (7)
3
1
5
12
2
11
7
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
  1. 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.
  2. Push 3. The heap holds 1 of at most 3 values, so nothing has to leave yet.
  3. Push 1. The heap holds 2 of at most 3 values, so nothing has to leave yet.
  4. Push 5. The heap holds 3 of at most 3 values, so nothing has to leave yet.
  5. Push 12. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
  6. 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.
  7. Push 2. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
  8. Pop the root, 2: the value we just pushed is the smallest, so it was never good enough for the top 3.
  9. Push 11. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
  10. 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.
  11. Push 7. Now the heap holds 4 values, one more than k: the smallest of them can't be in the top 3.
  12. 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.
  13. 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

min-heap of heads (badge = array)
empty tree (null)
array A (3)
1
4
9
▲ head
array B (3)
2
3
10
▲ head
array C (2)
5
6
▲ head
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
  1. 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).
  2. Push the first value of every array. The heap holds one entry per array, never more.
  3. Pop 1 from array A: it is the smallest head, so it is the next output value.
  4. Array A's next value, 4, becomes its new head: push it. The heap again has one entry per unfinished array.
  5. Pop 2 from array B: it is the smallest head, so it is the next output value.
  6. Array B's next value, 3, becomes its new head: push it. The heap again has one entry per unfinished array.
  7. Pop 3 from array B: it is the smallest head, so it is the next output value.
  8. Array B's next value, 10, becomes its new head: push it. The heap again has one entry per unfinished array.
  9. Pop 4 from array A: it is the smallest head, so it is the next output value.
  10. Array A's next value, 9, becomes its new head: push it. The heap again has one entry per unfinished array.
  11. Pop 5 from array C: it is the smallest head, so it is the next output value.
  12. Array C's next value, 6, becomes its new head: push it. The heap again has one entry per unfinished array.
  13. 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.
  14. 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.
  15. 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.
  16. 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

try-it.js
// 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 variantTimeExtra space
peekO(1)O(1)
push, pop on n itemsO(log n): one swap per levelO(1)
Build a heap from n itemsO(n log n) by pushing, O(n) bottom-upO(n)
Top k of n itemsO(n log k)O(k)
K-way merge of N items from k inputsO(N log k)O(k)
Running medianO(log n) per number, O(1) per medianO(n)

JavaScript you'll use

JavaScript refresher: Heaps and priority queues Classes for data structures Sorting and comparators

arr.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] pairs

Interview 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 3

Score 2 out of 3 or better to mark this page complete.

1. You must report the 10 largest values from a stream of a million numbers using little memory. What do you keep?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

In real systems

This pattern is not only for coding rounds. Here is where it does the work in a design interview.

Next: Graphs generalise trees: any node can link to any other.