Nếu bạn thấy từ khóa “top K”, “K-th largest/smallest”, “merge K”, “median” — đó là dấu hiệu cần dùng Heap. Heap cho O(logK) per operation thay vì O(NlogN) nếu sort toàn bộ.
Những gì FAANG interviewer mong đợi:
Kỹ năng
Mức độ quan trọng
Ghi chú
Biết heap là gì, cách hoạt động
⭐⭐⭐⭐⭐
Không được nhầm với BST
Implement MinHeap từ đầu
⭐⭐⭐⭐
Google hay hỏi
Top-K pattern với min-heap
⭐⭐⭐⭐⭐
Counter-intuitive: dùng MIN heap cho top K LARGEST
Merge K sorted lists
⭐⭐⭐⭐⭐
L5 classic
Median of data stream
⭐⭐⭐⭐⭐
Two heaps approach — L5 standard
Task Scheduler
⭐⭐⭐
Greedy + heap
Dijkstra’s (weighted shortest path)
⭐⭐⭐
L5/L6, heap + dist array
L4 expectation: Giải được Top-K, kth largest, biết dùng heap khi nào.
L5 expectation: Implement heap, giải Merge K Sorted Lists, Median from Stream, explain trade-offs vs sort.
3. Visual Thinking (Mermaid)
3.1 Binary Heap — Array Representation và Index Formula
graph TD
A["arr[0] = 1<br/>(root)"]
B["arr[1] = 3"]
C["arr[2] = 5"]
D["arr[3] = 7"]
E["arr[4] = 9"]
F["arr[5] = 8"]
G["arr[6] = 10"]
A --> B
A --> C
B --> D
B --> E
C --> F
C --> G
style A fill:#e74c3c,color:#fff
flowchart TD
subgraph Step1["Step 1: Insert 2 tại cuối"]
S1["[1, 3, 5, 7, 9, 8, 10, 2]<br/> ↑ new"]
end
subgraph Step2["Step 2: Bubble up — compare 2 với parent 7"]
S2["2 < 7 → swap!<br/>[1, 3, 5, 2, 9, 8, 10, 7]"]
end
subgraph Step3["Step 3: Compare 2 với parent 3"]
S3["2 < 3 → swap!<br/>[1, 2, 5, 3, 9, 8, 10, 7]"]
end
subgraph Step4["Step 4: Compare 2 với parent 1"]
S4["2 > 1 → STOP<br/>Heap property restored"]
end
Step1 --> Step2 --> Step3 --> Step4
3.3 Heapify Down (Extract-Min) Process
flowchart TD
subgraph Step1["Step 1: Remove root (1), move last element to root"]
S1["[10, 3, 5, 7, 9, 8]<br/> ↑ last element (10) becomes root"]
end
subgraph Step2["Step 2: Sift down — 10 vs children 3, 5 → smaller child = 3"]
S2["10 > 3 → swap!<br/>[3, 10, 5, 7, 9, 8]"]
end
subgraph Step3["Step 3: 10 vs children 7, 9 → smaller child = 7"]
S3["10 > 7 → swap!<br/>[3, 7, 5, 10, 9, 8]"]
end
subgraph Step4["Step 4: 10 có no children → STOP"]
S4["Heap property restored<br/>Min = 3 (extracted)"]
end
Step1 --> Step2 --> Step3 --> Step4
3.4 Two Heaps for Median
graph LR
subgraph MaxHeap["Max-Heap (lower half)<br/>[1, 2, 3, 4, 5]"]
direction TB
M5((5)) --> M3((3))
M5 --> M4((4))
M3 --> M1((1))
M3 --> M2((2))
end
MEDIAN["Median = (5 + 6) / 2 = 5.5"]
subgraph MinHeap["Min-Heap (upper half)<br/>[6, 8, 9]"]
direction TB
N6((6)) --> N8((8))
N6 --> N9((9))
end
MaxHeap --> MEDIAN
MEDIAN --> MinHeap
style MEDIAN fill:#f39c12,color:#000
4. TypeScript Template
4.1 MinHeap Class — Complete Implementation
class MinHeap { private heap: number[]; constructor() { this.heap = []; } // Helper: index formulas private parent(i: number): number { return Math.floor((i - 1) / 2); } private left(i: number): number { return 2 * i + 1; } private right(i: number): number { return 2 * i + 2; } private swap(i: number, j: number): void { [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]; } // Insert: O(log N) insert(val: number): void { this.heap.push(val); this.heapifyUp(this.heap.length - 1); } private heapifyUp(i: number): void { while (i > 0 && this.heap[i] < this.heap[this.parent(i)]) { this.swap(i, this.parent(i)); i = this.parent(i); } } // Extract minimum: O(log N) extractMin(): number | undefined { if (this.heap.length === 0) return undefined; if (this.heap.length === 1) return this.heap.pop(); const min = this.heap[0]; this.heap[0] = this.heap.pop()!; // Move last to root this.heapifyDown(0); return min; } private heapifyDown(i: number): void { const n = this.heap.length; let smallest = i; const l = this.left(i); const r = this.right(i); if (l < n && this.heap[l] < this.heap[smallest]) smallest = l; if (r < n && this.heap[r] < this.heap[smallest]) smallest = r; if (smallest !== i) { this.swap(i, smallest); this.heapifyDown(smallest); } } // Peek minimum without removing: O(1) peek(): number | undefined { return this.heap[0]; } size(): number { return this.heap.length; }}// MaxHeap: chỉ thay đổi comparison directionclass MaxHeap { private heap: number[]; constructor() { this.heap = []; } private parent(i: number): number { return Math.floor((i - 1) / 2); } private left(i: number): number { return 2 * i + 1; } private right(i: number): number { return 2 * i + 2; } private swap(i: number, j: number): void { [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]; } insert(val: number): void { this.heap.push(val); let i = this.heap.length - 1; while (i > 0 && this.heap[i] > this.heap[this.parent(i)]) { // Changed: > instead of < this.swap(i, this.parent(i)); i = this.parent(i); } } extractMax(): number | undefined { if (this.heap.length === 0) return undefined; if (this.heap.length === 1) return this.heap.pop(); const max = this.heap[0]; this.heap[0] = this.heap.pop()!; let i = 0; const n = this.heap.length; while (true) { let largest = i; const l = 2 * i + 1, r = 2 * i + 2; if (l < n && this.heap[l] > this.heap[largest]) largest = l; if (r < n && this.heap[r] > this.heap[largest]) largest = r; if (largest === i) break; this.swap(i, largest); i = largest; } return max; } peek(): number | undefined { return this.heap[0]; } size(): number { return this.heap.length; }}
Off-by-one Index — Nhớ công thức này
parent(i) = Math.floor((i - 1) / 2) ← không phải i/2
leftChild(i) = 2 * i + 1
rightChild(i) = 2 * i + 2
Root ở index 0. Nếu root ở index 1 (một số sách dạy vậy), công thức sẽ khác: parent = i/2, left = 2i, right = 2i+1.
4.2 kthLargestElement — Min-Heap of Size K
// LeetCode #215: Kth Largest Element in an Array// Approach: Dùng min-heap size K// Heap luôn chứa K phần tử lớn nhất đã thấy// Root của min-heap = phần tử nhỏ nhất trong K phần tử lớn nhất = Kth largestfunction findKthLargest(nums: number[], k: number): number { const minHeap = new MinHeap(); for (const num of nums) { minHeap.insert(num); if (minHeap.size() > k) { minHeap.extractMin(); // Loại bỏ phần tử nhỏ nhất — không thuộc top K } } return minHeap.peek()!; // Root = Kth largest}// ===== TEST CASES =====// [3,2,1,5,6,4], k=2 → 5// Sau khi process: heap = [5, 6], root = 5 = 2nd largest ✓// [3,2,3,1,2,4,5,5,6], k=4 → 4// Top 4 largest: [6, 5, 5, 4], Kth largest = 4 ✓// ===== COMPLEXITY =====// Time: O(N log K) — N elements, each insert/extract is O(log K)// Space: O(K) — heap contains at most K+1 elements at any time// vs Sort approach: O(N log N) time, O(1) space// Khi K << N: O(N log K) << O(N log N)
Counter-intuitive: Dùng MIN-heap cho top K LARGEST
Logic: Min-heap chứa K phần tử lớn nhất. Root = smallest trong K lớn nhất = Kth largest overall.
Khi thêm phần tử mới: nếu nó lớn hơn root → nó thuộc top K, extract root (loại K+1-th), insert mới.
Nếu dùng max-heap: phải giữ N-K+1 phần tử nhỏ nhất — tốn hơn.
4.3 mergeKSortedLists
class ListNode { val: number; next: ListNode | null; constructor(val: number = 0, next: ListNode | null = null) { this.val = val; this.next = next; }}// Approach: Min-heap luôn chứa head của mỗi list còn lại// Mỗi lần extract min, advance pointer trong list đófunction mergeKLists(lists: Array<ListNode | null>): ListNode | null { // Simulate min-heap với sorted array (trong interview thực tế) // Trong production: dùng proper heap implementation type HeapEntry = { val: number; node: ListNode }; // Proper binary min-heap on HeapEntry — KHÔNG dùng sort+shift! // sort+shift mỗi insert là O(N log N) → tổng O(NK log K · log K) — sai complexity. const heap: HeapEntry[] = []; const heapPush = (entry: HeapEntry): void => { heap.push(entry); let i = heap.length - 1; while (i > 0) { const p = (i - 1) >> 1; if (heap[p].val <= heap[i].val) break; [heap[p], heap[i]] = [heap[i], heap[p]]; i = p; } }; const heapPop = (): HeapEntry | undefined => { if (heap.length === 0) return undefined; const top = heap[0]; const last = heap.pop()!; if (heap.length > 0) { heap[0] = last; let i = 0; const n = heap.length; while (true) { const l = 2*i + 1, r = 2*i + 2; let smallest = i; if (l < n && heap[l].val < heap[smallest].val) smallest = l; if (r < n && heap[r].val < heap[smallest].val) smallest = r; if (smallest === i) break; [heap[smallest], heap[i]] = [heap[i], heap[smallest]]; i = smallest; } } return top; }; // Initialize: add head of each non-null list for (const head of lists) { if (head) heapPush({ val: head.val, node: head }); } const dummy = new ListNode(0); let curr = dummy; while (heap.length > 0) { const { node } = heapPop()!; curr.next = node; curr = curr.next; if (node.next) { heapPush({ val: node.next.val, node: node.next }); } } return dummy.next;}// ===== COMPLEXITY =====// K = số lists, N = tổng số nodes// Time: O(N log K) — N nodes, mỗi lần insert/extract là O(log K)// Space: O(K) — heap chứa tối đa K elements (một head per list)// vs Naive merge: O(N * K) — merge từng pair// vs Sort all: O(N log N)
4.4 findMedianFromDataStream — Two Heaps
Xem phần 8 cho complete solution đầy đủ.
// Concept sketch — chi tiết ở Section 8class MedianFinder { // lower half — max-heap (lưu nửa nhỏ hơn) private lowerMaxHeap: MaxHeap; // upper half — min-heap (lưu nửa lớn hơn) private upperMinHeap: MinHeap; constructor() { this.lowerMaxHeap = new MaxHeap(); this.upperMinHeap = new MinHeap(); } addNum(num: number): void { // Thêm vào lower half trước this.lowerMaxHeap.insert(num); // Balance: max của lower phải <= min của upper if ( this.upperMinHeap.size() > 0 && this.lowerMaxHeap.peek()! > this.upperMinHeap.peek()! ) { this.upperMinHeap.insert(this.lowerMaxHeap.extractMax()!); } // Balance sizes: lower có thể nhiều hơn upper 1 phần tử (nếu total là số lẻ) if (this.lowerMaxHeap.size() > this.upperMinHeap.size() + 1) { this.upperMinHeap.insert(this.lowerMaxHeap.extractMax()!); } else if (this.upperMinHeap.size() > this.lowerMaxHeap.size()) { this.lowerMaxHeap.insert(this.upperMinHeap.extractMin()!); } } findMedian(): number { if (this.lowerMaxHeap.size() === this.upperMinHeap.size()) { return (this.lowerMaxHeap.peek()! + this.upperMinHeap.peek()!) / 2; } return this.lowerMaxHeap.peek()!; // Lower luôn lớn hơn hoặc bằng upper }}
4.5 topKFrequentElements
// LeetCode #347: Top K Frequent Elements// Approach 1: Sort by frequency O(N log N)// Approach 2: Min-heap of size K O(N log K) — better khi K << N// Approach 3: Bucket sort O(N) — optimal!function topKFrequent(nums: number[], k: number): number[] { // Count frequencies const freq = new Map<number, number>(); for (const num of nums) { freq.set(num, (freq.get(num) || 0) + 1); } // Bucket sort: buckets[i] = list of numbers with frequency i const buckets: number[][] = Array.from({ length: nums.length + 1 }, () => []); for (const [num, count] of freq) { buckets[count].push(num); } // Collect top K from highest frequency buckets const result: number[] = []; for (let i = buckets.length - 1; i >= 0 && result.length < k; i--) { result.push(...buckets[i]); } return result.slice(0, k);}// ===== COMPLEXITY =====// Time: O(N) — bucket sort approach// Space: O(N) — freq map + buckets// Heap approach (Min-Heap size K) — O(N log K)// CHÚ Ý: dùng REAL binary heap, không phải sort+shift (sort+shift là O(N² log N)!)function topKFrequentHeap(nums: number[], k: number): number[] { const freq = new Map<number, number>(); for (const num of nums) freq.set(num, (freq.get(num) || 0) + 1); // Min-heap of [freq, num] — root là freq nhỏ nhất. // Khi heap size > k, pop root (freq nhỏ nhất) → giữ K phần tử freq lớn nhất. const heap: [number, number][] = []; const push = (item: [number, number]) => { heap.push(item); let i = heap.length - 1; while (i > 0) { const p = (i - 1) >> 1; if (heap[p][0] <= heap[i][0]) break; [heap[p], heap[i]] = [heap[i], heap[p]]; i = p; } }; const pop = () => { const top = heap[0]; const last = heap.pop()!; if (heap.length > 0) { heap[0] = last; let i = 0; const n = heap.length; while (true) { const l = 2*i + 1, r = 2*i + 2; let s = i; if (l < n && heap[l][0] < heap[s][0]) s = l; if (r < n && heap[r][0] < heap[s][0]) s = r; if (s === i) break; [heap[s], heap[i]] = [heap[i], heap[s]]; i = s; } } return top; }; for (const [num, count] of freq) { push([count, num]); if (heap.length > k) pop(); // Bỏ phần tử có freq nhỏ nhất → O(log K) } return heap.map(([, num]) => num);}
5. Complexity Deep-dive
Heap Operations
Operation
Time
Space
Ghi chú
Insert
O(logN)
O(1)
Heapify up — tối đa H levels
Extract-Min/Max
O(logN)
O(1)
Heapify down — tối đa H levels
Peek
O(1)
O(1)
Root luôn ở index 0
Build Heap từ array
O(N)
O(1)
Không phải O(NlogN)!
Search
O(N)
O(1)
Heap không hỗ trợ fast search
Delete arbitrary
O(logN)
O(1)
Sau khi tìm được node: O(N) find + O(log N) fix
Tại sao Build Heap là O(N), không phải O(NlogN)?
Trực giác: Khi build heap bằng cách heapify từ dưới lên (không phải insert từng phần tử):
Nodes ở level thấp nhất (nhiều nhất) chỉ cần sift down 0 bước
Nodes ở level tiếp theo cần tối đa 1 bước
Chỉ root (1 node) cần sift down logN bước
T=∑h=0logN2h+1N⋅h=O(N)
O(N) Build Heap — Khi nào dùng?
Build heap O(N) chỉ hữu ích khi bạn có sẵn toàn bộ array ngay từ đầu. Nếu elements đến theo stream (online), phải insert từng cái → O(log N) per element → O(N log N) total.
Top-K Comparison
Approach
Time
Space
Khi nào dùng
Sort toàn bộ
O(NlogN)
O(1)
K gần bằng N
Min-Heap size K
O(NlogK)
O(K)
K << N (thường dùng)
QuickSelect
O(N) average
O(1)
Chỉ cần Kth element, không cần sort
Bucket Sort
O(N)
O(N)
Khi values bounded (như frequency)
O(NlogK)≪O(NlogN) khi K≪N
6. Edge Cases & Pitfalls
JavaScript/TypeScript không có built-in Heap
// WRONG: Dùng array.sort() mỗi lần insert → O(N) per insertconst heap: number[] = [];heap.push(val);heap.sort((a, b) => a - b); // O(N log N)! Không phải heap!// CORRECT: Implement MinHeap class với heapifyUp/Down// Hoặc dùng thư viện ngoài (trong competitive programming)// Trong FAANG interview: implement từ đầu hoặc giả định có heap
Off-by-One trong Index Formula
// Nhớ: root ở index 0 (zero-indexed)parent(i) = Math.floor((i - 1) / 2) // Không phải Math.floor(i / 2)leftChild(i) = 2 * i + 1 // Không phải 2 * irightChild(i) = 2 * i + 2 // Không phải 2 * i + 1// Nếu root ở index 1 (one-indexed — một số code dùng):parent(i) = Math.floor(i / 2)leftChild(i) = 2 * irightChild(i) = 2 * i + 1
Top-K Largest: Dùng MIN-Heap, không phải MAX-Heap
Counter-intuitive nhất trong heap problems:
Muốn top K largest → dùng min-heap size K
Lý do: cần extract (loại bỏ) những phần tử nhỏ hơn Kth largest
Min-heap root = smallest trong top K = “threshold” — nếu new element > root, nó thuộc top K
Max-heap size K: sẽ loại phần tử lớn nhất → sai!
Two Heaps for Median — Balancing Rule
Invariant cần maintain:
lowerMaxHeap.peek() <= upperMinHeap.peek() — tất cả phần tử của lower ≤ tất cả phần tử của upper
|lowerSize - upperSize| <= 1 — hai heap gần bằng nhau về size (lower có thể nhiều hơn 1 nếu total lẻ)
Sau mỗi addNum, check và restore cả hai invariants theo thứ tự.
Heap vs Sorted Array — Trade-offs
Heap
Sorted Array
Insert
O(log N)
O(N) — shift elements
Extract min/max
O(log N)
O(1) — take from end
Search
O(N)
O(log N) — binary search
Dùng khi
Dynamic stream, frequent insert/extract
Static data, frequent search
7. Pattern Recognition
Khi thấy những từ khóa này → nghĩ đến Heap:
Từ khóa trong đề
Pattern
”top K elements”
Min-heap size K, O(NlogK)
“K-th largest / smallest”
Min/Max-heap size K, hoặc QuickSelect
”merge K sorted lists/arrays”
Min-heap with K heads
”median of data stream”
Two heaps (max lower + min upper)
“sliding window maximum”
Monotonic deque hoặc max-heap
”task scheduler / CPU scheduling”
Max-heap by frequency
”connect ropes with min cost”
Min-heap, greedy
”find closest K points”
Max-heap size K (by distance)
“stream / online algorithm”
Heap — maintain invariant incrementally
Heap vs Deque vs Segment Tree
Top K from static array: Sort O(NlogN) hoặc QuickSelect O(N)
Top K from stream: Min/Max-heap O(NlogK)
Sliding window max/min: Monotonic Deque O(N) — heap sẽ là O(NlogN)
Range max/min queries: Segment Tree O(logN) per query
Heap là workhorse cho “dynamic top K” — nhưng có những bài cần cấu trúc khác.