Heap / Priority Queue

When you need to repeatedly fetch the current min or max from a changing collection, a binary heap gives O(log n) push/pop and O(1) peek — turning brute-force re-sorting into a tight incremental update.

Topic guide11 problems
The unlock

A heap is a “give me the best one NOW” gadget that stays only partially sorted — so you pay O(log n) to order the single element you actually need, instead of O(n log n) to order all of them. The moment a problem says “repeatedly take the smallest / largest from a changing pile,” you want a heap.

MENTAL MODEL A tournament bracket that only ever crowns the next winner

Picture a tournament tree where every parent beat its two children. The overall champion sits at the top — that's the root, and reading it is free (O(1)). But the rest of the bracket is deliberately not fully ranked: you know each parent beats its kids, and nothing more. A full sort would rank all n players; a heap only ever cares about who's on top right now.

When you remove the champion or add a newcomer, you don't re-run the whole tournament — you just replay one path up or down the tree. That path islog n long, so each update is cheap. The genius is the storage: the tree is faked inside a flat array, no pointers at all.

The reframe → a heap isn't “a sorted thing.” It's a lazy thing: it refuses to sort anything until you ask for the extreme, and then it sorts only the one element you asked for.

SEE IT The tree is a lie — it lives in a flat array

The single most clarifying picture in heaps: the “tree” has no nodes and no pointers. It's an ordinary array, and the parent/child links are just arithmetic on indices:

Min-heap as a tree            Same heap as a flat array
(smallest wins, sits on top)  (children of i live at 2i+1, 2i+2)

            2                  idx :  0   1   2   3   4   5
          /   \               val : [ 2 ][ 5 ][ 4 ][ 9 ][ 7 ][ 8 ]
        5       4                      |    \___      \
       / \     /                       |        \        \
      9   7   8               parent 0 ──► kids 1,2
                              parent 1 ──► kids 3,4
                              parent 2 ──► kid  5

No left/right pointers. The array IS the tree: child(i)=2i+1 / 2i+2,
parent(i)=(i-1)/2. The root (idx 0) is the min — peek is O(1).

Now watch a push. You append at the end and bubble up, swapping with the parent while you're smaller. Only the nodes on one root-to-leaf path ever move:

push(1) into  [ 2 ][ 5 ][ 4 ][ 9 ][ 7 ][ 8 ]
  append at the end, then BUBBLE UP while smaller than parent:

  [..][ 1 ]  ◄ new leaf (idx 6), parent is idx 2 = 4
       swap with 4  ─►  1 rises
       swap with 2  ─►  1 rises to the root

  one swap PER LEVEL → at most log n swaps, never a full re-sort.
  The rest of the array (the other ~n elements) is never inspected.
The smell test →if you find yourself wanting to scan the whole collection just to grab one extreme value, and you'll do it again and again as things change, you're paying O(n) per query for what a heap does in O(log n).

HOW TO THINK The cold-start ladder — run this on any new problem

When a problem smells like “extremes,” climb these rungs in order. The right heap shape falls out at the bottom:

  1. Do I repeatedly need the extreme of a changing set? If yes — the min or max of a pile that grows/shrinks over time — reach for a heap. (If the set never changes and you ask once, just sort or use quickselect.)
  2. Is it “top / bottom k”? Use a size-k heap of the opposite polarity: k largest → a MIN-heap of size k (its root is the kth largest, the eviction door). Flip both for k smallest.
  3. Is it a streaming median or order statistic? Use two heaps straddling the middle — a max-heap of the low half facing a min-heap of the high half — rebalanced so the median sits at the seam.
  4. Am I merging several already-sorted sources? Use a heap of the current heads: pop the global min, then push the next element from that source. O(N log k).
  5. Is it greedy “always do the highest-priority thing next”? A priority queue drives the order — task scheduler, Dijkstra. Pop the best, act, push the consequences.
The one decision that unlocks Top-K → “to keep the k largest, the heap must make the smallest easy to throw away” — so it's a min-heap. Polarity is always opposite to the thing you're collecting.

SEE IT — TOP-K Watch the size-k heap evict losers as the stream flows

