平衡二叉树,又称为AVL树。实际上就是遵循以下两个特点的二叉树:
其实就是在二叉树的基础上,若树中每棵子树都满足其左子树和右子树的深度差都不超过 1,则这棵二叉树就是平衡二叉树。
图 1 平衡与不平衡的二叉树及结点的平衡因子
平衡因子:每个结点都有其各自的平衡因子,表示的就是其左子树深度同右子树深度的差。平衡二叉树中各结点平衡因子的取值只可能是:0、1和-1。
如图1所示,其中 (a) 的两棵二叉树中由于各个结点的平衡因子数的绝对值都不超过1,所以 (a) 中两棵二叉树都是平衡二叉树;而 (b) 的两棵二叉树中有结点的平衡因子数的绝对值超过1,所以都不是平衡二叉树。
为了排除动态查找表中不同的数据排列方式对算法性能的影响,需要考虑在不会破坏二叉排序树本身结构的前提下,将二叉排序树转化为平衡二叉树。
例如,在对查找表{13,24,37,90,53}构建二叉排序树时,当插入13和24时,二叉排序树此时还是平衡二叉树:
图 2 平衡二叉树
当继续插入37时,生成的二叉排序树如图3(a),平衡二叉树的结构被破坏,此时只需要对二叉排序树做“旋转”操作(如图3(b)),即整棵树以结点24为根结点,二叉排序树的结构没有破坏,同时将该树转化为了平衡二叉树:
图 3 二叉排序树变为平衡二叉树的过程
当二叉排序树的平衡性被打破时,就如同扁担的两头出现了一头重一头轻的现象,如图3(a)所示,此时只需要改变扁担的支撑点(树的树根),就能使其重新归为平衡。实际上图 3 中的 (b) 是对(a) 的二叉树做了一个向左逆时针旋转的操作。
继续插入90和53后,二叉排序树如图4(a)所示,导致二叉树中结点24和37的平衡因子的绝对值大于1,整棵树的平衡被打破。此时,需要做两步操作:
如图4(b)所示,将结点53和90整体向右顺时针旋转,使本该以90为根结点的子树改为以结点53为根结点;
如图4(c)所示,将以结点37为根结点的子树向左逆时针旋转,使本该以37为根结点的子树,改为以结点53为根结点; 图 4 二叉排序树转化为平衡二叉树
做完以上操作,即完成了由不平衡的二叉排序树转变为平衡二叉树。
当平衡二叉树由于新增数据元素导致整棵树的平衡遭到破坏时,就需要根据实际情况做出适当的调整,假设距离插入结点最近的“不平衡因子”为 a。则调整的规律可归纳为以下4种情况:
单向右旋平衡处理: 图 5 单向右旋
若由于结点a的左子树为根结点的左子树上插入结点,导致结点a的平衡因子由1增至2,致使以a为根结点的子树失去平衡,则只需进行一次向右的顺时针旋转,如下图这种情况:
单向左旋平衡处理:
如果由于结点a的右子树为根结点的右子树上插入结点,导致结点a的平衡因子由-1变为-2,则以a为根结点的子树需要进行一次向左的逆时针旋转,如下图这种情况:
双向旋转(先左后右)平衡处理: 图 7 双向旋转(先左后右)
如果由于结点a的左子树为根结点的右子树上插入结点,导致结点a平衡因子由1增至2,致使以a为根结点的子树失去平衡,则需要进行两次旋转操作,如下图这种情况:
注意:图 7 中插入结点也可以为结点C的右孩子,则(b)中插入结点的位置还是结点C右孩子,(c)中插入结点的位置为结点A的左孩子。
双向旋转(先右后左)平衡处理: 图 8 双向旋转(先右后左)
如果由于结点a的右子树为根结点的左子树上插入结点,导致结点a平衡因子由-1变为-2,致使以a为根结点的子树失去平衡,则需要进行两次旋转(先右旋后左旋)操作,如下图这种情况:
注意:图8中插入结点也可以为结点C的右孩子,则(b)中插入结点的位置改为结点B的左孩子,(c)中插入结点的位置为结点B的左孩子。
在对查找表{13,24,37,90,53}构建平衡二叉树时,由于符合第4条的规律,所以进行先右旋后左旋的处理,最终由不平衡的二叉排序树转变为平衡二叉树。
//分别定义平衡因子
#define LH +1
#define EH 0
#define RH -1
typedef struct BSTNode{
int data;
int bf;
BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//对以T为根结点的二叉树做右旋处理,令T指针指向新的根结点
void R_Rotate(BSTree *T){
BSTree lc = (*T)->lchild;
(*T)->lchild = lc->rchild;
lc->rchild = *T;
*T = lc;
}
//对以T为根结点的二叉树做左旋处理,令T指针指向新的根结点
void L_Rotate(BSTree *T){
BSTree rc = (*T)->rchild;
(*T)->rchild = rc->lchild;
rc->lchild = *T;
*T = rc;
}
//对以指针 T 所指向结点为根结点的二叉树作左子树的平衡处理,令指针 T 指向新的根结点
void LeftBalance(BSTree *T){
BSTree lc, rd;
lc = (*T)->lchild;
//查看以 T 的左子树为根结点的子树,失去平衡的原因,如果 bf 值为 1 ,则说明添加在左子树为根结点的左子树中,需要对其进行右旋处理;反之,如果 bf 值为 -1,说明添加在以左子树为根结点的右子树中,需要进行双向先左旋后右旋的处理
switch (lc->bf) {
case LH:
(*T)->bf = lc->bf = EH;
R_Rotate(T);
break;
case RH:
rd = lc->rchild;
switch (rd->bf) {
case LH:
(*T)->bf = RH;
lc->bf = EH;
break;
case EH:
(*T)->bf = lc->bf = EH;
break;
case RH:
(*T)->bf = EH;
lc->bf = LH;
break;
}
rd->bf = EH;
L_Rotate(&(*T)->lchild);
R_Rotate(T);
break;
}
}
//右子树的平衡处理同左子树的平衡处理完全类似
void RightBalance(BSTree* T)
{
BSTree lc,rd;
lc= (*T)->rchild;
switch (lc->bf)
{
case RH:
(*T)->bf = lc->bf = EH;
L_Rotate(T);
break;
case LH:
rd = lc->lchild;
switch(rd->bf)
{
case LH:
(*T)->bf = EH;
lc->bf = RH;
break;
case EH:
(*T)->bf = lc->bf = EH;
break;
case RH:
(*T)->bf = EH;
lc->bf = LH;
break;
}
rd->bf = EH;
R_Rotate(&(*T)->rchild);
L_Rotate(T);
break;
}
}
int InsertAVL(BSTree *T, int key, bool *taller){
if ((*T) == NULL){//如果本身为空树,则直接添加key为根结点
(*T) = new BSTNode;
(*T)->bf = EH;
(*T)->lchild = NULL;
(*T)->rchild = NULL;
(*T)->data = key;
*taller = true;
}
else if ((*T)->data == key){//若二叉排序树中存在关键字key,则不做任何处理
*taller = false;
return 0;
}
else if ((*T)->data > key){//若关键字小于结点T的数据域,则插入到T的左子树
if (!InsertAVL(&(*T)->lchild, key, taller)) return 0;//若插入不影响树的平衡,则直接结束
if (*taller){
//判断根结点 T 的平衡因子是多少,由于是在其左子树添加新结点的过程中导致失去平衡,所以当 T 结点的平衡因子本身为 1 时,需要进行左子树的平衡处理,否则更新树中各结点的平衡因子数
switch ((*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 {
//同样,当 key>T->data 时,需要插入到以 T 为根结点的树的右子树中,同样需要做和以上同样的操作
if (!InsertAVL(&(*T)->rchild, key, taller)) return 0;
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;
}
}
}
return 1;
}
//判断现有平衡二叉树中是否依据具有关键字key的结点
bool FindNode(BSTree T, int key, BSTree *pos){
BSTree p = T;
(*pos) = NULL;
while (p){
if (p->data == key){
(*pos) = p;
return true;
}
else if (p->data > key) p = p->lchild;
else p = p->rchild;
}
return false;
}
//中序遍历
void InorderTra(BSTree T){
if (T->lchild) InorderTra(T->lchild);
cout << T->data << " ";
if (T->rchild) InorderTra(T->rchild);
}
int main(){
int a[9] = {1, 23, 45, 34, 98, 9, 4, 35, 23};
BSTree root = NULL, pos;
bool taller;
for (int i = 0; i < 9; i++){
InsertAVL(&root, a[i], &taller);
}
if (FindNode(root, 45, &pos)) cout << pos->data << endl;
InorderTra(root);
return 0;
}
使用平衡二叉树进行查找操作的时间复杂度为O(logn)。
在平衡失衡后,不管如何旋转其目的都是为了1.降低高度;2.保持二叉树的性质。所以我们可以将三个结点的关键字进行“比较”中间值为根结点,最小值为左子树,最大值为右子树,如图所示。