本文是高级数据结构系列第1篇。
引入
维护一个数据结构,支持以下操作:
1、插入一个元素;
2、询问所插入元素中的最优值;
3、删除最优的元素。
一般情况下,元素就是一个32位整数,最优元素是最大或最小的整数。
朴素算法:
用类似插入排序的思想,把所有的数插入合适的位置,询问时输出a[1],删除时将所有数前移一位。
不足:时间复杂度O(nq),超时。
更高级的算法:
使用诸如平衡树之类的二叉查找树或01trie树(如果有错,欢迎dalao指正),进行查找、删除。
不足:代码复杂,常数大。
介绍
堆是一棵完全二叉树。
完全二叉树定义如下:
1、这是一棵二叉树。
2、除去深度最大的一层,其余几层构成满二叉树。
3、最后一层的所有节点都连续集中在最左边。
因此,它有如下性质:
1、按广搜遍历给节点编号后i号节点的父亲编号是i/2,左孩子编号为i*2,右孩子编号为i*2+1。
2、有n个节点的完全二叉树最大深度为log(n)。
而堆相较于一般完全二叉树有有如下性质:
每个父节点所存的值比它两个孩子更优。
为方便区分,以下称堆顶元素最小的堆为小根堆,反之为大根堆。
实现
堆可以用一个一维数组模拟,数组的下标表示节点编号,值表示节点所存的数值。其定义一般如下:
struct heap{
int a[maxn],top;
//接下来定义各种操作函数
//为方便说明,设这个堆是小根堆
}
其中top指堆中数字的个数。
接着达成开头所述的要求。
询问插入的元素中最优值就很简单了。
int getbest(){
return a[1];
}
接着是插入元素。
我们先在数组最后插入这个值,但这有可能打破堆的性质。
为使其重新符合这个性质,需要对其做一个操作:上调。
void up(int cur){
int i;
while (cur>1){//确保当前这个节点不是根节点,如果是根便无需上调
i=cur>>1;//等价于i=k/2
if (a[i]>a[cur]){//如果当前这个节点的值比父节点的值更小
swap(a[i],a[cur]);//把当前节点与其父节点对调
//这样就能保证父节点比其两个孩子的值更小
cur=i;//原来的父节点变成当前节点
}
else return;
}
}
void insert(int x){
a[++top]=x;
up(top);
}
当然,up上调还有更简单的写法:
void up(int cur){
while (cur>1&&a[cur>>1]>a[cur])
swap(a[cur>>1],a[cur]),cur>>=1;
}
接下来处理删除操作。
乍眼一看感觉似乎很繁,但是我们可以换一种思路:如果我们让堆的最后一个元素放到堆顶,然后对堆顶元素进行下调,同样可以完成任务。
int Min(int x,int y){
if (y>top) return x;
if (a[x]<a[y]) return x;
return y;
}
void down(){
int k=1,i;
while (k<<1<=top){//当k还有孩子,即不是叶节点,才可下调
i=Min(k<<1,k<<1|1);//k<<1=k*2,k<<1|1=k*2+1
//i表示k两个孩子中(如果有的话)值最小的孩子编号
if (a[i]<a[k]){//如果两个孩子中最小的一个比它小
swap(a[i],a[k]);
k=i;
}
else return;
}
}
void del(){
a[1]=a[top];
a[top--]=0;
down();
}
Min函数可以用 ? : 简化,在此不再赘述。
至此,三种操作全部完成。
复杂度分析
- 提取最优元素为O(1)
- 插入、删除与树最大深度有关,为O(log(n))
因此,总复杂度为O(log(n))。
完整的包:
struct heap{
int top,f[maxn];
heap_s (){
top=0;
memset(f,0,sizeof(f));
}
void up(int x){
while (x>1&&f[x>>1]>f[x])
swap(f[x>>1],f[x]),x>>=1;
}
int mini(int a,int b){
return b>top||f[b]>f[a]?a:b;
}
void down(){
int i,k=1;
while (k<<1<=top){
i=mini(k<<1,(k<<1)+1);
if (f[i]<f[k]) swap(f[i],f[k]),k=i;
else break;
}
}
int getop(){
return f[1];
}
void ins(int x){
f[++top]=x;
up(top);
}
void del(){
f[1]=f[top];
f[top--]=0;
down();
}
};
堆排序
一般的思想便是把数组中的元素一个个扔进堆里,再一个个取出。
但有一种简化方式。
for (i=n/2;i;--i)
down(i);//带有参数的down作用是对编号为i的节点下调。
从最后一个不是叶子的节点下调,这样就能满足堆的性质。
不错的例题
合并果子:直接用小根堆维护即可。
黑匣子:用两个堆分别维护大于等于中位数的数与小于中位数的数。