堆——神奇的优先队列

本文详细介绍了堆排序的原理和实现方法,包括如何建立最小堆、最大堆,以及如何插入和删除元素。堆排序的时间复杂度为O(NlogN),并讨论了其在优先队列和Dijkstra算法中的应用。此外,还提到了堆在求解数列中第K大或第K小数的问题上的高效解决方案。
摘要由CSDN通过智能技术生成

堆就是一种特殊的完全二叉树。

向上、向下调整:

在这里插入图片描述
有没有发现这棵二叉树有什么特点?就是所有的父结点都比子结点要小(注意:圆圈里面的数是值,圆圈上面的数是这个结点的编号)。符合这样特点的完全二叉树我们称为最小堆反之,如果所有的父结点都比子结点要大,这样的完全二叉树称为最大堆。
假如有14个数,分别是99、5、36、7、22、17、46、12、2、19、25、28、1和92 。现在我们需要删去其中的最小数并且增加一个23,再求这14个数中的最小数。
首先我们把这14个数按照最小堆的要求(就是所有的父结点都比子结点要小)放入一棵完全二叉树,就像下面这棵树一样。
在这里插入图片描述
我们将这个完全二叉树存入一个一维数组中。
在这里插入图片描述
很显然最小的数就在堆顶,假设储存这个堆的数组叫h,最小的数就是h[1] 。接下来我们删除堆顶部的数,再将新增加的23放到堆顶。再向下调整,与它的两个儿子2和5比较,选择较小的一个与它交换。一直向下调整直到符合最小堆的特性为止。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
综上所述,当新增加一个数被放到堆顶时,如果此时不符合最小堆的特性,则需要将这个数向下调整,直到找到合适的位置,使其重新符合最小堆的特性。
在这里插入图片描述
向下调整的代码如下:

void siftdown(int i){//传入一个需要向下调整的结点编号i,这里传入1,即从堆顶开始向下调整 
	int t, flag = 0;//flag用来标记是否需要继续向下调整 
		//当i结点有儿子(其实是至少有左儿子的情况下)并且有需要继续调整的时候,循环就执行 	
	while(i * 2 <= n && flag == 0){
		//首先判断它和左儿子的关系,并用t记录较小的结点编号 
		if(h[i] > h[i * 2]) t = i * 2;
		else t = i;
		//如果它有右儿子,再对右儿子进行讨论 
		if(i * 2 + 1 <= n){
			//如果右儿子的值更小,更新较小的结点编号 
			if(h[t] > h[i * 2 + 1]) 
				t = i * 2 + 1;
		}
		//如果发现最小的结点编号不是自己,说明子结点中有比父结点更小的 
		if(t != i){
			swap(t, i);//交换它们 
			i = t;//更新i为刚才与它交换的儿子的编号,便于接下来继续向下调整 
		}
		else flag = 1;//否则说明当前的父结点已经比两个子结点都要小了,不需要再进行调整了 
	}
	return ; 
} 

如果只是想新增一个值,而不是删除最小值,又该如何操作呢(即如何在原有的堆上直接插入一个新元素呢?)?只需要将新元素插入到末尾,再根据情况判断新元素是否需要上移,直到满足堆的特性为止。例如现在我们要新增一个数3.
在这里插入图片描述
在这里插入图片描述
向上调整代码:

//向上调整代码:
void siftup(int i){//传入需要向上调整的结点编号 
	int flag = 0;//用来标记是否需要向上调整 
	if(i == 1) return ;//如果是堆顶就返回,不用继续向上调整了。
	//不在堆顶,并且当前结点i的值比父结点的值小的时候就继续向上调整 
	while(i != 1 && flag == 0){
		//判断是否比父结点小 
		if(h[i] < h[i / 2])
			swap(i, i / 2);//如果比父结点小那么交换它和它爸爸的位置 
		else flag = 1;//表示已经不需要调整,当前结点的值比它父结点的值要大 
		i = i / 2;//这句话很重要,更新i的值为它父结点的值便于继续向上调整 
	}
	return ;
} 

如何建立堆:

法一:
可以从空堆开始,然后依次往堆中插入每一个元素,直到所有的数都被插入(转移到堆中)为止。代码如下:

//建立堆
n = 0;
for (int i = 1; i <= m; ++i){
	n++;
	h[n] = a[i];//或者写成cin >> h[n]; 
	siftup(n);
}

法二:
其实我们还有更快的方法建立堆。
把n个元素建立一个堆,首先我们可以将这个n个结点以自顶向下、从左到右的方式从1~n编码,这样就可以把这n个结点转换成一棵完全二叉树。紧接着从最后一个非叶结点(结点编号为n / 2)开始到根结点(结点编号为1),逐个扫描所有的结点,根据需要将当前结点向下调整,直到以当前结点为根结点的子树符合堆的特性。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码实现:

for (int i = n / 2; i >= 1; --i)
	siftdown(i); 

堆排序:

与快速排序一样,堆排序的时间复杂度也是O(NlogN)。

堆排序的实现:

法一:
比如我们现在要进行从小到大排序,可以先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。最终输出的或者存放在新数组中的数就已经是排序好的了。

//删除最小元素
int deletemin(){
	int t;
	t = h[1];//用一个临时变量记录堆顶的值 
	h[1] = h[n];//将堆的最后一个点赋值到堆顶 
	n--;//堆的元素减少1 
	siftdown(1);//向下调整 
	return t;//返回之前记录的堆的顶点的最小值 
} 

建堆以及堆排序的完整代码如下:

![在这里插入代码片](https://img-blog.csdnimg.cn/20210220150844442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xYQ18wMDc=,size_16,color_FFFFFF,t_70)

法二:
当然,堆排序还有更好的方法。从小到大排序的时候不建立最小堆而是最大堆,最大堆建立好以后,最大元素在h[1],因为我们的需求是从小到大的排序,希望最大的放在最后。因此我们可以把h[1]和h[n]交换,此时h[n]就是数组中的最大元素。请注意,交换后还需要将h[1]向下调整以保持堆的特性。OK,最大元素已经归为后我们需要将堆的大小减1,即n–,并将交换后的新h[1]向下调整以保持堆的特性。如此反复,直到堆的大小变成1为止。此时数组h中就已经是排好序的了。代码如下:

//堆排序
void heapsort(){
	while(n > 1){
		swap(1, n);
		n--;
		siftdown(1);
	}
}

完整的堆排序代码如下,注意使用这种方法需要建立最大堆。

//堆排序法二:
#include <bits/stdc++.h>
using namespace std;

int h[101];//用来存放堆的数组 
int n;//用来存储堆中的元素个数,也就是堆的大小 

//交换函数,用来交换堆中的两个元素的值 
void swap(int x, int y){
	int t;
	t = h[x];
	h[x] = h[y];
	h[y] = t;
	return ;
} 

//向下调整函数 
void siftdown(int i){//传入一个需要向下调整的结点的编号i,这里传入1,即从堆顶开始向下调整 
	int t, flag = 0;//用来标记是否需要继续向下调整
	//当i结点有儿子(其实是至少有左儿子)并且有需要调整的时候循环就执行 
	while(i * 2 <= n && flag == 0){
		//首先判断它和它左儿子的大小,并用t记录值较大的结点的编号 
		if(h[i] < h[2 * i]) t = i * 2;
		else t = i;
		//如果它有右儿子,再对右儿子进行讨论 
		if(i * 2 + 1 <= n){
			//如果右儿子的值更大,则更新为较大的结点的编号 
			if(h[t] < h[2 * i + 1]) t = 2 * i + 1;
		}
		//如果发现最大的结点编号不是自己,说明子结点中有比父结点更大的 
		if(t != i){
			swap(i, t);//交换它们 
			i = t;//i更新喂刚才与它交换的儿子结点的编号,便云接下来继续向下调整。 
		}
		else flag = 1;//否则说明当前的父结点已经比自己的两个子结点都要大了,不需要再进行调整了 
	}
	return ;
}

//建立堆 
void creat(){
	int i;
	//从最后一个非叶结点到第1个结点依次进行向下调整  
	for (i = n / 2; i >= 1; --i){
		siftdown(i);
	}
	return ;
}

//堆排序 
void heapsort(){
	while(n > 1){
		swap(1, n);
		n--;
		siftdown(1);
	}
	return ;
}

int main(){
	int i, num;
	//读入n个数 
	cin >> num;
	
	for (int i = 1; i <= num; ++i){
		cin >> h[i];
	}
	n = num;
	
	//建堆 
	creat();
	
	//堆排序 
	heapsort();
	
	//输出 
	for (i = 1; i <= num; ++i){
		cout << h[i] << ' ';
	}
	return 0;
}
//
测试样例 
//14
//99 5 36 7 22 17 46 12 2 19 25 28 1 92
//
运行结果
//1 2 5 7 12 17 19 22 25 28 36 46 92 99 

总结:

像注意支持插入元素和寻找最大(小)值的元素的数据结构称为优先队列。堆就是一种优先队列的实现。
另外Dijkstra算法中每次找离远点最近的一个顶点也可以用堆来优化。使算法的时间复杂度降到O((M + N)logN)。
堆还经常被用来求一个数列中的第K大数,只需要建立一个大小为K的最小堆,堆顶就是第K大数了。(举个例子,假设有10个数,要求第3大的数。第一步选取任意3个数,比如说是前3个数,并将这三个数建成最小堆,然后从第4个数开始,与堆顶的数比较,如果比堆顶的数要小,那么这个数就不要,如果比堆顶的数要大,则舍弃当前的堆顶而将这个新数作为新的堆顶,并再去维护,用同样的方法处理剩下的数。)
如果求一个数列中第K小的数,只需要建立一个大小为K的最大堆,堆顶就是第K小的数,这种方法的时间复杂度为O(NlogK)。当然也可以用来求前K大的数和前K小的数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值