「数据结构」二叉树1

news2024/11/25 2:28:58

🎇个人主页:Ice_Sugar_7
🎇所属专栏:C++启航
🎇欢迎点赞收藏加关注哦!

文章目录

  • 🍉树
  • 🍉二叉树
    • 🍌特殊二叉树
    • 🍌二叉树的性质
    • 🍌存储结构
  • 🍉堆
    • 🍌堆的结构
    • 🍌插入
      • 🥝向上调整算法
        • 🫐时间复杂度分析
    • 🍌删除
      • 🥝向下调整算法
        • 🫐时间复杂度分析
    • 🍌堆的创建(堆的初始化)
    • 🍌堆排序
    • 🍌top k 问题
  • 🍉写在最后

🍉树

●树是一种非线性的数据结构,它是由n(n>=0)个结点组成,具有层次关系
●有一个特殊的结点,称为根结点,根节点没有前驱结点
●除根节点外,其余结点被分成M(M>0)个互不相交的集合,每个集合是一棵子树

🍉二叉树

二叉树一个非空结点的子树为空或者至多两个子树(左子树和右子树)
在这里插入图片描述
从这个图可以看出:

二叉树不存在度大于2的结点
二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

🍌特殊二叉树

满二叉树每一层结点数都达到最大值的二叉树。如果一棵满二叉树有k层,那它结点总数就是2^k-1
完全二叉树最后一层抠掉几个结点的满二叉树,就是一般的完全二叉树(满二叉树是特殊的完全二叉树)

🍌二叉树的性质

二叉树的性质都在下图了:
在这里插入图片描述
在这里插入图片描述

🍌存储结构

二叉树一般使用两种结构存储:顺序结构链式结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实使用中只有堆才会使用数组来存储
二叉树顺序存储在物理结构上是一个数组,在逻辑结构上是一颗二叉树
我们的代码是按照其物理结构写的,而具体想实现的函数接口则是根据逻辑结构展开的(往下看入堆、出堆、调整等函数之后,你就能理解这句话了)
在这里插入图片描述

本文讲二叉树的顺序存储结构——堆(正文开始)


🍉堆

堆分为两种:大堆和小堆。

大堆:除了叶子结点外,所有结点的孩子都比自己小
小堆:除了叶子结点外,所有结点的孩子都比自己大

根据堆的逻辑结构可知,大堆是上面的结点(位于较低层次的结点)大,小堆是上面的结点小

🍌堆的结构

堆的物理结构就是顺序表,所以代码基本和顺序表一模一样

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

🍌插入

插入这一步很简单,就直接往数组插入元素(注意检查容量是否足够,并且插入后记得让size加1)
插入后,要对这个元素进行调整,采用向上调整

🥝向上调整算法

假设要建一个小堆,那就要拿它和它的双亲进行比较,如果它比双亲小,就和双亲交换位置。假设数组名为_a,大小为size,那插入的结点下标为size-1,它的双亲在数组中的下标就是(size-1-1)/2
这里要用循环,将插入的结点记为child,当child为0时(即它到了堆顶),循环终止。如果中途经比较后发现不用换位置的话,说明调整好了,直接break跳出循环
在这里插入图片描述
代码如下:

typedef int HPDataType;

void Swap(HPDataType* hp1, HPDataType* hp2) {
	HPDataType tmp = *hp1;
	*hp1 = *hp2;
	*hp2 = tmp;
}

//小堆向上调整
void AdjustUp(Heap* hp,int child) {
	assert(hp);
	int parent = (child - 1) / 2;
	while (child > 0) {
		if (hp->_a[child] >= hp->_a[parent]) //孩子比双亲大,退出循环
			break;
		else {
			Swap(&(hp->_a[child]),&(hp->_a[parent]));  //两结点交换
			child = parent;
			parent = (parent - 1) / 2;
		}
	}
}
🫐时间复杂度分析

