選單
×
   ❮   
HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE
     ❯   

DSA AVL 樹

AVL 樹是一種二叉搜尋樹,以兩位蘇聯發明家 Georgy Adelson-Velsky 和 Evgenii Landis 的名字命名,他們於 1962 年發明了 AVL 樹。

AVL 樹是自平衡的,這意味著樹的高度保持在最低水平,從而保證了搜尋、插入和刪除節點的執行時非常快,時間複雜度為 \(O( \log n)\)。

AVL 樹

普通 二叉搜尋樹 與 AVL 樹之間的唯一區別在於,AVL 樹還執行旋轉操作,以保持樹的平衡。

當左右子樹的高度差小於 2 時,二叉搜尋樹處於平衡狀態。

透過保持平衡,AVL 樹確保了最小的樹高,這意味著搜尋、插入和刪除操作可以非常快速地完成。

B G E K F P I M
二叉搜尋樹
(不平衡)
高度:6
G E K B F I P M
AVL 樹
(自平衡)
高度:3

上面的兩棵樹都是二叉搜尋樹,它們擁有相同的節點和相同的有序遍歷(按字母順序),但高度卻大不相同,因為 AVL 樹已經進行了自平衡。

在下面的動畫中,逐步演示 AVL 樹的構建過程,以瞭解平衡因子是如何更新的,以及何時需要執行旋轉操作來恢復平衡。

0 C 0 F 0 G 0 D 0 B 0 A

繼續閱讀,瞭解更多關於平衡因子是如何計算的,旋轉操作是如何執行的,以及 AVL 樹是如何實現的。


左旋和右旋

為了恢復 AVL 樹中的平衡,需要執行左旋或右旋,或者左右旋的組合。

前面的動畫展示了一個特定的左旋和一個特定的右旋。

但總的來說,左旋和右旋的操作就像下面的動畫所示。

X Y

請注意子樹是如何改變其父節點的。在旋轉過程中,子樹透過這種方式改變父節點,以保持正確的有序遍歷,並維持 BST 的屬性,即樹中所有節點,左子節點都小於右子節點。

同時也要記住,並非總是根節點會失去平衡並需要旋轉。


平衡因子

節點的平衡因子是子樹高度的差值。

子樹的高度儲存在 AVL 樹中每個節點的子樹高度中,平衡因子是根據其子樹高度計算的,以檢查樹是否失去平衡。

子樹的高度是子樹根節點與該子樹中最深葉節點之間的邊數。

節點 \(X\) 的平衡因子 (\(BF\)) 是其右子樹和左子樹的高度差。

\[ BF(X) = height(rightSubtree(X)) - height(leftSubtree(X)) \]

平衡因子值

  • 0:節點處於平衡狀態。
  • 大於 0:節點“右重”。
  • 小於 0:節點“左重”。

如果樹中一個或多個節點的平衡因子小於 -1 或大於 1,則認為該樹不平衡,需要執行旋轉操作來恢復平衡。

讓我們仔細看看 AVL 樹可以執行的四種不同型別的旋轉操作,以重新獲得平衡。


四種“失衡”情況

當僅一個節點的平衡因子小於 -1 或大於 1 時,該樹被視為失衡,需要進行旋轉以恢復平衡。

AVL 樹失衡有四種不同的情況,每種情況都需要不同的旋轉操作。

情況 描述 用於恢復平衡的旋轉
左-左 (LL) 失衡節點及其左子節點都呈左重。 單次右旋。
右-右 (RR) 失衡節點及其右子節點都呈右重。 單次左旋。
左-右 (LR) 失衡節點呈左重,其左子節點呈右重。 先對左子節點進行左旋,然後對失衡節點進行右旋。
右-左 (RL) 失衡節點呈右重,其右子節點呈左重。 先對右子節點進行右旋,然後對失衡節點進行左旋。

請參閱下面的動畫和解釋。


左-左 (LL) 情況

發現失衡的節點呈左重,並且該節點的左子節點也呈左重。

當發生 LL 情況時,對失衡節點進行單次右旋就足以恢復平衡。

逐步執行下面的動畫,以檢視 LL 情況,以及如何透過單次右旋恢復平衡。

-1 Q 0 P 0 D 0 L 0 C 0 B 0 K 0 A

