二叉树的实现大全(前/中/后/层序遍历,求节点数,求叶子节点数,求第K层节点数,查找值为X的节点,判断是否为完全二叉树,通过前序遍历的字符串构建二叉树)

一:前提 

前提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)是另一种遍历二叉树节点的算法。在中序遍历中,访问节点的顺序是:

  1. 中序遍历左子树首先递归地中序遍历根节点的左子树。
  2. 访问根节点然后访问根节点。
  3. 中序遍历右子树最后递归地中序遍历根节点的右子树。

简单来说,中序遍历的顺序是:左子树 -> 根节点 -> 右子树。

中序遍历的结果是:3-> 2 ->1 -> 5 -> 4 -> 6

这个过程可以这样理解:

  1. 递归地遍历左子树:访问节点 2 的左子树,即节点 3。(3)
  2. 访问节点 2。(3 2)
  3. 继续递归地遍历 2 的右子树,(这里为空,所以跳过)。
  4. 回到根节点 1,访问节点1。(3 2 1)
  5. 递归地遍历右子树:访问节点 4 的左子树,即节点 5(3 2 1 5)
  6. 访问节点4(3 2 1 5 4)
  7. 访问节点 4 的右子树,即节点 6(3 2 1 5 4 6)

 递归实现中序遍历函数:

 递归的路线:

左:

右:

 可以看出,打印这个步骤是次先的 ,都是在左子树走完之后

3:后序遍历 

模型:

后序遍历效果:

二叉树的后序遍历(Post-order Traversal)是遍历二叉树节点的另一种算法。在后序遍历中,访问节点的顺序是:

  1. 后序遍历左子树首先递归地后序遍历根节点的左子树。
  2. 后序遍历右子树然后递归地后序遍历根节点的右子树。
  3. 访问根节点:最后访问根节点。

简单来说,后序遍历的顺序是:左子树 -> 右子树 -> 根节点。

后序遍历的结果是:3 -> 2-> 5 -> 6 ->4 -> 1

这个过程可以这样理解:

  1. 递归地遍历左子树:首先访问节点 2 的左子树,即节点 3。(3)
  2. 继续递归地遍历 2 的右子树,(这里为空,所以跳过)。
  3. 访问节点 2。(3 2)
  4. 递归地遍历右子树:首先访问节点 4 的左子树,即节点 5(3 2 5),然后递归地遍历,4 的右子树,即节点 6。(3 2 5 6)
  5. 访问节点 4。(3 2 5 6 4)
  6. 最后访问根节点 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:检查传入的节点是否为空(即检查树是否为空或者是否到达了叶节点的子节点)。
  • 如果rootNULL(意味着当前子树为空),则返回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. 保持访问顺序:在层序遍历中,我们需要首先访问树的根节点,然后是根节点的所有子节点,接着是这些子节点的子节点,以此类推。队列可以保证节点按照它们被访问的顺序排队,从而确保遍历的顺序是正确的。

  2. 逐层访问:当我们从队列中取出一个节点进行访问时,我们可以同时将这个节点的所有子节点加入队列的末尾。由于队列是先进先出的,所以父节点会在子节点之前被访问,这确保了我们按照树的层级从上到下逐层访问。

  3. 方便扩展:队列的结构允许我们方便地添加新的节点。当遍历到一个节点时,它的子节点可以直接加入到队列的末尾,无需担心会破坏队列中已有的顺序。

具体步骤如下:

  • 首先将根节点入队。
  • 当队列不为空时:
    • 从队列前端取出一个节点(访问该节点)。
    • 将该节点的所有未被访问过的子节点入队。
  • 重复上述步骤,直到队列为空。

三:层序遍历函数代码

层序遍历效果:

 实现六: 判断是否为完全二叉树

二:思路

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文件

 

 

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值