因为堆是完全二叉树,而满二叉树也是完全二叉树,为了简化问题,就用满二叉树来证明了(时间复杂度本来看的就是近似值,多几个节点不影响最终结果),下面向下调整算法的时间复杂度也这样处理
假设有n个结点,那就有log(n+1)层,那每次向下调整最多遍历log(n+1)次,总共有n个结点,那么就遍历n*log(n+1)次,时间复杂度就是O(N*logN)


🍌删除

不能直接将数组往前挪一位,因为这样虽然在物理结构(数组)上没什么问题,但是在逻辑结构(完全二叉树)上就有问题了,会打乱结点间的关系(比如原先的兄弟现在变为父子,父子变兄弟)
有一个比较巧妙的解决办法,就是将根结点和尾结点(数组最后一个元素)交换位置,然后将新的尾结点删掉,这样就不会影响到结点间的关系了
删掉后要进行向下调整,这就涉及到向下调整算法了

🥝向下调整算法

现在有一个数组,它有n个元素,从逻辑结构上看成是完全二叉树,我们从根结点开始,通过向下调整算法可以把它调整为一个小堆
这种算法的前提是左右子树必须是一个堆,才能调整

int array[] = {27,15,19,18,28,34,65,49,25,37};

调整过程如图:
在这里插入图片描述
每次调整,先比较该结点两个孩子的大小现在要调整为小堆,就先找出较小的孩子,然后这个孩子和双亲进行比较,若孩子<双亲,就把它和双亲交换位置;反之则说明调整完毕

调整的过程显然也要用循环。我们将双亲结点记为parent,左孩子结点记为child(因为左右孩子下标相差1,没必要用leftchild和rightchild进行区分)。那右孩子就是child + 1,不过由于右孩子可能不存在(当child为叶子结点时可能会有这种情况),所以我们得在循环里面判断一下

这里采用假设法,就是我们先假设左孩子是较小的结点(因为右孩子可能不存在,不方便假设),如果右孩子存在的话,就拿左右孩子进行比较,最后将child赋给较小者
(假设法相比于if语句,可以有效简化代码,具体可以看我之前那篇判断相交链表的题解,or看下面的代码也ok)
文章链接:判断相交链表

那循环的终止条件呢?显然当child>=n的时候就要跳出循环了
代码如下:

void AdjustDown(HPDataType* a,int n,int parent) {  //n为数组大小
	assert(a);
	int child = 2 * parent + 1;  //左孩子下标
	while (child < n) {
		if (child + 1 < n && a[child+1] <= a[child]) {  //右结点存在并且右孩子比左孩子小
			child++;  //将child设为右孩子结点,等会儿拿它和双亲进行比较,决定是否交换
		}
		if (a[child] < a[parent]) {
			Swap(&a[child], &a[parent]);
			parent = child;  //更新双亲结点
			child = parent * 2 + 1;  //更新孩子结点
		}
		else
			break;  //不用换位置说明调整完毕
	}
}
🫐时间复杂度分析

在这里插入图片描述
“向下移动”的层数指的是最多要调整几次,即从这个结点开始,一直调整到叶子结点为止(最坏的情况)
从结果可以看出:向下调整的时间复杂度(O(N))比向上调整的小,所以建议使用向下调整


🍌堆的创建(堆的初始化)

建堆既可以建一个空堆,也可以根据一个现成的数组建堆
建空堆就是将数组赋为空指针,然后size和capacity都赋为0。和顺序表初始化一样,不多赘述
这里主要来讲数组建堆
思路是:给堆开辟空间+拷贝+调整
●开空间:数组多大就开多大
●拷贝:使用memcpy将数组的元素拷贝给堆
●调整:向上调整or向下调整

先展示向上调整

void HeapCreate(Heap* hp, HPDataType* a, int n) {
	assert(hp);
	assert(a);
	HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (tmp == NULL) {
		perror("malloc fail");
		exit(-1);
	}
	hp->_a = tmp;
	hp->_capacity = hp->_size = n;
	memcpy(hp->_a, a, n * sizeof(HPDataType));  //把数组数据拷贝到堆的数组中
	int parent = (hp->_size - 1) / 2;
	for (int i = 1; i < n; i++) {  //  调整建堆
		AdjustUp(hp, i);
	}
}

也可以复用刚才写的入堆函数,因为它自带向上调整函数。而且push函数是将数组的元素一个一个放进堆的,这样就不需要memcpy了,代码如下(为方便观察,我把向上调整函数和入堆函数也放在下面):

//小堆向上调整
void AdjustUp(Heap* hp, int child) {
	assert(hp);
	int parent = (child - 1) / 2;
	while (child > 0) {
		if (hp->_a[child] >= hp->_a[parent]) //孩子比双亲大,退出循环
			break;
		else {
			Swap(hp->_a[child], hp->_a[parent]);  //两结点交换
			child = parent;
			parent = (child - 1) / 2;
		}
	}
}

void HeapPush(Heap* hp, HPDataType x) {
	assert(hp);
	//如果满了  那就要扩容
	if (hp->_capacity == hp->_size) {
		int newcapacity = hp->_capacity == 0 ? 4 : 2 * hp->_capacity;
		HPDataType* tmp = (HPDataType*)realloc(hp->_a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL) {
			perror("realloc fail");
			exit(-1);
		}
		hp->_a = tmp;
		hp->_capacity = newcapacity;
	}
	hp->_a[hp->_size] = x;
	hp->_size++;
	AdjustUp(hp, hp->_size - 1);
}

void HeapCreate(Heap* hp, HPDataType* a, int n) {
	assert(hp);
	HeapInit(hp);  //初始化为空堆
	for (int i = 0; i < n; ++i) {
		HeapPush(hp, a[i]);
	}
}

由于向上调整的效率不及向下调整,所以建议采用向下调整建堆
向下调整要求左子树和右子树也都是堆,又因为单个叶子结点既可以看作是大堆,也可以看成小堆,所以我们从叶子结点的双亲开始向下调整

比如下面这个数组要建一个大堆

int a[] = {4,3,5,7,2,6,8,65,100,70,32,50,60};

在这里插入图片描述
调整后,红色方框内就是一个大堆了,对于3,5这两个结点而言,左右子树都是大堆,那它们也可以向下调整了
代码如下:

void HeapCreate(Heap* hp, HPDataType* a, int n) {
	assert(hp);
	HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (tmp == NULL) {
		perror("malloc fail");
		exit(-1);
	}
	hp->_a = tmp;
	hp->_capacity = hp->_size = n;
	memcpy(hp->_a, a, n * sizeof(HPDataType));  //把数组数据拷贝到堆的数组中
	int parent = (hp->_size - 1 - 1) / 2;  //最后一个结点的双亲结点
	for (int i = parent; i >= 0;i--) {     //从该结点开始进行向下调整
		AdjustDown(hp->_a, n, i);
	}
}

🍌堆排序

现在有一个数组,要把它排成升序
如果建小堆,那么很容易就可以找到最小的元素。但是要找次小元素的时候,把数组剩下的元素看作完全二叉树的话,它们之间的关系会乱掉

●所以要建大堆,建好后最大的元素就在根结点,将它和最后一个结点交换,就把最大的元素排好了
●然后size-1剔除最大的元素,对于剩下的元素,因为根结点的左右子树也都是大堆,可以采用向下调整,调整后可以把第二大的元素移动到堆顶(根结点),再和最后一个结点交换,第二大元素就排好了
●剩下的元素也如法炮制

void HeapSort(int* a, int k) {  //a为给定数组
	for (int i = (k - 1 - 1) / 2; i >= 0; i--) {   //调整为一个堆
		AdjustDown(a, k, i);
	}

	for (int i = k - 1; i >= 0; i--) {  //采用删除结点的思想,先交换,再调整
		Swap(a[0], a[i]);
		AdjustDown(a, i, 0);
	}
}

排序后得到:
在这里插入图片描述


🍌top k 问题

这个问题就是要找出数组中从大到小(或从小到大)的前k个数,下面以从大到小为例
如果要找从大到小的前k个数,我们可以先从数组中选k个数,建一个大小为k的小堆,然后将数组中剩下的数和堆顶的数进行比较,如果比它大,就替代它,然后向下调整。
这个方法的原理是:放一个比较小的数“卡”在堆顶,类似守门员,比它大的数就能进堆,不断把堆中较小的数踢出去,到最后就留下最大的前k个数

