二分探索木の応用 新人エンジニアでも理解できるデータ構造の基本

こんにちは。ゆうせいです。

今回は、「赤黒木(Red-Black Tree)」というデータ構造について、初心者のエンジニアにもわかりやすく丁寧に解説していきます。
「木ってなに?」「二分探索木との違いは?」という方も安心してください!順を追って一緒に学んでいきましょう。


赤黒木とは何か?

二分探索木をベースにした自己平衡な木構造

赤黒木は、「二分探索木(Binary Search Tree: BST)」の一種です。
でも、ちょっとだけ特別な性質を持っています。

それは、「木の高さが極端に偏らないように自動でバランスをとる」こと。
このような構造を自己平衡二分探索木(Self-Balancing BST)と呼びます。

たとえば、普通の二分探索木では、データの挿入順によっては木が「片側にばかり伸びてしまう」ことがあります。すると、探索にかかる時間が長くなってしまうんです。
これを防ぐのが赤黒木の役割です!


赤黒木の5つのルール

赤黒木には、次の5つのルールがあります。このルールを守ることで、木のバランスが保たれるようになっています。

ルール番号内容
1ノードは「赤」か「黒」のどちらかの色を持つ
2根(ルート)は必ず黒
3葉(NILノード)はすべて黒(※空のノードも黒と見なす)
4赤いノードの子は必ず黒(赤の連続は禁止)
5各ノードから子孫の葉ノードまでの「黒ノードの数」はすべて同じ

例えで理解しよう!

木を会社の組織図に例えてみましょう。

  • 社長(根)は黒いスーツを着ています(ルール2)
  • どの部署(ノード)も赤か黒の制服を着ている(ルール1)
  • 赤い制服の社員(ノード)の直属の部下は黒い制服を着なければならない(ルール4)
  • どの部門も、最下層の社員(葉)までに通る黒い制服の数は同じでなければならない(ルール5)

つまり、赤黒木は「見た目を揃えることで全体の秩序を保っている組織」のようなものなんです!


どうやってバランスを保つのか?

回転(Rotation)と色の変更で調整!

ノードの挿入や削除によって、ルールに違反しそうになったときは、「回転(rotation)」と「色の変更」を使って木を調整します。

回転の種類

回転の名前説明
左回転(Left Rotation)ノードを軸に左方向に回す
右回転(Right Rotation)ノードを軸に右方向に回す

この操作で、木のバランスを崩さずに形を調整できるんです。


赤黒木の性能は?

赤黒木の特徴は、常に「ほぼバランスが取れた木」になるということ。
そのため、探索・挿入・削除すべての操作がO(log n)で行えます。

記号の意味(高校数学レベルで解説)

  • O(log n)(オー・ログ・エヌ)というのは、データがn個あっても、処理にかかる時間は「nの対数」に比例するという意味です。
  • つまり、データが倍になっても、処理時間はちょっとしか増えないという優れた性質を表しています!

どんなときに使うの?

赤黒木は、次のような場面でよく使われます。

  • C++のstd::mapstd::set
  • JavaのTreeMapTreeSet
  • Linuxのプロセススケジューラ

メリットとデメリット

メリットデメリット
挿入・削除・検索すべてが安定して高速(O(log n))実装がやや複雑
最悪の場合でも性能が保証される単純なBSTよりオーバーヘッドがある
データが増えてもバランスが崩れにくい回転の処理がやや難解に感じることがある

図で見る赤黒木の構造

簡単な赤黒木の例を見てみましょう。

        [10(B)]
        /     \
     [5(R)]   [20(R)]
  • 根(10)は黒
  • 子ノード(5, 20)は赤
  • 赤ノード(5, 20)の下には必ず黒ノードが来る

こうしたルールを守って、全体がバランスのとれた形になるよう維持されます。


他の自己平衡木との違いは?

赤黒木のライバルに、「AVL木」というデータ構造があります。

特徴項目赤黒木AVL木
挿入・削除やや速い少し遅い
検索性能やや低い高い(バランスが厳密)
バランスの取り方ゆるやか厳密

AVL木はより厳密にバランスを取るぶん、回転操作が多くなりがちです。一方、赤黒木はバランスを多少ゆるめに保つことで操作回数を減らしています。

赤黒木(Red-Black Tree)は、自己平衡二分探索木の一種で、挿入や削除を行っても常に木のバランスが保たれるように設計されています。

ここでは、Pythonで簡略化した赤黒木の実装を紹介します。
実装には以下の機能を含めます:

  • ノードの定義(色付き)
  • 木構造の定義
  • 挿入(insert)機能
  • バランス調整(回転と色の調整)

Pythonによる赤黒木の実装(基本構造)

class Node:
    RED = True
    BLACK = False

    def __init__(self, key, color=RED, left=None, right=None, parent=None):
        self.key = key
        self.color = color  # True = Red, False = Black
        self.left = left
        self.right = right
        self.parent = parent


class RedBlackTree:
    def __init__(self):
        self.NIL = Node(key=None, color=Node.BLACK)  # 空のノード(すべての葉ノードに使う)
        self.root = self.NIL

    def insert(self, key):
        new_node = Node(key, color=Node.RED, left=self.NIL, right=self.NIL)
        parent = None
        current = self.root

        while current != self.NIL:
            parent = current
            if new_node.key < current.key:
                current = current.left
            else:
                current = current.right

        new_node.parent = parent

        if parent is None:
            self.root = new_node
        elif new_node.key < parent.key:
            parent.left = new_node
        else:
            parent.right = new_node

        self.fix_insert(new_node)

    def fix_insert(self, node):
        while node != self.root and node.parent.color == Node.RED:
            if node.parent == node.parent.parent.left:
                uncle = node.parent.parent.right
                if uncle.color == Node.RED:
                    # Case 1: 親と叔父が赤
                    node.parent.color = Node.BLACK
                    uncle.color = Node.BLACK
                    node.parent.parent.color = Node.RED
                    node = node.parent.parent
                else:
                    if node == node.parent.right:
                        # Case 2: 親が左、現在ノードが右 → 左回転
                        node = node.parent
                        self.left_rotate(node)
                    # Case 3: 親が左、現在ノードが左 → 右回転
                    node.parent.color = Node.BLACK
                    node.parent.parent.color = Node.RED
                    self.right_rotate(node.parent.parent)
            else:
                # 左右を逆にして同様に処理
                uncle = node.parent.parent.left
                if uncle.color == Node.RED:
                    node.parent.color = Node.BLACK
                    uncle.color = Node.BLACK
                    node.parent.parent.color = Node.RED
                    node = node.parent.parent
                else:
                    if node == node.parent.left:
                        node = node.parent
                        self.right_rotate(node)
                    node.parent.color = Node.BLACK
                    node.parent.parent.color = Node.RED
                    self.left_rotate(node.parent.parent)
        self.root.color = Node.BLACK

    def left_rotate(self, x):
        y = x.right
        x.right = y.left
        if y.left != self.NIL:
            y.left.parent = x
        y.parent = x.parent
        if x.parent is None:
            self.root = y
        elif x == x.parent.left:
            x.parent.left = y
        else:
            x.parent.right = y
        y.left = x
        x.parent = y

    def right_rotate(self, y):
        x = y.left
        y.left = x.right
        if x.right != self.NIL:
            x.right.parent = y
        x.parent = y.parent
        if y.parent is None:
            self.root = x
        elif y == y.parent.right:
            y.parent.right = x
        else:
            y.parent.left = x
        x.right = y
        y.parent = x

    def inorder(self, node=None):
        if node is None:
            node = self.root
        if node != self.NIL:
            self.inorder(node.left)
            print(f'{node.key}({"R" if node.color else "B"})', end=' ')
            self.inorder(node.right)


実行例

# 使用例
tree = RedBlackTree()
for key in [10, 20, 30, 15, 25, 5, 1]:
    tree.insert(key)

tree.inorder()
# 出力: 木構造を中間順で出力(例: 1(R) 5(B) 10(R) 15(B) 20(B) 25(R) 30(B))

AVL木とは何か?

高さバランスを厳しく保つ二分探索木

AVL木は、「自己平衡二分探索木(Self-Balancing Binary Search Tree)」の一種です。

まず、「二分探索木(Binary Search Tree, BST)」についてざっくり復習しましょう。

  • 左の子には「親より小さい値」
  • 右の子には「親より大きい値」
    が並ぶ木構造のことでしたね。

ただ、普通のBSTは、データの挿入順によっては形が極端に崩れてしまうことがあります。
すると、探索にかかる時間が最悪でO(n)になってしまうのです。

この「木の偏り」を防ぐために考え出されたのが、AVL木なんです。


AVL木の最大の特徴:バランスファクター

AVL木では、すべてのノードに「バランスファクター(平衡因子)」という値を持たせます。

バランスファクターとは?

各ノードにおいて:

バランスファクター = 左部分木の高さ - 右部分木の高さ

例を挙げてみます:

  • 左の高さが3、右の高さが2 → バランスファクター = 1
  • 左の高さが2、右の高さが4 → バランスファクター = -2(アウト!)

ルールはたった1つ!

すべてのノードでバランスファクターの絶対値が「1以下」になるように保つこと。

つまり、以下の条件が守られている限り、AVL木はバランスが取れた状態です:

|左の高さ − 右の高さ| ≦ 1

これによって、どんなにデータを入れても、常に高速な探索が可能になるというわけです!


木が傾いたらどうする?→ 回転(Rotation)でリカバリ!

AVL木では、ノードを挿入・削除してバランスが崩れそうになったとき、「回転(rotation)」という操作で形を修正します。

回転のパターンは4つ!

パターン名発生するケース修正方法
LL回転左の左に挿入された右回転(Right Rotate)
RR回転右の右に挿入された左回転(Left Rotate)
LR回転左の右に挿入された左→右回転の組み合わせ
RL回転右の左に挿入された右→左回転の組み合わせ

例えるなら、イスがグラグラして傾いたときに、ちょっと動かして元のバランスを取り戻すようなものです!


AVL木の性能は?

AVL木は、どんな状況でも常にバランスを維持しているため、最悪のケースでも探索や挿入・削除がO(log n)で行えます。

計算量の意味をおさらい!

  • O(log n):データ量が増えても、処理時間の伸びが「ゆるやか」。
  • 普通のBSTでは最悪O(n)(直線になる)ですが、AVL木ではそれを防げます。

図で見てみよう:AVL木の例

次のようなAVL木を考えてみましょう。

       [30]
      /    \
   [20]    [40]
  /   \
[10] [25]
  • どのノードも、左右の高さの差が±1以内
  • この木はAVL木の条件を満たしています

もし、ここに「5」を追加すると、「10」の左に追加されてバランスが崩れます。
このときは右回転(LL回転)を使って修正することになります。


赤黒木との違いは?

赤黒木(Red-Black Tree)も自己平衡木ですが、AVL木とはバランスの取り方が違います。

比較項目AVL木赤黒木
バランス精度厳密(±1まで)やや緩め(ルールで制御)
挿入速度やや遅いやや速い
検索性能高いやや低い
回転回数多め少なめ
実装の複雑さやや難しい比較的シンプル

どちらを使うべき?

  • 検索を高速にしたいなら → AVL木
  • 全体的な操作を平均化したいなら → 赤黒木

どちらも目的に応じて使い分けましょう!


メリットとデメリット

メリット

  • 常にバランスが保たれているため、探索が高速
  • 高さが最小に近いため、読み込み効率が良い

デメリット

  • 挿入・削除のたびにバランス調整が必要なので、回転が多くなる
  • 実装がやや複雑(初心者には少し難しい)

実装に含まれる要素

この簡易実装では以下の機能を備えています:

  • ノードクラスの定義(高さを保持)
  • AVL木本体(insertleft_rotateright_rotate
  • バランスファクターの計算と調整

PythonでのAVL木の実装

class Node:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None
        self.height = 1  # 初期状態では高さ1(自分だけ)


class AVLTree:
    def insert(self, root, key):
        # 通常の二分探索木の挿入
        if not root:
            return Node(key)
        elif key < root.key:
            root.left = self.insert(root.left, key)
        else:
            root.right = self.insert(root.right, key)

        # 高さを更新
        root.height = 1 + max(self.get_height(root.left), self.get_height(root.right))

        # バランスファクターを計算
        balance = self.get_balance(root)

        # バランスが崩れた場合の回転処理
        # LLパターン
        if balance > 1 and key < root.left.key:
            return self.right_rotate(root)
        # RRパターン
        if balance < -1 and key > root.right.key:
            return self.left_rotate(root)
        # LRパターン
        if balance > 1 and key > root.left.key:
            root.left = self.left_rotate(root.left)
            return self.right_rotate(root)
        # RLパターン
        if balance < -1 and key < root.right.key:
            root.right = self.right_rotate(root.right)
            return self.left_rotate(root)

        return root

    def left_rotate(self, z):
        y = z.right
        T2 = y.left

        # 回転処理
        y.left = z
        z.right = T2

        # 高さ更新
        z.height = 1 + max(self.get_height(z.left), self.get_height(z.right))
        y.height = 1 + max(self.get_height(y.left), self.get_height(y.right))

        return y

    def right_rotate(self, y):
        x = y.left
        T2 = x.right

        # 回転処理
        x.right = y
        y.left = T2

        # 高さ更新
        y.height = 1 + max(self.get_height(y.left), self.get_height(y.right))
        x.height = 1 + max(self.get_height(x.left), self.get_height(x.right))

        return x

    def get_height(self, node):
        if not node:
            return 0
        return node.height

    def get_balance(self, node):
        if not node:
            return 0
        return self.get_height(node.left) - self.get_height(node.right)

    def inorder(self, root):
        if not root:
            return
        self.inorder(root.left)
        print(f'{root.key}', end=' ')
        self.inorder(root.right)


実行例

# AVL木のテスト
avl = AVLTree()
root = None

for key in [10, 20, 30, 40, 50, 25]:
    root = avl.insert(root, key)

avl.inorder(root)
# 出力: 10 20 25 30 40 50 (昇順で表示される)


ポイントまとめ

  • 高さの差(バランスファクター)を元に回転操作を行うことで、常にバランスを保ちます
  • 回転には「左回転(LL)・右回転(RR)・2段回転(LR, RL)」の4種類があります
  • 各ノードは高さ情報を持っており、バランスチェックに使います

まずは基本から学びたい方へ

2分探索木 配置&探索ゲーム

木探索アルゴリズム学習ゲーム

セイ・コンサルティング・グループの新人エンジニア研修のメニューへのリンク

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。