「堆 heap」是一種滿足特定條件的完全二叉樹,主要可分為圖 8-1 所示的兩種類型。
圖 8-1 小頂堆與大頂堆
堆作為完全二叉樹的一個特例,具有以下特性。
需要指出的是,許多編程語言提供的是「優(yōu)先隊列 priority queue」,這是一種抽象數(shù)據(jù)結(jié)構(gòu),定義為具有優(yōu)先級排序的隊列。
實際上,堆通常用作實現(xiàn)優(yōu)先隊列,大頂堆相當于元素按從大到小順序出隊的優(yōu)先隊列。從使用角度來看,我們可以將“優(yōu)先隊列”和“堆”看作等價的數(shù)據(jù)結(jié)構(gòu)。因此,本書對兩者不做特別區(qū)分,統(tǒng)一使用“堆“來命名。
堆的常用操作見表 8-1 ,方法名需要根據(jù)編程語言來確定。
表 8-1 堆的操作效率
方法名 | 描述 | 時間復雜度 |
---|---|---|
push() | 元素入堆 |
|
pop() | 堆頂元素出堆 |
|
peek() | 訪問堆頂元素(大 / 小頂堆分別為最大 / 小值) |
|
size() | 獲取堆的元素數(shù)量 |
|
isEmpty() | 判斷堆是否為空 |
|
在實際應用中,我們可以直接使用編程語言提供的堆類(或優(yōu)先隊列類)。
Tip
類似于排序算法中的“從小到大排列”和“從大到小排列”,我們可以通過修改 Comparator 來實現(xiàn)“小頂堆”與“大頂堆”之間的轉(zhuǎn)換。
heap.cpp
/* 初始化堆 */
// 初始化小頂堆
priority_queue<int, vector<int>, greater<int>> minHeap;
// 初始化大頂堆
priority_queue<int, vector<int>, less<int>> maxHeap;
/* 元素入堆 */
maxHeap.push(1);
maxHeap.push(3);
maxHeap.push(2);
maxHeap.push(5);
maxHeap.push(4);
/* 獲取堆頂元素 */
int peek = maxHeap.top(); // 5
/* 堆頂元素出堆 */
// 出堆元素會形成一個從大到小的序列
maxHeap.pop(); // 5
maxHeap.pop(); // 4
maxHeap.pop(); // 3
maxHeap.pop(); // 2
maxHeap.pop(); // 1
/* 獲取堆大小 */
int size = maxHeap.size();
/* 判斷堆是否為空 */
bool isEmpty = maxHeap.empty();
/* 輸入列表并建堆 */
vector<int> input{1, 3, 2, 5, 4};
priority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());
下文實現(xiàn)的是大頂堆。若要將其轉(zhuǎn)換為小頂堆,只需將所有大小邏輯判斷取逆(例如,將 ≥ 替換為 ≤ )。感興趣的讀者可以自行實現(xiàn)。
我們在二叉樹章節(jié)中學習到,完全二叉樹非常適合用數(shù)組來表示。由于堆正是一種完全二叉樹,我們將采用數(shù)組來存儲堆。
當使用數(shù)組表示二叉樹時,元素代表節(jié)點值,索引代表節(jié)點在二叉樹中的位置。節(jié)點指針通過索引映射公式來實現(xiàn)。
如圖 8-2 所示,給定索引 i ,其左子節(jié)點索引為 2i+1 ,右子節(jié)點索引為 2i+2 ,父節(jié)點索引為 (i?1)/2(向下取整)。當索引越界時,表示空節(jié)點或節(jié)點不存在。
圖 8-2 堆的表示與存儲
我們可以將索引映射公式封裝成函數(shù),方便后續(xù)使用。
my_heap.cpp
/* 獲取左子節(jié)點索引 */
int left(int i) {
return 2 * i + 1;
}
/* 獲取右子節(jié)點索引 */
int right(int i) {
return 2 * i + 2;
}
/* 獲取父節(jié)點索引 */
int parent(int i) {
return (i - 1) / 2; // 向下取整
}
堆頂元素即為二叉樹的根節(jié)點,也就是列表的首個元素。
my_heap.cpp
/* 訪問堆頂元素 */
int peek() {
return maxHeap[0];
}
給定元素 val ,我們首先將其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立條件可能已被破壞。因此,需要修復從插入節(jié)點到根節(jié)點的路徑上的各個節(jié)點,這個操作被稱為「堆化 heapify」。
考慮從入堆節(jié)點開始,從底至頂執(zhí)行堆化。如圖 8-3 所示,我們比較插入節(jié)點與其父節(jié)點的值,如果插入節(jié)點更大,則將它們交換。然后繼續(xù)執(zhí)行此操作,從底至頂修復堆中的各個節(jié)點,直至越過根節(jié)點或遇到無須交換的節(jié)點時結(jié)束。
圖 8-3 元素入堆步驟
設節(jié)點總數(shù)為 n ,則樹的高度為
my_heap.cpp
/* 元素入堆 */
void push(int val) {
// 添加節(jié)點
maxHeap.push_back(val);
// 從底至頂堆化
siftUp(size() - 1);
}
/* 從節(jié)點 i 開始,從底至頂堆化 */
void siftUp(int i) {
while (true) {
// 獲取節(jié)點 i 的父節(jié)點
int p = parent(i);
// 當“越過根節(jié)點”或“節(jié)點無須修復”時,結(jié)束堆化
if (p < 0 || maxHeap[i] <= maxHeap[p])
break;
// 交換兩節(jié)點
swap(maxHeap[i], maxHeap[p]);
// 循環(huán)向上堆化
i = p;
}
}
堆頂元素是二叉樹的根節(jié)點,即列表首元素。如果我們直接從列表中刪除首元素,那么二叉樹中所有節(jié)點的索引都會發(fā)生變化,這將使得后續(xù)使用堆化修復變得困難。為了盡量減少元素索引的變動,我們采用以下操作步驟。
如圖 8-4 所示,“從頂至底堆化”的操作方向與“從底至頂堆化”相反,我們將根節(jié)點的值與其兩個子節(jié)點的值進行比較,將最大的子節(jié)點與根節(jié)點交換。然后循環(huán)執(zhí)行此操作,直到越過葉節(jié)點或遇到無須交換的節(jié)點時結(jié)束。
圖 8-4 堆頂元素出堆步驟
與元素入堆操作相似,堆頂元素出堆操作的時間復雜度也為 O(log?n) 。
my_heap.cpp
/* 元素出堆 */
void pop() {
// 判空處理
if (isEmpty()) {
throw out_of_range("堆為空");
}
// 交換根節(jié)點與最右葉節(jié)點(即交換首元素與尾元素)
swap(maxHeap[0], maxHeap[size() - 1]);
// 刪除節(jié)點
maxHeap.pop_back();
// 從頂至底堆化
siftDown(0);
}
/* 從節(jié)點 i 開始,從頂至底堆化 */
void siftDown(int i) {
while (true) {
// 判斷節(jié)點 i, l, r 中值最大的節(jié)點,記為 ma
int l = left(i), r = right(i), ma = i;
// 若節(jié)點 i 最大或索引 l, r 越界,則無須繼續(xù)堆化,跳出
if (l < size() && maxHeap[l] > maxHeap[ma])
ma = l;
if (r < size() && maxHeap[r] > maxHeap[ma])
ma = r;
// 若節(jié)點 i 最大或索引 l, r 越界,則無須繼續(xù)堆化,跳出
if (ma == i)
break;
swap(maxHeap[i], maxHeap[ma]);
// 循環(huán)向下堆化
i = ma;
}
}
更多建議: