堆與堆排序#
堆的定義#
堆是一種特殊的完全二元樹,滿足:
- 結構性質:完全二元樹(除最後一層外都填滿,最後一層靠左)
- 堆性質:每個節點 >= 或 <= 其子節點
| 類型 | 性質 | 堆頂 |
|---|---|---|
| 大頂堆 (Max Heap) | 父 >= 子 | 最大值 |
| 小頂堆 (Min Heap) | 父 <= 子 | 最小值 |
陣列存儲#
完全二元樹特別適合用陣列存儲(從索引 1 開始):
| 關係 | 公式 |
|---|---|
| 左子節點 | 2 * i |
| 右子節點 | 2 * i + 1 |
| 父節點 | i / 2 |
若從索引 0 開始:左子 =
2*i+1,右子 =2*i+2,父 =(i-1)/2
核心操作:堆化 (Heapify)#
上浮 (Sift Up) - 插入時使用#
def sift_up(heap, i):
while i > 1 and heap[i] > heap[i // 2]:
heap[i], heap[i // 2] = heap[i // 2], heap[i]
i = i // 2下沉 (Sift Down) - 刪除堆頂時使用#
def sift_down(heap, n, i):
while True:
max_pos = i
if 2*i <= n and heap[2*i] > heap[max_pos]:
max_pos = 2*i
if 2*i+1 <= n and heap[2*i+1] > heap[max_pos]:
max_pos = 2*i+1
if max_pos == i:
break
heap[i], heap[max_pos] = heap[max_pos], heap[i]
i = max_pos時間複雜度:O(log n)
堆排序#
步驟#
flowchart LR
subgraph 階段一["階段 1:建堆"]
A1[無序陣列] --> A2[從最後一個<br/>非葉節點開始]
A2 --> A3[逐一下沉]
A3 --> A4[大頂堆完成]
end
subgraph 階段二["階段 2:排序"]
B1[取堆頂] --> B2[與末尾交換]
B2 --> B3[縮小堆範圍]
B3 --> B4[堆頂下沉]
B4 --> B5{堆為空?}
B5 -->|否| B1
B5 -->|是| B6[排序完成]
end
A4 --> B1
style A4 fill:#e8f5e9
style B6 fill:#e8f5e9- 建堆:將無序陣列建成大頂堆
- 排序:反覆取堆頂放到末尾,縮小堆範圍
堆排序程式碼
def heap_sort(arr):
n = len(arr)
# 建堆 - 從最後一個非葉節點開始下沉
for i in range(n // 2 - 1, -1, -1):
sift_down(arr, n - 1, i)
# 排序 - 堆頂與末尾交換,縮小堆
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
sift_down(arr, i - 1, 0)複雜度#
- 建堆:O(n)(不是 O(n log n)!)
- 排序:O(n log n)
- 空間:O(1)(原地排序)
- 穩定性:不穩定
堆排序 vs 快速排序:雖然都是 O(n log n),但快排通常更快,因為:
- 堆排序資料訪問是跳躍的,對 CPU 快取不友好
- 建堆會打亂原有順序,增加交換次數
堆的應用#
1. 優先佇列 (Priority Queue)#
import heapq
pq = []
heapq.heappush(pq, 3) # 插入
heapq.heappop(pq) # 取出最小值2. Top K 問題#
維護大小為 K 的小頂堆:
def topK(nums, k):
heap = []
for num in nums:
if len(heap) < k:
heapq.heappush(heap, num)
elif num > heap[0]:
heapq.heapreplace(heap, num)
return heap時間複雜度:O(n log k)
3. 求中位數(動態資料)#
使用兩個堆:
- 大頂堆:存較小的一半
- 小頂堆:存較大的一半
- 中位數 = 大頂堆的堆頂
延伸應用:同樣方法可求任意百分位數,如 P99 響應時間。