AVL平衡二叉排序树 (C语言)


一 前言

  在了解平衡二叉排序树前,请大家务必掌握 BS二叉排序树的相关概念,关于二叉排序树的代码实现,小编另一篇博客: 二叉排序树结点的插入与删除操作 中有详细介绍,此处不再赘述,咱们直奔主题!!!

二 平衡二叉排序树阐述

1. 二叉排序树的不足

小编这里有两个不同的数组,请大家按照数组中从前到后的插入顺序,画出相应的二叉排序树。

  • [5, 9, 8, 3, 2, 4, 1, 7]
  • [1, 2, 3, 4, 5]

相信大家画出的树的形状如下:
画出的树的形状
  我们知道,对于一棵树而言,在树中实现查找等操作,效率的高低主要取决于树的高度,树高越小,查找的速度越快,反之查找的速度越慢。这也是我们为什么偏爱于完全二叉树、满二叉树和完美二叉树的原因了。
  由上图可以看出,若结点插入的顺序是升序或降序时,树会退化成链表, 查找的时间复杂度也从 O(logN) 下降到了 O(N),这显然是我们不想看到的,为此我们引入了平衡化概念,平衡化的方法有很多,我们最为熟知的就是AVL树,即把左右子树的树高进行作为平衡化的依据

2. 平衡二叉排序树的性质

  平衡二叉排序树也称为AVL树,这是根据发明者的姓名命名的。截止到现在小编写博客,算法提出已经58年,这足够称做“上古级别”的算法了。
  算法规定:任意结点左右子树的高度差不超过1, 从而使整棵树向满二叉树尽可能的靠近,提高查找效率。
平衡二叉排序树的性质

3. 平衡二叉排序树效率分析

现在我们来思考这样一个问题:
思考题
BS树的上下限和AVL树的上限都很好求,关键是AVL树的包含结点数量下界难以确定,不急,小编先公布答案再解释。
思考题ans
BS 树结点数量最小值:   H (树退化成链表)
BS树结点数量最大值:   2H - 1 (完美二叉树)
AVL树结点数量最大值:  2H - 1 (完美二叉树)

AVL树结点数量最小值: 5 5 \frac {\sqrt 5}{5} 55 * [ ( 5 + 1 2 ) (\frac{\sqrt 5 + 1}{2}) (25 +1) H - ( 1 − 5 2 ) (\frac{1 - \sqrt 5}{2}) (215 ) H ]
这个结论我们是通过数学归纳法得到的:设low(H)表示当AVL树高度为H时最少的结点数量,我们不难得出这样的一个递推公式: l o w ( H ) = l o w ( H − 1 ) + l o w ( H − 2 ) + 1 low(H) = low(H - 1) + low(H - 2) + 1 low(H)=low(H1)+low(H2)+1,忽略最后的常数项1,这就是我们熟知的斐波那契数列公式,关于求解的具体步骤,这里分享一篇博客 求斐波那契数列的特征方程和通项公式,这个通项公式对应指数曲线大致为:y = 1.5H ,由此,我们知道: 1.5H <= size(H) <= 2H - 1 所以对应效率在: l o g 1.5 H log_{1.5}^{H} log1.5H l o g 2 H log_{2}^{H} log2H之间,这个结果依旧在对数函数的范畴,所以效率还是很高的。

4. 左旋、右旋以及四种失衡类型

  AVL树千般好万般好,那当插入或删除结点时造成失衡现象到底应该如何处理呢?
  首先我们来说两种基本的旋转方式:左旋和右旋。
左旋
右旋
  接下来就是四种失衡类型:LL、 LR、 RL、 RR。
  这里需要解释一下这二个字母的意思:第一个字母表示该结点的左子树和右子树那个树更高,若左子树更高第一个字母为L,反之为R;第二个字母在第一个字母的基础上,以子树的树根为根结点,比较根结点的左右子树的高度,若左子树更高第一个字母为L,反之为R。
失衡类型
为了更好的了解旋转的过程,我们就LL和LR两种失衡类型进行讲解,剩余两种可以类比。

5. LL型和LR型的调整策略与合理性证明

  1. LL 类型
    失衡结点为K1, 根据类型我们知道,根结点左子树比右子树高,同时根结点左子树的左子树树高比根结点左子树的右子树的树高要高。
    对于这种情况(参照下图),我们直接执行大右旋操作,也就是直接将K2作为根结点,将K1作为K2的右孩子,K1原先的右孩子变成了K1的左孩子。
    我们假设子树A的树高用H(a),那么 H ( k 2 ) = H ( a ) + 1 , H ( k 1 ) = H ( a ) + 2 , H ( b ) = H ( a ) − 1 , H ( k 3 ) = H ( k 2 ) − 2 = H ( a ) − 1 , m a x ( H ( c ) , H ( d ) ) = H ( k 3 ) − 1 = H ( a ) − 2 ; H(k2) = H(a) + 1, H(k1) = H(a) + 2, H(b) = H(a) - 1, H(k3) = H(k2) - 2 = H(a) - 1, max(H(c), H(d)) = H(k3) - 1 = H(a) - 2; H(k2)=H(a)+1,H(k1)=H(a)+2,H(b)=H(a)1,H(k3)=H(k2)2=H(a)1,max(H(c),H(d))=H(k3)1=H(a)2;
    大右旋之后,除了K2, K1结点的高度发生改变,剩余的结点高度都没有变,对于K1结点,它现在的左右孩子分别为B,K3,在上面我们已求得: H ( b ) = H ( a ) − 1 , H ( k 3 ) = H ( a ) − 1 H(b) = H(a) - 1, H(k3) = H(a) - 1 H(b)=H(a)1,H(k3)=H(a)1,所以对于K2结点来说是满足AVL树性质的,接下来就是K2结点,K2结点的左右孩子分别是:A,K1,而 H ( a ) = H ( a ) , H ( k 1 ) = m a x ( H ( b ) , H ( k 3 ) ) + 1 = H ( a ) H(a) = H(a), H(k1) = max(H(b), H(k3)) + 1 = H(a) H(a)=H(a),H(k1)=max(H(b),H(k3))+1=H(a),所以这显然也是平衡的。
    至此,关于LL失衡类型的讲解就完成了。

    LL
  2. LR 类型
    失衡结点为K1, 根据类型我们知道,根结点左子树比右子树高,同时根结点左子树的右子树树高比根结点左子树的左子树的树高要高。
    对于这种情况(参照下图),我们先要执行小左旋操作,就是以左子树为作用对象,对左子树执行左旋操作,K2下移,K3接替K2的位置,同时B子树成为K2的右子树,小左旋完成。之后对整棵树实行大右旋操作,K1结点下移,K3接替K1的位置,同时K1成为K3的右子树,K3原来的右子树成为K1的左子树。至此大右旋完成。
    接下来,我们依旧和上面一样进行证明:设A子树的树高为:H(a), 那么 H ( k 3 ) = H ( a ) + 1 , H ( k 2 ) = H ( k 3 ) + 1 = H ( a ) + 2 , H ( k 1 ) = H ( a ) + 3 , H ( d ) = H ( k 2 ) − 2 = H ( a ) , m a x ( H ( b ) , H ( c ) ) = H ( k 3 ) − 1 = H ( a ) H(k3) = H(a) + 1, H(k2) = H(k3) + 1 = H(a) + 2, H(k1) = H(a) + 3, H(d) = H(k2) - 2 = H(a), max(H(b), H(c)) = H(k3) - 1 = H(a) H(k3)=H(a)+1,H(k2)=H(k3)+1=H(a)+2,H(k1)=H(a)+3,H(d)=H(k2)2=H(a),max(H(b),H(c))=H(k3)1=H(a),经过先小左旋后大右旋,结点K1,K2,K3的高度都发生了改变, H ( k 2 ) = m a x ( H ( a ) , H ( b ) ) + 1 = H ( a ) + 1 , H ( k 1 ) = m a x ( H ( c ) , H ( d ) ) + 1 = H ( a ) + 1 , H ( k 3 ) = H ( a ) + 2 , H(k2) = max(H(a), H(b)) + 1 = H(a) + 1, H(k1) = max(H(c), H(d)) + 1 = H(a) + 1, H(k3) = H(a) + 2, H(k2)=max(H(a),H(b))+1=H(a)+1,H(k1)=max(H(c),H(d))+1=H(a)+1,H(k3)=H(a)+2显然在计算高度是我们发现,K1,K2,K3结点的左右子树的高度差都不超过1,满足AVL树的性质。

LR1
LR2

  1. RR和RL类型
    关于RR和RL的调整和证明方法,可以类比上面两种,所以小编就不再赘述,现在咱们来总结一下调整方法。
    LL 类型:整体大右旋
    LR 类型:先对左子树小左旋,之后对整棵树大右旋
    RR 类型:整体左旋
    RL 类型:先对右子树小右旋,之后对整棵树大左旋

三 AVL树代码实现

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define H(root) (root)->h
#define K(root) (root)->key
#define L(root) (root)->lchild
#define R(root) (root)->rchild

typedef struct Node {
    int key, h;
    struct Node *lchild, *rchild;
} Node;

Node __NIL;
#define NIL (&__NIL)
__attribute__((constructor))
void init_NIL() {
    NIL->key = NIL->h = 0;
    NIL->lchild = NIL->rchild = NIL;
    return ;
}

// 根据传入的值,返回创建的结点
Node *getNewNode(int key) {
    Node *p = (Node *)malloc(sizeof(Node));
    p->key = key;
    p->h = 1;
    p->lchild = p->rchild = NIL;
    return p;
}

// 根据左右子树的高度,调整根结点的高度
void update_height(Node *root) {
    root->h = (H(L(root)) > H(R(root)) ? H(L(root)) : H(R(root))) + 1;
    return ;
}

// 左旋
Node *left_rotate(Node *root) {
    Node *temp = root->rchild;
    root->rchild = temp->lchild;
    temp->lchild = root;
    update_height(root);
    update_height(temp);
    return temp;
}

 // 右旋
Node *right_rotate(Node *root) {
    Node *temp = root->lchild;
    root->lchild = temp->rchild;
    temp->rchild = root;
    update_height(root);
    update_height(temp);
    return temp;
}

// 插入或删除结点之后,进行失衡调整
Node *maintain(Node *root) {
    // 未失衡
    // 正是在此处,因为可能左右子树中由一个为NULL,直接访问出错,所以我们引入NIL指针
    if (abs(H(L(root)) - H(R(root))) <= 1) return root;
    // 失衡,且第一个字母是L
    if (H(L(root)) > H(R(root))) {
        // root左子树的右子树高度比root左子树左子树更高,第二个字母为R, 小左旋
        if (H(R(L(root))) > H(L(L(root)))) {
            root->lchild = left_rotate(root->lchild);
        }
        // LL和LR 都需要大右旋
        root = right_rotate(root);
    } else {
        // 第二个字母为L,类型为RL,先小右旋
        if (H(L(R(root))) > H(R(R(root)))) {
            root->rchild = right_rotate(root->rchild);
        }
        // RR和RL都需要大左旋 
        root = left_rotate(root);
    }
    return root;
}

// 插入结点
Node *insert(Node *root, int key) {
    if (root == NIL) return getNewNode(key);
    if (root->key == key) return root;
    if (root->key > key) root->lchild = insert(root->lchild, key);
    else root->rchild = insert(root->rchild, key);
    update_height(root);
    return maintain(root);
}

// 找到结点的前驱结点
Node *predeccessor(Node *root) {
    Node *temp = root->lchild;
    while (temp->rchild != NIL) temp = temp->rchild;
    return temp;
}

// 删除值为key的结点
Node *erase(Node *root, int key) {
    if (root == NIL) return root;
    if (root->key > key) {
        root->lchild = erase(root->lchild, key);
    } else if (root->key < key) {
        root->rchild = erase(root->rchild, key);
    } else {
        // 删除度为0或1的结点
        if (root->lchild == NIL || root->rchild == NIL) {
            Node *temp = root->lchild == NIL ? root->rchild : root->lchild;
            free(root);
            return temp;
        } else {
            // 删除度为2的结点
            Node *temp = predeccessor(root);
            root->key = temp->key;
            root->lchild = erase(root->lchild, temp->key);
        }
    }
    return maintain(root);
}

// 删除树
void clear(Node *root) {
    if (root == NIL) return ;
    clear(root->lchild);
    clear(root->rchild);
    free(root);
    return ;
}

// 前序遍历输出
void output(Node *root) {
    if (root == NIL) return ;
    printf("(%d, %d, %d)\n", K(root), K(L(root)), K(R(root)));
    output(root->lchild);
    output(root->rchild);
    return ;
}

int main() {
    srand(time(0));
    #define MAX_OP 20
    Node *root = NIL;
    for (int i = 0; i < MAX_OP; i++) {
        int val = rand() % 100;
        root = insert(root, val);
    }
    output(root);
    int val;
    while (~scanf("%d", &val)) {
        root = erase(root, val);
        printf("erase %d from AVL tree\n", val);
        output(root);
    }
    return 0;
}

/* 输出如下:
(56, 45, 86)
(45, 21, 49)
(21, 9, 31)
(9, 0, 0)
(31, 0, 0)
(49, 48, 52)
(48, 0, 0)
(52, 0, 0)
(86, 75, 89)
(75, 69, 77)
(69, 58, 73)
(58, 0, 0)
(73, 0, 0)
(77, 0, 78)
(78, 0, 0)
(89, 87, 96)
(87, 0, 0)
(96, 0, 0)
56
erase 56 from AVL tree
(52, 45, 86)
(45, 21, 49)
(21, 9, 31)
(9, 0, 0)
(31, 0, 0)
(49, 48, 0)
(48, 0, 0)
(86, 75, 89)
(75, 69, 77)
(69, 58, 73)
(58, 0, 0)
(73, 0, 0)
(77, 0, 78)
(78, 0, 0)
(89, 87, 96)
(87, 0, 0)
(96, 0, 0)
*/

大家可以根据输出内容画出这棵树,删除或插入结点之后,可以再次查看画出这棵树,验证代码是否正确

四 其他平衡化方法

  除了最典型的AVL树(根据左右子树的高度来调整),我们还可以根据左右子树的结点的数量来实现平衡化
  这就是SB树,以下就是SB树的性质介绍,这里小编就不多说了,只是作为扩展内容介绍给大家。
SB Tree

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值