目录
一、树的基本概念和术语
1.1 基本定义与特点
(1)基本定义:
树是n(n>=0)个结点的有限集。当n =0时,称为空树。
在任意一棵非空树中应满足:
1)有且仅有一个特定的称为根的结点。
2)当n >1时,其余结点可分为m(m>0)个互不相交的有限集,其中每个集合本身又是一棵树,并且称为根的子树。
例如,在下图中,(a)是只有一个根结点的树;(b)是有13个结点的树,其中A是根,其余结点分成3个互不相交的子集:T1 = {B, E, F, K, L}, T2 = {C, G},T3={D, H, I, J, M}。T1、T2、T3都是根A的子树,且本身也是一棵树。例如T1,其根为B,其余结点分为两个互不相交的子集:T11 = 、{E,K,L}, T12={F}。
图1.1-1 树的示例
树的结构定义是一个递归的定义,即在树的定义中又用到树的定义,树是一种递归的数据结构。树 作为一种逻辑结构,同时也是一种分层结构。
(2)基本特点
1) 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
2) 树中所有结点可以有零个或多个后继。
树适合于表示具有层次结构的数据。树中的某个结点(除根结点外)最多只和上一层的一个结点(即其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的树中有n-1条边。 而树中每个结点与其下一层的零个或多个结点(即其子女结点)有直接关系。
1.2 树的基本术语
依旧结合上面给出的例子来归纳树的基本术语
图1.2-1 树的示例
(1)结点:树中的一个独立单元。包含一个数据元素及若干指向其子树的分支,如图中的A、B、C、D等。
(2) 结点的度:结点拥有的子树数称为结点的度。例如,A的度为3,C的度为1,F的度为0。
(3)树的度:树的度是树内各结点度的最大值。图中所示的树的度为3。
(4) 叶子:度为0的结点称为叶子或终端结点。结点K、L、F、G、M、I、J都是树的叶子。
(5)非终端结点:度不为0的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。
(6)双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B的双亲为A; B的孩子有E和F。
(7)兄弟:同一个双亲的孩子之间互称兄弟。例如,H、I和J互为兄弟。
(8)祖先:从根到该结点所经分支上的所有结点。例如,M的祖先为A、D和H。
(9)子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。如B的子孙为E、K、L、F。
(10)层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加1。
(11) 堂兄弟:双亲在同一层的结点互为堂兄弟。例如,结点G与E、F、H、I、J互为堂兄弟。
(12)树的深度:树中结点的最大层次称为树的深度或高度。图中所示的树的深度为4。
(13)有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换), 则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
(14)森林:是n(n>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以用森林和树相互递归的定义来描述树。
(15)路径和路径长度:树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成 的,而路径长度是路径上所经过的边的个数。
注意:由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。
1.3 树的基本性质
树具有如下最基本的性质:
- 1) 树中的结点数等于所有结点的度数之和加1。
- 2) 度为m的树中第i层上至多有结点。
- 3) 高度为h的m叉树至多有个结点。
- 4) 具有n个结点的m叉树的最小高度为向上取整。
二、二叉树
2.1 二叉树的定义与性质
(1)基本概念
二叉树(Binary Tree )是n(n>=0)个结点所构成的集合,它或为空树(n=0);或为非空树(n>0), 对于非空树T:
1)有且仅有一个称之为根的结点;
2)除根结点以外的其余结点分为两个互不相交的子集T1和T2 分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。
(2)与树的区别
二叉树与树一样具有递归性质,二叉树与树的区别主要有以下两点:
1)二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点);
2)二叉树的子树有左右之分,其次序不能任意颠倒。
二叉树的递归定义表明二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。由于这两棵子树也是二叉树,则由二叉树的定义,它们也可以是空树。由此,二叉树可以有5种基本形态,如下图所示:
图2.1-1 二叉树的5种基本形态
注意:区分m叉树和树的度为m。m叉树指一个节点最多有m个分支,可以理解为一种要求。树的度为m指树最少有一个节点最多有m个分支,可以理解为一种结果。
下面给出官方给的区别:
二叉树与度为2的有序树的区别:
- 度为2的树至少有3个结点,而二叉树可以为空。
- 度为2的有序树的孩子的左右次序是相对于另一孩子而言的,若某个结点只有一个孩子, 则这个孩子就无须区分其左右次序,而二叉树无论其孩子数是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一结点而言,而是确定的。
(3)几个特殊的二叉树
1) 满二叉树:一棵高度为知且含有2^h-1个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点,满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点度数均为2。
可以对满二叉树按层序编号:约定编号从根结点(根结点编号为1)起,自上而下,自左向右。这样,每个结点对应一个编号,对于编号为i的结点,若有双亲,则其双亲为i/2向下取整, 若有左孩子,则左孩子为2i;若有右孩子,则右孩子为2i+l。
图2.1-2 满二叉树
- 若i<=n/2向下取整,则结点i为分支结点,否则为叶子结点。
- 叶子结点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。
- 若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。
- 按层序编号后,一旦出现某结点(编号为i)为叶子结点或只有左孩子,则编号大于i 的结点均为叶子结点。
- 若n为奇数,则每个分支结点都有左孩子和右孩子;若n为偶数,则编号最大的分支结点(编号为n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
图2.1-3 完全二叉树
3) 二叉排序树:左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字;左子树和右子树又各是一棵二叉排序树。
4) 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。
2.2 二叉树的性质
1) 非空二叉树上的叶子结点数等于度为2的结点数加1,即为
证明:
设度为0, 1和2的结点个数分别为, 和,结点总数再看二叉树中的分支数,除根结点外,其余结点都有一个分支进入,设B为分支总数, 则n = B+1,由于这些分支是由度为1或2的结点射出的,所以又有 , 于是得,则
2) 非空二叉树上第k层上至多有个结点(k>=1)
第1层至多有个结点(根),第2层至多有个结点,以此类推,用数学归纳法可以证明其为一个公比为2的等比数列2。
3) 高度为h的二叉树至多有个结点(h>=1)
该结论利用性质2求前h项的和,即等比数列求和的结果。
4) 对完全二叉树按从上到下、从左到右的顺序依次编号1,2,…n,则有以下关系:
- 当i>l时,结点,的双亲的编号为i/2向下取整,即当i为偶数时,其双亲的编号为i/2,它是双亲的左孩子;当i为奇数时,其双亲的编号为(i - 1)/2,它是双亲的右孩子。
- 当2i<=n时,结点i的左孩子编号为2i,否则无左孩子。
- 当2i+1<=n时,结点i的右孩子编号为2i+l,否则无右孩子。
- 结点i所在层次(深度)为向下取整
图2.2-1 完全二叉树中结点i和i+1的左右孩子
5) 具有n个(n>0)结点的完全二叉树的高度为向上取整或向下取整
设高度为h,根据性质3和完全二叉树的定义有
或
得,因为h为正整数,所以h=向上取整或得 向下取整
2.3 二叉树的存储结构
(1)二叉树的顺序存储
顺序存储结构使用一组地址连续的存储单元来存储数据元素,为了能够在存储结构中反映出结点之间的逻辑关系,必须将二叉树中的结点依照一定的规律安排在这组单元中。
对于完全二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储结点元素,将完全二叉树上编号为i的结点元素存储在如上定义的一维数组中下标为i-1的分量中。
但对于一般的二叉树,为了让数组下标能反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中。图中以“0”表示不存在此结点。
图2.3-1 二叉树的顺序存储结构
由此可见,这种顺序存储结构仅适用于完全二叉树。因为,在最坏的情况下,一个深度为k 且只有k个结点的单支树(树中不存在度为2的结点)却需要长度为的一维数组。这造成了 存储空间的极大浪费,所以对于一般二叉树,更适合采取下面的链式存储结构。
(2)二叉树的链式存储
由二叉树的定义得知,二叉树的结点由一个数据元素和分别指向其左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含3个域:数据域和左、右指针域。有时,为了便于找到结点的双亲,还可在结点结构中增加一个指向其双亲结点的指针域,。利用这两种结点结构所得二叉树的存储结构分别称之为二叉链表和三叉链表。
图2.3-2 二叉树的结点及其存储结构
链表的头指针指向二叉树的根结点。容易证得,在含有n个结点的二叉链表中有n + 1个空链域。
图2.3-3 二叉链表存储结构
图2.3-4 链表存储结构
// 二叉树的二叉链表存储表示
typedef struct BiTNode
{ TElemType data; //数据域
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
// 二叉树的三叉链表存储表示
typedef struct TriTNode
{ TElemType data; //数据域
struct TriTNode *lchild, *rchild, *parent;
} TriTNode, *TriTree;
二叉树的二叉链表存储寻找双亲时需要从头遍历,但三叉链表存储则无需。
2.4 二叉树的遍历
(1)先中后序遍历(递归实现与栈实现)
遍历二叉树是指按某条搜索路径巡访树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。
遍历二叉树是二叉树最基本的操作,也是二叉树其他各种操作的基础,遍历的实质是对二叉树进行线性化的过程,即遍历的结果是将非线性结构的树中结点排成一个线性序列。
回顾二叉树的递归定义可知,二叉树是由3个基本单元组成:根结点、左子树和右子树。因此,若能依次遍历这三部分,便是遍历了整个二叉树。假如从L、D、R分别表示遍历左子树、访问根结点和遍历右子树,则可有DLR、LDR、LRD、DRL、RDL、RLD这6种遍历二叉树的方案。 若限定先左后右,则只有前3种情况,分别称之为先(根)序遍历、中(根)序遍历和后(根) 序遍历。
注意:先中后序中的序指的是访问根节点的顺序。
先序遍历二叉树的操作定义如下:
若二叉树为空,则空操作;否则
(1)访问根结点;(2)先序遍历左子树;(3)先序遍历右子树。
//递归实现先序遍历算法
void InOrder(BiTree T)
{
if(T!=NULL)
{
printf("%d\t",T->data);//访问根节点
InOrder(T->lchild); //递归访问左子树
InOrder(T->rchild);//递归访问右子树
}
}
中序遍历二叉树的操作定义如下:
若二叉树为空,则空操作;否则
(1)中序遍历左子树;(2)访问根结点;(3)中序遍历右子树。
//递归实现中序遍历算法
void InOrder(BiTree T)
{
if(T!=NULL)
{
InOrder(T->lchild); //递归访问左子树
printf("%d\t",T->data);//访问根节点
InOrder(T->rchild);//递归访问右子树
}
}
后序遍历二叉树的操作定义如下:
若二叉树为空,则空操作;否则
(1)后序遍历左子树;(2)后序遍历右子树;(3)访问根结点。
//递归实现后序遍历算法
void InOrder(BiTree T)
{
if(T!=NULL)
{
InOrder(T->lchild); //递归访问左子树
InOrder(T->rchild);//递归访问右子树
printf("%d\t",T->data);//访问根节点
}
}
图2.4-1 三种遍历过程示意图
其中,向下的箭头表示更深 一层的递归调用,向上的箭头表示从递归调用退出返回;虚线旁的三角形、圆形和方形内的字符分别表示在先序、中序和后序遍历的过程中访问结点时输出的信息。
例如,由于中序遍历中访问结点是在遍历左子树之后、遍历右子树之前进行的,则带圆形的字符标在向左递归返回和向右递归调用之间。由此,只要沿虚线从1出发到2结束,将沿途所见的三角形(或圆形或方形)内的字符记下,便得到遍历二叉树的先序(或中序或后序)序列。
举如下一个例子:
图2.4-2 表达式为a + b *(c - d)- e/f的二叉树
- 若先序遍历此二叉树,可得到二叉树的先序序列为-+ a * b - cd/ef
- 类似地,中序遍历此二叉树,可得此二叉树的中序序列为a + b*c-d-e/f
- 后序遍历此二叉树,可得此二叉树的后序序列为abed- * + ef/ -
从表达式来看,以上3个序列恰好为表达式的前缀表示、中缀表示和后缀表示。
以下为栈实现:
图2.4-3 二叉树
借助栈,我们来分析中序遍历的访问过程:
①沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点,此时栈内 元素依次为ABD。
②栈顶元素出栈并访问:若其右孩子为空,继续执行②;若其右孩 子不空,将右子树转执行①。栈顶D出栈并访问,它是中序序列的第一个结点;D右孩子为空,栈顶B出栈并访问;B右孩子不空,将其右孩子E入栈,E右孩子为空,栈顶E岀栈并访问;A右孩子不空,将其右孩子C入栈,C 左孩子为空,栈顶C出栈并访问。
由此得到中序序列DBEAC.
以下代码以中序遍历为例,先序和后序类似。
//中序遍历栈实现
void InorderTraverse(BiTree T)
{
InitStack(S);//初始化栈
p = T;
while(p||!StackEmpty(S))//树和栈至少一个不为空
{
if(p)
{
Push(S,p); //入栈
p =p->lchild;} //向左下走到头
else
{ Pop(S,p);//出栈
printf ("%d\t",p->data);
p = p->rchild; //向右走一步
}
}
}
(2)层次遍历
图2.4-4 二叉树的层次遍历
如图所示为二叉树的层次遍历,即按照箭头所指方向,按照1,2, 3, 4的层次顺序,对二叉树中的各个结点进行访问。
利用队列实现二叉树的层次遍历非递归算法:
- 将二叉树的根结点入队
- 队头元素出队并访问,将其非空左、右孩子入队(即以从左向右的顺序将下一层结点保存在队列中)
- 重复2直到队空为止
void LevelTrave(BiTree T)
{ int f=0,r=0;
BiTree p, q[M];
q[r++]=T;//根节点入队
while(f<r)//队列不为空时
{
p=q[f++]; //出队
printf("%c\t",p->data);
if(p->lchild)
q[r++]=p->lchild;
if(p->rchild)
q[r++]=p->rchild;
}
}
(3)根据遍历序列确定二叉树
- 由二叉树的先序序列和中序序列可以唯一地确定一棵二叉树。
- 由二叉树的后序序列和中序序列也可以唯一地确定一棵二叉树。
- 由二叉树的层序序列和中序序列也可以唯一地确定一棵二叉树。
注意:根据遍历序列确定二叉树必须要有中序序列、其余序列任选一个。
图2.4-5 根据遍历序列确定二叉树
- 以下以先序和中序序列为例:
在先序遍历序列中,第一个结点一定是二叉树的根结点;而在中序遍历中,根结点必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,后一个子序列是根结点的右子树的中序序列。根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中,左子序列的第一个结点是左子树的根结点,右子序列的第一个结点是右子树的根结点。如此递归地进行下去,便能唯一地确定这棵二叉树。
三、二叉树的经典实例
3.1 统计二叉树中叶子结点的个数
其基本思路为左子树叶子数+右子树叶子数。采用递归的方式实现。
void LeafCount (BiTree T)
{
static int count://定义静态变量,保证在递归的时候变量不丢失。或者可以直接调用函数传输两个值
if ( T )
{
if ((T->lchild==NULL)&& (T->rchild==NULL))//判断T是否为叶子结点
count++;
else
{
count=LeafCount( T->lchild); //求T左右子树上的叶子结点数。每次递归调用、当T为叶子
//时count自加
count=LeafCount( T->rchild);
}
}
return count;
}
3.2 求二叉树的深度(后序遍历)
如果是空树,递归结束,深度为0,否则执行以下操作:
- •递归计算左子树的深度记为m;
- •递归计算右子树的深度记为n;
- •如果m大于n,二叉树的深度为m+1,否则为n+1。
其实就是求max{左子树深度+1,右子树深度+1},其处理方式依旧为递归。
int Depth (BiTree T )
{
int depthval, depthLeft, depthRight;
if ( T==NULL ) depthval = 0;//T为空树时返回0
else
{
depthLeft = Depth( T->lchild );//递归求左子树深度
depthRight= Depth( T->rchild );//递归求右子树深度
depthval=depthLeft>depthRight ? depthLeft+1:depthRight+1;
}
return depthval;
}
3.3 复制二叉树(后序遍历)
复制二叉树就是利用已有的一棵二叉树复制得到另外一棵与其完全相同的二叉树。根据二叉树的特点,复制步骤如下:
若二叉树不空,则首先复制根结点,这相当于二叉树先序遍历算法中访问根结点的语句;然后分别复制二叉树根结点的左子树和右子树,这相当于先序遍历中递归遍历左子树和右子树的语句。因此,复制函数的实现与二叉树先序遍历的实现非常类似。
void Copy(BiTree T,BiTree &NewT)
{
//复制一棵和T完全相同的二叉树
if(T==NULL)
{
NewT=NULL;
return;
}
else
{
NewT=(BiTree*)malloc(sizeof(BiTree));//复制根节点
NewT-> data=T-> data;
Copy(T->lchild,NewT-> lchild);//递归左子树
Copy(T-> rchild,NewT-> rchild);//递归右子树
}
}
3.4 已知部分遍历序列构建二叉树
图3.4-1 以层次序列和中序序列为例
void CrtBT(BiTree& T, char pre[], char ino[],int ps, int is, int n ) {
// 已知pre[ps..ps+n-1]为二叉树的先序序列,
// ino[is..is+n-1]为二叉树的中序序列,本算
// 法由此两个序列构造二叉链表
if (n==0) T=NULL;
else {
k=Search(ino, pre[ps]); // 在中序序列中查询
if (k== -1) T=NULL;
else
{
T=(BiTNode*)malloc(sizeof(BiTNode));
T->data = pre[ps];
if (k==is) T->Lchild = NULL;
else CrtBT(T->Lchild, pre[], ino[], ps+1, is, k-is );
if (k=is+n-1) T->Rchild = NULL;
else CrtBT(T->Rchild, pre[], ino[], ps+1+(k-is), k+1, n-(k-is)-1 );
}
} //
} // CrtBT