【数据结构】——二叉树堆的实现

 大佬们点点关注,点点赞?!

前言

在上篇博客中我们已经介绍了树和二叉树的相关概念,相信大家都已经清楚了树和二叉树的基本思想,下面我们就来着重看看二叉树堆的实现。

在看堆的实现,我们先看看二叉树的顺序存储

一  二叉树的顺序存储

二叉树的顺序存储就是以顺序表来实现的,也就是把二叉树装进数组中去实现,其中坐标代表着各个结点的位置,这里我们用到一个二叉树的性质,也就是上篇博客的性质4。如果我们存的二叉树的的结构不是完全二叉树或者是满二叉树,那么就会有空间的浪费。

那就会有人问了,为啥空的地方不存储呢, 这也是我们的性质4的原因,因为我们在存的时候,需要找的孩子结点,或者父亲结点,他们的下标很重要

比如知道父亲坐标找孩子:leftchild=parent*2+1,rightchild=parent*2+2;

知道孩子坐标找父亲:parent=(leftchild-1)/2; 这里不需要判断是左孩子还是右孩子,因为这里是向下取整(因为int型向下取整的特点),所以得到的结果都是一样的。

所以如果用顺序存储最好是完全二叉树比较好,这样避免了空间的浪费,在操作中用数组存储的也就只有完全二叉树,这也从而让堆实现起来!

二  堆的概念

这里的堆和操作系统里面的堆是两个东西,一个是数据结构,一个是管理系统的一块分区,这里要区分开来

现在有一段数据,然后我们把这些数据按完全二叉树的形式存储到我们的数组中,如果这组数组拆分成二叉树,符合父亲结点大于孩子结点的我们称之为大堆,反之称为小堆。

三  堆的性质

1.堆的某个结点值总是不大于或者不小于其父亲结点的

2.堆总是完全二叉树

3.堆中的结点如果没有左孩子,那么肯定没有右孩子

4.堆不一定有序

四  堆的实现

4.1 结构体的创建

首先堆是用顺序结构实现的,所以我们用的结构也就是和顺序表的形式是一样的

typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType * a;
	int size;//当前大小
	int capacity;//容量
}Heap;//这里和顺序表一样
4.2  堆的初始化和销毁

这里的初始化也可以直接给空间,看个人的喜好

void HeapIint(Heap* hp)
{
	assert(hp);//断言,因为hp不可能为空
	hp->a = NULL;
	hp->size = 0;
	hp->capacity = 0;
}
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->size = 0;
	hp->capacity = 0;
}
4.3  堆的插入

堆的插入,也就是在最后面插入一个数据,这个数据插入以后,这个堆就不是堆了,因为如果是小堆,我们插入的数据比根节点还小,那么这就不符合堆的性质,所以我们需要向上调整这个算法

向上调整的算法思路就是 把这个插入的数据和他这条路径上的所有数进行比较,如果小那么就交换,最终把破坏的结构恢复过来,也就是8,16,12这条路径,其他的没影响

向上调整需要用到父亲结点,也就是我们插入的是孩子,所以我们需要用孩子的坐标算出父亲的坐标

void HeapPush(Heap* hp, HeapDataType x)
{
	assert(hp);
	if (hp->size == hp->capacity)//开辟空间
	{
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;//
		HeapDataType* tmp = (HeapDataType*)realloc(hp->a, sizeof(HeapDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("malloc fail");
			return;
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size] = x;//放入数据
	hp->size++;
	AdjustUp(hp->a, hp->size - 1);//向上调整
                                  //这里的参数一个是数组,一个是插入数据的坐标
}
4.4  向上调整算法 
void AdjustUp(HeapDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child>0)//当孩子在第一个位置的时候也就没必要比较了所以是>0
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);//交换函数,进行交换
			child = parent;//换位置,继续比较
			parent = (child - 1) / 2;
		}
		else
		{
			break;//如果符合堆的性质,那么不玩了,直接退出来就行
		}
	}
}

这里while的条件判断可能有的人会用parent>=0去比较,这虽然可以,但是纯属巧合,具有风险,这里还是推荐使用上面的条件判断

这里函数的参数没用传堆的结构,只是传了一个数组的结构,这是因为这个的算法应用面比较广,所以不能限制死了

