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>
BIN
ja/docs/assets/covers/chapter_appendix.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
ja/docs/assets/covers/chapter_array_and_linkedlist.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
ja/docs/assets/covers/chapter_backtracking.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
ja/docs/assets/covers/chapter_complexity_analysis.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
ja/docs/assets/covers/chapter_data_structure.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
ja/docs/assets/covers/chapter_divide_and_conquer.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
ja/docs/assets/covers/chapter_dynamic_programming.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
ja/docs/assets/covers/chapter_graph.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
ja/docs/assets/covers/chapter_greedy.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
ja/docs/assets/covers/chapter_hashing.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
ja/docs/assets/covers/chapter_heap.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
ja/docs/assets/covers/chapter_hello_algo.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
ja/docs/assets/covers/chapter_introduction.jpg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
ja/docs/assets/covers/chapter_preface.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
ja/docs/assets/covers/chapter_searching.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
ja/docs/assets/covers/chapter_sorting.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
ja/docs/assets/covers/chapter_stack_and_queue.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
ja/docs/assets/covers/chapter_tree.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
ja/docs/chapter_appendix/contribution.assets/edit_markdown.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
47
ja/docs/chapter_appendix/contribution.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# コントリビューション
|
||||
|
||||
著者の能力に限りがあるため、本書にはいくつかの省略や誤りが避けられません。ご理解をお願いします。誤字、リンク切れ、内容の欠落、文章の曖昧さ、説明の不明確さ、または不合理な文章構造を発見された場合は、読者により良質な学習リソースを提供するため、修正にご協力ください。
|
||||
|
||||
すべての[コントリビューター](https://github.com/krahets/hello-algo/graphs/contributors)のGitHub IDは、本書のリポジトリ、ウェブ、PDFバージョンのホームページに表示され、オープンソースコミュニティへの無私の貢献に感謝いたします。
|
||||
|
||||
!!! success "オープンソースの魅力"
|
||||
|
||||
紙の本の2つの印刷版の間隔はしばしば長く、内容の更新が非常に不便です。
|
||||
|
||||
しかし、このオープンソースの本では、内容の更新サイクルは数日、さらには数時間に短縮されます。
|
||||
|
||||
### 内容の微調整
|
||||
|
||||
下の図に示すように、各ページの右上角に「編集アイコン」があります。以下の手順に従ってテキストやコードを修正できます。
|
||||
|
||||
1. 「編集アイコン」をクリックします。「このリポジトリをフォークしますか」と促された場合は、同意してください。
|
||||
2. Markdownソースファイルの内容を修正し、内容の正確性を確認し、フォーマットの一貫性を保つようにしてください。
|
||||
3. ページの下部で修正説明を記入し、「Propose file change」ボタンをクリックします。ページがリダイレクトされた後、「Create pull request」ボタンをクリックしてプルリクエストを開始します。
|
||||
|
||||

|
||||
|
||||
図は直接修正できないため、新しい[Issue](https://github.com/krahets/hello-algo/issues)を作成するか、問題を説明するコメントが必要です。できるだけ早く図を再描画して置き換えます。
|
||||
|
||||
### 内容の作成
|
||||
|
||||
このオープンソースプロジェクトへの参加に興味がある場合、コードを他のプログラミング言語に翻訳したり、記事の内容を拡張したりすることを含めて、以下のプルリクエストワークフローを実装する必要があります。
|
||||
|
||||
1. GitHubにログインし、本書の[コードリポジトリ](https://github.com/krahets/hello-algo)を個人アカウントにフォークします。
|
||||
2. フォークしたリポジトリのウェブページに移動し、`git clone`コマンドを使用してリポジトリをローカルマシンにクローンします。
|
||||
3. ローカルで内容を作成し、完全なテストを実行してコードの正確性を検証します。
|
||||
4. ローカルで行った変更をコミットし、リモートリポジトリにプッシュします。
|
||||
5. リポジトリのウェブページを更新し、「Create pull request」ボタンをクリックしてプルリクエストを開始します。
|
||||
|
||||
### Dockerデプロイメント
|
||||
|
||||
`hello-algo`ルートディレクトリで、以下のDockerスクリプトを実行して`http://localhost:8000`でプロジェクトにアクセスします:
|
||||
|
||||
```shell
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
以下のコマンドを使用してデプロイメントを削除します:
|
||||
|
||||
```shell
|
||||
docker-compose down
|
||||
```
|
||||
3
ja/docs/chapter_appendix/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 付録
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 103 KiB |
68
ja/docs/chapter_appendix/installation.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# インストール
|
||||
|
||||
## IDEのインストール
|
||||
|
||||
ローカルの統合開発環境(IDE)として、オープンソースで軽量なVS Codeを使用することをお勧めします。[VS Code公式ウェブサイト](https://code.visualstudio.com/)にアクセスし、お使いのオペレーティングシステムに適したVS Codeのバージョンを選択してダウンロードし、インストールしてください。
|
||||
|
||||

|
||||
|
||||
VS Codeには強力な拡張機能エコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。例えば、「Python Extension Pack」をインストールした後、Pythonコードをデバッグできます。インストール手順を下の図に示します。
|
||||
|
||||

|
||||
|
||||
## 言語環境のインストール
|
||||
|
||||
### Python環境
|
||||
|
||||
1. [Miniconda3](https://docs.conda.io/en/latest/miniconda.html)をダウンロードしてインストールします。Python 3.10以降が必要です。
|
||||
2. VS Code拡張機能マーケットプレイスで`python`を検索し、Python Extension Packをインストールします。
|
||||
3. (オプション)コマンドラインで`pip install black`を入力して、コードフォーマッティングツールをインストールします。
|
||||
|
||||
### C/C++環境
|
||||
|
||||
1. Windowsシステムでは[MinGW](https://sourceforge.net/projects/mingw-w64/files/)をインストールする必要があります([設定チュートリアル](https://blog.csdn.net/qq_33698226/article/details/129031241))。MacOSにはClangが付属しているため、インストールは不要です。
|
||||
2. VS Code拡張機能マーケットプレイスで`c++`を検索し、C/C++ Extension Packをインストールします。
|
||||
3. (オプション)設定ページを開き、`Clang_format_fallback Style`コードフォーマッティングオプションを検索し、`{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }`に設定します。
|
||||
|
||||
### Java環境
|
||||
|
||||
1. [OpenJDK](https://jdk.java.net/18/)をダウンロードしてインストールします(バージョンはJDK 9より新しい必要があります)。
|
||||
2. VS Code拡張機能マーケットプレイスで`java`を検索し、Extension Pack for Javaをインストールします。
|
||||
|
||||
### C#環境
|
||||
|
||||
1. [.Net 8.0](https://dotnet.microsoft.com/en-us/download)をダウンロードしてインストールします。
|
||||
2. VS Code拡張機能マーケットプレイスで`C# Dev Kit`を検索し、C# Dev Kitをインストールします([設定チュートリアル](https://code.visualstudio.com/docs/csharp/get-started))。
|
||||
3. Visual Studioを使用することもできます([インストールチュートリアル](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022))。
|
||||
|
||||
### Go環境
|
||||
|
||||
1. [go](https://go.dev/dl/)をダウンロードしてインストールします。
|
||||
2. VS Code拡張機能マーケットプレイスで`go`を検索し、Goをインストールします。
|
||||
3. `Ctrl + Shift + P`を押してコマンドバーを呼び出し、goと入力し、`Go: Install/Update Tools`を選択し、すべてを選択してインストールします。
|
||||
|
||||
### Swift環境
|
||||
|
||||
1. [Swift](https://www.swift.org/download/)をダウンロードしてインストールします。
|
||||
2. VS Code拡張機能マーケットプレイスで`swift`を検索し、[Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)をインストールします。
|
||||
|
||||
### JavaScript環境
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/)をダウンロードしてインストールします。
|
||||
2. (オプション)VS Code拡張機能マーケットプレイスで`Prettier`を検索し、コードフォーマッティングツールをインストールします。
|
||||
|
||||
### TypeScript環境
|
||||
|
||||
1. JavaScript環境と同じインストール手順に従います。
|
||||
2. [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation)をインストールします。
|
||||
3. VS Code拡張機能マーケットプレイスで`typescript`を検索し、[Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)をインストールします。
|
||||
|
||||
### Dart環境
|
||||
|
||||
1. [Dart](https://dart.dev/get-dart)をダウンロードしてインストールします。
|
||||
2. VS Code拡張機能マーケットプレイスで`dart`を検索し、[Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code)をインストールします。
|
||||
|
||||
### Rust環境
|
||||
|
||||
1. [Rust](https://www.rust-lang.org/tools/install)をダウンロードしてインストールします。
|
||||
2. VS Code拡張機能マーケットプレイスで`rust`を検索し、[rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)をインストールします。
|
||||
137
ja/docs/chapter_appendix/terminology.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# 用語集
|
||||
|
||||
下の表は本書に登場する重要な用語をリストアップしており、以下の点に注意する価値があります。
|
||||
|
||||
- 英語文献を読みやすくするため、用語の英語名を覚えることをお勧めします。
|
||||
- 一部の用語は簡体字中国語と繁体字中国語で異なる名前を持ちます。
|
||||
|
||||
<p align="center"> 表 <id> データ構造とアルゴリズムの重要用語 </p>
|
||||
|
||||
| English | 日本語 | 简体中文 | 繁体中文 |
|
||||
| ------------------------------ | ---------------------- | -------------- | -------------- |
|
||||
| algorithm | アルゴリズム | 算法 | 演算法 |
|
||||
| data structure | データ構造 | 数据结构 | 資料結構 |
|
||||
| code | コード | 代码 | 程式碼 |
|
||||
| file | ファイル | 文件 | 檔案 |
|
||||
| function | 関数 | 函数 | 函式 |
|
||||
| method | メソッド | 方法 | 方法 |
|
||||
| variable | 変数 | 变量 | 變數 |
|
||||
| asymptotic complexity analysis | 漸近計算量解析 | 渐近复杂度分析 | 漸近複雜度分析 |
|
||||
| time complexity | 時間計算量 | 时间复杂度 | 時間複雜度 |
|
||||
| space complexity | 空間計算量 | 空间复杂度 | 空間複雜度 |
|
||||
| loop | ループ | 循环 | 迴圈 |
|
||||
| iteration | 反復 | 迭代 | 迭代 |
|
||||
| recursion | 再帰 | 递归 | 遞迴 |
|
||||
| tail recursion | 末尾再帰 | 尾递归 | 尾遞迴 |
|
||||
| recursion tree | 再帰木 | 递归树 | 遞迴樹 |
|
||||
| big-$O$ notation | ビッグO記法 | 大 $O$ 记号 | 大 $O$ 記號 |
|
||||
| asymptotic upper bound | 漸近上界 | 渐近上界 | 漸近上界 |
|
||||
| sign-magnitude | 符号と絶対値 | 原码 | 原碼 |
|
||||
| 1's complement | 1の補数 | 反码 | 一補數 |
|
||||
| 2's complement | 2の補数 | 补码 | 二補數 |
|
||||
| array | 配列 | 数组 | 陣列 |
|
||||
| index | インデックス | 索引 | 索引 |
|
||||
| linked list | 連結リスト | 链表 | 鏈結串列 |
|
||||
| linked list node, list node | 連結リストノード | 链表节点 | 鏈結串列節點 |
|
||||
| head node | 先頭ノード | 头节点 | 頭節點 |
|
||||
| tail node | 末尾ノード | 尾节点 | 尾節點 |
|
||||
| list | リスト | 列表 | 串列 |
|
||||
| dynamic array | 動的配列 | 动态数组 | 動態陣列 |
|
||||
| hard disk | ハードディスク | 硬盘 | 硬碟 |
|
||||
| random-access memory (RAM) | メモリ | 内存 | 記憶體 |
|
||||
| cache memory | キャッシュメモリ | 缓存 | 快取 |
|
||||
| cache miss | キャッシュミス | 缓存未命中 | 快取未命中 |
|
||||
| cache hit rate | キャッシュヒット率 | 缓存命中率 | 快取命中率 |
|
||||
| stack | スタック | 栈 | 堆疊 |
|
||||
| top of the stack | スタックトップ | 栈顶 | 堆疊頂 |
|
||||
| bottom of the stack | スタックボトム | 栈底 | 堆疊底 |
|
||||
| queue | キュー | 队列 | 佇列 |
|
||||
| double-ended queue | 両端キュー | 双向队列 | 雙向佇列 |
|
||||
| front of the queue | キューの先頭 | 队首 | 佇列首 |
|
||||
| rear of the queue | キューの末尾 | 队尾 | 佇列尾 |
|
||||
| hash table | ハッシュテーブル | 哈希表 | 雜湊表 |
|
||||
| hash set | ハッシュセット | 哈希集合 | 雜湊集合 |
|
||||
| bucket | バケット | 桶 | 桶 |
|
||||
| hash function | ハッシュ関数 | 哈希函数 | 雜湊函式 |
|
||||
| hash collision | ハッシュ衝突 | 哈希冲突 | 雜湊衝突 |
|
||||
| load factor | 負荷率 | 负载因子 | 負載因子 |
|
||||
| separate chaining | チェイン法 | 链式地址 | 鏈結位址 |
|
||||
| open addressing | オープンアドレス法 | 开放寻址 | 開放定址 |
|
||||
| linear probing | 線形プローブ法 | 线性探测 | 線性探查 |
|
||||
| lazy deletion | 遅延削除 | 懒删除 | 懶刪除 |
|
||||
| binary tree | 二分木 | 二叉树 | 二元樹 |
|
||||
| tree node | 木のノード | 树节点 | 樹節點 |
|
||||
| left-child node | 左の子ノード | 左子节点 | 左子節點 |
|
||||
| right-child node | 右の子ノード | 右子节点 | 右子節點 |
|
||||
| parent node | 親ノード | 父节点 | 父節點 |
|
||||
| left subtree | 左の部分木 | 左子树 | 左子樹 |
|
||||
| right subtree | 右の部分木 | 右子树 | 右子樹 |
|
||||
| root node | ルートノード | 根节点 | 根節點 |
|
||||
| leaf node | 葉ノード | 叶节点 | 葉節點 |
|
||||
| edge | エッジ | 边 | 邊 |
|
||||
| level | レベル | 层 | 層 |
|
||||
| degree | 次数 | 度 | 度 |
|
||||
| height | 高さ | 高度 | 高度 |
|
||||
| depth | 深さ | 深度 | 深度 |
|
||||
| perfect binary tree | 完全二分木 | 完美二叉树 | 完美二元樹 |
|
||||
| complete binary tree | 完全二分木 | 完全二叉树 | 完全二元樹 |
|
||||
| full binary tree | 満二分木 | 完满二叉树 | 完滿二元樹 |
|
||||
| balanced binary tree | 平衡二分木 | 平衡二叉树 | 平衡二元樹 |
|
||||
| binary search tree | 二分探索木 | 二叉搜索树 | 二元搜尋樹 |
|
||||
| AVL tree | AVL木 | AVL 树 | AVL 樹 |
|
||||
| red-black tree | 赤黒木 | 红黑树 | 紅黑樹 |
|
||||
| level-order traversal | レベル順走査 | 层序遍历 | 層序走訪 |
|
||||
| breadth-first traversal | 幅優先走査 | 广度优先遍历 | 廣度優先走訪 |
|
||||
| depth-first traversal | 深さ優先走査 | 深度优先遍历 | 深度優先走訪 |
|
||||
| binary search tree | 二分探索木 | 二叉搜索树 | 二元搜尋樹 |
|
||||
| balanced binary search tree | 平衡二分探索木 | 平衡二叉搜索树 | 平衡二元搜尋樹 |
|
||||
| balance factor | 平衡因子 | 平衡因子 | 平衡因子 |
|
||||
| heap | ヒープ | 堆 | 堆積 |
|
||||
| max heap | 最大ヒープ | 大顶堆 | 大頂堆積 |
|
||||
| min heap | 最小ヒープ | 小顶堆 | 小頂堆積 |
|
||||
| priority queue | 優先度キュー | 优先队列 | 優先佇列 |
|
||||
| heapify | ヒープ化 | 堆化 | 堆積化 |
|
||||
| top-$k$ problem | Top-$k$ 問題 | Top-$k$ 问题 | Top-$k$ 問題 |
|
||||
| graph | グラフ | 图 | 圖 |
|
||||
| vertex | 頂点 | 顶点 | 頂點 |
|
||||
| undirected graph | 無向グラフ | 无向图 | 無向圖 |
|
||||
| directed graph | 有向グラフ | 有向图 | 有向圖 |
|
||||
| connected graph | 連結グラフ | 连通图 | 連通圖 |
|
||||
| disconnected graph | 非連結グラフ | 非连通图 | 非連通圖 |
|
||||
| weighted graph | 重み付きグラフ | 有权图 | 有權圖 |
|
||||
| adjacency | 隣接 | 邻接 | 鄰接 |
|
||||
| path | パス | 路径 | 路徑 |
|
||||
| in-degree | 入次数 | 入度 | 入度 |
|
||||
| out-degree | 出次数 | 出度 | 出度 |
|
||||
| adjacency matrix | 隣接行列 | 邻接矩阵 | 鄰接矩陣 |
|
||||
| adjacency list | 隣接リスト | 邻接表 | 鄰接表 |
|
||||
| breadth-first search | 幅優先探索 | 广度优先搜索 | 廣度優先搜尋 |
|
||||
| depth-first search | 深さ優先探索 | 深度优先搜索 | 深度優先搜尋 |
|
||||
| binary search | 二分探索 | 二分查找 | 二分搜尋 |
|
||||
| searching algorithm | 探索アルゴリズム | 搜索算法 | 搜尋演算法 |
|
||||
| sorting algorithm | ソートアルゴリズム | 排序算法 | 排序演算法 |
|
||||
| selection sort | 選択ソート | 选择排序 | 選擇排序 |
|
||||
| bubble sort | バブルソート | 冒泡排序 | 泡沫排序 |
|
||||
| insertion sort | 挿入ソート | 插入排序 | 插入排序 |
|
||||
| quick sort | クイックソート | 快速排序 | 快速排序 |
|
||||
| merge sort | マージソート | 归并排序 | 合併排序 |
|
||||
| heap sort | ヒープソート | 堆排序 | 堆積排序 |
|
||||
| bucket sort | バケットソート | 桶排序 | 桶排序 |
|
||||
| counting sort | 計数ソート | 计数排序 | 計數排序 |
|
||||
| radix sort | 基数ソート | 基数排序 | 基數排序 |
|
||||
| divide and conquer | 分割統治法 | 分治 | 分治 |
|
||||
| hanota problem | ハノイの塔問題 | 汉诺塔问题 | 河內塔問題 |
|
||||
| backtracking algorithm | バックトラッキング | 回溯算法 | 回溯演算法 |
|
||||
| constraint | 制約 | 约束 | 約束 |
|
||||
| solution | 解 | 解 | 解 |
|
||||
| state | 状態 | 状态 | 狀態 |
|
||||
| pruning | 枝刈り | 剪枝 | 剪枝 |
|
||||
| permutations problem | 順列問題 | 全排列问题 | 全排列問題 |
|
||||
| subset-sum problem | 部分集合和問題 | 子集和问题 | 子集合問題 |
|
||||
| $n$-queens problem | $n$ クイーン問題 | $n$ 皇后问题 | $n$ 皇后問題 |
|
||||
| dynamic programming | 動的プログラミング | 动态规划 | 動態規劃 |
|
||||
| initial state | 初期状態 | 初始状态 | 初始狀態 |
|
||||
| state-transition equation | 状態遷移方程式 | 状态转移方程 | 狀態轉移方程 |
|
||||
| knapsack problem | ナップサック問題 | 背包问题 | 背包問題 |
|
||||
| edit distance problem | 編集距離問題 | 编辑距离问题 | 編輯距離問題 |
|
||||
| greedy algorithm | 貪欲アルゴリズム | 贪心算法 | 貪婪演算法 |
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 29 KiB |
221
ja/docs/chapter_array_and_linkedlist/array.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 配列
|
||||
|
||||
<u>配列</u>は線形データ構造で、同じような項目が並んでいるようなもので、コンピュータのメモリ内の連続した空間に一緒に格納されます。これは整理された格納を維持するシーケンスのようなものです。この並びの各項目には、<u>インデックス</u>として知られる独自の「位置」があります。以下の図を参照して、配列の動作を観察し、これらの重要な用語を理解してください。
|
||||
|
||||

|
||||
|
||||
## 配列の一般的な操作
|
||||
|
||||
### 配列の初期化
|
||||
|
||||
配列は必要に応じて2つの方法で初期化できます:初期値なしまたは指定された初期値付きです。初期値が指定されていない場合、ほとんどのプログラミング言語は配列要素を$0$に設定します:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
# 配列を初期化
|
||||
arr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ]
|
||||
nums: list[int] = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 配列を初期化 */
|
||||
// スタックに格納
|
||||
int arr[5];
|
||||
int nums[5] = { 1, 3, 2, 5, 4 };
|
||||
// ヒープに格納(手動でのメモリ解放が必要)
|
||||
int* arr1 = new int[5];
|
||||
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 配列を初期化 */
|
||||
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
|
||||
int[] nums = { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 配列を初期化 */
|
||||
int[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]
|
||||
int[] nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* 配列を初期化 */
|
||||
var arr [5]int
|
||||
// Goでは、長さを指定([5]int)すると配列を示し、指定しない([]int)とスライスを示します。
|
||||
// Goの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。
|
||||
// extend()メソッドの実装の便宜上、ここではSliceを配列として扱います。
|
||||
nums := []int{1, 3, 2, 5, 4}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* 配列を初期化 */
|
||||
let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
|
||||
let nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 配列を初期化 */
|
||||
var arr = new Array(5).fill(0);
|
||||
var nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 配列を初期化 */
|
||||
let arr: number[] = new Array(5).fill(0);
|
||||
let nums: number[] = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="array.dart"
|
||||
/* 配列を初期化 */
|
||||
List<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]
|
||||
List<int> nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="array.rs"
|
||||
/* 配列を初期化 */
|
||||
let arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
|
||||
let slice: &[i32] = &[0; 5];
|
||||
// Rustでは、長さを指定([i32; 5])すると配列を示し、指定しない(&[i32])とスライスを示します。
|
||||
// Rustの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。
|
||||
// 一般的にRustでは動的配列としてVectorが使用されます。
|
||||
// extend()メソッドの実装の便宜上、ここではベクターを配列として扱います。
|
||||
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
/* 配列を初期化 */
|
||||
int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }
|
||||
int nums[5] = { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="array.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 配列を初期化
|
||||
var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 }
|
||||
var nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
### 要素へのアクセス
|
||||
|
||||
配列内の要素は連続したメモリ空間に格納されるため、各要素のメモリアドレスを計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。
|
||||
|
||||

|
||||
|
||||
上の図で観察されるように、配列のインデックスは慣例的に$0$から始まります。これは直感に反するように見えるかもしれません。数を数えるのは通常$1$から始まるためですが、アドレス計算公式内では、**インデックスは本質的にメモリアドレスからのオフセット**です。最初の要素のアドレスでは、このオフセットは$0$で、そのインデックスが$0$であることを検証しています。
|
||||
|
||||
配列内の要素へのアクセスは非常に効率的で、$O(1)$時間で任意の要素にランダムアクセスできます。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{random_access}
|
||||
```
|
||||
|
||||
### 要素の挿入
|
||||
|
||||
配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中央に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。
|
||||
|
||||

|
||||
|
||||
配列の長さが固定されているため、要素を挿入すると必然的に配列の最後の要素が失われることに注意することが重要です。この問題を解決する方法は「リスト」の章で探求されます。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### 要素の削除
|
||||
|
||||
同様に、以下の図に示すように、インデックス$i$の要素を削除するには、インデックス$i$に続くすべての要素を1つずつ前に移動する必要があります。
|
||||
|
||||

|
||||
|
||||
削除後、元の最後の要素は「意味がない」ものになるため、特定の修正は必要ないことに注意してください。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
要約すると、配列の挿入と削除操作には以下の欠点があります:
|
||||
|
||||
- **高い時間計算量**:配列の挿入と削除の両方の平均時間計算量は$O(n)$で、ここで$n$は配列の長さです。
|
||||
- **要素の損失**:配列の長さが固定されているため、挿入時に配列の容量を超える要素は失われます。
|
||||
- **メモリの無駄**:より長い配列を初期化して前部分のみを利用すると、挿入時に「意味のない」末尾要素が生じ、メモリ空間の無駄につながります。
|
||||
|
||||
### 配列の走査
|
||||
|
||||
ほとんどのプログラミング言語では、インデックスを使用するか、各要素を直接反復することで配列を走査できます:
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{traverse}
|
||||
```
|
||||
|
||||
### 要素の検索
|
||||
|
||||
配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。
|
||||
|
||||
配列は線形データ構造であるため、この操作は一般的に「線形探索」と呼ばれます。
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
### 配列の拡張
|
||||
|
||||
複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。
|
||||
|
||||
配列を拡張するには、より大きな配列を作成し、元の配列から要素をコピーする必要があります。この操作の時間計算量は$O(n)$で、大きな配列では時間がかかる可能性があります。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{extend}
|
||||
```
|
||||
|
||||
## 配列の利点と制限
|
||||
|
||||
配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作の効率を最適化するために活用できる実質的な事前情報を提供します。
|
||||
|
||||
- **高い空間効率**:配列はデータのための連続したメモリブロックを割り当て、追加の構造的オーバーヘッドの必要性を排除します。
|
||||
- **ランダムアクセスのサポート**:配列は任意の要素への$O(1)$時間アクセスを可能にします。
|
||||
- **キャッシュ局所性**:配列要素にアクセスするとき、コンピュータはそれらを読み込むだけでなく、周囲のデータもキャッシュし、高速キャッシュを利用して後続の操作速度を向上させます。
|
||||
|
||||
しかし、連続空間格納は諸刃の剣で、以下の制限があります:
|
||||
|
||||
- **挿入と削除の効率が低い**:配列に多くの要素が蓄積されると、要素の挿入や削除には大量の要素をシフトする必要があります。
|
||||
- **固定長**:配列の長さは初期化後に固定されます。配列を拡張するには、すべてのデータを新しい配列にコピーする必要があり、大きなコストがかかります。
|
||||
- **空間の無駄**:割り当てられた配列サイズが必要以上に大きい場合、余分な空間が無駄になります。
|
||||
|
||||
## 配列の典型的な応用
|
||||
|
||||
配列は基本的で広く使用されるデータ構造です。様々なアルゴリズムで頻繁に応用され、複雑なデータ構造の実装に役立ちます。
|
||||
|
||||
- **ランダムアクセス**:配列はランダムサンプリングが必要なときのデータ格納に理想的です。インデックスに基づいてランダムシーケンスを生成することで、効率的にランダムサンプリングを実現できます。
|
||||
- **ソートと検索**:配列はソートと検索アルゴリズムで最も一般的に使用されるデータ構造です。クイックソート、マージソート、二分探索などの技術は主に配列で動作します。
|
||||
- **ルックアップテーブル**:配列は迅速な要素や関係の取得のための効率的なルックアップテーブルとして機能します。例えば、文字をASCIIコードにマッピングすることは、ASCIIコード値をインデックスとして使用し、対応する要素を配列に格納することで簡単になります。
|
||||
- **機械学習**:ニューラルネットワークの領域では、配列はベクトル、行列、テンソルを含む重要な線形代数演算の実行において重要な役割を果たします。配列はニューラルネットワークプログラミングにおいて主要かつ最も広範囲に使用されるデータ構造として機能します。
|
||||
- **データ構造の実装**:配列は、スタック、キュー、ハッシュ表、ヒープ、グラフなど、様々なデータ構造を実装するための構成要素として機能します。例えば、グラフの隣接行列表現は本質的に二次元配列です。
|
||||
9
ja/docs/chapter_array_and_linkedlist/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 配列と連結リスト
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
データ構造の世界は頑丈なレンガの壁に似ています。
|
||||
|
||||
配列では、レンガがぴったりと整列し、それぞれが次のものと継ぎ目なく隣り合って、統一された形成を作っている姿を想像してください。一方、連結リストでは、これらのレンガが自由に散らばり、それらの間を優雅に編み込む蔦に抱かれています。
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
686
ja/docs/chapter_array_and_linkedlist/linked_list.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# 連結リスト
|
||||
|
||||
メモリ空間は、すべてのプログラム間で共有されるリソースです。複雑なシステム環境では、使用可能なメモリがメモリ空間全体に分散している可能性があります。配列に割り当てられるメモリは連続している必要があることを理解していますが、非常に大きな配列の場合、十分な大きさの連続メモリ空間を見つけるのは困難な場合があります。ここで、連結リストの柔軟な利点が明らかになります。
|
||||
|
||||
<u>連結リスト</u>は線形データ構造であり、各要素はノードオブジェクトで、ノードは「参照」を通じて相互接続されています。これらの参照は後続ノードのメモリアドレスを保持し、1つのノードから次のノードへのナビゲーションを可能にします。
|
||||
|
||||
連結リストの設計では、ノードを連続するメモリアドレスを必要とせずに、メモリ位置全体に分散配置することができます。
|
||||
|
||||

|
||||
|
||||
上図に示すように、連結リストの基本的な構成要素は<u>ノード</u>オブジェクトです。各ノードは2つの主要なコンポーネントで構成されています:ノードの「値」と次のノードへの「参照」です。
|
||||
|
||||
- 連結リストの最初のノードは「ヘッドノード」、最後のノードは「テールノード」です。
|
||||
- テールノードは「null」を指し、Javaでは`null`、C++では`nullptr`、Pythonでは`None`として指定されます。
|
||||
- C、C++、Go、Rustなどのポインタをサポートする言語では、この「参照」は通常「ポインタ」として実装されます。
|
||||
|
||||
以下のコードが示すように、連結リストの`ListNode`は値を保持するだけでなく、追加の参照(またはポインタ)も維持する必要があります。したがって、**連結リストは同じ量のデータを格納する場合、配列よりも多くのメモリ空間を占有します**。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""連結リストノードクラス"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # ノード値
|
||||
self.next: ListNode | None = None # 次のノードへの参照
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 連結リストノード構造体 */
|
||||
struct ListNode {
|
||||
int val; // ノード値
|
||||
ListNode *next; // 次のノードへのポインタ
|
||||
ListNode(int x) : val(x), next(nullptr) {} // コンストラクタ
|
||||
};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 連結リストノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノード値
|
||||
ListNode next; // 次のノードへの参照
|
||||
ListNode(int x) { val = x; } // コンストラクタ
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 連結リストノードクラス */
|
||||
class ListNode(int x) { // コンストラクタ
|
||||
int val = x; // ノード値
|
||||
ListNode? next; // 次のノードへの参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 連結リストノード構造体 */
|
||||
type ListNode struct {
|
||||
Val int // ノード値
|
||||
Next *ListNode // 次のノードへのポインタ
|
||||
}
|
||||
|
||||
// NewListNode コンストラクタ、新しい連結リストを作成
|
||||
func NewListNode(val int) *ListNode {
|
||||
return &ListNode{
|
||||
Val: val,
|
||||
Next: nil,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 連結リストノードクラス */
|
||||
class ListNode {
|
||||
var val: Int // ノード値
|
||||
var next: ListNode? // 次のノードへの参照
|
||||
|
||||
init(x: Int) { // コンストラクタ
|
||||
val = x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* 連結リストノードクラス */
|
||||
class ListNode {
|
||||
constructor(val, next) {
|
||||
this.val = (val === undefined ? 0 : val); // ノード値
|
||||
this.next = (next === undefined ? null : next); // 次のノードへの参照
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* 連結リストノードクラス */
|
||||
class ListNode {
|
||||
val: number;
|
||||
next: ListNode | null;
|
||||
constructor(val?: number, next?: ListNode | null) {
|
||||
this.val = val === undefined ? 0 : val; // ノード値
|
||||
this.next = next === undefined ? null : next; // 次のノードへの参照
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 連結リストノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノード値
|
||||
ListNode? next; // 次のノードへの参照
|
||||
ListNode(this.val, [this.next]); // コンストラクタ
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
/* 連結リストノードクラス */
|
||||
#[derive(Debug)]
|
||||
struct ListNode {
|
||||
val: i32, // ノード値
|
||||
next: Option<Rc<RefCell<ListNode>>>, // 次のノードへのポインタ
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* 連結リストノード構造体 */
|
||||
typedef struct ListNode {
|
||||
int val; // ノード値
|
||||
struct ListNode *next; // 次のノードへのポインタ
|
||||
} ListNode;
|
||||
|
||||
/* コンストラクタ */
|
||||
ListNode *newListNode(int val) {
|
||||
ListNode *node;
|
||||
node = (ListNode *) malloc(sizeof(ListNode));
|
||||
node->val = val;
|
||||
node->next = NULL;
|
||||
return node;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
|
||||
```
|
||||
|
||||
=== "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;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 連結リストの一般的な操作
|
||||
|
||||
### 連結リストの初期化
|
||||
|
||||
連結リストの構築は2段階のプロセスです:まず各ノードオブジェクトを初期化し、次にノード間の参照リンクを形成します。初期化後、ヘッドノードから`next`参照をたどってすべてのノードを順次巡回できます。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linked_list.py"
|
||||
# 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4
|
||||
# 各ノードを初期化
|
||||
n0 = ListNode(1)
|
||||
n1 = ListNode(3)
|
||||
n2 = ListNode(2)
|
||||
n3 = ListNode(5)
|
||||
n4 = ListNode(4)
|
||||
# ノード間の参照を構築
|
||||
n0.next = n1
|
||||
n1.next = n2
|
||||
n2.next = n3
|
||||
n3.next = n4
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linked_list.cpp"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
ListNode* n0 = new ListNode(1);
|
||||
ListNode* n1 = new ListNode(3);
|
||||
ListNode* n2 = new ListNode(2);
|
||||
ListNode* n3 = new ListNode(5);
|
||||
ListNode* n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
n0->next = n1;
|
||||
n1->next = n2;
|
||||
n2->next = n3;
|
||||
n3->next = n4;
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
ListNode n0 = new ListNode(1);
|
||||
ListNode n1 = new ListNode(3);
|
||||
ListNode n2 = new ListNode(2);
|
||||
ListNode n3 = new ListNode(5);
|
||||
ListNode n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="linked_list.cs"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
ListNode n0 = new(1);
|
||||
ListNode n1 = new(3);
|
||||
ListNode n2 = new(2);
|
||||
ListNode n3 = new(5);
|
||||
ListNode n4 = new(4);
|
||||
// ノード間の参照を構築
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="linked_list.go"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
n0 := NewListNode(1)
|
||||
n1 := NewListNode(3)
|
||||
n2 := NewListNode(2)
|
||||
n3 := NewListNode(5)
|
||||
n4 := NewListNode(4)
|
||||
// ノード間の参照を構築
|
||||
n0.Next = n1
|
||||
n1.Next = n2
|
||||
n2.Next = n3
|
||||
n3.Next = n4
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linked_list.swift"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
let n0 = ListNode(x: 1)
|
||||
let n1 = ListNode(x: 3)
|
||||
let n2 = ListNode(x: 2)
|
||||
let n3 = ListNode(x: 5)
|
||||
let n4 = ListNode(x: 4)
|
||||
// ノード間の参照を構築
|
||||
n0.next = n1
|
||||
n1.next = n2
|
||||
n2.next = n3
|
||||
n3.next = n4
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="linked_list.js"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
const n0 = new ListNode(1);
|
||||
const n1 = new ListNode(3);
|
||||
const n2 = new ListNode(2);
|
||||
const n3 = new ListNode(5);
|
||||
const n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="linked_list.ts"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
const n0 = new ListNode(1);
|
||||
const n1 = new ListNode(3);
|
||||
const n2 = new ListNode(2);
|
||||
const n3 = new ListNode(5);
|
||||
const n4 = new ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="linked_list.dart"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
ListNode n0 = ListNode(1);
|
||||
ListNode n1 = ListNode(3);
|
||||
ListNode n2 = ListNode(2);
|
||||
ListNode n3 = ListNode(5);
|
||||
ListNode n4 = ListNode(4);
|
||||
// ノード間の参照を構築
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="linked_list.rs"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));
|
||||
let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));
|
||||
let n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));
|
||||
let n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));
|
||||
let n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));
|
||||
|
||||
// ノード間の参照を構築
|
||||
n0.borrow_mut().next = Some(n1.clone());
|
||||
n1.borrow_mut().next = Some(n2.clone());
|
||||
n2.borrow_mut().next = Some(n3.clone());
|
||||
n3.borrow_mut().next = Some(n4.clone());
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// 各ノードを初期化
|
||||
ListNode* n0 = newListNode(1);
|
||||
ListNode* n1 = newListNode(3);
|
||||
ListNode* n2 = newListNode(2);
|
||||
ListNode* n3 = newListNode(5);
|
||||
ListNode* n4 = newListNode(4);
|
||||
// ノード間の参照を構築
|
||||
n0->next = n1;
|
||||
n1->next = n2;
|
||||
n2->next = n3;
|
||||
n3->next = n4;
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="linked_list.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "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つのノード`n0`と`n1`の間に新しいノード`P`を挿入することを目指すとします。**これは2つのノード参照(ポインタ)を変更するだけで実現でき**、時間計算量は$O(1)$です。
|
||||
|
||||
比較すると、配列に要素を挿入する時間計算量は$O(n)$であり、大量のデータを扱う場合には効率が悪くなります。
|
||||
|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### ノードの削除
|
||||
|
||||
下図に示すように、連結リストからノードを削除することも非常に簡単で、**1つのノードの参照(ポインタ)を変更するだけです**。
|
||||
|
||||
重要な点は、ノード`P`が削除された後も`n1`を指し続けていることですが、連結リストの巡回中にはアクセスできなくなることです。これは事実上、`P`が連結リストの一部ではなくなったことを意味します。
|
||||
|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
### ノードへのアクセス
|
||||
|
||||
**連結リストでのノードへのアクセスは効率が悪いです**。前述したように、配列の任意の要素には$O(1)$時間でアクセスできます。対照的に、連結リストでは、プログラムはヘッドノードから開始して目的のノードが見つかるまで順次ノードを巡回する必要があります。つまり、連結リストの$i$番目のノードにアクセスするには、プログラムは$i - 1$個のノードを反復処理する必要があり、時間計算量は$O(n)$になります。
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{access}
|
||||
```
|
||||
|
||||
### ノードの検索
|
||||
|
||||
連結リストを巡回して、値が`target`に一致するノードを見つけ、連結リスト内でのそのノードのインデックスを出力します。この手順も線形検索の例です。対応するコードは以下のとおりです:
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## 配列 vs. 連結リスト
|
||||
|
||||
下表は配列と連結リストの特性をまとめ、様々な操作における効率も比較しています。それぞれが対照的な格納戦略を使用するため、それぞれの特性と操作効率は明確に対比されています。
|
||||
|
||||
<p align="center"> 表 <id> 配列と連結リストの効率比較 </p>
|
||||
|
||||
| | 配列 | 連結リスト |
|
||||
| ------------------ | ------------------------------------------------ | ----------------------- |
|
||||
| 格納方式 | 連続メモリ空間 | 分散メモリ空間 |
|
||||
| 容量拡張 | 固定長 | 柔軟な拡張 |
|
||||
| メモリ効率 | 要素あたりのメモリ少、潜在的な空間の無駄 | 要素あたりのメモリ多 |
|
||||
| 要素へのアクセス | $O(1)$ | $O(n)$ |
|
||||
| 要素の追加 | $O(n)$ | $O(1)$ |
|
||||
| 要素の削除 | $O(n)$ | $O(1)$ |
|
||||
|
||||
## 連結リストの一般的な種類
|
||||
|
||||
下図に示すように、連結リストには3つの一般的な種類があります。
|
||||
|
||||
- **単方向連結リスト**:これは前述した標準的な連結リストです。単方向連結リストのノードには値と次のノードへの参照が含まれます。最初のノードはヘッドノードと呼ばれ、null(`None`)を指す最後のノードはテールノードです。
|
||||
- **循環連結リスト**:これは単方向連結リストのテールノードがヘッドノードを指してループを作ることで形成されます。循環連結リストでは、任意のノードがヘッドノードとして機能できます。
|
||||
- **双方向連結リスト**:単方向連結リストとは対照的に、双方向連結リストは2つの方向で参照を維持します。各ノードには後続者(次のノード)と前任者(前のノード)の両方への参照(ポインタ)が含まれます。双方向連結リストはどちらの方向にも巡回できるより多くの柔軟性を提供しますが、より多くのメモリ空間も消費します。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""双方向連結リストノードクラス"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # ノード値
|
||||
self.next: ListNode | None = None # 後続ノードへの参照
|
||||
self.prev: ListNode | None = None # 前任ノードへの参照
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 双方向連結リストノード構造体 */
|
||||
struct ListNode {
|
||||
int val; // ノード値
|
||||
ListNode *next; // 後続ノードへのポインタ
|
||||
ListNode *prev; // 前任ノードへのポインタ
|
||||
ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // コンストラクタ
|
||||
};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノード値
|
||||
ListNode next; // 次のノードへの参照
|
||||
ListNode prev; // 前任ノードへの参照
|
||||
ListNode(int x) { val = x; } // コンストラクタ
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
class ListNode(int x) { // コンストラクタ
|
||||
int val = x; // ノード値
|
||||
ListNode next; // 次のノードへの参照
|
||||
ListNode prev; // 前任ノードへの参照
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 双方向連結リストノード構造体 */
|
||||
type DoublyListNode struct {
|
||||
Val int // ノード値
|
||||
Next *DoublyListNode // 後続ノードへのポインタ
|
||||
Prev *DoublyListNode // 前任ノードへのポインタ
|
||||
}
|
||||
|
||||
// NewDoublyListNode 初期化
|
||||
func NewDoublyListNode(val int) *DoublyListNode {
|
||||
return &DoublyListNode{
|
||||
Val: val,
|
||||
Next: nil,
|
||||
Prev: nil,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
class ListNode {
|
||||
var val: Int // ノード値
|
||||
var next: ListNode? // 次のノードへの参照
|
||||
var prev: ListNode? // 前任ノードへの参照
|
||||
|
||||
init(x: Int) { // コンストラクタ
|
||||
val = x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
class ListNode {
|
||||
constructor(val, next, prev) {
|
||||
this.val = val === undefined ? 0 : val; // ノード値
|
||||
this.next = next === undefined ? null : next; // 後続ノードへの参照
|
||||
this.prev = prev === undefined ? null : prev; // 前任ノードへの参照
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
class ListNode {
|
||||
val: number;
|
||||
next: ListNode | null;
|
||||
prev: ListNode | null;
|
||||
constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
|
||||
this.val = val === undefined ? 0 : val; // ノード値
|
||||
this.next = next === undefined ? null : next; // 後続ノードへの参照
|
||||
this.prev = prev === undefined ? null : prev; // 前任ノードへの参照
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 双方向連結リストノードクラス */
|
||||
class ListNode {
|
||||
int val; // ノード値
|
||||
ListNode next; // 次のノードへの参照
|
||||
ListNode prev; // 前任ノードへの参照
|
||||
ListNode(this.val, [this.next, this.prev]); // コンストラクタ
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/* 双方向連結リストノード型 */
|
||||
#[derive(Debug)]
|
||||
struct ListNode {
|
||||
val: i32, // ノード値
|
||||
next: Option<Rc<RefCell<ListNode>>>, // 後続ノードへのポインタ
|
||||
prev: Option<Rc<RefCell<ListNode>>>, // 前任ノードへのポインタ
|
||||
}
|
||||
|
||||
/* コンストラクタ */
|
||||
impl ListNode {
|
||||
fn new(val: i32) -> Self {
|
||||
ListNode {
|
||||
val,
|
||||
next: None,
|
||||
prev: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* 双方向連結リストノード構造体 */
|
||||
typedef struct ListNode {
|
||||
int val; // ノード値
|
||||
struct ListNode *next; // 後続ノードへのポインタ
|
||||
struct ListNode *prev; // 前任ノードへのポインタ
|
||||
} ListNode;
|
||||
|
||||
/* コンストラクタ */
|
||||
ListNode *newListNode(int val) {
|
||||
ListNode *node, *next;
|
||||
node = (ListNode *) malloc(sizeof(ListNode));
|
||||
node->val = val;
|
||||
node->next = NULL;
|
||||
node->prev = NULL;
|
||||
return node;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
|
||||
```
|
||||
|
||||
=== "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;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 連結リストの典型的な応用
|
||||
|
||||
単方向連結リストは、スタック、キュー、ハッシュ表、グラフの実装によく使用されます。
|
||||
|
||||
- **スタックとキュー**:単方向連結リストで、挿入と削除が同じ端で行われる場合、スタック(後入先出)のように動作します。逆に、挿入が一方の端で、削除がもう一方の端で行われる場合、キュー(先入先出)のように機能します。
|
||||
- **ハッシュ表**:連結リストは、ハッシュ衝突を解決する人気の方法である連鎖法で使用されます。ここでは、すべての衝突した要素が連結リストにグループ化されます。
|
||||
- **グラフ**:グラフ表現の標準的な方法である隣接リストは、各グラフ頂点を連結リストに関連付けます。このリストには、対応する頂点に接続された頂点を表す要素が含まれます。
|
||||
|
||||
双方向連結リストは、前後の要素への高速アクセスが必要なシナリオに最適です。
|
||||
|
||||
- **高度なデータ構造**:赤黒木やB木などの構造では、ノードの親へのアクセスが重要です。これは各ノードに親ノードへの参照を組み込むことで実現され、双方向連結リストに似ています。
|
||||
- **ブラウザ履歴**:Webブラウザでは、双方向連結リストにより、ユーザーが前進または後退ボタンをクリックしたときの訪問ページの履歴ナビゲーションが容易になります。
|
||||
- **LRUアルゴリズム**:双方向連結リストは、最近最少使用(LRU)キャッシュ削除アルゴリズムに適しており、最近最少使用データの迅速な識別と、高速なノード追加・削除を可能にします。
|
||||
|
||||
循環連結リストは、オペレーティングシステムでのリソーススケジューリングなど、周期的な操作が必要なアプリケーションに最適です。
|
||||
|
||||
- **ラウンドロビンスケジューリングアルゴリズム**:オペレーティングシステムでは、ラウンドロビンスケジューリングアルゴリズムは一般的なCPUスケジューリング方法であり、プロセスのグループを循環する必要があります。各プロセスにはタイムスライスが割り当てられ、期限切れになるとCPUは次のプロセスに回転します。この循環操作は循環連結リストを使用して効率的に実現でき、すべてのプロセス間で公平かつ時分割システムを可能にします。
|
||||
- **データバッファ**:循環連結リストは、オーディオやビデオプレーヤーなどのデータバッファでも使用され、データストリームが複数のバッファブロックに分割され、シームレスな再生のために循環方式で配置されます。
|
||||
906
ja/docs/chapter_array_and_linkedlist/list.md
Normal file
@@ -0,0 +1,906 @@
|
||||
# リスト
|
||||
|
||||
<u>リスト</u>は、要素へのアクセス、変更、追加、削除、走査などの操作をサポートする、順序付けられた要素のコレクションを表す抽象的なデータ構造の概念であり、ユーザーが容量制限を考慮する必要がありません。リストは連結リストまたは配列に基づいて実装できます。
|
||||
|
||||
- 連結リストは本質的にリストとして機能し、要素の追加、削除、検索、変更の操作をサポートし、サイズを動的に調整する柔軟性があります。
|
||||
- 配列もこれらの操作をサポートしますが、長さが不変であるため、長さ制限のあるリストと考えることができます。
|
||||
|
||||
配列を使用してリストを実装する場合、**長さの不変性によりリストの実用性が低下します**。これは、事前に格納するデータ量を予測することが困難な場合が多く、適切なリスト長を選択することが困難であるためです。長さが小さすぎると要件を満たさない可能性があり、大きすぎるとメモリ空間を無駄にする可能性があります。
|
||||
|
||||
この問題を解決するために、<u>動的配列</u>を使用してリストを実装できます。これは配列の利点を継承し、プログラム実行中に動的に拡張できます。
|
||||
|
||||
実際、**多くのプログラミング言語の標準ライブラリは動的配列を使用してリストを実装しています**。例えば、Pythonの`list`、Javaの`ArrayList`、C++の`vector`、C#の`List`などです。以下の議論では、「リスト」と「動的配列」を同義の概念として扱います。
|
||||
|
||||
## リストの一般的な操作
|
||||
|
||||
### リストの初期化
|
||||
|
||||
通常、「初期値なし」と「初期値あり」の2つの初期化方法を使用します。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# リストを初期化
|
||||
# 初期値なし
|
||||
nums1: list[int] = []
|
||||
# 初期値あり
|
||||
nums: list[int] = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* リストを初期化 */
|
||||
// 注意: C++では、vectorがここで説明されているnumsに相当します
|
||||
// 初期値なし
|
||||
vector<int> nums1;
|
||||
// 初期値あり
|
||||
vector<int> nums = { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
List<Integer> nums1 = new ArrayList<>();
|
||||
// 初期値あり(要素型はint[]のラッパークラスInteger[]である必要があります)
|
||||
Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 };
|
||||
List<Integer> nums = new ArrayList<>(Arrays.asList(numbers));
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
List<int> nums1 = [];
|
||||
// 初期値あり
|
||||
int[] numbers = [1, 3, 2, 5, 4];
|
||||
List<int> nums = [.. numbers];
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
nums1 := []int{}
|
||||
// 初期値あり
|
||||
nums := []int{1, 3, 2, 5, 4}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
let nums1: [Int] = []
|
||||
// 初期値あり
|
||||
var nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
const nums1 = [];
|
||||
// 初期値あり
|
||||
const nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
const nums1: number[] = [];
|
||||
// 初期値あり
|
||||
const nums: number[] = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
List<int> nums1 = [];
|
||||
// 初期値あり
|
||||
List<int> nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* リストを初期化 */
|
||||
// 初期値なし
|
||||
let nums1: Vec<i32> = Vec::new();
|
||||
// 初期値あり
|
||||
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// Cは組み込みの動的配列を提供していません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "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 });
|
||||
```
|
||||
|
||||
### 要素へのアクセス
|
||||
|
||||
リストは本質的に配列であるため、$O(1)$時間で要素にアクセスし更新することができ、非常に効率的です。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# 要素にアクセス
|
||||
num: int = nums[1] # インデックス1の要素にアクセス
|
||||
|
||||
# 要素を更新
|
||||
nums[1] = 0 # インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* 要素にアクセス */
|
||||
int num = nums[1]; // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* 要素にアクセス */
|
||||
int num = nums.get(1); // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums.set(1, 0); // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* 要素にアクセス */
|
||||
int num = nums[1]; // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* 要素にアクセス */
|
||||
num := nums[1] // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0 // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* 要素にアクセス */
|
||||
let num = nums[1] // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0 // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* 要素にアクセス */
|
||||
const num = nums[1]; // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* 要素にアクセス */
|
||||
const num: number = nums[1]; // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* 要素にアクセス */
|
||||
int num = nums[1]; // インデックス1の要素にアクセス
|
||||
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* 要素にアクセス */
|
||||
let num: i32 = nums[1]; // インデックス1の要素にアクセス
|
||||
/* 要素を更新 */
|
||||
nums[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// Cは組み込みの動的配列を提供していません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// 要素にアクセス
|
||||
var num = nums.items[1]; // インデックス1の要素にアクセス
|
||||
|
||||
// 要素を更新
|
||||
nums.items[1] = 0; // インデックス1の要素を0に更新
|
||||
```
|
||||
|
||||
### 要素の挿入と削除
|
||||
|
||||
配列と比較して、リストは要素の追加と削除においてより柔軟性を提供します。リストの末尾への要素追加は$O(1)$操作ですが、リストの他の場所での要素の挿入と削除の効率は配列と同じままで、時間計算量は$O(n)$です。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# リストをクリア
|
||||
nums.clear()
|
||||
|
||||
# 末尾に要素を追加
|
||||
nums.append(1)
|
||||
nums.append(3)
|
||||
nums.append(2)
|
||||
nums.append(5)
|
||||
nums.append(4)
|
||||
|
||||
# 中間に要素を挿入
|
||||
nums.insert(3, 6) # インデックス3に数値6を挿入
|
||||
|
||||
# 要素を削除
|
||||
nums.pop(3) # インデックス3の要素を削除
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* リストをクリア */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.push_back(1);
|
||||
nums.push_back(3);
|
||||
nums.push_back(2);
|
||||
nums.push_back(5);
|
||||
nums.push_back(4);
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.insert(nums.begin() + 3, 6); // インデックス3に数値6を挿入
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.erase(nums.begin() + 3); // インデックス3の要素を削除
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* リストをクリア */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.add(1);
|
||||
nums.add(3);
|
||||
nums.add(2);
|
||||
nums.add(5);
|
||||
nums.add(4);
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.add(3, 6); // インデックス3に数値6を挿入
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.remove(3); // インデックス3の要素を削除
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* リストをクリア */
|
||||
nums.Clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.Add(1);
|
||||
nums.Add(3);
|
||||
nums.Add(2);
|
||||
nums.Add(5);
|
||||
nums.Add(4);
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.Insert(3, 6);
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.RemoveAt(3);
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* リストをクリア */
|
||||
nums = nil
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums = append(nums, 1)
|
||||
nums = append(nums, 3)
|
||||
nums = append(nums, 2)
|
||||
nums = append(nums, 5)
|
||||
nums = append(nums, 4)
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス3に数値6を挿入
|
||||
|
||||
/* 要素を削除 */
|
||||
nums = append(nums[:3], nums[4:]...) // インデックス3の要素を削除
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* リストをクリア */
|
||||
nums.removeAll()
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.append(1)
|
||||
nums.append(3)
|
||||
nums.append(2)
|
||||
nums.append(5)
|
||||
nums.append(4)
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.insert(6, at: 3) // インデックス3に数値6を挿入
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.remove(at: 3) // インデックス3の要素を削除
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* リストをクリア */
|
||||
nums.length = 0;
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.push(1);
|
||||
nums.push(3);
|
||||
nums.push(2);
|
||||
nums.push(5);
|
||||
nums.push(4);
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.splice(3, 0, 6);
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.splice(3, 1);
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* リストをクリア */
|
||||
nums.length = 0;
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.push(1);
|
||||
nums.push(3);
|
||||
nums.push(2);
|
||||
nums.push(5);
|
||||
nums.push(4);
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.splice(3, 0, 6);
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.splice(3, 1);
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* リストをクリア */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.add(1);
|
||||
nums.add(3);
|
||||
nums.add(2);
|
||||
nums.add(5);
|
||||
nums.add(4);
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.insert(3, 6); // インデックス3に数値6を挿入
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.removeAt(3); // インデックス3の要素を削除
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* リストをクリア */
|
||||
nums.clear();
|
||||
|
||||
/* 末尾に要素を追加 */
|
||||
nums.push(1);
|
||||
nums.push(3);
|
||||
nums.push(2);
|
||||
nums.push(5);
|
||||
nums.push(4);
|
||||
|
||||
/* 中間に要素を挿入 */
|
||||
nums.insert(3, 6); // インデックス3に数値6を挿入
|
||||
|
||||
/* 要素を削除 */
|
||||
nums.remove(3); // インデックス3の要素を削除
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// Cは組み込みの動的配列を提供していません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "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の要素を削除
|
||||
```
|
||||
|
||||
### リストの反復
|
||||
|
||||
配列と同様に、リストはインデックスを使用して反復することも、各要素を直接反復することもできます。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# インデックスでリストを反復
|
||||
count = 0
|
||||
for i in range(len(nums)):
|
||||
count += nums[i]
|
||||
|
||||
# リスト要素を直接反復
|
||||
for num in nums:
|
||||
count += num
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* インデックスでリストを反復 */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
count = 0;
|
||||
for (int num : nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* インデックスでリストを反復 */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
count += nums.get(i);
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
for (int num : nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* インデックスでリストを反復 */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.Count; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
count = 0;
|
||||
foreach (int num in nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* インデックスでリストを反復 */
|
||||
count := 0
|
||||
for i := 0; i < len(nums); i++ {
|
||||
count += nums[i]
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
count = 0
|
||||
for _, num := range nums {
|
||||
count += num
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* インデックスでリストを反復 */
|
||||
var count = 0
|
||||
for i in nums.indices {
|
||||
count += nums[i]
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
count = 0
|
||||
for num in nums {
|
||||
count += num
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* インデックスでリストを反復 */
|
||||
let count = 0;
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
count = 0;
|
||||
for (const num of nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* インデックスでリストを反復 */
|
||||
let count = 0;
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
count = 0;
|
||||
for (const num of nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* インデックスでリストを反復 */
|
||||
int count = 0;
|
||||
for (var i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* リスト要素を直接反復 */
|
||||
count = 0;
|
||||
for (var num in nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
// インデックスでリストを反復
|
||||
let mut _count = 0;
|
||||
for i in 0..nums.len() {
|
||||
_count += nums[i];
|
||||
}
|
||||
|
||||
// リスト要素を直接反復
|
||||
_count = 0;
|
||||
for num in &nums {
|
||||
_count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// Cは組み込みの動的配列を提供していません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "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;
|
||||
}
|
||||
```
|
||||
|
||||
### リストの連結
|
||||
|
||||
新しいリスト`nums1`が与えられたとき、それを元のリストの末尾に追加できます。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# 2つのリストを連結
|
||||
nums1: list[int] = [6, 8, 7, 10, 9]
|
||||
nums += nums1 # nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* 2つのリストを連結 */
|
||||
vector<int> nums1 = { 6, 8, 7, 10, 9 };
|
||||
// nums1をnumsの末尾に連結
|
||||
nums.insert(nums.end(), nums1.begin(), nums1.end());
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* 2つのリストを連結 */
|
||||
List<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));
|
||||
nums.addAll(nums1); // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* 2つのリストを連結 */
|
||||
List<int> nums1 = [6, 8, 7, 10, 9];
|
||||
nums.AddRange(nums1); // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* 2つのリストを連結 */
|
||||
nums1 := []int{6, 8, 7, 10, 9}
|
||||
nums = append(nums, nums1...) // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* 2つのリストを連結 */
|
||||
let nums1 = [6, 8, 7, 10, 9]
|
||||
nums.append(contentsOf: nums1) // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* 2つのリストを連結 */
|
||||
const nums1 = [6, 8, 7, 10, 9];
|
||||
nums.push(...nums1); // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* 2つのリストを連結 */
|
||||
const nums1: number[] = [6, 8, 7, 10, 9];
|
||||
nums.push(...nums1); // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* 2つのリストを連結 */
|
||||
List<int> nums1 = [6, 8, 7, 10, 9];
|
||||
nums.addAll(nums1); // nums1をnumsの末尾に連結
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* 2つのリストを連結 */
|
||||
let nums1: Vec<i32> = vec![6, 8, 7, 10, 9];
|
||||
nums.extend(nums1);
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// Cは組み込みの動的配列を提供していません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "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の末尾に連結
|
||||
```
|
||||
|
||||
### リストのソート
|
||||
|
||||
リストがソートされると、「二分探索」や「双ポインタ」アルゴリズムなど、配列関連のアルゴリズム問題でよく使用されるアルゴリズムを使用できます。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# リストをソート
|
||||
nums.sort() # ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* リストをソート */
|
||||
sort(nums.begin(), nums.end()); // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* リストをソート */
|
||||
Collections.sort(nums); // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* リストをソート */
|
||||
nums.Sort(); // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* リストをソート */
|
||||
sort.Ints(nums) // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* リストをソート */
|
||||
nums.sort() // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* リストをソート */
|
||||
nums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* リストをソート */
|
||||
nums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* リストをソート */
|
||||
nums.sort(); // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* リストをソート */
|
||||
nums.sort(); // ソート後、リスト要素は昇順になります
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// Cは組み込みの動的配列を提供していません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="list.zig"
|
||||
// リストをソート
|
||||
std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32));
|
||||
```
|
||||
|
||||
## リストの実装
|
||||
|
||||
多くのプログラミング言語には、Java、C++、Pythonなどを含む組み込みリストが付属しています。それらの実装は、初期容量や拡張係数などの様々なパラメータを慎重に考慮した設定で、複雑になりがちです。興味のある読者は、さらなる学習のためにソースコードを調べることができます。
|
||||
|
||||
リストがどのように動作するかの理解を深めるために、3つの重要な設計側面に焦点を当てて、簡略化されたリストの実装を試みます:
|
||||
|
||||
- **初期容量**:配列に合理的な初期容量を選択します。この例では、初期容量として10を選択します。
|
||||
- **サイズ記録**:リスト内の現在の要素数を記録する変数`size`を宣言し、要素の挿入と削除でリアルタイムに更新します。この変数により、リストの末尾を特定し、拡張が必要かどうかを判断できます。
|
||||
- **拡張メカニズム**:要素挿入時にリストが満杯に達した場合、拡張プロセスが必要です。これには拡張係数に基づいてより大きな配列を作成し、現在の配列からすべての要素を新しい配列に転送することが含まれます。この例では、拡張のたびに配列サイズを2倍にすることを規定します。
|
||||
|
||||
```src
|
||||
[file]{my_list}-[class]{my_list}-[func]{}
|
||||
```
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 18 KiB |
71
ja/docs/chapter_array_and_linkedlist/ram_and_cache.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# メモリとキャッシュ *
|
||||
|
||||
この章の最初の2つのセクションでは、「連続格納」と「分散格納」をそれぞれ表現する2つの基本的なデータ構造である配列と連結リストを探究しました。
|
||||
|
||||
実際、**物理構造はプログラムがメモリとキャッシュをどの程度効率的に利用するかを大きく決定し**、これがアルゴリズムの全体的なパフォーマンスに影響を与えます。
|
||||
|
||||
## コンピュータ記憶装置
|
||||
|
||||
コンピュータには3種類の記憶装置があります:<u>ハードディスク</u>、<u>ランダムアクセスメモリ(RAM)</u>、および<u>キャッシュメモリ</u>です。以下の表は、コンピュータシステムにおけるそれぞれの役割とパフォーマンス特性を示しています。
|
||||
|
||||
<p align="center"> 表 <id> コンピュータ記憶装置 </p>
|
||||
|
||||
| | ハードディスク | メモリ | キャッシュ |
|
||||
| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| 用途 | OS、プログラム、ファイルなどのデータの長期保存 | 現在実行中のプログラムと処理中のデータの一時保存 | 頻繁にアクセスされるデータと命令を保存し、CPUのメモリへのアクセス数を削減 |
|
||||
| 揮発性 | 電源オフ後もデータは失われない | 電源オフ後にデータは失われる | 電源オフ後にデータは失われる |
|
||||
| 容量 | より大きい、TBレベル | より小さい、GBレベル | 非常に小さい、MBレベル |
|
||||
| 速度 | より遅い、数百から数千MB/s | より高速、数十GB/s | 非常に高速、数十から数百GB/s |
|
||||
| 価格(USD) | より安価、数セント/GB | より高価、数ドル/GB | 非常に高価、CPUと一緒に価格設定 |
|
||||
|
||||
コンピュータ記憶システムは、下図に示すようにピラミッドとして視覚化できます。ピラミッドの上部にある記憶装置ほど高速で、容量が小さく、より高価です。このマルチレベル設計は偶然ではなく、コンピュータ科学者とエンジニアによる慎重な検討の結果です。
|
||||
|
||||
- **ハードディスクをメモリに置き換えるのは困難です**。第一に、メモリ内のデータは電源オフ後に失われるため、長期データ保存には適していません。第二に、メモリはハードディスクよりも大幅に高価で、消費者市場での広範囲な使用の実現可能性を制限しています。
|
||||
- **キャッシュは大容量と高速のトレードオフに直面しています**。L1、L2、L3キャッシュの容量が増加するにつれて、その物理サイズが大きくなり、CPUコアからの距離が増加します。これによりデータ転送時間が長くなり、アクセス遅延が高くなります。現在の技術では、マルチレベルキャッシュ構造が容量、速度、コストの間の最適なバランスを提供します。
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
コンピュータの記憶階層は、速度、容量、コストの間の慎重なバランスを反映しています。このタイプのトレードオフは様々な業界で一般的であり、利益と制限の間の最適なバランスを見つけることが重要です。
|
||||
|
||||
全体的に、**ハードディスクは大量のデータの長期保存を提供し、メモリはプログラム実行中に処理されるデータの一時保存として機能し、キャッシュは頻繁にアクセスされるデータと命令を保存して実行効率を向上させます**。それらは一緒になってコンピュータシステムの効率的な動作を保証します。
|
||||
|
||||
下図に示すように、プログラム実行中、データはハードディスクからメモリに読み込まれ、CPU計算が行われます。CPUの拡張として機能するキャッシュは、**メモリからインテリジェントにデータを先読み**し、CPUのより高速なデータアクセスを可能にします。これによりプログラム実行効率が大幅に向上し、低速なメモリへの依存が減少します。
|
||||
|
||||

