数据结构中的树和二叉树(0基础讲解+代码)

news2025/7/20 13:07:26

树和二叉树

  • 树的定义
    • 树的一些基本概念
    • 树的代码链接方式
  • 二叉树
    • 完全二叉树和满二叉树
    • 二叉树的性质
  • 链式二叉树
    • 前序遍历
    • 后序遍历
    • 中序遍历
    • 层序遍历
      • 二叉树的深度
      • 二叉树第k层的结点个数
      • 二叉树的叶子节点个数
  • 总结

前言:
前面我们所学习的数据结构比如链表,顺序表,队列,堆等等都可以说是一对一的数据结构,也就是数据与数据之间并不存在分支的路径,无论是顺序的存储结构还是链式的存储结构。但是在实际的应用中还存在着大量的一对多的情况,就比如我们的公路并不都是直线的,有时必须存在两个或多个岔路口。

接下来要学习的一种学习结构就是一对多的情况,这种数据结构被称为“”。

树的定义

树(Tree)的定义:树是由n(n>=0)个结点的有限集,n=0时称为空树。在任意一颗非空的树中,1.有且只有一个特定的称为根的结点;2.当n>=1时,其余的结点可分为m(m>0)个互不相交的有限集,其中每个集合本身又是一颗树;并且称为根的子树。

这就是一颗简单的树:
在这里插入图片描述

根据定义我们可以知道,黄色和蓝色的部分就是A的子树。

关于树还需要强调几点:
(1)n>0的时候,根结点时唯一的,不可能存在多个根结点的情况。
(2)子树之间是不相交的。比如下图就符合树的定义。
在这里插入图片描述

因为D和E结点相交了。

树的一些基本概念

关于树有很多的概念需要先了解一下,要不然没法开展下面的学习,虽然多,但其实也不难记。
节点的度:一个节点含有的子树的个数称为该节点的度; 如下图:A结点的度为2。
叶节点或终端节点:度为0的节点称为叶节点; 如下图:G,E,H,F,M节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:B,C等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如下图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为2
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如下图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:E,F互为堂兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;
在这里插入图片描述

树的代码链接方式

树的结构在图中很容易就能看的懂,但是要想使用代码将树的结构链接起来,并不是那么容易的事。

主流的有三种链接方式
1,双亲表示法
2,孩子表示法
3,孩子兄弟表示法
前两种表示法这里不做解释,因为我们的重点并不是树的代码实现方法,而是接下来的二叉树,所以这里只解释第三种–孩子兄弟表示法。

孩子兄弟表示法的思路是这样的,我们设置两个指针(child和brothers),这俩个指针的关系指向如下图:
在这里插入图片描述
我们通过child指针来找到孩子结点,然后再通过brothers指针去找兄弟结点,这样就能实现将整个树都链接起来的目的了。

这也是我们即将要实现的二叉树的一大特性。

二叉树

在实际的代码应用中,其实相比于树,应用的更多的是树的另一种结构----二叉树。

那什么又是二叉树呢?

二叉树(Binary
Tree):是n(n>=0)个有限结点的集合,该集合或者为空,或者有一个根节点和两个互不相交的,分别称为根结点的左子树和右子树的二叉树组成。

二叉树的特点:
1.每个结点最多有两个子树
在这里插入图片描述
2.左子树和右子树是有序的。
3.即使某个结点只有一个子树,也要区分左右。
在这里插入图片描述

完全二叉树和满二叉树

谈到二叉树,就必须要说一下两种特殊的二叉树,这也是后面实现堆和堆排序的重要概念基础。

满二叉树

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。

比如下面的两中情况
在这里插入图片描述

完全二叉树

完全二叉树由满二叉树引出来,如果树的深度为k,那么k-1层的结点数必须是最大,并且第k层的结点必须是连续的。

在这里插入图片描述

二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(h - 1)个结点.
    在这里插入图片描述
    这个想必大家都能理解。
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h - 1

我们来看一下推导的过程,第一条性质我们已经知道了第h层的结点最多有2^(h - 1)个结点,那么深度为h的二叉树的最大结点数,就是满二叉树的结点数。
我们将从第一层到最后一层的结点数加起来。
在这里插入图片描述
推导出了,最大的结点情况,我们再来思考一下,最少的结点的表达式。

