学习笔记:堆
堆
性质
从二叉堆的结构说起,它是一棵二叉树,并且是完全二叉树,每个结点中存有一个元素(或者说,有个权值)。
堆性质:父亲的权值不小于儿子的权值(大根堆)。同样的,我们可以定义小根堆。本文以大根堆为例。
由堆性质,树根存的是最大值(getmax 操作就解决了)。
操作
插入
插入操作是指向二叉堆中插入一个元素,要保证插入后也是一棵完全二叉树。
最简单的方法就是,最下一层最右边的叶子之后插入。
如果最下一层已满,就新增一层。
插入之后可能会不满足堆性质?
向上调整:如果这个结点的权值大于它父亲的权值,就交换,重复此过程直到不满足或者到根。
可以证明,插入之后向上调整后,没有其他结点会不满足堆性质。
向上调整的时间复杂度是 $O(\log n)$ 的。
void insert(int x){
cnt++;heap[cnt] = x;
int now = cnt, nxt;
while(now >= 0){
nxt = now >> 1;
if(heap[nxt] > heap[now])swap(heap[nxt], heap[now]);
else break;
now = nxt;
}
}
这里再附上一张图方便理解:
删除
删除操作指删除堆中最大的元素,即删除根结点。
但是如果直接删除,则变成了两个堆,难以处理。
所以不妨考虑插入操作的逆过程,设法将根结点移到最后一个结点,然后直接删掉。
然而实际上不好做,我们通常采用的方法是,把根结点和最后一个结点直接交换。
于是直接删掉(在最后一个结点处的)根结点,但是新的根结点可能不满足堆性质……
向下调整:在该结点的儿子中,找一个最大的,与该结点交换,重复此过程直到底层。
可以证明,删除并向下调整后,没有其他结点不满足堆性质。
时间复杂度 $O(\log n)$。
void del(){
swap(heap[1], heap[cnt]);cnt--;
int now = 1, nxt;
while((now << 1) <= cnt){
nxt = now << 1;
if(nxt + 1 <= cnt && heap[nxt + 1] < heap[nxt])nxt++;
if(heap[nxt] < heap[now])swap(heap[nxt], heap[now]);
else break;
now = nxt;
}
}
查询
直接输出堆顶元素即可。
int query(){return heap[1];}
实现
可以考虑用一个序列来维护一个堆,其中第 $i$ 个点的左右儿子分别为第 $2i$ 和 $2i+1$ 个点。
代码
#include <iostream>
#define MAXN 1000005
using namespace std;
int n, op, x, heap[MAXN], cnt;
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
void write(int x){
if(x < 0){putchar('-');x = -x;}
if(x >= 10)write(x / 10);
putchar(x % 10 ^ 48);
}
void insert(int x){
cnt++;heap[cnt] = x;
int now = cnt, nxt;
while(now >= 0){
nxt = now >> 1;
if(heap[nxt] > heap[now])swap(heap[nxt], heap[now]);
else break;
now = nxt;
}
}
int query(){return heap[1];}
void del(){
swap(heap[1], heap[cnt]);cnt--;
int now = 1, nxt;
while((now << 1) <= cnt){
nxt = now << 1;
if(nxt + 1 <= cnt && heap[nxt + 1] < heap[nxt])nxt++;
if(heap[nxt] < heap[now])swap(heap[nxt], heap[now]);
else break;
now = nxt;
}
}
int main(){
n = read();
for(int i = 1 ; i <= n ; i ++){
op = read();
switch(op){
case 1:x = read();insert(x);break;
case 2:write(query());putchar('\n');break;
case 3:del();break;
default:cout << "jtskdjfkxqsvivo50wdnc" << endl;break;
}
}
return 0;
}
不过这边更加推荐直接使用 STL 中的优先队列(主要是为了偷懒)
#include <iostream>
#define MAXN 1000005
using namespace std;
int n, op, x;
priority_queue <int> q;
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
void write(int x){
if(x < 0){putchar('-');x = -x;}
if(x >= 10)write(x / 10);
putchar(x % 10 ^ 48);
}
int main(){
n = read();
for(int i = 1 ; i <= n ; i ++){
op = read();
switch(op){
case 1:x = read();q.push(~x);break;
case 2:write(~q.top());putchar('\n');break;
case 3:q.pop();break;
default:cout << "jtskdjfkxqsvivo50wdnc" << endl;break;
}
}
return 0;
}