This commit is contained in:
krahets
2025-10-17 05:33:23 +08:00
parent 9278f3c659
commit 68bb9afb16
113 changed files with 35936 additions and 0 deletions
@@ -0,0 +1,501 @@
---
comments: true
---
# 7.3   二分木の配列表現
連結リスト表現では、二分木の格納単位はノード`TreeNode`であり、ノードはポインタによって接続されます。連結リスト表現での二分木の基本操作については前の節で紹介しました。
では、配列を使って二分木を表現することはできるでしょうか?答えはイエスです。
## 7.3.1   完全二分木の表現
まず簡単なケースから分析してみましょう。完全二分木が与えられたとき、レベル順探索の順序に従ってすべてのノードを配列に格納し、各ノードは一意の配列インデックスに対応します。
レベル順探索の特性に基づいて、親ノードのインデックスとその子ノードの間の「マッピング公式」を導き出すことができます:**ノードのインデックスが$i$の場合、その左の子のインデックスは$2i + 1$、右の子のインデックスは$2i + 2$です**。下図は、さまざまなノードのインデックス間のマッピング関係を示しています。
![完全二分木の配列表現](array_representation_of_tree.assets/array_representation_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-12 &nbsp; 完全二分木の配列表現 </p>
**マッピング公式は、連結リストのノード参照(ポインタ)と同様の役割を果たします**。配列内の任意のノードが与えられたとき、マッピング公式を使用してその左(右)の子ノードにアクセスできます。
## 7.3.2 &nbsp; 任意の二分木の表現
完全二分木は特別なケースです。二分木の中間レベルには多くの`None`値が存在することがよくあります。レベル順探索のシーケンスにはこれらの`None`値が含まれないため、このシーケンスだけに依存して`None`値の数と分布を推測することはできません。**つまり、複数の二分木構造が同じレベル順探索シーケンスと一致する可能性があります**。
下図に示すように、完全でない二分木が与えられた場合、上記の配列表現方法は失敗します。
![レベル順探索シーケンスが複数の二分木の可能性に対応](array_representation_of_tree.assets/array_representation_without_empty.png){ class="animation-figure" }
<p align="center"> 図 7-13 &nbsp; レベル順探索シーケンスが複数の二分木の可能性に対応 </p>
この問題を解決するために、**レベル順探索シーケンスですべての`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=""
```
![任意の種類の二分木の配列表現](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" }
<p align="center"> 図 7-14 &nbsp; 任意の種類の二分木の配列表現 </p>
注目すべきは、**完備二分木は配列表現に非常に適している**ということです。完備二分木の定義を思い出すと、`None`は最下位レベルでのみ、かつ右側に向かって現れます。**つまり、すべての`None`値は確実にレベル順探索シーケンスの最後に現れます**。
これは、配列を使用して完備二分木を表現する際、すべての`None`値の格納を省略できることを意味し、非常に便利です。下図に例を示します。
![完備二分木の配列表現](array_representation_of_tree.assets/array_representation_complete_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-15 &nbsp; 完備二分木の配列表現 </p>
以下のコードは、配列表現に基づく二分木を実装し、次の操作を含みます:
- ノードが与えられたとき、その値、左(右)の子ノード、および親ノードを取得する。
- 前順、中順、後順、およびレベル順探索シーケンスを取得する。
=== "Python"
```python title="array_binary_tree.py"
class ArrayBinaryTree:
"""配列ベースの二分木クラス"""
def __init__(self, arr: list[int | None]):
"""コンストラクタ"""
self._tree = list(arr)
def size(self):
"""リストの容量"""
return len(self._tree)
def val(self, i: int) -> int | None:
"""インデックスiのノードの値を取得"""
# インデックスが範囲外の場合、Noneを返し、空席を表す
if i < 0 or i >= self.size():
return None
return self._tree[i]
def left(self, i: int) -> int | None:
"""インデックスiのノードの左の子のインデックスを取得"""
return 2 * i + 1
def right(self, i: int) -> int | None:
"""インデックスiのノードの右の子のインデックスを取得"""
return 2 * i + 2
def parent(self, i: int) -> int | None:
"""インデックスiのノードの親のインデックスを取得"""
return (i - 1) // 2
def level_order(self) -> list[int]:
"""レベル順走査"""
self.res = []
# 配列を走査
for i in range(self.size()):
if self.val(i) is not None:
self.res.append(self.val(i))
return self.res
def dfs(self, i: int, order: str):
"""深さ優先走査"""
if self.val(i) is None:
return
# 前順走査
if order == "pre":
self.res.append(self.val(i))
self.dfs(self.left(i), order)
# 中順走査
if order == "in":
self.res.append(self.val(i))
self.dfs(self.right(i), order)
# 後順走査
if order == "post":
self.res.append(self.val(i))
def pre_order(self) -> list[int]:
"""前順走査"""
self.res = []
self.dfs(0, order="pre")
return self.res
def in_order(self) -> list[int]:
"""中順走査"""
self.res = []
self.dfs(0, order="in")
return self.res
def post_order(self) -> list[int]:
"""後順走査"""
self.res = []
self.dfs(0, order="post")
return self.res
```
=== "C++"
```cpp title="array_binary_tree.cpp"
/* 配列ベースの二分木クラス */
class ArrayBinaryTree {
public:
/* コンストラクタ */
ArrayBinaryTree(vector<int> arr) {
tree = arr;
}
/* リストの容量 */
int size() {
return tree.size();
}
/* インデックス i のノードの値を取得 */
int val(int i) {
// インデックスが範囲外の場合、INT_MAX を返す(null を表す)
if (i < 0 || i >= size())
return INT_MAX;
return tree[i];
}
/* インデックス i のノードの左の子のインデックスを取得 */
int left(int i) {
return 2 * i + 1;
}
/* インデックス i のノードの右の子のインデックスを取得 */
int right(int i) {
return 2 * i + 2;
}
/* インデックス i のノードの親のインデックスを取得 */
int parent(int i) {
return (i - 1) / 2;
}
/* レベル順走査 */
vector<int> levelOrder() {
vector<int> res;
// 配列を走査
for (int i = 0; i < size(); i++) {
if (val(i) != INT_MAX)
res.push_back(val(i));
}
return res;
}
/* 前順走査 */
vector<int> preOrder() {
vector<int> res;
dfs(0, "pre", res);
return res;
}
/* 中順走査 */
vector<int> inOrder() {
vector<int> res;
dfs(0, "in", res);
return res;
}
/* 後順走査 */
vector<int> postOrder() {
vector<int> res;
dfs(0, "post", res);
return res;
}
private:
vector<int> tree;
/* 深さ優先走査 */
void dfs(int i, string order, vector<int> &res) {
// 空の位置の場合、戻る
if (val(i) == INT_MAX)
return;
// 前順走査
if (order == "pre")
res.push_back(val(i));
dfs(left(i), order, res);
// 中順走査
if (order == "in")
res.push_back(val(i));
dfs(right(i), order, res);
// 後順走査
if (order == "post")
res.push_back(val(i));
}
};
```
=== "Java"
```java title="array_binary_tree.java"
/* 配列ベースの二分木クラス */
class ArrayBinaryTree {
private List<Integer> tree;
/* コンストラクタ */
public ArrayBinaryTree(List<Integer> arr) {
tree = new ArrayList<>(arr);
}
/* リストの容量 */
public int size() {
return tree.size();
}
/* インデックス i のノードの値を取得 */
public Integer val(int i) {
// インデックスが範囲外の場合、null を返す(空の位置を表す)
if (i < 0 || i >= size())
return null;
return tree.get(i);
}
/* インデックス i のノードの左の子のインデックスを取得 */
public Integer left(int i) {
return 2 * i + 1;
}
/* インデックス i のノードの右の子のインデックスを取得 */
public Integer right(int i) {
return 2 * i + 2;
}
/* インデックス i のノードの親のインデックスを取得 */
public Integer parent(int i) {
return (i - 1) / 2;
}
/* レベル順走査 */
public List<Integer> levelOrder() {
List<Integer> res = new ArrayList<>();
// 配列を走査
for (int i = 0; i < size(); i++) {
if (val(i) != null)
res.add(val(i));
}
return res;
}
/* 深さ優先走査 */
private void dfs(Integer i, String order, List<Integer> res) {
// 空の位置の場合、戻る
if (val(i) == null)
return;
// 前順走査
if ("pre".equals(order))
res.add(val(i));
dfs(left(i), order, res);
// 中順走査
if ("in".equals(order))
res.add(val(i));
dfs(right(i), order, res);
// 後順走査
if ("post".equals(order))
res.add(val(i));
}
/* 前順走査 */
public List<Integer> preOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "pre", res);
return res;
}
/* 中順走査 */
public List<Integer> inOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "in", res);
return res;
}
/* 後順走査 */
public List<Integer> postOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "post", res);
return res;
}
}
```
=== "C#"
```csharp title="array_binary_tree.cs"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Go"
```go title="array_binary_tree.go"
[class]{arrayBinaryTree}-[func]{}
```
=== "Swift"
```swift title="array_binary_tree.swift"
[class]{ArrayBinaryTree}-[func]{}
```
=== "JS"
```javascript title="array_binary_tree.js"
[class]{ArrayBinaryTree}-[func]{}
```
=== "TS"
```typescript title="array_binary_tree.ts"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Dart"
```dart title="array_binary_tree.dart"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Rust"
```rust title="array_binary_tree.rs"
[class]{ArrayBinaryTree}-[func]{}
```
=== "C"
```c title="array_binary_tree.c"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Kotlin"
```kotlin title="array_binary_tree.kt"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Ruby"
```ruby title="array_binary_tree.rb"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Zig"
```zig title="array_binary_tree.zig"
[class]{ArrayBinaryTree}-[func]{}
```
## 7.3.3 &nbsp; 利点と制限
二分木の配列表現には以下の利点があります:
- 配列は連続したメモリ空間に格納されるため、キャッシュフレンドリーで、より高速なアクセスと探索が可能です。
- ポインタを格納する必要がないため、スペースを節約できます。
- ノードへのランダムアクセスが可能です。
しかし、配列表現にはいくつかの制限もあります:
- 配列格納には連続したメモリ空間が必要なため、大量のデータを持つ木の格納には適していません。
- ノードの追加や削除には配列の挿入や削除操作が必要で、効率が低くなります。
- 二分木に多くの`None`値がある場合、配列に含まれるノードデータの割合が低くなり、空間利用率が低下します。
File diff suppressed because it is too large Load Diff
+664
View File
@@ -0,0 +1,664 @@
---
comments: true
---
# 7.4 &nbsp; 二分探索木
下図に示すように、<u>二分探索木</u>は以下の条件を満たします。
1. 根ノードについて、左部分木のすべてのノードの値 $<$ 根ノードの値 $<$ 右部分木のすべてのノードの値。
2. 任意のノードの左と右の部分木も二分探索木です。つまり、条件`1.`も満たします。
![二分探索木](binary_search_tree.assets/binary_search_tree.png){ class="animation-figure" }
<p align="center"> 図 7-16 &nbsp; 二分探索木 </p>
## 7.4.1 &nbsp; 二分探索木の操作
二分探索木をクラス`BinarySearchTree`としてカプセル化し、木の根ノードを指すメンバー変数`root`を宣言します。
### 1. &nbsp; ノードの検索
ターゲットノード値`num`が与えられた場合、二分探索木の性質に従って検索できます。下図に示すように、ノード`cur`を宣言し、二分木の根ノード`root`から開始し、ノード値`cur.val``num`のサイズを比較するループを行います。
- `cur.val < num`の場合、ターゲットノードは`cur`の右部分木にあることを意味するため、`cur = cur.right`を実行します。
- `cur.val > num`の場合、ターゲットノードは`cur`の左部分木にあることを意味するため、`cur = cur.left`を実行します。
- `cur.val = num`の場合、ターゲットノードが見つかったことを意味するため、ループを終了してノードを返します。
=== "<1>"
![二分探索木でのノード検索例](binary_search_tree.assets/bst_search_step1.png){ class="animation-figure" }
=== "<2>"
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png){ class="animation-figure" }
=== "<3>"
![bst_search_step3](binary_search_tree.assets/bst_search_step3.png){ class="animation-figure" }
=== "<4>"
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png){ class="animation-figure" }
<p align="center"> 図 7-17 &nbsp; 二分探索木でのノード検索例 </p>
二分探索木での検索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドでケースの半分を排除します。ループ数は最大で二分木の高さです。二分木が平衡している場合、$O(\log n)$の時間を使用します。コード例は以下の通りです:
=== "Python"
```python title="binary_search_tree.py"
def search(self, num: int) -> TreeNode | None:
"""ノードを探索"""
cur = self._root
# ループで探索、葉ノードを通過した後にブレーク
while cur is not None:
# ターゲットノードはcurの右部分木にある
if cur.val < num:
cur = cur.right
# ターゲットノードはcurの左部分木にある
elif cur.val > num:
cur = cur.left
# ターゲットノードを発見、ループをブレーク
else:
break
return cur
```
=== "C++"
```cpp title="binary_search_tree.cpp"
/* ノードを検索 */
TreeNode *search(int num) {
TreeNode *cur = root;
// ループで検索、葉ノードを通り過ぎたら終了
while (cur != nullptr) {
// 目標ノードはcurの右部分木にある
if (cur->val < num)
cur = cur->right;
// 目標ノードはcurの左部分木にある
else if (cur->val > num)
cur = cur->left;
// 目標ノードを見つけた、ループを抜ける
else
break;
}
// 目標ノードを返す
return cur;
}
```
=== "Java"
```java title="binary_search_tree.java"
/* ノードを検索 */
TreeNode search(int num) {
TreeNode cur = root;
// ループで検索、葉ノードを通過後に終了
while (cur != null) {
// 対象ノードは cur の右部分木にある
if (cur.val < num)
cur = cur.right;
// 対象ノードは cur の左部分木にある
else if (cur.val > num)
cur = cur.left;
// 対象ノードを見つけた、ループを終了
else
break;
}
// 対象ノードを返す
return cur;
}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{Search}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{search}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{search}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{search}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{search}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{search}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{search}
```
=== "C"
```c title="binary_search_tree.c"
[class]{BinarySearchTree}-[func]{search}
```
=== "Kotlin"
```kotlin title="binary_search_tree.kt"
[class]{BinarySearchTree}-[func]{search}
```
=== "Ruby"
```ruby title="binary_search_tree.rb"
[class]{BinarySearchTree}-[func]{search}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{search}
```
### 2. &nbsp; ノードの挿入
挿入する要素`num`が与えられた場合、二分探索木の性質「左部分木 < 根ノード < 右部分木」を維持するため、挿入操作は下図に示すように進行します。
1. **挿入位置を見つける**: 検索操作と同様に、根ノードから開始し、現在のノード値と`num`のサイズ関係に従って下向きにループし、葉ノードを通過(`None`に走査)するまで、ループを終了します。
2. **この位置にノードを挿入**: ノード`num`を初期化し、`None`があった場所に配置します。
![二分探索木へのノード挿入](binary_search_tree.assets/bst_insert.png){ class="animation-figure" }
<p align="center"> 図 7-18 &nbsp; 二分探索木へのノード挿入 </p>
コード実装では、以下の2点に注意してください。
- 二分探索木は重複ノードの存在を許可しません。そうでなければ、その定義に違反します。したがって、挿入するノードが既に木に存在する場合、挿入は実行されず、ノードは直接戻ります。
- 挿入操作を実行するには、前のループからのノードを保存するためにノード`pre`を使用する必要があります。このようにして、`None`に走査したときに、その親ノードを取得でき、ノード挿入操作を完了できます。
=== "Python"
```python title="binary_search_tree.py"
def insert(self, num: int):
"""ノードを挿入"""
# 木が空の場合、ルートノードを初期化
if self._root is None:
self._root = TreeNode(num)
return
# ループで探索、葉ノードを通過した後にブレーク
cur, pre = self._root, None
while cur is not None:
# 重複ノードを発見したため、戻る
if cur.val == num:
return
pre = cur
# 挿入位置はcurの右部分木にある
if cur.val < num:
cur = cur.right
# 挿入位置はcurの左部分木にある
else:
cur = cur.left
# ノードを挿入
node = TreeNode(num)
if pre.val < num:
pre.right = node
else:
pre.left = node
```
=== "C++"
```cpp title="binary_search_tree.cpp"
/* ノードを挿入 */
void insert(int num) {
// 木が空の場合、ルートノードを初期化
if (root == nullptr) {
root = new TreeNode(num);
return;
}
TreeNode *cur = root, *pre = nullptr;
// ループで検索、葉ノードを通り過ぎたら終了
while (cur != nullptr) {
// 重複ノードを見つけた場合、戻る
if (cur->val == num)
return;
pre = cur;
// 挿入位置はcurの右部分木にある
if (cur->val < num)
cur = cur->right;
// 挿入位置はcurの左部分木にある
else
cur = cur->left;
}
// ノードを挿入
TreeNode *node = new TreeNode(num);
if (pre->val < num)
pre->right = node;
else
pre->left = node;
}
```
=== "Java"
```java title="binary_search_tree.java"
/* ノードを挿入 */
void insert(int num) {
// 木が空の場合、根ノードを初期化
if (root == null) {
root = new TreeNode(num);
return;
}
TreeNode cur = root, pre = null;
// ループで検索、葉ノードを通過後に終了
while (cur != null) {
// 重複ノードを見つけた場合、戻る
if (cur.val == num)
return;
pre = cur;
// 挿入位置は cur の右部分木にある
if (cur.val < num)
cur = cur.right;
// 挿入位置は cur の左部分木にある
else
cur = cur.left;
}
// ノードを挿入
TreeNode node = new TreeNode(num);
if (pre.val < num)
pre.right = node;
else
pre.left = node;
}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{Insert}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{insert}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{insert}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{insert}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{insert}
```
=== "C"
```c title="binary_search_tree.c"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Kotlin"
```kotlin title="binary_search_tree.kt"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Ruby"
```ruby title="binary_search_tree.rb"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{insert}
```
ノードの検索と同様に、ノードの挿入には$O(\log n)$の時間を使用します。
### 3. &nbsp; ノードの削除
まず、二分木でターゲットノードを見つけ、それを削除します。ノードの挿入と同様に、削除操作が完了した後も、二分探索木の性質「左部分木 < 根ノード < 右部分木」が満たされることを保証する必要があります。したがって、ターゲットノードの子ノード数に基づいて、0、1、2の3つのケースに分け、対応するノード削除操作を実行します。
下図に示すように、削除するノードの次数が$0$の場合、そのノードは葉ノードであることを意味し、直接削除できます。
![二分探索木でのノード削除(次数0](binary_search_tree.assets/bst_remove_case1.png){ class="animation-figure" }
<p align="center"> 図 7-19 &nbsp; 二分探索木でのノード削除(次数0) </p>
下図に示すように、削除するノードの次数が$1$の場合、削除するノードをその子ノードで置き換えるだけで十分です。
![二分探索木でのノード削除(次数1](binary_search_tree.assets/bst_remove_case2.png){ class="animation-figure" }
<p align="center"> 図 7-20 &nbsp; 二分探索木でのノード削除(次数1) </p>
削除するノードの次数が$2$の場合、直接削除することはできませんが、ノードを使用して置き換える必要があります。二分探索木の性質「左部分木 $<$ 根ノード $<$ 右部分木」を維持するため、**このノードは右部分木の最小ノードまたは左部分木の最大ノードのいずれかです**。
右部分木の最小ノード(中順走査での次のノード)を選択すると仮定すると、削除操作は下図に示すように進行します。
1. 削除するノードの「中順走査シーケンス」での次のノードを見つけ、`tmp`として示します。
2. 削除するノードの値を`tmp`の値で置き換え、木内でノード`tmp`を再帰的に削除します。
=== "<1>"
![二分探索木でのノード削除(次数2](binary_search_tree.assets/bst_remove_case3_step1.png){ class="animation-figure" }
=== "<2>"
![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png){ class="animation-figure" }
=== "<3>"
![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png){ class="animation-figure" }
=== "<4>"
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png){ class="animation-figure" }
<p align="center"> 図 7-21 &nbsp; 二分探索木でのノード削除(次数2) </p>
ノードを削除する操作も$O(\log n)$の時間を使用します。削除するノードを見つけるのに$O(\log n)$の時間が必要で、中順走査の後継ノードを取得するのに$O(\log n)$の時間が必要です。コード例は以下の通りです:
=== "Python"
```python title="binary_search_tree.py"
def remove(self, num: int):
"""ノードを削除"""
# 木が空の場合、戻る
if self._root is None:
return
# ループで探索、葉ノードを通過した後にブレーク
cur, pre = self._root, None
while cur is not None:
# 削除するノードを発見、ループをブレーク
if cur.val == num:
break
pre = cur
# 削除するノードはcurの右部分木にある
if cur.val < num:
cur = cur.right
# 削除するノードはcurの左部分木にある
else:
cur = cur.left
# 削除するノードが存在しない場合、戻る
if cur is None:
return
# 子ノード数 = 0 または 1
if cur.left is None or cur.right is None:
# 子ノード数 = 0/1の場合、child = null/その子ノード
child = cur.left or cur.right
# ノードcurを削除
if cur != self._root:
if pre.left == cur:
pre.left = child
else:
pre.right = child
else:
# 削除されるノードがルートの場合、ルートを再割り当て
self._root = child
# 子ノード数 = 2
else:
# curの中順走査の次のノードを取得
tmp: TreeNode = cur.right
while tmp.left is not None:
tmp = tmp.left
# 再帰的にノードtmpを削除
self.remove(tmp.val)
# curをtmpで置き換え
cur.val = tmp.val
```
=== "C++"
```cpp title="binary_search_tree.cpp"
/* ノードを削除 */
void remove(int num) {
// 木が空の場合、戻る
if (root == nullptr)
return;
TreeNode *cur = root, *pre = nullptr;
// ループで検索、葉ノードを通り過ぎたら終了
while (cur != nullptr) {
// 削除するノードを見つけた、ループを抜ける
if (cur->val == num)
break;
pre = cur;
// 削除するノードはcurの右部分木にある
if (cur->val < num)
cur = cur->right;
// 削除するノードはcurの左部分木にある
else
cur = cur->left;
}
// 削除するノードがない場合、戻る
if (cur == nullptr)
return;
// 子ノード数 = 0 または 1
if (cur->left == nullptr || cur->right == nullptr) {
// 子ノード数 = 0 / 1の場合、child = nullptr / その子ノード
TreeNode *child = cur->left != nullptr ? cur->left : cur->right;
// ノードcurを削除
if (cur != root) {
if (pre->left == cur)
pre->left = child;
else
pre->right = child;
} else {
// 削除されるノードがルートの場合、ルートを再割り当て
root = child;
}
// メモリを解放
delete cur;
}
// 子ノード数 = 2
else {
// curの中順走査の次のノードを取得
TreeNode *tmp = cur->right;
while (tmp->left != nullptr) {
tmp = tmp->left;
}
int tmpVal = tmp->val;
// ノードtmpを再帰的に削除
remove(tmp->val);
// curをtmpで置き換え
cur->val = tmpVal;
}
}
```
=== "Java"
```java title="binary_search_tree.java"
/* ノードを削除 */
void remove(int num) {
// 木が空の場合、戻る
if (root == null)
return;
TreeNode cur = root, pre = null;
// ループで検索、葉ノードを通過後に終了
while (cur != null) {
// 削除するノードを見つけた、ループを終了
if (cur.val == num)
break;
pre = cur;
// 削除するノードは cur の右部分木にある
if (cur.val < num)
cur = cur.right;
// 削除するノードは cur の左部分木にある
else
cur = cur.left;
}
// 削除するノードがない場合、戻る
if (cur == null)
return;
// 子ノード数 = 0 または 1
if (cur.left == null || cur.right == null) {
// 子ノード数 = 0/1 の場合、child = null/その子ノード
TreeNode child = cur.left != null ? cur.left : cur.right;
// ノード cur を削除
if (cur != root) {
if (pre.left == cur)
pre.left = child;
else
pre.right = child;
} else {
// 削除されるノードが根の場合、根を再割り当て
root = child;
}
}
// 子ノード数 = 2
else {
// cur の中順走査の次のノードを取得
TreeNode tmp = cur.right;
while (tmp.left != null) {
tmp = tmp.left;
}
// 再帰的にノード tmp を削除
remove(tmp.val);
// cur を tmp で置き換える
cur.val = tmp.val;
}
}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{Remove}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{remove}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{remove}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{remove}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{remove}
```
=== "C"
```c title="binary_search_tree.c"
[class]{BinarySearchTree}-[func]{removeItem}
```
=== "Kotlin"
```kotlin title="binary_search_tree.kt"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Ruby"
```ruby title="binary_search_tree.rb"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{remove}
```
### 4. &nbsp; 中順走査は順序付けされている
下図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の走査順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」のサイズ関係を満たします。
これは、二分探索木で中順走査を実行するときに、常に次に小さいノードが最初に走査されることを意味し、重要な性質につながります:**二分探索木の中順走査のシーケンスは昇順です**。
中順走査の昇順性質を使用して、二分探索木で順序付けされたデータを取得するには$O(n)$の時間のみが必要で、追加のソート操作は不要であり、非常に効率的です。
![二分探索木の中順走査シーケンス](binary_search_tree.assets/bst_inorder_traversal.png){ class="animation-figure" }
<p align="center"> 図 7-22 &nbsp; 二分探索木の中順走査シーケンス </p>
## 7.4.2 &nbsp; 二分探索木の効率
データのセットが与えられた場合、配列または二分探索木を使用して格納することを検討します。下の表を観察すると、二分探索木のすべての操作は対数時間計算量を持ち、安定して効率的です。配列は、頻繁な追加と検索や削除の頻度が少ないシナリオでのみ、二分探索木よりも効率的です。
<p align="center"> 表 7-2 &nbsp; 配列と探索木の効率比較 </p>
<div class="center-table" markdown>
| | 未ソート配列 | 二分探索木 |
| -------------- | -------------- | ------------------ |
| 要素の検索 | $O(n)$ | $O(\log n)$ |
| 要素の挿入 | $O(1)$ | $O(\log n)$ |
| 要素の削除 | $O(n)$ | $O(\log n)$ |
</div>
理想的には、二分探索木は「平衡」しており、任意のノードを$\log n$ループ内で見つけることができます。
しかし、二分探索木で継続的にノードを挿入および削除すると、下図に示すように連結リストに退化する可能性があり、さまざまな操作の時間計算量も$O(n)$に悪化します。
![二分探索木の退化](binary_search_tree.assets/bst_degradation.png){ class="animation-figure" }
<p align="center"> 図 7-23 &nbsp; 二分探索木の退化 </p>
## 7.4.3 &nbsp; 二分探索木の一般的な応用
- システムでの多レベルインデックスとして使用され、効率的な検索、挿入、削除操作を実装します。
- 特定の検索アルゴリズムの基盤となるデータ構造として機能します。
- データストリームを格納して、その順序付けされた状態を維持するために使用されます。
+678
View File
@@ -0,0 +1,678 @@
---
comments: true
---
# 7.1 &nbsp; 二分木
<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」とその下にあるすべてのノードで形成されます。
![親ノード、子ノード、部分木](binary_tree.assets/binary_tree_definition.png){ class="animation-figure" }
<p align="center"> 図 7-1 &nbsp; 親ノード、子ノード、部分木 </p>
## 7.1.1 &nbsp; 二分木の一般的な用語
二分木でよく使用される用語を下図に示します。
- <u>根ノード</u>:二分木の最上位レベルにあるノードで、親ノードを持ちません。
- <u>葉ノード</u>:子ノードを持たないノードで、両方のポインタが`None`を指しています。
- <u>辺</u>:2つのノードを結ぶ線分で、ノード間の参照(ポインタ)を表現します。
- ノードの<u>レベル</u>:上から下に向かって増加し、根ノードがレベル1です。
- ノードの<u>次数</u>:ノードが持つ子ノードの数です。二分木では、次数は0、1、または2になります。
- 二分木の<u>高さ</u>:根ノードから最も遠い葉ノードまでの辺の数です。
- ノードの<u>深さ</u>:根ノードからそのノードまでの辺の数です。
- ノードの<u>高さ</u>:最も遠い葉ノードからそのノードまでの辺の数です。
![二分木の一般的な用語](binary_tree.assets/binary_tree_terminology.png){ class="animation-figure" }
<p align="center"> 図 7-2 &nbsp; 二分木の一般的な用語 </p>
!!! tip
「高さ」と「深さ」は通常「通過する辺の数」として定義しますが、一部の問題や教科書では「通過するノードの数」として定義されることがあります。この場合、高さと深さの両方を1だけ増やす必要があります。
## 7.1.2 &nbsp; 二分木の基本操作
### 1. &nbsp; 二分木の初期化
連結リストと同様に、二分木の初期化では、まずノードを作成し、次にそれらの間の参照(ポインタ)を確立します。
=== "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"
```
### 2. &nbsp; ノードの挿入と削除
連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。下図に例を示します。
![二分木でのノードの挿入と削除](binary_tree.assets/binary_tree_add_remove.png){ class="animation-figure" }
<p align="center"> 図 7-3 &nbsp; 二分木でのノードの挿入と削除 </p>
=== "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
ノードの挿入は二分木の元の論理構造を変更する可能性があり、ノードの削除は通常そのノードとそのすべての部分木を削除することになることに注意してください。したがって、二分木では、挿入と削除は通常一連の操作を通じて実行され、意味のある結果を得ます。
## 7.1.3 &nbsp; 二分木の一般的な種類
### 1. &nbsp; 完全二分木
下図に示すように、<u>完全二分木</u>では、すべてのレベルがノードで完全に埋められています。完全二分木では、葉ノードの次数は$0$で、他のすべてのノードの次数は$2$です。ノードの総数は$2^{h+1} - 1$として計算でき、ここで$h$は木の高さです。これは標準的な指数関係を示し、自然界の細胞分裂の一般的な現象を反映しています。
!!! tip
中国語圏では、完全二分木はしばしば<u>満二分木</u>と呼ばれることに注意してください。
![完全二分木](binary_tree.assets/perfect_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-4 &nbsp; 完全二分木 </p>
### 2. &nbsp; 完備二分木
下図に示すように、<u>完備二分木</u>は、最下位レベルのみが完全に埋められていない可能性がある二分木で、最下位レベルのノードは左から右に連続して埋められる必要があります。完全二分木は完備二分木でもあることに注意してください。
![完備二分木](binary_tree.assets/complete_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-5 &nbsp; 完備二分木 </p>
### 3. &nbsp; 満二分木
下図に示すように、<u>満二分木</u>では、葉ノードを除いて、他のすべてのノードが2つの子ノードを持ちます。
![満二分木](binary_tree.assets/full_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-6 &nbsp; 満二分木 </p>
### 4. &nbsp; 平衡二分木
下図に示すように、<u>平衡二分木</u>では、任意のノードの左と右の部分木の高さの絶対差が1を超えません。
![平衡二分木](binary_tree.assets/balanced_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-7 &nbsp; 平衡二分木 </p>
## 7.1.4 &nbsp; 二分木の退化
下図は、二分木の理想的な構造と退化した構造を示しています。二分木は、すべてのレベルが埋められているときに「完全二分木」になり、すべてのノードが一方に偏っているときに「連結リスト」に退化します。
- 完全二分木は、二分木の「分割統治法」の利点を十分に活用できる理想的なシナリオです。
- 一方、連結リストは別の極端を表し、すべての操作が線形になり、時間計算量が$O(n)$になります。
![二分木の最良と最悪の構造](binary_tree.assets/binary_tree_best_worst_cases.png){ class="animation-figure" }
<p align="center"> 図 7-8 &nbsp; 二分木の最良と最悪の構造 </p>
下表に示すように、最良と最悪の構造では、二分木は葉ノード数、総ノード数、高さの最大値または最小値を達成します。
<p align="center"> 表 7-1 &nbsp; 二分木の最良と最悪の構造 </p>
<div class="center-table" markdown>
| | 完全二分木 | 連結リスト |
| ----------------------------------------------- | ------------------ | ----------- |
| レベル$i$のノード数 | $2^{i-1}$ | $1$ |
| 高さ$h$の木の葉ノード数 | $2^h$ | $1$ |
| 高さ$h$の木の総ノード数 | $2^{h+1} - 1$ | $h + 1$ |
| 総ノード数$n$の木の高さ | $\log_2 (n+1) - 1$ | $n - 1$ |
</div>
@@ -0,0 +1,431 @@
---
comments: true
---
# 7.2 &nbsp; 二分木の走査
物理的構造の観点から見ると、木は連結リストに基づくデータ構造です。したがって、その走査方法はポインタを通してノードに一つずつアクセスすることを含みます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑で、検索アルゴリズムの支援が必要です。
二分木の一般的な走査方法には、レベル順走査、前順走査、中順走査、後順走査があります。
## 7.2.1 &nbsp; レベル順走査
下図に示すように、<u>レベル順走査</u>は二分木を上から下へ、層ごとに走査します。各レベル内では、左から右へノードを訪問します。
レベル順走査は本質的に<u>幅優先走査</u>の一種で、<u>幅優先探索(BFS</u>とも呼ばれ、「周囲に向かって外向きに拡張する」層ごとの走査方法を体現しています。
![二分木のレベル順走査](binary_tree_traversal.assets/binary_tree_bfs.png){ class="animation-figure" }
<p align="center"> 図 7-9 &nbsp; 二分木のレベル順走査 </p>
### 1. &nbsp; コード実装
幅優先走査は通常「キュー」の助けを借りて実装されます。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとの進行」規則に従います。両者の基本的な考え方は一致しています。実装コードは以下の通りです:
=== "Python"
```python title="binary_tree_bfs.py"
def level_order(root: TreeNode | None) -> list[int]:
"""レベル順走査"""
# キューを初期化し、ルートノードを追加
queue: deque[TreeNode] = deque()
queue.append(root)
# 走査シーケンスを格納するリストを初期化
res = []
while queue:
node: TreeNode = queue.popleft() # キューからデキュー
res.append(node.val) # ノードの値を保存
if node.left is not None:
queue.append(node.left) # 左の子ノードをエンキュー
if node.right is not None:
queue.append(node.right) # 右の子ノードをエンキュー
return res
```
=== "C++"
```cpp title="binary_tree_bfs.cpp"
/* レベル順走査 */
vector<int> levelOrder(TreeNode *root) {
// キューを初期化、ルートノードを追加
queue<TreeNode *> queue;
queue.push(root);
// 走査順序を保存するリストを初期化
vector<int> vec;
while (!queue.empty()) {
TreeNode *node = queue.front();
queue.pop(); // キューからデキュー
vec.push_back(node->val); // ノード値を保存
if (node->left != nullptr)
queue.push(node->left); // 左の子ノードをエンキュー
if (node->right != nullptr)
queue.push(node->right); // 右の子ノードをエンキュー
}
return vec;
}
```
=== "Java"
```java title="binary_tree_bfs.java"
/* レベル順走査 */
List<Integer> levelOrder(TreeNode root) {
// キューを初期化し、根ノードを追加
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
// 走査順序を格納するリストを初期化
List<Integer> list = new ArrayList<>();
while (!queue.isEmpty()) {
TreeNode node = queue.poll(); // キューのデキュー
list.add(node.val); // ノードの値を保存
if (node.left != null)
queue.offer(node.left); // 左の子ノードをエンキュー
if (node.right != null)
queue.offer(node.right); // 右の子ノードをエンキュー
}
return list;
}
```
=== "C#"
```csharp title="binary_tree_bfs.cs"
[class]{binary_tree_bfs}-[func]{LevelOrder}
```
=== "Go"
```go title="binary_tree_bfs.go"
[class]{}-[func]{levelOrder}
```
=== "Swift"
```swift title="binary_tree_bfs.swift"
[class]{}-[func]{levelOrder}
```
=== "JS"
```javascript title="binary_tree_bfs.js"
[class]{}-[func]{levelOrder}
```
=== "TS"
```typescript title="binary_tree_bfs.ts"
[class]{}-[func]{levelOrder}
```
=== "Dart"
```dart title="binary_tree_bfs.dart"
[class]{}-[func]{levelOrder}
```
=== "Rust"
```rust title="binary_tree_bfs.rs"
[class]{}-[func]{level_order}
```
=== "C"
```c title="binary_tree_bfs.c"
[class]{}-[func]{levelOrder}
```
=== "Kotlin"
```kotlin title="binary_tree_bfs.kt"
[class]{}-[func]{levelOrder}
```
=== "Ruby"
```ruby title="binary_tree_bfs.rb"
[class]{}-[func]{level_order}
```
=== "Zig"
```zig title="binary_tree_bfs.zig"
[class]{}-[func]{levelOrder}
```
### 2. &nbsp; 計算量分析
- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間がかかります。ここで$n$はノード数です。
- **空間計算量は$O(n)$**: 最悪の場合、つまり完全二分木の場合、最下位レベルに走査する前に、キューは最大$(n + 1) / 2$個のノードを同時に含むことができ、$O(n)$の空間を占有します。
## 7.2.2 &nbsp; 前順、中順、後順走査
対応して、前順、中順、後順走査はすべて<u>深度優先走査</u>に属し、<u>深度優先探索(DFS</u>とも呼ばれ、「まず最後まで進み、その後バックトラックして続行する」走査方法を体現しています。
下図は二分木に対して深度優先走査を実行する動作原理を示しています。**深度優先走査は二分木全体を「歩き回る」ようなもので**、各ノードで3つの位置に遭遇し、それらは前順、中順、後順走査に対応しています。
![二分探索木の前順、中順、後順走査](binary_tree_traversal.assets/binary_tree_dfs.png){ class="animation-figure" }
<p align="center"> 図 7-10 &nbsp; 二分探索木の前順、中順、後順走査 </p>
### 1. &nbsp; コード実装
深度優先探索は通常再帰に基づいて実装されます:
=== "Python"
```python title="binary_tree_dfs.py"
def pre_order(root: TreeNode | None):
"""前順走査"""
if root is None:
return
# 訪問順序: ルートノード -> 左部分木 -> 右部分木
res.append(root.val)
pre_order(root=root.left)
pre_order(root=root.right)
def in_order(root: TreeNode | None):
"""中順走査"""
if root is None:
return
# 訪問順序: 左部分木 -> ルートノード -> 右部分木
in_order(root=root.left)
res.append(root.val)
in_order(root=root.right)
def post_order(root: TreeNode | None):
"""後順走査"""
if root is None:
return
# 訪問順序: 左部分木 -> 右部分木 -> ルートノード
post_order(root=root.left)
post_order(root=root.right)
res.append(root.val)
```
=== "C++"
```cpp title="binary_tree_dfs.cpp"
/* 前順走査 */
void preOrder(TreeNode *root) {
if (root == nullptr)
return;
// 訪問優先度:ルートノード -> 左部分木 -> 右部分木
vec.push_back(root->val);
preOrder(root->left);
preOrder(root->right);
}
/* 中順走査 */
void inOrder(TreeNode *root) {
if (root == nullptr)
return;
// 訪問優先度:左部分木 -> ルートノード -> 右部分木
inOrder(root->left);
vec.push_back(root->val);
inOrder(root->right);
}
/* 後順走査 */
void postOrder(TreeNode *root) {
if (root == nullptr)
return;
// 訪問優先度:左部分木 -> 右部分木 -> ルートノード
postOrder(root->left);
postOrder(root->right);
vec.push_back(root->val);
}
```
=== "Java"
```java title="binary_tree_dfs.java"
/* 前順走査 */
void preOrder(TreeNode root) {
if (root == null)
return;
// 訪問優先度: 根ノード -> 左部分木 -> 右部分木
list.add(root.val);
preOrder(root.left);
preOrder(root.right);
}
/* 中順走査 */
void inOrder(TreeNode root) {
if (root == null)
return;
// 訪問優先度: 左部分木 -> 根ノード -> 右部分木
inOrder(root.left);
list.add(root.val);
inOrder(root.right);
}
/* 後順走査 */
void postOrder(TreeNode root) {
if (root == null)
return;
// 訪問優先度: 左部分木 -> 右部分木 -> 根ノード
postOrder(root.left);
postOrder(root.right);
list.add(root.val);
}
```
=== "C#"
```csharp title="binary_tree_dfs.cs"
[class]{binary_tree_dfs}-[func]{PreOrder}
[class]{binary_tree_dfs}-[func]{InOrder}
[class]{binary_tree_dfs}-[func]{PostOrder}
```
=== "Go"
```go title="binary_tree_dfs.go"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
=== "Swift"
```swift title="binary_tree_dfs.swift"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
=== "JS"
```javascript title="binary_tree_dfs.js"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
=== "TS"
```typescript title="binary_tree_dfs.ts"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
=== "Dart"
```dart title="binary_tree_dfs.dart"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
=== "Rust"
```rust title="binary_tree_dfs.rs"
[class]{}-[func]{pre_order}
[class]{}-[func]{in_order}
[class]{}-[func]{post_order}
```
=== "C"
```c title="binary_tree_dfs.c"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
=== "Kotlin"
```kotlin title="binary_tree_dfs.kt"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
=== "Ruby"
```ruby title="binary_tree_dfs.rb"
[class]{}-[func]{pre_order}
[class]{}-[func]{in_order}
[class]{}-[func]{post_order}
```
=== "Zig"
```zig title="binary_tree_dfs.zig"
[class]{}-[func]{preOrder}
[class]{}-[func]{inOrder}
[class]{}-[func]{postOrder}
```
!!! tip
深度優先探索は反復に基づいても実装できます。興味のある読者は自分で学習してください。
下図は二分木の前順走査の再帰プロセスを示しており、これは「再帰」と「復帰」という2つの反対の部分に分けることができます。
1. 「再帰」は新しいメソッドを開始することを意味し、プログラムはこのプロセスで次のノードにアクセスします。
2. 「復帰」は関数が戻ることを意味し、現在のノードが完全にアクセスされたことを示します。
=== "<1>"
![前順走査の再帰プロセス](binary_tree_traversal.assets/preorder_step1.png){ class="animation-figure" }
=== "<2>"
![preorder_step2](binary_tree_traversal.assets/preorder_step2.png){ class="animation-figure" }
=== "<3>"
![preorder_step3](binary_tree_traversal.assets/preorder_step3.png){ class="animation-figure" }
=== "<4>"
![preorder_step4](binary_tree_traversal.assets/preorder_step4.png){ class="animation-figure" }
=== "<5>"
![preorder_step5](binary_tree_traversal.assets/preorder_step5.png){ class="animation-figure" }
=== "<6>"
![preorder_step6](binary_tree_traversal.assets/preorder_step6.png){ class="animation-figure" }
=== "<7>"
![preorder_step7](binary_tree_traversal.assets/preorder_step7.png){ class="animation-figure" }
=== "<8>"
![preorder_step8](binary_tree_traversal.assets/preorder_step8.png){ class="animation-figure" }
=== "<9>"
![preorder_step9](binary_tree_traversal.assets/preorder_step9.png){ class="animation-figure" }
=== "<10>"
![preorder_step10](binary_tree_traversal.assets/preorder_step10.png){ class="animation-figure" }
=== "<11>"
![preorder_step11](binary_tree_traversal.assets/preorder_step11.png){ class="animation-figure" }
<p align="center"> 図 7-11 &nbsp; 前順走査の再帰プロセス </p>
### 2. &nbsp; 計算量分析
- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間を使用します。
- **空間計算量は$O(n)$**: 最悪の場合、つまり木が連結リストに退化した場合、再帰の深さは$n$に達し、システムは$O(n)$のスタックフレーム空間を占有します。
+23
View File
@@ -0,0 +1,23 @@
---
comments: true
icon: material/graph-outline
---
# 第 7 章 &nbsp; 木
![](../assets/covers/chapter_tree.jpg){ class="cover-image" }
!!! abstract
そびえ立つ木は活力に満ちた本質を放ち、深い根と豊かな葉を誇りながらも、その枝は疎らに散らばり、幽玄な雰囲気を醸し出しています。
それはデータにおける分割統治の鮮やかな形を私たちに示しています。
## 章の内容
- [7.1 &nbsp; 二分木](binary_tree.md)
- [7.2 &nbsp; 二分木の走査](binary_tree_traversal.md)
- [7.3 &nbsp; 木の配列表現](array_representation_of_tree.md)
- [7.4 &nbsp; 二分探索木](binary_search_tree.md)
- [7.5 &nbsp; AVL木 *](avl_tree.md)
- [7.6 &nbsp; まとめ](summary.md)
+58
View File
@@ -0,0 +1,58 @@
---
comments: true
---
# 7.6 &nbsp; まとめ
### 1. &nbsp; 重要なポイント
- 二分木は非線形データ構造で、「一つを二つに分ける」分割統治のロジックを反映しています。各二分木ノードには値と2つのポインタが含まれ、それぞれ左と右の子ノードを指します。
- 二分木のノードについて、その左(右)子ノードとその下に形成される木は、まとめてそのノードの左(右)部分木と呼ばれます。
- 二分木に関連する用語には、根ノード、葉ノード、レベル、次数、エッジ、高さ、深さがあります。
- 二分木の初期化、ノードの挿入、ノードの削除の操作は、連結リストの操作と似ています。
- 一般的な二分木の種類には、完全二分木、完備二分木、満二分木、平衡二分木があります。完全二分木は理想的な状態を表し、連結リストは退化後の最悪の状態です。
- 二分木は、ノード値と空きスロットをレベル順走査シーケンスで配置し、親ノードと子ノード間のインデックスマッピング関係に基づいてポインタを実装することで、配列を使用して表現できます。
- 二分木のレベル順走査は幅優先探索手法で、「円を拡大しながら」の層ごとの走査方式を反映しています。通常はキューを使用して実装されます。
- 前順、中順、後順走査はすべて深度優先探索手法で、「まず最後まで行き、その後バックトラックして続行する」走査方式を反映しています。通常は再帰を使用して実装されます。
- 二分探索木は要素検索のための効率的なデータ構造で、検索、挿入、削除操作の時間計算量はすべて$O(\log n)$です。二分探索木が連結リストに退化すると、これらの時間計算量は$O(n)$に悪化します。
- AVL木は平衡二分探索木とも呼ばれ、回転操作を通して継続的なノード挿入と削除後も木が平衡を保つことを保証します。
- AVL木の回転操作には、右回転、左回転、右左回転、左右回転があります。ノードの挿入または削除後、AVL木はボトムアップ方式でこれらの回転を実行して自己平衡を取ります。
### 2. &nbsp; 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$個のノードがあります。