//取最大的前k个
void TopK(HPDataType* pa, int n, int k) {
	Heap ph;
	HeapInit(&ph);
	HeapCreate1(&ph, pa, k);  //建小堆
	for (int i = k; i < n; i++)  //遍历剩下的元素
	{
		if (pa[i] > ph._a[0]) {
			ph._a[0] = pa[i];
			AdjustDown(ph._a, k, 0);  //小堆向下调整
		}
	}
	HeapSort(ph._a, k);  //将得到前k数进行排序
	HeapPrint(&ph);
}

🍉写在最后

以上就是本篇文章的全部内容,如果你觉得本文对你有所帮助的话,那不妨点个小小的赞哦!(比心)

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

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

相关文章

通过层进行高效学习:探索深度神经网络中的层次稀疏表示

一、介绍 深度学习中的层次稀疏表示是人工智能领域日益重要的研究领域。本文将探讨分层稀疏表示的概念、它们在深度学习中的意义、应用、挑战和未来方向。 最大限度地提高人工智能的效率和性能&#xff1a;深度学习系统中分层稀疏表示的力量。 二、理解层次稀疏表示 分层稀疏表…

【Unity】运行时创建曲线(贝塞尔的运用)

[Unity]运行时创建线&#xff08;贝塞尔的运用&#xff09; 1. 实现的目标 在运行状态下创建一条可以使用贝塞尔方法实时编辑的网格曲线。 2. 原理介绍 2.1 曲线的创建 unity建立网格曲线可以参考Unity程序化网格体的实现方法。主要分为顶点&#xff0c;三角面&#xff0c…

22.JSP技术

JSP起源 在很多动态网页中&#xff0c;绝大部分内容都是固定不变的&#xff0c;只有局部内容需要动态产生和改变。如果使用Servlet程序来输出只有局部内容需要动态改变的网页&#xff0c;其中所有的静态内容也需要程序员用Java程序代码产生&#xff0c;整个Servlet程序的代码将…

智能优化算法应用:基于阿基米德优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于阿基米德优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于阿基米德优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.阿基米德优化算法4.实验参数设定…

智能 GPT 图书馆又重生了

智能 GPT 图书馆又重生了 作者&#xff1a;程序员小白条 1&#xff09;概述 自从大二寒假准备开始筹备这个项目&#xff0c;到现在已经一年了&#xff0c;这个项目能维护一年&#xff0c;不愧是我.jpg。本来这个项目只是想练练手&#xff0c;因为那时候刚学完 Spring Boot2 V…

构建强大应用的引擎:深度解析Spring Boot Starter机制

目录 引言1. Spring Boot Starter机制1.1 什么是Spring Boot Starter1.2 为什么要使用Spring Boot Starter1.3.应用场景1.4.自动加载核心注解说明 2. 综合案例配置类制作控制功能实现 总结 引言 在当今互联网时代&#xff0c;构建高性能、可维护的应用已成为开发者的首要任务。…

Ansible自动化运维以及模块使用

ansible的作用&#xff1a; 远程操作主机功能 自动化运维(playbook剧本基于yaml格式书写) ansible是基于python开发的配置管理和应用部署工具。在自动化运维中&#xff0c;现在是异军突起 ansible能够批量配置、部署、管理上千台主机。类似于Xshell的一键输入工具。不需要每…

vscode如何开发微信小程序?(保姆级教学)

1.安装“微信小程序开发工具”扩展 2.安装“vscode weapp api”扩展 3.安装“vscode wxml”扩展 4.安装“vscode-wechat”扩展 5.在终端执行命令&#xff1a; vue create -p dcloudio/uni-preset-vue uniapp-test uniapp-test就是我这里的项目名称了 6.如果遇到了这个错误&a…

【C盘清理】Jetbrains全家桶(PyCharm、Clion……)更改 IDE 特定文件(配置、缓存、插件、日志等)存储位置

