文章目录
一、平衡二叉查找树(AVL树)的定义
平衡二叉查找树(AVL树)是一种能够在key插入时一直保持平衡的二叉查找树。
AVL是发明者的名字缩写:G.M. Adelson-Velskii and E.M. Landis
利用AVL树实现ADT Map, 基本上与BST的实现相同,不同之处仅在于二叉树的生成与维护过程。
AVL树的实现中,需要对每个节点跟踪“平衡因子balance factor”参数。平衡因子是根据节点的左右子树的高度来定义的,确切地说,是左右子树的高度差:
-
平衡因子 > 0,称为“左重left-heavy”,即左子树高度更大;
-
平衡因子 < 0,称为“右重right-heavy”,即右子树高度更大;
-
平衡因子 = 0,称作平衡,即左、右子树高度一样。
比如,某个节点的平衡因子为2,则说明该节点:左子树高度-右子树高度=2。
如果一个二叉查找树中每个节点的平衡因子都在==-1,0,1之间,则把这个二叉查找树称为平衡树==。在平衡树操作过程中,有节点的平衡因子超出此范围,则需要重新进行平衡。
二、AVL树的性能
我们来分析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,观察这个通式,它很接近斐波那契数列!
-
定义斐波那契数列 F i F_i Fi,利用 F i F_i Fi重写 N h N_h Nh
F 0 = 0 F_0=0 F0=0
F 1 = 1 F_1=1 F1=1
F i = F i − 1 + F i − 2 F_i=F_{i-1}+F_{i-2} Fi=Fi−1+Fi−2,对于所有的 i ≥ 2 i\geq 2 i≥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\geq 1 Nh=Fh+2−1,h≥1
-
斐波那契数列的性质: F i / F i − 1 F_i/F_{i-1} Fi/Fi−1趋向于黄金分割 Φ Φ Φ
由 Φ = 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
-
将通式 F i F_i Fi带入 N h N_h Nh,得到 N h N_h Nh的通式
N h = Φ h + 2 5 − 1 N_h = \frac{\Phi^{h+2}}{\sqrt[]{5}}-1 Nh=5Φh+2−1
-
上述通式只有 N N N和 h h h了,我们解出 h h h
log N h + 1 = ( H + 2 ) log Φ − 1 2 log 5 \log N_h+1=(H+2)\log \Phi - \frac{1}{2}\log 5 logNh+1=(H+2)logΦ−21log5
h = log N h + 1 − 2 log Φ + 1 2 log 5 log Φ h=\frac{\log N_h +1-2\log \Phi + \frac{1}{2}\log 5}{\log \Phi} h=logΦlogNh+1−2logΦ+21log5
h = 1.44 log H h h=1.44\log H_h h=1.44logHh
最多搜索次数h和规模N的关系, 可以说AVL树最差情况下的搜索时间复杂度为 O ( log n ) O(\log n) O(logn)。
三、AVL树的python实现
既然AVL平衡树确实能够改进BST树的性能, 避免退化情形,我们来看看向AVL树插入一个新key, 如何才能保持AVL树的平衡性质。
-
首先,作为BST,新key必定以叶节点形式插入到AVL树中;
-
叶节点的平衡因子是0,其本身无需重新平衡,但会影响其父节点的平衡因子:
- 作为左子节点插入,则父节点平衡因子会增加1;
- 作为右子节点插入,则父节点平衡因子会减少1。
-
这种影响可能随着其父节点到根节点的路径一直传递上去,直到:
-
传递到根节点为止;
-
或者某个父节点平衡因子被调整到0,不再影响上层节点的平衡因子为止。(无论是从1还是-1,只要是调整到0,都不会改变子树高度)
-
1. put
方法
相比之前二叉查找树的put
方法,只需重新定义_put
方法即可,多了两步更新平衡因子的步骤:
def _put(self,key,val,currentNode):
# 如果key比当前节点小,那么放到到左子树
if key < currentNode.key:
# 如果有左子树,就递归进入左子树继续查找合适的空位置
if currentNode.hasLeftChild():
self._put(key,val,currentNode.leftChild)
# 如果没有左子树,那么key就成为左子节点
else:
currentNode.leftChild = TreeNode(key,val,parent=currentNode)
# 更新平衡因子
self.updateBalance(currentNode.leftChild)
# 如果key比当前节点大,那么放到到右子树
else:
# 如果有右子树,就递归进入右子树继续查找合适的空位置
if currentNode.hasRightChild():
self._put(key,val,currentNode.rightChild)
# 如果没有右子树,那么key就成为右子节点
else:
currentNode.rightChild = TreeNode(key,val,parent=currentNode)
# 更新平衡因子
self.updateBalance(currentNode.rightChild)
2. UpdateBalance
方法
def updateBalance(self,node):
# 如果当前节点的平衡因子大于1或小于-1,就进行重新平衡
if node.balanceFactor > 1 or node.balanceFactor < -1:
self.rebalance(node) # 重新平衡的方法后面再讲
return
# 如果平衡因子在正常范围内,且有父节点的话,就看父节点的平衡因子
if node.parent != None:
# 如果当前节点是其父节点的左子节点,就将父节点的平衡因子加1
if node.isLeftChild():
node.parent.balanceFactor += 1
# 如果当前节点是其父节点的右子节点,就将父节点的平衡因子减1
elif node.isRightChild():
node.parent.balanceFactor -= 1
# 如果当前节点的父节点平衡因子是否调整为0了,不为0就递归调用,传入父节点
if node.parent.balanceFactor != 0:
self.updateBalance(node.parent)
# 如果没有父节点,说明“影响”已经传递到根了,不用做任何操作
3. 重新平衡的思路
主要手段:将不平衡的子树进行旋转(rotation)。
根据“左重”或者“右重”进行不同方向的旋转。同时更新相关父节点引用,更新旋转后被影响节点的平衡因子。
如图,是一个“右重”子树A的左旋转(并保持BST性质):将右子节点B提升为子树的根,将旧根节点A作为新根节点B的左子节点。
如果新根节点B原来有左子节点,则将此节点设置为A的右子节点(A的右子节点一定有空,因为A原来的右子节点是B)。
更复杂一些的情况:如图的“左重”子树右旋转:
-
旋转后,新根节点C将旧根节点E作为右子节点,但是新根节点C原来已有右子节点D,需要将原有的右子节点D重新定位!
-
原有的右子节点D改到旧根节点E的左子节点同样, E的左子节点在旋转后一定有空。
4. rotateLeft
左旋(右旋的代码与之相似)
def rotateLeft(self,rotRoot):
# 新根(有可能只是某个子树的根)是rotRoot的右子节点
newRoot = rotRoot.rightChild
# rotRoot的右子节点指向新根的左子节点
rotRoot.rightChild = newRoot.leftChild
# 如果新根的左子节点不为空,让新根的左子节点指向rotRoot
if newRoot.leftChild is not None:
newRoot.leftChild.parent = rotRoot
newRoot.parent = rotRoot.parent
# 如果rotRoot是根节点,那么就将新根确定为根
if rotRoot.isRoot():
self.root = newRoot
# 如果rotRoot不是根节点,则根据rotRoot是其根节点的左或右子节点,确定新根的位置
else:
if rotRoot.isLeftChild():
rotRoot.parent.leftChild = newRoot
else:
rotRoot.parent.rightChild = newRoot
# 新根的左子节点指向rotRoot
newRoot.leftChild = rotRoot
# rotRoot的父节点指向新根
rotRoot.parent = newRoot
# 仅有两个Root结点需要调整因子,公式的讲解在下面
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)
左旋转对平衡因子的影响:保持了次序ABCDE,ACE的平衡因子不变。
主要看B、D的平衡因子的新旧关系:
新B= hA- hC,旧B= hA- 旧hD
而:
旧hD= 1+ max(hC, hE),所以:旧B= hA- (1+ max(hC, hE))
则:新B- 旧B= 1+ max(hC, hE)- hC
新B= 旧B+ 1+ max(hC, hE)- hC;把hC移进max函数里就有
新B= 旧B+ 1+ max(0, -旧D) <==> 新B= 旧B+ 1- min(0, 旧D)
5. 更复杂的情形
下图的“右重”子树, 单纯的左旋转无法实现平衡。左旋转后变成“左重”了,“左重”再右旋转,又回到“右重”。
-
所以,在左旋转之前检查右子节点的因子:如果==右子节点“左重”==的话,先对它进行右旋转,再实施原来的左旋转;
-
同样,在右旋转之前检查左子节点的因子:如果==左子节点“右重”==的话,先对它进行左旋转,再实施原来的右旋转。
6. rebalance
方法:重新平衡
def rebalance(self,node):
# 右重:需要左旋
if node.balanceFactor < 0:
# 右子节点左重:先右旋
if node.rightChild.balanceFactor > 0:
self.rotateRight(node.rightChild)
self.rotateLeft(node)
else:
self.rotateLeft(node)
# 左重:需要右旋
elif node.balanceFactor > 0:
# 左子节点右重:先左旋
if node.leftChild.balanceFactor < 0:
self.rotateLeft(node.leftChild)
self.rotateRight(node)
else:
self.rotateRight(node)
经过复杂的put方法, AVL树始终维持平衡, get方法也始终保持 O ( log n ) O(\log n) O(logn)高性能。
- 需要插入的新节点是叶节点,更新其所有父节点和祖先节点的代价最多为 O ( log n ) O(\log n) O(logn)
- 如果插入的新节点引发了不平衡,重新平衡最多需要2次旋转,但旋转的代价与问题规模无关,是常数 O ( 1 ) O(1) O(1)。
- 所以整个put方法的时间复杂度还是 O ( log n ) O(\log n) O(logn)
四、ADT Map的实现方法小结
我们采用了多种数据结构和算法来实现ADT Map,其时间复杂度数量级如下表
所示:
有序表 | 散列表 | 二叉查找树 | AVL树 | |
---|---|---|---|---|
put | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) |
get | O ( log 2 n ) O(\log_2n) O(log2n) | O ( 1 ) O(1) O(1) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) |
in | O ( log 2 n ) O(\log_2n) O(log2n) | O ( 1 ) O(1) O(1) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) |
del | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) -> O ( n ) O(n) O(n) | O ( log 2 n ) O(\log _2n) O(log2n) |