链式二叉树(二叉树看这一篇就够了)

news2024/12/23 18:16:24

        顾名思义就是使用链式存储来实现的二叉树,因为二叉树是递归定义的,所以二叉树的实现中,都是会使用递归来完成.这里面需要一些前置的二叉树理论知识,对这部分不是很理解的可以先看下这篇二叉树的概念.

下面开始进入正题了:


1.二叉树的创建

        假定现有"ABD##E#H##CF##G##"这样的一组的数据,'#'代表为NULL,意味着其为空结点.使用前序遍历的方式来完成二叉树的创建.

        首先要知道二叉树要看成三个部分: 根,左子树,右子树,树的每个结点都可以看作是根,向下分出的子结点是左右子树,左,右子树再分别继续看成是根,左子树,右子树,一直分叉下去,直到空.

        前序遍历可以说是深度优先的遍历方式,遍历的顺序是根->左子树->右子树.

        这是通过上面一组数据而画的树的图,从根节点从'A'开始,分出左子树'B',以左子树为根,又分出左子树'D',此时遇到了两个'#'代表结点的左右子树为空,开始返回到开始的'B'结点,分出右子树'E',以右子树'E'为根,左树遇到了'#'代表'E'的左子树为空,继续分出右子树'H',以'H'为根结点,此时遇到了两个'#'表示左右子树为空,开始回到最初的根节点'A'.

        此时左子树已经遍历生成完了.回到了最开始的'A'根结点,'A'分出右子树'C',以'C'为根节点分出左子树'F',遇'#',表示子节点为空,'F'的左右子树为空,回到'C'结点,分出右子树'G',遇'#',表示子节点为空,此时数据走完,树也就创建好了.

        这里理解起来可能比较复杂,需要自己多画图理解几遍,因为树的内容比较抽象.把树分成根,左子树,右子树,理解了这个,上述遍历应该就问题不大了.

下面是代码的实现:

typedef struct TreeNode
{
	struct TreeNode* left;
	struct TreeNode* right;
	int val;
}Node;
 
//构建先序二叉树
Node* constructTree(char* a, int* i) 
{
	if (a[(*i)] == '#')
	{
		(*i)++;
		return NULL;
	}
	Node* root = (Node*)malloc(sizeof(Node));
	root->val = a[(*i)];
	(*i)++;

	root->left = constructTree(a, i);
	root->right = constructTree(a, i);
	return root;
}

整体的逻辑就是: 

        首先判断当前字符是否为'#',如果是,则将指针i向后移动一位,并返回空指针,表示当前节点为空。

        如果当前字符不是'#',则创建一个新的节点,并将当前字符赋值给节点的val属性。然后将指针i向后移动一位。

        接下来,递归调用constructTree函数构建节点的左子树,将返回的节点赋值给当前节点的left属性。

        再次递归调用constructTree函数构建节点的右子树,将返回的节点赋值给当前节点的right属性。

        最后,返回根节点。

        这样,通过递归调用constructTree函数,可以根据给定的字符数组构建一棵二叉树


2二叉树的前,中,后序遍历

        所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。

        遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。因此理解和掌握二叉树是极其重要的,着会帮助你能更好的明白树的结构和后面树的更加复杂的运算.

前序/中序/后序的递归结构遍历:

  1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
  2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
  3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

        由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

上述都是遍历的一些理论知识,说明了树遍历方式的方式还是很多的,下面开始进入实战部分:

        如图所示,顺序是:根->左子树->右子树.

        遍历到一个结点后,继续以此结点为根,遍历左子树,直到一直递归到空,开始返回,再开始遍历右子树.这个图需要大家好好理解一下,可以利用图来明白树的遍历究竟是怎么回事.

        明白了树的前中后序是怎么回事,就可以开始着手来写代码实现了.

//前序
void Preorder(BTnode* root)
{
	if (root == NULL)
		return;
	printf("%c", root->val);
	Preorder(root->left);
	Preorder(root->right);
}

//中序 
void Inorder(BTnode* root)
{
	if (root == NULL)
		return;
	Inorder(root->left);
	printf("%c", root->val);
	Inorder(root->right);
}

