【嵌入式开发之数据结构】树的基本概念、逻辑结构和四种常用的遍历算法及实现

news2024/11/27 22:28:08

树(Tree)的定义及基本概念

树的定义

树(Tree)是n(n\geqslant 0)个结点的有限集合T,它满足两个条件:

  • 有且仅有一个特定的称为根(Root)的节点;
  • 其余的节点分为m(m\geqslant 0)个互不相交的有限合集T_{1},T_{2},...,T_{m},其中每一个集合又是一棵树,并称为其根的子树。

表示方法:树形表示法,目录表示法。 

树的基本概念

一个节点的子树的个数称为该节点的度数

一颗树的度数是指该树中节点的最大度数。

度数为零的节点称为树叶或终端节点

度数不为零的节点称为分支节点

一个节点系列k_{1},k_{2},...,k_{i},k_{i+1},...,k_{j},并满足k_{i}k_{i+1}的父节点,就称为一条从k_{1}k_{j}路径,路径的长度为j-1,即路径中的边数

路径中前面的节点是后面节点的祖先,后面节点是前面节点的子孙。 

节点的层数等于父节点的层数加一,根节点的层数定义为一。树中节点层数的最大值称为该树的高度或深度

若树中每个节点的各个子树的排列为从左到右,不能交换,即兄弟之间是有序的,则该树称为有序树

m(m\geqslant 0)棵互不相交的树的集合称为森林

树去掉根节点就成为森林,森林加上一个新的根节点就成为树。

树的逻辑结构

树中任何节点都可以有零个或多个直接后继节点(子节点),但至多只有一个直接前趋节点(父节点),根节点没有前趋节点,叶节点没有后继节点。

二叉树

二叉树的逻辑结构

 二叉树是n(n\geqslant0)个节点的有限集合,或者是空集(n=0) ,或者是由一个根节点以及两棵互不相交的、分别称为左子树右子树的二叉树组成 严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。

二叉树的性质

二叉树第i(i\geqslant 1)层上的节点最多为2^{k-1}-1个。

深度为k(k\geqslant 1)的二叉树最多有2^{k}-1个节点。

满二叉树

深度为k(k\geqslant 1)时,有2^{k}-1个节点的二叉树。

完全二叉树

只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。

具有n个节点的完全二叉树的深度为

(log2n)+1log2(n+1)

二叉树的存储结构

二叉树的顺序存储

完全二叉树节点的编号方法是从上到下,从左到右,根节点为1号节点,设完全二叉树的节点数为n,某节点编号为i

  • i> 1(不是根节点)时,有父节点,其编号为\frac{i}{2}
  • 2\times i\leqslant n时,有左孩子,其编号为2\times i,否则没有左孩子,本身是叶节点;
  • 2\times i+1\leqslant n时,有右孩子,其编号为2 \times i+1 ,否则没有右孩子;
  • i为奇数且不为1时,有左兄弟,其编号为i-1,否则没有左兄弟;
  • i为偶数且小于n时,有右兄弟,其编号为i+1,否则没有右兄弟。

n个节点的完全二叉树可以用有n+1个元素的数组进行顺序存储,节点号和数组下标一一对应,下标为零的元素不用

利用以上特性,可以从下标获得节点的逻辑关系。不完全二叉树通过添加虚节点构成完全二叉树,然后用数组存储,这要浪费一些存储空间。

二叉树的链式存储

定义一个二叉树节点类型结构体bitree,每个结点包括三个域:

  • 数据域data:存放每个节点的数据;
  • 左孩子指针域left:存放指向左孩子的指针,如果没有左孩子,则为NULL;
  • 右孩子指针域right:存放指向右孩子的指针,如果没有右孩子,则为NULL。

在头文件tree.h中定义二叉树结构体: 

typedef char data_t;

typedef struct node_t {
	data_t data;//二叉树节点数据域
	struct node_t *left;//二叉树节点左孩子指针域
	struct node_t *right;//二叉树节点右孩子指针域
}bitree;//二叉树节点类型别名

二叉树的四种基本遍历算法

遍历的含义

遍历是指沿某条搜索路径周游二叉树,对树中的每一个节点访问一次且仅访问一次。

二叉树是非线性结构,每个结点有两个后继,则存在如何遍历即按什么样的搜索路径进行遍历的问题。

由于二叉树的递归性质,遍历算法也是递归的

对于下面这棵树的遍历,可以通过什么算法实现呢?

遍历算法:先序遍历

先序遍历,是指先访问树根,再访问左子树,最后访问右子树

先序遍历算法:

若二叉树为空树,则空操作,否则:

  • 访问根节点:
  • 先序遍历左子树;
  • 先序遍历右子树。

这是一个递归算法,不断遍历左子树和右子树,直到左子树和右子树均为NULL,结束遍历,依次所访问的节点,即为遍历结果。

为了实现算法,在定义了上述bitree结构体后,声明一个数据类型为bitree的指针函数tree_create()和先序遍历函数preorder();

在tree.h文件中声明树的创建函数和先序遍历函数:
bitree *tree_create();//创建二叉树函数
void preorder(bitree *r);//先序遍历函数
在tree.c中实现树的创建函数:tree_create()
#include <stdio.h>
#include <stdlib.h>
#include "linkqueue.h"//队列头文件,在层次遍历过程中需要用到队列

bitree *tree_create() {
	data_t ch;
	bitree *r;

	scanf("%c", &ch);//获取用户输入的字符
	if (ch == '#') {
		return NULL;//如果获取到的字符是‘#’则返回NULL
	}
    
    //给二叉树的节点分配内存空间,如果分配失败,则返回NULL
	if ((r = (bitree *)malloc(sizeof(bitree))) == NULL) {
		printf("malloc failed.\n");
		return NULL;
	}

    //将获取到的字符存入二叉树节点的data域中,并递归创建左子树和右子树
	r->data = ch;
	r->left = tree_create();
	r->right = tree_create();

	return r;
}
在tree.c中实现先序遍历函数:preorder()
void preorder(bitree *r) {
    //传入参数验证,同时也是退出递归的条件
	if (r == NULL) {
		return;
	}

    //打印访问到的节点存储的值
	printf("%c", r->data);
 
    //递归遍历被访问的节点的左子树和右子树
	preorder(r->left);
	preorder(r->right);
}

先序遍历代码看似简单,但其中蕴含一个非常重要的思想,就是递归:

递归是指,原问题的解总是依赖于子问题的解决,在二叉树的先序遍历过程中,要想遍历完整个树,在访问了节点数据data之后,总是依赖于左子树和右子树的是否遍历完成,而左子树和右子树是否遍历完成,同样依赖于它们的左子树和右子树是否遍历完成,以此类推,直到传入preorder()函数的参数为NULL,再返回,完成遍历。

使用递归算法,一定要明确退出递归的条件,否则构成死循环,在这个算法中,退出递归的条件就是r为NULL。

遍历算法:中序遍历

中序遍历,是指先访问左子树,再访问树根,最后访问右子树

若二叉树为空树,则空操作,否则:

  • 中序遍历左子树;
  • 访问根节点;
  • 中序遍历右子树。
在tree.h文件中声明中序遍历函数inorder():
void inorder(bitree *r);//中序遍历函数
在tree.c文件中实现中序遍历函数inorder(): 
void inorder(bitree *r) {
    //传入参数验证,如果为NULL,返回,也是递归终止条件
	if (r == NULL) {
		return;
	}
	
	inorder(r->left);//递归遍历左子树
	printf("%c", r->data);//访问并打印节点data值
	inorder(r->right);//递归遍历右子树
}

在这里同样用到递归,退出条件依然是r为NULL,只是是先进行左子树的递归,再打印节点的值,再遍历右子树。 

遍历算法:后序遍历

后序遍历,是指先访问左子树,再访问右子树,最后访问树根

若二叉树为空树,则空操作,否则:

  • 后序遍历左子树;
  • 后序遍历右子树。
  • 访问根节点。
在tree.h文件中声明后序遍历函数postorder():
void postorder(bitree *r);//声明后序遍历函数
 在tree.c文件中实现后序遍历函数postorder(): 
void postorder(bitree *r) {
    //传入参数验证,如r为NULL,返回,递归终止条件
	if (r == NULL) {
		return;
	}
	
	postorder(r->left);//递归遍历左子树
	postorder(r->right);//递归遍历右子树
	printf("%c", r->data);//访问节点data值并打印
}

遍历算法:层次遍历

二叉树的层次遍历,是指是从左往右依次访问每层结点的过程。对于顺序表存储的二叉树,层次遍历较容易实现,只需逐层访问存储的结点。对于链表存储的二叉树,可利用队列实现层次遍历。

链表存储的二叉树层次遍历算法

  • 根结点入队,出队并访问;
  • 将其左右孩子入队,出队并访问;
  • 重复此过程直至队列为空。
图1

 在tree.h文件中声明后序遍历函数layerorder():
void layerorder(bitree *r);//层次遍历函数声明
 在linkqueue.h文件中定义节点和队列结构体,并声明相关函数:
#include "tree.h"
typedef bitree * datatype;//给bitree起别名datatype

//定义node结构体,并起别名listnode和linklist
typedef struct node {
	datatype data;
	struct node *next;
}listnode, *linklist;

//定义队列结构体,并起别名linkqueue
typedef struct {
	linklist front;//队头
	linklist rear;//队尾
}linkqueue;

linkqueue *queue_create();//声明创建队列函数
int enqueue(linkqueue *lq, datatype x);//声明入队函数
datatype dequeue(linkqueue *lq);//声明出队函数
int queue_empty(linkqueue *lq);//声明队列是否为空判断函数
int queue_clear(linkqueue *lq);//声明队列清空函数
linkqueue *queue_free(linkqueue *lq);//声明释放队列函数
 在linkqueue.c文件中实现队列的相关函数:
 实现队列的创建函数queue_create():
#include <stdio.h>
#include <stdlib.h>
//#include "tree.h"
#include "linkqueue.h"

linkqueue *queue_create() {
	linkqueue *lq;

	if ((lq = (linkqueue *)malloc(sizeof(linkqueue))) == NULL) {
		printf("malloc linkqueue failed.\n");
		return NULL;
	}

	lq->front = lq->rear = (linklist)malloc(sizeof(listnode));
	if (lq->front == NULL) {
		printf("malloc front failed.\n");
		return NULL;
	}

	lq->front->data = 0;
	lq->front->next = NULL;

	return lq;
}
 实现入队函数enqueue():
int enqueue(linkqueue *lq, datatype x) {
	linklist p;

	if (lq == NULL) {
		printf("lq is NULL.\n");
		return -1;
	}

	p = (linklist)malloc(sizeof(listnode));
	if (p == NULL) {
		printf("malloc front failed.\n");
		return -1;
	}
	p->data = x;
	p->next = NULL;

	lq->rear->next = p;
	lq->rear = p;

	return 0;
}
  实现出队函数dequeue():
datatype dequeue(linkqueue *lq) {
	linklist p;

	if (lq == NULL) {
		printf("lq is NULL.\n");
		return NULL;
	}

	p = lq->front;
	lq->front = p->next;
	free(p);
	p = NULL;

	return (lq->front->data);
}
 实现队列判断函数queue_empty():
int queue_empty(linkqueue *lq) {
	if (lq == NULL) {
		printf("lq is NULL.\n");
		return -1;
	}

	return (lq->front == lq->rear ? 1: 0);
}
 实现队列清空函数queue_clear():
int queue_clear(linkqueue *lq) {
	linklist p;

	if (lq == NULL) {
		printf("lq is NULL.\n");
		return -1;
	}

	while (lq->front->next) {
		p = lq->front;
		lq->front = p->next;
		//printf("clear free: %d\n", p->data);
		free(p);
		p = NULL;
	}
	
	return 0;

}
 实现队列释放函数queue_free():
linkqueue *queue_free(linkqueue *lq) {
	linklist p;

	if (lq == NULL) {
		printf("lq is NULL.\n");
		return NULL;
	}

	while (lq->front) {
		p = lq->front;
		lq->front = p->next;
		//printf("free: %d\n", p->data);
		free(p);
	}
	
	free(lq);
	lq = NULL;

	return 0;
}
  在tree.c文件中实现层次遍历函数layerorder(): 
void layerorder(bitree *r) {
    //声明一个队列lq
	linkqueue *lq;
	
    //创建队列lq,入创建失败直接返回
	if ((lq = queue_create()) == NULL)
		return;

    //验证传入参数r
	if (r == NULL) 
		return;

    //打印访问到的节点数值,并让该节点入队
	printf("%c", r->data);
	enqueue(lq, r);

    //当队列不为空,不断执行左孩子、右孩子的访问,入队和出队
	while (!queue_empty(lq)) {
		r = dequeue(lq);
		if (r->left != NULL) {
			printf("%c", r->left->data);
			enqueue(lq, r->left);
		}

		if (r->right != NULL) {
			printf("%c", r->right->data);
			enqueue(lq, r->right);
		}
	}
    
    //确认清空队列并释放内存
    queue_clear(lq);
    queue_free(lq);
}

遍历算法测试:test.c文件

#include <stdio.h>
#include <stdlib.h>
#include "tree.h"

int main(int argc, const char *argv[])
{
	bitree *r;

	if((r = tree_create()) == NULL) 
		return -1;

    //调用先序遍历函数
	preorder(r);
	puts("");

    //调用中序遍历函数
	inorder(r);
	puts("");

    //调用后序遍历函数
	postorder(r);
	puts("");

    //调用层次遍历函数
	layerorder(r);
	puts("");

	return 0;
}

运行结果

将树图1左孩子、有孩子缺失的部分用‘#’填充,运行程序后,输入A#BCEH###FI##J##D#GK###,得到以下结果:

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

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

相关文章

【无重叠空间】python刷题记录

润到贪心篇。 class Solution:def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:#十行贪心大神if not intervals:return 0#按照第第二个元素进行排序&#xff0c;贪心思想&#xff0c;参考活动安排都是以结束时间进行排序的intervals.sort(keylambda x:…

分离式网络变压器的集成化设计替代传统网络变压器(网络隔离滤波器)尝试

Hqst盈盛&#xff08;华强盛&#xff09;电子导读&#xff1a;今天分享的是应用了分离式网络变压器设计的的新型网络变压器&#xff08;网络隔离变压器&#xff09; 今天我们一起来看这款新型网络变压器&#xff0c;它就是应用分离式网络变压器集成到电路板上&#xff0c;加上外…

git协同开发与冲突解决

协同开发流程 基本的使用方法 # 1 多人开发同一个项目-张三&#xff1a;本地 版本库-李四&#xff1a;本地 版本库-我&#xff1a; 本地 版本库----远程仓库&#xff1a;本地版本库内容--》推送到远程仓库-----# 2 演示&#xff08;我是项目管理员&#xff09;-1 增加几个开发…

(二刷)代码随想录第21天|236. 二叉树的最近公共祖先

236. 二叉树的最近公共祖先 递归三部曲&#xff1a; 1、确定参数和返回值&#xff1a; class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {} 2、确定中止条件&#xff1a; if (root null || root p || root q) { // 递归结…

lua 游戏架构 之 SceneLoad场景加载之 SceneBase (三)

谢谢大家关注一下啊我的微信 框架上 设计一个 基类 SceneLoad&#xff1a;BaseSceneLoad lua 游戏架构 之 SceneLoad场景加载&#xff08;一&#xff09;-CSDN博客https://blog.csdn.net/heyuchang666/article/details/140560014?spm1001.2014.3001.5501 设计多个 场景类&a…

无需业务改造,一套数据库满足 OLTP 和 OLAP,GaiaDB 发布并行查询能力

在企业中通常存在两类数据处理场景&#xff0c;一类是在线事务处理场景&#xff08;OLTP&#xff09;&#xff0c;例如交易系统&#xff0c;另一类是在线分析处理场景&#xff08;OLAP&#xff09;&#xff0c;例如业务报表。 OLTP 数据库擅长处理数据的增、删、改&#xff0c…

STM32测测速---编码电机读取速度的计算

1、首先先了解一下计算的公式 速度计算&#xff1a; 轮胎每转一圈的脉冲数取决于编码器的分辨率&#xff0c;可由下面公式进行计算&#xff1a; PPR是电机的线数 以GA25-370电机为例。 图片来源&#xff1a;第四节&#xff1a;STM32定时器&#xff08;4.JGA25-370霍尔编码器…

规范:前端工程代码规范

准备工作 前端工程化介绍 前端工程化是指将前端开发流程标准化、自动化和模块化的过程。以下是一些常见的步骤或配置&#xff1a; 代码版本控制&#xff1a;使用 Git 等版本控制系统来管理代码的变更和协作。 依赖管理&#xff1a;使用 npm 或 yarn 等包管理工具来管理项目依…

园区道路车辆智能管控视频解决方案,打造安全畅通的园区交通环境

一、背景需求分析 随着企业园区的快速发展和扩张&#xff0c;道路车辆管理成为了保障园区秩序、提升运营效率及确保员工安全的重要任务。针对这一需求&#xff0c;旭帆科技TSINGSEE青犀提出了一种企业园区道路车辆管控的解决方案&#xff0c;通过整合视频监控、智能识别等技术…

6.2 基础功能介绍

本节重点介绍 : 数据源操作新增一个数据源dashboard操作folder操作alerting操作用户和组操作 本节重点总结 : 数据源操作dashboard操作folder操作alerting操作用户和组操作

linux时间服务器——软件安装,配置时间服务器客户端 ,配置时间服务器服务端

1 、软件安装 # 设置当前时区 [rootlocalhost ~] # timedatectl set-timezone Asia/Shanghai [rootlocalhost ~] # yum install -y chrony [rootlocalhost ~] # systemctl enable --now chronyd [rootserver1 ~] # cat /etc/chrony.conf # 设置时间服务器的服务端名字 s…

ceph log内容解析

log内容构造 如osd的一条log 分别表示 时间戳 线程id 日志等级 子模块 内容实体 剖析源码实现 每条log都是由一个Entry构成 定义在src/log/entry.h中 Entry(short pr, short sub) :m_stamp(clock().now()), // 打印日志时的时间戳m_thread(pthread_self()), // 打印日志的线…

MySQL进阶_11.主从复制

文章目录 一、主从复制概述1.1、如何提升数据库并发能力1.2、主从复制的作用 二、主从复制的原理2.1、原理剖析2.2、原理剖析 一、主从复制概述 1.1、如何提升数据库并发能力 应用对数据库而言都是“ 读多写少 ”&#xff0c;也就说对数据库读取数据的压力比较大&#xff0c;有…

【Android】使用视图绑定ViewBinding来代替findViewById

文章目录 介绍作用用法开启ViewBinding功能自动生成绑定类在Activity中使用访问视图控件 区别 介绍 ViewBinding 是 Android 开发中的一个功能&#xff0c;它简化了访问视图的过程&#xff0c;避免了使用 findViewById 的繁琐步骤。它通过生成与布局文件相对应的绑定类&#xf…

【python基础】组合数据类型:元组、列表、集合、映射

文章目录 一. 序列类型1. 元组类型2. 列表类型&#xff08;list&#xff09;2.1. 列表创建2.2 列表操作2.3. 列表元素遍历 ing元素列表求平均值删除散的倍数 二. 集合类型&#xff08;set&#xff09;三. 映射类型&#xff08;map&#xff09;1. 字典创建2. 字典操作3. 字典遍历…

OV7670寄存器读出0x00或0xFF

文章目录 问题描述原因分析解决方案 问题描述 OV7670的输出图像异常&#xff0c;怀疑寄存器没有正确配置&#xff0c;在SignalTap中观察到SIO_D在读出阶段一直为高或低 寄存器读出0x00 寄存器读出0xFF 原因分析 在确保电源、时钟和读写时序没有问题的情况下&#xff0c;有…

mysql的索引、事务和存储引擎

目录 索引 索引的概念 索引的作用 作用 索引的副作用 创建索引 创建索引的原则和依据 索引的类型 创建索引 查看索引 删除索引 drop 主键索引 普通索引 添加普通索引 唯一索引 添加唯一索引 组合索引 添加组合索引 查询组合索引 全文索引 添加全文索引 …

K12智慧校园智能化解决方案

1. 项目背景 “十三五”期间&#xff0c;教育信息化工作旨在为教育改革发展提供动力与手段&#xff0c;目标是到2020年建成与国家教育现代化发展目标相适应的教育信息化体系。 2. 建设需求 智慧校园系统框架解析与建设目标分析&#xff0c;旨在实现教育信息化目标任务。 3.…

利用双端队列 实现二叉树的非递归的中序遍历

双端队列&#xff1a;双向队列&#xff1a;支持插入删除元素的线性集合。 java官方文档推荐用deque实现栈&#xff08;stack&#xff09;。 pop(): 弹出栈中元素&#xff0c;也就是返回并移除队头元素&#xff0c;等价于removeFirst()&#xff0c;如果队列无元素&#xff0c;则…

QDockWidget

详细描述 QDockWidget 类提供了一个小部件&#xff0c;它可以停靠在QMainWindow内部&#xff0c;也可以作为桌面上的顶级窗口浮动。 QDockWidget 提供了停靠部件的概念&#xff0c;也称为工具调色板或实用窗口。停靠窗口是放置在 中央部件 周围的停靠部件区域中的辅助窗口&am…