【数据结构与算法】二叉树的运用要点

news2024/12/28 14:25:17

目录

 一,二叉树的结构深入认识

 二,二叉树的遍历

 三,二叉树的基本运算

3-1,计算二叉树的大小

3-2,统计二叉树叶子结点个数

3-3,计算第k层的节点个数

3-4,查找指定值的结点


一,二叉树的结构深入认识

        二叉树是不可随机访问的,二叉树的结构是通过双亲结点与孩子结点之间的连接进行遍历访问,因此,二叉树的结构是用链式结构来存储的。如下:

二叉树的结构

#include <stdio.h>
#include <stdlib.h>
typedef struct Tree {
    int val;//数据
    struct Tree* leftchild;//左孩子结点
    struct Tree* rightchild;//右孩子结点
}Tree;

        要说明的是,学习二叉树的结构不跟栈和队列之类的一样用于增删查改,二叉树没有这些操作,二叉树的运用比较复杂,下面会依次讲解。


 二,二叉树的遍历

        二叉树的遍历有:前序遍历,中序遍历,后序遍历,层序。通常,在这些遍历算法中除了层序遍历外,其它的遍历都要运用递归结构。

        1,前序遍历(也称先序遍历)——访问根结点的操作发生在遍历其左右子树之前,即遍历的

顺序为:根,左子树,右子树。

        2,中序遍历——访问根结点的操作发生在遍历其左右子树之间。即遍历的顺序为:左子树,根,右子树。

        3,后序遍历——访问根结点的操作发生在遍历其左右子树之后。即遍历的顺序为:左子树,右子树,根。

        4,层序遍历——从左到右一层一层的遍历,此遍历非常简单,就如从开始到结尾的遍历顺序表一样。

        注意:这里要说明的是,以上的遍历除了层序遍历外,其它的遍历都是递归式的遍历,可不是单纯普通式按照以上顺序循环式的遍历,正确的遍历如下图:

        说明一下,在二叉树的遍历中,前,中,后遍历的思路基本相同,而层序遍历一般要借助队的结构来实现,本篇文章先不做介绍,后文深入运用时会详细说明。前中后的三种遍历代码如下:

//前序遍历
void FrontOrder(struct Tree* root) {
	//当递归到空结点时退出
	if (root == NULL) {
		return;
	}
	//输出
	fprintf(stdout, "%d ", root->val);
	//递归左子树
	FrontOrder(root->leftchild);
	//递归右子树
	FrontOrder(root->rightchild);
}
//中序遍历
void MiddleOrder(struct Tree* root) {
	//当递归到空结点时退出
	if (root == NULL) {
		return;
	}
	//递归左子树
	MiddleOrder(root->leftchild);
	//输出
	fprintf(stdout, "%d ", root->val);
	//递归右子树
	MiddleOrder(root->rightchild);
}
//后序遍历
void RearOrder(struct Tree* root) {
	//当递归到空结点时退出
	if (root == NULL) {
		return;
	}
	//递归左子树
	RearOrder(root->val);
	//递归右子树
	RearOrder(root->val);
	//输出
	fprintf(stdout, "%d ", root->val);
}

解析:

        递归本身有点不好理解,而上面的递归遍历中,当进行递归遍历时,可看做从此函数的根结点开始不断往下面的左右孩子结点遍历,当递归到根结点为空时结束下一次递归,并返回到此结点的双亲结点,可以说二叉树的递归遍历是不断往下层进行的,只有当遍历到最下层或下层的空结点时才返回,也就是递归遍历是从最后开始,然后不断往回返,直到返回到二叉结构的根节点才结束整个递归程序。

补充:

        函数的递归其实跟我们学习的栈结构一样,递归入函数(即进入函数栈帧)相当于入栈,出函数(即函数栈帧的销毁)相当于出栈。

学习建议:

        遍历思想是学习二叉树的基本,很多二叉树的算法思想都要在此递归遍历的思想上进行开阔的,因此,如果这中递归思路不明白,一定要先理清思路,不建议先往后面看。


三,二叉树的基本运算

        在讲解这一部分内容前,我们要先明白递归中设置局部变量的用法。

二叉树递归设置局部变量的注意:

        在二叉树计数中,我们难免要设定局部变量,而在进行递归返回中可能有些人说函数的返回值会覆盖之前的值,不明白为什么覆盖后的值就是我们想要统计的数值。其实这原理很简单,在讲解二叉树遍历的解析说过,二叉树的递归遍历是不断往下层进行的,只有当遍历到最下层时才返回,也就是递归程序是从最后开始,不断往前返回,当我们递归遍历二叉树时,满足计数的条件会不断往上层返,这时计数的值相当于以此函数中root为根结点的子树的以下计数值,也就是我们要统计的数值。

