转载请注明出处:https://www.cnblogs.com/morningli/p/16033733.html

AVL树是带有平衡条件的二叉查找树,其每个节点的左子树和右子树的高度最多相差1。为了保持AVL树始终平衡,每次插入和删除都需要进行额外的平衡操作。

上面两个二叉搜索树,A是AVL树,而B不是。

为什么需要平衡二叉树?

二叉搜索树一定程度上可以提高搜索效率,但是因为二叉树没有对树的形状进行限制,很容易就退化成了一个链表,搜索效率降低为 O(n)。

这里说明会导致二叉搜索树退化的两种原因:

1、插入的数据是有序地,比如先后插入1,2,3,4,5,会产生下面的二叉搜索树:

2、因为二叉搜索树的删除操作总是用右子树的节点替换被删除的节点,所以在不断的插入删除后,左子树会比右子树更深。现在已经证明,如果交替插入和删除O(N2)次,那么树的期望深度将是O(√N)。

为了避免二叉查找树搜索效率的恶化,我们需要对二叉树的深度进行限制,避免过深的二叉搜索树。

一种解决办法是要有一个称为平衡(balance)的附加结构条件:任何节点的深度均不可以过深。AVL树就是这样的加了平衡条件的最古老的平衡查找树。

另一种办法是不对树的深度做限制,但是每次操作都对树做一些调整,使后面的操作效率更高,保证连续M次操作在最坏的情况下花费O(MlogN)的时间。这种数据结构叫伸展树(splay tree)。这种树我们平时较少遇到,可以有兴趣再去研究。

时间复杂度

AVL 树的只读操作涉及执行与在不平衡二叉搜索树上执行的操作相同的操作,但修改必须观察和恢复子树的高度平衡。

平衡系数(balance factor)

在二叉树中,节点 X 的平衡因子定义为它的两个子树的高度差:

如果不变量

对于每个节点成立,二叉树被定义为AVL 树。BF(X) < 0 的节点被称为`left-heavy`,BF(X) > 0 称为 `right-heavy`,BF(X) = 0 简单称为 `balanced`。

最小失衡子树

每次插入新节点后,只有那些从插入点到根节点的路径上的节点的平衡有可能被改变,因为只有这些节点的子树可能发生变化。在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。一棵失衡的树,是有可能有多棵子树同时失衡的。可以证明,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。

添加元素

我们把必须重新平衡的节点叫做 α。由于任意节点最多有两个儿子,因此出现高度不平衡就需要 | BF(α) | = 2。容易看出,这种不平衡可能出现在下面的4种情况中:

1. 对 α 的左儿子的左子树进行一次插入

2. 对 α 的左儿子的右子树进行一次插入

3. 对 α 的右儿子的左子树进行一次插入

4. 对 α 的右儿子的右子树进行一次插入

情形1和4是关于 α 点的镜像对称,情形2和3也是关于 α 点的镜像对称。理论上只有两种基本情况。

第一种是发生在“外边”的情况(即左左和右右的情况),这种情况只需要做一次单旋转(single rotation)可以完成调整。第二种情况是发生在“内部”的情况(即左右和右左的情况),这种情况通过稍微复杂的双旋转(double rotation)来处理。

动画显示了将几个元素插入到 AVL 树中。它包括左,右,左右和右左旋转。

单旋转

上图显示单旋转如何调整情形1。左边是旋转前,右边是旋转后。节点k2不满足AVL平衡性质,因为他的左子树比右子树深2层,RF(k2) = -2 。该图描述的只是情形1的一种可能的情况,在插入之前k2满足AVL性质,但是在插入之后这种性质被破坏了。子树X已经长出一层,这使得他比子树Z深出2层。下面分析Y可能所处的层数:

  1. Y不可能与新X在同一水平上,因为这样k2在插入之前已经失去平衡了。
  2. Y也不可能与Z在同一层,因为那样k1就会是在通向根的路径上破坏AVL平衡条件的第一个节点。

