本文偏重于如何手写堆, 还介绍优先队列的一点基本知识
堆的基本介绍
两种实现方式
- 手写堆
- 利用C++中的优先队列
相关概念
- 堆:一种支持插入,删除,查询最值的数据结构,是一颗完全二叉树,实际上就是一个优先队列priority_queue
- 大根堆:树中的任意一个节点的权值都小于等于其父节点(根节点是最大值)
小根堆:树中的任意一个节点的权值都大于等于其父节点(根节点是最小值)
手写堆
存储
- 一维数组存储,1号点为根节点
- x的左儿子是2x, x的右儿子是2x + 1
- 我们用size表示当前堆的大小,heap[]表示堆。
基本操作
- 插入一个数
- 求集合中最小值
- 删除最小值
- 删除任意一个元素
- 修改任意一个元素
- down(x) : down函数,将x向下调整,找到x的位置,保证堆的有序性
- up(x): up函数将x向上调整,找到x的位置,保证堆的有序性
插入操作
- 整个堆的最后一个位置插入x
- 不断把该数往上移
heap[++ size] = x;
up(size);
求当前堆中最小值
heap[1]
删除最小值
- 把最后一个元素覆盖到堆顶去
- size–;
- down()
heap[1] = heap[size];
size --;
down(1);
删除任意一个元素
与删除最小值相似
可以分情况讨论:
- 如heap[k]变大了,就应该down
- 变小了: up
不讨论: 不比较,直接down(k), up(k),
这俩个函数虽然都写上了,但实际上只会执行一个,
heap[k] = heap[size];
size --;
down(k);
up(k);
修改任意一个元素
heap[k] = x;
down(k);
up(k);
一道模板题
意在展示down函数的实现
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int h[N];
int siz, n, m;
void down(int u){
int t = u;
if(u * 2 <= siz && h[u * 2] < h[t]) t = u * 2;
if(u * 2 + 1 <= siz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if(t != u){
swap(h[u], h[t]);
down(t);
}
}
int main(void){
cin >> n >> m;
for(int i = 1; i <= n; i ++) scanf("%d", &h[i]);
siz = n;
for(int i = n / 2; i ; i --) down(i);
while(m --){
printf("%d ", h[1]);
h[1] = h[siz];
siz --;
down(1);
}
return 0;
}
注意细节
down操作时,开始创建堆的时候,元素是随机插入的,所以不能从根节点开始down。我们从 n / 2开始【下标最大(因为要往上遍历)】,往上递归处理,我们可以得到最小的堆顶元素, h[1]
例题2:模拟堆
题目描述
题目链接
维护一个集合,初始时集合为空,支持如下几种操作:
“I x”,插入一个数x;
“PM”,输出当前集合中的最小值;
“DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);
“D k”,删除第k个插入的数;
“C k x”,修改第k个插入的数,将其变为x;
现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。
输入格式
第一行包含整数N。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”PM”,”DM”,”D k”或”C k x”中的一种。
输出格式
对于每个输出指令“PM”,输出一个结果,表示当前集合中的最小值。
每个结果占一行。
数据范围
1≤N≤10^5
−10^9≤x≤10^9
数据保证合法。
题目分析
- 我们要删除修改插入的第k个数,我们得开额外数组记录
- 维护一个映射关系:插入点和堆里元素的一个映射。ph[k] = j存储的第k个插入数的在堆里的下标; hp[j] = k, 两者要一一对应,我们得用两个数组来存。
- strcmp 比较函数,如果相等返回0
#include <algorithm>
#include <iostream>
#include <string.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, h[N], ph[N], hp[N], siz;
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 <= siz && h[u * 2] < h[t]) t = u * 2;
if(u * 2 + 1 <= siz && 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 / 2] > h[u]){
heap_swap(u / 2, u);
u /= 2;
}
}
int main(void){
scanf("%d", &n);
m = 0; //存储当前插入的第几个数
while(n --){
char op[5];
int k, x;
scanf("%s", op);
if(!strcmp(op, "I")){
scanf("%d", &k);
m ++;
siz ++;
ph[m] = siz;
hp[siz] = m;
h[siz] = k;
up(siz);
}
else if(!strcmp(op, "PM")) printf("%d\n", h[1]);
else if(!strcmp(op, "DM")){
heap_swap(1, siz);
siz --;
down(1);
}
else if(!strcmp(op, "D")){
scanf("%d", &k);
k = ph[k];
heap_swap(k, siz);
siz --;
down(k);
up(k);
}
else{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
down(k);
up(k);
}
}
return 0;
}
优先队列基本介绍
- 能自动排序的队列 默认降序从大到小。
默认声明方式:priority_queue <int> ans;
声明一个名为ans的整形的优先队列 - 注意一个操作:
q.push(item)
在基于优先级的适当位置插入新元素 - 模板声明方式:
priority_queue<Type, Container, Functional>
其中Type 为数据类型, Container 为保存数据的容器,Functional 为元素比较方式。
priority_queue<int,vector<int>,less<int> >q; 降序 从大到小
priority_queue<int,vector<int>,greater<int> >q; 升序 从小到大