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