树的应用-堆(优先队列&堆排序)

树的介绍

  • 数和无向图很相像,但是树是没有回路的。
  • 因为树不包含回路这个特点,就被赋予了很多特性
    1.一棵树中的任意两个结点有且仅有唯一的一条路径连通
    2.一棵树中如果有n个结点,那么它一定恰好有n-1条边
    3.在一棵树中加一条边将会构成一个回路
  • 只要没有回路的连通无向图就是树
  • 同一棵树可以有多个形态,是因为把不同的点当成根结点的原因
  • 理解根结点,叶结点,父结点,子结点的定义

二叉树

  • 二叉树是一种特殊的树,其特点是每个结点最多有两个儿子,分别称为右儿子和左儿子
  • 更加严格的递归定义:二叉树要么为空,要么由根结点,左子树和右子树组成,而左子树和右子树分别是一颗二叉树
  • 满二叉树:一颗深度为h且有2的h次方-1个结点的二叉树
  • 完全二叉树:若设二叉树的高度为h,除第h层外,其他各层的结点个数都达到最大个数,第h层从右向左连续缺若干结点

堆-优先队列

  • 我们把完全二叉树从上到下,从左到右进行编号
  • 编完号后,我们可以发现,如果父结点编号为k,左儿子为2k,右儿子为2k+1。如果已知儿子的编号为x,则父结点编号为x/2

有两个定义:
1.最小堆:所有父结点都比子节点要小
2.最大堆:所有父结点都比子节点要大

  • 如果一个堆中因为几个数字使得它不满足最小堆的要求,便要使那个树进行向上调整或向下调整
  • 如要建立一个最小堆,当碰到一个父结点的值比两个子节点的值都大时,就要把父结点与较小的子节点进行交换,而且要从最后一个结点向第一个结点进行查找
  • 向下调整的代码:
//n为结点的总数,h数组是储存结点的值 
void siftdown(int i)//传入一个需要向下调整的结点编号 
{
	int t,flag=0;//flag作为一个标记,因为要一直向下调整,直至满足要求 
	while(i*2<=n&&flag==0){
		//看当前要调整的结点还有没有左分支 
		//把较小的值的结点赋给t 
		if(h[i]>h[i*2])
			t=i*2;
		else
			t=i;
		//看有没有右节点
		//把如果有就继续比较,把更小的值的结点赋给t 
		if(i*2+1<=n){
			if(h[t]>h[i*2+1])
				t=i*2+1;
		}
		//如果i不是最小的值,那么就要进行交换 
		if(t!=i){
			//swap函数是要自己写,把h[i]和h[t]进行交换 
			swap(i,t);
			//继续看交换后是否符合要求, i要变为被交换的数的结点,即为t 
			i=t;
		}
		//直到最大的值是自己,才能停止向下调整 
		else 
			flag=1;
	} 
	return;
}
  • 向上调整的代码:(向堆中添加一个数)
//向上调整
void siftup(int i)
{
	int flag=0;
	//因为是向上调整,所以当i等于1时,就不能继续调整了 
	if(i=1)
		return;
	while(i!=1&&flag==0){
		if(h[i]<h[i/2])
			swap(i,i/2);
		else
			flag=1;
		i=i/2;
	}
	return;
}
  • 依据这个向上调整代码,我们就可以建立一个最小堆,即从空堆开始,每加入一个数就对其进行向上调整,但这样的时间复杂度有点高
  • 我们就可以先建立一个数组来存储数,把这个当作完全二叉树,然后从最后一个非叶结点(n/2)向上扫描,遇到不合适的则将当前结点向下调整,子树将符合堆的特性
for(i=n/2;i>=1;i--)
	siftdown(i);

堆排序

  • 比如我们要从小到大排序,我们就可以先建立一个最大堆,建立好之后,最大的元素就在h[1],我们的需求是从小到大排序,希望最大的放在最后,我们可以将h[1]和h[n]交换,此时h[n]就是数组中的最大元素,但还需将h[1]继续向下调整,n–,再将h[1]和h[n]进行交换,直到堆大小变为1为止
  • 代码如下:
//建立最大堆
#include<stdio.h>
int h[101];//用来存放堆的数组 
int n;//用来存放堆中元素的个数 
//交换函数,用来交换堆中两个元素的值 
void swap(int x,int y){
	int t;
	t=h[x];
	h[x]=h[y];
	h[y]=t;
}
void siftdown(int i){//传入一个需要向下调整的结点编号i
                     //由于是最大堆,所以小的值要向下调整 
	int t,flag=0;//flag用来标记是否需要继续向下调整 
	//当i结点有儿子(至少有左儿子)并且需要继续调整的时候循环执行 
	while(i*2<=n&&flag==0){
		//首先判断i和zuo儿子的关系,并用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;
}
//建立堆的函数 
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个数 
	scanf("%d",&num);
	n=num;
	for(i=1;i<=num;i++){
		scanf("%d",&h[i]);
	}
	//建堆	
	creat();
	//堆排序 
	heapsort();
	//输出 
	for(i=1;i<=num;i++)
		printf("%d ",h[i]);
	return 0;
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值