为了使树恢复平衡,我们需要把X上移一层,并把Z下移一层。此时二叉树已经不符合AVL树的要求,我们需要重新安排节点形成一棵等价的树,如图右边的树所示。

如图,把k2左二子k1提升为新的根,这样左子树高度会减去1。二叉树的属性告诉我们k2>k1,所以在新树中k2应该是k1的右儿子,子树Y包含了大于k1小于k2的节点,把他放到k2的左儿子的位置上就可以满足二叉查找树的属性。通过这样的调整,新树称为了一棵等效的新的AVL树。因为X向上移动了一层,Y保持在原来的高度上,Z下移了一层。k2和k1不仅满足AVL树的要求,而且他们的子树恰好处在同一高度上。不仅如此,整棵树的新高度恰恰与插入前原树的高度相同,而插入操作却使得子树X升高了。因此,通向根节点的路径的高度不需要进一步的修正,因而也不需要进一步的旋转。

情形4代表的是对称的情形。情形1的调整是从左往右旋转,称为右旋,情形4需要反过来,称为左旋。

双旋转

上面描述的单旋转对于情形2和情形3无效,需要使用双旋转来解决。

上图表示了对于情形2进行单旋转的情形。单旋转只会让子树Y保持高度不变,不会减少Y的高度。图中Y已经有一个数据插入其中,可以保证子树Y非空,因此可以假设子树Y有一个根和两棵子树,如下图所示。

可以把整棵树看成是由三个节点连接的4棵子树。如图所示,恰好树B或者树C中有一棵比D深两层(除非他们都是空的),但是我们不能肯定是哪一棵。事实上这并不要紧。图里B和C都画得比D低了2层。为了重新平衡,我们看到不能再让k3做根了,而上面已经说明了在k1和k3之间的旋转解决不了问题,唯一的选择是把k2作为新的根。这迫使k1成为k2的左儿子,k3成为k2的右儿子,从而决定4棵树的位置,如上图3所示,最后得到的树满足AVL的性质,与单旋转类似,我们也把树的高度恢复到插入以前的水平,这就保证了所有重新平衡和高度更新是完善的。情形3也可以通过双旋转进行处理,方向跟情形2相反。

删除元素

AVL 树和二叉查找树的删除操作基本相同,只是在二叉查找树的删除逻辑后后需要重新检查平衡性并修正。删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对路径上最深的一个非平衡节点进行修正,而删除操作需要修正路径上所有非平衡节点。

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

引用:

https://zhuanlan.zhihu.com/p/56066942

《数据结构与算法分析——C++语言描述(第四版)》

https://en.wikipedia.org/wiki/AVL_tree

