一覧へ

二分探索木 (BST)

二分探索木の原理を理解し、挿入、探索、中位巡回をPythonコードで直接実装します。

中級
|
12
|
検証済み (2026-07)
二分探索木BSTbinary search tree挿入探索巡回
進捗0/23 (0%)

二分探索木 (BST)

このトピックを修了すると

二分探索木の基本的なルールを説明でき、挿入と探索をコードで実装でき、中位巡回によってソートされたデータを取得できます。


配列の限界

ソートされた配列での二分探索は O(log n) で高速です。しかし、挿入と削除は遅いです。なぜなら、途中に値を挿入するには、残りの要素をずらさなければならないからです。

text
[2, 5, 8, 12, 15]
     ↑ ここに 7 を挿入するには
[2, 5, 7, 8, 12, 15]  ← 8, 12, 15 を右にずらさなければならない (O(n))

検索も高速で、挿入も高速なデータ構造が必要です。それが二分探索木です。


基本的なルール

二分探索木 (Binary Search Tree, BST) は、たった 1 つのルールを守ります。

左の子 < 親 < 右の子

text
8
       / \
      3   10
     / \    \
    1   6    14
       / \   /
      4   7 13

どのノードを見ても、左部分木のすべての値はそれよりも小さく、右部分木のすべての値はそれよりも大きくなります。


ノードの実装

python
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None

各ノードは、値 1 つと、左/右の子を指すポインタ 2 つを持ちます。


挿入

新しい値を挿入するとき、ルートから始めてルールに従って下っていきます。

python
def insert(root, value):
if root is None:
return Node(value)
if value < root.value:
root.left = insert(root.left, value)
elif value > root.value:
root.right = insert(root.right, value)
return root
python
# ツリーの作成
root = None
for v in [8, 3, 10, 1, 6, 14, 4, 7, 13]:
root = insert(root, v)

値が現在のノードよりも小さい場合は左、大きい場合は右に進みます。空の場所を見つけたら、そこに新しいノードを作成します。


探索

python
def search(root, target):
if root is None:
return False
if target == root.value:
return True
elif target < root.value:
return search(root.left, target)
else:
return search(root.right, target)
python
print(search(root, 7)) # True
print(search(root, 5)) # False

二分探索と同じ原理です。各ステップで半分を破棄するため、バランスの取れたツリーでは O(log n) です。

探索の過程 (7 を探すとき):

text
8 → 7 < 8, 左
3 → 7 > 3, 右
6 → 7 > 6, 右
7 → 見つかった!

中位巡回 — ソートされた結果

ツリーを 「左 → 自分 → 右」 の順序で訪問すると、ソートされた順序で値を取得できます。これを 中位巡回 (in-order traversal) と呼びます。

python
def inorder(root):
if root is None:
return []
return inorder(root.left) + [root.value] + inorder(root.right)
print(inorder(root)) # [1, 3, 4, 6, 7, 8, 10, 13, 14]

BST のルール (左 < 親 < 右) に従って左から訪問すると、自然に昇順になります。個別のソートアルゴリズムなしで、ソートされたデータを取得することになります。


3 つの巡回

python
def preorder(root): # 前位: 自分 → 左 → 右
if root is None: return []
return [root.value] + preorder(root.left) + preorder(root.right)
def postorder(root): # 後位: 左 → 右 → 自分
if root is None: return []
return postorder(root.left) + postorder(root.right) + [root.value]
python
print(preorder(root)) # [8, 3, 1, 6, 4, 7, 10, 14, 13]
print(inorder(root)) # [1, 3, 4, 6, 7, 8, 10, 13, 14]
print(postorder(root)) # [1, 4, 7, 6, 3, 13, 14, 10, 8]
巡回順序用途
前位 (preorder)自分 → 左 → 右ツリーのコピー、シリアライズ
中位 (inorder)左 → 自分 → 右ソートされた出力
後位 (postorder)左 → 右 → 自分ツリーの削除、数式ツリーの評価

最悪の場合

BST のパフォーマンスは、ツリーのバランスによって決まります。

text
# バランスの取れたツリー (O(log n))     # 偏ったツリー (O(n)) — 実際には連結リスト
        8                    1
       / \                    \
      3   10                   2
     / \    \                   \
    1   6    14                  3
                                  \
                                   4

ソートされたデータを順番に挿入すると、偏ったツリーになります。この問題を解決するために、AVL 木Red-Black 木などの自己平衡ツリーがあります。Python の sorted()、Java の TreeMap の内部で、このような平衡ツリーを使用します。


時間計算量のまとめ

演算平均最悪 (偏り)
探索O(log n)O(n)
挿入O(log n)O(n)
削除O(log n)O(n)
巡回O(n)O(n)

BST の削除 — 3 つのケース

削除は、BST で最も複雑な演算です。

python
def delete(root, value):
if root is None:
return None
if value < root.value:
root.left = delete(root.left, value)
elif value > root.value:
root.right = delete(root.right, value)
else:
# ケース 1: 子がない (リーフ) — 単に削除
if root.left is None and root.right is None:
return None
# ケース 2: 子が 1 つ — 子がその場所を置き換え
elif root.left is None:
return root.right
elif root.right is None:
return root.left
# ケース 3: 子が 2 つ — 後続ノードで置き換え
else:
successor = find_min(root.right)
root.value = successor.value
root.right = delete(root.right, successor.value)
return root
def find_min(node):
while node.left:
node = node.left
return node

子ノードが 2 つあるノードを削除する場合、右部分木の最小値 (中位後続ノード) で置き換えます。BST のソート条件が維持されます。


実務で BST を直接使うか

Python には BST の組み込みモジュールはありません。代わりに:

python
from bisect import insort, bisect_left
sorted_list = []
insort(sorted_list, 5) # ソートを維持しながら挿入
insort(sorted_list, 3)
insort(sorted_list, 7)
print(sorted_list) # [3, 5, 7]
idx = bisect_left(sorted_list, 5) # 二分探索
print(sorted_list[idx]) # 5

bisect モジュールは、ソートされたリストで BST と同様の O(log n) 探索を提供します。挿入は O(n) (配列の移動) であるため、挿入が頻繁に行われる場合は、SortedContainers などのサードパーティライブラリを使用します。


BST vs ハッシュテーブル vs ソートされた配列

演算BST (バランス)ハッシュテーブルソートされた配列
検索O(log n)O(1) (平均)O(log n)
挿入O(log n)O(1) (平均)O(n)
削除O(log n)O(1) (平均)O(n)
最小/最大O(log n)O(n)O(1)
範囲検索O(log n + k)O(n)O(log n + k)
ソートされた出力O(n)O(n log n)O(n)

ハッシュテーブルは高速ですが、「ソートされた順序」が必要な場合は BST が有利です。たとえば、データベースのインデックスは、「100 以上 200 以下の値」のように範囲のクエリが必要な場合に BST が適しています。


BST は、「ソートされたデータを動的に維持」する必要がある場合に適しています。配列と連結リストの利点を組み合わせたデータ構造です。

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...