本文结合python数据结构一书
https://facert.gitbooks.io/python-data-structure-cn/6.%E6%A0%91%E5%92%8C%E6%A0%91%E7%9A%84%E7%AE%97%E6%B3%95/6.17.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0/
简介
相信各位有计算机基础的话,是对二叉树这种结构有一定了解的。二叉树表示简单,寻找方便,但若插入顺序不一,则形成的结构也不一样,从而很可能导致空间的浪费。今天我们要介绍的是一种二叉树的改进版,它通过平衡因子以及旋转的操作来维护整个二叉树,使其尽可能保持在一个平衡状态
平衡因子
平衡因子在AVL是一个比较重要的概念。我们将平衡因子定义为左子树的高度 减去 右子树的高度,在平衡二叉树中,各个节点的平衡因子均能保持在0,-1, 1这个范围内。
如果平衡因子大于0,我们说这个节点是左重的,因为左子树高度大于右子树
同理,如果平衡因子小于0,我们说这个节点是右重的,因为右子树高度大于左子树
我们看根节点,它的平衡因子就是左子树的高度(1),减去右子树高度(3)得到 -2
添加节点
平衡二叉树是基于二叉树改进的,引入了平衡因子后,我们添加节点的方法也要做一些小改变
首先新加入的节点平衡因子都为0,因此我们只需针对父节点来更新平衡因子
分以下几种情况
- 新节点是父节点的右节点,则将父节点平衡因子减1,因为右子树高度增加了
- 新节点是父节点的左节点,则将父节点平衡因子加1,因为左子树高度增加了
这个调用关系可以一直递归调用到祖先,直到树的根
因此我们仍需列出递归的两种基本情况
3. 递归调用已经到了树的根
4. 父节点的平衡因子已经调节到0
def _put(self,key,val,currentNode):
"""
重写_put方法
:param key:
:param val:
:param currentNode:
:return:
"""
if key < currentNode.key:
# 如果key小于当前节点的key,则要插入到左子树
if currentNode.hasLeftChild():
# 如果当前节点已经有左子树了,则递归调用,继续找
self._put(key, val, currentNode.leftChild)
else:
# 如果当前节点没有左子树,则创建一个TreeNode放置
currentNode.leftChild = TreeNode(key, val, parent=currentNode)
# 调用updateBalance方法更新平衡因子
self.updateBalance(currentNode.leftChild)
else:
# 如果key大于当前节点的key,则要插入到右子树
if currentNode.hasRightChild():
# 如果当前节点已经有右子树了,则递归调用,继续找
self._put(key, val, currentNode.rightChild)
else:
# 如果当前节点没有左子树,则创建一个TreeNode放置
currentNode.rightChild = TreeNode(key, val, parent=currentNode)
# 调用updateBalance方法更新平衡因子
self.updateBalance(currentNode.rightChild)
接下来是updateBalance方法的代码
def updateBalance(self, node):
if node.balanceFactor > 1 or node.balanceFactor < -1:
# 如果平衡因子不在范围内,则调用rebalance方法
self.rebalance(node)
return
if node.parent != None:
if node.isLeftChild():
# 如果当前插入节点是左孩子
# 则父节点的平衡因子加1
node.parent.balanceFactor += 1
elif node.isRightChild():
# 如果当前插入节点是右孩子
# 则父节点的平衡因子减1
node.parent.balanceFactor -= 1
if node.parent.balanceFactor != 0:
# 如果执行完后,父节点平衡因子不为0
# 则递归调用updateBalance方法
self.updateBalance(node.parent)
旋转操作
当平衡因子不在-1 0 1范围内,我们需要对当前节点做一个重新平衡
而重新平衡涉及到两个操作,分别是左旋转, 右旋转
旋转的目的是保证平衡因子在-1 0 1范围内,同时不改变二叉树的值大小顺序
上图是一个左旋的例子,我们可以看到A的平衡因子为-2,我们对其做一个左旋转,A就转到B的左边,而B此时变为根,同时ABC之间的大小顺序仍符合二叉树的顺序
左旋转
左旋转的具体操作有如下3个
- 将右子树作为根
- 将旧根作为新根的左孩子
- 若新根原本就有左孩子,则将新根的左孩子作为旧根的右孩子
第三个规则可能有点绕,我们知道新根是旧根的右子树,因此新根的孩子都是大于旧根的,所以将其作为旧根的右孩子
我们看以下这副图
这里附上左旋转的代码
def rotateLeft(self, rotRoot):
"""
左旋
1. 将右孩子作为根
2. 旧根移到左子
3. 若新根已经有一个左孩子,则将其作为旧根的右孩子
:param rotRoot:
:return:
"""
newRoot = rotRoot.rightChild
rotRoot.rightChild = newRoot.leftChild
if newRoot.leftChild != None:
# 若新根已经有一个左孩子,调整其指针,因为此时它是作为旧根的右孩子,因此parent调整为旧根
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):
"""
右旋
1. 将左孩子作为根
2. 将旧根作为新根的右子树
3. 如果新根已经有一个右孩子了,则作为新根的左孩子
:param rotRoot:
:return:
"""
newRoot = rotRoot.leftChild
rotRoot.leftChild = newRoot.rightChild
if newRoot.rightChild != None:
newRoot.rightChild.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.rightChild = rotRoot
rotRoot.parent = newRoot
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)
右旋转的代码和左旋转是对称的,除了我们调换的顺序之外,我们还要维护一下调换后节点的父节点和孩子节点的指针指向,如果不调整是会引发错误的
最后两行是调整旧根和新根的平衡因子
这需要一定的公式推导
我们以一个左旋转为例子
newBal表示旋转后的平衡因子
oldBal表示旋转前的平衡因子
H则代表各个节点的高度
我们就推出来这行代码
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
接下来我们看另一行代码的推导,原理是类似的
由此得到了后面一行代码
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)
重新平衡
只有在一定规则下进行左旋转和右旋转才能平衡节点
重新平衡的规则如下
- 如果子树需要左旋转使其平衡,首先检查右子节点的平衡因子。 如果右孩子是重的,那么对右孩子做右旋转,然后是原来的左旋转。
- 如果子树需要右旋转使其平衡,首先检查左子节点的平衡因子。 如果左孩子是重的,那么对左孩子做左旋转,然后是原来的右旋转。
这里的重指的就是该节点的平衡因子不为0
下面我们看一个例子
我们需要平衡节点A,A是右重的,因此需要左旋转,那么我们根据规则1,它的右孩子C平衡因子是1,是重的,因此对C做右旋转,再对A做左旋转
下面是我们的代码
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)