第十三讲:heap&priority_queue深入探索
heap概述
heap不是STL容器组件,它是扮演priority queue
的助手。priority queue
允许用户以任何次序将任何元素推入容器中,
但取出时一定是从优先权最高的元素开始取。binary max heap
正具有这样的特性,适合作为priority queue
的底层机制。
priority queue
选用binary heap
来实现,它是一种完全二叉树
,也就是说除了最底层的叶子节点之外,是填满的,且
最底层的叶节点由左至右不能右空隙。因为整棵树没有任何节点漏洞,我们可以使用array来存储所有的节点。
binary heap是一颗完全二叉树,那么它就具备完全二叉树的特点:当某个节点位于array的i处,其左子节点必位于array的 2i处,其右子节点一定位于array的2i+1处,其父节点必定位于"i/2"处。
binary heap
使用一个array(保存数据)和一组heap算法(用来插入元素、删除元素、取极值)。这种使用array表述
tree的方式,被称为隐式表达
。
当然heap要能动态的改变大小,所以用vector存储数据会更好。
heap可以分为max-heap以及min-heap,前者每个节点的键值都大于或等于其子节点键值,后者的每个节点键值都小于或 等于其子节点键值。可以推出,max-heap的最大键值在根节点,min-heap的最小键值在根节点。
heap算法
push_heap算法
在很多书籍当中,通常通过一个上浮的操作来完成push_heap。其基本原理可见下图(假设新加入的元素是50):
1.将50插入到数组的末尾。
2.将其于父元素相比较,发现24小于50,交换这两个元素。上浮一次
3.继续上述操作直到找到一个合适的位置,也即其父元素大于50的位置,则上浮结束。
一个简易的c++实现:
|
|
pop_heap算法
pop操作是类似的:
1.将最后一个元素与第一个元素(根元素)交换。
2.删除最后一个元素。
3.将第一个元素/根元素下沉到一个合适的位置。注意下沉的时候是与子节点较大的那个元素交换。
一个简单的c++实现:
|
|
sort_heap算法
既然每次pop_heap
可获得heap
中键值最大的元素,如果持续对整个heap
做pop_heap
操作,
每次将操作范围从后向前缩减一个元素(因为pop_heap
会把键值最大的元素放在底部容器的最尾端),
当整个程序执行完毕时,我们便有了一个递增序列。下图所示的是sort_heap
的实际操演情况。
可以看到不断对heap进行pop操作,便可达到排序效果
一个简单的c++实现:
|
|
make_heap算法
这个算法是将一段现有的数据转化为一个heap,主要依据就是前面所说的隐式表达
。
heap没有迭代器
因为heap的结构是一个完全二叉树,类似于红黑树,我们都不能对它进行遍历,这样会造成heap结构的破坏,因此它也就没有迭代器。
priority_queue
priority_queue概述
priority_queue
是一个拥有权值观念的queue
,允许加入新元素,移除旧元素。但由于这是一个queue,所以只允许在底端
加入元素,在顶端取出元素。
priority_queue
带有权值观念,其内的元素按照元素的权值来排列(通常权值以实值表示)。默认权值最高者,排在最前面。
缺省状态下的priority_queue
利用一个max_heap
实现,由一个使用vector
表现的完全二叉树,结构如下:
priority_queue定义完整列表
前面说的缺省状态下就是默认的优先队列形式,底部容器为vector加上堆的处理规则。我们修改某些借口,就会形成不同的特性, 具有这种性质者,被称为配接器。
源码如下(GNU2.9):
|
|
priority_queue的用法
- 第一种:只定义一个参数,默认形式下,这里是一个关于字符串的大顶堆(按字符串的第一个字符排,最大的在第一位)
|
|
- 第二种:改变第三个模板参数,实现小顶堆。
|
|
-
第三种:当第一个模板参数是自定义数据时,就需要自定义一个比较方式,可以是一个函数对象,可以是一个函数 (一般采用lamda表达式,因为它需要使用当前函数的传入参数,但又不能在函数体内定义函数,故采用这样的方式)。
- 采用函数对象(仿函数)作为第三个模板参数。指定第三个模板参数是cmp类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
struct Node{//也可以是class int x,y; Node(int a=0, int b=0): x(a), y(b) {} }; struct cmp{//也可以是class bool operator()(Node a, Node b){ if(a.x == b.x) return a.y>b.y; return a.x>b.x; } }; int main(){ priority_queue<Node, vector<Node>, cmp>p; for(int i=0; i<10; ++i) p.push(Node(rand(), rand())); while(!p.empty()){ cout<<p.top().x<<' '<<p.top().y<<endl; p.pop(); } return 0; }
- 实现一个第三个参数为函数指针的
priority_queue
,重点是decltype(cmp)和 pq(cmp)的使用方式。 个人理解是声明cmp函数(实际是以指针)为一个指针类,从而作为模板参数,并在命名的时候使用pq(cmp):
1 2 3 4 5 6 7
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k){ auto cmp=[&](const pair<int, int>& lhs, const pair<int, int>& rhs){ return nums1[lhs.first] + nums2[lhs.second] > nums1[rhs.first] + nums2[rhs.second]; //当后面进来下标pair对rhs下:nums1[rhs.first] + nums2[rhs.second]小时,将这个rhs和lhs置换,实现小顶堆 }; priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp); }
注意因为priority_queue是由底层容器+heap规则实现的,所以priority_queue也没有迭代器。
最后修改于 2022-06-28
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。