|
||||
|
||||
## データ構造のメモリ効率
|
||||
|
||||
メモリ空間利用の観点から、配列と連結リストにはそれぞれ利点と制限があります。
|
||||
|
||||
一方で、**メモリは限られており、複数のプログラム間で共有できない**ため、データ構造での空間使用の最適化は重要です。配列は要素が密接にパックされており、連結リストのように参照(ポインタ)のための追加メモリを必要としないため、空間効率的です。しかし、配列は連続したメモリブロックを事前に割り当てる必要があり、割り当てられた空間が実際の必要量を超える場合、無駄につながる可能性があります。配列の拡張も追加の時間と空間のオーバーヘッドを伴います。対照的に、連結リストは各ノードに対してメモリを動的に割り当て・解放し、ポインタのための追加メモリのコストでより大きな柔軟性を提供します。
|
||||
|
||||
一方で、プログラム実行中、**繰り返されるメモリの割り当てと解放はメモリの断片化を増加させ**、メモリ利用効率を低下させます。配列は連続記憶方式により、メモリ断片化を引き起こす可能性が比較的低いです。対照的に、連結リストは要素を非連続の場所に保存し、頻繁な挿入と削除はメモリ断片化を悪化させる可能性があります。
|
||||
|
||||
## データ構造のキャッシュ効率
|
||||
|
||||
キャッシュはメモリよりも空間容量がはるかに小さいですが、はるかに高速で、プログラム実行速度において重要な役割を果たします。限られた容量のため、キャッシュは頻繁にアクセスされるデータのサブセットのみを保存できます。CPUがキャッシュに存在しないデータにアクセスしようとすると、<u>キャッシュミス</u>が発生し、CPUは低速なメモリから必要なデータを取得する必要があり、パフォーマンスに影響を与える可能性があります。
|
||||
|
||||
明らかに、**キャッシュミスが少ないほど、CPUのデータ読み書き効率が高く**、プログラムパフォーマンスが向上します。CPUがキャッシュからデータを正常に取得する割合は<u>キャッシュヒット率</u>と呼ばれ、キャッシュ効率を測定するためによく使用される指標です。
|
||||
|
||||
より高い効率を達成するために、キャッシュは以下のデータロードメカニズムを採用します。
|
||||
|
||||
- **キャッシュライン**:キャッシュは個々のバイトではなく、キャッシュラインと呼ばれる単位でデータを保存・ロードして動作します。このアプローチは、一度により大きなデータブロックを転送することで効率を向上させます。
|
||||
- **先読みメカニズム**:プロセッサはデータアクセスパターン(例:連続または固定ストライドアクセス)を予測し、これらのパターンに基づいてデータをキャッシュに先読みして、キャッシュヒット率を向上させます。
|
||||
- **空間的局所性**:特定のデータがアクセスされると、近くのデータもまもなくアクセスされる可能性があります。これを活用するために、キャッシュは要求されたデータと一緒に隣接するデータをロードし、ヒット率を向上させます。
|
||||
- **時間的局所性**:データがアクセスされた場合、近い将来に再びアクセスされる可能性があります。キャッシュはこの原理を使用して、最近アクセスされたデータを保持してヒット率を向上させます。
|
||||
|
||||
実際、**配列と連結リストは異なるキャッシュ利用効率を持ち**、これは主に以下の側面に反映されます。
|
||||
|
||||
- **占有空間**:連結リスト要素は配列要素よりも多くの空間を占有するため、キャッシュに保持される有効データが少なくなります。
|
||||
- **キャッシュライン**:連結リストデータはメモリ全体に散在し、キャッシュは「行単位でロード」されるため、ロードされる無効データの割合が高くなります。
|
||||
- **先読みメカニズム**:配列のデータアクセスパターンは連結リストよりも「予測可能」で、つまりシステムがこれからロードされるデータを推測しやすいです。
|
||||
- **空間的局所性**:配列は連続したメモリ空間に保存されるため、ロードされているデータの近くのデータがまもなくアクセスされる可能性が高くなります。
|
||||
|
||||
全体的に、**配列はより高いキャッシュヒット率を持ち、一般的に連結リストよりも操作効率が高いです**。これにより、配列に基づくデータ構造はアルゴリズム問題の解決において人気があります。
|
||||
|
||||
**高いキャッシュ効率が配列が常に連結リストより優れているという意味ではない**ことに注意すべきです。データ構造の選択は特定のアプリケーション要件に依存すべきです。例えば、配列と連結リストの両方が「スタック」データ構造を実装できますが(次章で詳細説明)、それらは異なるシナリオに適しています。
|
||||
|
||||
- アルゴリズム問題では、より高い操作効率とランダムアクセス機能を提供するため、配列に基づくスタックを選択する傾向があります。唯一のコストは配列に対して一定量のメモリ空間を事前に割り当てる必要があることです。
|
||||
- データ量が非常に大きく、高度に動的で、スタックの予想サイズを推定するのが困難な場合、連結リストに基づくスタックがより良い選択です。連結リストは大量のデータをメモリの異なる部分に分散でき、配列拡張の追加オーバーヘッドを回避できます。
|
||||
81
ja/docs/chapter_array_and_linkedlist/summary.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# まとめ
|
||||
|
||||
### 重要な復習
|
||||
|
||||
- 配列と連結リストは2つの基本的なデータ構造であり、コンピュータメモリにおける2つの格納方法を表しています:連続空間格納と非連続空間格納です。それらの特性は互いに補完し合います。
|
||||
- 配列はランダムアクセスをサポートし、使用するメモリが少ない一方で、要素の挿入と削除は非効率的で、初期化後の長さが固定されています。
|
||||
- 連結リストは参照(ポインタ)の変更によって効率的なノードの挿入と削除を実装し、長さを柔軟に調整できますが、ノードアクセス効率が低く、より多くのメモリを消費します。
|
||||
- 連結リストの一般的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあり、それぞれに独自の応用シナリオがあります。
|
||||
- リストは要素の順序付けられたコレクションで、追加、削除、変更をサポートし、通常は動的配列に基づいて実装され、配列の利点を保持しながら柔軟な長さ調整を可能にします。
|
||||
- リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります。
|
||||
- プログラム実行中、データは主にメモリに格納されます。配列はより高いメモリ空間効率を提供し、連結リストはメモリ使用においてより柔軟です。
|
||||
- キャッシュは、キャッシュライン、先読み、空間的局所性、時間的局所性などのメカニズムを通じてCPUに高速データアクセスを提供し、プログラム実行効率を大幅に向上させます。
|
||||
- より高いキャッシュヒット率により、配列は一般的に連結リストよりも効率的です。データ構造を選択する際は、特定のニーズとシナリオに基づいて適切な選択をすべきです。
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**:配列をスタックに格納するかヒープに格納するかは、時間と空間効率に影響しますか?
|
||||
|
||||
スタックとヒープの両方に格納される配列は連続したメモリ空間に格納され、データ操作効率は本質的に同じです。しかし、スタックとヒープには独自の特性があり、以下の違いが生じます。
|
||||
|
||||
1. 割り当てと解放効率:スタックはより小さなメモリブロックで、コンパイラによって自動的に割り当てられます。ヒープメモリは比較的大きく、コードで動的に割り当てることができ、断片化しやすいです。したがって、ヒープでの割り当てと解放操作は一般的にスタックよりも遅くなります。
|
||||
2. サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般的に利用可能なメモリによって制限されます。したがって、ヒープは大きな配列の格納により適しています。
|
||||
3. 柔軟性:スタック上の配列のサイズはコンパイル時に決定される必要がありますが、ヒープ上の配列のサイズは実行時に動的に決定できます。
|
||||
|
||||
**Q**:なぜ配列は同じ型の要素を必要とし、連結リストは同じ型の要素を強調しないのですか?
|
||||
|
||||
連結リストは参照(ポインタ)によって接続されたノードで構成され、各ノードはint、double、string、objectなど、異なる型のデータを格納できます。
|
||||
|
||||
対照的に、配列要素は同じ型である必要があり、これにより対応する要素位置にアクセスするためのオフセットを計算できます。例えば、intとlong型の両方を含む配列で、単一要素がそれぞれ4バイトと8バイトを占有する場合、配列に2つの異なる長さの要素が含まれているため、以下の式を使用してオフセットを計算できません。
|
||||
|
||||
```shell
|
||||
# 要素メモリアドレス = 配列メモリアドレス + 要素長 * 要素インデックス
|
||||
```
|
||||
|
||||
**Q**:ノードを削除した後、`P.next`を`None`に設定する必要がありますか?
|
||||
|
||||
`P.next`を変更しなくても問題ありません。連結リストの観点から、ヘッドノードからテールノードまでの巡回で`P`に遭遇することはもうありません。これは、ノード`P`がリストから効果的に削除されたことを意味し、`P`が指す場所はもはやリストに影響しません。
|
||||
|
||||
ガベージコレクションの観点から、Java、Python、Goなどの自動ガベージコレクションメカニズムを持つ言語では、ノード`P`が収集されるかどうかは、それを指す参照がまだあるかどうかに依存し、`P.next`の値には依存しません。CやC++などの言語では、ノードのメモリを手動で解放する必要があります。
|
||||
|
||||
**Q**:連結リストでは、挿入と削除操作の時間計算量は`O(1)`です。しかし、挿入や削除前の要素検索には`O(n)`時間がかかるので、なぜ時間計算量は`O(n)`ではないのですか?
|
||||
|
||||
要素を最初に検索してから削除する場合、時間計算量は確かに`O(n)`です。しかし、連結リストの挿入と削除における`O(1)`の利点は他のアプリケーションで実現できます。例えば、連結リストを使用した両端キューの実装では、常にヘッドとテールノードを指すポインタを維持し、各挿入と削除操作を`O(1)`にします。
|
||||
|
||||
**Q**:「連結リストの定義と格納方法」の図で、薄青色の格納ノードは単一のメモリアドレスを占有しますか、それともノード値と半分を共有しますか?
|
||||
|
||||
図は単なる定性的な表現であり、定量的分析は特定の状況に依存します。
|
||||
|
||||
- 異なる型のノード値は異なる量の空間を占有します。例えば、int、long、double、オブジェクトインスタンスです。
|
||||
- ポインタ変数によって占有されるメモリ空間は、使用されるオペレーティングシステムとコンパイル環境に依存し、通常8バイトまたは4バイトです。
|
||||
|
||||
**Q**:リストの末尾への要素追加は常に`O(1)`ですか?
|
||||
|
||||
要素を追加することでリスト長を超える場合、リストは最初に拡張される必要があります。システムは新しいメモリブロックを要求し、元のリストのすべての要素を移動するため、この場合の時間計算量は`O(n)`になります。
|
||||
|
||||
**Q**:「リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります」という文は、容量、長さ、拡張係数などの追加変数によって占有されるメモリを指していますか?
|
||||
|
||||
ここでの空間の無駄は主に2つの側面を指します:一方で、リストは初期長で設定されますが、常に必要とは限りません。他方で、頻繁な拡張を防ぐため、拡張は通常$\times 1.5$などの係数で乗算されます。これにより多くの空きスロットが生まれ、通常は完全に埋めることができません。
|
||||
|
||||
**Q**:Pythonで`n = [1, 2, 3]`を初期化した後、これら3つの要素のアドレスは連続していますが、`m = [2, 1, 3]`を初期化すると、各要素の`id`は連続していないが`n`のものと同一です。これらの要素のアドレスが連続していない場合、`m`はまだ配列ですか?
|
||||
|
||||
リスト要素を連結リストノード`n = [n1, n2, n3, n4, n5]`に置き換える場合、これら5つのノードオブジェクトも通常メモリ全体に分散しています。しかし、リストインデックスが与えられれば、`O(1)`時間でノードのメモリアドレスにアクセスでき、対応するノードにアクセスできます。これは、配列がノード自体ではなく、ノードへの参照を格納するためです。
|
||||
|
||||
多くの言語とは異なり、Pythonでは数値もオブジェクトとしてラップされ、リストは数値自体ではなく、これらの数値への参照を格納します。したがって、2つの配列の同じ数値が同じ`id`を持ち、これらの数値のメモリアドレスは連続である必要がないことがわかります。
|
||||
|
||||
**Q**:C++ STLの`std::list`はすでに双方向連結リストを実装していますが、一部のアルゴリズム書籍では直接使用していないようです。何か制限がありますか?
|
||||
|
||||
一方で、アルゴリズムを実装する際は配列を使用することを好み、必要な場合のみ連結リストを使用します。主に2つの理由があります。
|
||||
|
||||
- 空間オーバーヘッド:各要素に2つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list`は通常`std::vector`よりも多くの空間を占有します。
|
||||
- キャッシュ非友好的:データが連続して格納されていないため、`std::list`はキャッシュ利用率が低くなります。一般的に、`std::vector`の方がパフォーマンスが優れています。
|
||||
|
||||
他方で、連結リストは主に二分木とグラフに必要です。スタックとキューは、連結リストではなく、プログラミング言語の`stack`と`queue`クラスを使用して実装されることが多いです。
|
||||
|
||||
**Q**:リスト`res = [0] * self.size()`を初期化すると、`res`の各要素は同じアドレスを参照しますか?
|
||||
|
||||
いいえ。しかし、この問題は二次元配列で発生します。例えば、二次元リスト`res = [[0]] * self.size()`を初期化すると、同じリスト`[0]`を複数回参照することになります。
|
||||
|
||||
**Q**:ノードを削除する際、その後続ノードへの参照を断つ必要がありますか?
|
||||
|
||||
データ構造とアルゴリズム(問題解決)の観点から、プログラムのロジックが正しい限り、リンクを断たなくても問題ありません。標準ライブラリの観点から、リンクを断つ方が安全で論理的に明確です。リンクを断たず、削除されたノードが適切にリサイクルされない場合、後続ノードのメモリのリサイクルに影響を与える可能性があります。
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
509
ja/docs/chapter_backtracking/backtracking_algorithm.md
Normal file
@@ -0,0 +1,509 @@
|
||||
# バックトラッキングアルゴリズム
|
||||
|
||||
<u>バックトラッキングアルゴリズム</u>は全数探索によって問題を解決する方法です。その核心概念は、初期状態から開始してすべての可能な解を総当たりで探索することです。アルゴリズムは正しいものを記録し、解が見つかるか、すべての可能な解が試されたが解が見つからないまで続けます。
|
||||
|
||||
バックトラッキングは通常「深さ優先探索」を使用して解空間を走査します。「二分木」の章で、前順、中順、後順走査はすべて深さ優先探索であることを述べました。次に、前順走査を使用してバックトラッキング問題を解決し、アルゴリズムの動作を段階的に理解していきます。
|
||||
|
||||
!!! question "例1"
|
||||
|
||||
二分木が与えられた場合、値が $7$ のすべてのノードを検索して記録し、リストで返してください。
|
||||
|
||||
この問題を解決するために、この木を前順で走査し、現在のノードの値が $7$ かどうかを確認します。そうであれば、ノードの値を結果リスト `res` に追加します。プロセスは以下の図に示されています:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 試行と後退
|
||||
|
||||
**解空間を探索する際に「試行」と「後退」戦略を使用するため、バックトラッキングアルゴリズムと呼ばれます**。探索中、満足のいく解を得るためにもはや進めない状態に遭遇するたびに、前の選択を取り消して前の状態に戻り、次の試行のために他の可能な選択を選択できるようにします。
|
||||
|
||||
例1では、各ノードの訪問が「試行」を開始します。そして葉ノードを通過するか、`return` 文で親ノードに戻ることが「後退」を示唆します。
|
||||
|
||||
**後退は単に関数の戻り値ではないことに注意してください**。例1の問題を少し拡張して、それが何を意味するかを説明します。
|
||||
|
||||
!!! question "例2"
|
||||
|
||||
二分木で、値が $7$ のすべてのノードを検索し、すべてのマッチングノードについて、**ルートノードからそのノードまでのパスを返してください**。
|
||||
|
||||
例1のコードに基づいて、訪問したノードパスを記録するために `path` というリストを使用する必要があります。値が $7$ のノードに到達すると、`path` をコピーして結果リスト `res` に追加します。走査後、`res` にはすべての解が保持されます。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_ii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
各「試行」で、現在のノードを `path` に追加することでパスを記録します。「後退」が必要なときはいつでも、`path` からノードをポップして**この失敗した試行前の状態を復元します**。
|
||||
|
||||
以下の図に示すプロセスを観察することで、**試行は「前進」のようで、後退は「元に戻す」のようです**。後者のペアは、対応するものに対する逆操作と見なすことができます。
|
||||
|
||||
=== "<1>"
|
||||

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
## 剪定
|
||||
|
||||
複雑なバックトラッキング問題は通常1つ以上の制約を含み、**これらは「剪定」によく使用されます**。
|
||||
|
||||
!!! question "例3"
|
||||
|
||||
二分木で、値が $7$ のすべてのノードを検索し、ルートからこれらのノードまでのパスを返してください。**ただし、パスには値が $3$ のノードを含まないという制限があります**。
|
||||
|
||||
上記の制約を満たすために、**剪定操作を追加する必要があります**:検索プロセス中に、値が $3$ のノードに遭遇した場合、そのパスを通じてさらに検索することを即座に中止します。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
「剪定」は非常に生き生きとした名詞です。以下の図に示すように、検索プロセスで、**制約を満たさない検索分岐を「切り取り」ます**。さらなる不要な試行を避け、検索効率を向上させます。
|
||||
|
||||

|
||||
|
||||
## フレームワークコード
|
||||
|
||||
今度は、バックトラッキングから「試行、後退、剪定」の主要なフレームワークを抽出して、コードの汎用性を向上させてみましょう。
|
||||
|
||||
以下のフレームワークコードでは、`state` は問題の現在の状態を表し、`choices` は現在の状態で利用可能な選択肢を表します:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
def backtrack(state: State, choices: list[choice], res: list[state]):
|
||||
"""バックトラッキングアルゴリズムフレームワーク"""
|
||||
# 解かどうかを確認
|
||||
if is_solution(state):
|
||||
# 解を記録
|
||||
record_solution(state, res)
|
||||
# 検索を停止
|
||||
return
|
||||
# すべての選択肢を反復
|
||||
for choice in choices:
|
||||
# 剪定:選択肢が有効かどうかを確認
|
||||
if is_valid(state, choice):
|
||||
# 試行:選択を行い、状態を更新
|
||||
make_choice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
# 後退:選択を取り消し、前の状態に戻す
|
||||
undo_choice(state, choice)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
|
||||
// 解かどうかを確認
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for (Choice choice : choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
void backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// 解かどうかを確認
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for (Choice choice : choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
void Backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// 解かどうかを確認
|
||||
if (IsSolution(state)) {
|
||||
// 解を記録
|
||||
RecordSolution(state, res);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
foreach (Choice choice in choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (IsValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
MakeChoice(state, choice);
|
||||
Backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
UndoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
func backtrack(state *State, choices []Choice, res *[]State) {
|
||||
// 解かどうかを確認
|
||||
if isSolution(state) {
|
||||
// 解を記録
|
||||
recordSolution(state, res)
|
||||
// 検索を停止
|
||||
return
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for _, choice := range choices {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if isValid(state, choice) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
func backtrack(state: inout State, choices: [Choice], res: inout [State]) {
|
||||
// 解かどうかを確認
|
||||
if isSolution(state: state) {
|
||||
// 解を記録
|
||||
recordSolution(state: state, res: &res)
|
||||
// 検索を停止
|
||||
return
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for choice in choices {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if isValid(state: state, choice: choice) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state: &state, choice: choice)
|
||||
backtrack(state: &state, choices: choices, res: &res)
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state: &state, choice: choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
function backtrack(state, choices, res) {
|
||||
// 解かどうかを確認
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for (let choice of choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
function backtrack(state: State, choices: Choice[], res: State[]): void {
|
||||
// 解かどうかを確認
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for (let choice of choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
void backtrack(State state, List<Choice>, List<State> res) {
|
||||
// 解かどうかを確認
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for (Choice choice in choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
fn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {
|
||||
// 解かどうかを確認
|
||||
if is_solution(state) {
|
||||
// 解を記録
|
||||
record_solution(state, res);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for choice in choices {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if is_valid(state, choice) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
make_choice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undo_choice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {
|
||||
// 解かどうかを確認
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res, numRes);
|
||||
// 検索を停止
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for (int i = 0; i < numChoices; i++) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (isValid(state, &choices[i])) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, &choices[i]);
|
||||
backtrack(state, choices, numChoices, res, numRes);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, &choices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
fun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {
|
||||
// 解かどうかを確認
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res)
|
||||
// 検索を停止
|
||||
return
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
for (choice in choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
### バックトラッキングアルゴリズムフレームワーク ###
|
||||
def backtrack(state, choices, res)
|
||||
# 解かどうかを確認
|
||||
if is_solution?(state)
|
||||
# 解を記録
|
||||
record_solution(state, res)
|
||||
return
|
||||
end
|
||||
|
||||
# すべての選択肢を反復
|
||||
for choice in choices
|
||||
# 剪定:選択肢が有効かどうかを確認
|
||||
if is_valid?(state, choice)
|
||||
# 試行:選択を行い、状態を更新
|
||||
make_choice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
# 後退:選択を取り消し、前の状態に戻す
|
||||
undo_choice(state, choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
次に、フレームワークコードに基づいて例題 3 を解きます。状態 `state` はノードの走査経路を表し、選択肢 `choices` は現在ノードの左子ノードと右子ノード、結果 `res` は経路リストです:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_template}-[class]{}-[func]{backtrack}
|
||||
```
|
||||
|
||||
問題文の意味に従い、値が $7$ のノードを見つけた後も探索を続ける必要があります。**したがって、解を記録した後の `return` 文を削除する必要があります**。次の図は、`return` 文を保持する場合と削除する場合の探索過程の比較です。
|
||||
|
||||

|
||||
|
||||
前順走査に基づくコード実装と比べると、バックトラッキングアルゴリズムのフレームワークに基づく実装はやや冗長に見えますが、汎用性はより高いです。実際、**多くのバックトラッキング問題はこのフレームワークの下で解くことができます**。具体的な問題に応じて `state` と `choices` を定義し、フレームワーク内の各メソッドを実装すればよいのです。
|
||||
|
||||
## よく使われる用語
|
||||
|
||||
アルゴリズム問題をより明確に分析するために、バックトラッキングアルゴリズムでよく使われる用語の意味をまとめ、例題 3 の対応例を以下の表に示します。
|
||||
|
||||
<p align="center"> 表 <id> バックトラッキングアルゴリズムでよく使われる用語 </p>
|
||||
|
||||
| 名称 | 定義 | 例題 3 |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
||||
| 解(solution) | 解は問題の特定条件を満たす答えであり、1 つまたは複数存在する可能性がある | 根ノードからノード $7$ までの制約条件を満たすすべての経路 |
|
||||
| 制約条件(constraint) | 制約条件は、解の実現可能性を制限する条件であり、通常は枝刈りに使用される | 経路にノード $3$ を含まない |
|
||||
| 状態(state) | 状態は、ある時点での問題の状況を表し、これまでに行った選択を含む | 現在訪問したノード経路、すなわち `path` ノードリスト |
|
||||
| 試行(attempt) | 試行は、利用可能な選択肢に基づいて解空間を探索する過程であり、選択を行い、状態を更新し、解かどうかを確認する | 左(右)子ノードを再帰的に訪問し、ノードを `path` に追加し、ノードの値が $7$ かを確認する |
|
||||
| バックトラック(backtracking) | 制約条件を満たさない状態に遭遇した場合、以前の選択を取り消して前の状態に戻ること | 葉ノードを越えたとき、探索終了、値が $3$ のノードに遭遇したとき探索を終了し、関数が戻る |
|
||||
| 枝刈り(pruning) | 問題の特性や制約条件に基づき、無意味な探索経路を避ける方法であり、探索効率を向上させる | 値が $3$ のノードに遭遇した場合、それ以上探索しない |
|
||||
|
||||
!!! tip
|
||||
|
||||
問題、解、状態などの概念は一般的なものであり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも関係します。
|
||||
|
||||
## 長所と限界
|
||||
|
||||
バックトラッキングアルゴリズムは本質的に深さ優先探索(DFS)アルゴリズムの一種であり、条件を満たす解を見つけるまであらゆる可能な解を試みます。この方法の利点は、すべての可能な解を見つけられる点であり、適切な枝刈りを行えば効率が高いことです。
|
||||
|
||||
しかし、大規模または複雑な問題を扱う場合、**バックトラッキングアルゴリズムの実行効率は許容できないほど低下する可能性があります**。
|
||||
|
||||
- **時間**:バックトラッキングアルゴリズムは通常、状態空間のすべての可能性を探索する必要があり、時間計算量は指数オーダーまたは階乗オーダーに達する可能性があります。
|
||||
- **空間**:再帰呼び出し中に現在の状態(例:経路、枝刈り用の補助変数など)を保存する必要があり、深さが大きい場合、空間の使用量が増加します。
|
||||
|
||||
それでもなお、**バックトラッキングアルゴリズムは特定の探索問題や制約満足問題の最良の解法であることが多いです**。これらの問題では、どの選択が有効な解を生成するかを予測できないため、すべての可能な選択を試す必要があります。このような場合、**効率の最適化が鍵**となります。一般的な最適化手法は次の 2 つです。
|
||||
|
||||
- **枝刈り**:解を生成しないことが確実な経路を避けることで、時間と空間を節約します。
|
||||
- **ヒューリスティック探索**:探索中に戦略や評価値を導入し、有効な解を生成する可能性が高い経路を優先的に探索します。
|
||||
|
||||
## バックトラッキングの典型的な例題
|
||||
|
||||
バックトラッキングアルゴリズムは、多くの探索問題、制約満足問題、組合せ最適化問題を解くのに使用できます。
|
||||
|
||||
**探索問題**:この種の問題の目標は、特定の条件を満たす解を見つけることです。
|
||||
|
||||
- 全順列問題:与えられた集合のすべての可能な順列を求める。
|
||||
- 部分和問題:与えられた集合と目標和に対して、和が目標値になるすべての部分集合を求める。
|
||||
- ハノイの塔:3 本の柱と異なるサイズの円盤があり、すべての円盤を 1 本の柱から別の柱に移す。1 回に 1 枚しか動かせず、大きな円盤を小さい円盤の上に置くことはできない。
|
||||
|
||||
**制約満足問題**:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。
|
||||
|
||||
- $n$ クイーン問題:$n imes n$ のチェス盤に $n$ 個のクイーンを配置し、互いに攻撃しないようにする。
|
||||
- 数独:$9 imes 9$ のグリッドに数字 $1$ \~ $9$ を入力し、各行、列、$3 imes 3$ のサブグリッドに重複がないようにする。
|
||||
- グラフ彩色問題:与えられた無向グラフに対し、隣接頂点が異なる色になるように最小限の色で彩色する。
|
||||
|
||||
**組合せ最適化問題**:この種の問題の目標は、組合せ空間内で特定の条件を満たす最適解を見つけることです。
|
||||
|
||||
- 0-1 ナップサック問題:与えられた物品群とバックパックがあり、各物品には価値と重さが設定されている。バックパックの容量制限内で、総価値を最大化する物品の選択を求める。
|
||||
- 旅行セールスマン問題:グラフ上で、1 つの点から出発し、すべての他の点を 1 回ずつ訪問して出発点に戻る最短経路を求める。
|
||||
- 最大クリーク問題:与えられた無向グラフの中で、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。
|
||||
|
||||
注意すべきは、多くの組合せ最適化問題に対して、バックトラッキングが最適解法ではないということです。
|
||||
|
||||
- 0-1 ナップサック問題は、時間効率を高めるために動的計画法がよく使用されます。
|
||||
- 旅行セールスマン問題は有名な NP-Hard 問題であり、遺伝的アルゴリズムやアントコロニーアルゴリズムなどの手法がよく使われます。
|
||||
- 最大クリーク問題はグラフ理論の古典的な問題であり、貪欲法などのヒューリスティックアルゴリズムで解くことができます。
|
||||
9
ja/docs/chapter_backtracking/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# バックトラッキング
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
迷路の探検家のように、私たちは前進する道で障害に遭遇することがあります。
|
||||
|
||||
バックトラッキングの力は、私たちに新しく始めること、試し続けること、そして最終的に光への出口を見つけることを可能にします。
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
53
ja/docs/chapter_backtracking/n_queens_problem.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Nクイーン問題
|
||||
|
||||
!!! question
|
||||
|
||||
チェスのルールによると、クイーンは同じ行、列、または対角線上の駒を攻撃できます。$n$ 個のクイーンと $n \times n$ のチェスボードが与えられた場合、2つのクイーンが互いに攻撃できない配置を見つけてください。
|
||||
|
||||
以下の図に示すように、$n = 4$ の場合、2つの解があります。バックトラッキングアルゴリズムの観点から、$n \times n$ のチェスボードには $n^2$ 個のマスがあり、すべての可能な選択肢 `choices` を示しています。チェスボードの状態 `state` は、各クイーンが配置されるにつれて継続的に変化します。
|
||||
|
||||

|
||||
|
||||
以下の図は、この問題の3つの制約を示しています:**複数のクイーンは同じ行、列、または対角線を占有できません**。対角線は主対角線 `\` と副対角線 `/` に分かれることに注意することが重要です。
|
||||
|
||||

|
||||
|
||||
### 行ごとの配置戦略
|
||||
|
||||
クイーンの数がチェスボードの行数と等しく、どちらも $n$ であるため、**チェスボードの各行には1つのクイーンのみが配置できることが**容易に結論付けられます。
|
||||
|
||||
これは、行ごとの配置戦略を採用できることを意味します:最初の行から開始して、最後の行に到達するまで行ごとに1つのクイーンを配置します。
|
||||
|
||||
以下の図は、4クイーン問題の行ごとの配置プロセスを示しています。スペースの制限により、図は最初の行の1つの検索分岐のみを展開し、列と対角線の制約を満たさない配置を剪定します。
|
||||
|
||||

|
||||
|
||||
本質的に、**行ごとの配置戦略は剪定関数として機能し**、同じ行に複数のクイーンを配置するすべての検索分岐を除去します。
|
||||
|
||||
### 列と対角線の剪定
|
||||
|
||||
列の制約を満たすために、長さ $n$ のブール配列 `cols` を使用して、各列にクイーンが占有されているかどうかを追跡できます。各配置決定の前に、`cols` を使用してすでにクイーンがある列を剪定し、バックトラッキング中に動的に更新されます。
|
||||
|
||||
!!! tip
|
||||
|
||||
行列の原点は左上隅にあり、行インデックスは上から下に増加し、列インデックスは左から右に増加することに注意してください。
|
||||
|
||||
対角線の制約はどうでしょうか?チェスボード上の特定のセルの行と列のインデックスを $(row, col)$ とします。特定の主対角線を選択することで、その対角線上のすべてのセルで差 $row - col$ が同じであることに気付きます。**つまり、$row - col$ は主対角線上で定数値です**。
|
||||
|
||||
言い換えると、2つのセルが $row_1 - col_1 = row_2 - col_2$ を満たす場合、それらは確実に同じ主対角線上にあります。このパターンを使用して、以下の図に示す配列 `diags1` を利用して、クイーンが主対角線上にあるかどうかを追跡できます。
|
||||
|
||||
同様に、**$row + col$ の和は副対角線上のすべてのセルで定数値です**。配列 `diags2` を使用して副対角線の制約も処理できます。
|
||||
|
||||

|
||||
|
||||
### コード実装
|
||||
|
||||
$n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で、$row + col$ の範囲は $[0, 2n - 2]$ であることに注意してください。したがって、主対角線と副対角線の数はどちらも $2n - 1$ で、配列 `diags1` と `diags2` の長さは $2n - 1$ です。
|
||||
|
||||
```src
|
||||
[file]{n_queens}-[class]{}-[func]{n_queens}
|
||||
```
|
||||
|
||||
$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)$ です**。
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 24 KiB |
95
ja/docs/chapter_backtracking/permutations_problem.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 順列問題
|
||||
|
||||
順列問題は、バックトラッキングアルゴリズムの典型的な応用です。これは、配列や文字列などの与えられた集合から要素のすべての可能な配置(順列)を見つけることを含みます。
|
||||
|
||||
以下の表は、入力配列とその対応する順列を含むいくつかの例を示しています。
|
||||
|
||||
<p align="center"> 表 <id> 順列の例 </p>
|
||||
|
||||
| 入力配列 | 順列 |
|
||||
| :----------- | :----------------------------------------------------------------- |
|
||||
| $[1]$ | $[1]$ |
|
||||
| $[1, 2]$ | $[1, 2], [2, 1]$ |
|
||||
| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ |
|
||||
|
||||
## 重複要素がない場合
|
||||
|
||||
!!! question
|
||||
|
||||
重複要素のない整数配列が与えられた場合、すべての可能な順列を返してください。
|
||||
|
||||
バックトラッキングの観点から、**順列を生成するプロセスを一連の選択として見ることができます。** 入力配列が $[1, 2, 3]$ だとします。最初に $1$ を選択し、次に $3$、最後に $2$ を選択すると、順列 $[1, 3, 2]$ が得られます。「バックトラッキング」は前の選択を取り消して、代替オプションを探索することを意味します。
|
||||
|
||||
コーディングの観点から、候補集合 `choices` は入力配列のすべての要素で構成され、`state` はこれまでに選択された要素を保持します。各要素は一度だけ選択できるため、**`state` のすべての要素は一意である必要があります**。
|
||||
|
||||
以下の図に示すように、検索プロセスを再帰木に展開できます。各ノードは現在の `state` を表します。ルートノードから開始して、3回の選択の後、葉ノードに到達します—それぞれが順列に対応します。
|
||||
|
||||

|
||||
|
||||
### 重複選択の剪定
|
||||
|
||||
各要素が一度だけ選択されることを保証するために、ブール配列 `selected` を導入します。ここで `selected[i]` は `choices[i]` が選択されたかどうかを示します。次に、この配列に基づいて剪定ステップを実行します:
|
||||
|
||||
- `choice[i]` を選択した後、`selected[i]` を $\text{True}$ に設定して選択されたとマークします。
|
||||
- `choices` を反復処理する際、選択されたとマークされたすべての要素をスキップします(つまり、それらの分岐を剪定します)。
|
||||
|
||||
以下の図に示すように、最初のラウンドで1を選択し、2番目のラウンドで3を選択し、最後のラウンドで2を選択するとします。2番目のラウンドで要素1の分岐と、3番目のラウンドで要素1と3の分岐を剪定する必要があります。
|
||||
|
||||

|
||||
|
||||
図から、この剪定プロセスが検索空間を $O(n^n)$ から $O(n!)$ に削減することがわかります。
|
||||
|
||||
### コード実装
|
||||
|
||||
この理解により、フレームワークコードの「空欄を埋める」ことができます。全体のコードを簡潔に保つため、フレームワークの各部分を個別に実装せず、代わりに `backtrack()` 関数ですべてを展開します:
|
||||
|
||||
```src
|
||||
[file]{permutations_i}-[class]{}-[func]{permutations_i}
|
||||
```
|
||||
|
||||
## 重複要素を考慮する場合
|
||||
|
||||
!!! question
|
||||
|
||||
**重複要素を含む可能性のある**整数配列が与えられた場合、すべての一意の順列を返してください。
|
||||
|
||||
入力配列が $[1, 1, 2]$ だとします。2つの同一要素 $1$ を区別するために、2番目を $\hat{1}$ とラベル付けします。
|
||||
|
||||
以下の図に示すように、この方法で生成される順列の半分は重複です:
|
||||
|
||||

|
||||
|
||||
では、これらの重複順列をどのように除去できるでしょうか?一つの直接的なアプローチは、すべての順列を生成した後にハッシュセットを使用して重複を除去することです。しかし、これはあまり優雅ではありません。**重複を生成する分岐は本来不要であり、事前に剪定されるべきだからです**、これによりアルゴリズムの効率が向上します。
|
||||
|
||||
### 等値要素の剪定
|
||||
|
||||
以下の図を見ると、最初のラウンドで $1$ または $\hat{1}$ を選択すると同じ順列につながるため、$\hat{1}$ を剪定します。
|
||||
|
||||
同様に、最初のラウンドで $2$ を選択した後、2番目のラウンドで $1$ または $\hat{1}$ を選択しても重複分岐につながるため、その時も $\hat{1}$ を剪定します。
|
||||
|
||||
本質的に、**私たちの目標は、複数の同一要素が選択の各ラウンドで一度だけ選択されることを保証することです。**
|
||||
|
||||

|
||||
|
||||
### コード実装
|
||||
|
||||
前の問題のコードに基づいて、各ラウンドでハッシュセット `duplicated` を導入します。このセットは、すでに試行した要素を追跡し、重複を剪定できるようにします:
|
||||
|
||||
```src
|
||||
[file]{permutations_ii}-[class]{}-[func]{permutations_ii}
|
||||
```
|
||||
|
||||
すべての要素が異なると仮定すると、$n$ 個の要素の順列は $n!$ (階乗)個あります。各結果を記録するには長さ $n$ のリストをコピーする必要があり、これには $O(n)$ 時間がかかります。**したがって、総時間計算量は $O(n!n)$ です。**
|
||||
|
||||
最大再帰深度は $n$ で、$O(n)$ のスタック空間を使用します。`selected` 配列も $O(n)$ 空間が必要です。一度に最大 $n$ 個の個別の `duplicated` セットが存在する可能性があるため、それらは集合的に $O(n^2)$ 空間を占有します。**したがって、空間計算量は $O(n^2)$ です。**
|
||||
|
||||
### 2つの剪定方法の比較
|
||||
|
||||
`selected` と `duplicated` はどちらも剪定メカニズムとして機能しますが、異なる問題をターゲットにしています:
|
||||
|
||||
- **重複選択の剪定**(`selected` 経由):検索全体に単一の `selected` 配列があり、現在の状態にすでにある要素を示します。これにより、同じ要素が `state` に複数回現れることを防ぎます。
|
||||
- **等値要素の剪定**(`duplicated` 経由):`backtrack` 関数の各呼び出しは独自の `duplicated` セットを使用し、その特定の反復(`for` ループ)ですでに選択された要素を記録します。これにより、等しい要素が選択の各ラウンドで一度だけ選択されることを保証します。
|
||||
|
||||
以下の図は、これら2つの剪定戦略の範囲を示しています。木の各ノードは選択を表します。ルートから任意の葉への経路は、1つの完全な順列に対応します。
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 21 KiB |
95
ja/docs/chapter_backtracking/subset_sum_problem.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 部分集合和問題
|
||||
|
||||
## 重複要素がない場合
|
||||
|
||||
!!! question
|
||||
|
||||
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。与えられた配列には重複要素がなく、各要素は複数回選択できます。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
|
||||
|
||||
例えば、入力集合 $\{3, 4, 5\}$ とターゲット整数 $9$ の場合、解は $\{3, 3, 3\}, \{4, 5\}$ です。以下の2点に注意してください。
|
||||
|
||||
- 入力集合の要素は無制限に選択できます。
|
||||
- 部分集合は要素の順序を区別しません。例えば $\{4, 5\}$ と $\{5, 4\}$ は同じ部分集合です。
|
||||
|
||||
### 順列解法の参考
|
||||
|
||||
順列問題と同様に、部分集合の生成を一連の選択として想像でき、選択プロセス中に「要素和」をリアルタイムで更新できます。要素和が `target` に等しくなったとき、部分集合を結果リストに記録します。
|
||||
|
||||
順列問題とは異なり、**この問題では要素は無制限に選択できるため**、要素が選択されたかどうかを記録するための `selected` ブール配列を使用する必要がありません。順列コードに軽微な修正を加えて、最初に問題を解決できます:
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i_naive}-[class]{}-[func]{subset_sum_i_naive}
|
||||
```
|
||||
|
||||
配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力すると、結果 $[3, 3, 3], [4, 5], [5, 4]$ が得られます。**和が $9$ のすべての部分集合を正常に見つけましたが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。
|
||||
|
||||
これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、$5$ の前に $4$ を選択することと $4$ の前に $5$ を選択することは異なる分岐ですが、同じ部分集合に対応します。
|
||||
|
||||

