【数据结构】树——链式存储二叉树的基础

news2024/11/26 17:02:38

写在前面

书接上文:【数据结构】树——顺序存储二叉树

本篇笔记主要讲解链式存储二叉树的主要思想、如何访问每个结点、结点之间的关联、如何递归查找每个结点,为后续更高级的树形结构打下基础。不了解树的小伙伴可以查看上文


文章目录

  • 写在前面
  • 一、链式二叉树的定义
  • 二、链式二叉树的遍历
    • 2.1、前序、中序以及后序遍历
        • 前序遍历的代码实现
        • 中序遍历的代码实现
        • 后序遍历的代码实现
  • 三、链式二叉树的元素个数
        • 代码实现
  • 四、链式二叉树的高度
        • 代码实现
  • 五、二叉树第k层节点个数
        • 代码实现
  • 六、二叉树查找值为x的节点
        • 代码实现
  • 七、链式存储二叉树的OJ
    • 7.1、单值二叉树
        • 代码实现
  • 八、二叉树的创建和销毁
        • 代码实现


一、链式二叉树的定义

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成数据域和左右指针域左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。(二叉树有二叉链和三叉链两种表示方式,本文主要讲解二叉链二叉链一般指孩子表示法,三叉连指孩子双亲表示法,这两种方式是二叉树最常见的表示方式,虽然还有孩子兄弟表示法,该中表示方式本质也是二叉链)

在这里插入图片描述在这里插入图片描述
链式存储的结构体:

// 二叉链
struct BinaryTreeNode
{
	struct BinTreeNode* _pLeft;// 指向当前节点左孩子
	struct BinTreeNode* _pRight; // 指向当前节点右孩子
	BTDataType _data; // 当前节点值域
};
// 三叉链
struct BinaryTreeNode
{
	struct BinTreeNode* _pParent; // 指向当前节点的双亲
	struct BinTreeNode* _pLeft;
	// 指向当前节点左孩子
	struct BinTreeNode* _pRight; // 指向当前节点右孩子
	BTDataType _data; // 当前节点值域
};
  • 上面的结构体就成功创建出链式存储的二叉树

二、链式二叉树的遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。( 不才默认左子树先运行后再运行右子树,再每次遍历先都需要规定一下左右子树的先后顺序。)
在这里插入图片描述
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

2.1、前序、中序以及后序遍历

前序、中序以及后序遍历的思想都是相同的,只是根节点访问的先后顺序不同,我们这里以前序遍历为例子

1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树中(间)
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树

  • 程序由上到下运行,应当先访问当前值,后访问左右结点。
    在这里插入图片描述

输出下图中的二叉树使用前序遍历的结果(结果需要包含NULL)

在这里插入图片描述

  • 结果为:1,2,3,NULL,NULL,NULL,4,5,NULL,NULL,6,NULL,NULL

我们画图分析,使用微元法每一个结点都逻辑细分一棵小树(根结点与左右结点),在开始根节点1时,对应的左结点2和右结点4
在这里插入图片描述

  • 之后先输出根结点的值1
  • 后访问左结点

使用微元法2结点微元成一棵小树
在这里插入图片描述

  • 之后先输出根结点的值2,此时结果为:1,2
  • 后访问左结点

使用微元法循环上述操作
在这里插入图片描述

  • 之后先输出根结点的值3,此时结果为:1,2,3
  • 后访问左结点

此时3的左节点使用微元法循环上述操作后可得下图
在这里插入图片描述

  • 之后先输出根结点的值NULL,此时结果为:1,2,3,NULL
  • 之后因为结点为NULL,所以结束访问,返回到结点3

此时结点3中,已经完成完成了左结点的范围,写在就需要范围右结点了
在这里插入图片描述
我们依旧使用微元法细分结点3的右结点,可得下图
在这里插入图片描述

  • 之后先输出根结点的值NULL,此时结果为:1,2,3,NULL,NULL
  • 之后因为结点为NULL,所以结束访问,返回到结点3

此时结点3的所以结点已经完成遍历,返回到结点2,结点2已经完成当前结点与左结点值的遍历,接下来进入右结点值遍历中
在这里插入图片描述
使用微元法循环上述操作,直到整棵树的值遍历一便即可得到结果:1,2,3,NULL,NULL,NULL,4,5,NULL,NULL,6,NULL,NULL

上面我们手搓的思想与递归简直一毛一样,那么我们在代码实现中,就可以使用递归来解决前序遍历。

根据我们手搓的画图,最终我们可以得出下图:

在这里插入图片描述

前序遍历的代码实现

递归的思想就是大事化小,对于递归方面有问题的小伙伴可以看不才写的递归笔记:【C语言】函数递归

代码实现:

void PreOrder(BTNode* root) {
	if (root == NULL) {
		printf("NULL ");
		return;
	}
	printf("%d ", root->val);
	PreOrder(root->left);
	PreOrder(root->right);
}

根据上面手搓推理的逻辑,继续使用微元法来创建递归实现前序遍历
在这里插入图片描述
在上图这颗微元树中

  • 首先需要判断这个树是否是空树,判断是否空树就是判断这个结点是否为NULL,如果结点是NULL就打印NULL并且返回。
  • 判断这个结点不为空后,因为是前序遍历,这时候就先打印根结点的值root->val
  • 打印完成后就访问左孩子结点,之后再访问右孩子结点,完成。
  • 在判断是否空树的条件中,恰好判断了结点为空的情况,这样也完成了我们访问左孩子或右孩子遇到结点为NULL需要返回的情况。

内存中的栈增情况:
在这里插入图片描述

中序遍历的代码实现

前序遍历的实现逻辑一模一样,只是执行根结点值打印时机不同

void InOrder(BTNode* root) {
	if (root == NULL) {
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->val);
	InOrder(root->right);
}
后序遍历的代码实现

前序遍历的实现逻辑一模一样,只是执行根结点值打印时机不同

void PostOrder(BTNode* root) {
	if (root == NULL) {
		printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->val);
}

三、链式二叉树的元素个数

求链式二叉树的元素个数,我们还是先使用微元法来手搓计算。
在这里插入图片描述

计算下面二叉树有多少个元素

在这里插入图片描述
我们使用微元法先拆分一颗小树来分析如何计算在这里插入图片描述
首先根节点不为NULL说明该根结点是有效结点,但是在这个结点中不能得到左孩子与右孩子右多少个元素个数。让左右孩子返回它有多少个元素结点。这样在这个小树中,我们才能返回这棵树有多少个结点
所以又需要进入左右孩子中,去判断它是否是有多少个结点(如下图)
在这里插入图片描述
此时,我们还是不知道左右孩子各有多少个结点,我们再进入到左右孩子中(如下图)。
在这里插入图片描述
此时,我们还是不知道左右孩子各有多少个结点,我们再进入到左右孩子中。此时3的左节点使用微元法循环上述操作后可得下图
在这里插入图片描述
这时结点为NULL,说明该结点不是有效结点,返回0
在这里插入图片描述
此时3结点就得到了左孩子结点的值为0,这时就需要进入右孩子中,让右孩子返回元素个数

同理,右孩子还是空,返回0,此时3结点得知左右孩子节点个数。在这里插入图片描述

3节点左孩子返回结果 + 右孩子返回结果 再加上自身是有效结点+1就得到该结点的元素个数,左右元素个数为03节点是有效节点,所以返回1
在这里插入图片描述
节点2中就得到了左节点的元素个数,此时在按照上面的逻辑得到右节点个数为0(如下图)
在这里插入图片描述2节点中左元素个数为1,右元素个数为02节点是有效节点,所以返回2(如下图)在这里插入图片描述

