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

写在前面

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

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


文章目录

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


一、链式二叉树的定义

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成数据域和左右指针域左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。

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

// 二叉链
struct BinaryTreeNode
{
	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:表情包来自网络,侵删🌹

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

qt之QFTP对文件夹(含嵌套文件夹和文件)、文件删除下载功能

一、前言 主要功能如下: 1.实现文件夹的下载和删除,网上很多资料都是单独对某个路径的文件操作的,并不能对文件夹操作 2.实现目标机中含中文名称自动转码,有些系统编码方式不同,下载出来的文件会乱码 3.实现ftp功能…

集群聊天服务器(7)数据模块

目录 Mysql数据库代码封装头文件与源文件 Mysql数据库代码封装 业务层代码不要直接写数据库,因为业务层和数据层的代码逻辑也想完全区分开。万一不想存储mysql,想存redis的话,就要改动大量业务代码。解耦合就是改起来很方便。 首先需要安装m…

手机远程控制电脑,让办公更快捷

在数字化办公的浪潮下,远程控制软件已成为连接工作与生活的桥梁。它使得用户能够通过一台设备(主控端)来操作另一台设备(被控端),无论它们是否位于同一局域网内。这种软件广泛应用于远程办公、手机远程控制…

面向FWA市场!移远通信高性能5G-A模组RG650V-NA通过北美两大重要运营商认证

近日,全球领先的物联网整体解决方案供应商移远通信宣布,其旗下符合3GPP R17标准的新一代5G-A模组RG650V-NA成功通过了北美两家重要运营商认证。凭借高速度、大容量、低延迟、高可靠等优势,该模组可满足CPE、家庭/企业网关、移动热点、高清视频…

72项!湖北省2024年度第二批省级科技计划项目拟立项项目公示!

本期精选 SCI&EI ●IEEE 1区TOP 计算机类(含CCF); ●EI快刊:最快1周录用! 知网(CNKI)、谷歌学术期刊 ●7天录用-检索(100%录用),1周上线; 免费稿件评估 免费匹配…

LeetCode 热题 100 回顾

目录 一、哈希部分 1.两数之和 (简单) 2.字母异位词分组 (中等) 3.最长连续序列 (中等) 二、双指针部分 4.移动零 (简单) 5.盛最多水的容器 (中等) 6…

Chrome 浏览器 131 版本开发者工具(DevTools)更新内容

Chrome 浏览器 131 版本开发者工具(DevTools)更新内容 一、使用 Gemini 调试 CSS Chrome DevTools 现在推出了一个新的实验性 AI 辅助面板,可以与 Gemini 聊天并获得帮助来调试 CSS。 在 Elements 面板中,右键点击一个元素并选…

网络编程-002-UDP通信

1.UDP通信的简单介绍 1.1不需要通信握手,无需维持连接,网络带宽需求较小,而实时性要求高 1.2 包大小有限制,不发大于路径MTU的数据包 1.3容易丢包 1.4 可以实现一对多,多对多 2.客户端与服务端=发送端与接收端 代码框架 收数据方一般都是客户端/接收端 3.头文件 #i…

websocket身份验证

websocket身份验证 前言 上一集我们就完成了websocket初始化的任务,那么我们完成这个内容之后就应该完成一个任务,当客户端与服务端连接成功之后,客户端应该主动发起一个身份认证的消息。 身份认证proto 我们看一眼proto文件的内容。 我…

初识C++(1)

C是在C语言的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加了许多有用的库以及编程范式等。 在C语言中&#xff0c;变量、函数和类的名称存在于全局作用域中&#xff0c;因此可能会发生许多冲突。比如&#xff1a; #include<stdio.h> #include<…

Axure9生成的阅览页面如何自动展开左侧页面导航?

问题 Axure9生成的阅览页面&#xff0c;默认情况是自动折叠的&#xff0c;如何自动展开左侧页面导航&#xff1f; 解决 Axure工具&#xff1a;发布 > 预览选项 > 播放器 > 打开页面列表

LeetCode:700. 二叉搜索树中的搜索

目录 题目描述: 代码: 题目描述: 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和一个整数值 val。 你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在&#xff0c;则返回 null 。 示例 1: 输入&#xff1a;root [4,2,7,1,3…

架构图解析:如何构建高效的微服务系统

在当今的数字化浪潮中&#xff0c;构建高效、灵活且可扩展的系统已成为企业的重要目标。微服务架构作为一种先进的软件设计模式&#xff0c;通过将复杂的应用程序分解为一系列小型、独立的服务&#xff0c;显著提升了系统的灵活性、可扩展性和维护性。本文将通过解析微服务系统…

【Android、IOS、Flutter、鸿蒙、ReactNative 】实现 MVP 架构

Android Studio 版本 Android Java MVP 模式 参考 模型层 model public class User {private String email;private String password;public User(String email, String password) {this.email = email;this.password = password;}public String getEmail() {return email;}…

【海思Hi3519DV500】双目网络相机套板硬件规划方案

Hi3519DV500双目网络相机套板是针对该芯片设计的一款 IP 编码板 PCBA&#xff0c;硬件接口支持双目sensor 接入&#xff0c;SDIO3.0 接口、USB2.0、USB3.0、UART 接口以及丰富的 IO 扩展应用&#xff0c;可根据各种使用场景设计相应扩展板&#xff0c;丰富外围接口&#xff0c;…

百度世界2024:智能体引领AI应用新纪元

在近日盛大举行的百度世界2024大会上&#xff0c;百度创始人李彦宏以一场题为“文心一言”的精彩演讲&#xff0c;再次将全球科技界的目光聚焦于人工智能&#xff08;AI&#xff09;的无限可能。作为一名科技自媒体&#xff0c;我深感这场演讲不仅是对百度AI技术实力的一次全面…

SPP:空间金字塔池化

今天水一篇博客&#xff0c;讲讲SPP池化结构&#xff1b;那这是个什么东西呢&#xff1f;它的作用又是什么呢&#xff1f;在了解它之前我们先简单了解一下大部分的神经网络&#xff1b; 引入&#xff1a; 在大部分的神经网络中&#xff0c;都将神经网络分为Backbone主干网络、…

Ubuntu Linux使用前准备动作_使用root登录图形化界面

Ubuntu默认是不允许使用 root 登录图形化界面的。这是出于安全考虑的设置。但如果有需要&#xff0c;可以通过以下步骤来实现使用 root 登录&#xff1a; 1、设置 root 密码 打开终端&#xff0c;使用当前的管理员账户登录系统。在终端中输入命令sudo passwd root&#xff0c…

ubuntu下连接了192.168.1.x和192.168.2.x两个网络段,如何让这个两个网段互相通信?

在 Ubuntu 上连接两个网络段&#xff08;如 个人终端A 192.168.1.10 和 个人终端B 192.168.2.10&#xff09;&#xff0c;需要配置路由和网络转发功能&#xff0c;使这两个网段能够相互通信。以下是实现方法&#xff1a; 步骤 1&#xff1a;确认网络配置 1. 确保 Ubuntu 机器…

React Native Mac 环境搭建

下载 Mac 版Android Studio 下载 安装 JDK 环境 Flutter 项目实战-环境变量配置一 安装 Node.js 方式一 通过Node.js 官网下载 下载完成后点击安装包进行安装 安装完成