「数据结构」二叉树2

🎇个人主页:Ice_Sugar_7
🎇所属专栏:初阶数据结构
🎇欢迎点赞收藏加关注哦!

文章目录

  • 🍉前言
  • 🍉链式结构
  • 🍉遍历二叉树
    • 🍌前序遍历
    • 🍌中序遍历
    • 🍌后序遍历
  • 🍉计数
    • 🍌求结点数
    • 🍌求叶子结点数
    • 🍌求第k层结点数
  • 🍉树的深度
  • 🍉查找结点
  • 🍉构建二叉树
  • 🍉销毁二叉树
  • 🍉层序遍历
  • 🍉判断是否为完全二叉树
    • 🍌补充
  • 🍉写在最后

🍉前言

在上一篇文章中我们讲了二叉树的顺序结构,但是普通二叉树因为结点不连续,没法使用顺序结构,这就需要使用链式结构进行存储。本文将讲解二叉树的链式结构及常见函数的实现


🍉链式结构

一个结点分为三个部分:存放当前结点的数值的数据域、分别指向左、右子树的指针

typedef char BTDataType;
typedef struct BinaryTreeNode
{BTDataType _data;struct BinaryTreeNode* _left;struct BinaryTreeNode* _right;
}BTNode;

🍉遍历二叉树

二叉树有三种遍历方式,通过递归实现
需要根据使用场景选择不同的遍历方式

🍌前序遍历

先访问根结点,接着是左子树,最后访问右子树(这里的“访问”指的是把结点的数值打印出来)
采用递归,那就要将大问题拆分为小问题,直到问题无法再继续拆分
●现在要访问左子树,那可以把问题拆解为“依次访问它的根结点、左子树、右子树”,拆解若干次之后,会抵达叶子结点,再往下就是空结点了,此时没法继续拆解问题,也就是到达递归的终点了,需要回归
●右子树也同理

void BinaryTreePrevOrder(BTNode* root) {if (!root) {  cout << "NULL" << " ";  //为了方便观察,所以把NULL也打印出来return;}cout << root->_data << " ";BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);
}

有了前序遍历作为参照,那中后序遍历也就很轻松就能写出来了,只要调整一下打印语句的位置就ok了,下面直接上代码

🍌中序遍历

void BinaryTreePrevOrder(BTNode* root) {if (!root) {  cout << "NULL" << " ";  //为了方便观察,所以把NULL也打印出来return;}BinaryTreePrevOrder(root->_left);cout << root->_data << " ";BinaryTreePrevOrder(root->_right);
}

🍌后序遍历

void BinaryTreePrevOrder(BTNode* root) {if (!root) {  cout << "NULL" << " ";  //为了方便观察,所以把NULL也打印出来return;}BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);cout << root->_data << " ";
}

🍉计数

🍌求结点数

求结点数,可以转化为左子树结点数+右子树结点数+1(根结点本身),如果遇到空结点,那就返回0

int BinaryTreeSize(BTNode* root) {if (!root)return 0;return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}

🍌求叶子结点数

和求结点数差不多,不过多了一个判断是否为叶子结点的语句,如果是,就返回1

//左右结点都为空返回1   
int BinaryTreeLeafSize(BTNode* root) {if (!root)return 0;if (root->_left == NULL && root->_right == NULL)return 1;//不为空or只有一个子树为空return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}

🍌求第k层结点数

这个的递归不太好找。
方法如下:
假设k为5,那么第k层相对于第一层就是第五层,而它相对第二层则是第四层,相对第三层是第三层……相对第五层就是第一层。也就是说要求第k层,实际上是求“第一层”的结点个数
(有点像求一个数的阶乘时用到的递归思想)

int BinaryTreeLevelKSize(BTNode* root, int k) {assert(k > 0);  //确保k为正数if (!root)return 0;if (k == 1)  //此时已经递归到了第k层return 1;return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}

🍉树的深度

深度是指从根结点到叶子结点的最长距离。这个问题可以拆解为求第二层的根结点到叶子结点的最长距离+1,再拆为第三层到叶子结点的最长距离+2……最后遇到空结点就可以停下来了
最后返回左子树和右子树二者深度的较大值

代码如下:

int BinaryTreeDepth(BTNode* root) {if (!root)return 0;int LeftSize = BinaryTreeDepth(root->_left);int RightSize = BinaryTreeDepth(root->_right);return LeftSize > RightSize ? LeftSize + 1 : RightSize + 1;
}

🍉查找结点

要在二叉树里面查找值为x的结点。每次递归先找左子树,找到就返回,找不到就去找右子树。
下面两个条件满足其一,递归终止:①到空结点时返回NULL;②找到值为x的结点就返回该结点

代码如下:

BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {if (!root)return NULL;if (root->_data == x)return root;BTNode* ret = BinaryTreeFind(root->_left, x);if (ret)  //如果左子树找到指定结点,就不用找右子树了return ret;return BinaryTreeFind(root->_right, x);
}

🍉构建二叉树

现在已知一个数组,它是某二叉树前序遍历的结果,该数组会用#表示空结点,现在要我们通过这个数组来构建原二叉树
●通过递归来构建。每次递归时数组当前位置的元素如果不为#,就创建一个结点,然后将它和双亲结点连起来。
●既然要让结点间能够连接起来,那函数就要返回结点或者NULL。这样在递归的过程中可以将新创建的结点或者NULL和双亲结点连接起来
●递归的停止条件就是遇到#,此时返回NULL,

BTNode* BinaryTreeCreate(BTDataType* a, int n, int pi) {  //n:数组大小  pi:指向数组下标if (a[pi] == '#') {++pi;return NULL;}BTNode* node = (BTNode*)malloc(sizeof(BTNode));node->_data = a[pi++];  //数组赋值给node之后记得++node->_left = BinaryTreeCreate(a, n, pi); //连接左子树node->_right = BinaryTreeCreate(a, n, pi); //连接右子树return node;  //返回根结点
}

这里要说一下这个pi因为我们使用递归而非迭代,需要知道数组下标(即递归到哪个元素了),所以要传下标


🍉销毁二叉树

要采用后序遍历销毁结点,如果采用前序或中序,根结点都不是最后销毁的,这样会导致无法找到某一边的子树。比如采用中序,销毁左子树后把根结点给销毁了,那就没法找到右子树了

void BinaryTreeDestory(BTNode** root) {assert(root);if (*root == NULL)return;BinaryTreeDestory(&(*root)->_left);BinaryTreeDestory(&(*root)->_right);free(*root);*root = NULL;
}

🍉层序遍历

前面讲的前、中、后序遍历都属于深度优先遍历,以前序遍历为例,会先递推到最左下的结点,然后才回归,这是一个纵向的过程。
而层序遍历又称为广度优先遍历,顾名思义,就是一层一层遍历。这种遍历需要使用队列
具体的过程为:先让根结点入队,标记为队首front,然后将它的左右子树根结点入队(前提是结点不为空,如果为空那就不用入),再让队头的根结点出队,子树的根结点成为新的队头。
重复上面这个过程,直到队列为空

举个栗子:
在这里插入图片描述
代码如下:

void BinaryTreeLevelOrder(BTNode* root) {if (!root)return;Queue q;QueueInit(&q);QueuePush(&q, root);while (!QueueEmpty(&q)) {QDataType front = QueueFront(&q); //队首元素cout << QueueFront(&q)->_data << endl;QueuePop(&q);  //队首元素出队  然后要让它的左右子树入队if (root->_left)QueuePush(&q, front->_left);if (root->_left)QueuePush(&q, front->_right);}QueueDestroy(&q);
}

🍉判断是否为完全二叉树

完全二叉树的特点就是结点连续,根据这个性质,我们采用层序遍历,不过这次层序遍历需要把空结点也入队。如果遇到空结点时后面的结点也全为空,那就是完全二叉树;反之,如果后面还有非空结点,那就不是
简而言之,我们要判断front为空结点时队列剩下的元素是否全为空

代码如下:

bool BinaryTreeComplete(BTNode* root) {if (!root)return true;Queue q;QueueInit(&q);QueuePush(&q, root);BTNode* front = root;while (!QueueEmpty(&q)){front = QueueFront(&q); if (front == NULL)  //队首是空结点,就是遇到的第一个空结点,跳出循环break;//空结点也要入队QueuePush(&q, front->_left);QueuePush(&q, front->_right);QueuePop(&q);  //队首元素出队}//到这里的时候说明已经遇到空结点//接下来要排查剩下的结点,看看是否有非空结点while (!QueueEmpty(&q)) {front = QueueFront(&q);QueuePop(&q);if (front) {  //若遇到不为空的结点,说明不是完全二叉树QueueDestroy(&q);return false;}}//到这里说明全部都是空结点,那就是完全二叉树了QueueDestroy(&q);return true;
}

🍌补充

	while (!QueueEmpty(&q)) {front = QueueFront(&q);QueuePop(&q);if (front) {  QueueDestroy(&q);return false;}}

我们已经将队首的结点pop掉,那后面的if语句还能否访问front?
答案是可以。因为front保存队首结点的值,而这个值是二叉树结点的地址,即指向二叉树的结点。(刚才上面那个图是为了方便理解,所以才把front的箭头指向队首)pop掉队首结点并不会影响树的结点,所以还是可以访问的


🍉写在最后

以上就是本篇文章的全部内容,如果你觉得本文对你有所帮助的话,那不妨点个小小的赞哦!(比心)

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于协同过滤的电影评论数据分析与推荐系统

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长 QQ 名片 :) 1. 项目介绍 随着社会的发展&#xff0c;人们生活水平的提高&#xff0c;欣赏电影逐渐成为人们闲暇时的主要娱乐方式之一。本文电影推荐系统是为了给顾客提供方便快捷的热门电影推荐以及查询电影资讯而建立的&…

MFC窗体背景颜色的设置、控件白色背景问题、控件文本显示重叠问题、被父窗体背景覆盖的问题

文章目录 设置mfc窗体背景颜色窗体设置背景颜色后解决控件白色背景解决重复修改控件文本后重叠的问题自绘控件被父窗体背景覆盖的问题 设置mfc窗体背景颜色 设置窗体的背景颜色非常简单&#xff0c;只需要在窗体的OnEraseBkgnd里面填充窗体背景就可以了&#xff0c;甚至直接画…

强化学习(五)-Deterministic Policy Gradient (DPG) 算法及公式推导

针对连续动作空间&#xff0c;策略函数没法预测出每个动作选择的概率。因此使用确定性策略梯度方法。 0 概览 1 actor输出确定动作2 模型目标&#xff1a; actor目标&#xff1a;使critic值最大 critic目标&#xff1a; 使TD error最大3 改进&#xff1a; 使用两个target 网络…

每日一题——轮转数组

1. 题目描述 给定一个整数数组nums&#xff0c;将数组中的元素向右轮转k个位置&#xff0c;其中k是非负数。 示例1: 输入&#xff1a;nums [1,2,3,4,5,6,7]&#xff0c;k 3 输出&#xff1a;[5,6,7,1,2,3,4] 解释&#xff1a; 向右轮转 1步&#xff1a;[7,1,2,3,4,5,6] 向右…

C++模版类

文章目录 class与typename区别template<class NameType, class AgeType>template<typename T>class vs typename 基本代码实现 class与typename区别 在 C 中&#xff0c;template<class NameType, class AgeType> 和 template<typename T> 都是模板声…

Downie 4 中文

Downie 4是一款备受推崇的视频下载工具&#xff0c;以其高效、便捷的特点在广大用户中获得了良好的口碑。这款工具不仅支持从各种不同的网站上下载视频&#xff0c;还具备了智能化、多线程下载的优势&#xff0c;让用户能够快速地获取所需的视频资源。 首先&#xff0c;Downie…

非阻塞 IO(NIO)

文章目录 非阻塞 IO(NIO)模型驱动程序应用程序模块使用 非阻塞 IO(NIO) 上一节中 https://blog.csdn.net/tyustli/article/details/135140523&#xff0c;使用等待队列头实现了阻塞 IO 程序使用时&#xff0c;阻塞 IO 和非阻塞 IO 的区别在于文件打开的时候是否使用了 O_NONB…

单词接龙[中等]

一、题目 字典wordList中从单词beginWord和endWord的 转换序列 是一个按下述规格形成的序列beginWord -> s1 -> s2 -> ... -> sk&#xff1a; 1、每一对相邻的单词只差一个字母。 2、对于1 < i < k时&#xff0c;每个si都在wordList中。注意&#xff0c;beg…

Android模拟器的安装和adb连接

一、前置说明 APP 自动化可以使用真机进行测试&#xff0c;也可以使用模拟器来模拟安卓设备。我们可以根据个人喜好安装模拟器&#xff0c;个人推荐安装两款模拟器&#xff1a;网易 MuMu 模拟器、夜神模拟器。 MuMu模拟器可以支持 Android 12 版本&#xff0c;优点是&#xf…

微信小程序开发学习(上强度):从0开始写项目

前置知识 1、配置插件 微信小程序 基础模板引入sass的两种方法_微信小程序使用sass-CSDN博客 之后在对应页面里新建一个scss文件&#xff0c;写css 2、注册小程序&#xff0c;有个自己的appid&#xff0c;不用测试号了 5.1.注册小程序账号获取appid及个人和企业版差异_哔哩…

Vue3中使用props和emits详解

前言 在Vue3中&#xff0c;父子组件之间的数据传递是一个常见的需求。本文将介绍如何在Vue3中传递对象&#xff0c;并且在子组件中访问和修改父组件对象中的属性值&#xff0c;以及子组件如何调用父组件中的方法。 在 Vue 3 中&#xff0c;父子组件之间传值有以下作用&#xf…

实用干货:公司规定所有接口都用 POST请求,为什么?

大家好&#xff0c;我是大澈&#xff01; 本文约1000字&#xff0c;整篇阅读大约需要2分钟。 感谢关注微信公众号&#xff1a;“程序员大澈”&#xff0c;免费领取"面试礼包"一份&#xff0c;然后免费加入问答群&#xff0c;从此让解决问题的你不再孤单&#xff01…