堆的本质是一棵完全二叉树,在计算机体系中占据非常重要的地位,面试也经常会遇到。在C++中,priority_queue就是堆,在算法竞赛中,常常用堆优化最短路(迪杰斯特拉)算法。
堆的基本知识
- 堆分为大根堆和小根堆,分别是指root在当前树结构中值最大(最小)。下述讲解均以小根堆为例。
- 堆是完全二叉树,完全二叉树满足的性质它都满足:i节点的左子节点为2i,右子节点为2i+1
堆的基本功能
- 插入一个数
- 求集合当中的最小值
- 删除最小值
- 删除任意一个元素
- 删除任意一个元素
堆的实现
堆中的核心操作只有两个:down(),up()。分别表示将某个数向下/向上调整归位。上述5个基本操作都可以用down和up组合完成。
//向下调整归位
void down(int u){
int t = u;
if(2*u <= si && h[2*u]<h[t]) t = 2*u;
if(2*u+1 <= si && h[2*u+1]<h[t]) t = 2*u+1;
if(t != u) {
swap(h[t],h[u]);
down(t);
}
}
//向上调整归位
void up(int u) {
while(u/2 && h[u]<h[u/2]){
swap(h[u],h[u/2]);
u /= 2;
}
}
- 插入一个数
heap[++ size] = x;
up[(size);
- 求集合当中的最小值
heap[1]
- 删除最小值:原理:先用最末尾的节点覆盖root节点,让size–,相当于去掉末尾节点,再从头至尾把heap[1]操作down一遍。
heap[1] = heap[size --];
down(1);
- 删除任意一个元素:原理:类似于删除最小值,先用末尾节点覆盖该节点,让size–相当于去掉末尾节点。不同的是,由于不知道修改后是更大了还是更小了,为写代码方便起见,直接一遍down,一遍up。
heap[k] = heap[size --];
down(k);up(k);
- 修改任意一个元素:原理:类似于删除任意一个元素,一遍down,一遍up。不同的是,不用覆盖,不用size–。直接修改heap[k]即可。
heap[k] = x;
down(k);up(k);
堆的另外一些操作
- 建堆(依次插入O(nlogn),O(n))
- 输出前k小(大)值
//1.建堆:
//依次插入O(nlogn)
for(int i = 1;i <= n;i++) {
int x;
scanf("%d",&x);
h[++si] = x;
up(si);
}
//O(n)从n/2开始down,相当于从完全二叉树的倒数第二层开始down。
for (int i = n / 2; i; i -- ) down(i);
//2.由删除最小值操作得到
for(int i = 1;i<=m;i++) {
printf("%d ",h[1]);
h[1] = h[si --];
down(1);
}
堆的模板:
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;
// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
作者:yxc
链接:https://www.acwing.com/blog/content/404/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
练习题
#include<iostream>
using namespace std;
int n,m,si,h[100010];
void down(int u){
int t = u;
if(2*u <= si && h[2*u]<h[t]) t = 2*u;
if(2*u+1 <= si && h[2*u+1]<h[t]) t = 2*u+1;
if(t != u) {
swap(h[t],h[u]);
down(t);
}
}
void up(int u) {
while(u/2 && h[u]<h[u/2]){
swap(h[u],h[u/2]);
u /= 2;
}
}
int main()
{
scanf("%d %d",&n,&m);
for(int i = 1;i <= n;i++) {
int x;
scanf("%d",&x);
h[++si] = x;
up(si);
}
for(int i = 1;i<=m;i++) {
printf("%d ",h[1]);
h[1] = h[si --];
down(1);
}
return 0;
}
#include<iostream>
using namespace std;
int n,m,si,h[100010],ph[100010],hp[100010],idx ;
char op[5];
void heap_swap(int a,int b) {
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a],hp[b]);
swap(h[a],h[b]);
}
void down(int u){
int t = u;
if(2*u <= si && h[2*u]<h[t]) t = 2*u;
if(2*u+1 <= si && h[2*u+1]<h[t]) t = 2*u+1;
if(t != u) {
heap_swap(t,u);
down(t);
}
}
void up(int u) {
while(u/2 && h[u]<h[u/2]){
heap_swap(u,u/2);
u /= 2;
}
}
int main()
{
scanf("%d",&n);
m = n;
while(n --) {
scanf("%s",op);
if(op[0] == 'P') {
printf("%d\n",h[1]);
}
else if(op[0] == 'D' && op[1] == 'M') {
//注意不能按原来的末尾直接覆盖的方法做,那样会在ph,hp的维护上存在缺陷,直接利用heap_swap交换末尾即可。
heap_swap(1,si--);
down(1);
}else if(op[0] == 'I') {
int x;
scanf("%d",&x);
idx ++;
h[++ si] = x;
ph[idx] = si;
hp[si] = idx;
up(si);
}else if(op[0] == 'D' && !op[1]) {
int k;
scanf("%d",&k);
int index = ph[k];
//注意不能按原来的末尾直接覆盖的方法做,那样会在ph,hp的维护上存在缺陷,直接利用heap_swap交换末尾即可。
heap_swap(index,si--);
down(index);
up(index);
}else {
int k,x;
scanf("%d %d",&k,&x);
int index = ph[k];
h[index] = x;
down(index);
up(index);
}
}
return 0;
}
priority_queue的用法
- 定义:默认定义为大根堆,怎么定义小根堆?
//压入操作时,直接压入-x,而非x。利用大根堆的性质,处理逻辑上的小根堆。
//直接定义小根堆
priority_queue<int,vector<int>,greater<int>> heap;
- 其他操作与queue相同
用于优化迪杰斯特拉算法的改进部分:
需要维护两个新的数组,分别表示ph[k]存储第k个插入的点在堆中的位置
,hp[k]存储堆中下标是k的点是第几个插入的.
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}