堆的功能:
1.插入一个数
2.求集合中的最小值
3.删除最小值
4.删除任意一个元素(间接实现)
5.修改任意一个元素(间接实现)
堆:一个完全二叉树
小根堆:每个节点的值都小于等于左右儿子,根结点就是整个数据结构里的最小值
堆的存储:一维数组heap[]来存储,1号下标存储的是根结点,节点x的左儿子是2x,节点x的右儿子是2x+1,为什么下标不从0开始,因为0左儿子还是0,不太方便,虽然也可以办到
操作:
//往下调整,注意这里的x是下标
down(x){
}
往下调整是什么意思呢?
这是一个小根堆,如果我们把根节点的位置从1改为6,如下图:
如果要实现恢复小根堆的话,就要往下调整,先找到该节点和它左右儿子中的最小值,然后进行交换
然后发现还是不对后,再进行一次上述操作
当发现不能继续操作后,就发现满足小根堆了
如果我们把某一个点的值变大了,那么我们就应该把它往下移
同理,往上移动就是把值变小了的节点往上移,如果他的父节点的值比他大,就进行交换
//往上调整,x是下标
up(x){
}
如何实现上述功能?
插入一个数:因为堆是一棵完全二叉树,所以插入的时候我们是在一维数组的最后插入,就是heap[++size]=x;再不断上移,up(size);
求集合中的最小值:heap[1];
删除一个最小值:意思就是把第一个元素删掉,把整个堆的最后一个元素覆盖到堆顶元素heap[1]=heap[size];,覆盖完之后,再size–;down(1);就可以了。这样做的意义就是我们用的是一维数组来存储,删除数组中的第一个元素比较困难,而删除数组中最后一个元素就比较方便。
删除任意一个数:和删除根结点类似,先覆盖heap[k]=heap[size];size–;再分析一下k这个位置的节点的值是变大还是变小了,如果变大了,就down(k);,变小了,就up(k);或者直接先down(k)一遍,再up(k)一遍,效果一样。
修改一个元素:同理,修改后heap[k]=x;先down(k);一遍,再up(k);一遍就可以了
我们再来看一个堆排序的题:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n, m;
//因为用size,VS会报错,所以改为cnt
int heap[N], cnt;
//往下调整
void down(int u) {
int t = u;
if (u * 2 <= cnt && heap[u * 2] < heap[t])t = u * 2;
//这里要用if而不是else if
//因为除了判断父节点和子节点的大小还要判断两个子节点之间的大小
if (u * 2 + 1 <= cnt && heap[u * 2 + 1] < heap[t])t = u * 2 + 1;
if (u != t) {
swap(heap[u], heap[t]);
down(t);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)scanf("%d", &heap[i]);
cnt = n;
//n/2就是树的倒数第二层,可以直接从这里开始进行down操作
//保证了每个节点的左右子树都是调整好了的
for (int i = n / 2; i; i--)down(i);
while (m--) {
printf("%d ", heap[1]);
heap[1] = heap[cnt--];
down(1);
}
}
堆排序的时间复杂度是跟数的高度成正比的
再来一道涉及五个操作的题
这个题比较麻烦的就是第四五种操作,操作前需要先找到第k个插入的数,相对较麻烦,所以我们开了两个额外的数组ph[],hp[]来存第k个插入的数的位置,和对应下标的节点是第几个插入的数
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N = 100010;
//ph[k]存的是第k个插入的点的下标
//hp[k]存的是下标为k的点是第几个插入的数
int h[N], ph[N], hp[N], cnt;
//函数实现交换ph[k],hp[k],h[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]);
}
void down(int u) {
int t = u;
if (u * 2 <= cnt && h[u * 2] < h[t])t = u * 2;
if (u * 2 + 1 <= cnt && 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;
}
}
int main() {
int n, m = 0;
scanf("%d", &n);
while (n--) {
char op[5];
int k, x;
scanf("%s", op);
//插入一个数
if (!strcmp(op, "I")) {
scanf("%d", &x);
ph[++m] = ++cnt, hp[cnt] = m;
h[cnt] = x;
up(cnt);
}
//输出当前集合中的最小值
else if (!strcmp(op, "PM"))printf("%d\n", h[1]);
//删除当前集合中的最小值
else if (!strcmp(op, "DM")) {
heap_swap(1, cnt--);
down(1);
}
//删除第k个插入的数
else if (!strcmp(op, "D")) {
scanf("%d", &k);
k = ph[k];
heap_swap(k, cnt--);
up(k);
down(k);
}
//修改第k个插入的数
else {
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
up(k);
down(k);
}
}
}
我们要记录第k个插入的点对应的下标是什么,直接用一个ph[]数组就可以了,为什么还要用一个hp呢?我们来进行如下操作试验一下
比如进行如下操作:
6
I -11
I -10
D 1
C 2 8
I 6
I 3
进行完第一二步就是上面的样子,接下来D 1
C 2 8
I 6
这里只进行了插入操作,还没进行插入后的up(2)操作,接下来,就会展示为什么除了用一个ph外还有另外开一个hp数组,如果我们没开hp数组,没进行hp数组的维护,并且在heap_swap函数第一条语句中仅仅写swap(ph[a],ph[b]);那么会出现下面这张图的效果
很明显6并不是第一个插入的数,8也不是第二个插入的数
而如果我们开了hp数组并维护,写的是swap(ph[hp[a]],ph[hp[b]])的话,那么就会出现正确的结果
这就是为什么需要开两个数组的原因