πŸ”οΈ Heap / Priority Queue

A learning sheet β€” when you repeatedly need the smallest or largest thing (but not everything sorted), a heap gives it to you in O(log n).

🧠 1 · How to think (and invent it)

A heap is a complete binary tree kept in a flat array, obeying one rule β€” the heap property: every parent is ≀ its children (min-heap) or β‰₯ (max-heap). That's it. It does not keep things fully sorted; it keeps just enough order that the extreme is always at the root. Peek is O(1); insert and extract are O(log n) (one walk up or down the tree height).

Because the array is implicit, the tree links are pure arithmetic: for index i, parent = (iβˆ’1)/2, children = 2i+1 and 2i+2. Two operations maintain the property: sift-up (after a push, bubble the newcomer up) and sift-down (after replacing the root, sink it down).

1 3 2 5 4 1 3 2 5 4 01234 parent(i) = (iβˆ’1)/2 kids(i) = 2i+1, 2i+2
The tree is just the array read level by level. The root (index 0) is always the minimum.

Watch it get invented β€” the k largest of a stream

  1. Start brute. Sort all n β†’ O(n log n), and impossible if the data streams in and doesn't fit in memory.
  2. Spot the structure. You only ever need the k best so far. A newcomer matters only if it beats the smallest of those k β€” so you need fast access to that smallest.
  3. The leap (and the twist). Keep a min-heap of size k. Push each number; if the heap exceeds k, pop the minimum. Counter-intuitively, you use a min-heap to track the largest k β€” its root is the weakest survivor, i.e. the k-th largest.
  4. Payoff. O(n log k) time, O(k) space, works on an infinite stream.
incomingaction (k = 2)heap after2nd largest = root
5push, over k β†’ pop min (1){3, 5}3
2push, pop min (2){3, 5}3
4push, pop min (3){4, 5}4

Stream [3,1,5,2,4], k = 2 β†’ the heap ends holding the top 2 = {4, 5}; its root 4 is the k-th largest.

🎯 2 · Attack plan

AskIf yes β†’Pattern
Need to implement the heap / a custom-priority queue?array + sift-up / sift-downMechanics (Β§4.1)
k largest/smallest, k-th, top-k (especially streaming)?a size-k heap (opposite type!)Top-K (Β§4.2)
Merge / pull from k sorted sources?min-heap of the k current headsK-way merge (Β§4.3)
Running median / keep two halves balanced?a max-heap + a min-heapTwo heaps (Β§4.4)
Repeatedly pick the best available (schedule, combine)?greedy, driven by a heapGreedy + heap (Β§4.5)
When a heap is the WRONG / overkill tool Β· You need the whole thing sorted β†’ just sort; popping everything off a heap is heapsort, no win.
Β· One-time k-th on static data β†’ quickselect is O(n), beating a heap's O(n log k). (Heaps win for streaming / repeated queries.)
Β· You need search / membership / delete-arbitrary β†’ a heap only exposes the extreme; use a balanced BST or hash set.

πŸ”Ž 3 Β· Recognition map

Words in the promptReach for
"k-th largest/smallest", "top k frequent", "k closest points"Top-K Β§4.2
"merge k sorted", "smallest range covering k lists", "k-th smallest in a sorted matrix"K-way merge Β§4.3
"median from a data stream", "sliding window median", "balance two halves"Two heaps Β§4.4
"meeting rooms II", "task scheduler", "reorganize string", "minimum cost to connect"Greedy + heap Β§4.5
"implement a priority queue", "design a heap"Mechanics Β§4.1

🧩 4 · The five core patterns

JS has no built-in PQ, so Β§4.1 gives a tiny correct MinHeap the others reuse. Each pattern: a one-liner, a πŸ”‘ invariant, a template, a 🎀 interview lens.

4.1 Β· Mechanics β€” sift-up & sift-down

β€œPush and bubble up; pop the root, move the last up, and sink it down.”

πŸ”‘ Invariant β€” why it's safe The heap property (parent ≀ both children) holds everywhere except at the one node being fixed. Sift-up/down swaps that node toward its correct level along a single root-to-leaf path β€” height log n β€” restoring the property globally.
class MinHeap<T> {
  private h: T[] = [];
  constructor(private less: (a: T, b: T) => boolean) {}   // less(a,b)=a<b β†’ min-heap
  size() { return this.h.length; }
  peek(): T { return this.h[0]; }
  push(x: T) { this.h.push(x); this.up(this.h.length - 1); }
  pop(): T {
    const top = this.h[0], last = this.h.pop()!;
    if (this.h.length) { this.h[0] = last; this.down(0); }
    return top;
  }
  private up(i: number) {
    while (i > 0) { const p = (i - 1) >> 1; if (!this.less(this.h[i], this.h[p])) break;
      [this.h[i], this.h[p]] = [this.h[p], this.h[i]]; i = p; }
  }
  private down(i: number) {
    const n = this.h.length;
    while (true) {
      let s = i; const l = 2 * i + 1, r = 2 * i + 2;
      if (l < n && this.less(this.h[l], this.h[s])) s = l;
      if (r < n && this.less(this.h[r], this.h[s])) s = r;
      if (s === i) break;
      [this.h[i], this.h[s]] = [this.h[s], this.h[i]]; i = s;
    }
  }
}
// a MAX-heap is just a MinHeap with the comparison flipped:
const maxHeap = () => new MinHeap<number>((a, b) => a > b);
🎀 Interview lens Probed: the index arithmetic, sift-up vs sift-down, and that build-heap is O(n), not O(n log n) (sift-down from the bottom up). Β· Escalation: build-heap β†’ heapsort β†’ a k-ary heap / decrease-key for Dijkstra.

4.2 Β· Top-K / k-th

β€œFor the k largest, keep a MIN-heap of size k β€” its root is the k-th largest.”

Setup: the heap of size k always holds the best k seen, and its root is the weakest of them β€” exactly the threshold a newcomer must beat.

πŸ”‘ Invariant β€” why it's safe After each step the heap contains the k largest elements seen so far, and its root is their minimum. Any newcomer smaller than the root cannot be among the top k, so popping the root when the size exceeds k never discards a true top-k element.
// k-th largest in an array (or stream)
function kthLargest(nums: number[], k: number): number {
  const heap = new MinHeap<number>((a, b) => a < b);   // MIN-heap (note!)
  for (const x of nums) {
    heap.push(x);
    if (heap.size() > k) heap.pop();    // drop the smallest β†’ keep the largest k
  }
  return heap.peek();                    // root = k-th largest; heap = the top k
}
🎀 Interview lens Probed: why a min-heap finds the max-k, the size cap, and heap O(n log k) vs quickselect O(n). Β· Escalation: k-th largest β†’ "Top K Frequent" (heap, or bucket sort) β†’ "K Closest Points to Origin."

4.3 Β· K-way merge

β€œHeap the k current heads; pop the smallest, push its successor.”

Setup: to merge k sorted sequences, the global next-smallest is always the min of the k front elements β€” exactly what a min-heap of size k serves.

πŸ”‘ Invariant β€” why it's safe The heap always contains exactly one (the current front) element from each not-yet-exhausted sequence. So its root is the smallest unemitted element overall; popping it and pushing that sequence's next keeps the invariant.
// Merge k sorted linked lists
function mergeKLists(lists: (ListNode | null)[]): ListNode | null {
  const heap = new MinHeap<ListNode>((a, b) => a.val < b.val);
  for (const l of lists) if (l) heap.push(l);
  const dummy = new ListNode(0); let tail = dummy;
  while (heap.size()) {
    const node = heap.pop();
    tail.next = node; tail = node;
    if (node.next) heap.push(node.next);   // pull the next from that list
  }
  return dummy.next;
}
🎀 Interview lens Probed: heap size stays k, so it's O(n log k) not O(n log n). Β· Escalation: merge k lists β†’ "Smallest Range Covering Elements from K Lists" β†’ "K-th Smallest in a Sorted Matrix."

