数据结构·二叉树(2)

目录

1 堆的概念

2 堆的实现

2.1 堆的初始化和销毁

2.2 获取堆顶数据和堆的判空

2.3 堆的向上调整算法

2.4 堆的向下调整算法

2.4 堆的插入

2.5 删除堆顶数据

2.6 建堆

3 建堆的时间复杂度

3.1 向上建堆的时间复杂度

3.2向下建堆的时间复杂度

4 堆的排序


前言:前面介绍了树以及二叉树及其二叉树的存储方式,本文就介绍基于二叉树模式下的一种结构——堆。


1 堆的概念

堆分为大堆和小堆,小堆是指每个父节点的值都小于子节点,大堆是子节点的值都大于父节点,

小堆是这样的,大堆同理就可以了。

堆在逻辑上是完全二叉树结构,实际的物理结构是数组,接下来就进入到重点——堆的实现。


2 堆的实现

实现堆的时候,我们不像之前实现顺序表的时候,有增删查改以及指定位置的删除增加等等,因为堆单纯用来存储数据是没有太大的意义的,所以实现的接口也不大一样。

堆同样用结构体定义,一个是数据,一个是空间大小,一个是有效数据个数。


typedef int HDataType;

typedef struct Heap
{
	HDataType* arr;
	int size;
	int capacity;
}Heap;

//堆的初始化
void HPInit(Heap* php);

//建堆
void HPInitArray(Heap* php,HDataType* a, int n);

//堆的销毁
void HPDestroy(Heap* php);

//堆的插入
void HPPush(Heap* php,HDataType x);

//获取堆顶数据
HDataType HPTop(Heap* hp);

//堆的删除数据
void HPPop(Heap* php);

//堆的判空
bool HPempty(Heap* php);

//堆的向上调整算法
void AdjustUp(HDataType* arr,int child);

//堆的向下调整算法
void AdjustDown(HDataType* arr,int size ,int parent);

2.1 堆的初始化和销毁

销毁和初始化与之前线性表的初始化基本上就是一样的,不用过多介绍

void HPInit(Heap* php)
{
	assert(php);
	php->arr = NULL;
	php->capacity = php->size = 0;
}
//堆的销毁
void HPDestroy(Heap* php)
{
	assert(php);
	free(php->arr);
	php->arr = NULL;
	php->capacity = php->size = 0;
}

2.2 获取堆顶数据和堆的判空

获取数据只需要判断堆是不是空的就行,判空只需要检查size的值就可以了。

bool HPempty(Heap* php)
{
	assert(php);
	return php->size == 0;
}
//获取堆顶数据
HDataType HPTop(Heap* php)
{
	assert(php);
	assert(!HPempty(php));
	return php->arr[0];
}

因为后面的向上调整和向下调整,我们对于数据的交换用的是很频繁的,所以我们单独创建一个函数用来交换数据:

//交换数据
void Swap(HDataType* px, HDataType* py)
{
	HDataType tem = *px;
	*px = *py;
	*py = tem;
}

2.3 堆的向上调整算法

堆的向上调整算法,即是我们插入数据之后,保持数据的结构依然是堆,所以向上调整就是从最后一个数据入手,往上依次调整,如果堆是小堆,那么就是子节点与父节点比较大小,子节点小于父节点,就交换,大堆同理可得。

那么向上调整,我们知道子节点,如何求的父节点呢?

其实通过节点之间的存储规律,我们可以得到

左子节点 = 父节点 * 2 + 1,右子节点 = 父节点 * 2 + 2;

知道任意子节点我们就可以求父节点,实际操作的时候我们求父节点的时候怎么知道子节点是左还是右呢?

解决方法就是不管三七二十一,父节点 = (子节点 - 1)  / 2,不管多出来的1,因为整型运算,1 / 2 = 0,所以1是被忽略了。

因为调整的次数可能不止一次,可能调整到高度的一半就停止了,或者是调整到了根部,所以我们使用while循环,循环条件就是子节点的下标,因为经历一次调整后,子节点会到父节点上,父节点又到该节点的父节点上,那么判断条件就应该是子节点的下标位置。

//堆的向上调整算法
void AdjustUp(HDataType* arr, int child)
{
	int parent = (child - 1) / 2;
	//注意大小堆的写法
	while (child)
	{
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child],&arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}	
}

2.4 堆的向下调整算法

如果说向上调整算法是从子节点入手,那么向下调整算法就是从父节点入手,父节点和子节点相互比较必然存在一个问题就是,子节点可能只能只有左节点,没有右节点,那我们还要考虑的是两个节点谁小的问题,父节点与两个子节点较小的节点比较,这里可以用到假设法解决。

传进去的参数是数组,堆的有效数据个数,父节点的下标。

这里同样用到while循环,因为是从上往下调整的,所以结束条件应该是child。

为什么是child而不是parent呢?因为调整到最后两层的时候,parent在倒数第二次就不用动了,已经调整结束了,所以向下调整比向上调整有一个明显的优势是在于最后一层不是干涉,时间复杂度会少很多很多,后面再介绍。

假设法找两个子节点中小的那个,为了防止存在越界访问,比如只有一个左孩子,但是child + 1就访问到了右孩子,就越界了,所以child + 1  < size就是为了防止越界访问的。

最后就是进行比较,交换,赋值了。

//堆的向下调整算法
void AdjustDown(HDataType* arr, int size, int parent)
{
	int child = parent * 2 + 1;
	//为什么不用child当作循环条件呢?
	while (child < size)
	{
		//先找两个孩子中小的那个 假设法
		if (arr[child + 1] < arr[child] && child + 1 < size)
		{
			child++;
		}
		//交换
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

2.4 堆的插入

 堆的插入很简单的,就跟顺序表的插入一样的, 无非是最后要保持数据依然是堆而已,因为数据是在最后位置插入的,所以可以用向上算法进行调整,前面就是判断空间够不够,不够扩容就行,就没其他要注意的:

//堆的插入
void HPPush(Heap* php, HDataType x) 
{
	assert(php);
	//判断空间是否足够
	if (php->capacity == php->size)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HDataType* tem = (HDataType*)realloc(php->arr, sizeof(HDataType) * newcapacity);
		if (tem == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->arr = tem;
		php->capacity = newcapacity;
	}
	php->arr[php->size++] = x;
	//插入后依然保持为堆
	AdjustUp(php->arr,php->size - 1);

}

2.5 删除堆顶数据

删除数据都是删除的堆顶数据,那么删除了之后我们该如何保持堆依然是大小堆呢?我们不能直接然后面的数据往前移动一位,这会让堆的数据完全乱套的,结构完全变化了。

前人是思路很是清奇的。我们不妨让第一个数据和末尾的数据进行交换,size--后,堆顶的数据就被删除了,问题是如何保持堆的结构呢?你看,向下调整这不就有大用了,从堆顶一直往下调整呗就,很清奇的这个思路,一下就删除好了。

结合这个思路,如果我们想要找最小,次小的数据是可以模拟这个思路的,后面介绍咯。

当然,没有数据肯定就不能删除了。

//堆的删除数据  删除的是堆顶数据
void HPPop(Heap* php)
{
	//数据删除之后依然保持为堆
	assert(php);
	assert(!HPempty(php));
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;
	AdjustDown(php->arr,php->size,0);
}

2.6 建堆

建堆有两个方法,一个是初始化一个堆之后,进行插入数据,调整操作,还有一种就是,初始化的这个过程就进行调整数据,即创建好一个满足堆需求的数组,再拷贝上去就行。

数据给好之后,该赋值的也都要赋值,然后就是调整数据部分,我们可以选择向上调整也可以选择向下调整,至于效率,是向下调整优先的,所以向上调整一般用的是比较少的,后面介绍。

//建堆
void HPInitArray(Heap* php,HDataType* a,int n)
{
	assert(php);
	php->arr = (HDataType*)malloc(sizeof(HDataType) * n);
	if (php->arr == NULL)
	{
		perror("malloc fail!");
		return;
	}
	memcpy(php->arr, a, sizeof(HDataType) * n);
	php->capacity = php->size = n;
	
	//向上建堆
	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(php->arr,i);
	//}
	//向下建堆
	for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->arr, php->size, i);
	}
}

