数据结构【二叉树】

前言

我们在前面学习了使用数组来实现二叉树,但是数组实现二叉树仅适用于完全二叉树(非完全二叉树会有空间浪费),所以我们本章讲解的是链式二叉树,但由于学习二叉树的操作需要有一颗树,才能学习相关的基本操作,由于这只是开头,为了降低学习的成本,所以我们手动的来创建一颗普通的二叉树,等到本文的后面,再讲解真正的创建

二叉树的基本结构
typedef int BTDataType;
typedef struct BinaryTreeNode
{BTDataType _data;struct BinaryTreeNode* _left;struct BinaryTreeNode* _right;
}BTNode;创建新结点
BTNode*BuyNode(BTDataType x)
{BTNode* Node = (BTNode*)malloc(sizeof(BTNode));if (Node == NULL){perror("malloc fail:");exit(1);}Node->_data = x;Node->_left = Node->_right = NULL;
}创造树
BTNode* CreateBinaryTree()
{BTNode* node1 = BuyNode(1);BTNode* node2 = BuyNode(2);BTNode* node3 = BuyNode(3);BTNode* node4 = BuyNode(4);BTNode* node5 = BuyNode(5);BTNode* node6 = BuyNode(6);BTNode* node7 = BuyNode(7);BTNode* node8 = BuyNode(8);node1->_left = node2;node2->_left = node3;node3->_right = node4;node1->_right = node5;node5->_right = node6;node6->_left = node7;node6->_right = node8;return node1;
}int main()
{BTNode* root = CreateBinaryTree();return 0;
}

最后效果如图
在这里插入图片描述

在完成二叉树的基本操作之前,我们先在这里简单的回顾一下二叉树的基本概念。
二叉树只有两个状态

  1. 空树
  2. 非空:由根结点,根节点的左子树,根结点的右子树组成在这里插入图片描述

从图中可以看出,二叉树定义是递归形式的(根结点的左孩子也能看作根,其左右孩子以及对于的联系也能看成左右子树,根的右孩子同理),所以我们下面的操作都是通过递归来实现。

以下所有的操作都会使用上面手搓的树

二叉树的遍历

所谓前中后序的遍历就是根结点的先后访问顺序,所以前中后序遍历也叫前根、中根、后根遍历。

  1. 前序(前根)的访问顺序:根、左子树、右子树
  2. 中序(中根)的访问顺序:左子树、根、右子树
  3. 后序(后根)的访问顺序:左子树、右子树、根

这里先将遍历的原因是后续的操作,都会用到遍历的思路。
要被遍历的树

前序遍历

一般说这个树的前序遍历是[1, 2, 3, 4, 5, 6, 7, 8]
但这不是最详细的表达,最详细的表达是[1, 2, 3, NULL, 4, NULL, NULL, NULL, 5, NULL, 6, 7, NULL, NULL, 8, NULL, NULL]

3 后面的NULL其实是 3 的左孩子,4 后面俩个NULL代表的是 4 的左孩子和右孩子,而 5 前面的NULL代表的是 2 的右孩子,5 后面的NULL代表 5 的左孩子,7 和 8 后面的NULL都是代表他们的左右孩子。

中序

一般说这个树的中序遍历是[3, 4, 2, 1, 5, 7, 6, 8 ];
实际则是[N, 3, N, 4, N, 2, N, 1, N, 5, N, 7, N, 6, N, 8, N](N代替NULL)
由于是先访问左子树,所以第一个真正被遍历的一定是NULL

3 前面的N就是 3 的左孩子,4 前后的 N则代表的是 4 的左右孩子,2 后面的N代表的是 2 的右孩子;5 前面的N代表 5 的左孩子,7 和 8 前后的N都代表他们的左右孩子。

后序

一般:[4, 3, 2, 7, 8, 6, 5, 1]
实际则是[N, N, N, 4, 3, N, 2, N, N, N, 7, N, N, 8, 6, 5, 1](N代替NULL)

第一个N是 3 的左孩子,第二第三个N是 4 的左右孩子,3 后面的N是 2 的右孩子;而 2 后面的第一个N是 5 的左孩子,7 和 8 的前俩个N都是代表他们的左右孩子

注意:无论是哪种遍历,孩子之间的顺序一定是先左孩子,再是右孩子。

层序遍历

就是我们正常的思维,一层一层、从左到右的依次遍历,这种遍历方式叫广度优先遍历(BFS),而前三种遍历方式叫深度优先遍历(DFS)。

层序遍历需要依靠队列来实现。

代码实现

前中后序的遍历的大体相同,只是打印的位置不同

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}前序时printf的位置在前面printf("%d ", root->_data);BinaryTreePrevOrder(root->_left);中序时printf的位置在中间printf("%d ", root->_data);BinaryTreePrevOrder(root->_right);后续时printf的位置在末尾printf("%d ", root->_data)}

用图像讲解递归过程

在这里插入图片描述

右子树的递归过程大体相同,注意实际情况并不会开那么多的空间,而是当你使用完返回再使用的时候,是将原来的空间给重新利用了。

层序遍历的实现

在完成层序遍历之前,我们需要有队列这个数据结构(我们可以直接将以前的代码拿过来:具体代码在数据结构【队列】)

具体思路是,先创建一个队列,将二叉树的根结点存放到队列里,每遍历一个结点就删掉这个在队列里的结点,删掉的同时,将该结点的左右孩子存放到队列内部这样依次往复。

这里的类型是结点的类型,并且存放的是指针,所以要带个*typedef struct BinaryTreeNode* QUEUEDATA;typedef struct QNode
{QUEUEDATA _val;struct QNode* _next;
}QNode;typedef struct Queue
{QNode* phead;QNode* ptail;int size;
}Queue;

层序遍历代码

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{Queue* q = (Queue*)malloc(sizeof(Queue));QueueInit(q);//先把root入到队列QueuePush(q, root);//当队列尾空时,就代表以及打印完了while (!QueueEmpty(q)){//取队头数据BTNode*tmp = QueueFirst(q);//然后删除数据,我只是操作队列内部,并没有动原来的二叉树QueuePop(q);//当为空时不加数据,这就能应对根结点是空时的情况,就不需要在外面再做一次判断if (tmp == NULL){printf("N ");}//非空,将左右孩子添加到队列else{printf("%d ", tmp->_data);QueuePush(q,tmp->_left);QueuePush(q,tmp->_right);}}
}

二叉树的计算

本文计算关于树的计算有四个

  1. 计算树结点的个数
  2. 计算树的叶子结点个数
  3. 计算第k层的节点个数
  4. 计算树的高度

计算节点个数

这就很简单了,就是左右子树加自己,但每个孩子又可以分为根,左子树,右子树,当根等于空时返回0就可以了。

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{if (root == NULL){return 0;}return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}

这里的+1就是加自己,当你来到下面那个return时,就代表该节点并不是空节点。

计算叶子节点个数

简单的回顾一下:叶子节点就是左右孩子都为空的节点。
所以就可以判断当左右孩子都为空时,就返回 1。

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}if (root->_left == NULL && root->_right == NULL){return 1;}return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}

计算第k层节点个数

这个也能很好的用递归来解决,第k层是对于根节点来说的,但对于根节点的下一层来说,第k层其实是第k-1层,所以可以一直减下去,直到当k==1时,return 1

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{if (root == NULL){return 0;}if (k == 1){return 1;}return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}

计算树的高度

那我就比较左子树和右子树的高度,比较出结果后再加自身的高度(比较出的高度+1)
结束递归的条件就是当我的子树为空,返回0。

// 二叉树的高度
int BinaryHeight(BTNode* root)
{if (root == NULL){return 0;}return 	BinaryHight(root->_left) > BinaryHight(root->_right) ? BinaryHight(root->_left) + 1: BinaryHight(root->_right) + 1;
}

这样也可以,但是如果用这个去做利扣的题是无法通过的,并不是因为程序结果错误,而是因为栈溢出。
看看为什么会栈溢出:我要比较出两个子树的长度,就一定会运行
return BinaryHight(root->_left) > BinaryHight(root->_right) ? BinaryHight(root->_left) + 1: BinaryHight(root->_right) + 1; 这有没有发现,我并没有记录比较高的值,我辛辛苦苦递归很多次才得到的左右子树中较高的子树,当我要返回高度的时候,诶?我前面的数是啥?所以我就又要进行 BinaryHight(root->_left) + 1或者BinaryHight(root->_right) + 1,这样我又会进行递归,再递归比较,然后再递归返回值->递归比较这样一直下去,直到最低层(root == NULL)。

所以,我们需要变量来记录两颗子树的高度,这样我们再比较的时候就不会重复递归了。


// 二叉树的高度
int BinaryHeight(BTNode* root)
{if (root == NULL){return 0;}//记录左树的高度int L = BinaryHight(root->_left);//记录右树的高度int R = BinaryHight(root->_right);//比较出较高的,加上自己这一层的高度return (L > R ? L : R) + 1;
}

二叉树的创建和销毁

二叉树的创建(前序)

这题是使用前序来创建二叉树

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);

'#'号代表空,a是数组,n是长度,pi是下标
先创建父亲节点,然后左子树 -> 创建左子树当中的父亲节点,然后创建左子树 —>直到这时的数据是'#'返回NULL,创建右子树,右子树创建完,函数自然结束,回到上一层让上一层来创建右子树。
当pi等于n的时候,就代表已经遍历完该数组了,这条数组里的数据已经被我创建成一个二叉树了;这时候就返回NULL;这个判断放在函数的开头。

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
//当下标与长度相等,返回NULLif (*pi == n){return NULL;}if (a[*pi] == '#'){(*pi)++;return NULL;}BTNode* root = (BTNode*)malloc(sizeof(BTNode));//先创建根节点,再创建左子树,再创建右子树root->_data = a[(*pi)++];root->_left = BinaryTreeCreate(a, n, pi);//先创建左子树root->_right = BinaryTreeCreate(a, n, pi);//再创建右子树return root;//返回根节点
}

二叉树的销毁(后序)

这题我们采用后序来删除,是因为后续并不需要记录节点,是从底层一点一点销毁节点,当我左右子树的节点都销毁了(或者都为NULL),才销毁我的根节点。

// 二叉树销毁
void BinaryTreeDestory(BTNode** root)

既然是销毁,那么就会修改原来的值,所以我们就传二叉树根节点的地址。

// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{if (root == NULL || *root == NULL){return;}BinaryTreeDestory(&(*root)->_left);//先销毁左子树BinaryTreeDestory(&(*root)->_right);//再销毁右子树//当左右节点都被销毁或者都为NULLfree(*root);//最后再销毁根节点
}

结语

到这里二叉树的基本函数就讲完啦。

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言,也可以前往我的主页看更多好文哦(点击此处跳转到主页)。
如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

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

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

相关文章

20240620日志:TAS-MRAM的电阻开放分析

TAS-MRAM的电阻开放缺陷分析 1 MRAM介绍开放电阻的缺陷 1 MRAM介绍 MRAM(Magnetic random access memory),磁随机存储器,利用磁性材料的状态来存储数据。MRAM的存储单元通常由一个磁隧道结( M T J 茅台酒 MTJ^{茅台酒} MTJ茅台酒&#xff0c…

【大模型驯化-Prompt】企业级大模型Prompt调试技巧与batch批量调用方法

【大模型驯化-Prompt】企业级大模型Prompt调试技巧 本次修炼方法请往下查看 🌈 欢迎莅临我的博客个人主页 👈这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合,智慧小天地! 🎇 免费获取相关内容文档关注&#x…

舒适佩戴,享受沉浸式音乐体验,西圣AVA2耳机体验

平时不管是听音乐,还是打电话,戴上一副耳机都可以让我们获得更好的隐私性,并且在公共场所,比如办公室、车厢里,也可以获得属于自己的空间。现在市面上耳机的选择非常多,音质、续航和佩戴的舒适度是我们选择…

接口自动化测试实战:测试用例也能自动生成

🍅 视频学习:文末有免费的配套视频可观看 🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快 作为测试,你可能会对以下场景感到似曾相识:开发改好的 BUG 反复横跳&…

智慧园区数字化能源云平台的多元化应用场景,您知道哪些?

智慧园区数字化能源云平台的多元化应用场景,您知道哪些? 智慧园区数字化能源云平台,作为新一代信息技术与传统能源管理深度融合的典范,正引领着产业园区向智慧化、绿色化转型的浪潮。该平台依托于大数据、云计算及人工智能等前沿…

跨境多账号需知:指纹浏览器需要用独立IP吗?

指纹浏览器也成为反检测浏览器,旨在安全管理多个账户。在跨境多账号中,多个账号容易引发网站怀疑并最终导致大量账户被暂停,使用反检测浏览器的主要目的是通过创建新的浏览器指纹来隐藏用户的真实浏览器指纹。 但浏览器指纹并不是网站关注的唯…

解决Few-shot问题的两大方法:元学习与微调

基于元学习(Meta-Learning)的方法: Few-shot问题或称为Few-shot学习是希望能通过少量的标注数据实现对图像的分类,是元学习(Meta-Learning)的一种。 Few-shot学习,不是为了学习、识别训练集上的数据,泛化…

Java中将文件转换为Base64编码的字节码

在Java中,将文件转换为Base64编码的字节码通常涉及以下步骤: 读取文件内容到字节数组。使用java.util.Base64类对字节数组进行编码。 下面是一个简单的Java示例代码,演示如何实现这个过程: import java.io.File; import java.io…

Ascend C Add算子样例代码详解

核函数定义 核函数(Kernel Function)是Ascend C算子设备侧实现的入口。在核函数中,需要为在一个核上执行的代码规定要进行的数据访问和计算操作,当核函数被调用时,多个核都执行相同的核函数代码,具有相同的…

L55--- 257.二叉树的所有路径(深搜)---Java版

1.题目描述 2.思路 (1)因为是求二叉树的所有路径 (2)然后是带固定格式的 所以我们要把每个节点的整数数值换成字符串数值 (3)首先先考虑根节点,也就是要满足节点不为空 返回递归的形式dfs(根节…

AI通用大模型不及垂直大模型?各有各的好

​​​​​​​AI时代,通用大模型和垂直大模型,两者孰优孰劣,一直众说纷纭。 通用大模型,聚焦基础层,如ChatGPT、百度文心一言,科大讯飞星火大模型等,都归属通用大模型,它们可以解答…

51单片机STC89C52RC——4.1 独立按键(数码管显示按键值)

目录 目录 目的 一,STC单片机模块 二,矩阵按键模块 2.1 针脚定义 ​编辑 2.2 矩阵按键位置 2.3 如何理解按键按下后针脚的高低电平 2.3.1 错误理解1 2.3.2 错误理解2 2.3.3 正确判定按下的是那个按键的逻辑 2.3.4 判定按键按下的依次扫描程…

游戏中插入音效

一、背景音乐 准备:素材音乐 方法: 1、方法1: (1) 将背景音乐 bgAudio 拖放到Hierarchy面板 (2) 选中 bgAudio,勾选开始运行就播放、循环播放。调节音量(volume) 2、方法2: (1) Create Empty&#x…

大语言模型-Transformer

目录 1.概述 2.作用 3.诞生背景 4.历史版本 5.优缺点 5.1.优点 5.2.缺点 6.如何使用 7.应用场景 7.1.十大应用场景 7.2.聊天机器人 8.Python示例 9.总结 1.概述 大语言模型-Transformer是一种基于自注意力机制(self-attention)的深度学习…

算法篇-二叉树

二叉树的遍历 分为前序、中序和后续的遍历&#xff0c;思想就是利用递归。 前序遍历-中左右 代码&#xff1a; public void travelTree(TreeNode node, List<Integer> resulst) {if (node null){return;}// 中resulst.add(node.val);// 左travelTree(node.left, resul…

DN-DETR

可以看到&#xff0c;与 DAB-DETR 相比&#xff0c;最大的差别仍然在 decoder 处&#xff0c;主要是 query 的输入。DN-DETR 认为可以把对 offsets 的学习&#xff0c;看作一种对噪声学习的过程&#xff0c;因此&#xff0c;可以直接在 GT 周围生成一些 noised boxes&#xff0…

【机器学习】transformer框架理论详解和代码实现

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

Tower 使用指南

Tower 使用指南 目录 打开 git 仓库查看分支历史切换分支提交修改推送修改创建标签自动拉取最新代码 打开 git 仓库 File -> Open然后选择项目目录 查看分支历史 切换分支 提交修改 推送修改 创建标签 自动拉取最新代码

【阿里云服务器】【弹性云服务ECS】通过ssh登录远程服务器

一、操作系统 使用Windows11主机上的Ubuntu子系统&#xff0c;如下图所示&#xff1a; 二、云服务器登录方法 需知道&#xff1a;服务器ip地址、登录名和自己设置的登录密码&#xff1a; 上述系统用户名为root&#xff0c;需要在Ubuntu子系统中同样切换至root用户&#xff…

3GPP R18冻结,哪些信息值得关注?

这两天网上到处都是R18冻结的新闻&#xff0c;小枣君也凑个热闹&#xff0c;聊聊这个话题。 首先给小白科普一下&#xff0c;所谓的R18&#xff0c;全称是Release-18。它是通信国际标准组织3GPP&#xff08;第三代合作伙伴计划&#xff09;所推出协议标准的一个版本。 自从1998…