4.4 Β· Two heaps β€” running median & balance

β€œMax-heap holds the lower half, min-heap the upper; the median sits between.”

Setup: split the data into a lower half (a max-heap, so its top is the lower-half max) and an upper half (a min-heap, top = upper-half min). Keep the sizes balanced and the median is at the tops.

max-heap Β· lower half top = max of lows min-heap Β· upper half top = min of highs median between the two tops
Every low ≀ every high, and the sizes differ by ≀ 1 β€” so the median is one top (odd) or the average of both (even).
πŸ”‘ Invariant β€” why it's safe Two facts held after every insert: (1) every element in lo ≀ every element in hi; (2) lo.size equals hi.size or is one larger. Then the median is lo.top (odd count) or the average of the two tops (even).
class MedianFinder {
  private lo = new MinHeap<number>((a, b) => a > b);  // MAX-heap (lower half)
  private hi = new MinHeap<number>((a, b) => a < b);  // MIN-heap (upper half)
  addNum(x: number) {
    this.lo.push(x);
    this.hi.push(this.lo.pop());                     // pass lo's max into hi (keeps lo ≀ hi)
    if (this.hi.size() > this.lo.size()) this.lo.push(this.hi.pop()); // rebalance: lo β‰₯ hi
  }
  findMedian(): number {
    return this.lo.size() > this.hi.size()
      ? this.lo.peek()
      : (this.lo.peek() + this.hi.peek()) / 2;
  }
}
🎀 Interview lens Probed: the two invariants and the funnel-then-rebalance trick (why push to lo, move to hi, maybe move back). Β· Escalation: stream median β†’ "Sliding Window Median" (lazy deletion of leaving elements) β†’ "IPO" (two heaps keyed by capital and profit).

4.5 Β· Greedy driven by a heap

β€œRepeatedly pull the best available option; push back what it spawns.”

Setup: many scheduling/simulation problems reduce to "always act on the current most-urgent / cheapest / earliest-ending item" β€” a heap serves that item in O(log n).

πŸ”‘ Why the greedy choice is safe The heap's top is the globally best available option right now (here, the earliest-freeing room). Taking it never forecloses a better future β€” anything you skip is still on the heap next round. For Meeting Rooms: if even the soonest-ending room isn't free by this meeting's start, none are, so a new room is genuinely required.
// Meeting Rooms II β€” fewest rooms needed
function minMeetingRooms(intervals: number[][]): number {
  intervals.sort((a, b) => a[0] - b[0]);                 // by start
  const ends = new MinHeap<number>((a, b) => a < b);     // min-heap of end times
  let rooms = 0;
  for (const [start, end] of intervals) {
    if (ends.size() && ends.peek() <= start) ends.pop();  // a room freed up
    ends.push(end);
    rooms = Math.max(rooms, ends.size());                // peak concurrency
  }
  return rooms;
}
🎀 Interview lens Probed: justify the greedy choice, and use lazy deletion when "remove a specific item" is needed (skip stale tops on pop). Β· Escalation: meeting rooms β†’ "Task Scheduler" / "Reorganize String" β†’ "Minimum Cost to Connect Sticks" (Huffman) β†’ Dijkstra / Prim (a heap of frontier edges).

⏱️ 5 · Complexity

OperationTimeNote
peek (min/max)O(1)it's just the root
push / popO(log n)one root-to-leaf path
build-heap (from n items)O(n)sift-down bottom-up β€” not n log n
Top-k / k-way mergeO(n log k)heap capped at size k
search / arbitrary deleteO(n)heaps are bad at this β€” use lazy deletion or a different structure
The size-k insight Capping the heap at k turns "process n items" into O(n log k) β€” a big win when k β‰ͺ n, and the reason top-k/k-way-merge use a heap rather than sorting everything.

