SGISTL源码探究-大根堆heap

前言

小根堆和大根堆的概念在数据结构都讲过,等下简单的过一下就行了。在SGISTL的实现中,它并不作为一种容器,而是一系列的算法,用于给priority_queue提供支持,使优先级队列能够体现其优先级。
在SGISTL实现heap中,采用的数据结构并不是使用二叉树实现,而是采用隐式表达,即使用数组表示一个堆。能用数组表示这是因为堆总是一棵完全二叉树(没有节点漏洞):即若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的节点都连续集中在最左边,这就是完全二叉树。

基本概念

小根堆

若根节点存在左孩子,则根节点的值小于左孩子的值;若根节点存在右孩子,则根节点的值小于右孩子的值。即根结点的值为所有结点中的最小值。

大根堆

若根节点存在左孩子,则根节点的值大于左孩子的值;若根节点存在右孩子,则根节点的值大于右孩子的值。即根结点的值为所有结点中的最大值。

插入元素

先将结点插入到堆的尾部,再将该结点逐层向上调整,直到依然构成一个堆,调整方法是看每个子树是否符合大(小)根堆的特点,不符合的话则调整叶子和根的位置。

弹出元素

将根节点弹出后用堆尾结点进行填补,调整二叉树,使之依然成为一个堆。

heap的实现

首先SGISTL中实现的是大根堆,即堆顶节点是元素值最大的节点。使用数组实现一个堆是利用了完全二叉树的性质。如图所示(0号元素不使用):
这里写图片描述
不难发现由于完全二叉树没有空缺的节点,所以第i号元素的左孩子为第2i号元素,而其右孩子为第2i+1号元素,父节点为第i/2号元素。比如1号元素(12)的左孩子为2号元素(9),右孩子为3号元素(10)。
拥有这样的特性,我们就不难实现关于堆以及它的一系列操作了,只需要一个vector以及一些heap操作就能满足我们的要求。不过在SGISTL的具体实现中,0号元素是使用了的。所以第i号元素的左孩子应为第2i+1号元素,而右孩子为第2i+2号元素,父节点为第(i - 1)/2号元素。

这里再提一点,SGISTL的实现heap的各种算法中,有重载了需要元素比较大小标准作为最后一个参数的版本,以下的都是没有元素比较大小标准的版本。

插入元素

提供给priority_queue使用的接口是push_heap,源码如下:

template <class RandomAccessIterator>
inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) {
  __push_heap_aux(first, last, distance_type(first), value_type(first));
}

插入元素的时候先将元素插入到末端,然后进行调整。
__push_heap_aux函数的代码如下:

template <class RandomAccessIterator, class Distance, class T>
inline void __push_heap_aux(RandomAccessIterator first,
                            RandomAccessIterator last, Distance*, T*) {
  __push_heap(first, Distance((last - first) - 1), Distance(0),
              T(*(last - 1)));
}

最核心的插入操作是由__push_heap完成的,如下:

template <class RandomAccessIterator, class Distance, class T>
void __push_heap(RandomAccessIterator first, Distance holeIndex,
                 Distance topIndex, T value) {
  //计算得出父节点
  Distance parent = (holeIndex - 1) / 2;
  /* 前面说过堆中插入元素的大致思路
   * 即先将结点插入到堆的尾部
   * 再将该结点逐层向上调整(与父节点交换)
   * 直到依然构成一个符合规则的堆
   */
  while (holeIndex > topIndex && *(first + parent) < value) {
    /* 还未到堆顶并且父节点的值小于插入的值
     * 则将父节点的值下移
     * 将当前节点移动到父节点上
     * 而parent也指向新的父节点
     */
    *(first + holeIndex) = *(first + parent);
    holeIndex = parent;
    parent = (holeIndex - 1) / 2;
  }    
  /* 出循环之后,证明找到了合适的插入位置
   * 进行赋值
   */
  *(first + holeIndex) = value;
}
弹出元素

提供给priority_queue的弹出元素的接口是pop_heap,源码如下:

template <class RandomAccessIterator>
inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last) {
  __pop_heap_aux(first, last, value_type(first));
}

__pop_heap_aux的源码实现如下:

template <class RandomAccessIterator, class T>
inline void __pop_heap_aux(RandomAccessIterator first,
                           RandomAccessIterator last, T*) {
  __pop_heap(first, last - 1, last - 1, T(*(last - 1)), distance_type(first));
}

__pop_heap的源码如下:

template <class RandomAccessIterator, class T, class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,
                       RandomAccessIterator result, T value, Distance*) {
  *result = *first;      //将堆顶元素的值放在堆的末尾
  __adjust_heap(first, Distance(0), Distance(last - first), value); //重新从被取走值的节点开始调整堆(这里的是根结点),使其符合规则,需要新插入的值为原来尾部的值
}

最核心的即__adjust_heap函数,实现如下:

template <class RandomAccessIterator, class Distance, class T>
void __adjust_heap(RandomAccessIterator first, Distance holeIndex,
                   Distance len, T value) {
  //topIndex指向传入的holeIndex结点
  Distance topIndex = holeIndex;
  //holeIndex结点的右孩子的索引
  Distance secondChild = 2 * holeIndex + 2;
  while (secondChild < len) {
    /* 比较左右孩子节点的大小,选择较大的节点作为新的父结点
     * 然后下移
     * 直到移动到该分支的最后一个叶节点为止
     */
    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的值同时大于左右两个节点,不满足max-heap这种情况
  __push_heap(first, holeIndex, topIndex, value);
}

执行了pop_back函数后,最大的元素是被置放到了底部容器的最后一个位置,所以可以利用back()等函数取得。

将数据转换成heap

该操作由__make_heap完成

template <class RandomAccessIterator, class T, class Distance>
void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*,
                 Distance*) {
  //长度小于等于1则无需排列
  if (last - first < 2) return;
  //需要重排的第一个子树的父节点
  Distance len = last - first;
  Distance parent = (len - 2)/2;

  while (true) {
    //调用__adjust_heap,对堆进行调整
    //每次while循环仅确保了first到parent之间的数据满足heap
    __adjust_heap(first, parent, len, T(*(first + parent)));
    //走到了根节点,则结束
    if (parent == 0) return;
    //已经重排了的子树父节点前移
    parent--;
  }
}
对堆进行排序

思路很简单,当我们执行pop_heap操作时,最大堆元素会被放置在容器最后一个元素上,连续多次调用pop_heap,则可以让容器成为一个递增序列了。

template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last) {
  while (last - first > 1) pop_heap(first, last--);
}

小结

本小节针对为实现priority_queue而使用的heap进行了分析。如果你对这种数据结构比较熟悉,那应该很容易理解,如果不太熟悉,也没关系,网上有大量的资料详细讲解堆结构并且辅有大量的例子,这里就不详细去介绍它了。
接下来我们就可以看到priority_queue的实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值