AVL树
1. AVL树的概念
下面我们通过探究AVL树的特点来了解它。
2. AVL树的特点
我们将AVL树的名字拆开,很容易就知道它到底是一棵什么样的树:自平衡的、二叉树、搜索树,所以它的特点有:
- 搜索树:任意一棵子树,它的左子树的所有节点都比根节点小,且右子树的所有节点都比根节点大;在搜索树中,我们有K型搜索树和K/V型搜索树,AVL树同样有这两种。
- 自平衡的:任意一棵子树,左子树和右子树的高度差不超过1,一旦超过1,就会自行进行树旋转操作,使得这棵AVL树的左右子树高度差不超过1。
- 二叉树:AVL树的数据结构中有三个指针,分别指向左孩子、右孩子、父节点;此外,部分AVL树的实现中,存在一个整形变量平衡因子,用以辅助实现自平衡功能(关于平衡因子,后文会进行解释)。
- 时间复杂度:查找、遍历、插入、删除都为O(log2N)。
在AVL树众多特点中,我们最最需要关注的是:AVL树是如何实现自平衡的?下面,我们就来揭开AVL树自平衡的神秘面纱。
3. AVL树的自平衡
AVL树如何实现自平衡呢?
实现自平衡,涉及到两个概念,一个是平衡因子,另一个是旋转,平衡因子用来记录AVL树是否平衡,旋转则是让不平衡的AVL树变得平衡。
3.1 平衡因子
-
平衡因子(
b
a
l
a
n
c
e
f
a
c
t
o
r
,简称
b
f
)
=
右子树的高度
−
左子树的高度
平衡因子(balance factor,简称bf)= 右子树的高度 - 左子树的高度
平衡因子(balancefactor,简称bf)=右子树的高度−左子树的高度
当然,反过来也是可以的,甚至不使用平衡因子也是可以的,我们为了理解方便,以下所有例子都是平衡因子=右子树的高度-左子树的高度。
-
对于一棵AVL树,任何一个节点的平衡因子的可能取值为
{-2,-1,0,1,2}
。 -
当插入一个新的节点,它的祖先节点的平衡因子可能会受到影响,因此我们需要一路向上更新祖先节点的平衡因子。
-
以下是插入了一个新的节点后,更新平衡因子的思路:
1)如果当前节点是它的父节点parent的左孩子,说明新节点插入到了parent的左子树,左子树的高度加了一,父节点的平衡因子减一;
2)如果当前节点是它的父节点parent的右孩子,说明新节点插入到了parent的右子树,右子树的高度加了一,父节点的平衡因子加一;
3)若当前 b f = 0 bf = 0 bf=0,表示左右子树一样高,处于平衡状态,说明再往上的祖先节点们都没有受到影响,更新结束。(说明:因为AVL树每次只能插入一个节点,若更新完平衡因子后,节点X的bf变成了0,说明节点X原来的bf不是-1就是1,也就是说原本节点X的左右子树高度相差1,在插入新节点后bf变成了0,也就是给左右子树中矮的那棵子树增高了1层,就使得节点X的左右子树一样高了。对于X节点和X的祖先来说,插入了一个新节点,并没有让自己左右子树的高度差变得更大,所以也就没有必要再往上更新平衡因子了。)
4)若当前 b f = ± 1 bf = \pm1 bf=±1,表示左右子树的高度差为1(虽然不是绝对的平衡,但是满足AVL树的要求),不必进行旋转操作,需要继续向上更新平衡因子(把当前节点cur的parent作为当前节点,parent的parent作为新的parent);
5)更新完平衡因子后,若当前 b f = ± 2 bf = \pm 2 bf=±2,表示左右子树的高度差为2,此时处于不平衡状态,需要进行旋转操作,使其平衡,旋转后更新结束。
6)更新到根节点时,更新结束。 -
值得注意的是,曾经我把平衡因子的计算方法记错了,以为 当前的 b f = 右孩子的 b f − 左孩子的 b f 当前的bf=右孩子的bf-左孩子的bf 当前的bf=右孩子的bf−左孩子的bf,导致代码出现严重的问题。其实真正的计算方法是:
平衡因子 = 右子树的高度 − 左子树的高度 平衡因子 = 右子树的高度 - 左子树的高度 平衡因子=右子树的高度−左子树的高度
3.2 旋转
当一棵AVL树的某个节点的平衡因子变成了2或-2,就需要进行旋转操作,将左右子树的高度重新变回{-1, 0, 1}之间。
旋转分为两种,一种是单旋转,另一种是双旋转。
下面,我们以插入操作来讲解旋转!
3.2.1. 单旋转(Single Rotation)
单旋又分为左单旋和右单旋,它们分别适用于不同的场景:
1)左单旋
右边“重”一点,需要把整个图像向左旋转。
核心步骤:
parent->right = curleft;
cur->left = parent;
下面我们针对h取不同的值图解分析:
-
h = 0
h=0时,有且仅有1种场景!注意:这里新节点只能插入到cur的右边!如果插入到cur的左边,那么就需要右左双旋了!
-
h = 1
新节点有2种插入位置,因此h=1时,有2种可能场景!注意:这里新节点可以插入到“70”的左或右,新节点“80”节点与“70”可以看做一个整体,新节点在哪个位置不影响左单旋!
-
h = 2
a子树有3种,b子树有3种,c子树有1种;新节点插入的位置有4种,因此h=2时,共有36种可能场景!为什么a子树和b子树可以是x、y、z当中任意一种,但是c子树只可能是z呢?
原因:a子树和b子树是什么类型,对于parent而言没有任何影响,不会让parent的平衡因子变得更大,因此都可以;
但是c子树不同,对于左单旋而言,新节点必然插入到c子树当中(为什么说必然,如果插入到a子树,AVL树依旧平衡,用不着旋转,如果插入到b子树,就是双旋了,我们后面再讲),因此:
1)假设c子树是x型的
新节点有三种插入的位置,左边两个位置插入后会诱发双旋(双旋请看后面),右边一个位置插入后AVL树依旧平衡。
2)假设c子树是y型的
新节点也有三种插入的位置,新节点插入在左边后AVL树依旧平衡。
插入在右边两个位置时,c子树本身就不平衡了,
当插入在最右的位置,c子树内部进行左单旋,使AVL树平衡,这属于"h=0",而不属于"h=2";
当插入在右起第二个位置,c子树内部进行右左双旋,AVL树平衡,这不属于左单旋。
3)只有c子树是z型的,parent节点的平衡因子才会变成2,才会需要进行左单旋。
- h取更大的值
会有越来越多可能出现的场景。
所以需要用到左单旋的场景有无数种!
2)右单旋
同样地,右单旋与左单旋一样,仅仅是位置变化罢了。
左边“重”一点,需要把整个图像向右旋转。
核心步骤:
parent->left = curright;
cur->right = parent;
右单旋这里就不再根据不同的h值画图分析了,它与左单旋大同小异。
h=0时仅仅有1种场景;
h=1时有2种场景;
h=2时有36种场景;
h越来越大,可能的场景越来越多…
总体来说右单旋也有无数种可能场景!
3.2.2 双旋转(Double Rotation)
所谓双旋,其实就是旋转两次,一次左单旋,一次右单旋。
双旋转也分为两种:右左双旋和左右双旋。
1)右左双旋
先右单旋,后左单旋。
所有符合下图所示的AVL树结构,都会导致右左双旋!
新节点既可能插入到b子树中,也可能插入到c子树中,这两种情况都会导致右左单旋!
-
h = 0
h=0时,有且仅有这一种场景! -
h = 1
新节点有两个可能插入的位置,因此h=1引发右左双旋有2种可能场景! -
h = 2
a子树和d子树各有3种,b子树和c子树可能分别有2种,因此符合h=2的右左双旋共有36种可能场景!
-
h为更大的值,会有更多可能引发右左双旋的场景!
因此,双旋与单旋一样,都有无数种可能的场景!
2)左右双旋
先进行左单旋,再进行右单旋。
所有符合下图所示结构的AVL树,都会引发左右双旋!
和右左双旋一样,b子树和c子树都有可能插入新节点!
当h=0时,有且仅有1种场景;
当h=1时,有2种可能场景;
当h=2时,有36种可能场景;
当h更大,有更多种可能的场景!
因此,左右双旋也有无数种可能场景!
AVL树的删除操作,比插入操作更加复杂,它不仅仅要满足搜索树的条件(这本身就够复杂了),还要满足“平衡”,删除导致的平衡因子的判断更为复杂!能力有限,本文不对删除操作进行讲解。
4. AVL树源码(插入操作)
下面附上AVL树插入操作的源代码: