堆積 (Heap)
My vault 資料結構筆記:堆積 (Heap)。
-
Heap 是一種 Complete Binary Tree
-
每個父節點一定大於或等於左右子節點(Max-Heap)
-
預設為 Max-Heap(你有註記)
-
Root 是整棵樹中最大值
-
適合用陣列儲存(Array 表示)
Bottom Up (heapify down)
Heap Adjust (heapify) 和 build
-
Heapify(上浮):向上與 parent 比較
-
若比 parent 大 → 交換 → 繼續上浮
-
一路「挑戰」到 root 為止,或失敗為止
-
x 往下 parent 挑戰直到 root 或失敗
-
// Max-Heap 的調整與建堆函式
// 調整以 i 為根的子堆,讓它符合 Max-Heap 規則(Heapify Down)
void adjust(int tree[], int i, int n) {
int j = 2 * i; // j 是左子節點
int x = tree[i]; // 暫存當前節點的值
while (j <= n) {
// 若右子節點存在且比左子大,選右子
if (j < n && tree[j] < tree[j + 1])
j = j + 1;
// 若已滿足 heap 性質(父比子大),就停止
if (x >= tree[j])
break;
// 較大的子節點上移
tree[j / 2] = tree[j];
j = 2 * j; // 繼續往下比較
}
// 將原值放到正確位置(j 已經超過邊界或比子大)
tree[j / 2] = x;
}
// 建立 Max-Heap:從 n/2 就是父節點開始往上對每個節點做 adjust
void buildheap(int tree[], int n) {
for (int i = n / 2; i >= 1; i--) {
adjust(tree, i, n);
}
}
建立 Heap 的時間複雜度推導
-
前提假設:
-
假設我們有一棵 完全二元樹(Complete Binary Tree),有 $n$ 個節點, 樹的高度為 $k = \lceil \log(n+1) \rceil$。
-
每個節點最多需要從目前位置下移(sift-down)到最底層,最多 $k - i$ 次比較(其中 $i$ 是該節點的層級,最底層為 $k$)。
-
-
層級資訊與每層節點數:
-
第 $i$ 層(由上至下編號)最多有 $2^{i-1}$ 個節點。
-
該層每個節點最多花費 $k - i$ 的時間進行 sift-down。
-
-
時間總和推導:
-
令總時間為 $S$,我們將每層的花費總和加總: $$ S = \sum_{i=1}{k} 2{i-1} \cdot (k - i) $$
-
我們令: $$ S = 20 \cdot (k - 1) + 21 \cdot (k - 2) + 22 \cdot (k - 3) + \cdots + 2{k-2} \cdot 1 + 2^{k-1} \cdot 0 $$
- 令此為式 $(1)$
-
再令: $$ 2S = 21 \cdot (k - 1) + 22 \cdot (k - 2) + \cdots + 2^{k-1} \cdot 1 + 2^{k} \cdot 0 $$
- 令此為式 $(2)$
-
$(2)$ 減 $(1)$ 得: $$ S = -20 \cdot (k - 1) + 21 + 22 + \cdots + 2{k-1} $$
-
使用等比數列求和公式: $$ \sum_{i=0}{k-1} 2i = 2^k - 1 $$
-
所以: $$ S = - (k - 1) + (2k - 1) = 2k - k $$
-
因為完全二元樹中最多有 $n = 2^k - 1$ 個節點,推出: $$ S \leq n - \log_2(n+1) + 1 $$
-
所以時間複雜度為: $$ \boxed{O(n)} $$
-
插入操作:Insert X
-
插入位置:將新元素
x插入至陣列最後一個位置(即最後一個節點)-
Insert: x 插到最後一個 the last node -
例:目前 n = 10,則插入
tree[11]
-
-
執行 Adjust (Heapify Up):
-
插入後從該節點往上比較其父節點
-
若
x > parent(Max-Heap),則交換並繼續往上 -
一直進行到 root 或符合 heap 規則為止
-
-
時間複雜度說明:
-
Heap 是 完全二元樹(Complete Binary Tree),節點數為 $n$ 時,高度為 $\lceil log₂(n + 1) \rceil$
-
最差情況下,插入的元素會從最底層一路上浮到 root(即最大移動距離)
-
⏱ 因此插入操作的時間複雜度為:$O(\log n)$
-
// 插入新元素 x 到 Max-Heap
void insert(int tree[], int x, int *n) {
int size = ++(*n); // 增加 heap 大小
tree[size] = x; // 先插入最末尾
int child = size;
int parent = child / 2;
// 若 x 大於 parent,則一路往上浮(heapify up)
while (parent >= 1) {
if (x > tree[parent]) {
tree[child] = tree[parent]; // 父節點下移
child = parent;
parent = parent / 2;
} else {
break; // 若已滿足 heap 規則則停止
}
}
tree[child] = x; // 將 x 放入正確位置
}
🔴 刪除最大值操作:Delete-Max(Max-Heap)
-
刪除 Root:
- 將根節點(最大值)移除,暫存回傳
-
替換 Root:
-
將最右下角節點(即最後一個元素)補上 root 位置
-
原本位置刪除(heap size - 1)
-
-
向下調整 Heap(Heapify Down):
-
從 root 開始與左右子節點比較
-
若不符合 Max-Heap 規則,則與較大者交換
-
持續向下直到符合規則或成為葉節點為止
-
-
⏱ 時間複雜度分析:
-
最差情況下,補上來的值會一路從 root 下沉到 leaf
-
Heap 是 完全二元樹(Complete Binary Tree),所以高度為 $\lceil log₂(n + 1) \rceil$
-
因此刪除操作的時間複雜度為:$O(\log n)$
-
// 刪除最大值(Root),並進行調整
int delmax(int tree[], int *n) {
int max = tree[1]; // 取出 root 作為最大值
int size = *n;
tree[1] = tree[size--]; // 用最後一個節點補上 root
*n = size; // 更新 heap 大小
adjust(tree, 1, size); // 從 root 開始往下調整(heapify down)
return max;
}
Top Down (flowing up)
-
前提假設:
-
Top-Down 是指從空的 heap 開始,一筆一筆將資料插入,每次插入都進行上浮調整(heapify-up)。
-
每筆資料插入時,會從陣列尾端插入,然後與父節點比較並進行交換,直到符合 heap 性質。
-
若有 $n$ 筆資料,則會執行 $n$ 次插入與上浮操作。
-
-
單次插入的時間分析:
-
Heap 是完全二元樹,最壞情況下一個元素從最底層往上走到 root。
-
高度為 $\lceil \log(n+1) \rceil$,因此單次插入的時間複雜度為:$O(\log n)$
-
-
總插入時間推導:
-
第 $i$ 筆資料插入時,最壞情況需要 $O(\log i)$ 時間。
-
所以總時間為: $$ T(n) = \sum_{i=1}^{n} \log i = \log(1) + \log(2) + \cdots + \log(n) = \log(n!) $$
-
使用斯特林公式近似估計: $$ \log(n!) = \Theta(n \log n) $$
-
結論:Top-Down 建堆的總時間複雜度為: $$ \boxed{O(n \log n)} $$
-
Top-Down vs Bottom-Up 建堆方法比較
| 項目 | Bottom-Up(heapify down) | Top-Down(flowing up) |
|---|---|---|
| 建堆方式 | 所有資料先放入陣列,從 $n/2$ 開始往上調整,簡單來說就是把二元樹建立好然後再調整 | 每次插入一筆資料,從尾端上浮,間單來說就是每次都調整 |
| 調整方向 | 向下調整(sift-down) | 向上調整(sift-up) |
| 每筆處理時間 | 根據層數而定,越底層越少 | 最壞 $O(\log n)$ |
| 總建堆時間複雜度 | $O(n)$ | $O(n \log n)$ |
| 優點 | 建堆效率極高,適合一次性整理大量資料 | 程式簡單,適合動態逐筆處理 |
| 缺點 | 難以用於線上插入場景 | 效率較差,重複調整次數多 |
| 實作對應操作 | buildheap() + adjust() |
insert() + heapify up |
| 適用情境 | 大量資料批次建堆時,如:Heap Sort 初始堆建立 | 資料逐筆產生時,如:Priority Queue 動態插入 |
常見操作與時間複雜度
| 操作說明 | 時間複雜度 |
|---|---|
| Insert X | $O(log n)$ |
| Delete-Max | $O(log n)$ |
| Heapify (or Adjust) | $O(log n)$ |
| Find-Max | $O(1$) |
| Build a Heap | $O(n)$ |
| Decrease key (Min-Heap) | $O(log n)$ |
| Merge two heaps into one heap | $O(n)$ |
| Search X | $O(n)$ |
合併兩個 Heap 的方法:
假設我們有兩個 Heap:
-
H₁ 有 $n$ 個元素
-
H₂ 有 $m$ 個元素
-
步驟如下:
-
將兩個 Heap 的資料依序放入新的陣列(時間 $O(n + m)$)
-
對新的陣列使用 Bottom-Up 的
buildheap()演算法來調整成合法 Heap
-
-
時間複雜度分析:
-
將兩堆資料合併為一個陣列:$O(n + m)$
-
使用 Bottom-Up 建堆:$O(n + m)$
-
所以整體時間為:$O(n + m)$
-
-
結論:
合併兩個 Heap 的方式是「直接合併成一個陣列後再建堆」,不需要逐一插入(否則會是 $O((n + m) \log(n + m))$)。
✅ 這種方法能保持時間複雜度為:$\boxed{O(n + m)}$