LeetCode 刷題日記
EP.20

EP.20 — Heap:
永遠能找到最大或最小

#215 Kth Largest Element · #347 Top K Frequent · #295 Find Median from Data Stream
— heapq 實戰,從 K 大值到雙 Heap 中位數

Joseph Chen

2026
13 min read
3 題精講

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 速查

heapq_cheatsheet.py
import heapq

# === Min-Heap(預設)===
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 4)

heap[0]               # 查看最小值,O(1)  → 1
heapq.heappop(heap)   # 彈出最小值,O(log n) → 1

# 直接從 list 建 heap,O(n)(比逐一 push 快)
nums = [3, 1, 4, 1, 5]
heapq.heapify(nums)   # in-place,O(n)

# heapreplace:pop + push,比分開做快(只做一次 sift)
heapq.heapreplace(heap, new_val)

# === Max-Heap(用負值模擬)===
max_heap = []
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -1)
heapq.heappush(max_heap, -4)

-max_heap[0]          # 最大值 → 4
-heapq.heappop(max_heap)  # 彈出最大值 → 4

# === 帶 key 的元素(存 tuple)===
# tuple 比較從第一個元素開始,第一個相同才比第二個
heapq.heappush(heap, (freq, word))  # 先按 freq 排序
操作函式複雜度
查看堆頂(最小值)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 的過程

push 3[3]

heap 小於 k,直接 push

push 2[2, 3]

heap 小於 k,直接 push

push 1[2, 3]

1 ≤ heap[0]=2,比堆頂小,不用換,丟掉

push 5[3, 5]

5 > heap[0]=2,heapreplace:pop 2,push 5

push 6[5, 6]

6 > heap[0]=3,heapreplace:pop 3,push 6

push 4[5, 6]

4 ≤ heap[0]=5,丟掉

結果[5, 6]

heap[0] = 5,即第 2 大 ✅

kth_largest.py
import heapq

def findKthLargest(nums: list[int], k: int) -> int:
    heap = []   # min-heap,大小維持在 k

    for num in nums:
        heapq.heappush(heap, num)
        if len(heap) > k:
            heapq.heappop(heap)   # 彈出最小的,保留前 k 大

    return heap[0]   # 堆頂就是第 k 大

# 更簡潔的寫法:heapq.nlargest 直接搞定
def findKthLargest_v2(nums: list[int], k: int) -> int:
    return heapq.nlargest(k, nums)[-1]   # 前 k 大中最小的
⏱ Time: O(n log k)💾 Space: O(k)

為什麼不直接排序?

排序是 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)開始,完美符合需求。

top_k_frequent_heap.py
from collections import Counter
import heapq

def topKFrequent(nums: list[int], k: int) -> list[int]:
    # Step 1:統計頻率
    count = Counter(nums)   # {num: freq}

    # Step 2:min-heap(按頻率排),維持大小 k
    heap = []
    for num, freq in count.items():
        heapq.heappush(heap, (freq, num))
        if len(heap) > k:
            heapq.heappop(heap)   # 踢掉頻率最低的

    return [num for freq, num in heap]

# nums=[1,1,1,2,2,3], k=2
# count = {1:3, 2:2, 3:1}
# 最終 heap = [(2,2), (3,1)](頻率 2 和 3)
# 回傳 [2, 1]
⏱ Time: O(n log k)💾 Space: O(n)

解法二:Bucket Sort(O(n),面試加分)

頻率最大只有 n(所有元素都一樣),可以用「頻率當 index」的桶排序, 把每個數字放進對應頻率的桶,再從高頻率桶往低頻率桶取 k 個元素。

top_k_frequent_bucket.py
def topKFrequent_bucket(nums: list[int], k: int) -> list[int]:
    count = Counter(nums)

    # bucket[i] = 出現頻率恰好為 i 的所有元素
    bucket = [[] for _ in range(len(nums) + 1)]
    for num, freq in count.items():
        bucket[freq].append(num)

    # 從高頻率往低頻率取,直到取夠 k 個
    result = []
    for freq in range(len(bucket) - 1, 0, -1):
        result.extend(bucket[freq])
        if len(result) >= k:
            return result[:k]

    return result

# nums=[1,1,1,2,2,3], k=2
# bucket[1]=[3], bucket[2]=[2], bucket[3]=[1]
# 從後往前:取 bucket[3]=[1] → result=[1]
#           取 bucket[2]=[2] → result=[1,2],夠了!
⏱ Time: O(n)💾 Space: O(n)

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)

1
|

right(min-heap)

中位數

1.0

left 先 push,left 有多 → 不平衡,left 頂移到 right?不,left 可以多一個

addNum(2)

left(max-heap)

1
|

right(min-heap)

2
中位數

1.5

2 > left 頂 1,push 到 right。左右等長 → 中位數 = (1+2)/2 = 1.5

addNum(3)

left(max-heap)

2
|

right(min-heap)

3
中位數

2.0

3 先 push 到 right,right 多一個 → 把 right 頂 3 移到 left?不對,3>2。left 頂到 right。最終 left=[2], right=[3],middle=2.0

find_median_data_stream.py
import heapq

class MedianFinder:
    def __init__(self):
        self.left  = []   # max-heap(存負值):較小的一半
        self.right = []   # min-heap:較大的一半

    def addNum(self, num: int) -> None:
        # Step 1:先 push 到 left(max-heap)
        heapq.heappush(self.left, -num)

        # Step 2:確保 left 的最大值 ≤ right 的最小值
        if self.right and (-self.left[0]) > self.right[0]:
            val = -heapq.heappop(self.left)
            heapq.heappush(self.right, val)

        # Step 3:平衡大小,讓 left 最多比 right 多 1 個
        if len(self.left) > len(self.right) + 1:
            val = -heapq.heappop(self.left)
            heapq.heappush(self.right, val)
        elif len(self.right) > len(self.left):
            val = heapq.heappop(self.right)
            heapq.heappush(self.left, -val)

    def findMedian(self) -> float:
        if len(self.left) > len(self.right):
            return -self.left[0]             # left 多一個,它就是中位數
        return (-self.left[0] + self.right[0]) / 2.0   # 兩側等長,取平均
⏱ Time: addNum O(log n),findMedian O(1)💾 Space: O(n)

為什麼不直接用排序陣列?

若用排序陣列,每次 addNum 插入需要 O(n)(保持排序),n 次操作就是 O(n²)。 雙 Heap 方案讓每次 addNum 只需 O(log n),findMedian O(1), n 次操作總計 O(n log n)——差距在百萬筆資料時極為明顯。

三題統一對比

題目Heap 類型Heap 大小答案在哪
#215 Kth LargestMin-Heapkheap[0]
#347 Top K FrequentMin-Heap(按頻率)kheap 裡的所有元素
#295 Find MedianMax-Heap + Min-Heapn/2 各兩堆堆頂的平均或左堆頂

識別 Heap 題目的訊號

  • 「第 K 大 / 小」、「前 K 個最...」——大小固定為 k 的 heap
  • 「動態資料流」、「持續插入,隨時查詢最值」——heap 是唯一 O(log n) 的動態結構
  • 「中位數」——雙 heap(左 max + 右 min),堆頂夾住中位數
  • 需要反覆取最大或最小,且資料還在變化——優先考慮 heap,不要排序

這篇學到什麼

🔧Python heapq 只有 min-heap;模擬 max-heap 只需把值取負後 push,pop 後再取負還原
🏆#215 Kth Largest:維護大小 k 的 min-heap,踢掉超出的最小值,堆頂就是第 k 大
📊#347 Top K Frequent:Counter 統計頻率,min-heap 按 (freq, num) 排序,bucket sort 可做到 O(n)
⚖️#295 Find Median:左 max-heap + 右 min-heap,維護「左 ≤ 右」且大小差 ≤ 1 的不變式,中位數在兩堆堆頂
💡Heap vs 排序:動態資料 / 只需前 k 個 → heap O(n log k);靜態一次性 → sort O(n log n) 程式碼更短
LeetCode
Heap
Priority Queue
Kth Largest
Top K
Median
Python
EP.20