二叉树链式结构的实现

前言


在上一篇中我们讲解了二叉树的顺序存储结构,并基于完全二叉树的顺序存储原理,深入学习了堆的原理与代码实现:

https://blog.csdn.net/gumidc/article/details/160929011?spm=1011.2124.3001.6209

但顺序存储只适合完全二叉树、满二叉树,对于普通形态不规则的二叉树,会造成大量空间浪费。

因此接下来,我们学习一下二叉树链式结构的实现,通过节点链式存储的方式,我们可以表示任意形态的二叉树。


1.前置说明

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。考虑到大家目前对二叉树结构掌握还不够深入,为了降低入门门槛,此处手动快速创建一棵简单的二叉树,让大家可以立刻上手,直观理解二叉树各类遍历逻辑。等大家把二叉树结构了解的差不多时,我们再回头讲解二叉树真正的创建方式。

注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后续会重点讲解。

2.二叉树的遍历

二叉树最核心的基础操作就是遍历,根据对根节点的访问时机不同,分为三大递归遍历方式:

1. 前序遍历(Preorder Traversal )------访问根结点的操作发生在遍历其左右子树之前。

2. 中序遍历(Inorder Traversal)------访问根结点的操作发生在遍历其左右子树之间。

3. 后序遍历(Postorder Traversal)------访问根结点的操作发生在遍历其左右子树之后。

接下来以上面我们创建的二叉树为例,完整演示三种遍历的执行流程与最终访问顺序(N代表空节点):

2.1 前序遍历(根 左 右)

遍历逻辑拆解:

1.先根:访问1

2.遍历左子树(以2为根):先根2 -> 左3(以3为根)-> 左空(N)-> 右空(N)-> 回退到2访问2的右子树(N)-> 回退到1

3.遍历右子树(以4为根):先根4 -> 左5(以5为根)-> 左空(N)-> 右空(N)-> 回退到4访问4的右子树6(以6为根)-> 左空(N)-> 右空(N)

代码实现:

接下来画一个代码调用过程的展开图,从逻辑上来进一步加深我们对于这个递归过程的理解,我画好了左子树的调用过程供参考,你可以试着画一画右子树:

递归物理底层原理:

整个递归过程,本质是函数栈帧的反复创建、调用与销毁:

每次递归调用函数,都会在栈上开辟新的栈帧;函数执行结束后,栈帧随即销毁。

比如节点3的左子树调用、和右子树调用,复用的是同一块栈内存空间,并不会额外持续占用内存。

2.2 中序遍历(左 根 右)

遍历逻辑拆解:

1.左子树:左空(N)-> 根3 -> 右空(N) -> 根2 -> 2的右子树空(N)

  1. 访问总跟:1

3.右子树:左空(N)-> 根5 -> 右空(N)-> 根4 ->右6(以6为根)-> 左空(N)-> 右空(N)

代码实现:

2.3 后序遍历(左 右 根)

遍历逻辑拆解:

1.左子树:左空(N) -> 右空(N)-> 根3 -> 回退到2访问2的右子树(N)-> 根2

2.右子树:左空(N)-> 右空(N)-> 根5 -> 回退到4访问4的右子树6的左子树(N)-> 6的右子树(N)-> 根6 -> 根4

3.最后访问总根:1

代码实现:

2.4 二叉树基础递归练习

2.4.1 求二叉树的节点个数

思路一:运用前序遍历,节点不为空size就+1。

但是上述代码是存在问题的,如下图:

static修饰的局部变量,只会在程序启动时初始化一次,函数结束后数值不会清零销毁。

• 第一次调用统计:正确返回节点总数6

• 第二次重复调用:数值会在上一次结果基础上继续累加,错误输出12,和预期不符

想要解决size局部变量的特性,我们可以考虑使用全局变量

但是这种写法也存在问题:全局变量size只会初始化一次,每次调用统计前必须在外层手动清零,代码维护性较差。

思路二:使用分治递归法,拆分问题:

代码实现:

2.4.2 求二叉树叶子节点的个数

思路:使用分治递归法,拆分问题:

代码实现:

2.4.3 求二叉树的高度

思路:同上,二叉树的高度根据节点有无分为如下两种情况:

代码实现:

但是这样的写法在一些情况下会出现问题,接下来我们画一下简化递归调用图来理解为什么:

根据上图,递归先沿红色路径一路左探,节点3左子树为空返回0;递归继续走到节点6,先后算出左右子树全空,双双返回0。

节点6完成左右子树高度比较后,需要计算自身高度+1,但代码未缓存临时结果,只能重新递归右子树(绿色路径)。

结果返回节点3后,节点3判定右子树更高,同样的问题再次出现:之前算出的右子树高度丢失,节点3只能再次对右子树进行完整的递归(蓝色路径)。

连带底层的节点6,又被迫把全套递归流程跑一遍(黄色路径)。

向上回溯到节点2、节点1时,这个问题会逐层放大:每一层都遗忘下层已经算完的高度,每次比较结束后,都要把更高的子树完整重算一遍。

最终造成:大量节点被反复遍历,大树、深树场景下程序运行极慢,还极易栈溢出崩溃。

优化如下:

先提前计算,临时保存左右子树高度,全程每个节点仅遍历一次。

2.4.4 求二叉树第k层节点的个数

思路:使用分治递归法,拆分问题:

代码实现:

如下为该过程的简化递归调用图,我画好了左子树的调用过程供参考,你可以试着画一画右子树来加深理解:

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

