深入理解二叉树:遍历、构建与性质探索的代码实现


在这里插入图片描述

📷 江池俊: 个人主页
🔥个人专栏: ✅数据结构冒险记 ✅C语言进阶之路
🌅 有航道的人,再渺小也不会迷途。


在这里插入图片描述

文章目录

    • 前言
    • 一、二叉树的存储结构
    • 二、二叉树链式结构的实现
    • 三、二叉树的前、中、后续遍历(三种遍历)
    • 四、二叉树的层次遍历
    • 五、二叉树节点个数以及高度等
      • 5.1 二叉树节点个数
      • 5.2 二叉树叶子节点个数
      • 5.3 二叉树的高度
      • 5.4 二叉树第k层节点个数
      • 5.5 二叉树查找值为x的节点
    • 六、根据所给数组构建一颗二叉树
    • 七、二叉树的销毁
    • 八、判断二叉树是否是完全二叉树

前言

二叉树的相关概念和结构在上一章节已经有详细介绍,传送门:二叉树:数据结构中的灵魂

一、二叉树的存储结构

BTDataType 表示二叉树的节点存储元素的类型,BTNode 表示二叉树的节点,leftright 分别表示节点的左右孩子节点,data 表示节点存储的元素。

typedef char BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

在这里插入图片描述


二、二叉树链式结构的实现

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;
// 申请节点
BTNode* BuyTreeNode(int x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	assert(node);

	node->data = x;
	node->left = NULL;
	node->right = NULL;

	return node;
}

//建树
BTNode* CreateTree()
{
	BTNode* node1 = BuyTreeNode(1);
	BTNode* node2 = BuyTreeNode(2);
	BTNode* node3 = BuyTreeNode(3);
	BTNode* node4 = BuyTreeNode(4);
	BTNode* node5 = BuyTreeNode(5);
	BTNode* node6 = BuyTreeNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;

	return node1;
}

注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式在本文后重点讲解。

在看二叉树基本操作前,再回顾下二叉树的概念,二叉树是

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

从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。


三、二叉树的前、中、后续遍历(三种遍历)

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

在这里插入图片描述

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历

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

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

下面请看二叉树三种遍历方法的代码实现:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	printf("%d ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL) 
	{
		return;
	}
	BinaryTreePrevOrder(root->left);
	printf("%d ", root->data);
	BinaryTreePrevOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
	printf("%d ", root->data);
}

下面主要分析前序递归遍历,中序与后序图解类似。

前序遍历递归图解:
在这里插入图片描述
在这里插入图片描述


四、二叉树的层次遍历

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

在这里插入图片描述
二叉树的层次遍历通常是通过队列来实现的(这是因为我们需要利用队列先进先出的特性来保存之前被访问过的节点的孩子节点),这是一种广度优先搜索(BFS)的策略。下面是一个基本的实现思路

  1. 首先,我们需要一个队列来保存待处理的节点。一开始,队列中只有根节点
  2. 然后,进入一个循环,条件是队列不为空。在循环中,我们首先取出队列的第一个节点,并访问它(比如打印节点的值)。
  3. 接着,如果这个节点有左孩子,就把左孩子加入队列的末尾。然后,如果这个节点有右孩子,就把右孩子也加入队列的末尾。
  4. 最后,把刚才取出的节点从队列中移除,然后回到步骤2,继续处理队列中的下一个节点。

这个过程会一直持续到队列为空,也就是所有的节点都已经被访问过了。

辅助队列代码:

// 链式结构:表示队列 
typedef struct BinaryTreeNode* QDataType; //元素类型,此处类型是二叉树节点的指针
//队列的节点
typedef struct QListNode
{
	QDataType data;
	struct QListNode* next;
}QNode;
// 队列的结构 
typedef struct Queue
{
	QNode* front; //对头
	QNode* rear; //队尾
	int size; //队列元素个数
}Queue;

