前言
小根堆和大根堆的概念在数据结构都讲过,等下简单的过一下就行了。在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的实现了。

本文介绍了SGISTL中Heap的实现方式,包括小根堆和大根堆的基本概念、插入与弹出元素的操作原理及具体实现代码。此外还详细说明了如何通过一系列算法将数据转换成Heap以及对Heap进行排序。
154

被折叠的 条评论
为什么被折叠?