实现逻辑(前序遍历查找)

  1. 空树:直接返回NULL,查找失败

  2. 当前节点的值就是目标值:直接返回当前节点地址

  3. 先递归查找左子树,左子树找到就直接返回结果

  4. 左子树没找到,继续递归查找右子树

代码实现:

2.5 层序遍历

层序遍历是一种广度优先搜索(BFS) 策略。它要求我们按照"从上到下,从左到右"的顺序,逐层访问二叉树中的所有节点。

与之前的递归遍历(深度优先)不同,层序遍历的实现必须借助一个队列(Queue) 来辅助完成。

思路:

  1. 初始化:创建一个队列,并将二叉树的根节点入队。

  2. 循环遍历:当队列不为空时,重复以下步骤:

◦ 出队:将队首的节点取出。

◦ 访问:对取出的节点进行访问(如打印其值)。

◦ 入队:将该节点的左孩子和右孩子依次入队(如果孩子存在)。

  1. 结束:队列为空时,遍历完成。

创建队列部分的代码在这里不做赘述,不熟悉的同学可以再去回顾一下:

https://blog.csdn.net/gumidc/article/details/160866387?spm=1011.2124.3001.6209

参考代码如下:

2.5.1 判断二叉树是否是完全二叉树

思路:

1.层序遍历,即使遇到空节点也要将其入队。

  1. 遇到第一个空节点后,停止向队列中加入新节点

  2. 停止入队后,检查队列中剩余的所有元素:

◦ 如果队列中剩下的全是空节点,则这棵树是完全二叉树。

◦ 如果队列中还存在任何非空节点,则这棵树不是完全二叉树。

这个思路对于如下场景可行吗?

答案是可行的。因为当层序出到空时,如果前面非空都出完了,那么这些非空的子节点一定也进队列了。

参考代码如下:

3.二叉树基础OJ练习

理论学习后,我们通过经典的OJ题目来检验与巩固所学知识。

3.1 单值二叉树

https://leetcode.cn/problems/univalued-binary-tree/

思路一:遍历,保存根节点的值,再对二叉树进行遍历,将各节点的值与根节点的值一一进行比较。

参考代码如下:

思路二:递归

参考代码如下:

3.2 相同的树

https://leetcode.cn/problems/same-tree/

参考代码如下:

3.3 对称二叉树

https://leetcode.cn/problems/symmetric-tree/

参考代码如下:

3.4二叉树的前序遍历

https://leetcode.cn/problems/binary-tree-preorder-traversal/

根据题意,我们要将二叉树中的元素全部拷贝到一个数组中去,并且数组的空间也需要我们自行开辟,所以我们封装一个新的函数TreeSize专门去管理开辟空间的大小。

注意:题目给出的原函数中的参数returnSize属于输出型参数,表示数组的大小

由此我们可以写出下述代码:

但是上述代码运行后是无法通过的。为什么?

下面的例子没有通过,那么我们就用这个例子来画代码递归图找问题:

根据上图我们就能清晰地找到问题了,由于i在遍历左树的时候+1并不会影响原来i的值,所以遍历右树的时候i接收的值还是1,所以最终得到的结果为【3,2,随机值】。

因此我们应该将 i 的地址传过去,修改后的代码如下:

3.5 另一棵树的子树

https://leetcode.cn/problems/subtree-of-another-tree/

参考代码如下:

我们借助一个例子画如下的代码递归图来进一步理解这个实现过程:

4. 二叉树的创建与销毁

4.1通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树

参考代码如下:

4.2二叉树的销毁

画如下简化递归图来帮助我们写代码:

参考代码如下:

理论看懂不算真正学会,接下来请大家独立完成下面的练习题,彻底吃透二叉树的全部核心操作:

二叉树遍历_牛客题霸_牛客网https://www.nowcoder.com/practice/4b91205483694f449f94c179883c1fef?tpId=60&&tqId=29483&rp=1&ru=/activity/oj&qru=/ta/tsing-kaoyan/question-ranking

相关推荐
战南诚1 小时前
力扣 之 198.打家劫舍
python·算法·leetcode
AllData公司负责人1 小时前
亲测丝滑,体验跃迁|AllData通过集成开源项目StreamPark,实时流任务调度更省心!
java·大数据·数据库·人工智能·算法·实时计算·实时开发平台
计算机安禾1 小时前
【c++面向对象编程】第46篇:CRTP(奇异递归模板模式):静态多态的妙用
开发语言·c++·算法
广州灵眸科技有限公司1 小时前
瑞芯微(EASY EAI)RV1126B 音频电路
开发语言·人工智能·深度学习·算法·yolo·音视频
Dlrb12112 小时前
数据结构-链表
数据结构·链表·逻辑结构·单向链表·物理结构·valgrind工具
小的~~2 小时前
算法题:只出现一次的数字
数据结构·算法
灵智实验室2 小时前
PX4状态估计技术EKF2详解(六):EKF2 磁力计融合——从航向修正到 3D 姿态约束
算法·无人机·px 4
JieE2122 小时前
手把手带你用虚拟头节点实现单链表,搞定所有边界问题
javascript·算法
搞科研的小刘选手2 小时前
【大连市计算机学会主办】第三届图像处理、智能控制与计算机工程国际学术会议(IPICE 2026)
图像处理·人工智能·深度学习·算法·计算机·数据挖掘·智能控制