3-1,计算二叉树的大小

        计算二叉树的大小说白了就是确定有几个不为空的结点,此算法比较简单,我们可直接遍历整个二叉结构不断加一来实现。代码如下:

//统计二叉树中叶子结点的个数
int TreeSize(Tree* root) {
	//当为空结点时说明此时为0
	if (root == NULL) {
		return 0;
	}
	//不断递归遍历,遍历一个结点加1。
	return TreeSize(root->leftchild) + TreeSize(root->rightchild) + 1;
}

        以上代码虽然省力,但可能对于部分人来说比较难理解,展开后的代码如下:

int TreeSize(Tree* root) {
	if (root == NULL) {
		return 0;
	}
	//遍历左子树的结点个数
	int leftsize = TreeSize(root->leftchild);
	//遍历右子树的结点个数
	int rightsize = TreeSize(root->rightchild);
	//返回以此结点为根结点的二叉树结点个数,此二叉树是子二叉树
	return leftsize + rightsize + 1;
}

        对于当初学者笔者建议用展开后的代码,对递归了解比较深入后再用合成版代码。

3-2,统计二叉树叶子结点个数

        此算法与计算二叉树的结点个数方法相似,不同的是,当进行递归遍历时,我们可利用叶子结点的左右孩子都为空的特点来计数,当递归遍历时满足这一特点进行计数,不满足进行递归遍历,代码如下:

int LeavesNodeSize(Tree* root) {
	if (root == NULL) {
		return 0;
	}
    //当此结点是叶子结点时,计数1
	if (root->leftchild == NULL && root->rightchild == NULL) {
		return 1;
	}
	//左孩子结点的叶子结点数
	int count1 = LeavesNodeSize(root->leftchild);
	//右孩子结点的叶子结点数
	int count2 = LeavesNodeSize(root->rightchild);
    //返回当前以root为根结点的子二叉树的叶子结点总个数
	return count1 + count2;
}

        当我们明白以上原理后可进行简化算法,跟之前一样,先理清以上思路再进行简化。如下:

int LeavesNodeSize(Tree* root) {
	if (root == NULL) {
		return 0;
	}
	if (root->leftchild == NULL && root->rightchild == NULL) {
		return 1;
	}
	return LeavesNodeSize(root->leftchild) + LeavesNodeSize(root->rightchild);
}
3-3,计算第k层的节点个数

        记录第k层的二叉树结点也是同理,在递归遍历过程中,往下层递归一次将k减1,当k==1时就递归到了第k层,也就是在此时开始计数,而函数返回值将返回以此函数中的root为根结点的子二叉树的第k层结点数。

int NodeCount(Tree* root, int k) {
	if (root == NULL) {
		return 0;
	}
	//当k == 1 时此时遍历到了第k层,此时计数
	if (k == 1) {
		return 1;
	}
	//以此函数的root为根结点以下的子二叉树第k层的左孩子结点数
	int leftchild = NodeCount(root->leftchild, k - 1);
	//以此函数的root为根结点以下的子二叉树第k层的右孩子结点数
	int rightchild = NodeCount(root->rightchild, k - 1);
	//返回以此函数的root为根结点以下的子二叉树第k层的结点数
	return leftchild + rightchild;
}

        算法合并简化后如下:

int NodeCount(Tree* root, int k) {
	if (root == NULL) {
		return 0;
	}
	if (k == 1) {
		return 1;
	}
	return NodeCount(root->leftchild, k - 1) + NodeCount(root->rightchild, k - 1);
}
3-4,查找指定值的结点

        此算法的坑比较多,首先我们要考虑的是当遍历到要查找的结点时如何停止遍历,并最终返回该结点。要知道一点,当我们要查找的结点在中间时,直接返回是上一次的递归函数,因此,我们要做的是让查找的指定结点不断返回,直到递归结束。

        算法详细步骤如下:

Tree* FoundNode(Tree* root, int x) {
	if (root == NULL) {
		return NULL;
	}
	if (root->val == x) {
		return root;
	}
	Tree* leftchild = FoundNode(root->leftchild, x);
	//当左孩子是我们要找的结点时就不往下继续遍历了,直接返回此结点
	if (leftchild != NULL && leftchild->val == x) {
		return leftchild;
	}
	Tree* rightchild = FoundNode(root->rightchild, x);
	//当左孩子是我们要找的结点时就不往下继续遍历了,直接返回此结点
	if (rightchild != NULL && rightchild->val == x) {
		return rightchild;
	}
	//当查找不到返回NULL
	return NULL;
}

        算法的化简代码如下:

