数据结构——二叉树的实现

文章目录

  • 一、二叉树概念的回顾
  • 二、二叉树结构的定义
  • 三、二叉树的创建
    • 方法一、写个创建结点的函数然后手动链接起来
      • 创建结点的函数
      • 手动链接
    • 方法二、通过前序遍历的数组的方式构建二叉树
      • 创建的函数声明
      • 创建函数的定义
  • 四、 二叉树的遍历
    • 前序遍历
    • 中序遍历
    • 后序遍历
    • 层序遍历
  • 五、二叉树的其他功能
    • 二叉树的销毁
    • 树的结点个数
    • 树的叶子结点个数
    • 第K层结点的个数
    • 树的高度
    • 查找值为k的结点
    • 判断是否是完全二叉树

一、二叉树概念的回顾

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

在这里插入图片描述
注意:
1. 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

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

二、二叉树结构的定义

前面我们提到了二叉树的结构既可以用二叉链也可以用三叉链,下面我们基于二叉链来实现二叉树

//定义二叉树结构
typedef int BTDataType;
typedef struct BinaryTree
{
	BTDataType val;//储存数据
	struct BinaryTree* left;//左孩子结点
	struct BinaryTree* right;//右孩子结点
}BTNode;

三、二叉树的创建

方法一、写个创建结点的函数然后手动链接起来

创建结点的函数

//创建结点
BTNode* BuyNode(BTDataType x)
{
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	root->val = x;
	root->left = NULL;
	root->right = NULL;
	return root;
}

手动链接

//手动创建一个二叉树
BTNode* CreateNode()
{
	BTNode* n1 = BuyNode(1);
	BTNode* n2 = BuyNode(2);
	BTNode* n3 = BuyNode(3);
	BTNode* n4 = BuyNode(4);
	BTNode* n5 = BuyNode(5);
	BTNode* n6 = BuyNode(6);
	//BTNode* n7 = BuyNode(6);

	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	//n2->right = n7;
	n4->left = n5;
	n4->right = n6;

	return n1;
}

这样我们就成功创建了一个二叉树,如图

在这里插入图片描述

方法二、通过前序遍历的数组的方式构建二叉树

这里我们先来了解一下二叉树的遍历
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

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

例如在下面的数组的基础上利用前序遍历的思想创建二叉树
在这里插入图片描述

创建的函数声明

BTNode* BTCreateNode(BTDataType* a,int n,int* pi);

1.根据前序遍历的思想,我们发现数组的首元素就是根,通过递归本函数,我们可以先将根创建完后再创建左子树,然后创建右子树

2.遇到 # 就将此位置置为空指针然后退出递归,回到上一级

3.创建二叉树的其实就是一个堆逻辑的数组,为了改变下标,我们需要一个能够在递归时确定当前元素下标的变量,因此我们可以传地址,这样即使在递归途中,元素的下标就可以不断改变

创建函数的定义

BTNode* BTCreateNode(BTDataType* a,int n,int* pi)
{
	
	if ((*pi) >= n || a[(*pi)] == '#')
	{
		(*pi)++;//不能在外面++,因为如果不是"#",就不能跳过此位置
		return NULL;
	}
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	node->val = a[(*pi)++];
	node->left = BTCreateNode(a, n, pi);
	node->right = BTCreateNode(a, n, pi);
	return node;
}

四、 二叉树的遍历

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

遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础

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

注意:前序遍历、中序遍历、后序遍历普遍用的是递归的思想

前序遍历

前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

其实访问顺序就是根 -> 左 ->右

下面画下前序遍历的递归过程
在这里插入图片描述
代码实现

//前序遍历
void PrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ", root->val);
	PrevOrder(root->left);
	PrevOrder(root->right);
}

中序遍历

中序遍历(Inorder Traversal) ——访问根结点的操作发生在遍历其左右子树之中(间)。
其实访问顺序就是左 -> 根 ->右

与前序遍历的思想差不多,就是访问顺序改变了一下

在这里插入图片描述

//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->val);
	InOrder(root->right);
}

后序遍历

后序遍历(Postorder Traversal) ——访问根结点的操作发生在遍历其左右子树之后。
其实访问顺序就是左 -> 右 ->根
与前序遍历的思想类似,改变访问顺序即可

在这里插入图片描述

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

层序遍历

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

层序遍历可以借助队列的结构来实现

在这里插入图片描述

//层序遍历 -> 借助队列
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* node = QueueFront(&q);
		QueuePop(&q);
		printf("%d ", node->val);
		if (node->left)
		{
			QueuePush(&q, node->left);
		}
		if (node->right)
		{
			QueuePush(&q, node->right);
		}
	}
	QueueDestroy(&q);
}

五、二叉树的其他功能

二叉树的销毁

首先我们容易想到从根结点开始以次销毁,但是如果先将根结点销毁,那么就找不到左、右孩子了, 故我们可以反过来想, 先销毁左、右孩子,然后再销毁根结点

//二叉树的销毁
void BTDestroy(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	//利用后序遍历的思想销毁二叉树
	BTDestroy(root->left);
	BTDestroy(root->right);
	free(root);
}

树的结点个数

采用递归的思想,有结点就去访问左、右孩子,没有结点就返回0

//树的结点个数
int BTSize(BTNode* root)
{
	return root == NULL ? 0 : BTSize(root->left) + BTSize(root->right) + 1;
}

树的叶子结点个数

在寻找树的结点个数的前提下,加个判断,只要左、右孩子同时为空,才能返回 1

//树的叶子结点个数
int BTLeaveSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return BTLeaveSize(root->left) + BTLeaveSize(root->right);
}

第K层结点的个数

容易想到第一层的结点个数为1个,第二层的结点个数是第一层的左、右孩子不为空的个数,以此类推,第K层结点个数是第K - 1 层的孩子结点存在个数

//第K层结点的个数
int BTKSize(BTNode* root,int k)
{
	if (root == 0)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return BTKSize(root->left, k - 1) + BTKSize(root->right, k - 1);
}

树的高度

树的高度就是该树的深度,即左、右孩子之中的深度最大的那一个
注意: 应该将左、右孩子的深度给保存,减少递归的次数

//树的高度
int BTHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	int leftHeight = BTHeight(root->left);
	int rightHeight = BTHeight(root->right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

查找值为k的结点

根结点找到就返回,否则在左、右孩子之间查找
注意: 如果在左孩子里面找到了,就可以直接返回了,没有必要再在右孩子里面查找

//查找值为K的结点
BTNode* BTFind(BTNode* root, BTDataType k)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->val == k)
	{
		return root;
	}
	BTNode* leftFind = BTFind(root->left, k);
	if (leftFind)
	{
		return leftFind;
	}
	BTNode* rightFind = BTFind(root->right, k);
	if (rightFind)
	{
		return rightFind;
	}
	return NULL;
}

判断是否是完全二叉树

其实就是在层序遍历的基础上,不管左、右孩子是不是为空,直接插入到队列里面

如果遇见第一个非空,就跳出循环,遍历此时的队列里面是否还有非空结点,没有就是完全二叉树,有就不是完全二叉树

//判断是否是完全二叉树
int CompleteTree(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* node = QueueFront(&q);
		QueuePop(&q);
		if (node == NULL)
		{
			break;
		}
		QueuePush(&q, node->left);
		QueuePush(&q, node->right);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* node = QueueFront(&q);
		QueuePop(&q);
		if (node)
		{
			QueueDestroy(&q);
			return 0;
		}
	}
	QueueDestroy(&q);
	return 1;
}

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

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

相关文章

基于python实现生命游戏

文章目录 一、生命游戏是什么二、生命游戏规则解释1.相邻细胞2.细胞状态 三、代码实现1.邻居细胞2.更新状态 四、整体代码 一、生命游戏是什么 生命游戏(Game of Life)是由英国数学家约翰何顿康威在1970年发明的一种细胞自动机(Cellular Aut…

SOL 交易机器人基本知识

有没有可以盈利的机器人? 是的,各行各业都有许多盈利机器人。在金融领域,交易机器人被广泛用于自动化投资策略并根据预定义的算法执行交易。这些机器人可以分析市场趋势并做出快速决策,从而可能带来可观的回报。同样,在…

JAVA:多线程常见的面试题和答案

请关注微信公众号:拾荒的小海螺 博客地址:http://lsk-ww.cn/ 1、并发编程三要素? 原 子 性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。可 见 性 可见性指多…

智能仓储物流系统(WMS)系列-货品与分类管理

好的应用系统应是细分简单,界面简洁易操作,程序代码简洁易懂的。

【傻呱呱】python安装phook3(Windows端)

前期准备 swig程序Visual Studio C 构建工具 配置swig程序 将下载好的“swig-4.2.1”压缩包解压到C盘从C盘打开“swig-4.2.1”文件夹并复制文件夹路径 在开始菜单里搜索“环境变量”,点击“编辑系统环境变量” 点击“环境变量” 找到“path”并双击 点击“新建” …

MFC工控项目实例一主菜单制作

1、本项目用在WIN10下安装的vc6.0兼容版实现。创建项目名为SEAL_PRESSURE的MFC对话框。在项目res文件下添加相关256色ico格式图片。 2、项目名称:密封压力试验机 主菜单名称: 系统参数 SYS_DATA 系统测试 SYS_TEST 选择型号 TYP_CHOICE 开始试验 TES_STA…

汽车电子学习【车载网络CAN/LIN】

车载网络CAN/LIN知识总结 STM32F1开发板测试 STM32测试程序 /** CAN 通信报文内容设置*/ void CAN_SetMsg(void) { #if CAN_STDTxMessage.StdId 0x12;TxMessage.IDE CAN_ID_STD; #elseTxMessage.ExtId 0x1314; //使用的扩展IDTxMessage.IDE CAN_ID_EXT; //扩展模式 #…

MySQL注入 — Dns 注入

DNS注入原理 通过子查询,将内容拼接到域名内,让load_file()去访问共享文件,访问的域名被记录此时变为显错注入,将盲注变显错注入,读取远程共享文件,通过拼接出函数做查询,拼接到域名中,访问时将访问服务器,…

AI大模型日报#0529:杨红霞创业入局“端侧模型”、Ilya左膀右臂被Claude团队挖走

导读:AI大模型日报,爬虫LLM自动生成,一文览尽每日AI大模型要点资讯!目前采用“文心一言”(ERNIE 4.0)、“零一万物”(Yi-34B)生成了今日要点以及每条资讯的摘要。欢迎阅读&#xff0…

计算机毕业设计 | SpringBoot+vue仓库管理系统(附源码)

1,绪论 1.1 项目背景 随着电子计算机技术和信息网络技术的发明和应用,使着人类社会从工业经济时代向知识经济时代发展。在这个知识经济时代里,仓库管理系统将会成为企业生产以及运作不可缺少的管理工具。这个仓库管理系统是由:一…

美团拼好饭小程序mtgsig1.2分析(补环境分析)

声明 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!wx a15018601872 本文章未…

AUS GLOBAL 荣获 Brokersview 颁奖盛典多项殊荣

2024年1月31日在迪拜 Sheikh Zayed Rd - Trade Centre - Trade Centre 1 举行的 Brokersview 颁奖盛典上,AUS GLOBAL(澳洲环球)再次展现了其在金融行业的卓越实力,并荣获多项殊荣。 AUS GLOBAL 作为一家全球领先的金融服务提供商…

【Linux进程篇】Linux进程管理——进程创建与终止

W...Y的主页 😊 代码仓库分享💕 目录 进程创建 fork函数初识 写时拷贝 fork常规用法 fork调用失败的原因 进程终止 进程退出场景 _exit函数 exit函数 return退出 进程创建 fork函数初识 在linux中fork函数时非常重要的函数,它从已…

【蓝桥杯嵌入式】 第六届国赛

目录 题目 配置 注意事项 代码 - 默写大师 EEPROM读写函数 LED驱动函数 ADC采集 上电初始化 LCD 按键 PWM互补输出 全部代码 hardware.c hardware.h control.c control.h main.c 题目 配置 注意事项 复制LCD的工程,先配置资源 --- 勾选完选项一…

Java基于saas模式云MES制造执行系统源码Spring Boot + Hibernate Validation什么是MES系统?

Java基于saas模式云MES制造执行系统源码Spring Boot Hibernate Validation 什么是MES系统? MES制造执行系统,通过互联网技术实现从订单下达到产品完成的整个生产过程进行优化管理。能有效地对生产现场的流程进行智能控制,防错防呆防漏&…

docker占用磁盘空间大小排查

首先进入到 /var/lib/docker/overlay2 目录下,查看谁占用的较多 cd /var/lib/docker/overlay2/du -s ./* | sort -rn | more再通过目录名查找容器名 docker ps -q | xargs docker inspect --format {{.State.Pid}}, {{.Id}}, {{.Name}}, {{.GraphDriver.Data.WorkDir}} | gre…

【4.vi编辑器使用(下)】

一、vi编辑器的光标移动 二、vi编辑器查找命令 1、命令::/string 查找字符串 n:继续查找 N:反向继续查找 /^the 查找以the开头的行 /end 查找以 查找以 查找以结尾的行 三、vi编辑器替换命令 1、语法: : s[范围,范围]str1/str2[g] g表示全…

如何在.NET中集成SignalR

SignalR 简介 SignalR是一个开放源代码库,可用于简化向应用添加实时Web功能,实时Web功能使服务器端代码能够将内容推送到客户端。 SignalR开源库:https://github.com/SignalR/SignalR SignalR 应用场景 需要高频次从服务器获取信息的应用&am…

Hack The Box-MagicGardens

总体思路 SMTP用户爆破->5000端口Docker注册表爆破->敏感数据泄露->Firefox远程调试LFI 信息收集&端口利用 nmap -sSVC 10.10.11.9目标开放了22、25、80、5000端口,先看80端口是否存在利用点 目录扫描结果大部分都是302跳转到admin界面,…

2.1色彩空间

色彩发送器 色彩认知 光源是出生点,光源发射出光线,光线通过直射反射折射等路径最终进入人眼。 但人眼接收到光线后,人眼的细胞产生了一系列化学反应。 由此把产生的信号传入大脑,最终大脑对颜色产生了认知感知。 光的要素 光…