|
||||
|
||||
重複する部分集合を除去するために、**直接的なアイデアは結果リストを重複除去することです**。しかし、この方法は2つの理由で非常に非効率的です。
|
||||
|
||||
- 配列要素が多い場合、特に `target` が大きい場合、検索プロセスで大量の重複する部分集合が生成されます。
|
||||
- 部分集合(配列)の差異を比較することは非常に時間がかかり、まず配列をソートし、次に配列の各要素の差異を比較する必要があります。
|
||||
|
||||
### 重複部分集合の剪定
|
||||
|
||||
**剪定を通じて検索プロセス中に重複除去を検討します**。以下の図を観察すると、異なる順序で配列要素を選択するときに重複する部分集合が生成されます。例えば、以下の状況です。
|
||||
|
||||
1. 最初のラウンドで $3$ を選択し、2番目のラウンドで $4$ を選択すると、これら2つの要素を含むすべての部分集合が生成され、$[3, 4, \dots]$ と表記されます。
|
||||
2. 後で、最初のラウンドで $4$ が選択されたとき、**2番目のラウンドは $3$ をスキップすべきです**。この選択によって生成される部分集合 $[4, 3, \dots]$ はステップ `1.` の部分集合と完全に重複するからです。
|
||||
|
||||
検索プロセスでは、各層の選択が左から右に一つずつ試行されるため、右側の分岐ほどより多く剪定されます。
|
||||
|
||||
1. 最初の2ラウンドで $3$ と $5$ を選択し、部分集合 $[3, 5, \dots]$ を生成します。
|
||||
2. 最初の2ラウンドで $4$ と $5$ を選択し、部分集合 $[4, 5, \dots]$ を生成します。
|
||||
3. 最初のラウンドで $5$ が選択された場合、**2番目のラウンドは $3$ と $4$ をスキップすべきです**。部分集合 $[5, 3, \dots]$ と $[5, 4, \dots]$ はステップ `1.` と `2.` で記述された部分集合と完全に重複するからです。
|
||||
|
||||