The Top-K trick feels backwards until you see it run. To keep the k largest, hold a min-heap capped at size k. Its root is the smallest of your current winners — exactly the one to kick out when something bigger arrives:

Keep the 3 LARGEST.  Heap = a MIN-heap capped at size 3.
The root is the SMALLEST of the current top-3 → the eviction door.

stream → 5   1   8   2   9   3
        ─────────────────────────────────────────────
 push 5   [5]
 push 1   [1 5]
 push 8   [1 5 8]
 push 2   [1 5 8] +2 → [1 2 5 8], size>3 → pop root(1) → [2 5 8]
 push 9   [2 5 8] +9 → pop root(2)                     → [5 8 9]
 push 3   3 < root(5) → evicted instantly, push+pop  → [5 8 9]
        ─────────────────────────────────────────────
 root = 5 = the 3rd largest.   top-3 = {5,8,9}.   cost O(n log k).

The heap never holds more than k items, so every push/pop is O(log k), and the whole stream costs O(n log k) — far below a full O(n log n) sort when k ≪ n. And it works on a stream: you never need all the data at once.

Why opposite polarity → the root must be the weakestwinner so it's cheap to evict. Min-heap → smallest at root → evict the smallest → the k largest survive.

SEE IT — TWO HEAPS A running median = two heaps facing each other

“Find the median from a data stream” looks impossible without re-sorting after every insert — until you split the numbers into a low half and a high half and keep each in its own heap, oriented so both medians-in-waiting sit at the roots:

        LOWER half            UPPER half
       (a MAX-heap)          (a MIN-heap)
   biggest-low at root   smallest-high at root
                  \          /
   ...  3   1   2 [ 4 ]    [ 6 ] 8   9  ...
                   ▲          ▲
                   └── the median seam ──┘

 odd count  → median = root of the bigger heap
 even count → median = average of the two facing roots
 invariant  → every low ≤ every high, and |sizes| differ by ≤ 1.

The max-heap's root is the biggest of the low half; the min-heap's root is the smallest of the high half. Keep the two sizes within 1 of each other and the median is always one root (odd count) or the average of both roots (even count) — answered in O(1), with each insert only O(log n).

The trick → two heaps let you grab both middle elements instantly. One heap guards the element just below center, the other just above; rebalancing keeps the seam exactly at the median.

SAY IT Say the polarity out loud before you type it

The number-one heap bug is getting the polarity backwards. Defuse it by stating the invariant in plain English first, then letting the code mirror it:

  • Top-K largest: “a MIN-heap of size k; the root is the smallest survivor, so it's the first to go, and peek()is the kth largest.”
  • Merge K sorted:“a min-heap of one head per list; pop the global minimum, then push thatlist's next element.”
  • Running median:“every low ≤ every high, and the two sizes never differ by more than one” — route to lower first, then rebalance.
# The size-k heap, said in words:

for each element x in the stream:
    push x onto the heap      # heap polarity is OPPOSITE the goal
    if heap.size() > k:       # one element too many?
        heap.pop()            # evict the root = loser of the top-k

# afterwards:
#   heap holds the k winners
#   heap.peek() (the root) = the kth best — the boundary element
Failure mode → if your “k largest” answer comes back as the k smallest, you used a max-heap where a min-heap belongs. Re-read your invariant sentence — the heap type must be the oppositeof what you're keeping.

WHEN NOT TO A heap is the wrong tool more often than you think

Heaps are seductive, but they only pay off when you repeatedly pull extremes from a changing set. Several lookalikes have strictly better tools:

  • You need the full sorted order → just sort. O(n log n) with no bookkeeping.
  • One-shot kth on a static array → quickselect is O(n) average, beating the heap's O(n log k).
  • Frequencies bounded by n (Top-K Frequent) → bucket sort by frequency is O(n), no heap needed.
  • You must delete arbitrary (non-root) elements often →a heap can't do that cleanly; reach for a balanced BST / sorted set.
The litmus test →repeatedly + extreme + changing set” = heap. Drop any one of those three words and a simpler tool usually wins.

MNEMONIC Bubble the winner to the top.

Bubble the winner to the top. A heap is a binary tree packed into an array (child of i at 2i+1/2i+2); inserts sift up and pops sift down in O(log n), so the min/max is always at the root. The Visualize tab bubbles each insert up the tree.

PATTERN Why a heap instead of sorting

A binary heap is a complete binary tree stored in an array. It maintains one invariant: the root is always the minimum (min-heap) or maximum (max-heap). That lets you peek at the extreme in O(1) and push or pop in O(log n).

The key contrast with sorting: sorting the whole array costs O(n log n) regardless. A heap only does work proportional to the number of insertions and extractions you actually perform.

JS has no built-in heap. In an interview you may declare a MinHeap / MaxHeap helper and implement it if asked, or state the assumption and proceed.

KEY IDEA The four canonical heap patterns

  • Top-K. Keep a size-k heap of the opposite extreme: a min-heap to track the k largest (the root is the smallest of those k, so you evict it when a larger element arrives).
  • Merge K sorted streams.Seed the heap with one head per stream; pop the global minimum, then push that stream's next element.
  • Two-heaps for a running median. A max-heap holds the lower half, a min-heap the upper half; rebalance after each insertion so sizes differ by at most 1.
  • Heap-based greedy / scheduling / Dijkstra. A priority queue drives greedy order: always process the highest-priority task next (task scheduler, Dijkstra shortest path).

COST Heap vs sort — when each wins

Full sort
O(n log n)
Every element participates. Best when you need all k or full order.
Size-k heap
O(n log k)
Only k slots maintained. Wins when k ≪ n or data streams in.

For a one-shot kth-largest on a static array, quickselect gives O(n) average with O(1) extra space — often the optimal interview answer when there is no streaming constraint.

COMPLEXITY Operation costs at a glance

  • peek()O(1): just read the root.
  • push(v)O(log n): append + bubble up.
  • pop()O(log n): swap root with last, remove, sift down.
  • heapify from an array — O(n) (not O(n log n) as intuition suggests; most sifts are cheap near the leaves).
Arbitrary deletion (remove an element that is not the root) is awkward on a heap — you need a separate index map and costs O(log n) but with significant bookkeeping. If you need this often, consider a sorted set / balanced BST instead.

RUN IT Bubble the winner to the top

step 0 / 17
STARTBuild a min-heap (smallest on top) by inserting one value at a time. Bubble the winner to the top. The array and the tree are the same thing — child of i lives at 2i+1 / 2i+2.
heap array
empty
value sifting upparent being compared
slowfast

TRIGGERS When you see ___ → reach for ___

Reach for a heap whenever the problem asks you to repeatedly extract an extreme value from a set that keeps changing, especially when the word "k" appears or data arrives as a stream.

"k largest / k smallest / k most frequent"size-k heap of the OPPOSITE extreme (min-heap for k largest)
"merge k sorted lists / arrays"heap of list heads, pop min and push that list's next element
"running median / median of a stream"two heaps: max-heap lower half + min-heap upper half
"schedule by priority / most frequent task first"max-heap greedy — always process the highest-frequency / highest-priority item
"repeatedly take the smallest / largest from a pool"heap — each extraction is O(log n) instead of O(n) linear scan
"kth largest in a stream" (online, elements arrive one by one)min-heap of size k — peek() is always the kth largest seen so far
"shortest path in a weighted graph" (Dijkstra)min-heap on (distance, node) — always relax the closest unvisited node next

RED FLAGSWhen it's NOT this pattern

  • You need the full sorted order. If the problem ultimately wants every element ranked, just sort — O(n log n) with a one-liner, no heap bookkeeping needed.
  • k is tiny and fixed (e.g., always k = 1 or k = 2). A single linear scan finds the minimum or maximum in O(n) with O(1) space — a heap is overkill.
  • One-shot kth on a static array. Quickselect runs O(n) average vs. the heap's O(n log k). Mention it as the optimal alternative when there is no streaming requirement.
  • You need to remove arbitrary elements frequently. A heap only efficiently removes its root. Random deletion requires an index map and extra complexity — a balanced BST (or JavaScript SortedList equivalent) is the cleaner tool.

