概叙
- heap并不属于STL容器组件,它是个幕后英雄,扮演priority queue的助手。
priority queue
允许用户以任意次序将任何元素推入容器内,但是取出时一定从优先级最高的元素开始存取。
为什么要用heap
作为priority queue
的底层机制呢?
- 如果
list
作为底层:- 如果不排序就拆入,那么插入是O(1)常数级别复杂度,如果要找极值,那么必须遍历链表,复杂度为O(n)
- 如果排序之后再插入,那么插入时O(n),删除和找极值时O(1)
- 如果用
binary search tree
作为底层机制:- 元素的插入和查找极值是
O(logN)
。 - 但是缺点是:
binary search tree
对输入元素有要求,而且代码比较难以实现
- 元素的插入和查找极值是
- 而
binary heap
的复杂度就是在list
和binary search tree
中间
所谓的binary heap
就是一种complete binary tree
(完全二叉树)。即:
- 完全二叉树除了最底层的叶节点之外,是填满的
- 而最底层的叶节点从左到右有不会有空隙
由于完全二叉树整棵树内没有任何节点漏洞,所以我们可以用array来存储所有的节点。
- 假如这里用一个小技巧,将array的
#0
元素保留(或者设为无限大值或者无限小值),那么当完全二叉树中某个节点位于array的i
处时- 其左节点一定在
2i
处 - 其右节点一定在
2i+1
处 - 其父节点一定在
i/2
处
- 其左节点一定在
- 这样编程就很容易写了。(STL并没有用这种技巧)
这种以array表示tree的方式,我们叫做隐式表示法
也就是说,我们只需要一个array和一组heap算法(用来插入元素、删除元素、取极值、将某一组数组成一个heap)。但是array没有办法动态改变大小,而heap却需要这个功能。因此,以vector代替array更好。
根据元素排列方式,heap可以分为两种
max-heap
:每个节点的key >= 子节点的key,因此其最大值在根节点,总是位于vector的头部min-heap
:每个节点的key <= 子节点的key,因此其最小值在根节点,总是位于vector的头部
STL中提供的式max-heap
heap算法
make_heap算法
make_heap用来将一段现有的数据转换为一个heap。
示例
#include <iostream>
#include <list>
#include <stack>
#include <queue>
#include <algorithm>
int main()
{
int a[9] = {0, 1, 2, 3, 4, 5, 9, 3, 5};
std::vector<int> vec(a, a+9);
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "\n";
std::make_heap(vec.begin(), vec.end());
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "\n";
}
实现
push_heap算法
- 为了满足完全二叉树的条件,新加入的元素一定要放到最下面一层作为叶子节点,并填补从左到右的第一个空格,也就是把新元素插入到底层vector的end()处
- 但是新元素不一定适合end()处
- 如果不满足max-heap的条件(每个节点的键值都大于等于其子节点的键值),那么需要指向[上溯]行为:将新节点与父节点比较,如果其key比父节点大,那么就父子对换位置。一直上溯,直到不需要对换或者到达根节点为止
- 如果不满足max-heap的条件(每个节点的键值都大于等于其子节点的键值),那么需要指向[上溯]行为:将新节点与父节点比较,如果其key比父节点大,那么就父子对换位置。一直上溯,直到不需要对换或者到达根节点为止
示例
#include <iostream>
#include <list>
#include <stack>
#include <queue>
#include <algorithm>
int main()
{
int a[9] = {0, 1, 2, 3, 4, 5, 9, 3, 5};
std::vector<int> vec(a, a+9);
std::make_heap(vec.begin(), vec.end());
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "\n";
vec.push_back(7);
std::make_heap(vec.begin(), vec.end());
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "\n";
}
实现
push_heap
算法接受两个迭代器,用来表现一个heap底部容器(vector)的头尾,并且新元素已经插入到底部容器的最尾端。如果不符合这两个条件,那么push_heap的指向结果不可预期。
pop_heap算法
- pop_heap取出堆顶数据,因为是max-heap,那么其堆顶数据为最大值。pop操作拿走根节点之后。为了满足完全二叉树的条件,不能直接取走,否则会出现空洞
我们稍微改变一下思路,如下图。我们把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。这就是从上往下的堆化方法
测试
#include <iostream>
#include <list>
#include <stack>
#include <queue>
#include <algorithm>
int main()
{
int a[9] = {0, 1, 2, 3, 4, 5, 9, 3, 5};
std::vector<int> vec(a, a+9);
std::make_heap(vec.begin(), vec.end() - 1);
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "; size = " <<vec.size() <<"\n";
std::pop_heap(vec.begin(), vec.end());
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "; size = " <<vec.size() <<"\n";
}
实现
pop_heap
算法接受两个迭代器,用来表现一个heap底部容器(vector)的头尾。如果不符合这两个条件,那么push_heap的指向结果不可预期。
注意,pop_heap之后,最大元素只是被放置到底部容器的最尾端,尚未被拿走。
- 如果要取其值,可以用底部容器vector提供的back()操作函数。
- 如果要移除它,可以用底部容器vector提供的pop_back()操作函数。
sort_heap
原理
既然每次pop_heap可以获得heap中键值最大的元素,如果持续对整个heap做pop_heap操作,每次将操作范围从后往前缩减一个元素(pop_heap会把键值最大的元素放到底部容器的最尾端),当整个持续被指向时,我们就有了一个递增序列
实现
sort_heap
算法接受两个迭代器,用来表现一个heap底部容器(vector)的头尾。如果不符合这两个条件,那么sort_heap的指向结果不可预期。注意,排序之后,原来的heap就不是一个合法的heap了
示例
#include <iostream>
#include <list>
#include <stack>
#include <queue>
#include <algorithm>
int main()
{
int a[9] = {0, 1, 2, 3, 4, 5, 9, 3, 5};
std::vector<int> vec(a, a+9);
std::make_heap(vec.begin(), vec.end() - 1);
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "; size = " <<vec.size() <<"\n";
std::sort_heap(vec.begin(), vec.end());
for(auto i : vec){
std::cout << i << "\t";
}
std::cout << "; size = " <<vec.size() <<"\n";
}
heap没有迭代器
heap的所有元素都必须遵循特别的(complete binary tree)的排列规则,所以heap不提供遍历功能,也不提供迭代器
指定array作为底层容器
#include <iostream>
#include <list>
#include <stack>
#include <queue>
#include <algorithm>
int main()
{
int a[9] = {0, 1, 2, 3, 4, 5, 9, 3, 5};
std::make_heap(a, a + 9);
for(auto i : a){
std::cout << i << "\t";
}
std::cout << "; " <<"\n";
std::sort_heap(a, a + 9);
for(auto i : a){
std::cout << i << "\t";
}
std::cout << "; " <<"\n";
std::make_heap(a, a + 9);
for(auto i : a){
std::cout << i << "\t";
}
std::cout << "; " <<"\n";
std::pop_heap(a, a + 9);
std::cout << a[8] << "; " <<"\n";
}