4.5  交换函数
void Swap(HeapDataType* p1, HeapDataType* p2)
{
	HeapDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
4.6  堆的删除 

这里堆的删除如果是删最后一个元素那么就会变的毫无价值,所以我们删除第一个元素!!

如果我们删除第一个元素,那么我们的堆也就被破坏了,有两种方法,第一种也就是挪动覆盖,和数组一样,但是这样第二个元素就变成了根结点,那么他的兄弟就变成了他的儿子?!然后我们再重新建堆,因为他们的关系全乱了,所以只能这样,建堆也就是上面的操作想想都复杂。

假如之前的堆是这样的,那么调整挪动后就变成这样了

很明显这已经是发生了很大的改变。

第二种方法就是向下调整,也就是不删除第一个元素,先把他和最后一个元素交换,把最后一个元素删除,然后再让第一个元素向下调整比较,比他小的就交换(因为是小堆),最后形成的结构还是小堆,同时也成功把第一个元素删除了


void HeapPop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	Swap(&hp->a[0], hp->a[hp->size - 1]);//交换,然后删除最后一个元素
	hp->size--;
	AdjustDown(a, hp->size, 0);
}
 4.7  向下调整算法

这里的我们得到的是父亲结点,需要算出孩子结点,所以我们传参的时候传父亲的下标,同时由于在比较过程中需要有一个跳出循环的条件,所以我们需要把n传进去

void AdjustDown(HeapDataType* a, int n, int parent)
{
	int child = parent*2+1;
	while (child<n)//如果孩子都大于等于n了,说明比较已经到最后了,也没必要比了,再比就越界了
	{
		if (child + 1 < n && a[child + 1] < a[child])//这里需要算出哪个孩子比较小(小堆)
                                                     //child+1就是右孩子
                                                     //同时右孩子可能不存在,所以要有前面的判断条件
		{
			child++;
		}
		if (a[child] < a[parent])//下面的就和向上调整的算法差不多了
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent*2+1;
		}
		else
		{
			break;
		}
	}
}

有了上面的操作,那么下面的操作也就变得比较简单了

4.8  取堆顶元素 
HeapDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	return hp->a[0];
}
4.9  堆的数据判空
bool HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->size == 0;
}
4.10  堆的测试

 

int main()
{
	Heap hp;
	HeapIint(&hp);
	int a[] = { 12 ,15, 16, 30, 45, 25, 8 };
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HeapPush(&hp, a[i]);//把数据一个个插入进去,最终形成堆
	}
	while (!HeapEmpty(&hp))
	{
		int top = HeapTop(&hp);//取出第一个元素
		cout << top << " ";//打印
		HeapPop(&hp);//然后删除第一个元素,但是后面还是保持堆,因为采取了向下调整去维护
	}
	HeapDestory(&hp);
	return 0;
}

打印结果: 8 12 15 16 25 30 45

但是我们的本意是堆排序,并不是把这堆数据打印出来,所以我们可以这样

int main()
{
	Heap hp;
	HeapIint(&hp);
	int a[] = { 12 ,15, 16, 30, 45, 25, 8 };
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HeapPush(&hp, a[i]);
	}
	int i = 0;
	while (!HeapEmpty(&hp))
	{
		a[i++] = HeapTop(&hp);//把取出来的数据放进数组里面去
		HeapPop(&hp);
	}
	HeapDestory(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		cout << a[i] << " ";
	}
	return 0;
}

 但是我们又会发现这样是不是太麻烦了每次都要建一个堆,才能排序

那我们就有了下面的方法

五  堆排序


5.1  向上调整建堆

假设给定一个数组,那么我们从第二个元素开始,依次去比较,比如先给第一个元素,那么这肯定是个堆。然后给第二个元素,和第一个元素比较,比较完以后,那么这个还是一个堆,然后依次放入元素,最后就形成了堆,并且还是在数组里面的

	for (int i = 1; i < n; i++)//第一个元素不用比较,所以从第二个元素开始
		{
			AdjustUp(a, i);
		}
5.2  向下调整建堆

向下调整建堆我们需要从最后一个父亲结点开始, 因为最后的元素肯定都是叶结点,开始的时候他们不用去比较。比较完以后,那么当前的子树就是一个堆,那么继续往向上一个结点去向下调整,最后只剩根结点,然后进行一次向下调整即可

for (int i = (n - 1 - 1) / 2; i >= 0; i--)//从最后一个父亲结点开始
	{
		AdjustDown(a, n, i);
	}

可能图画的比较抽象,但是画的没错!

既然有两种方法那可能有一个快一个慢,这里可以直接得出向下调整比较快时间复杂度为O(N),向上调整是N*O(logN);至于具体的解释这里就不说明了,自己也可以感觉一下,假如叶子结点有N个,那么向上调整会比向下调整的要多

5.3  排序 