TEMPLATE Size-k min-heap — Top-K Largest

When → Find the k largest (or k smallest) elements, either from a static array or a stream. Use a min-heap of size k so the root is always the weakest member of the current top-k — the first candidate to evict.

size-k-min-heap-top-k-largest.ts
// Top-K Largest — keep a MIN-heap of size k.
// The heap always holds the k largest seen so far;
// the root (minimum of those k) is our "eviction candidate."
function topKLargest(nums: number[], k: number): number[] {
  const heap = new MinHeap();           // min-heap (smallest at root)
  for (const n of nums) {
    heap.push(n);
    if (heap.size() > k) heap.pop();    // evict the smallest of the top-k
  }
  // heap now contains the k largest; root = kth largest
  return heap.toArray();
}
Polarity rule → to keep the k largest, use a min-heap (evict the smallest). To keep the k smallest, use a max-heap (evict the largest). Swapping these is the most common heap bug.

TEMPLATE Merge K sorted streams

When → You have k sorted lists (or iterators) and need one merged sorted output. Seed the heap with the first element of each list; each pop advances exactly that list.

merge-k-sorted-streams.ts
// Merge K Sorted Lists — heap of heads, comparator on value.
interface HeapNode { val: number; listIdx: number; elemIdx: number; }

function mergeKSorted(lists: number[][]): number[] {
  // min-heap ordered by val
  const heap = new MinHeap<HeapNode>((a, b) => a.val - b.val);
  for (let i = 0; i < lists.length; i++) {
    if (lists[i].length > 0) heap.push({ val: lists[i][0], listIdx: i, elemIdx: 0 });
  }
  const result: number[] = [];
  while (heap.size() > 0) {
    const { val, listIdx, elemIdx } = heap.pop();
    result.push(val);
    const next = elemIdx + 1;
    if (next < lists[listIdx].length) {
      heap.push({ val: lists[listIdx][next], listIdx, elemIdx: next });
    }
  }
  return result;
  // Time O(N log k) — N total elements, k-entry heap per operation.
}
Time O(N log k) — N total elements, and the heap never holds more than k entries. This is strictly better than re-sorting the concatenated array at O(N log N)when k ≪ N.

TEMPLATE Two-heaps — running median

When → Numbers arrive one at a time and you must answer findMedian() after each insertion. A max-heap holds the lower half, a min-heap the upper half; the median lives at one or both roots.

two-heaps-running-median.ts
// Running Median — max-heap (lower half) + min-heap (upper half).
// Invariant: |lower.size - upper.size| <= 1, lower.peek() <= upper.peek().
class MedianFinder {
  private lower = new MaxHeap(); // larger values in lower half
  private upper = new MinHeap(); // smaller values in upper half

  addNum(n: number): void {
    // 1. Route: always push to lower first, then re-check boundary.
    if (this.lower.size() === 0 || n <= this.lower.peek()) {
      this.lower.push(n);
    } else {
      this.upper.push(n);
    }
    // 2. Rebalance: sizes may differ by at most 1.
    if (this.lower.size() > this.upper.size() + 1) {
      this.upper.push(this.lower.pop());
    } else if (this.upper.size() > this.lower.size()) {
      this.lower.push(this.upper.pop());
    }
  }

  findMedian(): number {
    if (this.lower.size() > this.upper.size()) return this.lower.peek();
    return (this.lower.peek() + this.upper.peek()) / 2;
  }
}
Rebalancing rule →after every push, if the heaps' sizes differ by more than 1, move the root of the larger heap to the smaller. The invariant lower.peek() <= upper.peek() is maintained by always routing through lower first and then re-checking the boundary element.

TEMPLATE Top-K Frequent — bucket sort O(n) shortcut

When → The frequencies of n elements are bounded by n, so you can bucket-sort by frequency instead of heap-sorting. This upgrades Top-K Frequent from O(n log k) to O(n).

top-k-frequent-bucket-sort-o-n-shortcut.ts
// Top-K Frequent Elements.
// Approach A — size-k min-heap by frequency: O(n log k).
// Approach B — bucket sort by frequency: O(n) — often preferred in interviews.
function topKFrequentBucket(nums: number[], k: number): number[] {
  const freq = new Map<number, number>();
  for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1);

  // Bucket index = frequency (max frequency <= nums.length).
  const buckets: number[][] = Array.from({ length: nums.length + 1 }, () => []);
  for (const [num, f] of freq) buckets[f].push(num);

  const result: number[] = [];
  for (let f = buckets.length - 1; f >= 0 && result.length < k; f--) {
    result.push(...buckets[f]);
  }
  return result.slice(0, k);
}
When to prefer the heap version → if the value domain is huge but only a small fraction of values appear (sparse frequencies), a heap avoids allocating a large bucket array. In most interview settings, mentioning both approaches and choosing bucket sort for the optimal complexity is the ideal answer.

PITFALL Wrong heap polarity (the #1 heap bug)

To find the k largest, use a min-heap (so the root — the weakest member — is what you evict when size exceeds k). Using a max-heap here keeps the k smallest instead. Burn in the mnemonic: "opposite extreme for Top-K."

PITFALL JS has no native heap — comparator sign mistakes

When rolling a heap with a comparator, (a, b) => a - b gives a min-heap (a < b means a should be higher, so negative = a wins). Reversing to (a, b) => b - a gives a max-heap. Using the wrong sign silently inverts the heap, and bugs only surface at the boundary conditions.

PITFALL Two-heaps rebalancing / parity bugs

The invariant requires that sizes differ by at most 1after every insertion, and that the lower max-heap's root is always <= the upper min-heap's root. A common mistake is forgetting to re-check the boundary after routing: push to lower first, then if lower.peek() > upper.peek() move the root across before rebalancing sizes.

PITFALL Assuming heap operations are O(1)

Push and pop are O(log n), not O(1). For a problem doing n insertions into a size-k heap the total cost is O(n log k), not O(n). This matters when comparing against bucket sort (O(n)) or quickselect (O(n) average) — heap is not always the fastest option.

PROBLEMS

#347Top K Frequent ElementsCount frequencies, then either a size-k min-heap by frequency (O(n log k)) or bucket sort by frequency (O(n)). Bucket sort is the optimal answer.#295Find Median from Data StreamTwo-heap classic: max-heap for lower half, min-heap for upper half. Rebalance sizes after each addNum so the median is always at one or both roots.#703Kth Largest Element in a StreamMaintain a min-heap of exactly size k. After each push, if size > k pop the root. peek() is the kth largest element in the stream at all times.#1046Last Stone WeightMax-heap: pop the two largest stones, compute the remainder, push it back if nonzero. Repeat until at most one stone remains.#973K Closest Points to OriginSize-k max-heap by squared distance (avoid sqrt). Evict the farthest point when size exceeds k. Quickselect gives O(n) average if order within the k results does not matter.#215Kth Largest Element in an ArrayQuickselect is O(n) average / O(1) extra space — the optimal one-shot answer. A size-k min-heap gives O(n log k) and is simpler to code under pressure.#621Task SchedulerMax-heap by task frequency drives the greedy order; a cooldown queue (or cycle-based simulation) enforces the n-interval gap. A pure math formula (based on the max frequency) also gives O(1) but is harder to derive.#355Design TwitterEach user stores their recent tweets (capped list). getNewsFeed merges the followed users' tweet lists with a max-heap by timestamp, returning the 10 most recent overall.#218The Skyline ProblemGiven rectangular buildings, output the skyline as key points. Sweep left to right over the building edges, keep a max-heap of active heights, and emit a point whenever the running max height changes. O(n log n).#502IPOPick at most k projects to maximize capital, where each needs a minimum capital to start and pays a profit. Sort projects by capital, unlock the affordable ones into a max-heap keyed on profit, and greedily bank the richest each round. O((n + k) log n).#692Top K Frequent WordsReturn the k most frequent words with ties broken lexicographically. A size-k min-heap whose comparator evicts the lower frequency first and, on a tie, the lexicographically larger word keeps the answer in O(n log k).