一:前提
前提1:二叉树模型
二叉树模型可更改,博主本文所有函数的实现及验证就采取如图所示的模型
前提2:二叉树模型的构建
1:单个节点结构体的定义
解释:
一个节点应该有值val,还有两个指针指向它的左右孩子。
2:创建节点函数
解释:
此函数创建了一个节点,并且进行了初始化。
3:手动构建二叉树模型
解释:
a:根据模型已知:
共六个节点
值为1的节点左孩子为2,右孩子为4
值为2的节点左孩子为3,无右孩子
值为3的节点无孩子
值为4的节点左孩子为5,右孩子为6
值为5 ,6 的节点,无孩子
b:所以BuyNode函数创建了6个节点,再根据a的信息进行left和right的连接,无孩子即为NULL。在BuyNode函数内部已经初始化为NULL,不需要在这里再额外的置空。
到这里我们的所有前提工作就做好了,后面就是函数的实现了。
前提3:实现函数大纲:
实现一:前中后序遍历
1:前序遍历
模型如前提:
前序遍历效果:
解释:
二叉树的前序遍历(Pre-order Traversal)是一种遍历二叉树节点的算法。在前序遍历中,访问节点的顺序是:
a:访问根节点:首先访问二叉树的根节点。
b:前序遍历左子树:然后递归地前序遍历根节点的左子树。
c:前序遍历右子树:最后递归地前序遍历根节点的右子树。
简单来说,前序遍历的顺序是:根节点 -> 左子树 -> 右子树。
所以前序遍历的结果是:1 ->2 -> 3 -> 4 -> 5 -> 6
这个过程可以这样理解:
访问节点1(根节点)。(1)
递归地遍历左子树:访问节点 2,(1 2)然后递归地遍历 2的左子树(访问节点 3)(1 2 3),接着遍历2的右子树(NULL,所以跳过)。
递归地遍历右子树:访问节点 4(1 2 3 4),然后递归地遍历 4 的左子树(访问节点 5)(1 2 3 4 5),接着遍历 4 的右子树(访问节点 6)(1 2 3 4 5 6)。
递归实现前序遍历函数:
递归的路线:
(红色代表递,蓝色代表归)
不太清晰,分左右发
左:
右:
可以看出,打印这个步骤是最先的 ,都是在左右子树遍历前。
2:中序遍历
中序遍历效果:
解释:
二叉树的中序遍历(In-order Traversal)是另一种遍历二叉树节点的算法。在中序遍历中,访问节点的顺序是:
- 中序遍历左子树:首先递归地中序遍历根节点的左子树。
- 访问根节点:然后访问根节点。
- 中序遍历右子树:最后递归地中序遍历根节点的右子树。
简单来说,中序遍历的顺序是:左子树 -> 根节点 -> 右子树。
中序遍历的结果是:3-> 2 ->1 -> 5 -> 4 -> 6
这个过程可以这样理解:
- 递归地遍历左子树:访问节点 2 的左子树,即节点 3。(3)
- 访问节点 2。(3 2)
- 继续递归地遍历 2 的右子树,(这里为空,所以跳过)。
- 回到根节点 1,访问节点1。(3 2 1)
- 递归地遍历右子树:访问节点 4 的左子树,即节点 5(3 2 1 5)
- 访问节点4(3 2 1 5 4)
- 访问节点 4 的右子树,即节点 6(3 2 1 5 4 6)
递归实现中序遍历函数:
递归的路线:
左:
右:
可以看出,打印这个步骤是次先的 ,都是在左子树走完之后
3:后序遍历
模型:
后序遍历效果:
二叉树的后序遍历(Post-order Traversal)是遍历二叉树节点的另一种算法。在后序遍历中,访问节点的顺序是:
- 后序遍历左子树:首先递归地后序遍历根节点的左子树。
- 后序遍历右子树:然后递归地后序遍历根节点的右子树。
- 访问根节点:最后访问根节点。
简单来说,后序遍历的顺序是:左子树 -> 右子树 -> 根节点。
后序遍历的结果是:3 -> 2-> 5 -> 6 ->4 -> 1
这个过程可以这样理解:
- 递归地遍历左子树:首先访问节点 2 的左子树,即节点 3。(3)
- 继续递归地遍历 2 的右子树,(这里为空,所以跳过)。
- 访问节点 2。(3 2)
- 递归地遍历右子树:首先访问节点 4 的左子树,即节点 5(3 2 5),然后递归地遍历,4 的右子树,即节点 6。(3 2 5 6)
- 访问节点 4。(3 2 5 6 4)
- 最后访问根节点 1。(3 2 5 6 4 1)
递归实现后序遍历函数:
递归的路线:
左:
右:
可以看出,打印这个步骤是最后的 ,都是在左右子树走完之后
总结:
三种遍历二叉树的方法——前序遍历、中序遍历和后序遍历——的主要区别在于访问根节点的时机不同。
实现二:求节点/叶子节点/第K层节点数
二叉树模型依旧如前提所示:
1:求二叉树节点的个数
解释:
1:
int TreeSize(struct TreeNode* root)
:
- 这是一个函数定义,名为
TreeSize
,它接收一个指向二叉树节点的指针root
作为参数。 - 函数返回一个整数,这个整数代表了以
root
为根的整棵二叉树中的节点数量。
2:
return root==NULL? 0:TreeSize(root->left)+TreeSize(root->right)+1;
:
- 这是函数的返回语句,使用了条件运算符(也称为三元运算符)。
root==NULL
:检查传入的节点是否为空(即检查树是否为空或者是否到达了叶节点的子节点)。- 如果
root
为NULL
(意味着当前子树为空),则返回0
,因为空树没有节点。 - 如果
root
不为NULL
,则递归计算左子树和右子树的节点数量,并将它们相加,然后加上当前节点(root
),因此是TreeSize(root->left) + TreeSize(root->right) + 1
。
3:
递归的工作原理如下:
- 递归的基本情况(停止条件):当递归到叶子节点的子节点时(即遇到
NULL
),返回0
。 - 递归的递推情况:对于非空的树,函数递归地计算左子树和右子树的大小,将这两个值相加,并加上当前节点(
root
)本身。
递归最终会遍历二叉树中的每一个节点,并将它们全部计数一次。
4:
一种错的书写方式:
原因:表达式能用在三目中,而语句不能。return 0 这是一个语句,而0是一个表达式。
2:求叶子节点个数
解释:
1:递归的大事化小思路即:
叶子结点总数= 左子树的叶子节点+右子树的叶子节点
所以情况如下
a:一个节点是空返回0
b:一个节点不是空,但不是叶子节点,则向下递进
c:一个节点不是空,且是叶子节点,返回1,然后再向下递进。
如何判断是叶子节点?->本身不为空且左右孩子都为空,也就是前两个if都通过。
3:第k层的节点个数
解释:
1:递归的大事化小思路即:
当前树的第 K 层 = 左子树的第K-1层 + 右子树的第K-1层
也就是说:求第3层=求左子树的第2层+右子树的第2层
所以:我们要递进到第K层(k=1的时候,即到达了第K层),对每个节点判断,空则返回0,不为空且K=1则返回1,而且K一定为1,因为已经到了第K层。
2: 所以情况如下
a:节点为空,返回0
b:节点不为空,k不为1,向下递进
c:节点不为空,k为1,则返回1
也就是说:我们最后到底第k层的时候,也就是if中的K=1的时候,会对该k层的3(2的左子树) NULL(2的右子树) 5(4的左子树)6(4的右子树) 进行判断,空就是0,非空且k=1为1,而且K一定为1,因为已经到了第K层。
实现三:查找值为X的节点
模型依旧为:
1:代码
2:递归展开
假设找3:
假设找 7,7不存在,最后返回NULL
左:
右:
可知:
1:当ret不为空的时候,也就是找到x值的节点的时候,就会一级一级的往上返回
2:当ret一直都为空的时候,最后会return NULL 终止查找,该函数返回NULL
3:本篇为后序遍历查找,先查找跟,再left,再right
3:常见书写错误
A:
错在:
1:没有变量来接受左右孩子的TreeFind 的 返回值,所以无法根据返回值来判断是否还要继续查找
2:所以会不管找没找到,都会一直的查找下去
B:
错在:
1:|| 的返回值只会是真或者假,而 TreeFind函数的返回值是结构体指针,无法接受 || 的返回值
2:如果 TreeFind函数的返回值是 Bool值,到可以这么写,不过就变成了判断是否存在值为x的节点了。
C:
错在:
1:虽然有ret变量来接收左右孩子的 TreeFind函数的返回值,但是没有在两个函数之间进行判断,所以不管函数找没找到,都会一直查找,正确的节点给到了ret,但是会被覆盖。
实现四:通过前序遍历得到的字符串来构建(还原)二叉树
这里博主直接讲解一道进阶的题目:(将一串先序遍历字符串建立成一个二叉树,然后以中序遍历打印) ,相比实现四,多了一个中序遍历打印,自己省去即可。
一:题目
二:代码思路
前提:
此题一开始给我们的先序遍历字符串其中的 # 是代表NULL,所以我们接收到的字符串是二叉树的每个节点及其的左右子树(不管子树为不为NULL),所以我们按照前序遍历的思路进行还原即可。
举个例子:
前序和中序在此篇博客(有递归展开图)详细有讲:递归实现 前/中/后序 遍历二叉树 的详细讲解-CSDN博客
1:我们接收到的前序遍历字符串,肯定要保存到一个字符串数组中
2:还原成二叉树,所以要定义一个二叉树节点的结构体。
3:创建一个CreteTree函数,用来根据前序遍历字符串还原二叉树
4:一个中序遍历的函数,用来将还原的二叉树进行中序遍历打印
三:代码解读
第一个函数:
解释:
二叉树的单个节点的结构体定义,还原成二叉树,前提肯定是需要二叉树节点的定义的。
第二个函数:
解释:
递归展开图:
在int*pi下面的是下标,红色的线是递,蓝色的线是归,线旁边的是该线所处的第几步。
不清晰,从上到下分开发:
上:
中:
下:
第三个函数:
解释:
前序和中序在此篇博客(有递归展开图)详细有讲:递归实现 前/中/后序 遍历二叉树 的详细讲解-CSDN博客
主函数:
解释:
按照第二步的代码思路所写。
四:总代码截图:
实现五: 层序遍历
层序遍历用的是队列来实现
一:二叉树模型如前提
本文用到的队列的一切,在博主此篇博客中详细有讲:队列的实现(一篇包懂)_队列csdn-CSDN博客
二:
Q:为什么层序遍历能用队列实现?
A:
-
保持访问顺序:在层序遍历中,我们需要首先访问树的根节点,然后是根节点的所有子节点,接着是这些子节点的子节点,以此类推。队列可以保证节点按照它们被访问的顺序排队,从而确保遍历的顺序是正确的。
-
逐层访问:当我们从队列中取出一个节点进行访问时,我们可以同时将这个节点的所有子节点加入队列的末尾。由于队列是先进先出的,所以父节点会在子节点之前被访问,这确保了我们按照树的层级从上到下逐层访问。
-
方便扩展:队列的结构允许我们方便地添加新的节点。当遍历到一个节点时,它的子节点可以直接加入到队列的末尾,无需担心会破坏队列中已有的顺序。
具体步骤如下:
- 首先将根节点入队。
- 当队列不为空时:
- 从队列前端取出一个节点(访问该节点)。
- 将该节点的所有未被访问过的子节点入队。
- 重复上述步骤,直到队列为空。
三:层序遍历函数代码
层序遍历效果:
实现六: 判断是否为完全二叉树
二:思路
1:完全二叉树和非完全二叉树的区别在于->非空节点是连续的,就是完全二叉树、非空节点是不连续的,就不是完全二叉树。意思就是,完全二叉树不可能在节点中穿插一个NULL节点。
2:所以当我们队列中出到NULL节点的时候,此时队列中剩下的节点,应该全都是NULL,不可能有一个非空,但凡有一个非空,那一定不是完全二叉树
非完全二叉树:
出到NULL后,剩余的节点有非空
完全二叉树:
出到NULL后,层序后:
Q1:为什么能确保出到空的时候,此时队列里面的节点个数能够反映是否为完全二叉树呢?
因为空节点前的节点的下一层已经被带入到队列中,不可能存在:在下一层中被带入的节点之后才会有非空,因为这个不可能的非空只能在空节点的孩子中才能逃脱检测,但是这不可能
3:验证:将队列剩余的节点遍历
三:代码
四:函数的检测:
非完全二叉树,函数返回false即0
完全二叉树,函数返回true即1.
队列的.c和.h:
queque.h
queue.c
实现七:二叉树的销毁
一:代码
main函数中的使用:
解释:
Q1:为什么不在函数里面执行注释掉的那一行?
Q2:为什么再main函数中,使用完此函数后,还要对n1置空?这也起不到全部节点的置空啊?
A:
main函数中的TreeDestroy(n1)将一个结构体指针传给了函数,函数用root这个结构体指针来接受了n1这个结构体指针,现在两个结构体指针内容都是一样,但是两个结构体指针的地址不一样,此时在函数内置空root并不能置空n1,而在函数外置空n1,就能起到置空n1且无法访问root以下的节点
所有函数的验证:
所有函数.c文件