前言

Wiki:在计算机科学中,AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(logn)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL 树得名于它的发明者 G. M. Adelson-VelskyEvgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。

1 为什么要有平衡二叉树

二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图 1.1。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。

图 1.1

在此二叉搜索树中查找元素 6 需要查找 6 次。

二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为图 1.2 的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。

图 1.2

可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。

这种左右子树的高度相差不超过 1 的树为平衡二叉树。

2. 定义

平衡二叉查找树:简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质:

  1. 可以是空树。
  2. 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。

平衡之意,如天平,即两边的分量大约相同。

例如图 2.1 不是平衡二叉树,因为结点 60 的左子树不是平衡二叉树。

图 2.1

图 2.2 也不是平衡二叉树,因为虽然任何一个结点的左子树与右子树都是平衡二叉树,但高度之差已经超过 1 。

图 2.2

图 2.3 是平衡二叉树。

图 2.3

3. 平衡因子

定义:某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。

图 3.1 图 3.2 图 3.3

4. 节点结构

定义平衡二叉树的节点结构:

typedef struct AVLNode *Tree;

typedef int ElementType;

struct AVLNode{

    int depth; //深度,这里计算每个结点的深度,通过深度的比较可得出是否平衡

    Tree parent; //该结点的父节点

    ElementType val; //结点值

    Tree lchild;

    Tree rchild;

    AVLNode(int val=0) {
        parent = NULL;
        depth = 0;
        lchild = rchild = NULL;
        this->val=val;
    }
};

5. AVL树插入时的失衡与调整

图 5.1 是一颗平衡二叉树

图 5.1

在此平衡二叉树插入节点 99 ,树结构变为:

动图 5.2

在动图 5.2 中,节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2,树失去平衡。

在动图 5.2 中,以节点 66 为父节点的那颗树就称为 最小失衡子树

最小失衡子树:在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。

平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋右旋

旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。

5.1 左旋

图 5.1.1

以图 5.1.1 为例,加入新节点 99 后, 节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2。为保证树的平衡,此时需要对节点 66 做出旋转,因为右子树高度高于左子树,对节点进行左旋操作,流程如下:

(1)节点的右孩子替代此节点位置
(2)右孩子的左子树变为该节点的右子树
(3)节点本身变为右孩子的左子树

整个操作流程如动图 5.1.2 所示。

动图 5.1.2

  • 节点的右孩子替代此节点位置 —— 节点 66 的右孩子是节点 77 ,将节点 77 代替节点 66 的位置
  • 右孩子的左子树变为该节点的右子树 —— 节点 77 的左子树为节点 75,将节点 75 挪到节点 66 的右子树位置
  • 节点本身变为右孩子的左子树 —— 节点 66 变为了节点 77 的左子树

5.2 右旋

右旋操作与左旋类似,操作流程为:

(1)节点的左孩子代表此节点
(2)节点的左孩子的右子树变为节点的左子树
(3)将此节点作为左孩子节点的右子树。

动图 5.2.1

6. AVL树的四种插入节点方式

假设一颗 AVL 树的某个节点为 A,有四种操作会使 A 的左右子树高度差大于 1,从而破坏了原有 AVL 树的平衡性。平衡二叉树插入节点的情况分为以下四种:

插入方式 描述 旋转方式
LL 在 A 的左子树根节点的左子树上插入节点而破坏平衡 右旋转
RR 在 A 的右子树根节点的右子树上插入节点而破坏平衡 左旋转
LR 在A的左子树根节点的右子树上插入节点而破坏平衡 先左旋后右旋
RL 在 A 的右子树根节点的左子树上插入节点而破坏平衡 先右旋后左旋

具体分析如下:

6.1 A的左孩子的左子树插入节点(LL)

只需要执行一次右旋即可。

动图 6.1

实现代码如下:

//LL型调整函数
//返回:新父节点
Tree LL_rotate(Tree node){
    //node为离操作结点最近的失衡的结点
    Tree parent=NULL,son;
    //获取失衡结点的父节点
    parent=node->parent;
    //获取失衡结点的左孩子
    son=node->lchild;
    //设置son结点右孩子的父指针
    if (son->rchild!=NULL)  son->rchild->parent=node;
    //失衡结点的左孩子变更为son的右孩子
    node->lchild=son->rchild;
    //更新失衡结点的高度信息
    update_depth(node);
    //失衡结点变成son的右孩子
    son->rchild=node;
    //设置son的父结点为原失衡结点的父结点
    son->parent=parent;
    //如果失衡结点不是根结点,则开始更新父节点
    if (parent!=NULL){
        //如果父节点的左孩子是失衡结点,指向现在更新后的新孩子son
        if (parent->lchild==node){
            parent->lchild=son;
        }else{
             //父节点的右孩子是失衡结点
              parent->rchild=son;
        }
     }
    //设置失衡结点的父亲
    node->parent=son;
    //更新son结点的高度信息
    update_depth(son);
    return son;
}

6.2 A的右孩子的右子树插入节点(RR)

只需要执行一次左旋即可。

动图 6.2

实现代码如下:

//RR型调整函数
//返回新父节点
Tree RR_rotate(Tree node){
    //node为离操作结点最近的失衡的结点
    Tree parent=NULL,son;
    //获取失衡结点的父节点
    parent=node->parent;
    //获取失衡结点的右孩子
    son=node->rchild;
    //设置son结点左孩子的父指针
    if (son->lchild!=NULL){
          son->lchild->parent=node;
    }
    //失衡结点的右孩子变更为son的左孩子
    node->rchild=son->lchild;
    //更新失衡结点的高度信息
    update_depth(node);
    //失衡结点变成son的左孩子
    son->lchild=node;
    //设置son的父结点为原失衡结点的父结点
    son->parent=parent;
    //如果失衡结点不是根结点,则开始更新父节点
    if (parent!=NULL){
        //如果父节点的左孩子是失衡结点,指向现在更新后的新孩子son
        if (parent->lchild==node){
            parent->lchild=son;
        }else{
            //父节点的右孩子是失衡结点
            parent->rchild=son;
        } 
    }
    //设置失衡结点的父亲
    node->parent=son;
    //更新son结点的高度信息
    update_depth(son);
    return son;
}

6.3 A的左孩子的右子树插入节点(LR)

若 A 的左孩子节点 B 的右子树 E 插入节点 F ,导致节点 A 失衡,如图:

图 6.3

A 的平衡因子为 2 ,若仍按照右旋调整,则变化后的图形为这样:

图 6.3.1

经过右旋调整发现,调整后树仍然失衡,说明这种情况单纯的进行右旋操作不能使树重新平衡。那么这种插入方式需要执行两步操作,使得旋转之后为 原来根结点的左孩子的右孩子作为新的根节点

(1)对失衡节点 A 的左孩子 B 进行左旋操作,即上述 RR 情形操作。
(2)对失衡节点 A 做右旋操作,即上述 LL 情形操作。

调整过程如下:

图 6.3.2
图 6.3.3

也就是说,经过这两步操作,使得 原来根节点的左孩子的右孩子 E 节点成为了新的根节点

代码实现:

//LR型,先左旋转,再右旋转
//返回:新父节点
Tree LR_rotate(Tree node){
    RR_rotate(node->lchild);
    return LL_rotate(node);
}

6.4 A的右孩子的左子树插入节点(RL)

右孩子插入左节点的过程与左孩子插入右节点过程类似,也是需要执行两步操作,使得旋转之后为 原来根结点的右孩子的左孩子作为新的根节点

(1)对失衡节点 A 的右孩子 C 进行右旋操作,即上述 LL 情形操作。
(2)对失衡节点 A 做左旋操作,即上述 RR 情形操作。

图 6.4
图 6.4.1
图 6.4.2

也就是说,经过这两步操作,使得 原来根节点的右孩子的左孩子 D 节点成为了新的根节点

代码实现:

//RL型,先右旋转,再左旋转
//返回:新父节点
Tree RL_rotate(Tree node){
    LL_rotate(node->rchild);
    return RR_rotate(node);
}

补充

上述四种插入方式的代码实现的辅助代码如下:

//更新当前深度
void update_depth(Tree node){
    if (node==NULL){
        return;
    }else{
        int depth_Lchild=get_balance(node->lchild); //左孩子深度
        int depth_Rchild=get_balance(node->rchild); //右孩子深度
        node->depth=max(depth_Lchild,depth_Rchild)+1;
    }
} //获取当前结点的深度
int get_balance(Tree node){
    if (node==NULL){
         return 0;
    }
    return node->depth;
} //返回当前平衡因子
int is_balance(Tree node){
    if (node==NULL){
         return 0;
    }else{
         return get_balance(node->lchild)-get_balance(node->rchild); 
    }
}

6.5 小总结

  1. 在所有的不平衡情况中,都是按照先 寻找最小不平衡树,然后 寻找所属的不平衡类别,再 根据 4 种类别进行固定化程序的操作
  2. LL , LR ,RR ,RL其实已经为我们提供了最后哪个结点作为新的根指明了方向。如 LR 型最后的根结点为原来的根的左孩子的右孩子,RL 型最后的根结点为原来的根的右孩子的左孩子。只要记住这四种情况,可以很快地推导出所有的情况。
  3. 维护平衡二叉树,最麻烦的地方在于平衡因子的维护。建议读者们根据小吴提供的图片和动图,自己动手画一遍,这样可以更加感性的理解操作。

7. AVL树的四种删除节点方式

AVL 树和二叉查找树的删除操作情况一致,都分为四种情况:

(1)删除叶子节点
(2)删除的节点只有左子树
(3)删除的节点只有右子树
(4)删除的节点既有左子树又有右子树

只不过 AVL 树在删除节点后需要重新检查平衡性并修正,同时,删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,而删除操作需要修正栈中的所有非平衡节点。

删除操作的大致步骤如下:

  • 以前三种情况为基础尝试删除节点,并将访问节点入栈。
  • 如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。
  • 如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。
  • 再依次检查栈顶节点的平衡状态和修正直到栈空。

对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了。

总结

AVL 的旋转问题看似复杂,但实际上如果你亲自用笔纸操作一下还是很好理解的。

你可以前往我的个人博客进行阅读,获取更良好的阅读体验。 http://www.cxyxiaowu.com/posts/65aa991e/

