由于k是整数,所以有k=log2n+1。
性质5对于具有n个结点的完全二叉树,如果按照从上至下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为i的结点,有:
⑴如果i>1,则序号为i的结点的双亲结点的序号为i/2(“/”表示整除);如果i=1,则序号为i的结点是根结点,无双亲结点。
⑵如果2i≤n,则序号为i的结点的左孩子结点的序号为2i;如果2i>n,则序号为i的结点无左孩子。
⑶如果2i+1≤n,则序号为i的结点的右孩子结点的序号为2i+1;如果2i+1>n,则序号为i的结点无右孩子。
如果,若对二叉树的根结点从0开始编号,则相应的i号结点的双亲结点的编号为(i-1)/2,左孩子的编号为2i+1,右孩子的编号为2i+2。
此性质可采用数学归纳法证明。
证明略。
6.2二叉树的基本操作与存储实现
6.2.1二叉树的存储
⒈顺序存储结构
所谓二叉树的顺序存储,就是用一组连续的存储单元存放二叉树中的结点。
一般是按照二叉树结点从上至下、从左到右的顺序存储。
这样结点在存储位置上的前驱后继关系并不一定就是它们在逻辑上的邻接关系,然而只有通过一些方法确定某结点在逻辑上的前驱结点和后继结点,这种存储才有意义。
因此,依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以惟一地反映出结点之间的逻辑关系,这样既能够最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
图6-3(a)所示的完全二叉树的顺序存储示意如图6-4所示。
A
B
C
D
E
F
G
H
I
J
数组下标0123456789
图6-4完全二叉树的顺序存储示意图
对于一般的二叉树,如果仍按从上至下和从左到右的顺序将树中的结点顺序存储在一维数组中,则数组元素下标之间的关系不能够反映二叉树中结点之间的逻辑关系,只有增添一些并不存在的空结点,使之成为一棵完全二叉树的形式,然后再用一维数组顺序存储。
如图6-5给出了一棵一般二叉树改造后的完全二叉树形态和其顺序存储状态示意图。
显然,这种存储对于需增加许多空结点才能将一棵二叉树改造成为一棵完全二叉树的存储时,会造成空间的大量浪费,不宜用顺序存储结构。
最坏的情况是右单支树,如图6-6所示,一棵深度为k的右单支树,只有k个结点,却需分配2k-1个存储单元。
A
A
C
B
C
B
E
D
D
E
G
F
F
G
(a)一棵二叉树(b)改造后的完全二叉树
A
B
C
∧
D
E
∧
∧
∧
F
∧
∧
G
(c)改造后完全二叉树顺序存储状态
图6-5一般二叉树及其顺序存储示意图
A
A
B
B
C
C
D
D
(a)一棵右单支二叉树(b)改造后的右单支树对应的完全二叉树
A
∧
B
∧
∧
∧
C
∧
∧
∧
∧
∧
∧
∧
D
(c)单支树改造后完全二叉树的顺序存储状态
图6-6右单支二叉树及其顺序存储示意图
二叉树的顺序存储表示可描述为:
#defineMAXNODE…//二叉树的最大结点数
typedefDataTypeSqBiTree[MAXNODE]//0号单元存放根结点
SqBiTreebt;
即将bt定义为含有MAXNODE个DataType类型元素的一维数组。
⒉链式存储结构
所谓二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。
通常有下面两种形式。
⑴二叉链表存储
链表中每个结点由三个域组成,除了数据域外,还有两个指针域,分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。
结点的存储的结构为:
lchild
rchild
data
其中,data域存放某结点的数据信息;lchild与rchild分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或NULL表示)。
图6-7(a)给出了图6-3(b)所示的一棵二叉树的二叉链表示。
二叉链表也可以带头结点的方式存放,如图6-7(b)所示。
头指针bt头结点指针bt
A
A
C
∧
B
C
∧
B
∧
F
∧
∧
E
∧
D
∧
∧
F
∧
∧
E
∧
D
∧
∧
∧
G
∧
∧
G
(a)不带头结点的二叉链表(b)带头结点的二叉链表
图6-7图6-3(b)所示二叉树的二叉链表示意图
⑵三叉链表存储
每个结点由四个域组成,具体结构为:
parent
rchild
lchild
data
其中,data、lchild以及rchild三个域的意义同二叉链表结构;parent域为指向该结点双亲结点的指针。
这种存储结构既便于查找孩子结点,又便于查找双亲结点;但是,相对于二叉链表存储结构而言,它增加了空间开销。
图6-8给出了图6-3(b)所示的二叉树的三叉链表示。
A
∧
C
∧
B
∧
F
∧
∧
E
∧
D
∧
∧
G
∧
图6-8图6-3(b)所示二叉树的三叉链表表示示意图
尽管在二叉链表中无法由结点直接找到其双亲,但由于二叉链表结构灵活,操作方便,对于一般情况的二叉树,甚至比顺序存储结构还节省空间。
因此,二叉链表是最常用的二叉树存储方式。
如不加特殊说明,本书后面所涉及到的二叉树的链式存储结构都是指二叉链表结构。
二叉树的二叉链表存储表示可描述为:
typedefstructBiTNode{
DataTypedata;
structBiTNode*lchild;*rchild;//左右孩子指针
}BiTNode,*BiTree;
即将BiTree定义为指向二叉链表结点结构的指针类型。
6.2.2二叉树的基本操作及实现
⒈二叉树的基本操作
二叉树的基本操作通常有以下几种:
⑴Initiate(bt):
建立一棵空二叉树。
⑵Create(x,lbt,rbt):
生成一棵以x为根结点的数据域信息,以二叉树lbt和rbt为左子树和右子树的二叉树。
⑶InsertL(bt,x,parent):
将数据域信息为x的结点插入到二叉树bt中作为结点parent的左孩子结点。
如果结点parent原来有左孩子结点,则将结点parent原来的左孩子结点作为结点x的左孩子结点。
⑷InsertR(bt,x,parent):
将数据域信息为x的结点插入到二叉树bt中作为结点parent的右孩子结点。
如果结点parent原来有右孩子结点,则将结点parent原来的右孩子结点作为结点x的右孩子结点。
⑸DeleteL(bt,parent):
在二叉树bt中删除结点parent的左子树。
⑹DeleteR(bt,parent):
在二叉树bt中删除结点parent的右子树。
⑺Search(bt,x):
在二叉树bt中查找数据元素x。
⑻Traverse(bt):
按某种方式遍历二叉树bt的全部结点。
⒉算法的实现
算法的实现依赖于具体的存储结构,当二叉树采用不同的存储结构时,上述各种操作的实现算法是不同的。
下面讨论基于二叉链表存储结构的上述操作的实现算法。
⑴Initiate(bt)初始建立二叉树bt,并使bt指向头结点。
在二叉树根结点前建立头结点,就如同在单链表前建立的头结点,可以方便后边的一些操作实现。
intInitiate(BiTree*bt){
//初始建立一棵带头结点的二叉树
bt=newBiTNode;
if(!
bt)
return0;
*bt->lchild=NULL;
*bt->rchild=NULL;
return1;
}
【算法6-1】建立二叉树算法
在主函数中,可以通过如下方式调用Initiate函数:
main()
{BiTreet;
Initiate(&t);
}
⑵Create(x,lbt,rbt)建立一棵以x为根结点的数据域信息,以二叉树lbt和rbt为左右子树的二叉树。
建立成功时返回所建二叉树结点的指针;建立失败时返回空指针。
BiTreeCreate(DataTypex,BiTreelbt,BiTreerbt){
//生成一棵以x为根结点的数据域值以lbt和rbt为左右子树的二叉树
BiTreep;
P=newBiTNode;
if(!
p)
returnNULL;
p->data=x;
p->lchild=lbt;
p->rchild=rbt;
returnp;
}
【算法6-2】建立一棵已生成左右子树的二叉树的算法
⑶InsertL(bt,x,parent)
BiTreeInsertL(BiTreebt,DataTypex,BiTreeparent){
//在二叉树bt中的parent所指结点和其左子树之间插入数据元素为x的结点
BiTreep;
if(parent==NULL)
{cout<<”插入出错”<returnNULL;
}
p=newBiTNode;
if(!
p)
returnNULL;
p->data=x;
p->lchild=NULL;
p->rchild=NULL;
if(parent->lchild==NULL)
parent->lchild=p;
else
{p->lchild=parent->lchild;
parent->lchild=p;
}
returnbt;
}
【算法6-3】给二叉树某结点插入一个左孩子算法
⑷InsertR(bt,x,parent)功能类同于⑶,算法略。
⑸DeleteL(bt,parent)在二叉树bt中删除结点parent的左子树。
当parent或parent的左孩子结点为空时删除失败。
删除成功时返回根结点指针;删除失败时返回空指针。
BiTreeDeleteL(BiTreebt,BiTreeparent){
//在二叉树bt中删除结点parent的左子树
BiTreep;
if(parent==NULL||parent->lchild==NULL)
{cout<<”删除出错”<returnNULL;
}
p=parent->lchild;
parent->lchild=NULL;
deletep;//当p为非叶子结点时,这样删除仅释放了所删子树根结点的空间,
//若要删除子树分支中的结点,需用后面介绍的遍历操作来实现。
returnbt;
}
【算法6-4】删除二叉树中某结点的左子树算法
⑹DeleteR(bt,parent)功能类同于⑸,只是删除结点parent的右子树。
算法略。
操作Search(bt,x)实际是遍历操作Traverse(bt)的特例,关于二叉树的遍历操作的实现,将在下一节中重点介绍。
6.3二叉树的遍历
6.3.1二叉树的遍历方法及递归实现
二叉树的遍历是指按照某种顺序访问二叉树中的每个结点,使每个结点被访问一次且仅被访问一次。
遍历是二叉树中经常要用到的一种操作。
因为在实际应用问题中,常常需要按一定顺序对二叉树中的每个结点逐个进行访问,或查找具有某一特点的结点,然后对这些满足条件的结点进行处理。
通过一次完整的遍历,可使二叉树中结点信息由非线性排列变为某种意义上的线性序列。
也就是说,遍历操作使非线性结构线性化。
由二叉树的定义可知,一棵二叉树由根结点、根结点的左子树和根结点的右子树三部分组成。
因此,只要依次遍历这三部分,就可以遍历整个二叉树。
若以D、L、R分别表示访问根结点、遍历根结点的左子树、遍历根结点的右子树,则二叉树的遍历方式有六种:
DLR、LDR、LRD、DRL、RDL和RLD。
如果限定先左后右,则只有前三种方式,即DLR(称为先序遍历)、LDR(称为中序遍历)和LRD(称为后序遍历)。
⒈先序遍历(DLR)
先序遍历的递归过程为:
若二叉树为空,遍历结束。
否则,
⑴访问根结点;
⑵先序遍历根结点的左子树;
⑶先序遍历根结点的右子树。
先序遍历二叉树的递归算法如下:
voidPreOrder(BiTreebt){
//先序遍历二叉树bt
if(bt==NULL)
return;//递归调用的结束条件
Visit(bt->data);//访问结点的数据域
PreOrder(bt->lchild);//先序递归遍历bt的左子树
PreOrder(bt->rchild);//先序递归遍历bt的右子树
}
【算法6-5】二叉树先序遍历算法
对于图6-3(b)所示的二叉树,按先序遍历所得到的结点序列为:
ABDGCEF
⒉中序遍历(LDR)
中序遍历的递归过程为:
若二叉树为空,遍历结束。
否则,
⑴中序遍历根结点的左子树;
⑵访问根结点;
⑶中序遍历根结点的右子树。
中序遍历二叉树的递归算法如下:
voidInOrder(BiTreebt){
//中序遍历二叉树bt
if(bt==NULL)
return;//递归调用的结束条件
InOrder(bt->lchild);//中序递归遍历bt的左子树
Visit(bt->data);//访问结点的数据域
InOrder(bt->rchild);//中序递归遍历bt的右子树
}
【算法6-6】二叉树中序遍历算法
对于图6-3(b)所示的二叉树,按中序遍历所得到的结点序列为:
DGBAECF
⒊后序遍历(LRD)
后序遍历的递归过程为:
若二叉树为空,遍历结束。
否则,
⑴后序遍历根结点的左子树;
⑵后序遍历根结点的右子树。
⑶访问根结点;
后序遍历二叉树的递归算法如下:
voidPostOrder(BiTreebt){
//后序遍历二叉树bt
if(bt==NULL)
return;//递归调用的结束条件
PostOrder(bt->lchild);//后序递归遍历bt的左子树
PostOrder(bt->rchild);//后序递归遍历bt的右子树
Visite(bt->data);//访问结点的数据域
}
【算法6-7】二叉树后序遍历算法
对于图6-3(b)所示的二叉树,按后序遍历所得到的结点序列为:
GDBEFCA
⒋层次遍历
所谓二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。
对于图6-3(b)所示的二叉树,按层次遍历所得到的结点序列为:
ABCDEFG
下面讨论层次遍历的算法。
由层次遍历的定义可以推知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。
因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,然后从队头取出一个元素,每取一个元素,执行下面两个操作:
⑴访问该元素所指结点;
⑵若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队。
此过程不断进行,当队列为空时,二叉树的层次遍历结束。
在下面的层次遍历算法中,二叉树以二叉链表存放,一维数组Queue[MAXNODE]用以实现队列,变量front和rear分别表示队头和队尾指针,记录当前对头元素和队尾元素在数组中的位置。
voidLevelOrder(BiTreebt){
//层次遍历二叉树bt
BiTreeQueue[MAXNODE];
intfront,rear;
if(bt==NULL)
return;
front=-1;
rear=0;
queue[rear]=bt;
while(front!
=rear)
{front++;
Visit(queue[front]->data);//访问队首结点的数据域
if(queue[front]->lchild!
=NULL)//将队首结点的左孩子结点入队列
{rear++;
queue[rear]=queue[front]->lchild;
}
if(queue[front]->rchild!
=NULL)//将队首结点的右孩子结点入队列
{rear++;
queue[rear]=queue[front]->rchild;
}
}
}
【算法6-8】二叉树层次遍历算法
6.3.2二叉树遍历的非递归实现
前面给出的二叉树先序、中序和后序三种遍历算法都是递归算法。
当给出二叉树的链式存储结构以后,用具有递归功能的程序设计语言很方便就能实现上述算法。
然而,并非所有程序设计语言都允许递归;另一方面,递归程序虽然简洁,但可读性一般不好,执行效率也不高。
因