二分探索木の応用 新人エンジニアでも理解できるデータ構造の基本
こんにちは。ゆうせいです。
今回は、「赤黒木(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::map
やstd::set
- Javaの
TreeMap
やTreeSet
- 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木本体(
insert
、left_rotate
、right_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種類があります
- 各ノードは高さ情報を持っており、バランスチェックに使います
まずは基本から学びたい方へ
セイ・コンサルティング・グループの新人エンジニア研修のメニューへのリンク
投稿者プロフィール
