AVL Tree Implementation 自平衡二叉搜索树的实现
学习资料:
CSDN:【数据结构】AVL树详解
百度词条:AVL树
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。
现在我们已经证明保持AVL树的平衡将是一个很大的性能改进,让我们看看我们将如何扩充将新密钥插入树中的过程。由于所有新密钥都作为叶节点插入树中,并且我们知道新叶的平衡因子为零,因此对刚刚插入的节点没有新的要求。但是一旦添加了新叶子,我们必须更新其父叶子的平衡因子。这个新叶子如何影响父亲的平衡因子取决于叶子节点是左子节点还是右子节点。如果新节点是正确的子节点,则父节点的平衡因子将减1。如果新节点是左子节点,则父节点的平衡因子将增加1。这种关系可以递归地应用于新节点的祖父,并且可能一直应用于每个祖先一直到树的根。由于这是一个递归过程,让我们检查更新余额因子的两个基本案例:
- 递归调用已到达树的根。
- 父母的余额因子已调整为零。 你应该说服自己,一旦子树的平衡因子为零,那么它的祖先节点的平衡就不会改变
我们将实现AVL树作为BinarySearchTree的子类。 首先,我们将覆盖_put方法并编写一个新的updateBalance辅助方法。 清单1中显示了这些方法。您会注意到_put的定义与简单的二进制搜索树中的定义完全相同,只是在第7行和第13行添加了对updateBalance的调用。
def _put(self,key,val,currentNode):
if 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:
if currentNode.hasRightChild():
self._put(key,val,currentNode.rightChild)
else:
currentNode.rightChild = TreeNode(key,val,parent=currentNode)
self.updateBalance(currentNode.rightChild)
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)
新的updateBalance方法是完成大部分工作的地方。这实现了我们刚才描述的递归过程。 updateBalance方法首先检查当前节点是否不够平衡以至于需要重新平衡(第16行)。如果是这种情况,那么重新平衡就完成了,不需要进一步更新父母。如果当前节点不需要重新平衡,则调整父节点的平衡因子。如果父级的平衡因子不为零,则算法通过递归调用父级的updateBalance继续向树的方向向上移动。
当树的重新平衡是必要的时候,我们该怎么做呢?高效的重新平衡是使AVL Tree在不牺牲性能的情况下正常工作的关键。为了使AVL树恢复平衡,我们将在树上执行一次或多次旋转。
要了解旋转是什么,让我们看一个非常简单的例子。考虑图3左半部分的树。这棵树不平衡,平衡因子为-2。为了使这棵树平衡,我们将在以节点A为根的子树周围使用左旋转。
要执行左旋转,我们基本上执行以下操作:
促使正确的孩子(B)成为子树的根。
将旧根(A)移动到新根的左子节点。
如果新的根(B)已经有一个左孩子,那么将它作为新左孩子的正确孩子(A)。注意:由于新根(B)是A的正确子,因此A的右子女保证在此时为空。这允许我们将新节点添加为正确的子节点而无需进一步考虑。
虽然这个过程在概念上相当容易,但代码的细节有点棘手,因为我们需要以正确的顺序移动东西,以便保留二进制搜索树的所有属性。此外,我们需要确保适当地更新所有父指针。
让我们看一个稍微复杂的树来说明正确的旋转。图4的左侧显示了一棵左重的树,根部的平衡因子为2。要执行正确的旋转,我们基本上执行以下操作:
促使左子(C)成为子树的根。
将旧根(E)移动为新根的正确子项。
如果新的根(C)已经有一个正确的孩子(D),那么将它作为新的右子女(E)的左子女。注意:由于新根(C)是E的左子节点,因此此时E的左子节点保证为空。这允许我们添加一个新节点作为左子节点而无需进一步考虑。
现在您已经看到旋转并且基本了解旋转的工作方式,让我们看一下代码。清单2显示了右旋转和左旋转的代码。在第2行中,我们创建了一个临时变量来跟踪子树的新根。正如我们之前所说的,新根是前一个根的正确子。既然已经在这个临时变量中存储了对正确子项的引用,我们将旧的root的右子项替换为new的左子项。
下一步是调整两个节点的父指针。如果newRoot有一个左子项,则左子项的新父项将成为旧的子项。新根的父级设置为旧根的父级。如果旧根是整个树的根,那么我们必须将树的根设置为指向这个新根。否则,如果旧的根是左子,那么我们将左子的父级更改为指向新的根;否则我们将正确子项的父项更改为指向新项。 (第10-13行)。最后,我们将旧根的父级设置为新根。这是一个很复杂的簿记,所以我们鼓励你在看图3的同时追踪这个函数.convertingRight方法与rotateLeft是对称的,所以我们将留给你研究rotateRight的代码。
def rotateLeft(self,rotRoot):
newRoot = rotRoot.rightChild
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)