mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-12 07:26:07 +00:00
Re-translate the Japanese version (#1871)
* Retranslate Japanese docs with GPT-5.4 * Retranslate Japanese code with GPT-5.4
This commit is contained in:
@@ -1,34 +1,34 @@
|
||||
# 二分木の配列表現
|
||||
|
||||
連結リスト表現では、二分木の格納単位はノード`TreeNode`であり、ノードはポインタによって接続されます。連結リスト表現での二分木の基本操作については前の節で紹介しました。
|
||||
連結リスト表現では、二分木の記憶単位はノード `TreeNode` であり、ノード同士はポインタによって接続されます。前節では、連結リスト表現における二分木の各種基本操作を紹介しました。
|
||||
|
||||
では、配列を使って二分木を表現することはできるでしょうか?答えはイエスです。
|
||||
では、配列で二分木を表現できるでしょうか?答えはもちろん可能です。
|
||||
|
||||
## 完全二分木の表現
|
||||
## 充足二分木を表現する
|
||||
|
||||
まず簡単なケースから分析してみましょう。完全二分木が与えられたとき、レベル順探索の順序に従ってすべてのノードを配列に格納し、各ノードは一意の配列インデックスに対応します。
|
||||
まずは簡単な例を考えます。与えられた 1 本の充足二分木について、すべてのノードをレベル順走査の順に配列へ格納すると、各ノードは一意な配列インデックスに対応します。
|
||||
|
||||
レベル順探索の特性に基づいて、親ノードのインデックスとその子ノードの間の「マッピング公式」を導き出すことができます:**ノードのインデックスが$i$の場合、その左の子のインデックスは$2i + 1$、右の子のインデックスは$2i + 2$です**。下図は、さまざまなノードのインデックス間のマッピング関係を示しています。
|
||||
レベル順走査の性質に基づくと、親ノードのインデックスと子ノードのインデックスの間にある「対応式」を導けます。**あるノードのインデックスが $i$ なら、その左子ノードのインデックスは $2i + 1$ 、右子ノードのインデックスは $2i + 2$ です**。以下の図は、各ノードインデックス間の対応関係を示しています。
|
||||
|
||||

|
||||

|
||||
|
||||
**マッピング公式は、連結リストのノード参照(ポインタ)と同様の役割を果たします**。配列内の任意のノードが与えられたとき、マッピング公式を使用してその左(右)の子ノードにアクセスできます。
|
||||
**対応式は、連結リストにおけるノード参照(ポインタ)と同じ役割を果たします**。与えられた配列内の任意のノードについて、この対応式を使えばその左(右)子ノードにアクセスできます。
|
||||
|
||||
## 任意の二分木の表現
|
||||
## 任意の二分木を表現する
|
||||
|
||||
完全二分木は特別なケースです。二分木の中間レベルには多くの`None`値が存在することがよくあります。レベル順探索のシーケンスにはこれらの`None`値が含まれないため、このシーケンスだけに依存して`None`値の数と分布を推測することはできません。**つまり、複数の二分木構造が同じレベル順探索シーケンスと一致する可能性があります**。
|
||||
充足二分木は特殊なケースであり、一般の二分木では中間層に多数の `None` が存在することがよくあります。レベル順走査の列にはこれらの `None` が含まれないため、その列だけから `None` の数や分布位置を推定することはできません。**つまり、このレベル順走査列に一致する二分木構造は複数存在し得ます**。
|
||||
|
||||
下図に示すように、完全でない二分木が与えられた場合、上記の配列表現方法は失敗します。
|
||||
次の図のように、非充足二分木が与えられると、上記の配列表現はすでに成り立ちません。
|
||||
|
||||

|
||||

|
||||
|
||||
この問題を解決するために、**レベル順探索シーケンスですべての`None`値を明示的に書き出すことを検討できます**。下図に示すように、この処理後、レベル順探索シーケンスは二分木を一意に表現できます。サンプルコードは以下の通りです:
|
||||
この問題を解決するために、**レベル順走査列にすべての `None` を明示的に書き込む**ことを考えられます。次の図のように、このように処理すればレベル順走査列で二分木を一意に表現できます。コード例は以下のとおりです:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
# 二分木の配列表現
|
||||
# Noneを使用して空のスロットを表現
|
||||
# 空き位置を表すために None を使う
|
||||
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
```cpp title=""
|
||||
/* 二分木の配列表現 */
|
||||
// 最大整数値INT_MAXを使用して空のスロットをマーク
|
||||
// int の最大値 INT_MAX を使って空き位置を示す
|
||||
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
|
||||
```
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
```java title=""
|
||||
/* 二分木の配列表現 */
|
||||
// Integerラッパークラスを使用してnullで空のスロットをマーク
|
||||
// int のラッパークラス Integer を使えば、null で空き位置を示せる
|
||||
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
|
||||
```
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
```csharp title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullable int (int?)を使用してnullで空のスロットをマーク
|
||||
// nullable な int? 型を使えば、null で空き位置を示せる
|
||||
int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
```go title=""
|
||||
/* 二分木の配列表現 */
|
||||
// any型スライスを使用してnilで空のスロットをマーク
|
||||
// any 型のスライスを使えば、nil で空き位置を示せる
|
||||
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
|
||||
```
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
```swift title=""
|
||||
/* 二分木の配列表現 */
|
||||
// optional Int (Int?)を使用してnilで空のスロットをマーク
|
||||
// nullable な Int? 型を使えば、nil で空き位置を示せる
|
||||
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
|
||||
```
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
```javascript title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullを使用して空のスロットを表現
|
||||
// null を使って空き位置を表す
|
||||
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
```typescript title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullを使用して空のスロットを表現
|
||||
// null を使って空き位置を表す
|
||||
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
```dart title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullable int (int?)を使用してnullで空のスロットをマーク
|
||||
// nullable な int? 型を使えば、null で空き位置を示せる
|
||||
List<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
```rust title=""
|
||||
/* 二分木の配列表現 */
|
||||
// Noneを使用して空のスロットをマーク
|
||||
// None を使って空き位置を示す
|
||||
let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
```c title=""
|
||||
/* 二分木の配列表現 */
|
||||
// 最大int値を使用して空のスロットをマーク、したがってノード値はINT_MAXであってはならない
|
||||
// int の最大値で空き位置を示すため、ノード値は INT_MAX であってはならない
|
||||
int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
|
||||
```
|
||||
|
||||
@@ -116,43 +116,45 @@
|
||||
|
||||
```kotlin title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullを使用して空のスロットを表現
|
||||
val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
|
||||
// null を使って空き位置を表す
|
||||
val tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
|
||||
### 二分木の配列表現 ###
|
||||
# nil を使って空き位置を表す
|
||||
tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
注目すべきは、**完備二分木は配列表現に非常に適している**ということです。完備二分木の定義を思い出すと、`None`は最下位レベルでのみ、かつ右側に向かって現れます。**つまり、すべての`None`値は確実にレベル順探索シーケンスの最後に現れます**。
|
||||
補足すると、**完全二分木は配列による表現に非常に適しています**。完全二分木の定義を振り返ると、`None` は最下層の右側にしか現れないため、**すべての `None` は必ずレベル順走査列の末尾に現れます**。
|
||||
|
||||
これは、配列を使用して完備二分木を表現する際、すべての`None`値の格納を省略できることを意味し、非常に便利です。下図に例を示します。
|
||||
つまり、完全二分木を配列で表す場合は、すべての `None` の格納を省略できるため、非常に便利です。次の図に例を示します。
|
||||
|
||||

|
||||

|
||||
|
||||
以下のコードは、配列表現に基づく二分木を実装し、次の操作を含みます:
|
||||
以下のコードでは、配列ベースで表現した二分木を実装しており、次の操作を含みます。
|
||||
|
||||
- ノードが与えられたとき、その値、左(右)の子ノード、および親ノードを取得する。
|
||||
- 前順、中順、後順、およびレベル順探索シーケンスを取得する。
|
||||
- あるノードが与えられたとき、その値、左(右)子ノード、親ノードを取得する。
|
||||
- 前順走査、中順走査、後順走査、レベル順走査の列を取得する。
|
||||
|
||||
```src
|
||||
[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{}
|
||||
```
|
||||
|
||||
## 利点と制限
|
||||
## 利点と制約
|
||||
|
||||
二分木の配列表現には以下の利点があります:
|
||||
二分木の配列表現には主に次の利点があります。
|
||||
|
||||
- 配列は連続したメモリ空間に格納されるため、キャッシュフレンドリーで、より高速なアクセスと探索が可能です。
|
||||
- ポインタを格納する必要がないため、スペースを節約できます。
|
||||
- ノードへのランダムアクセスが可能です。
|
||||
- 配列は連続したメモリ空間に格納されるため、キャッシュ効率が高く、アクセスと走査が速い。
|
||||
- ポインタを格納する必要がなく、比較的省スペースである。
|
||||
- ノードへのランダムアクセスが可能である。
|
||||
|
||||
しかし、配列表現にはいくつかの制限もあります:
|
||||
ただし、配列表現にはいくつかの制約もあります。
|
||||
|
||||
- 配列格納には連続したメモリ空間が必要なため、大量のデータを持つ木の格納には適していません。
|
||||
- ノードの追加や削除には配列の挿入や削除操作が必要で、効率が低くなります。
|
||||
- 二分木に多くの`None`値がある場合、配列に含まれるノードデータの割合が低くなり、空間利用率が低下します。
|
||||
- 配列による格納には連続したメモリ空間が必要なため、データ量が大きすぎる木の格納には向かない。
|
||||
- ノードの追加と削除は配列の挿入・削除操作で実現する必要があり、効率は低い。
|
||||
- 二分木に大量の `None` が存在すると、配列に占める実ノードデータの比率が低くなり、空間利用率も低下する。
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# AVL木 *
|
||||
# AVL 木 *
|
||||
|
||||
「二分探索木」の節では、複数の挿入と削除の後、二分探索木が連結リストに退化する可能性があることを述べました。このような場合、すべての操作の時間計算量が$O(\log n)$から$O(n)$に悪化します。
|
||||
「二分探索木」章で述べたように、挿入と削除を何度も繰り返すと、二分探索木は連結リストへ退化する可能性があります。この場合、すべての操作の時間計算量は $O(\log n)$ から $O(n)$ へ劣化します。
|
||||
|
||||
下図に示すように、2つのノード削除操作の後、この二分探索木は連結リストに退化します。
|
||||
以下の図に示すように、ノード削除を 2 回行うと、この二分探索木は連結リストへ退化します。
|
||||
|
||||

|
||||

|
||||
|
||||
例えば、下図に示す完全二分木では、2つのノードを挿入した後、木が左に大きく傾き、検索操作の時間計算量も悪化します。
|
||||
別の例として、以下の図に示す完全二分木に 2 つのノードを挿入すると、木は大きく左に傾き、探索操作の時間計算量もそれに伴って劣化します。
|
||||
|
||||

|
||||

|
||||
|
||||
1962年、G. M. Adelson-VelskyとE. M. Landisが論文「An algorithm for the organization of information」で<u>AVL木</u>を提案しました。この論文では、ノードの継続的な追加と削除の後もAVL木が退化しないことを保証する一連の操作について詳述し、さまざまな操作の時間計算量を$O(\log n)$レベルに維持しました。つまり、頻繁な追加、削除、検索、変更が必要なシナリオで、AVL木は常に効率的なデータ操作性能を維持でき、大きな応用価値があります。
|
||||
1962 年、G. M. Adelson-Velsky と E. M. Landis は論文“An algorithm for the organization of information”の中で <u>AVL 木</u> を提案しました。論文では一連の操作が詳しく説明されており、ノードの追加と削除を続けても AVL 木が退化しないようにして、各種操作の時間計算量を $O(\log n)$ の水準に保ちます。言い換えると、追加・削除・探索・更新を頻繁に行う場面でも、AVL 木は常に高いデータ操作性能を維持でき、実用価値の高い構造です。
|
||||
|
||||
## AVL木の一般的な用語
|
||||
## AVL 木の基本用語
|
||||
|
||||
AVL木は二分探索木でありかつ平衡二分木でもあり、これら2つの種類の二分木のすべての性質を満たしているため、<u>平衡二分探索木</u>です。
|
||||
AVL 木は二分探索木であると同時に平衡二分木でもあり、これら 2 種類の二分木の性質をすべて満たします。したがって、<u>平衡二分探索木(balanced binary search tree)</u>の一種です。
|
||||
|
||||
### ノードの高さ
|
||||
|
||||
AVL木に関連する操作ではノードの高さを取得する必要があるため、ノードクラスに`height`変数を追加する必要があります:
|
||||
AVL 木の操作ではノードの高さを取得する必要があるため、ノードクラスに `height` 変数を追加します:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class TreeNode:
|
||||
"""AVL木ノード"""
|
||||
"""AVL 木ノードクラス"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # ノード値
|
||||
self.height: int = 0 # ノードの高さ
|
||||
self.left: TreeNode | None = None # 左の子への参照
|
||||
self.right: TreeNode | None = None # 右の子への参照
|
||||
self.left: TreeNode | None = None # 左の子ノード参照
|
||||
self.right: TreeNode | None = None # 右の子ノード参照
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
struct TreeNode {
|
||||
int val{}; // ノード値
|
||||
int height = 0; // ノードの高さ
|
||||
TreeNode *left{}; // 左の子
|
||||
TreeNode *right{}; // 右の子
|
||||
TreeNode *left{}; // 左の子ノード
|
||||
TreeNode *right{}; // 右の子ノード
|
||||
TreeNode() = default;
|
||||
explicit TreeNode(int x) : val(x){}
|
||||
};
|
||||
@@ -49,12 +49,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
class TreeNode {
|
||||
public int val; // ノード値
|
||||
public int height; // ノードの高さ
|
||||
public TreeNode left; // 左の子
|
||||
public TreeNode right; // 右の子
|
||||
public TreeNode left; // 左の子ノード
|
||||
public TreeNode right; // 右の子ノード
|
||||
public TreeNode(int x) { val = x; }
|
||||
}
|
||||
```
|
||||
@@ -62,36 +62,36 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
class TreeNode(int? x) {
|
||||
public int? val = x; // ノード値
|
||||
public int height; // ノードの高さ
|
||||
public TreeNode? left; // 左の子への参照
|
||||
public TreeNode? right; // 右の子への参照
|
||||
public TreeNode? left; // 左の子ノード参照
|
||||
public TreeNode? right; // 右の子ノード参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノード構造体 */
|
||||
type TreeNode struct {
|
||||
Val int // ノード値
|
||||
Height int // ノードの高さ
|
||||
Left *TreeNode // 左の子への参照
|
||||
Right *TreeNode // 右の子への参照
|
||||
Left *TreeNode // 左の子ノード参照
|
||||
Right *TreeNode // 右の子ノード参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
class TreeNode {
|
||||
var val: Int // ノード値
|
||||
var height: Int // ノードの高さ
|
||||
var left: TreeNode? // 左の子
|
||||
var right: TreeNode? // 右の子
|
||||
var left: TreeNode? // 左の子ノード
|
||||
var right: TreeNode? // 右の子ノード
|
||||
|
||||
init(x: Int) {
|
||||
val = x
|
||||
@@ -103,12 +103,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
class TreeNode {
|
||||
val; // ノード値
|
||||
height; // ノードの高さ
|
||||
left; // 左の子ポインタ
|
||||
right; // 右の子ポインタ
|
||||
height; //ノードの高さ
|
||||
left; // 左の子ノードポインタ
|
||||
right; // 右の子ノードポインタ
|
||||
constructor(val, left, right, height) {
|
||||
this.val = val === undefined ? 0 : val;
|
||||
this.height = height === undefined ? 0 : height;
|
||||
@@ -121,12 +121,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
class TreeNode {
|
||||
val: number; // ノード値
|
||||
height: number; // ノードの高さ
|
||||
left: TreeNode | null; // 左の子ポインタ
|
||||
right: TreeNode | null; // 右の子ポインタ
|
||||
left: TreeNode | null; // 左の子ノードポインタ
|
||||
right: TreeNode | null; // 右の子ノードポインタ
|
||||
constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {
|
||||
this.val = val === undefined ? 0 : val;
|
||||
this.height = height === undefined ? 0 : height;
|
||||
@@ -139,12 +139,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
class TreeNode {
|
||||
int val; // ノード値
|
||||
int height; // ノードの高さ
|
||||
TreeNode? left; // 左の子
|
||||
TreeNode? right; // 右の子
|
||||
TreeNode? left; // 左の子ノード
|
||||
TreeNode? right; // 右の子ノード
|
||||
TreeNode(this.val, [this.height = 0, this.left, this.right]);
|
||||
}
|
||||
```
|
||||
@@ -155,12 +155,12 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノード構造体 */
|
||||
struct TreeNode {
|
||||
val: i32, // ノード値
|
||||
height: i32, // ノードの高さ
|
||||
left: Option<Rc<RefCell<TreeNode>>>, // 左の子
|
||||
right: Option<Rc<RefCell<TreeNode>>>, // 右の子
|
||||
left: Option<Rc<RefCell<TreeNode>>>, // 左の子ノード
|
||||
right: Option<Rc<RefCell<TreeNode>>>, // 右の子ノード
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
@@ -179,8 +179,8 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* AVL木ノード */
|
||||
TreeNode struct TreeNode {
|
||||
/* AVL 木ノード構造体 */
|
||||
typedef struct TreeNode {
|
||||
int val;
|
||||
int height;
|
||||
struct TreeNode *left;
|
||||
@@ -203,29 +203,40 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* AVL木ノード */
|
||||
/* AVL 木ノードクラス */
|
||||
class TreeNode(val _val: Int) { // ノード値
|
||||
val height: Int = 0 // ノードの高さ
|
||||
val left: TreeNode? = null // 左の子
|
||||
val right: TreeNode? = null // 右の子
|
||||
val left: TreeNode? = null // 左の子ノード
|
||||
val right: TreeNode? = null // 右の子ノード
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
### AVL 木ノードクラス ###
|
||||
class TreeNode
|
||||
attr_accessor :val # ノード値
|
||||
attr_accessor :height # ノードの高さ
|
||||
attr_accessor :left # 左の子ノード参照
|
||||
attr_accessor :right # 右の子ノード参照
|
||||
|
||||
def initialize(val)
|
||||
@val = val
|
||||
@height = 0
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、つまり通過する「辺」の数を指します。重要なのは、葉ノードの高さは$0$で、nullノードの高さは$-1$であることです。ノードの高さを取得し、更新するための2つのユーティリティ関数を作成します:
|
||||
「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、すなわち通過する「辺」の本数を指します。特に、葉ノードの高さは $0$、空ノードの高さは $-1$ です。ここでは、ノードの高さを取得・更新するための 2 つの補助関数を用意します:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{update_height}
|
||||
```
|
||||
|
||||
### ノードの平衡因子
|
||||
### ノードの平衡係数
|
||||
|
||||
ノードの<u>平衡因子</u>は、そのノードの左部分木の高さから右部分木の高さを引いた値として定義され、nullノードの平衡因子は$0$として定義されます。後で使いやすくするため、ノードの平衡因子を取得する機能も関数にカプセル化します:
|
||||
ノードの<u>平衡係数(balance factor)</u>は、左部分木の高さから右部分木の高さを引いた値と定義し、空ノードの平衡係数は $0$ とします。同様に、ノードの平衡係数を取得する機能も関数にカプセル化して、後続で使いやすくします:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{balance_factor}
|
||||
@@ -233,17 +244,17 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
|
||||
!!! tip
|
||||
|
||||
平衡因子を$f$とすると、AVL木の任意のノードの平衡因子は$-1 \le f \le 1$を満たします。
|
||||
平衡係数を $f$ とすると、AVL 木の任意のノードの平衡係数は常に $-1 \le f \le 1$ を満たします。
|
||||
|
||||
## AVL木の回転
|
||||
## AVL 木の回転
|
||||
|
||||
AVL木の特徴的な機能は「回転」操作で、これは二分木の中順探索シーケンスに影響を与えることなく、不平衡なノードのバランスを回復できます。つまり、**回転操作は「二分探索木」の性質を維持しながら、木を「平衡二分木」に戻すことができます**。
|
||||
AVL 木の特徴は「回転」操作にあり、二分木の中順走査列を変えずに、不平衡ノードを再び平衡に戻せます。言い換えると、**回転操作は「二分探索木」の性質を保ちながら、木を再び「平衡二分木」に戻すことができます**。
|
||||
|
||||
絶対平衡因子が$> 1$のノードを「不平衡ノード」と呼びます。不平衡のタイプに応じて、4種類の回転があります:右回転、左回転、右左回転、左右回転です。以下、これらの回転操作について詳しく説明します。
|
||||
平衡係数の絶対値が $> 1$ のノードを「不平衡ノード」と呼びます。ノードの不平衡の形に応じて、回転操作は 4 種類に分かれます。右回転、左回転、右回転してから左回転、左回転してから右回転です。以下でこれらを順に説明します。
|
||||
|
||||
### 右回転
|
||||
|
||||
下図に示すように、二分木で下から上への最初の不平衡ノードは「ノード3」です。この不平衡ノードを根とする部分木に焦点を当て、これを`node`とし、その左の子を`child`として、「右回転」を実行します。右回転後、部分木は再びバランスが取れ、同時に二分探索木の性質も維持されます。
|
||||
以下の図では、ノードの下に平衡係数を示しています。下から上へ見ると、二分木で最初に不平衡になるのは「ノード 3」です。この不平衡ノードを根とする部分木に注目し、そのノードを `node`、左の子ノードを `child` として、「右回転」を行います。右回転後、部分木は平衡を回復し、なおかつ二分探索木の性質も保たれます。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -257,11 +268,11 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中
|
||||
=== "<4>"
|
||||

|
||||
|
||||
下図に示すように、`child`ノードに右の子(`grand_child`と表記)がある場合、右回転で手順を追加する必要があります:`grand_child`を`node`の左の子に設定します。
|
||||
以下の図に示すように、ノード `child` に右の子ノード(`grand_child` と記す)がある場合、右回転には 1 ステップ追加する必要があります。すなわち、`grand_child` を `node` の左の子ノードにします。
|
||||
|
||||

|
||||

|
||||
|
||||
「右回転」は比喩的な用語で、実際にはノードポインタを変更することで実現されます。以下のコードで示されます:
|
||||
「右に回転する」というのはあくまでイメージしやすい表現であり、実際にはノードポインタを変更して実現します。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{right_rotate}
|
||||
@@ -269,60 +280,60 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中
|
||||
|
||||
### 左回転
|
||||
|
||||
対応して、上記の不平衡二分木の「鏡像」を考慮すると、下図に示す「左回転」操作を実行する必要があります。
|
||||
対応する鏡像として、上記の不平衡二分木を左右反転して考えると、以下の図に示す「左回転」が必要になります。
|
||||
|
||||

|
||||

|
||||
|
||||
同様に、下図に示すように、`child`ノードに左の子(`grand_child`と表記)がある場合、左回転で手順を追加する必要があります:`grand_child`を`node`の右の子に設定します。
|
||||
同様に、以下の図に示すように、ノード `child` に左の子ノード(`grand_child` と記す)がある場合、左回転にも 1 ステップ追加する必要があります。すなわち、`grand_child` を `node` の右の子ノードにします。
|
||||
|
||||

|
||||

|
||||
|
||||
**右回転と左回転の操作は論理的に対称であり、2つの対称的な不平衡タイプを解決します**ことが観察できます。対称性に基づいて、右回転の実装コードですべての`left`を`right`に、すべての`right`を`left`に置き換えることで、左回転の実装コードを得ることができます:
|
||||
分かるように、**右回転と左回転は論理的に鏡像対称であり、それぞれが解決する 2 種類の不平衡も対称です**。この対称性に基づけば、右回転の実装コードにあるすべての `left` を `right` に、すべての `right` を `left` に置き換えるだけで、左回転の実装コードが得られます:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{left_rotate}
|
||||
```
|
||||
|
||||
### 左右回転
|
||||
### 左回転してから右回転
|
||||
|
||||
下図に示す不平衡ノード3の場合、左回転または右回転のいずれかだけでは部分木のバランスを回復できません。この場合、まず`child`に対して「左回転」を実行し、次に`node`に対して「右回転」を実行する必要があります。
|
||||
以下の図の不平衡ノード 3 では、左回転だけでも右回転だけでも部分木を平衡に戻せません。この場合は、まず `child` に「左回転」を行い、次に `node` に「右回転」を行います。
|
||||
|
||||

|
||||

|
||||
|
||||
### 右左回転
|
||||
### 右回転してから左回転
|
||||
|
||||
下図に示すように、上記の不平衡二分木の鏡像ケースでは、まず`child`に対して「右回転」を実行し、次に`node`に対して「左回転」を実行する必要があります。
|
||||
以下の図に示すように、上記の不平衡二分木の鏡像のケースでは、まず `child` に「右回転」を行い、次に `node` に「左回転」を行います。
|
||||
|
||||

|
||||

|
||||
|
||||
### 回転の選択
|
||||
|
||||
下図に示す4種類の不平衡は、それぞれ上記で説明したケースに対応し、右回転、左右回転、右左回転、左回転が必要です。
|
||||
以下の図に示す 4 種類の不平衡は、上の各ケースにそれぞれ対応しており、必要な操作は順に右回転、左回転してから右回転、右回転してから左回転、左回転です。
|
||||
|
||||

|
||||

|
||||
|
||||
下表に示すように、不平衡ノードの平衡因子とその高い側の子の平衡因子の符号を判断することで、不平衡ノードが上記のどのケースに属するかを決定します。
|
||||
以下の表に示すように、不平衡ノードの平衡係数と、高い側の子ノードの平衡係数の符号を判定することで、その不平衡ノードが上図のどのケースに属するかを判断できます。
|
||||
|
||||
<p align="center"> 表 <id> 4つの回転ケースの選択条件 </p>
|
||||
<p align="center"> 表 <id> 4 種類の回転ケースの選択条件 </p>
|
||||
|
||||
| 不平衡ノードの平衡因子 | 子ノードの平衡因子 | 使用する回転方法 |
|
||||
| --------------------- | ----------------- | --------------------------- |
|
||||
| $> 1$(左に傾いた木) | $\geq 0$ | 右回転 |
|
||||
| $> 1$(左に傾いた木) | $<0$ | 左回転してから右回転 |
|
||||
| $< -1$(右に傾いた木) | $\leq 0$ | 左回転 |
|
||||
| $< -1$(右に傾いた木) | $>0$ | 右回転してから左回転 |
|
||||
| 不平衡ノードの平衡係数 | 子ノードの平衡係数 | 採用すべき回転方法 |
|
||||
| ------------------ | ---------------- | ---------------- |
|
||||
| $> 1$ (左に偏った木) | $\geq 0$ | 右回転 |
|
||||
| $> 1$ (左に偏った木) | $<0$ | 左回転してから右回転 |
|
||||
| $< -1$ (右に偏った木) | $\leq 0$ | 左回転 |
|
||||
| $< -1$ (右に偏った木) | $>0$ | 右回転してから左回転 |
|
||||
|
||||
便宜上、回転操作を関数にカプセル化します。**この関数により、さまざまな種類の不平衡に対して回転を実行し、不平衡ノードのバランスを回復できます**。コードは以下の通りです:
|
||||
使いやすくするために、回転操作を 1 つの関数にカプセル化します。**この関数があれば、さまざまな不平衡ケースに対して回転を行い、不平衡ノードを再び平衡に戻せます**。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{rotate}
|
||||
```
|
||||
|
||||
## AVL木の一般的な操作
|
||||
## AVL 木の基本操作
|
||||
|
||||
### ノードの挿入
|
||||
|
||||
AVL木のノード挿入操作は二分探索木のそれと似ています。唯一の違いは、AVL木でノードを挿入した後、そのノードから根ノードまでのパス上に一連の不平衡ノードが現れる可能性があることです。したがって、**このノードから始めて上向きに回転操作を実行し、すべての不平衡ノードのバランスを回復する必要があります**。コードは以下の通りです:
|
||||
AVL 木のノード挿入は、基本的には二分探索木と同じです。唯一の違いは、AVL 木ではノード挿入後に、そのノードから根ノードまでの経路上に複数の不平衡ノードが現れる可能性があることです。したがって、**このノードから開始して、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります**。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{insert_helper}
|
||||
@@ -330,18 +341,18 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。
|
||||
|
||||
### ノードの削除
|
||||
|
||||
同様に、二分探索木でのノード削除方法に基づいて、下から上へ回転操作を実行してすべての不平衡ノードのバランスを回復する必要があります。コードは以下の通りです:
|
||||
同様に、二分探索木のノード削除メソッドを土台として、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{remove_helper}
|
||||
```
|
||||
|
||||
### ノードの検索
|
||||
### ノードの探索
|
||||
|
||||
AVL木でのノード検索操作は二分探索木のそれと一致しており、ここでは詳述しません。
|
||||
AVL 木のノード探索操作は二分探索木と同じなので、ここでは繰り返しません。
|
||||
|
||||
## AVL木の典型的な応用
|
||||
## AVL 木の代表的な応用
|
||||
|
||||
- 大量のデータの整理と格納に使用され、検索頻度が高く、挿入と削除の頻度が低いシナリオに適しています。
|
||||
- データベースのインデックスシステムの構築に使用されます。
|
||||
- 赤黒木も一般的な平衡二分探索木の一種です。AVL木と比較して、赤黒木はより緩い平衡条件を持ち、ノードの挿入と削除にかかる回転数が少なく、ノードの追加と削除操作の平均効率が高くなります。
|
||||
- 大規模データの整理・格納に用いられ、高頻度の探索と低頻度の追加・削除に適しています。
|
||||
- データベースのインデックスシステムの構築に使われます。
|
||||
- 赤黒木も代表的な平衡二分探索木の一つです。AVL 木と比べると、赤黒木は平衡条件がより緩く、ノードの挿入・削除に必要な回転操作が少ないため、平均的な更新効率はより高くなります。
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# 二分探索木
|
||||
|
||||
下図に示すように、<u>二分探索木</u>は以下の条件を満たします。
|
||||
以下の図に示すように、<u>二分探索木(binary search tree)</u>は次の条件を満たします。
|
||||
|
||||
1. 根ノードについて、左部分木のすべてのノードの値 $<$ 根ノードの値 $<$ 右部分木のすべてのノードの値。
|
||||
2. 任意のノードの左と右の部分木も二分探索木です。つまり、条件`1.`も満たします。
|
||||
2. 任意のノードの左部分木と右部分木も二分探索木であり、すなわち条件 `1.` も満たします。
|
||||
|
||||

|
||||
|
||||
## 二分探索木の操作
|
||||
|
||||
二分探索木をクラス`BinarySearchTree`としてカプセル化し、木の根ノードを指すメンバー変数`root`を宣言します。
|
||||
二分探索木をクラス `BinarySearchTree` としてカプセル化し、木の根ノードを指すメンバ変数 `root` を宣言します。
|
||||
|
||||
### ノードの検索
|
||||
### ノードの探索
|
||||
|
||||
ターゲットノード値`num`が与えられた場合、二分探索木の性質に従って検索できます。下図に示すように、ノード`cur`を宣言し、二分木の根ノード`root`から開始し、ノード値`cur.val`と`num`のサイズを比較するループを行います。
|
||||
目標ノードの値 `num` が与えられたら、二分探索木の性質に基づいて探索できます。以下の図に示すように、ノード `cur` を宣言し、二分木の根ノード `root` から出発して、ノード値 `cur.val` と `num` の大小関係を繰り返し比較します。
|
||||
|
||||
- `cur.val < num`の場合、ターゲットノードは`cur`の右部分木にあることを意味するため、`cur = cur.right`を実行します。
|
||||
- `cur.val > num`の場合、ターゲットノードは`cur`の左部分木にあることを意味するため、`cur = cur.left`を実行します。
|
||||
- `cur.val = num`の場合、ターゲットノードが見つかったことを意味するため、ループを終了してノードを返します。
|
||||
- `cur.val < num` の場合、目標ノードは `cur` の右部分木にあるため、`cur = cur.right` を実行します。
|
||||
- `cur.val > num` の場合、目標ノードは `cur` の左部分木にあるため、`cur = cur.left` を実行します。
|
||||
- `cur.val = num` の場合、目標ノードが見つかったことを表し、ループを抜けてそのノードを返します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

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

|
||||
@@ -31,7 +31,7 @@
|
||||
=== "<4>"
|
||||

|
||||
|
||||
二分探索木での検索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドでケースの半分を排除します。ループ数は最大で二分木の高さです。二分木が平衡している場合、$O(\log n)$の時間を使用します。コード例は以下の通りです:
|
||||
二分探索木の探索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドで半分の候補を除外します。ループ回数の上限は二分木の高さであり、二分木が平衡であれば $O(\log n)$ 時間です。コード例は次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
|
||||
@@ -39,45 +39,45 @@
|
||||
|
||||
### ノードの挿入
|
||||
|
||||
挿入する要素`num`が与えられた場合、二分探索木の性質「左部分木 < 根ノード < 右部分木」を維持するため、挿入操作は下図に示すように進行します。
|
||||
挿入する要素 `num` が与えられたとき、二分探索木の「左部分木 < 根ノード < 右部分木」という性質を保つため、挿入操作の流れは以下の図のようになります。
|
||||
|
||||
1. **挿入位置を見つける**: 検索操作と同様に、根ノードから開始し、現在のノード値と`num`のサイズ関係に従って下向きにループし、葉ノードを通過(`None`に走査)するまで、ループを終了します。
|
||||
2. **この位置にノードを挿入**: ノード`num`を初期化し、`None`があった場所に配置します。
|
||||
1. **挿入位置を探索する**:探索操作と同様に、根ノードから出発し、現在のノード値と `num` の大小関係に基づいて下方向へ探索を繰り返し、葉ノードを越えて(`None` まで到達して)ループを抜けます。
|
||||
2. **その位置にノードを挿入する**:ノード `num` を初期化し、そのノードを `None` の位置に置きます。
|
||||
|
||||

|
||||

|
||||
|
||||
コード実装では、以下の2点に注意してください。
|
||||
コード実装では、次の 2 点に注意が必要です。
|
||||
|
||||
- 二分探索木は重複ノードの存在を許可しません。そうでなければ、その定義に違反します。したがって、挿入するノードが既に木に存在する場合、挿入は実行されず、ノードは直接戻ります。
|
||||
- 挿入操作を実行するには、前のループからのノードを保存するためにノード`pre`を使用する必要があります。このようにして、`None`に走査したときに、その親ノードを取得でき、ノード挿入操作を完了できます。
|
||||
- 二分探索木では重複ノードを許可しません。そうでないと定義に反するためです。したがって、挿入対象のノードが木内にすでに存在する場合は、挿入を行わずそのまま返します。
|
||||
- ノード挿入を実現するために、ノード `pre` を用いて前回のループのノードを保持する必要があります。これにより、`None` までたどり着いたときにその親ノードを取得でき、ノード挿入を完了できます。
|
||||
|
||||
```src
|
||||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
|
||||
```
|
||||
|
||||
ノードの検索と同様に、ノードの挿入には$O(\log n)$の時間を使用します。
|
||||
ノード探索と同様に、ノード挿入には $O(\log n)$ 時間を要します。
|
||||
|
||||
### ノードの削除
|
||||
|
||||
まず、二分木でターゲットノードを見つけ、それを削除します。ノードの挿入と同様に、削除操作が完了した後も、二分探索木の性質「左部分木 < 根ノード < 右部分木」が満たされることを保証する必要があります。したがって、ターゲットノードの子ノード数に基づいて、0、1、2の3つのケースに分け、対応するノード削除操作を実行します。
|
||||
まず二分木内で目標ノードを見つけ、その後で削除します。ノード挿入と同様に、削除操作の完了後も二分探索木の「左部分木 < 根ノード < 右部分木」という性質が保たれる必要があります。そのため、目標ノードの子ノード数に応じて、0、1、2 の 3 つのケースに分けて対応する削除操作を行います。
|
||||
|
||||
下図に示すように、削除するノードの次数が$0$の場合、そのノードは葉ノードであることを意味し、直接削除できます。
|
||||
以下の図に示すように、削除対象ノードの次数が $0$ のとき、そのノードは葉ノードであり、直接削除できます。
|
||||
|
||||

|
||||

|
||||
|
||||
下図に示すように、削除するノードの次数が$1$の場合、削除するノードをその子ノードで置き換えるだけで十分です。
|
||||
以下の図に示すように、削除対象ノードの次数が $1$ のとき、削除対象ノードをその子ノードで置き換えれば十分です。
|
||||
|
||||

|
||||

|
||||
|
||||
削除するノードの次数が$2$の場合、直接削除することはできませんが、ノードを使用して置き換える必要があります。二分探索木の性質「左部分木 $<$ 根ノード $<$ 右部分木」を維持するため、**このノードは右部分木の最小ノードまたは左部分木の最大ノードのいずれかです**。
|
||||
削除対象ノードの次数が $2$ のときは、直接削除できず、別のノードでそのノードを置き換える必要があります。二分探索木の「左部分木 $<$ 根ノード $<$ 右部分木」という性質を保つ必要があるため、**このノードには右部分木の最小ノードまたは左部分木の最大ノードを使えます**。
|
||||
|
||||
右部分木の最小ノード(中順走査での次のノード)を選択すると仮定すると、削除操作は下図に示すように進行します。
|
||||
右部分木の最小ノード(中順走査で次のノード)を選ぶと仮定すると、削除操作の流れは以下の図のようになります。
|
||||
|
||||
1. 削除するノードの「中順走査シーケンス」での次のノードを見つけ、`tmp`として示します。
|
||||
2. 削除するノードの値を`tmp`の値で置き換え、木内でノード`tmp`を再帰的に削除します。
|
||||
1. 削除対象ノードの「中順走査列」における次のノードを見つけ、`tmp` と記します。
|
||||
2. `tmp` の値で削除対象ノードの値を上書きし、木の中でノード `tmp` を再帰的に削除します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

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

|
||||
@@ -88,42 +88,42 @@
|
||||
=== "<4>"
|
||||

|
||||
|
||||
ノードを削除する操作も$O(\log n)$の時間を使用します。削除するノードを見つけるのに$O(\log n)$の時間が必要で、中順走査の後継ノードを取得するのに$O(\log n)$の時間が必要です。コード例は以下の通りです:
|
||||
ノード削除操作も同様に $O(\log n)$ 時間を要します。削除対象ノードの探索に $O(\log n)$ 時間、中順走査の後続ノードの取得に $O(\log n)$ 時間が必要です。コード例は次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
|
||||
```
|
||||
|
||||
### 中順走査は順序付けされている
|
||||
### 中順走査は昇順
|
||||
|
||||
下図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の走査順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」のサイズ関係を満たします。
|
||||
以下の図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」という順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」という大小関係を満たします。
|
||||
|
||||
これは、二分探索木で中順走査を実行するときに、常に次に小さいノードが最初に走査されることを意味し、重要な性質につながります:**二分探索木の中順走査のシーケンスは昇順です**。
|
||||
これは、二分探索木で中順走査を行うと常に次の最小ノードが優先して走査されることを意味し、そこから重要な性質が導かれます。**二分探索木の中順走査列は昇順です**。
|
||||
|
||||
中順走査の昇順性質を使用して、二分探索木で順序付けされたデータを取得するには$O(n)$の時間のみが必要で、追加のソート操作は不要であり、非常に効率的です。
|
||||
中順走査が昇順になる性質を利用すれば、二分探索木から整列済みデータを取得するのに必要な時間は $O(n)$ のみで、追加のソート操作は不要です。非常に効率的です。
|
||||
|
||||

|
||||

|
||||
|
||||
## 二分探索木の効率
|
||||
|
||||
データのセットが与えられた場合、配列または二分探索木を使用して格納することを検討します。下の表を観察すると、二分探索木のすべての操作は対数時間計算量を持ち、安定して効率的です。配列は、頻繁な追加と検索や削除の頻度が少ないシナリオでのみ、二分探索木よりも効率的です。
|
||||
あるデータ集合が与えられたとき、配列または二分探索木で格納する場合を考えます。次の表を見ると、二分探索木の各操作の時間計算量はいずれも対数オーダーであり、安定して高効率です。高頻度の追加と低頻度の探索・削除という場面でのみ、配列のほうが二分探索木より効率的です。
|
||||
|
||||
<p align="center"> 表 <id> 配列と探索木の効率比較 </p>
|
||||
|
||||
| | 未ソート配列 | 二分探索木 |
|
||||
| -------------- | -------------- | ------------------ |
|
||||
| 要素の検索 | $O(n)$ | $O(\log n)$ |
|
||||
| 要素の挿入 | $O(1)$ | $O(\log n)$ |
|
||||
| 要素の削除 | $O(n)$ | $O(\log n)$ |
|
||||
| | 無秩序配列 | 二分探索木 |
|
||||
| -------- | -------- | ----------- |
|
||||
| 要素の探索 | $O(n)$ | $O(\log n)$ |
|
||||
| 要素の挿入 | $O(1)$ | $O(\log n)$ |
|
||||
| 要素の削除 | $O(n)$ | $O(\log n)$ |
|
||||
|
||||
理想的には、二分探索木は「平衡」しており、任意のノードを$\log n$ループ内で見つけることができます。
|
||||
理想的な状況では、二分探索木は「平衡」しており、その場合は $\log n$ 回のループ内で任意のノードを探索できます。
|
||||
|
||||
しかし、二分探索木で継続的にノードを挿入および削除すると、下図に示すように連結リストに退化する可能性があり、さまざまな操作の時間計算量も$O(n)$に悪化します。
|
||||
しかし、二分探索木でノードの挿入と削除を繰り返すと、二分木が以下の図のような連結リストへ退化する可能性があり、このとき各操作の時間計算量も $O(n)$ に退化します。
|
||||
|
||||

|
||||
|
||||
## 二分探索木の一般的な応用
|
||||
## 二分探索木の代表的な応用
|
||||
|
||||
- システムでの多レベルインデックスとして使用され、効率的な検索、挿入、削除操作を実装します。
|
||||
- 特定の検索アルゴリズムの基盤となるデータ構造として機能します。
|
||||
- データストリームを格納して、その順序付けされた状態を維持するために使用されます。
|
||||
- システム内の多段インデックスとして用いられ、効率的な探索、挿入、削除操作を実現します。
|
||||
- 一部の探索アルゴリズムの基盤データ構造として使われます。
|
||||
- データストリームを格納し、その順序状態を保つために使われます。
|
||||
|
||||
+181
-143
@@ -1,26 +1,26 @@
|
||||
# 二分木
|
||||
|
||||
<u>二分木</u>は、祖先と子孫の間の階層関係を表現し、「二つに分割する」分割統治法の論理を体現する非線形データ構造です。連結リストと同様に、二分木の基本単位はノードであり、各ノードは値、左の子ノードへの参照、右の子ノードへの参照を含みます。
|
||||
<u>二分木(binary tree)</u>は非線形データ構造の一種であり、「祖先」と「子孫」の派生関係を表し、「一つを二つに分ける」分割統治の考え方を体現しています。連結リストと同様に、二分木の基本単位はノードであり、各ノードは値、左子ノードへの参照、右子ノードへの参照を含みます。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class TreeNode:
|
||||
"""二分木ノード"""
|
||||
"""二分木ノードクラス"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # ノード値
|
||||
self.left: TreeNode | None = None # 左の子ノードへの参照
|
||||
self.right: TreeNode | None = None # 右の子ノードへの参照
|
||||
self.left: TreeNode | None = None # 左子ノード参照
|
||||
self.right: TreeNode | None = None # 右子ノード参照
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノード構造体 */
|
||||
struct TreeNode {
|
||||
int val; // ノード値
|
||||
TreeNode *left; // 左の子ノードへのポインタ
|
||||
TreeNode *right; // 右の子ノードへのポインタ
|
||||
TreeNode *left; // 左子ノードポインタ
|
||||
TreeNode *right; // 右子ノードポインタ
|
||||
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
|
||||
};
|
||||
```
|
||||
@@ -28,11 +28,11 @@
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノードクラス */
|
||||
class TreeNode {
|
||||
int val; // ノード値
|
||||
TreeNode left; // 左の子ノードへの参照
|
||||
TreeNode right; // 右の子ノードへの参照
|
||||
TreeNode left; // 左子ノード参照
|
||||
TreeNode right; // 右子ノード参照
|
||||
TreeNode(int x) { val = x; }
|
||||
}
|
||||
```
|
||||
@@ -40,18 +40,18 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノードクラス */
|
||||
class TreeNode(int? x) {
|
||||
public int? val = x; // ノード値
|
||||
public TreeNode? left; // 左の子ノードへの参照
|
||||
public TreeNode? right; // 右の子ノードへの参照
|
||||
public TreeNode? left; // 左子ノード参照
|
||||
public TreeNode? right; // 右子ノード参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノード構造体 */
|
||||
type TreeNode struct {
|
||||
Val int
|
||||
Left *TreeNode
|
||||
@@ -60,8 +60,8 @@
|
||||
/* コンストラクタ */
|
||||
func NewTreeNode(v int) *TreeNode {
|
||||
return &TreeNode{
|
||||
Left: nil, // 左の子ノードへのポインタ
|
||||
Right: nil, // 右の子ノードへのポインタ
|
||||
Left: nil, // 左子ノードポインタ
|
||||
Right: nil, // 右子ノードポインタ
|
||||
Val: v, // ノード値
|
||||
}
|
||||
}
|
||||
@@ -70,11 +70,11 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノードクラス */
|
||||
class TreeNode {
|
||||
var val: Int // ノード値
|
||||
var left: TreeNode? // 左の子ノードへの参照
|
||||
var right: TreeNode? // 右の子ノードへの参照
|
||||
var left: TreeNode? // 左子ノード参照
|
||||
var right: TreeNode? // 右子ノード参照
|
||||
|
||||
init(x: Int) {
|
||||
val = x
|
||||
@@ -85,11 +85,11 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノードクラス */
|
||||
class TreeNode {
|
||||
val; // ノード値
|
||||
left; // 左の子ノードへのポインタ
|
||||
right; // 右の子ノードへのポインタ
|
||||
left; // 左子ノードポインタ
|
||||
right; // 右子ノードポインタ
|
||||
constructor(val, left, right) {
|
||||
this.val = val === undefined ? 0 : val;
|
||||
this.left = left === undefined ? null : left;
|
||||
@@ -101,7 +101,7 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノードクラス */
|
||||
class TreeNode {
|
||||
val: number;
|
||||
left: TreeNode | null;
|
||||
@@ -109,8 +109,8 @@
|
||||
|
||||
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
|
||||
this.val = val === undefined ? 0 : val; // ノード値
|
||||
this.left = left === undefined ? null : left; // 左の子ノードへの参照
|
||||
this.right = right === undefined ? null : right; // 右の子ノードへの参照
|
||||
this.left = left === undefined ? null : left; // 左子ノード参照
|
||||
this.right = right === undefined ? null : right; // 右子ノード参照
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -118,11 +118,11 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノードクラス */
|
||||
class TreeNode {
|
||||
int val; // ノード値
|
||||
TreeNode? left; // 左の子ノードへの参照
|
||||
TreeNode? right; // 右の子ノードへの参照
|
||||
TreeNode? left; // 左子ノード参照
|
||||
TreeNode? right; // 右子ノード参照
|
||||
TreeNode(this.val, [this.left, this.right]);
|
||||
}
|
||||
```
|
||||
@@ -133,11 +133,11 @@
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノード構造体 */
|
||||
struct TreeNode {
|
||||
val: i32, // ノード値
|
||||
left: Option<Rc<RefCell<TreeNode>>>, // 左の子ノードへの参照
|
||||
right: Option<Rc<RefCell<TreeNode>>>, // 右の子ノードへの参照
|
||||
left: Option<Rc<RefCell<TreeNode>>>, // 左子ノード参照
|
||||
right: Option<Rc<RefCell<TreeNode>>>, // 右子ノード参照
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
@@ -155,12 +155,12 @@
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノード構造体 */
|
||||
typedef struct TreeNode {
|
||||
int val; // ノード値
|
||||
int height; // ノードの高さ
|
||||
struct TreeNode *left; // 左の子ノードへのポインタ
|
||||
struct TreeNode *right; // 右の子ノードへのポインタ
|
||||
struct TreeNode *left; // 左子ノードポインタ
|
||||
struct TreeNode *right; // 右子ノードポインタ
|
||||
} TreeNode;
|
||||
|
||||
/* コンストラクタ */
|
||||
@@ -179,61 +179,70 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* 二分木ノード */
|
||||
/* 二分木ノードクラス */
|
||||
class TreeNode(val _val: Int) { // ノード値
|
||||
val left: TreeNode? = null // 左の子ノードへの参照
|
||||
val right: TreeNode? = null // 右の子ノードへの参照
|
||||
val left: TreeNode? = null // 左子ノード参照
|
||||
val right: TreeNode? = null // 右子ノード参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
### 二分木ノードクラス ###
|
||||
class TreeNode
|
||||
attr_accessor :val # ノード値
|
||||
attr_accessor :left # 左子ノード参照
|
||||
attr_accessor :right # 右子ノード参照
|
||||
|
||||
def initialize(val)
|
||||
@val = val
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
各ノードは2つの参照(ポインタ)を持ち、それぞれ<u>左の子ノード</u>と<u>右の子ノード</u>を指しています。このノードは、これら2つの子ノードの<u>親ノード</u>と呼ばれます。二分木のノードが与えられたとき、このノードの左の子とその下にあるすべてのノードで形成される木を、このノードの<u>左部分木</u>と呼びます。同様に、<u>右部分木</u>も定義できます。
|
||||
各ノードは 2 つの参照(ポインタ)を持ち、それぞれ<u>左子ノード(left-child node)</u>と<u>右子ノード(right-child node)</u>を指します。このノードはこれら 2 つの子ノードの<u>親ノード(parent node)</u>と呼ばれます。二分木のあるノードが与えられたとき、そのノードの左子ノードとその配下のノードからなる木をそのノードの<u>左部分木(left subtree)</u>と呼び、同様に<u>右部分木(right subtree)</u>が定義されます。
|
||||
|
||||
**二分木では、葉ノードを除いて、他のすべてのノードは子ノードと空でない部分木を含みます。** 下図に示すように、「ノード2」を親ノードとして見ると、その左と右の子ノードはそれぞれ「ノード4」と「ノード5」です。左部分木は「ノード4」とその下にあるすべてのノードで形成され、右部分木は「ノード5」とその下にあるすべてのノードで形成されます。
|
||||
**二分木では、葉ノードを除くすべてのノードが子ノードと空でない部分木を持ちます**。以下の図に示すように、「ノード 2」を親ノードとみなすと、その左子ノードと右子ノードはそれぞれ「ノード 4」と「ノード 5」であり、左部分木は「ノード 4 とその配下のノードからなる木」、右部分木は「ノード 5 とその配下のノードからなる木」です。
|
||||
|
||||

|
||||
|
||||
## 二分木の一般的な用語
|
||||
## 二分木のよく使われる用語
|
||||
|
||||
二分木でよく使用される用語を下図に示します。
|
||||
二分木でよく使われる用語を以下の図に示します。
|
||||
|
||||
- <u>根ノード</u>:二分木の最上位レベルにあるノードで、親ノードを持ちません。
|
||||
- <u>葉ノード</u>:子ノードを持たないノードで、両方のポインタが`None`を指しています。
|
||||
- <u>辺</u>:2つのノードを結ぶ線分で、ノード間の参照(ポインタ)を表現します。
|
||||
- ノードの<u>レベル</u>:上から下に向かって増加し、根ノードがレベル1です。
|
||||
- ノードの<u>次数</u>:ノードが持つ子ノードの数です。二分木では、次数は0、1、または2になります。
|
||||
- 二分木の<u>高さ</u>:根ノードから最も遠い葉ノードまでの辺の数です。
|
||||
- ノードの<u>深さ</u>:根ノードからそのノードまでの辺の数です。
|
||||
- ノードの<u>高さ</u>:最も遠い葉ノードからそのノードまでの辺の数です。
|
||||
- <u>根ノード(root node)</u>:二分木の最上位にあるノードで、親ノードを持ちません。
|
||||
- <u>葉ノード(leaf node)</u>:子ノードを持たないノードで、2 本のポインタはいずれも `None` を指します。
|
||||
- <u>辺(edge)</u>:2 つのノードを結ぶ線分、すなわちノード参照(ポインタ)です。
|
||||
- ノードが属する<u>レベル(level)</u>:上から下へ向かって増加し、根ノードのレベルは 1 です。
|
||||
- ノードの<u>次数(degree)</u>:ノードの子ノードの数。二分木では次数の取り得る値は 0、1、2 です。
|
||||
- 二分木の<u>高さ(height)</u>:根ノードから最も遠い葉ノードまでに通る辺の数。
|
||||
- ノードの<u>深さ(depth)</u>:根ノードからそのノードまでに通る辺の数。
|
||||
- ノードの<u>高さ(height)</u>:そのノードから最も遠い葉ノードまでに通る辺の数。
|
||||
|
||||

|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
「高さ」と「深さ」は通常「通過する辺の数」として定義しますが、一部の問題や教科書では「通過するノードの数」として定義されることがあります。この場合、高さと深さの両方を1だけ増やす必要があります。
|
||||
なお、通常「高さ」と「深さ」は「通過した辺の数」と定義しますが、問題や教材によっては「通過したノードの数」と定義する場合もあります。その場合、高さと深さはいずれも 1 を加える必要があります。
|
||||
|
||||
## 二分木の基本操作
|
||||
|
||||
### 二分木の初期化
|
||||
### 二分木を初期化する
|
||||
|
||||
連結リストと同様に、二分木の初期化では、まずノードを作成し、次にそれらの間の参照(ポインタ)を確立します。
|
||||
連結リストと同様に、まずノードを初期化し、その後で参照(ポインタ)を構築します。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="binary_tree.py"
|
||||
# 二分木の初期化
|
||||
# ノードの初期化
|
||||
# 二分木を初期化する
|
||||
# ノードを初期化する
|
||||
n1 = TreeNode(val=1)
|
||||
n2 = TreeNode(val=2)
|
||||
n3 = TreeNode(val=3)
|
||||
n4 = TreeNode(val=4)
|
||||
n5 = TreeNode(val=5)
|
||||
# ノード間の参照(ポインタ)を結ぶ
|
||||
# ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2
|
||||
n1.right = n3
|
||||
n2.left = n4
|
||||
@@ -243,14 +252,14 @@
|
||||
=== "C++"
|
||||
|
||||
```cpp title="binary_tree.cpp"
|
||||
/* 二分木の初期化 */
|
||||
// ノードの初期化
|
||||
/* 二分木を初期化する */
|
||||
// ノードを初期化する
|
||||
TreeNode* n1 = new TreeNode(1);
|
||||
TreeNode* n2 = new TreeNode(2);
|
||||
TreeNode* n3 = new TreeNode(3);
|
||||
TreeNode* n4 = new TreeNode(4);
|
||||
TreeNode* n5 = new TreeNode(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1->left = n2;
|
||||
n1->right = n3;
|
||||
n2->left = n4;
|
||||
@@ -260,13 +269,13 @@
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_tree.java"
|
||||
// ノードの初期化
|
||||
// ノードを初期化する
|
||||
TreeNode n1 = new TreeNode(1);
|
||||
TreeNode n2 = new TreeNode(2);
|
||||
TreeNode n3 = new TreeNode(3);
|
||||
TreeNode n4 = new TreeNode(4);
|
||||
TreeNode n5 = new TreeNode(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2;
|
||||
n1.right = n3;
|
||||
n2.left = n4;
|
||||
@@ -276,14 +285,14 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_tree.cs"
|
||||
/* 二分木の初期化 */
|
||||
// ノードの初期化
|
||||
/* 二分木を初期化する */
|
||||
// ノードを初期化する
|
||||
TreeNode n1 = new(1);
|
||||
TreeNode n2 = new(2);
|
||||
TreeNode n3 = new(3);
|
||||
TreeNode n4 = new(4);
|
||||
TreeNode n5 = new(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2;
|
||||
n1.right = n3;
|
||||
n2.left = n4;
|
||||
@@ -293,14 +302,14 @@
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_tree.go"
|
||||
/* 二分木の初期化 */
|
||||
// ノードの初期化
|
||||
/* 二分木を初期化する */
|
||||
// ノードを初期化する
|
||||
n1 := NewTreeNode(1)
|
||||
n2 := NewTreeNode(2)
|
||||
n3 := NewTreeNode(3)
|
||||
n4 := NewTreeNode(4)
|
||||
n5 := NewTreeNode(5)
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.Left = n2
|
||||
n1.Right = n3
|
||||
n2.Left = n4
|
||||
@@ -310,13 +319,13 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_tree.swift"
|
||||
// ノードの初期化
|
||||
// ノードを初期化する
|
||||
let n1 = TreeNode(x: 1)
|
||||
let n2 = TreeNode(x: 2)
|
||||
let n3 = TreeNode(x: 3)
|
||||
let n4 = TreeNode(x: 4)
|
||||
let n5 = TreeNode(x: 5)
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2
|
||||
n1.right = n3
|
||||
n2.left = n4
|
||||
@@ -326,14 +335,14 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_tree.js"
|
||||
/* 二分木の初期化 */
|
||||
// ノードの初期化
|
||||
/* 二分木を初期化する */
|
||||
// ノードを初期化する
|
||||
let n1 = new TreeNode(1),
|
||||
n2 = new TreeNode(2),
|
||||
n3 = new TreeNode(3),
|
||||
n4 = new TreeNode(4),
|
||||
n5 = new TreeNode(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2;
|
||||
n1.right = n3;
|
||||
n2.left = n4;
|
||||
@@ -343,14 +352,14 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_tree.ts"
|
||||
/* 二分木の初期化 */
|
||||
// ノードの初期化
|
||||
/* 二分木を初期化する */
|
||||
// ノードを初期化する
|
||||
let n1 = new TreeNode(1),
|
||||
n2 = new TreeNode(2),
|
||||
n3 = new TreeNode(3),
|
||||
n4 = new TreeNode(4),
|
||||
n5 = new TreeNode(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2;
|
||||
n1.right = n3;
|
||||
n2.left = n4;
|
||||
@@ -360,14 +369,14 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_tree.dart"
|
||||
/* 二分木の初期化 */
|
||||
// ノードの初期化
|
||||
/* 二分木を初期化する */
|
||||
// ノードを初期化する
|
||||
TreeNode n1 = new TreeNode(1);
|
||||
TreeNode n2 = new TreeNode(2);
|
||||
TreeNode n3 = new TreeNode(3);
|
||||
TreeNode n4 = new TreeNode(4);
|
||||
TreeNode n5 = new TreeNode(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2;
|
||||
n1.right = n3;
|
||||
n2.left = n4;
|
||||
@@ -377,13 +386,13 @@
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_tree.rs"
|
||||
// ノードの初期化
|
||||
// ノードを初期化する
|
||||
let n1 = TreeNode::new(1);
|
||||
let n2 = TreeNode::new(2);
|
||||
let n3 = TreeNode::new(3);
|
||||
let n4 = TreeNode::new(4);
|
||||
let n5 = TreeNode::new(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.borrow_mut().left = Some(n2.clone());
|
||||
n1.borrow_mut().right = Some(n3);
|
||||
n2.borrow_mut().left = Some(n4);
|
||||
@@ -393,14 +402,14 @@
|
||||
=== "C"
|
||||
|
||||
```c title="binary_tree.c"
|
||||
/* 二分木の初期化 */
|
||||
// ノードの初期化
|
||||
/* 二分木を初期化する */
|
||||
// ノードを初期化する
|
||||
TreeNode *n1 = newTreeNode(1);
|
||||
TreeNode *n2 = newTreeNode(2);
|
||||
TreeNode *n3 = newTreeNode(3);
|
||||
TreeNode *n4 = newTreeNode(4);
|
||||
TreeNode *n5 = newTreeNode(5);
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1->left = n2;
|
||||
n1->right = n3;
|
||||
n2->left = n4;
|
||||
@@ -410,13 +419,13 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_tree.kt"
|
||||
// ノードの初期化
|
||||
// ノードを初期化する
|
||||
val n1 = TreeNode(1)
|
||||
val n2 = TreeNode(2)
|
||||
val n3 = TreeNode(3)
|
||||
val n4 = TreeNode(4)
|
||||
val n5 = TreeNode(5)
|
||||
// ノード間の参照(ポインタ)を結ぶ
|
||||
// ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2
|
||||
n1.right = n3
|
||||
n2.left = n4
|
||||
@@ -426,24 +435,39 @@
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_tree.rb"
|
||||
|
||||
# 二分木を初期化する
|
||||
# ノードを初期化する
|
||||
n1 = TreeNode.new(1)
|
||||
n2 = TreeNode.new(2)
|
||||
n3 = TreeNode.new(3)
|
||||
n4 = TreeNode.new(4)
|
||||
n5 = TreeNode.new(5)
|
||||
# ノード間の参照(ポインタ)を構築する
|
||||
n1.left = n2
|
||||
n1.right = n3
|
||||
n2.left = n4
|
||||
n2.right = n5
|
||||
```
|
||||
|
||||
??? pythontutor "実行の可視化"
|
||||
|
||||
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### ノードの挿入と削除
|
||||
|
||||
連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。下図に例を示します。
|
||||
連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。以下の図に 1 つの例を示します。
|
||||
|
||||

|
||||

|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="binary_tree.py"
|
||||
# ノードの挿入と削除
|
||||
p = TreeNode(0)
|
||||
# n1 -> n2の間にノードPを挿入
|
||||
# n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = p
|
||||
p.left = n2
|
||||
# ノードPを削除
|
||||
# ノード P を削除する
|
||||
n1.left = n2
|
||||
```
|
||||
|
||||
@@ -452,21 +476,23 @@
|
||||
```cpp title="binary_tree.cpp"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode* P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1->left = P;
|
||||
P->left = n2;
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1->left = n2;
|
||||
// メモリを解放する
|
||||
delete P;
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_tree.java"
|
||||
TreeNode P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
@@ -475,10 +501,10 @@
|
||||
```csharp title="binary_tree.cs"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode P = new(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
@@ -486,11 +512,11 @@
|
||||
|
||||
```go title="binary_tree.go"
|
||||
/* ノードの挿入と削除 */
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
p := NewTreeNode(0)
|
||||
n1.Left = p
|
||||
p.Left = n2
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.Left = n2
|
||||
```
|
||||
|
||||
@@ -498,10 +524,10 @@
|
||||
|
||||
```swift title="binary_tree.swift"
|
||||
let P = TreeNode(x: 0)
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = P
|
||||
P.left = n2
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.left = n2
|
||||
```
|
||||
|
||||
@@ -510,10 +536,10 @@
|
||||
```javascript title="binary_tree.js"
|
||||
/* ノードの挿入と削除 */
|
||||
let P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
@@ -522,10 +548,10 @@
|
||||
```typescript title="binary_tree.ts"
|
||||
/* ノードの挿入と削除 */
|
||||
const P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
@@ -534,10 +560,10 @@
|
||||
```dart title="binary_tree.dart"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
@@ -545,10 +571,10 @@
|
||||
|
||||
```rust title="binary_tree.rs"
|
||||
let p = TreeNode::new(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.borrow_mut().left = Some(p.clone());
|
||||
p.borrow_mut().left = Some(n2.clone());
|
||||
// ノードPを削除
|
||||
// ノード p を削除する
|
||||
n1.borrow_mut().left = Some(n2);
|
||||
```
|
||||
|
||||
@@ -557,80 +583,92 @@
|
||||
```c title="binary_tree.c"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode *P = newTreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1->left = P;
|
||||
P->left = n2;
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1->left = n2;
|
||||
// メモリを解放する
|
||||
free(P);
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_tree.kt"
|
||||
val P = TreeNode(0)
|
||||
// n1とn2の間にノードPを挿入
|
||||
// n1 -> n2 の間にノード P を挿入する
|
||||
n1.left = P
|
||||
P.left = n2
|
||||
// ノードPを削除
|
||||
// ノード P を削除する
|
||||
n1.left = n2
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_tree.rb"
|
||||
|
||||
# ノードの挿入と削除
|
||||
_p = TreeNode.new(0)
|
||||
# n1 -> n2 の間にノード _p を挿入する
|
||||
n1.left = _p
|
||||
_p.left = n2
|
||||
# ノードを削除する
|
||||
n1.left = n2
|
||||
```
|
||||
|
||||
??? pythontutor "実行の可視化"
|
||||
|
||||
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
!!! tip
|
||||
|
||||
ノードの挿入は二分木の元の論理構造を変更する可能性があり、ノードの削除は通常そのノードとそのすべての部分木を削除することになることに注意してください。したがって、二分木では、挿入と削除は通常一連の操作を通じて実行され、意味のある結果を得ます。
|
||||
注意すべき点として、ノードの挿入は二分木の元の論理構造を変える可能性があり、ノードの削除は通常、そのノードと配下のすべての部分木の削除を意味します。そのため、二分木における挿入と削除は、実際に意味のある操作を実現するために、通常は一連の操作を組み合わせて行います。
|
||||
|
||||
## 二分木の一般的な種類
|
||||
## 一般的な二分木の種類
|
||||
|
||||
### 充足二分木
|
||||
|
||||
以下の図に示すように、<u>充足二分木(perfect binary tree)</u>ではすべてのレベルのノードが完全に埋まっています。充足二分木では、葉ノードの次数は $0$ で、それ以外のすべてのノードの次数は $2$ です。木の高さを $h$ とすると、ノード総数は $2^{h+1} - 1$ となり、標準的な指数関係を示して、自然界でよく見られる細胞分裂の現象を反映しています。
|
||||
|
||||
!!! tip
|
||||
|
||||
なお、中国語圏では充足二分木は<u>満二分木</u>と呼ばれることもあります。
|
||||
|
||||

|
||||
|
||||
### 完全二分木
|
||||
|
||||
下図に示すように、<u>完全二分木</u>では、すべてのレベルがノードで完全に埋められています。完全二分木では、葉ノードの次数は$0$で、他のすべてのノードの次数は$2$です。ノードの総数は$2^{h+1} - 1$として計算でき、ここで$h$は木の高さです。これは標準的な指数関係を示し、自然界の細胞分裂の一般的な現象を反映しています。
|
||||
以下の図に示すように、<u>完全二分木(complete binary tree)</u>では最下層のノードだけが完全に埋まっていなくてもよく、しかも最下層のノードは左から右へ連続して詰められていなければなりません。なお、充足二分木も完全二分木の一種です。
|
||||
|
||||
!!! tip
|
||||

|
||||
|
||||
中国語圏では、完全二分木はしばしば<u>満二分木</u>と呼ばれることに注意してください。
|
||||
### 充満二分木
|
||||
|
||||

|
||||
以下の図に示すように、<u>充満二分木(full binary tree)</u>では、葉ノードを除くすべてのノードが 2 つの子ノードを持ちます。
|
||||
|
||||
### 完備二分木
|
||||
|
||||
下図に示すように、<u>完備二分木</u>は、最下位レベルのみが完全に埋められていない可能性がある二分木で、最下位レベルのノードは左から右に連続して埋められる必要があります。完全二分木は完備二分木でもあることに注意してください。
|
||||
|
||||

|
||||
|
||||
### 満二分木
|
||||
|
||||
下図に示すように、<u>満二分木</u>では、葉ノードを除いて、他のすべてのノードが2つの子ノードを持ちます。
|
||||
|
||||

|
||||

|
||||
|
||||
### 平衡二分木
|
||||
|
||||
下図に示すように、<u>平衡二分木</u>では、任意のノードの左と右の部分木の高さの絶対差が1を超えません。
|
||||
以下の図に示すように、<u>平衡二分木(balanced binary tree)</u>では、任意のノードについて左部分木と右部分木の高さの差の絶対値が 1 を超えません。
|
||||
|
||||

|
||||
|
||||
## 二分木の退化
|
||||
|
||||
下図は、二分木の理想的な構造と退化した構造を示しています。二分木は、すべてのレベルが埋められているときに「完全二分木」になり、すべてのノードが一方に偏っているときに「連結リスト」に退化します。
|
||||
以下の図は、二分木の理想的な構造と退化した構造を示しています。二分木の各レベルのノードがすべて埋まっていると「充足二分木」となり、すべてのノードが片側に偏ると二分木は「連結リスト」へ退化します。
|
||||
|
||||
- 完全二分木は、二分木の「分割統治法」の利点を十分に活用できる理想的なシナリオです。
|
||||
- 一方、連結リストは別の極端を表し、すべての操作が線形になり、時間計算量が$O(n)$になります。
|
||||
- 充足二分木は理想的なケースであり、二分木の「分割統治」の利点を十分に発揮できます。
|
||||
- 連結リストはその対極にあり、各種操作はすべて線形操作となり、時間計算量は $O(n)$ まで退化します。
|
||||
|
||||

|
||||

|
||||
|
||||
下表に示すように、最良と最悪の構造では、二分木は葉ノード数、総ノード数、高さの最大値または最小値を達成します。
|
||||
以下の表に示すように、最良構造と最悪構造では、二分木の葉ノード数、ノード総数、高さなどが極大または極小になります。
|
||||
|
||||
<p align="center"> 表 <id> 二分木の最良と最悪の構造 </p>
|
||||
<p align="center"> 表 <id> 二分木の最良構造と最悪構造 </p>
|
||||
|
||||
| | 完全二分木 | 連結リスト |
|
||||
| ----------------------------------------------- | ------------------ | ----------- |
|
||||
| レベル$i$のノード数 | $2^{i-1}$ | $1$ |
|
||||
| 高さ$h$の木の葉ノード数 | $2^h$ | $1$ |
|
||||
| 高さ$h$の木の総ノード数 | $2^{h+1} - 1$ | $h + 1$ |
|
||||
| 総ノード数$n$の木の高さ | $\log_2 (n+1) - 1$ | $n - 1$ |
|
||||
| | 充足二分木 | 連結リスト |
|
||||
| --------------------------- | ------------------ | ------- |
|
||||
| 第 $i$ レベルのノード数 | $2^{i-1}$ | $1$ |
|
||||
| 高さ $h$ の木の葉ノード数 | $2^h$ | $1$ |
|
||||
| 高さ $h$ の木のノード総数 | $2^{h+1} - 1$ | $h + 1$ |
|
||||
| ノード総数 $n$ の木の高さ | $\log_2 (n+1) - 1$ | $n - 1$ |
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
# 二分木の走査
|
||||
|
||||
物理的構造の観点から見ると、木は連結リストに基づくデータ構造です。したがって、その走査方法はポインタを通してノードに一つずつアクセスすることを含みます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑で、検索アルゴリズムの支援が必要です。
|
||||
物理構造の観点から見ると、木は連結リストを基盤としたデータ構造であり、その走査はポインタを通じてノードへ順にアクセスすることで行われます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑であり、検索アルゴリズムを用いて実現する必要があります。
|
||||
|
||||
二分木の一般的な走査方法には、レベル順走査、前順走査、中順走査、後順走査があります。
|
||||
二分木の一般的な走査方法には、レベル順走査、先行順走査、中間順走査、後行順走査などがあります。
|
||||
|
||||
## レベル順走査
|
||||
|
||||
下図に示すように、<u>レベル順走査</u>は二分木を上から下へ、層ごとに走査します。各レベル内では、左から右へノードを訪問します。
|
||||
次の図に示すように、<u>レベル順走査(level-order traversal)</u>では、二分木を上から下へ層ごとに走査し、各層では左から右の順にノードへアクセスします。
|
||||
|
||||
レベル順走査は本質的に<u>幅優先走査</u>の一種で、<u>幅優先探索(BFS)</u>とも呼ばれ、「周囲に向かって外向きに拡張する」層ごとの走査方法を体現しています。
|
||||
レベル順走査は本質的に<u>幅優先走査(breadth-first traversal)</u>に属し、<u>幅優先探索(breadth-first search, BFS)</u>とも呼ばれます。これは「同心円状に外側へ広がる」ような、層ごとの走査方法を表しています。
|
||||
|
||||

|
||||
|
||||
### コード実装
|
||||
### コードの実装
|
||||
|
||||
幅優先走査は通常「キュー」の助けを借りて実装されます。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとの進行」規則に従います。両者の基本的な考え方は一致しています。実装コードは以下の通りです:
|
||||
幅優先走査は通常「キュー」を用いて実装します。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとに進む」という規則に従います。両者の背後にある考え方は一致しています。実装コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{binary_tree_bfs}-[class]{}-[func]{level_order}
|
||||
```
|
||||
|
||||
### 計算量分析
|
||||
### 計算量
|
||||
|
||||
- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間がかかります。ここで$n$はノード数です。
|
||||
- **空間計算量は$O(n)$**: 最悪の場合、つまり完全二分木の場合、最下位レベルに走査する前に、キューは最大$(n + 1) / 2$個のノードを同時に含むことができ、$O(n)$の空間を占有します。
|
||||
- **時間計算量は $O(n)$** :すべてのノードを1回ずつ訪問するため、計算量は $O(n)$ です。ここで、$n$ はノード数です。
|
||||
- **空間計算量は $O(n)$** :最悪の場合、すなわち完全二分木では、最下層に到達する前に、キュー内には最大で同時に $(n + 1) / 2$ 個のノードが存在し、$O(n)$ の空間を使用します。
|
||||
|
||||
## 前順、中順、後順走査
|
||||
## 先行順・中間順・後行順走査
|
||||
|
||||
対応して、前順、中順、後順走査はすべて<u>深度優先走査</u>に属し、<u>深度優先探索(DFS)</u>とも呼ばれ、「まず最後まで進み、その後バックトラックして続行する」走査方法を体現しています。
|
||||
同様に、先行順・中間順・後行順走査はいずれも<u>深度優先走査(depth-first traversal)</u>に属し、<u>深度優先探索(depth-first search, DFS)</u>とも呼ばれます。これは「まず行き止まりまで進み、その後で戻って続ける」という走査方法を表しています。
|
||||
|
||||
下図は二分木に対して深度優先走査を実行する動作原理を示しています。**深度優先走査は二分木全体を「歩き回る」ようなもので**、各ノードで3つの位置に遭遇し、それらは前順、中順、後順走査に対応しています。
|
||||
次の図は、二分木に対して深度優先走査を行う仕組みを示しています。**深度優先走査は、二分木全体の外周をぐるりと「一周する」ようなものです**。各ノードでは3つの位置に出会い、それぞれが先行順走査・中間順走査・後行順走査に対応します。
|
||||
|
||||

|
||||

|
||||
|
||||
### コード実装
|
||||
### コードの実装
|
||||
|
||||
深度優先探索は通常再帰に基づいて実装されます:
|
||||
深度優先探索は通常、再帰に基づいて実装されます:
|
||||
|
||||
```src
|
||||
[file]{binary_tree_dfs}-[class]{}-[func]{post_order}
|
||||
@@ -43,15 +43,15 @@
|
||||
|
||||
!!! tip
|
||||
|
||||
深度優先探索は反復に基づいても実装できます。興味のある読者は自分で学習してください。
|
||||
深度優先探索は反復によって実装することもできます。興味のある読者は自身で調べてみてください。
|
||||
|
||||
下図は二分木の前順走査の再帰プロセスを示しており、これは「再帰」と「復帰」という2つの反対の部分に分けることができます。
|
||||
次の図は、二分木の先行順走査における再帰の過程を示しており、「行き」と「帰り」という2つの逆向きの部分に分けられます。
|
||||
|
||||
1. 「再帰」は新しいメソッドを開始することを意味し、プログラムはこのプロセスで次のノードにアクセスします。
|
||||
2. 「復帰」は関数が戻ることを意味し、現在のノードが完全にアクセスされたことを示します。
|
||||
1. 「行き」は新しいメソッドの開始を表し、この過程でプログラムは次のノードにアクセスします。
|
||||
2. 「帰り」は関数の復帰を表し、現在のノードへのアクセスが完了したことを意味します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

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

|
||||
@@ -83,7 +83,7 @@
|
||||
=== "<11>"
|
||||

|
||||
|
||||
### 計算量分析
|
||||
### 計算量
|
||||
|
||||
- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間を使用します。
|
||||
- **空間計算量は$O(n)$**: 最悪の場合、つまり木が連結リストに退化した場合、再帰の深さは$n$に達し、システムは$O(n)$のスタックフレーム空間を占有します。
|
||||
- **時間計算量は $O(n)$** :すべてのノードを1回ずつ訪問するため、計算量は $O(n)$ です。
|
||||
- **空間計算量は $O(n)$** :最悪の場合、すなわち木が連結リストに退化したとき、再帰の深さは $n$ に達し、システムは $O(n)$ のスタックフレーム空間を使用します。
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
!!! abstract
|
||||
|
||||
そびえ立つ木は活力に満ちた本質を放ち、深い根と豊かな葉を誇りながらも、その枝は疎らに散らばり、幽玄な雰囲気を醸し出しています。
|
||||
|
||||
それはデータにおける分割統治の鮮やかな形を私たちに示しています。
|
||||
大樹は生命力に満ち、根は深く葉は生い茂り、枝は豊かに広がる。
|
||||
|
||||
それはデータ分割統治の生き生きとした姿を私たちに示してくれる。
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
# まとめ
|
||||
|
||||
### 重要なポイント
|
||||
### 要点の振り返り
|
||||
|
||||
- 二分木は非線形データ構造で、「一つを二つに分ける」分割統治のロジックを反映しています。各二分木ノードには値と2つのポインタが含まれ、それぞれ左と右の子ノードを指します。
|
||||
- 二分木のノードについて、その左(右)子ノードとその下に形成される木は、まとめてそのノードの左(右)部分木と呼ばれます。
|
||||
- 二分木に関連する用語には、根ノード、葉ノード、レベル、次数、エッジ、高さ、深さがあります。
|
||||
- 二分木の初期化、ノードの挿入、ノードの削除の操作は、連結リストの操作と似ています。
|
||||
- 一般的な二分木の種類には、完全二分木、完備二分木、満二分木、平衡二分木があります。完全二分木は理想的な状態を表し、連結リストは退化後の最悪の状態です。
|
||||
- 二分木は、ノード値と空きスロットをレベル順走査シーケンスで配置し、親ノードと子ノード間のインデックスマッピング関係に基づいてポインタを実装することで、配列を使用して表現できます。
|
||||
- 二分木のレベル順走査は幅優先探索手法で、「円を拡大しながら」の層ごとの走査方式を反映しています。通常はキューを使用して実装されます。
|
||||
- 前順、中順、後順走査はすべて深度優先探索手法で、「まず最後まで行き、その後バックトラックして続行する」走査方式を反映しています。通常は再帰を使用して実装されます。
|
||||
- 二分探索木は要素検索のための効率的なデータ構造で、検索、挿入、削除操作の時間計算量はすべて$O(\log n)$です。二分探索木が連結リストに退化すると、これらの時間計算量は$O(n)$に悪化します。
|
||||
- AVL木は平衡二分探索木とも呼ばれ、回転操作を通して継続的なノード挿入と削除後も木が平衡を保つことを保証します。
|
||||
- AVL木の回転操作には、右回転、左回転、右左回転、左右回転があります。ノードの挿入または削除後、AVL木はボトムアップ方式でこれらの回転を実行して自己平衡を取ります。
|
||||
- 二分木は非線形データ構造の一種であり、「二分する」分割統治の考え方を体現している。各二分木ノードは 1 つの値と 2 本のポインタを持ち、それぞれ左子ノードと右子ノードを指す。
|
||||
- 二分木のあるノードについて、その左(右)子ノードおよびその配下から構成される木を、そのノードの左(右)部分木と呼ぶ。
|
||||
- 二分木に関する用語には、根ノード、葉ノード、レベル、次数、辺、高さ、深さなどがある。
|
||||
- 二分木の初期化、ノードの挿入、ノードの削除は、連結リストの操作方法と似ている。
|
||||
- 一般的な二分木の種類には、perfect 二分木、complete 二分木、full 二分木、平衡二分木がある。perfect 二分木が最も理想的な状態であり、連結リストは退化後の最悪の状態である。
|
||||
- 二分木は配列で表現できる。方法としては、ノード値と空き位置をレベル順走査の順に並べ、親ノードと子ノードのインデックス対応関係に基づいてポインタを実現する。
|
||||
- 二分木のレベル順走査は幅優先探索の一種であり、「同心円状に外へ広がる」ような逐次的な走査方式を表しており、通常はキューによって実装される。
|
||||
- 前順、中順、後順走査はいずれも深さ優先探索に属し、「まず末端まで進み、その後バックトラックして続ける」という走査方式を体現しており、通常は再帰で実装される。
|
||||
- 二分探索木は効率的な要素探索データ構造であり、探索、挿入、削除の時間計算量はいずれも $O(\log n)$ である。二分探索木が連結リストへ退化すると、各操作の時間計算量は $O(n)$ まで悪化する。
|
||||
- AVL 木は平衡二分探索木とも呼ばれ、回転操作によって、ノードの挿入と削除を繰り返した後も木が平衡を保つようにしている。
|
||||
- AVL 木の回転操作には、右回転、左回転、右回転してから左回転、左回転してから右回転がある。ノードの挿入または削除の後、AVL 木は下から上へ回転操作を行い、木を再び平衡状態に戻す。
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: 一つのノードのみを持つ二分木について、木の高さと根ノードの深さの両方が$0$ですか?
|
||||
**Q**:ノードが 1 つしかない二分木では、木の高さと根ノードの深さはどちらも $0$ ですか?
|
||||
|
||||
はい、高さと深さは通常「通過したエッジの数」として定義されるためです。
|
||||
はい。高さと深さは通常「通過した辺の本数」として定義されるからです。
|
||||
|
||||
**Q**: 二分木における挿入と削除は一般的に一連の操作によって達成されます。ここでの「一連の操作」とは何を指しますか?子ノードのリソースを解放することを意味しますか?
|
||||
**Q**:二分木における挿入と削除は通常一連の操作を組み合わせて完了しますが、ここでいう「一連の操作」とは何を指すのでしょうか?リソースの子ノードに対するリソース解放と理解できますか?
|
||||
|
||||
二分探索木を例に取ると、ノードを削除する操作は3つの異なるシナリオで処理する必要があり、それぞれ複数ステップのノード操作が必要です。
|
||||
二分探索木を例にすると、ノード削除は 3 つのケースに分けて処理する必要があり、各ケースで複数段階のノード操作が必要になります。
|
||||
|
||||
**Q**: 二分木のDFS走査で前順、中順、後順の3つのシーケンスがあるのはなぜですか?その用途は何ですか?
|
||||
**Q**:なぜ DFS による二分木走査には前順・中順・後順の 3 種類があり、それぞれどのような用途があるのですか?
|
||||
|
||||
配列の順次および逆順走査と同様に、前順、中順、後順走査は二分木を走査する3つの方法であり、特定の順序で走査結果を取得できます。例えば、二分探索木では、ノードサイズが「左子ノード値 < 根ノード値 < 右子ノード値」を満たすため、「左 $\rightarrow$ 根 $\rightarrow$ 右」の優先順位で木を走査することで、順序付けられたノードシーケンスを取得できます。
|
||||
配列の順方向走査と逆方向走査に似て、前順・中順・後順走査は二分木の 3 つの走査方法であり、特定の順序で走査結果を得るために使えます。たとえば二分探索木では、ノードの大小関係が `左子ノードの値 < 根ノードの値 < 右子ノードの値` を満たすため、「左 $\rightarrow$ 根 $\rightarrow$ 右」の優先順で木を走査すれば、整列済みのノード列を得られます。
|
||||
|
||||
**Q**: 不平衡ノード`node`、`child`、`grand_child`間の関係を処理する右回転操作において、右回転後に`node`とその親ノード間の接続と`node`の元のリンクが失われるのではありませんか?
|
||||
**Q**:右回転操作は不平衡ノード `node`、`child`、`grand_child` の関係を処理するものですが、`node` の親ノードと `node` の元の接続は維持しなくてよいのですか?右回転後に切れてしまいませんか?
|
||||
|
||||
この問題を再帰的な観点から見る必要があります。`right_rotate(root)`操作は部分木の根ノードを渡し、最終的に`return child`で回転された部分木の根ノードを返します。部分木の根ノードとその親ノード間の接続は、この関数が戻った後に確立され、これは右回転操作の保守範囲外です。
|
||||
この問題は再帰の視点から考える必要があります。右回転操作 `right_rotate(root)` に渡されるのは部分木の根ノードであり、最終的に `return child` によって回転後の部分木の根ノードを返します。部分木の根ノードとその親ノードの接続は、この関数の返却後に行われるため、右回転操作自身が管理する範囲には含まれません。
|
||||
|
||||
**Q**: C++では、関数は`private`と`public`セクションに分かれています。これにはどのような考慮事項がありますか?なぜ`height()`関数と`updateHeight()`関数がそれぞれ`public`と`private`に配置されているのですか?
|
||||
**Q**:C++ では関数を `private` と `public` に分けますが、この設計にはどのような考えがありますか?なぜ `height()` 関数と `updateHeight()` 関数をそれぞれ `public` と `private` に置くのですか?
|
||||
|
||||
これはメソッドの使用範囲によります。メソッドがクラス内でのみ使用される場合、`private`に設計されます。例えば、ユーザーが独自に`updateHeight()`を呼び出すことは意味がありません。これは挿入または削除操作の一ステップに過ぎないからです。しかし、`height()`はノードの高さにアクセスするためのもので、`vector.size()`と同様であるため、使用のために`public`に設定されています。
|
||||
主に、そのメソッドの利用範囲を見て決めます。メソッドがクラス内部でしか使われないなら、`private` に設計します。たとえば、利用者が `updateHeight()` を単独で呼び出しても意味はなく、これは挿入や削除の途中の 1 ステップにすぎません。一方で `height()` はノードの高さにアクセスするためのもので、`vector.size()` に似た役割を持つため、使いやすいように `public` に設定します。
|
||||
|
||||
**Q**: 入力データのセットから二分探索木をどのように構築しますか?根ノードの選択は非常に重要ですか?
|
||||
**Q**:入力データの集合から二分探索木をどのように構築しますか?根ノードの選び方は重要ですか?
|
||||
|
||||
はい、木を構築する方法は二分探索木コードの`build_tree()`メソッドで提供されています。根ノードの選択については、通常入力データをソートし、中央の要素を根ノードとして選択し、再帰的に左と右の部分木を構築します。このアプローチは木の平衡を最大化します。
|
||||
はい。木の構築方法は、二分探索木のコード中の `build_tree()` メソッドですでに示されています。根ノードの選択については、通常は入力データをソートし、その中央の要素を根ノードにしてから、左右の部分木を再帰的に構築します。こうすることで、木の平衡性を最大限に保てます。
|
||||
|
||||
**Q**: Javaでは、文字列比較に常に`equals()`メソッドを使用する必要がありますか?
|
||||
**Q**:Java では、文字列比較には必ず `equals()` メソッドを使うべきですか?
|
||||
|
||||
Javaでは、プリミティブデータ型の場合、`==`は2つの変数の値が等しいかどうかを比較するために使用されます。参照型の場合、2つのシンボルの動作原理は異なります。
|
||||
Java では、基本データ型については `==` を使って 2 つの変数の値が等しいかどうかを比較します。参照型については、この 2 つの記法の働き方は異なります。
|
||||
|
||||
- `==`: 2つの変数が同じオブジェクトを指しているかどうか、つまりメモリ内の位置が同じかどうかを比較するために使用されます。
|
||||
- `equals()`: 2つのオブジェクトの値が等しいかどうかを比較するために使用されます。
|
||||
- `==` :2 つの変数が同じオブジェクトを指しているか、つまりメモリ上の位置が同じかどうかを比較するために使います。
|
||||
- `equals()`:2 つのオブジェクトの値が等しいかどうかを比較するために使います。
|
||||
|
||||
したがって、値を比較するには`equals()`を使用すべきです。ただし、`String a = "hi"; String b = "hi";`で初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、`a == b`も2つの文字列の内容を比較するために使用できます。
|
||||
したがって、値を比較したい場合は `equals()` を使うべきです。ただし、`String a = "hi"; String b = "hi";` によって初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、`a == b` でも 2 つの文字列の内容を比較できます。
|
||||
|
||||
**Q**: 最下位レベルに到達する前に、幅優先走査でキュー内のノード数は$2^h$ですか?
|
||||
**Q**:幅優先走査で最下層に到達する前、キュー内のノード数は $2^h$ ですか?
|
||||
|
||||
はい、例えば高さ$h = 2$の満二分木は合計$n = 7$個のノードを持ち、最下位レベルには$4 = 2^h = (n + 1) / 2$個のノードがあります。
|
||||
はい。たとえば高さ $h = 2$ の充足二分木では、ノード総数は $n = 7$ であり、最下層のノード数は $4 = 2^h = (n + 1) / 2$ です。
|
||||
|
||||
Reference in New Issue
Block a user