c语言-数据结构-堆

news2024/10/7 18:21:26

       

目录

一、二叉树

1、二叉树的概念

 2、完全二叉树和满二叉树

3、完全二叉树的顺序存储

二、堆

2、堆的概念与结构

3、堆的创建及初始化

4、堆的插入(小堆)

 5、堆的删除

6、显示堆顶元素

7、显示堆里的元素个数

8、测试堆的各个功能

9、 实现堆排序

三、TOP-K的实现

结语:


前言:

        堆是数据结构中是很重要的一个知识点,通常用于实现堆排序, 比如在生活中我们要算出考试成绩的前十名,或者游戏排名的前一百名,这种场景下就用到堆排序了。堆其实是相同类型元素的集合,通常用数组的形式表示一个堆,数组内的所有元素都按照完全二叉树的形式进行存储,这时候又引出了二叉树的概念,只要明白了完全二叉树的概念就明白了什么是堆,因此下面先介绍二叉树的基本概念。

一、二叉树

1、二叉树的概念

        二叉树的根节点由一个左子树和一个右子树构成,也就是每一个结点可以少于两个孩子结点但是不能多于两个孩子结点,示意图如下:

        如上图,D可以是A的孩子结点,同时D也可以是H的父母节点,D同时也是E的兄弟节点,一个结点可以拥有多重身份。如果一棵树中的任意一个结点有超过2个孩子结点那么该树就不是一个二叉树,二叉树的其他情况如下:

 2、完全二叉树和满二叉树

        一颗二叉树的每一层的结点都是满的,将该二叉树称为满二叉树,完全二叉树则是在满二叉树的概念上引申而来的,即满二叉树的最后一层的结点可以不是满的,但是二叉树中的所有结点从左到右必须是按照顺序排序的,示意图如下:

         因此可以看出完全二叉树的一个特点:即每个节点必须是按照顺序进行存储的,即完全二叉树具有顺序性。

3、完全二叉树的顺序存储

        我们知道数组是一块连续的空间,而且数组里的元素都是“紧挨着”的,即不存在第一个元素和第三个元素中间空了位置不存储任何数据,这么做不符合逻辑也很浪费空间。由此看来,数组和完全二叉树的特点非常相似。

        因此一个完全二叉树可以用数组的形式表示出来,只要是一个数组就是一个完全二叉树。并且有了父母结点的下标就可以找到他的孩子结点,有了孩子的结点就能找到其父母结点的下标。 

        比如有了结点B的下标,可以找到他的两个孩子结点的下标:左孩子D=B的坐标*2,有孩子E=B坐标*2+1。

        B的父母结点A坐标=(B坐标-1)/2。

二、堆

2、堆的概念与结构

       堆是一个完全二叉树,但是他在完全二叉树的基本要求上增加了以下条件:树里任意一个父母结点都大于等于(或小于等于)他们的孩子结点,因此堆分为两种情况:大堆和小堆。

        大堆:从根节点开始,从上至下每个父亲结点必须大于他的两个孩子结点。

        小堆:从根节点开始,从上至下每个父亲结点必须小于他的两个孩子结点。

        示意图如下:

        因此从以上的分析来看,可以得出以下结论:数组其实就可以看成是一个完全二叉树,但是不一定是堆,但是是堆就一定是完全二叉树。而且即使是小堆也不一定是升序,大堆也不一定是降序(这里只是偶然)。当然,有序的数组一定是堆。 

        接下来用代码建立一个小堆。

3、堆的创建及初始化

        从以上得知,堆的本质是数组,因此堆的创建代码如下:

typedef int HeapDataType;//int类型重定义
typedef struct Heap
{
	HeapDataType* arr;//指针arr,即数组名表示数组的首地址
	int size;//数组的元素个数
	int capacity;//数组的容量
}Heap;

         堆初始化代码如下:

void HeapInit(Heap* php)//初始化
{
	assert(php);

	php->arr = NULL;//置为空
	php->size = 0;//刚开始没有元素因此为0
	php->capacity = 0;
}

4、堆的插入(小堆)

        原理就是把数据插入到数组中,所以在数组的末尾处添加数据即可,但是单单的插入数据并不能满足大堆和小堆的条件,因此每插入一个数据都要对数组进行调整,以便然数组满足小堆的条件。

        然而从数组的末尾处插入元素,从完全二叉树的角度来看,就是从树的最后一个结点插入元素,因此对数组进行调整其实就是调整插入元素与其父母结点的一个大小关系。插入最后一个结点的时候进行对树的调整的,该调整法称为向上调整法。向上调整示意图如下:

        建小堆和向上调整法代码如下:

void Swap(HeapDataType* p1, HeapDataType* p2)//交换函数
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}


void AdjustUp(HeapDataType* arr, int child)//向上调整函数
{
	assert(arr);

	int parent = (child - 1) / 2;//求出父母结点下标
	while (child > 0)//等于0才结束循环说明是最坏情况与根交换
	{
		if (arr[child] < arr[parent])//如果孩子小则与父母交换
		{
			Swap(&arr[child], &arr[parent]);//让孩子结点和父母结点交换

			child = parent;//让原先的父母结点变成孩子结点继续调整
			parent = (child - 1) / 2;//找到原先父母结点的父母结点
		}
		else//表示没有达到最坏情况
		{
			break;
		}
	}
}


void HeapPush(Heap* php, HeapDataType x)//入堆
{
	assert(php);

	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;//三目法更新容量
		HeapDataType* temp = (HeapDataType*)realloc
		(php->arr, sizeof(HeapDataType) * newcapacity);//开辟以及扩容空间

		if (temp == NULL)
		{
			perror("HeadFush");
			return;
		}
		php->arr = temp;//让arr指向新的空间
		php->capacity = newcapacity;//更新容量
	}
	php->arr[php->size] = x;//入堆
	php->size++;//更新size

	AdjustUp(php->arr, php->size - 1);//向上调整堆
}

 5、堆的删除

        堆的删除一般是将堆顶的元素删除,堆顶的元素即是下标为0的元素,也就是数组的第一个元素,如果将堆顶的元素直接删除,堆肯定会发生变化,会导致该数组不再是堆了,因此进行堆的删除的同时要调整堆。因为是堆顶的元素发生了改变,所以从堆顶的位置往下调整,进行向下调整。向下调整堆的具体逻辑如下图:

        向下调整法思路:把堆顶元素和堆尾元素进行交换,然后从堆顶处开始向下调整,一直调整到元素30的前一个元素。让堆顶元素跟他的两个孩子中最小的孩子进行比较,如果他的孩子比他小则他们俩进行交换,如此循环往下比较,直到树的最后一层。注意:此时的元素30是不参与调整的。

        堆的删除及向下调整法代码如下: 

bool Empty(Heap* php)//判空函数
{
	assert(php);

	return php->size == 0;//为空即返回真
}


