一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
树
本文部分截图来自王道考研,数据结构与算法-青岛大学-王卓
书籍参考:[1]王晓东.数据结构(语言描述)[M].北京:电子工业出版社.2019前言
前言
树是由一个集合在该集合上定义的一种层次关系构成的。集合中的元素就是树中的结点,之间的关系称为父子关系。树结点之间的父子关系形成了一种层次结构,在这种结构中,有一个结点是属于特殊的,这个结点是这棵树的开始结点,整个层次结构延申开始的地方,简称为树根,或者根结点。
- 单个结点也可以是一个树,树根就是结点本神
- 设是树,它们的结点为。此时用一个新结点作为的父亲,那么就可以得到一棵新树,为新树的树根,结点称为一组兄弟结点,所以树的固有特性,决定了树是由若干个子树构成的。
- 一个空集合,也可以看成一棵树,称为空树。表示符号为,空树中没有结点
假设有一棵树,是由有限集所构成的,其中为根结点,剩下3个互不相交的子集构成了这棵树的子树,其中本身又是一棵树,那么从可以由此画出这棵树的形式。
其中有一些关于树的基本概念和常用术语,其中许多术语借用了族谱树中的一些习惯用语。
-
一个结点的儿子结点的个数,称为该结点的度,一棵树的度是指该树种结点的最大度数为多少。树,它的结点F的度就是2,因为它只有两个儿子结点。
-
树中度为零的结点称为叶子结点或者终端结点,例如结点因为没有儿子结点,所以它的度就是零,那么它就是叶子结点,度不为零的结点,称为分枝结点或者非终端结点。除了根结点之外的分枝结点统一称为内部结点
-
若存在树中一个结点序列,使得结点是结点的父结点,则称该结点序列是树中从结点是到的一条路径。称这条路径的长度为。
-
树中一个结点的高度是指从该结点到各叶结点的最长路径的长度。树的高度是指根结点的高度,如,结点的高度就分别是2,0,1,而树的高度就是结点的高度,则就是3。
-
从树根到任一结点有唯一的一条路径,称这条路径长度为的深度或层数。根结点的深度为0,其余结点的深度为父结点的深度加1。如树中,结点的深度为2,结点的深度为3。
-
如果一棵树,按照从上到下,从左到右的顺序进行编号,那么这棵树就是一棵有序树,反之则是称为无序树。设根结点的所有儿子结点按照的顺序进行排列,称是最左儿子,简称左儿子,并称是的右邻兄弟,简称右儿子
注意:如果两棵树的结点分布相同,如果作为无序树中,它们是相同的,但是作为有序树不一定相同,因为编号有可能是不一样的。
树的遍历
树的3种最重要的遍历方式,分别称为前序遍历、中序遍历、后序遍历。以这三种方式遍历一棵树时,访问的结点依次排列起来,就分别是前序,中序,后序列表。
- 如果是一棵空树,那么对进行前序,中序,后序遍历操作,都是空操作。
- 如果是一棵单结点的树,那三种遍历操作,得到的都是结点本身
- 如树所示,以为树根,树的子树是,如果是前序遍历,那么先遍历结点,接着继续使用前序遍历,遍历。
- 中序遍历则是先遍历,接着是,最后对进行遍历
- 后序遍历则是,先对进行遍历,最后访问
可以使用树不同的顺序的遍历操作形成不同的表达式,例如后序操作就是后缀形式(波兰形式)。
树的表示法
双亲表示法/父结点数组表示法
设是为一棵树,其中结点的名称为。表示的一种最简单的方法就是用一个一维数组存储每一个结点的父结点。因为每一个结点的父结点都是唯一的,所以可以唯一的表示任何一棵树,具体如下图。
0 1 1 2 2 5 5 5 3 3
1 2 3 4 5 6 7 8 9 10
把数组下标当作元素,然后每一个下标中存储着当前结点的双亲结点,比如说结点2的双亲结点就是2,9和10的双亲结点就是3。
孩子表示法/儿子链表表示法
把每一个结点的孩子排列起来,看成一个单链表,称为儿子结点链表,主要因为每一个结点的孩子数不同,所以使用单链表来进行实现儿子结点链表。例如上图中的树的结点5为例
5->[6->7->8->NULL]
从图中,5有三个孩子,所以6,7,8组成了一个单链表,从最左边开始,每一个儿子结点的next指针都指向下一个临近的儿子结点,如果碰到没有下一个结点则指向为空。
左儿子右兄弟表示法
这种方法,又被称为二叉树表示法或者二叉链表表示法。用二叉链表作为树的存储结构,其中有两个链域分别指向该结点的最左儿子和右邻兄弟。
二叉树的基本概念
二叉树是一非常重要的特殊的树型结构,尽管二叉树和树有很多相似的地方,但是二叉树不是树的一种特殊情况,而是完全另一种数据结构。二叉树最多只能有两个儿子结点,也就是度为2,同时两个结点有左右儿子的区分,左结点称为左儿子,右结点称为右儿子。
因为二叉树的左右儿子其中一个哪怕是空,也是存在,进行序号编号的时候,不代表可以省略,左右儿子可以同时是空状态,所以二叉树具有5种基本形态。
二叉树的一些重要性质
- 高度为的二叉树至少有个结点
- 高度不超过的二叉树至多有个结点
- 含有个结点的二叉树的高度至多为
- 含有个结点的二叉树的高数至少为,高度为
- 在第层上至多有个结点
- 对任何二叉树,叶子树为,度为2的结点数为,则
满二叉树
一棵高度为的二叉树并且有个结点,这样的二叉树,称为满二叉树,它的每一个结点都达到了最大的度。
完全二叉树/近似二叉树
深度为的具有个结点的二叉树,当且仅当每一个结点都与深度为的满二叉树中编号为的结点对应的。
简单来说就是,编号和满二叉树一样对应,但是结点并没有达到最大结点数的二叉树就叫做完全二叉树。
满二叉树:
完全二叉树:
非完全二叉树:
二叉树的运算
BinaryInit(): // 创建一个棵c空的二叉树
BinaryEmpty(T): // 判断一棵二叉树T是否为空
Root(T): // 返回一棵二叉树T的根节点标号
MakeTree(x,T,L,R): // 以x为根节点元素,分别以L和R为左右子树构建一棵新的二叉树T
BreakTree(T,L,R): // MakeTree的逆运算,将二叉树T拆分为根节点元素,左子树L和右子树R等三个部分
PreOrder(visit,t): // 前序遍历二叉树
InOrder(visit,t): // 中序遍历二叉树
PostOrder(visit,t): // 后序遍历二叉树
PreOut(T): // 二叉树前序列表
InOut(T): // 二叉树中序列表
PostOut(T): // 二叉树后序列表
Delete(t): // 删除二叉树
Height(t): // 二叉树的高度
Size(t): // 二叉树的结点数
二叉树的实现
二叉树的顺序结构
二叉树的顺序存储,就是将所有节点,按照一定次序,存储到一片连续的存储单元,可以使用数组。将下标作为编号,元素存放结点的值,如果碰上没有儿子结点或者只有一个儿子结点的情况,那么空的那个编号,在数组中下标的相应位置也要空出来。
例如以上图A结点为例的一棵二叉树
下标:0 1 2 3 4 5 6 7
元素: A B C D E F
从1开始进行编号,按照自上到下,从左到右的方式进行,可以看到B结点的右儿子为空,它的编号为5,所以在一维数组中,下标为4的地方也要相应空出来。
所以二叉树的顺序存储结构又有以下性质:
- 仅当时,结点为根结点;
- 当结点大于1时,结点的父结点为;
- 结点的左儿子为;
- 结点的右儿子为;
- 当不为奇数时,结点的左兄弟结点为;
- 当为偶数时,结点的右兄弟结点为
指针实现二叉树
// 指针实现二叉树
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
void TreeItemShow(int x){
printf("%d\n",x);
}
typedef struct btnode{
// 二叉树结点结构
int data; // 结点元素
struct btnode *left; //左子树
struct btnode *right; //右子树
}Btnode;
typedef Btnode *btlink; // 二叉树结点指针类型
btlink NewBNode(){
/* 创建新的树结点 */
return (btlink)malloc(sizeof(Btnode));
}
typedef struct binarytree{ //二叉树结构
btlink root; // 树根
}BTree;
typedef BTree *BinaryTree; // 二叉树类型
void TreeTest(){
BinaryTree T;
}
int main(){
TreeTest();
return 0;
}
创建一个二叉树结点,由一个左子树和右子树还有结点元素构成。在创建一个一棵二叉树,设置树根,类型为一个二叉树结点类型。
初始化一棵二叉树
BinaryTree BinaryInit(){
BinaryTree T = (BinaryTree)malloc(sizeof(*T));
T->root = NULL;
return T;
}
判断非空
int BinaryEmpty(BinaryTree T){
// 判断非空
return T->root == NULL;
}
返回树根结点的元素/标号
int Root(BinaryTree T){
// 返回树根结点的元素,也可以称为标号
if (BinaryEmpty(T)){
exit(1);
}
return T->root->data;
}
构建一棵新的二叉树
void MakeTree(int x, BinaryTree T, BinaryTree L, BinaryTree R){
/* 构建新的二叉树 */
T->root = NewBNode(); // 为树根创建结点
T->root->data = x; // 放入结点元素
T->root->left = L->root; // 将结点的左儿子指向左子树的根结点
T->root->right = R->root; // 将结点的右儿子指向右子树的根节点
L->root = R->root = NULL; // 将左右子树的根节点置空
}
拆分一棵二叉树
int BreakTree(BinaryTree T, BinaryTree L, BinaryTree R){
/* 二叉树的拆分 */
if (BinaryEmpty(T)){
exit(1);
}
int x = T->root->data; // 返回节点值
L->root = T->root->left; // 将左子树的根结点置空
R->root = T->root->right; // 将右子树的根结点置空
T->root = NULL; // 根节点置空
return x;
}
二叉树有三种遍历方式,分别是前序遍历,中序遍历和后序遍历
D为根,L为左子树,R为右子树
前序遍历,先遍历根结点,然后遍历左子树,最后右子树,DLR
中序遍历,先遍历左子树,然后是根结点,最后是右子树,LDR
后序遍历,先遍历左子树,然后是右子树,最后根结点,LRD