最少的结点的情况就是完全二叉树的第h层只有一个结点的情况,那么我们可以求出前h-1层的结点的和再+1即可得到,深度为h的二叉树最少有多少结点的情况。
在这里插入图片描述
有了n和h的关系式,大家肯定也能反推出h的表达式,我就不再说了。

  1. 对任何一棵二叉树, 度为0的结点数,永远比度为2的结点数多1

这个性质是通过长期的观察得出的,并没什么推到的过程,大家可以多画几张图来验证一下这个。记住就好。

讲到这里,大部分的概念部分都讲完了,接下来就是二叉树的代码实现了。

链式二叉树

链式二叉树和我们之前所认识到的数据结构在代码的实现上有较大的区别,对于链式二叉树,我们不再把关注的重点放在任何对数据进行操作上(如:增删查改…),而是更加的关注它的结构特点,比如如何遍历二叉树,已经查找二叉树中的某个结点,个结点的个数个问题。

谈到链式的二叉树,就不得不谈一下二叉树的四种遍历方式前序遍历,中序遍历,后序遍历,层序遍历。
二叉树作为以后更难得复杂树的基础,掌握这些都是必要的。

想要完成对二叉树的乙一系列操作的话,就需要先存在一个二叉树,方便我们之后的研究。所以我们以下图中的二叉树为例,用代码链接一个二叉树出来。

在这里插入图片描述
代码如下:

//构建二叉树
typedef int BinaryTreeData;
typedef struct BinaryTree
{
	BinaryTreeData val;
	struct BinaryTree* left;
	struct BinaryTree* right;
}BinaryTree;

//构造结点
BinaryTree* BuyBinaryTree(int x)
{
	BinaryTree* ret = (BinaryTree*)malloc(sizeof(BinaryTree));
	if (!ret)
	{
		perror("BuyBinaryTree fail::");
		exit(-1);
	}

	ret->val = x;
	ret->left = ret->right = NULL;
	return ret;
}

//链接结点
int main()
{
	BinaryTree* n1 = BuyBinaryTree(1);
	BinaryTree* n2 = BuyBinaryTree(2);
	BinaryTree* n3 = BuyBinaryTree(3);
	BinaryTree* n4 = BuyBinaryTree(4);
	BinaryTree* n5 = BuyBinaryTree(5);
	BinaryTree* n6 = BuyBinaryTree(6);
	BinaryTree* n7 = BuyBinaryTree(7);
	//链接结点成二叉树
	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	n3->right = n7;
	n4->left = n5;
	n4->right = n6;
	
	return 0;
}

下面我们就开始遍历二叉树。

前序遍历

前序遍历的顺序:

  1. 先遍历根结点
  2. 再遍历左结点
  3. 再遍历右结点

还是先来画图,看一下逻辑的走法。
在这里插入图片描述

  1. 第一步先遍历根结点1
  2. 再遍历左子树和右子树
  3. 左子树的根节点为2。
  4. 然后再去遍历以结点2为根结点的左子树
  5. 那么接下来就是3,然后同样的道理,再去遍历3的左子树和右子树,
  6. 3的左子树为空,所以打印NULL。
  7. 右子树为7
  8. 2的左子树遍历完成之后,就去遍历2的右子树,也是空,所以打印NULL。
  9. 1的左子树遍历完成之后,就去遍历1的右子树
  10. 。。。。 同样的方式再走一遍即可。

最后的打印结果就是
在这里插入图片描述
在四种遍历二叉树的方式都是要大家对递归有深刻的理解的。

将NULL也打印出来是为了大家更好的去理解代码递归的过程,大家也可以选择不打印NULL。

下面就是前序遍历的代码:

//前序遍历
void PrevOrder(BinaryTree* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}

	//根结点
	printf("%d ", root->val);
	//遍历左子树
	PrevOrder(root->left);
	//遍历右子树
	PrevOrder(root->right);
}

看不懂的建议根据代码画一下递归的展开图,因为后面的中序和后序也是同样的道理,都是换汤不换药。

后序遍历

