数据结构之树的有关知识

297 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情


本文部分截图来自王道考研数据结构与算法-青岛大学-王卓

书籍参考:[1]王晓东.数据结构(语言描述)[M].北京:电子工业出版社.2019前言

前言

树是由一个集合在该集合上定义的一种层次关系构成的。集合中的元素就是树中的结点,之间的关系称为父子关系。树结点之间的父子关系形成了一种层次结构,在这种结构中,有一个结点是属于特殊的,这个结点是这棵树的开始结点,整个层次结构延申开始的地方,简称为树根,或者根结点。

  • 单个结点也可以是一个树,树根就是结点本神
  • T1,T2,....TkT_1,T_2,....T_k是树,它们的结点为n1,n2,...nkn_1,n_2,...n_k。此时用一个新结点nn作为n1,n2,...nkn_1,n_2,...n_k的父亲,那么就可以得到一棵新树,nn为新树的树根,结点n1,n2,...nkn_1,n_2,...n_k称为一组兄弟结点,所以树的固有特性,决定了树是由若干个子树构成的。
  • 一个空集合,也可以看成一棵树,称为空树。表示符号为Λ\Lambda,空树中没有结点

假设有一棵树,是由有限集T=A,B,C,D,E,F,G,H,I,JT={A,B,C,D,E,F,G,H,I,J}所构成的,其中AA为根结点,剩下3个互不相交的子集T1=B,E,F,I,J,T2=C,T3=D,G,HT_1={B,E,F,I,J},T_2={C},T_3={D,G,H}构成了这棵树的子树,其中T1,T2,T3T_1,T_2,T_3本身又是一棵树,那么从可以由此画出这棵树的形式。

image-20210328204958677

其中有一些关于树的基本概念和常用术语,其中许多术语借用了族谱树中的一些习惯用语。

  • 一个结点的儿子结点的个数,称为该结点的度,一棵树的度是指该树种结点的最大度数为多少。树TT,它的结点F的度就是2,因为它只有两个儿子结点I,JI,J

  • 树中度为零的结点称为叶子结点或者终端结点,例如结点JJ因为没有儿子结点,所以它的度就是零,那么它就是叶子结点,度不为零的结点,称为分枝结点或者非终端结点。除了根结点之外的分枝结点统一称为内部结点

  • 若存在树中一个结点序列k1,k2,...kjk_1,k_2,...k_j,使得结点kik_i是结点ki+1k_{i+1}的父结点,则称该结点序列是树中从结点k1k_1是到kjk_j的一条路径。称这条路径的长度为j1j-1

  • 树中一个结点的高度是指从该结点到各叶结点的最长路径的长度。树的高度是指根结点的高度,如TT,结点B,C,DB,C,D的高度就分别是2,0,1,而树的高度就是结点AA的高度,则就是3。

  • 从树根到任一结点nn有唯一的一条路径,称这条路径长度为nn的深度或层数。根结点的深度为0,其余结点的深度为父结点的深度加1。如树TT中,结点E,FE,F的深度为2,结点I,JI,J的深度为3。

  • 如果一棵树,按照从上到下,从左到右的顺序进行编号,那么这棵树就是一棵有序树,反之则是称为无序树。设根结点nn的所有儿子结点按照n1,n2,....nkn_1,n_2,....n_k的顺序进行排列,称n1n_1是最左儿子,简称左儿子,并称nin_ini1n_{i-1}的右邻兄弟,简称右儿子

    注意:如果两棵树的结点分布相同,如果作为无序树中,它们是相同的,但是作为有序树不一定相同,因为编号有可能是不一样的。

image-20210328211722364

树的遍历

树的3种最重要的遍历方式,分别称为前序遍历、中序遍历、后序遍历。以这三种方式遍历一棵树时,访问的结点依次排列起来,就分别是前序,中序,后序列表。

  • 如果TT是一棵空树,那么对TT进行前序,中序,后序遍历操作,都是空操作。
  • 如果TT是一棵单结点的树,那三种遍历操作,得到的都是结点本身

image-20210329213408354

  • 如树nn所示,以nn为树根,树的子树是T1,T2,T3T_1,T_2,T_3,如果是前序遍历,那么先遍历结点nn,接着继续使用前序遍历,遍历T1,T2,T3T_1,T_2,T_3
  • 中序遍历则是先遍历T1T_1,接着是nn,最后对T2,T3T_2,T_3进行遍历
  • 后序遍历则是,先对T1,T2,T3T_1,T_2,T_3进行遍历,最后访问nn

可以使用树不同的顺序的遍历操作形成不同的表达式,例如后序操作就是后缀形式(波兰形式)。

树的表示法

双亲表示法/父结点数组表示法

TT是为一棵树,其中结点的名称为1,2,3,....n1,2,3,....n。表示TT的一种最简单的方法就是用一个一维数组存储每一个结点的父结点。因为每一个结点的父结点都是唯一的,所以可以唯一的表示任何一棵树,具体如下图。

image-20210329214319927

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。

孩子表示法/儿子链表表示法

把每一个结点的孩子排列起来,看成一个单链表,称为儿子结点链表,主要因为每一个结点的孩子数不同,所以使用单链表来进行实现儿子结点链表。例如上图中的树TT的结点5为例

5->[6->7->8->NULL]

从图中,5有三个孩子,所以6,7,8组成了一个单链表,从最左边开始,每一个儿子结点的next指针都指向下一个临近的儿子结点,如果碰到没有下一个结点则指向为空。

左儿子右兄弟表示法

这种方法,又被称为二叉树表示法或者二叉链表表示法。用二叉链表作为树的存储结构,其中有两个链域分别指向该结点的最左儿子和右邻兄弟。

image-20210329220541654

二叉树的基本概念

二叉树是一非常重要的特殊的树型结构,尽管二叉树和树有很多相似的地方,但是二叉树不是树的一种特殊情况,而是完全另一种数据结构。二叉树最多只能有两个儿子结点,也就是度为2,同时两个结点有左右儿子的区分,左结点称为左儿子,右结点称为右儿子。

因为二叉树的左右儿子其中一个哪怕是空,也是存在,进行序号编号的时候,不代表可以省略,左右儿子可以同时是空状态,所以二叉树具有5种基本形态。

image-20210329221017430image-20210329221039225image-20210329221159012image-20210329221124680image-20210329221100025

二叉树的一些重要性质

  • 高度为h0h \geq 0的二叉树至少有h+1h+1个结点
  • 高度不超过hh的二叉树至多有2h+112^{h+1}-1个结点
  • 含有n1n \ge 1个结点的二叉树的高度至多为n1n-1
  • 含有n1n \ge 1个结点的二叉树的高数至少为logn\lfloor log_n \rfloor,高度为Ω(logn)\Omega(log_n)
  • 在第ii层上至多有2i12^{i-1}个结点
  • 对任何二叉树TT,叶子树为n0n_0,度为2的结点数为n2n_2,则n0=n2+1n_0=n_2+1

满二叉树

一棵高度为hh的二叉树并且有2h+112^{h+1}-1个结点,这样的二叉树,称为满二叉树,它的每一个结点都达到了最大的度。

完全二叉树/近似二叉树

深度为kk的具有nn个结点的二叉树,当且仅当每一个结点都与深度为kk的满二叉树中编号为1n1-n的结点对应的。

简单来说就是,编号和满二叉树一样对应,但是结点并没有达到最大结点数的二叉树就叫做完全二叉树。

满二叉树:

image-20210401205256055

完全二叉树:

image-20210401205319127

非完全二叉树:

image-20210401205344957

二叉树的运算

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): // 二叉树的结点数

二叉树的实现

二叉树的顺序结构

二叉树的顺序存储,就是将所有节点,按照一定次序,存储到一片连续的存储单元,可以使用数组。将下标作为编号,元素存放结点的值,如果碰上没有儿子结点或者只有一个儿子结点的情况,那么空的那个编号,在数组中下标的相应位置也要空出来。

image-20210401204932054

例如以上图A结点为例的一棵二叉树

下标:0 1 2 3 4 5 6 7
元素:  A B C D   E F

从1开始进行编号,按照自上到下,从左到右的方式进行,可以看到B结点的右儿子为空,它的编号为5,所以在一维数组中,下标为4的地方也要相应空出来。

所以二叉树的顺序存储结构又有以下性质:

  • 仅当i=1i=1时,结点ii为根结点;
  • 当结点ii大于1时,结点ii的父结点为i/2\lfloor i/2 \rfloor
  • 结点ii的左儿子为2i2i;
  • 结点ii的右儿子为2i+12i+1;
  • ii不为奇数时,结点ii的左兄弟结点为i1i-1
  • ii为偶数时,结点ii的右兄弟结点为i+1i+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