Joseph Chen
EP.17 的 Meeting Rooms II 用了 min-heap,EP.20 把 Heap 從頭到尾打透。
Heap(堆)是一種特殊的完全二元樹,保證根節點永遠是最大(max-heap)或最小(min-heap)值。 插入和刪除都是 O(log n),查看最大 / 最小值是 O(1)。 這讓它成為所有「需要動態維護前 K 大/小」問題的首選。
Python 的 heapq 只有 min-heap, 但只要把值取負,就能模擬 max-heap——這是 Python 刷題最常見的技巧之一。
Python heapq 速查
| 操作 | 函式 | 複雜度 |
|---|---|---|
| 查看堆頂(最小值) | heap[0] | O(1) |
| 插入元素 | heappush(heap, val) | O(log n) |
| 彈出最小值 | heappop(heap) | O(log n) |
| 建堆(from list) | heapify(list) | O(n) |
| 彈出最小 + 插入 | heapreplace(heap, val) | O(log n) |
| 取前 k 小 | heapq.nsmallest(k, iterable) | O(n log k) |
| 取前 k 大 | heapq.nlargest(k, iterable) | O(n log k) |
Part 1 — Kth Largest Element
#215 · Medium · 找第 K 大的元素
題目
給一個整數陣列和整數 k,找出陣列中第 k 大的元素(不是第 k 個不重複的)。
nums = [3,2,1,5,6,4], k = 2 → 5
nums = [3,2,3,1,2,4,5,5,6], k = 4 → 4
思路:維護一個大小為 k 的 min-heap
關鍵直覺:「第 k 大」= 最小的那個 heap 的頂端。 維護一個大小恰好為 k 的 min-heap,裡面存的是目前見過的前 k 大元素。 堆頂(最小值)就是第 k 大。
nums=[3,2,1,5,6,4], k=2 的過程
[3]heap 小於 k,直接 push
[2, 3]heap 小於 k,直接 push
[2, 3]1 ≤ heap[0]=2,比堆頂小,不用換,丟掉
[3, 5]5 > heap[0]=2,heapreplace:pop 2,push 5
[5, 6]6 > heap[0]=3,heapreplace:pop 3,push 6
[5, 6]4 ≤ heap[0]=5,丟掉
[5, 6]heap[0] = 5,即第 2 大 ✅
為什麼不直接排序?
排序是 O(n log n),heap 解法是 O(n log k)。當 k 遠小於 n 時(例如找前 10 大 out of 百萬筆), heap 明顯更快。而且 heap 支援串流資料——不需要一次拿到所有資料, 可以邊接收新資料邊維護前 k 大,排序做不到這點。
Part 2 — Top K Frequent Elements
#347 · Medium · 出現頻率前 K 高的元素
題目
給一個整數陣列,回傳出現頻率最高的前 k 個元素,答案可以任意順序。
nums = [1,1,1,2,2,3], k = 2 → [1, 2]
nums = [1], k = 1 → [1]
解法一:HashMap + Min-Heap(O(n log k))
先用 Counter 統計頻率,再用大小為 k 的 min-heap 維護「頻率前 k 大的元素」。 heap 裡存 (freq, num) tuple, Python tuple 比較從第一個元素(freq)開始,完美符合需求。
解法二:Bucket Sort(O(n),面試加分)
頻率最大只有 n(所有元素都一樣),可以用「頻率當 index」的桶排序, 把每個數字放進對應頻率的桶,再從高頻率桶往低頻率桶取 k 個元素。
Heap 解法
- ✅ O(n log k),通用性強
- ✅ 支援串流資料
- ✅ k 很小時特別快
Bucket Sort 解法
- ✅ O(n),理論最快
- ✅ 面試說出來加分
- ❕ 只適用於頻率有上限的場景
Part 3 — Find Median from Data Stream
#295 · Hard · 動態資料流的中位數
題目
設計一個支援持續插入數字的資料結構,每次插入後都能在 O(log n) 內回傳當前所有數字的中位數。
addNum(1) → addNum(2) → findMedian() → 1.5
addNum(3) → findMedian() → 2.0
核心想法:兩個 Heap 夾住中位數
這是這篇最精妙的題目。用兩個 heap 把數字分成兩半:
left:Max-Heap(較小的一半)
存所有較小的數字,堆頂是其中最大的。 Python 用負值模擬 max-heap。
right:Min-Heap(較大的一半)
存所有較大的數字,堆頂是其中最小的。 標準 min-heap 即可。
兩個 Heap 的不變式
- 1.
left的所有元素 ≤right的所有元素 - 2.大小差最多 1:
len(left)和len(right)相差不超過 1
維持這兩個不變式後,中位數就是: 若兩側等長 → (-left[0] + right[0]) / 2; 若 left 多一個 → -left[0]。
addNum(1, 2, 3) 的雙 Heap 狀態
addNum(1)
left(max-heap)
right(min-heap)
1.0
left 先 push,left 有多 → 不平衡,left 頂移到 right?不,left 可以多一個
addNum(2)
left(max-heap)
right(min-heap)
1.5
2 > left 頂 1,push 到 right。左右等長 → 中位數 = (1+2)/2 = 1.5
addNum(3)
left(max-heap)
right(min-heap)
2.0
3 先 push 到 right,right 多一個 → 把 right 頂 3 移到 left?不對,3>2。left 頂到 right。最終 left=[2], right=[3],middle=2.0
為什麼不直接用排序陣列?
若用排序陣列,每次 addNum 插入需要 O(n)(保持排序),n 次操作就是 O(n²)。 雙 Heap 方案讓每次 addNum 只需 O(log n),findMedian O(1), n 次操作總計 O(n log n)——差距在百萬筆資料時極為明顯。
三題統一對比
| 題目 | Heap 類型 | Heap 大小 | 答案在哪 |
|---|---|---|---|
| #215 Kth Largest | Min-Heap | k | heap[0] |
| #347 Top K Frequent | Min-Heap(按頻率) | k | heap 裡的所有元素 |
| #295 Find Median | Max-Heap + Min-Heap | n/2 各 | 兩堆堆頂的平均或左堆頂 |
識別 Heap 題目的訊號
- →「第 K 大 / 小」、「前 K 個最...」——大小固定為 k 的 heap
- →「動態資料流」、「持續插入,隨時查詢最值」——heap 是唯一 O(log n) 的動態結構
- →「中位數」——雙 heap(左 max + 右 min),堆頂夾住中位數
- →需要反覆取最大或最小,且資料還在變化——優先考慮 heap,不要排序
這篇學到什麼
上一篇
EP.19 — Trie
Implement Trie、Add and Search、Replace Words
下一篇
EP.21 — 即將推出
敬請期待