Tree* FoundNode(Tree* root, int x) {
	if (root == NULL) {
		return NULL;
	}
	if (root->val == x) {
		return root;
	}
	Tree* leftchild = FoundNode(root->leftchild, x);
	//当左孩子是我们要找的结点时就不往下继续遍历了,直接返回此结点
	if (leftchild != NULL && leftchild->val == x) {
		return leftchild;
	}
	//遍历右结点,当右孩子是我们要找的结点时返回此结点
	return FoundNode(root->rightchild, x);
}

总结:学习到这里学者们明显感到难度增大了,不过也不用担心,只要我们多多思考并加之练习,其实逻辑也不是那么难,上面的算法程序笔者之所以将步骤展开和步骤合并一并写入,就是为了让大家多多练习以上的思路,其实逻辑也都是一样的,只是为了强化思维罢了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

循环神经网络(Recurrent Neural Network)

1. 为什么需要循环神经网络 RNN 上图是一幅全连接神经网络图&#xff0c;我们可以看到输入层-隐藏层-输出层&#xff0c;他们每一层之间是相互独立地&#xff0c;(框框里面代表同一层)&#xff0c;每一次输入生成一个节点&#xff0c;同一层中每个节点之间又相互独立的话&#…

【jvm】虚拟机栈之操作数栈

目录 一、说明二、图解2.1 代码示例2.2 javap操作 三、图示3.1 bipush 153.2 istore_13.3 bipush 83.4 istore_23.5 iload_13.6 iload_23.7 iadd3.8 istore_33.9 return结束 四、附加 一、说明 1.Operand Stack 2.栈可以使用数组或链表来实现 3.每一个独立的栈帧包含一个后进先…

Mac 开机提示Google LLC 注册 无法登录进入系统

Google LLC 会在电脑启动时提示如下弹窗&#xff0c;并要求登录谷歌账户进行验证 此时很明显没有用来进行验证的账号&#xff0c;所以需要关掉这个验证程序 从日志里面可以看到LLC启动了一个Tiny.app的程序 只需要想办法把这个程序删掉即可 关机 按住 Command R 开机 进入R…

【MySQL架构篇】MySQL字符集、大小写规范及默认数据库

文章目录 1. 字符集与字符集比较规则2. 大小写规范3. 默认数据库4. 与文件系统相关 1. 字符集与字符集比较规则 MySQL有4个级别的字符集和比较规则&#xff0c;分别是 服务器级别数据库级别表级别列级别 当创建对应表或列未指定字符集时&#xff0c;默认会取其上一级别的字符…

JavaScript-1-菜鸟教程

将内容写到 HTML 文档中 - - - document.write() <script>// 括号里的内容要有引号document.write("<h1>这是一个标题</h1>");document.write(<div class"box">hello world</div>);</script><style>.box{width…

如何部署lvs负载均衡集群 DR模式

Lvs _DR 模式 也是最常见的lv负载方式 DR DIRECT ROUTING 直接路由模式 DR模式工作过程 1 .客户端请求vip 2、LVS的调度器接受请求之后&#xff0c;根据算法选择一台后端的真实服务器&#xff0c; 请求转发到后端RS,请求的报 文的目的MAC地址&#xff0c;修改成后端真实服务器的…

同为科技(TOWE)大功率带机械联锁工业插头插座箱

所谓工业机械联锁开关插座&#xff0c;是一种工业用途插座&#xff0c;带有一个旋钮开关&#xff0c;通过旋钮开关可以控制电源的通断。其特点是具有联动锁定机构&#xff0c;当旋钮开关断开操作后&#xff0c;联动锁定机构会自动撤销限位&#xff0c;使插头能够插入或拔出。当…

RabbitMQ基础篇 笔记

RabbitMQ 余额支付 同步调用 一步一步的来&#xff0c;支付业务写完后&#xff0c;如果之后加需求&#xff0c;还需要增加代码&#xff0c;不符合开闭原则。 性能上也有问题&#xff0c;openfeign是同步调用&#xff0c;性能太差。 同步调用耦合太多。 同步的优势是可以立…

Node编写用户登录接口

