2

5分钟了解二叉树之AVL树 - morningli

 1 year ago
source link: https://www.cnblogs.com/morningli/p/16033733.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

2748416-20220318101248186-1046599741.png

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

为什么需要平衡二叉树?

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

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

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

2748416-20220318101821782-108696571.png

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

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

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

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

时间复杂度

2748416-20220319094620853-931374303.png

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

平衡系数(balance factor)

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

2748416-20220319095648179-65594964.png

如果不变量

2748416-20220319095742688-1985804405.png

对于每个节点成立,二叉树被定义为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)来处理。

2748416-20220319095205862-809985100.gif

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

2748416-20220319135610743-923140685.png

上图显示单旋转如何调整情形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树的要求,我们需要重新安排节点形成一棵等价的树,如图右边的树所示。

2748416-20220319171303076-512501453.png

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

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

2748416-20220319172846810-1722989919.png

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

2748416-20220319173451407-582360701.png

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

 

2748416-20220319181818042-1576839397.png

 可以把整棵树看成是由三个节点连接的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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK