docs: add Japanese translate documents (#1812)
* docs: add Japanese documents (`ja/docs`) * docs: add Japanese documents (`ja/codes`) * docs: add Japanese documents * Remove pythontutor blocks in ja/ * Add an empty at the end of each markdown file. * Add the missing figures (use the English version temporarily). * Add index.md for Japanese version. * Add index.html for Japanese version. * Add missing index.assets * Fix backtracking_algorithm.md for Japanese version. * Add avatar_eltociear.jpg. Fix image links on the Japanese landing page. * Add the Japanese banner. --------- Co-authored-by: krahets <krahets@163.com>
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 22 KiB |
164
ja/docs/chapter_tree/array_representation_of_tree.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# 二分木の配列表現
|
||||
|
||||
連結リスト表現では、二分木の格納単位はノード`TreeNode`であり、ノードはポインタによって接続されます。連結リスト表現での二分木の基本操作については前の節で紹介しました。
|
||||
|
||||
では、配列を使って二分木を表現することはできるでしょうか?答えはイエスです。
|
||||
|
||||
## 完全二分木の表現
|
||||
|
||||
まず簡単なケースから分析してみましょう。完全二分木が与えられたとき、レベル順探索の順序に従ってすべてのノードを配列に格納し、各ノードは一意の配列インデックスに対応します。
|
||||
|
||||
レベル順探索の特性に基づいて、親ノードのインデックスとその子ノードの間の「マッピング公式」を導き出すことができます:**ノードのインデックスが$i$の場合、その左の子のインデックスは$2i + 1$、右の子のインデックスは$2i + 2$です**。下図は、さまざまなノードのインデックス間のマッピング関係を示しています。
|
||||
|
||||

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

|
||||
|
||||
この問題を解決するために、**レベル順探索シーケンスですべての`None`値を明示的に書き出すことを検討できます**。下図に示すように、この処理後、レベル順探索シーケンスは二分木を一意に表現できます。サンプルコードは以下の通りです:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
# 二分木の配列表現
|
||||
# Noneを使用して空のスロットを表現
|
||||
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 二分木の配列表現 */
|
||||
// 最大整数値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};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 二分木の配列表現 */
|
||||
// Integerラッパークラスを使用してnullで空のスロットをマーク
|
||||
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullable int (int?)を使用してnullで空のスロットをマーク
|
||||
int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 二分木の配列表現 */
|
||||
// any型スライスを使用してnilで空のスロットをマーク
|
||||
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 二分木の配列表現 */
|
||||
// optional Int (Int?)を使用してnilで空のスロットをマーク
|
||||
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullを使用して空のスロットを表現
|
||||
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullを使用して空のスロットを表現
|
||||
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullable int (int?)を使用してnullで空のスロットをマーク
|
||||
List<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
/* 二分木の配列表現 */
|
||||
// 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)];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* 二分木の配列表現 */
|
||||
// 最大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};
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* 二分木の配列表現 */
|
||||
// nullを使用して空のスロットを表現
|
||||
val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
以下のコードは、配列表現に基づく二分木を実装し、次の操作を含みます:
|
||||
|
||||
- ノードが与えられたとき、その値、左(右)の子ノード、および親ノードを取得する。
|
||||
- 前順、中順、後順、およびレベル順探索シーケンスを取得する。
|
||||
|
||||
```src
|
||||
[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{}
|
||||
```
|
||||
|
||||
## 利点と制限
|
||||
|
||||
二分木の配列表現には以下の利点があります:
|
||||
|
||||
- 配列は連続したメモリ空間に格納されるため、キャッシュフレンドリーで、より高速なアクセスと探索が可能です。
|
||||
- ポインタを格納する必要がないため、スペースを節約できます。
|
||||
- ノードへのランダムアクセスが可能です。
|
||||
|
||||
しかし、配列表現にはいくつかの制限もあります:
|
||||
|
||||
- 配列格納には連続したメモリ空間が必要なため、大量のデータを持つ木の格納には適していません。
|
||||
- ノードの追加や削除には配列の挿入や削除操作が必要で、効率が低くなります。
|
||||
- 二分木に多くの`None`値がある場合、配列に含まれるノードデータの割合が低くなり、空間利用率が低下します。
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
BIN
ja/docs/chapter_tree/avl_tree.assets/avltree_left_rotate.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 27 KiB |
BIN
ja/docs/chapter_tree/avl_tree.assets/avltree_rotation_cases.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
353
ja/docs/chapter_tree/avl_tree.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# AVL木 *
|
||||
|
||||
「二分探索木」の節では、複数の挿入と削除の後、二分探索木が連結リストに退化する可能性があることを述べました。このような場合、すべての操作の時間計算量が$O(\log n)$から$O(n)$に悪化します。
|
||||
|
||||
下図に示すように、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木は常に効率的なデータ操作性能を維持でき、大きな応用価値があります。
|
||||
|
||||
## AVL木の一般的な用語
|
||||
|
||||
AVL木は二分探索木でありかつ平衡二分木でもあり、これら2つの種類の二分木のすべての性質を満たしているため、<u>平衡二分探索木</u>です。
|
||||
|
||||
### ノードの高さ
|
||||
|
||||
AVL木に関連する操作ではノードの高さを取得する必要があるため、ノードクラスに`height`変数を追加する必要があります:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class TreeNode:
|
||||
"""AVL木ノード"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # ノード値
|
||||
self.height: int = 0 # ノードの高さ
|
||||
self.left: TreeNode | None = None # 左の子への参照
|
||||
self.right: TreeNode | None = None # 右の子への参照
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* AVL木ノード */
|
||||
struct TreeNode {
|
||||
int val{}; // ノード値
|
||||
int height = 0; // ノードの高さ
|
||||
TreeNode *left{}; // 左の子
|
||||
TreeNode *right{}; // 右の子
|
||||
TreeNode() = default;
|
||||
explicit TreeNode(int x) : val(x){}
|
||||
};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* AVL木ノード */
|
||||
class TreeNode {
|
||||
public int val; // ノード値
|
||||
public int height; // ノードの高さ
|
||||
public TreeNode left; // 左の子
|
||||
public TreeNode right; // 右の子
|
||||
public TreeNode(int x) { val = x; }
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* AVL木ノード */
|
||||
class TreeNode(int? x) {
|
||||
public int? val = x; // ノード値
|
||||
public int height; // ノードの高さ
|
||||
public TreeNode? left; // 左の子への参照
|
||||
public TreeNode? right; // 右の子への参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* AVL木ノード */
|
||||
type TreeNode struct {
|
||||
Val int // ノード値
|
||||
Height int // ノードの高さ
|
||||
Left *TreeNode // 左の子への参照
|
||||
Right *TreeNode // 右の子への参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* AVL木ノード */
|
||||
class TreeNode {
|
||||
var val: Int // ノード値
|
||||
var height: Int // ノードの高さ
|
||||
var left: TreeNode? // 左の子
|
||||
var right: TreeNode? // 右の子
|
||||
|
||||
init(x: Int) {
|
||||
val = x
|
||||
height = 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* AVL木ノード */
|
||||
class TreeNode {
|
||||
val; // ノード値
|
||||
height; // ノードの高さ
|
||||
left; // 左の子ポインタ
|
||||
right; // 右の子ポインタ
|
||||
constructor(val, left, right, height) {
|
||||
this.val = val === undefined ? 0 : val;
|
||||
this.height = height === undefined ? 0 : height;
|
||||
this.left = left === undefined ? null : left;
|
||||
this.right = right === undefined ? null : right;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* AVL木ノード */
|
||||
class TreeNode {
|
||||
val: number; // ノード値
|
||||
height: number; // ノードの高さ
|
||||
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;
|
||||
this.left = left === undefined ? null : left;
|
||||
this.right = right === undefined ? null : right;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* AVL木ノード */
|
||||
class TreeNode {
|
||||
int val; // ノード値
|
||||
int height; // ノードの高さ
|
||||
TreeNode? left; // 左の子
|
||||
TreeNode? right; // 右の子
|
||||
TreeNode(this.val, [this.height = 0, this.left, this.right]);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/* AVL木ノード */
|
||||
struct TreeNode {
|
||||
val: i32, // ノード値
|
||||
height: i32, // ノードの高さ
|
||||
left: Option<Rc<RefCell<TreeNode>>>, // 左の子
|
||||
right: Option<Rc<RefCell<TreeNode>>>, // 右の子
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
/* コンストラクタ */
|
||||
fn new(val: i32) -> Rc<RefCell<Self>> {
|
||||
Rc::new(RefCell::new(Self {
|
||||
val,
|
||||
height: 0,
|
||||
left: None,
|
||||
right: None
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* AVL木ノード */
|
||||
TreeNode struct TreeNode {
|
||||
int val;
|
||||
int height;
|
||||
struct TreeNode *left;
|
||||
struct TreeNode *right;
|
||||
} TreeNode;
|
||||
|
||||
/* コンストラクタ */
|
||||
TreeNode *newTreeNode(int val) {
|
||||
TreeNode *node;
|
||||
|
||||
node = (TreeNode *)malloc(sizeof(TreeNode));
|
||||
node->val = val;
|
||||
node->height = 0;
|
||||
node->left = NULL;
|
||||
node->right = NULL;
|
||||
return node;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* AVL木ノード */
|
||||
class TreeNode(val _val: Int) { // ノード値
|
||||
val height: Int = 0 // ノードの高さ
|
||||
val left: TreeNode? = null // 左の子
|
||||
val right: TreeNode? = null // 右の子
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、つまり通過する「辺」の数を指します。重要なのは、葉ノードの高さは$0$で、nullノードの高さは$-1$であることです。ノードの高さを取得し、更新するための2つのユーティリティ関数を作成します:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{update_height}
|
||||
```
|
||||
|
||||
### ノードの平衡因子
|
||||
|
||||
ノードの<u>平衡因子</u>は、そのノードの左部分木の高さから右部分木の高さを引いた値として定義され、nullノードの平衡因子は$0$として定義されます。後で使いやすくするため、ノードの平衡因子を取得する機能も関数にカプセル化します:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{balance_factor}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
平衡因子を$f$とすると、AVL木の任意のノードの平衡因子は$-1 \le f \le 1$を満たします。
|
||||
|
||||
## AVL木の回転
|
||||
|
||||
AVL木の特徴的な機能は「回転」操作で、これは二分木の中順探索シーケンスに影響を与えることなく、不平衡なノードのバランスを回復できます。つまり、**回転操作は「二分探索木」の性質を維持しながら、木を「平衡二分木」に戻すことができます**。
|
||||
|
||||
絶対平衡因子が$> 1$のノードを「不平衡ノード」と呼びます。不平衡のタイプに応じて、4種類の回転があります:右回転、左回転、右左回転、左右回転です。以下、これらの回転操作について詳しく説明します。
|
||||
|
||||
### 右回転
|
||||
|
||||
下図に示すように、二分木で下から上への最初の不平衡ノードは「ノード3」です。この不平衡ノードを根とする部分木に焦点を当て、これを`node`とし、その左の子を`child`として、「右回転」を実行します。右回転後、部分木は再びバランスが取れ、同時に二分探索木の性質も維持されます。
|
||||
|
||||
=== "<1>"
|
||||

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

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

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

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

|
||||
|
||||
「右回転」は比喩的な用語で、実際にはノードポインタを変更することで実現されます。以下のコードで示されます:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{right_rotate}
|
||||
```
|
||||
|
||||
### 左回転
|
||||
|
||||
対応して、上記の不平衡二分木の「鏡像」を考慮すると、下図に示す「左回転」操作を実行する必要があります。
|
||||
|
||||

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

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

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

|
||||
|
||||
### 回転の選択
|
||||
|
||||
下図に示す4種類の不平衡は、それぞれ上記で説明したケースに対応し、右回転、左右回転、右左回転、左回転が必要です。
|
||||
|
||||

|
||||
|
||||
下表に示すように、不平衡ノードの平衡因子とその高い側の子の平衡因子の符号を判断することで、不平衡ノードが上記のどのケースに属するかを決定します。
|
||||
|
||||
<p align="center"> 表 <id> 4つの回転ケースの選択条件 </p>
|
||||
|
||||
| 不平衡ノードの平衡因子 | 子ノードの平衡因子 | 使用する回転方法 |
|
||||
| --------------------- | ----------------- | --------------------------- |
|
||||
| $> 1$(左に傾いた木) | $\geq 0$ | 右回転 |
|
||||
| $> 1$(左に傾いた木) | $<0$ | 左回転してから右回転 |
|
||||
| $< -1$(右に傾いた木) | $\leq 0$ | 左回転 |
|
||||
| $< -1$(右に傾いた木) | $>0$ | 右回転してから左回転 |
|
||||
|
||||
便宜上、回転操作を関数にカプセル化します。**この関数により、さまざまな種類の不平衡に対して回転を実行し、不平衡ノードのバランスを回復できます**。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{rotate}
|
||||
```
|
||||
|
||||
## AVL木の一般的な操作
|
||||
|
||||
### ノードの挿入
|
||||
|
||||
AVL木のノード挿入操作は二分探索木のそれと似ています。唯一の違いは、AVL木でノードを挿入した後、そのノードから根ノードまでのパス上に一連の不平衡ノードが現れる可能性があることです。したがって、**このノードから始めて上向きに回転操作を実行し、すべての不平衡ノードのバランスを回復する必要があります**。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{insert_helper}
|
||||
```
|
||||
|
||||
### ノードの削除
|
||||
|
||||
同様に、二分探索木でのノード削除方法に基づいて、下から上へ回転操作を実行してすべての不平衡ノードのバランスを回復する必要があります。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{remove_helper}
|
||||
```
|
||||
|
||||
### ノードの検索
|
||||
|
||||
AVL木でのノード検索操作は二分探索木のそれと一致しており、ここでは詳述しません。
|
||||
|
||||
## AVL木の典型的な応用
|
||||
|
||||
- 大量のデータの整理と格納に使用され、検索頻度が高く、挿入と削除の頻度が低いシナリオに適しています。
|
||||
- データベースのインデックスシステムの構築に使用されます。
|
||||
- 赤黒木も一般的な平衡二分探索木の一種です。AVL木と比較して、赤黒木はより緩い平衡条件を持ち、ノードの挿入と削除にかかる回転数が少なく、ノードの追加と削除操作の平均効率が高くなります。
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 34 KiB |
BIN
ja/docs/chapter_tree/binary_search_tree.assets/bst_insert.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
129
ja/docs/chapter_tree/binary_search_tree.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 二分探索木
|
||||
|
||||
下図に示すように、<u>二分探索木</u>は以下の条件を満たします。
|
||||
|
||||
1. 根ノードについて、左部分木のすべてのノードの値 $<$ 根ノードの値 $<$ 右部分木のすべてのノードの値。
|
||||
2. 任意のノードの左と右の部分木も二分探索木です。つまり、条件`1.`も満たします。
|
||||
|
||||

|
||||
|
||||
## 二分探索木の操作
|
||||
|
||||
二分探索木をクラス`BinarySearchTree`としてカプセル化し、木の根ノードを指すメンバー変数`root`を宣言します。
|
||||
|
||||
### ノードの検索
|
||||
|
||||
ターゲットノード値`num`が与えられた場合、二分探索木の性質に従って検索できます。下図に示すように、ノード`cur`を宣言し、二分木の根ノード`root`から開始し、ノード値`cur.val`と`num`のサイズを比較するループを行います。
|
||||
|
||||
- `cur.val < num`の場合、ターゲットノードは`cur`の右部分木にあることを意味するため、`cur = cur.right`を実行します。
|
||||
- `cur.val > num`の場合、ターゲットノードは`cur`の左部分木にあることを意味するため、`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`があった場所に配置します。
|
||||
|
||||

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

|
||||
|
||||
## 二分探索木の一般的な応用
|
||||
|
||||
- システムでの多レベルインデックスとして使用され、効率的な検索、挿入、削除操作を実装します。
|
||||
- 特定の検索アルゴリズムの基盤となるデータ構造として機能します。
|
||||
- データストリームを格納して、その順序付けされた状態を維持するために使用されます。
|
||||
BIN
ja/docs/chapter_tree/binary_tree.assets/balanced_binary_tree.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
BIN
ja/docs/chapter_tree/binary_tree.assets/complete_binary_tree.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
ja/docs/chapter_tree/binary_tree.assets/full_binary_tree.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
ja/docs/chapter_tree/binary_tree.assets/perfect_binary_tree.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
654
ja/docs/chapter_tree/binary_tree.md
Normal file
@@ -0,0 +1,654 @@
|
||||
# 二分木
|
||||
|
||||
<u>二分木</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 # 右の子ノードへの参照
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 二分木ノード */
|
||||
struct TreeNode {
|
||||
int val; // ノード値
|
||||
TreeNode *left; // 左の子ノードへのポインタ
|
||||
TreeNode *right; // 右の子ノードへのポインタ
|
||||
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
|
||||
};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 二分木ノード */
|
||||
class TreeNode {
|
||||
int val; // ノード値
|
||||
TreeNode left; // 左の子ノードへの参照
|
||||
TreeNode right; // 右の子ノードへの参照
|
||||
TreeNode(int x) { val = x; }
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 二分木ノード */
|
||||
class TreeNode(int? x) {
|
||||
public int? val = x; // ノード値
|
||||
public TreeNode? left; // 左の子ノードへの参照
|
||||
public TreeNode? right; // 右の子ノードへの参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 二分木ノード */
|
||||
type TreeNode struct {
|
||||
Val int
|
||||
Left *TreeNode
|
||||
Right *TreeNode
|
||||
}
|
||||
/* コンストラクタ */
|
||||
func NewTreeNode(v int) *TreeNode {
|
||||
return &TreeNode{
|
||||
Left: nil, // 左の子ノードへのポインタ
|
||||
Right: nil, // 右の子ノードへのポインタ
|
||||
Val: v, // ノード値
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 二分木ノード */
|
||||
class TreeNode {
|
||||
var val: Int // ノード値
|
||||
var left: TreeNode? // 左の子ノードへの参照
|
||||
var right: TreeNode? // 右の子ノードへの参照
|
||||
|
||||
init(x: Int) {
|
||||
val = x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* 二分木ノード */
|
||||
class TreeNode {
|
||||
val; // ノード値
|
||||
left; // 左の子ノードへのポインタ
|
||||
right; // 右の子ノードへのポインタ
|
||||
constructor(val, left, right) {
|
||||
this.val = val === undefined ? 0 : val;
|
||||
this.left = left === undefined ? null : left;
|
||||
this.right = right === undefined ? null : right;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* 二分木ノード */
|
||||
class TreeNode {
|
||||
val: number;
|
||||
left: TreeNode | null;
|
||||
right: TreeNode | null;
|
||||
|
||||
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; // 右の子ノードへの参照
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 二分木ノード */
|
||||
class TreeNode {
|
||||
int val; // ノード値
|
||||
TreeNode? left; // 左の子ノードへの参照
|
||||
TreeNode? right; // 右の子ノードへの参照
|
||||
TreeNode(this.val, [this.left, this.right]);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/* 二分木ノード */
|
||||
struct TreeNode {
|
||||
val: i32, // ノード値
|
||||
left: Option<Rc<RefCell<TreeNode>>>, // 左の子ノードへの参照
|
||||
right: Option<Rc<RefCell<TreeNode>>>, // 右の子ノードへの参照
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
/* コンストラクタ */
|
||||
fn new(val: i32) -> Rc<RefCell<Self>> {
|
||||
Rc::new(RefCell::new(Self {
|
||||
val,
|
||||
left: None,
|
||||
right: None
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* 二分木ノード */
|
||||
typedef struct TreeNode {
|
||||
int val; // ノード値
|
||||
int height; // ノードの高さ
|
||||
struct TreeNode *left; // 左の子ノードへのポインタ
|
||||
struct TreeNode *right; // 右の子ノードへのポインタ
|
||||
} TreeNode;
|
||||
|
||||
/* コンストラクタ */
|
||||
TreeNode *newTreeNode(int val) {
|
||||
TreeNode *node;
|
||||
|
||||
node = (TreeNode *)malloc(sizeof(TreeNode));
|
||||
node->val = val;
|
||||
node->height = 0;
|
||||
node->left = NULL;
|
||||
node->right = NULL;
|
||||
return node;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* 二分木ノード */
|
||||
class TreeNode(val _val: Int) { // ノード値
|
||||
val left: TreeNode? = null // 左の子ノードへの参照
|
||||
val right: TreeNode? = null // 右の子ノードへの参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
各ノードは2つの参照(ポインタ)を持ち、それぞれ<u>左の子ノード</u>と<u>右の子ノード</u>を指しています。このノードは、これら2つの子ノードの<u>親ノード</u>と呼ばれます。二分木のノードが与えられたとき、このノードの左の子とその下にあるすべてのノードで形成される木を、このノードの<u>左部分木</u>と呼びます。同様に、<u>右部分木</u>も定義できます。
|
||||
|
||||
**二分木では、葉ノードを除いて、他のすべてのノードは子ノードと空でない部分木を含みます。** 下図に示すように、「ノード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>:最も遠い葉ノードからそのノードまでの辺の数です。
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
「高さ」と「深さ」は通常「通過する辺の数」として定義しますが、一部の問題や教科書では「通過するノードの数」として定義されることがあります。この場合、高さと深さの両方を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
|
||||
n2.right = n5
|
||||
```
|
||||
|
||||
=== "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;
|
||||
n2->right = n5;
|
||||
```
|
||||
|
||||
=== "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;
|
||||
n2.right = n5;
|
||||
```
|
||||
|
||||
=== "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;
|
||||
n2.right = n5;
|
||||
```
|
||||
|
||||
=== "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
|
||||
n2.Right = n5
|
||||
```
|
||||
|
||||
=== "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
|
||||
n2.right = n5
|
||||
```
|
||||
|
||||
=== "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;
|
||||
n2.right = n5;
|
||||
```
|
||||
|
||||
=== "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;
|
||||
n2.right = n5;
|
||||
```
|
||||
|
||||
=== "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;
|
||||
n2.right = n5;
|
||||
```
|
||||
|
||||
=== "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);
|
||||
n2.borrow_mut().right = Some(n5);
|
||||
```
|
||||
|
||||
=== "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;
|
||||
n2->right = n5;
|
||||
```
|
||||
|
||||
=== "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
|
||||
n2.right = n5
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_tree.rb"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_tree.zig"
|
||||
|
||||
```
|
||||
|
||||
### ノードの挿入と削除
|
||||
|
||||
連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。下図に例を示します。
|
||||
|
||||

|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="binary_tree.py"
|
||||
# ノードの挿入と削除
|
||||
p = TreeNode(0)
|
||||
# n1 -> n2の間にノードPを挿入
|
||||
n1.left = p
|
||||
p.left = n2
|
||||
# ノードPを削除
|
||||
n1.left = n2
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="binary_tree.cpp"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode* P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1->left = P;
|
||||
P->left = n2;
|
||||
// ノードPを削除
|
||||
n1->left = n2;
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_tree.java"
|
||||
TreeNode P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_tree.cs"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode P = new(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_tree.go"
|
||||
/* ノードの挿入と削除 */
|
||||
// n1とn2の間にノードPを挿入
|
||||
p := NewTreeNode(0)
|
||||
n1.Left = p
|
||||
p.Left = n2
|
||||
// ノードPを削除
|
||||
n1.Left = n2
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_tree.swift"
|
||||
let P = TreeNode(x: 0)
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.left = P
|
||||
P.left = n2
|
||||
// ノードPを削除
|
||||
n1.left = n2
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_tree.js"
|
||||
/* ノードの挿入と削除 */
|
||||
let P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_tree.ts"
|
||||
/* ノードの挿入と削除 */
|
||||
const P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_tree.dart"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode P = new TreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.left = P;
|
||||
P.left = n2;
|
||||
// ノードPを削除
|
||||
n1.left = n2;
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_tree.rs"
|
||||
let p = TreeNode::new(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.borrow_mut().left = Some(p.clone());
|
||||
p.borrow_mut().left = Some(n2.clone());
|
||||
// ノードPを削除
|
||||
n1.borrow_mut().left = Some(n2);
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_tree.c"
|
||||
/* ノードの挿入と削除 */
|
||||
TreeNode *P = newTreeNode(0);
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1->left = P;
|
||||
P->left = n2;
|
||||
// ノードPを削除
|
||||
n1->left = n2;
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_tree.kt"
|
||||
val P = TreeNode(0)
|
||||
// n1とn2の間にノードPを挿入
|
||||
n1.left = P
|
||||
P.left = n2
|
||||
// ノードPを削除
|
||||
n1.left = n2
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_tree.rb"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_tree.zig"
|
||||
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
ノードの挿入は二分木の元の論理構造を変更する可能性があり、ノードの削除は通常そのノードとそのすべての部分木を削除することになることに注意してください。したがって、二分木では、挿入と削除は通常一連の操作を通じて実行され、意味のある結果を得ます。
|
||||
|
||||
## 二分木の一般的な種類
|
||||
|
||||
### 完全二分木
|
||||
|
||||
下図に示すように、<u>完全二分木</u>では、すべてのレベルがノードで完全に埋められています。完全二分木では、葉ノードの次数は$0$で、他のすべてのノードの次数は$2$です。ノードの総数は$2^{h+1} - 1$として計算でき、ここで$h$は木の高さです。これは標準的な指数関係を示し、自然界の細胞分裂の一般的な現象を反映しています。
|
||||
|
||||
!!! tip
|
||||
|
||||
中国語圏では、完全二分木はしばしば<u>満二分木</u>と呼ばれることに注意してください。
|
||||
|
||||

|
||||
|
||||
### 完備二分木
|
||||
|
||||
下図に示すように、<u>完備二分木</u>は、最下位レベルのみが完全に埋められていない可能性がある二分木で、最下位レベルのノードは左から右に連続して埋められる必要があります。完全二分木は完備二分木でもあることに注意してください。
|
||||
|
||||

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

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

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

|
||||
|
||||
下表に示すように、最良と最悪の構造では、二分木は葉ノード数、総ノード数、高さの最大値または最小値を達成します。
|
||||
|
||||
<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$ |
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
89
ja/docs/chapter_tree/binary_tree_traversal.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 二分木の走査
|
||||
|
||||
物理的構造の観点から見ると、木は連結リストに基づくデータ構造です。したがって、その走査方法はポインタを通してノードに一つずつアクセスすることを含みます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑で、検索アルゴリズムの支援が必要です。
|
||||
|
||||
二分木の一般的な走査方法には、レベル順走査、前順走査、中順走査、後順走査があります。
|
||||
|
||||
## レベル順走査
|
||||
|
||||
下図に示すように、<u>レベル順走査</u>は二分木を上から下へ、層ごとに走査します。各レベル内では、左から右へノードを訪問します。
|
||||
|
||||
レベル順走査は本質的に<u>幅優先走査</u>の一種で、<u>幅優先探索(BFS)</u>とも呼ばれ、「周囲に向かって外向きに拡張する」層ごとの走査方法を体現しています。
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
### 計算量分析
|
||||
|
||||
- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間を使用します。
|
||||
- **空間計算量は$O(n)$**: 最悪の場合、つまり木が連結リストに退化した場合、再帰の深さは$n$に達し、システムは$O(n)$のスタックフレーム空間を占有します。
|
||||
9
ja/docs/chapter_tree/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 木
|
||||
|
||||

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