文章目录 一、官网说明二、更改 IDE 目录的位置1. 转到“帮助”|“编辑自定义属性”2. 各文件位置3. 以PyCharm系统目录为例4. 修改idea.properties 三、清理旧的 IDE 目录 一、官网说明 IDE 使用的目录官网说明 二、更改 IDE 目录的位置 默认情况下&#xff0c;PyCharm 将每…

c语言:输出26个英文字母|练习题

一、题目 分两排&#xff0c;输出26个英文字母 如图&#xff1a; 二、思路分析 1、从第13个字母分行显示 2、从A开始&#xff0c;在A的ASC码后面&#xff0c;按顺序加1~26 三、代码图片【带注释】 四、源代码【带注释】 #include <stdio.h> //题目:输入26个字母&#x…

企业电子招标采购系统源码Spring Cloud + Spring Boot + 前后端分离 + 二次开发

项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及审…

各种不同语言分别整理的拿来开箱即用的8个开源免费单点登录(SSO)系统

各种不同语言分别整理的拿来开箱即用的8个开源免费单点登录&#xff08;SSO&#xff09;系统。 单点登录&#xff08;SSO&#xff09;是一个登录服务层&#xff0c;通过一次登录访问多个应用。使用SSO服务可以提高多系统使用的用户体验和安全性&#xff0c;用户不必记忆多个密…

2023_Spark_实验二十九:Flume配置KafkaSink

实验目的&#xff1a;掌握Flume采集数据发送到Kafka的方法 实验方法&#xff1a;通过配置Flume的KafkaSink采集数据到Kafka中 实验步骤&#xff1a; 一、明确日志采集方式 一般Flume采集日志source有两种方式&#xff1a; 1.Exec类型的Source 可以将命令产生的输出作为源&…

HarmonyOS4.0从零开始的开发教程18后台代理提醒

HarmonyOS&#xff08;十六&#xff09;后台代理提醒 简介 随着生活节奏的加快&#xff0c;我们有时会忘记一些重要的事情或日子&#xff0c;所以提醒功能必不可少。应用可能需要在指定的时刻&#xff0c;向用户发送一些业务提醒通知。例如购物类应用&#xff0c;希望在指定时…

搭建Eureka服务

搭建Eureka服务 文章目录 搭建Eureka服务搭建EurekaServer注册user-service注册多个实例 在order-service中完成服务拉取和负载均衡 搭建EurekaServer <dependency><!--eureka服务器--><groupId>org.springframework.cloud</groupId><artifactId>…

QUIC在零信任解决方案的落地实践

一 前言 ZTNA为以“网络为中心”的传统企业体系架构向以“身份为中心”的新型企业安全体系架构转变&#xff0c;提供解决方案。随着传统网络边界不断弱化&#xff0c;企业SaaS规模化日益增多&#xff0c;给终端安全访问接入创造了多元化的空间。其中BYOD办公方式尤为突出&#…

什么是uniapp?如何开发uniapp?

大家好&#xff01;我是咕噜铁蛋&#xff01;随着移动应用市场的持续发展&#xff0c;开发者们面临着不断增长的需求和多样化的平台选择。在这个背景下&#xff0c;UniApp应运而生&#xff0c;成为一种跨平台开发框架&#xff0c;为开发者提供了一种高效、简便的方式来开发移动…

前端框架的单文件组件(Single File Component)

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

机器学习之线性回归(Linear Regression)

概念 线性回归(Linear Regression)是机器学习中的一种基本的监督学习算法,用于建立输入变量(特征)与输出变量(目标)之间的线性关系。它假设输入变量与输出变量之间存在线性关系,并试图找到最佳拟合线来描述这种关系。 在简单线性回归中,只涉及两个变量:一个是自变量…

【赠书活动】OpenCV4工业缺陷检测的六种方法

文章目录 前言机器视觉缺陷检测工业上常见缺陷检测方法延伸阅读推荐语 赠书活动 前言 随着工业制造的发展&#xff0c;对产品质量的要求越来越高。工业缺陷检测是确保产品质量的重要环节&#xff0c;而计算机视觉技术的应用能够有效提升工业缺陷检测的效率和精度。 OpenCV是一…