Menu
Coddy logo textTech

AVL木

最終更新

AVL木は自己平衡二分探索木です。通常のBSTのように動作しますが、挿入のたびに各祖先の平衡係数(左右の部分木の高さの差)を調べ、いずれかのノードが不均衡になった場合(差が1より大きい場合)、回転を行ってバランスを回復します。上部の再生ボタンを押すと、値が挿入され、木が回転して形を整える様子を見られます。

木がわずかにでも傾きすぎることを決して許さないため、AVL木は O(log n) の高さを保証し、検索・挿入・削除は常に O(log n) です。これは通常のBSTを台無しにするソート済み入力に対しても成り立ちます。その代償は、挿入ごとの追加の回転と高さの管理です。

時間計算量と空間計算量

操作計算量備考
検索O(log n)高さは常に ~1.44 log n
挿入O(log n)加えて O(1) 回転
削除O(log n)加えて O(log n) 回転
空間O(n)ノードごとに高さフィールドが1つ

4つの回転ケース

ケース不均衡修正
左-左左の子の左側が重い1回の右回転
右-右右の子の右側が重い1回の左回転
左-右左の子の右側が重い左回転してから右回転
右-左右の子の左側が重い右回転してから左回転

具体例

[10, 20, 30, 40, 50, 25] を1つずつ挿入:

ステップ構造動作
10 を挿入10最初のノードが根になる;均衡
20 を挿入10(_, 20)10 の右へ進む;まだ均衡
30 を挿入20(10, 30)10 で右-右、そこで1回の左回転が 20 を根へ押し上げる
40 を挿入20(10, 30(_, 40))30 の右へ進む;すべての平衡係数が ±1 以内に収まる
50 を挿入20(10, 40(30, 50))30 で右-右、そこで 30 での左回転が 40 を押し上げる
25 を挿入30(20(10, 25), 40(_, 50))20 で右-左:40 の部分木を右回転し、その後 20 を左回転

AVL木を使うべき場面

使うべきとき避けるべきとき
検索が挿入をはるかに上回り、可能な限り最も低い高さが欲しいとき挿入と削除が支配的なとき - 追加の回転と再平衡は赤黒木より高くつく
敵対的またはソート済みの入力に対してさえ、保証された O(log n) の最悪計算量が必要なとき単純なハッシュテーブルで十分なとき - 順序付き走査や範囲クエリが不要
順序付き操作が必要なとき:中間順走査、前者/後者、範囲クエリデータ集合が非常に小さいとき - 通常のBSTやソート済み配列の方が単純で十分に速い
データがすでにソート済みで届く可能性があり、不均衡なBSTを連結リストに変えてしまうとき非常に厳しいメモリ環境で、ノードごとの高さフィールドの追加メモリを負担できないとき

AVL Treeのコード

Python, JavaScript, Java, C++, Cによるクリーンで実行可能なAVL Treeの実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。

PythonでのAVL Treeのコード

Python
1class Node:2    def __init__(self, key):3        self.key = key4        self.left = None5        self.right = None6        self.height = 17
8
9def height(node):10    return node.height if node else 011
12
13def update(node):14    node.height = 1 + max(height(node.left), height(node.right))15
16
17def rotate_right(y):18    x = y.left19    y.left = x.right20    x.right = y21    update(y)22    update(x)23    return x24
25
26def rotate_left(x):27    y = x.right28    x.right = y.left29    y.left = x30    update(x)31    update(y)32    return y33
34
35def insert(node, key):36    if node is None:37        return Node(key)38    if key < node.key:39        node.left = insert(node.left, key)40    else:41        node.right = insert(node.right, key)42    update(node)43    balance = height(node.left) - height(node.right)44    # Four imbalance cases: LL, RR, LR, RL45    if balance > 1 and key < node.left.key:46        return rotate_right(node)47    if balance < -1 and key > node.right.key:48        return rotate_left(node)49    if balance > 1:50        node.left = rotate_left(node.left)51        return rotate_right(node)52    if balance < -1:53        node.right = rotate_right(node.right)54        return rotate_left(node)55    return node56
57
58def inorder(node):59    if node is None:60        return []61    return inorder(node.left) + [node.key] + inorder(node.right)62
63
64root = None65for key in [10, 20, 30, 40, 50, 25]:66    root = insert(root, key)67
68print("Inorder:", inorder(root))69print("Root:", root.key, "| tree height:", root.height)
このコードをPythonプレイグラウンドで実行

AVL木のよくある質問

AVL木における平衡係数とは何ですか?
ノードの平衡係数とは、その左部分木の高さから右部分木の高さを引いた値です。AVL木は各ノードの平衡係数を -1、0、+1 のいずれかに保ちます。挿入によってその範囲を外れた場合、回転がバランスを回復します。
AVL木と赤黒木の違いは何ですか?
どちらも O(log n) の操作を持つ自己平衡BSTです。AVL木はより厳密に平衡が取られているため検索がわずかに速いですが、挿入/削除でより多くの回転を行う場合があります。赤黒木はより緩やかに平衡が取られ、回転の少なさを優先します。そのため多くの標準ライブラリはマップや集合に赤黒木を使います。
なぜ通常の二分探索木ではなくAVL木を使うのですか?
通常のBSTは、値がソート順で届くと偏った連鎖を形成し、操作が O(n) に劣化することがあります。AVL木は回転して平衡を保ち、挿入順序に関わらず O(log n) の高さと操作を保証します。
データベースのインデックスにはAVL木と赤黒木のどちらが良いですか?
ワークロード次第です。AVL木はより厳格に平衡が取られているため、読み取りが支配的で最短の検索経路が欲しい場合に有利です。しかし、ほとんどのデータベースや言語ライブラリは赤黒木(またはB木)を選びます。なぜなら書き込みの多いワークロードでより少ない回転で再平衡でき、挿入や削除が頻繁なときにはそれがより重要だからです。
1回のAVL挿入に必要な回転は何回ですか?
1つの値を挿入した後に木を再平衡するには、単回転または二重(2段階)回転のいずれか、最大1回の回転で済みます。これは挿入が部分木の高さを最大でも1しか増やさないため、最も下の不均衡な祖先での1回の修正で十分だからです。削除は異なり、再平衡が根に向かって伝播するにつれて最大 O(log n) 回の回転が必要になることがあります。
回転のたびにノードの高さを更新する必要がありますか?
はい - よくあるバグは、ポインタを正しく回転させたのに、関与する2つのノードの高さの再計算を忘れることです。各回転の後、まず降格したノードの高さを、次に昇格したノードの高さを更新する必要があります。昇格したノードの高さは新しい子に依存するためです。これを怠ると古い平衡係数が残り、将来の再平衡の判断が壊れます。
Coddy programming languages illustration

Coddy でアルゴリズムをマスターしよう

始める