向下建堆有个要注意的点就是i = php->size - 1 - 1,这是为了防止越界访问,假设堆里面只有9个元素,传进去的i就是4,进入到向下调整之后,child = 9,可是这是size指向的位置,一访问就越界了。


3 建堆的时间复杂度

建堆无非就两种方式,向上建堆和向下建堆,两种方式看似相差不大,实际上时间复杂度是相差较大的,这里就来慢慢分析:

计算时间复杂度之前,我们不妨计算一下树的高度与节点个数之间的关系:
二叉树的节点是以二次方递增的,第一层有2^0个节点,第二层有2^1个节点……第h层有2^(h - 1)次方个节点,那么总节点个数N = 2^0 + 2^1 + 2^3 + …… + 2^(h - 1),这里使用高中的等比求和公式,可以得出,N = 2^h - 1,那么h = log(N + 1)。

3.1 向上建堆的时间复杂度

时间复杂度估算,即是估算每个节点执行多少次操作,第一层的节点,执行调整操作次数至多为0次,第二层1次,第三层2次,第四层3次,第h层 h -1 次。

总的执行次数就是该层的所有节点 * 该层节点执行的至多次数.

F( h ) = 2^0 * 0 + 2 ^ 1 * 1 + 2 ^ 2 * 2 + 2 ^ 3 * 3  + …… +2 ^ (h - 1) * (h - 1),这里利用高中的错位相减,可以得到F(h) = 2^(h - 2) + 2,那么F(N) = ( N  + 1 ) * (log( N + 1) - 2)  + 2。

所以向上建堆的时间复杂度就是O(N) = N * log(N)

3.2向下建堆的时间复杂度

同3.1,向下建堆与向上建堆不一样的是向下建堆止步于倒数第二层,这就是为什么向下建堆算法优于向上建堆算法。

节点向下调整至多调整到倒数第二层,所以第一层的节点执行的次数为h - 1,第二层为h - 2,倒数第二层执行的次数为1次,所以:
F(h) = 2^0 * (h - 1) + 2 ^ 1 * (h - 2) + 2 ^ 2 * (h - 3) + …… + 2 ^ (h-1) * 1,结合高中的数学知识可以得到F(h) = 2^h - h - 3,F(N) = (N + 1) - log( N + 1) - 3.

所以向下建堆的时间复杂度就是O(N) = N - log(N)

这样看来向下建堆的效率远高于向上建堆的效率。


4 堆的排序

堆用来存储数据意义不大,排序倒是有点意思,当我们想让一个数组变成升序,我们是大堆还是小堆呢? 一般来说,小堆就是子节点大于父节点,满足升序,但是实际操作发现哪哪儿都是坑,特别容易改变结构。

面的删除操作有着异曲同工之妙,我们实现升序就选择大堆,讲堆顶数据放在最后,size--就访问不了最大的数据,然后选出第二大的数据,再交换,再size--,再选择第三大的数据,再交换,再size--,重复操作,最后就实现了堆排。

//堆排
void HPsort(HDataType* arr, int size)
{
	for (int i = (size - 1 - 1); i >= 0 ; i--)
	{
		AdjustDown(arr,size,i);
	}
	int end = size - 1;
	while (end)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		end--;
	}
}

感谢阅读!

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

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

相关文章

2024年生骨肉冻干深度比较:希喂、NWN、PURPOSE,哪一款更胜一筹?

在科学养宠的当今时代&#xff0c;生骨肉冻干已经成为猫咪日常饮食中不可或缺的一部分。高肉含量的生骨肉冻干不仅易吸收、好消化&#xff0c;更能给猫提供其他猫粮所不能提供的微量物质&#xff0c;更满足猫的全面营养需求。然而&#xff0c;面对市面上琳琅满目的生骨肉冻干品…

鸿蒙OS实例:同步获取应用配置的【versionCode和versionName】

1.同步方式获取 首先需要导包&#xff1a; import bundleManager from ohos.bundle.bundleManager 工具类&#xff1a; public static async getVersionName(): Promise<string> {try {let bundleInfo await bundleManager.getBundleInfoForSelf(bundleManager.Bundle…

从零开始搭建游戏服务器 第七节 创建GameServer

目录 前言正文创建GameServer模块修改配置创建NettyClient连接到登录服登录服修改创建协议游戏服注册到登录服 总结 前言 上一节我们使用自定义注解反射简化了协议解包和逻辑处理分发流程。 那么到了这里登录服登录服的架构已经搭建的差不多了&#xff0c;一些比较简单的、并发…

Android卡顿掉帧问题分析之实战篇

本文将结合典型实战案例&#xff0c;分析常见的造成卡顿等性能问题的原因。从系统工程师的总体角度来看 &#xff0c;造成卡顿等性能问题的原因总体上大致分为三个大类&#xff1a;一类是流程执行异常&#xff1b;二是系统负载异常&#xff1b;三是编译问题引起。 1 流程执行异…

AIGC工具系列之——基于OpenAI的GPT大模型搭建自己的AIGC工具

今天我们来讲讲目前非常火的人工智能话题“AIGC”&#xff0c;以及怎么使用目前的AI技术来开发&#xff0c;构建自己的AIGC工具 什么是AIGC&#xff1f; AIGC它的英文全称为(Artificial Intelligence Generated Content)&#xff0c;中文翻译过来就是“人工智能生成内容”&…

共射极放大电路理论计算

目录&#xff1a; 1、概述 2、理论计算 3、Multisim仿真验证 1&#xff09;静态工作点与放大倍数 2&#xff09;输入阻抗仿真 1、概述 如下图所示的共射极放大电路&#xff0c;本内容主要计算静态工作点电压、电压放大倍数与输入输出阻抗。 2、理论计算 列出方程如下&am…

Apache Hive的基本使用语法

一、数据库操作 创建数据库 create database if not exists myhive;查看数据库 use myhive; desc database myhive;创建数据库并指定hdfs存储 create database myhive2 location /myhive2;删除空数据库&#xff08;如果有表会报错&#xff09; drop database myhive;…

【机器学习300问】54、如何找到有效的组合特征?

一、为什么需要去寻找有效的组合特征&#xff1f; 因为并不是所有的特征组合都会意义&#xff0c;都能带来价值。 例如在房价预测场景中&#xff0c;卧室数量和浴室数量的比值有意义&#xff0c;但房屋面积与建造年份相组合作为新的组合特征&#xff0c;可能就没有实际含义&…

Redis中的事件(二)

文件事件 文件事件的处理器 Redis为文件事件编写了多个处理器&#xff0c;这些事件处理器分别用于实现不同的网络通信需求&#xff0c;比如说: 1.为了对连接服务器的各个客户端进行应答&#xff0c;服务器要为监听套接字关联连接应答处理器2.为了接收客户端传来的命令请求&a…

零基础自学C语言|文件操作

✈为什么使用文件&#xff1f; 如果没有文件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失了&#xff0c;等再次运行程序&#xff0c;是看不到上次程序的数据的&#xff0c;如果要将数据进行持久化…

EFI Driver Model(下)-SCSI 驱动设计

