Files
hello-algo/ja/docs/chapter_tree/binary_search_tree.md
T
Yudong Jin 7257641e0e Retranslate the complete Japanese online edition from Chinese (#2008)
* Retranslate the complete Japanese online edition from Chinese

* Fit Japanese homepage labels and localize heading permalinks

* Point Japanese contribution instructions to the Japanese preview

* Replace locale conditionals with multilingual UI tables
2026-10-30 02:12:26 +08:00

130 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 二分探索木
下図の<u>二分探索木(binary search tree)</u>は、次の条件を満たします。
1. 根について、左の部分木のすべての値 $<$ 根の値 $<$ 右の部分木のすべての値が成り立ちます。
2. どのノードの左右の部分木も二分探索木であり、同じく条件 `1.` を満たします。
![二分探索木](binary_search_tree.assets/binary_search_tree.png)
## 二分探索木の操作
クラス `BinarySearchTree` にまとめ、根ノードを指すメンバー変数 `root` を宣言します。
### ノードの探索
目的の値 `num` が与えられたら、二分探索木の性質を使って探せます。下図のように、ノード `cur` を宣言して根 `root` から出発し、`cur.val` と `num` の大小を繰り返し比較します。
- `cur.val < num` なら、目的のノードは右の部分木にあるので、`cur = cur.right` とします。
- `cur.val > num` なら、目的のノードは左の部分木にあるので、`cur = cur.left` とします。
- `cur.val = num` なら、見つかったのでループを抜け、そのノードを返します。
=== "<1>"
![二分探索木でノードを探す例](binary_search_tree.assets/bst_search_step1.png)
=== "<2>"
![二分探索木での探索の手順 2](binary_search_tree.assets/bst_search_step2.png)
=== "<3>"
![二分探索木での探索の手順 3](binary_search_tree.assets/bst_search_step3.png)
=== "<4>"
![二分探索木での探索の手順 4](binary_search_tree.assets/bst_search_step4.png)
二分探索木の探索は、二分探索と同じく、各反復で候補の半分を除外する仕組みです。反復回数は最大で木の高さとなり、木が平衡なら $O(\log n)$ 時間です。コード例を示します。
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
```
### ノードの挿入
挿入する要素 `num` に対して、「左の部分木 < 根 < 右の部分木」という性質を保つため、下図の手順で挿入します。
1. **挿入位置を探す**:探索と同じく根から出発し、現在の値と `num` を比較しながら下へ進みます。葉を越えて `None` に達したらループを抜けます。
2. **その位置に挿入する**:値 `num` のノードを初期化し、`None` の位置へ置きます。
![二分探索木へのノードの挿入](binary_search_tree.assets/bst_insert.png)
実装では、次の二点に注意します。
- 重複するノードは定義に反するため許しません。挿入するノードがすでに存在すれば、何もせずに戻ります。
- 挿入のため、ノード `pre` に前回の反復のノードを保存します。これにより、`None` に達したときに親を取得して挿入できます。
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
```
探索と同じく、挿入には $O(\log n)$ 時間かかります。
### ノードの削除
まず目的のノードを見つけてから削除します。挿入と同じく、削除後も「左の部分木 < 根 < 右の部分木」を保つ必要があります。そのため、子の数が 0、1、2 の三つの場合に分けて処理します。
下図のように、削除するノードの次数が $0$ なら葉なので、直接削除できます。
![二分探索木のノードの削除(次数 0)](binary_search_tree.assets/bst_remove_case1.png)
下図のように、次数が $1$ なら、そのノードを子に置き換えれば済みます。
![二分探索木のノードの削除(次数 1)](binary_search_tree.assets/bst_remove_case2.png)
次数が $2$ の場合は直接削除できず、別のノードで置き換える必要があります。「左の部分木 $<$ 根 $<$ 右の部分木」を保つため、**右の部分木の最小ノードか、左の部分木の最大ノードを使えます**。
右の部分木の最小ノード、つまり通りがけ順で次のノードを選ぶとすると、手順は下図のとおりです。
1. 削除するノードの次に通りがけ順で現れるノードを探し、`tmp` とします。
2. 削除するノードの値を `tmp` の値で上書きし、木から `tmp` を再帰的に削除します。
=== "<1>"
![二分探索木のノードの削除(次数 2)](binary_search_tree.assets/bst_remove_case3_step1.png)
=== "<2>"
![次数 2 のノードの削除の手順 2](binary_search_tree.assets/bst_remove_case3_step2.png)
=== "<3>"
![次数 2 のノードの削除の手順 3](binary_search_tree.assets/bst_remove_case3_step3.png)
=== "<4>"
![次数 2 のノードの削除の手順 4](binary_search_tree.assets/bst_remove_case3_step4.png)
削除も $O(\log n)$ 時間です。削除対象の探索に $O(\log n)$、通りがけ順の後続ノードの取得に $O(\log n)$ 時間かかります。コード例を示します。
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
```
### 通りがけ順では整列済みになる
下図のように、通りがけ順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の順で進み、二分探索木では「左の子 $<$ 根 $<$ 右の子」の大小関係が成り立ちます。
そのため、通りがけ順では常に次に小さいノードを優先して訪問します。ここから、**二分探索木の通りがけ順走査の列は昇順になる**という重要な性質が得られます。
この性質を利用すれば、追加のソートなしに $O(n)$ 時間で整列済みのデータを取得でき、非常に効率的です。
![二分探索木の通りがけ順走査の列](binary_search_tree.assets/bst_inorder_traversal.png)
## 二分探索木の効率
データを配列または二分探索木で格納する場合を考えます。下表では、二分探索木の各操作の時間計算量はすべて対数時間で、安定した高い性能を示します。配列のほうが効率的なのは、追加が多く、探索と削除が少ない場合だけです。
<p align="center"> 表 <id> &nbsp; 配列と探索木の効率の比較 </p>
| | 未整列配列 | 二分探索木 |
| -------- | -------- | ----------- |
| 要素の探索 | $O(n)$ | $O(\log n)$ |
| 要素の挿入 | $O(1)$ | $O(\log n)$ |
| 要素の削除 | $O(n)$ | $O(\log n)$ |
理想的には二分探索木は平衡であり、どのノードも $\log n$ 回の反復で見つけられます。
ただし、挿入と削除を続けると、下図のように連結リストへ退化する可能性があります。その場合、各操作の時間計算量も $O(n)$ に悪化します。
![二分探索木の退化](binary_search_tree.assets/bst_degradation.png)
## 二分探索木の主な用途
- システムの多段階のインデックスとして、効率的な探索、挿入、削除を実現します。
- 一部の探索アルゴリズムの基盤となるデータ構造として使います。
- データストリームを格納し、整列状態を保ちます。