當你逐步執行上面的動畫時,發生了兩次 LL 情況。

  1. 當插入 D 時,Q 的平衡因子變為 -2,這意味著樹失衡。這是 LL 情況,因為失衡節點 Q 及其左子節點 P 都呈左重(負平衡因子)。在節點 Q 處進行單次右旋可以恢復樹的平衡。
  2. 在插入 L、C 和 B 節點後,P 的平衡因子為 -2,這意味著樹失衡。這也是 LL 情況,因為失衡節點 P 及其左子節點 D 都呈左重。單次右旋可以恢復平衡。

注意:動畫中第二次發生 LL 情況時,進行了右旋,L 從 D 的右子節點變成了 P 的左子節點。旋轉的發生方式是為了保持正確的有序遍歷(在上面的動畫中是“B, C, D, L, P, Q”)。旋轉時更改父節點的另一個原因是保持 BST 屬性,即左子節點始終小於節點,右子節點始終大於節點。


右-右 (RR) 情況

當一個節點失衡且呈右重,而其右子節點也呈右重時,就會發生右-右情況。

在 RR 情況中,對失衡節點進行單次左旋足以恢復平衡。

+1 A 0 B 0 D 0 C 0 E 0 F

上面的動畫中發生了兩次 RR 情況。

  1. 當插入節點 D 時,A 變得失衡,並且 A 和 B 都呈右重。在節點 A 處進行左旋可以恢復樹的平衡。
  2. 在插入 E、C 和 F 節點後,節點 B 變得失衡。這是 RR 情況,因為節點 B 及其右子節點 D 都呈右重。左旋可以恢復樹的平衡。

左-右 (LR) 情況

左-右情況是指失衡節點呈左重,但其左子節點呈右重。

在這種 LR 情況中,首先對左子節點進行左旋,然後對原始失衡節點進行右旋。

逐步執行下面的動畫,以檢視左-右情況可能如何發生,以及如何執行旋轉操作來恢復平衡。

-1 Q 0 E 0 K 0 C 0 F 0 G

在你構建上面動畫中的 AVL 樹時,左-右情況發生了 2 次,並且需要執行旋轉操作來恢復平衡。

  1. 當插入 K 時,節點 Q 因平衡因子為 -2 而失衡,所以它是左重的,而其左子節點 E 是右重的,因此這是左-右情況。
  2. 在插入 C、F 和 G 節點後,節點 K 變得失衡且呈左重,其左子節點 E 呈右重,因此這是左-右情況。

右-左 (RL) 情況

右-左情況是指失衡節點呈右重,而其右子節點呈左重。

在這種情況下,我們首先對失衡節點的右子節點進行右旋,然後對失衡節點本身進行左旋。

逐步執行下面的動畫,以檢視右-左情況可能如何發生,以及如何執行旋轉來恢復平衡。

+1 A 0 F 0 B 0 G 0 E 0 D

插入節點 B 後,我們得到一個右-左情況,因為節點 A 變得失衡且呈右重,而其右子節點呈左重。為了恢復平衡,首先在節點 F 上執行右旋,然後在節點 A 上執行左旋。

下一個右-左情況發生在插入 G、E 和 D 節點後。這是一個右-左情況,因為 B 失衡且呈右重,而其右子節點 F 呈左重。為了恢復平衡,首先在節點 F 上執行右旋,然後在節點 B 上執行左旋。


在 AVL 樹中回溯

在 AVL 樹中插入或刪除節點後,樹可能會失去平衡。為了確定樹是否失衡,我們需要更新所有祖先節點的高度並重新計算它們的平衡因子。

這個過程稱為回溯,透過遞迴處理。當插入或刪除後遞迴呼叫返回到根節點時,每個祖先節點的高度都會被更新,並重新計算平衡因子。如果發現任何祖先節點的平衡因子超出 -1 到 1 的範圍,則在該節點執行旋轉以恢復樹的平衡。

在下面的模擬中,在插入節點 F 後,節點 C、E 和 H 都失衡,但由於回溯是透過遞迴處理的,因此首先發現並修復節點 H 的失衡,在這種情況下,這也修復了節點 E 和 C 的失衡。

-1 A 0 B 0 C 0 D 0 E 0 G 0 H 0 F

在插入節點 F 後,程式碼將回溯,在向上傳播到根節點的過程中計算平衡因子。當到達節點 H 並計算出平衡因子 -2 時,將執行右旋。僅當完成此旋轉後,程式碼才會繼續回溯,計算祖先節點 E 和 C 上方的平衡因子。

由於進行了旋轉,節點 E 和 C 的平衡因子與插入節點 F 之前的情況保持不變。


AVL 插入節點實現

此程式碼基於前一頁關於插入節點的 BST 實現。

與 BST 相比,AVL 樹的每個節點只有一個新屬性,那就是高度,但由於 AVL 樹的自平衡機制,AVL 樹實現需要許多新函式和額外的程式碼行。

下面的實現根據一個字元列表構建 AVL 樹,以建立上面模擬中的 AVL 樹。插入的最後一個節點“F”也觸發了右旋,就像上面的模擬一樣。

示例

Python

class TreeNode:
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None
        self.height = 1

def getHeight(node):
    if not node:
        return 0
    return node.height

def getBalance(node):
    if not node:
        return 0
    return getHeight(node.left) - getHeight(node.right)

def rightRotate(y):
    print('Rotate right on node',y.data)
    x = y.left
    T2 = x.right
    x.right = y
    y.left = T2
    y.height = 1 + max(getHeight(y.left), getHeight(y.right))
    x.height = 1 + max(getHeight(x.left), getHeight(x.right))
    return x

def leftRotate(x):
    print('Rotate left on node',x.data)
    y = x.right
    T2 = y.left
    y.left = x
    x.right = T2
    x.height = 1 + max(getHeight(x.left), getHeight(x.right))
    y.height = 1 + max(getHeight(y.left), getHeight(y.right))
    return y

def insert(node, data):
    if not node:
        return TreeNode(data)

    if data < node.data:
        node.left = insert(node.left, data)
    elif data > node.data:
        node.right = insert(node.right, data)

    # Update the balance factor and balance the tree
    node.height = 1 + max(getHeight(node.left), getHeight(node.right))
    balance = getBalance(node)

    # Balancing the tree
    # Left Left
    if balance > 1 and getBalance(node.left) >= 0:
        return rightRotate(node)

    # Left Right
    if balance > 1 and getBalance(node.left) < 0:
        node.left = leftRotate(node.left)
        return rightRotate(node)

    # Right Right
    if balance < -1 and getBalance(node.right) <= 0:
        return leftRotate(node)

    # Right Left
    if balance < -1 and getBalance(node.right) > 0:
        node.right = rightRotate(node.right)
        return leftRotate(node)

    return node

def inOrderTraversal(node):
    if node is None:
        return
    inOrderTraversal(node.left)
    print(node.data, end=", ")
    inOrderTraversal(node.right)

# Inserting nodes
root = None
letters = ['C', 'B', 'E', 'A', 'D', 'H', 'G', 'F']
for letter in letters:
    root = insert(root, letter)

inOrderTraversal(root)
執行示例 »

AVL 刪除節點實現

當刪除一個非葉子節點時,AVL 樹需要 minValueNode() 函式來查詢節點的下一個節點(按有序遍歷)。這與在二叉搜尋樹中刪除節點相同,如前一頁所述。

要在 AVL 樹中刪除節點,與插入節點所需的程式碼一樣,也需要相同的程式碼來恢復平衡。

示例

Python

def minValueNode(node):
    current = node
    while current.left is not None:
        current = current.left
    return current

def delete(node, data):
    if not node:
        return node

    if data < node.data:
        node.left = delete(node.left, data)
    elif data > node.data:
        node.right = delete(node.right, data)
    else:
        if node.left is None:
            temp = node.right
            node = None
            return temp
        elif node.right is None:
            temp = node.left
            node = None
            return temp

        temp = minValueNode(node.right)
        node.data = temp.data
        node.right = delete(node.right, temp.data)

    if node is None:
        return node

    # Update the balance factor and balance the tree
    node.height = 1 + max(getHeight(node.left), getHeight(node.right))
    balance = getBalance(node)

    # Balancing the tree
    # Left Left
    if balance > 1 and getBalance(node.left) >= 0:
        return rightRotate(node)

    # Left Right
    if balance > 1 and getBalance(node.left) < 0:
        node.left = leftRotate(node.left)
        return rightRotate(node)

    # Right Right
    if balance < -1 and getBalance(node.right) <= 0:
        return leftRotate(node)

    # Right Left
    if balance < -1 and getBalance(node.right) > 0:
        node.right = rightRotate(node.right)
        return leftRotate(node)

    return node
執行示例 »

AVL 樹的時間複雜度

看看下面的不平衡二叉搜尋樹。搜尋“M”意味著必須比較除 1 之外的所有節點。但是,在下面的 AVL 樹中搜索“M”只需要訪問 4 個節點。

