SGI STL堆heap
heap簡介
heap不是STL容器元件,而是為了輔助priority queue(優先佇列)。priority queue允許使用者以任何次序將任何元素推入容器內,但取出時一定是從優先權最高(即數值最大)的元素開始取。二叉最大堆(binary max heap)正具有這樣的特性,適合作為priority queue的底層實現機制。
可以用陣列用來存放二叉堆(binary heap),而binary heap其實也是一種完全二叉樹(complate binary tree)。除了最底層葉子節點外,其餘地方都是填滿的。如下圖所示,是一個二叉最大堆:
其中,節點i(從陣列索引0開始計算)的左兒子位於陣列的2i + 1位置,右兒子位於陣列的2i + 2的位置。相對地,如果節點位置j,那麼父節點位置(j-1)/2。這種用陣列來表示tree方式,稱為隱式表述法(implicit representation)。
如此,要實現heap,只需要一個數組和一組heap演算法,用來插入元素、刪除元素、取極值,同時維持heap特性。由於heap插入資料後,可能需要陣列動態改變大小,因此選用vector,而不選用固定大小的array。
heap特性
heap分為max heap(最大堆),min heap(最小堆)。
最大堆:任意節點key值不小於左、右兒子的key值。也就是說,最大key值位於根節點。
最小堆:任意節點key值不大於左、右兒子的key值。也就是說,最小key值位於根節點。
不論是建堆,還是插入元素、刪除元素,都必須維持堆的特性。
下面的heap演算法,都以max heap為例,min heap的演算法類似。
heap演算法
push_heap 演算法
當heap插入一個數據後,該如何保持max-heap特性?
這就是push_heap演算法要做的事情。
下圖所示,是push_heap演算法的實際演練過程。新加入元素要放在樹最下面一層的葉子節點,並且填補vector從左到右的第一個空格。也就是說,新插入節點是放在vector的末尾(end())。
插入新元素50,為了維護大堆特性,會由新插入節點的父節點開始上溯,保持父節點key值永遠不小於兒子節點key值。如果違反這個特性,就要交換父節點、子節點位置。如此,直到不需要交換節點為止(因為其他節點結構沒動,大小關係不會改變)。
下面程式碼是push_heap演算法實現細節。函式接受2個迭代器first和last,1)用來表示底部容器vector的頭尾,2)並且新元素已經插入到底部容器尾端。如果不符合1)和2)兩點,函式執行結果未知。
//-----------------------------------------------
// push_heap 演算法
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__push_heap(_RandomAccessIterator __first,
_Distance __holeIndex, _Distance __topIndex, _Tp __value)
{
_Distance __parent = (__holeIndex - 1) / 2; // holeIndex父節點
// 如果父節點值 < 當前插入值value, 就把父節點值移動到洞號對應位置, 洞號移動到父節點位置
while (__holeIndex > __topIndex && *(__first + __parent) < __value) {
*(__first + __holeIndex) = *(__first + __parent);
__holeIndex = __parent;
__parent = (__holeIndex - 1) / 2; // 重新計算父節點位置
}
*(__first + __holeIndex) = __value; // 最後洞號就是插入值應該在的位置
}
template <class _RandomAccessIterator, class _Distance, class _Tp>
inline void
__push_heap_aux(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Distance*, _Tp*)
{
// 根據implicit representation heap結構特性:
// 新值必置於底部容器尾端(last-1), 即第一個洞號: (last-first)-1 (注意, 此時last已是插入元素後右移一格)
__push_heap(__first, _Distance((__last - __first) - 1), _Distance(0),
_Tp(*(__last - 1)));
}
// public介面
// 對[first, last)執行push_heap演算法, 確保插入元素仍保持堆特性
// 假設[first, last)表示底部容器的頭尾, 而且新元素已經插入到底部容器末尾
template <class _RandomAccessIterator>
inline void
push_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
__STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
__STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
_LessThanComparable);
// 此函式被呼叫, 新元素應已經置於底部容器的尾端
__push_heap_aux(__first, __last,
__DISTANCE_TYPE(__first), __VALUE_TYPE(__first));
}
pop_heap演算法
當heap移除一個元素時,max-heap如何維持堆特性?
這是pop_heap演算法要解決的問題。身為max-heap,最大值位於根節點,而且pop操作取走根節點,放到底部容器vector的最後一個元素之後。
為了滿足完全二叉樹的特性,要將最下一層最右邊的葉子節點拿掉,調換到根節點,然後從根節點開始對整個樹進行調整,為這個被拿掉的節點找一個適當位置。
為滿足max-heap特性(根節點key >= 子節點key),要執行一個percolate down(下溯)程式:將根節點(最大值被取走後,形成一個“洞”hole)填入上述那個失去生產空間的葉節點,再將它拿來和其兩個子節點比較鍵值(key),並與較大子節點交換位置。如此,直到這個“洞”的key >= 左右兒子key,或者直到下放到葉子節點(沒有子節點)為止。
注意:示例中從max-heap中移除的68還存在vector中,不過不屬於heap了。
下面程式碼是pop_heap實現細節。該函式接受2個迭代器,用來表示一個heap底部容器(vector)的頭尾。pop_heap假設直接的元素都是通過push_heap插入heap,已經滿足max-heap特性。如不符合這2個條件,pop_heap結果未定義。
//----------------------------------------------------------------
// pop_heap
// 不允許指定"大小比較標準"(比較子)的版本
// 以洞號為根節點, 重排堆, 使之符合堆特性
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
_Distance __len, _Tp __value)
{
_Distance __topIndex = __holeIndex;
_Distance __secondChild = 2 * __holeIndex + 2; // 洞節點右兒子
// 從洞節點開始, 找子樹中最大的兒子, 上移至洞節點
// 將洞號往下傳, 直到葉子
while (__secondChild < __len) { // 右兒子合法, 說明存在右兒子
// 比較洞節點左右2個兒子, 讓secondChild代表較大子節點
if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))
__secondChild--;
// 令較大兒子值為洞值, 再令洞號下移值較大子節點處
*(__first + __holeIndex) = *(__first + __secondChild);
__holeIndex = __secondChild;
// 找出新洞節點的右兒子節點
__secondChild = 2 * (__secondChild + 1);
}
if (__secondChild == __len) { // 不存在右兒子, 只有左兒子
// 令左兒子為洞值, 再令洞號下移至左兒子節點處
*(__first + __holeIndex) = *(__first + (__secondChild - 1));
__holeIndex = __secondChild - 1;
}
// 已經找到新洞號, 將欲調整值value填入目前的洞號內. 此時肯定滿足次序特性.
// 下面相當於 *(first + holeIndex) = value
__push_heap(__first, __holeIndex, __topIndex, __value);
}
// 不允許指定"大小比較標準"(比較子)的版本
template <class _RandomAccessIterator, class _Tp, class _Distance>
inline void
__pop_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
_RandomAccessIterator __result, _Tp __value, _Distance*)
{
*__result = *__first; // 設尾值為首值, 於是尾值即為所求結果.
// 稍後可由客戶端用底層容器的pop_back()取出尾值
// 因為原來的根節點成為洞, 堆元素個數少了1個, 因此需要重排堆
// 以根節點為子樹根節點, 重新調整heap, 洞號0(樹根), value是要調整的值(原來的尾值)
__adjust_heap(__first, _Distance(0), _Distance(__last - __first), __value);
}
template <class _RandomAccessIterator, class _Tp>
inline void
__pop_heap_aux(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Tp*)
{
// 根據implicit representation heap的次序特性, pop操作結果應為底部容器的第一個元素.
// 因此, 首先設定欲調整值為尾值, 然後將首值交換值尾節點(即迭代器last-1指向的最後一個元素),
// 然後重新調整[first, last-1), 使之符合堆特性
__pop_heap(__first, __last - 1, __last - 1,
_Tp(*(__last - 1)), __DISTANCE_TYPE(__first));
}
// public介面
// 彈出堆頂元素, 執行下溯. 此時, 堆頂尚未從堆中移除.
// [first, last)是heap底部容器所有元素區間, 假設已經符合heap特性
template <class _RandomAccessIterator>
inline void pop_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last)
{
__STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
__STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
_LessThanComparable);
__pop_heap_aux(__first, __last, __VALUE_TYPE(__first));
}
呼叫pop_heap之後,最大元素只是被放置在底部容器尾端,並沒有被取走。如果要取值,可以用底部容器vector提供的back();如果要移除,可以用pop_back()。
sort_heap演算法
pop_heap每次能獲得heap中key最大的元素,如果持續對整個heap做pop_heap操作,每次將操作範圍向前縮減一個元素,這樣整個程式執行完時,便有了一個遞增序列。這就是堆排序(sort_heap)。
// public介面, 不支援自定義比較子版本
// 堆排序
template <class _RandomAccessIterator>
void sort_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
__STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
__STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
_LessThanComparable);
// 每執行一次pop_heap(), 極值(STL heap中為極大值)即被放在尾端.
// 扣除尾端再執行一次pop_heap(), 次極值又被放在新尾端. 一直下去, 最後的堆排序結果
while (__last - __first > 1)
pop_heap(__first, __last--);
}
make_heap演算法
嚴格來說,堆排序分2個步驟:1)建堆;2)一個一個元素pop到底部容器尾端,形成有序序列。
建堆是指將一段現有資料轉化為heap,如何進行的呢?
這就需要用到make_heap演算法。
// 不接受比較子的版本
template <class _RandomAccessIterator, class _Tp, class _Distance>
void
__make_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Tp*, _Distance*)
{
if (__last - __first < 2) return; // 如果長度為0或1, 不必重新排列
_Distance __len = __last - __first; // 區間長度
// 找出第一個需要重排的子樹頭部(最後一個non-leaf節點), 以parent標示出.
_Distance __parent = (__len - 2)/2;
while (true) {
// 重排以parent為首的子樹, len是為了讓 __adjust_heap() 判斷操作範圍
__adjust_heap(__first, __parent, __len, _Tp(*(__first + __parent)));
if (__parent == 0) return; // 走完根節點就結束
__parent--; // 下一次重排的子樹, 頭部向前移動一個節點
}
}
// public介面, 建堆
// 將[first, last)轉換成堆
template <class _RandomAccessIterator>
inline void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
__STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
__STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
_LessThanComparable);
__make_heap(__first, __last,
__VALUE_TYPE(__first), __DISTANCE_TYPE(__first));
}
因此,堆排序的完整步驟是:
vector<int> vec = {2,10,-5,50,7,100,62};
// 建堆
make_heap(vec.begin(), vec.end());
// 對底部容器資料進行堆排序
sort_heap(vec.begin(), vec.end());
// 輸出有序佇列, 此時底部容易中資料已經有序(升序)
for (int i = 0; i < vec.size(); ++i) {
cout << vec[i];
if (i < vec.size() - 1)
cout << ",";
}
cout << endl;
heap沒有迭代器
heap所有元素都遵循complete binary tree(完全二叉樹)排列規則,不提供遍歷功能,也不提供迭代器。
heap測試示例
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
{ // test case1: heap以vector為底部容器
int ia[9] = { 0,1,2,3,4,8,9,3,5 };
vector<int> ivec(ia, ia + 9);
make_heap(ivec.begin(), ivec.end());
for (size_t i = 0; i < ivec.size(); i++) {
cout << ivec[i] << ' '; // 9 5 8 3 4 0 2 3 1
}
cout << endl;
ivec.push_back(7);
push_heap(ivec.begin(), ivec.end());
for (size_t i = 0; i < ivec.size(); i++) {
cout << ivec[i] << ' '; // 9 7 8 3 5 0 2 3 1 4
}
cout << endl;
pop_heap(ivec.begin(), ivec.end());
cout << ivec.back() << endl; // 9
ivec.pop_back(); // 移除最後一個元素
for (size_t i = 0; i < ivec.size(); i++) {
cout << ivec[i] << ' '; // 8 7 4 3 5 0 2 3 1
}
cout << endl;
sort_heap(ivec.begin(), ivec.end());
for (size_t i = 0; i < ivec.size(); i++) {
cout << ivec[i] << ' '; // 0 1 2 3 3 4 5 7 8
}
cout << endl;
}
{ // test case2: heap以array為底部容器
int ia[6] = { 4,1,7,6,2,5 };
make_heap(ia, ia + 6);
for (size_t i = 0; i < 6; i++) {
cout << ia[i] << ' '; // 7 6 5 1 2 4
}
cout << endl;
}
return 0;
}