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).
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).
n β O(n log n), and impossible if the data streams in and doesn't fit in memory.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.O(n log k) time, O(k) space, works on an infinite stream.| incoming | action (k = 2) | heap after | 2nd largest = root |
|---|---|---|---|
| 5 | push, over k β pop min (1) | {3, 5} | 3 |
| 2 | push, pop min (2) | {3, 5} | 3 |
| 4 | push, 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.
| Ask | If yes β | Pattern |
|---|---|---|
| Need to implement the heap / a custom-priority queue? | array + sift-up / sift-down | Mechanics (Β§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 heads | K-way merge (Β§4.3) |
| Running median / keep two halves balanced? | a max-heap + a min-heap | Two heaps (Β§4.4) |
| Repeatedly pick the best available (schedule, combine)? | greedy, driven by a heap | Greedy + heap (Β§4.5) |
O(n), beating a heap's O(n log k). (Heaps win for streaming / repeated queries.)| Words in the prompt | Reach 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 |
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.
βPush and bubble up; pop the root, move the last up, and sink it down.β
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);
O(n log n) (sift-down from the bottom up). Β· Escalation: build-heap β heapsort β a k-ary heap / decrease-key for Dijkstra.β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.
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
}
O(n log k) vs quickselect O(n). Β· Escalation: k-th largest β "Top K Frequent" (heap, or bucket sort) β "K Closest Points to Origin."β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.
// 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;
}
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."β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.
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;
}
}
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).β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).
// 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;
}
| Operation | Time | Note |
|---|---|---|
| peek (min/max) | O(1) | it's just the root |
| push / pop | O(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 merge | O(n log k) | heap capped at size k |
| search / arbitrary delete | O(n) | heaps are bad at this β use lazy deletion or a different structure |
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.| Bug | The 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. |
// 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;
}
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.Own each pattern and these are in range. Bold = Hard tier.
| Pattern | Representative LeetCode | Tier |
|---|---|---|
| Mechanics | 215 Kth Largest Β· 703 Kth Largest in a Stream | Easy β Med |
| Top-K | 347 Top K Frequent Β· 973 K Closest Points Β· 692 Top K Words | Med |
| K-way merge | 23 Merge k Sorted Lists Β· 373 K Pairs Β· 378 Kth in Sorted Matrix | Med β Hard |
| Two-heap median | 295 Median from Data Stream Β· 480 Sliding Window Median | Hard |
| Greedy | 1046 Last Stone Weight Β· 253 Meeting Rooms II Β· 621 Task Scheduler Β· 2402 Meeting Rooms III Β· 358 Rearrange k Apart Β· 1353 Max Events Β· 857 Hire K Workers | Easy β Med |
Cover the right column (blurred β hover/tap to reveal). Say your answer out loud first.
| Prompt | Answer |
|---|---|
| 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). |
MinHeap (push/pop, sift-up/down)kthLargest (min-heap of size k)mergeKListsMedianFinderminMeetingRoomsFor each: write "which heap (min/max), what's at the root, what invariant" as a comment before the code.