什么是平衡二叉树(AVL)的更多相关文章

  1. 数据结构与算法--从平衡二叉树(AVL)到红黑树

    数据结构与算法--从平衡二叉树(AVL)到红黑树 上节学习了二叉查找树.算法的性能取决于树的形状,而树的形状取决于插入键的顺序.在最好的情况下,n个结点的树是完全平衡的,如下图"最好情况&q ...

  2. 二叉查找树(BST)、平衡二叉树(AVL树)(只有插入说明)

    二叉查找树(BST).平衡二叉树(AVL树)(只有插入说明) 二叉查找树(BST) 特殊的二叉树,又称为排序二叉树.二叉搜索树.二叉排序树. 二叉查找树实际上是数据域有序的二叉树,即对树上的每个结点, ...

  3. 平衡二叉树AVL - 插入节点后旋转方法分析

    平衡二叉树 AVL( 发明者为Adel'son-Vel'skii 和 Landis)是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1. 首先我们知道,当插入一个节点,从此插入点到树根 ...

  4. 二叉查找树、平衡二叉树(AVL)、B+树、联合索引

    1. [定义] 二叉排序树(二拆查找树)中,左子树都比节点小,右子树都比节点大,递归定义. [性能] 二叉排序树的性能取决于二叉树的层数 最好的情况是 O(logn),存在于完全二叉排序树情况下,其访 ...

  5. Java 树结构实际应用 四(平衡二叉树/AVL树)

    平衡二叉树(AVL 树) 1 看一个案例(说明二叉排序树可能的问题) 给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.  左边 BST 存在的问题分析: ...

  6. 【数据结构】平衡二叉树—AVL树

    (百度百科)在计算机科学中,AVL树是最先发明的自平衡二叉查找树.在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树.查找.插入和删除在平均和最坏情况下都是O(log n).增 ...

  7. 平衡二叉树AVL删除

    平衡二叉树的插入过程:http://www.cnblogs.com/hujunzheng/p/4665451.html 对于二叉平衡树的删除采用的是二叉排序树删除的思路: 假设被删结点是*p,其双亲是 ...

  8. 平衡二叉树AVL插入

    平衡二叉树(Balancedbinary tree)是由阿德尔森-维尔斯和兰迪斯(Adelson-Velskiiand Landis)于1962年首先提出的,所以又称为AVL树. 定义:平衡二叉树或为 ...

  9. 数据结构快速回顾——平衡二叉树 AVL (转)

    平衡二叉树(Balanced Binary Tree)是二叉查找树的一个进化体,也是第一个引入平衡概念的二叉树.1962年,G.M. Adelson-Velsky 和 E.M. Landis发明了这棵 ...

  10. 平衡二叉树AVL

    1.定义 平衡二叉树(Balanced Binary Tree)是二叉查找树的一个改进,也是第一个引入平衡概念的二叉树.1962年,G.M. Adelson-Velsky 和 E.M. Landis发 ...

随机推荐

  1. 异步使用委托delegate --- BeginInvoke和EndInvoke方法

    当我们定义一个委托的时候,一般语言运行时会自动帮委托定义BeginInvoke 和 EndInvoke两个方法,这两个方法的作用是可以异步调用委托. 方法BeginInvoke有两个参数: Async ...

  2. [LeetCode] Keys and Rooms 钥匙与房间

    There are N rooms and you start in room 0.  Each room has a distinct number in 0, 1, 2, ..., N-1, an ...

  3. HTML5 部分新增语义化标签元素

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. mysql 删除指定字符

    mysql 删除指定字符 1.1 前言        实际需求中如果想删除指定的字符,一般需要使用到trim函数.trim函数默认删除字符的前后空格,如果想指定删除特定字符,则需要使用一下语句进行声明 ...

  5. [IOT] 自制蓝牙工牌办公室定位系统 (一)—— 阿里物联网平台概览及打通端到云(硬核·干货)

    目录:老少皆宜.超长干货文警告 1.快速入门创建产品 -- 小白,打包带走去吹牛 2.代码分析 -- 老炮,快速了解能用上 2.1 从start.sh分析开发环境如何自动构建 2.2 从sample. ...

  6. [BlueZ] 1、Download install and use the BlueZ and hcitool on PI 3B+

    星期日, 02. 九月 2018 11:58下午 - beautifulzzzz 1. Introduction Bluez is the default Bluetooth protocol sta ...

  7. 常见的java设计模式

    单例模式 简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例. getInstan ...

  8. [Swift]LeetCode729. 我的日程安排表 I | My Calendar I

    Implement a MyCalendar class to store your events. A new event can be added if adding the event will ...

  9. mysql+postgresql备份与恢复

    mysql备份一个库, mysqldump  -u用户名 -p密码 [选项] [数据库名] > /备份路径/备份文件名 mysqldump -uuser -p123123 auth > / ...

  10. Java高级特性-String、StringBuffer和StringBuilder

    Java高级特性-String.StringBuffer和StringBuilder String Java语言中的字符串值都属于String类,虽然有其他方法表示字符串(如字符串数组),但Java一 ...