实现了二叉堆的基本算法:adjust_heap
(下沉),pop_heap
,push_heap
,make_heap
,和sort_heap
。
二叉堆的实现
- 二叉堆是完全二叉树
- 一个高度为h的堆里,元素最多为$2^{(h+1)} - 1$个, 最少为$2^h$个.
SGI STL的heap
类算法并未使用哨兵,因为如果要用一个vector
来初始化priority_queue
,则需要把vector
中所有元素右移1位,增加了建堆隐含的常数时间。
- 根节点下标为0。
- i节点的左孩子为$2i + 1$,右孩子为$2i + 2$
- i节点的父亲节点为
(i - 1) / 2
注意:需要处理i == 0
的情况,否则对于size_t
,会溢出。 - 叶子节点开始的下标为
len - 2 / 2
注意:需要处理len < 2
的情况,否则对于size_t
,会溢出。
下沉(sink, percolate down)
实现1(GNU C++ STL采用的实现)
假设一个节点:
- 左右子树都满足二叉堆的性质
- 该节点小于其左孩子和(或)右孩子
此时需要调整该节点使得以该节点为根的树满足二叉堆的性质,进行的操作叫做下沉。
思路
- 把这个节点的值先保存下来,将这个节点看作一个洞。
- 比较这个洞的左孩子和右孩子的值,用有较大值的孩子节点的值填补这个洞。
- 洞转移当前面有较大值的孩子节点。
实现时因为可能一个节点没有右孩子,只有左孩子,所以我们先计算右孩子的值,并以右孩子不存在作为循环结束的条件。循环结束后,如果恰好右孩子是size
,那么此时的洞就是只有左孩子的节点。我们用洞的左孩子填这个洞,洞变为他的左孩子。最后,再用一开始记录的value
来填补这个洞。
最重要的实现细节:此时这这洞被填完了不算完成。我们还必须对这个刚被填好的洞做一次上浮swim
。因为我们在下沉这个洞的时候,没有考虑和最开始节点的值的关系。
STL这样实现的原因:TODO(为什么不看着value的值来决定还下不下沉呢?而是沉到底在浮上来,很神秘。)
最后的swim使用了限制上浮位置的swim,还不知道为什么这样做。不得不说STL里的有些实现真的是奇怪。
1 | template<typename It, typename Compare> |
限制了上浮位置的swim,供下沉的实现1使用,还不知道为什么要这样做。1
2
3
4
5
6
7
8
9
10
11
12
13template<typename It, typename Compare>
void swim_with_top(It first, It hole, It top, It last, const Compare& comp){
//if hole == first, distance(size_t) will underflow
if(first == hole || first == last) return;
auto value = *hole;
auto parent = first + (distance(first, hole) - 1) / 2;
while(hole > top && comp(*parent, value)){
*hole = *parent;
hole = parent;
parent = first + (distance(first, hole) - 1) / 2;
}
*hole = value;
}
实现2(CLRS采用的实现,推荐)
大体思路同STL中的实现。在循环里判断是否还需要下沉,不需要就break
循环。之后在对于没有右孩子的特殊处理处,要加一个需要继续下沉的判断comp(value, *(secondChild- 1))
。
1 | template<typename It, typename Compare> |
1 | template<typename It, typename Comp = less<typename iterator_traits<It>::value_type> > |
删除堆顶元素(pop_heap)
删除堆顶元素后仍要维护二叉堆的性质。我们只需将二叉堆最后一个元素放到堆顶,之后对堆顶进行下沉sink()
即可。
C++ STL中的pop_heap()
实现并不是删除堆顶元素,而是将堆顶元素放置到区间的末尾。
1 | template<typename It, typename Comp = less<typename iterator_traits<It>::value_type> > |
由于C++ STL中容器的元素删除必须通过容器的成员函数,所以下面的例子代码需要传入该容器的引用。
1 | template<typename Container, typename Compare> |
上浮(swim, percolate up)
- 将要上浮的节点的值储存在value中,并把该节点看作一个洞。
- 如果洞的父亲节点比value小,就把父亲节点的值复制到洞里,让父亲节点成为新的洞。
- 注意洞不能是first节点。
- 最后将洞用value填上。
1 | template<typename It, typename Compare> |
加入新元素(push_heap)
要求加入的新元素已经放在堆的最后面,对堆的最后面的元素做上浮即可。1
2
3
4template<typename It, typename Compare>
void push(It first, It last, const Compare& comp){
swim(first, (last - 1), last, comp);
}
建堆(make_heap)
Williams建堆
一个一个元素加入,每次加入都push_heap一下。需要额外空间。(不推荐)。时间复杂度$\Theta(N\log N)$
Floyd建堆
自底向上,从第一个叶子节点到根节点,做下沉。时间复杂度为$O(N)$
1 | template<typename It, typename Compare = less<typename It::value_type> > |
堆排序(sort_heap)
首先在给定的要排序的数组上建堆,之后不断将最大元素用pop_heap
从堆中移出,放在最后。最大堆得到的是从小到大的排序。最小堆得到的是从大到小的排序。
优先级队列(priority_queue)
一般在数组上建堆实现。C++ STL的priority_queue
默认使用vector
作为底层容器。仅提供top
,pop
,push
等接口。作为一个容器适配器,priority_queue
不提供迭代器,因此不能访问除了最大值以外的内容。
priority_queue
注意,它的第二个模版参数是Container,第三个才是Comapre。但是构造函数里,是先传Compare,再传Container。
如果需要更灵活的建堆和访问操作,可以使用algorithm
算法库中提供的仅支持随机访问迭代器的算法:
- `make_heap`
- `push_heap`(需保证想要放入堆中的元素已经放在末尾)
- `pop_heap`
- `sort_heap`
- `is_heap`
- `is_heap_until`
使用哨兵实现的二叉堆
- 一个有n个节点的二叉堆
- 叶子节点为$\lfloor n/2 \rfloor + 1,\,\lfloor n/2 \rfloor + 2, \cdots ,n$
- 非叶子节点为$\lfloor n/2 \rfloor,\,\lfloor n/2 \rfloor - 1, \cdots , 1$
- 下标为0的位置留做哨兵(sentinel),根节点下标为1。
- i节点的左孩子为$2i$,
i << 1
,右孩子为$2i + 1$,(i << 1) + 1
- i节点的父亲节点为$\lfloor i/2 \rfloor$,
i / 2
具体实现可参考: - C++实现: Github - xiexiexx - xPriority_Queue
- C实现: The Algorithm Design Manual, Second Edition, S.Skiena, Chapter 4.
K路归并
[CLRS]6.5-9, Merge k Sorted Lists
见链表#链表排序##合并k个排序的链表