5.1 树的基本概念
5.1.1 树的定义
树是n(n>=0)个结点的有限集,当n=0时,在任一一棵非空树中应该满足:
- 有且仅有一个特定的称为根的结点。
- 当n>1时,其余结点可以分为m(m>0)个互不相交的有限集,T1,T2,...,Tm,其中每个集合本身又是一棵树,并且称为根的子树。
显然,树的定义是递归的,即在树的定义中又用到了其自身,树是一种递归的数据结构。作为一种逻辑结构,具有一下两个特点:
- 树的根节点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中所有结点可以有0个或多个后继。
树适合于表示具有层次结构的数据,树中的某个结点(除根结点外)最多只和上一层的一个结点(即其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的树中有n-1条边,而树中的每个结点与其下一层的0个或多个结点(即其子女结点)有直接关系。
5.1.2 基本术语
- 考虑结点K,根A到结点K的唯一路径上的任意结点,称为结点K的祖先,如结点B是结点K的祖先,而结点K是结点B的子孙,路径上最接近结点K的结点E称为K的双亲,而K为结点E的孩子,根A是树中唯一没有双亲的结点,有相同双亲的结点称为兄弟。如结点K和结点L有相同的双亲E,即K和L为兄弟。
- 树中一个结点的孩子的个数称为该结点的度。树中结点的最大度称为树的度。如结点B的度为2,结点D的度为3,树的度为3。
- 度大于0的结点称为分支结点(又称为非终端结点);度为0(没有子女结点)的结点称为叶子结点(又称终端结点)在分支结点中,每个结点的分支数就是该结点的度。
- 结点的深度、高度和层次:结点的层次从树根开始定义,根结点在第一层,它的子节点在第2层,以此类推,双亲在同一层的结点互为堂兄弟,图中G与E,F,H,I,J互为堂兄弟。结点的深度是从根结点开始自顶向下逐层累加的,结点的高度是从叶结点开始自底逐层累加的。树的高度(或深度)是树中结点的最大层数,图中树的高度为4。
- 有序树和无序树,树中结点的各个子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。
- 路径和路径长度,树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。注意:由于树的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。
- 森林,森林是m(m>=0)棵互不相交的树的集合,森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林。反之,只要给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。
5.1.3 树的性质
- 树的结点树等于所有结点的度数加1.
- 度数为m的树中第i层上之多有个结点(i>=1)
- 高度为h的m叉树至多有(-1)/(m-1)个结点
- 具有n个结点的m叉树的最小高度为.
5.2 二叉树的概念
5.2.1 二叉树的定义及其主要特性
1.二叉树的定义
二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。
与树相似,二叉树也以递归的形式定义。二叉树是n(n>=0)个结点的有限集合:
- 或者为空二叉树,即n=0
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
二叉树是有序树,若将其左、右子树颠倒,则成为另一棵不同的二叉树。即使树中结点只有一棵子树,也要区分他是左子树还是右子树。二叉树基本的五种形态如下所示:
二叉树与度为2的有序树的区别:
- 度为2的树至少有三个结点,而且二叉树可以为空。
- 度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无须区分其左右次序,而二叉树无论其孩子数是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一个结点而言的,而是确定的。
2.几个特殊的二叉树
- 满二叉树:一棵高度为h,且含有个结点的二叉树称为满二叉树。 即树中每层都含有最多的结点。满儿二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点外每个结点的度都为2。 可以对满二叉树按层序编号:约定编号从根结点起,自上而下,自左向右。这样,每个结点对应一个编号,对于编号为i的结点,若有双亲,则其双亲为,若有左孩子,则左孩子为2i,若有右孩子,则右孩子为2i+1
- 完全二叉树:高度为h,有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中的编号为1~n的结点一一对应,称为完全二叉树
完全二叉树特点:
- 若i<=,则结点i为分支结点,否则为叶子结点。
- 叶子结点只可能在层次最大的两层上出现,对于最大层次中的叶子结点,都依次排列在该层的最左边的位置上。
- 若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。
- 按层序编号后,一旦出现某个结点(编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点。
- 若n为奇数,则每个分支结点都有左孩子和右孩子,若n为偶数,则编号最大的分支结点(编n为n/2)只有左孩子,没有右孩子,其余分支结点左右孩子都有。
3.二叉排序树:左子树上所有结点的关键字均小于根结点的关键字,右子树上的所有结点的关键字均大于根结点的关键字,左子树和右子树各是一棵二叉排序树。
4.平衡二叉树:树上的任意几点的左子树和右子树的深度之差不超过1.
3.二叉树的性质
- 非空二叉树上的叶子结点等于度为2的结点数加1,即n0=n2+1;
- 非空二叉树上第k层上至多有个结点(k>=1)
- 高度为h的二叉树至多有个结点(h>=1)
5.2.2 二叉树的存储结构
1.顺序存储结构
二叉树的顺序存储是指用一组连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下表为i-1的分量中。
2.链式存储结构
由于顺序存储的空间利用率低,因此二叉树一般采用链式存储结构,用链表结点来存储二叉树中的每个结点,在二叉树中,结点结构通常包括若干数据域和若干指针域,二叉链表至少包含3个域:数据域data、左指针域lchild和右指针域rchild
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild,*rchild; //左,右孩子指针
}BiTNode,*BITree;
在含有n个结点的二叉链表中,含有n+1个空链域。
5.3 二叉树的遍历和线索二叉树
5.3.1 二叉树的遍历
二叉树的遍历是指按某条搜索路径访问树中的每个结点,使得每个结点均被访问一次,而且仅被访问一次,由于二叉树是一种非线性结构,每个结点都可能有两棵子树,因而需要寻找一种规律,以便使二叉树上的结点能排列在一个线性队列上,进而便于遍历。
由二叉树的递归定义可知,遍历一棵二叉树便要决定对根结点N,左子树L和右子树R的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序(NLR),中序(LNR),和后序(LRN)三种遍历方法,其中“序”指的是根结点在何时被访问。
1.PreOrder
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
2.InOrder
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T);
InOrder(T->rchild); //递归遍历右子树
}
}
3.PostOrder
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
对于三种遍历算法来说,递归遍历左子树,右子树的顺序是固定的,只是访问根结点的顺序不同,时间复杂为O(n),在递归遍历中,递归工作栈深恰好为树的深度,所以在最坏的状况下,二叉树是有n个结点且深度为n的单支树,遍历算法的空间复杂度为O(n)。
4.递归算法和非递归算法的转换
中序遍历非递归算法
void InOrder2(BiTree T){
InitStack(S); BiTree p = T; //初始化栈S; p是遍历指针
while(p||!IsEmpty(S)){ //栈不空或p空时循环
if(p){
Push(S,p);
p=p->lchild;
}else{
Pop(S,p); visit(p);
p=p->rchild;
}
}
}
先序遍历非递归算法
void PreOrder2(BiTree T){
InitStack(S); BiTree p=T; //初始化栈S;p是遍历指针
while(p||!IsEmpty(S)){
if(p){
visit(p); Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);
p=p->rchild;
}
}
}
后序遍历非递归算法
void PostOrder2(BiTree T){
InitStack(S);
p=T;
r=NULL;
while(p||!IsEmpty(S)){
if(p){ //走到最左边
push(S,p);
p=p->lchild;
}
else{
GetTop(S,p);
if(p->rchild&&p->rchild!=r){
p=p->rchild;
push(S,p);
p=p->lchild;
}
else {
pop(S,p);
visit(p->data);
r=p;
p=NULL:
}
}
}
}
层次遍历
要进行层次遍历,需要借助一个队列,先将二叉树根结点入队,然后出队,访问出队结点,若它有左子树,访问出队结点,若它有左子树,则将左子树根结点入队,若它有右子树,则将右子树根结点入队。然后出队,访问出队结点......如此反复,直到队列为空。
void LevelOrder(BiTree T){
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根结点入队
while(!IsEMpty(Q)){ //队列不空则循环
DeQueue(Q,p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild); //左子树不空,则左子树根结点入队
if(p->rchild!=NULL)
EnQueue(Q,p->rchild); //右子树不空,则右子树根结点入队
}
}