简介
树和图是两种重要的非线性结构。线性结构中结点具有唯一前驱和唯一后继的关系,而非线性结构中结点之间的关系不再具有这种唯一性。其中,树形结构中结点间的关系是前驱唯一而后继不唯一,即元素之间是一对多的关系;在图结构中结点之间的关系是前驱、后继均不唯一,因此也就无所谓前驱、后继了。直观地看,树形结构既有分支关系,又具有层次关系,它非常类似于自然界中的树。树形结构在现实世界中广泛存在,如:家谱、行政组织机构等都用树形表示。计算机领域的DOS和Windows操作系统中对磁盘文件的管理就采用了树形目录结构;在数据库中,树形结构也是数据的重要组织形式之一。
一、树的定义及相关术语
1.1 定义
树(tree)是n(n>=0)个结点的有限集合。当n=0时,该集合满足以下条件:
(1)有且只有一个特殊的结点称为树的根(root),根结点没有直接前驱结点,但有零个或多个直接后继结点。
(2)跟结点之外的其余n-1个结点被分成m(m>0)个互相不相交的集合T1、T2、···、Tm,其中每一个集合Ti(1<=i<=m)本身又是一棵树。树T1,T2,···,Tm称为根节点的子树。
1.2 特点
(1)树的根结点没有直接前驱,除根结点之外的所有结点有且只有一个直接前驱。
(2)树中所有结点可以有零个或多个直接后继。
1.3 形式化
树的形式化二元组为:T = (D,R)。其中,D为树T中结点的集合;R为树中结点之间关系的集合。当树T为空时,D为空;当树T不为空树时,有:D = {Root} U Df },Root为树T的根结点,Df为树T的根Root的子树集合。
当树T的结点个数n<=1时,R为空;当树T 中结点个数n>1时有:R={<Root,ri>,i=1,2,···,m}。其中,Root为树T的根节点,ri 是树T的根结点Root的子树Ti 的根结点。
下图是一颗具有9个结点(ABCDEFGHI)的数T:
二元组:T = ({A,B,C,D,E,F,G,H,I },{<A,B> ,<A ,C>,<B,D>,<B,E>,<B,F>,<C,G>,<E,H>,<E,I>})
其中,以<A,B>为例,A是B的直接前驱,B是A的直接后继,也称为树的一条分支。
结点A为树T的根结点,除根结点A之外的其余结点分为两个不相交的集合:T1 = { B,D,E,F,H,I} 和 T2={C,G}。它们俩构成了结点A的两棵子树,T1和T2本身也是一棵树,例如子树T1的根结点为B,其余结点又分为三个不相交的集合即构成了结点B的三棵子树。
1.4 相关术语
(1)结点:包含一个数据元素及若干指向其他结点的分支信息的数据结构。
(2)结点的度:结点所拥有的子树的个数称为该结点的度。
(3)叶子结点:度为0的结点称为叶子结点,或者称为终端结点。
(4)分支结点:度不为0的结点称为分支结点,或者称为非终端结点。一棵树的结点除叶子结点外,其余的结点都是分支结点。
(5)孩子结点、双亲结点、兄弟结点:树中一个结点的子树的根结点称为这个结点的孩子结点,这个结点称为孩子结点的双亲结点。具有同一个双亲结点的孩子结点互称为兄弟结点。
(6)路径、路径长度:设n1,n2,···,nk为一棵树的结点序列,若结点ni是ni+i的双亲结点(1<=i <k),则把n1,n2,···,nk称为一条由n1至nk的路径。这条路径的长度是k-1。
(7)祖先、子孙:在树中,如果有一条路径从结点M到结点N,那么M就称为N的祖先,而N称为M的子孙。
(8)结点的层次:规定树的根结点的层数为1,其余结点的层数等于它的双亲结点层数加1。
(9)树的深度(高度):树中所有结点的层次的最大树称为树的深度。
(10)树的度:树中所有结点度的最大值称为该树的度。
(11)有序树和无序树:如果一棵树中结点的各子树从左到右是有次序的,即若交互了某结点各子树的相应位置,则构成不同的树,称这棵树为有序树;反之,则称为无序树。
(12)森林:m(m>=0)棵不想交的树的集合称为森林。自然界中树和森林是不同的概念,但在数据结构中,树和森林只有很小的差别。任何一棵树,删去根结点就变成了森林;反之,给森林增加一个统一的根结点,森林就变成一棵树。
1.5 树的基本操作
通常有以下几种:
(1)Initiate(t):初始化一棵树t。
(2)Root(x):求结点x所在树的根结点。
(3)Parent(t,x) :求树t中结点x的双亲结点。
(4)Child(t,x,i):求树t中结点x的第i个孩子结点。
(5)RightSibling(t,x):求树t中结点x右边的第一个兄弟结点,也称右兄弟结点。
(6)Insert(t,x,i,s):把以s为根结点的树插入到树t中作为结点x的第i棵子树。
(7)Delete(t,x,i):在树t中删除结点x的第i棵子树。
(8)Traverse(t):是树的遍历操作,即按某种方式访问树t中的每个结点,且使每个结点只被访问一次。遍历操作是非线性结构中非常常用的基本操作,许多对树的操作都是借助该操作实现的。
二、二叉树
二叉树是一种简单又非常重要的树形结构。由于任何数都可以转换为二叉树进行处理,而二叉树又有许多好的性质,非常适合于计算机处理,因此二叉树也是数据结构研究的重点。
2.1 基本概念
二叉树(Binary Tree)是有n个结点的有限集合,该集合或者为空、或者由一个称为根(Root)的结点及两个不相交、被分别称为根结点的左子树和右子树的二叉树组成。当集合为空时,称该二叉树为空二叉树。一颗二叉树中每个结点只能含有0、1或2个孩子结点,而且孩子节点分左、右孩子。如下图:
满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称为满二叉树。如下图:
完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(i<=n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。其特点是:叶子结点只能出现在最下层和次下层,且最下层的叶子结点在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。如下图:
2.2 二叉树的性质
2.3 二叉树的存储结构
(1)顺序存储结构:用一组连续的存储单元存放二叉树中的结点。一般按照二叉树结点从上至下、从左到右的顺序存储。对于一般的二叉树,如果仍按从上至小、从左到右的顺序将树中的结点顺序存储在一维数组中,则数组元素下标之间的关系不能反映二叉树中结点之间的逻辑关系,只有添加一些并不存在的空结点,使之成为一棵完全二叉树的形式,然后用一维数组顺序存储。显然,这种存储对于需增加许多空结点才能将一棵二叉树改造成为一棵完全二叉树的存储时,会造成空间的大量浪费,不宜用顺序存储结构。
代码如下:
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int CElemType; /* 树结点的数据类型,目前暂定为整型 */
typedef CElemType SqBiTree[MAX_TREE_SIZE]; /* 0号单元存储根结点 */
CElemType Nil = 0; /*设整型以0为空 或者以 INT_MAX(65535)*/
typedef struct {
int level; //结点层
int order; //本层的序号(按照满二叉树给定序号规则)
}Position;
//6.2 构造空二叉树T,因为T是固定数组,不会改变.
Status InitBiTree(SqBiTree T){
for (int i = 0; i < MAX_TREE_SIZE; i++) {
//将二叉树初始化值置空
T[i] = Nil;
}
return OK;
}
//6.3 按层序次序输入二叉树中的结点值(字符型或整型),构造顺序存储的二叉树T
Status CreateBiTree(SqBiTree T){
int i = 0;
//printf("按层序输入结点的值(整型),0表示空结点, 输入999结束.结点数<=%d\n",MAX_TREE_SIZE);
/*
1 -->1
2 3 -->2
4 5 6 7 -->3
8 9 10 -->4
1 2 3 4 5 6 7 8 9 10 Nil Nil Nil
*/
while (i < 10) {
T[i] = I+1;
printf("%d ",T[I]);
//结点不为空,且无双亲结点
if (i != 0 && T[(i+1)/2-1] == Nil && T[i] != Nil) {
printf("出现无双亲的非根结点%d\n",T[i]);
exit(ERROR);
}
I++;
}
//将空赋值给T的后面的结点
while (i < MAX_TREE_SIZE) {
T[i] = Nil;
I++;
}
return OK;
}
(2)链式存储结构:用链式结构来表示一棵二叉树,即用链指针来指示其元素的逻辑关系。
二叉链表存储,每个结点由三个域组成,除了数据域外,还有两个指针域,分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。其中,data域存放结点的数据信息;Lchild 与Rchild分别存放指向左孩子和右孩子的指针,指针域为空为^/NULL。
代码如下:
typedef char CElemType;
CElemType Nil=' '; /* 字符型以空格符为空 */
typedef struct BiTNode /* 结点结构 */
{
CElemType data; /* 结点数据 */
struct BiTNode *lchild,*rchild; /* 左右孩子指针 */
}BiTNode,*BiTree;
/* 7.2 构造空二叉树T */
Status InitBiTree(BiTree *T)
{
*T=NULL;
return OK;
}
/*7.4 创建二叉树
按前序输入二叉树中的结点值(字符),#表示空树;
*/
void CreateBiTree(BiTree *T){
CElemType ch;
//获取字符
ch = str[indexs++];
//判断当前字符是否为'#'
if (ch == '#') {
*T = NULL;
}else
{
//创建新的结点
*T = (BiTree)malloc(sizeof(BiTNode));
//是否创建成功
if (!*T) {
exit(OVERFLOW);
}
/* 生成根结点 */
(*T)->data = ch;
/* 构造左子树 */
CreateBiTree(&(*T)->lchild);
/* 构造右子树 */
CreateBiTree(&(*T)->rchild);
}
}
2.4 二叉树基本操作
1、Initiate(bt):建立一棵空二叉树。
2、Create(x,lbt,rbt):生成一棵x为根结点的树,以lbt、rbt为子树。
3、InsertL(bt,x,parent):将结点x插入到树bt中作为parent结点的左孩子结点。如果parent已经有左孩子,则将x作为左孩子结点的左孩子结点。
4、InsertR(bt,x,parent):插入到右孩子结点。于上同理。
5、DeleteL(bt,parent):在二叉树bt中删除结点parent的左子树。
6、DeleteR(bt,parent):在二叉树bt中删除结点parent的右子树。
7、Search(bt,x):在二叉树bt中查找数据元素x。
8、Traverse(bt):按某种方式遍历二叉树bt中的全部结点。
2.4 二叉树的遍历
遍历操作可以使非线性结构线性化。由二叉树定义可知,一棵二叉树由根结点及其左子树和右子树三部分组成。因此,只需依次遍历这三部分即可遍历整个二叉树。若以D、L、R分别表示根、左、右,且以从左到右的顺序遍历为:(先)前序DLR、中序LDR、后序LRD。
(1)先序遍历:先访问根结点,然后遍历根结点的左子树,最后遍历根结点的右子树。
代码如下:
/*
6.15 前序遍历二叉树 顺序存储
*/
void PreTraverse(SqBiTree T,int e){
//打印结点数据
visit(T[e]);
//先序遍历左子树
if (T[2 * e + 1] != Nil) {
PreTraverse(T, 2*e+1);
}
//最后先序遍历右子树
if (T[2 * e + 2] != Nil) {
PreTraverse(T, 2*e+2);
}
}
/*
7.8 前序递归遍历T 链式存储
初始条件:二叉树T存在;
操作结果: 前序递归遍历T
*/
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
(2)中序遍历:先遍历根据的左子树,再访问根结点,最后遍历根结点的右子树。
代码如下:
/*
6.16 中序遍历 顺序存储
*/
void InTraverse(SqBiTree T, int e){
/* 左子树不空 */
if (T[2*e+1] != Nil)
InTraverse(T, 2*e+1);
visit(T[e]);
/* 右子树不空 */
if (T[2*e+2] != Nil)
InTraverse(T, 2*e+2);
}
/*
7.9 中序递归遍历T 链式存储
初始条件:二叉树T存在;
操作结果: 中序递归遍历T
*/
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return ;
InOrderTraverse(T->lchild); /* 中序遍历左子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
InOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
(3)后序遍历:先遍历根结点的左子树,再遍历根结点的右子树,最后访问根结点。
代码如下:
/*
6.17 后序遍历 顺序存储
*/
void PostTraverse(SqBiTree T,int e)
{ /* 左子树不空 */
if(T[2*e+1]!=Nil)
PostTraverse(T,2*e+1);
/* 右子树不空 */
if(T[2*e+2]!=Nil)
PostTraverse(T,2*e+2);
visit(T[e]);
}
/*
7.10 后序递归遍历T 链式存储
初始条件:二叉树T存在;
操作结果: 中序递归遍历T
*/
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); /* 先后序遍历左子树 */
PostOrderTraverse(T->rchild); /* 再后序遍历右子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
}
(4)层次遍历:是指从二叉树的第一次根结点开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。得到的结果序列为:ABCDEFG。因此,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,然后从队头取出一个元素,每取出一个元素先访问该元素所指的结点,若该元素所指的结点左右孩子指针非空,则将该元素所指结点的非空左孩子指针和右孩子指针顺序入队。若队列非空,重复以上过程,当队列为空时,二叉树的层次遍历结束。在下面算法中,二叉树以二叉链表存储,一维数组Queue[MAXNODE]用以实现队列,变量front和rear分别表示当前队列首元素和队列尾元素在数组中的位置。
代码如下:
/*
6.14 层序遍历二叉树 顺序存储
*/
void LevelOrderTraverse(SqBiTree T){
int i = MAX_TREE_SIZE-1;
//找到最后一个非空结点的序号
while (T[i] == Nil) i--;
//从根结点起,按层序遍历二叉树
for (int j = 0; j <= i; j++)
//只遍历非空结点
if (T[j] != Nil)
visit(T[j]);
printf("\n");
}
/*
7.11 层序递归遍历T
初始条件:二叉树T存在; 链式存储
*/
void TierOrderTraverse(BiTree T)
{
BiTree Queue [MAXSIZE];
int front,rear;
if(T==NULL)
return;
front = 0;
rear = 1;
Queue[rear]=T;
while (front != rear)
{
front++;
printf("%c", Queue[front]->data);/* 显示结点数据,可以更改为其它对结点操作 */
if(Queue[front]->lchild != NULL)
{
Queue[++rear] = Queue[front]->lchild;
}
if(Queue[front]->rchild != NULL)
{
Queue[++rear] = Queue[front]->rchild;
}
}
}