转载请注明出处: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. 解释 Spring 框架中 bean 的生命周期?

    Spring 容器 从 XML 文件中读取 bean 的定义,并实例化 bean. Spring 根据 bean 的定义填充所有的属性. 如果 bean 实现了 BeanNameAware 接口,Sp ...

  2. 学习tomcat(三)

    一.tomcat安装 1.部署java环境 # yum install java-1.8.0 # java -version 2.部署tomcat # mkdir /data/soft -p # cd ...

  3. C++“拷贝构造函数”和“等号重载”有什么区别?

    CTypeA(const CTypeB& b)CTypeA& operator=(const CTypeB& b)一直没弄懂这两个有什么区别.只知道,重载了=号,下面复制的时候 ...

  4. 7_线性控制器设计(Linear Controller Design)

    开环系统中 状态方程,其中A的特征值将决定这个系统的表现(稳定性或者收敛速度:特征值小于0时系统稳定) 如果开环系统特征值大于0时(即系统不稳定时): 可以引入输入量U时(U是关于状态变量X的函数), ...

  5. css预编译--sass进阶篇

    基础篇中主要介绍了一些sass的基本特性,进阶篇中,主要是写一些我们常用的sass控制命令,函数和规则. 控制命令 可能看过基础篇的朋友会发现在有些代码中出现@if @else @each等,熟悉JS ...

  6. String能变化吗?和StringBuffer的区别是什么

    [新手可忽略不影响继续学习]看 过上面例子的童鞋一定会觉得很奇怪,s = s + s1.charAt(i); 马克-to-win, s不是老在变化吗?其实s = "";时,虚拟机会 ...

  7. 小程序滚动事件之头部渐隐渐现demo

    效果图: ==>  代码: //test1.wxml <view class='header' style="opacity:{{opacityStyle}}" hid ...

  8. Python入门-面向对象-装饰器

    1.变量作用域 全局变量和局部变量 #变量是有作用域的,分为全局变量和局部变量 num = 100 #这是全局变量 def change(): """ 查看变量作用域 & ...

  9. python---复杂度、斐波那切数列、汉诺塔

    时间复杂度 ​ 用来估计算法运行时间的一个式子. ​ 一般来说, 时间复杂度高的算法比复杂度低的算法慢. 常见的时间复杂度: ​ O(1) < O(logn) < O(n) < O( ...

  10. Spring Boot-@PropertySource注解

    @PropertySource:加载自己手动编写的资源文件 有关@ConfigurationProperties注解的作用请访问Spring Boot-@Value获取值和@Configuration ...