15--二叉树的应用之平衡二叉树

407 阅读14分钟

关于树和二叉树的相关知识,以及数据结构与算法的相关知识,请查看文章《数据结构与算法基础知识文章汇总》

一、问题引入

在文章# 14--二叉树的应用之二叉搜索树介绍了二叉搜索树实现动态查找的算法实现,具体分析了二叉搜索树的查找、插入和删除的实现,也分析了插入算法的实现,但插入的逻辑仅限于保持二叉搜索树的如下特性:

  • 1.若它的左子树不为空,则左子树上的所有结点的值都小于根结点的值;
  • 2.若它的右子树不为空,则右子树上的所有结点的值都大于根结点的值;
  • 3.它的左右子树也都是二叉搜索树

对于二叉搜索树而言有左子树的所有结点的值 < 根结点的值 < 右子树所有结点的值。这样的特性更适合于无序数据的搜索算法的实现。对于有序的数据,将无法实现二叉搜索树的高效性。比如:

image.png

对于这样数据元素依次递增的查找表,即使实现了二叉搜索树,其搜索效率也和顺序搜索一样。

所以在往二叉搜索树中插入数据的时候就应该想办法保持树的左右子树的结点数量的相对平衡,使得查找的时候能充分利用二叉搜索树的特性来增加查找的效率

二、平衡二叉树定义及构建逻辑

1.定义

1.平衡二叉树的定义

平衡二叉树是一棵特殊二叉搜索树(二叉排序树),其中每一个结点的左右子树的高度差至多为1。平衡二叉树也叫AVL树

2.高度平衡

平衡二叉树的任意结点的左子树右子树都为平衡二叉树,且任意结点的左子树与右子树的深度的差值不超过1的绝对值,我们称左右子树的差值为平衡因子BF(Balance Factor)。

2.平衡二叉树的识别

image.png

  • 1.图1是平衡二叉树,因为左子树的值<根结点的值<右子树的值,并且所有结点的平衡因子BF的绝对值都小于等于1;
  • 2.图2不是一棵平衡二叉树,因为虽然所有结点的平衡因子BF的绝对值都小于等于1,但58的左孩子的值59比58大不满足二叉排序树的特性。

image.png

  • 3.图3不是一棵平衡二叉树,因为58结点的平衡因子BF等于3。

image.png

  • 4.图4是一棵平稀二叉树,因为它既满足二叉搜索树的特性,也满足所有结点的理衡因子的绝对值小于等于1。

3.最小不平衡子树

距离插入结点最近的,且平衡因子BF的绝对值大于1的的结点为根结点的子树,称为最小不平衡子树

举个🌰: image.png

上图中以58为根结点的子树即为最小不平衡子树。

4.平衡二叉树的构建的基本思想

在构建二叉树的过程中,每插入一个结点时,先检查是否因为插入而破坏了树的平衡性。若是,则找到最小不平衡子树,在保持二叉排序树特性的前提下,调整最小不平衡子树各结点之间的连接关系,使最小不平衡子树达到新的平衡

5.平衡二叉树的构建模拟

对下面这棵二叉排序树来构建平衡二叉树

image.png

构建平衡二叉树的结果如下:

image.png

下面来分析一下构建过程。

1.插入3、2、1结点时

image.png

  • 1.依次插入3、2、1结点;
  • 2.当插入1结点时,3结点的平衡因子BF = 左子树的深度2 - 右子树的深度0 = 2;
  • 3.此时,最小不平衡子树根结点3
  • 4.为了保持树的平衡,需要调整以3为根结点的子树的各结点之间的连接关系,使之达到新的平衡
  • 5.在不破坏二叉排序树的特性的前提下,将结点2变成新的根结点结点3变成结点2的右子树结点1还是结点2的左子树
  • 6.即按图中虚线箭头的方向进行右旋

2.插入结点4

image.png

  • 1.结点4比结点3大,所以结点4成为结点3的右孩子
  • 2.结点4插入后,没有破坏平衡,无需调整。

3.插入结点5