因此,在最壞情況下,像搜尋、插入和刪除這樣的演算法必須遍歷整個樹的高度。這意味著使用 AVL 樹保持樹的低高度(\(h\)) 可以帶來更低的執行時。

B G E K F P I M
二叉搜尋樹
(不平衡)
G E K B F I P M
AVL 樹
(自平衡)

請參閱下面二叉搜尋樹和 AVL 樹之間時間複雜度的比較,以及時間複雜度如何與樹的高度 (\(h\)) 和樹中的節點數 (\(n\)) 相關。

  • BST 不會自動平衡。這意味著 BST 可能非常不平衡,幾乎像一個長鏈,高度幾乎與節點數相同。這使得像搜尋、刪除和插入節點這樣的操作變慢,時間複雜度為 \(O(h) = O(n)\)。
  • 然而,AVL 樹是自平衡的。這意味著樹的高度被保持在最低水平,因此搜尋、刪除和插入節點等操作更快,時間複雜度為 \(O(h) = O( \log n)\)。

\(O( \log n)\) 解釋

具有 \(h\) 高度和 \(n\) 個節點的 AVL 樹的搜尋、插入和刪除的時間複雜度為 \(O(h) = O( \log n)\) 這個事實可以這樣解釋:

想象一棵完美的二叉樹,除了最低級別外,所有節點都有兩個子節點,就像下面的 AVL 樹一樣。

H D B F E G A C L J N M O I K

這種 AVL 樹中每個級別的節點數是

\[1, 2, 4, 8, 16, 32, ..\]

這與

\[2^0, 2^1, 2^2, 2^3, 2^4, 2^5, ..\]

要獲得高度 \(h=3\) 的完美二叉樹中的節點數 \(n\),我們可以將每個級別的節點數相加

\[n_3=2^0 + 2^1 + 2^2 + 2^3 = 15\]

這實際上與

\[n_3=2^4 - 1 = 15\]

對於更大的樹也是如此!例如,如果我們想獲得高度 \(h=5\) 的樹中的節點數 \(n\),我們可以這樣計算節點數

\[n_5=2^6 - 1 = 63\]

所以總的來說,一棵完美二叉樹的高度 \(h\) 與其中的節點數 \(n\) 之間的關係可以表示為

\[n_h = 2^{h+1} - 1\]

注意:上面的公式也可以透過計算幾何級數 \(2^0 + 2^1 + 2^2+ 2^3 + ... + 2^n \) 的和來得到。

我們知道 AVL 樹中搜索、刪除或插入節點的時間複雜度是 \(O(h)\),但我們想論證的是時間複雜度實際上是 \(O(\log(n))\),所以我們需要找出由節點數 \(n\) 描述的高度 \(h\)。

\[ \begin{equation} \begin{aligned} n & = 2^{h+1}-1 \\ n+1 & = 2^{h+1} \\ \log_2(n+1) & = \log_2(2^{h+1}) \\ h & = \log_2(n+1) - 1 \\ \\ O(h) & = O(\log{n}) \end{aligned} \end{equation} \]

上面最後一行是如何推匯出來的可能不明顯,但對於具有大量節點(大 \(n\)) 的二叉樹,當我們考慮時間複雜度時,“+1”和“-1”項並不重要。有關如何使用大 O 符號計算時間複雜度的更多詳細資訊,請參閱此頁面

上面的數學表明,AVL 樹的搜尋、刪除和插入操作的時間複雜度 \(O(h)\) 實際上可以表示為 \(O(\log{n})\),這很快,比 BST 的時間複雜度 \(O(n)\) 快得多。


DSA 練習

透過練習來測試自己

練習

下面的 AVL 樹中,每個節點都與其平衡因子一起顯示。

AVL Tree

什麼是平衡因子?

The balance factor is the 
difference between each node's 
left and right subtree .

開始練習



×

聯絡銷售

如果您想將 W3Schools 服務用於教育機構、團隊或企業,請傳送電子郵件給我們
sales@w3schools.com

報告錯誤

如果您想報告錯誤,或想提出建議,請傳送電子郵件給我們
help@w3schools.com

W3Schools 經過最佳化,旨在方便學習和培訓。示例可能經過簡化,以提高閱讀和學習體驗。教程、參考資料和示例會不斷審查,以避免錯誤,但我們無法保證所有內容的完全正確性。使用 W3Schools 即表示您已閱讀並接受我們的使用條款Cookie 和隱私政策

版權所有 1999-2024 Refsnes Data。保留所有權利。W3Schools 由 W3.CSS 提供支援