二叉搜索树
二叉搜索树(AVL树)实现 Map 抽象数据类型就像一个常规的二叉搜索树,我们将节点的平衡因子定义为左子树的高度和右子树的高度之间的差:
balanceFactor=height(leftSubTree)−height(rightSubTree)
- 如果平衡因子大于零,则子树是左重的。
- 如果平衡因子小于零,则子树是右重的。
- 如果平衡因子是零,那么树是完美的平衡。
如果平衡因子是 -1,0 或 1,我们将定义树平衡。一旦树中的节点的平衡因子是在这个范围之外,我们将需要一个程序来使树恢复平衡。下图展示了不平衡,右重树和每个节点的平衡因子的示例:
平衡二叉树实现
添加新叶时,我们必须更新其父的平衡因子。这个新叶如何影响父的平衡因子取决于叶节点是左孩子还是右孩子。如果新节点是右子节点,则父节点的平衡因子将减少1。如果新节点是左子节点,则父节点的平衡因子将增加1。这个关系可以递归地应用到新节点的祖父节点,并且应用到每个祖先一直到树的根。由于这是一个递归过程,我们来看一下用于更新平衡因子的两种基本情况:
- 递归调用已到达树的根。
- 父节点的平衡因子已调整为零。一旦一个子树的平衡因子为零,那么它的祖先节点的平衡不会改变。
我们将实现 AVL 树作为 BinarySearchTree
的子类。首先,我们将覆盖_put
方法并编写一个新的 updateBalance
:
新的 updateBalance
方法完成了大多数工作。这实现了我们刚才描述的递归过程。 updateBalance
方法首先检查当前节点是否不够平衡,需要重新平衡:
如果平衡,则重新平衡完成,并且不需要对父节点进行进一步更新。
如果当前节点不需要重新平衡,则调整父节点的平衡因子。
如果父的平衡因子不为零,那么算法通过递归调用父对象上的 updateBalance
,继续沿树向根向上运行。
有效的重新平衡是使AVL树在不牺牲性能的情况下正常工作的关键。为了使AVL树恢复平衡,我们将在树上执行一个或多个旋转。
如下图所示,这棵树平衡因子为 -2,不平衡。为了使这棵树平衡,我们将使用以节点 A 为根的子树的左旋转。
要执行左旋转,我们基本上执行以下操作:
- 提升右孩子(B)成为子树的根。
- 将旧根(A)移动为新根的左子节点。
- 如果新根(B)已经有一个左孩子,那么使它成为新左孩子(A)的右孩子。注意:由于新根(B)是A的右孩子,A 的右孩子在这一点上保证为空。这允许我们添加一个新的节点作为右孩子,不需进一步的考虑。
下面这棵树在根处的平衡因子为 2。要执行右旋转,我们基本上执行以下操作:
- 提升左子节点(C)为子树的根。
- 将旧根(E)移动为新根的右子树。
- 如果新根(C)已经有一个正确的孩子(D),那么使它成为新的右孩子(E)的左孩子。注意:由于新根(C)是 E 的左子节点,因此 E 的左子节点在此时保证为空。这允许我们添加一个新节点作为左孩子,不需进一步的考虑。
def rotateLeft(self,rotRoot):
newRoot = rotRoot.rightChhild
rotRoot.rightChhild = 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
newRoot.leftChild = rotRoot
rotRoot.parent = newRoot
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor,0)
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor,0)
B 和 D 是关键节点,A,C,E 是它们的子树。 设hx 表示以节点 x 为根的特定子树的高度。 根据定义,我们知道以下:
\({\rm{newBal(B) = hA - hC}}\)
\({\rm{oldBal(B) = hA - hD}}\)
但我们知道,D 的旧高度也可以由 1 + max(hC,hE)
给出,也就是说,D 的高度比其两个孩子的最大高度大 1。 记住,hC
和 hE
没有改变。 所以,让我们用第二个方程来代替它:
$ oldBal(B)=hA-(1+max(hC,hE)) \( 然后减去这两个方程。 以下步骤进行减法并使用一些代数来简化 `newBal(B) `的等式。 \){\rm{newBal(B) - oldBal(B) = hA - hC - (hA - (1 + max(hC,hE)))}}\( \){\rm{newBal(B) - oldBal(B) = hA - hC - hA + (1 + max(hC,hE))}}\( \){\rm{newBal(B) - oldBal(B) = hA - hA + 1 + max(hC,hE) - hC}}\( \)newBal(B) - oldBal(B) = 1 + max(hC,hE) - hC$$
接下来我们将oldBal(B)
移动到方程的右边,并利用 max(a,b) -c = max(a-c,b-c)
。
$ newBal(B)=oldBal(B)+1+max(hC-hC,hE-hC) \( 但是,\)hE-hC$与 \(-oldBal(D)\) 相同。因此,我们可以使用另一个表示 \(max(-a, -b) = -min(a, b)\) 的标识。 因此,我们可以完成我们的 newBal(B) 的推导,具有以下步骤:
\({\rm{newBal(B) = oldBal(B) + 1 + max(0, - oldBal(D))}}\)
\({\rm{newBal(B) = oldBal(B) + 1 - min(0,oldBal(D))}}\)
现在我们有所有的部分,我们很容易知道。 我们记住 B 是 rotRoot 和 D 是newRoot 然后我们可以看到这正好对应:
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(0,newRoot.balanceFactor)
考虑下面的情况:
左旋后,我们现在已经在另一方面失去平衡:
如果我们做右旋以纠正这种情况,我们就回到我们开始的地方。
要纠正这个问题,我们必须使用以下规则集:
- 如果子树需要左旋转使其平衡,首先检查右子节点的平衡因子。 如果右孩子是重的,那么对右孩子做右旋转,然后是原来的左旋转。
- 如果子树需要右旋转使其平衡,首先检查左子节点的平衡因子。 如果左孩子是重的,那么对左孩子做左旋转,然后是原来的右旋转。
从围绕节点 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)
通过保持树在所有时间的平衡,我们可以确保 get 方法将按 \(O(log2(n))\) 时间运行。但问题是我们的 put 方法有什么成本?让我们将它分解为 put 执行的操作。由于将新节点作为叶子插入,更新所有父节点的平衡因子将需要最多 \(log2^n\) 运算,树的每层一个运算。如果发现子树不平衡,则需要最多两次旋转才能使树重新平衡。但是,每个旋转在 \(O(1)\)时间中工作,因此我们的put操作仍然是 \(O(log2^n)\)。
Map抽象数据结构总结
二叉搜索表,散列表,二叉搜索树和平衡二叉搜索树实现的性能对比: