1.定义
平衡二叉树是一种二叉排序树,其中每一个结点的左子树和右子树的高度至多等于1,平衡二叉树又称为AVL树。平衡二叉树是一种高度平衡的二叉排序树,意思是说,要么它是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上所有结点的平衡因子只可能是-1,0,1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
最小不平衡树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。
2.平衡二叉树的实现原理
基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡二叉树。
这里我们介绍一个例子,我们现在有一个数组a[10]={3,2,1,4,5,6,7,10,9,8},图1是在没有学习平衡二叉树之前,根据二叉排序树的特性,建成的二叉树,这对查找是非常不利的。图2是利用平衡二叉树的原理建成的二叉树,现在我们来分析是如何一步步建成图2的平衡二叉树的。
- 当我们正常构建到下图1所示时,此时结点3的平衡因子为2,这是我们将整个树进行右旋,如下图2所示。
- 继续插入结点4,如下图3所示,插入结点5如下图4所示,这是结点3的BF值为-2,由于BF是负值,所以我们对这棵最小平衡子树进行左旋,如图5所示。
- 继续插入结点6,发现根结点2的BF值变成了-2,如图6,对根结点左旋。注意此时本来结点3是4的左孩子,由于旋转后需要满足二叉排序树特性,因此它成了结点2的右孩子,如图7。增加结点7,同样的左旋。使得整棵树达到平衡,如图8和图9所示。
- 当增加结点10时,结构无变化,如图10,再增加结点9,此时结点7的BF变成了-2,理论上我们只需要旋转最小平衡子树7、9、10即可,但是如果左旋转后,结点9就成了10的右孩子,这是不符合二叉排序树的特性的,此时不能简单的旋转,如图11所示。仔细观察图11,发现根本原因在于结点7的BF是-2,而结点10的BF是1,它们俩一正一负,符号并不统一,而前面的左旋右旋,最小平衡子树的根结点与它的子结点符号都是相同个。这就是不能直接旋转的关键。
- 既然不统一,就把它们先转到符号都是相同的,我们先对结点9和结点10进行右旋,使得结点10成了9的右子树,,结点9的BF为-1,此时就与结点7的BF值符号统一了,如图12所示。
- 这样我们再以结点7为最小不平衡子树进行左旋,得到图13,接着插入8,情况和刚才类似,结点6的BF是-2,而它的右孩子9的BF是1,如图14,因此先以9为根结点,进行右旋,得到图15,再以根结点左旋,最终得到最后的平衡二叉树,如图16所示。
如果你认为结束了而且看得还算明白,好吧,你是对的。接下来看看代码,这里才是难点。
3.平衡二叉树的实现算法
首先改进二叉排序树的结点结构,增加一个bf,用来存储平衡因子。
//二叉树的二叉链表结点结构定义
typedef struct BiTNode//结点结构
{
int data;//结点数据
int bf;//结点的平衡因子
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;
然后,对于右旋操作,我们的代码如下。
void R_Rotate(BiTree *P)
{
BiTree L;
L=(*P)->lchild;//L指向P的左子树根结点
(*P)->lchild=L->rchild;//L的右子树挂接为P的左子树
L->rchild=(*P);
*P=L;//P指向新的根结点
}
左旋操作代码如下。
//对以P为根的二叉排序树作左旋处理
//处理之后P指向新的树根结点,即选择处理之前的右子树的根结点0
void L_Rotate(BiTree *p)
{
BiTree R;
R=(*P)->rchild;//R指向P的右子树根结点
(*P)->rchild=R->lchild;//R的左子树挂接为P的右子树
R->lchild=(*P);
*P=R;//P指向新的根结点
}
现在我们来看左平衡旋转处理的函数代码。
#define LH +1//左高
#define EH 0//等高
#define RH -1//右高
//对以指针T所指结点为根的二叉树作左平衡旋转处理
//本算法结束时,指针T指向新的根结点
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
L=(*T)->lchild;//L指向T的左子树根结点
switch(L->bf)
{
//检查T的左子树的平衡度,并作相应平衡处理
case LH://新结点插入在T的左孩子的左子树上,要作单右旋处理
(*T)->bf=L->bf=EH;
R_Rotate(T);
break;
case RH;//新结点插入在T的左孩子的右子树上,要作双旋处理
Lr->L->rchild;//Lr指向T的左孩子的右树根
switch(Lr->bf)//修改T及其左孩子的平衡因子
{
case LH:(*T)->bf=RH;
L->bf=EH;
break;
case EH:(*T)->bf=L->bf=EH;
break;
case RH:(*T)->bf=EH;
L->bf=LH;
break;
}
Lr->bf=EH;
L_Rotate(&(*T)->lchild);//对T的左子树作左平衡处理
R_Rotate(T);//对T作右旋平衡处理
}
}
下面我们来对代码进行解释说明:
- 1.函数被调用,传入一个需要调整平衡性的子树T。由于LeftBalance函数被调用是,其实是已经确认当前子树是不平衡状态,且左子树的高度大于右子树的高度。换句话说,此时T的根结点应该是平衡因子BF的值大于1的数。
- 2.第四行,我们将T的左孩子赋值给L。
- 3.第5~27行是分支判断。
- 4.当L的平衡因子为LH,即为1时,表明它与根结点的BF值符号相同,因此,第8行,将它们的BF值都改为0,并且第9行,进行右旋操作。
- 5.当L的平衡因子为RH,即为-1时,表明它与根结点的BF值符号相反,此时需要做双旋处理。第13~22行,针对L的右孩子Lr的BF作判断,修改根结点T和L的BF的值。第24行将当前Lr的BF改为0。
- 6.第25行,对根结点的左子树进行左旋。
- 7.对根结点进行右旋,完成平衡操作。这些过程一定要自己动手画一遍,这样理解起来会容易很多。
有了这些准备,我们的函数才算是正式登场了挺难受的其实
//若在平衡的二叉排序树T中不存在和e有相同关键字段的结点,则插入一个
//数据元素为e的新结点并返回1,否则返回0。若因插入而使二叉排序树失去平衡,
//则作平衡旋转处理,布尔变量taller反映T长高与否。
Status insertAVL(BiTree *T,int e,Status *taller)
{
if(!*T)
{
//插入新结点,树“长高”,置taller为TRUE
*T=(BiTree)malloc(sizeof(BiTNode));
(*T)->data=e;
(*T)->lchild=(*T)->rchild=NULL;
(*T)->bf=EH;
*taller=TRUE;
}
else{
if(e==(*T)->data)
{
//树中已存在和e有相同关键字的结点则不再插入
*taller=FALSE;
return FALSE;
}
if(e<(*T)->data)
{
//应继续在T的左子树中进行搜索
if(!InsertAVL(&(*T)->lchild,e,taller))//未插入
{
return FALSE;
}
if(*taller)//已插入到T的左子树中且,左子树“长高”
{
switch((*T)->bf)//检查T的平衡度
{
case LH://原本左子树比右子树高,需要左平衡处理
LeftBalance(T);
*taller=FALSE;
break;
case EH://原本左右子树等高,现因左子树增高而树增高
(*T)->bf=LH;
*taller=TRUE;
break;
case RH://原本右子树比左子树高,先左右子树等高
(*T)->bf=EH;
*taller=FALSE;
break;
}
}
}
else{
//应继续在T的右子树中进行搜索
if(!InsertAVL(&(*T)->rchild,e,taller))//未插入
{
return FALSE;
}
if(*taller)//已插入到T的左子树中且,左子树“长高”
{
switch((*T)->bf)//检查T的平衡度
{
case LH://原本左子树比右子树高,需要左平衡处理
(*T)->bf=EH;
*taller=FALSE;
break;
case EH://原本左右子树等高,现因左子树增高而树增高
(*T)->bf=RH;
*taller=TRUE;
break;
case RH://原本右子树比左子树高,先左右子树等高
RightBalance(T);
(*T)->bf=EH;
*taller=FALSE;
break;
}
}
}
}
return TRUE;
}
对于这段代码来说,我们只需要在需要构建平衡二叉树的时候执行如下列代码及可在内存中生成一棵平衡二叉树。
int i;
int a[10]={3,2,1,4,5,6,7,10,9,8};
BiTree T=NULL;
Status taller;
for(i=0;i<10;i++)
{
InsertAVL(&T,a[i],&taller);
}
不容易,终于讲完了,本算法代码有点长,而且有点难理解,编程中容易在很多细节上出错,要想真正掌握它,需要多加练习。已泪奔