mirror of
https://github.com/krahets/hello-algo.git
synced 2026-11-02 11:15:19 +08:00
* 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
130 lines
7.0 KiB
Markdown
130 lines
7.0 KiB
Markdown
# 二分探索木
|
||
|
||
下図の<u>二分探索木(binary search tree)</u>は、次の条件を満たします。
|
||
|
||
1. 根について、左の部分木のすべての値 $<$ 根の値 $<$ 右の部分木のすべての値が成り立ちます。
|
||
2. どのノードの左右の部分木も二分探索木であり、同じく条件 `1.` を満たします。
|
||
|
||

|
||
|
||
## 二分探索木の操作
|
||
|
||
クラス `BinarySearchTree` にまとめ、根ノードを指すメンバー変数 `root` を宣言します。
|
||
|
||
### ノードの探索
|
||
|
||
目的の値 `num` が与えられたら、二分探索木の性質を使って探せます。下図のように、ノード `cur` を宣言して根 `root` から出発し、`cur.val` と `num` の大小を繰り返し比較します。
|
||
|
||
- `cur.val < num` なら、目的のノードは右の部分木にあるので、`cur = cur.right` とします。
|
||
- `cur.val > num` なら、目的のノードは左の部分木にあるので、`cur = cur.left` とします。
|
||
- `cur.val = num` なら、見つかったのでループを抜け、そのノードを返します。
|
||
|
||
=== "<1>"
|
||

|
||
|
||
=== "<2>"
|
||

|
||
|
||
=== "<3>"
|
||

|
||
|
||
=== "<4>"
|
||

|
||
|
||
二分探索木の探索は、二分探索と同じく、各反復で候補の半分を除外する仕組みです。反復回数は最大で木の高さとなり、木が平衡なら $O(\log n)$ 時間です。コード例を示します。
|
||
|
||
```src
|
||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
|
||
```
|
||
|
||
### ノードの挿入
|
||
|
||
挿入する要素 `num` に対して、「左の部分木 < 根 < 右の部分木」という性質を保つため、下図の手順で挿入します。
|
||
|
||
1. **挿入位置を探す**:探索と同じく根から出発し、現在の値と `num` を比較しながら下へ進みます。葉を越えて `None` に達したらループを抜けます。
|
||
2. **その位置に挿入する**:値 `num` のノードを初期化し、`None` の位置へ置きます。
|
||
|
||

|
||
|
||
実装では、次の二点に注意します。
|
||
|
||
- 重複するノードは定義に反するため許しません。挿入するノードがすでに存在すれば、何もせずに戻ります。
|
||
- 挿入のため、ノード `pre` に前回の反復のノードを保存します。これにより、`None` に達したときに親を取得して挿入できます。
|
||
|
||
```src
|
||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
|
||
```
|
||
|
||
探索と同じく、挿入には $O(\log n)$ 時間かかります。
|
||
|
||
### ノードの削除
|
||
|
||
まず目的のノードを見つけてから削除します。挿入と同じく、削除後も「左の部分木 < 根 < 右の部分木」を保つ必要があります。そのため、子の数が 0、1、2 の三つの場合に分けて処理します。
|
||
|
||
下図のように、削除するノードの次数が $0$ なら葉なので、直接削除できます。
|
||
|
||

|
||
|
||
下図のように、次数が $1$ なら、そのノードを子に置き換えれば済みます。
|
||
|
||

|
||
|
||
次数が $2$ の場合は直接削除できず、別のノードで置き換える必要があります。「左の部分木 $<$ 根 $<$ 右の部分木」を保つため、**右の部分木の最小ノードか、左の部分木の最大ノードを使えます**。
|
||
|
||
右の部分木の最小ノード、つまり通りがけ順で次のノードを選ぶとすると、手順は下図のとおりです。
|
||
|
||
1. 削除するノードの次に通りがけ順で現れるノードを探し、`tmp` とします。
|
||
2. 削除するノードの値を `tmp` の値で上書きし、木から `tmp` を再帰的に削除します。
|
||
|
||
=== "<1>"
|
||

|
||
|
||
=== "<2>"
|
||

|
||
|
||
=== "<3>"
|
||

|
||
|
||
=== "<4>"
|
||

|
||
|
||
削除も $O(\log n)$ 時間です。削除対象の探索に $O(\log n)$、通りがけ順の後続ノードの取得に $O(\log n)$ 時間かかります。コード例を示します。
|
||
|
||
```src
|
||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
|
||
```
|
||
|
||
### 通りがけ順では整列済みになる
|
||
|
||
下図のように、通りがけ順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の順で進み、二分探索木では「左の子 $<$ 根 $<$ 右の子」の大小関係が成り立ちます。
|
||
|
||
そのため、通りがけ順では常に次に小さいノードを優先して訪問します。ここから、**二分探索木の通りがけ順走査の列は昇順になる**という重要な性質が得られます。
|
||
|
||
この性質を利用すれば、追加のソートなしに $O(n)$ 時間で整列済みのデータを取得でき、非常に効率的です。
|
||
|
||

|
||
|
||
## 二分探索木の効率
|
||
|
||
データを配列または二分探索木で格納する場合を考えます。下表では、二分探索木の各操作の時間計算量はすべて対数時間で、安定した高い性能を示します。配列のほうが効率的なのは、追加が多く、探索と削除が少ない場合だけです。
|
||
|
||
<p align="center"> 表 <id> 配列と探索木の効率の比較 </p>
|
||
|
||
| | 未整列配列 | 二分探索木 |
|
||
| -------- | -------- | ----------- |
|
||
| 要素の探索 | $O(n)$ | $O(\log n)$ |
|
||
| 要素の挿入 | $O(1)$ | $O(\log n)$ |
|
||
| 要素の削除 | $O(n)$ | $O(\log n)$ |
|
||
|
||
理想的には二分探索木は平衡であり、どのノードも $\log n$ 回の反復で見つけられます。
|
||
|
||
ただし、挿入と削除を続けると、下図のように連結リストへ退化する可能性があります。その場合、各操作の時間計算量も $O(n)$ に悪化します。
|
||
|
||

|
||
|
||
## 二分探索木の主な用途
|
||
|
||
- システムの多段階のインデックスとして、効率的な探索、挿入、削除を実現します。
|
||
- 一部の探索アルゴリズムの基盤となるデータ構造として使います。
|
||
- データストリームを格納し、整列状態を保ちます。
|