关于堆的那些事儿

堆的定义

堆是一颗完全二叉树。

完全二叉树是由满二叉树而引出来的。对于深度为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 添加億点细节

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值