image.png

  • 1.结点5比结点4大,所以结点5成为结点4的右孩子
  • 2.此时,结点3的平衡因子BF = 左子树的深度0 - 右子树的深度2 = -2
  • 3.所以需要调整以3为根结点最小不平衡子树
  • 4.结点3的前一个结点的平衡因子为-2,与结点3的平衡因子的符号相同;
  • 5.将结点2的右孩子指向结点4,将结点4的左孩子指向结点3,结点5为依然为结点4的右子树;
  • 6.即按图中虚线箭头的方向进行左旋

4.插入结点6

image.png

  • 1.结点6比结点5大,所以结点6成为结点5的右孩子
  • 2.此时,结点2的BF = -2,即为最小不平衡子树
  • 3.将最小不平衡子树进行左旋,使得结点4成为根结点结点2成为结点4的左子树结点3成为结点2的右子树
  • 4.最终使得最小不平衡子树得到了新的平衡。

5.插入结点7

image.png

  • 1.插入结点7和插入结点5的操作相同;
  • 2.最小不平衡子树的要结点为结点5,对最小不平衡子树进行左旋,得到新的平衡。

6.插入结点10

image.png

  • 结点10的插入没有破坏平衡性,无需调整。

7.插入结点9

image.png

  • 1.此时,最小平衡子树的根结点为7,BF为-2,说明左子树失衡,所以对最小不平衡子树进行左旋;
  • 2.对最小不平衡子树左旋后的结果如图12,结点10的右孩子为结点9,10<9,不满足二叉排序树的特性,所以不能左旋

image.png

  • 1.因为最小不平衡子树的BF = -2,说明是左失衡
  • 2.但是最小平衡子树的右子树的BF = 1,与最小不平衡子树的BF的符号相异
  • 3.所以,在调整最小不平衡子树前,先保证最小不平衡子树的右子树的BF与最小不平衡子树的BF符号相同;调整结点9成为结点7的右孩子,结点10成为结点9的右子树,即对以结点10为根结点的子树进行右旋
  • 4.此时再对最小不平衡子树进行左旋,使之得到新的平衡;
  • 5.这种先右旋(左旋)再左旋(右旋)的调整平衡性的方式称为双旋

8.插入结点8

image.png

  • 1.插入结点8后,最小不平衡子树为结点6为根结点,BF为-2,说明是左失衡
  • 2.而最小不平衡子树的右子树的BF为1,与最小不平衡子树的BF符号相异;所以要先对最小不平衡子树的右子树进行右旋,再对最小不平衡子树进行左旋
  • 3.即插入结点8需要进行双旋

最终结果如下:

image.png

小结:

  • 1.最小不平衡子树的BF大于0,最小不平衡子树的左子树的BF大于0,两者符号相同,说明右失衡,需要右旋
  • 2.最小不平衡子树的BF小于0,最小不平衡子树的右子树的BF小于0,两者符号相同,说明左失衡,需要左旋
  • 3.最小不平衡子树的BF大于0,最小不平衡子树的左子树的BF大于0,两者符号相异,说明右失衡,需要先左旋,再右旋
  • 4.最小不平衡子树的BF小于0,最小不平衡子树的右子树的BF大于0,两者符号相异,说明左失衡,需要先右旋,再左旋

三、平衡二叉树的代码实现

0.状态定义和数据结构设计

1.状态定义

#define OK 1

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define MAXSIZE 100

typedef int Status;

2.数据结构定义

