【手撕数据结构】链式二叉树

目录

  • 链式二叉树的结构及其声明
  • 链式二叉树的四种遍历方式
    • 前序遍历
    • 中序遍历(中根遍历)
    • 后序遍历
    • 层序遍历
    • 概念
    • 思路分析
    • 详细代码
  • 求树的节点个数
    • 变量累加法(错误)
    • 分治递归法
  • 求树的叶子节点个数
    • 警惕空指针
    • 正确代码
  • 求第k层节点个树
    • 思路分析及规则明细
    • 代码详细
  • 求树的深度/高度
    • 规则明细及思路分析
    • 错误代码示范
    • 代码详细(正确)
  • 查找指定节点
    • 思路分析
    • 代码详细
  • 销毁二叉树
    • 思路分析
    • 代码详细

链式二叉树的结构及其声明

  • 首先来看看它的结构声明。结构体中有三个成员,一个是当前结点的值,还有两个是指向当前结点左孩子结点的指针以及指向右孩子结点的指针
typedef int BTDataType;
typedef struct BinaryTreeNode {
	BTDataType data;
	struct BinaryTreeNode* lchild;
	struct BinaryTreeNode* rchild;
}BTNode;

  • 也就是下面这种样子

在这里插入图片描述

链式二叉树的四种遍历方式

前序遍历

  • 规则:根——左子树——右子树
    在这里插入图片描述
  • 总的来说,我们必须把对于每个节点根节点(自己)访问完,然后访问他的左孩子节点,而对于左孩子节点他也必须访问根节点(自己),然后访问他的左孩子节点,指定为NULL,然后开始访问他的右孩子节点.

在这里插入图片描述

  • 对于1这个节点,先访问本身,打印1;然后接着访问他的左孩子节点2,对于2也是访问他本身,打印2,然后访问他的左孩子节点4,对于4也是访问他本身,打印4,然后访问他的左孩子节点NULL,发现左孩子节点为NULL直接返回(也可以选择打印)
/*先序遍历*/
void PreOrder(BTNode* root)
{
	if (!root)
	{
		//printf("NULL ");可以选择打印
		return;
	}
	printf("%d ", root->data);
	PreOrder(root->lchild);
	PreOrder(root->rchild);
}

  • 结果就是1 2 4 3

中序遍历(中根遍历)

  • 规则:左子树——根——右子树
  • 了解了前序遍历,那中序遍历也不下话下。和先序做一个区分
  • 这不简单了吗?前面是先访问根节点,然后依次访问他的左孩子节点,然后左孩子节点又访问根节点(本身)然后又访问他的左还子节点
  • 然后咱们就依葫芦画瓢,先访问左孩子节点,然后左孩子节点又访问他的左孩子节点,访问为空,然后访问根节点,然后根节点访问完,又访问右孩子节点
/*中序遍历*/
void InOrder(BTNode* root)
{
	if (!root)
	{
		//printf("NULL ");
		return;
	}
	PreOrder(root->lchild);
	printf("%d ", root->data);
	PreOrder(root->rchild);
}

  • 以上一个二叉树1 2 3 4为例,他的中序遍历:4 2 1 3

后序遍历

规则:左子树——右子树——根

  • 这里我相信大家都能自己推出来了,直接上代码吧。
/*后序遍历*/
void PostOrder(BTNode* root)
{
	if (!root)
	{
		//printf("NULL ");
		return;
	}
	PreOrder(root->lchild);
	PreOrder(root->rchild);
	printf("%d ", root->data);
}

  • 1 2 3 4二叉树为例,结果: 4 2 3 1

层序遍历

概念

在这里插入图片描述

  • 就是一层一层从左到右遍历打印,比如上面的二叉树,一层一层从左到右打印就是1 2 3 4 5

思路分析

  • 这里层次遍历就无法使用递归了,递归要么是左子树递归到底,要么是右子树递归到底,我们这里必须把每层的左右都打印完
  • 观察一下,也就是满足 1 2 3 4 5,按节点顺序来,我们前面说的数据结构队列的原则就是先进先出,是不是进满足这个规则。

下面讲讲具体步骤

  • 第一步就是,把根节点入队列,此时这个节点是队头。(目的是让队列不为空)
  • 第二步就是,取队头,并且打印数据,然后出队列(按照队头顺序打印)
  • 第三步就是,如果队头的左右孩子不为空,将他们入队列(先进先出原则,把上一层的左右孩子打印)
  • 第四步,从第二步开始重复,直到队列为空。
    在这里插入图片描述在这里插入图片描述

详细代码

void LevelOrdre(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while(!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		printf("%d ", front->data);
		QueuePop(&q);
		if (front->left)
		{
			QueuePush(&q, front->left);
		}
		if (front->right)
		{
			QueuePush(&q, front->right);
		}
	}
	QueueDestroy(&q);
}
  • 注意改变了了队列存储节点的数据类型为二叉树节点
    在这里插入图片描述
  • 入队列的节点注意是取出队头的左右孩子节点

求树的节点个数

变量累加法(错误)

  • 有的同学看到,灵机一动,这么简单?直接定义一个局部变量累加就行了。想法挺好,但现实很残酷,我们每次调用函数都要开辟函数栈帧,而每个函数栈帧都要重新定义这个变量,变量每次都是从0开始。这时候结构每次就是1
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	int size = 0;
	size++;
	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);
	return size;
}
  • 于是有同学呢又想出一种办法,就是将这个变量定义为静态变量,因为静态变量是存放在静态区中的,不会随着某一个函数的调用结束而销毁,因此我们可以这么写
int TreeNodeNum1(BTNode* root)
{
	if (root == NULL)
		return;
	static int sz = 0;
	sz++;
	TreeNodeNum1(root->lchild);
	TreeNodeNum1(root->rchild);
	return sz;
}

  • 可以看到,第一次的计算确实是可以计算出来这棵树有6个结点,但是在我多调用几次之后结点个数却发生了一个累加,这就是静态变量的特点,均会在上一次的基础上去进行一个运算,无论你是调用其他的什么函数或者是多调用几次,那其实可以看到这里就出现BUG了,
    在这里插入图片描述
  • 其实这一块很简单,我们不需要静态变量,直接将这个变量放到函数外部,作为一个全局变量即可,因为函数内部的返回值我们不好控制,干脆就不要返回值,直接将这个记数的变量定义为全局变量,这样就很好控制了
int sz = 0;
void TreeNodeNum1(BTNode* root)
{
	if (root == NULL)
		return;
	sz++;
	TreeNodeNum1(root->lchild);
	TreeNodeNum1(root->rchild);
}

在这里插入图片描述

分治递归法

  • 我们始终将一个树分割为三个部分,【根】【左子树】【右子树】,因此进行一个分块求解然后再加起来就可以了
  • 也就是说我们把每一个子树的根节点和左孩子节点和右孩子节点的个数累加起来
    在这里插入图片描述
  • 注意的是,我们如果当前节点存在子树,子树应该至少有一个节点,所以统计每个子树节点个数的时候至少为1,如果节点为空,说明没有子树,直接返回0
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

在这里插入图片描述

求树的叶子节点个数

警惕空指针

  • 求解树的叶子结点个数,叶子节点就是左孩子和右孩子节点都为NULL
  • 前面说了如何去求一颗树的节点个数,那么求叶子节点个树就是不加上那些不是左右孩子都为NULL的节点
  • 于是有同学就写了这样的代码,我们来看看有什么问题
int BinaryTreeLeafSize(BTNode* root)
{
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

在这里插入图片描述

  • 所以我们要对当前节点是否为NULL进行判断,如果为NULL说明他肯定不是叶子节点

正确代码

  • 根据上面的推测我们可以写出
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);
}

求第k层节点个树

思路分析及规则明细

📚当k > 1 时,第k层的结点个数为第k - 1层左孩子节点个数 + k-1层右孩子的结点个数
📚当k == 1时,return 1

  • 除了判断k之外,别忘了每次都要判断一下传进来的根结点是否为空,防止访问空指针
    在这里插入图片描述

  • ,所以当k=1的时候,是第k层,第k层节点个数实际就是第k-1层的左右孩子节点个数,求k-1层节点的左右孩子节点即可

代码详细

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

求树的深度/高度

规则明细及思路分析

规则:二叉树高度 = 左子树和右子树中高度大的那个 + 1

  • 思路就是我们需要比较二叉树的右子树和左子树的高度谁更高,此时就是整棵树的深度。想知道对于祖宗节点左子树的深度,就得求出祖宗节点左子树的左子树深度,依次类推比较返回。
  • 值得注意的是,+1是因为如果二叉树不是空树,就至少高度为1(祖宗节点一层)

错误代码示范

int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	return TreeHeight(root->lchild) > TreeHeight(root->rchild) ?		
		   TreeHeight(root->lchild) + 1 : TreeHeight(root->rchild) + 1;
}

  • 从运行结果可以看出是可以计算出来的,树的高度为4,但是呢却存在一个很大的隐患

这里我们直接画图
在这里插入图片描述
在这里插入图片描述

  • 可以看到我们,如果每一次都不去存储左右子树的高度,他们比较完大小后需要重新递归去求左右子树的高度+1,也就造成了【重复计算】
  • 所以我们说说下面正确代码

代码详细(正确)

int BinaryTreeDeapth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int LeftDepth = BinaryTreeDeapth(root->left);
	int RightDepth = BinaryTreeDeapth(root->right);
	return LeftDepth > RightDepth ? LeftDepth + 1 : RightDepth + 1;
}
  • 这样把递归回调的结果存储起来,然后比较其左右子树的高度直接返回就可以避免重新递归。

查找指定节点

思路分析

  • 想要在一个二叉树中查找指定的节点,无法就是在左右子树的节点里面去找。那么我们先查找左子树,如果找到了,就没必要去查找右子树。如果左右子树都查找完没有找到,则是没有该节点

代码详细

在这里插入图片描述

  • 对于祖宗节点1的左右子树

在这里插入图片描述

  • 强调对于2节点的左右子树
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode* LeftNode = BinaryTreeFind(root->left, x);
	if (LeftNode)
	{
		return LeftNode;
	}
	BTNode* RightNode = BinaryTreeFind(root->right, x);
	if (RightNode)
	{
		return RightNode;
	}
	return NULL;
}

销毁二叉树

思路分析

  • 同其他思路,如果像销毁整个二叉树,那么就得先销毁他的左右子树,那么对于左右子树,如果他们也有左右子树,需要先销毁他的左右子树,如果先销毁根节点,就无法找到他的左右子树了就是后序遍历)

代码详细

在这里插入图片描述

void BinaryDestroy(BTNode** root)
{
	if (*root == NULL)
	{
		return;
	}
	BinaryDestroy(&((*root)->left));
	BinaryDestroy(&((*root)->right));
	free(*root);
	*root = NULL;
}
  • 这里二级指针是为了改变实参,如果需要接口统一,可以传一级指针,不过需要把实参手动设置NULL

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

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

相关文章

算法学习017 不同的二叉搜索树 c++算法学习 中小学算法思维学习 比赛算法题解 信奥算法解析

目录 C不同的二叉搜索树 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、运行结果 五、考点分析 六、推荐资料 C不同的二叉搜索树 一、题目要求 1、编程实现 给定一个整数n,求以1、2、3、......、n为节点组成的二叉搜索树有多少种…

C++ 设计模式——工厂方法模式

工厂方法模式 工厂方法模式主要组成部分代码实现工厂方法模式模式的 UML 图工厂方法模式 UML 图解析优点和缺点适用场景 工厂方法模式 工厂方法模式是一种创建型设计模式,它通过定义一个接口用于创建对象,但由子类决定实例化哪个类。与简单工厂模式不同…

数据结构----栈

一丶概念 只能在一端进行插入和删除操作的线性表(又称为堆栈),进行插入和删除操作的一端称为栈顶,另一端称为栈底 二丶特点 先进后出 FILO first in last out 后进先出 LIFO last in first out 三丶顺序栈 逻辑结构&…

什么是制造业项目管理软件?适合制造企业的项目管理软件具备哪些特征

当前,我国的制造业呈现出稳步增长与风险并存的现象。经济构建以国内大循环为主体,国产替代的浪潮正在席卷国内制造业,越来越多的制造领域企业开始启动数字化变革来支撑企业的迅猛发展,进一步优化项目管理流程,促进研发…

Impala 与 Hive 的比较

Impala 与 Hive 的关系 impala是基于hive的大数据分析查询引擎,直接使用hive的元数据库metadata,意味着impala元数据都存储在hive的metastore当中,并且impala兼容hive的绝大多数sql语法。所以需要安装impala的话,必须先安装hive&…

开发小运维-常用Linux资源监控命令

文章目录 简介常用命令/proc/meminfo(内存)free(内存信息)top(进程动态)df (磁盘信息)du(磁盘信息)ps(进程状态)vmstat(内…

iOS的App启动详细过程(底层知识)

1.虚拟内存 & ASLR 在早期计算机中数据是直接通过物理地址访问的,这就造成了下面两个问题 1.内存不够用 2.数据安全问题 内存不够 ---> 虚拟内存 虚拟内存就是通过创建一张物理地址和虚拟地址的映射表来管理内存,提高了CPU利用率,…

IDEA:如何在idea中设置自动导包

这里使用的是idea2020版本,但是不同版本操作不会有较大的差别. 在Editer中展开General之后,选中Auto Import,最后勾选中Add unambiguous imports on the fly.

DMHS数据同步工具

DMHS数据同步工具 ​ 本章节主要介绍DM数据同步工具DMHS的使用,通过将oracle11g的数据同步到DM8的过程来理解DMHS的功能和作用。 安装前的准备 端口、服务信息 IP地址服务名称版本端口安装路径192.168.19.136OracleOracle11.0.21521/opt/oracle/DMHS源端dmhs_V3…

深入理解Faiss:高效向量检索的利器

近年来,随着人工智能和机器学习技术的飞速发展,向量检索技术变得越来越重要。无论是在推荐系统、图像搜索还是自然语言处理等领域,向量检索都扮演着至关重要的角色。而在众多向量检索库中,Faiss(Facebook AI Similarit…

基于Springboot 和Vue 的高校宿舍管理系统源码

网络上很多宿舍管理系统都不完整,大多数缺少数据库文件,所在使用极其不方便,由于本人程序员,根据代码,自己花时间不全了数据库文件,并且可以完美运行!!!!&…

C++:C/C++的内存管理

目录 C/C内存分布 C语言中动态内存管理方式 C内存管理方式 new/delete操作内置类型 new/delete操作自定义类型 operator new与operator delete函数 new和delete的实现原理 定位new表达式 常见问题 malloc/free和new/delete的区别 内存泄漏 C/C内存分布 我们先来看以…

【傅里叶分析】复数基础知识

【傅里叶分析】复数基础知识 复数复数的几何意义与点的对应与向量的对应 复数与极坐标辐角与辐角主值三角函数 参考文献 本文参考了网上的其他文章,已在文末参考文献中列出;如有侵权,请联系我删除。 复变函数是傅里叶分析的基础,而…

【原创公式】【完全二叉树】叶结点的计算【数据结构】

完全二叉树叶结点的计算 【铺垫】1叶结点即度为0的结点 2完全二叉树中度为1的结点只可能有0或1个 3完全二叉树的设叶结点仅可能出现在最后2层 设有完全二叉树T 【区分】第k层有a个叶结点≠第k层有a个结点 (1)第k层有a个叶结点:T的形态不唯一&…

【Linux操作系统】进程控制

目录 一、进程创建1.1 认识fork1.2 写时拷贝 二、进程终止2.1 进程退出2.2 函数退出2.3 exit 三、进程等待四、程序替换 一、进程创建 1.1 认识fork fork函数是系统调用接口,用来创建子进程的 根据进程的pid,可以看出父进程fork后分为父进程和子进程…

单片机原理及技术(六)—— 中断系统的工作原理

目录 一、AT89S51中断技术概述 二、AT89S51中断系统结构 2.1 中断请求源 2.2 中断请求标志寄存器 2.2.1 TCON 寄存器 2.2.2 SCON 寄存器 三、中断允许与中断优先级的控制 3.1 中断允许寄存器 IE 3.2 中断优先级寄存器 IP 四、响应中断请求的条件 五、外部中断的触发…

深入理解java web分层架构的高内聚低耦合

​ 在软件开发中,构建一个高效、可维护且可扩展的应用系统一直是开发者追求的目标。分层架构和依赖注入(IOC)是实现这一目标的重要策略。本文将深入探讨三层架构的高内聚特性、低耦合的设计原则,以及如何通过IOC(控制反…

FPGA开发——在线调试工具Signal Tap的使用

一、简介 在我们进行FPGA进行开发时通常都会经历代码编写,仿真,下板验证等过程。使用FPGA进行开发的小伙伴都知道,在代码编写时往往花费不了太长的时间,下板验证更是。在开发中占绝大部分时间的是仿真,有时候编写代码只…

基于Python的火车票售票系统/基于django的火车购票系统

摘 要 随着信息技术和网络技术的飞速发展,人类已进入全新信息化时代,传统管理技术已无法高效,便捷地管理信息。为了迎合时代需求,优化管理效率,各种各样的管理系统应运而生,各行各业相继进入信息管理时代&…

Ubuntu虚拟机服务器的搭建

01.VMware安装 略。 02.Ubuntu虚拟机安装 略。 03.配置Ubuntu虚拟机网络 参考视频: Ubutu虚拟机网络配置(桥接)https://www.bilibili.com/video/BV1bG411V72A/?spm_id_from333.999.0.0&vd_sourced1fd4bcc46805ab35cc8bbb5a8bf318f…