目录
一、AVL树的定义
能够在key插入时一直保持平衡的二叉查找树: AVL树
(AVL是发明者的名字缩写)
- 利用AVL树实现ADT Map, 基本上与BST的实现相同,不同之处仅在于二叉树的生成与维护过程
- AVL树的实现中, 需要对每个节点跟踪“平衡因子balance factor”参数
- 平衡因子是根据节点的左右子树的高度来定义的, 确切地说, 是左右子树高度差:
– 平衡因子 > 0,称为“左重left-heavy”,
– 平衡因子 < 0,称为“右重right-heavy”,
– 平衡因子 = 0,称作平衡。
如果一个二叉查找树中每个节点的平衡因子都在-1, 0, 1之间, 则把这个二叉搜索
树称为平衡树
二、AVL树的性能O(log n)
我们来分析AVL树最差情形下的性能:即平衡因子为1或者-1
下图列出平衡因子为1的“左重”AVL树,树的高度从1开始,来看看问题规模(总节点数N)和比对次数(树的高度h)之间的关系如何?
观察上图h=1~4时, 总节点数N的变化:
h= 1, N= 1
h= 2, N= 2= 1+ 1
h= 3, N= 4= 1+ 1+ 2
h= 4, N= 7= 1+ 2+ 4
规律:
N
h
=
1
+
N
h
−
1
+
N
h
−
2
N_h=1+N_{h-1}+N_{h-2}
Nh=1+Nh−1+Nh−2 接近斐波那契数列
1. 定义斐波那契数列
F
i
F_i
Fi
F
0
=
0
F
1
=
1
F
i
=
F
i
−
1
+
F
i
−
2
F_0=0\\F_1 = 1\\F_i=F_{i-1}+F_{i-2}
F0=0F1=1Fi=Fi−1+Fi−2
由于
N
h
=
1
+
N
h
−
1
+
N
h
−
2
N_h=1+N_{h-1}+N_{h-2}
Nh=1+Nh−1+Nh−2 可得
N
h
=
F
h
+
2
−
1
,
h
≥
1
N_h=F_{h+2}-1,h\ge1
Nh=Fh+2−1,h≥1
2. 斐波那契数列的性质:
F
i
/
F
i
−
1
F_i/F_{i-1}
Fi/Fi−1趋向于黄金分割
Φ
\Phi
Φ
Φ
=
1
+
5
2
\Phi =\frac{1+\sqrt{5}}{2}
Φ=21+5
可以得到
F
i
F_i
Fi的通式
F
i
=
Φ
i
5
{{F}_{i}}=\frac{{{\Phi }^{i}}}{\sqrt{5}}
Fi=5Φi
3.
最多搜索次数h和规模N的关系, 可以说AVL树的搜索时间复杂度为 O ( l o g n ) O(log n) O(logn)
三、AVL树的Python实现
既然AVL平衡树确实能够改进BST树的性能, 避免退化情形,我们来看看向AVL树插入一个新key, 如
何才能保持AVL树的平衡性质。
- 首先, 作为BST, 新key必定以叶节点形式插入到AVL树中
- 叶节点的平衡因子是0, 其本身无需重新平衡,但会影响其父节点的平衡因子
- 这种影响可能随着其父节点到根节点的路径一直传递上去, 直到
传递到根节点为止;某个父节点平衡因子被调整到0,不再影响上层节点的平衡因子为止
3.1 put方法
相比二叉查找树,只需重新定义_put方法
以刚刚插入的新节点作为基础,去调整它的平衡因子
def _put(self, key, val, currentNode):
# 如果key比currentNode小,那么_put到左子树
if key < currentNode.key:
# 但如果没有左子树,那么key就成为左子节点
if currentNode.left:
self._put(key, val, currentNode.left) # 递归查找空左子树
else:
currentNode.left = TreeNode(key, val, parent=currentNode)
self.updataBalance(currentNode.left) #调整因子
# 如果key比currentNode大,那么_put到右子树
else:
# 但如果没有右子树,那么key就成为右子节点
if currentNode.right:
self._put(key, val, currentNode.right) # 递归右子树
else:
currentNode.right = TreeNode(key, val, parent=currentNode)
self.updataBalance(currentNode.right) #调整因子
3.2 UpdateBalance方法
def UpdateBalance(self,node):
if node.balanceFactor > 1 or node.balanceFactor < -1:
self.rebalance(node)
return
if node.parent:
if node.isLeftChild():
node.parent.balancefactor += 1
elif node.isRightChild():
node.parent.balancefactor -= 1
if node.parent.balancefactor != 0:
self.updataBalance(node.parent)
3.3 rebalance重新平衡
主要手段 :将不平衡的子树进行旋转
视“左重”或者“右重”进行不同方向的旋转,同时更新相关父节点引用,更新旋转后被影响节点的平衡因子
-
如图, 是一个“右重”子树A的左旋转(并保持BST性质)
- 将右子节点B提升为子树的根,将旧根节点A作为新根节点B的左子节点;
- 如果新根节点B原来有左子节点,则将此节点设置为A的右子节点(A的右子节点一定有空)
-
更复杂一些的情况:如图的“左重”子树右旋转
- 旋转后,新根节点将旧根节点作为右子节点,但是新根节点原来已有右子节点,需要将原有的右子节点重新定位!
- 原有的右子节点D改到旧根节点E的左子节点同样, E的左子节点在旋转后一定有空
3.4 rotateLeft 左旋
def rotateLeft(self,rotRoot):
newRoot = rotRoot.right #新根节点是旧根节点的右子节点
#把旧根节点的右子节点指向新根节点的左子节点
rotRoot.right = newRoot.left
if newRoot.left: #新根节点有左节点
newRoot.left.parent = rotRoot#将新根的左节点指向旧根结点
newRoot.parent = rotRoot.parent
#如果旧根节点是树的根,那么应该确定新的树根
if rotRoot.isRoot():
self.root = newRoot
else:
if rotRoot.isLeftChild(): #若旧根节点是左子节点
rotRoot.parent.left = newRoot #旧根节点的左子节点指向新根节点
else:
rotRoot.parent.right = newRoot
newRoot.left = rotRoot
rotRoot.parent = newRoot
#仅有两个结点需要调整因子
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)
3.5 如何调整平衡因子
3.6 AVL树的实现: 更复杂的情形
下图的“右重”子树, 单纯的左旋转无法实现平衡
所以, 在左旋转之前检查右子节点的因子
- 如果右子节点“左重”的话,先对它进行右旋转再实施原来的左旋转
同样, 在右旋转之前检查左子节点的因子
- 如果左子节点“右重”的话,先对它进行左旋转再实施原来的右旋转
3.6 rebalance
def rebalance(self,node):
if node.balanceFactor < 0: #右重需要左旋
if node.right.balanceFactor > 0:
# 右子节点左重先右旋
self.rotateRight(node.right)
self.rotateLeft(node)
else: self.rotateLeft(node)
elif node.balanceFactor > 0: #左重需要右旋
if node.left.balanceFactor < 0:
#左子节点右重先左旋
self.rotateLeft(node.left)
self.rotateRight(node)
else:
self.rotateRight(node)
经过复杂的put方法, AVL树始终维持平衡, get方法也始终保持O(log n)高性能
- 需要插入的新节点是叶节点,更新其所有父节点和祖先节点的代价最多为O(log n)
- 如果插入的新节点引发了不平衡,重新平衡最多需要2次旋转,但旋转的代价与问题规模无关,是常数O(1)
- 所以整个put方法的时间复杂度还是O(log n)