二叉树链式结构的实现及简单操作(画图超详细解释)

news2025/7/26 7:28:04

二叉树链式结构的实现及简单操作

    • 前置说明
    • 前序遍历
    • 中序遍历
    • 后序遍历
    • 层序遍历
    • 如何判断一颗二叉树是完全二叉树
    • 通过前序遍历的数组构建二叉树
    • 销毁二叉树
    • 总结

在这里插入图片描述

前置说明

由于我们要对二叉树进行操作,我们就得现有一个二叉树,而二叉树的构建又比较复杂,为此我们先来手动创建一颗二叉树!
比如现在我们就建立如图的二叉树:

在这里插入图片描述

typedef int BTDataType;
typedef struct BTNode
{
	struct BTNode* left;
	struct BTNode* right;
	BTDateType val;
}BTNode;
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BuyBTNode(BTDataType x)
{
	BTNode* ret = (BTNode*)malloc(sizeof(BTNode));
	if (!ret)
		exit(EXIT_FAILURE);
	ret->val = x;
	ret->left = NULL;
	ret->right = NULL;
	return ret;
}
void test2()
{
	//手动创建一颗二叉树
	BTNode* n1 = BuyBTNode(1);
	BTNode* n2 = BuyBTNode(2);
	BTNode* n3 = BuyBTNode(3);
	BTNode* n4 = BuyBTNode(4);
	BTNode* n5 = BuyBTNode(5);
	BTNode* n6 = BuyBTNode(6);
	n1->left = n2;
	n2->right = n4;
	n2->left = n3;
	n4->left = n5;
	n4->right = n6;
	//该二叉树的根节点为n1
}

注意上面并不是建立二叉树的真正方式,只是我们为了演示和方便手动创建的一颗二叉树!

前序遍历

首先我们可以将一颗二叉树看成三部分:
在这里插入图片描述
然后嘞左子树又可以看成三部分:
在这里插入图片描述
然后嘞我们的左子树又可以按照上面的规则又分为根、左子树、右子树!三个部分:
在这里插入图片描述
然后嘞我们再将左子树分:
在这里插入图片描述
我们发现这时候左子树已经不能再向上面那样分为根、左子树、右子树三个部分了,只有根这一个部分,没有左子树、右子树了,我们称为这样的一颗树为空树,这样的话我们也就不能再向下分了;那么同理右子树也是这样的!!
-------------------------------------------------------------------------------------------------------------------------------------
现在我们回归主题,我们来谈一谈前序遍历!!
前序遍历的顺序:根、左子树、右子树;
在这里插入图片描述

思路与上面是一样的!我们首先从root(根)入手,那么我们首先遇到的是根节点:
在这里插入图片描述
然后了我们将root节点的值打印一下!,我们再去遍历左子树,
在这里插入图片描述
然后我们又遇到了左子树的根节点,我们再打印它,然后我们再去遍历当前二叉树的左子树:
在这里插入图片描述
然后嘞我们再去遍历该二叉树的左子树:
在这里插入图片描述

这时候我们已经到达了空树,我们已经无法再分了,我们直接返回即可;
然后嘞我们就会来到上一层,我们开始遍历上一层的右子树,右子树也是一颗树啊!!又可以分为根、左子树、右子树,就这样不断分下去……,最后会分到空树,我们就不在往下分,直接返回就好了!!!
代码方面:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (!root)//如果是空树直接返回,不要再分了!!
	{
    printf("NULL ");
	return;
	}
	printf("%d ",root->val);
	BinaryTreePrevOrder(root->left);//去遍历左子树
	BinaryTreePrevOrder(root->right);//左子树遍历完,我们再去遍历右子树
}

在这里插入图片描述

为了便于理解这个过程,我们来画一画这个递归展开图:

在这里插入图片描述
我们打印一下root指向的节点的值:1,然后我们去访问该二叉树的左子树!
在这里插入图片描述
我们打印一下:2,然后我们去访问该二叉树的左子树!
在这里插入图片描述
我们打印一些根节点,然后访问其左子树:
在这里插入图片描述

