1. 程式人生 > 其它 >SGI STL堆heap

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;
}