从零开始的 STL 实现记录:Container-5.1
前言
heap 并不归属于 STL 容器组件,它是个幕后英雄,扮演 priority queue 的助手。顾名思义,priority queue 允许用户以任何次序将任何元素推人容器内,但取出时一定是从优先权最高 (也就是数值最高) 的元素开始取。binary max heap 正是具有这样的特性,适合作为 priority queue的底层机制。
———— 《STL源码剖析》侯捷
本节将会分析 heap 的实现。
heap
让我们做一点分析。如果使用 list 作为 priority queue 的底层机制,元素插人操作可享常数时间。但是要找到 list 中的极值,却需要对整个 list 进行线性扫描。我们也可以改变做法,让元素插人前先经过排序这-一关,使得 list 的元素值总是由小到大 (或由大到小),但这么一来,虽然取得极值以及元素删除操作达到最高效率,可元素的插人却只有线性表现。
虽然以 binary search tree 作为 priority queue 的底层机制,元素的插入和极值的取得就有 O(logN)
的表现。但杀鸡用牛刀,未免小题大做,一来 binary search tree 的输入需要足够的随机性,二来 binary search tree 并不容易实现。priority queue 的复杂度,最好介于 queue 和 binary search tree 之间,才算适得其所。binary heap 便是这种条件下的适当候选人。
严格意义上来讲 heap 并不是一个容器,所以他没有实现自己的迭代器,也就没有遍历操作,它只是一种算法。
根据元索排列方式,heap 可分为 max-heap 和 min-heap 两种,前者每个节点的键值 (key) 都大于或等于其子节点键值,后者的每个节点键值 (key) 都小于或等于其子节点键值。因此,max-heap 的最大值在根节点,并总是位于底层 array 或 vector 的起头处; min-heap 的最小值在根节点,亦总是位于底层 array 或 vector 的起头处。STL 供应的是 max-heap。
heap 算法
在开始介绍之前需要明确,heap 所支持的容器必须具有 RandomAccessIterator
,因而 list 不能支持 heap,进而导致了 list 不能使用 stl 的 sort()
算法,因为 stl 的 sort()
也会涉及堆排序的操作,而堆排序的底层结构就是堆了。关于这部分内容,会留到介绍 stl 算法时再详细说明。
push_heap
push_heap()
默认新元素已经插入到 heap 的末尾,之后从该元素开始调整 heap,即所谓的 percolate up
。具体做法为:将当前处理的节点(holeIndex)与父节点进行比较,如果键值比父节点大就交换位置,直到不需要交换或到达根节点为止。
1 | /*****************************************************************************************/ |
pop_heap
pop_heap()
会将最大元素(根节点)调整至堆的尾部,执行完之后再使用容器的 pop_back()
即可得到最大值。具体做法为:对头部元素执行 percolate down
过程,不断地与最大的子节点进行交换,即下放。在 STL 的实现中,做法为直接将根节点下放至叶子节点,随后再执行了一次 percolate up
过程,即调用 push_heap()
将原始尾部的值上放到合适的位置。这里最后一步绝对不能改为 *(first + holeIndex) = value
,侯捷老师的书中这里有误。
但如果在下放的过程中就检查洞节点的两个子节点与原始尾部元素之间的大小关系,在尾部元素大于两个子节点时就停止下放,之后就不用执行上放的过程,这时 *(first + holeIndex) = value
是合理的。
1 | /*****************************************************************************************/ |
验证
我们将最后一句改为 *(first + holeIndex) = value;
,与 std 进行对比,可以很清楚地看到这样做是绝对错误的。如果条件不判断与两个子节点的大小关系,就必须执行 push_heap
才能确保正确性。
1 | void test1() { |
sort_heap
既然每次 pop_heap()
可获得 heap 中键值最大的元素,如果持续对整个 heap 做 pop_heap
操作,每次将操作范围从后向前缩减一个元素 (因为 pop_heap
会把键值最大的元素放在底部容器的最尾端),当整个程序执行完毕时,我们便有了一个递增序列。
需要注意的是,排序过后,原来的 heap 就不再是一个合法的 heap 了。
1 | /*****************************************************************************************/ |
make_heap
make_heap()
用于将一段现有的数据转化为一个 heap,具体做法为:从第一个非叶子节点开始,逐个执行 adjust_heap()
程序,即调整堆,在调整到首个元素时即形成了一个合法的 heap。
1 | /*****************************************************************************************/ |
总结
heap
并不归属于 STL 容器组件,它是个幕后英雄,扮演 priority queue
的助手。heap
没有自己的迭代器,只要支持 RandomAccessIterator
的容器都可以作为 Heap 容器。heap
最重要的函数还是 pop
和push
的实现。下一节将会看到 priority queue
是如何利用 heap
实现的。