mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-25 12:36:07 +00:00
build
This commit is contained in:
@@ -122,14 +122,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 配列を初期化
|
||||
var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 }
|
||||
var nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
### 2. 要素へのアクセス
|
||||
|
||||
配列内の要素は連続したメモリ空間に格納されるため、各要素のメモリアドレスを計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。
|
||||
@@ -240,12 +232,6 @@ comments: true
|
||||
[class]{}-[func]{random_access}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
[class]{}-[func]{randomAccess}
|
||||
```
|
||||
|
||||
### 3. 要素の挿入
|
||||
|
||||
配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中央に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。
|
||||
@@ -356,12 +342,6 @@ comments: true
|
||||
[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### 4. 要素の削除
|
||||
|
||||
同様に、以下の図に示すように、インデックス$i$の要素を削除するには、インデックス$i$に続くすべての要素を1つずつ前に移動する必要があります。
|
||||
@@ -466,12 +446,6 @@ comments: true
|
||||
[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
要約すると、配列の挿入と削除操作には以下の欠点があります:
|
||||
|
||||
- **高い時間計算量**:配列の挿入と削除の両方の平均時間計算量は$O(n)$で、ここで$n$は配列の長さです。
|
||||
@@ -590,12 +564,6 @@ comments: true
|
||||
[class]{}-[func]{traverse}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
[class]{}-[func]{traverse}
|
||||
```
|
||||
|
||||
### 6. 要素の検索
|
||||
|
||||
配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。
|
||||
@@ -699,12 +667,6 @@ comments: true
|
||||
[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
### 7. 配列の拡張
|
||||
|
||||
複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。
|
||||
@@ -819,12 +781,6 @@ comments: true
|
||||
[class]{}-[func]{extend}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
[class]{}-[func]{extend}
|
||||
```
|
||||
|
||||
## 4.1.2 配列の利点と制限
|
||||
|
||||
配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作の効率を最適化するために活用できる実質的な事前情報を提供します。
|
||||
|
||||
@@ -171,26 +171,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// 連結リストノードクラス
|
||||
pub fn ListNode(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
val: T = 0, // ノード値
|
||||
next: ?*Self = null, // 次のノードへのポインタ
|
||||
|
||||
// コンストラクタ
|
||||
pub fn init(self: *Self, x: i32) void {
|
||||
self.val = x;
|
||||
self.next = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 4.2.1 連結リストの一般的な操作
|
||||
|
||||
### 1. 連結リストの初期化
|
||||
@@ -391,23 +371,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
// 連結リストを初期化
|
||||
// 各ノードを初期化
|
||||
var n0 = inc.ListNode(i32){.val = 1};
|
||||
var n1 = inc.ListNode(i32){.val = 3};
|
||||
var n2 = inc.ListNode(i32){.val = 2};
|
||||
var n3 = inc.ListNode(i32){.val = 5};
|
||||
var n4 = inc.ListNode(i32){.val = 4};
|
||||
// ノード間の参照を構築
|
||||
n0.next = &n1;
|
||||
n1.next = &n2;
|
||||
n2.next = &n3;
|
||||
n3.next = &n4;
|
||||
```
|
||||
|
||||
配列全体は1つの変数です。例えば、配列`nums`には`nums[0]`、`nums[1]`などの要素が含まれますが、連結リストは複数の異なるノードオブジェクトで構成されています。**通常、連結リストはそのヘッドノードで参照されます**。例えば、前のコードスニペットの連結リストは`n0`として参照されます。
|
||||
|
||||
### 2. ノードの挿入
|
||||
@@ -512,12 +475,6 @@ comments: true
|
||||
[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### 3. ノードの削除
|
||||
|
||||
下図に示すように、連結リストからノードを削除することも非常に簡単で、**1つのノードの参照(ポインタ)を変更するだけです**。
|
||||
@@ -631,12 +588,6 @@ comments: true
|
||||
[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
### 4. ノードへのアクセス
|
||||
|
||||
**連結リストでのノードへのアクセスは効率が悪いです**。前述したように、配列の任意の要素には$O(1)$時間でアクセスできます。対照的に、連結リストでは、プログラムはヘッドノードから開始して目的のノードが見つかるまで順次ノードを巡回する必要があります。つまり、連結リストの$i$番目のノードにアクセスするには、プログラムは$i - 1$個のノードを反復処理する必要があり、時間計算量は$O(n)$になります。
|
||||
@@ -741,12 +692,6 @@ comments: true
|
||||
[class]{}-[func]{access}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
[class]{}-[func]{access}
|
||||
```
|
||||
|
||||
### 5. ノードの検索
|
||||
|
||||
連結リストを巡回して、値が`target`に一致するノードを見つけ、連結リスト内でのそのノードのインデックスを出力します。この手順も線形検索の例です。対応するコードは以下のとおりです:
|
||||
@@ -857,12 +802,6 @@ comments: true
|
||||
[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## 4.2.2 配列 vs. 連結リスト
|
||||
|
||||
下表は配列と連結リストの特性をまとめ、様々な操作における効率も比較しています。それぞれが対照的な格納戦略を使用するため、それぞれの特性と操作効率は明確に対比されています。
|
||||
@@ -1065,28 +1004,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// 双方向連結リストノードクラス
|
||||
pub fn ListNode(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
val: T = 0, // ノード値
|
||||
next: ?*Self = null, // 後続ノードへのポインタ
|
||||
prev: ?*Self = null, // 前任ノードへのポインタ
|
||||
|
||||
// コンストラクタ
|
||||
pub fn init(self: *Self, x: i32) void {
|
||||
self.val = x;
|
||||
self.next = null;
|
||||
self.prev = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 4-8 連結リストの一般的な種類 </p>
|
||||
|
||||
@@ -136,15 +136,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// リストを初期化
|
||||
var nums = std.ArrayList(i32).init(std.heap.page_allocator);
|
||||
defer nums.deinit();
|
||||
try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 });
|
||||
```
|
||||
|
||||
### 2. 要素へのアクセス
|
||||
|
||||
リストは本質的に配列であるため、$O(1)$時間で要素にアクセスし更新することができ、非常に効率的です。
|
||||
@@ -260,16 +251,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// 要素にアクセス
|
||||
var num = nums.items[1]; // インデックス1の要素にアクセス
|
||||
|
||||
// 要素を更新
|
||||
nums.items[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
### 3. 要素の挿入と削除
|
||||
|
||||
配列と比較して、リストは要素の追加と削除においてより柔軟性を提供します。リストの末尾への要素追加は$O(1)$操作ですが、リストの他の場所での要素の挿入と削除の効率は配列と同じままで、時間計算量は$O(n)$です。
|
||||
@@ -486,26 +467,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// リストをクリア
|
||||
nums.clearRetainingCapacity();
|
||||
|
||||
// 末尾に要素を追加
|
||||
try nums.append(1);
|
||||
try nums.append(3);
|
||||
try nums.append(2);
|
||||
try nums.append(5);
|
||||
try nums.append(4);
|
||||
|
||||
// 中間に要素を挿入
|
||||
try nums.insert(3, 6); // インデックス3に数値6を挿入
|
||||
|
||||
// 要素を削除
|
||||
_ = nums.orderedRemove(3); // インデックス3の要素を削除
|
||||
```
|
||||
|
||||
### 4. リストの反復
|
||||
|
||||
配列と同様に、リストはインデックスを使用して反復することも、各要素を直接反復することもできます。
|
||||
@@ -678,23 +639,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// インデックスでリストを反復
|
||||
var count: i32 = 0;
|
||||
var i: i32 = 0;
|
||||
while (i < nums.items.len) : (i += 1) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
// リスト要素を直接反復
|
||||
count = 0;
|
||||
for (nums.items) |num| {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. リストの連結
|
||||
|
||||
新しいリスト`nums1`が与えられたとき、それを元のリストの末尾に追加できます。
|
||||
@@ -792,16 +736,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// 2つのリストを連結
|
||||
var nums1 = std.ArrayList(i32).init(std.heap.page_allocator);
|
||||
defer nums1.deinit();
|
||||
try nums1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 });
|
||||
try nums.insertSlice(nums.items.len, nums1.items); // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
### 6. リストのソート
|
||||
|
||||
リストがソートされると、「二分探索」や「双ポインタ」アルゴリズムなど、配列関連のアルゴリズム問題でよく使用されるアルゴリズムを使用できます。
|
||||
@@ -888,13 +822,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// リストをソート
|
||||
std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32));
|
||||
```
|
||||
|
||||
## 4.3.2 リストの実装
|
||||
|
||||
多くのプログラミング言語には、Java、C++、Pythonなどを含む組み込みリストが付属しています。それらの実装は、初期容量や拡張係数などの様々なパラメータを慎重に考慮した設定で、複雑になりがちです。興味のある読者は、さらなる学習のためにソースコードを調べることができます。
|
||||
@@ -1262,9 +1189,3 @@ comments: true
|
||||
```ruby title="my_list.rb"
|
||||
[class]{MyList}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="my_list.zig"
|
||||
[class]{MyList}-[func]{}
|
||||
```
|
||||
|
||||
@@ -122,12 +122,6 @@ comments: true
|
||||
[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="preorder_traversal_i_compact.zig"
|
||||
[class]{}-[func]{preOrder}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 13-1 前順走査でのノード検索 </p>
|
||||
@@ -266,12 +260,6 @@ comments: true
|
||||
[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="preorder_traversal_ii_compact.zig"
|
||||
[class]{}-[func]{preOrder}
|
||||
```
|
||||
|
||||
各「試行」で、現在のノードを `path` に追加することでパスを記録します。「後退」が必要なときはいつでも、`path` からノードをポップして**この失敗した試行前の状態を復元します**。
|
||||
|
||||
以下の図に示すプロセスを観察することで、**試行は「前進」のようで、後退は「元に戻す」のようです**。後者のペアは、対応するものに対する逆操作と見なすことができます。
|
||||
@@ -444,12 +432,6 @@ comments: true
|
||||
[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="preorder_traversal_iii_compact.zig"
|
||||
[class]{}-[func]{preOrder}
|
||||
```
|
||||
|
||||
「剪定」は非常に生き生きとした名詞です。以下の図に示すように、検索プロセスで、**制約を満たさない検索分岐を「切り取り」ます**。さらなる不要な試行を避け、検索効率を向上させます。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -796,12 +778,6 @@ comments: true
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
次に、フレームワークコードに基づいて例題 3 を解きます。状態 `state` はノードの走査経路を表し、選択肢 `choices` は現在ノードの左子ノードと右子ノード、結果 `res` は経路リストです:
|
||||
|
||||
=== "Python"
|
||||
@@ -1108,22 +1084,6 @@ comments: true
|
||||
[class]{}-[func]{backtrack}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="preorder_traversal_iii_template.zig"
|
||||
[class]{}-[func]{isSolution}
|
||||
|
||||
[class]{}-[func]{recordSolution}
|
||||
|
||||
[class]{}-[func]{isValid}
|
||||
|
||||
[class]{}-[func]{makeChoice}
|
||||
|
||||
[class]{}-[func]{undoChoice}
|
||||
|
||||
[class]{}-[func]{backtrack}
|
||||
```
|
||||
|
||||
問題文の意味に従い、値が $7$ のノードを見つけた後も探索を続ける必要があります。**したがって、解を記録した後の `return` 文を削除する必要があります**。次の図は、`return` 文を保持する場合と削除する場合の探索過程の比較です。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -283,14 +283,6 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で
|
||||
[class]{}-[func]{n_queens}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="n_queens.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
```
|
||||
|
||||
$n$ 個のクイーンを行ごとに配置し、列の制約を考慮して、最初の行から最後の行まで、$n$、$n-1$、$\dots$、$2$、$1$ の選択肢があり、$O(n!)$ 時間を使用します。解を記録する際、行列 `state` をコピーして `res` に追加する必要があり、コピー操作は $O(n^2)$ 時間を使用します。したがって、**全体の時間計算量は $O(n! \cdot n^2)$ です**。実際には、対角線制約に基づく剪定により検索空間を大幅に削減できるため、多くの場合、検索効率は上記の時間計算量よりも優れています。
|
||||
|
||||
配列 `state` は $O(n^2)$ 空間を使用し、配列 `cols`、`diags1`、`diags2` はそれぞれ $O(n)$ 空間を使用します。最大再帰深度は $n$ で、$O(n)$ のスタックフレーム空間を使用します。したがって、**空間計算量は $O(n^2)$ です**。
|
||||
|
||||
@@ -238,14 +238,6 @@ comments: true
|
||||
[class]{}-[func]{permutations_i}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="permutations_i.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{permutationsI}
|
||||
```
|
||||
|
||||
## 13.2.2 重複要素を考慮する場合
|
||||
|
||||
!!! question
|
||||
@@ -467,14 +459,6 @@ comments: true
|
||||
[class]{}-[func]{permutations_ii}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="permutations_ii.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{permutationsII}
|
||||
```
|
||||
|
||||
すべての要素が異なると仮定すると、$n$ 個の要素の順列は $n!$ (階乗)個あります。各結果を記録するには長さ $n$ のリストをコピーする必要があり、これには $O(n)$ 時間がかかります。**したがって、総時間計算量は $O(n!n)$ です。**
|
||||
|
||||
最大再帰深度は $n$ で、$O(n)$ のスタック空間を使用します。`selected` 配列も $O(n)$ 空間が必要です。一度に最大 $n$ 個の個別の `duplicated` セットが存在する可能性があるため、それらは集合的に $O(n^2)$ 空間を占有します。**したがって、空間計算量は $O(n^2)$ です。**
|
||||
|
||||
@@ -207,14 +207,6 @@ comments: true
|
||||
[class]{}-[func]{subset_sum_i_naive}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="subset_sum_i_naive.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{subsetSumINaive}
|
||||
```
|
||||
|
||||
配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力すると、結果 $[3, 3, 3], [4, 5], [5, 4]$ が得られます。**和が $9$ のすべての部分集合を正常に見つけましたが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。
|
||||
|
||||
これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、$5$ の前に $4$ を選択することと $4$ の前に $5$ を選択することは異なる分岐ですが、同じ部分集合に対応します。
|
||||
@@ -447,14 +439,6 @@ comments: true
|
||||
[class]{}-[func]{subset_sum_i}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="subset_sum_i.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{subsetSumI}
|
||||
```
|
||||
|
||||
以下の図は、配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -688,14 +672,6 @@ comments: true
|
||||
[class]{}-[func]{subset_sum_ii}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="subset_sum_ii.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{subsetSumII}
|
||||
```
|
||||
|
||||
以下の図は、配列 $[4, 4, 5]$ とターゲット要素 $9$ のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -116,12 +116,6 @@ comments: true
|
||||
[class]{}-[func]{for_loop}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="iteration.zig"
|
||||
[class]{}-[func]{forLoop}
|
||||
```
|
||||
|
||||
以下の図はこの合計関数を表しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -242,12 +236,6 @@ comments: true
|
||||
[class]{}-[func]{while_loop}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="iteration.zig"
|
||||
[class]{}-[func]{whileLoop}
|
||||
```
|
||||
|
||||
**`while`ループは`for`ループよりも柔軟性を提供します**。特に、条件変数のカスタム初期化と各ステップでの変更が可能です。
|
||||
|
||||
例えば、以下のコードでは、条件変数$i$が各ラウンドで2回更新されますが、これは`for`ループでは実装が不便です。
|
||||
@@ -364,12 +352,6 @@ comments: true
|
||||
[class]{}-[func]{while_loop_ii}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="iteration.zig"
|
||||
[class]{}-[func]{whileLoopII}
|
||||
```
|
||||
|
||||
全体的に、**`for`ループはより簡潔で、`while`ループはより柔軟です**。どちらも反復構造を実装できます。どちらを使用するかは、問題の具体的な要件に基づいて決定する必要があります。
|
||||
|
||||
### 3. ネストしたループ
|
||||
@@ -484,12 +466,6 @@ comments: true
|
||||
[class]{}-[func]{nested_for_loop}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="iteration.zig"
|
||||
[class]{}-[func]{nestedForLoop}
|
||||
```
|
||||
|
||||
以下の図はこのネストしたループを表しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -619,12 +595,6 @@ comments: true
|
||||
[class]{}-[func]{recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="recursion.zig"
|
||||
[class]{}-[func]{recur}
|
||||
```
|
||||
|
||||
以下の図はこの関数の再帰プロセスを示しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -763,12 +733,6 @@ comments: true
|
||||
[class]{}-[func]{tail_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="recursion.zig"
|
||||
[class]{}-[func]{tailRecur}
|
||||
```
|
||||
|
||||
末尾再帰の実行プロセスは以下の図に示されています。通常の再帰と末尾再帰を比較すると、合計操作のポイントが異なります。
|
||||
|
||||
- **通常の再帰**: 合計操作は「返却」フェーズで発生し、各レイヤーが返った後にもう一度合計が必要です。
|
||||
@@ -901,12 +865,6 @@ comments: true
|
||||
[class]{}-[func]{fib}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="recursion.zig"
|
||||
[class]{}-[func]{fib}
|
||||
```
|
||||
|
||||
上記のコードを観察すると、それ自体の中で2つの関数を再帰的に呼び出していることがわかります。**つまり、1回の呼び出しで2つの分岐呼び出しが生成されます**。以下の図に示されているように、この継続的な再帰呼び出しは最終的に深さ$n$の<u>再帰木</u>を作成します。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -1075,12 +1033,6 @@ comments: true
|
||||
[class]{}-[func]{for_loop_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="recursion.zig"
|
||||
[class]{}-[func]{forLoopRecur}
|
||||
```
|
||||
|
||||
上記のコードを観察すると、再帰が反復に変換されたとき、コードはより複雑になります。反復と再帰はしばしば相互に変換できますが、2つの理由でそうすることが常に推奨されるわけではありません:
|
||||
|
||||
- 変換されたコードは理解がより困難になり、読みにくくなる可能性があります。
|
||||
|
||||
@@ -322,12 +322,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
## 2.4.2 計算方法
|
||||
|
||||
空間計算量を計算する方法は時間計算量とほぼ同様で、統計対象を「操作数」から「使用空間のサイズ」に変更するだけです。
|
||||
@@ -474,12 +468,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
**再帰関数では、スタックフレーム空間を考慮に入れる必要があります**。以下のコードを考えてみましょう:
|
||||
|
||||
=== "Python"
|
||||
@@ -718,12 +706,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
`loop()`関数と`recur()`関数の時間計算量は両方とも$O(n)$ですが、それらの空間計算量は異なります。
|
||||
|
||||
- `loop()`関数はループ内で`function()`を$n$回呼び出し、各反復の`function()`は返ってそのスタックフレーム空間を解放するため、空間計算量は$O(1)$のままです。
|
||||
@@ -906,14 +888,6 @@ $$
|
||||
[class]{}-[func]{constant}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="space_complexity.zig"
|
||||
[class]{}-[func]{function}
|
||||
|
||||
[class]{}-[func]{constant}
|
||||
```
|
||||
|
||||
### 2. 線形オーダー $O(n)$ {data-toc-label="2. 線形オーダー"}
|
||||
|
||||
線形オーダーは配列、連結リスト、スタック、キューなどで一般的で、要素数は$n$に比例します:
|
||||
@@ -1033,12 +1007,6 @@ $$
|
||||
[class]{}-[func]{linear}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="space_complexity.zig"
|
||||
[class]{}-[func]{linear}
|
||||
```
|
||||
|
||||
下図に示されているように、この関数の再帰深度は$n$で、$n$個の未返却の`linear_recur()`関数インスタンスがあり、$O(n)$サイズのスタックフレーム空間を使用します:
|
||||
|
||||
=== "Python"
|
||||
@@ -1136,12 +1104,6 @@ $$
|
||||
[class]{}-[func]{linear_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="space_complexity.zig"
|
||||
[class]{}-[func]{linearRecur}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 2-17 Recursive function generating linear order space complexity </p>
|
||||
@@ -1255,12 +1217,6 @@ $$
|
||||
[class]{}-[func]{quadratic}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="space_complexity.zig"
|
||||
[class]{}-[func]{quadratic}
|
||||
```
|
||||
|
||||
下図に示されているように、この関数の再帰深度は$n$で、各再帰呼び出しで長さ$n$、$n-1$、$\dots$、$2$、$1$の配列が初期化され、平均$n/2$となり、全体として$O(n^2)$の空間を占有します:
|
||||
|
||||
=== "Python"
|
||||
@@ -1362,12 +1318,6 @@ $$
|
||||
[class]{}-[func]{quadratic_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="space_complexity.zig"
|
||||
[class]{}-[func]{quadraticRecur}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 2-18 Recursive function generating quadratic order space complexity </p>
|
||||
@@ -1477,12 +1427,6 @@ $$
|
||||
[class]{}-[func]{build_tree}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="space_complexity.zig"
|
||||
[class]{}-[func]{buildTree}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 2-19 Full binary tree generating exponential order space complexity </p>
|
||||
|
||||
@@ -181,21 +181,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// 特定の操作プラットフォーム下で
|
||||
fn algorithm(n: usize) void {
|
||||
var a: i32 = 2; // 1 ns
|
||||
a += 1; // 1 ns
|
||||
a *= 2; // 10 ns
|
||||
// n回ループ
|
||||
for (0..n) |_| { // 1 ns
|
||||
std.debug.print("{}\n", .{0}); // 5 ns
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上記の方法を使用すると、アルゴリズムの実行時間は$(6n + 12)$ nsとして計算できます:
|
||||
|
||||
$$
|
||||
@@ -445,29 +430,6 @@ $$
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// アルゴリズムAの時間計算量:定数オーダー
|
||||
fn algorithm_A(n: usize) void {
|
||||
_ = n;
|
||||
std.debug.print("{}\n", .{0});
|
||||
}
|
||||
// アルゴリズムBの時間計算量:線形オーダー
|
||||
fn algorithm_B(n: i32) void {
|
||||
for (0..n) |_| {
|
||||
std.debug.print("{}\n", .{0});
|
||||
}
|
||||
}
|
||||
// アルゴリズムCの時間計算量:定数オーダー
|
||||
fn algorithm_C(n: i32) void {
|
||||
_ = n;
|
||||
for (0..1000000) |_| {
|
||||
std.debug.print("{}\n", .{0});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
下図はこれら3つのアルゴリズムの時間計算量を示しています。
|
||||
|
||||
- アルゴリズム`A`には1つの印刷操作のみがあり、その実行時間は$n$とともに増加しません。その時間計算量は「定数オーダー」と考えられます。
|
||||
@@ -647,20 +609,6 @@ $$
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
fn algorithm(n: usize) void {
|
||||
var a: i32 = 1; // +1
|
||||
a += 1; // +1
|
||||
a *= 2; // +1
|
||||
// n回ループ
|
||||
for (0..n) |_| { // +1 (毎回i++が実行される)
|
||||
std.debug.print("{}\n", .{0}); // +1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
アルゴリズムの操作数を入力サイズ$n$の関数として表す関数を$T(n)$とすると、以下の例を考えてみましょう:
|
||||
|
||||
$$
|
||||
@@ -910,27 +858,6 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
fn algorithm(n: usize) void {
|
||||
var a: i32 = 1; // +0 (技法1)
|
||||
a = a + @as(i32, @intCast(n)); // +0 (技法1)
|
||||
|
||||
// +n (技法2)
|
||||
for(0..(5 * n + 1)) |_| {
|
||||
std.debug.print("{}\n", .{0});
|
||||
}
|
||||
|
||||
// +n*n (技法3)
|
||||
for(0..(2 * n)) |_| {
|
||||
for(0..(n + 1)) |_| {
|
||||
std.debug.print("{}\n", .{0});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以下の式は、簡略化前後のカウント結果を示しており、どちらも$O(n^2)$の時間計算量に導きます:
|
||||
|
||||
$$
|
||||
@@ -1078,12 +1005,6 @@ $$
|
||||
[class]{}-[func]{constant}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{constant}
|
||||
```
|
||||
|
||||
### 2. 線形オーダー $O(n)$ {data-toc-label="2. 線形オーダー"}
|
||||
|
||||
線形オーダーは、操作数が入力データサイズ$n$と線形に増加することを示します。線形オーダーは一般的に単一ループ構造で現れます:
|
||||
@@ -1183,12 +1104,6 @@ $$
|
||||
[class]{}-[func]{linear}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{linear}
|
||||
```
|
||||
|
||||
配列の走査や連結リストの走査などの操作は時間計算量が$O(n)$で、$n$は配列またはリストの長さです:
|
||||
|
||||
=== "Python"
|
||||
@@ -1291,12 +1206,6 @@ $$
|
||||
[class]{}-[func]{array_traversal}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{arrayTraversal}
|
||||
```
|
||||
|
||||
**入力データサイズ$n$は入力データの種類に基づいて決定する必要があります**。例えば、最初の例では、$n$は入力データサイズを表し、2番目の例では、配列の長さ$n$がデータサイズです。
|
||||
|
||||
### 3. 二次オーダー $O(n^2)$ {data-toc-label="3. 二次オーダー"}
|
||||
@@ -1408,12 +1317,6 @@ $$
|
||||
[class]{}-[func]{quadratic}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{quadratic}
|
||||
```
|
||||
|
||||
下図は定数オーダー、線形オーダー、二次オーダーの時間計算量を比較しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -1547,12 +1450,6 @@ $$
|
||||
[class]{}-[func]{bubble_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
```
|
||||
|
||||
### 4. 指数オーダー $O(2^n)$ {data-toc-label="4. 指数オーダー"}
|
||||
|
||||
生物学的「細胞分裂」は指数オーダー増加の典型例です:1つの細胞から始まり、1回の分裂後に2つ、2回の分裂後に4つとなり、$n$回の分裂後に$2^n$個の細胞になります。
|
||||
@@ -1671,12 +1568,6 @@ $$
|
||||
[class]{}-[func]{exponential}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{exponential}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 2-11 Exponential order time complexity </p>
|
||||
@@ -1775,12 +1666,6 @@ $$
|
||||
[class]{}-[func]{exp_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{expRecur}
|
||||
```
|
||||
|
||||
指数オーダーの増加は極めて急速で、全数探索法(ブルートフォース、バックトラッキングなど)でよく見られます。大規模問題では、指数オーダーは受け入れられず、しばしば動的プログラミングや貪欲アルゴリズムが解決策として必要になります。
|
||||
|
||||
### 5. 対数オーダー $O(\log n)$ {data-toc-label="5. 対数オーダー"}
|
||||
@@ -1889,12 +1774,6 @@ $$
|
||||
[class]{}-[func]{logarithmic}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{logarithmic}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 2-12 Logarithmic order time complexity </p>
|
||||
@@ -1993,12 +1872,6 @@ $$
|
||||
[class]{}-[func]{log_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{logRecur}
|
||||
```
|
||||
|
||||
対数オーダーは分割統治戦略に基づくアルゴリズムの典型で、「多くに分割」と「複雑な問題を単純化」するアプローチを体現しています。増加が遅く、定数オーダーの次に最も理想的な時間計算量です。
|
||||
|
||||
!!! tip "$O(\log n)$の底は何ですか?"
|
||||
@@ -2118,12 +1991,6 @@ $$
|
||||
[class]{}-[func]{linear_log_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{linearLogRecur}
|
||||
```
|
||||
|
||||
下図は線形対数オーダーがどのように生成されるかを示しています。二分木の各レベルには$n$個の操作があり、木には$\log_2 n + 1$レベルがあり、時間計算量は$O(n \log n)$になります。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -2248,12 +2115,6 @@ $$
|
||||
[class]{}-[func]{factorial_recur}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="time_complexity.zig"
|
||||
[class]{}-[func]{factorialRecur}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 2-14 Factorial order time complexity </p>
|
||||
@@ -2431,14 +2292,6 @@ $$
|
||||
[class]{}-[func]{find_one}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="worst_best_time_complexity.zig"
|
||||
[class]{}-[func]{randomNumbers}
|
||||
|
||||
[class]{}-[func]{findOne}
|
||||
```
|
||||
|
||||
最良ケース時間計算量は実際にはほとんど使用されないことに注意してください。通常は非常に低い確率でのみ達成可能で、誤解を招く可能性があるからです。**最悪ケース時間計算量はより実用的で、効率の安全値を提供し**、アルゴリズムを自信を持って使用できるようにします。
|
||||
|
||||
上記の例から、最悪ケースと最良ケースの時間計算量は両方とも「特殊なデータ分布」下でのみ発生し、発生確率が小さく、アルゴリズムの実行効率を正確に反映しない可能性があることが明らかです。対照的に、**平均時間計算量はランダム入力データ下でのアルゴリズムの効率を反映でき**、$\Theta$記法で表されます。
|
||||
|
||||
@@ -167,12 +167,3 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// 様々な基本データ型を使用して配列を初期化
|
||||
var numbers: [5]i32 = undefined;
|
||||
var decimals: [5]f32 = undefined;
|
||||
var characters: [5]u8 = undefined;
|
||||
var bools: [5]bool = undefined;
|
||||
```
|
||||
|
||||
@@ -214,11 +214,3 @@ comments: true
|
||||
|
||||
[class]{}-[func]{binary_search}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_recur.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
```
|
||||
|
||||
@@ -248,14 +248,6 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー
|
||||
[class]{}-[func]{build_tree}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="build_tree.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
```
|
||||
|
||||
以下の図は、二分木を構築する再帰過程を示しています。各ノードは再帰の「下降」段階で作成され、各エッジ(参照)は「上昇」段階で形成されます。
|
||||
|
||||
=== "<1>"
|
||||
|
||||
@@ -295,16 +295,6 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助
|
||||
[class]{}-[func]{solve_hanota}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hanota.zig"
|
||||
[class]{}-[func]{move}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
```
|
||||
|
||||
以下の図に示すように、ハノイの塔問題は高さ $n$ の再帰木として視覚化できます。各ノードは部分問題を表し、`dfs()` の呼び出しに対応します。**したがって、時間計算量は $O(2^n)$、空間計算量は $O(n)$ です。**
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -160,12 +160,6 @@ $$
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_cost_climbing_stairs_dp.zig"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
```
|
||||
|
||||
下の図は上記コードの動的プログラミングプロセスを示しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -284,12 +278,6 @@ $$
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_cost_climbing_stairs_dp.zig"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
```
|
||||
|
||||
## 14.2.2 無記憶性
|
||||
|
||||
無記憶性は動的プログラミングが問題解決に効果的であることを可能にする重要な特徴の1つです。その定義は:**特定の状態が与えられたとき、その将来の発展は現在の状態のみに関連し、過去に経験したすべての状態とは無関係である**。
|
||||
@@ -459,12 +447,6 @@ $$
|
||||
[class]{}-[func]{climbing_stairs_constraint_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="climbing_stairs_constraint_dp.zig"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
```
|
||||
|
||||
上記のケースでは、前の状態のみを考慮すればよいため、状態定義を拡張することで依然として無記憶性を満たすことができます。しかし、一部の問題では非常に深刻な「状態効果」があります。
|
||||
|
||||
!!! question "障害物生成付き階段登り"
|
||||
|
||||
@@ -232,12 +232,6 @@ $$
|
||||
[class]{}-[func]{min_path_sum_dfs}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_path_sum.zig"
|
||||
[class]{}-[func]{minPathSumDFS}
|
||||
```
|
||||
|
||||
下の図は $dp[2, 1]$ を根とする再帰木を示しており、いくつかの重複する部分問題を含み、その数はグリッド `grid` のサイズが増加すると急激に増加します。
|
||||
|
||||
本質的に、重複する部分問題の理由は:**左上隅から特定のセルに到達する複数のパスが存在する**ことです。
|
||||
@@ -368,12 +362,6 @@ $$
|
||||
[class]{}-[func]{min_path_sum_dfs_mem}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_path_sum.zig"
|
||||
[class]{}-[func]{minPathSumDFSMem}
|
||||
```
|
||||
|
||||
下の図に示すように、メモ化を導入した後、すべての部分問題の解は一度だけ計算される必要があるため、時間計算量は状態の総数、つまりグリッドサイズ $O(nm)$ に依存します。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -520,12 +508,6 @@ $$
|
||||
[class]{}-[func]{min_path_sum_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_path_sum.zig"
|
||||
[class]{}-[func]{minPathSumDP}
|
||||
```
|
||||
|
||||
下の図は最小経路和の状態遷移プロセスを示し、グリッド全体を走査するため、**時間計算量は $O(nm)$** です。
|
||||
|
||||
配列 `dp` のサイズは $n \times m$ であるため、**空間計算量は $O(nm)$** です。
|
||||
@@ -687,9 +669,3 @@ $$
|
||||
```ruby title="min_path_sum.rb"
|
||||
[class]{}-[func]{min_path_sum_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_path_sum.zig"
|
||||
[class]{}-[func]{minPathSumDPComp}
|
||||
```
|
||||
|
||||
@@ -221,12 +221,6 @@ $$
|
||||
[class]{}-[func]{edit_distance_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="edit_distance.zig"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
```
|
||||
|
||||
下の図に示すように、編集距離問題の状態遷移プロセスはナップサック問題と非常に似ており、二次元グリッドを埋めることと見なすことができます。
|
||||
|
||||
=== "<1>"
|
||||
@@ -408,9 +402,3 @@ $dp[i, j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1
|
||||
```ruby title="edit_distance.rb"
|
||||
[class]{}-[func]{edit_distance_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="edit_distance.zig"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
```
|
||||
|
||||
@@ -185,14 +185,6 @@ comments: true
|
||||
[class]{}-[func]{climbing_stairs_backtrack}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="climbing_stairs_backtrack.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{climbingStairsBacktrack}
|
||||
```
|
||||
|
||||
## 14.1.1 方法1:力任せ探索
|
||||
|
||||
バックトラッキングアルゴリズムは問題を明示的に部分問題に分解しません。代わりに、問題を一連の決定ステップとして扱い、試行と枝刈りを通じてすべての可能性を探索します。
|
||||
@@ -356,14 +348,6 @@ $$
|
||||
[class]{}-[func]{climbing_stairs_dfs}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="climbing_stairs_dfs.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{climbingStairsDFS}
|
||||
```
|
||||
|
||||
下の図は力任せ探索によって形成される再帰木を示しています。問題 $dp[n]$ について、その再帰木の深さは $n$ で、時間計算量は $O(2^n)$ です。この指数的増加により、$n$ が大きいとプログラムの実行がはるかに遅くなり、長い待機時間が生じます。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -540,14 +524,6 @@ $$
|
||||
[class]{}-[func]{climbing_stairs_dfs_mem}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="climbing_stairs_dfs_mem.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{climbingStairsDFSMem}
|
||||
```
|
||||
|
||||
下の図を観察すると、**メモ化後、すべての重複する部分問題は一度だけ計算される必要があり、時間計算量を $O(n)$ に最適化**します。これは大幅な改善です。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -679,12 +655,6 @@ $$
|
||||
[class]{}-[func]{climbing_stairs_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="climbing_stairs_dp.zig"
|
||||
[class]{}-[func]{climbingStairsDP}
|
||||
```
|
||||
|
||||
下の図は上記コードの実行プロセスをシミュレートしています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -810,12 +780,6 @@ $$
|
||||
[class]{}-[func]{climbing_stairs_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="climbing_stairs_dp.zig"
|
||||
[class]{}-[func]{climbingStairsDPComp}
|
||||
```
|
||||
|
||||
上記のコードを観察すると、配列 `dp` が占有していた空間が削除されるため、空間計算量は $O(n)$ から $O(1)$ に削減されます。
|
||||
|
||||
多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています**。
|
||||
|
||||
@@ -182,12 +182,6 @@ $$
|
||||
[class]{}-[func]{knapsack_dfs}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="knapsack.zig"
|
||||
[class]{}-[func]{knapsackDFS}
|
||||
```
|
||||
|
||||
下の図に示すように、各アイテムは選択しないと選択するという2つの探索分岐を生成するため、時間計算量は $O(2^n)$ です。
|
||||
|
||||
再帰木を観察すると、$dp[1, 10]$ などの重複する部分問題があることが容易にわかります。アイテムが多く、ナップサック容量が大きい場合、特に同じ重量のアイテムが多い場合、重複する部分問題の数は大幅に増加します。
|
||||
@@ -318,12 +312,6 @@ $$
|
||||
[class]{}-[func]{knapsack_dfs_mem}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="knapsack.zig"
|
||||
[class]{}-[func]{knapsackDFSMem}
|
||||
```
|
||||
|
||||
下の図はメモ化探索で枝刈りされる探索分岐を示しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -462,12 +450,6 @@ $$
|
||||
[class]{}-[func]{knapsack_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="knapsack.zig"
|
||||
[class]{}-[func]{knapsackDP}
|
||||
```
|
||||
|
||||
下の図に示すように、時間計算量と空間計算量の両方が配列 `dp` のサイズ、つまり $O(n \times cap)$ によって決定されます。
|
||||
|
||||
=== "<1>"
|
||||
@@ -671,9 +653,3 @@ $$
|
||||
```ruby title="knapsack.rb"
|
||||
[class]{}-[func]{knapsack_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="knapsack.zig"
|
||||
[class]{}-[func]{knapsackDPComp}
|
||||
```
|
||||
|
||||
@@ -166,12 +166,6 @@ $$
|
||||
[class]{}-[func]{unbounded_knapsack_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="unbounded_knapsack.zig"
|
||||
[class]{}-[func]{unboundedKnapsackDP}
|
||||
```
|
||||
|
||||
### 3. 空間最適化
|
||||
|
||||
現在の状態は左と上の状態から来るため、**空間最適化解法は $dp$ テーブルの各行に対して前方走査を実行する必要があります**。
|
||||
@@ -329,12 +323,6 @@ $$
|
||||
[class]{}-[func]{unbounded_knapsack_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="unbounded_knapsack.zig"
|
||||
[class]{}-[func]{unboundedKnapsackDPComp}
|
||||
```
|
||||
|
||||
## 14.5.2 コイン交換問題
|
||||
|
||||
ナップサック問題は動的プログラミング問題の大きなクラスの代表であり、コイン交換問題など多くの変種があります。
|
||||
@@ -526,12 +514,6 @@ $$
|
||||
[class]{}-[func]{coin_change_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change.zig"
|
||||
[class]{}-[func]{coinChangeDP}
|
||||
```
|
||||
|
||||
下の図はコイン交換問題の動的プログラミングプロセスを示しており、無制限ナップサック問題と非常に似ています。
|
||||
|
||||
=== "<1>"
|
||||
@@ -721,12 +703,6 @@ $$
|
||||
[class]{}-[func]{coin_change_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change.zig"
|
||||
[class]{}-[func]{coinChangeDPComp}
|
||||
```
|
||||
|
||||
## 14.5.3 コイン交換問題II
|
||||
|
||||
!!! question
|
||||
@@ -890,12 +866,6 @@ $$
|
||||
[class]{}-[func]{coin_change_ii_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change_ii.zig"
|
||||
[class]{}-[func]{coinChangeIIDP}
|
||||
```
|
||||
|
||||
### 3. 空間最適化
|
||||
|
||||
空間最適化アプローチは同じで、コインの次元を削除するだけです:
|
||||
@@ -1031,9 +1001,3 @@ $$
|
||||
```ruby title="coin_change_ii.rb"
|
||||
[class]{}-[func]{coin_change_ii_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change_ii.zig"
|
||||
[class]{}-[func]{coinChangeIIDPComp}
|
||||
```
|
||||
|
||||
@@ -346,12 +346,6 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま
|
||||
[class]{GraphAdjMat}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="graph_adjacency_matrix.zig"
|
||||
[class]{GraphAdjMat}-[func]{}
|
||||
```
|
||||
|
||||
## 9.2.2 隣接リストに基づく実装
|
||||
|
||||
総計$n$個の頂点と$m$個の辺を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装できます。
|
||||
@@ -670,12 +664,6 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま
|
||||
[class]{GraphAdjList}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="graph_adjacency_list.zig"
|
||||
[class]{GraphAdjList}-[func]{}
|
||||
```
|
||||
|
||||
## 9.2.3 効率の比較
|
||||
|
||||
グラフに$n$個の頂点と$m$個の辺があると仮定すると、下表は隣接行列と隣接リストの時間効率と空間効率を比較しています。
|
||||
|
||||
@@ -178,12 +178,6 @@ BFSは通常キューの助けを借りて実装されます(下記のコー
|
||||
[class]{}-[func]{graph_bfs}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="graph_bfs.zig"
|
||||
[class]{}-[func]{graphBFS}
|
||||
```
|
||||
|
||||
コードは比較的抽象的ですが、下図と比較することでより良く理解できます。
|
||||
|
||||
=== "<1>"
|
||||
@@ -406,14 +400,6 @@ BFSは通常キューの助けを借りて実装されます(下記のコー
|
||||
[class]{}-[func]{graph_dfs}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="graph_dfs.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{graphDFS}
|
||||
```
|
||||
|
||||
深さ優先探索のアルゴリズムプロセスを下図に示します。
|
||||
|
||||
- **破線は下向きの再帰を表し**、新しい頂点を訪問するために新しい再帰メソッドが開始されたことを示します。
|
||||
|
||||
@@ -231,14 +231,6 @@ comments: true
|
||||
[class]{}-[func]{fractional_knapsack}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="fractional_knapsack.zig"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、**時間計算量は $O(n)$** です。ここで $n$ はアイテムの数です。
|
||||
|
||||
`Item` オブジェクトリストが初期化されるため、**空間計算量は $O(n)$** です。
|
||||
|
||||
@@ -151,12 +151,6 @@ comments: true
|
||||
[class]{}-[func]{coin_change_greedy}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change_greedy.zig"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
感嘆するかもしれません:なんて簡潔なんだ!貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。
|
||||
|
||||
## 15.1.1 貪欲アルゴリズムの利点と制限
|
||||
|
||||
@@ -224,12 +224,6 @@ $$
|
||||
[class]{}-[func]{max_capacity}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="max_capacity.zig"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
### 3. 正しさの証明
|
||||
|
||||
貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。
|
||||
|
||||
@@ -203,12 +203,6 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積
|
||||
[class]{}-[func]{max_product_cutting}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="max_product_cutting.zig"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-16 切断後の最大積の計算方法 </p>
|
||||
|
||||
@@ -296,18 +296,6 @@ index = hash(key) % capacity
|
||||
[class]{}-[func]{rot_hash}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="simple_hash.zig"
|
||||
[class]{}-[func]{addHash}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
```
|
||||
|
||||
各ハッシュアルゴリズムの最後のステップが大きな素数$1000000007$の剰余を取ることで、ハッシュ値が適切な範囲内にあることを保証していることが観察されます。なぜ素数の剰余を取ることが強調されるのか、または合成数の剰余を取ることの欠点は何かを考える価値があります。これは興味深い質問です。
|
||||
|
||||
結論として:**大きな素数を剰余として使用することで、ハッシュ値の均等分散を最大化できます**。素数は他の数と共通因子を持たないため、剰余演算によって引き起こされる周期的パターンを減らし、ハッシュ衝突を回避できます。
|
||||
@@ -611,12 +599,6 @@ $$
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="built_in_hash.zig"
|
||||
|
||||
```
|
||||
|
||||
多くのプログラミング言語では、**不変オブジェクトのみがハッシュ表の`key`として機能できます**。リスト(動的配列)を`key`として使用する場合、リストの内容が変更されると、そのハッシュ値も変更され、ハッシュ表で元の`value`を見つけることができなくなります。
|
||||
|
||||
カスタムオブジェクト(連結リストノードなど)のメンバー変数は可変ですが、ハッシュ可能です。**これは、オブジェクトのハッシュ値が通常そのメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変更されても、メモリアドレスは同じままなので、ハッシュ値は変更されません。
|
||||
|
||||
@@ -414,12 +414,6 @@ comments: true
|
||||
[class]{HashMapChaining}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hash_map_chaining.zig"
|
||||
[class]{HashMapChaining}-[func]{}
|
||||
```
|
||||
|
||||
連結リストが非常に長い場合、クエリ効率$O(n)$が悪いことは注目に値します。**この場合、リストを「AVL木」または「赤黒木」に変換して**、クエリ操作の時間計算量を$O(\log n)$に最適化できます。
|
||||
|
||||
## 6.2.2 オープンアドレス法
|
||||
@@ -886,12 +880,6 @@ comments: true
|
||||
[class]{HashMapOpenAddressing}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hash_map_open_addressing.zig"
|
||||
[class]{HashMapOpenAddressing}-[func]{}
|
||||
```
|
||||
|
||||
### 2. 二次プローブ
|
||||
|
||||
二次プローブは線形プローブに似ており、オープンアドレス法の一般的な戦略の1つです。衝突が発生した場合、二次プローブは単純に固定ステップ数をスキップするのではなく、「プローブ回数の二乗」に等しいステップ数、つまり$1, 4, 9, \dots$ステップをスキップします。
|
||||
|
||||
@@ -283,12 +283,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hash_map.zig"
|
||||
|
||||
```
|
||||
|
||||
ハッシュ表を走査する一般的な方法は3つあります:キー値ペアの走査、キーの走査、値の走査。以下はコード例です:
|
||||
|
||||
=== "Python"
|
||||
@@ -480,12 +474,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hash_map.zig"
|
||||
// Zigの例は提供されていません
|
||||
```
|
||||
|
||||
## 6.1.2 ハッシュ表の簡単な実装
|
||||
|
||||
まず、最も簡単なケースを考えてみましょう:**配列のみを使ってハッシュ表を実装すること**。ハッシュ表において、配列の各空きスロットは<u>バケット</u>と呼ばれ、各バケットはキー値ペアを格納できます。したがって、クエリ操作は`key`に対応するバケットを見つけ、そこから`value`を取得することになります。
|
||||
@@ -864,14 +852,6 @@ index = hash(key) % capacity
|
||||
[class]{ArrayHashMap}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array_hash_map.zig"
|
||||
[class]{Pair}-[func]{}
|
||||
|
||||
[class]{ArrayHashMap}-[func]{}
|
||||
```
|
||||
|
||||
## 6.1.3 ハッシュ衝突とリサイズ
|
||||
|
||||
本質的に、ハッシュ関数の役割は、すべてのキーの入力空間全体を、すべての配列インデックスの出力空間にマッピングすることです。しかし、入力空間は出力空間よりもはるかに大きいことがよくあります。したがって、**理論的には、「複数の入力が同じ出力に対応する」ケースが常に存在します**。
|
||||
|
||||
@@ -127,12 +127,6 @@ comments: true
|
||||
[class]{MaxHeap}-[func]{initialize}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="my_heap.zig"
|
||||
[class]{MaxHeap}-[func]{init}
|
||||
```
|
||||
|
||||
## 8.2.3 計算量分析
|
||||
|
||||
次に、この第2のヒープ構築方法の時間計算量を計算してみましょう。
|
||||
|
||||
@@ -418,12 +418,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="heap.zig"
|
||||
|
||||
```
|
||||
|
||||
## 8.1.2 ヒープの実装
|
||||
|
||||
以下の実装は最大ヒープです。最小ヒープに変換するには、すべてのサイズ論理比較を反転させるだけです(例えば、$\geq$を$\leq$に置き換える)。興味のある読者は自分で実装することをお勧めします。
|
||||
@@ -596,16 +590,6 @@ comments: true
|
||||
[class]{MaxHeap}-[func]{parent}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="my_heap.zig"
|
||||
[class]{MaxHeap}-[func]{left}
|
||||
|
||||
[class]{MaxHeap}-[func]{right}
|
||||
|
||||
[class]{MaxHeap}-[func]{parent}
|
||||
```
|
||||
|
||||
### 2. ヒープの先頭要素へのアクセス
|
||||
|
||||
ヒープの先頭要素は二分木の根ノードで、リストの最初の要素でもあります:
|
||||
@@ -696,12 +680,6 @@ comments: true
|
||||
[class]{MaxHeap}-[func]{peek}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="my_heap.zig"
|
||||
[class]{MaxHeap}-[func]{peek}
|
||||
```
|
||||
|
||||
### 3. ヒープへの要素挿入
|
||||
|
||||
要素`val`が与えられた場合、まずそれをヒープの底に追加します。追加後、`val`がヒープ内の他の要素より大きい可能性があるため、ヒープの完全性が損なわれる可能性があります。**したがって、挿入されたノードから根ノードまでのパスを修復する必要があります**。この操作は<u>ヒープ化</u>と呼ばれます。
|
||||
@@ -897,14 +875,6 @@ comments: true
|
||||
[class]{MaxHeap}-[func]{sift_up}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="my_heap.zig"
|
||||
[class]{MaxHeap}-[func]{push}
|
||||
|
||||
[class]{MaxHeap}-[func]{siftUp}
|
||||
```
|
||||
|
||||
### 4. ヒープからの先頭要素削除
|
||||
|
||||
ヒープの先頭要素は二分木の根ノード、つまりリストの最初の要素です。リストから最初の要素を直接削除すると、二分木内のすべてのノードインデックスが変更され、後続の修復にヒープ化を使用することが困難になります。要素インデックスの変更を最小限に抑えるため、次の手順を使用します。
|
||||
@@ -1138,14 +1108,6 @@ comments: true
|
||||
[class]{MaxHeap}-[func]{sift_down}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="my_heap.zig"
|
||||
[class]{MaxHeap}-[func]{pop}
|
||||
|
||||
[class]{MaxHeap}-[func]{siftDown}
|
||||
```
|
||||
|
||||
## 8.1.3 ヒープの一般的な応用
|
||||
|
||||
- **優先度キュー**:ヒープは優先度キューを実装するための好ましいデータ構造で、エンキュー操作とデキュー操作の両方の時間計算量が$O(\log n)$、キュー構築の時間計算量が$O(n)$で、すべて非常に効率的です。
|
||||
|
||||
@@ -223,12 +223,6 @@ comments: true
|
||||
[class]{}-[func]{top_k_heap}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="top_k.zig"
|
||||
[class]{}-[func]{topKHeap}
|
||||
```
|
||||
|
||||
合計$n$回のヒープ挿入と削除が実行され、最大ヒープサイズが$k$であるため、時間計算量は$O(n \log k)$です。この方法は非常に効率的で、$k$が小さい場合、時間計算量は$O(n)$に近づき、$k$が大きい場合でも、時間計算量は$O(n \log n)$を超えません。
|
||||
|
||||
さらに、この方法は動的データストリームのシナリオに適しています。データを継続的に追加することで、ヒープ内の要素を維持し、最大$k$個の要素の動的更新を実現できます。
|
||||
|
||||
@@ -173,17 +173,6 @@ comments: true
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント
|
||||
|
||||
// 詳細を説明するためのコメント
|
||||
|
||||
// 複数行
|
||||
// コメント
|
||||
```
|
||||
|
||||
## 0.2.2 アニメーション図解による効率的学習
|
||||
|
||||
テキストと比較して、動画や画像は情報密度が高く、より構造化されており、理解しやすくなっています。この本では、**重要で難しい概念は主にアニメーションと図解を通じて提示され**、テキストは説明と補足として機能します。
|
||||
|
||||
@@ -177,12 +177,6 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲
|
||||
[class]{}-[func]{binary_search}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search.zig"
|
||||
[class]{}-[func]{binarySearch}
|
||||
```
|
||||
|
||||
**時間計算量は$O(\log n)$です**:二分ループにおいて、区間は各ラウンドで半分に減少するため、反復回数は$\log_2 n$となります。
|
||||
|
||||
**空間計算量は$O(1)$です**:ポインタ$i$と$j$は定数サイズの空間を占有します。
|
||||
@@ -316,12 +310,6 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲
|
||||
[class]{}-[func]{binary_search_lcro}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search.zig"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
```
|
||||
|
||||
下図に示すように、2つの区間表現タイプにおいて、二分探索アルゴリズムの初期化、ループ条件、区間縮小操作が異なります。
|
||||
|
||||
「閉区間」表現では両方の境界が包含的であるため、ポインタ$i$と$j$による区間縮小操作も対称的です。これによりエラーが発生しにくくなるため、**一般的に「閉区間」アプローチの使用が推奨されます**。
|
||||
|
||||
@@ -125,12 +125,6 @@ comments: true
|
||||
[class]{}-[func]{binary_search_left_edge}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_edge.zig"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
```
|
||||
|
||||
## 10.3.2 右境界を見つける
|
||||
|
||||
`target`の最も右の出現をどのように見つけるでしょうか?最も直接的な方法は、`nums[m] == target`の場合に探索境界を調整する方法を変更して、従来の二分探索ロジックを修正することです。コードはここでは省略されています。興味がある場合は、自分でコードを実装してみてください。
|
||||
@@ -261,12 +255,6 @@ comments: true
|
||||
[class]{}-[func]{binary_search_right_edge}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_edge.zig"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
```
|
||||
|
||||
### 2. 要素探索に変換する
|
||||
|
||||
配列に`target`が含まれていない場合、$i$と$j$は最終的に`target`より大きい最初の要素と小さい最初の要素をそれぞれ指します。
|
||||
|
||||
@@ -148,12 +148,6 @@ comments: true
|
||||
[class]{}-[func]{binary_search_insertion_simple}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_insertion.zig"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
```
|
||||
|
||||
## 10.2.2 重複要素がある場合
|
||||
|
||||
!!! question
|
||||
@@ -330,12 +324,6 @@ comments: true
|
||||
[class]{}-[func]{binary_search_insertion}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_insertion.zig"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
このセクションのコードは「閉区間」を使用しています。「左閉右開」に興味がある場合は、自分でコードを実装してみてください。
|
||||
|
||||
@@ -127,12 +127,6 @@ comments: true
|
||||
[class]{}-[func]{two_sum_brute_force}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="two_sum.zig"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
```
|
||||
|
||||
この方法の時間計算量は$O(n^2)$、空間計算量は$O(1)$で、大容量データでは非常に時間がかかる可能性があります。
|
||||
|
||||
## 10.4.2 ハッシュ探索:空間を時間と交換
|
||||
@@ -270,12 +264,6 @@ comments: true
|
||||
[class]{}-[func]{two_sum_hash_table}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="two_sum.zig"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
```
|
||||
|
||||
この方法は、ハッシュ探索を使用することで時間計算量を$O(n^2)$から$O(n)$に削減し、実行時効率を大幅に向上させます。
|
||||
|
||||
追加のハッシュテーブルを維持する必要があるため、空間計算量は$O(n)$です。**それにもかかわらず、この方法は全体的により均衡のとれた時空間効率を持ち、この問題の最適解となります**。
|
||||
|
||||
@@ -160,12 +160,6 @@ comments: true
|
||||
[class]{}-[func]{bubble_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="bubble_sort.zig"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
```
|
||||
|
||||
## 11.3.2 効率の最適化
|
||||
|
||||
「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。
|
||||
@@ -298,12 +292,6 @@ comments: true
|
||||
[class]{}-[func]{bubble_sort_with_flag}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="bubble_sort.zig"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
```
|
||||
|
||||
## 11.3.3 アルゴリズムの特性
|
||||
|
||||
- **$O(n^2)$の時間計算量、適応ソート。** 各「バブリング」ラウンドは長さ$n - 1$、$n - 2$、$\dots$、$2$、$1$の配列セグメントを横断し、合計は$(n - 1) n / 2$となります。`flag`最適化により、配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$に達する可能性があります。
|
||||
|
||||
@@ -171,12 +171,6 @@ comments: true
|
||||
[class]{}-[func]{bucket_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="bucket_sort.zig"
|
||||
[class]{}-[func]{bucketSort}
|
||||
```
|
||||
|
||||
## 11.8.2 アルゴリズムの特徴
|
||||
|
||||
バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに100万個の要素が含まれ、システムメモリの制限によりすべてのデータを同時にロードできない場合、データを1,000個のバケットに分割し、各バケットを個別にソートしてから結果をマージできます。
|
||||
|
||||
@@ -157,12 +157,6 @@ comments: true
|
||||
[class]{}-[func]{counting_sort_naive}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="counting_sort.zig"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
```
|
||||
|
||||
!!! note "計数ソートとバケットソートの関係"
|
||||
|
||||
バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。
|
||||
@@ -376,12 +370,6 @@ $$
|
||||
[class]{}-[func]{counting_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="counting_sort.zig"
|
||||
[class]{}-[func]{countingSort}
|
||||
```
|
||||
|
||||
## 11.9.3 アルゴリズムの特徴
|
||||
|
||||
- **時間計算量は $O(n + m)$、非適応ソート**:`nums` と `counter` の走査が含まれ、どちらも線形時間を使用します。一般的に、$n \gg m$ であり、時間計算量は $O(n)$ に近づきます。
|
||||
|
||||
@@ -268,14 +268,6 @@ comments: true
|
||||
[class]{}-[func]{heap_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="heap_sort.zig"
|
||||
[class]{}-[func]{siftDown}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
```
|
||||
|
||||
## 11.7.2 アルゴリズムの特徴
|
||||
|
||||
- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープの構築は $O(n)$ 時間を使用します。ヒープから最大要素を抽出するには $O(\log n)$ 時間がかかり、$n - 1$ ラウンドループします。
|
||||
|
||||
@@ -141,12 +141,6 @@ comments: true
|
||||
[class]{}-[func]{insertion_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="insertion_sort.zig"
|
||||
[class]{}-[func]{insertionSort}
|
||||
```
|
||||
|
||||
## 11.4.2 アルゴリズムの特性
|
||||
|
||||
- **時間計算量は$O(n^2)$、適応ソート**:最悪の場合、各挿入操作には$n - 1$、$n-2$、...、$2$、$1$のループが必要で、合計は$(n - 1) n / 2$となり、時間計算量は$O(n^2)$です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量$O(n)$を実現します。
|
||||
|
||||
@@ -274,14 +274,6 @@ comments: true
|
||||
[class]{}-[func]{merge_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="merge_sort.zig"
|
||||
[class]{}-[func]{merge}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
```
|
||||
|
||||
## 11.6.2 アルゴリズムの特性
|
||||
|
||||
- **$O(n \log n)$の時間計算量、非適応ソート**:分割により高さ$\log n$の再帰ツリーが作成され、各層で合計$n$回の操作をマージし、全体的な時間計算量は$O(n \log n)$となります。
|
||||
|
||||
@@ -183,14 +183,6 @@ comments: true
|
||||
[class]{QuickSort}-[func]{partition}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="quick_sort.zig"
|
||||
[class]{QuickSort}-[func]{swap}
|
||||
|
||||
[class]{QuickSort}-[func]{partition}
|
||||
```
|
||||
|
||||
## 11.5.1 アルゴリズムプロセス
|
||||
|
||||
クイックソートの全体的なプロセスは下図に示されます。
|
||||
@@ -310,12 +302,6 @@ comments: true
|
||||
[class]{QuickSort}-[func]{quick_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="quick_sort.zig"
|
||||
[class]{QuickSort}-[func]{quickSort}
|
||||
```
|
||||
|
||||
## 11.5.2 アルゴリズムの特徴
|
||||
|
||||
- **$O(n \log n)$の時間計算量、非適応ソート**:平均的なケースでは、ピボット分割の再帰レベルは$\log n$で、レベルあたりのループの総数は$n$であり、全体で$O(n \log n)$の時間を使用します。最悪の場合、各ラウンドのピボット分割は長さ$n$の配列を長さ$0$と$n - 1$の2つのサブ配列に分割し、再帰レベル数が$n$に達すると、各レベルのループ数は$n$で、使用される総時間は$O(n^2)$です。
|
||||
@@ -520,14 +506,6 @@ comments: true
|
||||
[class]{QuickSortMedian}-[func]{partition}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="quick_sort.zig"
|
||||
[class]{QuickSortMedian}-[func]{medianThree}
|
||||
|
||||
[class]{QuickSortMedian}-[func]{partition}
|
||||
```
|
||||
|
||||
## 11.5.5 末尾再帰最適化
|
||||
|
||||
**特定の入力では、クイックソートはより多くの空間を占有する可能性があります**。例えば、完全に順序付けられた入力配列を考えてみましょう。再帰でのサブ配列の長さを$m$とします。各ラウンドのピボット分割で、長さ$0$の左サブ配列と長さ$m - 1$の右サブ配列が生成されます。これは、再帰呼び出しごとに問題サイズが1つの要素のみ減少することを意味し、各レベルの再帰での削減が非常に小さくなります。
|
||||
@@ -654,9 +632,3 @@ comments: true
|
||||
```ruby title="quick_sort.rb"
|
||||
[class]{QuickSortTailCall}-[func]{quick_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="quick_sort.zig"
|
||||
[class]{QuickSortTailCall}-[func]{quickSort}
|
||||
```
|
||||
|
||||
@@ -280,16 +280,6 @@ $$
|
||||
[class]{}-[func]{radix_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="radix_sort.zig"
|
||||
[class]{}-[func]{digit}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
```
|
||||
|
||||
!!! question "なぜ最下位桁から開始するのか?"
|
||||
|
||||
連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が $a < b$ で、2番目のラウンドが $a > b$ の場合、2番目のラウンドの結果が最初のラウンドの結果を置き換えます。上位桁は下位桁より優先されるため、上位桁の前に下位桁をソートすることが理にかなっています。
|
||||
|
||||
@@ -170,12 +170,6 @@ comments: true
|
||||
[class]{}-[func]{selection_sort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="selection_sort.zig"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
## 11.2.1 アルゴリズムの特性
|
||||
|
||||
- **$O(n^2)$の時間計算量、非適応ソート**:外側ループに$n - 1$回の反復があり、未ソートセクションの長さは最初の反復で$n$から始まり、最後の反復で$2$まで減少します。つまり、各外側ループ反復にはそれぞれ$n$、$n - 1$、$\dots$、$3$、$2$回の内側ループ反復が含まれ、合計は$\frac{(n - 1)(n + 2)}{2}$となります。
|
||||
|
||||
@@ -340,12 +340,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="deque.zig"
|
||||
|
||||
```
|
||||
|
||||
## 5.3.2 両端キューの実装 *
|
||||
|
||||
両端キューの実装は通常のキューの実装と似ており、連結リストまたは配列を基盤となるデータ構造として使用できます。
|
||||
@@ -845,14 +839,6 @@ comments: true
|
||||
[class]{LinkedListDeque}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linkedlist_deque.zig"
|
||||
[class]{ListNode}-[func]{}
|
||||
|
||||
[class]{LinkedListDeque}-[func]{}
|
||||
```
|
||||
|
||||
### 2. 配列に基づく実装
|
||||
|
||||
下図に示すように、配列でキューを実装するのと同様に、循環配列を使って両端キューを実装することもできます。
|
||||
@@ -1245,12 +1231,6 @@ comments: true
|
||||
[class]{ArrayDeque}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array_deque.zig"
|
||||
[class]{ArrayDeque}-[func]{}
|
||||
```
|
||||
|
||||
## 5.3.3 両端キューの応用
|
||||
|
||||
両端キューはスタックとキューの両方のロジックを組み合わせているため、**それぞれのすべてのユースケースを実装でき、より大きな柔軟性を提供します**。
|
||||
|
||||
@@ -318,12 +318,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="queue.zig"
|
||||
|
||||
```
|
||||
|
||||
## 5.2.2 キューの実装
|
||||
|
||||
キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列の両方がこの要件を満たします。
|
||||
@@ -611,12 +605,6 @@ comments: true
|
||||
[class]{LinkedListQueue}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linkedlist_queue.zig"
|
||||
[class]{LinkedListQueue}-[func]{}
|
||||
```
|
||||
|
||||
### 2. 配列ベースの実装
|
||||
|
||||
配列の最初の要素を削除する時間計算量は$O(n)$で、デキュー操作が非効率になります。しかし、この問題は以下のように巧妙に回避できます。
|
||||
@@ -915,12 +903,6 @@ comments: true
|
||||
[class]{ArrayQueue}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array_queue.zig"
|
||||
[class]{ArrayQueue}-[func]{}
|
||||
```
|
||||
|
||||
上記のキュー実装にはまだ制限があります:長さが固定されています。しかし、この問題は解決が困難ではありません。配列を必要に応じて自動拡張できる動的配列に置き換えることができます。興味のある読者は自分で実装してみてください。
|
||||
|
||||
2つの実装の比較はスタックの場合と一貫しており、ここでは繰り返しません。
|
||||
|
||||
@@ -312,12 +312,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="stack.zig"
|
||||
|
||||
```
|
||||
|
||||
## 5.1.2 スタックの実装
|
||||
|
||||
スタックがどのように動作するかをより深く理解するために、自分でスタッククラスを実装してみましょう。
|
||||
@@ -580,12 +574,6 @@ comments: true
|
||||
[class]{LinkedListStack}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linkedlist_stack.zig"
|
||||
[class]{LinkedListStack}-[func]{}
|
||||
```
|
||||
|
||||
### 2. 配列ベースの実装
|
||||
|
||||
配列を使用してスタックを実装する場合、配列の末尾をスタックのトップと考えることができます。下図に示すように、プッシュとポップ操作は、それぞれ配列の末尾での要素の追加と削除に対応し、どちらも時間計算量$O(1)$です。
|
||||
@@ -795,12 +783,6 @@ comments: true
|
||||
[class]{ArrayStack}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array_stack.zig"
|
||||
[class]{ArrayStack}-[func]{}
|
||||
```
|
||||
|
||||
## 5.1.3 2つの実装の比較
|
||||
|
||||
**サポートされる操作**
|
||||
|
||||
@@ -134,12 +134,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 7-14 任意の種類の二分木の配列表現 </p>
|
||||
@@ -480,12 +474,6 @@ comments: true
|
||||
[class]{ArrayBinaryTree}-[func]{}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array_binary_tree.zig"
|
||||
[class]{ArrayBinaryTree}-[func]{}
|
||||
```
|
||||
|
||||
## 7.3.3 利点と制限
|
||||
|
||||
二分木の配列表現には以下の利点があります:
|
||||
|
||||
@@ -225,12 +225,6 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、つまり通過する「辺」の数を指します。重要なのは、葉ノードの高さは$0$で、nullノードの高さは$-1$であることです。ノードの高さを取得し、更新するための2つのユーティリティ関数を作成します:
|
||||
|
||||
=== "Python"
|
||||
@@ -361,14 +355,6 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
[class]{AVLTree}-[func]{update_height}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="avl_tree.zig"
|
||||
[class]{AVLTree}-[func]{height}
|
||||
|
||||
[class]{AVLTree}-[func]{updateHeight}
|
||||
```
|
||||
|
||||
### 2. ノードの平衡因子
|
||||
|
||||
ノードの<u>平衡因子</u>は、そのノードの左部分木の高さから右部分木の高さを引いた値として定義され、nullノードの平衡因子は$0$として定義されます。後で使いやすくするため、ノードの平衡因子を取得する機能も関数にカプセル化します:
|
||||
@@ -471,12 +457,6 @@ AVL木に関連する操作ではノードの高さを取得する必要があ
|
||||
[class]{AVLTree}-[func]{balance_factor}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="avl_tree.zig"
|
||||
[class]{AVLTree}-[func]{balanceFactor}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
平衡因子を$f$とすると、AVL木の任意のノードの平衡因子は$-1 \le f \le 1$を満たします。
|
||||
@@ -626,12 +606,6 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中
|
||||
[class]{AVLTree}-[func]{right_rotate}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="avl_tree.zig"
|
||||
[class]{AVLTree}-[func]{rightRotate}
|
||||
```
|
||||
|
||||
### 2. 左回転
|
||||
|
||||
対応して、上記の不平衡二分木の「鏡像」を考慮すると、下図に示す「左回転」操作を実行する必要があります。
|
||||
@@ -761,12 +735,6 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中
|
||||
[class]{AVLTree}-[func]{left_rotate}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="avl_tree.zig"
|
||||
[class]{AVLTree}-[func]{leftRotate}
|
||||
```
|
||||
|
||||
### 3. 左右回転
|
||||
|
||||
下図に示す不平衡ノード3の場合、左回転または右回転のいずれかだけでは部分木のバランスを回復できません。この場合、まず`child`に対して「左回転」を実行し、次に`node`に対して「右回転」を実行する必要があります。
|
||||
@@ -965,12 +933,6 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中
|
||||
[class]{AVLTree}-[func]{rotate}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="avl_tree.zig"
|
||||
[class]{AVLTree}-[func]{rotate}
|
||||
```
|
||||
|
||||
## 7.5.3 AVL木の一般的な操作
|
||||
|
||||
### 1. ノードの挿入
|
||||
@@ -1136,14 +1098,6 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。
|
||||
[class]{AVLTree}-[func]{insert_helper}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="avl_tree.zig"
|
||||
[class]{AVLTree}-[func]{insert}
|
||||
|
||||
[class]{AVLTree}-[func]{insertHelper}
|
||||
```
|
||||
|
||||
### 2. ノードの削除
|
||||
|
||||
同様に、二分探索木でのノード削除方法に基づいて、下から上へ回転操作を実行してすべての不平衡ノードのバランスを回復する必要があります。コードは以下の通りです:
|
||||
@@ -1359,14 +1313,6 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。
|
||||
[class]{AVLTree}-[func]{remove_helper}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="avl_tree.zig"
|
||||
[class]{AVLTree}-[func]{remove}
|
||||
|
||||
[class]{AVLTree}-[func]{removeHelper}
|
||||
```
|
||||
|
||||
### 3. ノードの検索
|
||||
|
||||
AVL木でのノード検索操作は二分探索木のそれと一致しており、ここでは詳述しません。
|
||||
|
||||
@@ -167,12 +167,6 @@ comments: true
|
||||
[class]{BinarySearchTree}-[func]{search}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_tree.zig"
|
||||
[class]{BinarySearchTree}-[func]{search}
|
||||
```
|
||||
|
||||
### 2. ノードの挿入
|
||||
|
||||
挿入する要素`num`が与えられた場合、二分探索木の性質「左部分木 < 根ノード < 右部分木」を維持するため、挿入操作は下図に示すように進行します。
|
||||
@@ -345,12 +339,6 @@ comments: true
|
||||
[class]{BinarySearchTree}-[func]{insert}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_tree.zig"
|
||||
[class]{BinarySearchTree}-[func]{insert}
|
||||
```
|
||||
|
||||
ノードの検索と同様に、ノードの挿入には$O(\log n)$の時間を使用します。
|
||||
|
||||
### 3. ノードの削除
|
||||
@@ -615,12 +603,6 @@ comments: true
|
||||
[class]{BinarySearchTree}-[func]{remove}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_tree.zig"
|
||||
[class]{BinarySearchTree}-[func]{remove}
|
||||
```
|
||||
|
||||
### 4. 中順走査は順序付けされている
|
||||
|
||||
下図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の走査順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」のサイズ関係を満たします。
|
||||
|
||||
@@ -196,12 +196,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
各ノードは2つの参照(ポインタ)を持ち、それぞれ<u>左の子ノード</u>と<u>右の子ノード</u>を指しています。このノードは、これら2つの子ノードの<u>親ノード</u>と呼ばれます。二分木のノードが与えられたとき、このノードの左の子とその下にあるすべてのノードで形成される木を、このノードの<u>左部分木</u>と呼びます。同様に、<u>右部分木</u>も定義できます。
|
||||
|
||||
**二分木では、葉ノードを除いて、他のすべてのノードは子ノードと空でない部分木を含みます。** 下図に示すように、「ノード2」を親ノードとして見ると、その左と右の子ノードはそれぞれ「ノード4」と「ノード5」です。左部分木は「ノード4」とその下にあるすべてのノードで形成され、右部分木は「ノード5」とその下にあるすべてのノードで形成されます。
|
||||
@@ -443,12 +437,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_tree.zig"
|
||||
|
||||
```
|
||||
|
||||
### 2. ノードの挿入と削除
|
||||
|
||||
連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。下図に例を示します。
|
||||
@@ -603,12 +591,6 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_tree.zig"
|
||||
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
ノードの挿入は二分木の元の論理構造を変更する可能性があり、ノードの削除は通常そのノードとそのすべての部分木を削除することになることに注意してください。したがって、二分木では、挿入と削除は通常一連の操作を通じて実行され、意味のある結果を得ます。
|
||||
|
||||
@@ -147,12 +147,6 @@ comments: true
|
||||
[class]{}-[func]{level_order}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_tree_bfs.zig"
|
||||
[class]{}-[func]{levelOrder}
|
||||
```
|
||||
|
||||
### 2. 計算量分析
|
||||
|
||||
- **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間がかかります。ここで$n$はノード数です。
|
||||
@@ -371,16 +365,6 @@ comments: true
|
||||
[class]{}-[func]{post_order}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_tree_dfs.zig"
|
||||
[class]{}-[func]{preOrder}
|
||||
|
||||
[class]{}-[func]{inOrder}
|
||||
|
||||
[class]{}-[func]{postOrder}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
深度優先探索は反復に基づいても実装できます。興味のある読者は自分で学習してください。
|
||||
|
||||
Reference in New Issue
Block a user