平衡二叉搜索樹


二叉搜索樹

二叉搜索樹(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。 記住,hChE 沒有改變。 所以,讓我們用第二個方程來代替它:
$ 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抽象數據結構總結

二叉搜索表,散列表,二叉搜索樹和平衡二叉搜索樹實現的性能對比:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM