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

树(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###,得到以下结果:

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-20 15:44:01       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-20 15:44:01       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-20 15:44:01       45 阅读
  4. Python语言-面向对象

    2024-07-20 15:44:01       55 阅读

热门阅读

  1. 量化交易对短期收益的提升效果

    2024-07-20 15:44:01       17 阅读
  2. ArcGIS Pro SDK (九)几何 9 立方贝塞尔线段

    2024-07-20 15:44:01       15 阅读
  3. glibc: getifaddrs_internal 占用大量cpu

    2024-07-20 15:44:01       13 阅读
  4. 【关于使用swoole的知识点整理】

    2024-07-20 15:44:01       14 阅读
  5. 弹框管理类demo

    2024-07-20 15:44:01       17 阅读
  6. 单机 Redission 存在的问题以及怎么解决

    2024-07-20 15:44:01       15 阅读
  7. 力扣(LeetCode)——70. 爬楼梯

    2024-07-20 15:44:01       14 阅读
  8. 如何使用fiddler 查看手机端数据包

    2024-07-20 15:44:01       19 阅读
  9. AI艺术创作:掌握Midjourney和DALL-E的技巧与策略

    2024-07-20 15:44:01       17 阅读
  10. 快速创建 vue 项目并添加 Dockerfile 文件

    2024-07-20 15:44:01       14 阅读