本文为学习笔记,感兴趣的读者可在MOOC中搜索《数据结构与算法Python版》或阅读《数据结构(C语言版)》(严蔚敏)
目录链接:https://blog.csdn.net/floating_heart/article/details/123991211
4.3 AVL树
4.3.1 初识AVL树
根据4.2 BST的介绍(链接:)我们可知,BST的性能和二叉搜索树的深度紧密相关:当key值分布极端时,大量的数据集中在单一分支上,此时树的深度最大,性能最差;当key值分布均匀,数据均匀分布在不同分支上时,树的深度最小,性能最佳。
为了保证BST的搜索效率,让树深度降低,后续学者提出了“自平衡二叉树(Self-Balancing Binary Search Tree)”。故名思意,自平衡二叉树能够自己调节节点的分布使其保持“平衡”,进而维持 O ( l o g n ) O(logn) O(logn)的高效查询效率,常见的自平衡二叉树有AVL树和红黑树等,下面以AVL树为例,了解该结构自平衡的实现方法。
AVL树是最先发明的自平衡查找树,在AVL树中任何节点的两个子树的高度最大差别为1,所以也被称为高度平衡树。它具有以下特点:
- 本身是一棵二叉搜索树;
- 带有平衡条件:每个节点的左右子树的高度差的绝对值(平衡因子)最多为1。
4.3.2 AVL树判断平衡的标准:平衡因子
在AVL树的实现中,需要对每个结点跟踪“平衡因子balance factor”参数。
平衡因子是根据结点的左右子树的高度来定义的,确切地说,是左右子树的高度差:
b
a
l
a
n
c
e
F
a
c
t
o
r
=
h
e
i
g
h
t
(
l
e
f
t
S
u
b
T
r
e
e
)
−
h
e
i
g
h
t
(
r
i
g
h
t
S
u
b
T
r
e
e
)
balanceFactor = height(leftSubTree)-height(rightSubTree)
balanceFactor=height(leftSubTree)−height(rightSubTree)
如果平衡因子大于0,称为“左重left-heavy”;小于0,称为“右重right-heavy”;等于0,则称作平衡。
如果一个二叉查找树中每个节点的平衡因子都在-1,0,1之间,则把这个二叉搜索树称为平衡树。
在平衡树的操作过程中(如增加、删除等操作),节点的平衡因子会发生变化,如下图所示:
这种影响可能会随着父节点到根节点的路径一直传递上去,直到:
- 传递到根节点为止;
- 或者某个父节点平衡因子被调整到0,不再影响上层节点的平衡因子为止(如下图所示)。
AVLTree的大部分功能和BST类似,我们修改BST来适应AVL的要求,BST的内容可见4.2章节。
首先,我们改造TreeNode类,加入balanceFactor属性作为平衡因子,新节点添加在末尾,所以平衡因子默认为0。
class TreeNode:
def __init__(self,key,val,left=None,right=None,parent=None) -> None:
self.key = key # 键值
self.payload = val # 数据项
self.leftChild = left # 左子节点
self.rightChild = right # 右子节点
self.parent = parent # 父节点
self.balanceFactor = 0 # 平衡因子
根据平衡因子影响的方式,我们在加入一个新节点之后,只需逐层向上传播影响即可。为此,我们适当改造_put()
函数(插入非根节点的新节点的函数),加入updateBalance()
方法从底到顶更新平衡因子。
# put中_put方法
def _put(self,key,val,currentNode):
if key < currentNode.key: # key比currentNode.key小,递归左子树
if currentNode.hasLeftChild():
self._put(key,val,currentNode.leftChild)
else:
currentNode.leftChild = TreeNode(key,val,parent=currentNode)
self.updateBalance(currentNode.leftChild) # 插入节点后,调整平衡因子
else: # key比currentNode.key大,递归右子树
if currentNode.hasRightChild():
self._put(key,val,currentNode.rightChild)
else:
currentNode.rightChild = TreeNode(key,val,parent=currentNode)
self.updateBalance(currentNode.rightChild) # 插入节点后,调整平衡因子
updateBalance()
方法更新平衡因子,在平衡因子超标(大于1或小于-1)时,调用rebalance()
方法调节平衡具体如下:
- 如果本节点平衡因子超标,调用
rebalance()
方法调节平衡,调节之后影响中止,调节的方法在下节介绍; - 本节点平衡因子没有超标,则将影响传递到parent节点:是左子树parent节点平衡因子+1,是右子树parent节点平衡因子-1,然后在parent节点平衡因子不为0的情况下向上递归
# 更新平衡因子
def updateBalance(self,node):
# 调节平衡
if node.balanceFactor > 1 or node.balanceFactor < -1:
self.rebalance(node)
return
# 影响向上传播
if node.parent != None:
if node.isLeftChild():
node.parent.balanceFactor += 1
elif node.isRightChild():
node.parent.balanceFactor -= 1
if node.parent.balanceFactor != 0:
self.updateBalance(node.parent)
4.3.3 AVL树调节平衡的方法:旋转
在updateBalance()
函数中对于平衡因子超标的节点调用了rebalance()
方法用于重新调节平衡并中断影响,中断影响的条件前文已述:该结点的平衡因子被调节为0。那么如何实现平衡调节就是这一部分的重点。
AVL树重新平衡的主要手段为:将不平衡的子树进行旋转rotation——视“左重”或者“右重”进行不同方向的旋转,同时更新相关父节点引用,更新旋转后被影响节点的平衡因子。
左旋:LL
对于“右重”的子树需要进行左旋,以下图子树A为例:
-
首先将子节点B提升为子树的根,将旧节点A作为新根节点B的左子节点;
-
如果新根节点原来有左子节点,则将此节点设置为A的右子节点(A的右子节点一定有空,且符合BST性质)。
-
各个节点的平衡因子随之变动。
右旋:RR
对于“左重”的子树需要进行右旋,以下图子树E为例:
- 首先将子节点C提升为子树的根,将旧根节点E作为新根节点C的右子节点;
- 新根节点C原有的右子节点D作为旧根节点E的左子节点;
- 各个节点平衡因子随之变动。
特殊情况:LR和RL旋转
存在一些特殊情况无法用单纯的“左旋”或“右旋”解决,如下图所示:
上图中的子树,左旋之后会变成“左重”,再右旋后又变回了“右重”。可以采取以下方案:
- 在左旋之前检查右子节点的平衡因子,如果右子节点“左重”的话,先对右子节点进行右旋,再实施原来的左旋(RL);
- 在右旋之前检查左子节点的平衡因子,如果左子节点“右重”的话,先对左子节点进行左旋,再实施原来的右旋(LR)。
如此操作,上面的例子就可以解决:
旋转操作对平衡因子的影响:
上述旋转操作中都提到了“各个节点平衡因子随之变动”,此处计算下旋转中平衡因子的变动方式以方便后续旋转操作的实现。
左旋和右旋是向对称的一组操作,平衡因子的变动相同。
- 左旋操作平衡因子变化的情况如下:
(符号说明:旧节点代表左图变换前的节点,新节点代表右图变换后的节点,如旧B代表作图根节点B的平衡因子;h代表子树高度,如hA代表子树A的高度。)
首先,子树A、C、E在变换过程中不涉及内部子节点的变化,hA、hC、hE不变,故主要为B和D节点平衡因子的变化。
左旋B的变化:新B-旧B
新
B
=
h
A
−
h
C
新B=hA-hC
新B=hA−hC
旧
B
=
h
A
−
旧
h
D
=
h
A
−
(
1
+
m
a
x
(
h
C
,
h
E
)
)
旧B = hA - 旧hD=hA-(1+max(hC,hE))
旧B=hA−旧hD=hA−(1+max(hC,hE))
新
B
−
旧
B
=
h
A
−
h
C
−
h
A
+
(
1
+
m
a
x
(
h
C
,
h
E
)
)
=
1
+
m
a
x
(
h
C
,
h
E
)
−
h
C
新B-旧B=hA-hC-hA+(1+max(hC,hE))=1+max(hC,hE)-hC
新B−旧B=hA−hC−hA+(1+max(hC,hE))=1+max(hC,hE)−hC
新
B
−
旧
B
=
1
+
m
a
x
(
h
C
,
h
E
)
−
h
C
=
1
+
m
a
x
(
h
C
−
h
C
,
h
E
−
h
C
)
=
1
+
m
a
x
(
0
,
−
旧
D
)
新B-旧B=1+max(hC,hE)-hC=1+max(hC-hC,hE-hC)=1+max(0,-旧D)
新B−旧B=1+max(hC,hE)−hC=1+max(hC−hC,hE−hC)=1+max(0,−旧D)
故: 新 B = 旧 B + 1 − m i n ( 0 , 旧 D ) 新B=旧B+1-min(0,旧D) 新B=旧B+1−min(0,旧D)
左旋D的变化:新D-旧D
新
D
=
新
h
B
−
h
E
=
(
1
+
m
a
x
(
h
A
,
h
C
)
)
−
h
E
新D=新hB-hE=(1+max(hA,hC))-hE
新D=新hB−hE=(1+max(hA,hC))−hE
旧
D
=
h
C
−
h
E
旧D=hC-hE
旧D=hC−hE
新
D
−
旧
D
=
(
1
+
m
a
x
(
h
A
,
h
C
)
)
−
h
E
−
(
h
C
−
h
E
)
=
1
+
m
a
x
(
h
A
,
h
C
)
−
h
C
新D-旧D=(1+max(hA,hC))-hE-(hC-hE)=1+max(hA,hC)-hC
新D−旧D=(1+max(hA,hC))−hE−(hC−hE)=1+max(hA,hC)−hC
新
D
−
旧
D
=
1
+
m
a
x
(
h
A
−
h
C
,
0
)
=
1
+
m
a
x
(
新
B
,
0
)
新D-旧D=1+max(hA-hC,0)=1+max(新B,0)
新D−旧D=1+max(hA−hC,0)=1+max(新B,0)
故: 新 D = 旧 D + 1 + m a x ( 新 B , 0 ) 新D=旧D+1+max(新B,0) 新D=旧D+1+max(新B,0)
- 右旋操作与之类似,以上图镜像对称的子树为例,直接给出结论:
右旋旧根节点变化:
新
B
=
旧
B
−
1
−
m
a
x
(
旧
D
,
0
)
新B=旧B-1-max(旧D,0)
新B=旧B−1−max(旧D,0)
右旋新根节点变化:
新
D
=
旧
D
−
1
+
m
i
n
(
新
B
,
0
)
新D=旧D-1+min(新B,0)
新D=旧D−1+min(新B,0)
根据以上分析,我们大概认识了旋转操作必要的组成部分,构建代码则变得比较容易。
下面,我们从rebalance()
开始,构建旋转操作的代码:
# rebalance:再平衡,判断旋转方式进行响应旋转操作
def rebalance(self,node):
if node.balanceFactor < 0: # 右重-左旋
if node.rightChild.balance.balanceFactor > 0: # 右子节点左重-RL旋转
self.rotateRight(node.rightChild)
self.rotateLeft(node)
else: # LL旋转
self.rotateLeft(node)
elif node.balanceFactor > 0: # 左重-右旋
if node.leftChild.balanceFactor < 0: # 左子节点右重-LR旋转
self.rotateLeft(node.leftChild)
self.rotateRight(node)
else: # RR旋转
self.rotateRight(node)
相应的左旋和右旋操作如下:
# 左旋操作
def rotateLeft(self,rotRoot):
# 提出新根节点
newRoot = rotRoot.rightChild
# 旋转操作:节点之间的连接的建立-newRoot和rotRoot之间的关系已知,可以从此处入手
# 旧根节点向下连接
rotRoot.rightChild = newRoot.leftChild
if newRoot.leftChild != None:
newRoot.leftChild.parent = rotRoot
# 新根节点向上连接
newRoot.parent = rotRoot.parent
if rotRoot.isRoot():
self.root = newRoot
else:
if rotRoot.isLeftChild():
rotRoot.parent.leftChild = newRoot
else:
rotRoot.parent.rightChild = newRoot
# 旧根节点向上连接
newRoot.leftChild = rotRoot
rotRoot.parent = newRoot
# 平衡因子调整
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor,0)
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor,0)
# 右旋操作
def rotateRight(self,rotRoot):
newRoot = rotRoot.leftChild
rotRoot.leftChild = newRoot.rightChild
if newRoot.rightChild != None:
newRoot.rightChild.parent = rotRoot
if rotRoot.isRoot():
self.root = newRoot
else:
if rotRoot.isLeftChild():
rotRoot.parent.leftChild = newRoot
else:
rotRoot.parent.rightChild = newRoot
newRoot.rightChild = rotRoot
rotRoot.parent = newRoot
rotRoot.balanceFactor = rotRoot.balanceFactor - 1 - max(newRoot.balanceFactor,0)
newRoot.balanceFactor = newRoot.balanceFactor - 1 + min(rotRoot.balanceFactor,0)
4.3.4 AVL树时间复杂度分析
put操作的性能:
AVL树的put操作分为三个部分:
- 向下遍历查找插入位置,这一操作和AVL树的高度有关,复杂度最多为 O ( l o g n ) O(log n) O(logn)
- 向上遍历调节平衡因子,这一操作同样和AVL树的高度有关,在最差的情况下,复杂度为 O ( l o g n ) O(logn) O(logn)
- 向上遍历的过程中,如果触发了旋转操作,重新平衡最多需要两次旋转,复杂度为 O ( 1 ) O(1) O(1)
所以,AVL树put操作的时间复杂度于BST相同,为 O ( l o g n ) O(logn) O(logn)
get操作的性能:
与BST相同,get操作的性能和树的高度有关,由于AVL树总能保持平衡,所以其复杂度最多为 O ( l o g n ) O(logn) O(logn)
为什么说AVL树的高度和数据规模之间为 l o g n logn logn倍的关系:
我们首先以最差的形式来考虑AVL树的高度,即数据规模(N)尽量小而树的高度(h)尽量高的情况,如下图所示:
h
=
1
,
N
=
1
h=1,N=1
h=1,N=1
h
=
2
,
N
=
2
=
1
+
1
h=2,N=2=1+1
h=2,N=2=1+1
h
=
3
,
N
=
4
=
1
+
1
+
2
h=3,N=4=1+1+2
h=3,N=4=1+1+2
h
=
4
,
N
=
7
=
1
+
2
+
4
h=4,N=7=1+2+4
h=4,N=7=1+2+4
陈斌老师讲解中,给出通式:
N
h
=
1
+
N
h
−
1
+
N
n
−
2
N_h=1+N_{h-1}+N_{n-2}
Nh=1+Nh−1+Nn−2 (汗!笔者没看出此通式,只能听老师的话)
此通式很接近斐波那契数列!(这点笔者看出来了!哈哈!)
将 h h h、 N h N_h Nh和斐波那契数列 F i F_i Fi,列出如下:
h / i h/i h/i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
N h N_h Nh | 0 | 1 | 2 | 4 | 7 | 12 | 20 | 33 | 54 |
F i F_i Fi | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |
可以明显看出 N h N_h Nh和 F i F_i Fi的关系如下:
F
0
=
0
,
F
1
=
1
N
h
=
1
+
N
h
−
1
+
N
h
−
2
F_0=0,F_1=1\quad N_h=1+N_{h-1}+N_{h-2}
F0=0,F1=1Nh=1+Nh−1+Nh−2
F
i
=
F
i
−
1
+
F
i
−
2
f
o
r
a
l
l
i
≥
2
N
h
=
F
h
+
2
−
1
F_i=F_{i-1}+F_{i-2}\, for\: all\:i≥2\quad N_h=F_{h+2}-1
Fi=Fi−1+Fi−2foralli≥2Nh=Fh+2−1
斐波那契数列中,
F
i
/
F
i
−
1
F_i/F_{i-1}
Fi/Fi−1趋近于黄金分割
ϕ
=
1
+
5
2
\phi=\frac{1+\sqrt5}{2}
ϕ=21+5
所以斐波那契数列的通式接近于
F
i
=
ϕ
i
/
5
F_i=\phi^i/\sqrt5
Fi=ϕi/5
于此,可以得到 N h N_h Nh的通式: N h = ϕ h + 2 5 − 1 N_h=\frac{\phi^{h+2}}{\sqrt5}-1 Nh=5ϕh+2−1
通式中存在
h
h
h和
N
h
N_h
Nh的关系,化简后得到:(得到的公式和陈斌老师的不同,不知道哪里出错了)
h
=
log
(
N
h
+
1
)
+
1
2
log
5
+
2
log
ϕ
log
ϕ
h=\frac{\log(N_h+1)+\frac{1}{2}\log5+2\log\phi}{\log\phi}
h=logϕlog(Nh+1)+21log5+2logϕ
所以,AVL树的高度和数据量之间为
log
n
\log n
logn倍的关系。