Heap

Heap(堆積) 是一種非常特殊的 Binary Tree 資料結構,它必須滿足以下兩個嚴格的條件:
- 結構特性(完全二元樹 Complete Binary Tree):
- 除了最後一層外,其餘各層的節點必須全部填滿
- 而且最後一層的節點必須由左至右依序填入,不能留空
- 堆積特性(Heap Property):父節點與子節點之間存在固定的順序關係。根據這個關係,Heap 分為兩大類:
- Max Heap
- 任何一個父節點的值,都大於或等於它的子節點。因此,Root 一定是整棵樹的最大值。
- Min Heap
- 任何一個父節點的值,都小於或等於它的子節點。因此,Root 一定是整棵樹的最小值。
- Max Heap
想像軍中的排隊場合,長官每次都挑最高的人走,不在乎隊伍長怎樣只在乎「目前這群人裡面誰最高」,並不要求所有人乖乖排成一直線。只要長官隨時能一眼確認最高的人在哪,一旦這個人被請走,剩下的人重新比一下誰最高就好,不需要花時間把所有人重新排序。
Heap 就是這種「只保證極值隨時找得到,其他順序不管」的結構。
Heap 不保證左子節點和右子節點誰大誰小,它只管 「父節點與子節點」 的上下關係。
實作
由於 JavaScript 內建沒有提供 Heap 這個資料結構(不像 Python 有 heapq,C++ 有 priority_queue),因此 JavaScript 通常需要自己動手寫一個 Heap。
因為 Heap 是一棵完全二元樹,節點由上到下、由左到右非常緊密,所以不需要像一般的樹結構一樣用複雜的 pointer 連來連去。我們可以直接用一個 Array 來實作:

以 Max Heap 為例,任意 Node 的 index 是 i,則
- Left child 的 index 是
2*i + 1 - Right child 的 index 是
2*i + 2 - Parent 的 index 是
(i-1)/2(小數無條件捨去)
10 (i=0)
/ \
8 7
(i=1) (i=2)
/ \
4 2
(i=3)(i=4)
- Python
- JavaScript
- Java
class MaxBinaryHeap:
def __init__(self):
self.values = []
def insert(self, val):
"""
新增元素:O(log N)
在尾端加入 Node,新加入的值會和 parent 的值比較
如果大於 parent 則交換位置(稱為 Bubble Up)
直到整個 Heap 的狀態符合 parent 大於 children 的情況。
"""
if val is None:
return self.values
self.values.append(val)
if len(self.values) > 1:
self._bubble_up()
return self.values
def _bubble_up(self):
idx = len(self.values) - 1
# 當前 Node 還不是 Root 時,持續檢查
while idx > 0:
parent_idx = (idx - 1) // 2
# 如果當前 Node value 大於 parent,則進行交換
if self.values[idx] > self.values[parent_idx]:
self.values[idx], self.values[parent_idx] = self.values[parent_idx], self.values[idx]
idx = parent_idx # 繼續往上追蹤
else:
break # 符合最大堆積特性,提早結束
def extract_max(self):
"""
取出最大值:O(log N)
取出 Root 的值並將最後的 Node 移到 Root 的位置
接著讓這個值和 children 比較大小
如果小於 children 則交換位置(這個動作稱為 Sink Down)
直到整個 Heap 的狀態符合 parent 大於 children 的情況
"""
if len(self.values) == 0:
return None
if len(self.values) == 1:
return self.values.pop()
max_val = self.values[0]
# 用最後一個元素覆蓋根節點,並移出末端元素
self.values[0] = self.values.pop()
# 如果移除後還有剩餘元素,執行向下調整
if len(self.values) > 0:
self._sink_down()
return max_val
def _sink_down(self):
idx = 0
length = len(self.values)
element = self.values[0]
while True:
left_idx = 2 * idx + 1
right_idx = 2 * idx + 2
left_child = right_child = None
swap_idx = None # 用來記錄這輪應該跟誰交換(左、右或都不換)
# 1. 安全檢查:確認左子節點是否存在
if left_idx < length:
left_child = self.values[left_idx]
# 如果左子節點比目前節點大,暫定與左子節點交換
if left_child > element:
swap_idx = left_idx
# 2. 安全檢查:確認右子節點是否存在
if right_idx < length:
right_child = self.values[right_idx]
# 右子節點要勝出的條件:
# 情況 A:目前節點比右子節點小(swap_idx 仍是 None),且右子節點存在。
# 情況 B:左右子節點都比目前節點大(swap_idx 已是 left_idx),但右子節點比左子節點更大。
if (swap_idx is None and right_child > element) or (
swap_idx is not None and right_child > left_child
):
swap_idx = right_idx
# 3. 檢查終止條件:如果 swap_idx 依然是 None,代表目前節點已經比左右子節點都大,不需要再換了
if swap_idx is None:
break
# 4. 執行交換,並更新索引繼續下一輪
self.values[idx], self.values[swap_idx] = self.values[swap_idx], self.values[idx]
idx = swap_idx
class MaxBinaryHeap {
constructor() {
this.values = [];
}
/*
新增元素:O(log N)
在尾端加入 Node,新加入的值會和 parent 的值比較
如果大於 parent 則交換位置(稱為 Bubble Up)
直到整個 Heap 的狀態符合 parent 大於 children 的情況。
*/
insert(val) {
if (val === undefined) return this.values;
this.values.push(val);
if (this.values.length > 1) {
this.bubbleUp();
}
return this.values;
}
bubbleUp() {
let idx = this.values.length - 1;
// 當前 Node 還不是 Root 時,持續檢查
while (idx > 0) {
let parentIdx = Math.floor((idx - 1) / 2);
// 如果當前 Node value 大於 parent,則進行交換
if (this.values[idx] > this.values[parentIdx]) {
[this.values[idx], this.values[parentIdx]] = [this.values[parentIdx], this.values[idx]];
idx = parentIdx; // 繼續往上追蹤
} else {
break; // 符合最大堆積特性,提早結束
}
}
}
/**
取出最大值:O(log N)
取出 Root 的值並將最後的 Node 移到 Root 的位置
接著讓這個值和 children 比較大小
如果小於 children 則交換位置(這個動作稱為 Sink Down)
直到整個 Heap 的狀態符合 parent 大於 children 的情況
*/
extractMax() {
if (this.values.length === 0) return undefined;
if (this.values.length === 1) return this.values.pop();
const max = this.values[0];
// 用最後一個元素覆蓋根節點,並移出末端元素
this.values[0] = this.values.pop();
// 如果移除後還有剩餘元素,執行向下調整
if (this.values.length > 0) {
this.sinkDown();
}
return max;
}
sinkDown() {
let idx = 0;
const length = this.values.length;
const element = this.values[0];
while (true) {
let leftIdx = 2 * idx + 1;
let rightIdx = 2 * idx + 2;
let leftChild, rightChild;
let swapIdx = null; // 用來記錄這輪應該跟誰交換(左、右或都不換)
// 1. 安全檢查:確認左子節點是否存在
if (leftIdx < length) {
leftChild = this.values[leftIdx];
// 如果左子節點比目前節點大,暫定與左子節點交換
if (leftChild > element) {
swapIdx = leftIdx;
}
}
// 2. 安全檢查:確認右子節點是否存在
if (rightIdx < length) {
rightChild = this.values[rightIdx];
// 右子節點要勝出的條件:
// 情況 A:目前節點比右子節點小(swapIdx 仍是 null),且右子節點存在。
// 情況 B:左右子節點都比目前節點大(swapIdx 已是 leftIdx),但右子節點比左子節點更大。
if (
(swapIdx === null && rightChild > element) ||
(swapIdx !== null && rightChild > leftChild)
) {
swapIdx = rightIdx;
}
}
// 3. 檢查終止條件:如果 swapIdx 依然是 null,代表目前節點已經比左右子節點都大,不需要再換了
if (swapIdx === null) break;
// 4. 執行交換,並更新索引繼續下一輪
[this.values[idx], this.values[swapIdx]] = [this.values[swapIdx], this.values[idx]];
idx = swapIdx;
}
}
}
import java.util.ArrayList;
import java.util.List;
class MaxBinaryHeap {
private List<Integer> values;
public MaxBinaryHeap() {
this.values = new ArrayList<>();
}
/**
* 新增元素:O(log N)
*
* 在尾端加入 Node,新加入的值會和 parent 的值比較
* 如果大於 parent 則交換位置(稱為 Bubble Up)
* 直到整個 Heap 的狀態符合 parent 大於 children 的情況。
*/
public List<Integer> insert(Integer val) {
if (val == null) return this.values;
this.values.add(val);
if (this.values.size() > 1) {
bubbleUp();
}
return this.values;
}
private void bubbleUp() {
int idx = this.values.size() - 1;
// 當前 Node 還不是 Root 時,持續檢查
while (idx > 0) {
int parentIdx = (idx - 1) / 2;
// 如果當前 Node value 大於 parent,則進行交換
if (this.values.get(idx) > this.values.get(parentIdx)) {
int temp = this.values.get(idx);
this.values.set(idx, this.values.get(parentIdx));
this.values.set(parentIdx, temp);
idx = parentIdx; // 繼續往上追蹤
} else {
break; // 符合最大堆積特性,提早結束
}
}
}
/**
* 取出最大值:O(log N)
*
* 取出 Root 的值並將最後的 Node 移到 Root 的位置
* 接著讓這個值和 children 比較大小
* 如果小於 children 則交換位置(這個動作稱為 Sink Down)
* 直到整個 Heap 的狀態符合 parent 大於 children 的情況
*/
public Integer extractMax() {
if (this.values.isEmpty()) return null;
if (this.values.size() == 1) return this.values.remove(0);
int max = this.values.get(0);
// 用最後一個元素覆蓋根節點,並移出末端元素
this.values.set(0, this.values.remove(this.values.size() - 1));
// 如果移除後還有剩餘元素,執行向下調整
if (!this.values.isEmpty()) {
sinkDown();
}
return max;
}
private void sinkDown() {
int idx = 0;
int length = this.values.size();
int element = this.values.get(0);
while (true) {
int leftIdx = 2 * idx + 1;
int rightIdx = 2 * idx + 2;
Integer leftChild = null, rightChild = null;
Integer swapIdx = null; // 用來記錄這輪應該跟誰交換(左、右或都不換)
// 1. 安全檢查:確認左子節點是否存在
if (leftIdx < length) {
leftChild = this.values.get(leftIdx);
// 如果左子節點比目前節點大,暫定與左子節點交換
if (leftChild > element) {
swapIdx = leftIdx;
}
}
// 2. 安全檢查:確認右子節點是否存在
if (rightIdx < length) {
rightChild = this.values.get(rightIdx);
// 右子節點要勝出的條件:
// 情況 A:目前節點比右子節點小(swapIdx 仍是 null),且右子節點存在。
// 情況 B:左右子節點都比目前節點大(swapIdx 已是 leftIdx),但右子節點比左子節點更大。
if ((swapIdx == null && rightChild > element) ||
(swapIdx != null && rightChild > leftChild)) {
swapIdx = rightIdx;
}
}
// 3. 檢查終止條件:如果 swapIdx 依然是 null,代表目前節點已經比左右子節點都大,不需要再換了
if (swapIdx == null) break;
// 4. 執行交換,並更新索引繼續下一輪
int temp = this.values.get(idx);
this.values.set(idx, this.values.get(swapIdx));
this.values.set(swapIdx, temp);
idx = swapIdx;
}
}
}
上面這份手刻的 MaxBinaryHeap 只處理了 Max Heap。如果想要 Min Heap,最直覺的做法是複製一份幾乎一模一樣的程式碼,把 bubbleUp、sinkDown 裡所有的比較方向反過來(> 全部換成 <),但這樣一來,兩份程式碼幾乎相同,只差在比較方向,維護起來很容易顧此失彼。下面分別介紹 Python 內建的解法,以及一份程式碼就能同時支援兩種 Heap 的寫法。
Python 內建的 heapq:預設就是 Min Heap
Python 標準函式庫的 heapq 模組,直接提供現成的 Heap 操作,不需要自己刻 bubbleUp / sinkDown。要注意的是,heapq 預設實作的是 Min Heap,也就是 heap[0] 永遠是最小值:
import heapq
nums = [5, 3, 8, 1, 9, 2]
heapq.heapify(nums) # 原地把 list 轉成合法的 min heap 結構:O(n)
heapq.heappush(nums, 4) # 新增元素:O(log n)
print(nums[0]) # 直接看 index 0,就是目前最小值:O(1) → 1
smallest = heapq.heappop(nums) # 取出並移除最小值:O(log n)
print(smallest) # 1
如果需要 Max Heap,heapq 並沒有直接提供,最常見的技巧是把存進去的數字都先取負號,取出時再取一次負號還原:
import heapq
nums = [5, 3, 8, 1, 9, 2]
max_heap = [-n for n in nums]
heapq.heapify(max_heap) # 對「取負號後的數字」做 heapify,最小的負數 = 原本最大的正數
heapq.heappush(max_heap, -4) # 新增元素時記得先取負號
print(-max_heap[0]) # 取出時再取一次負號,還原成正確的最大值 → 9
largest = -heapq.heappop(max_heap) # 取出並移除最大值
print(largest) # 9
取負號這招只適用於「純數字」的情境。如果 Heap 裡放的是字串、物件,或需要依照多個欄位排序的 tuple,直接取負號就不管用了,這時候通常需要自訂排序邏輯(例如放進 tuple 讓 Python 依序比較),或是改用下面介紹的 Bridge Pattern 寫法。
用 Bridge Pattern 讓一份程式碼同時支援 Min Heap 與 Max Heap
不管是 Max Heap 還是 Min Heap,bubbleUp 和 sinkDown 的陣列索引運算完全一樣(一樣是 2*i+1、2*i+2、(i-1)/2),唯一的差別只在於「兩個值比較時,誰應該被換到上面」這個判斷方向。
Bridge Pattern 的做法,就是把「Heap 結構本身怎麼運作」和「比較大小的規則」拆成兩塊:Heap 類別只負責處理陣列索引的搬移邏輯,實際的比較規則則透過建構子傳入一個 cmp 函式決定。這樣一來,Min Heap 和 Max Heap 就能共用同一份程式碼,不需要複製貼上再手動反轉每一個比較符號:
- Python
- JavaScript
- Java
def default_cmp(x, y):
return x > y # 預設是 maxHeap
class Heap:
def __init__(self, compare_func=default_cmp):
self.values = []
self.cmp = compare_func
def insert(self, val):
values, cmp = self.values, self.cmp
values.append(val)
index = len(values) - 1
while index > 0:
parent_idx = (index - 1) // 2
if not cmp(values[index], values[parent_idx]):
return
values[index], values[parent_idx] = values[parent_idx], values[index]
index = parent_idx
def extract(self):
values, cmp = self.values, self.cmp
if not values:
return None
values[0], values[-1] = values[-1], values[0]
res = values.pop()
length = len(values)
index = 0
exchange = 2 * index + 1
while exchange < length:
right = 2 * index + 2
if right < length and cmp(values[right], values[exchange]):
# 如果右子節點存在,且
# 在 maxHeap 中:右子節點的值 > 左子節點的值
# 在 minHeap 中:右子節點的值 < 左子節點的值
exchange = right
if not cmp(values[exchange], values[index]):
break
values[index], values[exchange] = values[exchange], values[index]
index = exchange
exchange = 2 * index + 1
return res
def peek(self):
if self.values:
return self.values[0]
return None
const defaultCmp = (x, y) => x > y; // 預設是 maxHeap
const swap = (arr, i, j) => ([arr[i], arr[j]] = [arr[j], arr[i]]);
class Heap {
constructor(compareFunc = defaultCmp) {
this.values = [];
this.cmp = compareFunc;
}
insert(val) {
const { values, cmp } = this;
values.push(val);
let index = values.length - 1;
while (index > 0) {
const parentIdx = Math.floor((index - 1) / 2);
if (!cmp(values[index], values[parentIdx])) return;
swap(values, index, parentIdx);
index = parentIdx;
}
}
extract() {
const { values, cmp } = this;
if (!values.length) return null;
swap(values, 0, values.length - 1);
const res = values.pop();
const { length } = values;
let index = 0,
exchange = 2 * index + 1;
while (exchange < length) {
const right = 2 * index + 2;
if (right < length && cmp(values[right], values[exchange])) {
// 如果右子節點存在,且
// 在 maxHeap 中:右子節點的值 > 左子節點的值
// 在 minHeap 中:右子節點的值 < 左子節點的值
exchange = right;
}
if (!cmp(values[exchange], values[index])) break;
swap(values, index, exchange);
index = exchange;
exchange = 2 * index + 1;
}
return res;
}
peek() {
if (this.values.length) return this.values[0];
return null;
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiPredicate;
class Heap<T extends Comparable<T>> {
private List<T> values;
private BiPredicate<T, T> cmp;
public Heap() {
this((x, y) -> x.compareTo(y) > 0); // 預設是 maxHeap
}
public Heap(BiPredicate<T, T> compareFunc) {
this.values = new ArrayList<>();
this.cmp = compareFunc;
}
private void swap(int i, int j) {
T temp = this.values.get(i);
this.values.set(i, this.values.get(j));
this.values.set(j, temp);
}
public void insert(T val) {
this.values.add(val);
int index = this.values.size() - 1;
while (index > 0) {
int parentIdx = (index - 1) / 2;
if (!this.cmp.test(this.values.get(index), this.values.get(parentIdx))) return;
swap(index, parentIdx);
index = parentIdx;
}
}
public T extract() {
if (this.values.isEmpty()) return null;
swap(0, this.values.size() - 1);
T res = this.values.remove(this.values.size() - 1);
int length = this.values.size();
int index = 0;
int exchange = 2 * index + 1;
while (exchange < length) {
int right = 2 * index + 2;
if (right < length && this.cmp.test(this.values.get(right), this.values.get(exchange))) {
// 如果右子節點存在,且
// 在 maxHeap 中:右子節點的值 > 左子節點的值
// 在 minHeap 中:右子節點的值 < 左子節點的值
exchange = right;
}
if (!this.cmp.test(this.values.get(exchange), this.values.get(index))) break;
swap(index, exchange);
index = exchange;
exchange = 2 * index + 1;
}
return res;
}
public T peek() {
if (!this.values.isEmpty()) return this.values.get(0);
return null;
}
}
要切換 Min Heap 或 Max Heap,只需要在建立實例時傳入不同的比較函式:
- Python
- JavaScript
- Java
max_heap = Heap() # 不傳 compare_func,使用預設的 default_cmp -> Max Heap
for n in [5, 3, 8, 1]:
max_heap.insert(n)
print(max_heap.peek()) # 8(目前最大值)
print(max_heap.extract()) # 8
print(max_heap.extract()) # 5
min_heap = Heap(lambda x, y: x < y) # 把比較方向反過來 -> Min Heap
for n in [5, 3, 8, 1]:
min_heap.insert(n)
print(min_heap.peek()) # 1(目前最小值)
print(min_heap.extract()) # 1
print(min_heap.extract()) # 3
const maxHeap = new Heap(); // 不傳 cmp,使用預設的 defaultCmp -> Max Heap
[5, 3, 8, 1].forEach((n) => maxHeap.insert(n));
console.log(maxHeap.peek()); // 8(目前最大值)
console.log(maxHeap.extract()); // 8
console.log(maxHeap.extract()); // 5
const minHeap = new Heap((x, y) => x < y); // 把比較方向反過來 -> Min Heap
[5, 3, 8, 1].forEach((n) => minHeap.insert(n));
console.log(minHeap.peek()); // 1(目前最小值)
console.log(minHeap.extract()); // 1
console.log(minHeap.extract()); // 3
Heap<Integer> maxHeap = new Heap<>(); // 不傳 cmp,使用預設的 defaultCmp -> Max Heap
for (int n : new int[]{5, 3, 8, 1}) {
maxHeap.insert(n);
}
System.out.println(maxHeap.peek()); // 8(目前最大值)
System.out.println(maxHeap.extract()); // 8
System.out.println(maxHeap.extract()); // 5
Heap<Integer> minHeap = new Heap<>((x, y) -> x < y); // 把比較方向反過來 -> Min Heap
for (int n : new int[]{5, 3, 8, 1}) {
minHeap.insert(n);
}
System.out.println(minHeap.peek()); // 1(目前最小值)
System.out.println(minHeap.extract()); // 1
System.out.println(minHeap.extract()); // 3
defaultCmp 的意思是「x 應該排在 y 上面嗎?」。
insert 在往上比對時,只要 cmp(child, parent) 成立就往上交換;extract 在往下比對時,只要 cmp(child, current) 成立就往下交換。傳入 (x, y) => x > y,交換條件就變成「子節點比較大就往上換」,結果自然是 Max Heap;傳入 (x, y) => x < y,交換條件反過來,結果就是 Min Heap。
Heap 結構本身的陣列索引運算完全沒有改變,改變的只有「決定誰該在上面」的規則,這正是 Bridge Pattern「把不變的結構,和可以自由抽換的規則拆開」的精神。
複雜度
Heap 最大的優勢在於動態維護極值。
當我們新增或刪除資料時,它會透過「Heapify」的調整過程,在極短的時間內恢復 Heap 的特性。
| 操作 | 說明 | 時間複雜度 |
|---|---|---|
| Get Min/Max | 直接看陣列第一個元素(Root),取得極值。 | O(1) |
| Insert | 把新元素加到 array 最後面,然後「由下往上」與 parent 比較並交換,直到符合規則。 | O(log N) |
| Delete Min/Max | 移走 Root。將 array 最後一個元素放到 Root,然後「由上往下」與較大/較小的子節點比較並交換。 | O(log N) |
適用情況
只要遇到 「要動態、即時取得極值」 的情境,Heap 絕對是首選!
- 優先佇列(Priority Queue)
- 普通的 Queue 是先進先出,但 Priority Queue 會讓「優先權最高(最大或最小)」的元素先出列。
- Heap 就是實作 Priority Queue 最完美的底層結構。
- 尋找第 K 個極值:例如「在 100 萬筆資料中,動態找出前 10 大的數字」(Top K Elements),用 Max Heap 處理效能極高。
- Heap Sort:利用 Heap 每次拔出 Root(最大或最小值)的特性來排序,時間複雜度是穩定的 O(N logN),而且不需要額外的記憶體空間。
- 圖形演算法的優化
- 例如 Dijkstra's Algorithm(找最短路徑)和 Prim's Algorithm(找最小生成樹),都會用 Min-Heap 來動態挑選下一個距離最近的節點。
常見誤區
- 誤以為 Heap 內部是「完全排序好」的:Heap 只保證 Root 一定是最大(或最小)值,除了這個規則之外,其他節點之間並沒有嚴格的大小順序,千萬不要以為把 Heap 的 array 直接印出來就是排序好的結果。
- 和 Binary Search Tree 搞混:如同文中提醒的,Heap 不保證左右子節點誰大誰小,也不支援像 BST 那樣快速搜尋「任意值」,Heap 只擅長快速取得極值。
- 忘記處理陣列邊界:實作
bubbleUp或sinkDown時,如果沒有先確認 left/right child 的 index 是否超出陣列長度,很容易讀到undefined,導致比較結果出錯。 - 誤以為 Python 的
heapq預設是 Max Heap:heapq只提供 Min Heap,heap[0]永遠是最小值,需要 Max Heap 時要自己用負號技巧,或改用 Bridge Pattern 的寫法。 - 自訂
cmp函式時,把交換方向想反:cmp(a, b)回傳true代表「a應該被換到b上面」,如果把回傳值的意義搞反,寫出來的 Heap 順序會整個顛倒(Max Heap 變成 Min Heap,反之亦然),建議寫完後用幾筆資料實際跑一次驗證方向是否正確。