数据结构:二叉树的链式结构及相关算法详解

目录

一.链式结构的实现

1.二叉树结点基本结构,初始化与销毁:

二.链式结构二叉树的几种遍历算法

1.几种算法的简单区分:

2.前序遍历:

3.中序遍历:

4.后序遍历:

5.层序遍历(广度优先遍历BFS):

三.链式二叉树的几种使用

1.计算树的结点个数:

2.计算树的叶子结点个数:

3.计算树的第k层结点个数:

4.计算树的深度:

5.查找结点为X的结点:

6.判断二叉树是否为完全二叉树:


一.链式结构的实现

1.二叉树结点基本结构,初始化与销毁:

(1)⽤链表来表示⼀棵⼆叉树,即⽤链来指示元素的逻辑关系。通常的⽅法是链表中每个结点由三个域组成,数据域左右指针域,左右指针分别⽤来给出该结点左孩子和右孩子所在的链结点的存储地址, 其结构如下:

(2)二叉树结点的初始化与树的创建:

#include"Tree.h"
//结点的初始化
BTNode* buyNode(char x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	node->data = x;
	node->left = node->right = NULL;

	return node;
}
//树结构的创建
BTNode* creatBinaryTree()
{
	BTNode* nodeA = buyNode('A');
	BTNode* nodeB = buyNode('B');
	BTNode* nodeC = buyNode('C');
	BTNode* nodeD = buyNode('D');
	BTNode* nodeE = buyNode('E');
	BTNode* nodeF = buyNode('F');



	nodeA->left = nodeB;
	nodeA->right = nodeC;
	nodeB->left = nodeD;
	nodeC->right = nodeE;
	nodeC->right = nodeF;
}

(3)链式二叉树的销毁:

(使用后序遍历,我在下文会写出具体操作)

void BinaryTreeDestory(BTNode** root)
{
	//这里因为要改变根节点,应该传入的是根节点的地址,所以得拿二级指针接收
	//递归出口
	if ((*root) == NULL)
	{
		return;
	}
	//自叶向根方向的释放
	//如果先释放的话,就找不到叶子节点了
	BinaryTreeDestory(&((*root)->left));
	BinaryTreeDestory(&((*root)->right));
	free(*root);
	*root = NULL;
}


二.链式结构二叉树的几种遍历算法

1.几种算法的简单区分:

举一个普遍的例子具体说明一下,如图:

2.前序遍历:

简单说明一下前序遍历的基本逻辑:

假设一个树:   

    A
   / \
  B   C
 / \
D   E

那么其遍历过程为:
访问根节点  A 。
递归遍历左子树(以  B  为根):
访问  B 
递归遍历左子树(以  D  为根):
访问  D 
递归遍历右子树(以  E  为根):
访问  E 
递归遍历右子树(以  C  为根):
访问  C 

//前序遍历
void preOrder(BTNode* root)
{
	//头 左 右
	//递归出口
	if (root == NULL)
	{
		printf("NULL");
		return;
	}
	printf("%c",root->data);
	preOrder(root->left);
	preOrder(root->right);
}

3.中序遍历:

void inOrder(BTNode* root)
{
	//左 头 右
	//递归出口
	if (root == NULL)
	{
		printf("NULL");
		return;
	}
	inOrder(root->left); //注意别调用错了,调用中序的
	printf("%c", root->data);
	inOrder(root->right);
}

4.后序遍历:

void postOrder(BTNode* root)
{
	//递归出口
	if (root == NULL)
	{
		printf("NULL");
		return;
	}
	postOrder(root->left);
	postOrder(root->right);
	printf("%c", root->data);
}

总结:如上述所示,前中后序遍历的代码共同点也是相当明显了,主要就是递归顺序左右子树的顺序不同而影响的代码输出顺序不同,其根本上来说就是函数栈帧的不断进行嵌套式的创建与销毁,如果挨个遍历可能会显得比较复杂,但通过代码所示的这种递归算法,前中后序的遍历实现也显得十分简洁明了

但还有一点尤其需要注意:就是不同于层序遍历,我上面所说的三种遍历都属于深度优先遍历(DFS),而层序遍历却属于广度优先遍历,关于这两大类遍历的优劣我会在实现层序遍历后作详细介绍

5.层序遍历(广度优先遍历BFS):

思路:

借助数据结构:队列,先通过根节点入队列,再循环判断队列是否为空,不为空则取队头然后出队头,并将队头结点的左右孩子入队列

