SGISTL源码探究-大根堆heap
2017-09-15 19:14
399 查看
前言
小根堆和大根堆的概念在数据结构都讲过,等下简单的过一下就行了。在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源码探究-迭代器的类型
- SGISTL源码探究-stack配接器
- SGISTL源码探究-STL中的红黑树(下)
- SGISTL源码探究-queue配接器
- SGISTL源码探究-关联式容器:hash_multiset
- SGISTL源码探究-stl_numeric.h中的数值算法
- SGISTL源码探究-STL中的红黑树(上)
- SGISTL源码探究-stl_algobase.h中的算法
- SGISTL源码探究-关联式容器:hash_multimap
- SGISTL源码探究-空间配置器
- SGISTL源码探究-pair的实现
- SGISTL源码探究-STL中的hashtable(下)
- SGISTL源码探究-stl_algo.h中的基础算法
- SGISTL源码探究-第一级配置器
- SGISTL源码探究-关联式容器:map
- SGISTL源码探究-关联式容器:hash_set
- SGISTL源码探究-内存池
- SGISTL源码探究-vector容器(下)
- SGISTL源码探究-list容器(上)
- SGISTL源码探究-deque容器(上)