0x01.平衡二叉树的由来
问题:二叉排序树广泛应用于动态查找中(查找的同时会往动态表里增删数据),优势就在于增删查找数据的时候,时间代价比线性表小很多,但二叉排序树的插入操作中,也可能创建了一棵这样的树:
在是一棵左斜树,也完全符合二叉排序树的特征,但这样的排序树是没有意义的,因为它失去了减少时间代价的意义,就和一个普通的线性表一样,发挥不出树的优势了。
解决办法:对插入的结点进行规范,时刻调整这棵树,使之平衡,也就是让它变成一棵平衡的排序二叉树。
0x02.平衡二叉树的相关概念
平衡二叉树(也叫AVL树),是一棵高度平衡的二叉排序树,需要满足以下的特征:
- 是一棵二叉排序树
- 每一个结点的左子树和右子树的高度差至多等于1
平衡因子BF:二叉树上结点的左子树的深度减去右子树的深度的值称为平衡因子BF。平衡二叉树的BF只可能是1,0,-1。
最小不平衡子树:距离插入点最结点最近的,且平衡因子的绝对值大于1的结点为根的子树。
如图,以48为根的子树j就是最小不平衡子树。
0x03.如何让二叉排序树平衡
最简单的开始,这棵树怎样才能平衡呢?
很明显,48这个结点为根的左子树 深度是2,右子树深度是0,BF值是2,不平衡,这时只需要将48变成43的子树,这棵树就平衡了。
再如下:
这只需要将37变为43的左子树就行了。
其实第一种操作叫右璇操作,原因就是左边的多了,要放一些到右边去,再来看一个复杂一点的右旋操作。
此时根结点需要右璇,应该将45作为根结点,48作为45的右孩子,45原来的右孩子47变成48的左孩子,50还是48的右孩子。
到这我们可以理解右璇的基本操作原理:将原来根结点的左孩子作为根结点,原来根结点的左孩子变为新的根结点的右孩子,原来根结点的右孩子不变。
我们再来看一下复杂一点的左璇操作:
这时,应该将47作为新的根结点,39作为47的左孩子,45变为39的右孩子。
我们再来看一个特殊的例子:
此时有两个结点不平衡,47和48,48的BF为-2,最小不平衡子树是以48为根结点,47的BF也为-2,50的BF为1,此时按道理只需要旋转最下不平衡子树就行了,也就是48,50,49这棵子树,但如果左璇,会发现下图:
此时这明显不满足二叉排序树的要求,因为,48<50,但49却是50的右子树。
我们观察根本原因在于,50的BF值为1,而48的BF值为-2,而在上述的左璇中,最小不平衡子树的BF值与子结点的BF值相同,所以我们需要使它的结点的BF值相同再操作。调整50,49的BF值为负值,也就是将50,49先右璇,如图:
此时再对48,49,50进行右璇就可以得到平衡的二叉树:
通过上述的调整,我们发现,只要出现了不平衡,那么先找最小不平衡子树的根结点,再对这个结点进行相应的旋转,就能使树平衡。
0x04.代码
首先我们要看一下平衡二叉树的结构,与普通二叉树不同的是,多出了一个bf值,用于判断是否平衡。
//定义左高,右高,等高的 值
#define LH 1
#define RH -1
#define EH 0
typedef struct TreeNode
{
int data;
int bf;
struct TreeNode* Left;
struct TreeNode* Right;
}TreeNode, * BinTree;
然后,我们需要写出操作二叉树使之平衡的最基本的代码,也就是,左璇和右璇,根据上述的原理,我们可以得出下面的代码:
右璇:
//传入需要做右璇处理的子树的根结点
//处理完毕后P指向新的子树的根结点
void R_Rotate(BinTree* P)
{
BinTree L;
L = (*P)->Left;
(*P)->Left = L->Right;
L->Right=(*P);
*P = L;
}
左璇:
void L_Rotate(BinTree* P)
{
BinTree R;
R = (*P)->Right;
(*P)->Right = R->Left;
R->Left = (*P);
*P = R;
}
有了最基本的左璇和右璇代码后,需要处理平衡左子数和平衡右子树的代码:
左子树平衡旋转代码:
//传进来需要左平衡处理的结点,结束后,T指向新的子树的根结点
//既然已经需要做左平衡处理,那么它的BF值肯定为正,只要判断它的左子树的BF值进行了
void LeftBalance(BinTree* T)
{
BinTree L, Lr;
L = (*T)->Left;
switch (L->bf)//检查左子树的平衡度
{
case LH://如果左边高,那么BF值都为正,只需要做右璇处理就行了
(*T)->bf = L->bf = EH;//处理完毕后,BF值都为0
R_Rotate(T);
break;
case RH://如果左子树的BF值为负,说明符号为负,需要先把符号变为相同,再进行右璇处理
Lr = L->Right;
//后面会进行双旋操作
switch (Lr->bf)//通过判断Lr的bf值,修改T和T的左孩子的平衡因子
{
case LH://如果L的右子树是左高,那么双旋过后,T应该就是右高
(*T)->bf = RH;
L->bf = EH;
break;
case EH://如果等高,那么双旋过后应该是都是等高
(*T)->bf = L->bf = EH;
break;
case RH://如果是右高,那么双旋过后,L应该是左高
(*T)->bf = EH;
L->bf = LH;
break;
}
Lr -> bf = EH;
L_Rotate(&(*T)->Left);//对T的左子树进行左旋
R_Rotate(T);//对T进行右旋,整个是个双旋操作
}
}
同理可得右平衡代码
右子树平衡旋转代码:
void RightBalance(BinTree* T)
{
BinTree R, Rl;
R = (*T)->Right;
switch (R -> bf)
{
case RH:
(*T)->bf = R->bf = EH;
break;
case LH:
Rl = R->Left;
switch (Rl->bf)
{
case LH:
(*T)->bf = EH;
R->bf = RH;
case EH:
(*T)->bf = R->bf = EH;
break;
case RH:
(*T)->bf = LH;
R->bf = EH;
}
Rl->bf = EH;
R_Rotate(&(*T)->Right);
L_Rotate(T);
}
}
有了这些平衡操作,我们就可以将二叉排序树的插入过程改为生成AVL树的过程了。
AVL树插入过程代码:
//若在二叉排序树中不存在关键字为e的结点,则将e插入
//插入的过程通过旋转操作保持树的平衡
//插入成功返回true,否则返回false
//taller是一个判断二叉树的高度是否增加的值
int InsertAVL(BinTree* T, int e, int* taller)
{
if (!*T)//找到位置,插入结点
{
*T = (BinTree)malloc(sizeof(TreeNode));
(*T)->data = e;
(*T)->Left = (*T)->Right = NULL;
(*T)->bf = EH;
*taller = true;
}
else
{
if (e == (*T)->data)//树中有关键字和e相同的结点,不进行插入操作
{
*taller = false;
return false;
}
if (e < (*T)->data)//在左子树中进行搜索
{
if (!InsertAVL(&(*T)->Left, e, taller))//在左子树中递归,若最终返回false,则该步返回false
{
return false;
}
if (*taller)//这个结点插入到了左子树(到这步的时候,实际上已经从InsertAVL返回了)
{
switch ((*T)->bf)//判断T的平衡因子,这个T的bf值仍然是插入操作之前的值
{
case LH://原来是左子树高,现在左子树又增加一个结点,左子树偏高,做左子树平衡处理
LeftBalance(T);
*taller = false;//平衡完后重置标志变量
break;
case EH://原来是等高,现在左子树加了,说明左子树高了
(*T)->bf = LH;
*taller = true;//加入后,仍然左高,那么高度是真的增加了
break;
case RH://原来右边高,现在左边增加了一个结点,等高了
(*T)->bf = EH;
*taller = false;//因为是等高,所以重置标志变量
break;
}
}
}
else//在右子树中搜素,过程与上面差不多
{
if (!InsertAVL(&(*T)->Right, e, taller))
{
return false;
}
if (*taller)
{
switch ((*T)->bf)
{
case LH://原来左边高,现在等高
(*T)->bf = EH;
*taller = false;
break;
case EH://原来等高,现在右边高
(*T)->bf = RH;
*taller = true;
break;
case RH://原来右边高,现在又加了一个,需要左右平衡处理
RightBalance(T);
*taller = false;
break;
}
}
}
}
}
0x05.总结
平衡二叉树对树进行了高度的旋转处理,保证了平衡性,对动态查找非常有好处。