(由于后续层序遍历的实现需要用到好些队列的知识,所有我先将队列的一些简单用法附在下面,需要的可以稍微看看)

//定义结点结构
typedef int QDataTpe;
typedef struct QueueNode
{
	struct QueneNode* next;
	QDataTpe data;
}QueueNode;



//定义队列的结构
typedef struct Queue
{
	QueueNode* phead;//队头
	QueueNode* ptail;//队尾
	int size;
}Queue;

BTNode* buyNode(char x);



//队列初始化
void QueueInit(Queue* pq)
{
	assert(pq);
	pq->phead = pq->ptail = NULL;
}

//入队(从队尾入)
void QueuePush(Queue* pq, QDataTpe x)
{
	assert(pq);
	//申请一个结点
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	//队列为空,newnode是对头也是队尾
	if (pq->phead == NULL)
	{
		pq->phead = pq->ptail = newnode;
	}
	else //队列非空,尾插
	{
		pq->ptail->next = newnode;
		pq->ptail = pq->ptail->next;
	}
	pq->size++;
}
//判断队列是否为空
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->phead == 0;
}

//出队(从队头出)
void QueuePop(Queue* pq)
{
	assert(!QueueEmpty(pq));
	//只有一个结点的情况下,要把队尾和队头的两个指针都考虑到
	if (pq->phead == pq->ptail)
	{
		free(pq->ptail);
		pq->phead = pq->ptail = NULL;
	}
	QueueNode* next = pq->phead->next;
	free(pq->phead);
	pq->phead = next;  
	pq->size--;
}

//取队头数据
QDataTpe QueueFront(Queue* pq)
{
	assert(!QueueEmpty(pq));
	return pq->phead->data;
}
//取队尾数据
QDataTpe QueueBack(Queue* pq)
{
	assert(!QueueEmpty(pq));
	return pq->ptail->data;
}

//队列的销毁
void QueueDestory(Queue* pq)
{
	assert(pq);
	QueueNode* pcur = pq->phead;
	while (pcur)
	{
		QueueNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	pq->phead = pq->ptail = NULL;
}

//取队列元素个数
int QueueSize(Queue* pq)
{
	return pq->size;
}
//层序遍历
void LeveIOrder(BTNode* root)
{
	Queue q;//创建一个队列
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		//取队头出队头,打印结点值
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		printf("%c", top->data);
		//将队头结点的非空左右孩子结点入队列
		if (top->left)
		{
			QueuePush(&q, top->left);
		}
		if (top->right)
		{
			QueuePush(&q, top->right);
		}
		QueuDestroy(&q);
	}

}

小结: 
DFS 优点(深度优先遍历)
节点少时效率高,节省内存
适合需要“全探索”或“找到任意解即可”的场景(如迷宫路径)

 
DFS 缺点
可能陷入无限循环(需记录已访问节点)
不保证最短路径(除非剪枝优化)


BFS 优点 (广度优先遍历)
保证找到最短路径(无权图中)
层级分明,易于理解

 
BFS 缺点
内存占用大(需存储整个层级节点)
不适合大规模数据或深层结构(如树深度极大)


三.链式二叉树的几种使用

1.计算树的结点个数:

法一:把size作为一个函数的形参,然后把这个树遍历一遍,每遍历一个节点就size(节点个数)加一,但需要注意的是,需要传入size的地址才能改变size的值

void BinaryTreeSize(BTNode* root,int* size)
{
	if (root == NULL)
	{
		return;
	}
	(*size)++;
	BinaryTreeSize(root->left,size);
	BinaryTreeSize(root->right, size);
}

法二:递归,  节点个数=左子树节点个数+右子树节点个数,所以我们以此为基础递归就可以了

int BinaryTreeSize(BTNode* root)
{
	//节点个数=左子树节点个数+右子树节点个数
	//递归出口
	if (root == NULL)
	{
		return 0;
	}
	return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

2.计算树的叶子结点个数:

叶子结点:即没有左右孩子结点的结点4

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);
}

3.计算树的第k层结点个数:

如上图,当k=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);
}

4.计算树的深度:

int BinaryTreeDeep(BTNode* root)
{
	//计算树的深度
	if (root == NULL)
	{
		return 0;
	}
	int lefDep = BinaryTreeDeep(root->left);
	int rigDep = BinaryTreeDeep(root->right);
	return 1 + (lefDep > rigDep ? lefDep : rigDep);
}