建完堆以后我们就要开始排序了,排序用向下调整算法,因为向下调整可以选出最大的和最小的数,因为他可以跟他的左右孩子比较,向上调整就不具备这样的优势

	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0],&a[end]);//倒数第一个数和第一个元素交换
		AdjustDown(a,end, 0);//这里的end是一直在变动的,因为排好的元素不需要进行比较了
		end--;
	}
	for (int i = 0; i < n; i++)
		cout << a[i] << " ";
}

如果是升序:建大堆

如果是降序:建小堆

因为后面的排序会把第一个元素放到后面,所以排完以后,就会变成有序的序列 

 

5.3  topK问题

如果我们要求从一个数组中选出前k个最大的数或者最小的数,那么我们的思路是直接建立一个大堆然后依次pop9次就行,因为最后一个不用pop,直接拿出来就行,每次pop,都是最大的数所以能够完成,虽然这行得通,但是如果是几亿的数,这种方法也会很慢

所以有了下面的改进

我们可以先建立一个k个数的小堆,然后与后面N-k个元素进行比较,不满足就替换堆顶元素,然后先下调整,这里调整的范围是K,不是N,所有比较大的数会在这个小堆的底部,最后形成一个k个数最大的小堆

	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, k, i);//建小堆
	}
	for (int i = k; i < n; i++)
	{
		if (a[i] > a[0])//选出k个最大的数
		{
			a[0] = a[i];
			AdjustDown(a, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
		cout << a[i] << " ";
}

以上便是堆排序的全内容 希望大家喜欢,都看到这里了

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在视频号上开店怎么样?聊下我做视频号店铺后的感受

我是王路飞。 说到创业找项目&#xff0c;电商无疑是现在最受欢迎的行业了。 毕竟现在的直播带货有多火相信大家也都明白&#xff0c;但是直播带货的门槛要远比开一个店铺的门槛高很多。 所以&#xff0c;很多普通人想分到直播带货这波红利的&#xff0c;都选择了开一个店铺…

什么是智慧公厕?智慧城市下的智慧公厕有什么功能和特点?

随着科技的不断进步和城市化的加快发展&#xff0c;智慧城市已经成为我们生活中的一部分。而在智慧城市的建设中&#xff0c;智慧公厕作为城市基础设施的重要组成部分发挥着重要的作用。那么什么是智慧公厕&#xff1f;智慧公厕是针对公共厕所的日常使用、运行、管理、运营等过…

如何在CentOS7部署Wiki.js知识库并实现分享好友公网远程使用【内网穿透】

文章目录 1. 安装Docker2. 获取Wiki.js镜像3. 本地服务器打开Wiki.js并添加知识库内容4. 实现公网访问Wiki.js5. 固定Wiki.js公网地址 不管是在企业中还是在自己的个人知识整理上&#xff0c;我们都需要通过某种方式来有条理的组织相应的知识架构&#xff0c;那么一个好的知识整…

javaWeb项目-高校实验室管理系统功能介绍

项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 1、JSP技术 JSP(Jav…

【docker】Dockerfile自定义镜像

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;中间件 ⛺️稳中求进&#xff0c;晒太阳 1.Dockerfile自定义镜像 常见的镜像在DockerHub就能找到&#xff0c;但是我们自己写的项目就必须自己构建镜像了。 而要自定义镜像&#xff0c;就…

Sqoop 的安装与配置

目录 1 下载并解压2 修改配置文件3 添加环境变量4 拷贝 JDBC 驱动5 测试Sqoop是否能够成功连接数据库 下载地址 1 下载并解压 &#xff08;1&#xff09;上传安装包 sqoop-1.4.6.bin__hadoop-2.0.4-alpha.tar.gz 到 hadoop101 的 /opt/software 路径中 &#xff08;2&#xf…

简化公文校对:掌握4大技巧,注意这10点

公文校对听起来可能挺专业&#xff0c;其实就是让文章更通顺&#xff0c;避免让人看着别扭。有个简单口诀&#xff1a;要么加点东西&#xff08;如果感觉不完整&#xff09;&#xff0c;要么减掉点东西&#xff08;如果太啰嗦&#xff09;&#xff0c;换掉不合适的词&#xff0…

吉利汽车×实在智能丨看RPA如何在财务领域实现100%自动化,300%的效率提升

【吉利汽车集团】隶属于浙江吉利控股集团&#xff0c;总部位于中国浙江杭州&#xff0c;是中国领先的汽车制造商和第19届杭州亚运会官方汽车服务合作伙伴。浙江吉利控股集团资产总值超过4800亿元&#xff0c;员工总数超过12万人&#xff0c;是戴姆勒股份公司第一大股东&#xf…

视频素材免费网站有哪些?8个视频素材库网站下载推荐

在视频创作领域&#xff0c;选择正确的高质量无水印素材网站能够极大地丰富您的作品&#xff0c;让每一帧都鲜活起来。下面&#xff0c;我们继续为您介绍更多优质的视频素材网站&#xff0c;每一个都是您创作旅程中的宝贵资源。 1. 蛙学府&#xff08;中国&#xff09; 集合了…

积木画(动态规划c++实现)

题目 小明最近迷上了积木画&#xff0c;有这么两种类型的积木&#xff0c;分别为 I 型&#xff08;大小为 2 个单位面积&#xff09;和 L 型&#xff08;大小为 3 个单位面积&#xff09;&#xff1a; 同时&#xff0c;小明有一块面积大小为 2N 的画布&#xff0c;画布由 2N …

Learning To Count Everything

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读&#xff1a;学习数一切东西1、研究背景2、提出方法3、模块详细3.1、多尺度特征提取模块3.2、密度预测模块 4、损失函数5、性能对比6、贡献 二…

Vue element-plus 导航栏 [el-menu]

导航栏 [el-menu] Menu 菜单 | Element Plus el-menu有很多属性和子标签&#xff0c;为网站提供导航功能的菜单。 常用标签&#xff1a; 它里面有两个子标签。el-menu-item&#xff0c;它其实就是el-menu每一个里面的item&#xff0c;item就是真实匹配到路由的每个栏目&#…

[优选算法专栏]专题十五:FloodFill算法(二)

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

剑指offer--数组中重复的数字

一.题目描述 在一个长度为 n 的数组 nums 里的所有数字都在 0&#xff5e;n-1 的范围内。数组中某些数字是重复的&#xff0c;但不知道有几个数字重复了&#xff0c;也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。 算法1.排序,然后遍历,时间复杂度O(nlogn),空…

openstack云计算(二)——使用Packstack安装器安装一体化OpenStack云平台

初步掌握OpenStack快捷安装的方法。掌握OpenStack图形界面的基本操作。 一【准备阶段】 &#xff08;1&#xff09;准备一台能够安装OpenStack的实验用计算机&#xff0c;建议使用VMware虚拟机。 &#xff08;2&#xff09;该计算机应安装CentOS 7&#xff0c;建议采用CentO…

【Java面试题】Redis中篇(高可用:主从复制、哨兵、集群)

文章目录 高可用14.Redis如何保证高可用&#xff1f;15.Redis的主从复制&#xff1f;16.Redis主从有几种常见的拓扑结构&#xff1f;17.Redis的主从复制原理了解吗&#xff1f;18.说说主从数据同步的方式&#xff1f;19.主从复制存在的问题&#xff1f;20.Redis Sentinel(哨兵)…

Swap交易所系统开发流程与区块链交易所系统规划方案

随着区块链技术的迅速发展&#xff0c;交易所在加密货币行业中扮演着至关重要的角色。Swap交易所因其独特的去中心化交易模式备受关注。在本文中&#xff0c;我们将深入探讨Swap交易所系统的开发流程&#xff0c;并详细介绍区块链交易所系统的规划方案&#xff0c;以帮助读者更…

智慧城市数字孪生,综合治理一屏统览

现代城市作为一个复杂系统&#xff0c;牵一发而动全身&#xff0c;城市化进程中产生新的矛盾和社会问题都会影响整个城市系统的正常运转。智慧城市是应对这些问题的策略之一。城市工作要树立系统思维&#xff0c;从构成城市诸多要素、结构、功能等方面入手&#xff0c;系统推进…

算法沉淀 —— 深度搜索(dfs)

算法沉淀 —— 深度搜索&#xff08;dfs&#xff09; 一、计算布尔二叉树的值二、求根节点到叶节点数字之和三、二叉树剪枝四、验证二叉搜索树五、二叉搜索树中第K小的元素 一、计算布尔二叉树的值 【题目链接】&#xff1a;2331. 计算布尔二叉树的值 【题目】&#xff1a; …

Pyhon 大模型常见的微调方式,LLMs常见的Finetune方式;chatglm3微调实战;大模型微调通俗易懂总结

一、 LLMs微调 微调&#xff08;Fine-tuning&#xff09;是指在一个已经训练好的神经网络模型基础上&#xff0c;使用额外的数据集或调整超参数&#xff0c;以实现特定任务的训练过程。在微调中&#xff0c;通常会固定预训练模型的大部分参数&#xff0c;只调整最后几层或特定层…