• 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;
    }
    
  • 相关阅读:
    java io系列23之 BufferedReader(字符缓冲输入流)
    java io系列22之 FileReader和FileWriter
    java io系列21之 InputStreamReader和OutputStreamWriter
    java io系列20之 PipedReader和PipedWriter
    java io系列19之 CharArrayWriter(字符数组输出流)
    java io系列18之 CharArrayReader(字符数组输入流)
    java io系列17之 System.out.println("hello world")原理
    java io系列16之 PrintStream(打印输出流)详解
    java io系列15之 DataOutputStream(数据输出流)的认知、源码和示例
    java io系列14之 DataInputStream(数据输入流)的认知、源码和示例
  • 原文地址:https://www.cnblogs.com/fortunely/p/16249158.html
Copyright © 2020-2023  润新知