堆的定义
堆是一颗完全二叉树。
完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。——百度
举(地)个(狱)栗(绘)子(图):
就像这样,左边是完全二叉树,而右边不是。
我们可以认为:一颗完全二叉树相当于一颗满二叉树延伸出了一些叶子节点。
作为一个特殊的数据结构,堆中某个节点的值总是不大于或不小于其父节点的值。我们根据根结点的大小来对堆进行分类:根节点最小叫小根堆(最小堆),最大叫大根堆(最大堆)。
堆的操作
作为一个数据结构,堆当然支持各种各样的操作:
就像这样:
好好我们言归正传
堆支持建立,调整,插入,弹出(删除)四种操作。
下面均展示小根堆的做法。
什么,你就要大根堆?
operator是个好东西
1. 堆调整
有向上调整和向下调整
说白了就是把不符合堆性质的结点通过交换使其符合堆的性质
当我们更新时,让最小的元素当父亲,比它大元素当儿子
void downdate(int p){
while(p<<1<=Size){
p<<=1;
if(a[p]>a[p+1]&&p<Size)p++;
if(a[p]<a[p>>1])swap(a[p],a[p>>1]);
else break;
}
}
void update(int p){
while(p>1){
if(a[p]<a[p>>1])swap(a[p],a[p>>1]);
else break;
p>>=1;
}
}
它们的时间复杂度在O(1)和O(log(N))之间。
下调(下沉)downdate
即将选定元素下调至适当的位置。
再(二次)举(地)个(狱)栗(绘)子(图):
我们要调整这个5,这个时候我们可以看到,作为一个小根堆,5>3,5显然得是3的儿子,所以把5和3的位置做一个交换——
接下来又因5>4,就交换5和4的位置
然后5就到了它应该在的位置啦~
特殊情况:
如果两个儿子都比它小呢?
我全都要
当然是把小的上调咯~因为是小根堆,父亲一定要小于这两个儿子
上调(上浮)update
即将选定元素上调至适当位置
就像这样——
-1这个重量级人物,一路过关斩将,坐上了根结点的王位~~~
2. 堆插入
让新的元素入堆
这个时候我们就需要向上更新
void push(int x){
Size++;
a[Size]=x;
update(Size);
}
3. 堆删除
删除堆顶元素
同时让剩余元素重新组成一个堆
void pop(){
if(Size==0)return;
a[1]=0;
swap(a[1],a[Size]);
Size--;
downdate(1);
}
4. 建堆
建堆有两种方式,你可以一个一个把元素插入堆,也可以一次性读完所有元素,然后让存储数组变成一个堆
void build(){
for(int i=1;i<=n;i++){
h.push(cun[x]);
}
}
//or
void build(){
for(int i=n/2;i>=1;i--)downdate(i);//叶子节点不下沉,所以从n/2开始
//用倒序是因为downdate是自上而下的,越在上面的节点越后面更新
}
它们的时间复杂度均为O(Nlog(N))。
优先队列
你们用堆跟我用优先队列有什么关系?——鲁迅
优先队列和堆非常相似(几乎能互相代替)。所以在竞赛的时候,c语言的选手经常会用STL的优先队列从而代替(碾压)手写堆(pascal党)。
定义
struct data{
int c,x,k;
};
bool opreator <(const data &a,const data &b){
return a.x<b.x;
}//大根堆
bool opreator <(const data &a,const data &b){
return a.x>b.x;
}//小根堆
priority_queue<int>q; //默认大根堆
priority_queue<int,vector<int>,greater<int> >;//由小到大递增,小根堆(greater<int>后的空格很重要!)
priority_queue<int,vector<int>,less<int> >;//由大到小递减,大根堆(less<int>后的空格很重要!)
priority_queue<data>q;//自定义结构体
操作
q.push(x);//插入x
q.top();//访问堆顶元素
q.pop();//删除堆顶元素
q.empty();//判断堆是否为空
堆的应用
1. 找最值
找最值肯定要建堆啊
建堆的时间复杂度O(Nlog(N))
直接扫描数组时间复杂度O(N)
乍一看建堆多此一举
但是当询问的次数多起来时:
建堆的时间复杂度O(Nlog(N))
直接扫描数组时间复杂度O(QN)
Q==N时
建堆的时间复杂度O(Nlog(N))
直接扫描数组时间复杂度O(N^2)
ん?堆竟成功反杀
如此高效率 的数据结构当然时常会与贪心挂钩:
Luogu P1090 合并果子
每次用堆取出两个最小的合并成一堆,再放进堆里,直到只剩一堆为止。
Luogu P2723 丑数
先将集合内的质数入堆,然后每次取最小的值乘集合里的质数。
然后你就得到了一份TLE的代码
这个时候我们发现这样做会出现重复入堆的情况(12=2x3x2=2x2x3)
为了避免这种情况,我们记录一下入堆时的乘数,下次入堆时只需要乘不小于上一个乘数的数就行了。
然后你就得到了一份MLE的代码
这个时候我们又双叒叕发现入堆过多的情况(每次都入K个是十分要命的)
解决它只需要部分地入堆,即乘上上一个乘数,或者先除以上一个乘数,再乘上第一个大于上一个乘数的数。
2. 排序
既然我们有了快排,堆的排序又有何用呢?
当然没有用
当然有用!
当遇到特殊数据(如有序序列)时,快排会退化成O(N^2)
这时堆排序可能就成为了最好的排序方法。
关于堆排序就不过多阐述了,直接贴链接,我个人觉得挺有帮助:
堆排序
竡冰先森 写于2019.7.31
2019.8.1 更新堆调整(注入灵魂)
2019.8.2 添加億点细节