//结点结构
typedef struct BiTNode{
    //结点数据
    int data;

    //结点的平衡因子
    int bf;

    //结点左右孩子指针
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

3.平衡性状态定义

#define LH +1    //左高

#define EH 0     //等高 

#define RH -1    //右高

1.右旋的实现

1.实现逻辑 image.png

  • 1.P为最小不平衡子树的根结点,右旋最小不平衡子树;
  • 2.Lr成为P的左子树P成为L的右子树
  • 3.L成为新的根结点
  • 4.最终将最小不平衡子树变成了平衡二叉树。

2.实现代码

/*
 对以p为根的二叉排序树作右旋处理;
 处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点;
 */
void R_Rotate(BiTree *p){
    BiTree L;

    //① L是p的左子树;
     L = (*p)->lchild;

    //② L的右子树作为p的左子树
    (*p)->lchild =  L->rchild;

    //③ 将p作为L的右子树
     L->rchild = (*p);

    //④ 将L替换原有p的根结点位置
    *p =  L;
}

2.左旋的实现

1.实现逻辑 image.png

  • 1.P为最小不平衡子树的根结点,左旋最小不平衡子树;
  • 2.Rl成为P的右子树P成为R的左子树
  • 3.R成为新的根结点
  • 4.最终将最小不平衡子树变成了平衡二叉树。

2.实现代码

void L_Rotate(BiTree *p){
    BiTree R;

    //① R是p的右子树
    R = (*p)->rchild;

    //② R的左子树作为R的右子树
    (*p)->rchild = R->lchild;

    //③ 将p作为R的左子树;
    R->lchild = (*p);

    //④ 将R替换原有p的根结点的位置
    *p = R;
}

3.左平衡树失衡处理

/*
 对指针T所指结点为根的二叉树作左平衡旋转处理,算法结束后,指针T指向平衡处理后新的根结点
 */
void LeftBalance(BiTree *T)
{
    BiTree L,Lr;

    //1.L指向T的左子树根结点
    L=(*T)->lchild;

    //2.检查T的左子树的平衡度,并作相应平衡处理
    switch(L->bf)
    {
        //① 新结点插入在T的左孩子的左子树上,要作单右旋处理(如图1-平衡二叉树右旋解释图)
        case LH:
        {
            //L的平衡因子为LH,即为1时,表示它与根结点BF符合相同,则将它们(T,L)的BF值都改为EH(0)
            (*T)->bf=L->bf=EH;
            
            //对最小不平衡子树T进行右旋;
            R_Rotate(T);
            break;
        }

        //② LH的平衡因子为RH(-1)时,它与跟结点的BF值符合相反.此时需要做双旋处理(2次旋转处理)
        //新结点插入在T的左孩子的右子树上,要作 双旋处理
        case RH:
        {
            //Lr指向T的左孩子的右子树根
            Lr=L->rchild;

            //修改T及其左孩子的平衡因子
            switch(Lr->bf)
            {
                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;

            //对T的左子树作左旋平衡处理
            L_Rotate(&(*T)->lchild);

            //对T作右旋平衡处理
            R_Rotate(T);
        }
    }
}
  • 1.获取最小不平衡子树T的左子树L
  • 2.如果L的BF为LH,即T与L的BF符号相同,只需要右旋,T和L都达到平衡EH;
  • 3.如果L的BF为RH,即T与L的BF符号相异,此时需要调整T的各结点的BF值,然后再进行先左旋,再右旋的操作,使得T达到新的平衡。

4.右平衡树失衡处理

/*
 4. 右平衡树失衡处理
 对以指针T所指结点为根的二叉树作右平衡旋转处理
 本算法结束时,指针T指向新的根结点
 */
void RightBalance(BiTree *T)
{
    BiTree R,Rl;

    //1.R指向T的右子树根结点
    R=(*T)->rchild;

    //2. 检查T的右子树的平衡度,并作相应平衡处理
    switch(R->bf)
    {
        //① 新结点插入在T的右孩子的右子树上,要作单左旋处理
        case RH:
        {
            (*T)->bf=R->bf=EH;
            L_Rotate(T);
            break;
        }
        
        //新结点插入在T的右孩子的左子树上,要作双旋处理
        case LH:
        {
            //Rl指向T的右孩子的左子树根
            Rl=R->lchild;
            
            //修改T及其右孩子的平衡因子
            switch(Rl->bf)
            {
                case RH:
                {
                    (*T)->bf=LH;
                    R->bf=EH;
                    break;
                }
                case EH:
                {
                    (*T)->bf=R->bf=EH;
                    break;
                }
                case LH:
                {
                    (*T)->bf=EH;
                    R->bf=RH;
                    break;
                }
            }
            Rl->bf=EH;

            //对T的右子树作右旋平衡处理
            R_Rotate(&(*T)->rchild);

            //对T作左旋平衡处理
            L_Rotate(T);
        }
    }
}
  • 1.获取最小不平衡子树T的左子树R
  • 2.如果R的BF为RH,即T与R的BF符号相同,只需要左旋,T和R都达到平衡EH;
  • 3.如果R的BF为LH,即T与R的BF符号相异,此时需要调整T的各结点的BF值,然后再进行先右旋,再左旋的操作,使得T达到新的平衡。

5.平衡二叉树的插入实现

Status InsertAVL(BiTree *T,int e,Status *taller)
{
    if(!*T)
    {   //1.插入新结点,树“长高”,置taller为TRUE
        //① 开辟一个新结点T;
        *T=(BiTree)malloc(sizeof(BiTNode));
        
        //② 对新结点T的data赋值,并且让其左右孩子指向为空,T的BF值为EH;
        (*T)->data=e;

        (*T)->lchild = (*T)->rchild = NULL;
        (*T)->bf = EH;

        //③ 新结点默认"长高"
        *taller = TRUE;
    }
    else
    {
        if (e==(*T)->data)
        {  //2.树中已存在和e有相同关键字的结点则不再插入
            *taller = FALSE;
            return FALSE;
        }

        if (e<(*T)->data)
        {
            //3.应继续在T的左子树中进行搜索
            if(!InsertAVL(&(*T)->lchild,e,taller))
            {
                //未插入
                return FALSE;
            }

            //4.已插入到T的左子树中且左子树“长高”
            if(*taller)
            {
                //5.检查T的平衡度
                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
        { //6.应继续在T的右子树中进行搜索
            //未插入
            if(!InsertAVL(&(*T)->rchild,e,taller))
            {
                return FALSE;
            }

            //已插入到T的右子树且右子树“长高”
            if(*taller)
            {
                // 检查T的平衡度
                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 TRUE;
}

6.平衡二叉树查找

Status SearchBST(BiTree T,int key,BiTree f, BiTree *p){

    if(!T)    //查找不成功
    {
        *p = f;
        return FALSE;
    }
    else if(key==T->data) //查找成功
    {
        *p = T;
        return TRUE;
    }
    else if(key<T->data)
    {
        return SearchBST(T->lchild, key, T, p);  //在左子树中继续查找
    }
    else
    {
        return SearchBST(T->rchild, key, T, p);  //在右子树中继续查找 
    }
}

7.调试代码

printf("平衡二叉树 !\n");
    int i;

    int a[10]={3,2,1,4,5,6,7,10,9,8};

    //调整数组的顺序,最终生成的平衡二叉树高度是一样的.
    //int a[10]={8,9,1,4,5,6,7,10,2,3};
    //int a[10]={9,4,1,2,7,6,5,10,3,8};

    BiTree T = NULL;
    Status taller;

    int sum = 0;
    
    for(i=0;i<10;i++)
    {
        InsertAVL(&T,a[i],&taller);
        sum += taller;
        printf("插入%d,是否增加树的高度(%d)[YES->1 / NO->0]\n",a[i],taller);

    }

    printf("将数组a插入到平衡二叉树后,最终形成高度为%d的平衡二叉树\n",sum);

    BiTree p;

    int statusValue = SearchBST(T, 10, NULL, &p);

    printf("查找%d是否成功:%d (1->YES/0->NO)\n",p->data,statusValue);

四、总结

  • 1.平衡二叉树是一棵特殊的二叉排序树,它的任意结点的平衡因子BF的绝对值都小于等于1,并且以任意结点为根结点的子树都是平衡二叉树;
  • 2.平衡二叉树是针对有序数列构建二叉排序树在插入数据时为实现查找算法更加高效而设计的;
  • 3.构建平衡二叉树时,在插入结点后,要检查树是否失衡;找到最小不平衡子树,通过调整最小不平衡子树中各结点连接关系,来达到新的平衡

最小不平衡子树的调整有如下规则:

  • 1.最小不平衡子树的BF大于0,最小不平衡子树的左子树的BF大于0,两者符号相同,说明右失衡,需要右旋
  • 2.最小不平衡子树的BF小于0,最小不平衡子树的右子树的BF小于0,两者符号相同,说明左失衡,需要左旋
  • 3.最小不平衡子树的BF大于0,最小不平衡子树的左子树的BF大于0,两者符号相异,说明右失衡,需要先左旋,再右旋
  • 4.最小不平衡子树的BF小于0,最小不平衡子树的右子树的BF大于0,两者符号相异,说明左失衡,需要先右旋,再左旋