平衡探索木の完全ガイド:AVL・赤黒木・Splay・Treap・B木の特徴と実装の選択ポイント

平衡探索木とは — 概要

平衡探索木(へいこうたんさくぎ、balanced search tree)は、探索・挿入・削除などの操作を効率的に行うために、木構造の高さ(深さ)がある条件で制限されるように設計された探索木(主に二分探索木や多分木)の総称です。高さが制限されることで、最悪ケースでも操作の計算量を対数オーダー(O(log n))に抑えられ、性能の予測可能性が向上します。

なぜ“平衡(バランス)”が重要か

単純な二分探索木はデータの挿入順序によっては偏り、線形リストに近い形(高さが O(n))になり得ます。これでは探索や更新が最悪 O(n) になり、パフォーマンスが劣化します。平衡探索木は木の高さを O(log n) に保つことで、探索・挿入・削除を効率的に行い、スケーラブルな性能を実現します。

主要な平衡探索木の種類(概要と特徴)

  • AVL木 — 非常に厳密に高さを管理する二分探索木。各ノードは左右部分木の高さ差(バランス因子)が -1, 0, +1 に保たれる。検索は非常に高速(比較回数が少ない)が、挿入・削除時に再平衡の回転が多くなることがある。高さは最小に近い。

  • 赤黒木(Red-Black Tree) — ノードに色(赤/黒)を持たせることで木の黒高さを制御する二分探索木。AVLより緩やかな平衡条件を持ち、挿入・削除での回転が少なめ。最悪高さは 2·log2(n+1) 以内と知られる。C++の std::map や Java の TreeMap、Linux カーネルの rb-tree 等でよく使われる。

  • Splay木 — アクセスしたノードを根に“そろばん式”に回転(splaying)して移動させる自己調整木。単一操作の最悪時間は O(n) だが、任意の操作列に対しては振る舞いが良く、各操作の償却時間(amortized)は O(log n)。局所性の高いアクセスに強い。

  • Treap(ランダム化BST) — 各ノードにランダムな優先度を割り当て、二分探索木かつヒープ性を満たす木。期待計算量は O(log n)。実装が比較的簡潔でランダム化により平均性能が保証される。

  • 2-3木 / 2-3-4木 / AA木 / スケープゴート木(Scapegoat)など — 様々な設計目標(実装の容易さ、回転回数削減、メモリ効率など)に応じたバリエーションが存在する。スケープゴート木は再構築を使って平衡を保ち、回転を使わない代わりに再構築コストを負う。

  • B木 / B+木 — ディスクやキャッシュを前提にした多分木。各ノードが多数の子を持つ(ノード内に複数のキーを格納)ことで高さを極端に低くし、ブロック読取回数を減らす。データベースやファイルシステムで広く使用される。B+木は内部ノードにキーのみ、葉ノードに全データを保持し、レンジクエリが高速。

平衡の操作(回転と再構成)の基本

多くの平衡二分探索木では、挿入や削除により局所的に条件が破られた場合、回転(rotation)と呼ばれる操作で局所再構成を行います。主なもの:

  • 単回転(single rotation) — 一方向の偏りを直す際に使う。AVLや赤黒木で一般的。

  • 二重回転(double rotation) — 子孫の偏りが逆向きの場合に使用(AVL の LR / RL)。

  • 色反転と回転(赤黒木) — ノードの色を変更しつつ、必要に応じ回転を行って黒高さなどの条件を回復する。

  • splay 操作(Splay木) — zig / zig-zig / zig-zag というパターンで連続回転を行い、アクセスノードを根に持ってくる。

計算量の見積もり

  • AVL木・赤黒木・Treap・B木(ノード内キー数を m とする) — 検索・挿入・削除は O(log n)(B木は高さが O(log_m n))。赤黒木は最悪ケースでも O(log n) が保証される。AVL はより厳密な高さ制約で検索が速いが、更新コストはやや高い。

  • Splay木 — 個々の操作は最悪 O(n) の場合があるが、任意の長い操作列では各操作の償却時間が O(log n)。局所的なアクセスの偏りがある場合に有利。

  • 実運用での定数係数 — 同じ O(log n) でも、ノードあたりのメタ情報(親ポインタ、色ビット、子配列など)や回転回数の差で実行時間に差が出るため、用途に応じた選択が重要。

どの平衡木を選ぶべきか(用途別アドバイス)

  • メモリ内で高速なランダムアクセスが主目的 — AVL は検索が速く、読み出し中心のワークロードに向く。実装はやや複雑。

  • 汎用的なマップ実装(ライブラリ) — 赤黒木は挿入・削除のオーバーヘッドが少なく実装が安定しているため多用される(C++ std::map, Java TreeMap など)。

  • ディスク・SSD 上での大規模データ/データベース — B木 / B+木。ノードをディスクブロックに合わせることで I/O を低減できる。レンジスキャンが多い場合は B+木 が有利。

  • 局所性の高いアクセスやキャッシュ効率 — Splay 木は最近アクセスしたデータが速く返ってくる特性を持つ。B+木 はブロックアクセスを圧縮できるため I/O とキャッシュ効率に優れる。

  • 並列・並行アクセス — 通常の平衡二分木はロック制御が難しい。データベース等では B-link 木やロックフリー構造、または skip list(平衡木の代替)を利用することが多い。

実装上の注意点とトレードオフ

  • メモリオーバーヘッド — 各ノードが持つポインタや色ビット、キーと値の格納形態によりメモリ使用量が変わる。B木ではノード内に配列を持つためオーバーヘッドは増えるが高さは低くなる。

  • キャッシュ局所性 — 多分木(B木/B+木)はノード内に多くのキーを詰められるためメモリ/ディスクの局所性が良く、実際の速度で優れることが多い。

  • 操作の安定性と予測可能性 — 赤黒木や AVL は最悪ケース保証があり、リアルタイム性が要求される場面で有利。splayは長期的に平均は良いが単発操作で遅くなる可能性がある。

  • デバッグと保守性 — AVL は回転ケースが多く実装が複雑。赤黒木の方が実装が比較的扱いやすく、ライブラリ実装が豊富。

実世界での利用例

  • データベースやファイルシステム:B木/B+木(例:InnoDB の一部実装や多くのRDBMS/ファイルシステム)

  • 標準ライブラリの連想配列:C++ std::map(一般的に赤黒木実装)、Java TreeMap(赤黒木)

  • OSカーネルのデータ構造:Linux カーネルは rb-tree を内部データ構造として使用

  • キャッシュ・局所性活用:splay 木はアクセス局所性の高い用途で検討されることがある

まとめ

平衡探索木は、データ構造設計における基本であり、用途に応じた選択が重要です。メモリ内での高速検索を重視するのか、ディスクI/Oを最小化するのか、並行性を優先するのかで最適解は変わります。赤黒木や AVL、B木/B+木、splay 木、treap などの特徴を理解し、アクセスパターン・ハードウェア特性・実装コストを踏まえて選ぶことが成功の鍵です。

参考文献