5.1 二叉树
5.1.1二叉树的定义
二叉树或为空树,或是由一个根结点加上两棵分别称为左子树和右子树的‘互不相交的二叉树组成。
特点:每个结点至多有两棵子树,子树有左右之分,其次序不能任意颠倒。
特殊的二叉树
(1)满二叉树
二叉树中每一层的结点数都达到最大,所有叶子结点均在最后一层。
(2)完全二叉树
除最后一层外,其余各层均满。
最后一层或是满的,或者右边缺少连续的若干结点。即叶子结点只能出现在最后一层或者次上一层。
(3)理想平衡树
在一棵二叉树中,除最后一层外,其余层都是满的,称为理想平衡树;满二叉树和完全二叉树都是理想平衡树,但理想平衡树不一定是完全二叉树。
5.1.2二叉树的性质
1.在二叉树的第i层上,至多有2i-1个结点(i>=1)。
推广:m叉树的第i层上至多有mi-1个结点。
2.一个深度为k的二叉树中,至多有ki-1个结点。
推广:深度为k的m叉树中,至多有(mk-1)/(m-1)个结点。
3.对于一个非空二叉树,如果叶子结点数为n0,度数为2的结点数为n2,则有n0 = n2+1。
4.具有n个结点的完全二叉树的深度k为(log2n)+1。(k为整数,表示不大于log2n的整数)
推广:具有n个结点的m叉树的最小深度是log2(n(m-1)+1)。
5.如果对一颗有n个节点的完全二叉树的结点按层序编号(从上到下,从左到右),则对任一结点i(1<=i<=n)有:
(1)如果i=1,i为根。若i>1则其双亲是结点(i/2)取整。
(2)如果2i>n,则结点i为叶子,否则其左孩子是结点2i。
(3)如果2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1。
5.1.3二叉树的存储结构
1.顺序存储
顺序存储就是用一组连续的存储单元存放二叉树中的结点。通常是按照从上到下,从左到右的顺序存储。这样存储的话前后继关系不一定是按照树中的来,所以要通过一些方法确定前后继关系才可以。不难看出只有完全二叉树和满二叉树采用顺序结构存储才比较合适,这时二叉树中结点的序号可以唯一地反映出结点之间的逻辑关系。这样既能够最大限度地节省空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
优点:根据二叉树的性质5,直接利用元素在数组中的位置表示其逻辑关系,方便寻找某个节点的双亲结点以及左右孩子结点。
缺点:若不是完全二叉树,会浪费空间,适合完全二叉树或者和完全二叉树相近的二叉树。
顺序结构的定义:
#define MAXNODE 100//二叉树最大结点数
typedef ElemType SqBiTree[MAXNODE+1];//1号单元存放根结点
结论: 顺序存储适合存放完全二叉树,便于寻找双亲和孩子。
2.二叉链表存储结构
链表中每个结点由三个域组成,除了数据域外,还有两个指针域,分别用来存放该结点左孩子和右孩子结点的存储地址。
性质:在含有n个结点的二叉链表中含有n+1个空指针域。
typedef struct BiTree{
DataType data;//DataType表示结点中存放数据的类型
struct BiTNode * lchild;//存放左子树根结点的地址
struct BiTNode * rchild;//存放右子树根结点的地址
}BiTree,*BiTree;//BiTree为二叉链表的结点类型
结论: 二叉链表适合找孩子,不适合找双亲。
3.三叉链表
相较于二叉链表多了一个parent,用来指向该结点双亲结点。好处是可以查找双亲结点,但是缺点是增加了空间开销。
typedef struct TriTNode{
DataType data;//DataType表示结点中存放数据的类型
struct TriTNode* lchild;//存放左子树根结点的地址
struct TriTNode* rchild;//存放右子树根结点的地址
struct TriTNode *parent;//存放双亲结点的地址
}TriTNode,*TriTNode;//TriTNode为三叉链表的结点类型
结论: 三叉链表即适合找孩子又适合找双亲。
5.1.4二叉树的遍历及其应用
遍历操作可以使非线性结构线性化。
三种访问先序(根左右)、中序(左根右)、后序(左右根)。
先序遍历算法实现;
void preorder(BiTree T){
if(T){
printf(T->data);
preorder(T->lchild);
preorder(T->rchild);
}
}
中序遍历算法实现:
void inorder(BiTree T){
if(T){
inorder(T->lchild);
printf(T->data);
inorder(T->rchild);
}
}
后续遍历算法的实现;
void postorder(BiTree T){
if(T){
inorder(T->lchild);
inorder(T->rchild);
printf(T->data);
}
}
2.二叉树遍历的非递归实现
常用的二叉树遍历非递归实现有两种放法:基于任务分析的方法和基于遍历路径分析的方法。
(1)基于任务分析的方法
在每一种遍历方法中,都会进行三次的操作,即遍历左子树’访问根结点‘遍历右子树,但是在访问左右子树的时候还会继续调用这三个方法,所以需要使用自定义栈保存对根结点布置的子任务。分别按照不同的遍历方法对三种情况的重要程度,规定进栈的顺序。
栈的数据元素类型定义:
typedef struct {
BiTree ptr;//指向根结点的指针
int task;//任务性质,1表示遍历,0表示访问
}ElemType;
栈的类型定义为:
#define StackMax 20
typedef struct{
ElemType data[StackMax];
int top;
}SqStack;
基于任务分析的非递归的中序遍历算法如下:
void inOrder_iter(BiTree T){
//利用栈实现中序遍历二叉树,BT为指向二叉树的根结点的头指针。
ElemType e;
SqStack s;
InitStack(s);
e.ptr = T;//布置初始任务
e.task = 1;
if(T)//防止栈为空,将遍历左子树放入栈中
Push(s,e);
while(!StackEmpty(s)){
Pop(s,e);//每次处理一项任务
if(e.task == 0) //e.task == 0处理访问任务
printf(e.ptr->data);
else{//处理遍历任务
p=e.ptr;
e.ptr = p->rchild;
if(e.ptr)
Push(s,e);//遍历右子树
e.ptr = p;
e.task = 0;
Push(s,e);//访问根结点
e.ptr = p->lchild;
e.task = 1;
if(e.ptr)
Push(s,e);//遍历左子树
}
}
}
3.基于搜索路径分析的二叉树遍历算法的非递归实现
路径分析法是根据遍历的路线从根结点开始沿左子树深入下去,当深入到最左端,无法再深入下去时,则返回,逐一进入刚才深入时遇到结点的右子树,在进行如此的深入和返回,直到最后从根结点的右子树返回到根结点为止。
从根结点出发,在沿左子树深入时,深入一个结点入栈一个结点,若为先序遍历,则在入栈之前访问之;当沿左分支深入不下去时,则返回,即从堆栈中弹出前面压入的结点;若为中序遍历,则访问该结点,然后从该结点的右子树继续深入;若为后序遍历,则将该结点再次入栈,然后从该结点的右子树继续深入。与左子树类同,仍为深入一个结点入栈一个结点,深入不下去再返回,直到第二次从栈里弹出该点才访问之。
void NrPostorder(BiTree T){
SqStack S;
InitStack(&S);
char lrtag(STACK_INIT_SIZE)="";//标记数组
BiTree t;
t=PriGoFarLeft(T,&S,lrtag);//找T的最左下的结点
while(t){
lrtag[S.top]='R';//第二次遇到修改标记
if(t->rchild)
t=PriGoFarLeft(t->rchild,&s,lrtag);//找t的右子树最左下的结点
else
while(!StackEmpty(S) && lrtag[S.top] == 'R'){//第三次遇到,出栈并输出
Pop(&S,&t);
printf(t->data);
}
if(!StackEmpty(S))
GetTop(S,&t);
else
t==NULL;
}
}
BiTree PriGoFarLedt(BiTree T,SqStack *S,charc[]){
//找T的左下方结点
if(!T)
return NULL;
while(T){
Push(S,T); //第一次遇到进栈
c[S->top]='L';
if(T->lchild == NULL)
break;
T=T->lchild;
}
return T;
}
4.层次遍历
从二叉树的根结点出发,从上至下、从左至右依序访问每一个结点。
先访问双亲再访问孩子,符合先进先出的特点,可以使用队列进行保存要访问的每一个点的指针。
void layer(BiTree T){
InitQueue(Q);//初始化队列
if(bt)
EnQueue(Q.bt);//进队列
while(!QueueEmpty(Q)){
p=DeQueue(Q);//出队列
printf(p->data);//访问结点
if(p->lchild)//左子树跟进队列
EnQueue(Q,p->lchild);
if(p->rchild)//右子树跟进队列
EnQueue(Q,p->rchild);
}
}
5.创建二叉树的二叉链表存储结构
以字符串“根,左子树,右子树”形式创建二叉链表的算法如下:
void crt_tree(BiTree *T){
scanf("%c".&ch);
if(ch=='#')
*T = NULL:
else{
*T = (BiTree)malloc(sizeof(BiTNode));//创建根结点
(*T)->data = ch;
crt_tree(&(*T)->lchild);//创建左子树
crt_tree(&(*T)->lchild);//创建右子树
}
}
(2)读入边创建二叉链表的非递归算法
算法核心:
1.每读一条边,生成孩子结点,并作为叶子结点;之后将该结点的指针保存在队列中。
2.从队头找该结点的双亲结点指针。如果队头不是,出队列,直至队头是该结点的双亲结点指针。再按lrflag值建立双亲结点的左右孩子关系。
void Create_BiTree(BiTree *T){
InitQueue(Q);
*T->Null;
scanf(fa,ch,lrflag);
while(ch!='#'){
p=(BiTree)molloc(sizeof(BiTree));
p->data = ch;//创建叶子结点
p->lchild=p->rchild=NULL;//做成叶子结点
EnQueue(Q,p);//指针入队列
if(fa=='#')//建立根结点
*T=p;
else{
s=GetHead(Q);//取队列头元素
while(s->data != fa){
DeQueue(Q);
s=GetHead(Q);
}//在队列中找到双亲结点
if(lrchild == 0)//链接左孩子结点
s->lchild = p;
else//链接右孩子结点
s->rchild=p;
}//非根结点的情况
scanf(fa,ch,lrflag);
}
}
(3)由二叉树的遍历序列确定二叉树