后序遍历的顺序:

  1. 先遍历左结点
  2. 再遍历右结点
  3. 再遍历根结点

走的过程也是和前序遍历一样的,都是层层的递归先去找左子树,然后再去找根,接着就是右子树。

代码:

//后序遍历
void PostOrder(BinaryTree* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}

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

结果:
在这里插入图片描述

中序遍历

中序遍历的顺序:

  1. 先遍历左结点
  2. 再遍历根结点
  3. 再遍历右结点

顺带的提一嘴,不知道大家发现没有,这三种命名是有规律的,它们都是根据根结点的位置命名的。比如中序遍历,代表的就是根结点在中间的位置。

下面就是中序遍历的代码:

//中序遍历
void InOrder(BinaryTree* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->val);
	InOrder(root->right);
}

结果:
在这里插入图片描述

层序遍历

层序遍历的过程和前三种遍历的方式有所不同,它是按照树的层来打印对应的结点。

按照上图中的二叉树结构进行层序打印的话,结果就是:

在这里插入图片描述
第一层只有根节点1
第二层是 2 4
第三层是 3 5 6
第四层是 7
第五层是空,这里就是不打印了,因为层序结构的逻辑遍历思想还是很简单的。

那么层序遍历如何实现呢?
这就需要借助我们之前学过的一种数据结构了—队列。

我们先回想一下队列的特点:先进的先出,一端为进数据,一端为出数据。

那么我们只需要按照根先队列,如何左子树进队列,接着右子树进队列。也就是说一层一层的进队列,结果就是这样的:
在这里插入图片描述
然后出队列即可。

大概的思路有了之后我们就需要用代码来实现一下,因为队列我们在之前就实现过了,所以这里我直接将之前的队列的代码copy过来作为,我们实现层序遍历的基础。

代码:

//层序遍历
void LeveInOrder(BinaryTree* root)
{
	//先创建一个队列
	Queue q;
	QueueInit(&q);
	//先将根结点入队列
	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		//先将1打印出来
		BinaryTree* ps = QueueFront(&q);
		printf("%d ", ps->val);
		//销毁1
		QueuePop(&q);

		//如果左子树不为空就入队列
		if (ps->left)
			QueuePush(&q, ps->left);
		//同样的如果右子树不为空就入队列
		if (ps->right)
			QueuePush(&q, ps->right);
		
	}
	printf("\n");

	QueueDestroy(&q);
}

队列的代码在本专栏的前面的文章中。

遍历的内容讲完之后,后面接着就是二叉树的深度,和结点个数的问题。
也是尤为重要的内容。

二叉树的深度

有了前面遍历二叉树的四种方式之后,我想对于二叉树的深度的遍历讲起来,大家应该能更容易理解一点。

同样也是采用递归的方式,我们遍历完左右子树后,取较大的+1就是完整的二叉树的深度,+1加的是根节点。

图解如下:
在这里插入图片描述
细节的逻辑图:
在这里插入图片描述
因为3的左子树为空所以返回0。同样的5结点和6结点的左右子树为空和7结点一样。所以它们返回的都是0。

从下面向上层层的递归,选取最大的+1递归给上一层。

依次就能达到获取二叉树的深度的目的
代码:

int BinaryTreeHight(BinaryTree* root)
{
	if (!root)
		return 0;

	//记录下左右子树最大的深度,不用再重复的递归计算
	int lefthight = BinaryTreeHight(root->left);
	int righthight = BinaryTreeHight(root->right);

	return lefthight > righthight ? lefthight + 1 : righthight + 1;
}

有人觉得上面的写法太过于的复杂,于是优化成了下面的代码:

int BinaryTreeHight(BinaryTree* root)
{
	if (!root)
		return 0;
	else
		return BinaryTreeHight(root->left) > BinaryTreeHight(root->right) ?
			   BinaryTreeHight(root->left) + 1 : BinaryTreeHight(root->right) + 1;
}

大家思考一下这两种写法,哪一种更好。

二叉树第k层的结点个数

要向获取第k层的结点个数,听起来好像很难办到,但是仔细思考一下,我们其实不必走到第k层,因为就算走到了第k层,也没法获取该层的结点个数,我们不容走到第k-1层,也就是k的上一层,然后通过左右吧孩子是否为空判断,为空的就返回0,不为空的话就返回1。

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

int TreeKLevelSize(BinaryTree* root, int k)
{
	if (!root)
		return 0;
	if (k == 1)
		return 1;

	// k > 1 子树的k-1
	return TreeKLevelSize(root->left, k - 1)
		+ TreeKLevelSize(root->right, k - 1);
}

注意root是否为空的判断必须在k是否为1的判断之前,否则可能会引起空指针的引用。

二叉树的叶子节点个数

叶子结点的判断就更好判断了,根据叶子结点的特点:左右子树都为空,就是返回1,遇到空结点的话就返回0。

逻辑图解:
在这里插入图片描述
还是根据递归,从下向上层层的返回值,最后便能确定下叶子结点的个数,和上第k层的结点个数,有异曲同工之妙。

代码:

int TreeLeafSize(BinaryTree* root)
{
	if (root == NULL)
		return 0;

	if (root->left == 0 && root->right == 0)
		return 1;
	
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

总结

这次的重点便是二叉树的链接,以及对二叉树结构的各种研究,其实大量的运用了递归的知识,如果觉得看不懂的,就跟着代码调试着去画递归的结构图。
多画点时间就能正真正的掌握二叉树的各种操作了,二叉树对后面的更复杂的数据结构的学习做了一个铺垫,熟练的掌握二叉树的操作才能更好的学习之后的知识。

最后将本文章所有的完整的代码链接给出(包阔队列)
二叉树

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

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

相关文章

【博客542】k8s使用EndpointSlices扩展大规模service后端服务数量

k8s使用EndpointSlices扩展大规模service后端服务数量 EndpointSlices 端点切片(EndpointSlices) 提供了一种简单的方法来跟踪 Kubernetes 集群中的网络端点(network endpoints)。 它们为 Endpoints 提供了一种可扩缩和可拓展的替…

长时间序列模型DLinear(代码解析)

前言 今年时间序列SOTA,DLinear模型,论文下载链接,也可以看我写的论文解析当然最好是读原文。Dlinear,NLinear模型Github项目地址,下载项目文件这里提供我写过注释的项目文件,下载地址 参数设定模块(run_…

图神经网络之预训练大模型结合:ERNIESage在链接预测任务应用

1.ERNIESage运行实例介绍(1.8x版本) 本项目原链接:https://aistudio.baidu.com/aistudio/projectdetail/5097085?contributionType1 本项目主要是为了直接提供一个可以运行ERNIESage模型的环境, https://github.com/PaddlePaddle/PGL/blob/develop/e…

笔记redis

redis特点: 1.这些数据类型都支持 push/pop. add/remove 及取交集并集和差集及更丰富的操作 2.Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中进行持久化 3.单线路IO多路复用 4.redis操作是原子性操作 redis 单线程模型: Redis 内…

chrome插件开发(manifest_version版本V3 + Ant Design Vue)

1.什么是 Chrome 插件 谷歌浏览器插件是一种小型的定制浏览器体验的程序,通过插件可以自定义浏览器的一些行为来适合个人的需求,例如上面的查看服务器状态插件。 在应用商店中下载下来的插件基本上都是以.crx 为文件后缀,该文件其实就是一个…

Word2Vec

Word2Vec 在自然语言发展的早期阶段,词的表示经历了不断地发展和改进,直到后来有一种word vector的思想被提出以及后续的实现,才极大地促进了NLP的发展。 word vector的核心思想: 为每个单词构建一个密集向量,选择后…

傻白入门芯片设计,先进封装技术(五)

集成电路芯片与封装之间是不可分割的整体。没有一个芯片可以不用封装就能正常工作,封装对芯片来说是必不可少的,随着IC生产技术的进步,封装技术也不断更新换代,每一代IC都与新一代的IC封装技术紧密相连。 一、什么是封装&#xf…

什么蓝牙耳机颜值高音质好?颜值高音质好的蓝牙耳机推荐

朋友让我推荐蓝牙耳机的时候,总是喜欢问哪款蓝牙耳机的性能更强,想要直接入手那款性能更强的蓝牙耳机,以此节省对比的时间。但是用户自行进行对比的步骤,显然是不能省的,所以今天我给四款蓝牙耳机做了横向对比&#xf…

C语言tips-NULL指针和void指针

0.写在最前 最近因为工作需要开始重新学c语言,越学越发现c语言深不可测,当初用python轻轻松松处理的一些数据,但是c语言写起来却异常的复杂,这个板块就记录一下我的c语言复习之路 1. void指针 1.1 解释 void 用在函数定义中可以表…

身份安全风险分析

摘要 从勒索软件到 APT,身份风险是重要的攻击向量。 管理 Active Directory 的复杂性,导致所有组织都存在1/6的可利用的特权身份风险。 这些身份风险包括使用过时密码的本地管理员、具有不必要权限的错误配置用户、在终端上暴露的缓存凭据等。 当攻击者…

让学前端不再害怕英语单词(四)

|| 欢迎关注csdn前端领域博主: 前端小王hs || email: 337674757qq.com || 前端交流群: 598778642前三章直通车↓↓↓ 让学前端不再害怕英语单词(一) 让学前端不再害怕英语单词(二) 让学前端不再害怕英语单词&#xff0…

单目标应用:最有价值球员算法(Most Valuable Player Algorithm,MVPA)求解旅行商问题TSP

一、最有价值球员算法 最有价值球员算法(Most Valuable Player Algorithm,MVPA)由Bouchekara 等人于2017年提出,该算法受到体育比赛的启发,球员们为了赢得冠军而组成队伍进行队伍竞争,他们也为了赢得最有价…

使用VMware安装系统Window、Linux操作系统

使用VMware安装系统Window、Linux操作系统 下载镜像文件打开VMware 下载镜像文件地址或链接: Windows全家桶镜像文件下载网站:msdn.itellyou.cnWindows 10 种子文件 ed2k://|file|cn_windows_10_business_editions_version_1803_updated_aug_2019_x64_dv…

Js逆向教程-14反调试

Js逆向教程-14反调试 一、检测是否在调试 键盘监听(F12)检测浏览器的高度插值检测开发者人员工具变量是否为true利用console.log调用次数利用代码运行的时间差利用toString检测非浏览器 二、显性 2.1 debugger: function xx() {debugger;…

盘点re:Invent历年重磅创新:今年亚马逊云科技又将有哪些重磅发布?

“云计算春晚”re:Invent即将开幕,这一次亚马逊云科技会带来哪些重磅发布? 一年一度的亚马逊云科技re:Invent大会即将在下周开幕。作为云计算行业的顶级盛事,亚马逊云科技re:Invent大会看出云计算领域创新的风向标,也…

QQ小程序——无法正常创建项目与uniapp联动问题

目录 一、使用QQ小程序开发者工具无法创建项目 情境引入 检查原因 解决方法 总结: 二、QQ小程序开发者工具无法与uniapp联动 情境导入 问题排错 解决方法 三、QQ小程序开发者工具无法正常上传 情境导入 错误分析 解决方法 心得分享 分清工作和学习的区别…

运动耳机什么牌子的好,推荐几款排行靠前的耳机

骨传导耳机近些年在耳机界迅速燃起,其设计原理是由贝多芬含棍棒发声所启发,而佩戴骨传导耳机主要是为了防止细菌在耳道内的滋生,除此之外骨传导耳机的佩戴方式也是直接将耳机套在头骨处即可完成佩戴,不用进入耳道,能够…

【torch-sparse及pytorch-geometric 安装】

torch-sparse及pytorch-geometric 安装 pip安装完pytorch-geometric之后,报错No module named torch_sparse 然后安装pip安装torch-sparse,结果报错,百度一下怎么安装,无果,转战官方文档,成功! …

实验讲解-线程池停止执行任务 Executor.execute shutdown awaitTermination shutdownNow()

1 Executor.execute public interface Executor { 在将来的某个时间执行给定的可运行的任务。该可运行的任务可以在新线程、池线程或调用线程中执行,由Executor实现决定。 参数: command–可运行的任务 投掷: RejectedExecutionException–…

Kafka生产者之分区

一、分区好处 (1)便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果; (2&…