目录 前言 服务器 编写登录接口API 使用sql语句查询数据库中是否有该用户 判断密码是否正确 生成JWT的Token字符串 配置解析token的中间件 配置捕获错误中间件 完整的登录接口代码 前言 本文介绍如何使用node编写登录接口以及解密生成token&#xff0c;如何编写注册接…

侯捷C++面向对象程序设计笔记(上)-Object Based(基于对象)部分

基于对象就是对于单一class的设计。 对于有指针的&#xff1a;complex.h complex-test.cpp 对于没有指针的&#xff1a; string.h string-test.cpp https://blog.csdn.net/ncepu_Chen/article/details/113843775?spm1001.2014.3001.5501#commentBox 没有指针成员——以复数co…

【单片机学习笔记】Windows+Vscode+STM32F4+freeRTOS+FatFs gcc环境搭建

为摒弃在接受keil邮件&#xff0c;研究了下gun编译&#xff0c;以STM32F407为例&#xff0c;简单记录 1. 软件包准备 Git 选择对应版本直接安装即可https://git-scm.com/download/winmakegcc ​ 1&#xff09;将上述软件包放置于C盘根目录 2&#xff09;添加环境变量 3&am…

分类预测 | MATLAB实现SSA-CNN-BiLSTM-Attention数据分类预测(SE注意力机制)

分类预测 | MATLAB实现SSA-CNN-BiLSTM-Attention数据分类预测&#xff08;SE注意力机制&#xff09; 目录 分类预测 | MATLAB实现SSA-CNN-BiLSTM-Attention数据分类预测&#xff08;SE注意力机制&#xff09;分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.MAT…

Android View拖拽startDragAndDrop,Kotlin

Android View拖拽startDragAndDrop&#xff0c;Kotlin import android.os.Bundle import android.util.Log import android.view.DragEvent import android.view.View import android.view.View.OnDragListener import android.view.View.OnLongClickListener import android.w…

Spring中方法拦截器

一、MethodInterceptor 在动态代理中要想添加一个额外功能&#xff0c;只要去实现MethodBeforeAdvice这个接口就行了&#xff0c;但是实现了这个接口的额外功能只能运行在目标类执行之前&#xff0c;如果是想在目标类执行之后呢&#xff1f;那这个需求就完成不了&#xff0c;所…

SpringBoot Lombok的使用

目录 下载Lombok插件 Lombok的用法 获取日志对象 生成get,set方法 Lombok框架的实现原理 Lombok的常用注解 下载Lombok插件 要使用Lombok首先要确保idea安装了lombok插件 在项目中添加 lombok依赖 在<dependency>里右键生成点击edit starters 插件(没有就下载,可…

行业模型应该如何去拆解?

行业模型应该如何去拆解&#xff1f; 拆解行业模型是一个复杂的过程&#xff0c;涉及对整个行业的深入分析和理解。下面是一些步骤和方法&#xff0c;可以帮助你系统地拆解行业模型&#xff1a; 1. 确定行业范围 定义行业&#xff1a;明确你要分析的行业是什么&#xff0c;包括…

寻找二叉树一个节点的后继节点

后继节点&#xff1a;中序遍历的后一个节点 普通二叉树&#xff1a;中序遍历得到一个list&#xff0c;时间复杂度O(n) 本题的二叉树&#xff1a;有父节点的指针&#xff0c;后继节点与原节点的距离为1&#xff0c;因此可以直接通过父节点找到下一个节点 优化&#xff1a;节点…

出差学小白知识No6:LD_PRELOAD变量路径不对找不到库文件

交叉编译的时候出现以下问题&#xff0c;显示LD_PRELOAD变量找不到路劲 首先先查看一下LD_PRELOAD的路径&#xff1a;echo $LD_PRELOAD 如果输出一大串&#xff0c;那么先进行清空&#xff1a;unset LD_PRELOAD 重新给LD_PRELOAD进行赋值他的路径和库文件&#xff1a; expor…

亚马逊、eBay、wish、Lazada、shoppe和mercari如何降低测评成本提高测评效率?

测评&#xff08;补单&#xff09;是跨境卖家在如亚马逊、Lazada、Shopee、wish、eBay、速卖通、阿里国际、沃尔玛、newegg、mercari等平台运营中不可或缺的需求。近年来&#xff0c;测评技巧在跨境卖家圈中越来越活跃。那么为什么跨境测评对于亚马逊卖家来说如此重要呢&#x…

【Leetcode】 213. 打家劫舍 II ?

你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋&#xff0c;每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈&#xff0c;这意味着第一个房屋和最后一个房屋是紧挨着的。同时&#xff0c;相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚…