第五章 树和二叉树
5.1 树的逻辑结构
5.1.1 树的定义和基本术语
1.树的定义
在树中常常将数据元素称为结点。
树是n(n>=0)个结点的有限集合。当n=0时,称为空树;任意
(1)有且仅有一个特定的称为根;
(2)当n>1时,除根结点之外的其余结点被分成m(m>0)个互不相交的有限集合T1,T2,…,Tm,其中每个集合又是一棵树,并称为这个根结点的子树。
2.树的基本术语
结点的度、树的度
某结点所拥有的子树的个数称为该结点的度;树中各结点度的最大值称为该树的度。
叶子结点、分支结点
度为0的结点称为叶子结点,也称为终端结点;度不为0的结点称为分支结点,也称为非终端结点。
孩子结点、双亲结点、兄弟结点
某结点的子树的根结点称为该结点的孩子结点;反之,该结点称为其孩子结点的双亲结点;具有同一个双亲结点的孩子结点互称为兄弟结点。
路径、路径长度
如果数的结点序列n1,n2,、、、,nk满足如下关系:结点ni是结点ni+1d的双亲(1<=i<k),则把n1,n2,、、、,nk称为一条由n1至nk的路径;路径上经过的边数称为路径长度。在树中路径是唯一的。
祖先、子孙
如果从结点x到结点y有一条路径,那么x就称为y的祖先,y称为x的子孙。以某结点为根的子树中的任一结点都是该结点的子孙。
结点的层数、树的深度(高度)
规定结点的层数为1,对其余任何结点,若该结点在第k层,则其孩子结点在第k+1层;树中所有结点的最大层数称为树的深度,也称为树的高度。
层序编号
将树中结点按照从上层到下层、同层从左层到右层的次序依次给它们编以从1开始的连续自然数,树的这种编号方式称为层序编号。
有序树、无序树
如果一棵树中结点的各子树从左到右是有次序的,即若交换了结点各子树的相对位置,则构成不同的树,称这棵树为有序树;反之,称为无序树。
森林
m(m>=0)棵互不相交的树的集合构成森林。任何一棵树,删去根结点就变成了森林。
5.1.2 树的抽象数据类型定义
树的抽象数据类型定义的例子:
ADT Tree
Data
树是由一个根结点和若干棵子树构成,树中结点具有相同数据类型及层次关系
Operation
InitTree
前置条件:树不存在
输入:无
功能:初始化一棵树
输出:无
后置条件:构造一棵树
DestroyTree
前置条件:树已存在
输入:无
功能:销毁一棵树
输出:无
后置条件:释放该树占用的存储空间
PreOrder
前置条件:树已存在
输入:无
功能:前序遍历树
输出:树的前序遍历序列
后置条件:树保持不变
PostOrder
前置条件:树已存在
输入:无
功能:后序遍历树
输出:树的后序遍历序列
后置条件:树保持不变
LeverOrder
前置条件:树已存在
输入:无
功能:层序遍历树
输出:树的层序遍历序列
后置条件:树保持不变
EndADT
5.1.3 树的遍历操作
树的遍历是指从根结点出发,按照某种次序访问树中所有结点,使得每个结点被访问一次且仅被访问一次。
1.前序遍历
若树为空,则空操作返回;否则
(1) 访问根结点;
(2) 按照从左到右的顺序前序遍历根结点的每一棵子树。
2.后序遍历
若树为空,则空操作返回;否则
(1) 按照从左到右的顺序后序遍历根结点的每一棵子树。
(2) 访问根结点;
3.层序遍历(广度遍历)
从树的第一层(即根结点)开始,自上而下逐层遍历,在同一层中,按照
左到右的顺序对结点逐个访问。
5.2树的存储结构
5.2.1双亲表示法
由树的定义可知,树中的每个结点都有且仅有一个双亲结点。根据这一特性,可以用一维数组存储树的各个节点,数组中的一个元素对应树中的一个结点,数组元素包括树中结点的数据信息以及该结点双亲在数组中的下标。树的这种存储方式称为双亲表示法,它实质上是一个静态链表.data为数据域,存储树中结点的数据信息;parent为指针域即游标,存储该结点的双亲在数组中的下标。
5.2.2孩子表示法
1. 多重链表表示法
由于树中的每个结点都可能有多个孩子,因此,链表中的每个结点包括一个数据域和多个指针域,每个指针域指向该结点的一个孩子的结点,由于树中的各结点的度不同,因此指针域的设置有两种方法。
① 指针域的个数等于该结点的度
② 指针域的个数等于树的度。
对于方法①,虽然在一定程度上节约了存储空间,但由于链表中各结点是不同构的,树的各种操作是不容易实现的,所以这种方法很少采用;对于方法②,链表中各结点是同构的,各种操作相对容易实现,但为此付出的代价是存储空间的浪费
⒉孩子链的表表示法
孩子链表表示法是一种用多个单链表表示树的方法,即把每个结点的孩子排列起来,看成是一个线性表,且以单链表存储,称为该结点的孩子链表。则n个结点共有n个孩子链表(叶子结点的孩子链表为空表)。这n个单链表共有n个头指针,这n个头指针又构成了一个线性表,为了便于进行查找操作,可采用顺序存储。最后,将存放n个头指针数组和存放n个结点数据信息的数组结合起来,构成孩子链表的表头数组。所以在孩子链表表示法中,存在两类结点:孩子结点和表头结点。
孩子链表表示法不仅表示了孩子结点的信息,而且链在同一个单链表中的结点具有兄弟关系。与双亲表示法相反,在孩子链表表示法中查找双亲比较困难。把双亲表示法和孩子链表表示法结合起来,就形成了双亲孩子表示法。
5.2.3双亲孩子表示法
双亲孩子表示法是将双亲表示法和孩子链表表示法相结合的存储方法。仍将各结点的孩子结点分别组成单链表,同时用一维数组顺序存储树中的各个结点,数组元素除了包括结点的数据信息和该结点孩子链表的头指针之外,还增设一个域存储该结点双亲结点在数组中的下标。
5.2.4孩子兄弟表示法
树的孩子兄弟表示法又称为二叉链表表示法,其方法是链表中的每一个结点除数据域外,还设置了两个指针分别指向该结点的第一个孩子和右兄弟。
可用C++语言的结构体类型定义上述结点
Template <class DataType>
Struct TNote
{
DataType data;
TNode<DataType>*firstchild,*rightsib;
};
这种存储方法便于实现树的各种操作。例如,若要访问某结点x的第i个孩子,只需从该结点的第一个孩子指针找到第一个孩子后,沿着孩子的结点的右兄弟域连续走i-1步,便可找到结点x的第i个孩子。
5.3二叉树的逻辑结构
二叉树是一种最简单的树结构,特别适合计算机处理,而且任何树都可以简单地转换为二叉树。
5.3.1二叉树的定义
二叉树:是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两颗互不相交的、分别称为左子树和右子树的二叉树组成。
特点:①每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点;
②二叉树是有序的,其次序不能任意颠倒,即使树中的某个结点只有一颗子树,也要区分它是左子树还是右子树。
所以 二叉树和树是两种结构!
特殊二叉树
1. 斜树
所有结点都只有左子树的结点称为左斜树,所有结点都只有右子树的结点称为右斜树;左斜树和右斜树统称为斜树。
在斜树中,每一层只有一个结点,所以斜树的结点个数与其深度相同。
2.满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
特点:①叶子只能出现在最下一层;
②只有度为0和度为2的结点。
3.完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
(满二叉树从最右下角顺序的删除若干个结点)
特点:①叶子结点只能出现在最下两层,且最下层的叶子结点都集中在二叉树左侧连续的位置;
②如果与度为1的结点,只可能有一个,且该结点只能有左孩子;
③深度为k的完全二叉树在k-1层上一定是满二叉树;
④在同样结点个数的二叉树中,完全二叉树的深度最小。
5.3.2 二叉树的基本性质
性质5-1 二叉树的第i层上最多有2i-1个结点(i≥1)。
性质5-2 在一棵深度为k的二叉树中,最多有2k-1个结点,最少有k个结点。
性质5-3 在一棵二叉树中,如果叶子结点的个数为n0,度为2的结点个数为n2,则n0=n2+1。
性质5-4 具有n个结点的完全二叉树的深度为[log2n]+1。
性质5-5 对一棵具有n个结点的完全二叉树中的结点从1开始按层序编号,则对于任意的编号为i(1≤i≤n)的结点(简称为结点i),有:
(1) 双亲为i/2;
(2) 左孩子为2i;
(3) 右孩子为2i+1。
5.3.3 二叉树的抽象数据类型定义
二叉树的抽象数据类型定义的例子:
ADT BiTree
Data
二叉树是由一个根结点和两棵互不相交的左右子树构成
二叉树中的结点具有相同数据类型及层次关系
Operation
InitBiTree
前置条件:无
输入:无
功能:初始化一棵二叉树
输出:无
后置条件:构造一棵空的二叉树
DestroyBiTree
前置条件:二叉树已存在
输入:无
功能:销毁一棵二叉树
输出:无
后置条件:释放该二叉树占用的存储空间
PreOrder
前置条件:二叉树已存在
输入:无
功能:前序遍历二叉树
输出:二叉树的前序遍历序列
后置条件:二叉树不变
InOrder
前置条件:二叉树已存在
输入:无
功能:中序遍历二叉树
输出:二叉树的中序遍历序列
后置条件:二叉树不变
PostOrder
前置条件:二叉树已存在
输入:无
功能:后序遍历二叉树
输出:二叉树的后序遍历序列
后置条件:二叉树不变
LeverOrder
前置条件:二叉树已存在
输入:无
功能:层序遍历二叉树
输出:二叉树的层序遍历序列
后置条件:二叉树保持不变
endADT
5.3.4二叉树的遍历操作
1.前序遍历
前序遍历二叉树的操作定义为:
若二叉树为空,则空操作返回;否则
(1) 访问根结点;
(2) 前序遍历根结点的左子树;
(3) 前序遍历根结点的右子树。
2. 中序遍历
中序遍历二叉树的操作定义为:
若二叉树为空,则空操作返回;否则
(1) 中序遍历根结点的左子树;
(2) 访问根结点;
(3) 中序遍历根结点的右子树。
3. 后续遍历
后续遍历二叉树的操作定义为:
若二叉树为空,则空操作返回;否则
(1) 后续遍历根结点的左子树;
(2) 后续遍历根结点的右子树;
(3) 访问根结点。
4. 层序遍历
二叉树的层序遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,则按从左至右的顺序对结点逐个访问。
5.4 二叉树的存储结构及实现
存储二叉树的关键是如何表示节点之间的逻辑关系,也就是双亲和孩子之间的关系。在具体应用中,可能要求从任一节点能直接访问到它的孩子,或直接访问到它的双亲,或同时访问到其双亲和孩子。
5.4.1 顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的节点,并且用节点的存储位置(下标)表示节点之间的逻辑关系——父子关系。
完全二叉树中的节点的层序编号可以唯一的反映节点的逻辑关系。对于一般的二叉树,可以增添一些并不存在的空节点,使之成为一颗完全二叉树的形式,然后再用一维数组存储,具体步骤如下:
(1) 将二叉树按完全二叉树编号。
(2) 将二叉树中的结点以编号顺序存储到一维数组中。
5.4.2二叉链表
二叉树一般多采用二叉链表(bianry linked list)存储,其基本思想是:令二叉树的每个结点对应一个链表结点,链表结点除了存放与二叉树结点有关的数据信息外,还要设置指示左右孩子的指针。
可以用C++语言中的结构体类型描述二叉链表的结点,由于二叉树中结点的数据类型不确定,所以采用C++的模板机制。
将二叉树的抽象数据类型定义在二叉链表存储结构下用C++语言的类实现。
1. 前序遍历
由二叉树前序遍历的操作定义,可以很容易的写出前序遍历的递归算法。
二叉树前序遍历递归算法PreOrder
template <class DataType>
void BiTree<DataType>::PreOrder(BiNode<DataType>*bt)
{
if(bt==NULL)
else{
cout<<bt->data;
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
}
2. 中序遍历
根据二叉树中序遍历的操作定义,可以很容易的写出中序遍历的递归算法。
二叉树中序遍历递归算法 InOrder
template <class DataType>
void BiTree<DataType>::InOrder(BiNode<DataType>*bt)
{
if(bt==NULL)return;
else{
InOrder(bt->lchild);
cout<<bt->data;
InOrder(bt->rchild);
}
}
3. 后序遍历
根据二叉树后序遍历的操作定义,可以很容易的写出后序遍历的递归算法。
二叉树后续遍历递归算法PostOrder
template <class DataType>
void BiTree<DataType>::PostOrder(BiNode<DataType>*bt)
{
if(bt==NULL)return;
else{
PostOrder(bt->lchild);
PostOrder(bt->rchild);
cout<<bt->data;
}
}
4. 层序遍历
遍历从二叉树的根节点开始,首先将根指针入队,然后从队头取出一个元素并执行下述操作:
(1) 访问该指针所指结点;
(2) 若该指针所指结点的左,右孩子结点非空,则将其左孩子指针和右孩子指针入队。
二叉树层序遍历算法 LeverOrder
template <class DataType>
void BiTree<DataType>::LeverOrder()
{
front=rear=-1;
if(root==NULL)return;
Q[++rear]=root;
while(front!=rear)
{
q=Q[++front];
cout<<q->data;
if(q->lchild!=NULL)Q[++rear]=q->lchild;
if(q->rchild!=NULL)Q[++rear]=q->rchild;
}
}
5. 构造函数
扩展二叉树的一个遍历序列就能唯一确定一颗二叉树。
建立二叉链表算法 Creat
template <class DataType>
BiNode<DataType>*BiTree<DataType>::Creat(BiNode<DataType>*bt)
{
cin>>ch;
if(ch=='#')bt=NULL;
else{
bt=new BiNode;bt->data=ch;
bt->lchild=creat(bt->lchild);
bt->rchild=creat(bt->rchild);
}
return bt;
}
6. 析构函数
二叉链表属于动态存储分配,需要在析构函数中释放二叉链表中的所有结点。
释放二叉链表算法 Release
template <class DataType>
void BiTree<DataType>::Release(BiNode<DataType>*bt)
{
if(bt!=NULL){
Release(bt->lchild);
Release(bt->rchild);
delete bt;
}
}
5.4.3 三叉链表
这种存储结构即便于查找孩子结点,又便于查找双亲结点。但是,相对于二叉链表而言,它增加了空间开销。
5.4.4 线索链表
按照某种遍历次序对二叉树进行遍历,可以把二叉树的所有结点排成一个线性序列。
1. 构造函数
建立二叉链表(带线索标志)算法 Creat
ThrNode <DataType>*InThrTree<DataType>::Creat(ThrNode<DataType>*bt)
{
cin>>ch;
if(ch=='#')bt=NULL;
else{
bt=new ThrNode;bt->data=ch;
bt->ltag=0;bt->rtag=0;
bt->lchild=Creat(bt->lchild);
bt->rchild=Creat(bt->rchild);
}
return bt;
}
中序线索化链表算法 ThrBiTree
template <class DataType>
void InThrBiTree<DataType>::ThrBiTree(ThrNode<DataType>*bt,ThrNode<DataType>*pre)
{
if(bt==NULL)return;
ThrBiTree(bt->lchild,pre);
if(bt->lchild==NULL){
bt->ltag=1;
bt->lchild=pre;
}
if(bt->rchild==NULL)bt->rtag=1;
if(pre->rtag=1)per->rchild=bt;
pre=bt;
ThrBiTree(bt->rchild,pre);
}
中序线索链表构造函数算法 InThrBiTree
template <class DataType>
InThrBiTree<DataType>::InThrBiTree()
{
root=creat(root);
pre=NULL;
ThrBiTree(root,pre);
}
2.查找后继结点
中序线索链表查找后继算法 Next
template <class DataType>
ThrNode<DataType>*InThrBiTree<DataType>::Next(ThrNode<DataType>*p)
{
if(p->rtag==1)q=p->rchild;
else{
q=p->rchild;
while(q->ltag==0)
q=q->lchild;
}
return q;
}
3.遍历操作
中序线索链表的遍历算法 InOrder
template <class DataType>
void InThrBiTree<DataType>::InOrder()
{
if(root==NULL)return;
p=root;
while(p->ltag==0)
p=p->lchild;
cout<<p->data;
while(p->rchild!=NULL)
{
p=next(p);
cout<<p->data;
}
}
5.5二叉树遍历的非递归算法
5.5.1前序遍历的非递归算法
二叉树前序遍历非递归算法的关键是:在前序遍历过某结点的整个左子树后,如何找到该结点的右子树的根指针。
分析二叉树前序遍历的执行过程可以看出,在访问完该结点后,应将该结点的指针保存在栈中,一遍以后能通过它找到该结点的右子树。一般的,在前序遍历中,设要遍历二叉树的根指针为bt,可能有两种情况:
⑴若b!=NULL,则表明当前的二叉树不空,此时,应输出根结点bt的值并将bt的值并将bt保存中栈中,准备继续遍历bt的左子树。
⑵若b=NULL,,则表明以bt为根指针的二叉树的遍历完毕,并且bt是栈顶指针所指结点的左子树,若栈不空,则应根据栈顶指针所指结点找到待遍历右子树的根指针并赋予bt,以继续遍历下去;若栈空,则表明整个二叉树遍历完毕,应结束。
二叉树前序遍历非递归算法PreOrder
template <class DataType>
void BiTree<DataType>::PreOrder (BiNode<DataType> * bt)
{
top=-1;
while (bt!=NULL||top!=-1)
{
while(bt!=NULL)
{
cout<<bt->data;
s[++top]=bt;
bt=bt->lchild;
}
if(top!=-1){
bt=s[top--];
bt=bt->rchild;
}
}
}
5.5.2中序遍历非递归算法
在二叉树的中序遍历中,访问结点的操作发生在该结点的左子树遍历完毕并准备遍历右子树时,所以,在遍历过程中遇到某结点时并不能立即访问它,而是将它压栈,等到它的左子树遍历完毕后,再从栈中弹出并访问之。中序遍历的非递归算法只需将前序遍历的非递归算法中的输出语句cout<<bt->data移到bt=s[top--]之后即可。
二叉树中序遍历非递归算法
template <class DataType>
void BiTree<DataType>::InOrder (BiNode<DataType> *bt)
{
top=-1;
while(bt!=NULL||top!=-1)
{
while(bt!=NULL)
{
s[++top]=bt;
bt=bt->lchild;
}
if(top!=-1){
bt=s[top++];
cout<<root->data;
bt=bt->rchild;
}
}
}
5.5.3后序遍历非递归算法
后序遍历而后前序遍历和中序遍历不同在后序遍历中,结点要入两次栈,出两次栈,这两种含义和处理方法为:
⑴第一次出栈:只遍历完左子树,右子树尚未遍历,则该结点不出栈,利用栈顶结点找到它的右子树,准备遍历它的右子树。
⑵第二次出栈:遍历完右子树,将该结点出栈,并访问它。
因此为了区别同一个结点的两次出栈,设置标志flag。令:
Flag== 1 第一次出栈,只遍历完左子树,该结点不能访问
2 第二次出栈,遍历完右子树,该结点可以访问
用c++语言中的结构类型来定义栈元素的类型。
Template <class DataType>
Struct element
{
BiNode<DataType> *ptr;
Int flag;
};
设根指针为bt,则可能有以下两种情况:
⑴若bt不等于NULL,则bt及标志flag(置为1)入栈,遍历其左子树。
⑵若bt等于NULL,此时若栈空,则整个遍历结束;若 栈不空,则表明栈结点的左子树或右子树已遍历完毕。若栈顶结点的标志flag=1,则表明栈顶结点的左子树已遍历完毕,将flag修改为2,并遍历栈顶结点的右子树;若栈顶结点的标志flag=2,则表明栈顶结点的右子树也遍历完毕,输出栈顶结点。
二叉树后序遍历非递归算法PostOrder
template <class DataType>
void BiTree<DataType>::PostOrder (BiNode<DataType> *bt)
{
top=-1;
while(bt!=NULL||top!=-1)
{
while(bt!=NULL)
{
top++;
s[top].ptr=bt;s[top].flag=1;
bt=bt->lchild;
}
while (top!=-1 && s[top].flag==2)
{
bt=s[top--].ptr;
cout<<bt->data;
}
if(top!=-1){
s[top].flag=2;
bt=s[top].ptr->rchild;
}
}
}
5.6 树、森林与二叉树的转换
树的孩子兄弟表示法实质上是二叉树的二叉链表存储形式,第一个孩子指针和右兄弟指针分别相当于二叉链表的左孩子指针和右孩子指针。
给定一棵树,可以找到唯一的一棵二叉树与之对应。
1. 树转换为二叉树
(1) 加线——树中所有相邻兄弟结点之间加一条连线;
(2) 去线——对树中的每个结点,只保留它与第一个孩子结点之间的连线,删去它与其他孩子结点之间的连线。
(3) 层次调整——以根结点为轴心,将树顺时针转动一定的角度,使之层次分明。
树的遍历序列与对应二叉树的遍历序列之间具有如下对应关系:
树的前序遍历 == 二叉树的前序遍历
树的后序遍历 == 二叉树的中序遍历
2. 森林转换为二叉树
具体转换方法:
(1). 将森林中的每棵树转换成二叉树
(2). 从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,当所有二叉树连起来后,此时所有得到的二叉树就是由森林转换得到的二叉树。
3. 二叉树转换为树或森林
具体转换方法:
(1). 加线——若某结点x是其双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子、···,都与结点y用线连起来;
(2).去线——删去原二叉树中所有的双亲结点与右孩子结点的连线;
(3). 层次调整——整理由(1)(2)两步所得到的树或森林,使之层次分明。
4. 森林的遍历
森林有两种遍历方法:前序遍历森林、后序遍历森林。
5.7 应用举例
二叉树的应举例-------哈夫曼树及哈夫曼编码
哈夫曼树也称最优二叉树,在实际中有着广泛的应用。
叶子结点的权值
叶子结点的权值是对叶子结点赋予的一个有意义的数值量。
二叉树的帯权路径长度
设二叉树具有n个带权值的叶子结点,从根结点到叶子结点的路径长度与相应叶子结点权值的乘积之和叫做二叉树的帯权路径长度。记为:
哈夫曼树
给定一组具有确定权值的叶子结点,可以构造出不同的二叉树,将其中带权路径长度最小的二叉树称为哈夫曼树。
哈夫曼算法
void HuffmanTree(element huffmanTree[],int w[],int n)
{
for(i=0;i<2*n-1;i++)
{
huffTree[i].parent=-1;
huffTree[i].lchild=-1;
huffTree[i].rchild=-1;
}
for(i=0;i<n;i++)
huffTree[i].weight=w[i];
for(k=n;k<2*n-1;k++)
{
Select(huffTree,i1,i2);
huffTree[i1].parent=k;
huffTree[i2].parent=k;
huffTree[k].weight=huffTree[i1].weight+huffTree[i2].weight;
huffTree[k].lchild=i1;
huffTree[k].rchild=i2;
}
}