数据结构——二叉树的链式结构

news2024/11/27 8:37:36

 个人主页日刷百题

系列专栏〖C语言小游戏〗〖Linux〗〖数据结构〗 〖C语言〗

🌎欢迎各位点赞👍+收藏⭐️+留言📝 

一、二叉树的创建

这里我们使用先序遍历的思想来创建二叉树,这里的内容对于刚接触二叉树的朋友可能有些难理解,不妨先看完下面的二叉树各种遍历再来看创建就会简单很多,为了保持文章的整体性,先讲二叉树的创建。

当然为了后续内容能够衔接,我们先手动创建一个固定的树,就是上面这棵树,后续内容全部围绕这棵树

typedef int DataType;
typedef struct TreeNode
{
	DataType data;
	struct  TreeNode* left;
	struct  TreeNode* right;
}TreeNode;
TreeNode* BuyNode(int x)
{
	TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
	if (node == NULL)
	{
		perror("malloc fail:");
		return NULL;
	}
	node->data = x;
	node->left = node->right = NULL;
}
	
TreeNode* CreatTree()
{
	TreeNode* node1 = BuyNode(1);
	TreeNode* node2 = BuyNode(2);
	TreeNode* node3 = BuyNode(3);
	TreeNode* node4 = BuyNode(4);
	TreeNode* node5 = BuyNode(5);
	TreeNode* node6= BuyNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;
	return node1;
}

现在开始讲如何用前序遍历方式来进行创建二叉树,这里给俩种创建方法。

1.1  返回根节点指针创建

注:我们用前序遍历方式输入数字,-1代表空,上面的二叉树可写为:1 2 3 -1 -1 -1 4 5 -1 -1 6 -1 -1

TreeNode* CreatTree()
{
	int i;
	scanf("%d", &i);
	

	if (i == -1)
	{
		return NULL;
	}
	TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
	if (root == NULL)
	{
		perror("malloc fail:");
		exit(-1);
	}
	root->data = i;
	root->left =  CreatTree();
	root->right = CreatTree();
	return root;
}

注:return root 是不能省略的,递归返回时,遇到空返回;或者构建完子数,返回根节点,作为上一级左子树,构建完子树,返回根节点,作为上一级右子树,依次递归回去,直到返回整个数的根节点

1.2 二级指针传参创建

同样的,依然构建上面的而二叉树,用前序遍历方式输入数字,-1代表空,上面的二叉树可写为:1 2 3 -1 -1 -1 4 5 -1 -1 6 -1 -1

void CreatTree(TreeNode** root)
{
	int i;
	scanf("%d", &i);
	if (i == -1)
	{
		*root = NULL;
	}
	else
	{
		*root = (TreeNode*)malloc(sizeof(TreeNode));
		if (*root == NULL)
		{
			perror("malloc fail:");
			exit(-1);
		}
		(*root)->data = i;
		CreatTree(&((*root)->left));
		CreatTree(&((*root)->right));
	}

}

 注:二级指针传参可以改变一级指针的指向,同样的,这里创建好根节点后,创造左子树,在创造右子树,依次递归下去。

二、二叉树的遍历

我们先从最简单的操作----遍历学起。所谓二叉树遍历(Traversal)就是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个节点有且只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。二叉树的遍历分为四种:前序遍历、中序遍历、后序遍历和层序遍历。

2.1 前序遍历

前序遍历(Preorder Traversal)又称先根遍历,即先遍历根结点,再遍历左子树,最后遍历右子树。而对于子树的遍历,也服从上述规则。利用递归,我们可以很快地写出代码:

// 二叉树前序遍历
void PreOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return ;
	}
	printf("%d ", root->data);
	 PreOrder(root->left);
	 PreOrder(root->right);

}

便于我们更好的理解,我们画出递归展开图

2.2 中序遍历

中序遍历(Inorder Traversal)又称中根遍历,即先遍历左子树,再遍历根结点,最后遍历右子树。同样,子树的遍历规则也是如此。递归代码如下:

// 二叉树中序遍历
void InOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return ;
	}
	
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

2.3 后序遍历

后序遍历(Inorder Traversal)又称后根遍历,即先遍历左子树,再遍历右子树,最后遍历根结点递归代码如下:

// 二叉树后序遍历
void PostOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return ;
	}

	PostOrder(root->left);
	
	PostOrder(root->right);
	printf("%d ", root->data);
}

2.4  层序遍历

除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在
层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推, 自上而下,自左至右逐层访问树的结点 的过程就是层序遍历。
层序遍历借助队列实现,思路解析图如下:
//层序遍历
void LevelOrder(TreeNode* root)
{
	Queue pq;
	QueueInit(&pq);
	if (root == NULL)
	{
     QueueDestroy(&pq);
		return;
	}
	QueuePush(&pq,root);
	while (!QueueEmpty(&pq))
	{
		TreeNode* front= QueueFront(&pq);
		printf("%d ", front->data);
		 QueuePop(&pq);
		 if (front->left!= NULL)
		 {
			 QueuePush(&pq, front->left);
			 
		 }
		 if (front->right != NULL)
		 {
			 QueuePush(&pq, front->right);
		 }
	}
	

	QueueDestroy(&pq);
}

思考:当然层序遍历这里有一个变形,我们能不能将二叉树每一层打印单独放一行,该怎么做呢?

思路:

(1)设二叉树的根节点所在层数为1,第一层根节点进队列,队列元素个数为1,size==1
(2)每出队列一次,size--,根节点出完队列,俩个子节点进队列,此时队列元素个数为第二层节点个数,size==2
(3)当我们出队列size次,把第二层元素出完,队列剩下的元素是第三层节点,size==QueueSize
以此类推,以size为循环条件,则可实现每层单独打印一行

void LevelOrder(TreeNode* root)
{
	Queue pq;
	QueueInit(&pq);
	if (root == NULL)
	{
		QueueDestroy(&pq);
		return;
	}
	QueuePush(&pq,root);
	int size = 1;
	while (!QueueEmpty(&pq))
	{
		while (size--)
		{
			TreeNode* front = QueueFront(&pq);
			printf("%d ", front->data);
			QueuePop(&pq);
			if (front->left != NULL)
			{
				QueuePush(&pq, front->left);

			}
			if (front->right != NULL)
			{
				QueuePush(&pq, front->right);
			}
		}
		size = QueueSize(&pq);
		printf("\n");
	}
	

	QueueDestroy(&pq);
}

三、二叉树的结点

3.1 二叉树的总结点数

一颗二叉树的结点数我们可以看作是根结点+左子树结点数+右子树结点数,那左右子树的结点数又是多少呢?按照相同的方法继续拆分,层层递归直到左右子树为空树,返回空树的结点数0即可。递归代码如下:

// 二叉树节点个数
int TreeSize(TreeNode* root)
{
	return root == NULL? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

3.2 二叉树的叶子结点数

左右子树都为空的结点即是叶子结点。这里分为两种情况:左右子树都为空和左右子树不都为空。

(1)当左右子树都为空时,则这颗树的叶子结点数为1(根节点)。

(2)当左右子树不都为空,即根结点不是叶子结点时,这棵树的叶子结点数就为左子树叶子结点数+右子树叶子结点数(空树没有叶子结点)。
 

// 二叉树叶子节点个数
int  TreeLeafSize(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

3.3 二叉树第k层结点数

一颗树第k层的结点数我们可以拆分为其左子树第k-1层结点+右子树第k-1层结点(注:当前节点为第一层)

(1)若为空树,无论哪层节点数都是0

(2)若不是空树且k==1,则只有一个节点(根节点)

// 二叉树第k层节点个数
int TreeLevelKSize(TreeNode* root, int k)
{
	assert(k > 0);
	if (root!=NULL&&k == 1)
	{
		return 1;
	}
	if (root == NULL)
	{
		return 0;
	}
	if (k > 1)
	{
		return TreeLevelKSize(root->left, k - 1) + TreeLevelKSize(root->right, k - 1);
	}
}
// 判断二叉树是否是完全二叉树
bool TreeComplete(TreeNode* root)
{
	Queue pq;
	QueueInit(&pq);
	if (root == NULL)
	{
		QueueDestroy(&pq);
		return;
	}
	QueuePush(&pq, root);
	
	while (!QueueEmpty(&pq))
	{
		
			TreeNode* front = QueueFront(&pq);
			
			QueuePop(&pq);
			if (front == NULL)
			{
				break;
			}
				QueuePush(&pq, front->left);

			
				QueuePush(&pq, front->right);
			
	}
	while (!QueueEmpty(&pq))
	{
		TreeNode* front = QueueFront(&pq);

		QueuePop(&pq);
		if (front != NULL)
		{
			return false;
		}
	}
	QueueDestroy(&pq);
	return true;
}

四、二叉树的查找

二叉树的查找本质上就是一种遍历,只不过是将之前的打印操作换为查找操作而已。我们可以使用前序遍历来进行查找:

(1)先比较根结点是否为我们要查找的结点,如果是,返回根节点地址

(2)如果不是,遍历左子树,如果左子树是,直接返回节点地址;不是则遍历右子树,如果右子树是,直接返回节点地址,不是返回空,说明左右子树都没找到。

// 二叉树查找值为x的节点
TreeNode* TreeFind(TreeNode* root, DataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	TreeNode* node1 = TreeFind(root->left, x);
	if (node1)
	{
		return node1;
	}
	TreeNode* node2 = TreeFind(root->right, x);
	if (node2)
	{
		return node2;
	}
	return NULL;
}

五、二叉树的高度/深度

树中结点的最大层次称为二叉树的高度。因此,一颗二叉树的高度我们可以看作是

1(根结点)+左右子树高度的较大值。层层递归下去直到遇到空树返回0即可,

这里值得注意的是:不要写成

return TreeHeight(root->left)>TreeHeight(root->right) ? 
		TreeHeight(root->left)+1:TreeHeight(root->right)+1;
}
这里比较好左右子树较大的一颗后,又会从新递归较大那颗子树高度,会造成重复计算,时间复杂度增高。

我们要保存好左右子树层数,避免重复计算,代码如下:

//二叉树的高度
int TreeHeight(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int left = TreeHeight(root->left);
	int right = TreeHeight(root->right);
	return  left>right ? 
		left+1:right+1;
}

六、完全二叉树的判断

这里的思路利用了层序遍历,不同的是,将空节点指针也入队列,当我们遇到第一个空节点指针则退出循环,然后对队列进行检测,若第一个空节点指针以后全都是空,则为完全二叉树,反之,不为完全二叉树。

注:当在队列遇到第一个空节点指针时,二叉树中空节点指针之后所有非空节点指针全部进队列

思路解析图如下:

代码如下:

// 判断二叉树是否是完全二叉树
bool TreeComplete(TreeNode* root)
{
	Queue pq;
	QueueInit(&pq);
	if (root == NULL)
	{
		QueueDestroy(&pq);
		return;
	}
	QueuePush(&pq, root);
	
	while (!QueueEmpty(&pq))
	{
		
			TreeNode* front = QueueFront(&pq);
			
			QueuePop(&pq);
			if (front == NULL)
			{
				break;
			}
				QueuePush(&pq, front->left);

			
				QueuePush(&pq, front->right);
			
	}
	while (!QueueEmpty(&pq))
	{
		TreeNode* front = QueueFront(&pq);

		QueuePop(&pq);
		if (front != NULL)
		{
			return false;
		}
	}
	QueueDestroy(&pq);
	return true;
}

 七、二叉树的销毁

7.1 一级指针传参销毁

同样的,和创建节点一样,我们给出俩个销毁方式:

(1)一种是传一级指针方式,这种方式不是改变根节点的指向,需要在销毁函数结束后,将root置为NULL

void TreeDestroy(TreeNode* root)//出来将root=NULL
{
	if (root == NULL)
	{
		return;
	}
	TreeDestroy(root->left);
	TreeDestroy(root->right);
	free(root);

}

7.2 二级指针传参销毁 

(2)另一种是传二级指针,直接在函数内部将每一个销毁的节点指针置为NULL.

void TreeDestroy(TreeNode** root)
{
	if (*root == NULL)
	{
		return;
	}
	TreeDestroy(&(*root)->left);
	TreeDestroy(&(*root)->right);
	free(*root);
	*root = NULL;
}

 总结:本篇文章将二叉树的基础知识差不多囊括了,后续的话还需要大量练习做巩固加强,递归比较抽象难以理解,需要动手画递归展开图进行帮助理解。

希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,百题一定会认真阅读!

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

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

相关文章

将单体应用程序迁移到微服务

多年来,我处理过多个单体应用,并将其中一些迁移到了微服务架构。我打算写下我所学到的东西以及我从经验中用到的策略,以实现成功的迁移。在这篇文章中,我将以AWS为例,但基本原则保持不变,可用于任何类型的基…

医院信息系统源码,采用JAVA编程,支持跨平台部署应用,满足一级综合医院(专科二级及以下医院500床)的日常业务应用

医院HIS系统源码,HIS系统全套源码,支持电子病历4级,自主版权 his医院信息系统内设门诊/住院医生工作站、门诊/住院护士工作站。各工作站主要功能依据职能要求进行研发。如医生工作站主要功能为编辑电子病历、打印、处理医嘱;护士工…

LAMP安装部署网站

目录 什么是LAMP? 实验(搭建一个论坛) 一,安装apache 1.关闭防火墙,将安装Apache所需软件包传到/opt目录下 2.安装环境依赖包 3.配置软件模块 4.编译及安装 5.优化配置文件路径,并把httpd服务的可执行程序文件…

本科毕业论文查重的依据

大家好,今天来聊聊本科毕业论文查重的依据,希望能给大家提供一点参考。 以下是针对论文重复率高的情况,提供一些修改建议和技巧: 本科毕业论文查重依据:维护学术诚信的基石 摘要: 本科毕业论文是衡量学生学…

“探究HarmonyOS:深入解析鸿蒙操作系统架构”

前言 一、鸿蒙操作系统是什么? 二、为什么要学习鸿蒙操作系统 1.从开发者角度看: 2.从使用者角度看: 总结 前言 随着智能化时代的到来,操作系统的发展也越来越快,人们对于智能化生活的需求也越来越强烈。鸿蒙操作系统作…

idea__SpringBoot微服务05——JSR303校验(新注解)(新的依赖),配置文件优先级,多环境切换

JSR303校验,配置文件优先级,多环境切换 一、JSR303数据校验二、配置文件优先级三、多环境切换一、properties多环境切换二、yaml多环境切换————————创作不易,如觉不错,随手点赞,关注,收藏(*&#x…

华为数通---配置Smart Link主备备份示例

定义 Smart Link,又叫做备份链路。一个Smart Link由两个接口组成,其中一个接口作为另一个的备份。Smart Link常用于双上行组网,提供可靠高效的备份和快速的切换机制。 目的 下游设备连接到上游设备,当使用单上行方式时&…

HarmonyOS应用程序框架——UIAbility实操

UIAbility概述 UIAbility是一种包含用户界面的应用组件,主要用于和用户进行交互。UIAbility也是系统调度的单元,为应用提供窗口在其中绘制界面。 每一个UIAbility实例,都对应于一个最近任务列表中的任务。 一个应用可以有一个UIAbility&…

【LuatOS】笔记(二)基础框架

开发环境搭建 合宙官方搭建的是:vscodeLuatOS-SOC推荐拓展包(vscode插件),原文链接:LuatOS开发环境搭建。安装完创建项目文件,创建main.lua文件,就可以开始编写了。 函数与使用 LuatOS-SOC接口文档1,该文档…

OpenCL学习笔记(四)手动编译开发库(ubuntu+gcc+rk3588)

前言 笔者本次使用的是RK3588的开发板,内部烧写的是ubuntu20.04,gcc版本是9 本文档简单记录下编译的过程,有需要的小伙伴可以参考下 一、安装所需软件 1.安装git,教程比较多,不再重复 2.安装cmake,教程…

SSL 协议

SSL 是用于安全传输数据的一种通信协议。它采用公钥加密技术、对称密钥加密技术等保护两个应用之间的信息传输的机密性和完整性。但是,SSL 也有一个不足,就是它本身不能保证传输信息的不可否认性。 SSL 协议包括服务器认证、客户认证、SSL 链路上的数据完…

【qt】Qt+OpenCv读取带有中文路径的图片

【opencv4.5.1版本】下载exe解压即可。。。https://opencv.org/releases/page/2/ 【qt5.15.2】 pro文件 QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c17# You can make your code fail to compile if it uses deprecated APIs. # In order to …

CentOS 7.9安装宝塔面板,安装gitlab服务器

docker安装方式比较慢,安装包1.3GB 安装后启动很慢 docker logs q18qgztxdvozdv_gitlab-ce-gitlab-1 docker ps docker exec -it q18qgztxdvozdv_gitlab-ce-gitlab-1 sh cd /etc/gitlab cat initial_root_password 软件商店安装方式,失败了2023.12…

日志门面slf4j和各日志框架

简介 简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架, 其主要意义在于提供接口,具体的实现可以交由其他日志框架,如log4j、logback、log4j2。 对于一般的Java项目而言&#xff…

在线教育小程序正在成为教育行业的新生力量

教育数字化转型是目前教育领域的一个热门话题,那么到底什么是教育数字化转型?如何做好教育数字化转型? 教育数字化转型是利用信息技术和数字工具改变和优化教育的过程。主要特征包括技术整合、在线学习、个性化学习、大数据分析、云计算、虚拟…

Linux下通过find找文件---通过修改时间查找(-mtime)

通过man手册查找和-mtime选项相关的内容 man find | grep -A 3 mtime # 这里简单介绍了 -mtime ,还有一个简单的示例-mtime n Files data was last modified n*24 hours ago. See the comments for -atime to understand how rounding affects the interpretati…

芯片-开发板设计相关收集

在高性能计算、消费类电子、通信与汽车应用领域中, SoC是一种主要的芯片产品形态。SoC与ASIC最大的区别就是形成了一个完整的片上系统,其中包括计算、存储、外设以及层次化总线等子系统,由此在一颗芯片上实现了一个完整的计算机系统结构组成 …

常见的中间件--消息队列中间件测试点

最近刷题,看到了有问中间件的题目,于是整理了一些中间件的知识,大多是在小破站上的笔记,仅供大家参考~ 主要分为七个部分来分享: 一、常见的中间件 二、什么是队列? 三、常见消息队列MQ的比较 四、队列…

【开源】基于JAVA的木马文件检测系统

项目编号: S 041 ,文末获取源码。 \color{red}{项目编号:S041,文末获取源码。} 项目编号:S041,文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 木马分类模块2.3 木…

从互联网到云计算再到 AI 原生,百度智能云数据库的演进

1 数据库行业发展概述 如果说今年科技圈什么最火,我估计大家会毫不犹豫选择 ChatGPT。ChatGPT 是 2022 年 11 月 30 日由 OpenAI 发布的聊天应用。它创造了有史以来用户增长最快的纪录:自 11 月 30 日发布起,5 天就拥有了 100 万活跃用户&am…