|
||||
|
||||
要約すると、入力配列 $[x_1, x_2, \dots, x_n]$ が与えられた場合、検索プロセスでの選択シーケンスは $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$ であるべきで、$i_1 \leq i_2 \leq \dots \leq i_m$ を満たす必要があります。**この条件を満たさない選択シーケンスは重複を引き起こし、剪定されるべきです**。
|
||||
|
||||
### コード実装
|
||||
|
||||
この剪定を実装するために、変数 `start` を初期化し、これは走査の開始点を示します。**選択 $x_{i}$ を行った後、次のラウンドをインデックス $i$ から開始するように設定します**。これにより、選択シーケンスが $i_1 \leq i_2 \leq \dots \leq i_m$ を満たすことが保証され、部分集合の一意性が保証されます。
|
||||
|
||||
さらに、コードに以下の2つの最適化を行いました。
|
||||
|
||||
- 検索を開始する前に、配列 `nums` をソートします。すべての選択の走査で、**部分集合和が `target` を超えたときにループを直接終了します**。後続の要素はより大きく、それらの部分集合和は確実に `target` を超えるからです。
|
||||
- 要素和変数 `total` を除去し、**`target` に対して減算を実行して要素和をカウントします**。`target` が $0$ に等しくなったとき、解を記録します。
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i}-[class]{}-[func]{subset_sum_i}
|
||||
```
|
||||
|
||||
以下の図は、配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。
|
||||
|
||||

|
||||
|
||||
## 重複要素がある場合を考慮
|
||||
|
||||
!!! question
|
||||
|
||||
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。**与えられた配列には重複要素が含まれる可能性があり、各要素は一度だけ選択できます**。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
|
||||
|
||||
前の問題と比較して、**この問題の入力配列には重複要素が含まれる可能性があり**、新しい問題が導入されます。例えば、配列 $[4, \hat{4}, 5]$ とターゲット要素 $9$ が与えられた場合、既存のコードの出力結果は $[4, 5], [\hat{4}, 5]$ となり、重複する部分集合が生成されます。
|
||||
|
||||
**この重複の理由は、特定のラウンドで等しい要素が複数回選択されることです**。以下の図では、最初のラウンドに3つの選択肢があり、そのうち2つが $4$ であり、2つの重複する検索分岐を生成し、重複する部分集合を出力します。同様に、2番目のラウンドの2つの $4$ も重複する部分集合を生成します。
|
||||
|
||||

|
||||
|
||||
### 等値要素の剪定
|
||||
|
||||
この問題を解決するために、**等しい要素がラウンドごとに一度だけ選択されるように制限する必要があります**。実装は非常に巧妙です:配列がソートされているため、等しい要素は隣接しています。これは、特定のラウンドの選択で、現在の要素がその左側の要素と等しい場合、それはすでに選択されていることを意味するため、現在の要素を直接スキップします。
|
||||
|
||||
同時に、**この問題では各配列要素は一度だけ選択できると規定されています**。幸い、変数 `start` を使用してこの制約も満たすことができます:選択 $x_{i}$ を行った後、次のラウンドをインデックス $i + 1$ から前方に開始するように設定します。これにより、重複する部分集合が除去されるだけでなく、要素の重複選択も回避されます。
|
||||
|
||||
### コード実装
|
||||
|
||||
```src
|
||||
[file]{subset_sum_ii}-[class]{}-[func]{subset_sum_ii}
|
||||
```
|
||||
|
||||
以下の図は、配列 $[4, 4, 5]$ とターゲット要素 $9$ のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。
|
||||
|
||||

|
||||
23
ja/docs/chapter_backtracking/summary.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# まとめ
|
||||
|
||||
### 重要な復習
|
||||
|
||||
- バックトラッキングアルゴリズムの本質は全数探索です。解空間の深さ優先走査を実行することで条件を満たす解を求めます。検索中に満足のいく解が見つかった場合、それを記録し、すべての解が見つかるか走査が完了するまで続けます。
|
||||
- バックトラッキングアルゴリズムの検索プロセスには試行と後退が含まれます。深さ優先探索を使用して様々な選択を探索し、選択が制約を満たさない場合、前の選択を取り消します。そして前の状態に戻って他のオプションを試し続けます。試行と後退は反対方向の操作です。
|
||||
- バックトラッキング問題には通常複数の制約が含まれます。これらの制約は剪定操作を実行するために使用できます。剪定は不要な検索分岐を事前に終了し、検索効率を大幅に向上させることができます。
|
||||
- バックトラッキングアルゴリズムは主に検索問題と制約満足問題を解決するために使用されます。組み合わせ最適化問題はバックトラッキングを使用して解決できますが、多くの場合、より効率的または効果的な解決方法が利用可能です。
|
||||
- 順列問題は、与えられた集合の要素のすべての可能な順列を検索することを目的とします。各要素が選択されたかどうかを記録するために配列を使用し、同じ要素の重複選択を避けます。これにより、各要素が一度だけ選択されることが保証されます。
|
||||
- 順列問題では、集合に重複要素が含まれている場合、最終結果に重複順列が含まれます。同一要素が各ラウンドで一度だけ選択できるように制限する必要があり、これは通常ハッシュセットを使用して実装されます。
|
||||
- 部分集合和問題は、与えられた集合でターゲット値に合計する全ての部分集合を見つけることを目的とします。集合は要素の順序を区別しませんが、検索プロセスでは重複する部分集合が生成される可能性があります。これは、アルゴリズムが異なる要素順序を独特のパスとして探索するために発生します。バックトラッキングの前に、データをソートし、各ラウンドの走査の開始点を示す変数を設定します。これにより、重複する部分集合を生成する検索分岐を剪定できます。
|
||||
- 部分集合和問題では、配列内の等しい要素は重複集合を生成する可能性があります。配列がすでにソートされているという前提条件を使用して、隣接する要素が等しいかどうかを判定することで剪定を行います。これにより、等しい要素がラウンドごとに一度だけ選択されることが保証されます。
|
||||
- $n$ クイーン問題は、2つのクイーンが互いに攻撃できないように $n \times n$ のチェスボードに $n$ 個のクイーンを配置する方案を見つけることを目的とします。問題の制約には行制約、列制約、および主対角線と副対角線の制約が含まれます。行制約を満たすために、行ごとに1つのクイーンを配置する戦略を採用し、各行に1つのクイーンが配置されることを保証します。
|
||||
- 列制約と対角線制約の処理は似ています。列制約については、各列にクイーンがあるかどうかを記録する配列を使用し、選択されたセルが合法かどうかを示します。対角線制約については、2つの配列を使用して主対角線と副対角線にそれぞれクイーンの存在を記録します。課題は、同じ主対角線または副対角線上のセルの行と列のインデックス間の関係を決定することです。
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: バックトラッキングと再帰の関係をどのように理解すればよいですか?
|
||||
|
||||
全体的に、バックトラッキングは「アルゴリズム戦略」であり、再帰はより「ツール」です。
|
||||
|
||||
- バックトラッキングアルゴリズムは通常再帰に基づいています。しかし、バックトラッキングは再帰の応用シナリオの一つであり、特に検索問題においてです。
|
||||
- 再帰の構造は「部分問題分解」の問題解決パラダイムを反映します。分割統治、バックトラッキング、動的プログラミング(メモ化再帰)を含む問題の解決でよく使用されます。
|
||||
9
ja/docs/chapter_computational_complexity/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 複雑度解析
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
複雑度解析は、アルゴリズムの広大な宇宙における時空のナビゲーターのようなものです。
|
||||
|
||||
時間と空間の次元をより深く探求し、より優雅な解決策を求めるためのガイドとなります。
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
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}
|
||||
```
|
||||
|
||||
以下の図はこの合計関数を表しています。
|
||||
|
||||

