我们称这条路径的长度为j-1,它是该路径所经过的边(即连接两个结点的线段)的数目。
树中任一结点有一条到其自身的长度为零的路径。
如果在树中存在一条从结点K到结点M的路径,则称结点K是结点M的祖先,也称结点M是结点K的子孙或后裔。
树中一个结点的高度是从该结点到作为它的子孙的各叶结点的最长路径的长度。
树的高度是指根结点的高度。
从树根到任一结点n有唯一的一条路径,我们称这条路径的长度为结点n的深度或层数。
根结点的深度为0,其余结点的深度为其父结点的深度加1。
深度相同的结点属于同一层。
结点的层次:
树中根结点的层次为1,根结点子树的根为第2层,以此类推。
树的深度:
树中所有结点层次的最大值。
有序树:
如果一棵树中结点的子树从左到右是有次序的,即若交换了某结点子树的相对位置,则构成不同的树,称这棵树为有序树;反之,则称为无序树。
森林:
零棵或有限棵不相交的树的集合称为森林。
任何一棵树,删去根结点就变成了森林。
森林是m(m≥0)棵互不相交的树的集合。
在树结构中,结点之间的关系也可以用家族关系描述,定义如下:
孩子、双亲:
结点子树的根称为这个结点的孩子,而这个结点又被称为孩子的双亲。
子孙:
以某结点为根的子树中的所有结点都被称为该结点的子孙。
祖先:
从根结点到该结点路径上的所有结点。
同一个双亲的孩子之间互为兄弟。
双亲在同一层的结点互为堂兄弟。
6.1.2树的表示
树的表示方法有以下四种。
1.直观表示法
树的直观表示法是以倒着的分支树的形式表示,图6-1(a)就是一棵树的直观表示。
其特点是对树的逻辑结构的描述非常直观。
2.嵌套集合表示法
嵌套集合是指由一些集合构成的整体,对于其中任何两个集合,或者不相交,或者一个包含另一个。
用嵌套集合的形式表示树,是将根结点视为一个大的集合,其若干棵子树构成这个大集合中若干个互不相交的子集,如此嵌套下去,即构成一棵树的嵌套集合表示。
图6-2(a)是一棵树的嵌套集合表示。
3.凹入表示法
树的凹入表示法如图6-2(c)所示。
主要用于树的屏幕和打印输出。
4.广义表表示法(括号表示法)
用广义表表示树,就是将根作为表头,其子树的根依序作为表尾,并对其子树进行同样层层嵌套所构造的广义表。
图6-2(b)就是一棵树的广义表表示。
6.2二叉树
二叉树是树形结构的一个重要类型。
许多实际问题抽象出来的数据结构往往是二叉树的形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单。
6.2.1二叉树的定义
1.定义:
二叉树是满足下列条件的树形结构。
(1)每个结点最多有两棵子树;
(2)子树有左右之分。
二叉树也可以用递归的形式定义。
即:
二叉树是n(n≥0)个结点的有限集合。
当n=0时,称为空二叉树;当n>0时,有且仅有一个结点为二叉树的根,其余结点被分成两个互不相交的子集,一个作为左子集,另一个作为右子集,每个子集又是一个二叉树。
二叉树是有序的,即若将其左、右子树颠倒,就成为另一棵不同的二叉树。
即使结点只有一棵子树也要区分是左子树还是右子树。
因此二叉树具有五种基本形态,如图6-3所示。
在有序树中,虽然一个结点的孩子之间是有左右次序的,但是若该结点只有一个孩子,就无须区分其左右次序。
而在二叉树中,即使是一个孩子也有左右之分。
2.二叉树的相关概念
满二叉树:
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称作满二叉树。
如图6-4(a)就是一棵满二叉树,(b)图则不是满二叉树,因为,虽然其所有结点要么是含有左右子树的分支结点,要么是叶子结点,但由于其叶子未在同一层上,故不是满二叉树。
完全二叉树:
叶子结点只出现在最下层和次最下层,且最下层的叶子结点集中在树的左部。
如图6-5(a)为一棵完全二叉树,(b)不是完全二叉树。
3.二叉树的基本运算
(1)构造一棵二叉树CreateBTree(BT)
(2)清空以BT为根的二叉树ClearBTree(BT)
(3)判断二叉树是否为空BTreeEmpty(BT)
(4)获取给定结点的左孩子和右孩子LeftChild(BT,linklist),RightChild(BT,linklist)
(5)获取给定结点的双亲Parent(BT,linklist)
(6)遍历二叉树Traverse(BT)
4.二叉树的性质
【性质1】在二叉树的第i层上最多有2i-1个结点(i≥1)。
二叉树的第1层只有一个根结点,所以,i=1时,2i-1=21-1=20=1成立。
假设对所有的j,1≤j<i成立,即第j层上最多有2j-1个结点成立。
由于j=i-1时,第i-1层上最多有2i-2个结点。
再由二叉树中每个结点的度最大为2,可以推导出第i层的结点个数最多是第i-1层的结点个数的2倍,即2i-2*2=2i-1。
由性质1易知.
【性质2】深度为K的二叉树最多有2k-1个结点(K≥1)。
【性质3】对于任意一棵二叉树,如果度为0的结点个数为n0,度为2的结点个数为n2,则n0=n2+1。
证明:
假设度为1的结点个数为n1,结点总数为n,B为二叉树中的分支数。
因为二叉树中结点的度均小于或等于2,所以结点总数为:
n=n0+n1+n2
(1)
在二叉树中,除根结点之外,每个结点都有一个分支指向它,所以,总的结点个数n与分支数B之间的关系为:
B=n-1。
又因为在二叉树中,度为1的结点产生1个分支,度为2的结点产生2个分支,所以分支数B又可以表示为:
B=n1+2n2。
将此式代入上式得:
n=n1+2n2+1
(2)
用
(1)式减
(2)式,整理后可得:
n0=n2+1。
【性质4】具有n个结点的完全二叉树的深度k=log2n+1。
证明:
根据完全二叉树的定义和性质2可知,当一棵完全二叉树的深度为k、结点个数为n时,有2k-1-1<n≤2k-1
即2k-1≤n<2k
对不等式取对数,有k-1≤log2n<k
由于k是整数,所以有k=log2n+1。
【性质5】对有n个结点的完全二叉树中的所有结点按从上到下,从左到右的顺序进行编号,则对任意一个结点i(1≤i≤n),都有:
(1)如果i=1,则结点i是这棵完全二叉树的根,没有双亲;否则其双亲结点的编号为i/2。
(2)如果2i>n,则结点i没有左孩子;否则其左孩子结点的编号为2i。
(3)如果2i+1>n,则结点i没有右孩子;否则其右孩子结点的编号为2i+1。
下面我们利用数学归纳法证明这个性质。
先证
(2)和(3)。
当i=1时,若n≥3,则根的左、右孩子的编号分别是2,3;若n<3,则根没有右孩子;若n<2,则根将没有左、右孩子;以上对于
(2)和(3)均成立。
假设:
对于所有的1≤j≤i结论成立。
即:
结点j的左孩子编号为2j;右孩子编号为2j+1。
由完全二叉树的结构可以看出:
结点i+1或者与结点i同层且紧邻i结点的右侧,或者i位于某层的最右端,i+1位于下一层的最左端。
可以看出,i+1的左、右孩子紧邻在结点i的孩子后面,由于结点i的左、右孩子编号分别为2i和2i+1,所以,结点i+1的左、右孩子编号分别为2i+2和2i+3,经提取公因式可以得到:
2(i+1)和2(i+1)+1,即结点i+1的左孩子编号为2(i+1);右孩子编号为2(i+1)+1。
又因为二叉树由n个结点组成,所以,当2(i+1)+1>n,且2(i+1)=n时,结点i+1只有左孩子,而没有右孩子;当2(i+1)>n,结点i+1既没有左孩子也没有右孩子。
即
(2)和(3)成立。
基于
(2)、(3)有:
对于任意一个结点i,若2i≤n,则左孩子的编号为2i,反过来结点2i的双亲就是i,而2i/2=i;若2i+1≤n,则右孩子的编号为2i+1,反过来结点2i+1的双亲就是i,而(2i+1)/2=i,即
(1)成立。
6.2.2二叉树的存储结构
1.顺序存储结构
按照二叉树结点从上至下、从左到右的顺序依次把各结点存储到一组连续的存储单元中。
由二叉树的性质知,完全二叉树和满二叉树中结点的序号可以唯一地反映出结点之间的逻辑关系。
采用顺序存储既能够最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置及结点之间的关系。
对于一般的二叉树,如果仍按从上至下和从左到右的顺序将树中的结点顺序存储在一维数组中,则数组元素下标之间的关系不能够反映二叉树中结点之间的逻辑关系。
可通过增添一些并不存在的空结点,使之成为一棵完全二叉树后,再用一维数组顺序存储。
如图6-8给出了一棵一般二叉树改造后的完全二叉树形态和其顺序存储状态示意图。
显然,这种存储会造成空间的浪费,最坏的情况是右单支树,如图6-9所示,一棵深度为k的右单支树,只有k个结点,却需分配2k-1个存储单元。
二叉树的顺序存储表示可描述为:
#defineMAXNODE/*二叉树的最大结点数*/
typedefelemtypeSqBiTree[MAXNODE]/*0号单元存放根结点*/
SqBiTreebt;
即将bt定义为含有MAXNODE个elemtype类型元素的一维数组。
顺序存储结构的优缺点:
①对完全二叉树而言,顺序存储结构既简单又节省存储空间。
②一般的二叉树采用顺序存储结构时,虽然简单,但易造成存储空间的浪费。
③在对顺序存储的二叉树做插入和删除结点操作时,要大量移动结点。
2.链式存储结构
在顺序存储结构中,利用编号表示元素的位置及元素之间孩子或双亲的关系,因此对于非完全二叉树,需要将空缺的位置用特定的符号填补,若空缺结点较多,势必造成空间利用率的下降。
在这种情况下,应使用链式存储结构。
(1)二叉链表存储
所谓二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。
通常有下面两种形式。
常见的二叉树结点结构如下所示:
Lchild
data
Rchild
其中,Lchild和Rchild是分别指向该结点左孩子和右孩子的指针,data是数据元素的内容。
图6-10为树的链式存储结构示意图。
在C语言中的类型定义为:
typedefstructBTLinklist
{
Elemtypedata;
structBTLinklist*Lchild,*Rchlid;
}BTLinklist,*BTree;
下面是一棵二叉树及相应的链式存储结构
这种存储结构的特点是寻找孩子结点容易,寻找双亲比较困难。
(2)三叉链表存储
每个结点由四个域组成,具体结构为:
其中,data、lchild以及rchild三个域的意义同二叉链表结构;parent域为指向该结点双亲结点的指针。
这种存储结构既便于查找孩子结点,又便于查找双亲结点;但是,相对于二叉链表存储结构而言,它增加了空间开销。
图6-11给出了图6-5(b)所示的一棵二叉树的三叉链表示。
尽管在二叉链表中无法由结点直接找到其双亲,但由于二叉链表结构灵活,操作方便,对于一般情况的二叉树,甚至比顺序存储结构还节省空间。
因此,二叉链表是最常用的二叉树存储方式。
本书后面所涉及到的二叉树的链式存储结构如不加特别说明均指二叉链表结构。
6.2.3遍历二叉树
在对二叉树进行操作时,常需对每个数据元素实施操作,由此提出了二叉树的遍历操作。
所谓遍历二叉树就是按某种顺序访问二叉树中的每个结点一次且仅一次的过程。
这里的访问可以是输出、比较、更新、查看元素内容等等各种操作。
图6-12树的三种遍历示意图
二叉树的遍历方式分为两大类:
一类按根、左子树和右子树三个部分进行访问;另一类按层次访问。
下面分别进行讨论。
按根、左子树和右子树三部分进行遍历,遍历二叉树的顺序存在下面6种可能:
TLR(根左右),TRL(根右左)
LTR(左根右),RTL(右根左)
LRT(左右根),RLT(右左根)
通常采用TLR、LTR和LRT方式遍历二叉数,并分别称为先序遍历、中序遍历和后序遍历。
(1)先序遍历
若二叉树为空,则结束遍历操作;否则
访问根结点;
遍历左子树;
遍历右子树。
(2)中序遍历
若二叉树为空,则结束遍历操作;否则
遍历左子树;
访问根结点;
遍历右子树。
(3)后序遍历
若二叉树为空,则结束遍历操作;否则
遍历左子树;
遍历右子树;
访问根结点。
下面是一棵二叉树及其经过三种遍历得到的相应序列。
中序遍历二叉树时,若将二叉树严格地按左子树的所有结点位于根结点的左侧,右子树的所有结点位于根结点右侧的形式绘制,则可对每个结点做一条垂线,映射到下面的水平线上,由此得到的顺序就是该二叉树的中序遍历序列。
图6-13为树的中序遍历映射示意图。
由此可以看出:
(1)遍历操作实际上是将非线性结构线性化的过程,其结果为线性序列。
遍历操作是一个递归的过程,因此,这三种遍历操作的算法可以用递归函数实现。
1.先序遍历递归算法
voidPreOrder(BTreeBT)
{if(BT)
{Visit(BT);
PreOrder(BT->Lchild);
PreOrder(BT->Rchild);
}
}
2.中序遍历递归算法
voidInOrder(BTreeBT)
{if(BT)
{InOrder(BT->Lchild);
Visit(BT);
InOrder(BT->Rchild);
}
}
3.后序遍历递归算法
voidPostOrder(BTreeBT)
{if(BT)
{PostOrder(BT->Lchild);
PostOrder(BT->Rchild);
Visit(BT);
}
}
上述二叉树先序、中序和后序三种遍历算法都是递归算法。
然而,并非所有程序设计语言都允许递归;另一方面,递归程序虽然简洁,但可读性一般不好,执行效率也不高。
通过对三种遍历方法的实质分析,可得到遍历二叉树的非递归算法,有兴趣的读者可参见有关参考书。
6.2.4二叉树的基本操作
1.输入二叉树的先序序列,构造这棵二叉树
为了保证唯一地构造出所希望的二叉树,在键入这棵树的先序序列时,需要在所有空二叉树的位置上填补一个特殊的字符,比如,'#'。
在算法中,需要对每个输入的字符进行判断,如果对应的字符是'#',则在相应的位置上构造一棵空二叉树;否则,创建一个新结点。
整个算法结构以先序遍历递归算法为基础,二叉树中结点之间的指针连接是通过指针参数在递归调用返回时完成的。
BTreePre_Create_BT()
{getch(ch);
if(ch=='#')returnNULL;//构造空树
else
{BT=(BTree)malloc(sizeof(BTLinklist));//构造新结点
BT->data=ch;
BT->lchild=Pre_Create_BT();//构造左子树
BT->rchild=Pre_Create_BT();//构造右子树
returnBT;
}
}
2.计算一棵二叉树的叶子结点数目
这个操作可以使用三种遍历顺序中的任何一种,只需将访问操作变成判断该结点是否为叶子结点,如果是叶子结点将累加器加1即可。
下面是中序遍历的实现。
voidLeaf(BTreeBT,int*count)
{if(BT)
{Leaf(BT->child,&count);//计算左子树的叶子结点个数
if(BT->lchild==NULL&&BT->rchild==NULL)(*count)++;
Leaf(BT->rchild,&count);//计算右子树的叶子结点个数
}
}
3.交换二叉树的左右子树
voidchange_left_right(BTreeBT)
{if(BT)
{change_left_right(BT->lchild);
change_left_right(BT->rchild);
BT->lchild<->BT->rchild;
}
}
4.求二叉树的高度
使用后序遍历,首先分别求出左右子树的高度,在左右子树较大的高度值加1得出该棵树的高度。
inthight(BTreeBT)
{//h1和h2分别是以BT为根的左右子树的高度
if(BT==NULL)return0;
else{h1=hight(BT->lchild);
h2=hight(BT->right);
returnmax(h1,h2)+1;
}
}
6.3树和森林
6.3.1树的存储结构
树属非线性结构,其存储方式需反映出树中各结点间的非线性关系。
下面介绍三种常用的树的存储结构:
双亲表示法,孩子表示法,孩子兄弟表示法。
1.双亲表示法
在树中,除根结点没有双亲外,其他每个结点的双亲是唯一确定的。
因此,根据树的这一性质,存储树中结点时,可以包含两个信息:
结点的值data和该结点的双亲信息parent。
借助于每个结点的这两个信息便可唯一地表示任何一棵树。
这种表示方法称为双亲表示法。
例如:
图6-14是图6-1(a)所示树的双亲表示法示意。
在C语言中,这种存储形式定义如下:
#defineMAX_TREE_LINKLIST_SIZE10
typedefstructChildLinklist
{intchild;//该孩子结点在一维数组中的下标值
structChileLinklist*next;//指向下一个孩子结点
}CLinklist;
typedefstruct
{Elemtypeinfo;//结点信息
CLinklist*firstchild;//指向第一个孩子结点的指针
}TLinklist;
typedefstruct
{TLinklistelem[MAX_TREE_LINKLIST_SIZE];
intn,root;//n为树中当前结点的数目,root为根结点在一维数组中的位置
}ChildTree;
这种存储方法的特点是寻找结点的双亲很容易,但寻找结点的孩子比较困难。
2.孩子表示法
孩子表示法描述孩子关系。
由于各结点的孩子数可能不同,利用链式存储结构更加合适。
孩子表示法如图6-15所示。
其主体是一个一维数组,数组的每一个元素有两个域组成,一个域用来存放结点信息,另一个用来存放指针,该指针指向由该结点孩子组成的单链表的首位置。