来到空树我们就返回上一层,并且开始遍历上一层的右子树:
在这里插入图片描述
这时,3的右子树也为NULL,不能再分了,直接返回!!,这是2的左子树就已经遍历完了,我们该开始遍历2的右子树了!
在这里插入图片描述
2的右子树为空,不可再分,直接返回,这是1的左子树被遍历完了,我们再来访问1的右子树:
在这里插入图片描述
这时进来4又可以分为根、左子树、右子树,我们来访问4的左子树:
在这里插入图片描述
这时我们进来,又是一颗以5为根的树,又可以分为:根、左子树、右子树:
我们按照前序遍历的规则,根被访问完了,我们立马去访问左子树:
在这里插入图片描述
这时遇到了空树,直接返回就好,那么这时候回到上一层过后,就代表着5的左子树已经访问完毕了,接下来改访问5的右子树了:
在这里插入图片描述
很好,此时5的右数也为空,我们直接返回就好,至此,代表着5的左右子树都已经遍历完毕(也可以了理解为4的左子树遍历完毕!),那么这时候我们就会返回5的上一层,开始遍历4的右子树:
在这里插入图片描述

这时候我们就来到了以6为根节点的二叉树,我们此时按照规则,先访问根节点、再访问左子树、右子树,现在我们开始访问6的左子树:
在这里插入图片描述
我们发现6的左子树是空树,直接返回到上一层,此时6的左子树数被遍历完毕!,接下来改变了6的右子树:
在这里插入图片描述
6的右子树也是空树,我们直接返回,至此6的左子树右子树被遍历完毕,也可以说6这颗二叉树被遍历完毕,也可以说是4的右子树遍历完毕,那么附带一些列的连锁反应,4这整颗数也被访问完毕,1的右子树被访问完毕!,1这整颗树被访问完毕!,整颗二叉树被访问完毕!,至此整个递归过程结束:
全家福:
在这里插入图片描述
画的可能有点乱,但是我们整体看起来这个递归过程也是像一颗二叉树!!
要注意其中箭头指向的位置和开始位置!!

其实递归嘞,就是先将这个函数当作已知条件,先假设我们递归了自己会得到一个什么样的结果,然后再根据这个结果去做后面的操作就行了,我们再设计算法的时候,不必在意函数递归调用的细节,你就认为我只要将参数交给这个函数,就能得到想要的结果,然后根据结果再设计接下来的操作就可以了!!,比如前序遍历:我们知道这个函数的功能是遍历真个二叉树,那么我们先访问根节点,然后我们再去前序遍历左子树,我们只需将参数交给递归调用的函数就可以,不必关系它具体是怎末实现的!!然后假设交给它过后我们已经遍历完左子树了,然后同理将右子树的参数传给它,我们就完成了前序遍历!

中序遍历

中序遍历:左子树、根、右子树
我们只要弄懂前、中、后中随意一个遍历,那么另外两个就是手到擒来的事了:
还是跟上面一样我们已经知道这个函数的功能了,以中序遍历二叉树,那么我们先将左子树的参数给他,默认他已经帮我们完成了,我们紧接着打印根节点,然后同样的道理让其帮我遍历右节点:

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%d ",root->val);
	BinaryTreeInOrder(root->right);
}

在这里插入图片描述

后序遍历

后序遍历:左子树、右子树、根

// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%d ", root->val);

}

在这里插入图片描述

层序遍历

层序遍历不同前面的遍历,层序遍历再有些地方也叫做广度优先遍历,那么这个遍历是个啥意思嘞:
在这里插入图片描述
在这里呢我们需要借助一下队列来实现:
大致思路:
在这里插入图片描述
然后呢,我们取出队头,并且删掉队头数据!然后打印一下取出来的节点数据,然后呢我们再将该节点的所有孩子节点全部入队(有就入,没有就不入)
在这里插入图片描述
代码方面:

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q,root);
	while (QueueEmpty(&q)==false)
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%d ",front->val);
		if (front->left)
			QueuePush(&q,front->left);
		if (front->right)
			QueuePush(&q,front->right);
	}
	QueueDestroy(&q);
}

在这里插入图片描述

如何判断一颗二叉树是完全二叉树

首先我们得知道完全二叉树的概念:

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K
的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对
应时称之为完全二叉树。

其中完全二叉树有一个很重要的概念!连续!!!
只要不是完全二叉树,那么一定不是连续的比如:
在这里插入图片描述
上面就不是一颗完全二叉树!
那么如果我们每一个节点,每一个节点的去验证,当我们遇到NULL指针节点时,我们验证一下其后面还有没有有效节点,如果没有则说明这是一颗完全二叉树;若有,则说明这不是一颗完全二叉树;
这里遍历每一个节点和判断NULL节点后面有没有有效值,我们需要借助层序遍历的思想:
在这里插入图片描述

代码实现:

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q,root);
	while (1)
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
			QueuePush(&q,front->left);
			QueuePush(&q,front->right);
		}
		else
		{
			while (QueueEmpty(&q) == false)
			{
				if (QueueFront(&q))
					return false;
				else
					QueuePop(&q);
			}
			return true;
		}
	}
}

现在我们来解释一下,为什么只要NULL指针之后如果没有有效元素的话,他就是一颗完全二叉树!万一后面还有元素只是还没来到及入队嘞,我们的判断是不是过早了?
首先我们将每个节点的孩子节点都画出来(包括空孩子)
在这里插入图片描述
从图中我们可以知道再队列中的元素一定是队头元素的兄弟节点和它(比它大的兄弟节点的孩子节点),就比如图中,假设现在我队头元素就是第3层的空指针,那么此时队列中存在元素依次是5、6
、NULL、NULL此时不会有队头元素的孩子节点(因为我队头元素都还没有出队,自然也就不可能让孩子进队)!!那么这时候你在仔细品一品,假设我这事颗完全二叉树,那么我停下来判断的话,一定是在如下位置:
在这里插入图片描述
但是如果不是一颗完全二叉树的话,那么在这里插入图片描述
我们刚才上面分析了,队列中一定存的是,队头元素以后的兄弟节点和队头元素以前的兄弟节点的孩子节点!这两部份!!现在我们是遇到NULL指针才队列中的元素,如果队列中不存在队头节点以后的兄弟节点的话,同时也不存在队头节点以前的兄弟节点的孩子节点的话,那么队列中存的就全是NULL,那么这时候判断出来就是一颗完全二叉树;如果队列中存在两部分中任意一部分的有效节点,那么对不起,这不是一颗完全二叉树!!
---------------------------------------------------------------------------------------------------------
以上便是博主对于如何判断完全二叉树的愚见!欢迎大家指正!

通过前序遍历的数组构建二叉树

就比如:
“ABD##E#H##CF##G##”
#:代表NULL
是通过前序遍历出来的结果,我们现在想要通过这个结果,逆推回二叉树:
这道题还是递归:
根据我们上面书写前序遍历算法的经验:
我们假设一个函数是通过前序遍历创建二叉树的:
那么根据前序遍历的规则我们首先建立一个根节点,现在接下来就是构建左子树和右子树了,我们将数据交给这个函数,让它帮我们完成即可,我们无需关系其中细节:
画图再理解一下:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
代码实现:

BTNode* BuyBTNode(BTDataType x)
{
	BTNode* ret = (BTNode*)malloc(sizeof(BTNode));
	if (!ret)
		exit(EXIT_FAILURE);
	ret->val = x;
	ret->left = NULL;
	ret->right = NULL;
	return ret;
}
BTNode* BinaryTreeCreate(BTDataType* a,int* pi)
{
	if (a[*pi]=='#')
	{
		*pi += 1;
		return NULL;
	}
	BTNode* root = BuyBTNode(a[*pi]);
	*pi += 1;
	root->left=BinaryTreeCreate(a, pi);//构建左子树;
	root->right=BinaryTreeCreate(a,pi);//构建右子树
	return root;
}

当然利用中序、后序遍历建立二叉树也是同样的道理!
在这里插入图片描述
相关题目连接:题目链接

销毁二叉树

刚才我们利用前序遍历的方法建立了一个二叉树,我们建立了,不用了就得销毁啊!
为此销毁也是分3种方式!
1、利用前序遍历销毁:根、左子树、右子树
只不过我们需要注意,我们是先销毁的根,为了避免根被销毁过后找不到左子树、右子树,我们需要先保留一下左子树的根节点和右子树的根节点!

void PrevDestroy(BTNode* root)
{
	if (root == NULL)
		return;
	BTNode* Left = root->left;//
	BTNode* Right = root->right;//需要提前保存避免找不到左右子树
	free(root);
	PrevDestroy(Left);//销毁左子树
	PrevDestroy(Right);//销毁右子树
}

void BinaryTreeDestory(BTNode** root)
{
	BTNode* head = *root;
	PrevDestroy(head);
	*root = NULL;
}

