二叉树中堆的实现

news2024/12/25 9:11:53

完全二叉树

满二叉树:二叉树每个节点的度都达到最大值(2),由此可有等比求和计算出节点总数:2^k-1

完全二叉树:除了最后一层。前面节点的度都满了,最后一层可以不满,但是必须从左至右连续,所以满二叉树也是完全二叉树的一种特殊形式,其总结点的范围在:2^(k-1) ~ 2^k-1

而满二叉树也是完全二叉树的一种特殊形式 

 二叉树的顺序结构与实现

其实堆就是一种特殊的二叉树结构(完全二叉树),但是堆实现排序相较于冒泡排序是相当快的,并且堆也可以迅速的找出一堆数据中的前几个最值,而堆恰恰就是用顺序表数组来实现的,所以就先来了解一下二叉树顺序结构。

 二叉树的顺序结构

完全二叉树的形式可以很好的与数组契合,因为节点的连续性,所以用顺序表结构的数组存储完全二叉树是十分合适的,并且可以直接通过数组下标的关系找到对应的子节点和父节点。

 而对于二叉树的其他形式是不适合用数组来存储的,可能会复杂并且存在空间的浪费。

堆的概念与结构 

首先我们要知道,堆的结构是完全二叉树,并且推也分为两种:大(根)堆,小(根)堆。

 而堆也只有大堆和小堆两种形式,其他不满足条件的都不属于堆

大(根)堆:树中的任意一个节点存储的值都小于等于其父节点存储的的值

小(根 )堆:树中的任意一个节点存储的值都大于等于其父节点存储的的值

堆的基本实现

 堆是可以用顺序表结构的数组来实现的,所以可以借鉴前面栈的实现方法,而下面介绍的是大(根)堆的写法:

需要实现的函数

typedef struct Heap
{
	int sz;
	int capacity;
	int* arr;

}Heap;

void Init(Heap* hp);//初始化堆
void Push(Heap* hp, int x);//增数据
void Pop(Heap* hp);//删数据
int GetTop(Heap* hp);//得到根数据
void Destroy(Heap* hp);//空间释放
void Init(Heap* hp)
{
	hp->arr = (int*)malloc(sizeof(int) * 3);
	hp->capacity = 3;
	hp->sz = 0;//指向实际数据的下一个节点
}

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 =  *p2;
	*p2 = tmp;
}

void Adjust_up(int* arr,int child)
{
	while (child > 0)
	{
		int parent = (child - 1) / 2;//不可以作为while的条件,child==0时
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
		}
		else
			return;

	}
}
void Push(Heap* hp, int x)
{
	if (hp->sz == hp->capacity)
	{
		hp->capacity *= 2;
		int* tmp = (int*)realloc(hp->arr, sizeof(int) * hp->capacity);
		assert(tmp);
		hp->arr = tmp;
	}
	hp->arr[hp->sz] = x;
	
	hp->sz++;
	//向上调整保证是堆
	Adjust_up(hp->arr,hp->sz-1);
}

void Adjust_down(int* arr,int last)
{
	int parent = 0;
	int child = parent * 2 + 1;//假设较大值是左孩子
	while (child<last)
	{
		if (child + 1 < last && arr[child] < arr[child + 1])//先防止越界,再验证较大值(只有一个左孩子时可能会越界)
			child += 1;

		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			return;
	}
}
void Pop(Heap* hp)//删除根节点的数据
{
	assert(hp->sz);//保证有数据
	//防止改变堆的父子大小关系,保证其他数据的关系不变,则选择尾元素换到头
	hp->arr[0] = hp->arr[hp->sz - 1];
	hp->sz--;

	//向下调整
	Adjust_down(hp->arr,hp->sz);
}

int GetTop(Heap* hp)
{
	assert(hp->sz);
	return hp->arr[0];
}

void Destroy(Heap* hp)
{
	free(hp->arr);
	 
	hp->capacity = hp->sz = 0;
}

这里其实看一下Adjust_down 和 Adjust_up 这两个函数的实现就行了,主要就是插入数据时要用到向上调整数据,而删除根元素时会用到向下调整数据,这两个函数使用之前都要保证原数据的父子关系不会发生改变,即:除了增加或删除的数据,其余子树都是堆的形式。

用堆进行排序 

经过堆的实现,我们知道一个数据按照堆去存放,根节点的值要么是最大值要么就是最小值,所以我们多进行几次不就可以依次得到最大值(最小值)。假如用实现大(根)堆的方式来实现,首先将数组中的数一个个按照向上调整建堆的方式插入进去,这样根就是最大的一个数,此时接下来就有两种途径:1.将剩余的的数继续按照向上调整的方式再次建堆,找第二大的数(这样就改变了其他数之间的父子关系,故要全部重新插入)。2.将最后一个数和根(最大值)进行交换,再运用向下调整(类似于Pop 函数)实现接下来操作(这样不会改变原数据的父子关系,所以子树依旧是大堆的形式)。所以第二种方式就轻松了很多。


向上调整建堆法

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void Push(int* arr,int child)
{
	while (child > 0)
	{
		int parent = (child - 1) / 2;
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
		}
		else
			return;

	}
}
void Pop(int* arr,int len)
{

	int parent = 0;
	int child = parent * 2 + 1;//假设较大值是左孩子
	while (child<len)
	{
		if (child + 1 < len && arr[child] < arr[child + 1])//先防止越界,再验证较大值(只有一个左孩子)
			child += 1;//可能存在越界

		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			return;
	}
}
int main()
{

	int arr[] = { 6,1,7,0,3,5,8,2,9,4 };
	int len = sizeof(arr) / sizeof(int);
	for (int i = 1; i < len; i++)//向上建堆
	{
		Push(arr, i);//传下标
	}

	while(len>0)
	{
		Swap(&arr[0], &arr[len-1]);//将最大值放到最后面
		Pop(arr, len - 1);//传最后一个数的下一个下标
		printf("%d ", arr[len-1]);
		len--;//每循环一次就排好了一个数
	}

	return 0;
}

有一点要注意的是,每次找到最大的数换到数组的最后,则每向下调整一次就得到当前推中的最大值,数据就会逐渐少一,所以依次进行之后数据就按照升序的方式排列了,即:运用大根堆法实现升序,运用小根堆法实现降序。

向下调整建堆法

上面的堆排序其实是用到了向下调整建堆,向下调整找重建堆找最值。那么可不可以建堆也用向下调整的方式来完成呢,而在一份完整的数据面前我们想用向下调整的条件是:该节点的左右子树都是大堆(小堆)的形式才可以。

假如我们想要建立一个小堆。就以上面代码中的数组为例,我们现在的目的就是找可以向下调整的节点,即:该节点的左右子树都是小堆,而我们从图中可以确定的小堆就是最靠近叶节点的分支节点,因为该分支节点的两个分支是叶节点,故一定是小堆,即图中为3的节点,所以我们就从该位置开始向下调整,并且要逆着继续执行,找前面的分支节点,就依次从后向前,这样就可以保证你任何时候向下调整的节点的左右子树一定是堆。

步骤即:

代码实现

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void Adjust_down(int* arr, int i,int len)
{
	int parent = i;
	int child = i * 2 + 1;

	while (child < len)
	{
		if (child + 1 < len && arr[child] > arr[child + 1])//防止有右孩子不存在的情况
			child++;//找较小的子节点
		if (arr[parent] > arr[child])//如果父节点本来就小,不用换
		{
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			return;
	}

}
int main()
{
	int arr[]= { 6,1,7,0,3,5,8,2,9,4 };
	int len = sizeof(arr) / sizeof(int);
	//先向下调整建堆
	for (int i = (len - 1 - 1) / 2; i >= 0; i--)//i指向元素下标而len-1是最后一个元素下标
	{
		Adjust_down(arr, i,len);//len是最后一个元素的下一个下标
	}
	//向下调整找最值
	while (len > 0)
	{
		Swap(&arr[0], &arr[len - 1]);//交换一次就保留一个最小值放到最后面
		Adjust_down(arr, 0, len - 1);//此时最后一个元素就不计算在内
		printf("%d ", arr[len - 1]);//从后向前打印数据
		len--;
	}

	return 0;
}

复杂度分析

向上调整建堆:当层数节点多时,需要调整次数也多。所以数据越多,所执行调整的次数也就越多,时间复杂度也就越高。 


向下调整建堆:而相对于向上调整而言,向下调整建堆就明显避开了层数节点多时,需要调整次数也多等问题,恰恰相反,层数节点越多,需要调整的次数越少,时间复杂度是O(n-logn) 即:O(n) 。计算就留给你们了,和上面的方法一样。


而上面的方法实现堆排序的时间复杂度是O(n+n*logn),即:O(n log n)        

堆中的TOP-K问题

我们了解到堆排序相比冒泡排序而言,效率要高得多,而且向下调整建堆又比向上调整建堆效率高,所以我们就引申到堆中的 TOP-K 的问题,即:找所有数中最大(最小)的前 K 个数。

那么直接将这给的所有数据建成堆并向下调整排序不就成了,可是1.如果给的数据过大,空间不够用呢 2.只想要前 K 个最值,但是再向之前一样建存放所有数据的堆,是不是就存在严重的空间浪费了呢。

假如说我们要找所有数据的前5个最大的值:所以我们就想到了建一个只能存放5个数的小堆,然后依次将后面的数与堆顶的值进行比较,如果大于堆顶的数就交换,进行向下调整,重新建堆,然后依次将后面的数进行上面操作就可以完成。

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Adjust_down(int* arr, int i, int len)//i是下标,len是堆大小
{
	int parent = i;
	int child = parent * 2 + 1;
	while (child<len)
	{
		if (child + 1 < len && arr[child] > arr[child + 1])
			child += 1;
		if (arr[parent] > arr[child])
		{
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			return;
	}
}
int main()
{
	srand((unsigned int)time(NULL));
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("pf=NULL");
		return 1;
	}
	for (int i = 0; i < 1000; i++)
	{
		int x = rand() % 1000;//产生一万个随机数放在文件里
		fprintf(pf, "%d\n", x);
	}
	fclose(pf);//处理完数据之后一定要及时关闭,否则数据可能丢失

	//直接开辟相应大小的空间
	int k = 7;
	int* arr  = (int*)malloc(sizeof(int) * k);
	assert(arr);
	//读取文件中前面7个数据放到数组中
	FILE* po = fopen("data.txt", "r");
	assert(po);
	
	for(int i=0;i<k;i++)
	{
		fscanf(po, "%d", &arr[i]);//将前7个数存到数组里
	}
	for (int i = (k - 2) / 2; i >= 0; i--)//七个数向下调整建小堆
	{
		Adjust_down(arr, i, k);
	}
	//读取文件后面的数据
	int val = 0;
	while (feof(po)==0)
	{
		fscanf(po, "%d", &val);
		if (val > arr[0])
		{
			arr[0] = val;
			Adjust_down(arr, 0, k);//每执行一次,堆顶都是最小值
		}
	}
	for (int i = 0; i < k; i++)
		printf("%d ", arr[i]);

	return 0;
}

而当数据过多时,我们就会把数据存在文件当中,从文件中逐一的读取数据。使用小堆其实就保证了最小值在堆顶的位置,所以我们每次新来一个值就和堆顶(最小值比较),大的话就可以直接换掉最小值,再次调整,现在堆中的最小值又在堆顶....这样就方便了很多。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/593422.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用时序数据库 DolphinDB 实现地震波形的分析预警

1. 绪论 波形数据的存储与实时流处理是地震预警、地震速报、地震烈度速报、震源机制解等数字地震台网综合处理系统的前提&#xff0c;合理的存储方案与高效的实时流处理架构能极大地节约存储成本、降低响应延时、方便震源分析。 本篇教程会为有该方面需求的客户提供一个基于 …

如何在不丢失格式的情况下将 Excel 转换为 PDF?

Microsoft Excel 因其专业的数据或信息显示能力而被广泛采用。但是&#xff0c;它在兼容性上有一个恼人的缺点。 首先&#xff0c;Excel只能在Windows和MacOS上使用&#xff0c;Linux或其他操作系统的用户将无法使用&#xff1b;其次&#xff0c;即使您有一个可以打开和阅读 E…

PERMA幸福模型

PERMA幸福模型 由“积极心理学之父”马丁塞利格曼&#xff08;Martin Seligman&#xff09;在《持续的幸福 》一书中提出。 模型介绍 PERMA指的是创造充实、快乐和有意义的生活所必需的五大要素&#xff0c;它们是: 正向情绪Positive emotions 积极情绪是快乐人生的一个重要…

三种不同的存储芯片性能比较

为了进行性能比较&#xff0c;使用了三种不同的存储芯片&#xff0c;即Everspin EM064LX 64Mib STT‐MRAM、Micron MT25Q 128Mib NOR闪存和Micron MT29F 1Gib SLC NAND闪存。 该基准测试在STM32H755ZI上运行&#xff0c;Cortex‐M7内核的时钟频率为240MHz。Quad SPI总线用于1‐…

安泰功率放大器ATA-8202在EMC电磁兼容测试中的具体应用

作为电子及电器产品质量衡量指标之一的电磁兼容性&#xff08;EMC&#xff09;测试&#xff0c;它的测试正确与否直接关系到产品自身使用的可靠性及安全性&#xff0c;并且它还有可能对于同系统中的其他部分造成影响&#xff0c;不良的电磁兼容性会直接导致系统无法正常运转&am…

GIS数据转换器坐标生成功能

​很多时候我们会拿到一些没有坐标的图片或栅格数据&#xff0c;我们如何给它赋予坐标呢&#xff1f;下面介绍一种为这些数据生成坐标的方法。使用工具是GIS数据转换器-栅格-V4.0。 GIS数据转换器坐标生成功能 使用图片是这样的&#xff0c;网上找的不知道哪位大神画的三国时期…

【毕业季征文】追光人,终将光芒万丈!

追光人&#xff0c;终将光芒万丈&#xff01; 写在最前面最后从0到1 我的二进制探索之路一、从0到1&#xff0c;破茧成蝶二、从0到2n&#xff0c;全面发展三、从0到∞&#xff0c;逐光而行 参考 写在最前面 参与活动#毕业季征文|未来可期# 岁月不居&#xff0c;时节如流。转眼…

朋友轻松拿下字节27K的offer,羡慕了....

最近有朋友去字节面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

C++ 范围for循环

1. 在C98中&#xff0c;不同的容器和数组&#xff0c;遍历的方法不尽相同&#xff0c;写法不统一&#xff0c;也不够简洁&#xff0c;而C11基于范围的 for循环以统一、简洁的方式来遍历容器和数组&#xff0c;用起来更方便了。 C 容器都有哪些 简单介绍&#xff1a; //STL C标…

7分钟的面试,我太难了.....

干了两年外包&#xff0c;本来想出来正儿八经找个互联网公司上班&#xff0c;没想到算法死在另一家厂子。 自从加入这家外包公司&#xff0c;每天都在加班&#xff0c;钱倒是给的不少&#xff0c;所以也就忍了。没想到11月一纸通知&#xff0c;所有人不许加班&#xff0c;薪资…

leetcode-最大二叉树(java)

leetcode654题--最大二叉树 leetcode654 - 最大二叉树解题思路代码演示二叉树专题 leetcode654 - 最大二叉树 leetcode654 - 最大二叉树 原题链接 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中…

幻读与不可重复读的区分

幻读 VS 不可重复读 幻读重点在于数据是否存在。原本不存在的数据却真实的存在了&#xff0c;这便是幻读。 在同一个事务中&#xff0c;第一次读取到结果集和第二次读取到的结果集不同。引起幻读的原因在于另一个事务进行了INSERT操作。 不可重复读重点在于数…

中国人民大学与加拿大女王大学金融硕士——跟5月说再见,期待新的精彩

岁月清浅&#xff0c;时光无言。5月的风即将吹来6月的的绚烂&#xff0c;在这个美好的季节&#xff0c;你有新的期盼了吗&#xff1f;在职的你&#xff0c;是否需要再学习呢&#xff0c;中国人民大学与加拿大女王大学金融硕士项目为你提供在职读研的平台&#xff0c;在这里开启…

CMU 15-445 Project #0 - C++ Primer

Project #0 - C Primer 一、题目链接二、准备工作1.项目构建2.代码测试3.代码格式化4.压缩与提交 三、部分实现 一、题目链接 二、准备工作 以下操作在题目文档中均有提及&#xff0c;这里进行简要整理。 1.项目构建 首先需要从远程仓库克隆项目文件&#xff0c;由于该仓库会…

GB28181——实时视频显示

一、主要实现功能 1、支持四路设备接入 2、实时视频分屏显示 3、接入设备信息存储 二、待完善功能&#xff08;现阶段&#xff09; 1、设备的删除 &#xff08;已更新&#xff09; 2、语音对讲 &#xff08;暂时不处理&#xff09; 3、视频输出通道的实时修改 &#xff08;已更…

Nginx详细解说

1 什么是Nginx Nginx是lgor Sysoev为俄罗斯访问量第二的rambler.ru站点设计开发的。从2004年发布至今&#xff0c;凭借开源的力量&#xff0c;已经接近成熟与完善。 Nginx功能丰富&#xff0c;可作为HTTP服务器&#xff0c;也可作为反向代理服务器&#xff0c;邮件服务器。支…

科技云报道:大模型的中场战事,深入垂直行业腹地

科技云报道原创。 自从OpenAI于2022年11月推出ChatGPT后&#xff0c;一场波及全球科技界的“AI海啸”就此爆发。 自今年以来&#xff0c;国内已有超过30家企业入局大模型赛道。从百度“文心一言”、阿里“通义千问”的发布&#xff0c;到网易“玉言”、科大讯飞“星火”、昆仑…

Centos7匹配Locust+prometheus+grafana性能监控平台

一、安装Loust 之前已写过&#xff0c;这里忽略一万字。。。 Locust初次体验【解决webUI没数据】_Vikesgao的博客-CSDN博客 二、安装prometheus Prometheus 是一个开源的服务监控系统和时序数据库&#xff0c;其提供了通用的数据模型和快捷数据采集、存储和查询接口。它的核心…

LLM:LLaMA模型和微调的Alpaca模型

待写 LLaMA模型 论文原文:https://arxiv.org/abs/2302.13971v1 预训练数据 模型架构 模型就是用的transformer的decoder,所以在结构上它与GPT是非常类似的,只是有一些细节需要注意一下。 1、RMS Pre-Norm 2、SwiGLU激活函数 3、RoPE旋转位置编码 Alpaca模型 [Stanford …

cuda编程学习——CUDA全局内存性能优化(八)

前言 参考资料&#xff1a; 高升博客 《CUDA C编程权威指南》 以及 CUDA官方文档 CUDA编程&#xff1a;基础与实践 樊哲勇 文章所有代码可在我的GitHub获得&#xff0c;后续会慢慢更新 文章、讲解视频同步更新公众《AI知识物语》&#xff0c;B站&#xff1a;出门吃三碗饭 …