Joseph Chen
我第一次真正碰到 Heap,不是從資料結構課本開始,而是從 #347 Top K Frequent Elements 開始。 題目問的是:給一串數字,找出出現頻率最高的前 k 個元素。
一開始我其實沒有想到 Heap。最直覺的做法是:先用 Counter 統計每個數字出現幾次, 再用 most_common(k) 直接拿前 k 高頻的元素。 這個解法很好懂,也很適合當作這篇文章的起點。
但如果只停在 most_common(),會知道「怎麼用」,卻不一定知道「底層為什麼可以做到」。 所以這篇會照著我的學習路線:先從能過題目的寫法開始,再往下看 heapq.nlargest(), 最後自己用 heappush 和 heappop 實作一次。
這篇文章的學習順序
先解題
Counter(nums).most_common(k)先拿到能過題目的版本,建立 Top K Frequent 的問題感。
整理答案
[num for num, freq in ...]用串列生成式把 tuple 清成 LeetCode 需要的答案格式。
看往底層
heapq.nlargest(k, ...)不用 most_common 時,改用 heap 思維取前 k 大。
手刻 nlargest
heappush / heappop理解 heap 如何只保留最有價值的 k 個元素。
看結構
binary heap把 list index 和完全二元樹對起來,理解 heapq 為什麼能 O(log k)。
我覺得這個順序更適合初學者
因為它不是一開始就丟出抽象的 Heap 定義,而是先從你真的遇到的題目和 Python 函式開始。 讀者會先知道「這題到底要解什麼」,再逐步理解「為什麼 Top K 問題會需要 Heap」。
Step 1 — 用 Counter 統計頻率
先把「每個數字出現幾次」算出來
Counter 可以把 iterable 裡的元素拿來計數。 在這題裡,key 是數字本身,value 是出現次數。 所以 Counter([1,1,1,2,2,3]) 會得到1:3, 2:2, 3:1。
| 用法 | 意思 |
|---|---|
| Counter(nums) | 統計 nums 裡每個元素出現幾次 |
| count[1] | 查詢 1 出現幾次,這裡會得到 3 |
| count.items() | 取得所有 (num, freq),例如 (1, 3)、(2, 2) |
Step 2 — most_common(k) 直接拿前 k 高頻
這是最直覺,也最適合先理解題目的寫法
most_common(k) 會回傳一個 list, 裡面每個元素都是 (元素, 次數) 的 tuple, 並且已經按照次數由高到低排列。
拆開看
Counter(nums)Counter({1: 3, 2: 2, 3: 1})count.most_common(2)[(1, 3), (2, 2)][num for num, freq in top_k][1, 2]Step 3 — 串列生成式怎麼理解?
把「迴圈產生新 list」寫成一行
most_common(k) 回傳的是[(num, freq), ...], 但 LeetCode 要的答案通常只需要數字本身,所以要把每個 tuple 裡的 num 抽出來。
串列生成式的閱讀方式
[num for num, freq in top_k] 可以讀成: 「對 top_k 裡的每一組 (num, freq),我只把 num 放進新的 list」。 前面的 num 是你要收集的結果, 後面的 for num, freq in top_k 是資料從哪裡來。
Step 4 — 不用 most_common,改用 heapq.nlargest
這一步開始進入 Heap 思維
先把 heapq.nlargest() 當成一句白話來看: 「請你從一堆候選資料裡,依照我指定的比較規則,找出最大的 k 個」。 在 #347 裡,候選資料是「不重複的數字」,比較規則是「出現頻率」。
這段程式的重點是 key。heapq.nlargest(k, iterable, key=...) 不是直接比較元素本身, 而是先把元素丟進 key 函式,拿回來的值才是排序和比較依據。
展開版函式
適合初學時閱讀,因為函式有名字,也可以在裡面放多行邏輯。
lambda 縮寫版
lambda 是匿名函式。 冒號左邊是參數,右邊是回傳值;適合邏輯很短時使用。
| 片段 | 意思 |
|---|---|
| k | 要取前幾大的元素 |
| count.keys() | 候選元素,也就是所有不重複的數字 |
| lambda | 匿名函式,用來寫很短的比較規則或轉換規則 |
| key=lambda num: count[num] | 比較時看的是 num 的出現頻率,不是 num 的大小 |
Step 5 — 自己實作 heapq.nlargest
用 heappush / heappop 維護最頂端的 k 個值
要理解 nlargest,核心就是一句話:用一個大小最多為 k 的 min-heap,保存目前看過的前 k 大元素。因為 heap 頂端永遠是這 k 個元素裡最小的,所以一旦 heap 超過 k,就把最小的 pop 掉。
這裡 heap 裡放的是 (score, item)。 對 #347 來說,score 就是出現頻率,item 就是數字本身。 Python tuple 比較會先比第一個值,所以 heap 會自動用頻率維持大小順序。
nums=[1,1,1,2,2,3], k=2 的 heap 維護過程
[(3,1)]1 出現 3 次,heap 還沒滿,直接放入。
[(2,2), (3,1)]2 出現 2 次,heap 大小剛好到 k=2。
[(1,3), (3,1), (2,2)]3 出現 1 次,先 push 進來檢查。
[(2,2), (3,1)]heap 超過 k,pop 掉頻率最小的 (1,3)。
[(2,2), (3,1)]heap 裡剩下的就是頻率最高的 2 個元素:2 和 1。
Step 6 — Binary Heap 長什麼樣子?
heapq 表面上是 list,邏輯上是一棵完全二元樹
Python 的 heapq 不會真的建立 TreeNode。 它底層就是一個 list,但用 index 關係把 list 當成 binary tree 來看。
heap = [(2, 2), (3, 1), (4, 4)]parent
(i - 1) // 2left child
2 * i + 1right child
2 * i + 2Min-heap 的規則是:每個 parent 都要小於或等於自己的 children。 所以 list 的第 0 個位置永遠是整個 heap 裡最小的元素。 當我們維護「前 k 大」時,這個最小值就是目前 Top K 裡最弱的那個。
延伸:如果不是查最大,而是查最小呢?
Top K 類型可以分成 Top K Largest 和 Top K Smallest
#347 原題是「找出現頻率最高的前 k 個元素」。如果題目反過來問: 「找出現頻率最低的前 k 個元素」,思路其實一樣,只是把nlargest 換成nsmallest。
| 需求 | 直覺寫法 | 自己維護 heap 時 |
|---|---|---|
| 找前 k 大 | heapq.nlargest(k, nums) | 用大小 k 的 min-heap,超過 k 就 pop 最小 |
| 找前 k 小 | heapq.nsmallest(k, nums) | 可用 max-heap 概念,Python 常用負值模擬 |
heapq 常用函式速查
第一次學 Heap 時,先掌握這幾個函式就夠用
Python 的 heapq 預設是 min-heap, 也就是 heap[0] 永遠是目前最小的元素。 如果要模擬 max-heap,常見做法是把數字乘上 -1 存進去,取出時再乘回 -1。
| 函式 | 用途 | 複雜度 |
|---|---|---|
| heapq.heappush(heap, item) | 把 item 放進 heap,並自動維持 heap 規則。 | O(log n) |
| heapq.heappop(heap) | 取出並移除 heap 裡最小的元素,也就是 heap[0]。 | O(log n) |
| heapq.heapify(nums) | 把既有 list 原地轉成 heap,不會回傳新 list。 | O(n) |
| heapq.heappushpop(heap, item) | 先 push 再 pop,但比分兩行更有效率。 | O(log n) |
| heapq.heapreplace(heap, item) | 先 pop 再 push,適合 heap 已經不為空的情境。 | O(log n) |
| heapq.nlargest(k, iterable, key=...) | 取出依 key 比較後最大的 k 個元素。 | O(n log k) |
| heapq.nsmallest(k, iterable, key=...) | 取出依 key 比較後最小的 k 個元素。 | O(n log k) |
heappushpop 和 heapreplace 差在哪?
heappushpop 是「先放新值,再把最小的拿掉」, 所以如果新值本身很小,它可能立刻被拿掉。heapreplace 則是「一定先拿掉原本的 heap[0],再放新值」, 適合你確定一定要替換堆頂的情境。
這篇學到什麼
上一篇
EP.19 — Trie
Implement Trie、Add and Search、Replace Words
下一篇
EP.21 — Bit Manipulation
XOR、位移、Counting Bits