2、利用中序遍历销毁二叉树:左子树、根、右子树

//利用中序销毁二叉树
void InDestroy(BTNode* root)
{
	if (root == NULL)
		return;
	BTNode* Left = root->left;//
	BTNode* Right = root->right;//需要提前保存避免找不到左右子树
	InDestroy(Left);//先销毁左子树
	free(root);
	InDestroy(Right);//在销毁右子树
}
void BinaryTreeDestory(BTNode** root)
{
	BTNode* head = *root;
	InDestroy(head);
	*root = NULL;
}

3、利用后序遍历销毁二叉树:左子树、右子树、根
这时候我们就可以不用保存左右子树的根节点了,因为我们是最后才销毁根节点,可以放心的删除!

//利用后序销毁二叉树
void PostDestroy(BTNode* root)
{
	if (root == NULL)
		return;
	BTNode* Left = root->left;//
	BTNode* Right = root->right;//
	PostDestroy(Right);//先销毁右子树
	PostDestroy(Left);//在销毁左子树
	free(root);
}
void BinaryTreeDestory(BTNode** root)
{
	BTNode* head = *root;
	PostDestroy(head);
	*root = NULL;
}

当然我们还可以利用层序遍历来销毁二叉树,这里就不写了,读者可以自己写一下!

总结

当然以上便是二叉树的一些常见操作;
如果向进一步加强对于二叉树和递归的理解;
建议刷一刷下面的题:

  1. 单值二叉树。Oj链接
  2. 检查两颗树是否相同。OJ链接
  3. 对称二叉树。OJ链接
  4. 二叉树的前序遍历。 OJ链接
  5. 二叉树中序遍历 。OJ链接
  6. 二叉树的后序遍历 。OJ链接
  7. 另一颗树的子树。OJ链接

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

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

相关文章

李宏毅机器学习作业10——Adversarial Attack

目录 目标和方法 评价方法 导包 Global Settings Data transform Dataset 代理网络 评估模型在非攻击性图像上的表现 Attack Algorithm FGSM I-FGSM MI-FGSM Diverse Input (DIM) 攻击函数 Attack Ensemble Attack 集成模型函数 构建集成模型 进行攻击 FG…

【Node.js】第八章 express编写接口

目录 1. 编写接口 1.1 编写GET接口 2.2 编写POST接口 2. 接口跨域问题 2.1 跨域问题 2.2 使用cors中间件解决跨域问题 2.3 CORS ​2.4 JSONP接口 1. 编写接口 1.1 编写GET接口 2.2 编写POST接口 2. 接口跨域问题 2.1 跨域问题 2.2 使用cors中间件解决跨域问题 cor…

家里Win7电脑如何连接公司Win10电脑?快解析+远程桌面

什么是远程桌面?通俗地讲,就是可以在任何地点登陆位于其他地点的电脑,可以看到远程登陆电脑的一切东西,可以进行添加、改变、删除文件等任何操作,就像自己在那台电脑前操作一样。远程桌面有丰富的应用场景,…

如何在TIA博途中在线更新PLC的CPU固件版本?

如何在TIA博途中在线更新PLC的CPU固件版本? S7-1200PLC最新的V4.6.0版本的固件出来了,本次就以V4.6版本的固件为例,演示如何在博途中对PLC的固件版本进行更新。 (为防止更新过程中出现意外,强烈建议对PLC的程序进行备份!备份!备份!) 如下图所示,打开某个项目,选中PL…

nm命令使用详解,让你加快学习速度

nm 命令详解 符号是每个ELF文件的一个重要部分,因为它保存了程序实现或使用的所有(全局)变量和函数。符号表中保存了查找程序符号、为符号赋值、重定位符号所需要的全部信息。Linux中 nm用来列出目标文件的符号表;如果nm指令没有指出目标文件,则nm假定目…

模拟电路设计(34)---脉宽调制型开关电路

在开关稳压电源中,直流变换器中的功率晶体管工作在开关状态。目前开关电源的工作频率在几百kHz,有些甚至已经到了MHz量级。如下图所示是DC-DC开关变换器的原理框图: ​DC-DC开关变换器的原理框图 开关电源的实现方式有很多种,如最…

[附源码]Python计算机毕业设计二手图书回收销售网站

项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等等。 环境需要 1.运行环境:最好是python3.7.7,…

Spring:AOP事务管理(14)

Sprin事务Spring事务简介相关概念介绍转账案例-需求分析转账案例-环境搭建事务管理Spring事务角色Spring事务属性事务配置转账业务追加日志案例事务传播行为Spring事务简介 相关概念介绍 事务作用:在数据层保障一系列的数据库操作同成功同失败。Spring事务作用&am…

电子作业票系统:消除安全管理漏洞,科技赋能企业业务洞察

电子作业票系统采用定位、物联网、人脸识别、大数据技术对现场作业进行严格管控;通过风险大数据风险辨识模型,实现作业风险辨识,对动火、高处、受限空间、临时用电、吊装、断路、管线打开、挖掘作业等特殊作业票证智能化管理。 在危化企业实际…

Excel VS BI,谁才是真正的大数据分析工具?

有人说,Excel能聚合运算,能分析,能做数据分析报表。而BI数据分析工具,看上去也就是做出来的报表更好看一些。事实真的是这样吗?当然不是。外行人看热闹,内行人看门道,BI数据分析工具比起Excel更…

访问权限控制

访问控制目的 在实际的组织中,为了完成组织的业务工作,需要在组织内部设置不同的职位,职位既表示一种业务分工,又表示一种责任与权利。根据业务分工的需要,职位被划分给不同群体,各个群体的人根据其工作任…

零代码极限封装的【接口自动化测试框架】震碎你的三观

随着互联网寒冬的到来,测试行业裁员的裁员,找工作的找工作,内卷越来越加剧,那么选择一个学习提升的平台尤为重要,接下来我要说的事情将震碎你的三观,震掉你的眼球,和每个测试人息息相关&#xf…

图像处理 QImage

在Qt中有四种处理图像的方法: QImage :使用I/O ,可以对像素进行处理QPixmap:主要用在屏幕的显示QBitmap: QPixmap的子类,处理颜色深度,只能显示黑白两种颜色,用于遮罩QPicture&…

Qt通过Doc模式读取XML并设计一个增删改查方便的一个操作类

前言 如果对开源库TinyXml有兴趣的可以去看看这篇文章。 C使用TinyXml(开源库)读取*.XMl文件 目录前言DOC 文档对象模型QtXML基本结构操作XML部署环境添加信息头读取XML文件添加根节点添加一个没有属性的节点添加一个有属性的节点添加一个元素节点给节点单独设置属性删除所有同…

开源软件安全与应对策略探讨 - Java 机密计算技术应用实践

据统计,90% 以上的应用都在使用第三方软件库,这些软件大部分都是开源的。与此同时,有超过一半的全球 500 强公司都在使用存在漏洞的开源软件。这幅漫画生动的描述了一个大型应用软件的组件架构,它可能建立在一个极其脆弱的开源组件…

SpringBoot整合Swagger2+Knife4j,注解使用

SpringBoot整合Swagger2Knife4j,注解使用 文章目录SpringBoot整合Swagger2Knife4j,注解使用前言一、swagger是什么?二、使用步骤1.引入库2.使用swagger的相关注解总结前言 swagger和postman都是后端编写的测试接口使用方式,由于实际开发的需求和使用习惯…

基于springboot的汽车租赁管理系统的设计与实现

项目描述 临近学期结束,还是毕业设计,你还在做java程序网络编程,期末作业,老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下,你想解决的问…

pytorch实现mnist手写数字识别

🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍦 参考文章地址: 365天深度学习训练营-第P1周:mnist手写数字识别🍖 作者:K同学啊一、 前期准备 1. 设置GPU import torch import torch.nn a…

MySQL8.0爬坑二三事

【下载】 地址:MySQL :: Download MySQL Community Serverhttps://dev.mysql.com/downloads/mysql/ 【安装】 1.解压复制下面一堆文件到你需要的路径 2.如下图添加一个data文件夹,专门来放数据 3.新建一个my.ini的配置文件,里面内如如下 4…

如何将草料二维码收集到的表单信息同步至腾讯文档

在进行工业巡检场景如消防栓检查时,需要到达巡检地点后,扫描草料二维码,然后填写巡检的结果。事后,还需要有一个工作人员将草料二维码中的信息手动复制粘贴至腾讯文档中。那么能不能将我们信息填写后,自动就汇总至腾讯…