6 min read
Heaps & Priority Queue
A heap is a complete binary tree satisfying the heap property. Used whenever you need the min/max element repeatedly.
- Min-heap: parent ≤ children →
peek()= global minimum - Max-heap: parent ≥ children →
peek()= global maximum
JavaScript has no built-in heap — you implement one or simulate with a sorted array (acceptable in interviews, note the trade-off).
Min-Heap Implementation
tsclass MinHeap {
private data: number[] = [];
get size() { return this.data.length; }
peek(): number | undefined { return this.data[0]; }
push(val: number): void {
this.data.push(val);
this._bubbleUp(this.data.length - 1);
}
pop(): number | undefined {
if (this.size === 0) return undefined;
const min = this.data[0];
const last = this.data.pop()!;
if (this.size > 0) {
this.data[0] = last;
this._sinkDown(0);
}
return min;
}
private _bubbleUp(i: number): void {
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.data[parent] <= this.data[i]) break;
[this.data[parent], this.data[i]] = [this.data[i], this.data[parent]];
i = parent;
}
}
private _sinkDown(i: number): void {
const n = this.size;
while (true) {
let smallest = i;
const l = 2 * i + 1, r = 2 * i + 2;
if (l < n && this.data[l] < this.data[smallest]) smallest = l;
if (r < n && this.data[r] < this.data[smallest]) smallest = r;
if (smallest === i) break;
[this.data[smallest], this.data[i]] = [this.data[i], this.data[smallest]];
i = smallest;
}
}
}
// Usage:
const h = new MinHeap();
[5, 3, 8, 1, 4].forEach(n => h.push(n));
console.log(h.pop()); // 1
console.log(h.pop()); // 3
console.log(h.peek()); // 4Complexity: push O(log n), pop O(log n), peek O(1), build-heap O(n)
Generic Heap (with comparator)
tsclass Heap<T> {
private data: T[] = [];
constructor(private cmp: (a: T, b: T) => number) {}
get size() { return this.data.length; }
peek(): T | undefined { return this.data[0]; }
push(val: T): void {
this.data.push(val);
let i = this.data.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (this.cmp(this.data[p], this.data[i]) <= 0) break;
[this.data[p], this.data[i]] = [this.data[i], this.data[p]];
i = p;
}
}
pop(): T | undefined {
if (!this.size) return undefined;
const top = this.data[0];
const last = this.data.pop()!;
if (this.size) { this.data[0] = last; this._sink(0); }
return top;
}
private _sink(i: number): void {
const n = this.size;
while (true) {
let best = i;
const l = 2*i+1, r = 2*i+2;
if (l < n && this.cmp(this.data[l], this.data[best]) < 0) best = l;
if (r < n && this.cmp(this.data[r], this.data[best]) < 0) best = r;
if (best === i) break;
[this.data[best], this.data[i]] = [this.data[i], this.data[best]];
i = best;
}
}
}
// Min-heap of numbers:
const minH = new Heap<number>((a, b) => a - b);
// Max-heap of numbers:
const maxH = new Heap<number>((a, b) => b - a);
// Min-heap by object property:
const taskH = new Heap<{priority: number; name: string}>((a, b) => a.priority - b.priority);Problem 1: K Largest Elements
ts// Return k largest elements from array — O(n log k)
function kLargest(nums: number[], k: number): number[] {
// Keep a min-heap of size k
const heap = new Heap<number>((a, b) => a - b);
for (const n of nums) {
heap.push(n);
if (heap.size > k) heap.pop(); // evict smallest
}
const result: number[] = [];
while (heap.size) result.unshift(heap.pop()!);
return result; // sorted ascending — last k elements are largest
}
console.log(kLargest([3, 1, 5, 12, 2, 11], 3)); // [5, 11, 12]Problem 2: K-th Largest Element in Stream
tsclass KthLargest {
private heap: Heap<number>;
constructor(private k: number, nums: number[]) {
this.heap = new Heap<number>((a, b) => a - b);
for (const n of nums) this.add(n);
}
add(val: number): number {
this.heap.push(val);
while (this.heap.size > this.k) this.heap.pop();
return this.heap.peek()!;
}
}
const kth = new KthLargest(3, [4, 5, 8, 2]);
console.log(kth.add(3)); // 4
console.log(kth.add(5)); // 5
console.log(kth.add(10)); // 5
console.log(kth.add(9)); // 8Problem 3: Merge K Sorted Lists
tsinterface ListNode { val: number; next: ListNode | null; }
function mergeKLists(lists: (ListNode | null)[]): ListNode | null {
const heap = new Heap<ListNode>((a, b) => a.val - b.val);
// Seed with heads
for (const node of lists) {
if (node) heap.push(node);
}
const dummy: ListNode = { val: 0, next: null };
let cur = dummy;
while (heap.size) {
const node = heap.pop()!;
cur.next = node;
cur = node;
if (node.next) heap.push(node.next);
}
return dummy.next;
}
// Time: O(n log k) where n = total nodes, k = number of lists
// Space: O(k) for the heapProblem 4: Top K Frequent Elements
tsfunction topKFrequent(nums: number[], k: number): number[] {
const freq = new Map<number, number>();
for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1);
// Min-heap by frequency — keep top k
const heap = new Heap<[number, number]>((a, b) => a[1] - b[1]);
for (const [num, count] of freq) {
heap.push([num, count]);
if (heap.size > k) heap.pop();
}
const result: number[] = [];
while (heap.size) result.push(heap.pop()![0]);
return result;
}
console.log(topKFrequent([1,1,1,2,2,3], 2)); // [2, 1] (order may vary)Problem 5: Find Median from Data Stream
ts// Classic two-heap trick: max-heap for lower half, min-heap for upper half
class MedianFinder {
private lo = new Heap<number>((a, b) => b - a); // max-heap
private hi = new Heap<number>((a, b) => a - b); // min-heap
addNum(num: number): void {
this.lo.push(num);
// Balance: lo's max must be ≤ hi's min
this.hi.push(this.lo.pop()!);
// Keep lo size >= hi size
if (this.lo.size < this.hi.size) this.lo.push(this.hi.pop()!);
}
findMedian(): number {
if (this.lo.size > this.hi.size) return this.lo.peek()!;
return (this.lo.peek()! + this.hi.peek()!) / 2;
}
}
const mf = new MedianFinder();
mf.addNum(1); mf.addNum(2);
console.log(mf.findMedian()); // 1.5
mf.addNum(3);
console.log(mf.findMedian()); // 2Problem 6: Task Scheduler
ts// Given tasks (letters), cooldown n, find minimum intervals needed
function leastInterval(tasks: string[], n: number): number {
const freq = new Array(26).fill(0);
for (const t of tasks) freq[t.charCodeAt(0) - 65]++;
const maxH = new Heap<number>((a, b) => b - a);
for (const f of freq) if (f > 0) maxH.push(f);
let time = 0;
const queue: [number, number][] = []; // [remaining, availableAt]
while (maxH.size || queue.length) {
time++;
if (maxH.size) {
const remaining = maxH.pop()! - 1;
if (remaining > 0) queue.push([remaining, time + n]);
}
if (queue.length && queue[0][1] === time) {
maxH.push(queue.shift()![0]);
}
}
return time;
}
console.log(leastInterval(['A','A','A','B','B','B'], 2)); // 8Problem 7: Dijkstra with Heap
tsfunction dijkstra(graph: Map<number, [number, number][]>, src: number): Map<number, number> {
const dist = new Map<number, number>();
const heap = new Heap<[number, number]>((a, b) => a[0] - b[0]); // [cost, node]
heap.push([0, src]);
dist.set(src, 0);
while (heap.size) {
const [cost, u] = heap.pop()!;
if (cost > (dist.get(u) ?? Infinity)) continue; // stale entry
for (const [v, w] of graph.get(u) ?? []) {
const newCost = cost + w;
if (newCost < (dist.get(v) ?? Infinity)) {
dist.set(v, newCost);
heap.push([newCost, v]);
}
}
}
return dist;
}Interview Cheatsheet
| Problem pattern | Heap type | Why |
|---|---|---|
| K largest | Min-heap size k | Pop when > k, root = kth largest |
| K smallest | Max-heap size k | Pop when > k, root = kth smallest |
| Merge k sorted | Min-heap of heads | Always extract global min |
| Median stream | Max + Min heap | Lower/upper halves balanced |
| Sliding window max | Monotonic deque | Heap works but deque is O(n) |
| Shortest path | Min-heap by cost | Dijkstra's algorithm |
| Task scheduling | Max-heap by freq | Greedy: always schedule most frequent |
Complexity Summary
| Operation | Binary Heap |
|---|---|
| push | O(log n) |
| pop | O(log n) |
| peek | O(1) |
| build from array | O(n) |
| search | O(n) |
Common Mistakes
- Forgetting to handle stale entries in Dijkstra — check
cost > dist[u]and skip - Using max-heap for K largest — you'd need to push all n elements first (O(n log n)); min-heap of size k is O(n log k)
- Off-by-one in two-heap median — lo must always be >= hi in size
- Not re-seeding heap after pop in merge-k — push
node.nextafter extracting node
[prev·next]