void AdjustDown(HeapDataType* arr, int size, int parent)//向下调整堆
{
	assert(arr);

	int child = parent * 2 + 1;//找出其左孩子
	while (child < size)//若孩子的下标已经超出了数组元素个数,说明数组已经是堆
	{
		if ((child + 1 < size) && arr[child + 1] < arr[child])//此判断可以找出最小的孩子
		{
			child++;
		}
		if (arr[child] < arr[parent])//若孩子小于父母则交换
		{
			Swap(&arr[child], &arr[parent]);

			parent = child;//进行往下遍历
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapPop(Heap* php)//删除堆顶
{
	assert(php);
	assert(!Empty(php));//堆为空则Empty函数返回真,对其取非,则堆为空assert生效

	Swap(&php->arr[0], &php->arr[php->size - 1]);//交换函数
	php->size--;szie需要--因为此时的size是最后一个元素的后一个位置

	AdjustDown(php->arr, php->size, 0);//向下调整堆,从堆顶开始到堆尾结束
}

6、显示堆顶元素

        显示堆顶元素就很简单了,堆顶表示的是数组的首元素,首元素下标为0,显示堆顶代码如下:

HeapDataType HeapTop(Heap* php)//展示堆顶
{
	assert(php);

	return php->arr[0];
}

7、显示堆里的元素个数

        堆的大小就是size的值,因为每次赋值后size都会++,所以刚好可以用size表示元素个数,代码如下:

int HeapSize(Heap* php)//堆的元素的总数
{
	assert(php);

	return php->size;
}

8、测试堆的各个功能

       以上介绍了那么多的功能,现在对这些功能进行测试,代码如下:

void test1()
{
	Heap hp;//创建堆的结构体变量
	HeapInit(&hp);//初始化

	int a[] = { 7,8,3,5,1,9,4,5 };
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HeapPush(&hp, a[i]);//把数组a里的元素入进堆里
	}
	printf("size:%d\n", HeapSize(&hp));//打印堆里元素总数
	int i = 0;
	while (!Empty(&hp))//循环打印出堆里的所有元素
	{
		printf("%d ",HeapTop(&hp));//打印堆顶的元素
		HeapPop(&hp);//删除堆顶元素
	}
	HeapDestroy(&hp);//释放堆
}

int main()
{
	test1();
	return 0;

}

        运行结果:

        可以看到打印出来的结果是一个有序的数组,原因是每次打印堆顶的元素然后就删除该堆顶元素并且进行了向下调整,调整完之后此时堆顶元素就是第二小的元素,所有才能打印出有序的序列。 

9、 实现堆排序

        以上的测试可以发现是在数组arr中进行的,但是原数组a并没有受到改变,若想实现排序,则必须在原数组内对原数组里的顺序进行排序。

        堆排序代码如下:

void test2()//堆排序
{
	Heap hp;
	HeapInit(&hp);//初始化

	int arr[] = { 7,8,3,5,1,9,4,5 };//创建一个数组
	
	int k = sizeof(arr) / sizeof(int);

	//对原数组进行建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, k, i);//
	}

	//建完堆之后不一定是有序的数组,因此还需要对其进行调整
	int end = sizeof(arr) / sizeof(int) - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);//堆顶元素与堆尾元素进行交换

		AdjustDown(arr, end, 0);//向下调整到堆尾的前一个元素
		end--;//堆尾下标--,准备下一次的交换
	}

	//打印
	for (int i = 0; i < k; i++)
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	test1();
	return 0;

}

        首先在原数组a中进行建堆,建完堆之后不代表数组a就是有序的,因此还需要对其进行调整,我们可以保证的是数组在建完堆之后堆顶的元素是最小的,因此把堆顶元素跟堆里的最后一个元素进行交换(用end表示堆尾的下标),然后从堆顶开始向下调整到end的前一个位置结束调整,思路与堆的删除相似。

        从上图分析来看,最后会得到一组降序的数组,因此得出结论建立小堆得到的是降序,而建立大堆得到的是升序。

三、TOP-K的实现

        一般我们要查询某个专业的前十名,或者前世界500强的公司有哪些的时候都会用到TOP-K来解决,当要搜索的范围很大的时候,用正常的排序是难以解决的,比如从10亿个数中求出前五个最大的数,这时候如果把10亿个数排好序则要耗费大量的内存,很明显排序的办法不行。

        这时候就需要堆来解决了,可以先建立一个5个数字的小堆,然后从这10个数中不断的取出数据跟堆顶元素进行比较,如果比堆顶元素大则替换堆顶元素然后再进行向下调整堆,最后堆里的5个元素就10亿中最大的5个元素。用堆解决的优势在于这10亿个数不需要全部拿到内存中,而是存在磁盘里的文件中就可以进行对比了,需要文件操作函数来实现。

        这里我就用1000个数作为测试用例,TOP-K代码如下:

void CreatData()//创造数据
{
	int n = 1000;//这里我们只创建1000个数用来测试
	srand(time(0));//生成随机数
	FILE* fin = fopen("data.txt", "w");//以写的方式打开文件
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; i++)
	{
		int x = rand() % 10000;//生成10000以内的随机值
		fprintf(fin, "%d\n", x);//把这些值写进文件中
	}
	fclose(fin);//关闭文件
	fin = NULL;
}

void TOP_K(int k)//选出5个最大的数
{
	FILE* fin = fopen("data.txt", "r");//以读的方式打开文件
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
	//创建数组
	int* arr = (int*)malloc(sizeof(int)*k);
	if (arr == NULL)
	{
		perror("malloc error");
		return;
	}

	//倒数据
	for (int i = 0; i < k; i++)
	{
		fscanf(fin, "%d", arr + i);//把文件里的前五个数给到数组arr,准备建堆
	}

	//建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, k, i);//向下调整建立小堆
	}

	//TOP-K
	int val = 0;//创建val变量
	while (!feof(fin))//遍历文件内容
	{
		fscanf(fin, "%d", &val);//把文件里的值以%d的形式给到val变量
		if (val > arr[0])//如果val的值大于堆顶元素
		{
			arr[0] = val;//将val的值赋给堆顶
			AdjustDown(arr, k, 0);//并且重新向下调整堆,保证数组是一个堆
		}
	}
	//打印数组
	for (int i = 0; i < k; i++)
	{
		printf("%d ", arr[i]);
	}
	fclose(fin);//关闭文件
	fin = NULL;
}

int main()
{
	CreatData();//创建数据
	TOP_K(5);//TOP-K实现
	return 0;

}

        创建文件数据的结果:

        运行结果:

         从结果可以看到TOP-K确实帮助我们从1000个数中找出了最大的五个数。