//后序
void Postorder(BTnode* root)
{
	if (root == NULL)
		return;
	Postorder(root->left);
	Postorder(root->right);
	printf("%c ", root->val);
}

        首先判断树为不为空,为空就返回,没必要再往下递归了.当不为空,根据前中后序的不同,遍历的顺序也就不相同, 

  1.         前序:根->左子树->右子树
  2.         中序:左子树->根->右子树
  3.         后序:左子树->右子树->根         

        前中后序遍历的代码实现,可以直观的看到通过修改递归左右子树的顺序,就可以完成对前中后序的遍历.


3.层序遍历

        上面咱们对前(先)序遍历、中序遍历、后序遍历进行了讲解,除此之外还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
        这里的层序遍历和前中后序不同的是,它是一种自上而下,从左到右的遍历方式,以根结点开始,一层一层往下.

        如图所示,层序的遍历的遍历方式就是这样.理解了层序遍历的方式后,下一步就是写代码俩实现它了.这里因为层序遍历的特殊性,因此需要借助队列来完成.

void Leveloder(BTnode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTnode* front = QueueFornt(&q);
		printf("%d ", front->val);

		if (front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);

		QueuePop(&q);
	}
	printf("\n");
}

        队列的特性是:先进先出.可以利用这一特点,先将根结点入队,取出队头的结点,然后再入左右子结点.

        开始的时候队列为空,往队中push根结点,取出结点,此时队列为空,往队中push左右子结点,然后pop掉队头的数据,此时第一层就遍历完了.发现了嘛,聪明的你一定看出来了吧,上一层遍历完的时候,会把下一层的结点都push到队列中,上一层遍历完,队中放的数据就是下一层的结点数据.


4.结点个数及高度

        接下来的这些都是在二叉树的遍历基础上进行稍微的改变,利用二叉树的遍历机制,我们可以干更多的事情.

4.1二叉树的结点个数

        结点个数就是统计这颗二叉树有多少个结点,即树的枝叶有多少.这里可以使用上面遍历的任意一种方式来得出.

int TreeSize(BTnode* root)
{
	if (root == NULL)
		return 0;

	return TreeSize(root->left) + TreeSize(root->right) + 1;
}

        首先判断根节点是否为空,如果为空,则说明该树为空树,节点个数为0,直接返回0。如果根节点不为空,那么递归地调用TreeSize函数来计算左子树和右子树的节点个数,然后将左子树节点个数和右子树节点个数相加,再加上根节点本身,即可得到整个二叉树的节点个数。

4.2二叉树叶子结点个数

        叶子结点,即这个结点没有子结点.

int TreeLeafSize(BTnode* root)
{
	if (root == NULL)
		return 0;
	else if (root->left == NULL && root->right == NULL)
		return 1;
	else
		return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

        首先判断根节点是否为空,如果为空,则说明该树为空树,叶子节点个数为0,直接返回0.接下来判断根节点是否为叶子节点,即左子树和右子树都为空。如果是叶子节点,则返回1。如果根节点不是叶子节点,那么递归地调用TreeLeafSize函数来计算左子树和右子树的叶子节点个数,然后将左子树叶子节点个数和右子树叶子节点个数相加,即可得到整个二叉树的叶子节点个数。

4.3二叉树第k层结点个数

        树通过形状我们可以知道,它是分层的,上面我们学会了树的层序遍历,理解了层序遍历,要求第k层的结点树还是没那么难的.

第k层的节点
int BinaryTreeLevelKSize(BTnode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	return BinaryTreeLevelKSize(root->left, --k) + BinaryTreeLevelKSize(root->right, --k);
}

        

        先使用assert函数来断言k的值大于0,确保k是一个合法的层数。然后判断根节点是否为空,如果为空,则说明该树为空树,第k层的节点个数为0,直接返回0。如果k等于1,说明此时的结点是在k层上的,因此返回1。往下递归调用左子树和右子树,这里的--k,递归到下一层,k的层数就跟着减减.

        假设k的层数是3,要求第三层的结点个数,利用--k,当k为1的时候表明此时已经到了指定的层数,因此开始返回1.

4.4二叉树查找值为x的结点

        相当于是遍历这棵树,找到与之符合的值,并返回此结点的地址.

//查找值为x的节点
BTnode* TreeFind(BTnode* root, TreeData x)
{
	if (root == NULL)
		return NULL;
	if (root->val == x)
		return root;

	BTnode* ret = NULL;
	ret = TreeFind(root->left, x);
	if (ret)
		return ret;

	ret = TreeFind(root->right, x);
	if (ret)
		return ret;
	return NULL;
}

         首先判断根节点是否为空,如果为空,则说明该树为空树,直接返回NULL。

        然后判断根节点的值是否等于x,如果等于x,则说明找到了目标节点,直接返回根节点的指针。

        如果以上条件都不满足,说明目标节点可能在左子树或右子树中,那么递归地调用TreeFind函数来在左子树和右子树中查找值为x的节点。

        再在左子树中查找,将返回的节点指针保存在ret变量中,然后判断ret是否不为空,如果不为空,则说明在左子树中找到了目标节点,直接返回ret。

        如果在左子树中没有找到目标节点,则继续在右子树中查找,将返回的节点指针保存在ret变量中,然后判断ret是否不为空,如果不为空,则说明在右子树中找到了目标节点,直接返回ret。

        左子树和右子树中都没有找到目标节点,则说明目标节点不存在于该二叉树中,返回NULL。


5.销毁

        二叉树的销毁并不能够直接free掉根结点,因为当根结点给释放后,就再也找不到左右子树了,因此需要从树的底部开始free,最后再free掉根结点.这里的销毁还是使用递归的方式来实现.

void TreeDestory(BTnode* root)
{
	if (root == NULL)
		return;

	TreeDestory(root->left);
	TreeDestory(root->right);

	free(root);
}

        结点为空就返回,不为空继续往下走,走到叶子结点,free掉,再递归回去,一直到根结点.

        这里的销毁其实和二叉树的后序遍历很相似,左子树走完,走右子树,最后再是根.


        以上就是本篇的所有内容啦,希望能够让你对二叉树的理解提升一个台阶,可以的话一键三连十分感谢,家人们的支持是我前行的最大动力。 

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

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

相关文章

PDF文件压缩软件 PDF Squeezer mac中文版​软件特点

PDF Squeezer mac是一款macOS平台上的PDF文件压缩软件,可以帮助用户快速地压缩PDF文件,从而减小文件大小,使其更容易共享、存储和传输。PDF Squeezer使用先进的压缩算法,可以在不影响文件质量的情况下减小文件大小。 PDF Squeezer…

[Linux]多线程编程

[Linux]多线程编程 文章目录 [Linux]多线程编程pthread_create函数pthread_join函数pthread_exit函数pthread_cancel函数pthread_self函数pthread_detach函数理解线程库和线程id Linux操作系统下,并没有真正意义上的线程,而是由进程中的轻量级进程&#…

vue3 踩坑记(汇总)

1、在 vue3 中,GET 请求接口时,传入一个数组,默认是以“xxx[]: 1, 2, 3”的形式传递的,报错:“400 Bad Request” 解决方案: 传参时,需要将数组字符串化,比如:ids: sele…

二维码智慧门牌管理系统:提升社会治理效率的利器

文章目录 前言一、技术背景与特点二、数据准确性和一致性三、综合服务平台四、应用领域 前言 在当今科技不断发展的时代,我们的生活正逐渐数字化和智能化。近期,一种名为“二维码智慧门牌管理系统”的新型技术引起广泛关注。这一系统的出现不仅为我们的…

软件测试之银行测试,银行测试YYDS

为什么要做金融类软件测试 举个例子,比如银行的软件测试工程师,横向和互联网公司的测试人员比较来说,工资比较稳定,加班很少甚至没有,业务稳定。 实在是测试类岗位中的香饽饽! 同时,我也准备了…

如何使用ArcGIS Pro制作标准地图样式国界

相信大家都浏览过标准地图服务提供的标准地图,不知道你有没有想过尝试制作里面的国界,这里为大家介绍一下制作方法,希望能对你有所帮助。 制作已定国界 在地图数据内,国界分为已定国界、未定国界和海岸线,我们先对已定…

一文详解:什么是进销存管理系统?2023年top10进销存管理系统大推荐!

进销存管理系统是什么?进销存管理系统的优势在哪里?进销存管理系统都能为企业提供什么?有哪些便宜适合的进销存管理系统?本文将带大家深入浅出的聊聊进销存管理系统,并且为大家提供2023年十大进销存管理系统大盘点&…

企业简化客户服务的5种方法

在现代商业中,提供优质客户服务是企业能否成功的关键所在。为了满足客户的需求,企业需要保证客户服务的质量和效率。而许多公司却发现,随着公司的发展,客户服务的过程变得越来越复杂。许多企业陷入了自己制造的困境,面…

简易磁盘自动监控服务

本文旨在利用crontab定时任务(脚本请参考附件)来监控单个服务节点上所有磁盘使用情况,一旦超过既定阈值则会通过邮件形式告警相关利益人及时介入处理。 1. 开启SMTP服务 为了能够成功接收告警信息,需要邮件接收客户都安开启SMTP服务。简要流程请参考下…

燃尽图是什么?如何用它提升敏捷项目流程?

**敏捷项目管理**的核心是透明度和持续改进。燃尽图是轻松实现这两点的秘密武器。这种动态的可视化工具能有效地说明团队在一段时间内的进展情况,突出显示剩余的工作,并揭示你的团队是否在实现目标的正轨上。 敏捷项目管理中的燃尽图 燃尽图是敏捷项目…

【git入门教程--基于gitee】

1.git 下载安装 首先下载windows版本的git安装包 https://git-scm.com/download/win 我这里选择64位 windows版本,大部分人用的也是这个版本。安装过程很简单,基本都是下一步再下一步。 2.用户配置 git安装完成之后,在电脑文件夹的任意位…

python程序主动退出进程的方式:五种方式总有一种适合你

一、使用os.kill() os.kill()是一种向进程发送信号的方法,可以用来强制结束一个进程的运行。如果你的程序中包含有线程,用这种方式绝对没错!当使用os.kill()方法结束一个进程时,需要指定该进程的PID(进程号&#xff0…

【办公自动化】用Python将PDF文件转存为图片(文末送书)

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

【Java SE】反射与枚举

目录 ♫反射 ♪什么是反射 ♪与反射相关的类 ♪什么是Class类 ♪获取Class类 ♪class类的常用方法 ♪反射的使用 ♪反射私有方法 ♪反射的优缺点 ♫枚举 ♪什么是枚举 ♪枚举的常用方法 ♪枚举的构造方法 ♫枚举与反射 ♫反射 ♪什么是反射 Java反射是Java语言的一…

【VUE复习·1】单向数据绑定v-bind;双向数据绑定v-model

总览 1.单向数据绑定&#xff1a;v-bind 2.双向数据绑定&#xff1a;v-model 一、v-bind 单向数据绑定 1.图解 data 中的值能够影响页面上的值&#xff0c;但是在页面上更改却不能影响 data 中的值。 2.用法说明 <div><input v-bind:value"name">&l…

ES查询数据的时报错:circuit_breaking_exception[[parent] Data too large

ES配置的官方网站&#xff1a;https://www.elastic.co/guide/en/elasticsearch/reference/7.2/circuit-breaker.html 报错&#xff1a; circuit_breaking_exception[[parent] Data too large, data for [<transport_request>] would be [12318476937/11.2gb], which is…

Vue之ElementUI之动态树+数据表格+分页(项目功能)

目录 前言 一、实现动态树形菜单 1. 配置相应路径 2. 创建组件 3. 配置组件与路由的关系 index.js 4. 编写动态树形菜单 5. 页面效果演示 二、实现数据表格绑定及分页功能 1. 配置相应路径 2. 编写数据表格显示及分页功能代码 BookList.vue 3. 演示效果 总结 前言…

数据结构 - 泛型

目录 前言 1. 什么是泛型? 2. 为什么需要泛型? 引入泛型之前 引入泛型之后 3.泛型类 4.泛型的界限 1.上下界 2.通配符 前言 今天给大家介绍一下泛型的使用 1. 什么是泛型? 一般的类和方法&#xff0c;只能使用具体的类型: 要么是基本类型&#xff0c;要么是自定义…

抖音短视频seo矩阵系统源代码开发系统架构及功能解析

短视频seo源码&#xff0c;短视频seo矩阵系统底层框架上支持了从ai视频混剪&#xff0c;视频批量原创产出&#xff0c;云存储批量视频制作&#xff0c;账号矩阵&#xff0c;视频一键分发&#xff0c;站内实现关键词、短视频批量搜索排名&#xff0c;数据统计分类多功能细节深度…

在多台服务器上运行相同命令(二)、clush

介绍安装配置互信认证参数含义基本使用节点组拷贝文件 介绍 Clush&#xff08;Cluster Shell&#xff09;是一个用于管理和执行集群操作的工具&#xff0c;它允许你在多台远程主机上同时执行命令&#xff0c;以便批量管理服务器。Clush 提供了一种简单而强大的方式来管理大规模…