根据上面的逻辑,我们可以轻松算出1结点右子树元素个树为3
在这里插入图片描述

此时1结点中左孩子返回结果为2 右孩子返回结果为3 再加上自身是有效结点+1就得到该结点的元素个数为:6

虽然也是遍历求解,但是我们这里是让每个底层结点自己统计完成后,逐步向上层返回结点个数

代码实现
int BinaryTreeSize(BTNode* root) {
	if (root == NULL) {
		return 0;
	}
	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
  • 如果是空结点就返回0
  • 使用微元法左右结点自己返回有多少结点,再加上自身结点就是该小树的元素个数

四、链式二叉树的高度

求链式二叉树的高度,还是使用微元法,逻辑与求个数相似。
在这里插入图片描述

手搓下面二叉树的高度

在这里插入图片描述
我们使用微元法先拆分一颗小树来分析如何计算在这里插入图片描述
在上图小树中,我们知道根节点不是空结点,这时候需要判断我们需要判断左右结点哪个孩子的高度高,之后我们在最高的子结点高度加上根结点的高度(+1)即可得到这个小树的高度,我们直接微元到原二叉树左子树的最底层中(如下图)在这里插入图片描述
在上图中,左右孩子的高度是0,但次结点时有效结点,所以返回结果是:0+1。(最高的子结点高度加上根结点),

此时上层结点2就接收到左结点的高度是1,易得右结点的高度是0在这里插入图片描述
之后最高的子结点高度(1)加上根结点的高度(1)返回2(如下图)
在这里插入图片描述
根据上述逻辑,我们易的。1结点的右孩子返回高度是2(如下图)
在这里插入图片描述
这时两个孩子的高度都为2,所以该二叉树的高度为:2 + 1 = 3

代码实现
int TreeHeight(BTNode* root) {
	if (root == NULL) {
		return 0;
	}

	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

  • 在递归中,返回的数据需要进行处理的返回值都必须进行保存,这样可以避免大量重复的递归。
  • 每一次都需要保存左右结点的高度,这样就不需要多次重复递归即可判断出左孩子与右孩子哪个高度高

五、二叉树第k层节点个数

同样使用微元法求出第K层中的节点个数。
在这里插入图片描述

手搓下面二叉树中第3层的高度

在这里插入图片描述
首先,我们需要有一个变量i来记录我们当前所在的层次是否在所对于的K层中,我们使用微元法先拆分一颗小树来分析如何计算在这里插入图片描述
每进入一层,i变量就--,直到i变量为1时,说明当前的层次是正确的。但此时i == 3,这时候就需要进入根节点的左右孩子中,让左右孩子告诉根节点i==1是有多少个节点
在这里插入图片描述
首先进入到左孩子中,此时i==2,还是没进入到对于的节点,再次进入左右孩子中,此时发现i==1,这时候左孩子3是有效节点,返回1右孩子NULL不是有效节点。在这里插入图片描述
这时候节点2就需要把左右孩子返回的数据相加后返回
在这里插入图片描述
同理可得出右子树返回结果为:2
在这里插入图片描述
即的出第3层的元素个数为:3

代码实现
int TreeKLeve(BTNode* root,int k) {
	if (root == NULL) {//节点为空时,返回0
		return 0;
	}

	if (k == 1) { //节点不为空且层次为1时,返回1
		return 1;
	}
	
	int lnum = TreeKLeve(root->left, k - 1);//记录左孩子在K层时有多少个节点
	int rnum = TreeKLeve(root->right, k - 1);//记录右孩子在K层时有多少个节点

	return lnum + rnum;//返回左孩子在K层中节点 + 右孩子在K层中节点。即为当前根下K层中所有节点个数
} 
  • 在递归中,返回的数据需要进行处理的返回值都必须进行保存,这样可以避免大量重复的递归。

六、二叉树查找值为x的节点

相对简单,但是代码逻辑中会有坑
在这里插入图片描述

在下面二叉树中,找到值为5的节点,并且返回该节点

在这里插入图片描述
只需要遍历找到值为5的节点,之后返回即可,逻辑与求元素个数大差不差。

代码实现
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
	if (root == NULL) {
		return NULL;
	}
	if (root->val == x) {
		return root;
	}
	BTNode* lren = BinaryTreeFind(root->left,x);
	if (lren)
		return lren;
	BTNode* rren = BinaryTreeFind(root->right,x);
	if (rren)
		return rren;

	return NULL;
}
  • 代码实现逻辑不断的遍历往下走,直到为空返回空,或者找到对应的值返回该节点。
  • 如果既不为空也不是相对应的值,则接着往左右孩子中寻找X值。
  • 如果左右孩子中都没有找到所对应的X值,则返回空值。
  • 若找到了所对应的X值。则返回对应的节点。因为递归是函数的栈增return不能直接返回到函数调用的阶段需要一步步返回
  • 为了确保每次返回都是所对应的节点,我们需要进行判断每次返回的节点是否为空。如果不为空,则返回所找到的节点。
  • 一旦涉及到需要对递归值进行判断的,都需要进行储存。

如果不进行判断返回,则在下一次函数递归返回中就直接返回空。(错误案例)

BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
	if (root == NULL) {
		return NULL;
	}
	if (root->val == x) {
		return root;
	}
	return NULL;
}

七、链式存储二叉树的OJ

7.1、单值二叉树

如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树

只有给定的树是单值二叉树时,才返回 true;否则返回 false
在这里插入图片描述

输入:[1,1,1,1,1,null,1]
输出:true

本题不可使用遍历。这里我们使用微元法判断一下我们这么确认每个节点都是相同的

在这里插入图片描述
使用根节点val左右孩子的val值分别比较,即可判断出这个节点是否与左右孩子的值相同。
但是我们也不知道左右孩子的全部节点是否相同,但是如果此时不满住条件我们直接可以判断为假,所以我们先对该节点的左右孩子进行判断
在这里插入图片描述
左节点判断结果不为假,继续判断右结点
在这里插入图片描述
此时右结点也不为假,所以我们需要递归往下判断,左右孩子的节点是否为假。如此递归,当遇到节点为NULL时,返回true,则说明到叶子节点都不为假,一旦为假,直接返回。

代码实现
bool isUnivalTree(struct TreeNode* root) {
    if (root == NULL) {  // 基本情况:如果当前节点为空,返回 true(空树被认为是单值树)
        return true;
    }
    // 检查左子节点是否存在且值不同
    if (root->left != NULL && root->val != root->left->val) {
        return false;
    }
    // 检查右子节点是否存在且值不同
    if (root->right != NULL && root->val != root->right->val) {
        return false;
    }
    // 递归检查左子树和右子树
    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

相似的逻辑,我们试试下面的题目:

  • 100. 相同的树
  • 101. 对称二叉树
  • 572. 另一棵树的子树

八、二叉树的创建和销毁

通过前序遍历的数组:1,2,3,NULL,NULL,NULL,4,5,NULL,NULL,6,NULL,NULL。构建二叉树

首先,我们需要把前序遍历的数组还原为二叉树,还原过程:
在这里插入图片描述
由上图可知,前序遍历的数组的还原过程,在数组下标+1我们就进行左孩子的赋值,当左孩子赋值NULL后,程序就返回节点,之后进行右结点的赋值,当右节点也赋值完成后,返回节点地址。

代码实现
typedef char BTDataType;
typedef struct BinaryTreeNode
{
	struct BinTreeNode* left;// 指向当前节点左孩子

	struct BinTreeNode* right; // 指向当前节点右孩子

	BTDataType val; // 当前节点值域

}BTNode;
//前序递归创建二叉树
BTNode* CreateTree(char* arr, int* pi) {
	if (arr[*pi] == '#') {
		(*pi)++;
		return NULL;
	}
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	node->val = arr[(*pi)++];
	node->left = CreateTree(arr, pi);
	node->right = CreateTree(arr, pi);
	return node;
}

void test2() {
	char arr[] = { '1','2','3','#','#','#','4','5','#','#','6','#','#' };//'#'代表NULL
	int i = 0;

	BTNode* node = CreateTree(arr, &i);
}

我们画部分的递归展开图以便理解:
在这里插入图片描述


以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 如果对大家有帮助的话,就请多多为我点赞收藏吧~~~💖💖
请添加图片描述

ps:表情包来自网络,侵删🌹

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

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

相关文章

泷羽sec-linux

基础之linux 声明! 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团…

重新定义社媒引流:AI社媒引流王如何为品牌赋能?

在社交媒体高度竞争的时代,引流已经不再是单纯追求流量的数字游戏,而是要找到“对的用户”,并与他们建立真实的连接。AI社媒引流王通过技术创新和智能策略,重新定义了社媒引流的方式,帮助品牌在精准触达和高效互动中脱…

centos 服务器 docker 使用代理

宿主机使用代理 在宿主机的全局配置文件中添加代理信息 vim /etc/profile export http_proxyhttp://127.0.0.1:7897 export https_proxyhttp://127.0.0.1:7897 export no_proxy"localhost,127.0.0.1,::1,172.171.0.0" docker 命令使用代理 例如我想在使用使用 do…

WebRTC音视频同步原理与实现详解(上)

第一章、RTP时间戳与NTP时间戳 1.1 RTP时间戳 时间戳,用来定义媒体负载数据的采样时刻,从单调线性递增的时钟中获取,时钟的精度由 RTP 负载数据的采样频率决定。 音频和视频的采样频率是不一样的,一般音频的采样频率有 8KHz、…

Matlab 深度学习工具箱 案例学习与测试————求二阶微分方程

clc clear% 定义输入变量 x linspace(0,2,10000);% 定义网络的层参数 inputSize 1; layers [featureInputLayer(inputSize,Normalization"none")fullyConnectedLayer(10)sigmoidLayerfullyConnectedLayer(1)sigmoidLayer]; % 创建网络 net dlnetwork(layers);% 训…

互联网直播/点播EasyDSS视频推拉流平台视频点播有哪些技术特点?

在数字化时代,视频点播应用已经成为我们生活中不可或缺的一部分。监控技术与视频点播的结合正悄然改变着我们获取和享受媒体内容的方式。这一变革不仅体现在技术层面的进步,更深刻地影响了我们。 EasyDSS视频直播点播平台是一款高性能流媒体服务软件。E…

神经网络(系统性学习二):单层神经网络(感知机)

此前篇章: 神经网络中常用的激活函数 神经网络(系统性学习一):入门篇 单层神经网络(又叫感知机) 单层网络是最简单的全连接神经网络,它仅有输入层和输出层,没有隐藏层。即&#x…

构建 Java Web 应用程序:从 Servlet 到数据库交互(Eclipse使用JDBC连接Mysql数据库)

第 1 部分:环境设置 安装 Java Development Kit (JDK):下载并安装 JDK。设置 IDE:安装并配置 IDE(如 IntelliJ IDEA 或 Eclipse)。安装数据库:下载并安装 MySQL 数据库。配置数据库:创建数据库…

进程间通信5:信号

引入 我们之前学习了信号量,信号量和信号可不是一个东西,不能混淆。 信号是什么以及一些基础概念 信号是一种让进程给其他进程发送异步消息的方式 信号是随时产生的,无法预测信号可以临时保存下来,之后再处理信号是异步发送的…

jQuery-Word-Export 使用记录及完整修正文件下载 jquery.wordexport.js

参考资料: jQuery-Word-Export导出word_jquery.wordexport.js下载-CSDN博客 近期又需要自己做个 Html2Doc 的解决方案,因为客户又不想要 Html2pdf 的下载了,当初还给我费尽心思解决Html转pdf时中文输出的问题(html转pdf文件下载之…

docker镜像、容器、仓库介绍

docker docker介绍docker镜像命令docker容器命令docker仓库 docker介绍 官网 Docker 是一种开源的容器化平台,用于开发、部署和运行应用。它通过将应用程序及其依赖项打包到称为“容器”的单一包中,使得应用能够在任何环境下运行,不受底层系…

51单片机-独立按键与数码管联动

独立键盘和矩阵键盘检测原理及实现 键盘的分类:编码键盘和非编码键盘 键盘上闭合键的识别由专用的硬件编码器实现,并产生键编码号或键值的称为编码键盘,如:计算机键盘。靠软件编程识别的称为非编码键盘;在单片机组成…

嵌入式驱动开发详解3(pinctrl和gpio子系统)

文章目录 前言pinctrl子系统pin引脚配置pinctrl驱动详解 gpio子系统gpio属性配置gpio子系统驱动gpio子系统API函数与gpio子系统相关的of函数 pinctrl和gpio子系统的使用设备树配置驱动层部分用户层部分 前言 如果不用pinctrl和gpio子系统的话,我们开发驱动时需要先…

STM32C011开发(1)----开发板测试

STM32C011开发----1.开发板测试 概述硬件准备视频教学样品申请源码下载参考程序生成STM32CUBEMX串口配置LED配置堆栈设置串口重定向主循环演示 概述 STM32C011F4P6-TSSOP20 评估套件可以使用户能够无缝评估 STM32C0 系列TSSOP20 封装的微控制器功能,基于 ARM Corte…

【漏洞复现】|百易云资产管理运营系统/mobilefront/c/2.php前台文件上传

漏洞描述 湖南众合百易信息技术有限公司(简称:百易云)成立于2017年是一家专注于不动产领域数字化研发及服务的国家高新技术企业,公司拥有不动产领域的数字化全面解决方案、覆盖住宅、写字楼、商业中心、专业市场、产业园区、公建、…

Spring Boot 实战:基于 Validation 注解实现分层数据校验与校验异常拦截器统一返回处理

1. 概述 本文介绍了在spring boot框架下,使用validation数据校验注解,针对不同请求链接的前端传参数据,进行分层视图对象的校验,并通过配置全局异常处理器捕获传参校验失败异常,自动返回校验出错的异常数据。 2. 依赖…

量子神经网络

感知机只是一个神经元,若有多个神经元共同作用,则构成神经网络。目前,最常见的量子神经网络模型为基于参数化量子线路的量子神经网络,该模型用参数化量子线路代替神经网络结构,使用经典优化算法更新参数化量子线路的参…

非交换几何与黎曼ζ函数:数学中的一场革命性对话

非交换几何与黎曼ζ函数:数学中的一场革命性对话 非交换几何(Noncommutative Geometry, NCG)是数学的一个分支领域,它将经典的几何概念扩展到非交换代数的框架中。非交换代数是一种结合代数,其中乘积不是交换性的&…

VUE3项目 关于金额:分转化为元 ;元转化为分;

1.在components 文件夹下新建moneyHandle.ts 文件 2.ts文件中写如下代码(保留两位小数) //分转化为元 - 正则解决精度 export const regFenToYuan (fen:any) >{var num fen;numfen*0.01;num;var reg num.indexOf(.) >-1 ? /(\d{1,3})(?(?:…

从0开始学习Linux——搭建自己的专属Linux系统

温馨提示本篇文章字数超过5000字! 往期目录: 1、从0开始学习Linux——Linux简介&安装https://blog.csdn.net/diamond_5446/article/details/141190487 上一个章节我们简单了解了Linux,并且安装好了虚拟机以及下载好了Centos镜像&#…