⚠️ 6 Β· Pitfalls β€” and the test that catches each

BugThe input that exposes it
Using a max-heap for "k largest" (keeps everything).Huge n, small k β†’ O(n) memory and O(n log n) time instead of O(n log k).
Forgetting to cap the heap at size k.n ≫ k β†’ the heap grows to n; the "k-th" is still right but the cost isn't.
Sift-down picks the wrong child / off-by-one in 2i+1, 2i+2.A 3-element heap β†’ the property breaks after the first pop.
Two heaps: rebalance lets sizes drift by > 1.Alternating inserts β†’ median comes from the wrong top.
Implementing a max-heap as a min-heap without flipping the comparator.Any input β†’ you get the min where you wanted the max.
No lazy deletion when items must be removed (sliding median / Dijkstra).Stale entries linger and get popped β†’ wrong answer; skip-on-pop fixes it.
πŸ”— This pattern, elsewhere Heapsort and the top-k vs quickselect trade-off live in sorting.html (heap: streaming/online; quickselect: one-shot static). Merge k lists upgrades the two-list merge / two-sequence pointer. Dijkstra & Prim are a min-heap over a frontier β€” see graph.html. "Connect sticks" is Huffman coding; two heaps is the balance trick reused in many median/percentile problems.
βž• Heap + greedy escalations β€” Meeting Rooms III (2402) Β· Rearrange String k Apart (358) Β· Max Events (1353) Β· Hire K Workers (857)
Four ways a heap drives a greedy: two heaps for rooms, a cooldown queue, earliest-deadline-first, and sort by one key, heap on another.
// 2402 Meeting Rooms III β€” free rooms (by index) + busy rooms (by end time, then index)
function mostBooked(n: number, meetings: number[][]): number {
  meetings.sort((a, b) => a[0] - b[0]);
  const free = new MinHeap<number>((a, b) => a < b);
  for (let r = 0; r < n; r++) free.push(r);
  const busy = new MinHeap<[number, number]>((a, b) => a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]));   // [end, room]
  const used: number[] = new Array(n).fill(0);
  for (const [s, e] of meetings) {
    while (busy.size() && busy.peek()[0] <= s) free.push(busy.pop()[1]);   // release rooms that finished
    let room: number, end: number;
    if (free.size()) { room = free.pop(); end = e; }
    else { const [t, r] = busy.pop(); room = r; end = t + (e - s); }       // delayed: keeps its DURATION
    busy.push([end, room]); used[room]++;
  }
  let best = 0;
  for (let r = 1; r < n; r++) if (used[r] > used[best]) best = r;          // ties β†’ lowest index
  return best;
}

// 358 Rearrange String k Distance Apart β€” max-heap by count + cooldown queue
function rearrangeString(s: string, k: number): string {
  if (k <= 1) return s;
  const cnt = new Map<string, number>();
  for (const ch of s) cnt.set(ch, (cnt.get(ch) ?? 0) + 1);
  const heap = new MinHeap<[number, string]>((a, b) => a[0] > b[0] || (a[0] === b[0] && a[1] < b[1]));   // most frequent first
  for (const [ch, c] of cnt) heap.push([c, ch]);
  const wait: [number, string][] = []; let res = '';
  while (heap.size()) {
    const [c, ch] = heap.pop(); res += ch;
    wait.push([c - 1, ch]);
    if (wait.length >= k) { const [c2, ch2] = wait.shift()!; if (c2 > 0) heap.push([c2, ch2]); }   // usable again after k placements
  }
  return res.length === s.length ? res : '';
}

// 1353 Maximum Number of Events That Can Be Attended β€” earliest deadline first
function maxEvents(events: number[][]): number {
  events.sort((a, b) => a[0] - b[0]);
  const heap = new MinHeap<number>((a, b) => a < b);                       // end days
  let i = 0, day = 0, count = 0;
  while (i < events.length || heap.size()) {
    if (!heap.size()) day = Math.max(day, events[i][0]);                   // jump over empty days
    while (i < events.length && events[i][0] <= day) heap.push(events[i++][1]);
    while (heap.size() && heap.peek() < day) heap.pop();                   // expired
    if (heap.size()) { heap.pop(); count++; }                              // attend the one ending soonest
    day++;
  }
  return count;
}

// 857 Minimum Cost to Hire K Workers β€” sort by wage/quality, heap keeps the K smallest qualities
function mincostToHireWorkers(quality: number[], wage: number[], k: number): number {
  const workers = quality.map((q, i) => ({ q, r: wage[i] / q })).sort((a, b) => a.r - b.r);
  const heap = new MinHeap<number>((a, b) => a > b);                       // max-heap of qualities
  let sumQ = 0, best = Infinity;
  for (const w of workers) {
    heap.push(w.q); sumQ += w.q;
    if (heap.size() > k) sumQ -= heap.pop();                               // drop the LARGEST quality
    if (heap.size() === k) best = Math.min(best, w.r * sumQ);              // w.r is the group rate (max ratio so far)
  }
  return best;
}
⚠️ Traps 2402: a delayed meeting keeps its duration; break end-time ties by room index. 358: release a character only after k placements. 1353: drop expired events before attending; jump day when the heap is empty. 857: compute the cost only when the heap holds exactly k; the current ratio is valid only because you sorted ascending.

πŸ“Š Coverage β€” pattern β†’ LeetCode (medium & hard)

Own each pattern and these are in range. Bold = Hard tier.

PatternRepresentative LeetCodeTier
Mechanics215 Kth Largest Β· 703 Kth Largest in a StreamEasy β†’ Med
Top-K347 Top K Frequent Β· 973 K Closest Points Β· 692 Top K WordsMed
K-way merge23 Merge k Sorted Lists Β· 373 K Pairs Β· 378 Kth in Sorted MatrixMed β†’ Hard
Two-heap median295 Median from Data Stream Β· 480 Sliding Window MedianHard
Greedy1046 Last Stone Weight Β· 253 Meeting Rooms II Β· 621 Task Scheduler Β· 2402 Meeting Rooms III Β· 358 Rearrange k Apart Β· 1353 Max Events Β· 857 Hire K WorkersEasy β†’ Med

βœ… 7 Β· Self-check

Cover the right column (blurred β€” hover/tap to reveal). Say your answer out loud first.

PromptAnswer
What does a heap guarantee, and what does it not?The extreme is at the root (peek O(1), push/pop O(log n)); it is not fully sorted and can't search/delete arbitrary elements cheaply.
For the k largest, which heap type and size, and why?A min-heap of size k. Its root is the weakest of the current top-k β€” exactly the bar a newcomer must clear; pop it when size > k.
What are parent/child indices for index i?parent = (iβˆ’1)/2; children = 2i+1 and 2i+2.
Why is k-way merge O(n log k) rather than O(n log n)?The heap holds only the k current heads, so each push/pop is log k.
State the two invariants of the median two-heap.Every lo ≀ every hi; and lo.size equals hi.size or is one bigger.
When should you use quickselect instead of a heap?A one-shot k-th on static, in-memory data β€” quickselect is O(n). Heaps win for streams / repeated queries.
How do you "remove a specific element" from a heap?Lazy deletion: mark it removed, and skip stale entries when they surface at the top.
Meeting Rooms III β€” what happens when every room is busy?Pop the room that frees up earliest and run the meeting there starting at that time, keeping its duration (end = t + (e βˆ’ s)).
Minimum Cost to Hire K Workers β€” why sort by wage/quality?Everyone in a group is paid at the group's max ratio; sorted ascending, the current worker's ratio is that max, so you just want the K smallest total quality (max-heap).

Write these cold (no peeking)

For each: write "which heap (min/max), what's at the root, what invariant" as a comment before the code.