平衡二叉树(AVL树)

前言

之前看Nginx的红黑树结构,发现对平衡二叉树的相关内容基本都忘光了。当然,以前也就摸过皮毛,所以再学习学习。

AVL树

平衡二叉树,一种高度平衡的二叉排序树,它要么是一颗空树,要么每一节点的左子树左子树与右子树高度差绝对值至多为1。
因此,判断一刻平衡二叉树,两点:
  1. 首先是二叉排序树;
  2. 每一节点的左右子树深度只差的绝对值不超过1,这个差值我们称为平衡因子BF(Balance Factor)
距离插入节点最近的,且平衡因子绝对值大于1的节点为根的子树,称为 最小不平衡子树

用书中图表示下:
新插入节点为37,则以58为根节点的子树即为最小不平衡子树。


AVL树实现原理

基本思想:在构建二叉排序树的过程中,每当插入一个节点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。然后,在保持二叉排序树特性的前提下,调整最小不平衡子树中各节点之间的连接关系,进行对应旋转,使之成为新的平衡子树。

调整不平衡子树的对应情况如下:(左上角为临时的BF值)
  • a) BF值均为正,将树右转
             
  • b) BF值均为负,将树左转
             
  • c) BF值有正有负即不统一,则先旋转为统一,再重复a) 或 b),即双旋转
             

AVL树实现算法

先定义下AVL树节点结构以及预定宏:
#define    status  int
#define    TRUE    1   
#define    FALSE   0   

#define    LH      +1   /* 左高 */
#define    EH      0    /* 等高 */
#define    RH      -1   /* 右高 */

typedef struct avl_node_s  avl_node_t;
struct avl_node_s
{
    int data;
    int bf;
    avl_node_t *lchild;
    avl_node_t *rchild;
};
向右旋转方法:
/* 参考向右旋转图 */
void avl_right_rotate(avl_node_t **P)
{
    avl_node_t *L = NULL;
    
    /* L为向右旋转后的根节点 */
    L = (*P)->lchild;
    (*P)->lchild = L->rchild;
    L->rchild = (*P);
    *P = L;
}
此源码即为右旋操作,参考上图a),意为传入一个二叉排序树p,将其左孩子节点定义为L,将L的右子树变为P的左子树,再将P改成L的右子树,最后L替换P作为根节点。

同理,向左旋转方法:
void avl_left_rotate(avl_node_t **P)
{
    avl_node_t *R = NULL;
    
    R = (*P)->rchild;
    (*P)->rchild = R->lchild;
    R->lchild = (*P);
    *P = R;
}
下面是左旋转保持平衡的代码:
/* 算法结束时,指针P指向新的根节点 */
void avl_left_balance(avl_node_t **P)
{
    avl_node_t *L = NULL;
    avl_node_t *Lr = NULL;

    L = (*P)->lchild;
    switch (L->bf)
    {
        case LH:                   // 这里相当于上面的右转方法
            (*P)->bf = L->bf = EH;
            avl_right_rotate(P);
            break;
        case RH:                   // 这里相当于BF不统一,原理同上面的双旋转
            Lr = L->rchild;
            switch (Lr->bf)        // 这里用于修改传入的根节点P及其左孩子的平衡因子,不是很好理解,具体下面用图解释~
            {
                case LH:           // 下图(a)
                    (*P)->bf = RH;
                    L->bf = EH;
                    break;
                case EH:           // 下图(b)
                    (*P)->bf = L->bf = EH;
                    break;
                case RH:           // 下图(c)
                    (*P)->bf = EH;
                    L->bf = LH;
                    break;
            }
            Lr->bf = EH;
            avl_left_rotate(&(*P)->lchild);
            avl_right_rotate(P);
    }   
}
这段代码最不好理解的就是双旋转时,对于根节点及其左孩子最后的BF值修改。这里头必须自己先理清楚旋转之后的树结构,然后才能定下BF值。另外这有一个很重要的前提,就是必须保证 根节点P是最小不平衡子树的根。好吧,我是花了很久时间去理解这个了。如下面些图:
  • 情形(a) 
             
  • 情形(b)
             
  • 情形(c)
             
同理,右旋转保持平衡代码:
void avl_right_balance(avl_node_t **P)
{
    avl_node_t *R = NULL;
    avl_node_t *Rl = NULL;
    
    R = (*P)->rchild;
    switch (R->bf)
    {
        case RH:
            (*P)->bf = R->bf = EH;
            avl_left_rotate(P);
            break;
        case LH:
            Rl = R->lchild;
            switch (Rl->bf)
            {
                case LH:
                    (*P)->bf = EH;
                    R->bf = RH;
                    break;
                case EH:
                    (*P)->bf = R->bf = EH;
                    break;
                case RH:
                    (*P)->bf = EH;
                    R->bf = LH;
                    break;
            }
            Rl->bf = EH;
            avl_right_rotate(&(*P)->rchild);
            avl_left_rotate(P);
            break;
    }
}
下面,就是构建一棵AVL树的主函数:
/*
 * 创建一棵新AVL树及插入新节点。
 * P为需创建或需插入节点的AVL树,e为插入元素,taller反映P是否”增高“
 */
status avl_node_insert(avl_node_t **P, int e, status *taller)
{
    if (!(*P))
    {
        /* 空树,开始创建一颗新AVL树 */
        *P = (avl_node_t *)malloc(sizeof(avl_node_t));
        (*P)->data = e;
        (*P)->lchild = (*P)->rchild = NULL;
        (*P)->bf = EH;
        *taller = TRUE;
    }
    else
    {
        if (e == (*P)->data)
        {
            /* AVL树中已存在内容为e的节点,直接返回,不作插入操作 */
            *taller = FALSE;
            return FALSE;
        }
        
        if (e < (*P)->data)
        {
            /* 在P的左子树中做插入操作 */
            if (!avl_node_insert(&(*P)->lchild, e, taller)) // 未插入,则退出
            {
                return FALSE;
            }
            
            if (*taller)      /* 已插入到P中左子树中,并且树“增高” */
            {
                switch ((*P)->bf)
                {
                    case LH:  // 原本左子树就比右子树高,需做左平衡操作
                        avl_left_balance(P);
                        *taller = FALSE;
                        break;
                    case EH:  // 原本左右子树等高
                        (*P)->bf = LH;
                        *taller = TRUE;
                        break;
                    case RH:  // 原本左子树比右子树低,现在则等高
                        (*P)->bf = EH;
                        *taller = FALSE;
                        break;
                }
            }
        }
        else
        {
            /* 在P的右子树中做插入操作,原理同左子树 */
            if (!avl_node_insert(&(*P)->rchild, e, taller))
            {
                return FALSE;
            }
            
            if (*taller)
            {
                switch ((*P)->bf)
                {
                    case LH:
                        (*P)->bf = EH;
                        *taller = FALSE;
                        break;
                    case EH:
                        (*P)->bf = RH;
                        *taller = TRUE;
                        break;
                    case RH:
                        avl_right_balance(P);
                        *taller = FALSE;
                        break;
                }
            }
        }
    }
    
    return TRUE;
}
测试函数,具体的打印函数我就不写了,略懒。。用gdb等调试工具跟一下,就知道没什么问题。我已经单步跟了,确保没有问题。
void main()
{
    int i;
    int a[10] = {3, 2, 1, 4, 5, 6, 7, 10, 9, 8};
    avl_node_t *P = NULL;
    status taller;

    for (i = 0; i < 10; i++)
    {
        avl_node_insert(&P, a[i], &taller);
    }

    printf("Root's data == %d !\n", P->data);
}

总结

总结的话,就AVL树的内容而言没什么特别难的地方,就是每插入一个节点都需要确保保持自平衡。但就实现的算法而言,真正理解起来还是有点难度的,尤其是对于平衡因子的修改的那些代码,确实花了很多时间。

主要参考

《大话数据结构》

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值