STL源码笔记(14)—堆和优先级队列
priority_queue是拥有权值观念的queue,跟queue类似,其只能在一端push,一端pop,不同的是,每次push元素之后再容器内部元素将按照一定次序排列,使得pop得到的元素始终是当前权值的极大值。
很显然,满足这个条件就需要某些机制了,缺省情况下使用max-heap大顶堆来实现,联想堆排序的实现,使用大顶完成序列从小到大的排序,过程大概是:
- 把堆的根元素(堆中极大值)交换到最后
- 堆的长度减1
这样每次取出堆中的极大值完成排序,刚好与优先级队列要求的权值最高类似,因此可以利用这个特性完成优先级队列。
对于一个优先级队列来说,他使用vector<T>
作为其底层默认容器,与stack,queue类似,priority_queue也是一个adapter(配接器),被归类为container adapter
注:在SGI STL源码中priority_queue的源码位于文件stl_queue.h中,heap算法的源码位于stl_heap.h中。
heap算法
heap算法是实现优先级队列的核心。主要有:
1.push_heap()
2.pop_heap()
3.__adjust_heap()
4.sort_heap()
5.make_heap()
除了__adjust_heap()外,其余四个都是可以在外部使用的函数,如果要实现堆排序的话可以直接使用上述4,5函数。
push_heap函数
书中给了一个很形象的表述叫做percolate up(上溯)操作,这里假设:
在容器vector中,前n个元素(索引0~n-1)为已经排好的max-heap,现在待插入元素的索引为n,这里,程序会在这里挖个坑,称之为holeindex
,可以理解为我现在要插入一个元素,但是我并不知道要插在哪里,所以我先挖一个坑,然后经过一系列的算法,这个holeindex
,要开始慢慢往上爬,以满足大顶堆条件,其实这里的上爬做了两件事:
- 将比value小的parent节点往下拉
- 将原来的holeindex往上拉,占据原来parent所在的位置
持续操作,直到holeindex爬到顶端(根节点),以及满足大顶堆的条件了,此时holeindex已经上溯到正确的位置,只需要将value值填进去即可。
template <class _RandomAccessIterator, class _Distance, class _Tp,
class _Compare>
void
__push_heap(_RandomAccessIterator __first, _Distance __holeIndex,
_Distance __topIndex, _Tp __value, _Compare __comp)
{
_Distance __parent = (__holeIndex - 1) / 2;//父节点位置
while (__holeIndex > __topIndex && __comp(*(__first + __parent), __value)) {//__comp默认是小于:父节点小于子节点
//调整hole的位置,如果满足条件hole将一直上溯
*(__first + __holeIndex) = *(__first + __parent);
__holeIndex = __parent;
__parent = (__holeIndex - 1) / 2;
}
*(__first + __holeIndex) = __value;//把value放到新的位置
}
template <class _RandomAccessIterator, class _Compare,
class _Distance, class _Tp>
inline void
__push_heap_aux(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Compare __comp,
_Distance*, _Tp*)
{
//直接调用__push_heap
__push_heap(__first, _Distance((__last - __first) - 1), _Distance(0),
_Tp(*(__last - 1)), __comp);
}
template <class _RandomAccessIterator, class _Compare>
inline void
push_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Compare __comp)
{
__STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
__push_heap_aux(__first, __last, __comp,
__DISTANCE_TYPE(__first), __VALUE_TYPE(__first));
}
pop_heap与__adjust_heap函数
与push_heap对应的就是“出堆”操作了,在堆排序中,我们对已经建好的堆进行“出堆”,操作是每次将堆顶根元素换到容器末尾,先对堆的长度减1,再对堆进行调堆操作,重复进行直到堆中所有元素出来,由于出堆操作每次出的是堆顶元素,也就是极大值,因此完成出堆操作后就完成排序工作。
pop_heap与push_heap对应的是percolate down(下溯)操作,这里很容易理解因此堆顶的根元素被取走以后,这里原来的根元素就是一个没有元素的空节点了,也就是所谓的holeindex,这里就是要将这个holeindex调整到合适的位置,holeindex之前的元素是一个堆,实际上做的工作是:
- 找到holeindex对应两个孩子中较大的将其换到holeindex的位置
- 将holeindex下溯到上述较大孩子的索引的位子
- 重复执行上述操作,最终holeindex将落在某个叶子节点处。
上述第2点需要考虑没有右子树的情况,例如:
当下溯到(索引2处)原65节点时,此时没有右节点,右节点索引second==len
由于此时并没有考虑从容器末尾换出来的value值,所以要对堆的索引为 0~holeindex(模拟一下过程可以很容易得到此时的holeindex位于叶子节点处) 的元素进行一次push_heap操作,使得value插入到合适的位置。
所以说,pop_heap的工作实际上很简单,主要任务是极大值出堆后要对剩余的元素进行重新调整,使得其成一个堆。
//4.
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
_Distance __len, _Tp __value)
{
_Distance __topIndex = __holeIndex;//holeindex在顶端
_Distance __secondChild = 2 * __holeIndex + 2;//从右孩子开始
while (__secondChild < __len) {//表示holeindex还没有到叶子节点,要不停下溯
//取左右孩子的最大值
if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))
__secondChild--;
*(__first + __holeIndex) = *(__first + __secondChild);//较大的孩子上调
__holeIndex = __secondChild;//holeindex下溯
__secondChild = 2 * (__secondChild + 1);
}
if (__secondChild == __len) {//当前holeindex只有左孩子
*(__first + __holeIndex) = *(__first + (__secondChild - 1));
__holeIndex = __secondChild - 1;
}
__push_heap(__first, __holeIndex, __topIndex, __value);//重新插入
}
//3.
template <class _RandomAccessIterator, class _Tp, class _Distance>
inline void
__pop_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
_RandomAccessIterator __result, _Tp __value, _Distance*)
{
*__result = *__first;//首先将堆中极大值赋给堆末尾位置
//这里的last已经完成减1操作
//调堆操作
__adjust_heap(__first, _Distance(0), _Distance(__last - __first), __value);
}
//2.
template <class _RandomAccessIterator, class _Tp>
inline void
__pop_heap_aux(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Tp*)
{
//直接调用,这里已经获取堆中最后一个元素的值,堆中最后一个元素的迭代器变量
__pop_heap(__first, __last - 1, __last - 1,
_Tp(*(__last - 1)), __DISTANCE_TYPE(__first));
}
//1.
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));//直接调用
}
make_heap和heap_sort
这两个函数实现了堆排序。
make_heap是将一段现有的数据转化成一个heap,操作就是,从第一个非叶子节点向前进行调堆操作,为什么要选择第最后一个非叶子节点呢?因为,调堆操作是对holeindex进行下溯,现在你都是叶子节点了还怎么下?
至于最后一个非叶子节点的计算公式:
index=(len?2)/2(1)
其中len是数组的长度。
heap_sort函数就简单了,不断的执行pop_heap操作即可:
template <class _RandomAccessIterator, class _Tp, class _Distance>
void
__make_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Tp*, _Distance*)
{
if (__last - __first < 2) return;
_Distance __len = __last - __first;
//找出第一个需要重排的子树头部(因为需要执行perlocate down操作,而叶子节点不需要执行该操作)
_Distance __parent = (__len - 2)/2;
while (true) {
__adjust_heap(__first, __parent, __len, _Tp(*(__first + __parent)));
if (__parent == 0) return;
__parent--;
}
}
//将一段现有的数据转化成一个heap...在堆排序中就是build_heap
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));
}
//每次将一个极大值放到尾端
template <class _RandomAccessIterator>
void sort_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
__STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
__STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
_LessThanComparable);
while (__last - __first > 1)
pop_heap(__first, __last--);
}