|
||||
|
||||
この合計関数での操作数は入力データのサイズ$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}
|
||||
```
|
||||
|
||||
以下の図はこのネストしたループを表しています。
|
||||
|
||||

|
||||
|
||||
このような場合、関数の操作数は$n^2$に比例します。つまり、アルゴリズムの実行時間と入力データのサイズ$n$には「二次関係」があります。
|
||||
|
||||
さらにネストしたループを追加することで複雑度を高めることができ、各レベルのネストは事実上「次元を増加」させ、時間計算量を「三次」、「四次」などに引き上げます。
|
||||
|
||||
## 再帰
|
||||
|
||||
<u>再帰</u>は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に2つのフェーズが含まれます:
|
||||
|
||||
1. **呼び出し**: プログラムが自分自身を繰り返し呼び出し、しばしばより小さいまたはより単純な引数で、「終了条件」に向かって進みます。
|
||||
2. **返却**: 「終了条件」がトリガーされると、プログラムは最も深い再帰関数から返り始め、各レイヤーの結果を集約します。
|
||||
|
||||
実装の観点から、再帰コードは主に3つの要素を含みます。
|
||||
|
||||
1. **終了条件**: 「呼び出し」から「返却」にいつ切り替えるかを決定します。
|
||||
2. **再帰呼び出し**: 「呼び出し」に対応し、関数が自分自身を呼び出し、通常はより小さいまたはより単純化されたパラメータで行います。
|
||||
3. **結果の返却**: 「返却」に対応し、現在の再帰レベルの結果が前のレイヤーに返されます。
|
||||
|
||||
以下のコードを観察してください。単純に関数`recur(n)`を呼び出すだけで$1 + 2 + \dots + n$の合計を計算できます:
|
||||
|
||||
```src
|
||||
[file]{recursion}-[class]{}-[func]{recur}
|
||||
```
|
||||
|
||||
以下の図はこの関数の再帰プロセスを示しています。
|
||||
|
||||

|
||||
|
||||
反復と再帰は計算の観点から同じ結果を達成できますが、**それらは思考と問題解決の全く異なるパラダイムを表します**。
|
||||
|
||||
- **反復**: 「ボトムアップ」で問題を解決します。最も基本的なステップから始まり、タスクが完了するまでこれらのステップを繰り返し追加または累積します。
|
||||
- **再帰**: 「トップダウン」で問題を解決します。元の問題をより小さなサブ問題に分解し、各サブ問題は元の問題と同じ形式を持ちます。これらのサブ問題は、解が分かっているベースケースで停止するまで、さらに小さなサブ問題に分解されます。
|
||||
|
||||
先ほどの合計関数の例を取ってみましょう。$f(n) = 1 + 2 + \dots + n$として定義されます。
|
||||
|
||||
- **反復**: このアプローチでは、ループ内で合計プロセスをシミュレートします。$1$から始まり$n$まで横断し、各反復で合計操作を実行して最終的に$f(n)$を計算します。
|
||||
- **再帰**: ここでは、問題はサブ問題に分解されます:$f(n) = n + f(n-1)$。この分解は、ベースケースの$f(1) = 1$に到達するまで再帰的に続き、そこで再帰が終了します。
|
||||
|
||||
### 呼び出しスタック
|
||||
|
||||
再帰関数が自分自身を呼び出すたびに、システムは新しく開始された関数にメモリを割り当てて、ローカル変数、戻りアドレス、その他の関連情報を格納します。これは2つの主要な結果をもたらします。
|
||||
|
||||
- 関数のコンテキストデータは「スタックフレーム空間」と呼ばれるメモリ領域に格納され、関数が返された後にのみ解放されます。したがって、**再帰は一般的に反復よりも多くのメモリ空間を消費します**。
|
||||
- 再帰呼び出しは追加のオーバーヘッドを導入します。**したがって、再帰は通常ループよりも時間効率が劣ります。**
|
||||
|
||||
以下の図に示されているように、終了条件がトリガーされる前に$n$個の未返却の再帰関数があり、**再帰の深さが$n$であることを示しています**。
|
||||
|
||||

|
||||
|
||||
実際には、プログラミング言語で許可される再帰の深さは通常制限されており、過度に深い再帰はスタックオーバーフローエラーを引き起こす可能性があります。
|
||||
|
||||
### 末尾再帰
|
||||
|
||||
興味深いことに、**関数が返す直前の最後のステップとして再帰呼び出しを実行する場合**、コンパイラまたはインタープリターによって反復と同じ空間効率になるように最適化できます。このシナリオは<u>末尾再帰</u>として知られています。
|
||||
|
||||
- **通常の再帰**: 標準的な再帰では、関数が前のレベルに戻ったとき、さらにコードを実行し続けるため、システムは前の呼び出しのコンテキストを保存する必要があります。
|
||||
- **末尾再帰**: ここでは、再帰呼び出しは関数が返す前の最終操作です。これは、前のレベルに戻った際に、さらなるアクションが必要ないことを意味するため、システムは前のレベルのコンテキストを保存する必要がありません。
|
||||
|
||||
例えば、$1 + 2 + \dots + n$の計算では、結果変数`res`を関数のパラメータにすることで、末尾再帰を実現できます:
|
||||
|
||||
```src
|
||||
[file]{recursion}-[class]{}-[func]{tail_recur}
|
||||
```
|
||||
|
||||
末尾再帰の実行プロセスは以下の図に示されています。通常の再帰と末尾再帰を比較すると、合計操作のポイントが異なります。
|
||||
|
||||
- **通常の再帰**: 合計操作は「返却」フェーズで発生し、各レイヤーが返った後にもう一度合計が必要です。
|
||||
- **末尾再帰**: 合計操作は「呼び出し」フェーズで発生し、「返却」フェーズは各レイヤーを通じて返すだけです。
|
||||
|
||||

|
||||
|
||||
!!! 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>を作成します。
|
||||
|
||||

|
||||
|
||||
基本的に、再帰は「問題をより小さなサブ問題に分解する」パラダイムを体現しています。この分割統治戦略は重要です。
|
||||
|
||||
- アルゴリズムの観点から、探索、ソート、バックトラッキング、分割統治、動的プログラミングなどの多くの重要な戦略は、直接的または間接的にこの思考方法を使用しています。
|
||||
- データ構造の観点から、再帰は連結リスト、木、グラフを扱うのに自然に適しており、これらは分割統治アプローチを使用した分析に適しているためです。
|
||||
|
||||
## 比較
|
||||
|
||||
上記の内容をまとめると、以下の表は実装、性能、適用性の観点から反復と再帰の違いを示しています。
|
||||
|
||||
<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
|
||||
|
||||
複雑度の概念についてまだ混乱している場合でも、心配しないでください。以降の章で詳しく取り上げます。
|
||||
|
||||
複雑度解析は、アルゴリズムの効率を評価する「ものさし」を提供し、実行に必要な時間と空間リソースを測定し、異なるアルゴリズムの効率を比較することを可能にします。
|
||||
|
||||
複雑度は数学的概念であり、初心者には抽象的で困難かもしれません。この観点から、複雑度解析は最初に紹介するのに最も適したトピックではないかもしれません。しかし、特定のデータ構造やアルゴリズムの特性について議論するとき、その速度と空間使用量を分析することを避けるのは困難です。
|
||||
|
||||
要約すると、データ構造とアルゴリズムに深く入る前に複雑度解析の基本的な理解を身につけることをお勧めします。**これにより、簡単なアルゴリズムで複雑度解析を実行できるようになります**。
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 17 KiB |
803
ja/docs/chapter_computational_complexity/space_complexity.md
Normal file
@@ -0,0 +1,803 @@
|
||||
# 空間計算量
|
||||
|
||||
<u>空間計算量</u>は、データ量が増加するにつれてアルゴリズムが占有するメモリ空間の増加傾向を測定するために使用されます。この概念は時間計算量と非常に似ていますが、「実行時間」が「占有メモリ空間」に置き換えられています。
|
||||
|
||||
## アルゴリズムに関連する空間
|
||||
|
||||
アルゴリズムが実行中に使用するメモリ空間には、主に以下の種類があります。
|
||||
|
||||
- **入力空間**: アルゴリズムの入力データを格納するために使用されます。
|
||||
- **一時空間**: アルゴリズムの実行中に変数、オブジェクト、関数コンテキスト、その他のデータを格納するために使用されます。
|
||||
- **出力空間**: アルゴリズムの出力データを格納するために使用されます。
|
||||
|
||||
一般的に、空間計算量の統計範囲には「一時空間」と「出力空間」の両方が含まれます。
|
||||
|
||||
一時空間はさらに3つの部分に分けることができます。
|
||||
|
||||
- **一時データ**: アルゴリズムの実行中に様々な定数、変数、オブジェクトなどを保存するために使用されます。
|
||||
- **スタックフレーム空間**: 呼び出された関数のコンテキストデータを保存するために使用されます。システムは関数が呼び出されるたびにスタックの頂上にスタックフレームを作成し、関数が返された後にスタックフレーム空間を解放します。
|
||||
- **命令空間**: コンパイル済みプログラム命令を格納するために使用され、実際の統計では通常無視できます。
|
||||
|
||||
プログラムの空間計算量を分析する際、**通常は一時データ、スタックフレーム空間、出力データをカウントします**。以下の図に示されています。
|
||||
|
||||

|
||||
|
||||
関連するコードは以下の通りです:
|
||||
|
||||
=== "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}
|
||||
$$
|
||||
|
||||

|
||||
|
||||
### 定数オーダー $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}
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 二次オーダー $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}
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 指数オーダー $O(2^n)$
|
||||
|
||||
指数オーダーは二分木で一般的です。下図を観察すると、$n$レベルの「完全二分木」は$2^n - 1$個のノードを持ち、$O(2^n)$の空間を占有します:
|
||||
|
||||
```src
|
||||
[file]{space_complexity}-[class]{}-[func]{build_tree}
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 対数オーダー $O(\log n)$
|
||||
|
||||
対数オーダーは分割統治アルゴリズムで一般的です。例えば、マージソートでは、長さ$n$の配列が各ラウンドで再帰的に半分に分割され、高さ$\log n$の再帰木を形成し、$O(\log n)$のスタックフレーム空間を使用します。
|
||||
|
||||
別の例は、数値を文字列に変換することです。正の整数$n$が与えられた場合、その桁数は$\log_{10} n + 1$で、文字列の長さに対応するため、空間計算量は$O(\log_{10} n + 1) = O(\log n)$です。
|
||||
|
||||
## 時間と空間のバランス
|
||||
|
||||
理想的には、時間計算量と空間計算量の両方が最適であることを目指します。しかし、実際には両方を同時に最適化することはしばしば困難です。
|
||||
|
||||
**時間計算量を下げることは通常、空間計算量の増加を代償とし、その逆も同様です**。アルゴリズムの速度を向上させるためにメモリ空間を犠牲にするアプローチは「時空トレードオフ」として知られ、その逆は「空時トレードオフ」として知られています。
|
||||
|
||||
選択は、どちらの側面をより重視するかに依存します。ほとんどの場合、時間は空間よりも貴重であるため、「時空トレードオフ」がより一般的な戦略です。もちろん、大量のデータを扱う際は空間計算量を制御することも非常に重要です。
|
||||
49
ja/docs/chapter_computational_complexity/summary.md
Normal file
@@ -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$の場合、増加傾向が支配的になるため、選択がはるかに容易になります。
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 12 KiB |