5.查找结点为X的结点:

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	//递归出口
	if (root == NULL)
	{
		return 0;
	}
	if (root->data == x)
	{
		return root;
	}
	//代码走到这里证明根节点并不是我们要找的结点
	//接下来是左右子树各自分开递归搜查
	BTNode* left = BinaryTreeFind(root->left, x);
	if (left)//由于函数最后返回NULL,所以如果这个if条件可以进入就足以说明找到了需要的结点
	{
		return root;
	}
	BTNode* right = BinaryTreeFind(root->right, x);
	if (right)
	{
		return root;
	}
	return NULL;
}

6.判断二叉树是否为完全二叉树:

// 判断⼆叉树是否是完全⼆叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		//取队头,出队头
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		if (top == NULL)
		{
			break;
		}
		//队头结点的左右孩子入队列
		QueuePush(&q, top->left);
		QueuePush(&q, top->right);
	}
	//队列不为空,继续取队头出队头
	//1)队头存在非空结点----非完全二叉树
	//2)队头不存在非空结点----完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		if (top != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

第一个while是为了验证根结点,如果根结点为空,直接返回false,如果根结点不为空,就将树里的结点循环入队列然后继续将子结点入队列并且判断其是否为空,同样的第一次循环的的结束条件是当出现空为止,因此,当来到第二次循环时,树的结点里说明已经第一次循环出了空结点,而当树是完全二叉树时,第二次循环之后队列里应该全是空结点,因此,只要当第二次循环里出现非空结点时,就可以判断其时非完全二叉树(这题思路比较复杂,可以多想一会)

欧克,本次关于链式二叉树的知识点就到此为止了,相关的题目我也会在未来附上

ok,全文终

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

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

相关文章

VSCode 移除EmmyLua插件的红色波浪线提示

VSCode 中安装插件EmmyLua,然后打开lua文件的时候,如果lua代码引用了C#脚本的变量,经常出现 “undefined global variable: UnityEngineEmmyLua(undefined-global)” 的红色波浪线提示,这个提示看着比较烦人,我们可以通…

MWC 2025 | 紫光展锐联合移远通信推出全面支持R16特性的5G模组RG620UA-EU

2025年世界移动通信大会(MWC 2025)期间,紫光展锐联合移远通信,正式发布了全面支持5G R16特性的模组RG620UA-EU,以强大的灵活性和便捷性赋能产业。 展锐芯加持,关键性能优异 RG620UA-EU模组基于紫光展锐V62…

springboot425-基于SpringBoot的BUG管理系统(源码+数据库+纯前后端分离+部署讲解等)

💕💕作者: 爱笑学姐 💕💕个人简介:十年Java,Python美女程序员一枚,精通计算机专业前后端各类框架。 💕💕各类成品Java毕设 。javaweb,ssm&#xf…

机器人“照镜子”:开启智能新时代

机器人也爱 “照镜子”? 在科技飞速发展的今天,机器人的身影越来越频繁地出现在我们的生活和工作中。它们承担着各种各样的任务,从工业生产线上的精密操作,到家庭中的清洁服务,再到危险环境下的救援工作。然而&#xf…

让 LabVIEW 程序更稳定

LabVIEW 开发的系统,尤其是工业级应用,往往需要长时间稳定运行,容不得崩溃、卡顿或数据丢失。然而,许多系统在实际运行中会遭遇内存泄漏、通信中断、界面卡顿等问题,导致生产中断甚至设备损坏。如何设计一个既稳定又易…

基于CURL命令封装的JAVA通用HTTP工具

文章目录 一、简要概述二、封装过程1. 引入依赖2. 定义脚本执行类 三、单元测试四、其他资源 一、简要概述 在Linux中curl是一个利用URL规则在命令行下工作的文件传输工具,可以说是一款很强大的http命令行工具。它支持文件的上传和下载,是综合传输工具&…

npm ERR! code 128 npm ERR! An unknown git error occurred

【问题描述】 【问题解决】 管理员运行cmd(右键window --> 选择终端管理员) 执行命令 git config --global url.“https://”.insteadOf ssh://git cd 到项目目录 重新执行npm install 个人原因,这里执行npm install --registryhttps:…

汽车视频智能包装创作解决方案,让旅途记忆一键升级为影视级大片

在智能汽车时代,行车记录已不再是简单的影像留存,而是承载情感与创意的载体。美摄科技依托20余年视音频领域技术积累,推出汽车视频智能包装创作解决方案,以AI驱动影像处理与艺术创作,重新定义车载视频体验,…

Qt中txt文件输出为PDF格式

main.cpp PdfReportGenerator pdfReportGenerator;// 加载中文字体if (QFontDatabase::addApplicationFont(":/new/prefix1/simsun.ttf") -1) {QMessageBox::warning(nullptr, "警告", "无法加载中文字体");}// 解析日志文件QVector<LogEntr…

nlp进阶

1 Rnn RNN(Recurrent Neural Network),中文称作循环神经网络,它一般以序列数据为输入,通过网络内部的结构段计有效捕捉序列之间的关系特征,一般也是以序列形式进行输出. 单层网络结构 在循环 rnn处理的过程 rnn类别 n - n n - 1 使用sigmoid 或者softmax处理 应用在分类中…

2024 JAVA面试题

第一章-Java基础篇 1、你是怎样理解OOP面向对象 面向对象是利于语言对现实事物进行抽象。面向对象具有以下特征&#xff1a; 继承****&#xff1a;****继承是从已有类得到继承信息创建新类的过程 封装&#xff1a;封装是把数据和操作数据的方法绑定起来&#xff0c;对数据的…

浅色系可视化大屏看起来确实很漂亮,但用到的地方确实很少

在数字化信息飞速发展的时代&#xff0c;可视化大屏作为信息展示的重要载体&#xff0c;广泛应用于各类场景。其中&#xff0c;浅色系可视化大屏以其独特的视觉风格&#xff0c;在众多展示方案中脱颖而出&#xff0c;给人以清新、舒适的视觉感受。然而&#xff0c;尽管浅色系可…

蓝桥杯备考:动态规划线性dp之下楼梯问题进阶版

老规矩&#xff0c;按照dp题的顺序 step1 定义状态表达 f[i]表示到第i个台阶的方案数 step2:推导状态方程 step3:初始化 初始化要保证 1.数组不越界 2.推导结果正确 如图这种情况就越界了&#xff0c;我们如果把1到k的值全初始化也不现实&#xff0c;会增加程序的时间复杂度…

LLM 大模型基础认知篇

目录 1、基本概述 2、大模型工作原理 3、关键知识点 &#xff08;1&#xff09;RAG 知识库 &#xff08;2&#xff09;蒸馏 &#xff08;3&#xff09;微调 &#xff08;4&#xff09;智能体 1、基本概述 大型语言模型&#xff08;Large Language Model, LLM&#xff09…

物业管理系统源码 物业小程序源码

物业管理系统源码 物业小程序源码 一、基础信息管理 1. 房产信息管理 记录楼栋、单元、房间的详细信息&#xff08;面积、户型、产权等&#xff09;。 管理业主/租户的档案&#xff0c;包括联系方式、合同信息等。 2. 公共资源管理 管理停车场、电梯、绿化带、公…

Delphi连接MySql数据库房

在看Delpih6数据库开发实例导航这本书时&#xff0c;里面的数据库管理系统用的InterBase&#xff0c;但是Delphi11中已经没有这个东西了&#xff0c;我就想到利用MS的access但是里面有很多的SQL语句不支持&#xff0c;比如设置字段的默认值等&#xff0c;后来我想到连接到MySQL…

[51 单片机] --串口编程

1&#xff0c;通讯方式基本概念 1&#xff0c;按照 --> 数据传送方式串行通讯&#xff1a;使用一条数据线&#xff0c;将数据一位一位地依次传输&#xff0c;每一位数据占据一个固定的时间长度&#xff0c;串行通信的特点&#xff1a;传输线少&#xff0c;长距离传送时成本…

基础算法——模拟

模拟&#xff0c;顾名思义&#xff0c;就是题⽬让你做什么你就做什么&#xff0c;考察的是将思路转化成代码的代码能⼒。 这类题⼀般较为简单&#xff0c;属于竞赛⾥⾯的签到题&#xff08;但是&#xff0c;万事⽆绝对&#xff0c;也有可能会出现让⼈⾮常难受的 模拟题&#xf…

SparkStreaming之04:调优

SparkStreaming调优 一 、要点 4.1 SparkStreaming运行原理 深入理解 4.2 调优策略 4.2.1 调整BlockReceiver的数量 案例演示&#xff1a; object MultiReceiverNetworkWordCount {def main(args: Array[String]) {val sparkConf new SparkConf().setAppName("Networ…

Jenkins 删除历史构建记录

中文:系统管理 > 脚本命令行: 英文:Manage Jenkins > Script Console def jobName "Wens-Web" //删除的项目名称 def maxNumber 105 // 保留的最小编号&#xff0c;意味着小于该编号的构建都将被删除 Jenkins.instance.getItemByFullName(jobName).build…