5分钟了解二叉树之AVL树的更多相关文章

  1. 二叉树与AVL树

    二叉树 什么是二叉树? 父节点至多只有两个子树的树形结构成为二叉树.如下图所示,图1不是二叉树,图2是一棵二叉树. 图1 普通的树                                    ...

  2. 二叉树,AVL树和红黑树

    为了接下来能更好的学习TreeMap和TreeSet,讲解一下二叉树,AVL树和红黑树. 1. 二叉查找树 2. AVL树 2.1. 树旋转 2.1.1. 左旋和右旋 2.1.2. 左左,右右,左右, ...

  3. 二叉树-二叉查找树-AVL树-遍历

    一.二叉树 定义:每个节点都不能有多于两个的儿子的树. 二叉树节点声明: struct treeNode { elementType element; treeNode * left; treeNod ...

  4. python常用算法(5)——树,二叉树与AVL树

    1,树 树是一种非常重要的非线性数据结构,直观的看,它是数据元素(在树中称为节点)按分支关系组织起来的结构,很像自然界中树那样.树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树形 ...

  5. 二叉树之AVL树的平衡实现(递归与非递归)

    这篇文章用来复习AVL的平衡操作,分别会介绍其旋转操作的递归与非递归实现,但是最终带有插入示例的版本会以递归呈现. 下面这张图绘制了需要旋转操作的8种情况.(我要给做这张图的兄弟一个赞)后面会给出这八 ...

  6. 二叉树之AVL树

    高度为 h 的 AVL 树,节点数 N 最多2^h − 1: 最少N(h)=N(h− 1) +N(h− 2) + 1. 最少节点数n 如以斐波那契数列可以用数学归纳法证明: 即: N(0) = 0 ( ...

  7. 04-树4. Root of AVL Tree-平衡查找树AVL树的实现

    对于一棵普通的二叉查找树而言,在进行多次的插入或删除后,容易让树失去平衡,导致树的深度不是O(logN),而接近O(N),这样将大大减少对树的查找效率.一种解决办法就是要有一个称为平衡的附加的结构条件 ...

  8. AVL树和伸展树 -数据结构(C语言实现)

    读数据结构与算法分析 AVL树 带有平衡条件的二叉树,通常要求每颗树的左右子树深度差<=1 可以将破坏平衡的插入操作分为四种,最后通过旋转恢复平衡 破坏平衡的插入方式 描述 恢复平衡旋转方式 L ...

  9. 【数据结构】什么是AVL树

    目录 什么是AVL树 1. 什么是AVL树 2. 节点的实现 3. AVL树的调整 3.1 LL旋转 3.2 RR旋转 3.3 RL旋转 3.4 LR旋转 什么是AVL树 二叉查找树的一个局限性就是有 ...

随机推荐

  1. 4.RDD操作

    目录 一. RDD创建 从本地文件系统中加载数据创建RDD 从HDFS加载数据创建RDD 通过并行集合(列表)创建RDD 二. RDD操作 转换操作 filter(func) map(func) fl ...

  2. 手把手教你从零写一个简单的 VUE--模板篇

    教程目录1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 Hello,我又回来了,上一次的文章教会了大家如何书写一个简单 VUE,里面实现了VUE 的数据驱动视图 ...

  3. PAT 1048数字加密

    本题要求实现一种数字加密方法.首先固定一个加密用正整数 A,对任一正整数 B,将其每 1 位数字与 A 的对应位置上的数字进行以下运算:对奇数位,对应位的数字相加后对 13 取余--这里用 J 代表 ...

  4. Java中jdk安装与环境变量配置

    Java中jdk安装与环境变量配置 提示:下面是jdk1.7和jdk1.8的百度网盘链接 链接:https://pan.baidu.com/s/1SuHf4KlwpiG1zrf1LLAERQ 提取码: ...

  5. 第一阶段:Java基础之异常和处理

    文章目录 Java中异常处理机制的简单和应用 一.异常的体系结构&分类 二.问题扩展 三.应用场景 Java中异常处理机制的简单和应用 异常也是一种对象,Java中有很多异常类,并且定义了基类 ...

  6. Hyperledger Fabric的test-network启动过程Bash源码详解

    前言 在基于Debian搭建Hyperledger Fabric 2.4开发环境及运行简单案例中,我们已经完成了Fabric 2.4的环境搭建及fabric-samples/test-network官 ...

  7. MySQL创建高性能索引

    参考<高性能MySQL>第3版 1 索引基础 1.1 索引作用 在MySQL中,查找数据时先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行,假如要运行下面查询语句: 如果在u ...

  8. node.js - http、模块化、npm

    今天是node学习的第二天,其实越往后面学越感觉有点熟悉的味道了,光针对于node来说哈,为什么呢,因为我之前学过一点云计算的东西,当时感觉没什么用搞了下服务器客户端这些,没想到这里还能用一用,至少看 ...

  9. switch语法

    1. js 代码 // 1. switch 语句也是多分支语句 也可以实现多选1 // 2. 语法结构 switch 转换.开关 case 小例子或者选项的意思 // switch (表达式) { / ...

  10. Go 语言 结构体和方法

    @ 目录 1. 结构体别名定义 2. 工厂模式 3. Tag 原信息 4. 匿名字段 5. 方法 1. 结构体别名定义 变量别名定义 package main import "fmt&quo ...