结语:

        以上就是关于堆的一系列的解析与延申,堆的重点在于对向上调整和向下调整的理解,这两个调整法才是创建堆的核心。希望本文可以带给你更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!!谢谢大家!!(❁´◡`❁)

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

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

相关文章

TMS320F28335使用多个串口时,SCIRXST Register出现错误

TMS320F28335使用多个串口时&#xff0c;SCIRXST Register出现错误 void ClearErrorState(void) {if((SciaRegs.SCIRXST.bit.FE 1)||(SciaRegs.SCIRXST.bit.BRKDT 1)){SciaRegs.SCICTL1.bit.SWRESET 0;SciaRegs.SCICTL1.bit.SWRESET 1;}if((ScibRegs.SCIRXST.bit.FE 1)||(S…

面试题 Android 如何实现自定义View 固定帧率绘制

曾经遇到的面试题, 如何实现自定义View 1s内固定帧率的绘制. 当时对Android理解不深, 考虑的不全面, 直接回答了在onDraw结束时通过postDelay发送一个(1000 / 帧数)ms的延时消息触发invalidate进行下一次绘制. 但实际上这样做存在明显的问题 实际上1s绘制的帧数是不符合期望帧…

FindMy技术用于保温杯

在即将到来的冬季&#xff0c;每个人都开始给自己准备一个保温杯&#xff0c;保温杯是一种盛水的容器&#xff0c;主要由陶瓷或不锈钢制成&#xff0c;并加入真空层&#xff0c;以实现保温效果。这种杯子顶部有盖&#xff0c;密封严实&#xff0c;能够延缓内部液体散热&#xf…

基于黏菌算法优化概率神经网络PNN的分类预测 - 附代码

基于黏菌算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于黏菌算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于黏菌优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

echarts 横向柱状图示例

该示例有如下几个特点&#xff1a; ①实现tooltip自定义样式&#xff08;echarts 实现tooltip提示框样式自定义-CSDN博客&#xff09; ②实现数据过多时滚动展示&#xff08;echarts 数据过多时展示滚动条-CSDN博客&#xff09; ③柱状图首尾展示文字&#xff0c;文字内容嵌入图…

【MMC/SD/SDIO】读写操作

SD 总线是基于命令和数据流&#xff0c;它们由一个开始 Bit 发起&#xff0c;由一个停止 Bit 结束。 Command&#xff1a;命令开始一个操作。命令由 Host 驱动&#xff0c;或者给单卡&#xff08;寻址命令&#xff09;&#xff0c;或者给所有连接的卡&#xff08;广播命令&…

【EI会议征稿】第七届大数据与应用统计国际学术研讨会(ISBDAS 2024)

第七届大数据与应用统计国际学术研讨会&#xff08;ISBDAS 2024&#xff09; 2024 7th International Symposium on Big Data and Applied Statistics 第七届大数据与应用统计国际学术研讨会&#xff08;ISBDAS 2024&#xff09;定于2024年3月8-10日在中国上海举行。会议旨在…

红黑树的插入与验证

红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红黑树确保没有一条路 径会比其他路径长出俩倍&#xff0c;因而是接近平衡的…

微信小程序获取手机号报错getPhoneNumber:fail no permission

目录 一、问题描述二、解决方法 一、问题描述 微信小程序调用 API 获取手机号报错&#xff1a; getPhoneNumber:fail no permission二、解决方法 小程序没有认证&#xff0c;需要对小程序进行微信认证。如果是复用公众号资质认证&#xff0c;在公众号关联小程序后&#xff0…

EtherCAT 伺服控制功能块实现

EtherCAT 是运动控制领域主要的通信协议&#xff0c;开源EtherCAT 主站协议栈 IgH 和SOEM 两个项目&#xff0c;IgH 相对更普及一些&#xff0c;但是它是基于Linux 内核的方式&#xff0c;比SOEM更复杂一些。使用IgH 协议栈编写一个应用程序&#xff0c;控制EtherCAT 伺服电机驱…

MIKE水动力笔记19_统计平均潮差

本文目录 前言Step 1 ArcGIS中创建渔网点Step 2 将dfsu数据提取到渔网点Step 3 Python统计平均潮差 前言 日平均潮差&#xff08;average daily tidal range&#xff09;&#xff1a;日高潮潮高合计之和除以实有高潮个数为日平均高潮潮高&#xff0c;日低潮潮高合计之和除以实…

【漏洞复现】NUUO摄像头存在远程命令执行漏洞

漏洞描述 NUUO摄像头是中国台湾NUUO公司旗下的一款网络视频记录器&#xff0c;该设备存在远程命令执行漏洞&#xff0c;攻击者可利用该漏洞执行任意命令&#xff0c;进而获取服务器的权限。 免责声明 技术文章仅供参考&#xff0c;任何个人和组织使用网络应当遵守宪法法律&…

【C语法学习】25 - strncpy()函数

文章目录 1 函数原型2 参数3 返回值4 使用说明5 示例5.1 示例15.2 示例2 1 函数原型 strncpy()&#xff1a;将str指向的字符串的前n个字符拷贝至dest&#xff0c;函数原型如下&#xff1a; char *strncpy(char *dest, const char *src, size_t n);2 参数 strncpy()函数有三个…

linux进程间通信之共享内存(mmap,shm_open)

共享内存&#xff0c;顾名思义就是允许两个不相关的进程访问同一个逻辑内存&#xff0c;共享内存是两个正在运行的进 程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中&#xff0c…

信号的机制——信号处理函数的注册

在 Linux 操作系统中&#xff0c;为了响应各种各样的事件&#xff0c;也是定义了非常多的信号。我们可以通过 kill -l 命令&#xff0c;查看所有的信号。 # kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS …

【Spring】加载properties文件

文章目录 在Spring Context中加载properties文件测试总结 在Spring Context中加载properties文件 分为三步&#xff0c;如下图所示&#xff1a; 完整代码&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.…

【Linux】U盘安装的cfg引导文件配置

isolinux.cfg文件 default vesamenu.c32 timeout 600display boot.msg# Clear the screen when exiting the menu, instead of leaving the menu displayed. # For vesamenu, this means the graphical background is still displayed without # the menu itself for as long …

计算机是如何工作的(简单介绍)

目录 一、冯诺依曼体系 二、CPU基本流程工作 逻辑⻔ 电⼦开关——机械继电器(Mechanical Relay) ⻔电路(Gate Circuit) 算术逻辑单元 ALU&#xff08;Arithmetic & Logic Unit&#xff09; 算术单元(ArithmeticUnit) 逻辑单元(Logic Unit) ALU 符号 寄存器(Regis…

java:IDEA中的Scratches and Consoles

背景 IntelliJ IDEA中的Scratches and Consoles是一种临时的文件编辑环境&#xff0c;用于写一些文本内容或者代码片段。 其中&#xff0c;Scratch files拥有完整的运行和debug功能&#xff0c;这些文件需要指定编程语言类型并且指定后缀。 举例&#xff1a;调接口 可以看到…

Unity——URP相机详解

2021版本URP项目下的相机&#xff0c;一般新建一个相机有如下组件 1:Render Type(渲染类型) 有Base和Overlay两种选项&#xff0c;默认是Base选项 Base:主相机使用该种渲染方式&#xff0c;负责渲染场景中的主要图形元素 Overlay&#xff08;叠加&#xff09;:使用了Oveylay的…