docs: add Japanese translate documents (#1812)

* docs: add Japanese documents (`ja/docs`)

* docs: add Japanese documents (`ja/codes`)

* docs: add Japanese documents

* Remove pythontutor blocks in ja/

* Add an empty at the end of each markdown file.

* Add the missing figures (use the English version temporarily).

* Add index.md for Japanese version.

* Add index.html for Japanese version.

* Add missing index.assets

* Fix backtracking_algorithm.md for Japanese version.

* Add avatar_eltociear.jpg. Fix image links on the Japanese landing page.

* Add the Japanese banner.

---------

Co-authored-by: krahets <krahets@163.com>
This commit is contained in:
Ikko Eltociear Ashimine
2025-10-17 06:04:43 +09:00
committed by GitHub
parent 2487a27036
commit 954c45864b
886 changed files with 33569 additions and 0 deletions
@@ -0,0 +1,9 @@
# 複雑度解析
![Complexity analysis](../assets/covers/chapter_complexity_analysis.jpg)
!!! abstract
複雑度解析は、アルゴリズムの広大な宇宙における時空のナビゲーターのようなものです。
時間と空間の次元をより深く探求し、より優雅な解決策を求めるためのガイドとなります。
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1,194 @@
# 反復と再帰
アルゴリズムにおいて、タスクの繰り返し実行は非常に一般的であり、複雑度の分析と密接に関係しています。したがって、時間計算量と空間計算量の概念を詳しく学ぶ前に、まずプログラミングで繰り返しタスクを実装する方法を探究しましょう。これには、2つの基本的なプログラミング制御構造である反復と再帰の理解が含まれます。
## 反復
<u>反復</u>は、タスクを繰り返し実行するための制御構造です。反復では、プログラムは特定の条件が満たされている限りコードブロックを繰り返し実行し、この条件が満たされなくなるまで続けます。
### forループ
`for`ループは反復の最も一般的な形式の1つであり、**反復回数が事前に分かっている場合に特に適しています**。
以下の関数は`for`ループを使用して$1 + 2 + \dots + n$の合計を実行し、合計を変数`res`に格納します。Pythonでは、`range(a, b)``a`を含み`b`を除く区間を作成することに注意してください。つまり、$a$から$b−1$までの範囲で反復します。
```src
[file]{iteration}-[class]{}-[func]{for_loop}
```
以下の図はこの合計関数を表しています。
![Flowchart of the sum function](iteration_and_recursion.assets/iteration.png)
この合計関数での操作数は入力データのサイズ$n$に比例する、つまり線形関係があります。**この「線形関係」こそが時間計算量が記述するものです**。このトピックについては次のセクションで詳しく説明します。
### whileループ
`for`ループと同様に、`while`ループは反復を実装するためのもう1つのアプローチです。`while`ループでは、プログラムは各反復の開始時に条件をチェックし、条件が真の場合は実行を継続し、そうでなければループを終了します。
以下では`while`ループを使用して合計$1 + 2 + \dots + n$を実装します。
```src
[file]{iteration}-[class]{}-[func]{while_loop}
```
**`while`ループは`for`ループよりも柔軟性を提供します**。特に、条件変数のカスタム初期化と各ステップでの変更が可能です。
例えば、以下のコードでは、条件変数$i$が各ラウンドで2回更新されますが、これは`for`ループでは実装が不便です。
```src
[file]{iteration}-[class]{}-[func]{while_loop_ii}
```
全体的に、**`for`ループはより簡潔で、`while`ループはより柔軟です**。どちらも反復構造を実装できます。どちらを使用するかは、問題の具体的な要件に基づいて決定する必要があります。
### ネストしたループ
1つのループ構造を別のループ構造内にネストできます。以下は`for`ループを使用した例です:
```src
[file]{iteration}-[class]{}-[func]{nested_for_loop}
```
以下の図はこのネストしたループを表しています。
![Flowchart of the nested loop](iteration_and_recursion.assets/nested_iteration.png)
このような場合、関数の操作数は$n^2$に比例します。つまり、アルゴリズムの実行時間と入力データのサイズ$n$には「二次関係」があります。
さらにネストしたループを追加することで複雑度を高めることができ、各レベルのネストは事実上「次元を増加」させ、時間計算量を「三次」、「四次」などに引き上げます。
## 再帰
<u>再帰</u>は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に2つのフェーズが含まれます:
1. **呼び出し**: プログラムが自分自身を繰り返し呼び出し、しばしばより小さいまたはより単純な引数で、「終了条件」に向かって進みます。
2. **返却**: 「終了条件」がトリガーされると、プログラムは最も深い再帰関数から返り始め、各レイヤーの結果を集約します。
実装の観点から、再帰コードは主に3つの要素を含みます。
1. **終了条件**: 「呼び出し」から「返却」にいつ切り替えるかを決定します。
2. **再帰呼び出し**: 「呼び出し」に対応し、関数が自分自身を呼び出し、通常はより小さいまたはより単純化されたパラメータで行います。
3. **結果の返却**: 「返却」に対応し、現在の再帰レベルの結果が前のレイヤーに返されます。
以下のコードを観察してください。単純に関数`recur(n)`を呼び出すだけで$1 + 2 + \dots + n$の合計を計算できます:
```src
[file]{recursion}-[class]{}-[func]{recur}
```
以下の図はこの関数の再帰プロセスを示しています。
![Recursive process of the sum function](iteration_and_recursion.assets/recursion_sum.png)
反復と再帰は計算の観点から同じ結果を達成できますが、**それらは思考と問題解決の全く異なるパラダイムを表します**。
- **反復**: 「ボトムアップ」で問題を解決します。最も基本的なステップから始まり、タスクが完了するまでこれらのステップを繰り返し追加または累積します。
- **再帰**: 「トップダウン」で問題を解決します。元の問題をより小さなサブ問題に分解し、各サブ問題は元の問題と同じ形式を持ちます。これらのサブ問題は、解が分かっているベースケースで停止するまで、さらに小さなサブ問題に分解されます。
先ほどの合計関数の例を取ってみましょう。$f(n) = 1 + 2 + \dots + n$として定義されます。
- **反復**: このアプローチでは、ループ内で合計プロセスをシミュレートします。$1$から始まり$n$まで横断し、各反復で合計操作を実行して最終的に$f(n)$を計算します。
- **再帰**: ここでは、問題はサブ問題に分解されます:$f(n) = n + f(n-1)$。この分解は、ベースケースの$f(1) = 1$に到達するまで再帰的に続き、そこで再帰が終了します。
### 呼び出しスタック
再帰関数が自分自身を呼び出すたびに、システムは新しく開始された関数にメモリを割り当てて、ローカル変数、戻りアドレス、その他の関連情報を格納します。これは2つの主要な結果をもたらします。
- 関数のコンテキストデータは「スタックフレーム空間」と呼ばれるメモリ領域に格納され、関数が返された後にのみ解放されます。したがって、**再帰は一般的に反復よりも多くのメモリ空間を消費します**。
- 再帰呼び出しは追加のオーバーヘッドを導入します。**したがって、再帰は通常ループよりも時間効率が劣ります。**
以下の図に示されているように、終了条件がトリガーされる前に$n$個の未返却の再帰関数があり、**再帰の深さが$n$であることを示しています**。
![Recursion call depth](iteration_and_recursion.assets/recursion_sum_depth.png)
実際には、プログラミング言語で許可される再帰の深さは通常制限されており、過度に深い再帰はスタックオーバーフローエラーを引き起こす可能性があります。
### 末尾再帰
興味深いことに、**関数が返す直前の最後のステップとして再帰呼び出しを実行する場合**、コンパイラまたはインタープリターによって反復と同じ空間効率になるように最適化できます。このシナリオは<u>末尾再帰</u>として知られています。
- **通常の再帰**: 標準的な再帰では、関数が前のレベルに戻ったとき、さらにコードを実行し続けるため、システムは前の呼び出しのコンテキストを保存する必要があります。
- **末尾再帰**: ここでは、再帰呼び出しは関数が返す前の最終操作です。これは、前のレベルに戻った際に、さらなるアクションが必要ないことを意味するため、システムは前のレベルのコンテキストを保存する必要がありません。
例えば、$1 + 2 + \dots + n$の計算では、結果変数`res`を関数のパラメータにすることで、末尾再帰を実現できます:
```src
[file]{recursion}-[class]{}-[func]{tail_recur}
```
末尾再帰の実行プロセスは以下の図に示されています。通常の再帰と末尾再帰を比較すると、合計操作のポイントが異なります。
- **通常の再帰**: 合計操作は「返却」フェーズで発生し、各レイヤーが返った後にもう一度合計が必要です。
- **末尾再帰**: 合計操作は「呼び出し」フェーズで発生し、「返却」フェーズは各レイヤーを通じて返すだけです。
![Tail recursion process](iteration_and_recursion.assets/tail_recursion_sum.png)
!!! tip
多くのコンパイラやインタープリターは末尾再帰最適化をサポートしていないことに注意してください。例えば、Pythonはデフォルトで末尾再帰最適化をサポートしていないため、関数が末尾再帰の形式であっても、スタックオーバーフローの問題に遭遇する可能性があります。
### 再帰木
「分割統治」に関連するアルゴリズムを扱う際、再帰は反復よりもしばしばより直感的なアプローチとより読みやすいコードを提供します。「フィボナッチ数列」を例に取ってみましょう。
!!! question
フィボナッチ数列$0, 1, 1, 2, 3, 5, 8, 13, \dots$が与えられた場合、数列の$n$番目の数を求めなさい。
フィボナッチ数列の$n$番目の数を$f(n)$とすると、2つの結論を簡単に導き出せます:
- 数列の最初の2つの数は$f(1) = 0$と$f(2) = 1$です。
- 数列の各数は前の2つの数の合計です。つまり、$f(n) = f(n - 1) + f(n - 2)$です。
再帰関係を使用し、最初の2つの数を終了条件として考慮すると、再帰コードを書けます。`fib(n)`を呼び出すとフィボナッチ数列の$n$番目の数が得られます:
```src
[file]{recursion}-[class]{}-[func]{fib}
```
上記のコードを観察すると、それ自体の中で2つの関数を再帰的に呼び出していることがわかります。**つまり、1回の呼び出しで2つの分岐呼び出しが生成されます**。以下の図に示されているように、この継続的な再帰呼び出しは最終的に深さ$n$の<u>再帰木</u>を作成します。
![Fibonacci sequence recursion tree](iteration_and_recursion.assets/recursion_tree.png)
基本的に、再帰は「問題をより小さなサブ問題に分解する」パラダイムを体現しています。この分割統治戦略は重要です。
- アルゴリズムの観点から、探索、ソート、バックトラッキング、分割統治、動的プログラミングなどの多くの重要な戦略は、直接的または間接的にこの思考方法を使用しています。
- データ構造の観点から、再帰は連結リスト、木、グラフを扱うのに自然に適しており、これらは分割統治アプローチを使用した分析に適しているためです。
## 比較
上記の内容をまとめると、以下の表は実装、性能、適用性の観点から反復と再帰の違いを示しています。
<p align="center"> 表: 反復と再帰の特性の比較 </p>
| | 反復 | 再帰 |
| ----------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| アプローチ | ループ構造 | 関数が自分自身を呼び出す |
| 時間効率 | 一般的により高い効率、関数呼び出しのオーバーヘッドなし | 各関数呼び出しがオーバーヘッドを生成 |
| メモリ使用量 | 通常は固定サイズのメモリ空間を使用 | 累積的な関数呼び出しが大量のスタックフレーム空間を使用する可能性 |
| 適用可能な問題 | 単純なループタスクに適している、直感的で読みやすいコード | 問題の分解に適している(木、グラフ、分割統治、バックトラッキングなど)、簡潔で明確なコード構造 |
!!! tip
以下の内容が理解しにくい場合は、「スタック」の章を読んだ後に再び訪れることを検討してください。
それでは、反復と再帰の本質的な関連は何でしょうか?上記の再帰関数を例に取ると、合計操作は再帰の「返却」フェーズで発生します。これは、最初に呼び出された関数が最後に合計操作を完了することを意味し、**スタックの「後入れ先出し」原理を反映しています**。
「呼び出しスタック」や「スタックフレーム空間」などの再帰用語は、再帰とスタックの密接な関係を示しています。
1. **呼び出し**: 関数が呼び出されると、システムは「呼び出しスタック」上にその関数用の新しいスタックフレームを割り当て、ローカル変数、パラメータ、戻りアドレス、その他のデータを格納します。
2. **返却**: 関数が実行を完了して返ると、対応するスタックフレームが「呼び出しスタック」から削除され、前の関数の実行環境が復元されます。
したがって、**明示的なスタックを使用して呼び出しスタックの動作をシミュレートできます**。これにより再帰を反復形式に変換できます:
```src
[file]{recursion}-[class]{}-[func]{for_loop_recur}
```
上記のコードを観察すると、再帰が反復に変換されたとき、コードはより複雑になります。反復と再帰はしばしば相互に変換できますが、2つの理由でそうすることが常に推奨されるわけではありません:
- 変換されたコードは理解がより困難になり、読みにくくなる可能性があります。
- 一部の複雑な問題では、システムの呼び出しスタックの動作をシミュレートすることは非常に困難です。
結論として、**反復または再帰を選択するかは問題の具体的な性質によります**。プログラミングの実践では、両方の長所と短所を比較検討し、手元の状況に最も適したアプローチを選択することが重要です。
@@ -0,0 +1,49 @@
# アルゴリズムの効率評価
アルゴリズム設計において、私たちは順序に従って以下の2つの目標を追求します。
1. **問題の解決策を見つける**: アルゴリズムは、指定された入力範囲内で確実に正しい解を見つけることができるべきです。
2. **最適解を求める**: 同じ問題に対して複数の解決策が存在する場合があり、私たちは可能な限り最も効率的なアルゴリズムを見つけることを目指します。
つまり、問題を解決できることを前提として、アルゴリズムの効率がアルゴリズムを評価する主要な基準となっており、これには以下の2つの次元が含まれます。
- **時間効率**: アルゴリズムが実行される速度。
- **空間効率**: アルゴリズムが占有するメモリ空間のサイズ。
要するに、**私たちの目標は、高速でメモリ効率の良いデータ構造とアルゴリズムを設計することです**。アルゴリズムの効率を効果的に評価することは重要です。なぜなら、そうすることで初めて様々なアルゴリズムを比較し、アルゴリズムの設計と最適化プロセスを導くことができるからです。
効率評価には主に2つの方法があります:実際のテストと理論的推定です。
## 実際のテスト
アルゴリズム`A``B`があり、どちらも同じ問題を解決でき、それらの効率を比較する必要があるとします。最も直接的な方法は、コンピュータを使用してこれら2つのアルゴリズムを実行し、実行時間とメモリ使用量を監視・記録することです。この評価方法は実際の状況を反映しますが、大きな制限があります。
一方で、**テスト環境からの干渉を排除することは困難です**。ハードウェア構成はアルゴリズムの性能に影響を与える可能性があります。例えば、並列度の高いアルゴリズムはマルチコアCPUでの実行により適していますし、集約的なメモリ操作を含むアルゴリズムは高性能メモリでより良い性能を発揮します。アルゴリズムのテスト結果は、異なるマシン間で変わる可能性があります。これは、平均効率を計算するために複数のマシンでテストすることが実用的でないことを意味します。
一方で、**完全なテストを実施することは非常にリソース集約的です**。アルゴリズムの効率は入力データサイズによって変わります。例えば、データ量が少ない場合はアルゴリズム`A``B`より速く実行される可能性がありますが、データ量が多い場合はテスト結果が逆になる可能性があります。したがって、説得力のある結論を導くためには、幅広い入力データサイズをテストする必要があり、これには過度な計算リソースが必要になります。
## 理論的推定
実際のテストの大きな制限により、計算のみでアルゴリズムの効率を評価することを検討できます。この推定方法は<u>漸近的複雑度解析</u>、または単に<u>複雑度解析</u>として知られています。
複雑度解析は、アルゴリズムの実行に必要な時間と空間リソースと入力データのサイズとの関係を反映します。**これは、入力データのサイズが増加するにつれて、アルゴリズムに必要な時間と空間の増加傾向を記述します**。この定義は複雑に聞こえるかもしれませんが、より良く理解するために3つの重要なポイントに分解できます。
- 「時間と空間リソース」は、それぞれ<u>時間計算量</u>と<u>空間計算量</u>に対応します。
- 「入力データのサイズが増加するにつれて」は、複雑度がアルゴリズムの効率と入力データ量との関係を反映することを意味します。
- 「時間と空間の増加傾向」は、複雑度解析が実行時間や占有空間の具体的な値ではなく、時間や空間が増加する「率」に焦点を当てることを示します。
**複雑度解析は実際のテスト方法の欠点を克服します**。これは以下の側面で反映されます:
- 実際にコードを実行する必要がないため、より環境に優しく、エネルギー効率が良いです。
- テスト環境に依存せず、すべての動作プラットフォームに適用できます。
- 異なるデータ量でのアルゴリズムの効率を反映でき、特に大量データでのアルゴリズムの性能を示します。
!!! tip
複雑度の概念についてまだ混乱している場合でも、心配しないでください。以降の章で詳しく取り上げます。
複雑度解析は、アルゴリズムの効率を評価する「ものさし」を提供し、実行に必要な時間と空間リソースを測定し、異なるアルゴリズムの効率を比較することを可能にします。
複雑度は数学的概念であり、初心者には抽象的で困難かもしれません。この観点から、複雑度解析は最初に紹介するのに最も適したトピックではないかもしれません。しかし、特定のデータ構造やアルゴリズムの特性について議論するとき、その速度と空間使用量を分析することを避けるのは困難です。
要約すると、データ構造とアルゴリズムに深く入る前に複雑度解析の基本的な理解を身につけることをお勧めします。**これにより、簡単なアルゴリズムで複雑度解析を実行できるようになります**。
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,803 @@
# 空間計算量
<u>空間計算量</u>は、データ量が増加するにつれてアルゴリズムが占有するメモリ空間の増加傾向を測定するために使用されます。この概念は時間計算量と非常に似ていますが、「実行時間」が「占有メモリ空間」に置き換えられています。
## アルゴリズムに関連する空間
アルゴリズムが実行中に使用するメモリ空間には、主に以下の種類があります。
- **入力空間**: アルゴリズムの入力データを格納するために使用されます。
- **一時空間**: アルゴリズムの実行中に変数、オブジェクト、関数コンテキスト、その他のデータを格納するために使用されます。
- **出力空間**: アルゴリズムの出力データを格納するために使用されます。
一般的に、空間計算量の統計範囲には「一時空間」と「出力空間」の両方が含まれます。
一時空間はさらに3つの部分に分けることができます。
- **一時データ**: アルゴリズムの実行中に様々な定数、変数、オブジェクトなどを保存するために使用されます。
- **スタックフレーム空間**: 呼び出された関数のコンテキストデータを保存するために使用されます。システムは関数が呼び出されるたびにスタックの頂上にスタックフレームを作成し、関数が返された後にスタックフレーム空間を解放します。
- **命令空間**: コンパイル済みプログラム命令を格納するために使用され、実際の統計では通常無視できます。
プログラムの空間計算量を分析する際、**通常は一時データ、スタックフレーム空間、出力データをカウントします**。以下の図に示されています。
![Space types used in algorithms](space_complexity.assets/space_types.png)
関連するコードは以下の通りです:
=== "Python"
```python title=""
class Node:
"""クラス"""
def __init__(self, x: int):
self.val: int = x # ノード値
self.next: Node | None = None # 次のノードへの参照
def function() -> int:
"""関数"""
# 特定の操作を実行...
return 0
def algorithm(n) -> int: # 入力データ
A = 0 # 一時データ(定数、通常大文字)
b = 0 # 一時データ(変数)
node = Node(0) # 一時データ(オブジェクト)
c = function() # スタックフレーム空間(関数呼び出し)
return A + b + c # 出力データ
```
=== "C++"
```cpp title=""
/* 構造体 */
struct Node {
int val;
Node *next;
Node(int x) : val(x), next(nullptr) {}
};
/* 関数 */
int func() {
// 特定の操作を実行...
return 0;
}
int algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node* node = new Node(0); // 一時データ(オブジェクト)
int c = func(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "Java"
```java title=""
/* クラス */
class Node {
int val;
Node next;
Node(int x) { val = x; }
}
/* 関数 */
int function() {
// 特定の操作を実行...
return 0;
}
int algorithm(int n) { // 入力データ
final int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node node = new Node(0); // 一時データ(オブジェクト)
int c = function(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "C#"
```csharp title=""
/* クラス */
class Node {
int val;
Node next;
Node(int x) { val = x; }
}
/* 関数 */
int Function() {
// 特定の操作を実行...
return 0;
}
int Algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node node = new(0); // 一時データ(オブジェクト)
int c = Function(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "Go"
```go title=""
/* 構造体 */
type node struct {
val int
next *node
}
/* ノード構造体を作成 */
func newNode(val int) *node {
return &node{val: val}
}
/* 関数 */
func function() int {
// 特定の操作を実行...
return 0
}
func algorithm(n int) int { // 入力データ
const a = 0 // 一時データ(定数)
b := 0 // 一時データ(変数)
newNode(0) // 一時データ(オブジェクト)
c := function() // スタックフレーム空間(関数呼び出し)
return a + b + c // 出力データ
}
```
=== "Swift"
```swift title=""
/* クラス */
class Node {
var val: Int
var next: Node?
init(x: Int) {
val = x
}
}
/* 関数 */
func function() -> Int {
// 特定の操作を実行...
return 0
}
func algorithm(n: Int) -> Int { // 入力データ
let a = 0 // 一時データ(定数)
var b = 0 // 一時データ(変数)
let node = Node(x: 0) // 一時データ(オブジェクト)
let c = function() // スタックフレーム空間(関数呼び出し)
return a + b + c // 出力データ
}
```
=== "JS"
```javascript title=""
/* クラス */
class Node {
val;
next;
constructor(val) {
this.val = val === undefined ? 0 : val; // ノード値
this.next = null; // 次のノードへの参照
}
}
/* 関数 */
function constFunc() {
// 特定の操作を実行
return 0;
}
function algorithm(n) { // 入力データ
const a = 0; // 一時データ(定数)
let b = 0; // 一時データ(変数)
const node = new Node(0); // 一時データ(オブジェクト)
const c = constFunc(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "TS"
```typescript title=""
/* クラス */
class Node {
val: number;
next: Node | null;
constructor(val?: number) {
this.val = val === undefined ? 0 : val; // ノード値
this.next = null; // 次のノードへの参照
}
}
/* 関数 */
function constFunc(): number {
// 特定の操作を実行
return 0;
}
function algorithm(n: number): number { // 入力データ
const a = 0; // 一時データ(定数)
let b = 0; // 一時データ(変数)
const node = new Node(0); // 一時データ(オブジェクト)
const c = constFunc(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "Dart"
```dart title=""
/* クラス */
class Node {
int val;
Node next;
Node(this.val, [this.next]);
}
/* 関数 */
int function() {
// 特定の操作を実行...
return 0;
}
int algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node node = Node(0); // 一時データ(オブジェクト)
int c = function(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* 構造体 */
struct Node {
val: i32,
next: Option<Rc<RefCell<Node>>>,
}
/* コンストラクタ */
impl Node {
fn new(val: i32) -> Self {
Self { val: val, next: None }
}
}
/* 関数 */
fn function() -> i32 {
// 特定の操作を実行...
return 0;
}
fn algorithm(n: i32) -> i32 { // 入力データ
const a: i32 = 0; // 一時データ(定数)
let mut b = 0; // 一時データ(変数)
let node = Node::new(0); // 一時データ(オブジェクト)
let c = function(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "C"
```c title=""
/* 関数 */
int func() {
// 特定の操作を実行...
return 0;
}
int algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
int c = func(); // スタックフレーム空間(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "Kotlin"
```kotlin title=""
```
=== "Zig"
```zig title=""
```
## 計算方法
空間計算量を計算する方法は時間計算量とほぼ同様で、統計対象を「操作数」から「使用空間のサイズ」に変更するだけです。
しかし、時間計算量とは異なり、**通常は最悪ケース空間計算量のみに焦点を当てます**。これは、メモリ空間がハード要件であり、すべての入力データの下で十分なメモリ空間が確保されていることを保証する必要があるためです。
以下のコードを考えてみましょう。最悪ケース空間計算量の「最悪ケース」という用語には2つの意味があります。
1. **最悪の入力データに基づく**: $n < 10$の場合、空間計算量は$O(1)$ですが、$n > 10$の場合、初期化された配列`nums`が$O(n)$の空間を占有するため、最悪ケース空間計算量は$O(n)$です。
2. **アルゴリズムの実行中に使用されるピークメモリに基づく**: 例えば、最後の行を実行する前、プログラムは$O(1)$の空間を占有します。配列`nums`を初期化する際、プログラムは$O(n)$の空間を占有するため、最悪ケース空間計算量は$O(n)$です。
=== "Python"
```python title=""
def algorithm(n: int):
a = 0 # O(1)
b = [0] * 10000 # O(1)
if n > 10:
nums = [0] * n # O(n)
```
=== "C++"
```cpp title=""
void algorithm(int n) {
int a = 0; // O(1)
vector<int> b(10000); // O(1)
if (n > 10)
vector<int> nums(n); // O(n)
}
```
=== "Java"
```java title=""
void algorithm(int n) {
int a = 0; // O(1)
int[] b = new int[10000]; // O(1)
if (n > 10)
int[] nums = new int[n]; // O(n)
}
```
=== "C#"
```csharp title=""
void Algorithm(int n) {
int a = 0; // O(1)
int[] b = new int[10000]; // O(1)
if (n > 10) {
int[] nums = new int[n]; // O(n)
}
}
```
=== "Go"
```go title=""
func algorithm(n int) {
a := 0 // O(1)
b := make([]int, 10000) // O(1)
var nums []int
if n > 10 {
nums := make([]int, n) // O(n)
}
fmt.Println(a, b, nums)
}
```
=== "Swift"
```swift title=""
func algorithm(n: Int) {
let a = 0 // O(1)
let b = Array(repeating: 0, count: 10000) // O(1)
if n > 10 {
let nums = Array(repeating: 0, count: n) // O(n)
}
}
```
=== "JS"
```javascript title=""
function algorithm(n) {
const a = 0; // O(1)
const b = new Array(10000); // O(1)
if (n > 10) {
const nums = new Array(n); // O(n)
}
}
```
=== "TS"
```typescript title=""
function algorithm(n: number): void {
const a = 0; // O(1)
const b = new Array(10000); // O(1)
if (n > 10) {
const nums = new Array(n); // O(n)
}
}
```
=== "Dart"
```dart title=""
void algorithm(int n) {
int a = 0; // O(1)
List<int> b = List.filled(10000, 0); // O(1)
if (n > 10) {
List<int> nums = List.filled(n, 0); // O(n)
}
}
```
=== "Rust"
```rust title=""
fn algorithm(n: i32) {
let a = 0; // O(1)
let b = [0; 10000]; // O(1)
if n > 10 {
let nums = vec![0; n as usize]; // O(n)
}
}
```
=== "C"
```c title=""
void algorithm(int n) {
int a = 0; // O(1)
int b[10000]; // O(1)
if (n > 10)
int nums[n] = {0}; // O(n)
}
```
=== "Kotlin"
```kotlin title=""
```
=== "Zig"
```zig title=""
```
**再帰関数では、スタックフレーム空間を考慮に入れる必要があります**。以下のコードを考えてみましょう:
=== "Python"
```python title=""
def function() -> int:
# 特定の操作を実行
return 0
def loop(n: int):
"""ループ O(1)"""
for _ in range(n):
function()
def recur(n: int):
"""再帰 O(n)"""
if n == 1:
return
return recur(n - 1)
```
=== "C++"
```cpp title=""
int func() {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 再帰 O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Java"
```java title=""
int function() {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* 再帰 O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "C#"
```csharp title=""
int Function() {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
void Loop(int n) {
for (int i = 0; i < n; i++) {
Function();
}
}
/* 再帰 O(n) */
int Recur(int n) {
if (n == 1) return 1;
return Recur(n - 1);
}
```
=== "Go"
```go title=""
func function() int {
// 特定の操作を実行
return 0
}
/* サイクル O(1) */
func loop(n int) {
for i := 0; i < n; i++ {
function()
}
}
/* 再帰 O(n) */
func recur(n int) {
if n == 1 {
return
}
recur(n - 1)
}
```
=== "Swift"
```swift title=""
@discardableResult
func function() -> Int {
// 特定の操作を実行
return 0
}
/* サイクル O(1) */
func loop(n: Int) {
for _ in 0 ..< n {
function()
}
}
/* 再帰 O(n) */
func recur(n: Int) {
if n == 1 {
return
}
recur(n: n - 1)
}
```
=== "JS"
```javascript title=""
function constFunc() {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
function loop(n) {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 再帰 O(n) */
function recur(n) {
if (n === 1) return;
return recur(n - 1);
}
```
=== "TS"
```typescript title=""
function constFunc(): number {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
function loop(n: number): void {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 再帰 O(n) */
function recur(n: number): void {
if (n === 1) return;
return recur(n - 1);
}
```
=== "Dart"
```dart title=""
int function() {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* 再帰 O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Rust"
```rust title=""
fn function() -> i32 {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
fn loop(n: i32) {
for i in 0..n {
function();
}
}
/* 再帰 O(n) */
void recur(n: i32) {
if n == 1 {
return;
}
recur(n - 1);
}
```
=== "C"
```c title=""
int func() {
// 特定の操作を実行
return 0;
}
/* サイクル O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 再帰 O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Kotlin"
```kotlin title=""
```
=== "Zig"
```zig title=""
```
`loop()`関数と`recur()`関数の時間計算量は両方とも$O(n)$ですが、それらの空間計算量は異なります。
- `loop()`関数はループ内で`function()`を$n$回呼び出し、各反復の`function()`は返ってそのスタックフレーム空間を解放するため、空間計算量は$O(1)$のままです。
- 再帰関数`recur()`は実行中に$n$個の未返却の`recur()`インスタンスが同時に存在するため、$O(n)$のスタックフレーム空間を占有します。
## 一般的な種類
入力データのサイズを$n$とすると、下図は一般的な空間計算量の種類を示しています(低いものから高いものへと並べられています)。
$$
\begin{aligned}
& O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
& \text{定数} < \text{対数} < \text{線形} < \text{二次} < \text{指数}
\end{aligned}
$$
![Common types of space complexity](space_complexity.assets/space_complexity_common_types.png)
### 定数オーダー $O(1)$
定数オーダーは、入力データサイズ$n$とは無関係な定数、変数、オブジェクトで一般的です。
ループで変数を初期化したり関数を呼び出したりするために占有されるメモリは、次のサイクルに入る際に解放され、空間上で累積されないため、空間計算量は$O(1)$のままです:
```src
[file]{space_complexity}-[class]{}-[func]{constant}
```
### 線形オーダー $O(n)$
線形オーダーは配列、連結リスト、スタック、キューなどで一般的で、要素数は$n$に比例します:
```src
[file]{space_complexity}-[class]{}-[func]{linear}
```
下図に示されているように、この関数の再帰深度は$n$で、$n$個の未返却の`linear_recur()`関数インスタンスがあり、$O(n)$サイズのスタックフレーム空間を使用します:
```src
[file]{space_complexity}-[class]{}-[func]{linear_recur}
```
![Recursive function generating linear order space complexity](space_complexity.assets/space_complexity_recursive_linear.png)
### 二次オーダー $O(n^2)$
二次オーダーは行列やグラフで一般的で、要素数は$n$の二乗に比例します:
```src
[file]{space_complexity}-[class]{}-[func]{quadratic}
```
下図に示されているように、この関数の再帰深度は$n$で、各再帰呼び出しで長さ$n$、$n-1$、$\dots$、$2$、$1$の配列が初期化され、平均$n/2$となり、全体として$O(n^2)$の空間を占有します:
```src
[file]{space_complexity}-[class]{}-[func]{quadratic_recur}
```
![Recursive function generating quadratic order space complexity](space_complexity.assets/space_complexity_recursive_quadratic.png)
### 指数オーダー $O(2^n)$
指数オーダーは二分木で一般的です。下図を観察すると、$n$レベルの「完全二分木」は$2^n - 1$個のノードを持ち、$O(2^n)$の空間を占有します:
```src
[file]{space_complexity}-[class]{}-[func]{build_tree}
```
![Full binary tree generating exponential order space complexity](space_complexity.assets/space_complexity_exponential.png)
### 対数オーダー $O(\log n)$
対数オーダーは分割統治アルゴリズムで一般的です。例えば、マージソートでは、長さ$n$の配列が各ラウンドで再帰的に半分に分割され、高さ$\log n$の再帰木を形成し、$O(\log n)$のスタックフレーム空間を使用します。
別の例は、数値を文字列に変換することです。正の整数$n$が与えられた場合、その桁数は$\log_{10} n + 1$で、文字列の長さに対応するため、空間計算量は$O(\log_{10} n + 1) = O(\log n)$です。
## 時間と空間のバランス
理想的には、時間計算量と空間計算量の両方が最適であることを目指します。しかし、実際には両方を同時に最適化することはしばしば困難です。
**時間計算量を下げることは通常、空間計算量の増加を代償とし、その逆も同様です**。アルゴリズムの速度を向上させるためにメモリ空間を犠牲にするアプローチは「時空トレードオフ」として知られ、その逆は「空時トレードオフ」として知られています。
選択は、どちらの側面をより重視するかに依存します。ほとんどの場合、時間は空間よりも貴重であるため、「時空トレードオフ」がより一般的な戦略です。もちろん、大量のデータを扱う際は空間計算量を制御することも非常に重要です。
@@ -0,0 +1,49 @@
# まとめ
### 重要なレビュー
**アルゴリズム効率評価**
- 時間効率と空間効率は、アルゴリズムの優劣を評価する2つの主要な基準です。
- 実際のテストによってアルゴリズムの効率を評価できますが、テスト環境の影響を排除することは困難で、大量の計算リソースを消費します。
- 複雑度分析は実際のテストの欠点を克服できます。その結果はすべての動作プラットフォームに適用でき、異なるデータスケールでのアルゴリズムの効率を明らかにできます。
**時間計算量**
- 時間計算量は、データ量の増加に伴うアルゴリズムの実行時間の傾向を測定し、アルゴリズムの効率を効果的に評価します。しかし、入力データ量が少ない場合や時間計算量が同じ場合など、特定のケースでは失敗することがあり、アルゴリズムの効率を正確に比較することが困難になります。
- 最悪ケース時間計算量はビッグ$O$記法を使用して表記され、漸近上限を表し、$n$が無限大に近づくにつれての操作数$T(n)$の増加レベルを反映します。
- 時間計算量の計算には2つのステップが含まれます:まず操作数をカウントし、次に漸近上限を決定します。
- 一般的な時間計算量は、低いものから高いものへと並べると、$O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$、$O(n!)$などが含まれます。
- 一部のアルゴリズムの時間計算量は固定されておらず、入力データの分布に依存します。時間計算量は最悪、最良、平均のケースに分けられます。最良ケースは、入力データが最良ケースを達成するために厳格な条件を満たす必要があるため、ほとんど使用されません。
- 平均時間計算量は、ランダムデータ入力下でのアルゴリズムの効率を反映し、実際のアプリケーションでのアルゴリズムの性能に密接に類似しています。平均時間計算量の計算には、入力データの分布とその後の数学的期待値を考慮する必要があります。
**空間計算量**
- 空間計算量は、時間計算量と同様に、データ量の増加に伴うアルゴリズムが占有するメモリ空間の傾向を測定します。
- アルゴリズムの実行中に使用される関連メモリ空間は、入力空間、一時空間、出力空間に分けることができます。一般的に、入力空間は空間計算量の計算に含まれません。一時空間は一時データ、スタックフレーム空間、命令空間に分けることができ、スタックフレーム空間は通常、再帰関数でのみ空間計算量に影響します。
- 通常は最悪ケース空間計算量のみに焦点を当てます。これは、最悪の入力データと操作の最悪の瞬間でのアルゴリズムの空間計算量を計算することを意味します。
- 一般的な空間計算量は、低いものから高いものへと並べると、$O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$、$O(2^n)$などが含まれます。
### Q & A
**Q**: 末尾再帰の空間計算量は$O(1)$ですか?
理論的には、末尾再帰関数の空間計算量は$O(1)$に最適化できます。しかし、ほとんどのプログラミング言語(Java、Python、C++、Go、C#など)は末尾再帰の自動最適化をサポートしていないため、一般的に空間計算量は$O(n)$と考えられています。
**Q**: 「関数」と「メソッド」という用語の違いは何ですか?
<u>関数</u>は独立して実行でき、すべてのパラメータが明示的に渡されます。<u>メソッド</u>はオブジェクトに関連付けられ、それを呼び出すオブジェクトに暗黙的に渡され、クラスのインスタンス内に含まれるデータを操作できます。
一般的なプログラミング言語からの例をいくつか示します:
- Cは手続き型プログラミング言語で、オブジェクト指向の概念がないため、関数のみがあります。しかし、構造体(struct)を作成することでオブジェクト指向プログラミングをシミュレートでき、これらの構造体に関連付けられた関数は他のプログラミング言語のメソッドと同等です。
- JavaとC#はオブジェクト指向プログラミング言語で、コードブロック(メソッド)は通常クラスの一部です。静的メソッドはクラスにバインドされ、特定のインスタンス変数にアクセスできないため、関数のように動作します。
- C++とPythonは手続き型プログラミング(関数)とオブジェクト指向プログラミング(メソッド)の両方をサポートしています。
**Q**: 「空間計算量の一般的な種類」の図は、占有空間の絶対サイズを反映していますか?
いいえ、図は空間計算量を示しており、これは増加傾向を反映するものであり、占有空間の絶対サイズではありません。
$n = 8$を取ると、各曲線の値がその関数に対応していないことに気づくかもしれません。これは、各曲線に定数項が含まれているためで、値の範囲を視覚的に快適な範囲に圧縮することを意図しています。
実際には、通常は各メソッドの「定数項」複雑度を知らないため、複雑度のみに基づいて$n = 8$の最良ソリューションを選択することは一般的に不可能です。しかし、$n = 8^5$の場合、増加傾向が支配的になるため、選択がはるかに容易になります。
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff