树
树的定义
- 首先“树”的数据结构形式为一对多,和线性存储结构有了根本的区别;
- 树(Tree)是\(n(n\geq0)\)个节点的有限集。\(n=0\)时称为空树。在任意的一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当\(n>1\)时,其余结点可分为\(m(m>0)\)个互不相交的有限集\(T_1、T_2、\cdots、T_m\),其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。树的结构如图1所示。
- 树的定义是一种递归的方法。也就是说在树的定义之中还用到了树的概念。下图所示的子树\(T_1\)和子树\(T_2\)就是根结点\(A\)的子树。当然,\(D、G、H、I\)组成的树又是以\(B\)为结点的子树,\(E、F、J\)组成的树是以\(C\)为结点的子树。
-
对于树的定义当中需要注意以下两点:
-
\(n>0\)时,根结点是唯一的,不可能存在多个根结点。
-
\(m>0\)时,子树的个数是没有限制的,但它们一定是互不相交的。如下面两图中的结构就不符合树的定义,因为它们有相交的子树。
-
结点分类
树的结点是树中的一个独立单元,包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度(Degree)。度为0的结点称为叶子(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。下图所示的树的度为3,因为结点D的度为3。
结点间的关系
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。同一个双亲的孩子之间互称兄弟(Sibling)。结点的祖先是从根到结点所经分支上的所有结点。所以对于下图中的H来说,D、B、A都是它的祖先。反之,以某节点为根的子树中的任一节点都称为该结点的子孙。B的子孙有D、G、H、I。
树的其他相关概念
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某节点在\(l\)层,则其子树的根就在第\(l+1\)层。其双亲在同一层的结点互为堂兄弟。显然,下图中的D、E、F是堂兄弟,而G、H、I、J也是。树中结点的最大层次称为树的深度(Depth)或高度,下图中的树的深度为4。
有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
森林:是\(m(m\geq0)\)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
二叉树的定义
二叉树(Binary Tree)是\(n(n\geq0)\)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。下图所示就是一棵二叉树。
二叉树特点
二叉树的特点有:
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的节点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
- 左子树和右子树是由顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。如下图中所示,树1和树2是同一棵树,但它们却是不同的二叉树。
二叉树具有五种基本形态:
- 空二叉树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
特殊的二叉树
-
斜树
所有的结点都只有左子树的二叉树叫左斜树。所有的结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。线性表结构可以理解为是树的一种及其特殊的表现形式。
-
满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。如下图所示。
满二叉树的特点有:
- 叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
- 非叶子节点的度一定是2。否则就是“缺胳膊少腿”了。
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。
-
完全二叉树
如果二叉树中除去最后一层结点为满二叉树,且最后一层的结点依次从左到右分布。
按照上面的定义,下图中的三棵树就都不是完全二叉树。树1中,最后一层的结点不连续;树2中,除去最后一层不是满二叉树;树3中,与树1是同样的道理。所以只有上面图中的二叉树是完全二叉树。
如此,可以总结出如下的完全二叉树特点:
- 叶子结点只能出现在最下两层;
- 最下层的叶子一定集中在左部连续位置;
- 倒数第二层若有叶子结点,一定都在右部连续位置;
- 如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况;
- 同样结点树的二叉树,完全二叉树的深度最小。
二叉树的存储结构
二叉树的顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。
先看一下完全二叉树的顺序存储,如下图所示的一棵完全二叉树:
将这棵树存入到数组中,相应的下标对应其同样的位置,如下所示:
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
内容 | A | B | C | D | E | F | G | H | I | J |
由于图中的树是完全二叉树,可以使用顺序结构表现出来。
对于一般的二叉树,可以先将其按照完全二叉树编号,把不存在的结点设置为“^”而已。如下图所示的二叉树:
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
内容 | A | B | C | ^ | E | ^ | G | ^ | ^ | J |
考虑下面的这种极端情况,一棵深度为\(k\)的右斜树,它只有\(k\)个结点,却需要分配\(2^k-1\)个存储单元空间,这就会对空间造成极大的浪费。如下图所示:
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
内容 | A | ^ | B | ^ | ^ | ^ | C | ^ | ^ | ^ | ^ | ^ | ^ | ^ | D |
根据上面的分析可以发现:顺序存储结构一般只适用于完全二叉树
//二叉树的顺序存储表示
#define MAXTSIZE 100 //二叉树的最大结点数
typedef TELemType SqBinaryTree[MAXSIZE] //0号单元存储根结点
SqBinaryTree bt;
二叉树的链式存储结构
由二叉树的定义得知,二叉树的结点(见下图中的(a))有一个数据元素和分别指向其左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含3个域:数据域和左、右指针域。如下图中的(b)所示。有时,为了便于找到结点的双亲,还可在结点结构中增加一个指向其双亲结点的指针域,如下图中的(c)所示。
利用上图(b)、(c)两种结构所得二叉树的存储结构分别称之为二叉链表和三叉链表,如下图所示。
//二叉树的二叉链表存储表示
typedef struct BinaryTreeNode{
char Data;
struct BinaryTreeNode* Lchild,Rchild;
}BiTNode,*BiTree;
遍历二叉树
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
访问的含义很广,可以是对结点做各种处理,包括输出结点的信息,对结点进行运算和修改等。
二叉树遍历方法及算法实现
二叉树的遍历方式可以很多,如果限制了从左到右的习惯方式,那么主要就分为四种:
前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。如下图所示的的二叉树,前序遍历的顺序为:ABDGHCEIF。
/*二叉树的前序遍历递归算法*/
void PreOrderTraverse(pBiTree T)
{
if(T == NULL)
return;
printf("%c",T->Data);
PreOrderTraverse(T->Lchild);
PreOrderTraverse(T->Rchild);
}
假设现在有下图所示的一棵二叉树\(T\)。这棵树已经用二叉链表结构存储在内存当中。
那么当调用PreOrderTraverse(pBiTree T)函数时,看一下程序是如何运行的。
-
调用PreOrderTraverse(T),T的根结点不为NULL,所以执行printf,打印字母A,如下图所示。
-
调用PreOrderTraverse(T->Lchild);访问了A结点的左孩子,不为NULL,执行printf显示字母B,如下图所示。
-
此时再次递归调用PreOrderTraverse(T->lchild);访问了B结点的左孩子,不为NULL,执行printf显示字母D,如下图所示。
-
再次递归调用PreOrderTraverse(T->lchild);访问了D结点的左孩子,不为NULL,执行printf显示字母H,如下图所示。
-
调用PreOrderTraverse(T->lchild);访问了H结点的左孩子,此时因为H结点无左孩子所以T==NULL,返回此函数,此时递归调用PreOrderTraverse(T->rchild);访问了H结点的右孩子,printf显示字母K,如下图所示。
-
再次递归调用PreOrderTraverse(T->lchild);访问了K结点的左孩子,K结点无左孩子,返回,调用PreOrderTraverse(T->rchild);访问了K结点的右孩子,也是NULL,返回。于是此函数执行完毕,返回到上一级递归的函数(即打印H结点时的函数),也执行完毕,返回到打印结点D时的函数,调用PreOrderTraverse(T->rchild);访问了D结点的右孩子,为NULL,返回到B结点,调用PreOrderTraverse(T->rchild);找到了结点E,打印字母E,如下图所示。
-
由于结点E没有左右孩子,返回打印结点B时的递归函数,递归执行完毕,返回到最初的PreOrderTraverse,调用PreOrderTraverse(T->rchild);访问了结点A的右孩子,打印字母C,如下图所示。
-
后面的执行过程就类似前面的递归调用,依次继续打印F、I、G、J。
中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。如下图所示的二叉树,中序遍历的顺序为:GDHBAEICF。
/*二叉树的中序遍历递归算法*/
void InOrderTraverse(pBitree T)
{
if(T == NULL)
return;
InOrderTraverse(T->lchild);
printf("%c",T->Data);
InOrderTraverse(T->rchild);
}
后序遍历
规则是若树为空,则空操作返回,否则先后序遍历左子树,再后序遍历右子树,最后访问根结点。如下图所示的二叉树,后序遍历的顺序为:GHDBIEFCA
/*二叉树的后序遍历递归算法*/
void PostOrderTraverse(pBiTree T)
{
if(T == NULL)
return;
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
printf("%c",T->Data);
}
推导遍历结果
由二叉树的先序序列和中序序列,能唯一地确定一棵二叉树。
由二叉树的后序序列和中序序列,能唯一地确定一棵二叉树。
-
已知一棵二叉树的先序序列和中序序列分别是ABFDCLMN和BFACDLNM,请画出这棵二叉树。
-
由先序遍历可知,该树的根结点一定是A;
-
由中序遍历可知,根结点一定在中间,而且其左部必全部是左子树子孙(BF),其右部必全部是右子树子孙(CDLNM);
-
再看前中序中的B和F,在前序中为BF,在中序中也为BF,所以B是A的左孩子,F是B的左孩子还是右孩子根据前序序列无法判断,只能通过中序序列判断,根据中序序列的特征,F只能是B的右孩子(如果是左孩子,那中序序列的顺序则变成了FB);
-
再来看根结点A的右孩子,根据先序和中序的特征,D一定是A结点的右孩子,那根据中序序列得出C只能是D的左孩子;
-
最后看L、N、M,在先序中的顺序是LMN,在中序中的顺序是LNM,那么L一定就是结点D的右孩子,NM必全部是L右子树的子孙;
-
最后来判断N、M和L的关系,在先、中序中的顺序分别为MN和NM,所以M一定是L的右孩子,最后N只能是M的左孩子。
-
-
已知一棵二叉树的中序序列和后序序列分别是BDCEAFHG和DECBHGFA,请画出这棵二叉树。
- 由后序序列的特征可知,根结点必在后序序列尾部,即根结点是A;
- 由中序序列特征,根结点必在其中间,而且左部必全部是左子树子孙(BDCE),其右部必全部是右子树子孙(FHG);
- 然后,根据后序中的DECB顺序可以确定B是A的左孩子,根据HGF判断F是A的右孩子。由此类推,可以唯一地确定一棵二叉树如下图所示。
二叉树遍历算法的应用
“遍历”是二叉树各种操作的基础,通过“遍历”可以实现如下集中二叉树的遍历算法
建立二叉链表
利用递归原理实现先序遍历的顺序建立二叉链表,算法步骤如下:
-
扫描字符序列,读入字符ch;
-
如果ch是一个“#”字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作:
-
申请一个结点空间T;
-
将ch赋给T->data;
-
递归创建T的左子树;
-
递归创建T的右子树;
-
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
#include<math.h>
typedef struct BiTreeNode
{
char Data;
struct BiTreeNode* Lchild;
struct BiTreeNode* Rchild;
}BiTreeNode,*BiTree,*pBiTreeNode;
void CreateBiTree(BiTree *T)
{
char ch;
scanf("%c",&ch);
if(ch == '#'){
*T = NULL;
}
else{
*T = (pBiTreeNode)malloc(sizeof(BiTreeNode));
if(*T == NULL)
exit(OVERFLOW);
(*T)->Data = ch;
CreateBiTree(&((*T)->Lchild));
CreateBiTree(&((*T)->Rchild));
}
}
void InOrderTraverse(BiTree T)
{
if(T == NULL){
return;
}
else{
InOrderTraverse(T->Lchild);
printf("%c",T->Data);
InOrderTraverse(T->Rchild);
}
}
void PostOrderTraverse(BiTree T)
{
if(T == NULL){
return;
}
else{
PostOrderTraverse(T->Lchild);
PostOrderTraverse(T->Rchild);
printf("%c",T->Data);
}
}
int main(void)
{
BiTree T;
CreateBiTree(&T);
printf("中序递归遍历结果为:");
InOrderTraverse(T);
printf("\n");
printf("后序递归遍历结果为:");
PostOrderTraverse(T);
return 0;
}
先序次序构造二叉树时,不仅要按先序次序输入结点的值,而且还要把叶子结点的左右孩子指针和度为1的结点的空指针输入。其原因是只根据结点的先序次序还不能唯一确定二叉树的形状。如下图所示,三棵树的先序次序都是abc。这样,在调用函数CreateBiTree()
时,输入abc就会产生多义性。如果把叶子结点的左右孩子指针和度为1的结点的空指针(以#表示)也按先序输入,可以唯一确定二叉树的形状。