// 初始化队列 
void QueueInit(Queue* q)
{
	assert(q);

	q->front = q->rear = NULL;
	q->size = 0;
}
// 队尾入队列 
void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}

	newnode->data = data;
	newnode->next = NULL;

	if (q->front == NULL)
	{
		q->front = q->rear = newnode;
	}
	else
	{
		q->rear->next = newnode;
		q->rear = newnode;
	}
	
	q->size++;//队列元素加一
}
// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	//队列不为空
	assert(q->front);
	QNode* cur = q->front;
	q->front = q->front->next;
	//队列只有一个元素的情况,要考虑队尾的指针,防止野指针
	if (q->front == NULL)
		q->rear = NULL;

	free(cur);

	q->size--;//队列元素减一
}
// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	//队列不为空
	assert(q->front);
	
	return q->front->data;
}
// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);

	return q->size;
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q)
{
	assert(q);

	return q->front == NULL;
}
// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);

	QNode* cur = q->front;
	while (cur)//当cur为空时结束
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}

	q->front = q->rear = NULL;
	q->size = 0;
}

二叉树层次遍历代码:

// 非递归遍历二叉树
// 层序遍历(利用队列“先进先出”-->出去一个节点就立即带入此节点的左右节点进队)
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
	
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%c ", front->data);

		if (front->left)
			QueuePush(&q, front->left);

		if (front->right)
			QueuePush(&q, front->right);
	}

	QueueDestroy(&q);
}
// 一层一层访问节点
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	int levelSize = 1; //控制层数,第一层数据个数为1
	while (!QueueEmpty(&q))
	{
		// 一层一层出数据
		while (levelSize--)
		{
			BTNode* front = QueueFront(&q); 
			QueuePop(&q); 
			printf("%c ", front->data); 
			 
			if (front->left) 
				QueuePush(&q, front->left); 

			if (front->right) 
				QueuePush(&q, front->right); 
		}
		printf("\n");

		levelSize = QueueSize(&q);
	}

	QueueDestroy(&q);
}

五、二叉树节点个数以及高度等

5.1 二叉树节点个数

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

5.2 二叉树叶子节点个数

// 二叉树叶子节点个数
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);
}

5.3 二叉树的高度

// 二叉树的高度
int BinaryTreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	int leftHeight = BinaryTreeHeight(root->left); // 左子树的高度
	int rightHeight = BinaryTreeHeight(root->right); //右子树的高度

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

5.4 二叉树第k层节点个数

// 二叉树第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);
}

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

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return 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;
}

六、根据所给数组构建一颗二叉树

例: 通过前序遍历的数组 “ABD##E#H##CF##G##” 构建二叉树,“#”表示的是空格,空格字符代表空树。

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi) //这里pi的类型为int*是为了递归时确保值的完整性
{
	assert(*pi >= 0 && *pi < n);
	if (a[(*pi)] == '#')
	{
		(*pi)++;
		return NULL;
	}

	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc error");
		return;
	}

	root->data = a[(*pi)++];
	root->left = BinaryTreeCreate(a, n, pi);
	root->right = BinaryTreeCreate(a, n, pi);

	return root;
}

该函数接受三个参数:a 是指向前序遍历数组的指针,n 是数组的长度,pi 是一个指向整数的指针,用于记录当前处理的元素索引。

  1. 函数首先使用断言 assert(*pi >= 0 && *pi < n) 确保索引 *pi 在合法范围内。然后判断当前元素是否为特殊字符 '#',如果是,则表示当前节点为空,将索引 *pi 加一后返回 NULL
  2. 如果当前元素不是特殊字符,则创建一个新的二叉树节点 root,并为其分配内存空间。如果内存分配失败,则打印错误信息并返回。
  3. 接下来,将当前元素的值赋给 root 节点的数据域 root->data,并将索引 *pi 加一。然后递归调用 BinaryTreeCreate 函数来构建 左子树右子树,分别赋值给 root->leftroot->right
  4. 最后,返回根节点 root。

在这里插入图片描述


七、二叉树的销毁

由于在遍历二叉树时,前序和中序都需要保存根节点,而后续遍历不用,故使用后续遍历递归销毁二叉树最简单。

// 二叉树销毁(利用后序遍历销毁,前中序遍历都需要保存根节点)
void BinaryTreeDestory(BTNode** root)
{
	assert(root);
	if (*root == NULL)
		return;

	BinaryTreeDestory(&(*root)->left); 
	BinaryTreeDestory(&(*root)->right); 
	free(*root);
	*root = NULL;
}

八、判断二叉树是否是完全二叉树

思路:利用队列来进行层序遍历,并在遍历过程中检查是否存在未被访问的节点。

  1. 初始化队列:首先,创建一个队列 q,并将根节点 root 加入队列,如果根节点存在的话。
  2. 层序遍历:然后,通过层序遍历的方式访问二叉树的每个节点。遍历过程中,如果遇到 NULL 节点,就跳出循环。
  3. 检查空节点:完成层序遍历后,再次检查队列中的节点。如果队列不为空队首节点不为 NULL,则说明存在非空节点未被访问,即该二叉树不是完全二叉树。函数返回 false
  4. 清理队列:如果上述检查中没有发现非空节点未被访问,则说明该二叉树是完全二叉树。在返回 true 之前,销毁队列以释放内存。
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	int levelSize = 1; //控制层数,第一层数据个数为1
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
			
		if (front == NULL)
			break;

		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q); 
		QueuePop(&q);

		if (front)
		{
			QueueDestroy(&q); 
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

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

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

相关文章

小程序定制开发:解析定制化移动应用的未来

引言 在当今数字化时代&#xff0c;移动应用已经成为人们生活不可或缺的一部分。随着智能手机的普及&#xff0c;移动应用的需求呈现出爆发式增长&#xff0c;企业们也纷纷投身于这场数字化浪潮。然而&#xff0c;众多企业在竞争激烈的市场中&#xff0c;如何突显个性、提高用…

Springboot校验注解

Spring Boot 提供了一组基于 Hibernate Validator 的校验注解&#xff0c;用于验证请求参数、实体对象等数据的合法性。下面是一些常用的 Spring Boot 校验注解及其功能&#xff1a; 导入依赖 <dependency><groupId>org.springframework.boot</groupId><…

【智能家居入门2】(MQTT协议、微信小程序、STM32、ONENET云平台)

此篇智能家居入门与前两篇类似&#xff0c;但是是使用MQTT协议接入ONENET云平台&#xff0c;实现微信小程序与下位机的通信&#xff0c;这里相较于使用http协议的那两篇博客&#xff0c;在主程序中添加了独立看门狗防止程序卡死和服务器掉线问题。后续还有使用MQTT协议连接MQTT…

配置nginx作为静态文件托管服务器

下载nginx windows上是个压缩包 解压后, 使用命令行输入 nginx 进行启动 nginx -s stop 进行停止 nginx -s status 查看状态 可以配置一下环境变量 主要是配置文件, windows的nginx配置文件在 conf文件夹下 在http标签下 添加如下配置 其他地方不用更改,保持原样即可, 以…

isctf---web

圣杯战争 php反序列 ?payloadO:6:"summon":2:{s:5:"Saber";O:8:"artifact":2:{s:10:"excalibuer";O:7:"prepare":1:{s:7:"release";O:5:"saber":1:{s:6:"weapon";s:52:"php://filter…

001集—shapefile(.shp)格式详解——arcgis

一、什么是shapefile Shapefile 是一种用于存储地理要素的几何位置和属性信息的非拓扑简单格式。shapefile 中的地理要素可通过点、线或面&#xff08;区域&#xff09;来表示。包含 shapefile 的工作空间还可以包含 dBASE 表&#xff0c;它们用于存储可连接到 shapefile 的要…

Adobe Camera Raw forMac/win:掌控原始之美的秘密武器

Adobe Camera Raw&#xff0c;这款由Adobe开发的插件&#xff0c;已经成为摄影师和设计师们的必备工具。对于那些追求完美、渴望探索更多创意可能性的专业人士来说&#xff0c;它不仅仅是一个插件&#xff0c;更是一个能够释放无尽创造力的平台。 在数字摄影时代&#xff0c;R…

【Ubuntu 22.04.3 LTS】apt-get下载安装有关问题可能原因及解决方法

ubuntu 22.04.3 LTS unaccountably error 装啥啥没依赖 可能是用了不合适的源&#xff0c;换个就好了 Now, let’s take a look at the lsb_release output, with a special focus on the Codename, which could be a crucial piece of information. The lsb_release comm…

普通spring项目配置加密

概述 本文主要介绍普通spring项目(非springboot)怎么进行配置加密。 出于安全考虑&#xff0c;生产配置不能明文出现在配置文件中。对于SpringBoot可以使用jasypt-spring-boot这个组件来为配置属性提供加密。 普通的spring项目暂时就没有找到合适的加密工具。这时候那就只能…

Banana Pi BPI-R4开源路由器开发板快速上手用户手册,采用联发科MT7988芯片设计

介绍 Banana Pi BPI-R4 路由器板采用 MediaTek MT7988A (Filogic 880) 四核 ARM Corex-A73 设计&#xff0c;4GB DDR4 RAM&#xff0c;8GB eMMC&#xff0c;板载 128MB SPI-NAND 闪存&#xff0c;还有 2x 10Gbe SFP、4x Gbe 网络端口&#xff0c;带 USB3 .2端口&#xff0c;M.2…

basic CNN

文章目录 回顾卷积神经网络卷积卷积核卷积过程卷积后图像尺寸计算公式&#xff1a;代码 padding代码 Stride代码 MaxPooling代码 一个简单的卷积神经网络用卷积神经网络来对MINIST数据集进行分类如何使用GPU代码 练习 回顾 下面这种由线形层构成的网络是全连接网络。 对于图像…

Codesys与威纶通触摸屏标签通信步骤

codesys软件&#xff0c;添加对象结构体。 添加对象全局变量列表。设置变量列表属性。 添加对象符号配置&#xff0c;编译。 勾选变量&#xff0c;编译。 文件夹出现xml文件。 打开威纶通软件&#xff0c;添加设备&#xff0c;导入标签&#xff0c;选择上图的文件。

RX-4571LC/NB/SA实时时钟模块

RX-4571LC实时时钟模块是EPSON推出的一求款额定频率32.768KHz&#xff0c;接口为SPI(3-wire)&#xff0c;月偏差为60 s的实时时钟模块&#xff0c;12脚贴片&#xff0c;具有小尺寸&#xff0c;高稳定性。该款实时时钟模块&#xff0c;可以在-40~85 C的温度内稳定工作,频率公差为…

解决Could not transfer artifact org.springframework.boot的问题

进行maven更新的时候&#xff0c;发现报错了 Could not transfer artifact org.springframework.boot&#xff0c;提示网络错误&#xff0c;搜了一下&#xff0c;应该是要忽略https 在maven设置中添加如下语句 -Dmaven.wagon.http.ssl.insecuretrue -Dmaven.wagon.http.ssl.a…

Redis(十)SpringBoot集成Redis

文章目录 连接单机mvnYMLController.javaRedisConfig.java 连接集群YML问题复现 RedisTemplate方式 连接单机 mvn <!--Redis--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</art…

[C++]类和对象(上)

目录 一:面向过程与面向对象的区别 二:类的定义 三:类的访问限定符和封装 3.1访问限定符 3.2 封装 四:类的实例化 五:类对象模型 如何计算类的大小 类对象的存储方式 六:this指针 this指针的引出 this指针的特性 一:面向过程与面向对象的区别 面向过程 C语言是面…

C#: 软件任务栏托盘图标添加关闭软件菜单等

说明&#xff1a;在软件在任务栏右下角的系统托盘的图标添加个右键弹出菜单功能&#xff0c;案例实现右键弹窗菜单关闭软件功能。 1.添加系统托盘图标控件 NotifyIcon 2.右键打开控件属性菜单添加鼠标点击事件函数 3.事件函数添加代码 //右键点击任务栏图标弹出关闭菜单 priv…

LeetCode Hot100 回顾(二)

子串 560.和为K的子数组 使用前缀和预处理一下题目给的数组, 然后用二重循环遍历一遍就可以了。 239.滑动窗口最大值 看题面比较容易想到的是用优先级队列来解决, 但是STL中的priority_queue不支持随机删除, 如果要用优先级队列来解决这道题的话比较复杂。这道题的一种正确…

leetcode刷题(剑指offer) 82.删除排序链表中的重复元素Ⅱ

82.删除排序链表中的重复元素Ⅱ 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,3,4,4,5] 输出&#xff1a;[1,2,5]示例 2&#xff1a…

数据监测的频次应如何设定

品牌在做控价时&#xff0c;需要先对线上数据进行监测&#xff0c;监测就要考虑监测的时间点&#xff0c;是白天监测还是夜晚监测&#xff0c;或者一天要监测几轮&#xff0c;这些问题都需要提前考虑好&#xff0c;因为待监测结果出来还要做破价治理&#xff0c;所以时间结点必…