1、SCSI简介 SCSI是Small Computer System Interface&#xff08;小型计算机系统接口&#xff09;的缩写&#xff0c;使用50针接口&#xff0c;外观和普通硬盘接口有些相似。SCSI硬盘和普通IDE硬盘相比有很多优点&#xff1a;接口速度快&#xff0c;并且由于主要用于服务器&…

记一次Tomcat启动失败的经历

首先&#xff0c;下载tomcat10.1.20后&#xff0c;双击启动bin下的startup.bat闪退&#xff0c;查了资料&#xff0c;说是依赖JDK环境和JRE环境&#xff0c;当然&#xff0c;我Java是能正常用的&#xff0c;毕竟写了这么多东西它有没有我还不清楚吗 可问题就来了&#xff0c;既…

软件应用实例,租赁系统软件操作教程,脚手架租赁管理集装箱租赁管理系统教程

软件应用实例&#xff0c;租赁系统软件操作教程&#xff0c;脚手架租赁管理集装箱租赁管理系统教程 一、前言 以下软件操作教程以&#xff0c;佳易王租赁管理系统软件V17.0为例说明 件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 1、软件可以记录&#x…

GEE土地分类——分类后样本点值提取至点过程中,导出的csv数据表中不存在geometry的位置信息

值提取至点导出的csv数据表中不存在geometry的位置信息 错误提示: {"type":"MultiPoint","coordinates":[]} 问题分析 问题主要出现在在reduceregions中所使用的第二个参数中。在reduceregions中,第二个参数用于指定geometry信息,以便将r…

约克中央空调YES-will系列,舒适冷暖与高品质家居的优选

漫漫寒冬,室内一片寒意,开启空调多久才能享受到暖意?如果冬季气温较低,空调能否保持正常的制热运行? 炎炎夏季,即便在室内也同样是“暴汗”不断,身上黏糊糊,什么样的家用中央空调才能快速制冷,让全家人感受到舒适,同时又能避免传统空调直吹带来的一系列问题? 遇上梅雨季节…

【采购季】全网云服务器采购季活动大盘点 网站博客搭建、程序员职场毕业神器 低至50/年 阿里云 京东云 腾讯云

《最新对比表》已更新在文章头部—腾讯云文档&#xff0c;文章具有时效性&#xff0c;请以腾讯文档为准&#xff01; 【腾讯文档实时更新】云服务器1分钟教会你如何选择教程 2024-开年采购活动 云服务器专区 京东云 阿里云 腾讯云 配置最新价格表 与 官方活动地址 ​ 当前活动…

双碳目标下基于全球模式比较计划CMIP6与区域气候-化学耦合模式WRF-Chem的未来大气污染变化模拟教程

原文链接&#xff1a;双碳目标下基于全球模式比较计划CMIP6与区域气候-化学耦合模式WRF-Chem的未来大气污染变化模拟教程https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247599209&idx7&sn2fb78bcb18e6ec709853a7595d8822d9&chksmfa82058ecdf58c9852bf4…

鸿蒙HarmonyOS应用开发之使用Node-API接口进行异步任务开发

场景介绍 napi_create_async_work是Node-API接口之一&#xff0c;用于创建一个异步工作对象。可以在需要执行耗时操作的场景中使用&#xff0c;以避免阻塞主线程&#xff0c;确保应用程序的性能和响应性能。例如以下场景&#xff1a; 文件操作&#xff1a;读取大型文件或执行复…

朋友圈运营攻略,还有多号群发朋友圈教程

为什么需要打造朋友圈&#xff1f; 私域朋友圈运营运营者和私域流量理论上其实就是“网友”的关系 要维持稳定的社交关系&#xff0c;做好私域流量运营&#xff0c;就必须持续地进行自身价值塑造&#xff01;而朋友圈就是最好的“战场” 打造优质朋友圈的关键点&#xff1a; …
最新文章