当前位置: 首页 > news >正文

DS:二叉树的链式存储及遍历

欢迎来到Harper.Lee的学习世界!
博主主页传送门:Harper.Lee的博客主页
想要一起进步的uu可以来后台找我哦!

一、引入

1.1 二叉树的存储方式

在之前接触到的满二叉树和完全二叉树使用的是数组的存储方式(DS:树与二叉树的相关概念):二叉树因为是顺序结构,比较适合数组形式的存储,但如果不是这两种树形结构,而是其他非完全二叉树,如果用数组实现,那数组的中间部分就可能会存在大量的空间浪费,因此就不适合数组存储,而采用的是链式存储方式
二叉树使用的是链式结构存储,即使用链表来存储二叉树,就不会有上述的缺陷。链式结构中有左孩子指针和右孩子指针,如果少了左孩子或者右孩子,就让相应的指针置为NULL即可。
虽然链式结构可以表示所有类型的二叉树,但是由于二叉树本身存储数据的价值并不大(链表、顺序表远远优于二叉树),所以实现增删查改是没有太大意义的,学习链式二叉树真正的意义是学会如何去控制、遍历二叉树的结构,为我们后期学习搜索二叉树做好铺垫。而以下的学习中要重点理解二叉树中的递归思想和分治思想!

1.2 二叉树的应用之一——搜索二叉树

二叉树常用的应用方式就是搜索二叉树,搜索二叉树的特点:左子树的所有值比根节点小,右子树的所有值比根节点大,如下图。(左子树<根<右子树)
image.png
为什么叫二叉搜索树呢?顾名思义,它有数据的搜索功能。例如,查找数据43:43比27大,搜索范围变成右子树,43比45小,搜索范围在左子树,43比42大,搜索范围在右子树,43=43,查找到该数据,且搜索范围逐渐缩小了。
这种查找方式有点类似于二分法,但是二者有很大的不同,搜索二叉树比它效率高好几倍:

对比
1. 二分法

2. 二叉搜索树
数据的存储方式数组存储,增删查改效率低:可能会涉及到数据的挪动等左子树的所有值比根节点小,右子树的所有值比根节点大
可适用于各种类型的数据,但同一棵树中的数据是同一类型
要求需要排序:排序本身消耗就大不要求排序,兄弟之间无序,父子之间谁大谁当爹或者谁小谁当爹

二分法在实际中本就不常用二叉搜索树的最大缺陷:不适用于单支的树状结构(如下图)

image.png
本次搜索二叉树的学习主要有两个目的:1. 最注重的是为后面的学习打基础:对于二叉树的学习,不只是有搜索二叉树,还有AVL树红黑树等二叉平衡搜索树、B数系列多叉平衡搜索树等,他们的基础都在搜索二叉树上。2. 本次学习重点还需要理解二叉树中的递归思想和分治思想。

二、链式二叉树的实现

2.1 相关结构体

typedef int BTDataType;typedef struct BinaryTreeNode
{BTDataType data;//二叉树节点的数据域struct BinaryTreeNode* left;//左孩子struct BinaryTreeNode* right;//右孩子
}BTNode;//重命名为比较简单的名字

2.2 手搓一棵树

在之前学习链表的时候,我们是先插入数据来进行对实现的功能的测试;数组测试我们可以直接给一个数组。同样在这里首先我们需要创建一棵树,才能进行测试,二叉树的增删查改没啥意义,加入二叉搜索树后就有意义了。手搓二叉树只是为了方便测试。

//手搓一棵二叉树
BTNode* CreatBinaryTree()
{//1. 先创建6个节点BTNode* node1 = BuyNode(1);//2. BuyNode节点创建函数的实现BTNode* node2 = BuyNode(2);BTNode* node3 = BuyNode(3);BTNode* node4 = BuyNode(4);BTNode* node5 = BuyNode(5);BTNode* node6 = BuyNode(6);//3. 用left、right连接节点,使其变成自己想要的树的形状node1->left = node2;node1->right = node4;node2->left = node3;node4->left = node5;node4->right = node6;return node1;//直接返回根节点,有了根节点,就有了左膀右臂,直接开火车就可以找到整棵树
}

三、遍历

链式二叉树的增删查改操作没有太大的实际意义,因此我们主要探索的是它的遍历方式。**对于任意一棵树,访问的顺序有三种:前序(前根遍历)、中序(中根遍历)、后序(后根遍历)。 **

3.1 前序遍历(Preorder Traversal )

3.1.1 代码实现

//前序遍历
void PrevOrder(BTNode* root)
{//1. 判空if (root == NULL){printf("N ");//为空的地方打印N,这样更能表示出访问的位置return;}//可以不写else//2. 开始前序遍历//3. 先访问根(打印根节点的数据)printf("%d ", root->data);//4. 再访问左子树leftPrevOrder(root->left);//5. 最后访问右子树PrevOrder(root->right);
}

3.1.2 分析过程

逻辑过程分析(使用代码进行分析):这里其实就是在重复执行一套指令,只不过我为了更加形象地描述它,选择了用代码展开分析讨论。
image.png
物理过程分析:函数调用是建立栈桢的过程,每个函数调用都是独立的栈桢,调用结束,栈桢销毁。
image.png

如果递归深度太深了,容易导致栈溢出的情况。 如上图的最后。
根据上述画图中,我们可以知道,PrevOrder函数的递归调用过程中一直在重复使用同一套指令。一套指令重复使用多次,只是每次传入的参数不同,参数不同,执行的逻辑就不同,树状开枝散叶的成都也就不同。此过程中,函数递归的调用并不是死循环的调用,它会遇到NULL,通过return结束该小部分的函数调用,返回上一个调用的函数。
此外,函数递归调用时的参数是存储在栈中的。每个函数调用都是独立的栈桢,栈桢之间相互独立。

3.2 中序遍历(Inorder Traversal)


中序遍历分析的逻辑过程、逻辑过程和前序遍历是一样的分析方法,只需照猫画虎即可。

//中序遍历
void InOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}

3.3 后序遍历(Postorder Traversal)

后序遍历的物理过程和逻辑过程和前序遍历的分析方式一样。

3.4 层序遍历(BFS)

层序遍历是一种广度优先遍历(BFS),

四、对树的相关计算

4.1 递归的分治思想

image.png我们一般在没写出代码的时候就很难画出像3.1.2那样的分析图的,因此,在不知道代码如何写的情况下,我们选择递归的分治思想,大概分析出整个过程的思维框架,以便于更好地写出代码。下图是以树的节点个数为例子的一种分析过程:
image.png
根据上图再逐步接触对应的代码。

//分治思想求树的节点个数
int TreeSize(BTNode* root)
{return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

分治,即分而治之,类似于上下级的管理。例如校长要求学校中统计在校学生人数、在校师生人数等。校长肯定不是自己挨个地去数人数,而是通过各种管理层统计后进行汇总。就像各个班长汇总数据给辅导员,各个辅导员汇总数据给院长,各个院长再汇总数据给校长,校长就得到了最终的数据。
在这个管理层中,级别最低的就是没当班长的学生,他们就是这个管理树中的叶子节点。
image.png

4.2 求树的节点个数

(1)错误代码

//求节点个数的错误代码
int TreeSzie(BTNode* root)
{static int size = 0;//size定义为静态变量:局部静态只会初始化一次,即在第一次的时候才会初始化,其他都是直接积累就用了//如果树为空树if (root == NULL)return 0;else++size;//树不是空树//开始遍历(遍历方式随便一种,这里是前序遍历)TreeSzie(root->left);TreeSzie(root->right);return size;
}

(2)正确代码

a. 定义全局变量

//求节点个数
int size = 0;//定义为全局变量int TreeSzie(BTNode* root)
{//如果树为空树if (root == NULL)return 0;else++size;//树不是空树//开始遍历(遍历方式随便一种,这里是前序遍历)TreeSzie(root->left);TreeSzie(root->right);return size;
}

b. 定义指针

int TreeSize(BTNode* root,int* psize)
{if (root == NULL)return 0;else++(*psize);//遍历树的任一方式:TreeSize(root->left, psize);TreeSize(root->right, psize);return *psize;//可以不用返回值,返回类型为void
}

4.3 二叉树的叶子节点个数

注意不要忽略空树的情况,空树进入函数,->相当于解引用,堆空指针解引用报错!

//求叶子节点的个数
int TreeLeafSize(BTNode* root)
{if (root == NULL)return 0;else if (root->left && root->right)return 1;elsereturn TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

4.4 二叉树的高度

image.png
欲扬先抑,要求这个,我就先找下面另外一个,就像校长统计在校师生人数一样,校长直接找各位院长得到数据+1汇总,而院长直接就是各位辅导员数据+1汇总,辅导员数据为班长数据+1(班长自己),层层向下,就像命令在传递一样。班长得到数据后返回辅导员,辅导员得到班长数据后返回院长,院长得到辅导员数据后返回校长,整个过程一来一回,数据就统计出来了。
image.png

//求树的高度
int TreeHeight(BTNode* root)
{if (root == NULL)return 0;elsereturn TreeHeight(root->left) > TreeHeight(root->right) + 1 ?TreeHeight(root->left) : TreeHeight(root->right) + 1;
}

缺陷:如果该二叉树是一个节点的高度很大的树,那么每个位置会出现重复计算的情况,且重复计算并不一定是两次,可能会重复计算多次。因此我们需要用变量记录每次计算的结果,若还需要该结果,可直接使用该变量。就是在这一点上,我们常常错误的认为重复计算两次即时间消耗为2倍,其实时间消耗远不止2倍。分析过程如下图。

//求树的高度
//时间复杂度:O(N)
int TreeHeight(BTNode* root)
{if (root == NULL)return 0;//保存记录数据int leftHeight = TreeHeight(root->left);int rightHeight = TreeHeight(root->right);//可以使用fmax函数return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

image.png
这就相当于不记事的某一层领导因为忘记数据,又再安排下层领导一次,该领导这条路就又重复一次。

4.5 二叉树查找值为x的节点

(1)分析过程如下:

image.png

(2)代码实现

//二叉树中查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL)retur NULL;if (root->data == x)return root;BTNode* ret1 = BinaryTreeFind(root->left, x);if (ret1)return ret1;BTNode* ret2 = BinaryTreeFind(root->right, x);if (ret2)return ret2;return NULL;
}

改进版本:

//改进版本:
BTNode* _BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL)retur NULL;if (root->data == x)return root;BTNode* ret1 = _BinaryTreeFind(root->left, x);if (ret1)return ret1;return _BinaryTreeFind(root->right, x);
}

喜欢的朋友记得三连支持哦!
求赞

相关文章:

  • 解析Java中1000个常用类:AbstractSequentialList类,你学会了吗?
  • 视觉新纪元:解码LED显示屏的视角、可视角、最佳视角的最终奥秘
  • DDMA信号处理以及数据处理的流程---距离速度测量
  • 三目运算符中间的表达式可以省略吗(a?:c)?
  • 【pytorch01】简单回归问题
  • 空间复杂度 线性表,顺序表尾插。
  • 离线linux通过USB连接并使用手机网络
  • 初学者应该掌握的MySQL数据库的基本组成部分及概念
  • 【Docker】——安装镜像和创建容器,详解镜像和Dockerfile
  • 【Qt】QList<QVariantMap>中数据修改
  • ic基础|功耗篇03:ic设计人员如何在代码中降低功耗?一文带你了解行为级以及RTL级低功耗技术
  • 指纹浏览器与虚拟机的区别及在跨境电商中的应用
  • LeetCode 每日一题 2024/6/17-2024/6/23
  • ChatGPT 简介
  • 日语 13 14
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • create-react-app做的留言板
  • ES6简单总结(搭配简单的讲解和小案例)
  • Hibernate【inverse和cascade属性】知识要点
  • HTML5新特性总结
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • Java面向对象及其三大特征
  • log4j2输出到kafka
  • Magento 1.x 中文订单打印乱码
  • MobX
  • October CMS - 快速入门 9 Images And Galleries
  • Odoo domain写法及运用
  • 前嗅ForeSpider教程:创建模板
  • 数据科学 第 3 章 11 字符串处理
  • 长三角G60科创走廊智能驾驶产业联盟揭牌成立,近80家企业助力智能驾驶行业发展 ...
  • ​草莓熊python turtle绘图代码(玫瑰花版)附源代码
  • #如何使用 Qt 5.6 在 Android 上启用 NFC
  • (007)XHTML文档之标题——h1~h6
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (AngularJS)Angular 控制器之间通信初探
  • (done) NLP “bag-of-words“ 方法 (带有二元分类和多元分类两个例子)词袋模型、BoW
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (待修改)PyG安装步骤
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (五)MySQL的备份及恢复
  • (转)视频码率,帧率和分辨率的联系与区别
  • (转载)Linux 多线程条件变量同步
  • (转载)跟我一起学习VIM - The Life Changing Editor
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • .aanva
  • .Net IOC框架入门之一 Unity
  • .NET Project Open Day(2011.11.13)
  • .net 无限分类
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .net的socket示例
  • .NET简谈设计模式之(单件模式)
  • .NET建议使用的大小写命名原则
  • .NET设计模式(8):适配器模式(Adapter Pattern)
  • @拔赤:Web前端开发十日谈