This commit is contained in:
krahets
2025-10-17 05:33:23 +08:00
parent 9278f3c659
commit 68bb9afb16
113 changed files with 35936 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
---
comments: true
---
# 16.2   コントリビューション
著者の能力に限りがあるため、本書にはいくつかの省略や誤りが避けられません。ご理解をお願いします。誤字、リンク切れ、内容の欠落、文章の曖昧さ、説明の不明確さ、または不合理な文章構造を発見された場合は、読者により良質な学習リソースを提供するため、修正にご協力ください。
すべての[コントリビューター](https://github.com/krahets/hello-algo/graphs/contributors)のGitHub IDは、本書のリポジトリ、ウェブ、PDFバージョンのホームページに表示され、オープンソースコミュニティへの無私の貢献に感謝いたします。
!!! success "オープンソースの魅力"
紙の本の2つの印刷版の間隔はしばしば長く、内容の更新が非常に不便です。
しかし、このオープンソースの本では、内容の更新サイクルは数日、さらには数時間に短縮されます。
### 1.   内容の微調整
下の図に示すように、各ページの右上角に「編集アイコン」があります。以下の手順に従ってテキストやコードを修正できます。
1. 「編集アイコン」をクリックします。「このリポジトリをフォークしますか」と促された場合は、同意してください。
2. Markdownソースファイルの内容を修正し、内容の正確性を確認し、フォーマットの一貫性を保つようにしてください。
3. ページの下部で修正説明を記入し、「Propose file change」ボタンをクリックします。ページがリダイレクトされた後、「Create pull request」ボタンをクリックしてプルリクエストを開始します。
![ページ編集ボタン](contribution.assets/edit_markdown.png){ class="animation-figure" }
<p align="center"> 図 16-3 &nbsp; ページ編集ボタン </p>
図は直接修正できないため、新しい[Issue](https://github.com/krahets/hello-algo/issues)を作成するか、問題を説明するコメントが必要です。できるだけ早く図を再描画して置き換えます。
### 2. &nbsp; 内容の作成
このオープンソースプロジェクトへの参加に興味がある場合、コードを他のプログラミング言語に翻訳したり、記事の内容を拡張したりすることを含めて、以下のプルリクエストワークフローを実装する必要があります。
1. GitHubにログインし、本書の[コードリポジトリ](https://github.com/krahets/hello-algo)を個人アカウントにフォークします。
2. フォークしたリポジトリのウェブページに移動し、`git clone`コマンドを使用してリポジトリをローカルマシンにクローンします。
3. ローカルで内容を作成し、完全なテストを実行してコードの正確性を検証します。
4. ローカルで行った変更をコミットし、リモートリポジトリにプッシュします。
5. リポジトリのウェブページを更新し、「Create pull request」ボタンをクリックしてプルリクエストを開始します。
### 3. &nbsp; Dockerデプロイメント
`hello-algo`ルートディレクトリで、以下のDockerスクリプトを実行して`http://localhost:8000`でプロジェクトにアクセスします:
```shell
docker-compose up -d
```
以下のコマンドを使用してデプロイメントを削除します:
```shell
docker-compose down
```

View File

@@ -0,0 +1,14 @@
---
comments: true
icon: material/help-circle-outline
---
# 第 16 章 &nbsp; 付録
![付録](../assets/covers/chapter_appendix.jpg){ class="cover-image" }
## 章の内容
- [16.1 &nbsp; プログラミング環境のインストール](installation.md)
- [16.2 &nbsp; 一緒に創作に参加](contribution.md)
- [16.3 &nbsp; 用語集](terminology.md)

View File

@@ -0,0 +1,76 @@
---
comments: true
---
# 16.1 &nbsp; インストール
## 16.1.1 &nbsp; IDEのインストール
ローカルの統合開発環境IDEとして、オープンソースで軽量なVS Codeを使用することをお勧めします。[VS Code公式ウェブサイト](https://code.visualstudio.com/)にアクセスし、お使いのオペレーティングシステムに適したVS Codeのバージョンを選択してダウンロードし、インストールしてください。
![公式ウェブサイトからVS Codeをダウンロード](installation.assets/vscode_installation.png){ class="animation-figure" }
<p align="center"> 図 16-1 &nbsp; 公式ウェブサイトからVS Codeをダウンロード </p>
VS Codeには強力な拡張機能エコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。例えば、「Python Extension Pack」をインストールした後、Pythonコードをデバッグできます。インストール手順を下の図に示します。
![VS Code拡張機能パックのインストール](installation.assets/vscode_extension_installation.png){ class="animation-figure" }
<p align="center"> 図 16-2 &nbsp; VS Code拡張機能パックのインストール </p>
## 16.1.2 &nbsp; 言語環境のインストール
### 1. &nbsp; 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`を入力して、コードフォーマッティングツールをインストールします。
### 2. &nbsp; 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 }`に設定します。
### 3. &nbsp; Java環境
1. [OpenJDK](https://jdk.java.net/18/)をダウンロードしてインストールしますバージョンはJDK 9より新しい必要があります
2. VS Code拡張機能マーケットプレイスで`java`を検索し、Extension Pack for Javaをインストールします。
### 4. &nbsp; 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))。
### 5. &nbsp; Go環境
1. [go](https://go.dev/dl/)をダウンロードしてインストールします。
2. VS Code拡張機能マーケットプレイスで`go`を検索し、Goをインストールします。
3. `Ctrl + Shift + P`を押してコマンドバーを呼び出し、goと入力し、`Go: Install/Update Tools`を選択し、すべてを選択してインストールします。
### 6. &nbsp; 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)をインストールします。
### 7. &nbsp; JavaScript環境
1. [Node.js](https://nodejs.org/en/)をダウンロードしてインストールします。
2. オプションVS Code拡張機能マーケットプレイスで`Prettier`を検索し、コードフォーマッティングツールをインストールします。
### 8. &nbsp; 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)をインストールします。
### 9. &nbsp; Dart環境
1. [Dart](https://dart.dev/get-dart)をダウンロードしてインストールします。
2. VS Code拡張機能マーケットプレイスで`dart`を検索し、[Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code)をインストールします。
### 10. &nbsp; 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)をインストールします。

View File

@@ -0,0 +1,145 @@
---
comments: true
---
# 16.3 &nbsp; 用語集
下の表は本書に登場する重要な用語をリストアップしており、以下の点に注意する価値があります。
- 英語文献を読みやすくするため、用語の英語名を覚えることをお勧めします。
- 一部の用語は簡体字中国語と繁体字中国語で異なる名前を持ちます。
<p align="center"> 表 16-1 &nbsp; データ構造とアルゴリズムの重要用語 </p>
<div class="center-table" markdown>
| 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 | 貪欲アルゴリズム | 贪心算法 | 貪婪演算法 |
</div>

View File

@@ -0,0 +1,850 @@
---
comments: true
---
# 4.1 &nbsp; 配列
<u>配列</u>は線形データ構造で、同じような項目が並んでいるようなもので、コンピュータのメモリ内の連続した空間に一緒に格納されます。これは整理された格納を維持するシーケンスのようなものです。この並びの各項目には、<u>インデックス</u>として知られる独自の「位置」があります。以下の図を参照して、配列の動作を観察し、これらの重要な用語を理解してください。
![配列の定義と格納方法](array.assets/array_definition.png){ class="animation-figure" }
<p align="center"> 図 4-1 &nbsp; 配列の定義と格納方法 </p>
## 4.1.1 &nbsp; 配列の一般的な操作
### 1. &nbsp; 配列の初期化
配列は必要に応じて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 };
```
### 2. &nbsp; 要素へのアクセス
配列内の要素は連続したメモリ空間に格納されるため、各要素のメモリアドレスを計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。
![配列要素のメモリアドレス計算](array.assets/array_memory_location_calculation.png){ class="animation-figure" }
<p align="center"> 図 4-2 &nbsp; 配列要素のメモリアドレス計算 </p>
上の図で観察されるように、配列のインデックスは慣例的に$0$から始まります。これは直感に反するように見えるかもしれません。数を数えるのは通常$1$から始まるためですが、アドレス計算公式内では、**インデックスは本質的にメモリアドレスからのオフセット**です。最初の要素のアドレスでは、このオフセットは$0$で、そのインデックスが$0$であることを検証しています。
配列内の要素へのアクセスは非常に効率的で、$O(1)$時間で任意の要素にランダムアクセスできます。
=== "Python"
```python title="array.py"
def random_access(nums: list[int]) -> int:
"""要素へのランダムアクセス"""
# 区間 [0, len(nums)-1] から数値をランダムに選択
random_index = random.randint(0, len(nums) - 1)
# ランダムな要素を取得して返す
random_num = nums[random_index]
return random_num
```
=== "C++"
```cpp title="array.cpp"
/* 要素への乱数アクセス */
int randomAccess(int *nums, int size) {
// [0, size)の範囲で乱数を選択
int randomIndex = rand() % size;
// 乱数要素を取得して返却
int randomNum = nums[randomIndex];
return randomNum;
}
```
=== "Java"
```java title="array.java"
/* 要素へのランダムアクセス */
int randomAccess(int[] nums) {
// 区間 [0, nums.length) からランダムに数を選択
int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);
// ランダム要素を取得して返す
int randomNum = nums[randomIndex];
return randomNum;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{RandomAccess}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{randomAccess}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{randomAccess}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{randomAccess}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{randomAccess}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{randomAccess}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{random_access}
```
=== "C"
```c title="array.c"
[class]{}-[func]{randomAccess}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{randomAccess}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{random_access}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{randomAccess}
```
### 3. &nbsp; 要素の挿入
配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中央に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。
![配列要素挿入の例](array.assets/array_insert_element.png){ class="animation-figure" }
<p align="center"> 図 4-3 &nbsp; 配列要素挿入の例 </p>
配列の長さが固定されているため、要素を挿入すると必然的に配列の最後の要素が失われることに注意することが重要です。この問題を解決する方法は「リスト」の章で探求されます。
=== "Python"
```python title="array.py"
def insert(nums: list[int], num: int, index: int):
"""インデックス index に要素 num を挿入"""
# インデックス index より後のすべての要素を1つ後ろに移動
for i in range(len(nums) - 1, index, -1):
nums[i] = nums[i - 1]
# num を index の位置の要素に代入
nums[index] = num
```
=== "C++"
```cpp title="array.cpp"
/* `index`に要素numを挿入 */
void insert(int *nums, int size, int num, int index) {
// `index`より後のすべての要素を1つ後ろに移動
for (int i = size - 1; i > index; i--) {
nums[i] = nums[i - 1];
}
// indexの位置にnumを代入
nums[index] = num;
}
```
=== "Java"
```java title="array.java"
/* `index` に要素 num を挿入 */
void insert(int[] nums, int num, int index) {
// `index` より後のすべての要素を1つ後ろに移動
for (int i = nums.length - 1; i > index; i--) {
nums[i] = nums[i - 1];
}
// index の要素に num を代入
nums[index] = num;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Insert}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{insert}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{insert}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{insert}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{insert}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{insert}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{insert}
```
=== "C"
```c title="array.c"
[class]{}-[func]{insert}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{insert}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{insert}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{insert}
```
### 4. &nbsp; 要素の削除
同様に、以下の図に示すように、インデックス$i$の要素を削除するには、インデックス$i$に続くすべての要素を1つずつ前に移動する必要があります。
![配列要素削除の例](array.assets/array_remove_element.png){ class="animation-figure" }
<p align="center"> 図 4-4 &nbsp; 配列要素削除の例 </p>
削除後、元の最後の要素は「意味がない」ものになるため、特定の修正は必要ないことに注意してください。
=== "Python"
```python title="array.py"
def remove(nums: list[int], index: int):
"""インデックス index の要素を削除"""
# インデックス index より後のすべての要素を1つ前に移動
for i in range(index, len(nums) - 1):
nums[i] = nums[i + 1]
```
=== "C++"
```cpp title="array.cpp"
/* `index`の要素を削除 */
void remove(int *nums, int size, int index) {
// `index`より後のすべての要素を1つ前に移動
for (int i = index; i < size - 1; i++) {
nums[i] = nums[i + 1];
}
}
```
=== "Java"
```java title="array.java"
/* `index` の要素を削除 */
void remove(int[] nums, int index) {
// `index` より後のすべての要素を1つ前に移動
for (int i = index; i < nums.length - 1; i++) {
nums[i] = nums[i + 1];
}
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Remove}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{remove}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{remove}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{remove}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{remove}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{remove}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{remove}
```
=== "C"
```c title="array.c"
[class]{}-[func]{removeItem}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{remove}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{remove}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{remove}
```
要約すると、配列の挿入と削除操作には以下の欠点があります:
- **高い時間計算量**:配列の挿入と削除の両方の平均時間計算量は$O(n)$で、ここで$n$は配列の長さです。
- **要素の損失**:配列の長さが固定されているため、挿入時に配列の容量を超える要素は失われます。
- **メモリの無駄**:より長い配列を初期化して前部分のみを利用すると、挿入時に「意味のない」末尾要素が生じ、メモリ空間の無駄につながります。
### 5. &nbsp; 配列の走査
ほとんどのプログラミング言語では、インデックスを使用するか、各要素を直接反復することで配列を走査できます:
=== "Python"
```python title="array.py"
def traverse(nums: list[int]):
"""配列の走査"""
count = 0
# インデックスによる配列の走査
for i in range(len(nums)):
count += nums[i]
# 配列要素の走査
for num in nums:
count += num
# データのインデックスと要素の両方を走査
for i, num in enumerate(nums):
count += nums[i]
count += num
```
=== "C++"
```cpp title="array.cpp"
/* 配列の走査 */
void traverse(int *nums, int size) {
int count = 0;
// インデックスによる配列の走査
for (int i = 0; i < size; i++) {
count += nums[i];
}
}
```
=== "Java"
```java title="array.java"
/* 配列を走査 */
void traverse(int[] nums) {
int count = 0;
// インデックスによる配列の走査
for (int i = 0; i < nums.length; i++) {
count += nums[i];
}
// 配列要素の走査
for (int num : nums) {
count += num;
}
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Traverse}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{traverse}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{traverse}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{traverse}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{traverse}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{traverse}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{traverse}
```
=== "C"
```c title="array.c"
[class]{}-[func]{traverse}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{traverse}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{traverse}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{traverse}
```
### 6. &nbsp; 要素の検索
配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。
配列は線形データ構造であるため、この操作は一般的に「線形探索」と呼ばれます。
=== "Python"
```python title="array.py"
def find(nums: list[int], target: int) -> int:
"""配列内の指定された要素を検索"""
for i in range(len(nums)):
if nums[i] == target:
return i
return -1
```
=== "C++"
```cpp title="array.cpp"
/* 配列内の指定要素を検索 */
int find(int *nums, int size, int target) {
for (int i = 0; i < size; i++) {
if (nums[i] == target)
return i;
}
return -1;
}
```
=== "Java"
```java title="array.java"
/* 配列内で指定された要素を検索 */
int find(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target)
return i;
}
return -1;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Find}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{find}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{find}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{find}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{find}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{find}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{find}
```
=== "C"
```c title="array.c"
[class]{}-[func]{find}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{find}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{find}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{find}
```
### 7. &nbsp; 配列の拡張
複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。
配列を拡張するには、より大きな配列を作成し、元の配列から要素をコピーする必要があります。この操作の時間計算量は$O(n)$で、大きな配列では時間がかかる可能性があります。コードは以下の通りです:
=== "Python"
```python title="array.py"
def extend(nums: list[int], enlarge: int) -> list[int]:
"""配列の長さを拡張"""
# 拡張された長さの配列を初期化
res = [0] * (len(nums) + enlarge)
# 元の配列のすべての要素を新しい配列にコピー
for i in range(len(nums)):
res[i] = nums[i]
# 拡張後の新しい配列を返す
return res
```
=== "C++"
```cpp title="array.cpp"
/* 配列長の拡張 */
int *extend(int *nums, int size, int enlarge) {
// 拡張された長さの配列を初期化
int *res = new int[size + enlarge];
// 元の配列の全要素を新しい配列にコピー
for (int i = 0; i < size; i++) {
res[i] = nums[i];
}
// メモリを解放
delete[] nums;
// 拡張後の新しい配列を返却
return res;
}
```
=== "Java"
```java title="array.java"
/* 配列長の拡張 */
int[] extend(int[] nums, int enlarge) {
// 拡張された長さの配列を初期化
int[] res = new int[nums.length + enlarge];
// 元の配列のすべての要素を新しい配列にコピー
for (int i = 0; i < nums.length; i++) {
res[i] = nums[i];
}
// 拡張後の新しい配列を返す
return res;
}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{Extend}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{extend}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{extend}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{extend}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{extend}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{extend}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{extend}
```
=== "C"
```c title="array.c"
[class]{}-[func]{extend}
```
=== "Kotlin"
```kotlin title="array.kt"
[class]{}-[func]{extend}
```
=== "Ruby"
```ruby title="array.rb"
[class]{}-[func]{extend}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{extend}
```
## 4.1.2 &nbsp; 配列の利点と制限
配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作の効率を最適化するために活用できる実質的な事前情報を提供します。
- **高い空間効率**:配列はデータのための連続したメモリブロックを割り当て、追加の構造的オーバーヘッドの必要性を排除します。
- **ランダムアクセスのサポート**:配列は任意の要素への$O(1)$時間アクセスを可能にします。
- **キャッシュ局所性**:配列要素にアクセスするとき、コンピュータはそれらを読み込むだけでなく、周囲のデータもキャッシュし、高速キャッシュを利用して後続の操作速度を向上させます。
しかし、連続空間格納は諸刃の剣で、以下の制限があります:
- **挿入と削除の効率が低い**:配列に多くの要素が蓄積されると、要素の挿入や削除には大量の要素をシフトする必要があります。
- **固定長**:配列の長さは初期化後に固定されます。配列を拡張するには、すべてのデータを新しい配列にコピーする必要があり、大きなコストがかかります。
- **空間の無駄**:割り当てられた配列サイズが必要以上に大きい場合、余分な空間が無駄になります。
## 4.1.3 &nbsp; 配列の典型的な応用
配列は基本的で広く使用されるデータ構造です。様々なアルゴリズムで頻繁に応用され、複雑なデータ構造の実装に役立ちます。
- **ランダムアクセス**:配列はランダムサンプリングが必要なときのデータ格納に理想的です。インデックスに基づいてランダムシーケンスを生成することで、効率的にランダムサンプリングを実現できます。
- **ソートと検索**:配列はソートと検索アルゴリズムで最も一般的に使用されるデータ構造です。クイックソート、マージソート、二分探索などの技術は主に配列で動作します。
- **ルックアップテーブル**配列は迅速な要素や関係の取得のための効率的なルックアップテーブルとして機能します。例えば、文字をASCIIコードにマッピングすることは、ASCIIコード値をインデックスとして使用し、対応する要素を配列に格納することで簡単になります。
- **機械学習**:ニューラルネットワークの領域では、配列はベクトル、行列、テンソルを含む重要な線形代数演算の実行において重要な役割を果たします。配列はニューラルネットワークプログラミングにおいて主要かつ最も広範囲に使用されるデータ構造として機能します。
- **データ構造の実装**:配列は、スタック、キュー、ハッシュ表、ヒープ、グラフなど、様々なデータ構造を実装するための構成要素として機能します。例えば、グラフの隣接行列表現は本質的に二次元配列です。

View File

@@ -0,0 +1,22 @@
---
comments: true
icon: material/view-list-outline
---
# 第 4 章 &nbsp; 配列と連結リスト
![配列と連結リスト](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" }
!!! abstract
データ構造の世界は頑丈なレンガの壁に似ています。
配列では、レンガがぴったりと整列し、それぞれが次のものと継ぎ目なく隣り合って、統一された形成を作っている姿を想像してください。一方、連結リストでは、これらのレンガが自由に散らばり、それらの間を優雅に編み込む蔦に抱かれています。
## 章の内容
- [4.1 &nbsp; 配列](array.md)
- [4.2 &nbsp; 連結リスト](linked_list.md)
- [4.3 &nbsp; リスト](list.md)
- [4.4 &nbsp; メモリとキャッシュ *](ram_and_cache.md)
- [4.5 &nbsp; まとめ](summary.md)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
---
comments: true
---
# 4.4 &nbsp; メモリとキャッシュ *
この章の最初の2つのセクションでは、「連続格納」と「分散格納」をそれぞれ表現する2つの基本的なデータ構造である配列と連結リストを探究しました。
実際、**物理構造はプログラムがメモリとキャッシュをどの程度効率的に利用するかを大きく決定し**、これがアルゴリズムの全体的なパフォーマンスに影響を与えます。
## 4.4.1 &nbsp; コンピュータ記憶装置
コンピュータには3種類の記憶装置があります<u>ハードディスク</u>、<u>ランダムアクセスメモリRAM</u>、および<u>キャッシュメモリ</u>です。以下の表は、コンピュータシステムにおけるそれぞれの役割とパフォーマンス特性を示しています。
<p align="center"> 表 4-2 &nbsp; コンピュータ記憶装置 </p>
<div class="center-table" markdown>
| | ハードディスク | メモリ | キャッシュ |
| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| 用途 | OS、プログラム、ファイルなどのデータの長期保存 | 現在実行中のプログラムと処理中のデータの一時保存 | 頻繁にアクセスされるデータと命令を保存し、CPUのメモリへのアクセス数を削減 |
| 揮発性 | 電源オフ後もデータは失われない | 電源オフ後にデータは失われる | 電源オフ後にデータは失われる |
| 容量 | より大きい、TBレベル | より小さい、GBレベル | 非常に小さい、MBレベル |
| 速度 | より遅い、数百から数千MB/s | より高速、数十GB/s | 非常に高速、数十から数百GB/s |
| 価格USD | より安価、数セント/GB | より高価、数ドル/GB | 非常に高価、CPUと一緒に価格設定 |
</div>
コンピュータ記憶システムは、下図に示すようにピラミッドとして視覚化できます。ピラミッドの上部にある記憶装置ほど高速で、容量が小さく、より高価です。このマルチレベル設計は偶然ではなく、コンピュータ科学者とエンジニアによる慎重な検討の結果です。
- **ハードディスクをメモリに置き換えるのは困難です**。第一に、メモリ内のデータは電源オフ後に失われるため、長期データ保存には適していません。第二に、メモリはハードディスクよりも大幅に高価で、消費者市場での広範囲な使用の実現可能性を制限しています。
- **キャッシュは大容量と高速のトレードオフに直面しています**。L1、L2、L3キャッシュの容量が増加するにつれて、その物理サイズが大きくなり、CPUコアからの距離が増加します。これによりデータ転送時間が長くなり、アクセス遅延が高くなります。現在の技術では、マルチレベルキャッシュ構造が容量、速度、コストの間の最適なバランスを提供します。
![コンピュータ記憶システム](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" }
<p align="center"> 図 4-9 &nbsp; コンピュータ記憶システム </p>
!!! tip
コンピュータの記憶階層は、速度、容量、コストの間の慎重なバランスを反映しています。このタイプのトレードオフは様々な業界で一般的であり、利益と制限の間の最適なバランスを見つけることが重要です。
全体的に、**ハードディスクは大量のデータの長期保存を提供し、メモリはプログラム実行中に処理されるデータの一時保存として機能し、キャッシュは頻繁にアクセスされるデータと命令を保存して実行効率を向上させます**。それらは一緒になってコンピュータシステムの効率的な動作を保証します。
下図に示すように、プログラム実行中、データはハードディスクからメモリに読み込まれ、CPU計算が行われます。CPUの拡張として機能するキャッシュは、**メモリからインテリジェントにデータを先読み**し、CPUのより高速なデータアクセスを可能にします。これによりプログラム実行効率が大幅に向上し、低速なメモリへの依存が減少します。
![ハードディスク、メモリ、キャッシュ間のデータフロー](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" }
<p align="center"> 図 4-10 &nbsp; ハードディスク、メモリ、キャッシュ間のデータフロー </p>
## 4.4.2 &nbsp; データ構造のメモリ効率
メモリ空間利用の観点から、配列と連結リストにはそれぞれ利点と制限があります。
一方で、**メモリは限られており、複数のプログラム間で共有できない**ため、データ構造での空間使用の最適化は重要です。配列は要素が密接にパックされており、連結リストのように参照(ポインタ)のための追加メモリを必要としないため、空間効率的です。しかし、配列は連続したメモリブロックを事前に割り当てる必要があり、割り当てられた空間が実際の必要量を超える場合、無駄につながる可能性があります。配列の拡張も追加の時間と空間のオーバーヘッドを伴います。対照的に、連結リストは各ノードに対してメモリを動的に割り当て・解放し、ポインタのための追加メモリのコストでより大きな柔軟性を提供します。
一方で、プログラム実行中、**繰り返されるメモリの割り当てと解放はメモリの断片化を増加させ**、メモリ利用効率を低下させます。配列は連続記憶方式により、メモリ断片化を引き起こす可能性が比較的低いです。対照的に、連結リストは要素を非連続の場所に保存し、頻繁な挿入と削除はメモリ断片化を悪化させる可能性があります。
## 4.4.3 &nbsp; データ構造のキャッシュ効率
キャッシュはメモリよりも空間容量がはるかに小さいですが、はるかに高速で、プログラム実行速度において重要な役割を果たします。限られた容量のため、キャッシュは頻繁にアクセスされるデータのサブセットのみを保存できます。CPUがキャッシュに存在しないデータにアクセスしようとすると、<u>キャッシュミス</u>が発生し、CPUは低速なメモリから必要なデータを取得する必要があり、パフォーマンスに影響を与える可能性があります。
明らかに、**キャッシュミスが少ないほど、CPUのデータ読み書き効率が高く**、プログラムパフォーマンスが向上します。CPUがキャッシュからデータを正常に取得する割合は<u>キャッシュヒット率</u>と呼ばれ、キャッシュ効率を測定するためによく使用される指標です。
より高い効率を達成するために、キャッシュは以下のデータロードメカニズムを採用します。
- **キャッシュライン**:キャッシュは個々のバイトではなく、キャッシュラインと呼ばれる単位でデータを保存・ロードして動作します。このアプローチは、一度により大きなデータブロックを転送することで効率を向上させます。
- **先読みメカニズム**:プロセッサはデータアクセスパターン(例:連続または固定ストライドアクセス)を予測し、これらのパターンに基づいてデータをキャッシュに先読みして、キャッシュヒット率を向上させます。
- **空間的局所性**:特定のデータがアクセスされると、近くのデータもまもなくアクセスされる可能性があります。これを活用するために、キャッシュは要求されたデータと一緒に隣接するデータをロードし、ヒット率を向上させます。
- **時間的局所性**:データがアクセスされた場合、近い将来に再びアクセスされる可能性があります。キャッシュはこの原理を使用して、最近アクセスされたデータを保持してヒット率を向上させます。
実際、**配列と連結リストは異なるキャッシュ利用効率を持ち**、これは主に以下の側面に反映されます。
- **占有空間**:連結リスト要素は配列要素よりも多くの空間を占有するため、キャッシュに保持される有効データが少なくなります。
- **キャッシュライン**:連結リストデータはメモリ全体に散在し、キャッシュは「行単位でロード」されるため、ロードされる無効データの割合が高くなります。
- **先読みメカニズム**:配列のデータアクセスパターンは連結リストよりも「予測可能」で、つまりシステムがこれからロードされるデータを推測しやすいです。
- **空間的局所性**:配列は連続したメモリ空間に保存されるため、ロードされているデータの近くのデータがまもなくアクセスされる可能性が高くなります。
全体的に、**配列はより高いキャッシュヒット率を持ち、一般的に連結リストよりも操作効率が高いです**。これにより、配列に基づくデータ構造はアルゴリズム問題の解決において人気があります。
**高いキャッシュ効率が配列が常に連結リストより優れているという意味ではない**ことに注意すべきです。データ構造の選択は特定のアプリケーション要件に依存すべきです。例えば、配列と連結リストの両方が「スタック」データ構造を実装できますが(次章で詳細説明)、それらは異なるシナリオに適しています。
- アルゴリズム問題では、より高い操作効率とランダムアクセス機能を提供するため、配列に基づくスタックを選択する傾向があります。唯一のコストは配列に対して一定量のメモリ空間を事前に割り当てる必要があることです。
- データ量が非常に大きく、高度に動的で、スタックの予想サイズを推定するのが困難な場合、連結リストに基づくスタックがより良い選択です。連結リストは大量のデータをメモリの異なる部分に分散でき、配列拡張の追加オーバーヘッドを回避できます。

View File

@@ -0,0 +1,85 @@
---
comments: true
---
# 4.5 &nbsp; まとめ
### 1. &nbsp; 重要な復習
- 配列と連結リストは2つの基本的なデータ構造であり、コンピュータメモリにおける2つの格納方法を表しています連続空間格納と非連続空間格納です。それらの特性は互いに補完し合います。
- 配列はランダムアクセスをサポートし、使用するメモリが少ない一方で、要素の挿入と削除は非効率的で、初期化後の長さが固定されています。
- 連結リストは参照(ポインタ)の変更によって効率的なノードの挿入と削除を実装し、長さを柔軟に調整できますが、ノードアクセス効率が低く、より多くのメモリを消費します。
- 連結リストの一般的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあり、それぞれに独自の応用シナリオがあります。
- リストは要素の順序付けられたコレクションで、追加、削除、変更をサポートし、通常は動的配列に基づいて実装され、配列の利点を保持しながら柔軟な長さ調整を可能にします。
- リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります。
- プログラム実行中、データは主にメモリに格納されます。配列はより高いメモリ空間効率を提供し、連結リストはメモリ使用においてより柔軟です。
- キャッシュは、キャッシュライン、先読み、空間的局所性、時間的局所性などのメカニズムを通じてCPUに高速データアクセスを提供し、プログラム実行効率を大幅に向上させます。
- より高いキャッシュヒット率により、配列は一般的に連結リストよりも効率的です。データ構造を選択する際は、特定のニーズとシナリオに基づいて適切な選択をすべきです。
### 2. &nbsp; 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**:ノードを削除する際、その後続ノードへの参照を断つ必要がありますか?
データ構造とアルゴリズム(問題解決)の観点から、プログラムのロジックが正しい限り、リンクを断たなくても問題ありません。標準ライブラリの観点から、リンクを断つ方が安全で論理的に明確です。リンクを断たず、削除されたノードが適切にリサイクルされない場合、後続ノードのメモリのリサイクルに影響を与える可能性があります。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
---
comments: true
icon: material/map-marker-path
---
# 第 13 章 &nbsp; バックトラッキング
![バックトラッキング](../assets/covers/chapter_backtracking.jpg){ class="cover-image" }
!!! abstract
迷路の探検家のように、私たちは前進する道で障害に遭遇することがあります。
バックトラッキングの力は、私たちに新しく始めること、試し続けること、そして最終的に光への出口を見つけることを可能にします。
## 章の内容
- [13.1 &nbsp; バックトラッキングアルゴリズム](backtracking_algorithm.md)
- [13.2 &nbsp; 順列問題](permutations_problem.md)
- [13.3 &nbsp; 部分集合和問題](subset_sum_problem.md)
- [13.4 &nbsp; Nクイーン問題](n_queens_problem.md)
- [13.5 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,296 @@
---
comments: true
---
# 13.4 &nbsp; Nクイーン問題
!!! question
チェスのルールによると、クイーンは同じ行、列、または対角線上の駒を攻撃できます。$n$ 個のクイーンと $n \times n$ のチェスボードが与えられた場合、2つのクイーンが互いに攻撃できない配置を見つけてください。
以下の図に示すように、$n = 4$ の場合、2つの解があります。バックトラッキングアルゴリズムの観点から、$n \times n$ のチェスボードには $n^2$ 個のマスがあり、すべての可能な選択肢 `choices` を示しています。チェスボードの状態 `state` は、各クイーンが配置されるにつれて継続的に変化します。
![4クイーン問題の解](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" }
<p align="center"> 図 13-15 &nbsp; 4クイーン問題の解 </p>
以下の図は、この問題の3つの制約を示しています**複数のクイーンは同じ行、列、または対角線を占有できません**。対角線は主対角線 `\` と副対角線 `/` に分かれることに注意することが重要です。
![Nクイーン問題の制約](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" }
<p align="center"> 図 13-16 &nbsp; Nクイーン問題の制約 </p>
### 1. &nbsp; 行ごとの配置戦略
クイーンの数がチェスボードの行数と等しく、どちらも $n$ であるため、**チェスボードの各行には1つのクイーンのみが配置できることが**容易に結論付けられます。
これは、行ごとの配置戦略を採用できることを意味します最初の行から開始して、最後の行に到達するまで行ごとに1つのクイーンを配置します。
以下の図は、4クイーン問題の行ごとの配置プロセスを示しています。スペースの制限により、図は最初の行の1つの検索分岐のみを展開し、列と対角線の制約を満たさない配置を剪定します。
![行ごとの配置戦略](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" }
<p align="center"> 図 13-17 &nbsp; 行ごとの配置戦略 </p>
本質的に、**行ごとの配置戦略は剪定関数として機能し**、同じ行に複数のクイーンを配置するすべての検索分岐を除去します。
### 2. &nbsp; 列と対角線の剪定
列の制約を満たすために、長さ $n$ のブール配列 `cols` を使用して、各列にクイーンが占有されているかどうかを追跡できます。各配置決定の前に、`cols` を使用してすでにクイーンがある列を剪定し、バックトラッキング中に動的に更新されます。
!!! tip
行列の原点は左上隅にあり、行インデックスは上から下に増加し、列インデックスは左から右に増加することに注意してください。
対角線の制約はどうでしょうか?チェスボード上の特定のセルの行と列のインデックスを $(row, col)$ とします。特定の主対角線を選択することで、その対角線上のすべてのセルで差 $row - col$ が同じであることに気付きます。**つまり、$row - col$ は主対角線上で定数値です**。
言い換えると、2つのセルが $row_1 - col_1 = row_2 - col_2$ を満たす場合、それらは確実に同じ主対角線上にあります。このパターンを使用して、以下の図に示す配列 `diags1` を利用して、クイーンが主対角線上にあるかどうかを追跡できます。
同様に、**$row + col$ の和は副対角線上のすべてのセルで定数値です**。配列 `diags2` を使用して副対角線の制約も処理できます。
![列と対角線の制約の処理](n_queens_problem.assets/n_queens_cols_diagonals.png){ class="animation-figure" }
<p align="center"> 図 13-18 &nbsp; 列と対角線の制約の処理 </p>
### 3. &nbsp; コード実装
$n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で、$row + col$ の範囲は $[0, 2n - 2]$ であることに注意してください。したがって、主対角線と副対角線の数はどちらも $2n - 1$ で、配列 `diags1``diags2` の長さは $2n - 1$ です。
=== "Python"
```python title="n_queens.py"
def backtrack(
row: int,
n: int,
state: list[list[str]],
res: list[list[list[str]]],
cols: list[bool],
diags1: list[bool],
diags2: list[bool],
):
"""バックトラッキングアルゴリズムn クイーン"""
# すべての行が配置されたら、解を記録
if row == n:
res.append([list(row) for row in state])
return
# すべての列を走査
for col in range(n):
# セルに対応する主対角線と副対角線を計算
diag1 = row - col + n - 1
diag2 = row + col
# 枝刈り:セルの列、主対角線、副対角線にクイーンを配置しない
if not cols[col] and not diags1[diag1] and not diags2[diag2]:
# 試行:セルにクイーンを配置
state[row][col] = "Q"
cols[col] = diags1[diag1] = diags2[diag2] = True
# 次の行を配置
backtrack(row + 1, n, state, res, cols, diags1, diags2)
# 撤回:セルを空のスポットに復元
state[row][col] = "#"
cols[col] = diags1[diag1] = diags2[diag2] = False
def n_queens(n: int) -> list[list[list[str]]]:
"""n クイーンを解く"""
# n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す
state = [["#" for _ in range(n)] for _ in range(n)]
cols = [False] * n # クイーンがある列を記録
diags1 = [False] * (2 * n - 1) # クイーンがある主対角線を記録
diags2 = [False] * (2 * n - 1) # クイーンがある副対角線を記録
res = []
backtrack(0, n, state, res, cols, diags1, diags2)
return res
```
=== "C++"
```cpp title="n_queens.cpp"
/* バックトラッキングアルゴリズムn クイーン */
void backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,
vector<bool> &diags1, vector<bool> &diags2) {
// すべての行が配置されたら、解を記録
if (row == n) {
res.push_back(state);
return;
}
// すべての列を走査
for (int col = 0; col < n; col++) {
// セルに対応する主対角線と副対角線を計算
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪定:セルの列、主対角線、副対角線にクイーンを配置することを許可しない
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 試行:セルにクイーンを配置
state[row][col] = "Q";
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 次の行を配置
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:セルを空のスポットに復元
state[row][col] = "#";
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* n クイーンを解く */
vector<vector<vector<string>>> nQueens(int n) {
// n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す
vector<vector<string>> state(n, vector<string>(n, "#"));
vector<bool> cols(n, false); // クイーンのある列を記録
vector<bool> diags1(2 * n - 1, false); // クイーンのある主対角線を記録
vector<bool> diags2(2 * n - 1, false); // クイーンのある副対角線を記録
vector<vector<vector<string>>> res;
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Java"
```java title="n_queens.java"
/* バックトラッキングアルゴリズムn クイーン */
void backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,
boolean[] cols, boolean[] diags1, boolean[] diags2) {
// すべての行が配置されたら、解を記録
if (row == n) {
List<List<String>> copyState = new ArrayList<>();
for (List<String> sRow : state) {
copyState.add(new ArrayList<>(sRow));
}
res.add(copyState);
return;
}
// すべての列を走査
for (int col = 0; col < n; col++) {
// セルに対応する主対角線と副対角線を計算
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪定:セルの列、主対角線、副対角線にクイーンを配置することを許可しない
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 試行:セルにクイーンを配置
state.get(row).set(col, "Q");
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 次の行を配置
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:セルを空のスポットに復元
state.get(row).set(col, "#");
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* n クイーンを解く */
List<List<List<String>>> nQueens(int n) {
// n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す
List<List<String>> state = new ArrayList<>();
for (int i = 0; i < n; i++) {
List<String> row = new ArrayList<>();
for (int j = 0; j < n; j++) {
row.add("#");
}
state.add(row);
}
boolean[] cols = new boolean[n]; // クイーンのある列を記録
boolean[] diags1 = new boolean[2 * n - 1]; // クイーンのある主対角線を記録
boolean[] diags2 = new boolean[2 * n - 1]; // クイーンのある副対角線を記録
List<List<List<String>>> res = new ArrayList<>();
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "C#"
```csharp title="n_queens.cs"
[class]{n_queens}-[func]{Backtrack}
[class]{n_queens}-[func]{NQueens}
```
=== "Go"
```go title="n_queens.go"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Swift"
```swift title="n_queens.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "JS"
```javascript title="n_queens.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "TS"
```typescript title="n_queens.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Dart"
```dart title="n_queens.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Rust"
```rust title="n_queens.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{n_queens}
```
=== "C"
```c title="n_queens.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Kotlin"
```kotlin title="n_queens.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Ruby"
```ruby title="n_queens.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{n_queens}
```
=== "Zig"
```zig title="n_queens.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
$n$ 個のクイーンを行ごとに配置し、列の制約を考慮して、最初の行から最後の行まで、$n$、$n-1$、$\dots$、$2$、$1$ の選択肢があり、$O(n!)$ 時間を使用します。解を記録する際、行列 `state` をコピーして `res` に追加する必要があり、コピー操作は $O(n^2)$ 時間を使用します。したがって、**全体の時間計算量は $O(n! \cdot n^2)$ です**。実際には、対角線制約に基づく剪定により検索空間を大幅に削減できるため、多くの場合、検索効率は上記の時間計算量よりも優れています。
配列 `state` は $O(n^2)$ 空間を使用し、配列 `cols`、`diags1`、`diags2` はそれぞれ $O(n)$ 空間を使用します。最大再帰深度は $n$ で、$O(n)$ のスタックフレーム空間を使用します。したがって、**空間計算量は $O(n^2)$ です**。

View File

@@ -0,0 +1,493 @@
---
comments: true
---
# 13.2 &nbsp; 順列問題
順列問題は、バックトラッキングアルゴリズムの典型的な応用です。これは、配列や文字列などの与えられた集合から要素のすべての可能な配置(順列)を見つけることを含みます。
以下の表は、入力配列とその対応する順列を含むいくつかの例を示しています。
<p align="center"> 表 13-2 &nbsp; 順列の例 </p>
<div class="center-table" markdown>
| 入力配列 | 順列 |
| :----------- | :----------------------------------------------------------------- |
| $[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]$ |
</div>
## 13.2.1 &nbsp; 重複要素がない場合
!!! question
重複要素のない整数配列が与えられた場合、すべての可能な順列を返してください。
バックトラッキングの観点から、**順列を生成するプロセスを一連の選択として見ることができます。** 入力配列が $[1, 2, 3]$ だとします。最初に $1$ を選択し、次に $3$、最後に $2$ を選択すると、順列 $[1, 3, 2]$ が得られます。「バックトラッキング」は前の選択を取り消して、代替オプションを探索することを意味します。
コーディングの観点から、候補集合 `choices` は入力配列のすべての要素で構成され、`state` はこれまでに選択された要素を保持します。各要素は一度だけ選択できるため、**`state` のすべての要素は一意である必要があります**。
以下の図に示すように、検索プロセスを再帰木に展開できます。各ノードは現在の `state` を表します。ルートードから開始して、3回の選択の後、葉ードに到達します—それぞれが順列に対応します。
![順列の再帰木](permutations_problem.assets/permutations_i.png){ class="animation-figure" }
<p align="center"> 図 13-5 &nbsp; 順列の再帰木 </p>
### 1. &nbsp; 重複選択の剪定
各要素が一度だけ選択されることを保証するために、ブール配列 `selected` を導入します。ここで `selected[i]``choices[i]` が選択されたかどうかを示します。次に、この配列に基づいて剪定ステップを実行します:
- `choice[i]` を選択した後、`selected[i]` を $\text{True}$ に設定して選択されたとマークします。
- `choices` を反復処理する際、選択されたとマークされたすべての要素をスキップします(つまり、それらの分岐を剪定します)。
以下の図に示すように、最初のラウンドで1を選択し、2番目のラウンドで3を選択し、最後のラウンドで2を選択するとします。2番目のラウンドで要素1の分岐と、3番目のラウンドで要素1と3の分岐を剪定する必要があります。
![順列の剪定例](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" }
<p align="center"> 図 13-6 &nbsp; 順列の剪定例 </p>
図から、この剪定プロセスが検索空間を $O(n^n)$ から $O(n!)$ に削減することがわかります。
### 2. &nbsp; コード実装
この理解により、フレームワークコードの「空欄を埋める」ことができます。全体のコードを簡潔に保つため、フレームワークの各部分を個別に実装せず、代わりに `backtrack()` 関数ですべてを展開します:
=== "Python"
```python title="permutations_i.py"
def backtrack(
state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]
):
"""バックトラッキングアルゴリズム:順列 I"""
# 状態の長さが要素数と等しいとき、解を記録
if len(state) == len(choices):
res.append(list(state))
return
# すべての選択肢を走査
for i, choice in enumerate(choices):
# 枝刈り:要素の重複選択を許可しない
if not selected[i]:
# 試行:選択を行い、状態を更新
selected[i] = True
state.append(choice)
# 次の選択ラウンドに進む
backtrack(state, choices, selected, res)
# 撤回:選択を取り消し、前の状態に復元
selected[i] = False
state.pop()
def permutations_i(nums: list[int]) -> list[list[int]]:
"""順列 I"""
res = []
backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)
return res
```
=== "C++"
```cpp title="permutations_i.cpp"
/* バックトラッキングアルゴリズム:順列 I */
void backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {
// 状態の長さが要素数と等しくなったら、解を記録
if (state.size() == choices.size()) {
res.push_back(state);
return;
}
// すべての選択肢を走査
for (int i = 0; i < choices.size(); i++) {
int choice = choices[i];
// 剪定:要素の重複選択を許可しない
if (!selected[i]) {
// 試行:選択を行い、状態を更新
selected[i] = true;
state.push_back(choice);
// 次のラウンドの選択に進む
backtrack(state, choices, selected, res);
// 回退:選択を取り消し、前の状態に復元
selected[i] = false;
state.pop_back();
}
}
}
/* 順列 I */
vector<vector<int>> permutationsI(vector<int> nums) {
vector<int> state;
vector<bool> selected(nums.size(), false);
vector<vector<int>> res;
backtrack(state, nums, selected, res);
return res;
}
```
=== "Java"
```java title="permutations_i.java"
/* バックトラッキングアルゴリズム:順列 I */
void backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {
// 状態の長さが要素数と等しくなったら、解を記録
if (state.size() == choices.length) {
res.add(new ArrayList<Integer>(state));
return;
}
// すべての選択肢を走査
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
// 剪定:要素の重複選択を許可しない
if (!selected[i]) {
// 試行:選択を行い、状態を更新
selected[i] = true;
state.add(choice);
// 次のラウンドの選択に進む
backtrack(state, choices, selected, res);
// 回退:選択を取り消し、前の状態に復元
selected[i] = false;
state.remove(state.size() - 1);
}
}
}
/* 順列 I */
List<List<Integer>> permutationsI(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);
return res;
}
```
=== "C#"
```csharp title="permutations_i.cs"
[class]{permutations_i}-[func]{Backtrack}
[class]{permutations_i}-[func]{PermutationsI}
```
=== "Go"
```go title="permutations_i.go"
[class]{}-[func]{backtrackI}
[class]{}-[func]{permutationsI}
```
=== "Swift"
```swift title="permutations_i.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "JS"
```javascript title="permutations_i.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "TS"
```typescript title="permutations_i.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Dart"
```dart title="permutations_i.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Rust"
```rust title="permutations_i.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_i}
```
=== "C"
```c title="permutations_i.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Kotlin"
```kotlin title="permutations_i.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Ruby"
```ruby title="permutations_i.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_i}
```
=== "Zig"
```zig title="permutations_i.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
## 13.2.2 &nbsp; 重複要素を考慮する場合
!!! question
**重複要素を含む可能性のある**整数配列が与えられた場合、すべての一意の順列を返してください。
入力配列が $[1, 1, 2]$ だとします。2つの同一要素 $1$ を区別するために、2番目を $\hat{1}$ とラベル付けします。
以下の図に示すように、この方法で生成される順列の半分は重複です:
![重複順列](permutations_problem.assets/permutations_ii.png){ class="animation-figure" }
<p align="center"> 図 13-7 &nbsp; 重複順列 </p>
では、これらの重複順列をどのように除去できるでしょうか?一つの直接的なアプローチは、すべての順列を生成した後にハッシュセットを使用して重複を除去することです。しかし、これはあまり優雅ではありません。**重複を生成する分岐は本来不要であり、事前に剪定されるべきだからです**、これによりアルゴリズムの効率が向上します。
### 1. &nbsp; 等値要素の剪定
以下の図を見ると、最初のラウンドで $1$ または $\hat{1}$ を選択すると同じ順列につながるため、$\hat{1}$ を剪定します。
同様に、最初のラウンドで $2$ を選択した後、2番目のラウンドで $1$ または $\hat{1}$ を選択しても重複分岐につながるため、その時も $\hat{1}$ を剪定します。
本質的に、**私たちの目標は、複数の同一要素が選択の各ラウンドで一度だけ選択されることを保証することです。**
![重複順列の剪定](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" }
<p align="center"> 図 13-8 &nbsp; 重複順列の剪定 </p>
### 2. &nbsp; コード実装
前の問題のコードに基づいて、各ラウンドでハッシュセット `duplicated` を導入します。このセットは、すでに試行した要素を追跡し、重複を剪定できるようにします:
=== "Python"
```python title="permutations_ii.py"
def backtrack(
state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]
):
"""バックトラッキングアルゴリズム:順列 II"""
# 状態の長さが要素数と等しいとき、解を記録
if len(state) == len(choices):
res.append(list(state))
return
# すべての選択肢を走査
duplicated = set[int]()
for i, choice in enumerate(choices):
# 枝刈り:要素の重複選択を許可せず、等しい要素の重複選択も許可しない
if not selected[i] and choice not in duplicated:
# 試行:選択を行い、状態を更新
duplicated.add(choice) # 選択された要素値を記録
selected[i] = True
state.append(choice)
# 次の選択ラウンドに進む
backtrack(state, choices, selected, res)
# 撤回:選択を取り消し、前の状態に復元
selected[i] = False
state.pop()
def permutations_ii(nums: list[int]) -> list[list[int]]:
"""順列 II"""
res = []
backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)
return res
```
=== "C++"
```cpp title="permutations_ii.cpp"
/* バックトラッキングアルゴリズム:順列 II */
void backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {
// 状態の長さが要素数と等しくなったら、解を記録
if (state.size() == choices.size()) {
res.push_back(state);
return;
}
// すべての選択肢を走査
unordered_set<int> duplicated;
for (int i = 0; i < choices.size(); i++) {
int choice = choices[i];
// 剪定:要素の重複選択を許可せず、等しい要素の重複選択も許可しない
if (!selected[i] && duplicated.find(choice) == duplicated.end()) {
// 試行:選択を行い、状態を更新
duplicated.emplace(choice); // 選択された要素値を記録
selected[i] = true;
state.push_back(choice);
// 次のラウンドの選択に進む
backtrack(state, choices, selected, res);
// 回退:選択を取り消し、前の状態に復元
selected[i] = false;
state.pop_back();
}
}
}
/* 順列 II */
vector<vector<int>> permutationsII(vector<int> nums) {
vector<int> state;
vector<bool> selected(nums.size(), false);
vector<vector<int>> res;
backtrack(state, nums, selected, res);
return res;
}
```
=== "Java"
```java title="permutations_ii.java"
/* バックトラッキングアルゴリズム:順列 II */
void backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {
// 状態の長さが要素数と等しくなったら、解を記録
if (state.size() == choices.length) {
res.add(new ArrayList<Integer>(state));
return;
}
// すべての選択肢を走査
Set<Integer> duplicated = new HashSet<Integer>();
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
// 剪定:要素の重複選択を許可せず、等しい要素の重複選択も許可しない
if (!selected[i] && !duplicated.contains(choice)) {
// 試行:選択を行い、状態を更新
duplicated.add(choice); // 選択された要素値を記録
selected[i] = true;
state.add(choice);
// 次のラウンドの選択に進む
backtrack(state, choices, selected, res);
// 回退:選択を取り消し、前の状態に復元
selected[i] = false;
state.remove(state.size() - 1);
}
}
}
/* 順列 II */
List<List<Integer>> permutationsII(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);
return res;
}
```
=== "C#"
```csharp title="permutations_ii.cs"
[class]{permutations_ii}-[func]{Backtrack}
[class]{permutations_ii}-[func]{PermutationsII}
```
=== "Go"
```go title="permutations_ii.go"
[class]{}-[func]{backtrackII}
[class]{}-[func]{permutationsII}
```
=== "Swift"
```swift title="permutations_ii.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "JS"
```javascript title="permutations_ii.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "TS"
```typescript title="permutations_ii.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Dart"
```dart title="permutations_ii.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Rust"
```rust title="permutations_ii.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_ii}
```
=== "C"
```c title="permutations_ii.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Kotlin"
```kotlin title="permutations_ii.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Ruby"
```ruby title="permutations_ii.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_ii}
```
=== "Zig"
```zig title="permutations_ii.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
すべての要素が異なると仮定すると、$n$ 個の要素の順列は $n!$ (階乗)個あります。各結果を記録するには長さ $n$ のリストをコピーする必要があり、これには $O(n)$ 時間がかかります。**したがって、総時間計算量は $O(n!n)$ です。**
最大再帰深度は $n$ で、$O(n)$ のスタック空間を使用します。`selected` 配列も $O(n)$ 空間が必要です。一度に最大 $n$ 個の個別の `duplicated` セットが存在する可能性があるため、それらは集合的に $O(n^2)$ 空間を占有します。**したがって、空間計算量は $O(n^2)$ です。**
### 3. &nbsp; 2つの剪定方法の比較
`selected` と `duplicated` はどちらも剪定メカニズムとして機能しますが、異なる問題をターゲットにしています:
- **重複選択の剪定**`selected` 経由):検索全体に単一の `selected` 配列があり、現在の状態にすでにある要素を示します。これにより、同じ要素が `state` に複数回現れることを防ぎます。
- **等値要素の剪定**`duplicated` 経由):`backtrack` 関数の各呼び出しは独自の `duplicated` セットを使用し、その特定の反復(`for` ループ)ですでに選択された要素を記録します。これにより、等しい要素が選択の各ラウンドで一度だけ選択されることを保証します。
以下の図は、これら2つの剪定戦略の範囲を示しています。木の各ードは選択を表します。ルートから任意の葉への経路は、1つの完全な順列に対応します。
![2つの剪定条件の範囲](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" }
<p align="center"> 図 13-9 &nbsp; 2つの剪定条件の範囲 </p>

View File

@@ -0,0 +1,703 @@
---
comments: true
---
# 13.3 &nbsp; 部分集合和問題
## 13.3.1 &nbsp; 重複要素がない場合
!!! question
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。与えられた配列には重複要素がなく、各要素は複数回選択できます。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
例えば、入力集合 $\{3, 4, 5\}$ とターゲット整数 $9$ の場合、解は $\{3, 3, 3\}, \{4, 5\}$ です。以下の2点に注意してください。
- 入力集合の要素は無制限に選択できます。
- 部分集合は要素の順序を区別しません。例えば $\{4, 5\}$ と $\{5, 4\}$ は同じ部分集合です。
### 1. &nbsp; 順列解法の参考
順列問題と同様に、部分集合の生成を一連の選択として想像でき、選択プロセス中に「要素和」をリアルタイムで更新できます。要素和が `target` に等しくなったとき、部分集合を結果リストに記録します。
順列問題とは異なり、**この問題では要素は無制限に選択できるため**、要素が選択されたかどうかを記録するための `selected` ブール配列を使用する必要がありません。順列コードに軽微な修正を加えて、最初に問題を解決できます:
=== "Python"
```python title="subset_sum_i_naive.py"
def backtrack(
state: list[int],
target: int,
total: int,
choices: list[int],
res: list[list[int]],
):
"""バックトラッキングアルゴリズム:部分集合の和 I"""
# 部分集合の和が target と等しいとき、解を記録
if total == target:
res.append(list(state))
return
# すべての選択肢を走査
for i in range(len(choices)):
# 枝刈り:部分集合の和が target を超える場合、その選択をスキップ
if total + choices[i] > target:
continue
# 試行:選択を行い、要素と total を更新
state.append(choices[i])
# 次の選択ラウンドに進む
backtrack(state, target, total + choices[i], choices, res)
# 撤回:選択を取り消し、前の状態に復元
state.pop()
def subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:
"""部分集合の和 I を解く(重複する部分集合を含む)"""
state = [] # 状態(部分集合)
total = 0 # 部分集合の和
res = [] # 結果リスト(部分集合リスト)
backtrack(state, target, total, nums, res)
return res
```
=== "C++"
```cpp title="subset_sum_i_naive.cpp"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (total == target) {
res.push_back(state);
return;
}
// すべての選択肢を走査
for (int i = 0; i < choices.size(); i++) {
// 剪定部分集合の和がtargetを超えた場合、その選択をスキップ
if (total + choices[i] > target) {
continue;
}
// 試行選択を行い、要素とtotalを更新
state.push_back(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target, total + choices[i], choices, res);
// 回退:選択を取り消し、前の状態に復元
state.pop_back();
}
}
/* 部分集合和 I を解く(重複する部分集合を含む) */
vector<vector<int>> subsetSumINaive(vector<int> nums, int target) {
vector<int> state; // 状態(部分集合)
int total = 0; // 部分集合の和
vector<vector<int>> res; // 結果リスト(部分集合リスト)
backtrack(state, target, total, nums, res);
return res;
}
```
=== "Java"
```java title="subset_sum_i_naive.java"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (total == target) {
res.add(new ArrayList<>(state));
return;
}
// すべての選択肢を走査
for (int i = 0; i < choices.length; i++) {
// 剪定部分集合の和がtargetを超えた場合、その選択をスキップ
if (total + choices[i] > target) {
continue;
}
// 試行選択を行い、要素とtotalを更新
state.add(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target, total + choices[i], choices, res);
// 回退:選択を取り消し、前の状態に復元
state.remove(state.size() - 1);
}
}
/* 部分集合和 I を解く(重複する部分集合を含む) */
List<List<Integer>> subsetSumINaive(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状態(部分集合)
int total = 0; // 部分集合の和
List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)
backtrack(state, target, total, nums, res);
return res;
}
```
=== "C#"
```csharp title="subset_sum_i_naive.cs"
[class]{subset_sum_i_naive}-[func]{Backtrack}
[class]{subset_sum_i_naive}-[func]{SubsetSumINaive}
```
=== "Go"
```go title="subset_sum_i_naive.go"
[class]{}-[func]{backtrackSubsetSumINaive}
[class]{}-[func]{subsetSumINaive}
```
=== "Swift"
```swift title="subset_sum_i_naive.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "JS"
```javascript title="subset_sum_i_naive.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "TS"
```typescript title="subset_sum_i_naive.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Dart"
```dart title="subset_sum_i_naive.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Rust"
```rust title="subset_sum_i_naive.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i_naive}
```
=== "C"
```c title="subset_sum_i_naive.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Kotlin"
```kotlin title="subset_sum_i_naive.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Ruby"
```ruby title="subset_sum_i_naive.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i_naive}
```
=== "Zig"
```zig title="subset_sum_i_naive.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力すると、結果 $[3, 3, 3], [4, 5], [5, 4]$ が得られます。**和が $9$ のすべての部分集合を正常に見つけましたが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。
これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、$5$ の前に $4$ を選択することと $4$ の前に $5$ を選択することは異なる分岐ですが、同じ部分集合に対応します。
![部分集合の検索と境界外の剪定](subset_sum_problem.assets/subset_sum_i_naive.png){ class="animation-figure" }
<p align="center"> 図 13-10 &nbsp; 部分集合の検索と境界外の剪定 </p>
重複する部分集合を除去するために、**直接的なアイデアは結果リストを重複除去することです**。しかし、この方法は2つの理由で非常に非効率的です。
- 配列要素が多い場合、特に `target` が大きい場合、検索プロセスで大量の重複する部分集合が生成されます。
- 部分集合(配列)の差異を比較することは非常に時間がかかり、まず配列をソートし、次に配列の各要素の差異を比較する必要があります。
### 2. &nbsp; 重複部分集合の剪定
**剪定を通じて検索プロセス中に重複除去を検討します**。以下の図を観察すると、異なる順序で配列要素を選択するときに重複する部分集合が生成されます。例えば、以下の状況です。
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.` で記述された部分集合と完全に重複するからです。
![異なる選択順序による重複部分集合](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" }
<p align="center"> 図 13-11 &nbsp; 異なる選択順序による重複部分集合 </p>
要約すると、入力配列 $[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$ を満たす必要があります。**この条件を満たさない選択シーケンスは重複を引き起こし、剪定されるべきです**。
### 3. &nbsp; コード実装
この剪定を実装するために、変数 `start` を初期化し、これは走査の開始点を示します。**選択 $x_{i}$ を行った後、次のラウンドをインデックス $i$ から開始するように設定します**。これにより、選択シーケンスが $i_1 \leq i_2 \leq \dots \leq i_m$ を満たすことが保証され、部分集合の一意性が保証されます。
さらに、コードに以下の2つの最適化を行いました。
- 検索を開始する前に、配列 `nums` をソートします。すべての選択の走査で、**部分集合和が `target` を超えたときにループを直接終了します**。後続の要素はより大きく、それらの部分集合和は確実に `target` を超えるからです。
- 要素和変数 `total` を除去し、**`target` に対して減算を実行して要素和をカウントします**。`target` が $0$ に等しくなったとき、解を記録します。
=== "Python"
```python title="subset_sum_i.py"
def backtrack(
state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]
):
"""バックトラッキングアルゴリズム:部分集合の和 I"""
# 部分集合の和が target と等しいとき、解を記録
if target == 0:
res.append(list(state))
return
# すべての選択肢を走査
# 枝刈り二start から走査を開始して重複する部分集合の生成を避ける
for i in range(start, len(choices)):
# 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了
# これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため
if target - choices[i] < 0:
break
# 試行選択を行い、target、start を更新
state.append(choices[i])
# 次の選択ラウンドに進む
backtrack(state, target - choices[i], choices, i, res)
# 撤回:選択を取り消し、前の状態に復元
state.pop()
def subset_sum_i(nums: list[int], target: int) -> list[list[int]]:
"""部分集合の和 I を解く"""
state = [] # 状態(部分集合)
nums.sort() # nums をソート
start = 0 # 走査の開始点
res = [] # 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res)
return res
```
=== "C++"
```cpp title="subset_sum_i.cpp"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.push_back(state);
return;
}
// すべての選択肢を走査
// 剪定二startから走査を開始し、重複する部分集合の生成を回避
for (int i = start; i < choices.size(); i++) {
// 剪定一部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 試行選択を行い、target、startを更新
state.push_back(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i, res);
// 回退:選択を取り消し、前の状態に復元
state.pop_back();
}
}
/* 部分集合和 I を解く */
vector<vector<int>> subsetSumI(vector<int> nums, int target) {
vector<int> state; // 状態(部分集合)
sort(nums.begin(), nums.end()); // nums をソート
int start = 0; // 走査の開始点
vector<vector<int>> res; // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "Java"
```java title="subset_sum_i.java"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.add(new ArrayList<>(state));
return;
}
// すべての選択肢を走査
// 剪定二startから走査を開始し、重複する部分集合の生成を回避
for (int i = start; i < choices.length; i++) {
// 剪定一部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 試行選択を行い、target、startを更新
state.add(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i, res);
// 回退:選択を取り消し、前の状態に復元
state.remove(state.size() - 1);
}
}
/* 部分集合和 I を解く */
List<List<Integer>> subsetSumI(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状態(部分集合)
Arrays.sort(nums); // nums をソート
int start = 0; // 走査の開始点
List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "C#"
```csharp title="subset_sum_i.cs"
[class]{subset_sum_i}-[func]{Backtrack}
[class]{subset_sum_i}-[func]{SubsetSumI}
```
=== "Go"
```go title="subset_sum_i.go"
[class]{}-[func]{backtrackSubsetSumI}
[class]{}-[func]{subsetSumI}
```
=== "Swift"
```swift title="subset_sum_i.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "JS"
```javascript title="subset_sum_i.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "TS"
```typescript title="subset_sum_i.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Dart"
```dart title="subset_sum_i.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Rust"
```rust title="subset_sum_i.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i}
```
=== "C"
```c title="subset_sum_i.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Kotlin"
```kotlin title="subset_sum_i.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Ruby"
```ruby title="subset_sum_i.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i}
```
=== "Zig"
```zig title="subset_sum_i.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
以下の図は、配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。
![部分集合和 I のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" }
<p align="center"> 図 13-12 &nbsp; 部分集合和 I のバックトラッキングプロセス </p>
## 13.3.2 &nbsp; 重複要素がある場合を考慮
!!! question
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。**与えられた配列には重複要素が含まれる可能性があり、各要素は一度だけ選択できます**。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
前の問題と比較して、**この問題の入力配列には重複要素が含まれる可能性があり**、新しい問題が導入されます。例えば、配列 $[4, \hat{4}, 5]$ とターゲット要素 $9$ が与えられた場合、既存のコードの出力結果は $[4, 5], [\hat{4}, 5]$ となり、重複する部分集合が生成されます。
**この重複の理由は、特定のラウンドで等しい要素が複数回選択されることです**。以下の図では、最初のラウンドに3つの選択肢があり、そのうち2つが $4$ であり、2つの重複する検索分岐を生成し、重複する部分集合を出力します。同様に、2番目のラウンドの2つの $4$ も重複する部分集合を生成します。
![等しい要素による重複部分集合](subset_sum_problem.assets/subset_sum_ii_repeat.png){ class="animation-figure" }
<p align="center"> 図 13-13 &nbsp; 等しい要素による重複部分集合 </p>
### 1. &nbsp; 等値要素の剪定
この問題を解決するために、**等しい要素がラウンドごとに一度だけ選択されるように制限する必要があります**。実装は非常に巧妙です:配列がソートされているため、等しい要素は隣接しています。これは、特定のラウンドの選択で、現在の要素がその左側の要素と等しい場合、それはすでに選択されていることを意味するため、現在の要素を直接スキップします。
同時に、**この問題では各配列要素は一度だけ選択できると規定されています**。幸い、変数 `start` を使用してこの制約も満たすことができます:選択 $x_{i}$ を行った後、次のラウンドをインデックス $i + 1$ から前方に開始するように設定します。これにより、重複する部分集合が除去されるだけでなく、要素の重複選択も回避されます。
### 2. &nbsp; コード実装
=== "Python"
```python title="subset_sum_ii.py"
def backtrack(
state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]
):
"""バックトラッキングアルゴリズム:部分集合の和 II"""
# 部分集合の和が target と等しいとき、解を記録
if target == 0:
res.append(list(state))
return
# すべての選択肢を走査
# 枝刈り二start から走査を開始して重複する部分集合の生成を避ける
# 枝刈り三start から走査を開始して同じ要素の重複選択を避ける
for i in range(start, len(choices)):
# 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了
# これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため
if target - choices[i] < 0:
break
# 枝刈り四:要素が左の要素と等しい場合、検索分岐が重複していることを示すため、スキップ
if i > start and choices[i] == choices[i - 1]:
continue
# 試行選択を行い、target、start を更新
state.append(choices[i])
# 次の選択ラウンドに進む
backtrack(state, target - choices[i], choices, i + 1, res)
# 撤回:選択を取り消し、前の状態に復元
state.pop()
def subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:
"""部分集合の和 II を解く"""
state = [] # 状態(部分集合)
nums.sort() # nums をソート
start = 0 # 走査の開始点
res = [] # 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res)
return res
```
=== "C++"
```cpp title="subset_sum_ii.cpp"
/* バックトラッキングアルゴリズム:部分集合和 II */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.push_back(state);
return;
}
// すべての選択肢を走査
// 剪定二startから走査を開始し、重複する部分集合の生成を回避
// 剪定三startから走査を開始し、同じ要素の繰り返し選択を回避
for (int i = start; i < choices.size(); i++) {
// 剪定一部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 試行選択を行い、target、startを更新
state.push_back(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:選択を取り消し、前の状態に復元
state.pop_back();
}
}
/* 部分集合和 II を解く */
vector<vector<int>> subsetSumII(vector<int> nums, int target) {
vector<int> state; // 状態(部分集合)
sort(nums.begin(), nums.end()); // nums をソート
int start = 0; // 走査の開始点
vector<vector<int>> res; // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "Java"
```java title="subset_sum_ii.java"
/* バックトラッキングアルゴリズム:部分集合和 II */
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.add(new ArrayList<>(state));
return;
}
// すべての選択肢を走査
// 剪定二startから走査を開始し、重複する部分集合の生成を回避
// 剪定三startから走査を開始し、同じ要素の繰り返し選択を回避
for (int i = start; i < choices.length; i++) {
// 剪定一部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 試行選択を行い、target、startを更新
state.add(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:選択を取り消し、前の状態に復元
state.remove(state.size() - 1);
}
}
/* 部分集合和 II を解く */
List<List<Integer>> subsetSumII(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状態(部分集合)
Arrays.sort(nums); // nums をソート
int start = 0; // 走査の開始点
List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "C#"
```csharp title="subset_sum_ii.cs"
[class]{subset_sum_ii}-[func]{Backtrack}
[class]{subset_sum_ii}-[func]{SubsetSumII}
```
=== "Go"
```go title="subset_sum_ii.go"
[class]{}-[func]{backtrackSubsetSumII}
[class]{}-[func]{subsetSumII}
```
=== "Swift"
```swift title="subset_sum_ii.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "JS"
```javascript title="subset_sum_ii.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "TS"
```typescript title="subset_sum_ii.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Dart"
```dart title="subset_sum_ii.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Rust"
```rust title="subset_sum_ii.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_ii}
```
=== "C"
```c title="subset_sum_ii.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Kotlin"
```kotlin title="subset_sum_ii.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Ruby"
```ruby title="subset_sum_ii.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_ii}
```
=== "Zig"
```zig title="subset_sum_ii.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
以下の図は、配列 $[4, 4, 5]$ とターゲット要素 $9$ のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。
![部分集合和 II のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" }
<p align="center"> 図 13-14 &nbsp; 部分集合和 II のバックトラッキングプロセス </p>

View File

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

View File

@@ -0,0 +1,22 @@
---
comments: true
icon: material/timer-sand
---
# 第 2 章 &nbsp; 複雑度解析
![Complexity analysis](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" }
!!! abstract
複雑度解析は、アルゴリズムの広大な宇宙における時空のナビゲーターのようなものです。
時間と空間の次元をより深く探求し、より優雅な解決策を求めるためのガイドとなります。
## 章の内容
- [2.1 &nbsp; アルゴリズム効率評価](performance_evaluation.md)
- [2.2 &nbsp; 反復と再帰](iteration_and_recursion.md)
- [2.3 &nbsp; 時間計算量](time_complexity.md)
- [2.4 &nbsp; 空間計算量](space_complexity.md)
- [2.5 &nbsp; まとめ](summary.md)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
---
comments: true
---
# 2.1 &nbsp; アルゴリズムの効率評価
アルゴリズム設計において、私たちは順序に従って以下の2つの目標を追求します。
1. **問題の解決策を見つける**: アルゴリズムは、指定された入力範囲内で確実に正しい解を見つけることができるべきです。
2. **最適解を求める**: 同じ問題に対して複数の解決策が存在する場合があり、私たちは可能な限り最も効率的なアルゴリズムを見つけることを目指します。
つまり、問題を解決できることを前提として、アルゴリズムの効率がアルゴリズムを評価する主要な基準となっており、これには以下の2つの次元が含まれます。
- **時間効率**: アルゴリズムが実行される速度。
- **空間効率**: アルゴリズムが占有するメモリ空間のサイズ。
要するに、**私たちの目標は、高速でメモリ効率の良いデータ構造とアルゴリズムを設計することです**。アルゴリズムの効率を効果的に評価することは重要です。なぜなら、そうすることで初めて様々なアルゴリズムを比較し、アルゴリズムの設計と最適化プロセスを導くことができるからです。
効率評価には主に2つの方法があります実際のテストと理論的推定です。
## 2.1.1 &nbsp; 実際のテスト
アルゴリズム`A``B`があり、どちらも同じ問題を解決でき、それらの効率を比較する必要があるとします。最も直接的な方法は、コンピュータを使用してこれら2つのアルゴリズムを実行し、実行時間とメモリ使用量を監視・記録することです。この評価方法は実際の状況を反映しますが、大きな制限があります。
一方で、**テスト環境からの干渉を排除することは困難です**。ハードウェア構成はアルゴリズムの性能に影響を与える可能性があります。例えば、並列度の高いアルゴリズムはマルチコアCPUでの実行により適していますし、集約的なメモリ操作を含むアルゴリズムは高性能メモリでより良い性能を発揮します。アルゴリズムのテスト結果は、異なるマシン間で変わる可能性があります。これは、平均効率を計算するために複数のマシンでテストすることが実用的でないことを意味します。
一方で、**完全なテストを実施することは非常にリソース集約的です**。アルゴリズムの効率は入力データサイズによって変わります。例えば、データ量が少ない場合はアルゴリズム`A``B`より速く実行される可能性がありますが、データ量が多い場合はテスト結果が逆になる可能性があります。したがって、説得力のある結論を導くためには、幅広い入力データサイズをテストする必要があり、これには過度な計算リソースが必要になります。
## 2.1.2 &nbsp; 理論的推定
実際のテストの大きな制限により、計算のみでアルゴリズムの効率を評価することを検討できます。この推定方法は<u>漸近的複雑度解析</u>、または単に<u>複雑度解析</u>として知られています。
複雑度解析は、アルゴリズムの実行に必要な時間と空間リソースと入力データのサイズとの関係を反映します。**これは、入力データのサイズが増加するにつれて、アルゴリズムに必要な時間と空間の増加傾向を記述します**。この定義は複雑に聞こえるかもしれませんが、より良く理解するために3つの重要なポイントに分解できます。
- 「時間と空間リソース」は、それぞれ<u>時間計算量</u>と<u>空間計算量</u>に対応します。
- 「入力データのサイズが増加するにつれて」は、複雑度がアルゴリズムの効率と入力データ量との関係を反映することを意味します。
- 「時間と空間の増加傾向」は、複雑度解析が実行時間や占有空間の具体的な値ではなく、時間や空間が増加する「率」に焦点を当てることを示します。
**複雑度解析は実際のテスト方法の欠点を克服します**。これは以下の側面で反映されます:
- 実際にコードを実行する必要がないため、より環境に優しく、エネルギー効率が良いです。
- テスト環境に依存せず、すべての動作プラットフォームに適用できます。
- 異なるデータ量でのアルゴリズムの効率を反映でき、特に大量データでのアルゴリズムの性能を示します。
!!! tip
複雑度の概念についてまだ混乱している場合でも、心配しないでください。以降の章で詳しく取り上げます。
複雑度解析は、アルゴリズムの効率を評価する「ものさし」を提供し、実行に必要な時間と空間リソースを測定し、異なるアルゴリズムの効率を比較することを可能にします。
複雑度は数学的概念であり、初心者には抽象的で困難かもしれません。この観点から、複雑度解析は最初に紹介するのに最も適したトピックではないかもしれません。しかし、特定のデータ構造やアルゴリズムの特性について議論するとき、その速度と空間使用量を分析することを避けるのは困難です。
要約すると、データ構造とアルゴリズムに深く入る前に複雑度解析の基本的な理解を身につけることをお勧めします。**これにより、簡単なアルゴリズムで複雑度解析を実行できるようになります**。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
---
comments: true
---
# 2.5 &nbsp; まとめ
### 1. &nbsp; 重要なレビュー
**アルゴリズム効率評価**
- 時間効率と空間効率は、アルゴリズムの優劣を評価する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)$などが含まれます。
### 2. &nbsp; 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$の場合、増加傾向が支配的になるため、選択がはるかに容易になります。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
---
comments: true
---
# 3.2 &nbsp; 基本データ型
コンピュータ内のデータについて考える際、テキスト、画像、動画、音声、3Dモデルなど、様々な形式が思い浮かびます。これらの組織的な形式は異なりますが、すべて様々な基本データ型から構成されています。
**基本データ型とは、CPUが直接操作できるもの**であり、アルゴリズムで直接使用されます。主に以下が含まれます。
- 整数型:`byte``short``int``long`
- 浮動小数点型:`float``double`、小数を表現するために使用
- 文字型:`char`、様々な言語の文字、句読点、さらには絵文字を表現するために使用
- ブール型:`bool`、「はい」または「いいえ」の判断を表現するために使用
**基本データ型は、コンピュータ内で二進形式で格納されます**。1つの二進桁は1ビットです。ほとんどの現代的なオペレーティングシステムでは、1バイトは8ビットで構成されています。
基本データ型の値の範囲は、それらが占める空間のサイズに依存します。以下では、Javaを例に説明します。
- 整数型`byte`は1バイト = 8ビットを占め、$2^8$個の数値を表現できます。
- 整数型`int`は4バイト = 32ビットを占め、$2^{32}$個の数値を表現できます。
以下の表は、Javaにおける様々な基本データ型が占める空間、値の範囲、デフォルト値を示しています。この表を暗記する必要はありませんが、一般的な理解を持ち、必要時に参照することをお勧めします。
<p align="center"> 表 3-1 &nbsp; 基本データ型が占める空間と値の範囲 </p>
<div class="center-table" markdown>
| 型 | シンボル | 占有空間 | 最小値 | 最大値 | デフォルト値 |
| ------- | -------- | -------- | ------------------------ | ----------------------- | -------------- |
| 整数 | `byte` | 1バイト | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | 0 |
| | `short` | 2バイト | $-2^{15}$ | $2^{15} - 1$ | 0 |
| | `int` | 4バイト | $-2^{31}$ | $2^{31} - 1$ | 0 |
| | `long` | 8バイト | $-2^{63}$ | $2^{63} - 1$ | 0 |
| 浮動小数点 | `float` | 4バイト | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ |
| | `double` | 8バイト | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | 0.0 |
| 文字 | `char` | 2バイト | 0 | $2^{16} - 1$ | 0 |
| ブール | `bool` | 1バイト | $\text{false}$ | $\text{true}$ | $\text{false}$ |
</div>
上記の表はJavaの基本データ型に特有であることにご注意ください。すべてのプログラミング言語には独自のデータ型定義があり、占有空間、値の範囲、デフォルト値が異なる場合があります。
- Pythonでは、整数型`int`は任意のサイズになることができ、利用可能なメモリによってのみ制限されます。浮動小数点`float`は倍精度64ビットです。`char`型は存在せず、単一文字は実際には長さ1の文字列`str`です。
- CおよびC++では基本データ型のサイズが指定されておらず、実装とプラットフォームによって異なります。上記の表はLP64[データモデル](https://en.cppreference.com/w/cpp/language/types#Properties)に従っており、LinuxやmacOSを含むUnix 64ビットオペレーティングシステムで使用されています。
- CおよびC++における`char`のサイズは1バイトですが、ほとんどのプログラミング言語では、特定の文字エンコーディング方法に依存し、詳細は「文字エンコーディング」の章で説明されています。
- ブール値の表現には1ビット0または1のみが必要ですが、通常はメモリ内に1バイトとして格納されます。これは、現代のコンピュータCPUが通常1バイトを最小のアドレス可能なメモリ単位として使用するためです。
では、基本データ型とデータ構造の関係は何でしょうか?データ構造とは、コンピュータ内でデータを組織化し格納する方法であることを知っています。ここでの焦点は「データ」ではなく「構造」です。
「数値の列」を表現したい場合、自然に配列の使用を考えます。これは、配列の線形構造が数値の隣接性と順序性を表現できるためですが、格納される内容が整数`int`、小数`float`、文字`char`のいずれであっても、「データ構造」とは無関係です。
言い換えると、**基本データ型はデータの「内容型」を提供し、データ構造はデータの「組織化方法」を提供します**。例えば、以下のコードでは、同じデータ構造(配列)を使用して、`int``float``char``bool`などの異なる基本データ型を格納し表現しています。
=== "Python"
```python title=""
# 様々な基本データ型を使用して配列を初期化
numbers: list[int] = [0] * 5
decimals: list[float] = [0.0] * 5
# Pythonの文字は実際には長さ1の文字列
characters: list[str] = ['0'] * 5
bools: list[bool] = [False] * 5
# Pythonのリストは様々な基本データ型とオブジェクト参照を自由に格納可能
data = [0, 0.0, 'a', False, ListNode(0)]
```
=== "C++"
```cpp title=""
// 様々な基本データ型を使用して配列を初期化
int numbers[5];
float decimals[5];
char characters[5];
bool bools[5];
```
=== "Java"
```java title=""
// 様々な基本データ型を使用して配列を初期化
int[] numbers = new int[5];
float[] decimals = new float[5];
char[] characters = new char[5];
boolean[] bools = new boolean[5];
```
=== "C#"
```csharp title=""
// 様々な基本データ型を使用して配列を初期化
int[] numbers = new int[5];
float[] decimals = new float[5];
char[] characters = new char[5];
bool[] bools = new bool[5];
```
=== "Go"
```go title=""
// 様々な基本データ型を使用して配列を初期化
var numbers = [5]int{}
var decimals = [5]float64{}
var characters = [5]byte{}
var bools = [5]bool{}
```
=== "Swift"
```swift title=""
// 様々な基本データ型を使用して配列を初期化
let numbers = Array(repeating: 0, count: 5)
let decimals = Array(repeating: 0.0, count: 5)
let characters: [Character] = Array(repeating: "a", count: 5)
let bools = Array(repeating: false, count: 5)
```
=== "JS"
```javascript title=""
// JavaScriptの配列は様々な基本データ型とオブジェクトを自由に格納可能
const array = [0, 0.0, 'a', false];
```
=== "TS"
```typescript title=""
// 様々な基本データ型を使用して配列を初期化
const numbers: number[] = [];
const characters: string[] = [];
const bools: boolean[] = [];
```
=== "Dart"
```dart title=""
// 様々な基本データ型を使用して配列を初期化
List<int> numbers = List.filled(5, 0);
List<double> decimals = List.filled(5, 0.0);
List<String> characters = List.filled(5, 'a');
List<bool> bools = List.filled(5, false);
```
=== "Rust"
```rust title=""
// 様々な基本データ型を使用して配列を初期化
let numbers: Vec<i32> = vec![0; 5];
let decimals: Vec<f32> = vec![0.0, 5];
let characters: Vec<char> = vec!['0'; 5];
let bools: Vec<bool> = vec![false; 5];
```
=== "C"
```c title=""
// 様々な基本データ型を使用して配列を初期化
int numbers[10];
float decimals[10];
char characters[10];
bool bools[10];
```
=== "Kotlin"
```kotlin title=""
```
=== "Zig"
```zig title=""
// 様々な基本データ型を使用して配列を初期化
var numbers: [5]i32 = undefined;
var decimals: [5]f32 = undefined;
var characters: [5]u8 = undefined;
var bools: [5]bool = undefined;
```

View File

@@ -0,0 +1,97 @@
---
comments: true
---
# 3.4 &nbsp; 文字エンコーディング *
コンピュータシステムでは、すべてのデータが二進形式で格納され、`char`も例外ではありません。文字を表現するために、各文字と二進数の一対一のマッピングを定義する「文字セット」を開発する必要があります。文字セットがあれば、コンピュータは表を参照して二進数を文字に変換できます。
## 3.4.1 &nbsp; ASCII文字セット
<u>ASCIIコード</u>は最も初期の文字セットの一つで、正式にはAmerican Standard Code for Information Interchangeとして知られています。7つの二進桁1バイトの下位7ビットを使用して文字を表現し、最大128種類の異なる文字を表現できます。以下の図に示すように、ASCIIには英語の大文字と小文字、0〜9の数字、様々な句読点、特定の制御文字改行やタブなどが含まれています。
![ASCIIコード](character_encoding.assets/ascii_table.png){ class="animation-figure" }
<p align="center"> 図 3-6 &nbsp; ASCIIコード </p>
しかし、**ASCIIは英語の文字のみを表現できます**。コンピュータのグローバル化に伴い、より多くの言語を表現するために<u>EASCII</u>と呼ばれる文字セットが開発されました。ASCIIの7ビット構造から8ビットに拡張し、256文字の表現を可能にしました。
世界的に、様々な地域固有のEASCII文字セットが導入されました。これらのセットの最初の128文字はASCIIと一致していますが、残りの128文字は異なる言語の要件に対応するために異なって定義されています。
## 3.4.2 &nbsp; GBK文字セット
後に、**EASCIIでも多くの言語の文字要件を満たすことができない**ことが判明しました。例えば、中国語には約10万の漢字があり、そのうち数千が定期的に使用されています。1980年、中国標準化委員会は6763の中国語文字を含む<u>GB2312</u>文字セットを発表し、中国語のコンピュータ処理ニーズを本質的に満たしました。
しかし、GB2312は一部の稀少文字や繁体字を処理できませんでした。<u>GBK</u>文字セットはGB2312を拡張し、21886の中国語文字を含んでいます。GBKエンコーディングスキームでは、ASCII文字は1バイトで表現され、中国語文字は2バイトを使用します。
## 3.4.3 &nbsp; Unicode文字セット
コンピュータ技術の急速な発展と多数の文字セットおよびエンコーディング標準により、数多くの問題が発生しました。一方では、これらの文字セットは一般的に特定の言語の文字のみを定義し、多言語環境では適切に機能できませんでした。他方では、同じ言語に対する複数の文字セット標準の存在により、異なるエンコーディング標準を使用するコンピュータ間で情報交換を行う際に文字化けが発生しました。
当時の研究者たちは考えました:**世界のすべての言語と記号を含む包括的な文字セットが開発されれば、言語横断環境と文字化けに関連する問題を解決できるのではないでしょうか?** このアイデアにインスパイアされて、広範囲な文字セットであるUnicodeが誕生しました。
<u>Unicode</u>は中国語で「统一码」統一コードと呼ばれ、理論的に100万文字以上を収容できます。世界中のすべての文字を単一のセットに組み込み、様々な言語の処理と表示のための汎用文字セットを提供し、異なるエンコーディング標準による文字化けの問題を減らすことを目指しています。
1991年のリリース以来、Unicodeは新しい言語と文字を含むよう継続的に拡張されています。2022年9月現在、Unicodeには149,186文字が含まれており、様々な言語の文字、記号、さらには絵文字も含まれています。広大なUnicode文字セットでは、一般的に使用される文字は2バイトを占有し、一部の稀少な文字は3バイトまたは4バイトを占有する場合があります。
Unicodeは各文字に数値「コードポイント」と呼ばれるを割り当てる汎用文字セットですが、**これらの文字コードポイントがコンピュータシステムにどのように格納されるべきかは指定していません**。疑問が生じるかもしれませんシステムはテキスト内の異なる長さのUnicodeコードポイントをどのように解釈するのでしょうか例えば、2バイトのコードが与えられた場合、システムはそれが単一の2バイト文字を表すのか、2つの1バイト文字を表すのかをどのように判断するのでしょうか
**この問題に対する簡単な解決策は、すべての文字を等長エンコーディングとして格納することです**。以下の図に示すように、「Hello」の各文字は1バイトを占有し、「算法」アルゴリズムの各文字は2バイトを占有します。上位ビットをゼロで埋めることで、「Hello 算法」のすべての文字を2バイトとしてエンコードできます。この方法により、システムは2バイトごとに文字を解釈し、フレーズの内容を復元できます。
![Unicodeエンコーディング例](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" }
<p align="center"> 図 3-7 &nbsp; Unicodeエンコーディング例 </p>
しかし、ASCIIが示したように、英語のエンコーディングには1バイトのみが必要です。上記のアプローチを使用すると、英語テキストが占有する空間がASCIIエンコーディングと比較して2倍になり、メモリ空間の無駄になります。したがって、より効率的なUnicodeエンコーディング方法が必要です。
## 3.4.4 &nbsp; UTF-8エンコーディング
現在、UTF-8は国際的に最も広く使用されているUnicodeエンコーディング方法になっています。**これは可変長エンコーディング**で、文字の複雑さに応じて1〜4バイトを使用して文字を表現します。ASCII文字は1バイトのみが必要で、ラテン文字とギリシャ文字は2バイト、一般的に使用される中国語文字は3バイト、その他の稀少な文字は4バイトが必要です。
UTF-8のエンコーディング規則は複雑ではなく、2つのケースに分けることができます
- 1バイト文字の場合、最上位ビットを$0$に設定し、残りの7ビットをUnicodeコードポイントに設定します。注目すべきは、ASCII文字がUnicodeセットの最初の128コードポイントを占有することです。これは**UTF-8エンコーディングがASCIIと後方互換性がある**ことを意味します。これは、UTF-8を使用して古いASCIIテキストを解析できることを意味します。
- 長さ$n$バイトの文字($n > 1$)の場合、最初のバイトの最上位$n$ビットを$1$に設定し、$(n + 1)^{\text{th}}$ビットを$0$に設定します。2番目のバイトから、各バイトの最上位2ビットを$10$に設定します。残りのビットはUnicodeコードポイントを埋めるために使用されます。
以下の図は「Hello算法」のUTF-8エンコーディングを示しています。最上位$n$ビットが$1$に設定されているため、システムは最上位ビットで$1$に設定されたビット数を数えることで文字の長さを$n$として決定できることが観察できます。
しかし、なぜ残りのバイトの最上位2ビットを$10$に設定するのでしょうか?実際、この$10$は一種のチェックサムとして機能します。システムが間違ったバイトからテキストの解析を開始した場合、バイトの先頭の$10$によりシステムは異常を迅速に検出できます。
$10$をチェックサムとして使用する理由は、UTF-8エンコーディング規則の下では、文字の最上位2ビットが$10$になることは不可能だからです。これは矛盾により証明できます文字の最上位2ビットが$10$の場合、文字の長さが$1$であることを示し、これはASCIIに対応します。しかし、ASCII文字の最上位ビットは$0$であるべきで、これは仮定と矛盾します。
![UTF-8エンコーディング例](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" }
<p align="center"> 図 3-8 &nbsp; UTF-8エンコーディング例 </p>
UTF-8以外にも、他の一般的なエンコーディング方法には以下があります
- **UTF-16エンコーディング**2または4バイトを使用して文字を表現します。すべてのASCII文字と一般的に使用される非英語文字は2バイトで表現され、少数の文字は4バイトが必要です。2バイト文字の場合、UTF-16エンコーディングはUnicodeコードポイントと等しくなります。
- **UTF-32エンコーディング**すべての文字が4バイトを使用します。これは、UTF-32がUTF-8やUTF-16よりも多くの空間を占有することを意味し、特にASCII文字の割合が高いテキストでは顕著です。
ストレージ空間の観点から、UTF-8を使用して英語文字を表現することは1バイトのみが必要なため非常に効率的です。UTF-16を使用して一部の非英語文字中国語などをエンコードすることは、2バイトのみが必要なためより効率的になる場合があります。一方、UTF-8では3バイトが必要になる場合があります。
互換性の観点から、UTF-8は最も汎用性があり、多くのツールとライブラリがUTF-8を優先的にサポートしています。
## 3.4.5 &nbsp; プログラミング言語における文字エンコーディング
歴史的に、多くのプログラミング言語はプログラム実行中の文字列処理にUTF-16やUTF-32などの固定長エンコーディングを利用していました。これにより文字列を配列として処理でき、いくつかの利点があります
- **ランダムアクセス**UTF-16でエンコードされた文字列は簡単にランダムアクセスできます。可変長エンコーディングであるUTF-8の場合、$i^{th}$文字の位置を特定するには文字列の開始から$i^{th}$位置まで走査する必要があり、$O(n)$時間がかかります。
- **文字数カウント**ランダムアクセスと同様に、UTF-16でエンコードされた文字列の文字数をカウントすることは$O(1)$操作です。しかし、UTF-8でエンコードされた文字列の文字数をカウントするには文字列全体を走査する必要があります。
- **文字列操作**分割、連結、挿入、削除などの多くの文字列操作は、UTF-16でエンコードされた文字列で簡単です。これらの操作は一般的に、UTF-8エンコーディングの有効性を確保するためにUTF-8でエンコードされた文字列で追加の計算が必要です。
プログラミング言語における文字エンコーディングスキームの設計は、様々な要因を含む興味深いトピックです:
- Javaの`String`型はUTF-16エンコーディングを使用し、各文字が2バイトを占有します。これは、16ビットがすべての可能な文字を表現するのに十分であるという初期の信念に基づいており、後に間違いであることが証明されました。Unicode標準が16ビットを超えて拡張されると、Javaの文字は「サロゲートペア」として知られる16ビット値のペアで表現される場合があります。
- JavaScriptとTypeScriptは、Javaと同様の理由でUTF-16エンコーディングを使用します。JavaScriptが1995年にNetscapeによって最初に導入されたとき、Unicodeはまだ初期段階にあり、16ビットエンコーディングはすべてのUnicode文字を表現するのに十分でした。
- C#はUTF-16エンコーディングを使用し、これは主にMicrosoftによって設計された.NETプラットフォーム、および多くのMicrosoft技術Windowsオペレーティングシステムを含むがUTF-16エンコーディングを広範囲に使用しているためです。
文字数の過小評価により、これらの言語は16ビットを超えるUnicode文字を表現するために「サロゲートペア」を使用する必要がありました。このアプローチには欠点がありますサロゲートペアを含む文字列は2バイトまたは4バイトを占有する文字を持つ場合があり、固定長エンコーディングの利点を失います。さらに、サロゲートペアの処理はプログラミングに複雑さとデバッグの困難さを追加します。
これらの課題に対処するため、一部の言語は代替エンコーディング戦略を採用しています:
- Pythonの`str`型は、文字のストレージ長が文字列内の最大のUnicodeコードポイントに依存する柔軟な表現でUnicodeエンコーディングを使用します。すべての文字がASCIIの場合、各文字は1バイトを占有し、基本多言語面BMP内の文字は2バイト、BMPを超える文字は4バイトを占有します。
- Goの`string`型は内部的にUTF-8エンコーディングを使用します。Goは個別のUnicodeコードポイントを表現するための`rune`型も提供します。
- Rustの`str``String`型は内部的にUTF-8エンコーディングを使用します。Rustは個別のUnicodeコードポイント用の`char`型も提供します。
上記の議論は、プログラミング言語での文字列の格納方法に関するものであり、**ファイルでの文字列の格納方法やネットワーク上での送信方法とは異なる**ことに注意することが重要です。ファイルストレージやネットワーク送信では、文字列は通常、最適な互換性と空間効率のためにUTF-8形式でエンコードされます。

View File

@@ -0,0 +1,58 @@
---
comments: true
---
# 3.1 &nbsp; データ構造の分類
一般的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュ表、木、ヒープ、グラフがあります。これらは「論理構造」と「物理構造」に分類できます。
## 3.1.1 &nbsp; 論理構造:線形と非線形
**論理構造はデータ要素間の論理的関係を明らかにします**。配列と連結リストでは、データは特定の順序で配置され、データ間の線形関係を示しています。一方、木では、データは上から下へ階層的に配置され、「祖先」と「子孫」間の派生関係を示しています。そして、グラフはノードとエッジから構成され、複雑なネットワーク関係を反映しています。
下図に示されているように、論理構造は「線形」と「非線形」の2つの主要カテゴリに分けることができます。線形構造はより直感的で、データが論理関係において線形に配置されていることを示しています。非線形構造は、逆に非線形に配置されています。
- **線形データ構造**:配列、連結リスト、スタック、キュー、ハッシュ表。要素が一対一の順次関係を持ちます。
- **非線形データ構造**:木、ヒープ、グラフ、ハッシュ表。
非線形データ構造は、さらに木構造とネットワーク構造に分けることができます。
- **木構造**:木、ヒープ、ハッシュ表。要素が一対多の関係を持ちます。
- **ネットワーク構造**:グラフ。要素が多対多の関係を持ちます。
![Linear and non-linear data structures](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" }
<p align="center"> 図 3-1 &nbsp; Linear and non-linear data structures </p>
## 3.1.2 &nbsp; 物理構造:連続と分散
**アルゴリズムの実行中、処理されるデータはメモリに格納されます**。下図はコンピュータのメモリスティックを示しており、各黒い正方形は物理メモリ空間です。メモリを巨大なExcelスプレッドシートと考えることができ、各セルは一定量のデータを格納できます。
**システムはメモリアドレスによって目標位置のデータにアクセスします**。下図に示されているように、コンピュータは特定のルールに従って表の各セルに一意の識別子を割り当て、各メモリ空間が一意のメモリアドレスを持つことを保証します。これらのアドレスにより、プログラムはメモリに格納されたデータにアクセスできます。
![Memory stick, memory spaces, memory addresses](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" }
<p align="center"> 図 3-2 &nbsp; Memory stick, memory spaces, memory addresses </p>
!!! tip
メモリをExcelスプレッドシートに比較することは簡略化された類推であることに注意してください。メモリの実際の動作メカニズムはより複雑で、アドレス空間、メモリ管理、キャッシュメカニズム、仮想メモリ、物理メモリなどの概念が関係しています。
メモリはすべてのプログラムの共有リソースです。あるメモリブロックが1つのプログラムによって占有されると、他のプログラムが同時に使用することはできません。**したがって、メモリリソースはデータ構造とアルゴリズムの設計における重要な考慮事項です**。例えば、アルゴリズムのピークメモリ使用量は、システムの残り空きメモリを超えてはいけません。連続したメモリブロックが不足している場合は、非連続メモリブロックに格納できるデータ構造を選択する必要があります。
下図に示されているように、**物理構造はコンピュータメモリにおけるデータの格納方法を反映し**、連続空間格納配列と非連続空間格納連結リストに分けることができます。2つのタイプの物理構造は、時間効率と空間効率の観点で補完的な特性を示します。
![Contiguous space storage and dispersed space storage](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" }
<p align="center"> 図 3-3 &nbsp; Contiguous space storage and dispersed space storage </p>
**すべてのデータ構造は配列、連結リスト、またはその組み合わせに基づいて実装されていることに注意してください**。例えば、スタックとキューは配列または連結リストのどちらでも実装できます。ハッシュ表の実装には配列と連結リストの両方が関係する場合があります。
- **配列ベースの実装**:スタック、キュー、ハッシュ表、木、ヒープ、グラフ、行列、テンソル(次元$\geq 3$の配列)。
- **連結リストベースの実装**:スタック、キュー、ハッシュ表、木、ヒープ、グラフなど。
配列に基づいて実装されたデータ構造は「静的データ構造」とも呼ばれ、初期化後に長さを変更できないことを意味します。逆に、連結リストに基づいたものは「動的データ構造」と呼ばれ、プログラム実行中にサイズを調整できます。
!!! tip
物理構造を理解するのが困難な場合は、次の章「配列と連結リスト」を読んでから、この節に戻ることをお勧めします。

View File

@@ -0,0 +1,22 @@
---
comments: true
icon: material/shape-outline
---
# 第 3 章 &nbsp; データ構造
![Data structures](../assets/covers/chapter_data_structure.jpg){ class="cover-image" }
!!! abstract
データ構造は堅牢で多様なフレームワークとして機能します。
データの整然とした組織化のための設計図を提供し、その上でアルゴリズムが生き生きと動き出します。
## 章の内容
- [3.1 &nbsp; データ構造の分類](classification_of_data_structure.md)
- [3.2 &nbsp; 基本データ型](basic_data_types.md)
- [3.3 &nbsp; 数値エンコーディング *](number_encoding.md)
- [3.4 &nbsp; 文字エンコーディング *](character_encoding.md)
- [3.5 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,162 @@
---
comments: true
---
# 3.3 &nbsp; 数値エンコーディング *
!!! tip
本書では、アスタリスク「*」が付いた章は任意読書です。時間が不足している場合や難しいと感じる場合は、最初はこれらをスキップして、必須の章を完了した後に戻ることができます。
## 3.3.1 &nbsp; 整数エンコーディング
前の節の表で、すべての整数型は正の数よりも1つ多い負の数を表現できることを観察しました。例えば、`byte`の範囲は$[-128, 127]$です。この現象は直感に反するように見え、その根本的な理由には符号絶対値、1の補数、2の補数エンコーディングの知識が関与しています。
まず重要なことは、**数値はコンピュータ内で2の補数形式で格納される**ということです。なぜそうなのかを分析する前に、これら3つのエンコーディング方法を定義しましょう
- **符号絶対値**:数値の二進表現の最上位ビットを符号ビットとし、$0$は正の数、$1$は負の数を表します。残りのビットは数値の値を表します。
- **1の補数**正の数の1の補数は符号絶対値と同じです。負の数の場合、符号ビット以外のすべてのビットを反転して得られます。
- **2の補数**正の数の2の補数は符号絶対値と同じです。負の数の場合、その1の補数に$1$を加えて得られます。
以下の図は、符号絶対値、1の補数、2の補数間の変換を示しています
![符号絶対値、1の補数、2の補数間の変換](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" }
<p align="center"> 図 3-4 &nbsp; 符号絶対値、1の補数、2の補数間の変換 </p>
<u>符号絶対値</u>は最も直感的ですが、制限があります。一つには、**符号絶対値の負の数は計算で直接使用できません**。例えば、符号絶対値で$1 + (-2)$を計算すると$-3$になり、これは正しくありません。
$$
\begin{aligned}
& 1 + (-2) \newline
& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline
& = 1000 \; 0011 \newline
& \rightarrow -3
\end{aligned}
$$
この問題に対処するため、コンピュータは<u>1の補数</u>を導入しました。1の補数に変換して$1 + (-2)$を計算し、結果を符号絶対値に戻すと、正しい結果$-1$が得られます。
$$
\begin{aligned}
& 1 + (-2) \newline
& \rightarrow 0000 \; 0001 \; \text{(符号絶対値)} + 1000 \; 0010 \; \text{(符号絶対値)} \newline
& = 0000 \; 0001 \; \text{(1の補数)} + 1111 \; 1101 \; \text{(1の補数)} \newline
& = 1111 \; 1110 \; \text{(1の補数)} \newline
& = 1000 \; 0001 \; \text{(符号絶対値)} \newline
& \rightarrow -1
\end{aligned}
$$
また、**符号絶対値では0に2つの表現があります**$+0$と$-0$です。これは0に対して2つの異なる二進エンコーディングがあることを意味し、曖昧さを引き起こす可能性があります。例えば、条件チェックで正と負の0を区別しないと、正しくない結果になる可能性があります。この曖昧さに対処するには追加のチェックが必要で、計算効率が低下する可能性があります。
$$
\begin{aligned}
+0 & \rightarrow 0000 \; 0000 \newline
-0 & \rightarrow 1000 \; 0000
\end{aligned}
$$
符号絶対値と同様に、1の補数も正と負の0の曖昧さに悩まされます。そのため、コンピュータはさらに<u>2の補数</u>を導入しました。符号絶対値、1の補数、2の補数における負の0の変換過程を観察してみましょう
$$
\begin{aligned}
-0 \rightarrow \; & 1000 \; 0000 \; \text{(符号絶対値)} \newline
= \; & 1111 \; 1111 \; \text{(1の補数)} \newline
= 1 \; & 0000 \; 0000 \; \text{(2の補数)} \newline
\end{aligned}
$$
負の0の1の補数に$1$を加えると桁上がりが発生しますが、`byte`の長さは8ビットのみのため、9番目のビットへの桁上がり$1$は破棄されます。したがって、**負の0の2の補数は$0000 \; 0000$**で、正の0と同じになり、曖昧さが解決されます。
最後の謎は、`byte`の$[-128, 127]$の範囲で、追加の負の数$-128$があることです。$[-127, +127]$の区間では、すべての整数に対応する符号絶対値、1の補数、2の補数があり、相互変換が可能であることを観察します。
しかし、**2の補数$1000 \; 0000$は対応する符号絶対値を持たない例外です**。変換方法によると、その符号絶対値は$0000 \; 0000$で、0を示します。これは矛盾を示しています。なぜなら、その2の補数は自分自身を表すべきだからです。コンピュータは、この特別な2の補数$1000 \; 0000$を$-128$を表すものとして指定しています。実際、2の補数での$(-1) + (-127)$の計算結果は$-128$になります。
$$
\begin{aligned}
& (-127) + (-1) \newline
& \rightarrow 1111 \; 1111 \; \text{(符号絶対値)} + 1000 \; 0001 \; \text{(符号絶対値)} \newline
& = 1000 \; 0000 \; \text{(1の補数)} + 1111 \; 1110 \; \text{(1の補数)} \newline
& = 1000 \; 0001 \; \text{(2の補数)} + 1111 \; 1111 \; \text{(2の補数)} \newline
& = 1000 \; 0000 \; \text{(2の補数)} \newline
& \rightarrow -128
\end{aligned}
$$
お気づきかもしれませんが、これらの計算はすべて加算であり、重要な事実を示唆しています:**コンピュータの内部ハードウェア回路は主に加算演算を中心に設計されています**。これは、加算が乗算、除算、減算などの他の演算と比較してハードウェアで実装しやすく、並列化が容易で高速計算が可能だからです。
これはコンピュータが加算のみを実行できることを意味するものではありません。**加算と基本的な論理演算を組み合わせることで、コンピュータは様々な他の数学演算を実行できます**。例えば、減算$a - b$は$a + (-b)$に変換でき、乗算と除算は複数の加算または減算に変換できます。
コンピュータで2の補数を使用する理由をまとめることができます2の補数表現により、コンピュータは同じ回路と演算を使用して正と負の数の加算を処理でき、減算用の特別なハードウェア回路の必要性を排除し、正と負の0の曖昧さを回避できます。これによりハードウェア設計が大幅に簡素化され、計算効率が向上します。
2の補数の設計は非常に巧妙で、スペースの制約により、ここで停止します。興味のある読者はさらに探求することを奨励します。
## 3.3.2 &nbsp; 浮動小数点数エンコーディング
興味深いことに気づいたかもしれません同じ4バイトの長さにもかかわらず、なぜ`float``int`と比較してはるかに大きい値の範囲を持つのでしょうか?これは直感に反するように見えます。`float`は分数を表現する必要があるため、範囲が縮小すると予想されるからです。
実際、**これは浮動小数点数(`float`)で使用される異なる表現方法によるものです**。32ビットの二進数を次のように考えてみましょう
$$
b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
$$
IEEE 754標準によると、32ビットの`float`は次の3つの部分で構成されます
- 符号ビット$\mathrm{S}$1ビットを占有し、$b_{31}$に対応します。
- 指数ビット$\mathrm{E}$8ビットを占有し、$b_{30} b_{29} \ldots b_{23}$に対応します。
- 仮数ビット$\mathrm{N}$23ビットを占有し、$b_{22} b_{21} \ldots b_0$に対応します。
二進`float`数の値は次のように計算されます:
$$
\text{val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2 - 127} \times \left(1 . b_{22} b_{21} \ldots b_0\right)_2
$$
十進公式に変換すると、次のようになります:
$$
\text{val} = (-1)^{\mathrm{S}} \times 2^{\mathrm{E} - 127} \times (1 + \mathrm{N})
$$
各成分の範囲は:
$$
\begin{aligned}
\mathrm{S} \in & \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline
(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} \times 2^{-i}) \subset [1, 2 - 2^{-23}]
\end{aligned}
$$
![IEEE 754標準での浮動小数点数の計算例](number_encoding.assets/ieee_754_float.png){ class="animation-figure" }
<p align="center"> 図 3-5 &nbsp; IEEE 754標準での浮動小数点数の計算例 </p>
上の図を観察すると、例のデータ$\mathrm{S} = 0$、$\mathrm{E} = 124$、$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$が与えられた場合:
$$
\text{val} = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
$$
これで最初の質問に答えることができます:**`float`の表現には指数ビットが含まれているため、`int`よりもはるかに大きい範囲を持ちます**。上記の計算に基づくと、`float`で表現可能な最大正の数は約$2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$で、最小負の数は符号ビットを切り替えることで得られます。
**しかし、`float`の拡張された範囲のトレードオフは精度の犠牲です**。整数型`int`は32ビットすべてを数値表現に使用し、値は均等に分布していますが、指数ビットのため、`float`の値が大きいほど、隣接する数値間の差が大きくなります。
以下の表に示すように、指数ビット$\mathrm{E} = 0$と$\mathrm{E} = 255$は特別な意味を持ち、**0、無限大、$\mathrm{NaN}$などを表現するために使用されます**。
<p align="center"> 表 3-2 &nbsp; 指数ビットの意味 </p>
<div class="center-table" markdown>
| 指数ビットE | 仮数ビット$\mathrm{N} = 0$ | 仮数ビット$\mathrm{N} \ne 0$ | 計算公式 |
| ------------------ | ----------------------------- | ------------------------------- | ---------------------------------------------------------------------- |
| $0$ | $\pm 0$ | 非正規化数 | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ |
| $1, 2, \dots, 254$ | 正規化数 | 正規化数 | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ |
| $255$ | $\pm \infty$ | $\mathrm{NaN}$ | |
</div>
非正規化数は浮動小数点数の精度を大幅に向上させることは注目に値します。最小の正の正規化数は$2^{-126}$で、最小の正の非正規化数は$2^{-126} \times 2^{-23}$です。
倍精度`double``float`と同様の表現方法を使用しますが、簡潔さのためここでは詳述しません。

View File

@@ -0,0 +1,70 @@
---
comments: true
---
# 3.5 &nbsp; まとめ
### 1. &nbsp; 重要なポイント
- データ構造は論理構造と物理構造の2つの観点から分類できます。論理構造はデータ間の論理的関係を記述し、物理構造はデータがメモリにどのように格納されるかを記述します。
- よく使用される論理構造には、線形構造、木、ネットワークがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)に分けます。ハッシュ表の実装は線形と非線形の両方のデータ構造を含む場合があります。
- プログラムが実行中の際、データはメモリに格納されます。各メモリ空間には対応するアドレスがあり、プログラムはこれらのアドレスを通じてデータにアクセスします。
- 物理構造は連続空間格納(配列)と離散空間格納(連結リスト)に分けることができます。すべてのデータ構造は配列、連結リスト、またはその両方の組み合わせを使用して実装されます。
- コンピュータの基本データ型には、整数(`byte``short``int``long`)、浮動小数点数(`float``double`)、文字(`char`)、ブール値(`bool`)が含まれます。データ型の値の範囲は、そのサイズと表現に依存します。
- 符号絶対値、1の補数、2の補数は、コンピュータで整数をエンコードする3つの方法であり、相互に変換することができます。符号絶対値の最上位ビットは符号ビットで、残りのビットは数値の値を表します。
- 整数はコンピュータで2の補数によってエンコードされます。この表現の利点には、(i)コンピュータが正と負の整数の加算を統一できる、(ii)減算用の特別なハードウェア回路を設計する必要がない、(iii)正と負の0の曖昧さがない、があります。
- 浮動小数点数のエンコーディングは、1つの符号ビット、8つの指数ビット、23の仮数ビットで構成されます。指数ビットのため、浮動小数点数の範囲は整数よりもはるかに大きくなりますが、精度を犠牲にします。
- ASCIIは最初期の英語文字セットで、1バイトの長さで計127文字です。GBKは人気のある中国語文字セットで、2万文字以上の中国語文字を含みます。Unicodeは世界の様々な言語の文字を含む完全な文字セット標準を提供することを目的とし、文字エンコーディング方法の不一致による文字化け問題を解決します。
- UTF-8は最も人気があり一般的なUnicodeエンコーディング方法です。これは可変長エンコーディング方法で、優れた拡張性と空間効率を持ちます。UTF-16とUTF-32は固定長エンコーディング方法です。中国語文字をエンコードする際、UTF-16はUTF-8よりも少ない空間を使用します。JavaやC#などのプログラミング言語はデフォルトでUTF-16エンコーディングを使用します。
### 2. &nbsp; Q & A
**Q**: なぜハッシュ表は線形と非線形の両方のデータ構造を含むのですか?
ハッシュ表の基礎構造は配列です。ハッシュ衝突を解決するために、「チェイン法」を使用する場合があります(後の節「ハッシュ衝突」で説明):配列の各バケットは連結リストを指し、その長さが特定の閾値より大きくなると木(通常は赤黒木)に変換される可能性があります。
格納の観点から、ハッシュ表の基礎構造は配列で、各バケットには値、連結リスト、または木が含まれる場合があります。したがって、ハッシュ表は線形データ構造(配列、連結リスト)と非線形データ構造(木)の両方を含む場合があります。
**Q**: `char`型の長さは1バイトですか
`char`型の長さは、プログラミング言語のエンコーディング方法によって決まります。例えば、Java、JavaScript、TypeScript、C#はすべてUTF-16エンコーディングUnicodeコードポイントを保存するためを使用するため、`char`型の長さは2バイトです。
**Q**: 配列ベースのデータ構造を「静的データ構造」と呼ぶことに曖昧さはありませんか?スタックもプッシュやポップなどの「動的」操作を実行できます。
スタックは動的なデータ操作を実装できますが、データ構造は依然として「静的」です(長さが固定)。配列ベースのデータ構造は動的に要素を追加または削除できますが、その容量は固定されています。スタックサイズが事前に割り当てられたサイズを超える場合、古い配列は新しく作成されたより大きな配列にコピーされます。
**Q**: スタック(キュー)を構築する際、そのサイズが指定されていないのに、なぜ「静的データ構造」なのですか?
高級プログラミング言語では、スタックキューの初期容量を手動で指定する必要はありません。このタスクはクラス内で自動的に完了されます。例えば、Javaの`ArrayList`の初期容量は通常10です。さらに、拡張操作も自動的に完了されます。詳細については、後続の「リスト」の章を参照してください。
**Q**: 符号絶対値を2の補数に変換する方法は「最初に否定してから1を加える」ですので、2の補数を符号絶対値に変換することはその逆操作「最初に1を減算してから否定する」であるべきです。
しかし、2の補数も「最初に否定してから1を加える」を通じて符号絶対値に変換できます。なぜですか
**A**: これは、符号絶対値と2の補数間の相互変換が「補数」の計算と等価だからです。まず補数を定義します$a + b = c$と仮定すると、$a$は$b$の$c$に対する補数と言い、逆に$b$は$a$の$c$に対する補数と言います。
長さ$n = 4$の二進数$0010$が与えられた場合、この数が符号絶対値符号ビットを無視の場合、その2の補数は「最初に否定してから1を加える」ことで得られます
$$
0010 \rightarrow 1101 \rightarrow 1110
$$
符号絶対値と2の補数の和が$0010 + 1110 = 10000$であることを観察します。つまり、2の補数$1110$は符号絶対値$0010$の$10000$に対する「補数」です。**これは、上記の「最初に否定してから1を加える」が$10000$に対する補数の計算と等価であることを意味します**。
では、$1110$の$10000$に対する「補数」は何でしょうか「最初に否定してから1を加える」ことで計算できます
$$
1110 \rightarrow 0001 \rightarrow 0010
$$
言い換えると、符号絶対値と2の補数は互いに$10000$に対する「補数」であるため、「符号絶対値から2の補数」と「2の補数から符号絶対値」は同じ操作最初に否定してから1を加えるで実装できます。
もちろん、「最初に否定してから1を加える」の逆操作を使用して2の補数$1110$の符号絶対値を求めることもできます。つまり、「最初に1を減算してから否定する」
$$
1110 \rightarrow 1101 \rightarrow 0010
$$
要約すると、「最初に否定してから1を加える」と「最初に1を減算してから否定する」は両方とも$10000$に対する補数を計算しており、等価です。
本質的に、「否定」操作は実際には$1111$に対する補数を求めることです(`符号絶対値 + 1の補数 = 1111`が常に成り立つため。そして1の補数に1を加えることは$10000$に対する2の補数と等しくなります。
上記では$n = 4$を例に取りましたが、任意の桁数の任意の二進数に一般化できます。

View File

@@ -0,0 +1,224 @@
---
comments: true
---
# 12.2 &nbsp; 分割統治検索戦略
私たちは検索アルゴリズムが主に2つのカテゴリに分類されることを学びました。
- **総当たり検索**:データ構造を走査することで実装され、時間計算量は $O(n)$ です。
- **適応検索**:独特なデータ組織形式や事前情報を利用し、時間計算量は $O(\log n)$ または $O(1)$ に達することができます。
実際、**時間計算量が $O(\log n)$ の検索アルゴリズムは通常分割統治戦略に基づいています**。例えば、二分探索や木などです。
- 二分探索の各ステップは、問題(配列内でターゲット要素を検索する)をより小さな問題(配列の半分でターゲット要素を検索する)に分割し、配列が空になるかターゲット要素が見つかるまで続けます。
- 木は分割統治のアイデアを表現し、二分探索木、AVL木、ヒープなどのデータ構造では、様々な操作の時間計算量は $O(\log n)$ です。
二分探索の分割統治戦略は以下の通りです。
- **問題を分割できる**:二分探索は元の問題(配列内での検索)を部分問題(配列の半分での検索)に再帰的に分割し、中間要素とターゲット要素を比較することで実現されます。
- **部分問題は独立している**:二分探索では、各ラウンドで一つの部分問題を処理し、他の部分問題に影響されません。
- **部分問題の解をマージする必要がない**:二分探索は特定の要素を見つけることを目的としているため、部分問題の解をマージする必要がありません。部分問題が解決されると、元の問題も解決されます。
分割統治は検索効率を向上させることができます。なぜなら、総当たり検索はラウンドごとに1つの選択肢しか除去できませんが、**分割統治は選択肢の半分を除去できるからです**。
### 1. &nbsp; 分割統治に基づく二分探索の実装
前の章では、二分探索は反復に基づいて実装されました。今度は、分割統治(再帰)に基づいて実装します。
!!! question
長さ $n$ の順序付けられた配列 `nums` が与えられ、すべての要素が一意である場合、要素 `target` を見つけてください。
分割統治の観点から、検索区間 $[i, j]$ に対応する部分問題を $f(i, j)$ と表します。
元の問題 $f(0, n-1)$ から開始して、以下のステップで二分探索を実行します。
1. 検索区間 $[i, j]$ の中点 $m$ を計算し、それを使用して検索区間の半分を除去します。
2. 半分のサイズに縮小された部分問題を再帰的に解決します。これは $f(i, m-1)$ または $f(m+1, j)$ になる可能性があります。
3. `target` が見つかるか区間が空になってリターンするまで、ステップ `1.``2.` を繰り返します。
以下の図は、配列内で要素 $6$ を探す二分探索の分割統治過程を示しています。
![二分探索の分割統治過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" }
<p align="center"> 図 12-4 &nbsp; 二分探索の分割統治過程 </p>
実装コードでは、問題 $f(i, j)$ を解決するために再帰関数 `dfs()` を宣言します:
=== "Python"
```python title="binary_search_recur.py"
def dfs(nums: list[int], target: int, i: int, j: int) -> int:
"""二分探索:問題 f(i, j)"""
# 区間が空の場合、対象要素がないことを示すため、-1 を返す
if i > j:
return -1
# 中点インデックス m を計算
m = (i + j) // 2
if nums[m] < target:
# 再帰部分問題 f(m+1, j)
return dfs(nums, target, m + 1, j)
elif nums[m] > target:
# 再帰部分問題 f(i, m-1)
return dfs(nums, target, i, m - 1)
else:
# 対象要素を発見したため、そのインデックスを返す
return m
def binary_search(nums: list[int], target: int) -> int:
"""二分探索"""
n = len(nums)
# 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1)
```
=== "C++"
```cpp title="binary_search_recur.cpp"
/* 二分探索:問題 f(i, j) */
int dfs(vector<int> &nums, int target, int i, int j) {
// 区間が空の場合、対象要素が存在しないことを示すため、-1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
int m = i + (j - i) / 2;
if (nums[m] < target) {
// 再帰的な部分問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 再帰的な部分問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 対象要素が見つかったため、そのインデックスを返す
return m;
}
}
/* 二分探索 */
int binarySearch(vector<int> &nums, int target) {
int n = nums.size();
// 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1);
}
```
=== "Java"
```java title="binary_search_recur.java"
/* 二分探索:問題 f(i, j) */
int dfs(int[] nums, int target, int i, int j) {
// 区間が空の場合、対象要素が存在しないことを示すため、-1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
int m = i + (j - i) / 2;
if (nums[m] < target) {
// 再帰的な部分問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 再帰的な部分問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 対象要素が見つかったため、そのインデックスを返す
return m;
}
}
/* 二分探索 */
int binarySearch(int[] nums, int target) {
int n = nums.length;
// 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1);
}
```
=== "C#"
```csharp title="binary_search_recur.cs"
[class]{binary_search_recur}-[func]{DFS}
[class]{binary_search_recur}-[func]{BinarySearch}
```
=== "Go"
```go title="binary_search_recur.go"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
=== "Swift"
```swift title="binary_search_recur.swift"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
=== "JS"
```javascript title="binary_search_recur.js"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
=== "TS"
```typescript title="binary_search_recur.ts"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
=== "Dart"
```dart title="binary_search_recur.dart"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
=== "Rust"
```rust title="binary_search_recur.rs"
[class]{}-[func]{dfs}
[class]{}-[func]{binary_search}
```
=== "C"
```c title="binary_search_recur.c"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
=== "Kotlin"
```kotlin title="binary_search_recur.kt"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
=== "Ruby"
```ruby title="binary_search_recur.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{binary_search}
```
=== "Zig"
```zig title="binary_search_recur.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```

View File

@@ -0,0 +1,298 @@
---
comments: true
---
# 12.3 &nbsp; 二分木構築問題
!!! question
二分木の前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスが与えられた場合、二分木を構築してそのルートノードを返してください。二分木に重複するノード値がないと仮定します(以下の図に示すように)。
![二分木構築のサンプルデータ](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" }
<p align="center"> 図 12-5 &nbsp; 二分木構築のサンプルデータ </p>
### 1. &nbsp; 分割統治問題かどうかの判定
`preorder``inorder` シーケンスから二分木を構築する元の問題は、典型的な分割統治問題です。
- **問題を分解できる**分割統治の観点から、元の問題を2つの部分問題左の部分木の構築と右の部分木の構築とルートードの初期化という1つの操作に分割できます。各部分木部分問題について、同じアプローチを継続的に適用し、より小さな部分木部分問題に分割し、最小の部分問題空の部分木に到達するまで続けます。
- **部分問題は独立している**:左と右の部分木は重複しません。左の部分木を構築する際、左の部分木に対応する中順走査と前順走査のセグメントのみが必要です。右の部分木にも同じアプローチが適用されます。
- **部分問題の解を組み合わせることができる**:左と右の部分木(部分問題の解)を構築したら、それらをルートノードに接続して元の問題の解を取得できます。
### 2. &nbsp; 部分木の分割方法
上記の分析に基づいて、この問題は分割統治を使用して解決できます。**しかし、前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスを使用して左と右の部分木をどのように分割すればよいでしょうか?**
定義により、`preorder``inorder` シーケンスの両方を3つの部分に分割できます
- 前順走査:`[ ルート | 左の部分木 | 右の部分木 ]`。例えば、図では、木は `[ 3 | 9 | 2 1 7 ]` に対応します。
- 中順走査:`[ 左の部分木 | ルート | 右の部分木 ]`。例えば、図では、木は `[ 9 | 3 | 1 2 7 ]` に対応します。
前の図のデータを使用して、次の図に示すステップに従って分割結果を取得できます:
1. 前順走査の最初の要素3がルートードの値です。
2. `inorder` シーケンス内でルートード3のインデックスを見つけ、このインデックスを使用して `inorder``[ 9 | 3 1 2 7 ]` に分割します。
3. `inorder` シーケンスの分割に従って、左と右の部分木がそれぞれ1個と3個のードを含むことが簡単に決定できるため、`preorder` シーケンスを `[ 3 | 9 | 2 1 7 ]` に対応して分割できます。
![前順走査と中順走査での部分木の分割](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" }
<p align="center"> 図 12-6 &nbsp; 前順走査と中順走査での部分木の分割 </p>
### 3. &nbsp; 変数に基づく部分木範囲の記述
上記の分割方法に基づいて、**`preorder``inorder` シーケンスにおけるルート、左の部分木、右の部分木のインデックス範囲を取得しました**。これらのインデックス範囲を記述するために、いくつかのポインタ変数を使用します。
- 現在の木のルートノードの `preorder` シーケンスでのインデックスを $i$ とします。
- 現在の木のルートノードの `inorder` シーケンスでのインデックスを $m$ とします。
- 現在の木の `inorder` シーケンスでのインデックス範囲を $[l, r]$ とします。
以下の表に示すように、これらの変数は `preorder` シーケンスでのルートノードのインデックスと `inorder` シーケンスでの部分木のインデックス範囲を表します。
<p align="center"> 表 12-1 &nbsp; 前順走査と中順走査でのルートノードと部分木のインデックス </p>
<div class="center-table" markdown>
| | `preorder` でのルートノードインデックス | `inorder` での部分木インデックス範囲 |
| ------------- | ------------------------------------- | ----------------------------------- |
| 現在の木 | $i$ | $[l, r]$ |
| 左の部分木 | $i + 1$ | $[l, m-1]$ |
| 右の部分木 | $i + 1 + (m - l)$ | $[m+1, r]$ |
</div>
右の部分木のルートインデックスの $(m-l)$ は「左の部分木のノード数」を表すことに注意してください。より明確な理解のために、以下の図を参照することが役立つ場合があります。
![ルートノードと左右の部分木のインデックス](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" }
<p align="center"> 図 12-7 &nbsp; ルートノードと左右の部分木のインデックス </p>
### 4. &nbsp; コード実装
$m$ の問い合わせの効率を向上させるために、ハッシュテーブル `hmap` を使用して `inorder` シーケンスの要素からそのインデックスへのマッピングを格納します:
=== "Python"
```python title="build_tree.py"
def dfs(
preorder: list[int],
inorder_map: dict[int, int],
i: int,
l: int,
r: int,
) -> TreeNode | None:
"""二分木の構築:分割統治"""
# 部分木の区間が空のとき終了
if r - l < 0:
return None
# ルートノードを初期化
root = TreeNode(preorder[i])
# m をクエリして左部分木と右部分木を分割
m = inorder_map[preorder[i]]
# 部分問題:左部分木を構築
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
# 部分問題:右部分木を構築
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
# ルートノードを返す
return root
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
"""二分木を構築"""
# ハッシュテーブルを初期化、中順走査の要素からインデックスへのマッピングを保存
inorder_map = {val: i for i, val in enumerate(inorder)}
root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)
return root
```
=== "C++"
```cpp title="build_tree.cpp"
/* 二分木の構築:分割統治 */
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
// 部分木の区間が空の場合に終了
if (r - l < 0)
return NULL;
// ルートノードを初期化
TreeNode *root = new TreeNode(preorder[i]);
// m を問い合わせて左右の部分木を分割
int m = inorderMap[preorder[i]];
// 部分問題:左の部分木を構築
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 部分問題:右の部分木を構築
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// ルートノードを返す
return root;
}
/* 二分木の構築 */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
// ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
return root;
}
```
=== "Java"
```java title="build_tree.java"
/* 二分木の構築:分割統治 */
TreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {
// 部分木の区間が空の場合に終了
if (r - l < 0)
return null;
// ルートノードを初期化
TreeNode root = new TreeNode(preorder[i]);
// m を問い合わせて左右の部分木を分割
int m = inorderMap.get(preorder[i]);
// 部分問題:左の部分木を構築
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 部分問題:右の部分木を構築
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// ルートノードを返す
return root;
}
/* 二分木の構築 */
TreeNode buildTree(int[] preorder, int[] inorder) {
// ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納
Map<Integer, Integer> inorderMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
}
TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "C#"
```csharp title="build_tree.cs"
[class]{build_tree}-[func]{DFS}
[class]{build_tree}-[func]{BuildTree}
```
=== "Go"
```go title="build_tree.go"
[class]{}-[func]{dfsBuildTree}
[class]{}-[func]{buildTree}
```
=== "Swift"
```swift title="build_tree.swift"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
=== "JS"
```javascript title="build_tree.js"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
=== "TS"
```typescript title="build_tree.ts"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
=== "Dart"
```dart title="build_tree.dart"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
=== "Rust"
```rust title="build_tree.rs"
[class]{}-[func]{dfs}
[class]{}-[func]{build_tree}
```
=== "C"
```c title="build_tree.c"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
=== "Kotlin"
```kotlin title="build_tree.kt"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
=== "Ruby"
```ruby title="build_tree.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{build_tree}
```
=== "Zig"
```zig title="build_tree.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
以下の図は、二分木を構築する再帰過程を示しています。各ノードは再帰の「下降」段階で作成され、各エッジ(参照)は「上昇」段階で形成されます。
=== "<1>"
![二分木構築の再帰過程](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" }
=== "<2>"
![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png){ class="animation-figure" }
=== "<3>"
![built_tree_step3](build_binary_tree_problem.assets/built_tree_step3.png){ class="animation-figure" }
=== "<4>"
![built_tree_step4](build_binary_tree_problem.assets/built_tree_step4.png){ class="animation-figure" }
=== "<5>"
![built_tree_step5](build_binary_tree_problem.assets/built_tree_step5.png){ class="animation-figure" }
=== "<6>"
![built_tree_step6](build_binary_tree_problem.assets/built_tree_step6.png){ class="animation-figure" }
=== "<7>"
![built_tree_step7](build_binary_tree_problem.assets/built_tree_step7.png){ class="animation-figure" }
=== "<8>"
![built_tree_step8](build_binary_tree_problem.assets/built_tree_step8.png){ class="animation-figure" }
=== "<9>"
![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png){ class="animation-figure" }
<p align="center"> 図 12-8 &nbsp; 二分木構築の再帰過程 </p>
各再帰関数の `preorder` と `inorder` シーケンスの分割は以下の図に示されています。
![各再帰関数での分割](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" }
<p align="center"> 図 12-9 &nbsp; 各再帰関数での分割 </p>
二分木が $n$ 個のノードを持つと仮定すると、各ノードの初期化(再帰関数 `dfs()` の呼び出し)には $O(1)$ 時間がかかります。**したがって、全体の時間計算量は $O(n)$ です**。
ハッシュテーブルは `inorder` 要素からそのインデックスへのマッピングを格納するため、$O(n)$ スペースが必要です。最悪の場合、二分木が連結リストに退化すると、再帰の深さは $n$ に達し、$O(n)$ のスタックスペースを消費する可能性があります。**したがって、全体の空間計算量は $O(n)$ です**。

View File

@@ -0,0 +1,101 @@
---
comments: true
---
# 12.1 &nbsp; 分割統治アルゴリズム
<u>分割統治</u>は重要で人気のあるアルゴリズム戦略です。名前が示すように、アルゴリズムは通常再帰的に実装され、「分割」と「統治」の2つのステップから構成されます。
1. **分割(分割段階)**元の問題を再帰的に2つ以上の小さな部分問題に分解し、最小の部分問題に到達するまで続けます。
2. **統治(マージ段階)**:解決方法が既知の最小の部分問題から開始し、部分問題の解をボトムアップ方式でマージして元の問題の解を構築します。
以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。
1. **分割**元の配列元の問題を再帰的に2つの副配列部分問題に分割し、副配列が1つの要素のみになるまで最小の部分問題続けます。
2. **統治**:順序付けられた副配列(部分問題の解)をボトムアップでマージして、順序付けられた元の配列(元の問題の解)を取得します。
![マージソートの分割統治戦略](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" }
<p align="center"> 図 12-1 &nbsp; マージソートの分割統治戦略 </p>
## 12.1.1 &nbsp; 分割統治問題を特定する方法
問題が分割統治解決に適しているかどうかは、通常以下の基準に基づいて決定できます。
1. **問題をより小さなものに分解できる**:元の問題をより小さく類似した部分問題に分割でき、そのような過程を同じ方法で再帰的に実行できます。
2. **部分問題は独立している**:部分問題間に重複がなく、独立しており、個別に解決できます。
3. **部分問題の解をマージできる**:元の問題の解は、部分問題の解を組み合わせることで導出されます。
明らかに、マージソートはこれら3つの基準を満たしています。
1. **問題をより小さなものに分解できる**配列元の問題を再帰的に2つの副配列部分問題に分割します。
2. **部分問題は独立している**:各副配列は独立してソートできます(部分問題は独立して解決できます)。
3. **部分問題の解をマージできる**2つの順序付けられた副配列部分問題の解を1つの順序付けられた配列元の問題の解にマージできます。
## 12.1.2 &nbsp; 分割統治による効率の向上
**分割統治戦略はアルゴリズム問題を効果的に解決するだけでなく、しばしば効率を向上させます**。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートは、分割統治戦略を適用しているため、選択ソート、バブルソート、挿入ソートよりも高速です。
私たちの心には疑問があるかもしれません:**なぜ分割統治はアルゴリズムの効率を向上させることができ、その根本的な論理は何ですか?** つまり、問題を部分問題に分解し、それらを解決し、それらの解を組み合わせて元の問題に対処することが、元の問題を直接解決するよりも効率的である理由は何ですかこの質問は2つの側面から分析できます操作数と並列計算。
### 1. &nbsp; 操作数の最適化
「バブルソート」を例にとると、長さ $n$ の配列を処理するのに $O(n^2)$ 時間が必要です。以下の図に示すように、配列を中点から2つの副配列に分割するとします。そのような分割には $O(n)$ 時間が必要です。各副配列のソートには $O((n / 2)^2)$ 時間が必要です。そして2つの副配列のマージには $O(n)$ 時間が必要です。したがって、全体の時間計算量は:
$$
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
$$
![配列分割前後のバブルソート](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png){ class="animation-figure" }
<p align="center"> 図 12-2 &nbsp; 配列分割前後のバブルソート </p>
以下の不等式を計算してみましょう。左側は分割前の総操作数を表し、右側は分割後の総操作数をそれぞれ表します:
$$
\begin{aligned}
n^2 & > \frac{n^2}{2} + 2n \newline
n^2 - \frac{n^2}{2} - 2n & > 0 \newline
n(n - 4) & > 0
\end{aligned}
$$
**これは $n > 4$ の場合、分割後の操作数が少なく、より良いパフォーマンスにつながることを意味します**。分割後の時間計算量は依然として二次 $O(n^2)$ ですが、計算量の定数係数が減少していることに注意してください。
さらに進むことができます。**副配列をその中点からさらに2つの副配列に分割し続けて、副配列が1つの要素のみになるまで続けたらどうでしょうか** このアイデアは実際には「マージソート」で、時間計算量は $O(n \log n)$ です。
少し違うことを試してみましょう。**2つではなく、より多くの分割に分割したらどうでしょうか** 例えば、元の配列を $k$ 個の副配列に均等に分割しますか?このアプローチは「バケットソート」と非常に似ており、大量のデータのソートに非常に適しています。理論的には、時間計算量は $O(n + k)$ に達することができます。
### 2. &nbsp; 並列計算による最適化
分割統治によって生成される部分問題は互いに独立していることが分かっています。**これは、それらを並列で解決できることを意味します。** その結果、分割統治はアルゴリズムの時間計算量を減らすだけでなく、**現代のオペレーティングシステムによる並列最適化も促進します。**
並列最適化は、複数のコアやプロセッサを持つ環境で特に効果的です。システムが複数の部分問題を同時に処理できるため、計算リソースを完全に活用し、全体的な実行時間が大幅に短縮されます。
例えば、以下の図に示す「バケットソート」では、大量のデータを様々なバケットに均等に分解します。各バケットのソート作業は、利用可能な計算ユニットに割り当てることができます。すべての作業が完了すると、すべてのソートされたバケットがマージされて最終結果が生成されます。
![バケットソートの並列計算](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png){ class="animation-figure" }
<p align="center"> 図 12-3 &nbsp; バケットソートの並列計算 </p>
## 12.1.3 &nbsp; 分割統治の一般的な応用
分割統治は多くの古典的なアルゴリズム問題を解決するために使用できます。
- **最近点対の発見**このアルゴリズムは点の集合を2つの半分に分割することで動作します。そして各半分で再帰的に最近点対を見つけます。最後に、2つの半分にまたがるペアを考慮して、全体の最近点対を見つけます。
- **大整数の乗算**一つのアルゴリズムはKaratsubaと呼ばれます。大整数の乗算をいくつかの小さな整数の乗算と加算に分解します。
- **行列の乗算**一例はStrassenアルゴリズムです。大きな行列の乗算を複数の小さな行列の乗算と加算に分解します。
- **ハノイの塔問題**:ハノイの塔問題は再帰的に解決でき、分割統治戦略の典型的な応用です。
- **転倒対の解決**シーケンスで、前の数が後の数より大きい場合、これら2つの数は転倒対を構成します。転倒対問題の解決は、マージソートの助けを借りて、分割統治のアイデアを利用できます。
分割統治はアルゴリズムとデータ構造の設計にも広く応用されています。
- **二分探索**二分探索は、ソート済み配列を中点インデックスから2つの半分に分割します。そして、ターゲット値と中間要素値の比較結果に基づいて、一方の半分が破棄されます。同じプロセスで残りの半分で検索が続行され、ターゲットが見つかるか残りの要素がなくなるまで続きます。
- **マージソート**:この節の冒頭ですでに紹介したため、さらなる詳述は不要です。
- **クイックソート**クイックソートはピボット値を選択して配列を2つの副配列に分割し、一方はピボットより小さい要素、もう一方はピボットより大きい要素を持ちます。このプロセスは、これら2つの副配列のそれぞれに対して、1つの要素のみを保持するまで続きます。
- **バケットソート**:バケットソートの基本的なアイデアは、データを複数のバケットに分散させることです。各バケット内の要素をソートした後、バケットから順序よく要素を取得して順序付けられた配列を取得します。
- **木**例えば、二分探索木、AVL木、赤黒木、B木、B+木など。その操作(検索、挿入、削除)はすべて分割統治戦略の応用と見なすことができます。
- **ヒープ**:ヒープは特別なタイプの完全二分木です。その様々な操作(挿入、削除、ヒープ化)は、実際に分割統治のアイデアを含意しています。
- **ハッシュテーブル**:ハッシュテーブルは直接分割統治を適用しませんが、一部のハッシュ衝突解決ソリューションは間接的にこの戦略を適用します。例えば、チェイン法の長いリストは、クエリ効率を向上させるために赤黒木に変換される場合があります。
**分割統治は巧妙に浸透するアルゴリズムアイデア**であり、様々なアルゴリズムとデータ構造に組み込まれていることが分かります。

View File

@@ -0,0 +1,318 @@
---
comments: true
---
# 12.4 &nbsp; ハノイの塔問題
マージソートと二分木構築の両方で、元の問題を2つの部分問題に分解し、それぞれが元の問題のサイズの半分でした。しかし、ハイの塔では、異なる分解戦略を採用します。
!!! question
3つの柱があり、それぞれ `A``B``C` と表記されます。最初、柱 `A` には $n$ 枚の円盤があり、上から下に向かって昇順のサイズで配置されています。私たちのタスクは、これらの $n$ 枚の円盤を柱 `C` に移動し、元の順序を維持することです(以下の図に示すように)。移動中には以下のルールが適用されます:
1. 円盤は柱の上部からのみ取り除くことができ、別の柱の上部に置く必要があります。
2. 一度に移動できるのは1枚の円盤のみです。
3. 小さい円盤は常に大きい円盤の上にある必要があります。
![ハノイの塔の例](hanota_problem.assets/hanota_example.png){ class="animation-figure" }
<p align="center"> 図 12-10 &nbsp; ハノイの塔の例 </p>
**サイズ $i$ のハノイの塔問題を $f(i)$ と表記します**。例えば、$f(3)$ は3枚の円盤を柱 `A` から柱 `C` に移動することを表します。
### 1. &nbsp; 基本ケースを考える
以下の図に示すように、問題 $f(1)$円盤が1枚のみについては、`A` から `C` に直接移動できます。
=== "<1>"
![サイズ1の問題の解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png){ class="animation-figure" }
<p align="center"> 図 12-11 &nbsp; サイズ1の問題の解 </p>
$f(2)$円盤が2枚については、**柱 `B` の助けを借りて小さい円盤を大きい円盤の上に保つ**必要があります。以下の図に示すように:
1. まず、小さい円盤を `A` から `B` に移動します。
2. 次に、大きい円盤を `A` から `C` に移動します。
3. 最後に、小さい円盤を `B` から `C` に移動します。
=== "<1>"
![サイズ2の問題の解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png){ class="animation-figure" }
=== "<3>"
![hanota_f2_step3](hanota_problem.assets/hanota_f2_step3.png){ class="animation-figure" }
=== "<4>"
![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png){ class="animation-figure" }
<p align="center"> 図 12-12 &nbsp; サイズ2の問題の解 </p>
$f(2)$ を解決する過程は次のように要約できます:**`B` の助けを借りて2枚の円盤を `A` から `C` に移動する**。ここで、`C` をターゲット柱、`B` をバッファ柱と呼びます。
### 2. &nbsp; 部分問題の分解
問題 $f(3)$つまり、円盤が3枚の場合については、状況がやや複雑になります。
すでに $f(1)$ と $f(2)$ の解が分かっているので、分割統治の観点を採用し、**`A` の上の2枚の円盤を1つの単位として扱い**、以下の図に示すステップを実行できます。これにより、3枚の円盤を `A` から `C` に正常に移動できます。
1. `B` をターゲット柱、`C` をバッファ柱として、2枚の円盤を `A` から `B` に移動します。
2. 残りの円盤を `A` から直接 `C` に移動します。
3. `C` をターゲット柱、`A` をバッファ柱として、2枚の円盤を `B` から `C` に移動します。
=== "<1>"
![サイズ3の問題の解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png){ class="animation-figure" }
=== "<3>"
![hanota_f3_step3](hanota_problem.assets/hanota_f3_step3.png){ class="animation-figure" }
=== "<4>"
![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png){ class="animation-figure" }
<p align="center"> 図 12-13 &nbsp; サイズ3の問題の解 </p>
本質的に、**$f(3)$ を2つの $f(2)$ 部分問題と1つの $f(1)$ 部分問題に分解します**。これら3つの部分問題を順次解決することで、元の問題が解決され、部分問題が独立しており、それらの解をマージできることを示しています。
ここから、以下の図に示すハノイの塔の分割統治戦略を要約できます。元の問題 $f(n)$ を2つの部分問題 $f(n-1)$ と1つの部分問題 $f(1)$ に分割し、以下の順序でこれら3つの部分問題を解決します
1. `C` をバッファとして使用し、$n-1$ 枚の円盤を `A` から `B` に移動します。
2. 残りの円盤を `A` から直接 `C` に移動します。
3. `A` をバッファとして使用し、$n-1$ 枚の円盤を `B` から `C` に移動します。
各 $f(n-1)$ 部分問題について、**同じ再帰分割を適用でき**、最小の部分問題 $f(1)$ に到達するまで続けます。$f(1)$ は単一の移動のみが必要であることがすでに分かっているため、解決するのは簡単です。
![ハノイの塔を解決するための分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" }
<p align="center"> 図 12-14 &nbsp; ハノイの塔を解決するための分割統治戦略 </p>
### 3. &nbsp; コード実装
コードでは、再帰関数 `dfs(i, src, buf, tar)` を定義します。これは柱 `src` から上の $i$ 枚の円盤を柱 `tar` に移動し、柱 `buf` をバッファとして使用します:
=== "Python"
```python title="hanota.py"
def move(src: list[int], tar: list[int]):
"""円盤を移動"""
# src の上から円盤を取り出す
pan = src.pop()
# 円盤を tar の上に置く
tar.append(pan)
def dfs(i: int, src: list[int], buf: list[int], tar: list[int]):
"""ハノイの塔問題 f(i) を解く"""
# src に円盤が 1 つだけ残っている場合、それを tar に移動
if i == 1:
move(src, tar)
return
# 部分問題 f(i-1)tar の助けを借りて src の上の i-1 個の円盤を buf に移動
dfs(i - 1, src, tar, buf)
# 部分問題 f(1):残りの 1 個の円盤を src から tar に移動
move(src, tar)
# 部分問題 f(i-1)src の助けを借りて buf の上の i-1 個の円盤を tar に移動
dfs(i - 1, buf, src, tar)
def solve_hanota(A: list[int], B: list[int], C: list[int]):
"""ハノイの塔問題を解く"""
n = len(A)
# B の助けを借りて A の上の n 個の円盤を C に移動
dfs(n, A, B, C)
```
=== "C++"
```cpp title="hanota.cpp"
/* 円盤を移動 */
void move(vector<int> &src, vector<int> &tar) {
// src の最上部から円盤を取り出す
int pan = src.back();
src.pop_back();
// 円盤を tar の最上部に配置
tar.push_back(pan);
}
/* ハノイの塔問題 f(i) を解く */
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
// src に円盤が1つだけ残っている場合、それを tar に移動
if (i == 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1)tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動
dfs(i - 1, src, tar, buf);
// 部分問題 f(1)残りの1つの円盤を src から tar に移動
move(src, tar);
// 部分問題 f(i-1)src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動
dfs(i - 1, buf, src, tar);
}
/* ハノイの塔問題を解く */
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
int n = A.size();
// B の助けを借りて、上位 n 個の円盤を A から C に移動
dfs(n, A, B, C);
}
```
=== "Java"
```java title="hanota.java"
/* 円盤を移動 */
void move(List<Integer> src, List<Integer> tar) {
// src の最上部から円盤を取り出す
Integer pan = src.remove(src.size() - 1);
// 円盤を tar の最上部に配置
tar.add(pan);
}
/* ハノイの塔問題 f(i) を解く */
void dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {
// src に円盤が1つだけ残っている場合、それを tar に移動
if (i == 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1)tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動
dfs(i - 1, src, tar, buf);
// 部分問題 f(1)残りの1つの円盤を src から tar に移動
move(src, tar);
// 部分問題 f(i-1)src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動
dfs(i - 1, buf, src, tar);
}
/* ハノイの塔問題を解く */
void solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {
int n = A.size();
// B の助けを借りて、上位 n 個の円盤を A から C に移動
dfs(n, A, B, C);
}
```
=== "C#"
```csharp title="hanota.cs"
[class]{hanota}-[func]{Move}
[class]{hanota}-[func]{DFS}
[class]{hanota}-[func]{SolveHanota}
```
=== "Go"
```go title="hanota.go"
[class]{}-[func]{move}
[class]{}-[func]{dfsHanota}
[class]{}-[func]{solveHanota}
```
=== "Swift"
```swift title="hanota.swift"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "JS"
```javascript title="hanota.js"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "TS"
```typescript title="hanota.ts"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Dart"
```dart title="hanota.dart"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Rust"
```rust title="hanota.rs"
[class]{}-[func]{move_pan}
[class]{}-[func]{dfs}
[class]{}-[func]{solve_hanota}
```
=== "C"
```c title="hanota.c"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Kotlin"
```kotlin title="hanota.kt"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Ruby"
```ruby title="hanota.rb"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solve_hanota}
```
=== "Zig"
```zig title="hanota.zig"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
以下の図に示すように、ハノイの塔問題は高さ $n$ の再帰木として視覚化できます。各ノードは部分問題を表し、`dfs()` の呼び出しに対応します。**したがって、時間計算量は $O(2^n)$、空間計算量は $O(n)$ です。**
![ハノイの塔の再帰木](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" }
<p align="center"> 図 12-15 &nbsp; ハノイの塔の再帰木 </p>
!!! quote
イの塔は古代の伝説に由来します。古代インドの寺院で、僧侶たちは3本の高いダイヤモンドの柱と、異なるサイズの $64$ 枚の金の円盤を持っていました。彼らは、最後の円盤が正しく置かれたとき、世界が終わると信じていました。
しかし、僧侶たちが1秒に1枚の円盤を移動したとしても、約 $2^{64} \approx 1.84×10^{19}$ —約5850億年—かかり、宇宙の年齢の現在の推定をはるかに超えています。したがって、この伝説が真実であれば、世界の終わりについて心配する必要はおそらくないでしょう。

View File

@@ -0,0 +1,22 @@
---
comments: true
icon: material/set-split
---
# 第 12 章 &nbsp; 分割統治
![分割統治](../assets/covers/chapter_divide_and_conquer.jpg){ class="cover-image" }
!!! abstract
困難な問題は層を重ねて分解され、各分解によってより単純になります。
分割統治は深い真理を明らかにします:単純さから始めれば、複雑さは解決される。
## 章の内容
- [12.1 &nbsp; 分割統治アルゴリズム](divide_and_conquer.md)
- [12.2 &nbsp; 分割統治探索戦略](binary_search_recur.md)
- [12.3 &nbsp; 二分木構築問題](build_binary_tree_problem.md)
- [12.4 &nbsp; ハノイの塔問題](hanota_problem.md)
- [12.5 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,15 @@
---
comments: true
---
# 12.5 &nbsp; まとめ
- 分割統治は一般的なアルゴリズム設計戦略で、分割分割と統治マージの2つの段階から構成され、一般的に再帰を使用して実装されます。
- 問題が分割統治アプローチに適しているかどうかを判断するために、問題が分解可能かどうか、部分問題が独立しているかどうか、部分問題をマージできるかどうかを確認します。
- マージソートは分割統治戦略の典型的な例です。配列を再帰的に2つの等しい長さの副配列に分割し、1つの要素のみが残るまで続け、次にこれらの副配列を層ごとにマージしてソートを完了します。
- 分割統治戦略の導入は、しばしばアルゴリズムの効率を向上させます。一方では操作数を減らし、他方では分割後のシステムの並列最適化を促進します。
- 分割統治は多数のアルゴリズム問題に適用でき、データ構造とアルゴリズム設計で広く使用され、多くのシナリオに現れます。
- 総当たり検索と比較して、適応検索はより効率的です。時間計算量が $O(\log n)$ の検索アルゴリズムは、通常分割統治戦略に基づいています。
- 二分探索は分割統治戦略のもう一つの古典的な応用です。部分問題の解のマージを含まず、再帰的な分割統治アプローチで実装できます。
- 二分木構築問題では、木の構築(元の問題)を左の部分木と右の部分木の構築(部分問題)に分割できます。これは前順走査と中順走査のインデックス範囲を分割することで実現できます。
- ハノイの塔問題では、サイズ $n$ の問題をサイズ $n-1$ の2つの部分問題とサイズ $1$ の1つの部分問題に分解できます。これら3つの部分問題を順次解決することで、元の問題が解決されます。

View File

@@ -0,0 +1,476 @@
---
comments: true
---
# 14.2 &nbsp; 動的プログラミング問題の特徴
前のセクションでは、動的プログラミングが問題を部分問題に分解することで元の問題を解決する方法を学びました。実際、部分問題の分解は一般的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングでは異なる重点があります。
- 分割統治法アルゴリズムは元の問題を複数の独立した部分問題に再帰的に分割し、最小の部分問題に到達するまで続け、バックトラッキング時に部分問題の解を組み合わせて最終的に元の問題の解を得ます。
- 動的プログラミングも問題を再帰的に分解しますが、分割統治法アルゴリズムとの主な違いは、動的プログラミングの部分問題が相互依存的であり、分解プロセス中に多くの重複する部分問題が現れることです。
- バックトラッキングアルゴリズムは試行錯誤によってすべての可能な解を網羅し、枝刈りによって不必要な探索分岐を避けます。元の問題の解は一連の決定ステップから構成され、各決定ステップ前の各部分シーケンスを部分問題として考えることができます。
実際、動的プログラミングは最適化問題を解決するためによく使用され、これらは重複する部分問題を含むだけでなく、他に2つの主要な特徴があります最適部分構造と無記憶性です。
## 14.2.1 &nbsp; 最適部分構造
階段登り問題を少し修正して、最適部分構造の概念を実証するのにより適したものにします。
!!! question "階段登りの最小コスト"
階段があり、一度に1段または2段上ることができ、階段の各段にはその段で支払う必要があるコストを表す非負の整数があります。非負の整数配列 $cost$ が与えられ、$cost[i]$ は $i$ 段目で支払う必要があるコストを表し、$cost[0]$ は地面(開始点)です。頂上に到達するために必要な最小コストは何ですか?
下の図に示すように、1段目、2段目、3段目のコストがそれぞれ $1$、$10$、$1$ の場合、地面から3段目に登る最小コストは $2$ です。
![3段目に登る最小コスト](dp_problem_features.assets/min_cost_cs_example.png){ class="animation-figure" }
<p align="center"> 図 14-6 &nbsp; 3段目に登る最小コスト </p>
$dp[i]$ を $i$ 段目に登る累積コストとします。$i$ 段目は $i-1$ 段目または $i-2$ 段目からのみ来ることができるため、$dp[i]$ は $dp[i-1] + cost[i]$ または $dp[i-2] + cost[i]$ のいずれかしかありえません。コストを最小化するために、2つのうち小さい方を選択すべきです
$$
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
$$
これにより最適部分構造の意味がわかります:**元の問題の最適解は部分問題の最適解から構築される**。
この問題は明らかに最適部分構造を持っています2つの部分問題 $dp[i-1]$ と $dp[i-2]$ の最適解からより良い方を選択し、それを使用して元の問題 $dp[i]$ の最適解を構築します。
では、前のセクションの階段登り問題は最適部分構造を持っているでしょうか?その目標は解の数を求めることで、これは数え上げ問題のようですが、別の方法で尋ねてみましょう:「解の最大数を求める」。驚くことに、**問題が変わったにもかかわらず、最適部分構造が現れた**ことがわかります:$n$ 段目での解の最大数は、$n-1$ 段目と $n-2$ 段目での解の最大数の和に等しいです。したがって、最適部分構造の解釈は非常に柔軟で、異なる問題では異なる意味を持ちます。
状態遷移方程式と初期状態 $dp[1] = cost[1]$ および $dp[2] = cost[2]$ に従って、動的プログラミングコードを得ることができます:
=== "Python"
```python title="min_cost_climbing_stairs_dp.py"
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:
"""最小コスト階段登り:動的プログラミング"""
n = len(cost) - 1
if n == 1 or n == 2:
return cost[n]
# dp テーブルを初期化、部分問題の解を格納するために使用
dp = [0] * (n + 1)
# 初期状態:最小の部分問題の解を事前設定
dp[1], dp[2] = cost[1], cost[2]
# 状態遷移:小さい部分問題から大きい部分問題を段階的に解く
for i in range(3, n + 1):
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
return dp[n]
```
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
/* 最小コスト階段登り:動的プログラミング */
int minCostClimbingStairsDP(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
return cost[n];
// DPテーブルを初期化し、部分問題の解を格納するために使用
vector<int> dp(n + 1);
// 初期状態:最小の部分問題の解を事前設定
dp[1] = cost[1];
dp[2] = cost[2];
// 状態遷移:小さな問題から大きな部分問題を段階的に解く
for (int i = 3; i <= n; i++) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Java"
```java title="min_cost_climbing_stairs_dp.java"
/* 最小コスト階段登り:動的プログラミング */
int minCostClimbingStairsDP(int[] cost) {
int n = cost.length - 1;
if (n == 1 || n == 2)
return cost[n];
// DPテーブルを初期化し、部分問題の解を格納するために使用
int[] dp = new int[n + 1];
// 初期状態:最小の部分問題の解を事前設定
dp[1] = cost[1];
dp[2] = cost[2];
// 状態遷移:小さな問題から大きな部分問題を段階的に解く
for (int i = 3; i <= n; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "C#"
```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDP}
```
=== "Go"
```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "Swift"
```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "JS"
```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "TS"
```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "Dart"
```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```
=== "C"
```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "Kotlin"
```kotlin title="min_cost_climbing_stairs_dp.kt"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "Ruby"
```ruby title="min_cost_climbing_stairs_dp.rb"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```
=== "Zig"
```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDP}
```
下の図は上記コードの動的プログラミングプロセスを示しています。
![階段登りの最小コストの動的プログラミングプロセス](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" }
<p align="center"> 図 14-7 &nbsp; 階段登りの最小コストの動的プログラミングプロセス </p>
この問題も空間最適化が可能で、1次元を0に圧縮し、空間計算量を $O(n)$ から $O(1)$ に削減できます:
=== "Python"
```python title="min_cost_climbing_stairs_dp.py"
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:
"""最小コスト階段登り:空間最適化動的プログラミング"""
n = len(cost) - 1
if n == 1 or n == 2:
return cost[n]
a, b = cost[1], cost[2]
for i in range(3, n + 1):
a, b = b, min(a, b) + cost[i]
return b
```
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
/* 最小コスト階段登り:空間最適化動的プログラミング */
int minCostClimbingStairsDPComp(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Java"
```java title="min_cost_climbing_stairs_dp.java"
/* 最小コスト階段登り:空間最適化動的プログラミング */
int minCostClimbingStairsDPComp(int[] cost) {
int n = cost.length - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = Math.min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "C#"
```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDPComp}
```
=== "Go"
```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "Swift"
```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "JS"
```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "TS"
```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "Dart"
```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```
=== "C"
```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "Kotlin"
```kotlin title="min_cost_climbing_stairs_dp.kt"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "Ruby"
```ruby title="min_cost_climbing_stairs_dp.rb"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```
=== "Zig"
```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
## 14.2.2 &nbsp; 無記憶性
無記憶性は動的プログラミングが問題解決に効果的であることを可能にする重要な特徴の1つです。その定義は**特定の状態が与えられたとき、その将来の発展は現在の状態のみに関連し、過去に経験したすべての状態とは無関係である**。
階段登り問題を例に取ると、状態 $i$ が与えられたとき、それは状態 $i+1$ と $i+2$ に発展し、それぞれ1段ジャンプと2段ジャンプに対応します。これら2つの選択をするとき、状態 $i$ より前の状態を考慮する必要はありません。なぜなら、それらは状態 $i$ の将来に影響しないからです。
しかし、階段登り問題に制約を追加すると、状況が変わります。
!!! question "制約付き階段登り"
$n$ 段の階段があり、毎回1段または2段上ることができますが、**1段を2回連続でジャンプすることはできません**。頂上に登る方法は何通りありますか?
下の図に示すように、3段目に登る実行可能な選択肢は2つだけで、1段を3回連続でジャンプする選択肢は制約条件を満たさないため破棄されます。
![制約付きで3段目に登る実行可能な選択肢の数](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" }
<p align="center"> 図 14-8 &nbsp; 制約付きで3段目に登る実行可能な選択肢の数 </p>
この問題では、前回が1段ジャンプだった場合、次回は必ず2段ジャンプでなければなりません。これは**次のステップの選択が現在の状態(現在の階段段数)だけでは独立して決定できず、前の状態(前回の階段段数)にも依存する**ことを意味します。
この問題がもはや無記憶性を満たさず、状態遷移方程式 $dp[i] = dp[i-1] + dp[i-2]$ も失敗することは容易にわかります。なぜなら $dp[i-1]$ は今回の1段ジャンプを表しますが、多くの「前回が1段ジャンプだった」選択肢を含んでおり、制約を満たすためにはこれらを直接 $dp[i]$ に含めることはできません。
このため、状態定義を拡張する必要があります:**状態 $[i, j]$ は $i$ 段目にいて、前回が $j$ 段ジャンプだったことを表す**。ここで $j \in \{1, 2\}$ です。この状態定義は前回が1段ジャンプだったか2段ジャンプだったかを効果的に区別し、現在の状態がどこから来たかを適切に判断できます。
- 前回が1段ジャンプだった場合、前々回は必ず2段ジャンプを選択していたはずです。つまり、$dp[i, 1]$ は $dp[i-1, 2]$ からのみ遷移できます。
- 前回が2段ジャンプだった場合、前々回は1段ジャンプまたは2段ジャンプを選択できました。つまり、$dp[i, 2]$ は $dp[i-2, 1]$ または $dp[i-2, 2]$ から遷移できます。
下の図に示すように、$dp[i, j]$ は状態 $[i, j]$ の解の数を表します。この時点で、状態遷移方程式は次のようになります:
$$
\begin{cases}
dp[i, 1] = dp[i-1, 2] \\
dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
\end{cases}
$$
![制約を考慮した再帰関係](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" }
<p align="center"> 図 14-9 &nbsp; 制約を考慮した再帰関係 </p>
最終的に、$dp[n, 1] + dp[n, 2]$ を返せばよく、この2つの合計が $n$ 段目に登る解の総数を表します:
=== "Python"
```python title="climbing_stairs_constraint_dp.py"
def climbing_stairs_constraint_dp(n: int) -> int:
"""制約付き階段登り:動的プログラミング"""
if n == 1 or n == 2:
return 1
# dp テーブルを初期化、部分問題の解を格納するために使用
dp = [[0] * 3 for _ in range(n + 1)]
# 初期状態:最小の部分問題の解を事前設定
dp[1][1], dp[1][2] = 1, 0
dp[2][1], dp[2][2] = 0, 1
# 状態遷移:小さい部分問題から大きい部分問題を段階的に解く
for i in range(3, n + 1):
dp[i][1] = dp[i - 1][2]
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
return dp[n][1] + dp[n][2]
```
=== "C++"
```cpp title="climbing_stairs_constraint_dp.cpp"
/* 制約付き階段登り:動的プログラミング */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// DPテーブルを初期化し、部分問題の解を格納するために使用
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
// 初期状態:最小の部分問題の解を事前設定
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 状態遷移:小さな問題から大きな部分問題を段階的に解く
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "Java"
```java title="climbing_stairs_constraint_dp.java"
/* 制約付き階段登り:動的プログラミング */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// DPテーブルを初期化し、部分問題の解を格納するために使用
int[][] dp = new int[n + 1][3];
// 初期状態:最小の部分問題の解を事前設定
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 状態遷移:小さな問題から大きな部分問題を段階的に解く
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "C#"
```csharp title="climbing_stairs_constraint_dp.cs"
[class]{climbing_stairs_constraint_dp}-[func]{ClimbingStairsConstraintDP}
```
=== "Go"
```go title="climbing_stairs_constraint_dp.go"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "Swift"
```swift title="climbing_stairs_constraint_dp.swift"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "JS"
```javascript title="climbing_stairs_constraint_dp.js"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "TS"
```typescript title="climbing_stairs_constraint_dp.ts"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "Dart"
```dart title="climbing_stairs_constraint_dp.dart"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "Rust"
```rust title="climbing_stairs_constraint_dp.rs"
[class]{}-[func]{climbing_stairs_constraint_dp}
```
=== "C"
```c title="climbing_stairs_constraint_dp.c"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "Kotlin"
```kotlin title="climbing_stairs_constraint_dp.kt"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "Ruby"
```ruby title="climbing_stairs_constraint_dp.rb"
[class]{}-[func]{climbing_stairs_constraint_dp}
```
=== "Zig"
```zig title="climbing_stairs_constraint_dp.zig"
[class]{}-[func]{climbingStairsConstraintDP}
```
上記のケースでは、前の状態のみを考慮すればよいため、状態定義を拡張することで依然として無記憶性を満たすことができます。しかし、一部の問題では非常に深刻な「状態効果」があります。
!!! question "障害物生成付き階段登り"
$n$ 段の階段があり、毎回1段または2段上ることができます。**$i$ 段目に登ったとき、システムが自動的に $2i$ 段目に障害物を置き、その後のすべてのラウンドで $2i$ 段目にジャンプすることが禁止される**と規定されています。例えば、最初の2ラウンドで2段目と3段目にジャンプした場合、その後は4段目と6段目にジャンプできません。頂上に登る方法は何通りありますか
この問題では、次のジャンプはすべての過去の状態に依存します。各ジャンプがより高い段に障害物を置き、将来のジャンプに影響するからです。このような問題では、動的プログラミングはしばしば解決に苦労します。
実際、多くの複雑な組み合わせ最適化問題(巡回セールスマン問題など)は無記憶性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を選択して、限られた時間内に使用可能な局所最適解を得ます。

View File

@@ -0,0 +1,695 @@
---
comments: true
---
# 14.3 &nbsp; 動的プログラミング問題解決アプローチ
前の2つのセクションでは、動的プログラミング問題の主要な特徴を紹介しました。次に、より実用的な2つの問題を一緒に探索しましょう。
1. 問題が動的プログラミング問題かどうかをどのように判断するか?
2. 動的プログラミング問題を解決する完全なステップは何か?
## 14.3.1 &nbsp; 問題の判定
一般的に言えば、問題が重複する部分問題、最適部分構造を含み、無記憶性を示す場合、通常動的プログラミング解法に適しています。しかし、問題の説明から直接これらの特徴を抽出することはしばしば困難です。したがって、通常は条件を緩和し、**まず問題がバックトラッキング(全探索)を使用した解決に適しているかどうかを観察**します。
**バックトラッキングに適した問題は通常「決定木モデル」に適合**し、これは木構造を使用して記述でき、各ノードは決定を表し、各パスは決定のシーケンスを表します。
言い換えると、問題が明示的な決定概念を含み、解が一連の決定を通じて生成される場合、それは決定木モデルに適合し、通常バックトラッキングを使用して解決できます。
この基礎の上で、動的プログラミング問題を判定するための「ボーナスポイント」があります。
- 問題に最大化(最小化)または最も(最も少ない)最適な解を見つけるという記述が含まれている。
- 問題の状態がリスト、多次元行列、または木を使用して表現でき、状態がその周囲の状態と再帰関係を持っている。
対応して、「ペナルティポイント」もあります。
- 問題の目標は最適解だけでなく、すべての可能な解を見つけることである。
- 問題の説明に順列と組み合わせの明らかな特徴があり、特定の複数の解を返す必要がある。
問題が決定木モデルに適合し、比較的明らかな「ボーナスポイント」を持つ場合、それが動的プログラミング問題であると仮定し、解決プロセス中に検証できます。
## 14.3.2 &nbsp; 問題解決ステップ
動的プログラミング問題解決プロセスは問題の性質と難易度によって異なりますが、一般的に次のステップに従います:決定の記述、状態の定義、$dp$ テーブルの確立、状態遷移方程式の導出、境界条件の決定など。
問題解決ステップをより具体的に説明するために、古典的な問題「最小経路和」を例として使用します。
!!! question
$n \times m$ の二次元グリッド `grid` が与えられ、グリッドの各セルには負でない整数が含まれ、そのセルのコストを表します。ロボットは左上のセルから始まり、各ステップで下または右にのみ移動でき、右下のセルに到達するまで続けます。左上から右下への最小経路和を返してください。
下の図は例を示しており、与えられたグリッドの最小経路和は $13$ です。
![最小経路和の例データ](dp_solution_pipeline.assets/min_path_sum_example.png){ class="animation-figure" }
<p align="center"> 図 14-10 &nbsp; 最小経路和の例データ </p>
**第1ステップ各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
この問題の各ラウンドの決定は、現在のセルから下または右に1ステップ移動することです。現在のセルの行と列のインデックスが $[i, j]$ であると仮定すると、下または右に移動した後、インデックスは $[i+1, j]$ または $[i, j+1]$ になります。したがって、状態には2つの変数が含まれるべきです行インデックスと列インデックス、$[i, j]$ と表記されます。
状態 $[i, j]$ は部分問題に対応します:開始点 $[0, 0]$ から $[i, j]$ への最小経路和、$dp[i, j]$ と表記されます。
このようにして、下の図に示す二次元 $dp$ 行列を得ます。そのサイズは入力グリッド $grid$ と同じです。
![状態定義とDPテーブル](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png){ class="animation-figure" }
<p align="center"> 図 14-11 &nbsp; 状態定義とDPテーブル </p>
!!! note
動的プログラミングとバックトラッキングは決定のシーケンスとして記述でき、状態はすべての決定変数から構成されます。問題解決の進行を記述するすべての変数を含むべきで、次の状態を導出するのに十分な情報を含んでいる必要があります。
各状態は部分問題に対応し、すべての部分問題の解を保存するための $dp$ テーブルを定義します。状態の各独立変数は $dp$ テーブルの次元です。本質的に、$dp$ テーブルは状態と部分問題の解の間のマッピングです。
**第2ステップ最適部分構造を特定し、状態遷移方程式を導出する**
状態 $[i, j]$ について、それは上のセル $[i-1, j]$ または左のセル $[i, j-1]$ からのみ導出できます。したがって、最適部分構造は:$[i, j]$ に到達する最小経路和は、$[i, j-1]$ と $[i-1, j]$ の最小経路和の小さい方によって決定されます。
上記の分析に基づいて、下の図に示す状態遷移方程式を導出できます:
$$
dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
$$
![最適部分構造と状態遷移方程式](dp_solution_pipeline.assets/min_path_sum_solution_state_transition.png){ class="animation-figure" }
<p align="center"> 図 14-12 &nbsp; 最適部分構造と状態遷移方程式 </p>
!!! note
定義された $dp$ テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解をどのように構築するか、つまり最適部分構造を見つけます。
最適部分構造を特定したら、それを使用して状態遷移方程式を構築できます。
**第3ステップ境界条件と状態遷移順序を決定する**
この問題では、最初の行の状態は左の状態からのみ来ることができ、最初の列の状態は上の状態からのみ来ることができるため、最初の行 $i = 0$ と最初の列 $j = 0$ が境界条件です。
下の図に示すように、各セルは左のセルと上のセルから導出されるため、ループを使用して行列を走査し、外側のループは行を反復し、内側のループは列を反復します。
![境界条件と状態遷移順序](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png){ class="animation-figure" }
<p align="center"> 図 14-13 &nbsp; 境界条件と状態遷移順序 </p>
!!! note
境界条件は動的プログラミングで $dp$ テーブルを初期化するために使用され、探索では枝刈りに使用されます。
状態遷移順序の核心は、現在の問題の解を計算するとき、それが依存するすべての小さな部分問題が既に正しく計算されていることを確保することです。
上記の分析に基づいて、動的プログラミングコードを直接書くことができます。しかし、部分問題の分解はトップダウンアプローチであるため、「力任せ探索 → メモ化探索 → 動的プログラミング」の順序で実装することが習慣的な思考により適合します。
### 1. &nbsp; 方法1力任せ探索
状態 $[i, j]$ から探索を開始し、それを常により小さな状態 $[i-1, j]$ と $[i, j-1]$ に分解します。再帰関数には以下の要素が含まれます。
- **再帰パラメータ**:状態 $[i, j]$。
- **戻り値**$[0, 0]$ から $[i, j]$ への最小経路和 $dp[i, j]$。
- **終了条件**$i = 0$ かつ $j = 0$ のとき、コスト $grid[0, 0]$ を返す。
- **枝刈り**$i < 0$ または $j < 0$ でインデックスが範囲外のとき、コスト $+\infty$ を返し、実行不可能性を表す。
実装コードは以下の通りです:
=== "Python"
```python title="min_path_sum.py"
def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:
"""最小パス和:ブルートフォース探索"""
# 左上のセルの場合、探索を終了
if i == 0 and j == 0:
return grid[0][0]
# 行または列のインデックスが範囲外の場合、+∞ コストを返す
if i < 0 or j < 0:
return inf
# 左上から (i-1, j) と (i, j-1) への最小パスコストを計算
up = min_path_sum_dfs(grid, i - 1, j)
left = min_path_sum_dfs(grid, i, j - 1)
# 左上から (i, j) への最小パスコストを返す
return min(left, up) + grid[i][j]
```
=== "C++"
```cpp title="min_path_sum.cpp"
/* 最小パス和:ブルートフォース探索 */
int minPathSumDFS(vector<vector<int>> &grid, int i, int j) {
// 左上のセルの場合、探索を終了
if (i == 0 && j == 0) {
return grid[0][0];
}
// 行または列のインデックスが範囲外の場合、+∞ のコストを返す
if (i < 0 || j < 0) {
return INT_MAX;
}
// 左上から (i-1, j) と (i, j-1) への最小パスコストを計算
int up = minPathSumDFS(grid, i - 1, j);
int left = minPathSumDFS(grid, i, j - 1);
// 左上から (i, j) への最小パスコストを返す
return min(left, up) + grid[i][j];
}
```
=== "Java"
```java title="min_path_sum.java"
/* 最小パス和:ブルートフォース探索 */
int minPathSumDFS(int[][] grid, int i, int j) {
// 左上のセルの場合、探索を終了
if (i == 0 && j == 0) {
return grid[0][0];
}
// 行または列のインデックスが範囲外の場合、+∞ のコストを返す
if (i < 0 || j < 0) {
return Integer.MAX_VALUE;
}
// 左上から (i-1, j) と (i, j-1) への最小パスコストを計算
int up = minPathSumDFS(grid, i - 1, j);
int left = minPathSumDFS(grid, i, j - 1);
// 左上から (i, j) への最小パスコストを返す
return Math.min(left, up) + grid[i][j];
}
```
=== "C#"
```csharp title="min_path_sum.cs"
[class]{min_path_sum}-[func]{MinPathSumDFS}
```
=== "Go"
```go title="min_path_sum.go"
[class]{}-[func]{minPathSumDFS}
```
=== "Swift"
```swift title="min_path_sum.swift"
[class]{}-[func]{minPathSumDFS}
```
=== "JS"
```javascript title="min_path_sum.js"
[class]{}-[func]{minPathSumDFS}
```
=== "TS"
```typescript title="min_path_sum.ts"
[class]{}-[func]{minPathSumDFS}
```
=== "Dart"
```dart title="min_path_sum.dart"
[class]{}-[func]{minPathSumDFS}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dfs}
```
=== "C"
```c title="min_path_sum.c"
[class]{}-[func]{minPathSumDFS}
```
=== "Kotlin"
```kotlin title="min_path_sum.kt"
[class]{}-[func]{minPathSumDFS}
```
=== "Ruby"
```ruby title="min_path_sum.rb"
[class]{}-[func]{min_path_sum_dfs}
```
=== "Zig"
```zig title="min_path_sum.zig"
[class]{}-[func]{minPathSumDFS}
```
下の図は $dp[2, 1]$ を根とする再帰木を示しており、いくつかの重複する部分問題を含み、その数はグリッド `grid` のサイズが増加すると急激に増加します。
本質的に、重複する部分問題の理由は:**左上隅から特定のセルに到達する複数のパスが存在する**ことです。
![力任せ探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs.png){ class="animation-figure" }
<p align="center"> 図 14-14 &nbsp; 力任せ探索の再帰木 </p>
各状態には下と右の2つの選択があるため、左上隅から右下隅までの総ステップ数は $m + n - 2$ で、最悪時間計算量は $O(2^{m + n})$ です。この計算方法はグリッドエッジ近くの状況を考慮していないことに注意してください。ネットワークエッジに到達したとき、選択肢が1つしか残らないため、実際のパス数はより少なくなります。
### 2. &nbsp; 方法2メモ化探索
グリッド `grid` と同じサイズのメモリスト `mem` を導入し、様々な部分問題の解を記録し、重複する部分問題を枝刈りします:
=== "Python"
```python title="min_path_sum.py"
def min_path_sum_dfs_mem(
grid: list[list[int]], mem: list[list[int]], i: int, j: int
) -> int:
"""最小パス和:記憶化探索"""
# 左上のセルの場合、探索を終了
if i == 0 and j == 0:
return grid[0][0]
# 行または列のインデックスが範囲外の場合、+∞ コストを返す
if i < 0 or j < 0:
return inf
# 記録がある場合、それを返す
if mem[i][j] != -1:
return mem[i][j]
# 左と上のセルからの最小パスコスト
up = min_path_sum_dfs_mem(grid, mem, i - 1, j)
left = min_path_sum_dfs_mem(grid, mem, i, j - 1)
# 左上から (i, j) への最小パスコストを記録して返す
mem[i][j] = min(left, up) + grid[i][j]
return mem[i][j]
```
=== "C++"
```cpp title="min_path_sum.cpp"
[class]{}-[func]{minPathSumDFSMem}
```
=== "Java"
```java title="min_path_sum.java"
/* 最小パス和:メモ化探索 */
int minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {
// 左上のセルの場合、探索を終了
if (i == 0 && j == 0) {
return grid[0][0];
}
// 行または列のインデックスが範囲外の場合、+∞ のコストを返す
if (i < 0 || j < 0) {
return Integer.MAX_VALUE;
}
// 記録がある場合、それを返す
if (mem[i][j] != -1) {
return mem[i][j];
}
// 左と上のセルからの最小パスコスト
int up = minPathSumDFSMem(grid, mem, i - 1, j);
int left = minPathSumDFSMem(grid, mem, i, j - 1);
// 左上から (i, j) への最小パスコストを記録して返す
mem[i][j] = Math.min(left, up) + grid[i][j];
return mem[i][j];
}
```
=== "C#"
```csharp title="min_path_sum.cs"
[class]{min_path_sum}-[func]{MinPathSumDFSMem}
```
=== "Go"
```go title="min_path_sum.go"
[class]{}-[func]{minPathSumDFSMem}
```
=== "Swift"
```swift title="min_path_sum.swift"
[class]{}-[func]{minPathSumDFSMem}
```
=== "JS"
```javascript title="min_path_sum.js"
[class]{}-[func]{minPathSumDFSMem}
```
=== "TS"
```typescript title="min_path_sum.ts"
[class]{}-[func]{minPathSumDFSMem}
```
=== "Dart"
```dart title="min_path_sum.dart"
[class]{}-[func]{minPathSumDFSMem}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dfs_mem}
```
=== "C"
```c title="min_path_sum.c"
[class]{}-[func]{minPathSumDFSMem}
```
=== "Kotlin"
```kotlin title="min_path_sum.kt"
[class]{}-[func]{minPathSumDFSMem}
```
=== "Ruby"
```ruby title="min_path_sum.rb"
[class]{}-[func]{min_path_sum_dfs_mem}
```
=== "Zig"
```zig title="min_path_sum.zig"
[class]{}-[func]{minPathSumDFSMem}
```
下の図に示すように、メモ化を導入した後、すべての部分問題の解は一度だけ計算される必要があるため、時間計算量は状態の総数、つまりグリッドサイズ $O(nm)$ に依存します。
![メモ化探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png){ class="animation-figure" }
<p align="center"> 図 14-15 &nbsp; メモ化探索の再帰木 </p>
### 3. &nbsp; 方法3動的プログラミング
動的プログラミング解法を反復的に実装します。コードは以下の通りです:
=== "Python"
```python title="min_path_sum.py"
def min_path_sum_dp(grid: list[list[int]]) -> int:
"""最小パス和:動的プログラミング"""
n, m = len(grid), len(grid[0])
# dp テーブルを初期化
dp = [[0] * m for _ in range(n)]
dp[0][0] = grid[0][0]
# 状態遷移:最初の行
for j in range(1, m):
dp[0][j] = dp[0][j - 1] + grid[0][j]
# 状態遷移:最初の列
for i in range(1, n):
dp[i][0] = dp[i - 1][0] + grid[i][0]
# 状態遷移:残りの行と列
for i in range(1, n):
for j in range(1, m):
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]
return dp[n - 1][m - 1]
```
=== "C++"
```cpp title="min_path_sum.cpp"
/* 最小パス和:動的プログラミング */
int minPathSumDP(vector<vector<int>> &grid) {
int n = grid.size(), m = grid[0].size();
// DPテーブルを初期化
vector<vector<int>> dp(n, vector<int>(m));
dp[0][0] = grid[0][0];
// 状態遷移:最初の行
for (int j = 1; j < m; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 状態遷移:最初の列
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 状態遷移:残りの行と列
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
}
}
return dp[n - 1][m - 1];
}
```
=== "Java"
```java title="min_path_sum.java"
/* 最小パス和:動的プログラミング */
int minPathSumDP(int[][] grid) {
int n = grid.length, m = grid[0].length;
// DPテーブルを初期化
int[][] dp = new int[n][m];
dp[0][0] = grid[0][0];
// 状態遷移:最初の行
for (int j = 1; j < m; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 状態遷移:最初の列
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 状態遷移:残りの行と列
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
}
}
return dp[n - 1][m - 1];
}
```
=== "C#"
```csharp title="min_path_sum.cs"
[class]{min_path_sum}-[func]{MinPathSumDP}
```
=== "Go"
```go title="min_path_sum.go"
[class]{}-[func]{minPathSumDP}
```
=== "Swift"
```swift title="min_path_sum.swift"
[class]{}-[func]{minPathSumDP}
```
=== "JS"
```javascript title="min_path_sum.js"
[class]{}-[func]{minPathSumDP}
```
=== "TS"
```typescript title="min_path_sum.ts"
[class]{}-[func]{minPathSumDP}
```
=== "Dart"
```dart title="min_path_sum.dart"
[class]{}-[func]{minPathSumDP}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dp}
```
=== "C"
```c title="min_path_sum.c"
[class]{}-[func]{minPathSumDP}
```
=== "Kotlin"
```kotlin title="min_path_sum.kt"
[class]{}-[func]{minPathSumDP}
```
=== "Ruby"
```ruby title="min_path_sum.rb"
[class]{}-[func]{min_path_sum_dp}
```
=== "Zig"
```zig title="min_path_sum.zig"
[class]{}-[func]{minPathSumDP}
```
下の図は最小経路和の状態遷移プロセスを示し、グリッド全体を走査するため、**時間計算量は $O(nm)$** です。
配列 `dp` のサイズは $n \times m$ であるため、**空間計算量は $O(nm)$** です。
=== "<1>"
![最小経路和の動的プログラミングプロセス](dp_solution_pipeline.assets/min_path_sum_dp_step1.png){ class="animation-figure" }
=== "<2>"
![min_path_sum_dp_step2](dp_solution_pipeline.assets/min_path_sum_dp_step2.png){ class="animation-figure" }
=== "<3>"
![min_path_sum_dp_step3](dp_solution_pipeline.assets/min_path_sum_dp_step3.png){ class="animation-figure" }
=== "<4>"
![min_path_sum_dp_step4](dp_solution_pipeline.assets/min_path_sum_dp_step4.png){ class="animation-figure" }
=== "<5>"
![min_path_sum_dp_step5](dp_solution_pipeline.assets/min_path_sum_dp_step5.png){ class="animation-figure" }
=== "<6>"
![min_path_sum_dp_step6](dp_solution_pipeline.assets/min_path_sum_dp_step6.png){ class="animation-figure" }
=== "<7>"
![min_path_sum_dp_step7](dp_solution_pipeline.assets/min_path_sum_dp_step7.png){ class="animation-figure" }
=== "<8>"
![min_path_sum_dp_step8](dp_solution_pipeline.assets/min_path_sum_dp_step8.png){ class="animation-figure" }
=== "<9>"
![min_path_sum_dp_step9](dp_solution_pipeline.assets/min_path_sum_dp_step9.png){ class="animation-figure" }
=== "<10>"
![min_path_sum_dp_step10](dp_solution_pipeline.assets/min_path_sum_dp_step10.png){ class="animation-figure" }
=== "<11>"
![min_path_sum_dp_step11](dp_solution_pipeline.assets/min_path_sum_dp_step11.png){ class="animation-figure" }
=== "<12>"
![min_path_sum_dp_step12](dp_solution_pipeline.assets/min_path_sum_dp_step12.png){ class="animation-figure" }
<p align="center"> 図 14-16 &nbsp; 最小経路和の動的プログラミングプロセス </p>
### 4. &nbsp; 空間最適化
各セルは左と上のセルのみに関連するため、単一行配列を使用して $dp$ テーブルを実装できます。
配列 `dp` は1行の状態のみを表現できるため、最初の列の状態を事前に初期化できず、各行を走査するときに更新することに注意してください
=== "Python"
```python title="min_path_sum.py"
def min_path_sum_dp_comp(grid: list[list[int]]) -> int:
"""最小パス和:空間最適化動的プログラミング"""
n, m = len(grid), len(grid[0])
# dp テーブルを初期化
dp = [0] * m
# 状態遷移:最初の行
dp[0] = grid[0][0]
for j in range(1, m):
dp[j] = dp[j - 1] + grid[0][j]
# 状態遷移:残りの行
for i in range(1, n):
# 状態遷移:最初の列
dp[0] = dp[0] + grid[i][0]
# 状態遷移:残りの列
for j in range(1, m):
dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]
return dp[m - 1]
```
=== "C++"
```cpp title="min_path_sum.cpp"
[class]{}-[func]{minPathSumDPComp}
```
=== "Java"
```java title="min_path_sum.java"
/* 最小パス和:空間最適化動的プログラミング */
int minPathSumDPComp(int[][] grid) {
int n = grid.length, m = grid[0].length;
// DPテーブルを初期化
int[] dp = new int[m];
// 状態遷移:最初の行
dp[0] = grid[0][0];
for (int j = 1; j < m; j++) {
dp[j] = dp[j - 1] + grid[0][j];
}
// 状態遷移:残りの行
for (int i = 1; i < n; i++) {
// 状態遷移:最初の列
dp[0] = dp[0] + grid[i][0];
// 状態遷移:残りの列
for (int j = 1; j < m; j++) {
dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
}
}
return dp[m - 1];
}
```
=== "C#"
```csharp title="min_path_sum.cs"
[class]{min_path_sum}-[func]{MinPathSumDPComp}
```
=== "Go"
```go title="min_path_sum.go"
[class]{}-[func]{minPathSumDPComp}
```
=== "Swift"
```swift title="min_path_sum.swift"
[class]{}-[func]{minPathSumDPComp}
```
=== "JS"
```javascript title="min_path_sum.js"
[class]{}-[func]{minPathSumDPComp}
```
=== "TS"
```typescript title="min_path_sum.ts"
[class]{}-[func]{minPathSumDPComp}
```
=== "Dart"
```dart title="min_path_sum.dart"
[class]{}-[func]{minPathSumDPComp}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dp_comp}
```
=== "C"
```c title="min_path_sum.c"
[class]{}-[func]{minPathSumDPComp}
```
=== "Kotlin"
```kotlin title="min_path_sum.kt"
[class]{}-[func]{minPathSumDPComp}
```
=== "Ruby"
```ruby title="min_path_sum.rb"
[class]{}-[func]{min_path_sum_dp_comp}
```
=== "Zig"
```zig title="min_path_sum.zig"
[class]{}-[func]{minPathSumDPComp}
```

View File

@@ -0,0 +1,416 @@
---
comments: true
---
# 14.6 &nbsp; 編集距離問題
編集距離は、レーベンシュタイン距離とも呼ばれ、一つの文字列を別の文字列に変換するために必要な最小修正回数を指し、情報検索や自然言語処理で2つのシーケンス間の類似度を測定するためによく使用されます。
!!! question
2つの文字列 $s$ と $t$ が与えられたとき、$s$ を $t$ に変換するために必要な最小編集回数を返してください。
文字列に対して3種類の編集を実行できます文字の挿入、文字の削除、または文字を他の任意の文字に置換。
下の図に示すように、`kitten``sitting` に変換するには3回の編集が必要で、2回の置換と1回の挿入を含みます。`hello``algo` に変換するには3ステップが必要で、2回の置換と1回の削除を含みます。
![編集距離の例データ](edit_distance_problem.assets/edit_distance_example.png){ class="animation-figure" }
<p align="center"> 図 14-27 &nbsp; 編集距離の例データ </p>
**編集距離問題は決定木モデルで自然に説明できます**。文字列は木のードに対応し、1ラウンドの決定編集操作は木のエッジに対応します。
下の図に示すように、操作に制限がない場合、各ードは多くのエッジを導出でき、それぞれが1つの操作に対応するため、`hello``algo` に変換する可能な経路は多数あります。
決定木の観点から、この問題の目標は、ノード `hello` とノード `algo` の間の最短経路を見つけることです。
![決定木モデルに基づいて表現された編集距離問題](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" }
<p align="center"> 図 14-28 &nbsp; 決定木モデルに基づいて表現された編集距離問題 </p>
### 1. &nbsp; 動的プログラミングアプローチ
**ステップ1各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
各ラウンドの決定は、文字列 $s$ に対して1つの編集操作を実行することを含みます。
編集プロセス中に問題のサイズを段階的に縮小することを目指し、これにより部分問題を構築できます。文字列 $s$ と $t$ の長さをそれぞれ $n$ と $m$ とします。まず、両方の文字列の末尾文字 $s[n-1]$ と $t[m-1]$ を考慮します。
- $s[n-1]$ と $t[m-1]$ が同じ場合、それらをスキップして直接 $s[n-2]$ と $t[m-2]$ を考慮できます。
- $s[n-1]$ と $t[m-1]$ が異なる場合、$s$ に対して1つの編集挿入、削除、置換を実行して、2つの文字列の末尾文字を一致させ、それらをスキップしてより小規模な問題を考慮できるようにする必要があります。
したがって、文字列 $s$ での各ラウンドの決定(編集操作)は、$s$ と $t$ でマッチされる残りの文字を変更します。したがって、状態は $s$ と $t$ で現在考慮されている $i$ 番目と $j$ 番目の文字であり、$[i, j]$ と表記されます。
状態 $[i, j]$ は部分問題に対応します:**$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集回数**。
これから、サイズ $(i+1) \times (j+1)$ の二次元 $dp$ テーブルを得ます。
**ステップ2最適部分構造を特定し、状態遷移方程式を導出する**
部分問題 $dp[i, j]$ を考慮すると、これに対応する2つの文字列の末尾文字は $s[i-1]$ と $t[j-1]$ であり、下の図に示すように3つのシナリオに分けることができます。
1. $s[i-1]$ の後に $t[j-1]$ を追加すると、残りの部分問題は $dp[i, j-1]$ です。
2. $s[i-1]$ を削除すると、残りの部分問題は $dp[i-1, j]$ です。
3. $s[i-1]$ を $t[j-1]$ に置換すると、残りの部分問題は $dp[i-1, j-1]$ です。
![編集距離の状態遷移](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" }
<p align="center"> 図 14-29 &nbsp; 編集距離の状態遷移 </p>
上記の分析に基づいて、最適部分構造を決定できます:$dp[i, j]$ の最小編集回数は、$dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ の中の最小値に編集ステップ $1$ を加えたものです。対応する状態遷移方程式は:
$$
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
$$
注意してください。**$s[i-1]$ と $t[j-1]$ が同じ場合、現在の文字に対して編集は必要ありません**。この場合、状態遷移方程式は:
$$
dp[i, j] = dp[i-1, j-1]
$$
**ステップ3境界条件と状態遷移の順序を決定する**
両方の文字列が空の場合、編集回数は $0$ です。つまり、$dp[0, 0] = 0$ です。$s$ が空で $t$ が空でない場合、最小編集回数は $t$ の長さに等しく、つまり最初の行 $dp[0, j] = j$ です。$s$ が空でなく $t$ が空の場合、最小編集回数は $s$ の長さに等しく、つまり最初の列 $dp[i, 0] = i$ です。
状態遷移方程式を観察すると、$dp[i, j]$ の解決は左、上、左上の解に依存するため、二重ループを使用して正しい順序で $dp$ テーブル全体を走査できます。
### 2. &nbsp; コード実装
=== "Python"
```python title="edit_distance.py"
def edit_distance_dp(s: str, t: str) -> int:
"""編集距離:動的プログラミング"""
n, m = len(s), len(t)
dp = [[0] * (m + 1) for _ in range(n + 1)]
# 状態遷移:最初の行と最初の列
for i in range(1, n + 1):
dp[i][0] = i
for j in range(1, m + 1):
dp[0][j] = j
# 状態遷移:残りの行と列
for i in range(1, n + 1):
for j in range(1, m + 1):
if s[i - 1] == t[j - 1]:
# 2 つの文字が等しい場合、これら 2 つの文字をスキップ
dp[i][j] = dp[i - 1][j - 1]
else:
# 最小編集数 = 3 つの操作(挿入、削除、置換)からの最小編集数 + 1
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
return dp[n][m]
```
=== "C++"
```cpp title="edit_distance.cpp"
/* 編集距離:動的プログラミング */
int editDistanceDP(string s, string t) {
int n = s.length(), m = t.length();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 状態遷移:最初の行と最初の列
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 状態遷移:残りの行と列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// 2つの文字が等しい場合、これら2つの文字をスキップ
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最小編集数 = 3つの操作挿入、削除、置換からの最小編集数 + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Java"
```java title="edit_distance.java"
/* 編集距離:動的プログラミング */
int editDistanceDP(String s, String t) {
int n = s.length(), m = t.length();
int[][] dp = new int[n + 1][m + 1];
// 状態遷移:最初の行と最初の列
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 状態遷移:残りの行と列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// 2つの文字が等しい場合、これら2つの文字をスキップ
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最小編集数 = 3つの操作挿入、削除、置換からの最小編集数 + 1
dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "C#"
```csharp title="edit_distance.cs"
[class]{edit_distance}-[func]{EditDistanceDP}
```
=== "Go"
```go title="edit_distance.go"
[class]{}-[func]{editDistanceDP}
```
=== "Swift"
```swift title="edit_distance.swift"
[class]{}-[func]{editDistanceDP}
```
=== "JS"
```javascript title="edit_distance.js"
[class]{}-[func]{editDistanceDP}
```
=== "TS"
```typescript title="edit_distance.ts"
[class]{}-[func]{editDistanceDP}
```
=== "Dart"
```dart title="edit_distance.dart"
[class]{}-[func]{editDistanceDP}
```
=== "Rust"
```rust title="edit_distance.rs"
[class]{}-[func]{edit_distance_dp}
```
=== "C"
```c title="edit_distance.c"
[class]{}-[func]{editDistanceDP}
```
=== "Kotlin"
```kotlin title="edit_distance.kt"
[class]{}-[func]{editDistanceDP}
```
=== "Ruby"
```ruby title="edit_distance.rb"
[class]{}-[func]{edit_distance_dp}
```
=== "Zig"
```zig title="edit_distance.zig"
[class]{}-[func]{editDistanceDP}
```
下の図に示すように、編集距離問題の状態遷移プロセスはナップサック問題と非常に似ており、二次元グリッドを埋めることと見なすことができます。
=== "<1>"
![編集距離の動的プログラミングプロセス](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" }
=== "<2>"
![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png){ class="animation-figure" }
=== "<3>"
![edit_distance_dp_step3](edit_distance_problem.assets/edit_distance_dp_step3.png){ class="animation-figure" }
=== "<4>"
![edit_distance_dp_step4](edit_distance_problem.assets/edit_distance_dp_step4.png){ class="animation-figure" }
=== "<5>"
![edit_distance_dp_step5](edit_distance_problem.assets/edit_distance_dp_step5.png){ class="animation-figure" }
=== "<6>"
![edit_distance_dp_step6](edit_distance_problem.assets/edit_distance_dp_step6.png){ class="animation-figure" }
=== "<7>"
![edit_distance_dp_step7](edit_distance_problem.assets/edit_distance_dp_step7.png){ class="animation-figure" }
=== "<8>"
![edit_distance_dp_step8](edit_distance_problem.assets/edit_distance_dp_step8.png){ class="animation-figure" }
=== "<9>"
![edit_distance_dp_step9](edit_distance_problem.assets/edit_distance_dp_step9.png){ class="animation-figure" }
=== "<10>"
![edit_distance_dp_step10](edit_distance_problem.assets/edit_distance_dp_step10.png){ class="animation-figure" }
=== "<11>"
![edit_distance_dp_step11](edit_distance_problem.assets/edit_distance_dp_step11.png){ class="animation-figure" }
=== "<12>"
![edit_distance_dp_step12](edit_distance_problem.assets/edit_distance_dp_step12.png){ class="animation-figure" }
=== "<13>"
![edit_distance_dp_step13](edit_distance_problem.assets/edit_distance_dp_step13.png){ class="animation-figure" }
=== "<14>"
![edit_distance_dp_step14](edit_distance_problem.assets/edit_distance_dp_step14.png){ class="animation-figure" }
=== "<15>"
![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png){ class="animation-figure" }
<p align="center"> 図 14-30 &nbsp; 編集距離の動的プログラミングプロセス </p>
### 3. &nbsp; 空間最適化
$dp[i, j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1]$ の解から導出され、直接走査では左上の解 $dp[i-1, j-1]$ が失われ、逆走査では事前に $dp[i, j-1]$ を構築できないため、どちらの走査順序も実行可能ではありません。
この理由で、変数 `leftup` を使用して左上の $dp[i-1, j-1]$ からの解を一時的に保存し、左と上の解のみを考慮すればよくなります。この状況は無制限ナップサック問題と似ており、直接走査が可能です。コードは以下の通りです:
=== "Python"
```python title="edit_distance.py"
def edit_distance_dp_comp(s: str, t: str) -> int:
"""編集距離:空間最適化動的プログラミング"""
n, m = len(s), len(t)
dp = [0] * (m + 1)
# 状態遷移:最初の行
for j in range(1, m + 1):
dp[j] = j
# 状態遷移:残りの行
for i in range(1, n + 1):
# 状態遷移:最初の列
leftup = dp[0] # dp[i-1, j-1] を一時的に保存
dp[0] += 1
# 状態遷移:残りの列
for j in range(1, m + 1):
temp = dp[j]
if s[i - 1] == t[j - 1]:
# 2 つの文字が等しい場合、これら 2 つの文字をスキップ
dp[j] = leftup
else:
# 最小編集数 = 3 つの操作(挿入、削除、置換)からの最小編集数 + 1
dp[j] = min(dp[j - 1], dp[j], leftup) + 1
leftup = temp # 次の dp[i-1, j-1] のために更新
return dp[m]
```
=== "C++"
```cpp title="edit_distance.cpp"
[class]{}-[func]{editDistanceDPComp}
```
=== "Java"
```java title="edit_distance.java"
/* 編集距離:空間最適化動的プログラミング */
int editDistanceDPComp(String s, String t) {
int n = s.length(), m = t.length();
int[] dp = new int[m + 1];
// 状態遷移:最初の行
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// 状態遷移:残りの行
for (int i = 1; i <= n; i++) {
// 状態遷移:最初の列
int leftup = dp[0]; // dp[i-1, j-1] を一時的に格納
dp[0] = i;
// 状態遷移:残りの列
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// 2つの文字が等しい場合、これら2つの文字をスキップ
dp[j] = leftup;
} else {
// 最小編集数 = 3つの操作挿入、削除、置換からの最小編集数 + 1
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 次のラウンドの dp[i-1, j-1] のために更新
}
}
return dp[m];
}
```
=== "C#"
```csharp title="edit_distance.cs"
[class]{edit_distance}-[func]{EditDistanceDPComp}
```
=== "Go"
```go title="edit_distance.go"
[class]{}-[func]{editDistanceDPComp}
```
=== "Swift"
```swift title="edit_distance.swift"
[class]{}-[func]{editDistanceDPComp}
```
=== "JS"
```javascript title="edit_distance.js"
[class]{}-[func]{editDistanceDPComp}
```
=== "TS"
```typescript title="edit_distance.ts"
[class]{}-[func]{editDistanceDPComp}
```
=== "Dart"
```dart title="edit_distance.dart"
[class]{}-[func]{editDistanceDPComp}
```
=== "Rust"
```rust title="edit_distance.rs"
[class]{}-[func]{edit_distance_dp_comp}
```
=== "C"
```c title="edit_distance.c"
[class]{}-[func]{editDistanceDPComp}
```
=== "Kotlin"
```kotlin title="edit_distance.kt"
[class]{}-[func]{editDistanceDPComp}
```
=== "Ruby"
```ruby title="edit_distance.rb"
[class]{}-[func]{edit_distance_dp_comp}
```
=== "Zig"
```zig title="edit_distance.zig"
[class]{}-[func]{editDistanceDPComp}
```

View File

@@ -0,0 +1,24 @@
---
comments: true
icon: material/table-pivot
---
# 第 14 章 &nbsp; 動的プログラミング
![動的プログラミング](../assets/covers/chapter_dynamic_programming.jpg){ class="cover-image" }
!!! abstract
川が流れて海に注ぐように、
動的プログラミングは小さな問題の解を織り合わせて、より大きな問題の解へと導きます。一歩一歩進んで、最終的な答えが待つ彼岸へと向かいます。
## 章の内容
- [14.1 &nbsp; 動的プログラミング入門](intro_to_dynamic_programming.md)
- [14.2 &nbsp; DP問題の特性](dp_problem_features.md)
- [14.3 &nbsp; DP問題解決アプローチ](dp_solution_pipeline.md)
- [14.4 &nbsp; 0-1ナップサック問題](knapsack_problem.md)
- [14.5 &nbsp; 無制限ナップサック問題](unbounded_knapsack_problem.md)
- [14.6 &nbsp; 編集距離問題](edit_distance_problem.md)
- [14.7 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,821 @@
---
comments: true
---
# 14.1 &nbsp; 動的プログラミングの紹介
<u>動的プログラミング</u>は重要なアルゴリズムパラダイムであり、問題を一連の小さな部分問題に分解し、これらの部分問題の解を保存することで冗長な計算を避け、時間効率を大幅に向上させます。
このセクションでは、古典的な問題から始めて、まず力任せの探索法による解法を提示し、重複する部分問題を特定してから、より効率的な動的プログラミング解法を段階的に導出します。
!!! question "階段登り"
$n$ 段の階段があり、一度に $1$ 段または $2$ 段上ることができます。頂上に到達する方法は何通りありますか?
下の図に示すように、$3$ 段の階段の頂上に到達する方法は $3$ 通りあります。
![3段目に到達する方法の数](intro_to_dynamic_programming.assets/climbing_stairs_example.png){ class="animation-figure" }
<p align="center"> 図 14-1 &nbsp; 3段目に到達する方法の数 </p>
この問題は**バックトラッキングを用いてすべての可能性を網羅**することで方法の数を計算することを目的としています。具体的には、階段登りの問題を多段階選択プロセスとして考えます:地面から始めて、毎回 $1$ 段または $2$ 段上るかを選択し、階段の頂上に到達したら方法の数をカウントし、頂上を超えた場合はプルーニング(枝刈り)を行います。コードは以下の通りです:
=== "Python"
```python title="climbing_stairs_backtrack.py"
def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:
"""バックトラッキング"""
# n 段目に登ったとき、解の数に 1 を加える
if state == n:
res[0] += 1
# すべての選択肢を走査
for choice in choices:
# 枝刈りn 段を超えて登ることを許可しない
if state + choice > n:
continue
# 試行:選択を行い、状態を更新
backtrack(choices, state + choice, n, res)
# 撤回
def climbing_stairs_backtrack(n: int) -> int:
"""階段登り:バックトラッキング"""
choices = [1, 2] # 1 段または 2 段登ることを選択可能
state = 0 # 0 段目から登り始める
res = [0] # res[0] を使用して解の数を記録
backtrack(choices, state, n, res)
return res[0]
```
=== "C++"
```cpp title="climbing_stairs_backtrack.cpp"
/* バックトラッキング */
void backtrack(vector<int> &choices, int state, int n, vector<int> &res) {
// n段目に到達したとき、解の数に1を加える
if (state == n)
res[0]++;
// すべての選択肢を走査
for (auto &choice : choices) {
// 剪定n段を超えて登ることを許可しない
if (state + choice > n)
continue;
// 試行:選択を行い、状態を更新
backtrack(choices, state + choice, n, res);
// 撤回
}
}
/* 階段登り:バックトラッキング */
int climbingStairsBacktrack(int n) {
vector<int> choices = {1, 2}; // 1段または2段登ることを選択可能
int state = 0; // 0段目から登り始める
vector<int> res = {0}; // res[0] を使用して解の数を記録
backtrack(choices, state, n, res);
return res[0];
}
```
=== "Java"
```java title="climbing_stairs_backtrack.java"
/* バックトラッキング */
void backtrack(List<Integer> choices, int state, int n, List<Integer> res) {
// n段目に到達したとき、解の数に1を加える
if (state == n)
res.set(0, res.get(0) + 1);
// すべての選択肢を走査
for (Integer choice : choices) {
// 剪定n段を超えて登ることを許可しない
if (state + choice > n)
continue;
// 試行:選択を行い、状態を更新
backtrack(choices, state + choice, n, res);
// 撤回
}
}
/* 階段登り:バックトラッキング */
int climbingStairsBacktrack(int n) {
List<Integer> choices = Arrays.asList(1, 2); // 1段または2段登ることを選択可能
int state = 0; // 0段目から登り始める
List<Integer> res = new ArrayList<>();
res.add(0); // res[0] を使用して解の数を記録
backtrack(choices, state, n, res);
return res.get(0);
}
```
=== "C#"
```csharp title="climbing_stairs_backtrack.cs"
[class]{climbing_stairs_backtrack}-[func]{Backtrack}
[class]{climbing_stairs_backtrack}-[func]{ClimbingStairsBacktrack}
```
=== "Go"
```go title="climbing_stairs_backtrack.go"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
=== "Swift"
```swift title="climbing_stairs_backtrack.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
=== "JS"
```javascript title="climbing_stairs_backtrack.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
=== "TS"
```typescript title="climbing_stairs_backtrack.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
=== "Dart"
```dart title="climbing_stairs_backtrack.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
=== "Rust"
```rust title="climbing_stairs_backtrack.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbing_stairs_backtrack}
```
=== "C"
```c title="climbing_stairs_backtrack.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
=== "Kotlin"
```kotlin title="climbing_stairs_backtrack.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
=== "Ruby"
```ruby title="climbing_stairs_backtrack.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbing_stairs_backtrack}
```
=== "Zig"
```zig title="climbing_stairs_backtrack.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbingStairsBacktrack}
```
## 14.1.1 &nbsp; 方法1力任せ探索
バックトラッキングアルゴリズムは問題を明示的に部分問題に分解しません。代わりに、問題を一連の決定ステップとして扱い、試行と枝刈りを通じてすべての可能性を探索します。
この問題を分解アプローチを使って分析できます。$dp[i]$ を $i$ 段目に到達する方法の数とします。この場合、$dp[i]$ が元の問題であり、その部分問題は次のようになります:
$$
dp[i-1], dp[i-2], \dots, dp[2], dp[1]
$$
各移動は $1$ 段または $2$ 段しか進めないため、$i$ 段目に立っているとき、前のステップは $i-1$ 段目または $i-2$ 段目のいずれかにいたはずです。つまり、$i$ 段目には $i-1$ 段目または $i-2$ 段目からしか到達できません。
これにより重要な結論が得られます:**$i-1$ 段目に到達する方法の数に $i-2$ 段目に到達する方法の数を加えたものが、$i$ 段目に到達する方法の数に等しい**。式は以下の通りです:
$$
dp[i] = dp[i-1] + dp[i-2]
$$
これは、階段登り問題において部分問題間に再帰関係があることを意味し、**元の問題の解は部分問題の解から構築できます**。下の図はこの再帰関係を示しています。
![解の数の再帰関係](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png){ class="animation-figure" }
<p align="center"> 図 14-2 &nbsp; 解の数の再帰関係 </p>
再帰式に従って力任せ探索解法を得ることができます。$dp[n]$ から始めて、**より大きな問題を再帰的に2つの小さな部分問題の和に分解**し、解が既知の最小の部分問題 $dp[1]$ と $dp[2]$ に到達するまで続けます。$dp[1] = 1$ と $dp[2] = 2$ で、それぞれ1段目と2段目に登る方法が $1$ 通りと $2$ 通りあることを表します。
以下のコードを観察すると、標準的なバックトラッキングコードと同様に深さ優先探索に属しますが、より簡潔です:
=== "Python"
```python title="climbing_stairs_dfs.py"
def dfs(i: int) -> int:
"""探索"""
# 既知の dp[1] と dp[2] は、それらを返す
if i == 1 or i == 2:
return i
# dp[i] = dp[i-1] + dp[i-2]
count = dfs(i - 1) + dfs(i - 2)
return count
def climbing_stairs_dfs(n: int) -> int:
"""階段登り:探索"""
return dfs(n)
```
=== "C++"
```cpp title="climbing_stairs_dfs.cpp"
/* 探索 */
int dfs(int i) {
// 既知の dp[1] と dp[2] を返す
if (i == 1 || i == 2)
return i;
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1) + dfs(i - 2);
return count;
}
/* 階段登り:探索 */
int climbingStairsDFS(int n) {
return dfs(n);
}
```
=== "Java"
```java title="climbing_stairs_dfs.java"
/* 探索 */
int dfs(int i) {
// 既知の dp[1] と dp[2] を返す
if (i == 1 || i == 2)
return i;
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1) + dfs(i - 2);
return count;
}
/* 階段登り:探索 */
int climbingStairsDFS(int n) {
return dfs(n);
}
```
=== "C#"
```csharp title="climbing_stairs_dfs.cs"
[class]{climbing_stairs_dfs}-[func]{DFS}
[class]{climbing_stairs_dfs}-[func]{ClimbingStairsDFS}
```
=== "Go"
```go title="climbing_stairs_dfs.go"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
=== "Swift"
```swift title="climbing_stairs_dfs.swift"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
=== "JS"
```javascript title="climbing_stairs_dfs.js"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
=== "TS"
```typescript title="climbing_stairs_dfs.ts"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
=== "Dart"
```dart title="climbing_stairs_dfs.dart"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
=== "Rust"
```rust title="climbing_stairs_dfs.rs"
[class]{}-[func]{dfs}
[class]{}-[func]{climbing_stairs_dfs}
```
=== "C"
```c title="climbing_stairs_dfs.c"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
=== "Kotlin"
```kotlin title="climbing_stairs_dfs.kt"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
=== "Ruby"
```ruby title="climbing_stairs_dfs.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{climbing_stairs_dfs}
```
=== "Zig"
```zig title="climbing_stairs_dfs.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFS}
```
下の図は力任せ探索によって形成される再帰木を示しています。問題 $dp[n]$ について、その再帰木の深さは $n$ で、時間計算量は $O(2^n)$ です。この指数的増加により、$n$ が大きいとプログラムの実行がはるかに遅くなり、長い待機時間が生じます。
![階段登りの再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" }
<p align="center"> 図 14-3 &nbsp; 階段登りの再帰木 </p>
上の図を観察すると、**指数時間計算量は「重複する部分問題」によって引き起こされる**ことがわかります。例えば、$dp[9]$ は $dp[8]$ と $dp[7]$ に分解され、$dp[8]$ はさらに $dp[7]$ と $dp[6]$ に分解され、両方とも部分問題 $dp[7]$ を含んでいます。
このように、部分問題にはさらに小さな重複する部分問題が含まれ、これは無限に続きます。計算リソースの大部分がこれらの重複する部分問題に浪費されています。
## 14.1.2 &nbsp; 方法2メモ化探索
アルゴリズムの効率を向上させるため、**すべての重複する部分問題を一度だけ計算したい**と考えます。この目的のため、各部分問題の解を記録する配列 `mem` を宣言し、探索プロセス中に重複する部分問題を枝刈りします。
1. $dp[i]$ が初めて計算されるとき、後で使用するために `mem[i]` に記録します。
2. $dp[i]$ を再度計算する必要があるとき、`mem[i]` から直接結果を取得でき、その部分問題の冗長な計算を避けられます。
コードは以下の通りです:
=== "Python"
```python title="climbing_stairs_dfs_mem.py"
def dfs(i: int, mem: list[int]) -> int:
"""記憶化探索"""
# 既知の dp[1] と dp[2] は、それらを返す
if i == 1 or i == 2:
return i
# dp[i] の記録がある場合、それを返す
if mem[i] != -1:
return mem[i]
# dp[i] = dp[i-1] + dp[i-2]
count = dfs(i - 1, mem) + dfs(i - 2, mem)
# dp[i] を記録
mem[i] = count
return count
def climbing_stairs_dfs_mem(n: int) -> int:
"""階段登り:記憶化探索"""
# mem[i] は i 段目に登る解の総数を記録、-1 は記録なしを意味する
mem = [-1] * (n + 1)
return dfs(n, mem)
```
=== "C++"
```cpp title="climbing_stairs_dfs_mem.cpp"
/* メモ化探索 */
int dfs(int i, vector<int> &mem) {
// 既知の dp[1] と dp[2] を返す
if (i == 1 || i == 2)
return i;
// dp[i] の記録がある場合、それを返す
if (mem[i] != -1)
return mem[i];
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1, mem) + dfs(i - 2, mem);
// dp[i] を記録
mem[i] = count;
return count;
}
/* 階段登り:メモ化探索 */
int climbingStairsDFSMem(int n) {
// mem[i] は i 段目に登る総解数を記録、-1 は記録なしを意味する
vector<int> mem(n + 1, -1);
return dfs(n, mem);
}
```
=== "Java"
```java title="climbing_stairs_dfs_mem.java"
/* メモ化探索 */
int dfs(int i, int[] mem) {
// 既知の dp[1] と dp[2] を返す
if (i == 1 || i == 2)
return i;
// dp[i] の記録がある場合、それを返す
if (mem[i] != -1)
return mem[i];
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1, mem) + dfs(i - 2, mem);
// dp[i] を記録
mem[i] = count;
return count;
}
/* 階段登り:メモ化探索 */
int climbingStairsDFSMem(int n) {
// mem[i] は i 段目に登る総解数を記録、-1 は記録なしを意味する
int[] mem = new int[n + 1];
Arrays.fill(mem, -1);
return dfs(n, mem);
}
```
=== "C#"
```csharp title="climbing_stairs_dfs_mem.cs"
[class]{climbing_stairs_dfs_mem}-[func]{DFS}
[class]{climbing_stairs_dfs_mem}-[func]{ClimbingStairsDFSMem}
```
=== "Go"
```go title="climbing_stairs_dfs_mem.go"
[class]{}-[func]{dfsMem}
[class]{}-[func]{climbingStairsDFSMem}
```
=== "Swift"
```swift title="climbing_stairs_dfs_mem.swift"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFSMem}
```
=== "JS"
```javascript title="climbing_stairs_dfs_mem.js"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFSMem}
```
=== "TS"
```typescript title="climbing_stairs_dfs_mem.ts"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFSMem}
```
=== "Dart"
```dart title="climbing_stairs_dfs_mem.dart"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFSMem}
```
=== "Rust"
```rust title="climbing_stairs_dfs_mem.rs"
[class]{}-[func]{dfs}
[class]{}-[func]{climbing_stairs_dfs_mem}
```
=== "C"
```c title="climbing_stairs_dfs_mem.c"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFSMem}
```
=== "Kotlin"
```kotlin title="climbing_stairs_dfs_mem.kt"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFSMem}
```
=== "Ruby"
```ruby title="climbing_stairs_dfs_mem.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{climbing_stairs_dfs_mem}
```
=== "Zig"
```zig title="climbing_stairs_dfs_mem.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{climbingStairsDFSMem}
```
下の図を観察すると、**メモ化後、すべての重複する部分問題は一度だけ計算される必要があり、時間計算量を $O(n)$ に最適化**します。これは大幅な改善です。
![メモ化探索による再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" }
<p align="center"> 図 14-4 &nbsp; メモ化探索による再帰木 </p>
## 14.1.3 &nbsp; 方法3動的プログラミング
**メモ化探索は「トップダウン」方式**です:元の問題(根ノード)から始めて、より大きな部分問題をより小さなものに再帰的に分解し、最小の既知の部分問題(葉ノード)の解に到達するまで続けます。その後、バックトラッキングにより部分問題の解を収集し、元の問題の解を構築します。
一方、**動的プログラミングは「ボトムアップ」方式**です:最小の部分問題の解から始めて、元の問題が解決されるまで、より大きな部分問題の解を反復的に構築します。
動的プログラミングはバックトラッキングを必要としないため、ループを使った反復のみが必要で、再帰は不要です。以下のコードでは、配列 `dp` を初期化して部分問題の解を保存し、メモ化探索の配列 `mem` と同じ記録機能を果たします:
=== "Python"
```python title="climbing_stairs_dp.py"
def climbing_stairs_dp(n: int) -> int:
"""階段登り:動的プログラミング"""
if n == 1 or n == 2:
return n
# dp テーブルを初期化、部分問題の解を格納するため使用
dp = [0] * (n + 1)
# 初期状態:最小の部分問題の解を事前設定
dp[1], dp[2] = 1, 2
# 状態遷移:小さい部分問題から大きい部分問題を段階的に解く
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
```
=== "C++"
```cpp title="climbing_stairs_dp.cpp"
/* 階段登り:動的プログラミング */
int climbingStairsDP(int n) {
if (n == 1 || n == 2)
return n;
// DPテーブルを初期化し、部分問題の解を格納するために使用
vector<int> dp(n + 1);
// 初期状態:最小の部分問題の解を事前設定
dp[1] = 1;
dp[2] = 2;
// 状態遷移:小さな問題から大きな部分問題を段階的に解く
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
```
=== "Java"
```java title="climbing_stairs_dp.java"
/* 階段登り:動的プログラミング */
int climbingStairsDP(int n) {
if (n == 1 || n == 2)
return n;
// DPテーブルを初期化し、部分問題の解を格納するために使用
int[] dp = new int[n + 1];
// 初期状態:最小の部分問題の解を事前設定
dp[1] = 1;
dp[2] = 2;
// 状態遷移:小さな問題から大きな部分問題を段階的に解く
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
```
=== "C#"
```csharp title="climbing_stairs_dp.cs"
[class]{climbing_stairs_dp}-[func]{ClimbingStairsDP}
```
=== "Go"
```go title="climbing_stairs_dp.go"
[class]{}-[func]{climbingStairsDP}
```
=== "Swift"
```swift title="climbing_stairs_dp.swift"
[class]{}-[func]{climbingStairsDP}
```
=== "JS"
```javascript title="climbing_stairs_dp.js"
[class]{}-[func]{climbingStairsDP}
```
=== "TS"
```typescript title="climbing_stairs_dp.ts"
[class]{}-[func]{climbingStairsDP}
```
=== "Dart"
```dart title="climbing_stairs_dp.dart"
[class]{}-[func]{climbingStairsDP}
```
=== "Rust"
```rust title="climbing_stairs_dp.rs"
[class]{}-[func]{climbing_stairs_dp}
```
=== "C"
```c title="climbing_stairs_dp.c"
[class]{}-[func]{climbingStairsDP}
```
=== "Kotlin"
```kotlin title="climbing_stairs_dp.kt"
[class]{}-[func]{climbingStairsDP}
```
=== "Ruby"
```ruby title="climbing_stairs_dp.rb"
[class]{}-[func]{climbing_stairs_dp}
```
=== "Zig"
```zig title="climbing_stairs_dp.zig"
[class]{}-[func]{climbingStairsDP}
```
下の図は上記コードの実行プロセスをシミュレートしています。
![階段登りの動的プログラミングプロセス](intro_to_dynamic_programming.assets/climbing_stairs_dp.png){ class="animation-figure" }
<p align="center"> 図 14-5 &nbsp; 階段登りの動的プログラミングプロセス </p>
バックトラッキングアルゴリズムと同様に、動的プログラミングも「状態」の概念を使用して問題解決の特定の段階を表現し、各状態は部分問題とその局所最適解に対応します。例えば、階段登り問題の状態は現在のステップ番号 $i$ として定義されます。
上記の内容に基づいて、動的プログラミングでよく使用される用語をまとめることができます。
- 配列 `dp` は<u>DPテーブル</u>と呼ばれ、$dp[i]$ は状態 $i$ に対応する部分問題の解を表します。
- 最小の部分問題(ステップ $1$ と $2$)に対応する状態は<u>初期状態</u>と呼ばれます。
- 再帰式 $dp[i] = dp[i-1] + dp[i-2]$ は<u>状態遷移方程式</u>と呼ばれます。
## 14.1.4 &nbsp; 空間最適化
注意深い読者は**$dp[i]$ は $dp[i-1]$ と $dp[i-2]$ のみに関連するため、すべての部分問題の解を保存するために配列 `dp` を使用する必要がない**ことに気づくでしょう。単に2つの変数を使って反復的に進めることができます。コードは以下の通りです
=== "Python"
```python title="climbing_stairs_dp.py"
def climbing_stairs_dp_comp(n: int) -> int:
"""階段登り:空間最適化動的プログラミング"""
if n == 1 or n == 2:
return n
a, b = 1, 2
for _ in range(3, n + 1):
a, b = b, a + b
return b
```
=== "C++"
```cpp title="climbing_stairs_dp.cpp"
/* 階段登り:空間最適化動的プログラミング */
int climbingStairsDPComp(int n) {
if (n == 1 || n == 2)
return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int tmp = b;
b = a + b;
a = tmp;
}
return b;
}
```
=== "Java"
```java title="climbing_stairs_dp.java"
/* 階段登り:空間最適化動的プログラミング */
int climbingStairsDPComp(int n) {
if (n == 1 || n == 2)
return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int tmp = b;
b = a + b;
a = tmp;
}
return b;
}
```
=== "C#"
```csharp title="climbing_stairs_dp.cs"
[class]{climbing_stairs_dp}-[func]{ClimbingStairsDPComp}
```
=== "Go"
```go title="climbing_stairs_dp.go"
[class]{}-[func]{climbingStairsDPComp}
```
=== "Swift"
```swift title="climbing_stairs_dp.swift"
[class]{}-[func]{climbingStairsDPComp}
```
=== "JS"
```javascript title="climbing_stairs_dp.js"
[class]{}-[func]{climbingStairsDPComp}
```
=== "TS"
```typescript title="climbing_stairs_dp.ts"
[class]{}-[func]{climbingStairsDPComp}
```
=== "Dart"
```dart title="climbing_stairs_dp.dart"
[class]{}-[func]{climbingStairsDPComp}
```
=== "Rust"
```rust title="climbing_stairs_dp.rs"
[class]{}-[func]{climbing_stairs_dp_comp}
```
=== "C"
```c title="climbing_stairs_dp.c"
[class]{}-[func]{climbingStairsDPComp}
```
=== "Kotlin"
```kotlin title="climbing_stairs_dp.kt"
[class]{}-[func]{climbingStairsDPComp}
```
=== "Ruby"
```ruby title="climbing_stairs_dp.rb"
[class]{}-[func]{climbing_stairs_dp_comp}
```
=== "Zig"
```zig title="climbing_stairs_dp.zig"
[class]{}-[func]{climbingStairsDPComp}
```
上記のコードを観察すると、配列 `dp` が占有していた空間が削除されるため、空間計算量は $O(n)$ から $O(1)$ に削減されます。
多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています**。

View File

@@ -0,0 +1,679 @@
---
comments: true
---
# 14.4 &nbsp; 0-1ナップサック問題
ナップサック問題は動的プログラミングの優れた入門問題であり、動的プログラミングで最も一般的な問題タイプです。0-1ナップサック問題、無制限ナップサック問題、複数ナップサック問題など、多くの変種があります。
このセクションでは、まず最も一般的な0-1ナップサック問題を解決します。
!!! question
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できます。容量制限下でナップサックに入れることができるアイテムの最大値は何ですか
下の図を観察すると、アイテム番号 $i$ は1から数え始め、配列インデックスは0から始まるため、アイテム $i$ の重量は $wgt[i-1]$ に対応し、値は $val[i-1]$ に対応します。
![0-1ナップサックの例データ](knapsack_problem.assets/knapsack_example.png){ class="animation-figure" }
<p align="center"> 図 14-17 &nbsp; 0-1ナップサックの例データ </p>
0-1ナップサック問題を $n$ ラウンドの決定から構成されるプロセスとして考えることができます。各アイテムについて入れない、または入れるという2つの決定があり、したがって問題は決定木モデルに適合します。
この問題の目的は「限られた容量の下でナップサックに入れることができるアイテムの値を最大化する」ことであり、動的プログラミング問題である可能性が高いです。
**第1ステップ各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
各アイテムについて、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。これから状態定義を得ることができます:現在のアイテム番号 $i$ とナップサック容量 $c$、$[i, c]$ と表記されます。
状態 $[i, c]$ は部分問題に対応します:**容量 $c$ のナップサックでの最初の $i$ 個のアイテムの最大値**、$dp[i, c]$ と表記されます。
探している解は $dp[n, cap]$ であるため、サイズ $(n+1) \times (cap+1)$ の二次元 $dp$ テーブルが必要です。
**第2ステップ最適部分構造を特定し、状態遷移方程式を導出する**
アイテム $i$ の決定を行った後、残るのは最初の $i-1$ 個のアイテムの決定の部分問題であり、これは2つのケースに分けることができます。
- **アイテム $i$ を入れない**:ナップサック容量は変わらず、状態は $[i-1, c]$ に変わります。
- **アイテム $i$ を入れる**:ナップサック容量は $wgt[i-1]$ だけ減少し、値は $val[i-1]$ だけ増加し、状態は $[i-1, c-wgt[i-1]]$ に変わります。
上記の分析により、この問題の最適部分構造が明らかになります:**最大値 $dp[i, c]$ は、アイテム $i$ を入れない方案とアイテム $i$ を入れる方案の2つのうち、より大きな値に等しい**。これから状態遷移方程式を導出できます:
$$
dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
$$
現在のアイテムの重量 $wgt[i - 1]$ が残りのナップサック容量 $c$ を超える場合、唯一の選択肢はナップサックに入れないことであることに注意することが重要です。
**第3ステップ境界条件と状態遷移の順序を決定する**
アイテムがない場合またはナップサック容量が $0$ の場合、最大値は $0$ です。つまり、最初の列 $dp[i, 0]$ と最初の行 $dp[0, c]$ はどちらも $0$ に等しいです。
現在の状態 $[i, c]$ は直接上の状態 $[i-1, c]$ と左上の状態 $[i-1, c-wgt[i-1]]$ から遷移するため、2層のループを通じて $dp$ テーブル全体を順序通りに走査します。
上記の分析に従って、次に力任せ探索、メモ化探索、動的プログラミングの順序で解法を実装します。
### 1. &nbsp; 方法1力任せ探索
探索コードには以下の要素が含まれます。
- **再帰パラメータ**:状態 $[i, c]$。
- **戻り値**:部分問題 $dp[i, c]$ の解。
- **終了条件**:アイテム番号が範囲外 $i = 0$ またはナップサックの残り容量が $0$ のとき、再帰を終了し値 $0$ を返す。
- **枝刈り**:現在のアイテムの重量がナップサックの残り容量を超える場合、唯一の選択肢はナップサックに入れないことです。
=== "Python"
```python title="knapsack.py"
def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:
"""0-1 ナップサック:ブルートフォース探索"""
# すべてのアイテムが選択されたかナップサックに残り容量がない場合、値 0 を返す
if i == 0 or c == 0:
return 0
# ナップサック容量を超える場合、ナップサックに入れないことしか選択できない
if wgt[i - 1] > c:
return knapsack_dfs(wgt, val, i - 1, c)
# アイテム i を入れないのと入れるのとの最大値を計算
no = knapsack_dfs(wgt, val, i - 1, c)
yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]
# 2 つの選択肢のうち大きい値を返す
return max(no, yes)
```
=== "C++"
```cpp title="knapsack.cpp"
/* 0-1 ナップサック:ブルートフォース探索 */
int knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {
// すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// アイテム i を入れない場合と入れる場合の最大値を計算
int no = knapsackDFS(wgt, val, i - 1, c);
int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの選択肢のより大きい値を返す
return max(no, yes);
}
```
=== "Java"
```java title="knapsack.java"
/* 0-1 ナップサック:ブルートフォース探索 */
int knapsackDFS(int[] wgt, int[] val, int i, int c) {
// すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// アイテム i を入れない場合と入れる場合の最大値を計算
int no = knapsackDFS(wgt, val, i - 1, c);
int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの選択肢のより大きい値を返す
return Math.max(no, yes);
}
```
=== "C#"
```csharp title="knapsack.cs"
[class]{knapsack}-[func]{KnapsackDFS}
```
=== "Go"
```go title="knapsack.go"
[class]{}-[func]{knapsackDFS}
```
=== "Swift"
```swift title="knapsack.swift"
[class]{}-[func]{knapsackDFS}
```
=== "JS"
```javascript title="knapsack.js"
[class]{}-[func]{knapsackDFS}
```
=== "TS"
```typescript title="knapsack.ts"
[class]{}-[func]{knapsackDFS}
```
=== "Dart"
```dart title="knapsack.dart"
[class]{}-[func]{knapsackDFS}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dfs}
```
=== "C"
```c title="knapsack.c"
[class]{}-[func]{knapsackDFS}
```
=== "Kotlin"
```kotlin title="knapsack.kt"
[class]{}-[func]{knapsackDFS}
```
=== "Ruby"
```ruby title="knapsack.rb"
[class]{}-[func]{knapsack_dfs}
```
=== "Zig"
```zig title="knapsack.zig"
[class]{}-[func]{knapsackDFS}
```
下の図に示すように、各アイテムは選択しないと選択するという2つの探索分岐を生成するため、時間計算量は $O(2^n)$ です。
再帰木を観察すると、$dp[1, 10]$ などの重複する部分問題があることが容易にわかります。アイテムが多く、ナップサック容量が大きい場合、特に同じ重量のアイテムが多い場合、重複する部分問題の数は大幅に増加します。
![0-1ナップサック問題の力任せ探索再帰木](knapsack_problem.assets/knapsack_dfs.png){ class="animation-figure" }
<p align="center"> 図 14-18 &nbsp; 0-1ナップサック問題の力任せ探索再帰木 </p>
### 2. &nbsp; 方法2メモ化探索
重複する部分問題が一度だけ計算されることを確保するために、部分問題の解を記録するメモ化リスト `mem` を使用します。ここで `mem[i][c]` は $dp[i, c]$ に対応します。
メモ化を導入した後、**時間計算量は部分問題の数に依存**し、$O(n \times cap)$ になります。実装コードは以下の通りです:
=== "Python"
```python title="knapsack.py"
def knapsack_dfs_mem(
wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int
) -> int:
"""0-1 ナップサック:記憶化探索"""
# すべてのアイテムが選択されたかナップサックに残り容量がない場合、値 0 を返す
if i == 0 or c == 0:
return 0
# 記録がある場合、それを返す
if mem[i][c] != -1:
return mem[i][c]
# ナップサック容量を超える場合、ナップサックに入れないことしか選択できない
if wgt[i - 1] > c:
return knapsack_dfs_mem(wgt, val, mem, i - 1, c)
# アイテム i を入れないのと入れるのとの最大値を計算
no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)
yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]
# 2 つの選択肢のうち大きい値を記録して返す
mem[i][c] = max(no, yes)
return mem[i][c]
```
=== "C++"
```cpp title="knapsack.cpp"
[class]{}-[func]{knapsackDFSMem}
```
=== "Java"
```java title="knapsack.java"
/* 0-1 ナップサック:メモ化探索 */
int knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {
// すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// 記録がある場合、それを返す
if (mem[i][c] != -1) {
return mem[i][c];
}
// ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, mem, i - 1, c);
}
// アイテム i を入れない場合と入れる場合の最大値を計算
int no = knapsackDFSMem(wgt, val, mem, i - 1, c);
int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの選択肢のより大きい値を記録して返す
mem[i][c] = Math.max(no, yes);
return mem[i][c];
}
```
=== "C#"
```csharp title="knapsack.cs"
[class]{knapsack}-[func]{KnapsackDFSMem}
```
=== "Go"
```go title="knapsack.go"
[class]{}-[func]{knapsackDFSMem}
```
=== "Swift"
```swift title="knapsack.swift"
[class]{}-[func]{knapsackDFSMem}
```
=== "JS"
```javascript title="knapsack.js"
[class]{}-[func]{knapsackDFSMem}
```
=== "TS"
```typescript title="knapsack.ts"
[class]{}-[func]{knapsackDFSMem}
```
=== "Dart"
```dart title="knapsack.dart"
[class]{}-[func]{knapsackDFSMem}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dfs_mem}
```
=== "C"
```c title="knapsack.c"
[class]{}-[func]{knapsackDFSMem}
```
=== "Kotlin"
```kotlin title="knapsack.kt"
[class]{}-[func]{knapsackDFSMem}
```
=== "Ruby"
```ruby title="knapsack.rb"
[class]{}-[func]{knapsack_dfs_mem}
```
=== "Zig"
```zig title="knapsack.zig"
[class]{}-[func]{knapsackDFSMem}
```
下の図はメモ化探索で枝刈りされる探索分岐を示しています。
![0-1ナップサック問題のメモ化探索再帰木](knapsack_problem.assets/knapsack_dfs_mem.png){ class="animation-figure" }
<p align="center"> 図 14-19 &nbsp; 0-1ナップサック問題のメモ化探索再帰木 </p>
### 3. &nbsp; 方法3動的プログラミング
動的プログラミングは本質的に状態遷移中に $dp$ テーブルを埋めることを含みます。コードは下の図に示されています:
=== "Python"
```python title="knapsack.py"
def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:
"""0-1 ナップサック:動的プログラミング"""
n = len(wgt)
# dp テーブルを初期化
dp = [[0] * (cap + 1) for _ in range(n + 1)]
# 状態遷移
for i in range(1, n + 1):
for c in range(1, cap + 1):
if wgt[i - 1] > c:
# ナップサック容量を超える場合、アイテム i を選択しない
dp[i][c] = dp[i - 1][c]
else:
# アイテム i を選択しないのと選択するのとで大きい値
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])
return dp[n][cap]
```
=== "C++"
```cpp title="knapsack.cpp"
/* 0-1 ナップサック:動的プログラミング */
int knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// DPテーブルを初期化
vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));
// 状態遷移
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサックの容量を超える場合、アイテム i を選択しない
dp[i][c] = dp[i - 1][c];
} else {
// 選択しない場合とアイテム i を選択する場合のより大きい値
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[n][cap];
}
```
=== "Java"
```java title="knapsack.java"
/* 0-1 ナップサック:動的プログラミング */
int knapsackDP(int[] wgt, int[] val, int cap) {
int n = wgt.length;
// DPテーブルを初期化
int[][] dp = new int[n + 1][cap + 1];
// 状態遷移
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサックの容量を超える場合、アイテム i を選択しない
dp[i][c] = dp[i - 1][c];
} else {
// 選択しない場合とアイテム i を選択する場合のより大きい値
dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[n][cap];
}
```
=== "C#"
```csharp title="knapsack.cs"
[class]{knapsack}-[func]{KnapsackDP}
```
=== "Go"
```go title="knapsack.go"
[class]{}-[func]{knapsackDP}
```
=== "Swift"
```swift title="knapsack.swift"
[class]{}-[func]{knapsackDP}
```
=== "JS"
```javascript title="knapsack.js"
[class]{}-[func]{knapsackDP}
```
=== "TS"
```typescript title="knapsack.ts"
[class]{}-[func]{knapsackDP}
```
=== "Dart"
```dart title="knapsack.dart"
[class]{}-[func]{knapsackDP}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dp}
```
=== "C"
```c title="knapsack.c"
[class]{}-[func]{knapsackDP}
```
=== "Kotlin"
```kotlin title="knapsack.kt"
[class]{}-[func]{knapsackDP}
```
=== "Ruby"
```ruby title="knapsack.rb"
[class]{}-[func]{knapsack_dp}
```
=== "Zig"
```zig title="knapsack.zig"
[class]{}-[func]{knapsackDP}
```
下の図に示すように、時間計算量と空間計算量の両方が配列 `dp` のサイズ、つまり $O(n \times cap)$ によって決定されます。
=== "<1>"
![0-1ナップサック問題の動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_step1.png){ class="animation-figure" }
=== "<2>"
![knapsack_dp_step2](knapsack_problem.assets/knapsack_dp_step2.png){ class="animation-figure" }
=== "<3>"
![knapsack_dp_step3](knapsack_problem.assets/knapsack_dp_step3.png){ class="animation-figure" }
=== "<4>"
![knapsack_dp_step4](knapsack_problem.assets/knapsack_dp_step4.png){ class="animation-figure" }
=== "<5>"
![knapsack_dp_step5](knapsack_problem.assets/knapsack_dp_step5.png){ class="animation-figure" }
=== "<6>"
![knapsack_dp_step6](knapsack_problem.assets/knapsack_dp_step6.png){ class="animation-figure" }
=== "<7>"
![knapsack_dp_step7](knapsack_problem.assets/knapsack_dp_step7.png){ class="animation-figure" }
=== "<8>"
![knapsack_dp_step8](knapsack_problem.assets/knapsack_dp_step8.png){ class="animation-figure" }
=== "<9>"
![knapsack_dp_step9](knapsack_problem.assets/knapsack_dp_step9.png){ class="animation-figure" }
=== "<10>"
![knapsack_dp_step10](knapsack_problem.assets/knapsack_dp_step10.png){ class="animation-figure" }
=== "<11>"
![knapsack_dp_step11](knapsack_problem.assets/knapsack_dp_step11.png){ class="animation-figure" }
=== "<12>"
![knapsack_dp_step12](knapsack_problem.assets/knapsack_dp_step12.png){ class="animation-figure" }
=== "<13>"
![knapsack_dp_step13](knapsack_problem.assets/knapsack_dp_step13.png){ class="animation-figure" }
=== "<14>"
![knapsack_dp_step14](knapsack_problem.assets/knapsack_dp_step14.png){ class="animation-figure" }
<p align="center"> 図 14-20 &nbsp; 0-1ナップサック問題の動的プログラミングプロセス </p>
### 4. &nbsp; 空間最適化
各状態は上の行の状態のみに関連するため、2つの配列を使用してローリング前進させ、空間計算量を $O(n^2)$ から $O(n)$ に削減できます。
さらに考えてみると、1つの配列だけで空間最適化を達成できるでしょうか各状態が直接上のセルまたは左上のセルから遷移することが観察できます。配列が1つしかない場合、$i$ 行目の走査を開始するとき、その配列はまだ $i-1$ 行目の状態を保存しています。
- 通常の順序で走査する場合、$dp[i, j]$ に走査したとき、左上の $dp[i-1, 1]$ $dp[i-1, j-1]$ の値がすでに上書きされている可能性があり、正しい状態遷移結果を得ることができません。
- 逆順で走査する場合、上書き問題はなく、状態遷移を正しく実行できます。
下の図は単一配列での $i = 1$ 行目から $i = 2$ 行目への遷移プロセスを示しています。通常順序走査と逆順走査の違いについて考えてみてください。
=== "<1>"
![0-1ナップサックの空間最適化動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_comp_step1.png){ class="animation-figure" }
=== "<2>"
![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png){ class="animation-figure" }
=== "<3>"
![knapsack_dp_comp_step3](knapsack_problem.assets/knapsack_dp_comp_step3.png){ class="animation-figure" }
=== "<4>"
![knapsack_dp_comp_step4](knapsack_problem.assets/knapsack_dp_comp_step4.png){ class="animation-figure" }
=== "<5>"
![knapsack_dp_comp_step5](knapsack_problem.assets/knapsack_dp_comp_step5.png){ class="animation-figure" }
=== "<6>"
![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png){ class="animation-figure" }
<p align="center"> 図 14-21 &nbsp; 0-1ナップサックの空間最適化動的プログラミングプロセス </p>
コード実装では、配列 `dp` の最初の次元 $i$ を削除し、内側のループを逆走査に変更するだけです:
=== "Python"
```python title="knapsack.py"
def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:
"""0-1 ナップサック:空間最適化動的プログラミング"""
n = len(wgt)
# dp テーブルを初期化
dp = [0] * (cap + 1)
# 状態遷移
for i in range(1, n + 1):
# 逆順で走査
for c in range(cap, 0, -1):
if wgt[i - 1] > c:
# ナップサック容量を超える場合、アイテム i を選択しない
dp[c] = dp[c]
else:
# アイテム i を選択しないのと選択するのとで大きい値
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])
return dp[cap]
```
=== "C++"
```cpp title="knapsack.cpp"
/* 0-1 ナップサック:空間最適化動的プログラミング */
int knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// DPテーブルを初期化
vector<int> dp(cap + 1, 0);
// 状態遷移
for (int i = 1; i <= n; i++) {
// 逆順で走査
for (int c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 選択しない場合とアイテム i を選択する場合のより大きい値
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
```
=== "Java"
```java title="knapsack.java"
/* 0-1 ナップサック:空間最適化動的プログラミング */
int knapsackDPComp(int[] wgt, int[] val, int cap) {
int n = wgt.length;
// DPテーブルを初期化
int[] dp = new int[cap + 1];
// 状態遷移
for (int i = 1; i <= n; i++) {
// 逆順で走査
for (int c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 選択しない場合とアイテム i を選択する場合のより大きい値
dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
```
=== "C#"
```csharp title="knapsack.cs"
[class]{knapsack}-[func]{KnapsackDPComp}
```
=== "Go"
```go title="knapsack.go"
[class]{}-[func]{knapsackDPComp}
```
=== "Swift"
```swift title="knapsack.swift"
[class]{}-[func]{knapsackDPComp}
```
=== "JS"
```javascript title="knapsack.js"
[class]{}-[func]{knapsackDPComp}
```
=== "TS"
```typescript title="knapsack.ts"
[class]{}-[func]{knapsackDPComp}
```
=== "Dart"
```dart title="knapsack.dart"
[class]{}-[func]{knapsackDPComp}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dp_comp}
```
=== "C"
```c title="knapsack.c"
[class]{}-[func]{knapsackDPComp}
```
=== "Kotlin"
```kotlin title="knapsack.kt"
[class]{}-[func]{knapsackDPComp}
```
=== "Ruby"
```ruby title="knapsack.rb"
[class]{}-[func]{knapsack_dp_comp}
```
=== "Zig"
```zig title="knapsack.zig"
[class]{}-[func]{knapsackDPComp}
```

View File

@@ -0,0 +1,27 @@
---
comments: true
---
# 14.7 &nbsp; まとめ
- 動的プログラミングは問題を分解し、部分問題の解を保存することで冗長な計算を避け、計算効率を向上させます。
- 時間を考慮しなければ、すべての動的プログラミング問題はバックトラッキング(力任せ探索)を使用して解決できますが、再帰木には多くの重複する部分問題があり、効率が非常に低くなります。記憶化リストを導入することで、計算されたすべての部分問題の解を保存し、重複する部分問題が一度だけ計算されることを保証できます。
- 記憶化探索はトップダウンの再帰解法であり、動的プログラミングはボトムアップの反復アプローチに対応し、「表を埋める」ことに似ています。現在の状態は特定の局所状態のみに依存するため、dpテーブルの1次元を削除して空間計算量を削減できます。
- 部分問題の分解は汎用的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングで特徴が異なります。
- 動的プログラミング問題には3つの主要な特徴があります重複する部分問題、最適部分構造、無記憶性。
- 元の問題の最適解がその部分問題の最適解から構築できる場合、最適部分構造を持ちます。
- 無記憶性とは、状態の将来の発展が現在の状態のみに依存し、過去に経験したすべての状態に依存しないことを意味します。多くの組み合わせ最適化問題にはこの特性がなく、動的プログラミングを使用して迅速に解決することはできません。
**ナップサック問題**
- ナップサック問題は最も典型的な動的プログラミング問題の1つで、0-1ナップサック、無制限ナップサック、複数ナップサックなどの変種があります。
- 0-1ナップサックの状態定義は、最初の $i$ 個のアイテムを含む容量 $c$ のナップサックでの最大値です。アイテムをナップサックに入れないまたは入れるという決定に基づいて、最適部分構造を特定し、状態遷移方程式を構築できます。空間最適化では、各状態が直接上と左上の状態に依存するため、左上の状態の上書きを避けるためにリストを逆順で走査する必要があります。
- 無制限ナップサック問題では、各種類のアイテムを選択できる数に制限がないため、アイテムを含める状態遷移は0-1ナップサックと異なります。状態が直接上と左の状態に依存するため、空間最適化では前方走査を含める必要があります。
- コイン交換問題は無制限ナップサック問題の変種で、「最大」値を求めることから「最小」コイン数を求めることに変わり、状態遷移方程式は $\max()$ を $\min()$ に変更する必要があります。ナップサックの容量を「超えない」ことを追求することから、正確に目標金額を求めることに変わり、「目標金額を構成できない」無効解を表すために $amt + 1$ を使用します。
- コイン交換問題IIは「最小コイン数」を求めることから「コインの組み合わせ数」を求めることに変わり、状態遷移方程式を $\min()$ から和算演算子に変更します。
**編集距離問題**
- 編集距離レーベンシュタイン距離は2つの文字列間の類似度を測定し、一つの文字列を別の文字列に変更するために必要な最小編集ステップ数として定義され、編集操作には追加、削除、置換が含まれます。
- 編集距離問題の状態定義は、$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集ステップ数です。$s[i] \ne t[j]$ の場合、追加、削除、置換の3つの決定があり、それぞれに対応する残余部分問題があります。これから最適部分構造を特定し、状態遷移方程式を構築できます。$s[i] = t[j]$ の場合、現在の文字の編集は必要ありません。
- 編集距離では、状態が直接上、左、左上の状態に依存します。したがって、空間最適化後、前方走査も逆走査も正しく状態遷移を実行できません。これに対処するため、変数を使用して左上の状態を一時的に保存し、無制限ナップサック問題の状況と同等にし、空間最適化後に前方走査を可能にします。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
---
comments: true
---
# 9.1 &nbsp; グラフ
<u>グラフ</u>は非線形データ構造の一種で、<u>頂点</u>と<u>辺</u>で構成されます。グラフ$G$は、頂点の集合$V$と辺の集合$E$の組み合わせとして抽象的に表現できます。以下の例は、5つの頂点と7つの辺を含むグラフを示しています。
$$
\begin{aligned}
V & = \{ 1, 2, 3, 4, 5 \} \newline
E & = \{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \} \newline
G & = \{ V, E \} \newline
\end{aligned}
$$
頂点をノード、辺をノードを接続する参照(ポインタ)と見なすと、グラフは連結リストから拡張されたデータ構造として見ることができます。下図に示すように、**線形関係(連結リスト)や分割統治関係(木)と比較して、ネットワーク関係(グラフ)は自由度が高いため、より複雑です**。
![連結リスト、木、グラフの関係](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" }
<p align="center"> 図 9-1 &nbsp; 連結リスト、木、グラフの関係 </p>
## 9.1.1 &nbsp; グラフの一般的な種類と用語
グラフは、辺に方向があるかどうかによって<u>無向グラフ</u>と<u>有向グラフ</u>に分けることができます(下図参照)。
- 無向グラフでは、辺は2つの頂点間の「双方向」接続を表します。例えば、Facebookの「友達」関係です。
- 有向グラフでは、辺に方向性があります。つまり、辺$A \rightarrow B$と$A \leftarrow B$は互いに独立しています。例えば、InstagramやTikTokの「フォロー」と「フォロワー」の関係です。
![有向グラフと無向グラフ](graph.assets/directed_graph.png){ class="animation-figure" }
<p align="center"> 図 9-2 &nbsp; 有向グラフと無向グラフ </p>
すべての頂点が接続されているかどうかによって、グラフは<u>連結グラフ</u>と<u>非連結グラフ</u>に分けることができます(下図参照)。
- 連結グラフでは、任意の頂点から開始して他の任意の頂点に到達することが可能です。
- 非連結グラフでは、任意の開始頂点から到達できない頂点が少なくとも1つ存在します。
![連結グラフと非連結グラフ](graph.assets/connected_graph.png){ class="animation-figure" }
<p align="center"> 図 9-3 &nbsp; 連結グラフと非連結グラフ </p>
辺に重み変数を追加することもでき、その結果として<u>重み付きグラフ</u>が生まれます下図参照。例えば、Instagramでは、システムがあなたと他のユーザーとの間の相互作用レベルいいね、閲覧、コメントなどによってフォロワーとフォロー中のリストをソートします。このような相互作用ネットワークは重み付きグラフで表現できます。
![重み付きグラフと重みなしグラフ](graph.assets/weighted_graph.png){ class="animation-figure" }
<p align="center"> 図 9-4 &nbsp; 重み付きグラフと重みなしグラフ </p>
グラフデータ構造には、以下のような一般的に使用される用語があります。
- <u>隣接</u>2つの頂点を接続する辺がある場合、これら2つの頂点は「隣接」していると言われます。上図では、頂点1の隣接頂点は頂点2、3、5です。
- <u>パス</u>頂点Aから頂点Bまでに通過する辺のシーケンスを、AからBへのパスと呼びます。上図では、辺のシーケンス1-5-2-4は頂点1から頂点4へのパスです。
- <u>次数</u>:頂点が持つ辺の数です。有向グラフの場合、<u>入次数</u>はその頂点を指す辺の数、<u>出次数</u>はその頂点から出る辺の数を指します。
## 9.1.2 &nbsp; グラフの表現
グラフの一般的な表現には「隣接行列」と「隣接リスト」があります。以下の例では無向グラフを使用します。
### 1. &nbsp; 隣接行列
グラフの頂点数を$n$とすると、<u>隣接行列</u>は$n \times n$の行列を使用してグラフを表現します。各行は頂点を表し、行列要素は辺を表し、2つの頂点間に辺があるかどうかを$1$または$0$で示します。
下図に示すように、隣接行列を$M$、頂点のリストを$V$とすると、行列要素$M[i, j] = 1$は頂点$V[i]$と頂点$V[j]$の間に辺があることを示し、逆に$M[i, j] = 0$は2つの頂点間に辺がないことを示します。
![隣接行列によるグラフの表現](graph.assets/adjacency_matrix.png){ class="animation-figure" }
<p align="center"> 図 9-5 &nbsp; 隣接行列によるグラフの表現 </p>
隣接行列には以下の特性があります。
- 頂点は自分自身に接続することはできないため、隣接行列の主対角線上の要素は意味がありません。
- 無向グラフの場合、両方向の辺は等価であるため、隣接行列は主対角線に関して対称です。
- 隣接行列の要素を$1$と$0$から重みに置き換えることで、重み付きグラフを表現できます。
隣接行列でグラフを表現する場合、行列要素に直接アクセスして辺を取得できるため、追加、削除、検索、変更の操作が効率的で、すべて時間計算量$O(1)$です。ただし、行列の空間計算量は$O(n^2)$で、より多くのメモリを消費します。
### 2. &nbsp; 隣接リスト
<u>隣接リスト</u>は$n$個の連結リストを使用してグラフを表現し、各連結リストノードは頂点を表します。$i$番目の連結リストは頂点$i$に対応し、すべての隣接頂点(その頂点に接続された頂点)を含みます。下図は隣接リストを使用して格納されたグラフの例を示しています。
![隣接リストによるグラフの表現](graph.assets/adjacency_list.png){ class="animation-figure" }
<p align="center"> 図 9-6 &nbsp; 隣接リストによるグラフの表現 </p>
隣接リストは実際の辺のみを格納し、辺の総数は$n^2$よりもはるかに少ないことが多く、より空間効率的です。ただし、隣接リストで辺を見つけるには連結リストを走査する必要があるため、その時間効率は隣接行列ほど良くありません。
上図を観察すると、**隣接リストの構造はハッシュテーブルの「チェイン法」と非常に似ているため、同様の方法を使用して効率を最適化できます**。例えば、連結リストが長い場合、それをAVL木や赤黒木に変換して、時間効率を$O(n)$から$O(\log n)$に最適化できます。連結リストをハッシュテーブルに変換することで、時間計算量を$O(1)$に削減することもできます。
## 9.1.3 &nbsp; グラフの一般的な応用
下表に示すように、多くの現実世界のシステムはグラフでモデル化でき、対応する問題はグラフ計算問題に削減できます。
<p align="center"> 表 9-1 &nbsp; 現実生活の一般的なグラフ </p>
<div class="center-table" markdown>
| | 頂点 | 辺 | グラフ計算問題 |
| -------------- | -------------- | -------------------------------- | --------------------------- |
| ソーシャルネットワーク | ユーザー | フォロー / フォロワー関係 | 潜在的フォロー推薦 |
| 地下鉄路線 | 駅 | 駅間の接続性 | 最短ルート推薦 |
| 太陽系 | 天体 | 天体間の重力 | 惑星軌道計算 |
</div>

View File

@@ -0,0 +1,698 @@
---
comments: true
---
# 9.2 &nbsp; グラフの基本操作
グラフの基本操作は「辺」に対する操作と「頂点」に対する操作に分けることができます。「隣接行列」と「隣接リスト」の2つの表現方法の下では、実装が異なります。
## 9.2.1 &nbsp; 隣接行列に基づく実装
$n$個の頂点を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装されます。
- **辺の追加または削除**:隣接行列内の指定された辺を直接変更し、$O(1)$時間を使用します。無向グラフであるため、両方向の辺を同時に更新する必要があります。
- **頂点の追加**:隣接行列の末尾に行と列を追加し、すべて$0$で埋めます。$O(n)$時間を使用します。
- **頂点の削除**:隣接行列内の行と列を削除します。最悪の場合は最初の行と列が削除されるときで、$(n-1)^2$個の要素を「上と左に移動」する必要があり、$O(n^2)$時間を使用します。
- **初期化**$n$個の頂点を渡し、長さ$n$の頂点リスト`vertices`を初期化し、$O(n)$時間を使用します。$n \times n$サイズの隣接行列`adjMat`を初期化し、$O(n^2)$時間を使用します。
=== "隣接行列の初期化"
![隣接行列での初期化、辺の追加と削除、頂点の追加と削除](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" }
=== "辺の追加"
![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_step2_add_edge.png){ class="animation-figure" }
=== "辺の削除"
![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_step3_remove_edge.png){ class="animation-figure" }
=== "頂点の追加"
![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_step4_add_vertex.png){ class="animation-figure" }
=== "頂点の削除"
![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_step5_remove_vertex.png){ class="animation-figure" }
<p align="center"> 図 9-7 &nbsp; 隣接行列での初期化、辺の追加と削除、頂点の追加と削除 </p>
以下は隣接行列を使用して表現されたグラフの実装コードです:
=== "Python"
```python title="graph_adjacency_matrix.py"
class GraphAdjMat:
"""隣接行列に基づく無向グラフクラス"""
def __init__(self, vertices: list[int], edges: list[list[int]]):
"""コンストラクタ"""
# 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す
self.vertices: list[int] = []
# 隣接行列、行と列のインデックスは「頂点インデックス」に対応
self.adj_mat: list[list[int]] = []
# 頂点を追加
for val in vertices:
self.add_vertex(val)
# 辺を追加
# edges要素は頂点インデックスを表す
for e in edges:
self.add_edge(e[0], e[1])
def size(self) -> int:
"""頂点数を取得"""
return len(self.vertices)
def add_vertex(self, val: int):
"""頂点を追加"""
n = self.size()
# 頂点リストに新しい頂点値を追加
self.vertices.append(val)
# 隣接行列に行を追加
new_row = [0] * n
self.adj_mat.append(new_row)
# 隣接行列に列を追加
for row in self.adj_mat:
row.append(0)
def remove_vertex(self, index: int):
"""頂点を削除"""
if index >= self.size():
raise IndexError()
# 頂点リストから`index`の頂点を削除
self.vertices.pop(index)
# 隣接行列から`index`の行を削除
self.adj_mat.pop(index)
# 隣接行列から`index`の列を削除
for row in self.adj_mat:
row.pop(index)
def add_edge(self, i: int, j: int):
"""辺を追加"""
# パラメータi、jは頂点要素のインデックスに対応
# インデックスの範囲外と等価性を処理
if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:
raise IndexError()
# 無向グラフでは、隣接行列は主対角線について対称、すなわち (i, j) == (j, i) を満たす
self.adj_mat[i][j] = 1
self.adj_mat[j][i] = 1
def remove_edge(self, i: int, j: int):
"""辺を削除"""
# パラメータi、jは頂点要素のインデックスに対応
# インデックスの範囲外と等価性を処理
if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:
raise IndexError()
self.adj_mat[i][j] = 0
self.adj_mat[j][i] = 0
def print(self):
"""隣接行列を出力"""
print("頂点リスト =", self.vertices)
print("隣接行列 =")
print_matrix(self.adj_mat)
```
=== "C++"
```cpp title="graph_adjacency_matrix.cpp"
/* 隣接行列に基づく無向グラフクラス */
class GraphAdjMat {
vector<int> vertices; // 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す
vector<vector<int>> adjMat; // 隣接行列、行と列のインデックスは「頂点インデックス」に対応
public:
/* コンストラクタ */
GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {
// 頂点を追加
for (int val : vertices) {
addVertex(val);
}
// 辺を追加
// 辺の要素は頂点インデックスを表す
for (const vector<int> &edge : edges) {
addEdge(edge[0], edge[1]);
}
}
/* 頂点数を取得 */
int size() const {
return vertices.size();
}
/* 頂点を追加 */
void addVertex(int val) {
int n = size();
// 頂点リストに新しい頂点値を追加
vertices.push_back(val);
// 隣接行列に行を追加
adjMat.emplace_back(vector<int>(n, 0));
// 隣接行列に列を追加
for (vector<int> &row : adjMat) {
row.push_back(0);
}
}
/* 頂点を削除 */
void removeVertex(int index) {
if (index >= size()) {
throw out_of_range("Vertex does not exist");
}
// 頂点リストから`index`の頂点を削除
vertices.erase(vertices.begin() + index);
// 隣接行列から`index`の行を削除
adjMat.erase(adjMat.begin() + index);
// 隣接行列から`index`の列を削除
for (vector<int> &row : adjMat) {
row.erase(row.begin() + index);
}
}
/* 辺を追加 */
// パラメータi、jは頂点要素のインデックスに対応
void addEdge(int i, int j) {
// インデックス範囲外と等価性を処理
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
throw out_of_range("Vertex does not exist");
}
// 無向グラフでは、隣接行列は主対角線について対称、即ち(i, j) == (j, i)を満たす
adjMat[i][j] = 1;
adjMat[j][i] = 1;
}
/* 辺を削除 */
// パラメータi、jは頂点要素のインデックスに対応
void removeEdge(int i, int j) {
// インデックス範囲外と等価性を処理
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
throw out_of_range("Vertex does not exist");
}
adjMat[i][j] = 0;
adjMat[j][i] = 0;
}
/* 隣接行列を印刷 */
void print() {
cout << "頂点リスト = ";
printVector(vertices);
cout << "隣接行列 =" << endl;
printVectorMatrix(adjMat);
}
};
```
=== "Java"
```java title="graph_adjacency_matrix.java"
/* 隣接行列に基づく無向グラフクラス */
class GraphAdjMat {
List<Integer> vertices; // 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す
List<List<Integer>> adjMat; // 隣接行列、行と列のインデックスは「頂点インデックス」に対応
/* コンストラクタ */
public GraphAdjMat(int[] vertices, int[][] edges) {
this.vertices = new ArrayList<>();
this.adjMat = new ArrayList<>();
// 頂点を追加
for (int val : vertices) {
addVertex(val);
}
// 辺を追加
// 辺の要素は頂点インデックスを表す
for (int[] e : edges) {
addEdge(e[0], e[1]);
}
}
/* 頂点数を取得 */
public int size() {
return vertices.size();
}
/* 頂点を追加 */
public void addVertex(int val) {
int n = size();
// 頂点リストに新しい頂点値を追加
vertices.add(val);
// 隣接行列に行を追加
List<Integer> newRow = new ArrayList<>(n);
for (int j = 0; j < n; j++) {
newRow.add(0);
}
adjMat.add(newRow);
// 隣接行列に列を追加
for (List<Integer> row : adjMat) {
row.add(0);
}
}
/* 頂点を削除 */
public void removeVertex(int index) {
if (index >= size())
throw new IndexOutOfBoundsException();
// 頂点リストから `index` の頂点を削除
vertices.remove(index);
// 隣接行列から `index` の行を削除
adjMat.remove(index);
// 隣接行列から `index` の列を削除
for (List<Integer> row : adjMat) {
row.remove(index);
}
}
/* 辺を追加 */
// パラメータ i、j は頂点要素のインデックスに対応
public void addEdge(int i, int j) {
// インデックスの範囲外と等価性を処理
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)
throw new IndexOutOfBoundsException();
// 無向グラフでは、隣接行列は主対角線について対称、すなわち (i, j) == (j, i) を満たす
adjMat.get(i).set(j, 1);
adjMat.get(j).set(i, 1);
}
/* 辺を削除 */
// パラメータ i、j は頂点要素のインデックスに対応
public void removeEdge(int i, int j) {
// インデックスの範囲外と等価性を処理
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)
throw new IndexOutOfBoundsException();
adjMat.get(i).set(j, 0);
adjMat.get(j).set(i, 0);
}
/* 隣接行列を出力 */
public void print() {
System.out.print("頂点リスト = ");
System.out.println(vertices);
System.out.println("隣接行列 =");
PrintUtil.printMatrix(adjMat);
}
}
```
=== "C#"
```csharp title="graph_adjacency_matrix.cs"
[class]{GraphAdjMat}-[func]{}
```
=== "Go"
```go title="graph_adjacency_matrix.go"
[class]{graphAdjMat}-[func]{}
```
=== "Swift"
```swift title="graph_adjacency_matrix.swift"
[class]{GraphAdjMat}-[func]{}
```
=== "JS"
```javascript title="graph_adjacency_matrix.js"
[class]{GraphAdjMat}-[func]{}
```
=== "TS"
```typescript title="graph_adjacency_matrix.ts"
[class]{GraphAdjMat}-[func]{}
```
=== "Dart"
```dart title="graph_adjacency_matrix.dart"
[class]{GraphAdjMat}-[func]{}
```
=== "Rust"
```rust title="graph_adjacency_matrix.rs"
[class]{GraphAdjMat}-[func]{}
```
=== "C"
```c title="graph_adjacency_matrix.c"
[class]{GraphAdjMat}-[func]{}
```
=== "Kotlin"
```kotlin title="graph_adjacency_matrix.kt"
[class]{GraphAdjMat}-[func]{}
```
=== "Ruby"
```ruby title="graph_adjacency_matrix.rb"
[class]{GraphAdjMat}-[func]{}
```
=== "Zig"
```zig title="graph_adjacency_matrix.zig"
[class]{GraphAdjMat}-[func]{}
```
## 9.2.2 &nbsp; 隣接リストに基づく実装
総計$n$個の頂点と$m$個の辺を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装できます。
- **辺の追加**:対応する頂点の連結リストの末尾に辺を追加するだけで、$O(1)$時間を使用します。無向グラフであるため、両方向に同時に辺を追加する必要があります。
- **辺の削除**:対応する頂点の連結リスト内で指定された辺を見つけて削除し、$O(m)$時間を使用します。無向グラフでは、両方向の辺を同時に削除する必要があります。
- **頂点の追加**:隣接リストに連結リストを追加し、新しい頂点をリストのヘッドノードにし、$O(1)$時間を使用します。
- **頂点の削除**:隣接リスト全体を走査し、指定された頂点を含むすべての辺を削除する必要があり、$O(n + m)$時間を使用します。
- **初期化**:隣接リストに$n$個の頂点と$2m$個の辺を作成し、$O(n + m)$時間を使用します。
=== "隣接リストの初期化"
![隣接リストでの初期化、辺の追加と削除、頂点の追加と削除](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" }
=== "辺の追加"
![adjacency_list_add_edge](graph_operations.assets/adjacency_list_step2_add_edge.png){ class="animation-figure" }
=== "辺の削除"
![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_step3_remove_edge.png){ class="animation-figure" }
=== "頂点の追加"
![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_step4_add_vertex.png){ class="animation-figure" }
=== "頂点の削除"
![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png){ class="animation-figure" }
<p align="center"> 図 9-8 &nbsp; 隣接リストでの初期化、辺の追加と削除、頂点の追加と削除 </p>
以下は隣接リストのコード実装です。上図と比較して、実際のコードには以下の違いがあります。
- 頂点の追加と削除の便宜、およびコードの簡素化のため、連結リストの代わりにリスト(動的配列)を使用します。
- ハッシュテーブルを使用して隣接リストを格納し、`key`が頂点インスタンス、`value`がその頂点の隣接頂点のリスト(連結リスト)です。
さらに、隣接リストで頂点を表現するために`Vertex`クラスを使用します。その理由は:隣接行列のようにリストインデックスを使用して異なる頂点を区別する場合、インデックス$i$の頂点を削除したい場合、隣接リスト全体を走査し、$i$より大きいすべてのインデックスを1つずつ減少させる必要があり、これは非常に非効率的です。しかし、各頂点が一意の`Vertex`インスタンスである場合、頂点を削除しても他の頂点に変更を加える必要がありません。
=== "Python"
```python title="graph_adjacency_list.py"
class GraphAdjList:
"""隣接リストに基づく無向グラフクラス"""
def __init__(self, edges: list[list[Vertex]]):
"""コンストラクタ"""
# 隣接リスト、キー: 頂点、値: その頂点の隣接する全頂点
self.adj_list = dict[Vertex, list[Vertex]]()
# すべての頂点と辺を追加
for edge in edges:
self.add_vertex(edge[0])
self.add_vertex(edge[1])
self.add_edge(edge[0], edge[1])
def size(self) -> int:
"""頂点数を取得"""
return len(self.adj_list)
def add_edge(self, vet1: Vertex, vet2: Vertex):
"""辺を追加"""
if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:
raise ValueError()
# 辺 vet1 - vet2 を追加
self.adj_list[vet1].append(vet2)
self.adj_list[vet2].append(vet1)
def remove_edge(self, vet1: Vertex, vet2: Vertex):
"""辺を削除"""
if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:
raise ValueError()
# 辺 vet1 - vet2 を削除
self.adj_list[vet1].remove(vet2)
self.adj_list[vet2].remove(vet1)
def add_vertex(self, vet: Vertex):
"""頂点を追加"""
if vet in self.adj_list:
return
# 隣接リストに新しい連結リストを追加
self.adj_list[vet] = []
def remove_vertex(self, vet: Vertex):
"""頂点を削除"""
if vet not in self.adj_list:
raise ValueError()
# 隣接リストから頂点vetに対応する連結リストを削除
self.adj_list.pop(vet)
# 他の頂点の連結リストを走査し、vetを含むすべての辺を削除
for vertex in self.adj_list:
if vet in self.adj_list[vertex]:
self.adj_list[vertex].remove(vet)
def print(self):
"""隣接リストを出力"""
print("隣接リスト =")
for vertex in self.adj_list:
tmp = [v.val for v in self.adj_list[vertex]]
print(f"{vertex.val}: {tmp},")
```
=== "C++"
```cpp title="graph_adjacency_list.cpp"
/* 隣接リストに基づく無向グラフクラス */
class GraphAdjList {
public:
// 隣接リスト、キー:頂点、値:その頂点のすべての隣接頂点
unordered_map<Vertex *, vector<Vertex *>> adjList;
/* ベクターから指定されたノードを削除 */
void remove(vector<Vertex *> &vec, Vertex *vet) {
for (int i = 0; i < vec.size(); i++) {
if (vec[i] == vet) {
vec.erase(vec.begin() + i);
break;
}
}
}
/* コンストラクタ */
GraphAdjList(const vector<vector<Vertex *>> &edges) {
// すべての頂点と辺を追加
for (const vector<Vertex *> &edge : edges) {
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
}
}
/* 頂点数を取得 */
int size() {
return adjList.size();
}
/* 辺を追加 */
void addEdge(Vertex *vet1, Vertex *vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("Vertex does not exist");
// 辺 vet1 - vet2 を追加
adjList[vet1].push_back(vet2);
adjList[vet2].push_back(vet1);
}
/* 辺を削除 */
void removeEdge(Vertex *vet1, Vertex *vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("Vertex does not exist");
// 辺 vet1 - vet2 を削除
remove(adjList[vet1], vet2);
remove(adjList[vet2], vet1);
}
/* 頂点を追加 */
void addVertex(Vertex *vet) {
if (adjList.count(vet))
return;
// 隣接リストに新しい連結リストを追加
adjList[vet] = vector<Vertex *>();
}
/* 頂点を削除 */
void removeVertex(Vertex *vet) {
if (!adjList.count(vet))
throw invalid_argument("Vertex does not exist");
// 隣接リストから頂点vetに対応する連結リストを削除
adjList.erase(vet);
// 他の頂点の連結リストを走査し、vetを含むすべての辺を削除
for (auto &adj : adjList) {
remove(adj.second, vet);
}
}
/* 隣接リストを印刷 */
void print() {
cout << "隣接リスト =" << endl;
for (auto &adj : adjList) {
const auto &key = adj.first;
const auto &vec = adj.second;
cout << key->val << ": ";
printVector(vetsToVals(vec));
}
}
};
```
=== "Java"
```java title="graph_adjacency_list.java"
/* 隣接リストに基づく無向グラフクラス */
class GraphAdjList {
// 隣接リスト、キー: 頂点、値: その頂点のすべての隣接頂点
Map<Vertex, List<Vertex>> adjList;
/* コンストラクタ */
public GraphAdjList(Vertex[][] edges) {
this.adjList = new HashMap<>();
// すべての頂点と辺を追加
for (Vertex[] edge : edges) {
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
}
}
/* 頂点数を取得 */
public int size() {
return adjList.size();
}
/* 辺を追加 */
public void addEdge(Vertex vet1, Vertex vet2) {
if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)
throw new IllegalArgumentException();
// 辺 vet1 - vet2 を追加
adjList.get(vet1).add(vet2);
adjList.get(vet2).add(vet1);
}
/* 辺を削除 */
public void removeEdge(Vertex vet1, Vertex vet2) {
if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)
throw new IllegalArgumentException();
// 辺 vet1 - vet2 を削除
adjList.get(vet1).remove(vet2);
adjList.get(vet2).remove(vet1);
}
/* 頂点を追加 */
public void addVertex(Vertex vet) {
if (adjList.containsKey(vet))
return;
// 隣接リストに新しい連結リストを追加
adjList.put(vet, new ArrayList<>());
}
/* 頂点を削除 */
public void removeVertex(Vertex vet) {
if (!adjList.containsKey(vet))
throw new IllegalArgumentException();
// 隣接リストから頂点 vet に対応する連結リストを削除
adjList.remove(vet);
// 他の頂点の連結リストを走査し、vet を含むすべての辺を削除
for (List<Vertex> list : adjList.values()) {
list.remove(vet);
}
}
/* 隣接リストを出力 */
public void print() {
System.out.println("隣接リスト =");
for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {
List<Integer> tmp = new ArrayList<>();
for (Vertex vertex : pair.getValue())
tmp.add(vertex.val);
System.out.println(pair.getKey().val + ": " + tmp + ",");
}
}
}
```
=== "C#"
```csharp title="graph_adjacency_list.cs"
[class]{GraphAdjList}-[func]{}
```
=== "Go"
```go title="graph_adjacency_list.go"
[class]{graphAdjList}-[func]{}
```
=== "Swift"
```swift title="graph_adjacency_list.swift"
[class]{GraphAdjList}-[func]{}
```
=== "JS"
```javascript title="graph_adjacency_list.js"
[class]{GraphAdjList}-[func]{}
```
=== "TS"
```typescript title="graph_adjacency_list.ts"
[class]{GraphAdjList}-[func]{}
```
=== "Dart"
```dart title="graph_adjacency_list.dart"
[class]{GraphAdjList}-[func]{}
```
=== "Rust"
```rust title="graph_adjacency_list.rs"
[class]{GraphAdjList}-[func]{}
```
=== "C"
```c title="graph_adjacency_list.c"
[class]{AdjListNode}-[func]{}
[class]{GraphAdjList}-[func]{}
```
=== "Kotlin"
```kotlin title="graph_adjacency_list.kt"
[class]{GraphAdjList}-[func]{}
```
=== "Ruby"
```ruby title="graph_adjacency_list.rb"
[class]{GraphAdjList}-[func]{}
```
=== "Zig"
```zig title="graph_adjacency_list.zig"
[class]{GraphAdjList}-[func]{}
```
## 9.2.3 &nbsp; 効率の比較
グラフに$n$個の頂点と$m$個の辺があると仮定すると、下表は隣接行列と隣接リストの時間効率と空間効率を比較しています。
<p align="center"> 表 9-2 &nbsp; 隣接行列と隣接リストの比較 </p>
<div class="center-table" markdown>
| | 隣接行列 | 隣接リスト(連結リスト) | 隣接リスト(ハッシュテーブル) |
| ---------------- | -------------- | ----------------------- | ----------------------------- |
| 隣接性の判定 | $O(1)$ | $O(m)$ | $O(1)$ |
| 辺の追加 | $O(1)$ | $O(1)$ | $O(1)$ |
| 辺の削除 | $O(1)$ | $O(m)$ | $O(1)$ |
| 頂点の追加 | $O(n)$ | $O(1)$ | $O(1)$ |
| 頂点の削除 | $O(n^2)$ | $O(n + m)$ | $O(n)$ |
| メモリ空間使用量 | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ |
</div>
上表を観察すると、隣接リスト(ハッシュテーブル)が最高の時間効率と空間効率を持っているように見えます。しかし、実際には、隣接行列での辺に対する操作がより効率的で、単一の配列アクセスまたは代入操作のみが必要です。全体的に、隣接行列は「空間と時間のトレードオフ」の原則を例示し、隣接リストは「時間と空間のトレードオフ」を例示しています。

View File

@@ -0,0 +1,469 @@
---
comments: true
---
# 9.3 &nbsp; グラフ走査
木は「一対多」の関係を表現し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木をグラフの特別なケースと見なすことができます。明らかに、**木の走査操作もグラフ走査操作の特別なケースです**。
グラフと木の両方で、走査操作を実装するために探索アルゴリズムの応用が必要です。グラフ走査は2つのタイプに分けることができます<u>幅優先探索BFS</u>と<u>深さ優先探索DFS</u>です。
## 9.3.1 &nbsp; 幅優先探索
**幅優先探索は近くから遠くへの走査方法で、ある頂点から開始し、常に最も近い頂点を優先的に訪問し、層ごとに外側に展開していきます**。下図に示すように、左上の頂点から開始し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、以下同様に、すべての頂点が訪問されるまで続けます。
![グラフの幅優先走査](graph_traversal.assets/graph_bfs.png){ class="animation-figure" }
<p align="center"> 図 9-9 &nbsp; グラフの幅優先走査 </p>
### 1. &nbsp; アルゴリズムの実装
BFSは通常キューの助けを借りて実装されます下記のコード参照。キューは「先入先出」で、これは「近くから遠くへ」走査するBFSの考え方と一致します。
1. 開始頂点`startVet`をキューに追加し、ループを開始します。
2. ループの各反復で、キューの先頭の頂点をポップし、それを訪問済みとして記録し、次にその頂点のすべての隣接頂点をキューの末尾に追加します。
3. すべての頂点が訪問されるまで手順`2.`を繰り返します。
頂点の再訪問を防ぐために、ハッシュセット`visited`を使用してどのノードが訪問されたかを記録します。
=== "Python"
```python title="graph_bfs.py"
def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:
"""幅優先走査"""
# 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得
# 頂点走査シーケンス
res = []
# ハッシュセット、訪問済み頂点を記録するために使用
visited = set[Vertex]([start_vet])
# BFSを実装するために使用されるキュー
que = deque[Vertex]([start_vet])
# 頂点vetから開始し、すべての頂点が訪問されるまでループ
while len(que) > 0:
vet = que.popleft() # キューの先頭の頂点をデキュー
res.append(vet) # 訪問済み頂点を記録
# その頂点のすべての隣接頂点を走査
for adj_vet in graph.adj_list[vet]:
if adj_vet in visited:
continue # 既に訪問済みの頂点をスキップ
que.append(adj_vet) # 未訪問の頂点のみをエンキュー
visited.add(adj_vet) # 頂点を訪問済みとしてマーク
# 頂点走査シーケンスを返す
return res
```
=== "C++"
```cpp title="graph_bfs.cpp"
/* 幅優先走査 */
// 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得
vector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {
// 頂点走査順序
vector<Vertex *> res;
// ハッシュセット、訪問済み頂点を記録するために使用
unordered_set<Vertex *> visited = {startVet};
// BFSを実装するために使用されるキュー
queue<Vertex *> que;
que.push(startVet);
// 頂点vetから開始し、すべての頂点が訪問されるまでループ
while (!que.empty()) {
Vertex *vet = que.front();
que.pop(); // キューの先頭の頂点をデキュー
res.push_back(vet); // 訪問済み頂点を記録
// その頂点のすべての隣接頂点を走査
for (auto adjVet : graph.adjList[vet]) {
if (visited.count(adjVet))
continue; // すでに訪問済みの頂点をスキップ
que.push(adjVet); // 未訪問の頂点のみをエンキュー
visited.emplace(adjVet); // 頂点を訪問済みとしてマーク
}
}
// 頂点走査順序を返す
return res;
}
```
=== "Java"
```java title="graph_bfs.java"
/* 幅優先走査 */
// 隣接リストを使用してグラフを表現し、指定した頂点のすべての隣接頂点を取得
List<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {
// 頂点走査順序
List<Vertex> res = new ArrayList<>();
// ハッシュセット、訪問済みの頂点を記録するために使用
Set<Vertex> visited = new HashSet<>();
visited.add(startVet);
// BFS を実装するために使用するキュー
Queue<Vertex> que = new LinkedList<>();
que.offer(startVet);
// 頂点 vet から開始し、すべての頂点が訪問されるまでループ
while (!que.isEmpty()) {
Vertex vet = que.poll(); // キューの先頭の頂点をデキュー
res.add(vet); // 訪問した頂点を記録
// その頂点のすべての隣接頂点を走査
for (Vertex adjVet : graph.adjList.get(vet)) {
if (visited.contains(adjVet))
continue; // すでに訪問済みの頂点をスキップ
que.offer(adjVet); // 未訪問の頂点のみをエンキュー
visited.add(adjVet); // 頂点を訪問済みとしてマーク
}
}
// 頂点走査順序を返す
return res;
}
```
=== "C#"
```csharp title="graph_bfs.cs"
[class]{graph_bfs}-[func]{GraphBFS}
```
=== "Go"
```go title="graph_bfs.go"
[class]{}-[func]{graphBFS}
```
=== "Swift"
```swift title="graph_bfs.swift"
[class]{}-[func]{graphBFS}
```
=== "JS"
```javascript title="graph_bfs.js"
[class]{}-[func]{graphBFS}
```
=== "TS"
```typescript title="graph_bfs.ts"
[class]{}-[func]{graphBFS}
```
=== "Dart"
```dart title="graph_bfs.dart"
[class]{}-[func]{graphBFS}
```
=== "Rust"
```rust title="graph_bfs.rs"
[class]{}-[func]{graph_bfs}
```
=== "C"
```c title="graph_bfs.c"
[class]{Queue}-[func]{}
[class]{}-[func]{isVisited}
[class]{}-[func]{graphBFS}
```
=== "Kotlin"
```kotlin title="graph_bfs.kt"
[class]{}-[func]{graphBFS}
```
=== "Ruby"
```ruby title="graph_bfs.rb"
[class]{}-[func]{graph_bfs}
```
=== "Zig"
```zig title="graph_bfs.zig"
[class]{}-[func]{graphBFS}
```
コードは比較的抽象的ですが、下図と比較することでより良く理解できます。
=== "<1>"
![グラフの幅優先探索の手順](graph_traversal.assets/graph_bfs_step1.png){ class="animation-figure" }
=== "<2>"
![graph_bfs_step2](graph_traversal.assets/graph_bfs_step2.png){ class="animation-figure" }
=== "<3>"
![graph_bfs_step3](graph_traversal.assets/graph_bfs_step3.png){ class="animation-figure" }
=== "<4>"
![graph_bfs_step4](graph_traversal.assets/graph_bfs_step4.png){ class="animation-figure" }
=== "<5>"
![graph_bfs_step5](graph_traversal.assets/graph_bfs_step5.png){ class="animation-figure" }
=== "<6>"
![graph_bfs_step6](graph_traversal.assets/graph_bfs_step6.png){ class="animation-figure" }
=== "<7>"
![graph_bfs_step7](graph_traversal.assets/graph_bfs_step7.png){ class="animation-figure" }
=== "<8>"
![graph_bfs_step8](graph_traversal.assets/graph_bfs_step8.png){ class="animation-figure" }
=== "<9>"
![graph_bfs_step9](graph_traversal.assets/graph_bfs_step9.png){ class="animation-figure" }
=== "<10>"
![graph_bfs_step10](graph_traversal.assets/graph_bfs_step10.png){ class="animation-figure" }
=== "<11>"
![graph_bfs_step11](graph_traversal.assets/graph_bfs_step11.png){ class="animation-figure" }
<p align="center"> 図 9-10 &nbsp; グラフの幅優先探索の手順 </p>
!!! question "幅優先走査のシーケンスは一意ですか?"
一意ではありません。幅優先走査は「近くから遠く」の順序で走査することのみを要求し、**同じ距離の頂点の走査順序は任意にできます**。例えば、上図では、頂点$1$と$3$の訪問順序を交換できますし、頂点$2$、$4$、$6$の順序も同様です。
### 2. &nbsp; 計算量分析
**時間計算量**:すべての頂点が一度ずつエンキューおよびデキューされ、$O(|V|)$時間を使用します。隣接頂点を走査する過程で、無向グラフであるため、すべての辺が$2$回訪問され、$O(2|E|)$時間を使用します。全体で$O(|V| + |E|)$時間を使用します。
**空間計算量**:リスト`res`、ハッシュセット`visited`、キュー`que`の最大頂点数は$|V|$で、$O(|V|)$空間を使用します。
## 9.3.2 &nbsp; 深さ優先探索
**深さ優先探索は可能な限り遠くまで行き、それ以上のパスがない場合にバックトラックする走査方法です**。下図に示すように、左上の頂点から開始し、それ以上のパスがなくなるまで現在の頂点のいずれかの隣接頂点を訪問し、次に戻って続行し、すべての頂点が走査されるまで続けます。
![グラフの深さ優先走査](graph_traversal.assets/graph_dfs.png){ class="animation-figure" }
<p align="center"> 図 9-11 &nbsp; グラフの深さ優先走査 </p>
### 1. &nbsp; アルゴリズムの実装
この「可能な限り遠くまで行ってから戻る」アルゴリズムパラダイムは通常再帰に基づいて実装されます。幅優先探索と同様に、深さ優先探索でも、再訪問を避けるために訪問済み頂点を記録するハッシュセット`visited`の助けが必要です。
=== "Python"
```python title="graph_dfs.py"
def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):
"""深さ優先走査のヘルパー関数"""
res.append(vet) # 訪問済み頂点を記録
visited.add(vet) # 頂点を訪問済みとしてマーク
# その頂点のすべての隣接頂点を走査
for adjVet in graph.adj_list[vet]:
if adjVet in visited:
continue # 既に訪問済みの頂点をスキップ
# 隣接頂点を再帰的に訪問
dfs(graph, visited, res, adjVet)
def graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:
"""深さ優先走査"""
# 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得
# 頂点走査シーケンス
res = []
# ハッシュセット、訪問済み頂点を記録するために使用
visited = set[Vertex]()
dfs(graph, visited, res, start_vet)
return res
```
=== "C++"
```cpp title="graph_dfs.cpp"
/* 深さ優先走査ヘルパー関数 */
void dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {
res.push_back(vet); // 訪問済み頂点を記録
visited.emplace(vet); // 頂点を訪問済みとしてマーク
// その頂点のすべての隣接頂点を走査
for (Vertex *adjVet : graph.adjList[vet]) {
if (visited.count(adjVet))
continue; // すでに訪問済みの頂点をスキップ
// 隣接頂点を再帰的に訪問
dfs(graph, visited, res, adjVet);
}
}
/* 深さ優先走査 */
// 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得
vector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {
// 頂点走査順序
vector<Vertex *> res;
// ハッシュセット、訪問済み頂点を記録するために使用
unordered_set<Vertex *> visited;
dfs(graph, visited, res, startVet);
return res;
}
```
=== "Java"
```java title="graph_dfs.java"
/* 深さ優先走査の補助関数 */
void dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {
res.add(vet); // 訪問した頂点を記録
visited.add(vet); // 頂点を訪問済みとしてマーク
// その頂点のすべての隣接頂点を走査
for (Vertex adjVet : graph.adjList.get(vet)) {
if (visited.contains(adjVet))
continue; // すでに訪問済みの頂点をスキップ
// 隣接頂点を再帰的に訪問
dfs(graph, visited, res, adjVet);
}
}
/* 深さ優先走査 */
// 隣接リストを使用してグラフを表現し、指定した頂点のすべての隣接頂点を取得
List<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {
// 頂点走査順序
List<Vertex> res = new ArrayList<>();
// ハッシュセット、訪問済みの頂点を記録するために使用
Set<Vertex> visited = new HashSet<>();
dfs(graph, visited, res, startVet);
return res;
}
```
=== "C#"
```csharp title="graph_dfs.cs"
[class]{graph_dfs}-[func]{DFS}
[class]{graph_dfs}-[func]{GraphDFS}
```
=== "Go"
```go title="graph_dfs.go"
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
=== "Swift"
```swift title="graph_dfs.swift"
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
=== "JS"
```javascript title="graph_dfs.js"
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
=== "TS"
```typescript title="graph_dfs.ts"
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
=== "Dart"
```dart title="graph_dfs.dart"
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
=== "Rust"
```rust title="graph_dfs.rs"
[class]{}-[func]{dfs}
[class]{}-[func]{graph_dfs}
```
=== "C"
```c title="graph_dfs.c"
[class]{}-[func]{isVisited}
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
=== "Kotlin"
```kotlin title="graph_dfs.kt"
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
=== "Ruby"
```ruby title="graph_dfs.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{graph_dfs}
```
=== "Zig"
```zig title="graph_dfs.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{graphDFS}
```
深さ優先探索のアルゴリズムプロセスを下図に示します。
- **破線は下向きの再帰を表し**、新しい頂点を訪問するために新しい再帰メソッドが開始されたことを示します。
- **曲線の破線は上向きのバックトラックを表し**、この再帰メソッドがこのメソッドが開始された位置に戻ったことを示します。
理解を深めるため、下図とコードを組み合わせて、DFSプロセス全体を頭の中でシミュレートまたは描画することをお勧めします。各再帰メソッドがいつ開始され、いつ戻るかを含めてです。
=== "<1>"
![グラフの深さ優先探索の手順](graph_traversal.assets/graph_dfs_step1.png){ class="animation-figure" }
=== "<2>"
![graph_dfs_step2](graph_traversal.assets/graph_dfs_step2.png){ class="animation-figure" }
=== "<3>"
![graph_dfs_step3](graph_traversal.assets/graph_dfs_step3.png){ class="animation-figure" }
=== "<4>"
![graph_dfs_step4](graph_traversal.assets/graph_dfs_step4.png){ class="animation-figure" }
=== "<5>"
![graph_dfs_step5](graph_traversal.assets/graph_dfs_step5.png){ class="animation-figure" }
=== "<6>"
![graph_dfs_step6](graph_traversal.assets/graph_dfs_step6.png){ class="animation-figure" }
=== "<7>"
![graph_dfs_step7](graph_traversal.assets/graph_dfs_step7.png){ class="animation-figure" }
=== "<8>"
![graph_dfs_step8](graph_traversal.assets/graph_dfs_step8.png){ class="animation-figure" }
=== "<9>"
![graph_dfs_step9](graph_traversal.assets/graph_dfs_step9.png){ class="animation-figure" }
=== "<10>"
![graph_dfs_step10](graph_traversal.assets/graph_dfs_step10.png){ class="animation-figure" }
=== "<11>"
![graph_dfs_step11](graph_traversal.assets/graph_dfs_step11.png){ class="animation-figure" }
<p align="center"> 図 9-12 &nbsp; グラフの深さ優先探索の手順 </p>
!!! question "深さ優先走査のシーケンスは一意ですか?"
幅優先走査と同様に、深さ優先走査シーケンスの順序も一意ではありません。ある頂点が与えられた場合、どの方向を最初に探索することも可能です。つまり、隣接頂点の順序は任意にシャッフルできますが、すべて深さ優先走査の一部です。
木の走査を例に取ると、「根 $\rightarrow$ 左 $\rightarrow$ 右」、「左 $\rightarrow$ 根 $\rightarrow$ 右」、「左 $\rightarrow$ 右 $\rightarrow$ 根」は、それぞれ前順、中順、後順走査に対応します。これらは3つの異なる走査優先度を示していますが、3つすべてが深さ優先走査と見なされます。
### 2. &nbsp; 計算量分析
**時間計算量**:すべての頂点が一度訪問され、$O(|V|)$時間を使用します。すべての辺が2回訪問され、$O(2|E|)$時間を使用します。全体で$O(|V| + |E|)$時間を使用します。
**空間計算量**:リスト`res`、ハッシュセット`visited`の最大頂点数は$|V|$で、最大再帰深度は$|V|$です。したがって、$O(|V|)$空間を使用します。

View File

@@ -0,0 +1,21 @@
---
comments: true
icon: material/graphql
---
# 第 9 章 &nbsp; グラフ
![グラフ](../assets/covers/chapter_graph.jpg){ class="cover-image" }
!!! abstract
人生の旅路において、私たちの一人一人はノードであり、無数の見えない辺で結ばれています。
一つ一つの出会いと別れが、この広大な人生のグラフに独特の印を残していきます。
## 章の内容
- [9.1 &nbsp; グラフ](graph.md)
- [9.2 &nbsp; グラフの基本操作](graph_operations.md)
- [9.3 &nbsp; グラフの走査](graph_traversal.md)
- [9.4 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,35 @@
---
comments: true
---
# 9.4 &nbsp; まとめ
### 1. &nbsp; 重要な復習
- グラフは頂点と辺で構成されます。頂点の集合と辺の集合として記述できます。
- 線形関係(連結リストなど)や階層関係(木など)と比較して、ネットワーク関係(グラフ)はより大きな柔軟性を提供し、より複雑になります。
- 有向グラフでは、辺に方向があります。連結グラフでは、任意の頂点から他の任意の頂点に到達できます。重み付きグラフでは、各辺に関連する重み変数があります。
- 隣接行列は、行列2次元配列を使用してグラフを表現する方法です。行と列は頂点を表します。行列要素の値は、2つの頂点間に辺があるかどうかを示し、辺がある場合は$1$、ない場合は$0$を使用します。隣接行列は辺の追加、削除、チェックなどの操作に非常に効率的ですが、より多くのスペースが必要です。
- 隣接リストは、連結リストの集合を使用してグラフを表現するもう一つの一般的な方法です。グラフ内の各頂点には、その隣接するすべての頂点を含むリストがあります。$i$番目のリストは頂点$i$を表します。隣接リストは隣接行列と比較してより少ないスペースを使用します。ただし、辺を見つけるためにリストを走査する必要があるため、時間効率は低くなります。
- 隣接リストの連結リストが十分に長くなったとき、ルックアップ効率を向上させるために赤黒木やハッシュテーブルに変換できます。
- アルゴリズム設計の観点から、隣接行列は「空間と時間のトレードオフ」の概念を反映し、隣接リストは「時間と空間のトレードオフ」を反映します。
- グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実世界のシステムをモデル化するために使用できます。
- 木はグラフの特別なケースであり、木の走査もグラフ走査の特別なケースです。
- グラフの幅優先走査は、近くから遠くへと層ごとに拡張する探索方法で、通常キューを使用します。
- グラフの深さ優先走査は、それ以上のパスがない場合にバックトラックする前に、まず終端に到達することを優先する探索方法です。しばしば再帰を使用して実装されます。
### 2. &nbsp; Q & A
**Q**: パスは頂点のシーケンスとして定義されますか、それとも辺のシーケンスとして定義されますか?
グラフ理論では、グラフ内のパスは頂点のシーケンスを結ぶ有限または無限の辺のシーケンスです。
この文書では、パスは頂点のシーケンスではなく、辺のシーケンスと考えられます。これは、2つの頂点を結ぶ複数の辺がある可能性があり、その場合各辺がパスに対応するためです。
**Q**: 非連結グラフでは、走査できない点がありますか?
非連結グラフでは、特定の点から到達できない頂点が少なくとも1つあります。非連結グラフを走査するには、グラフのすべての連結成分を走査するために複数の開始点を設定する必要があります。
**Q**: 隣接リストで、「その頂点に接続されたすべての頂点」の順序は重要ですか?
任意の順序で構いません。ただし、実際のアプリケーションでは、頂点が追加された順序や頂点値の順序など、特定のルールに従ってそれらをソートする必要がある場合があります。これにより、特定の極値を持つ頂点を素早く見つけることができます。

View File

@@ -0,0 +1,258 @@
---
comments: true
---
# 15.2 &nbsp; 分数ナップサック問題
!!! question
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できますが、**アイテムの一部を選択することができ、その値は選択された重量の割合に基づいて計算されます**。限られた容量の下でナップサック内のアイテムの最大値は何ですか?例を下の図に示します。
![分数ナップサック問題の例データ](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" }
<p align="center"> 図 15-3 &nbsp; 分数ナップサック問題の例データ </p>
分数ナップサック問題は全体的に0-1ナップサック問題と非常に似ており、現在のアイテム $i$ と容量 $c$ を含み、ナップサックの限られた容量内で値を最大化することを目的としています。
違いは、この問題ではアイテムの一部のみを選択できることです。下の図に示すように、**アイテムを任意に分割し、重量の割合に基づいて対応する値を計算できます**。
1. アイテム $i$ について、その単位重量あたりの値は $val[i-1] / wgt[i-1]$ で、単位値と呼ばれます。
2. 重量 $w$ のアイテム $i$ の一部をナップサックに入れるとすると、ナップサックに追加される値は $w \times val[i-1] / wgt[i-1]$ です。
![アイテムの単位重量あたりの値](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" }
<p align="center"> 図 15-4 &nbsp; アイテムの単位重量あたりの値 </p>
### 1. &nbsp; 貪欲戦略の決定
ナップサック内のアイテムの総値を最大化することは、**本質的に単位重量あたりの値を最大化することを意味します**。これから、下の図に示す貪欲戦略を導出できます。
1. アイテムを単位値の高い順から低い順にソートします。
2. すべてのアイテムを反復し、**各ラウンドで最も高い単位値を持つアイテムを貪欲に選択**します。
3. ナップサックの残り容量が不十分な場合、現在のアイテムの一部を使用してナップサックを満たします。
![分数ナップサック問題の貪欲戦略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" }
<p align="center"> 図 15-5 &nbsp; 分数ナップサック問題の貪欲戦略 </p>
### 2. &nbsp; コード実装
アイテムを単位値でソートするために `Item` クラスを作成しました。ナップサックが満たされるまでループして貪欲な選択を行い、その後終了して解を返します:
=== "Python"
```python title="fractional_knapsack.py"
class Item:
"""アイテム"""
def __init__(self, w: int, v: int):
self.w = w # アイテムの重量
self.v = v # アイテムの価値
def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
"""分数ナップサック:貪欲法"""
# アイテムリストを作成、2 つの属性を含む:重量、価値
items = [Item(w, v) for w, v in zip(wgt, val)]
# 単位価値 item.v / item.w で高い順にソート
items.sort(key=lambda item: item.v / item.w, reverse=True)
# 貪欲選択をループ
res = 0
for item in items:
if item.w <= cap:
# 残り容量が十分な場合、アイテム全体をナップサックに入れる
res += item.v
cap -= item.w
else:
# 残り容量が不十分な場合、アイテムの一部をナップサックに入れる
res += (item.v / item.w) * cap
# 残り容量がなくなったため、ループを中断
break
return res
```
=== "C++"
```cpp title="fractional_knapsack.cpp"
/* アイテム */
class Item {
public:
int w; // アイテムの重量
int v; // アイテムの価値
Item(int w, int v) : w(w), v(v) {
}
};
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
// アイテムリストを作成、2つの属性を含む重量、価値
vector<Item> items;
for (int i = 0; i < wgt.size(); i++) {
items.push_back(Item(wgt[i], val[i]));
}
// 単位価値 item.v / item.w で高い順にソート
sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });
// 貪欲選択をループ
double res = 0;
for (auto &item : items) {
if (item.w <= cap) {
// 残り容量が十分な場合、アイテム全体をナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が不十分な場合、アイテムの一部をナップサックに入れる
res += (double)item.v / item.w * cap;
// 残り容量がなくなったため、ループを中断
break;
}
}
return res;
}
```
=== "Java"
```java title="fractional_knapsack.java"
/* アイテム */
class Item {
int w; // アイテムの重量
int v; // アイテムの価値
public Item(int w, int v) {
this.w = w;
this.v = v;
}
}
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(int[] wgt, int[] val, int cap) {
// アイテムリストを作成、2つの属性を含む重量、価値
Item[] items = new Item[wgt.length];
for (int i = 0; i < wgt.length; i++) {
items[i] = new Item(wgt[i], val[i]);
}
// 単位価値 item.v / item.w で高い順にソート
Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));
// 貪欲選択をループ
double res = 0;
for (Item item : items) {
if (item.w <= cap) {
// 残り容量が十分な場合、アイテム全体をナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が不十分な場合、アイテムの一部をナップサックに入れる
res += (double) item.v / item.w * cap;
// 残り容量がなくなったため、ループを中断
break;
}
}
return res;
}
```
=== "C#"
```csharp title="fractional_knapsack.cs"
[class]{Item}-[func]{}
[class]{fractional_knapsack}-[func]{FractionalKnapsack}
```
=== "Go"
```go title="fractional_knapsack.go"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
=== "Swift"
```swift title="fractional_knapsack.swift"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
=== "JS"
```javascript title="fractional_knapsack.js"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
=== "TS"
```typescript title="fractional_knapsack.ts"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
=== "Dart"
```dart title="fractional_knapsack.dart"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
=== "Rust"
```rust title="fractional_knapsack.rs"
[class]{Item}-[func]{}
[class]{}-[func]{fractional_knapsack}
```
=== "C"
```c title="fractional_knapsack.c"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
=== "Kotlin"
```kotlin title="fractional_knapsack.kt"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
=== "Ruby"
```ruby title="fractional_knapsack.rb"
[class]{Item}-[func]{}
[class]{}-[func]{fractional_knapsack}
```
=== "Zig"
```zig title="fractional_knapsack.zig"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、**時間計算量は $O(n)$** です。ここで $n$ はアイテムの数です。
`Item` オブジェクトリストが初期化されるため、**空間計算量は $O(n)$** です。
### 3. &nbsp; 正しさの証明
背理法を使用します。アイテム $x$ が最高の単位値を持ち、あるアルゴリズムが最大値 `res` を生成するが、解にアイテム $x$ が含まれていないと仮定します。
今、ナップサックから任意のアイテムの単位重量を取り除き、アイテム $x$ の単位重量で置き換えます。アイテム $x$ の単位値が最高であるため、置き換え後の総値は確実に `res` より大きくなります。**これは `res` が最適解であるという仮定と矛盾し、最適解には必ずアイテム $x$ が含まれることを証明します**。
この解の他のアイテムについても、上記の矛盾を構築できます。全体的に、**単位値がより大きいアイテムは常により良い選択**であり、貪欲戦略が効果的であることを証明します。
下の図に示すように、アイテムの重量と単位値をそれぞれ二次元チャートの横軸と縦軸と見なすと、分数ナップサック問題は「限られた横軸範囲内で囲まれる最大面積を求める」ことに変換できます。この類推は、幾何学的観点から貪欲戦略の効果を理解するのに役立ちます。
![分数ナップサック問題の幾何学的表現](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" }
<p align="center"> 図 15-6 &nbsp; 分数ナップサック問題の幾何学的表現 </p>

View File

@@ -0,0 +1,230 @@
---
comments: true
---
# 15.1 &nbsp; 貪欲アルゴリズム
<u>貪欲アルゴリズム</u>は最適化問題を解決するための一般的なアルゴリズムで、基本的に問題の各意思決定段階で最も良い選択をすること、つまり局所的に最適な決定を貪欲に行い、グローバルに最適な解を見つけることを望みます。貪欲アルゴリズムは簡潔で効率的であり、多くの実用的な問題で広く使用されています。
貪欲アルゴリズムと動的プログラミングは、どちらも最適化問題を解決するためによく使用されます。両者は最適部分構造の性質に依存するなど、いくつかの類似点を共有していますが、動作方法が異なります。
- 動的プログラミングは現在の決定段階ですべての以前の決定を考慮し、過去の部分問題の解を使用して現在の部分問題の解を構築します。
- 貪欲アルゴリズムは過去の決定を考慮せず、代わりに貪欲な選択を続け、問題が解決されるまで問題の範囲を継続的に狭めます。
まず、「完全ナップサック問題」の章で紹介された「コイン交換」の例を通じて貪欲アルゴリズムの動作原理を理解しましょう。すでによく知っていると思います。
!!! question
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。各種類のコインは無制限に利用可能で、目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください。
この問題で採用される貪欲戦略を下の図に示します。目標金額が与えられたとき、**それに最も近く、それを超えないコインを貪欲に選択**し、目標金額が満たされるまでこのステップを繰り返します。
![コイン交換の貪欲戦略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" }
<p align="center"> 図 15-1 &nbsp; コイン交換の貪欲戦略 </p>
実装コードは以下の通りです:
=== "Python"
```python title="coin_change_greedy.py"
def coin_change_greedy(coins: list[int], amt: int) -> int:
"""硬貨交換:貪欲法"""
# coins リストがソートされていると仮定
i = len(coins) - 1
count = 0
# 残り金額がなくなるまで貪欲選択をループ
while amt > 0:
# 残り金額に最も近く、それより小さい硬貨を見つける
while i > 0 and coins[i] > amt:
i -= 1
# coins[i] を選択
amt -= coins[i]
count += 1
# 実行可能な解が見つからない場合、-1 を返す
return count if amt == 0 else -1
```
=== "C++"
```cpp title="coin_change_greedy.cpp"
/* 硬貨両替:貪欲法 */
int coinChangeGreedy(vector<int> &coins, int amt) {
// 硬貨リストが順序付けされていると仮定
int i = coins.size() - 1;
int count = 0;
// 残り金額がなくなるまで貪欲選択をループ
while (amt > 0) {
// 残り金額に近く、それ以下の最小硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択
amt -= coins[i];
count++;
}
// 実行可能な解が見つからない場合、-1 を返す
return amt == 0 ? count : -1;
}
```
=== "Java"
```java title="coin_change_greedy.java"
/* 硬貨両替:貪欲法 */
int coinChangeGreedy(int[] coins, int amt) {
// 硬貨リストが順序付けされていると仮定
int i = coins.length - 1;
int count = 0;
// 残り金額がなくなるまで貪欲選択をループ
while (amt > 0) {
// 残り金額に近く、それ以下の最小硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択
amt -= coins[i];
count++;
}
// 実行可能な解が見つからない場合、-1 を返す
return amt == 0 ? count : -1;
}
```
=== "C#"
```csharp title="coin_change_greedy.cs"
[class]{coin_change_greedy}-[func]{CoinChangeGreedy}
```
=== "Go"
```go title="coin_change_greedy.go"
[class]{}-[func]{coinChangeGreedy}
```
=== "Swift"
```swift title="coin_change_greedy.swift"
[class]{}-[func]{coinChangeGreedy}
```
=== "JS"
```javascript title="coin_change_greedy.js"
[class]{}-[func]{coinChangeGreedy}
```
=== "TS"
```typescript title="coin_change_greedy.ts"
[class]{}-[func]{coinChangeGreedy}
```
=== "Dart"
```dart title="coin_change_greedy.dart"
[class]{}-[func]{coinChangeGreedy}
```
=== "Rust"
```rust title="coin_change_greedy.rs"
[class]{}-[func]{coin_change_greedy}
```
=== "C"
```c title="coin_change_greedy.c"
[class]{}-[func]{coinChangeGreedy}
```
=== "Kotlin"
```kotlin title="coin_change_greedy.kt"
[class]{}-[func]{coinChangeGreedy}
```
=== "Ruby"
```ruby title="coin_change_greedy.rb"
[class]{}-[func]{coin_change_greedy}
```
=== "Zig"
```zig title="coin_change_greedy.zig"
[class]{}-[func]{coinChangeGreedy}
```
感嘆するかもしれませんなんて簡潔なんだ貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。
## 15.1.1 &nbsp; 貪欲アルゴリズムの利点と制限
**貪欲アルゴリズムは直接的で実装が簡単であるだけでなく、通常非常に効率的でもあります**。上記のコードで、最小のコイン額面を $\min(coins)$ とすると、貪欲な選択ループは最大 $amt / \min(coins)$ 回実行され、時間計算量は $O(amt / \min(coins))$ になります。これは動的プログラミング解法の時間計算量 $O(n \times amt)$ よりも一桁小さいです。
しかし、**一部のコイン額面の組み合わせでは、貪欲アルゴリズムは最適解を見つけることができません**。下の図は2つの例を示しています。
- **正の例 $coins = [1, 5, 10, 20, 50, 100]$**:このコインの組み合わせでは、任意の $amt$ に対して、貪欲アルゴリズムは最適解を見つけることができます。
- **負の例 $coins = [1, 20, 50]$**$amt = 60$ とすると、貪欲アルゴリズムは組み合わせ $50 + 1 \times 10$ しか見つけられず、合計11枚のコインですが、動的プログラミングは最適解 $20 + 20 + 20$ を見つけることができ、3枚のコインのみが必要です。
- **負の例 $coins = [1, 49, 50]$**$amt = 98$ とすると、貪欲アルゴリズムは組み合わせ $50 + 1 \times 48$ しか見つけられず、合計49枚のコインですが、動的プログラミングは最適解 $49 + 49$ を見つけることができ、2枚のコインのみが必要です。
![貪欲アルゴリズムが最適解を見つけられない例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" }
<p align="center"> 図 15-2 &nbsp; 貪欲アルゴリズムが最適解を見つけられない例 </p>
これは、コイン交換問題において、貪欲アルゴリズムがグローバルに最適な解を見つけることを保証できず、非常に悪い解を見つける可能性があることを意味します。動的プログラミングの方が適しています。
一般的に、貪欲アルゴリズムの適用性は2つのカテゴリに分類されます。
1. **最適解を見つけることが保証される**:これらの場合、貪欲アルゴリズムはしばしば最良の選択で、バックトラッキングや動的プログラミングよりも効率的である傾向があります。
2. **準最適解を見つけることができる**:貪欲アルゴリズムはここでも適用可能です。多くの複雑な問題では、グローバル最適解を見つけることは非常に困難であり、高効率の準最適解を見つけることも非常に価値があります。
## 15.1.2 &nbsp; 貪欲アルゴリズムの特徴
それでは、どのような問題が貪欲アルゴリズムで解決するのに適しているのでしょうか?言い換えれば、どのような条件下で貪欲アルゴリズムは最適解を見つけることを保証できるのでしょうか?
動的プログラミングと比較して、貪欲アルゴリズムはより厳しい使用条件を持ち、主に問題の2つの性質に焦点を当てています。
- **貪欲選択性**:局所的に最適な選択が常にグローバルに最適な解に導くことができる場合のみ、貪欲アルゴリズムは最適解を得ることを保証できます。
- **最適部分構造**:元の問題の最適解はその部分問題の最適解を含みます。
最適部分構造は「動的プログラミング」の章ですでに紹介されているため、ここではこれ以上議論しません。一部の問題には明らかな最適部分構造がありませんが、それでも貪欲アルゴリズムを使用して解決できることに注意することが重要です。
主に貪欲選択性を決定する方法を探索します。その記述は単純に見えますが、**実際には、多くの問題の貪欲選択性を証明することは容易ではありません**。
例えば、コイン交換問題では、貪欲選択性を反証するために反例を簡単に挙げることができますが、それを証明することははるかに困難です。**コインの組み合わせが貪欲アルゴリズムを使用して解決できるためには、どのような条件を満たす必要があるか**と尋ねられた場合、厳密な数学的証明を提供することが困難であるため、しばしば直感や例に頼って曖昧な答えを提供しなければなりません。
!!! quote
ある論文では、コインの組み合わせが任意の金額に対して貪欲アルゴリズムを使用して最適解を見つけることができるかどうかを判定するための時間計算量 $O(n^3)$ のアルゴリズムが提示されています。
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
## 15.1.3 &nbsp; 貪欲アルゴリズムによる問題解決のステップ
貪欲問題の問題解決プロセスは、一般的に以下の3つのステップに分けることができます。
1. **問題分析**:問題の特徴を整理し理解する。状態定義、最適化目標、制約などを含みます。このステップはバックトラッキングや動的プログラミングでも関与します。
2. **貪欲戦略の決定**:各ステップで貪欲な選択をする方法を決定する。この戦略は各ステップで問題の規模を縮小し、最終的に問題全体を解決できます。
3. **正確性の証明**:通常、問題が貪欲選択性と最適部分構造の両方を持つことを証明する必要があります。このステップには、帰納法や背理法などの数学的証明が必要な場合があります。
貪欲戦略の決定は問題解決の核心ステップですが、実装は容易ではない場合があります。主な理由は以下の通りです。
- **異なる問題間で貪欲戦略は大きく異なる**。多くの問題では、貪欲戦略はかなり直接的で、一般的な思考と試行を通じて思いつくことができます。しかし、一部の複雑な問題では、貪欲戦略は非常に見つけにくく、これは個人の問題解決経験とアルゴリズム能力の真のテストです。
- **一部の貪欲戦略は非常に誤解を招く**。自信を持って貪欲戦略を設計し、コードを書いてテストに提出したとき、一部のテストケースが通らない可能性が高いです。これは設計された貪欲戦略が「部分的に正しい」だけであるためで、上記のコイン交換の例で説明した通りです。
正確性を確保するために、貪欲戦略に対して厳密な数学的証明を提供すべきで、**通常は背理法や数学的帰納法を含みます**。
しかし、正確性を証明することは容易な作業ではない場合があります。途方に暮れた場合、通常はテストケースに基づいてコードをデバッグし、貪欲戦略を段階的に修正し検証することを選択します。
## 15.1.4 &nbsp; 貪欲アルゴリズムで解決される典型的な問題
貪欲アルゴリズムは、貪欲選択と最適部分構造の性質を満たす最適化問題によく適用されます。以下は典型的な貪欲アルゴリズム問題のいくつかです。
- **コイン交換問題**:一部のコインの組み合わせでは、貪欲アルゴリズムは常に最適解を提供します。
- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれが一定期間にわたって行われるとします。目標はできるだけ多くのタスクを完了することです。常に最も早く終了するタスクを選択すると、貪欲アルゴリズムは最適解を達成できます。
- **分数ナップサック問題**:アイテムのセットと運搬容量が与えられ、目標は総重量が運搬容量を超えず、総価値が最大化されるようなアイテムのセットを選択することです。常に最高の価値対重量比(価値/重量)のアイテムを選択すると、貪欲アルゴリズムは一部のケースで最適解を達成できます。
- **株式取引問題**:株価の履歴のセットが与えられ、複数回の取引を行うことができますが、すでに株式を所有している場合は売却後でないと再度購入できません。目標は最大利益を達成することです。
- **ハフマン符号化**ハフマン符号化は無損失データ圧縮に使用される貪欲アルゴリズムです。ハフマン木を構築することにより、常に最低頻度の2つのードを統合し、最小重み付きパス長符号化長のハフマン木を生成します。
- **ダイクストラのアルゴリズム**:これは与えられたソース頂点から他のすべての頂点への最短経路問題を解決するための貪欲アルゴリズムです。

View File

@@ -0,0 +1,22 @@
---
comments: true
icon: material/head-heart-outline
---
# 第 15 章 &nbsp; 貪欲法
![貪欲法](../assets/covers/chapter_greedy.jpg){ class="cover-image" }
!!! abstract
ひまわりは太陽の方を向き、常に自分にとって最大の成長を求めます。
貪欲な戦略は、一連の単純な選択を通じて、段階的に最良の答えへと導きます。
## 章の内容
- [15.1 &nbsp; 貪欲アルゴリズム](greedy_algorithm.md)
- [15.2 &nbsp; 分数ナップサック問題](fractional_knapsack_problem.md)
- [15.3 &nbsp; 最大容量問題](max_capacity_problem.md)
- [15.4 &nbsp; 最大積切断問題](max_product_cutting_problem.md)
- [15.5 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,249 @@
---
comments: true
---
# 15.3 &nbsp; 最大容量問題
!!! question
配列 $ht$ を入力します。各要素は垂直仕切りの高さを表します。配列内の任意の2つの仕切りと、それらの間のスペースによってコンテナを形成できます。
コンテナの容量は高さと幅の積面積で、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。
コンテナの容量を最大化する2つの仕切りを配列から選択し、この最大容量を返してください。例を下の図に示します。
![最大容量問題の例データ](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" }
<p align="center"> 図 15-7 &nbsp; 最大容量問題の例データ </p>
コンテナは任意の2つの仕切りによって形成されるため、**この問題の状態は2つの仕切りのインデックスで表現され、$[i, j]$ と表記されます**。
問題の記述によれば、容量は高さと幅の積に等しく、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。容量 $cap[i, j]$ の式は:
$$
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
$$
配列の長さを $n$ と仮定すると、2つの仕切りの組み合わせ数状態の総数は $C_n^2 = \frac{n(n - 1)}{2}$ です。最も直接的なアプローチは**すべての可能な状態を列挙する**ことで、時間計算量は $O(n^2)$ になります。
### 1. &nbsp; 貪欲戦略の決定
この問題にはより効率的な解法があります。下の図に示すように、インデックス $i < j$ かつ高さ $ht[i] < ht[j]$ の状態 $[i, j]$ を選択します。つまり、$i$ は短い仕切り、$j$ は高い仕切りです。
![初期状態](max_capacity_problem.assets/max_capacity_initial_state.png){ class="animation-figure" }
<p align="center"> 図 15-8 &nbsp; 初期状態 </p>
下の図に示すように、**高い仕切り $j$ を短い仕切り $i$ に近づけて移動すると、容量は確実に減少します**。
これは、高い仕切り $j$ を移動すると、幅 $j-i$ が確実に減少するためです。高さは短い仕切りによって決定されるため、高さは同じまま($i$ が短い仕切りのまま)か減少(移動した $j$ が短い仕切りになる)しかありません。
![高い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" }
<p align="center"> 図 15-9 &nbsp; 高い仕切りを内側に移動した後の状態 </p>
逆に、**短い仕切り $i$ を内側に移動することによってのみ容量を増加させることが可能です**。幅は確実に減少しますが、**高さが増加する可能性があります**(移動した短い仕切り $i$ が高くなる場合)。例えば、下の図では、短い仕切りを移動した後に面積が増加しています。
![短い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" }
<p align="center"> 図 15-10 &nbsp; 短い仕切りを内側に移動した後の状態 </p>
これにより、この問題の貪欲戦略が導かれますコンテナの両端に2つのポインタを初期化し、各ラウンドで短い仕切りに対応するポインタを内側に移動し、2つのポインタが出会うまで続けます。
下の図は貪欲戦略の実行を示しています。
1. 最初に、ポインタ $i$ と $j$ が配列の両端に配置されます。
2. 現在の状態の容量 $cap[i, j]$ を計算し、最大容量を更新します。
3. 仕切り $i$ と $j$ の高さを比較し、短い仕切りを1ステップ内側に移動します。
4. $i$ と $j$ が出会うまでステップ `2.``3.` を繰り返します。
=== "<1>"
![最大容量問題の貪欲プロセス](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" }
=== "<2>"
![max_capacity_greedy_step2](max_capacity_problem.assets/max_capacity_greedy_step2.png){ class="animation-figure" }
=== "<3>"
![max_capacity_greedy_step3](max_capacity_problem.assets/max_capacity_greedy_step3.png){ class="animation-figure" }
=== "<4>"
![max_capacity_greedy_step4](max_capacity_problem.assets/max_capacity_greedy_step4.png){ class="animation-figure" }
=== "<5>"
![max_capacity_greedy_step5](max_capacity_problem.assets/max_capacity_greedy_step5.png){ class="animation-figure" }
=== "<6>"
![max_capacity_greedy_step6](max_capacity_problem.assets/max_capacity_greedy_step6.png){ class="animation-figure" }
=== "<7>"
![max_capacity_greedy_step7](max_capacity_problem.assets/max_capacity_greedy_step7.png){ class="animation-figure" }
=== "<8>"
![max_capacity_greedy_step8](max_capacity_problem.assets/max_capacity_greedy_step8.png){ class="animation-figure" }
=== "<9>"
![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png){ class="animation-figure" }
<p align="center"> 図 15-11 &nbsp; 最大容量問題の貪欲プロセス </p>
### 2. &nbsp; 実装
コードは最大 $n$ 回ループするため、**時間計算量は $O(n)$** です。
変数 $i$、$j$、$res$ は一定量の追加スペースを使用するため、**空間計算量は $O(1)$** です。
=== "Python"
```python title="max_capacity.py"
def max_capacity(ht: list[int]) -> int:
"""最大容量:貪欲法"""
# i、j を初期化、配列の両端で分割させる
i, j = 0, len(ht) - 1
# 初期最大容量は 0
res = 0
# 2 つの板が出会うまで貪欲選択をループ
while i < j:
# 最大容量を更新
cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
# 短い板を内側に移動
if ht[i] < ht[j]:
i += 1
else:
j -= 1
return res
```
=== "C++"
```cpp title="max_capacity.cpp"
/* 最大容量:貪欲法 */
int maxCapacity(vector<int> &ht) {
// i、j を初期化し、配列の両端で分割させる
int i = 0, j = ht.size() - 1;
// 初期最大容量は 0
int res = 0;
// 2つの板が出会うまで貪欲選択をループ
while (i < j) {
// 最大容量を更新
int cap = min(ht[i], ht[j]) * (j - i);
res = max(res, cap);
// より短い板を内側に移動
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Java"
```java title="max_capacity.java"
/* 最大容量:貪欲法 */
int maxCapacity(int[] ht) {
// i、j を初期化し、配列の両端で分割させる
int i = 0, j = ht.length - 1;
// 初期最大容量は 0
int res = 0;
// 2つの板が出会うまで貪欲選択をループ
while (i < j) {
// 最大容量を更新
int cap = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// より短い板を内側に移動
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "C#"
```csharp title="max_capacity.cs"
[class]{max_capacity}-[func]{MaxCapacity}
```
=== "Go"
```go title="max_capacity.go"
[class]{}-[func]{maxCapacity}
```
=== "Swift"
```swift title="max_capacity.swift"
[class]{}-[func]{maxCapacity}
```
=== "JS"
```javascript title="max_capacity.js"
[class]{}-[func]{maxCapacity}
```
=== "TS"
```typescript title="max_capacity.ts"
[class]{}-[func]{maxCapacity}
```
=== "Dart"
```dart title="max_capacity.dart"
[class]{}-[func]{maxCapacity}
```
=== "Rust"
```rust title="max_capacity.rs"
[class]{}-[func]{max_capacity}
```
=== "C"
```c title="max_capacity.c"
[class]{}-[func]{maxCapacity}
```
=== "Kotlin"
```kotlin title="max_capacity.kt"
[class]{}-[func]{maxCapacity}
```
=== "Ruby"
```ruby title="max_capacity.rb"
[class]{}-[func]{max_capacity}
```
=== "Zig"
```zig title="max_capacity.zig"
[class]{}-[func]{maxCapacity}
```
### 3. &nbsp; 正しさの証明
貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。
例えば、$i$ が短い仕切りで $j$ が高い仕切りである状態 $cap[i, j]$ の下で、短い仕切り $i$ を貪欲に1ステップ内側に移動すると、下の図に示す「スキップされた」状態につながります。**これは、これらの状態の容量を後で検証できないことを意味します**。
$$
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
$$
![短い仕切りの移動によってスキップされる状態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" }
<p align="center"> 図 15-12 &nbsp; 短い仕切りの移動によってスキップされる状態 </p>
観察すると、**これらのスキップされた状態は実際には高い仕切り $j$ が内側に移動したすべての状態**です。高い仕切りを内側に移動すると容量が確実に減少することをすでに証明しました。したがって、スキップされた状態は最適解である可能性がなく、**それらをスキップしても最適解を見逃すことはありません**。
分析により、短い仕切りを移動する操作は「安全」であり、貪欲戦略が効果的であることが示されます。

View File

@@ -0,0 +1,229 @@
---
comments: true
---
# 15.4 &nbsp; 最大積切断問題
!!! question
正の整数 $n$ が与えられたとき、それを合計が $n$ になる少なくとも2つの正の整数に分割し、これらの整数の最大積を求めてください。下の図に示すとおりです。
![最大積切断問題の定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" }
<p align="center"> 図 15-13 &nbsp; 最大積切断問題の定義 </p>
$n$ を $m$ 個の整数因子に分割すると仮定し、$i$ 番目の因子を $n_i$ と表記すると、
$$
n = \sum_{i=1}^{m}n_i
$$
この問題の目標は、すべての整数因子の最大積を見つけることです。すなわち、
$$
\max(\prod_{i=1}^{m}n_i)
$$
考慮すべき点:分割数 $m$ はどの程度大きくすべきか、各 $n_i$ は何であるべきか?
### 1. &nbsp; 貪欲戦略の決定
経験的に、2つの整数の積は多くの場合その和よりも大きくなります。$n$ から因子 $2$ を分割すると仮定すると、その積は $2(n-2)$ です。この積を $n$ と比較します:
$$
\begin{aligned}
2(n-2) & \geq n \newline
2n - n - 4 & \geq 0 \newline
n & \geq 4
\end{aligned}
$$
下の図に示すように、$n \geq 4$ のとき、$2$ を分割すると積が増加します。**これは4以上の整数を分割すべきであることを示しています**。
**貪欲戦略1**:分割スキームが $\geq 4$ の因子を含む場合、それらはさらに分割されるべきです。最終的な分割は因子 $1$、$2$、$3$ のみを含むべきです。
![分割による積の増加](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" }
<p align="center"> 図 15-14 &nbsp; 分割による積の増加 </p>
次に、どの因子が最適かを考慮します。因子 $1$、$2$、$3$ の中で、明らかに $1$ が最悪です。$1 \times (n-1) < n$ が常に成り立つため、$1$ を分割すると実際に積が減少します。
下の図に示すように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ です。**これは $3$ を分割する方が $2$ を分割するよりも良いことを意味します**。
**貪欲戦略2**分割スキームには最大で2つの $2$ があるべきです。3つの $2$ は常に2つの $3$ に置き換えてより高い積を得ることができるからです。
![最適な分割因子](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" }
<p align="center"> 図 15-15 &nbsp; 最適な分割因子 </p>
上記から、以下の貪欲戦略を導出できます。
1. 入力整数 $n$ について、余りが $0$、$1$、または $2$ になるまで因子 $3$ を継続的に分割します。
2. 余りが $0$ の場合、$n$ が $3$ の倍数であることを意味するため、それ以上の行動は取りません。
3. 余りが $2$ の場合、さらに分割を続けず、そのまま保持します。
4. 余りが $1$ の場合、$2 \times 2 > 1 \times 3$ であるため、最後の $3$ を $2$ に置き換えるべきです。
### 2. &nbsp; コード実装
下の図に示すように、整数を分割するためにループを使用する必要はなく、床除算演算を使用して $3$ の数 $a$ を取得し、剰余演算を使用して余り $b$ を取得できます。したがって:
$$
n = 3a + b
$$
$n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積は $1 \times (n - 1)$ であることに注意してください。
=== "Python"
```python title="max_product_cutting.py"
def max_product_cutting(n: int) -> int:
"""切断の最大積:貪欲法"""
# n <= 3 の場合、1 を切り出す必要がある
if n <= 3:
return 1 * (n - 1)
# 貪欲的に 3 を切り出す、a は 3 の個数、b は余り
a, b = n // 3, n % 3
if b == 1:
# 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換
return int(math.pow(3, a - 1)) * 2 * 2
if b == 2:
# 余りが 2 の場合、何もしない
return int(math.pow(3, a)) * 2
# 余りが 0 の場合、何もしない
return int(math.pow(3, a))
```
=== "C++"
```cpp title="max_product_cutting.cpp"
/* 最大積切断:貪欲法 */
int maxProductCutting(int n) {
// n <= 3 の場合、1 を切り出す必要がある
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出す。a は 3 の個数、b は余り
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換
return (int)pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 の場合、何もしない
return (int)pow(3, a) * 2;
}
// 余りが 0 の場合、何もしない
return (int)pow(3, a);
}
```
=== "Java"
```java title="max_product_cutting.java"
/* 最大積切断:貪欲法 */
int maxProductCutting(int n) {
// n <= 3 の場合、1 を切り出す必要がある
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出す。a は 3 の個数、b は余り
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換
return (int) Math.pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 の場合、何もしない
return (int) Math.pow(3, a) * 2;
}
// 余りが 0 の場合、何もしない
return (int) Math.pow(3, a);
}
```
=== "C#"
```csharp title="max_product_cutting.cs"
[class]{max_product_cutting}-[func]{MaxProductCutting}
```
=== "Go"
```go title="max_product_cutting.go"
[class]{}-[func]{maxProductCutting}
```
=== "Swift"
```swift title="max_product_cutting.swift"
[class]{}-[func]{maxProductCutting}
```
=== "JS"
```javascript title="max_product_cutting.js"
[class]{}-[func]{maxProductCutting}
```
=== "TS"
```typescript title="max_product_cutting.ts"
[class]{}-[func]{maxProductCutting}
```
=== "Dart"
```dart title="max_product_cutting.dart"
[class]{}-[func]{maxProductCutting}
```
=== "Rust"
```rust title="max_product_cutting.rs"
[class]{}-[func]{max_product_cutting}
```
=== "C"
```c title="max_product_cutting.c"
[class]{}-[func]{maxProductCutting}
```
=== "Kotlin"
```kotlin title="max_product_cutting.kt"
[class]{}-[func]{maxProductCutting}
```
=== "Ruby"
```ruby title="max_product_cutting.rb"
[class]{}-[func]{max_product_cutting}
```
=== "Zig"
```zig title="max_product_cutting.zig"
[class]{}-[func]{maxProductCutting}
```
![切断後の最大積の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" }
<p align="center"> 図 15-16 &nbsp; 切断後の最大積の計算方法 </p>
**時間計算量はプログラミング言語のべき乗演算の実装に依存します**。Pythonでは、よく使用されるべき乗計算関数は3種類あります
- 演算子 `**` と関数 `pow()` の両方の時間計算量は $O(\log a)$ です。
- `math.pow()` 関数は内部でC言語ライブラリの `pow()` 関数を呼び出し、浮動小数点べき乗を実行し、時間計算量は $O(1)$ です。
変数 $a$ と $b$ は一定サイズの追加スペースを使用するため、**空間計算量は $O(1)$** です。
### 3. &nbsp; 正しさの証明
背理法を使用し、$n \geq 3$ のケースのみを分析します。
1. **すべての因子 $\leq 3$**:最適分割スキームが因子 $x \geq 4$ を含むと仮定すると、それを確実に $2(x-2)$ にさらに分割でき、より大きな積を得られます。これは仮定と矛盾します。
2. **分割スキームに $1$ が含まれない**:最適分割スキームが因子 $1$ を含むと仮定すると、それを確実に別の因子と結合してより大きな積を得られます。これは仮定と矛盾します。
3. **分割スキームには最大で2つの $2$ が含まれる**最適分割スキームが3つの $2$ を含むと仮定すると、それらを確実に2つの $3$ に置き換えて、より高い積を達成できます。これは仮定と矛盾します。

View File

@@ -0,0 +1,16 @@
---
comments: true
---
# 15.5 &nbsp; まとめ
- 貪欲アルゴリズムは最適化問題を解決するためによく使用され、原理は各決定段階で局所的に最適な決定を行い、グローバルに最適な解を達成することです。
- 貪欲アルゴリズムは貪欲な選択を次々と反復的に行い、各ラウンドで問題をより小さな部分問題に変換し、問題が解決されるまで続けます。
- 貪欲アルゴリズムは実装が簡単なだけでなく、問題解決効率も高いです。動的プログラミングと比較して、貪欲アルゴリズムは一般的により低い時間計算量を持ちます。
- コイン交換問題において、貪欲アルゴリズムは特定のコインの組み合わせに対して最適解を保証できますが、他の組み合わせでは貪欲アルゴリズムが非常に悪い解を見つける可能性があります。
- 貪欲アルゴリズム解法に適した問題は2つの主要な性質を持ちます貪欲選択性と最適部分構造。貪欲選択性は貪欲戦略の効果を表します。
- 一部の複雑な問題では、貪欲選択性を証明することは簡単ではありません。逆に、無効性を証明することはしばしばより容易で、コイン交換問題などがその例です。
- 貪欲問題の解決は主に3つのステップから構成されます問題分析、貪欲戦略の決定、正しさの証明。このうち、貪欲戦略の決定が重要なステップであり、正しさの証明がしばしば挑戦となります。
- 分数ナップサック問題は0-1ナップサック問題に基づいてアイテムの一部の選択を可能にし、したがって貪欲アルゴリズムを使用して解決できます。貪欲戦略の正しさは背理法によって証明できます。
- 最大容量問題は全探索法で解決でき、時間計算量は $O(n^2)$ です。貪欲戦略を設計することで、各ラウンドで短い板を内側に移動し、時間計算量を $O(n)$ に最適化します。
- 切断後の最大積問題において、2つの貪欲戦略を導出します$\geq 4$ の整数は継続的に切断されるべきで、最適な切断因子は $3$ です。コードにはべき乗演算が含まれ、時間計算量はべき乗演算の実装方法に依存し、一般的に $O(1)$ または $O(\log n)$ です。

View File

@@ -0,0 +1,624 @@
---
comments: true
---
# 6.3 &nbsp; ハッシュアルゴリズム
前の2つの節では、ハッシュ表の動作原理とハッシュ衝突を処理する方法を紹介しました。しかし、オープンアドレス法と連鎖法はどちらも**衝突が発生した際にハッシュ表が正常に機能することのみを保証でき、ハッシュ衝突の発生頻度を減らすことはできません**。
ハッシュ衝突があまりにも頻繁に発生すると、ハッシュ表の性能は劇的に悪化します。下図に示すように、連鎖法ハッシュ表では、理想的なケースではキー値ペアがバケット間に均等に分散され、最適なクエリ効率を実現します。最悪のケースでは、すべてのキー値ペアが同じバケットに格納され、時間計算量が$O(n)$に悪化します。
![ハッシュ衝突の理想的および最悪のケース](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" }
<p align="center"> 図 6-8 &nbsp; ハッシュ衝突の理想的および最悪のケース </p>
**キー値ペアの分布はハッシュ関数によって決定されます**。ハッシュ関数の計算ステップを思い出すと、まずハッシュ値を計算し、次に配列長で剰余を取ります:
```shell
index = hash(key) % capacity
```
上記の式を観察すると、ハッシュ表の容量`capacity`が固定されている場合、**ハッシュアルゴリズム`hash()`が出力値を決定し**、それによってハッシュ表におけるキー値ペアの分布を決定します。
これは、ハッシュ衝突の確率を減らすために、ハッシュアルゴリズム`hash()`の設計に焦点を当てるべきであることを意味します。
## 6.3.1 &nbsp; ハッシュアルゴリズムの目標
「高速で安定した」ハッシュ表データ構造を実現するために、ハッシュアルゴリズムは以下の特性を持つべきです:
- **決定性**: 同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成するべきです。そうでなければハッシュ表は信頼できません。
- **高効率**: ハッシュ値を計算するプロセスは十分に高速である必要があります。計算オーバーヘッドが小さいほど、ハッシュ表はより実用的になります。
- **均等分散**: ハッシュアルゴリズムはキー値ペアがハッシュ表に均等に分散されることを保証するべきです。分散が均等であるほど、ハッシュ衝突の確率は低くなります。
実際、ハッシュアルゴリズムはハッシュ表の実装だけでなく、他の分野でも広く応用されています。
- **パスワード保存**: ユーザーパスワードのセキュリティを保護するために、システムは通常平文パスワードを保存せず、パスワードのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力のハッシュ値を計算し、保存されているハッシュ値と比較します。一致すれば、パスワードは正しいと見なされます。
- **データ整合性チェック**: データ送信者はデータのハッシュ値を計算して一緒に送信できます。受信者は受信したデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。一致すれば、データは完全であると見なされます。
暗号化アプリケーションでは、ハッシュ値から元のパスワードを推測するなどの逆行分析を防ぐために、ハッシュアルゴリズムはより高いレベルのセキュリティ機能が必要です。
- **一方向性**: ハッシュ値から入力データに関する情報を推測することは不可能であるべきです。
- **衝突耐性**: 同じハッシュ値を生成する2つの異なる入力を見つけることは極めて困難であるべきです。
- **雪崩効果**: 入力の小さな変更は、出力に大きく予測不可能な変化をもたらすべきです。
**「均等分散」と「衝突耐性」は2つの別々の概念**であることに注意してください。均等分散を満たしても、必ずしも衝突耐性があるとは限りません。例えば、ランダムな入力`key`の下で、ハッシュ関数`key % 100`は均等に分散された出力を生成できます。しかし、このハッシュアルゴリズムは過度にシンプルで、下二桁が同じすべての`key`は同じ出力を持つため、ハッシュ値から使用可能な`key`を簡単に推測でき、パスワードを破ることができます。
## 6.3.2 &nbsp; ハッシュアルゴリズムの設計
ハッシュアルゴリズムの設計は多くの要因を考慮する必要がある複雑な問題です。しかし、要求が少ない一部のシナリオでは、いくつかの簡単なハッシュアルゴリズムを設計することもできます。
- **加算ハッシュ**: 入力の各文字のASCIIコードを合計し、合計をハッシュ値として使用します。
- **乗算ハッシュ**: 乗算の非相関性を利用し、各ラウンドで定数を乗算し、各文字のASCIIコードをハッシュ値に累積します。
- **XORハッシュ**: 入力データの各要素をXORすることでハッシュ値を累積します。
- **回転ハッシュ**: 各文字のASCIIコードをハッシュ値に累積し、各累積前にハッシュ値に回転操作を実行します。
=== "Python"
```python title="simple_hash.py"
def add_hash(key: str) -> int:
"""加法ハッシュ"""
hash = 0
modulus = 1000000007
for c in key:
hash += ord(c)
return hash % modulus
def mul_hash(key: str) -> int:
"""乗法ハッシュ"""
hash = 0
modulus = 1000000007
for c in key:
hash = 31 * hash + ord(c)
return hash % modulus
def xor_hash(key: str) -> int:
"""XORハッシュ"""
hash = 0
modulus = 1000000007
for c in key:
hash ^= ord(c)
return hash % modulus
def rot_hash(key: str) -> int:
"""回転ハッシュ"""
hash = 0
modulus = 1000000007
for c in key:
hash = (hash << 4) ^ (hash >> 28) ^ ord(c)
return hash % modulus
```
=== "C++"
```cpp title="simple_hash.cpp"
/* 加算ハッシュ */
int addHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* 乗算ハッシュ */
int mulHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (31 * hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* XORハッシュ */
int xorHash(string key) {
int hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash ^= (int)c;
}
return hash & MODULUS;
}
/* 回転ハッシュ */
int rotHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;
}
return (int)hash;
}
```
=== "Java"
```java title="simple_hash.java"
/* 加算ハッシュ */
int addHash(String key) {
long hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash = (hash + (int) c) % MODULUS;
}
return (int) hash;
}
/* 乗算ハッシュ */
int mulHash(String key) {
long hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash = (31 * hash + (int) c) % MODULUS;
}
return (int) hash;
}
/* XORハッシュ */
int xorHash(String key) {
int hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash ^= (int) c;
}
return hash & MODULUS;
}
/* 回転ハッシュ */
int rotHash(String key) {
long hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;
}
return (int) hash;
}
```
=== "C#"
```csharp title="simple_hash.cs"
[class]{simple_hash}-[func]{AddHash}
[class]{simple_hash}-[func]{MulHash}
[class]{simple_hash}-[func]{XorHash}
[class]{simple_hash}-[func]{RotHash}
```
=== "Go"
```go title="simple_hash.go"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
=== "Swift"
```swift title="simple_hash.swift"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
=== "JS"
```javascript title="simple_hash.js"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
=== "TS"
```typescript title="simple_hash.ts"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
=== "Dart"
```dart title="simple_hash.dart"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
=== "Rust"
```rust title="simple_hash.rs"
[class]{}-[func]{add_hash}
[class]{}-[func]{mul_hash}
[class]{}-[func]{xor_hash}
[class]{}-[func]{rot_hash}
```
=== "C"
```c title="simple_hash.c"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
=== "Kotlin"
```kotlin title="simple_hash.kt"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
=== "Ruby"
```ruby title="simple_hash.rb"
[class]{}-[func]{add_hash}
[class]{}-[func]{mul_hash}
[class]{}-[func]{xor_hash}
[class]{}-[func]{rot_hash}
```
=== "Zig"
```zig title="simple_hash.zig"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
各ハッシュアルゴリズムの最後のステップが大きな素数$1000000007$の剰余を取ることで、ハッシュ値が適切な範囲内にあることを保証していることが観察されます。なぜ素数の剰余を取ることが強調されるのか、または合成数の剰余を取ることの欠点は何かを考える価値があります。これは興味深い質問です。
結論として:**大きな素数を剰余として使用することで、ハッシュ値の均等分散を最大化できます**。素数は他の数と共通因子を持たないため、剰余演算によって引き起こされる周期的パターンを減らし、ハッシュ衝突を回避できます。
例えば、合成数$9$を剰余として選択するとします。これは$3$で割り切れるため、$3$で割り切れるすべての`key`はハッシュ値$0$、$3$、$6$にマッピングされます。
$$
\begin{aligned}
\text{modulus} & = 9 \newline
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
\text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \}
\end{aligned}
$$
入力`key`がたまたまこの種の等差数列分布を持つ場合、ハッシュ値がクラスターし、ハッシュ衝突を悪化させます。今度は`modulus`を素数$13$に置き換えるとします。`key`と`modulus`の間に共通因子がないため、出力ハッシュ値の均等性が大幅に改善されます。
$$
\begin{aligned}
\text{modulus} & = 13 \newline
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
\text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \}
\end{aligned}
$$
`key`がランダムで均等に分散されることが保証されている場合、剰余として素数または合成数を選択しても、両方とも均等に分散されたハッシュ値を生成できることは注目に値します。しかし、`key`の分布にある種の周期性がある場合、合成数の剰余はクラスタリングを引き起こしやすくなります。
要約すると、通常は素数を剰余として選択し、この素数は周期的パターンを可能な限り排除し、ハッシュアルゴリズムの堅牢性を向上させるために十分大きくある必要があります。
## 6.3.3 &nbsp; 一般的なハッシュアルゴリズム
上記で言及した簡単なハッシュアルゴリズムはかなり「脆弱」で、ハッシュアルゴリズムの設計目標から程遠いことは難しくありません。例えば、加算とXORは交換法則に従うため、加算ハッシュとXORハッシュは同じ内容だが順序が異なる文字列を区別できず、ハッシュ衝突を悪化させ、セキュリティ問題を引き起こす可能性があります。
実際には、通常MD5、SHA-1、SHA-2、SHA-3などの標準ハッシュアルゴリズムを使用します。これらは任意の長さの入力データを固定長のハッシュ値にマッピングできます。
過去1世紀にわたって、ハッシュアルゴリズムは継続的なアップグレードと最適化のプロセスにありました。一部の研究者はハッシュアルゴリズムの性能向上に努め、ハッカーを含む他の人々はハッシュアルゴリズムのセキュリティ問題を見つけることに専念しています。以下の表は、実用的なアプリケーションで一般的に使用されるハッシュアルゴリズムを示しています。
- MD5とSHA-1は複数回攻撃に成功しており、さまざまなセキュリティアプリケーションで放棄されています。
- SHA-2シリーズ、特にSHA-256は、現在最も安全なハッシュアルゴリズムの1つで、成功した攻撃は報告されておらず、さまざまなセキュリティアプリケーションとプロトコルで一般的に使用されています。
- SHA-3はSHA-2と比較して実装コストが低く、計算効率が高いですが、現在の使用範囲はSHA-2シリーズほど広範囲ではありません。
<p align="center"> 表 6-2 &nbsp; 一般的なハッシュアルゴリズム </p>
<div class="center-table" markdown>
| | MD5 | SHA-1 | SHA-2 | SHA-3 |
| --------------- | ----------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------- | ---------------------------- |
| リリース年 | 1992 | 1995 | 2002 | 2008 |
| 出力長 | 128 bit | 160 bit | 256/512 bit | 224/256/384/512 bit |
| ハッシュ衝突 | 頻繁 | 頻繁 | まれ | まれ |
| セキュリティレベル | 低、攻撃に成功している | 低、攻撃に成功している | 高 | 高 |
| アプリケーション | 放棄、データ整合性チェックにまだ使用 | 放棄 | 暗号通貨取引検証、デジタル署名など | SHA-2の代替として使用可能 |
</div>
# データ構造におけるハッシュ値
ハッシュ表のキーは整数、小数、文字列などのさまざまなデータ型にできることを知っています。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュ表のバケットインデックスを計算します。Pythonを例に取ると、`hash()`関数を使用してさまざまなデータ型のハッシュ値を計算できます。
- 整数とブール値のハッシュ値は、それら自身の値です。
- 浮動小数点数と文字列のハッシュ値の計算はより複雑で、興味のある読者は自分で研究することをお勧めします。
- タプルのハッシュ値は、その各要素のハッシュ値の組み合わせで、単一のハッシュ値になります。
- オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすることで、内容に基づいてハッシュ値を生成できます。
!!! tip
異なるプログラミング言語における組み込みハッシュ値計算関数の定義と方法は異なることに注意してください。
=== "Python"
```python title="built_in_hash.py"
num = 3
hash_num = hash(num)
# 整数3のハッシュ値は3
bol = True
hash_bol = hash(bol)
# ブール値Trueのハッシュ値は1
dec = 3.14159
hash_dec = hash(dec)
# 小数3.14159のハッシュ値は326484311674566659
str = "Hello 算法"
hash_str = hash(str)
# 文字列"Hello 算法"のハッシュ値は4617003410720528961
tup = (12836, "小哈")
hash_tup = hash(tup)
# タプル(12836, '小哈')のハッシュ値は1029005403108185979
obj = ListNode(0)
hash_obj = hash(obj)
# ListNodeオブジェクト0x1058fd810のハッシュ値は274267521
```
=== "C++"
```cpp title="built_in_hash.cpp"
int num = 3;
size_t hashNum = hash<int>()(num);
// 整数3のハッシュ値は3
bool bol = true;
size_t hashBol = hash<bool>()(bol);
// ブール値1のハッシュ値は1
double dec = 3.14159;
size_t hashDec = hash<double>()(dec);
// 小数3.14159のハッシュ値は4614256650576692846
string str = "Hello 算法";
size_t hashStr = hash<string>()(str);
// 文字列"Hello 算法"のハッシュ値は15466937326284535026
// C++では、組み込みstd::hash()は基本データ型のハッシュ値のみを提供
// 配列とオブジェクトのハッシュ値は別途実装が必要
```
=== "Java"
```java title="built_in_hash.java"
int num = 3;
int hashNum = Integer.hashCode(num);
// 整数3のハッシュ値は3
boolean bol = true;
int hashBol = Boolean.hashCode(bol);
// ブール値trueのハッシュ値は1231
double dec = 3.14159;
int hashDec = Double.hashCode(dec);
// 小数3.14159のハッシュ値は-1340954729
String str = "Hello 算法";
int hashStr = str.hashCode();
// 文字列"Hello 算法"のハッシュ値は-727081396
Object[] arr = { 12836, "小哈" };
int hashTup = Arrays.hashCode(arr);
// 配列[12836, 小哈]のハッシュ値は1151158
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode();
// ListNodeオブジェクトutils.ListNode@7dc5e7b4のハッシュ値は2110121908
```
=== "C#"
```csharp title="built_in_hash.cs"
int num = 3;
int hashNum = num.GetHashCode();
// 整数3のハッシュ値は3;
bool bol = true;
int hashBol = bol.GetHashCode();
// ブール値trueのハッシュ値は1;
double dec = 3.14159;
int hashDec = dec.GetHashCode();
// 小数3.14159のハッシュ値は-1340954729;
string str = "Hello 算法";
int hashStr = str.GetHashCode();
// 文字列"Hello 算法"のハッシュ値は-586107568;
object[] arr = [12836, "小哈"];
int hashTup = arr.GetHashCode();
// 配列[12836, 小哈]のハッシュ値は42931033;
ListNode obj = new(0);
int hashObj = obj.GetHashCode();
// ListNodeオブジェクト0のハッシュ値は39053774;
```
=== "Go"
```go title="built_in_hash.go"
// Goには組み込みのハッシュコード関数が提供されていません
```
=== "Swift"
```swift title="built_in_hash.swift"
let num = 3
let hashNum = num.hashValue
// 整数3のハッシュ値は9047044699613009734
let bol = true
let hashBol = bol.hashValue
// ブール値trueのハッシュ値は-4431640247352757451
let dec = 3.14159
let hashDec = dec.hashValue
// 小数3.14159のハッシュ値は-2465384235396674631
let str = "Hello 算法"
let hashStr = str.hashValue
// 文字列"Hello 算法"のハッシュ値は-7850626797806988787
let arr = [AnyHashable(12836), AnyHashable("小哈")]
let hashTup = arr.hashValue
// 配列[AnyHashable(12836), AnyHashable("小哈")]のハッシュ値は-2308633508154532996
let obj = ListNode(x: 0)
let hashObj = obj.hashValue
// ListNodeオブジェクトutils.ListNodeのハッシュ値は-2434780518035996159
```
=== "JS"
```javascript title="built_in_hash.js"
// JavaScriptには組み込みのハッシュコード関数が提供されていません
```
=== "TS"
```typescript title="built_in_hash.ts"
// TypeScriptには組み込みのハッシュコード関数が提供されていません
```
=== "Dart"
```dart title="built_in_hash.dart"
int num = 3;
int hashNum = num.hashCode;
// 整数3のハッシュ値は34803
bool bol = true;
int hashBol = bol.hashCode;
// ブール値trueのハッシュ値は1231
double dec = 3.14159;
int hashDec = dec.hashCode;
// 小数3.14159のハッシュ値は2570631074981783
String str = "Hello 算法";
int hashStr = str.hashCode;
// 文字列"Hello 算法"のハッシュ値は468167534
List arr = [12836, "小哈"];
int hashArr = arr.hashCode;
// 配列[12836, 小哈]のハッシュ値は976512528
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode;
// ListNodeオブジェクトInstance of 'ListNode'のハッシュ値は1033450432
```
=== "Rust"
```rust title="built_in_hash.rs"
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let num = 3;
let mut num_hasher = DefaultHasher::new();
num.hash(&mut num_hasher);
let hash_num = num_hasher.finish();
// 整数3のハッシュ値は568126464209439262
let bol = true;
let mut bol_hasher = DefaultHasher::new();
bol.hash(&mut bol_hasher);
let hash_bol = bol_hasher.finish();
// ブール値trueのハッシュ値は4952851536318644461
let dec: f32 = 3.14159;
let mut dec_hasher = DefaultHasher::new();
dec.to_bits().hash(&mut dec_hasher);
let hash_dec = dec_hasher.finish();
// 小数3.14159のハッシュ値は2566941990314602357
let str = "Hello 算法";
let mut str_hasher = DefaultHasher::new();
str.hash(&mut str_hasher);
let hash_str = str_hasher.finish();
// 文字列"Hello 算法"のハッシュ値は16092673739211250988
let arr = (&12836, &"小哈");
let mut tup_hasher = DefaultHasher::new();
arr.hash(&mut tup_hasher);
let hash_tup = tup_hasher.finish();
// タプル(12836, "小哈")のハッシュ値は1885128010422702749
let node = ListNode::new(42);
let mut hasher = DefaultHasher::new();
node.borrow().val.hash(&mut hasher);
let hash = hasher.finish();
// ListNodeオブジェクトRefCell { value: ListNode { val: 42, next: None } }のハッシュ値は15387811073369036852
```
=== "C"
```c title="built_in_hash.c"
// Cには組み込みのハッシュコード関数が提供されていません
```
=== "Kotlin"
```kotlin title="built_in_hash.kt"
```
=== "Zig"
```zig title="built_in_hash.zig"
```
多くのプログラミング言語では、**不変オブジェクトのみがハッシュ表の`key`として機能できます**。リスト(動的配列)を`key`として使用する場合、リストの内容が変更されると、そのハッシュ値も変更され、ハッシュ表で元の`value`を見つけることができなくなります。
カスタムオブジェクト(連結リストノードなど)のメンバー変数は可変ですが、ハッシュ可能です。**これは、オブジェクトのハッシュ値が通常そのメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変更されても、メモリアドレスは同じままなので、ハッシュ値は変更されません。
異なるコンソールで出力されるハッシュ値が異なることに気づいたかもしれません。**これは、Pythonインタープリターが起動するたびに文字列ハッシュ関数にランダムソルトを追加するためです**。このアプローチはHashDoS攻撃を効果的に防ぎ、ハッシュアルゴリズムのセキュリティを向上させます。

View File

@@ -0,0 +1,928 @@
---
comments: true
---
# 6.2 &nbsp; ハッシュ衝突
前節で述べたように、**ほとんどの場合、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論的にはハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列容量のサイズの場合、複数の整数が必然的に同じバケットインデックスにマッピングされます。
ハッシュ衝突は誤ったクエリ結果につながり、ハッシュ表の使いやすさに深刻な影響を与える可能性があります。この問題に対処するために、ハッシュ衝突が発生するたびに、衝突が消えるまでハッシュ表のリサイズを実行します。このアプローチは非常にシンプルで直接的であり、うまく機能します。しかし、テーブルの拡張には大量のデータ移行とハッシュコードの再計算が含まれ、これらは高コストであるため、非常に非効率的に見えます。効率を向上させるために、以下の戦略を採用できます:
1. **ハッシュ衝突が発生した場合でも、ターゲット要素の検索が適切に機能する**ようにハッシュ表のデータ構造を改善する。
2. 深刻な衝突が観察され、必要になる前に、拡張は最後の手段とする。
ハッシュ表の構造を改善する主な方法は2つあります「連鎖法」と「オープンアドレス法」です。
## 6.2.1 &nbsp; 連鎖法
元のハッシュ表では、各バケットは1つのキー値ペアのみを格納できます。<u>連鎖法</u>は単一の要素を連結リストに変換し、キー値ペアをリストノードとして扱い、衝突するすべてのキー値ペアを同じ連結リストに格納します。下図は連鎖法を使用したハッシュ表の例を示しています。
![連鎖法ハッシュ表](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" }
<p align="center"> 図 6-5 &nbsp; 連鎖法ハッシュ表 </p>
連鎖法で実装されたハッシュ表の操作は以下のように変更されます:
- **要素のクエリ**: `key`を入力し、ハッシュ関数を通してバケットインデックスを取得し、連結リストのヘッドノードにアクセスします。連結リストを走査してキーを比較し、ターゲットキー値ペアを見つけます。
- **要素の追加**: ハッシュ関数を通して連結リストのヘッドノードにアクセスし、ノード(キー値ペア)をリストに追加します。
- **要素の削除**: ハッシュ関数の結果に基づいて連結リストのヘッドにアクセスし、連結リストを走査してターゲットノードを見つけて削除します。
連鎖法には以下の制限があります:
- **空間使用量の増加**: 連結リストにはノードポインタが含まれており、配列よりも多くのメモリ空間を消費します。
- **クエリ効率の低下**: 対応する要素を見つけるために連結リストの線形走査が必要になるためです。
以下のコードは連鎖法ハッシュ表の簡単な実装を提供し、注意すべき2つの点があります
- 簡単にするために、連結リストの代わりにリスト(動的配列)を使用します。この設定では、ハッシュ表(配列)は複数のバケットを含み、各バケットはリストです。
- この実装にはハッシュ表のリサイズメソッドが含まれています。負荷率が$\frac{2}{3}$を超えると、ハッシュ表を元のサイズの2倍に拡張します。
=== "Python"
```python title="hash_map_chaining.py"
class HashMapChaining:
"""チェーンアドレス法ハッシュテーブル"""
def __init__(self):
"""コンストラクタ"""
self.size = 0 # キー値ペアの数
self.capacity = 4 # ハッシュテーブルの容量
self.load_thres = 2.0 / 3.0 # 拡張をトリガーする負荷率の閾値
self.extend_ratio = 2 # 拡張の倍数
self.buckets = [[] for _ in range(self.capacity)] # バケット配列
def hash_func(self, key: int) -> int:
"""ハッシュ関数"""
return key % self.capacity
def load_factor(self) -> float:
"""負荷率"""
return self.size / self.capacity
def get(self, key: int) -> str | None:
"""照会操作"""
index = self.hash_func(key)
bucket = self.buckets[index]
# バケットを走査し、キーが見つかれば対応する val を返す
for pair in bucket:
if pair.key == key:
return pair.val
# キーが見つからない場合、None を返す
return None
def put(self, key: int, val: str):
"""追加操作"""
# 負荷率が閾値を超えた場合、拡張を実行
if self.load_factor() > self.load_thres:
self.extend()
index = self.hash_func(key)
bucket = self.buckets[index]
# バケットを走査し、指定されたキーに遭遇した場合、対応する val を更新して返す
for pair in bucket:
if pair.key == key:
pair.val = val
return
# キーが見つからない場合、キー値ペアを末尾に追加
pair = Pair(key, val)
bucket.append(pair)
self.size += 1
def remove(self, key: int):
"""削除操作"""
index = self.hash_func(key)
bucket = self.buckets[index]
# バケットを走査し、その中からキー値ペアを削除
for pair in bucket:
if pair.key == key:
bucket.remove(pair)
self.size -= 1
break
def extend(self):
"""ハッシュテーブルを拡張"""
# 元のハッシュテーブルを一時的に保存
buckets = self.buckets
# 拡張された新しいハッシュテーブルを初期化
self.capacity *= self.extend_ratio
self.buckets = [[] for _ in range(self.capacity)]
self.size = 0
# 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動
for bucket in buckets:
for pair in bucket:
self.put(pair.key, pair.val)
def print(self):
"""ハッシュテーブルを出力"""
for bucket in self.buckets:
res = []
for pair in bucket:
res.append(str(pair.key) + " -> " + pair.val)
print(res)
```
=== "C++"
```cpp title="hash_map_chaining.cpp"
/* チェイン法ハッシュテーブル */
class HashMapChaining {
private:
int size; // キー値ペアの数
int capacity; // ハッシュテーブルの容量
double loadThres; // 拡張をトリガーする負荷率の閾値
int extendRatio; // 拡張倍率
vector<vector<Pair *>> buckets; // バケット配列
public:
/* コンストラクタ */
HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {
buckets.resize(capacity);
}
/* デストラクタ */
~HashMapChaining() {
for (auto &bucket : buckets) {
for (Pair *pair : bucket) {
// メモリを解放
delete pair;
}
}
}
/* ハッシュ関数 */
int hashFunc(int key) {
return key % capacity;
}
/* 負荷率 */
double loadFactor() {
return (double)size / (double)capacity;
}
/* クエリ操作 */
string get(int key) {
int index = hashFunc(key);
// バケットを走査、キーが見つかった場合、対応するvalを返却
for (Pair *pair : buckets[index]) {
if (pair->key == key) {
return pair->val;
}
}
// キーが見つからない場合、空文字列を返却
return "";
}
/* 追加操作 */
void put(int key, string val) {
// 負荷率が閾値を超えた場合、拡張を実行
if (loadFactor() > loadThres) {
extend();
}
int index = hashFunc(key);
// バケットを走査、指定キーに遭遇した場合、対応するvalを更新して返却
for (Pair *pair : buckets[index]) {
if (pair->key == key) {
pair->val = val;
return;
}
}
// キーが見つからない場合、キー値ペアを末尾に追加
buckets[index].push_back(new Pair(key, val));
size++;
}
/* 削除操作 */
void remove(int key) {
int index = hashFunc(key);
auto &bucket = buckets[index];
// バケットを走査、キー値ペアを削除
for (int i = 0; i < bucket.size(); i++) {
if (bucket[i]->key == key) {
Pair *tmp = bucket[i];
bucket.erase(bucket.begin() + i); // キー値ペアを削除
delete tmp; // メモリを解放
size--;
return;
}
}
}
/* ハッシュテーブルを拡張 */
void extend() {
// 元のハッシュテーブルを一時保存
vector<vector<Pair *>> bucketsTmp = buckets;
// 拡張された新しいハッシュテーブルを初期化
capacity *= extendRatio;
buckets.clear();
buckets.resize(capacity);
size = 0;
// 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動
for (auto &bucket : bucketsTmp) {
for (Pair *pair : bucket) {
put(pair->key, pair->val);
// メモリを解放
delete pair;
}
}
}
/* ハッシュテーブルを印刷 */
void print() {
for (auto &bucket : buckets) {
cout << "[";
for (Pair *pair : bucket) {
cout << pair->key << " -> " << pair->val << ", ";
}
cout << "]\n";
}
}
};
```
=== "Java"
```java title="hash_map_chaining.java"
/* チェイン法ハッシュテーブル */
class HashMapChaining {
int size; // キー値ペアの数
int capacity; // ハッシュテーブルの容量
double loadThres; // 拡張をトリガーする負荷率の閾値
int extendRatio; // 拡張倍率
List<List<Pair>> buckets; // バケット配列
/* コンストラクタ */
public HashMapChaining() {
size = 0;
capacity = 4;
loadThres = 2.0 / 3.0;
extendRatio = 2;
buckets = new ArrayList<>(capacity);
for (int i = 0; i < capacity; i++) {
buckets.add(new ArrayList<>());
}
}
/* ハッシュ関数 */
int hashFunc(int key) {
return key % capacity;
}
/* 負荷率 */
double loadFactor() {
return (double) size / capacity;
}
/* クエリ操作 */
String get(int key) {
int index = hashFunc(key);
List<Pair> bucket = buckets.get(index);
// バケットを走査、キーが見つかった場合対応するvalを返す
for (Pair pair : bucket) {
if (pair.key == key) {
return pair.val;
}
}
// キーが見つからない場合、nullを返す
return null;
}
/* 追加操作 */
void put(int key, String val) {
// 負荷率が閾値を超えた場合、拡張を実行
if (loadFactor() > loadThres) {
extend();
}
int index = hashFunc(key);
List<Pair> bucket = buckets.get(index);
// バケットを走査、指定したキーに遭遇した場合、対応するvalを更新して戻る
for (Pair pair : bucket) {
if (pair.key == key) {
pair.val = val;
return;
}
}
// キーが見つからない場合、キー値ペアを末尾に追加
Pair pair = new Pair(key, val);
bucket.add(pair);
size++;
}
/* 削除操作 */
void remove(int key) {
int index = hashFunc(key);
List<Pair> bucket = buckets.get(index);
// バケットを走査、その中からキー値ペアを削除
for (Pair pair : bucket) {
if (pair.key == key) {
bucket.remove(pair);
size--;
break;
}
}
}
/* ハッシュテーブルを拡張 */
void extend() {
// 元のハッシュテーブルを一時的に保存
List<List<Pair>> bucketsTmp = buckets;
// 拡張された新しいハッシュテーブルを初期化
capacity *= extendRatio;
buckets = new ArrayList<>(capacity);
for (int i = 0; i < capacity; i++) {
buckets.add(new ArrayList<>());
}
size = 0;
// 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動
for (List<Pair> bucket : bucketsTmp) {
for (Pair pair : bucket) {
put(pair.key, pair.val);
}
}
}
/* ハッシュテーブルを印刷 */
void print() {
for (List<Pair> bucket : buckets) {
List<String> res = new ArrayList<>();
for (Pair pair : bucket) {
res.add(pair.key + " -> " + pair.val);
}
System.out.println(res);
}
}
}
```
=== "C#"
```csharp title="hash_map_chaining.cs"
[class]{HashMapChaining}-[func]{}
```
=== "Go"
```go title="hash_map_chaining.go"
[class]{hashMapChaining}-[func]{}
```
=== "Swift"
```swift title="hash_map_chaining.swift"
[class]{HashMapChaining}-[func]{}
```
=== "JS"
```javascript title="hash_map_chaining.js"
[class]{HashMapChaining}-[func]{}
```
=== "TS"
```typescript title="hash_map_chaining.ts"
[class]{HashMapChaining}-[func]{}
```
=== "Dart"
```dart title="hash_map_chaining.dart"
[class]{HashMapChaining}-[func]{}
```
=== "Rust"
```rust title="hash_map_chaining.rs"
[class]{HashMapChaining}-[func]{}
```
=== "C"
```c title="hash_map_chaining.c"
[class]{Node}-[func]{}
[class]{HashMapChaining}-[func]{}
```
=== "Kotlin"
```kotlin title="hash_map_chaining.kt"
[class]{HashMapChaining}-[func]{}
```
=== "Ruby"
```ruby title="hash_map_chaining.rb"
[class]{HashMapChaining}-[func]{}
```
=== "Zig"
```zig title="hash_map_chaining.zig"
[class]{HashMapChaining}-[func]{}
```
連結リストが非常に長い場合、クエリ効率$O(n)$が悪いことは注目に値します。**この場合、リストを「AVL木」または「赤黒木」に変換して**、クエリ操作の時間計算量を$O(\log n)$に最適化できます。
## 6.2.2 &nbsp; オープンアドレス法
<u>オープンアドレス法</u>は追加のデータ構造を導入せず、代わりに「複数回プローブ」を通してハッシュ衝突を処理します。プローブ方法には主に線形プローブ、二次プローブ、二重ハッシュがあります。
線形プローブを例にして、オープンアドレス法ハッシュ表のメカニズムを紹介しましょう。
### 1. &nbsp; 線形プローブ
線形プローブは固定ステップの線形検索をプローブに使用し、通常のハッシュ表とは異なります。
- **要素の挿入**: ハッシュ関数を使用してバケットインデックスを計算します。バケットに既に要素が含まれている場合、衝突位置から線形に前方に走査し(通常ステップサイズは$1$)、空のバケットが見つかるまで進み、要素を挿入します。
- **要素の検索**: ハッシュ衝突に遭遇した場合、同じステップサイズを使用して線形に前方に走査し、対応する要素が見つかったら`value`を返します。空のバケットに遭遇した場合、ターゲット要素がハッシュ表にないことを意味するため、`None`を返します。
下図はオープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布を示しています。このハッシュ関数によると、下二桁が同じキーは同じバケットにマッピングされます。線形プローブを通して、それらはそのバケットとその下のバケットに順次格納されます。
![オープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布](hash_collision.assets/hash_table_linear_probing.png){ class="animation-figure" }
<p align="center"> 図 6-6 &nbsp; オープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布 </p>
しかし、**線形プローブは「クラスタリング」を作りやすい傾向があります**。具体的には、配列内の連続的に占有された位置が長いほど、これらの連続した位置でハッシュ衝突が発生する確率が高くなり、その位置でのクラスタリングの成長をさらに促進し、悪循環を形成し、最終的に挿入、削除、クエリ、更新操作の効率低下につながります。
**オープンアドレス法ハッシュ表では要素を直接削除できない**ことに注意することが重要です。要素を削除すると、配列に空のバケット`None`が作成されます。要素を検索する際、線形プローブがこの空のバケットに遭遇すると戻ってしまい、このバケットの下の要素にアクセスできなくなります。プログラムはこれらの要素が存在しないと誤って仮定する可能性があります。下図に示すとおりです。
![オープンアドレス法での削除によるクエリ問題](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" }
<p align="center"> 図 6-7 &nbsp; オープンアドレス法での削除によるクエリ問題 </p>
この問題を解決するために、<u>遅延削除</u>メカニズムを採用できます:ハッシュ表から要素を直接削除する代わりに、**定数`TOMBSTONE`を使用してバケットをマークします**。このメカニズムでは、`None`と`TOMBSTONE`の両方が空のバケットを表し、キー値ペアを保持できます。ただし、線形プローブが`TOMBSTONE`に遭遇した場合、その下にまだキー値ペアがある可能性があるため、走査を続ける必要があります。
しかし、**遅延削除はハッシュ表の性能劣化を加速する可能性があります**。削除操作のたびに削除マークが生成され、`TOMBSTONE`が増加すると、線形プローブがターゲット要素を見つけるために複数の`TOMBSTONE`をスキップする必要がある可能性があるため、検索時間も増加します。
これに対処するために、線形プローブ中に最初に遭遇した`TOMBSTONE`のインデックスを記録し、検索されたターゲット要素とその`TOMBSTONE`の位置を交換することを検討してください。これを行う利点は、要素がクエリまたは追加されるたびに、要素がその理想的な位置(プローブの開始点)により近いバケットに移動され、クエリ効率が最適化されることです。
以下のコードは、遅延削除を使用したオープンアドレス法(線形プローブ)ハッシュ表を実装しています。ハッシュ表の空間をより有効に活用するために、ハッシュ表を「循環配列」として扱います。配列の終わりを超えると、最初に戻って走査を続けます。
=== "Python"
```python title="hash_map_open_addressing.py"
class HashMapOpenAddressing:
"""オープンアドレス法ハッシュテーブル"""
def __init__(self):
"""コンストラクタ"""
self.size = 0 # キー値ペアの数
self.capacity = 4 # ハッシュテーブルの容量
self.load_thres = 2.0 / 3.0 # 拡張をトリガーする負荷率の閾値
self.extend_ratio = 2 # 拡張の倍数
self.buckets: list[Pair | None] = [None] * self.capacity # バケット配列
self.TOMBSTONE = Pair(-1, "-1") # 削除マーク
def hash_func(self, key: int) -> int:
"""ハッシュ関数"""
return key % self.capacity
def load_factor(self) -> float:
"""負荷率"""
return self.size / self.capacity
def find_bucket(self, key: int) -> int:
"""key に対応するバケットインデックスを検索"""
index = self.hash_func(key)
first_tombstone = -1
# 線形探査、空のバケットに遭遇したらブレーク
while self.buckets[index] is not None:
# キーに遭遇した場合、対応するバケットインデックスを返す
if self.buckets[index].key == key:
# 削除マークが以前に遭遇していた場合、キー値ペアをそのインデックスに移動
if first_tombstone != -1:
self.buckets[first_tombstone] = self.buckets[index]
self.buckets[index] = self.TOMBSTONE
return first_tombstone # 移動されたバケットインデックスを返す
return index # バケットインデックスを返す
# 最初に遭遇した削除マークを記録
if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:
first_tombstone = index
# バケットインデックスを計算、末尾を超えた場合は先頭に戻る
index = (index + 1) % self.capacity
# キーが存在しない場合、挿入ポイントのインデックスを返す
return index if first_tombstone == -1 else first_tombstone
def get(self, key: int) -> str:
"""照会操作"""
# key に対応するバケットインデックスを検索
index = self.find_bucket(key)
# キー値ペアが見つかれば、対応する val を返す
if self.buckets[index] not in [None, self.TOMBSTONE]:
return self.buckets[index].val
# キー値ペアが存在しない場合、None を返す
return None
def put(self, key: int, val: str):
"""追加操作"""
# 負荷率が閾値を超えた場合、拡張を実行
if self.load_factor() > self.load_thres:
self.extend()
# key に対応するバケットインデックスを検索
index = self.find_bucket(key)
# キー値ペアが見つかれば、val を上書きして返す
if self.buckets[index] not in [None, self.TOMBSTONE]:
self.buckets[index].val = val
return
# キー値ペアが存在しない場合、キー値ペアを追加
self.buckets[index] = Pair(key, val)
self.size += 1
def remove(self, key: int):
"""削除操作"""
# key に対応するバケットインデックスを検索
index = self.find_bucket(key)
# キー値ペアが見つかれば、削除マークで覆う
if self.buckets[index] not in [None, self.TOMBSTONE]:
self.buckets[index] = self.TOMBSTONE
self.size -= 1
def extend(self):
"""ハッシュテーブルを拡張"""
# 元のハッシュテーブルを一時的に保存
buckets_tmp = self.buckets
# 拡張された新しいハッシュテーブルを初期化
self.capacity *= self.extend_ratio
self.buckets = [None] * self.capacity
self.size = 0
# 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動
for pair in buckets_tmp:
if pair not in [None, self.TOMBSTONE]:
self.put(pair.key, pair.val)
def print(self):
"""ハッシュテーブルを出力"""
for pair in self.buckets:
if pair is None:
print("None")
elif pair is self.TOMBSTONE:
print("TOMBSTONE")
else:
print(pair.key, "->", pair.val)
```
=== "C++"
```cpp title="hash_map_open_addressing.cpp"
/* オープンアドレス法ハッシュテーブル */
class HashMapOpenAddressing {
private:
int size; // キー値ペアの数
int capacity = 4; // ハッシュテーブルの容量
const double loadThres = 2.0 / 3.0; // 拡張をトリガーする負荷率の閾値
const int extendRatio = 2; // 拡張倍率
vector<Pair *> buckets; // バケット配列
Pair *TOMBSTONE = new Pair(-1, "-1"); // 削除マーク
public:
/* コンストラクタ */
HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {
}
/* デストラクタ */
~HashMapOpenAddressing() {
for (Pair *pair : buckets) {
if (pair != nullptr && pair != TOMBSTONE) {
delete pair;
}
}
delete TOMBSTONE;
}
/* ハッシュ関数 */
int hashFunc(int key) {
return key % capacity;
}
/* 負荷率 */
double loadFactor() {
return (double)size / capacity;
}
/* keyに対応するバケットインデックスを検索 */
int findBucket(int key) {
int index = hashFunc(key);
int firstTombstone = -1;
// 線形探査、空のバケットに遭遇したら中断
while (buckets[index] != nullptr) {
// keyに遭遇した場合、対応するバケットインデックスを返却
if (buckets[index]->key == key) {
// 以前に削除マークに遭遇していた場合、キー値ペアをそのインデックスに移動
if (firstTombstone != -1) {
buckets[firstTombstone] = buckets[index];
buckets[index] = TOMBSTONE;
return firstTombstone; // 移動されたバケットインデックスを返却
}
return index; // バケットインデックスを返却
}
// 最初に遭遇した削除マークを記録
if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {
firstTombstone = index;
}
// バケットインデックスを計算、末尾を超えた場合は先頭に戻る
index = (index + 1) % capacity;
}
// keyが存在しない場合、挿入ポイントのインデックスを返却
return firstTombstone == -1 ? index : firstTombstone;
}
/* クエリ操作 */
string get(int key) {
// keyに対応するバケットインデックスを検索
int index = findBucket(key);
// キー値ペアが見つかった場合、対応するvalを返却
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
return buckets[index]->val;
}
// キー値ペアが存在しない場合、空文字列を返却
return "";
}
/* 追加操作 */
void put(int key, string val) {
// 負荷率が閾値を超えた場合、拡張を実行
if (loadFactor() > loadThres) {
extend();
}
// keyに対応するバケットインデックスを検索
int index = findBucket(key);
// キー値ペアが見つかった場合、valを上書きして返却
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
buckets[index]->val = val;
return;
}
// キー値ペアが存在しない場合、キー値ペアを追加
buckets[index] = new Pair(key, val);
size++;
}
/* 削除操作 */
void remove(int key) {
// keyに対応するバケットインデックスを検索
int index = findBucket(key);
// キー値ペアが見つかった場合、削除マークで覆う
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
delete buckets[index];
buckets[index] = TOMBSTONE;
size--;
}
}
/* ハッシュテーブルを拡張 */
void extend() {
// 元のハッシュテーブルを一時保存
vector<Pair *> bucketsTmp = buckets;
// 拡張された新しいハッシュテーブルを初期化
capacity *= extendRatio;
buckets = vector<Pair *>(capacity, nullptr);
size = 0;
// 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動
for (Pair *pair : bucketsTmp) {
if (pair != nullptr && pair != TOMBSTONE) {
put(pair->key, pair->val);
delete pair;
}
}
}
/* ハッシュテーブルを印刷 */
void print() {
for (Pair *pair : buckets) {
if (pair == nullptr) {
cout << "nullptr" << endl;
} else if (pair == TOMBSTONE) {
cout << "TOMBSTONE" << endl;
} else {
cout << pair->key << " -> " << pair->val << endl;
}
}
}
};
```
=== "Java"
```java title="hash_map_open_addressing.java"
/* オープンアドレス法ハッシュテーブル */
class HashMapOpenAddressing {
private int size; // キー値ペアの数
private int capacity = 4; // ハッシュテーブルの容量
private final double loadThres = 2.0 / 3.0; // 拡張をトリガーする負荷率の閾値
private final int extendRatio = 2; // 拡張倍率
private Pair[] buckets; // バケット配列
private final Pair TOMBSTONE = new Pair(-1, "-1"); // 削除マーク
/* コンストラクタ */
public HashMapOpenAddressing() {
size = 0;
buckets = new Pair[capacity];
}
/* ハッシュ関数 */
private int hashFunc(int key) {
return key % capacity;
}
/* 負荷率 */
private double loadFactor() {
return (double) size / capacity;
}
/* keyに対応するバケットインデックスを検索 */
private int findBucket(int key) {
int index = hashFunc(key);
int firstTombstone = -1;
// 線形探査、空のバケットに遭遇したら終了
while (buckets[index] != null) {
// keyに遭遇した場合、対応するバケットインデックスを返す
if (buckets[index].key == key) {
// 以前に削除マークに遭遇していた場合、キー値ペアをそのインデックスに移動
if (firstTombstone != -1) {
buckets[firstTombstone] = buckets[index];
buckets[index] = TOMBSTONE;
return firstTombstone; // 移動後のバケットインデックスを返す
}
return index; // バケットインデックスを返す
}
// 最初に遭遇した削除マークを記録
if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {
firstTombstone = index;
}
// バケットインデックスを計算、末尾を超えた場合は先頭に戻る
index = (index + 1) % capacity;
}
// keyが存在しない場合、挿入ポイントのインデックスを返す
return firstTombstone == -1 ? index : firstTombstone;
}
/* クエリ操作 */
public String get(int key) {
// keyに対応するバケットインデックスを検索
int index = findBucket(key);
// キー値ペアが見つかった場合、対応するvalを返す
if (buckets[index] != null && buckets[index] != TOMBSTONE) {
return buckets[index].val;
}
// キー値ペアが存在しない場合、nullを返す
return null;
}
/* 追加操作 */
public void put(int key, String val) {
// 負荷率が閾値を超えた場合、拡張を実行
if (loadFactor() > loadThres) {
extend();
}
// keyに対応するバケットインデックスを検索
int index = findBucket(key);
// キー値ペアが見つかった場合、valを上書きして戻る
if (buckets[index] != null && buckets[index] != TOMBSTONE) {
buckets[index].val = val;
return;
}
// キー値ペアが存在しない場合、キー値ペアを追加
buckets[index] = new Pair(key, val);
size++;
}
/* 削除操作 */
public void remove(int key) {
// keyに対応するバケットインデックスを検索
int index = findBucket(key);
// キー値ペアが見つかった場合、削除マークで覆う
if (buckets[index] != null && buckets[index] != TOMBSTONE) {
buckets[index] = TOMBSTONE;
size--;
}
}
/* ハッシュテーブルを拡張 */
private void extend() {
// 元のハッシュテーブルを一時的に保存
Pair[] bucketsTmp = buckets;
// 拡張された新しいハッシュテーブルを初期化
capacity *= extendRatio;
buckets = new Pair[capacity];
size = 0;
// 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動
for (Pair pair : bucketsTmp) {
if (pair != null && pair != TOMBSTONE) {
put(pair.key, pair.val);
}
}
}
/* ハッシュテーブルを印刷 */
public void print() {
for (Pair pair : buckets) {
if (pair == null) {
System.out.println("null");
} else if (pair == TOMBSTONE) {
System.out.println("TOMBSTONE");
} else {
System.out.println(pair.key + " -> " + pair.val);
}
}
}
}
```
=== "C#"
```csharp title="hash_map_open_addressing.cs"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "Go"
```go title="hash_map_open_addressing.go"
[class]{hashMapOpenAddressing}-[func]{}
```
=== "Swift"
```swift title="hash_map_open_addressing.swift"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "JS"
```javascript title="hash_map_open_addressing.js"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "TS"
```typescript title="hash_map_open_addressing.ts"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "Dart"
```dart title="hash_map_open_addressing.dart"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "Rust"
```rust title="hash_map_open_addressing.rs"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "C"
```c title="hash_map_open_addressing.c"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "Kotlin"
```kotlin title="hash_map_open_addressing.kt"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "Ruby"
```ruby title="hash_map_open_addressing.rb"
[class]{HashMapOpenAddressing}-[func]{}
```
=== "Zig"
```zig title="hash_map_open_addressing.zig"
[class]{HashMapOpenAddressing}-[func]{}
```
### 2. &nbsp; 二次プローブ
二次プローブは線形プローブに似ており、オープンアドレス法の一般的な戦略の1つです。衝突が発生した場合、二次プローブは単純に固定ステップ数をスキップするのではなく、「プローブ回数の二乗」に等しいステップ数、つまり$1, 4, 9, \dots$ステップをスキップします。
二次プローブには以下の利点があります:
- 二次プローブは、プローブ回数の二乗の距離をスキップすることで、線形プローブのクラスタリング効果を軽減しようとします。
- 二次プローブはより大きな距離をスキップして空の位置を見つけ、データをより均等に分散するのに役立ちます。
しかし、二次プローブは完璧ではありません:
- クラスタリングは依然として存在し、つまり一部の位置は他の位置よりも占有される可能性が高いです。
- 二乗の成長により、二次プローブはハッシュ表全体をプローブできない可能性があり、ハッシュ表に空のバケットがあっても、二次プローブがアクセスできない可能性があります。
### 3. &nbsp; 二重ハッシュ
名前が示すように、二重ハッシュ法は複数のハッシュ関数$f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$をプローブに使用します。
- **要素の挿入**: ハッシュ関数$f_1(x)$が衝突に遭遇した場合、$f_2(x)$を試し、以下同様に、空の位置が見つかって要素が挿入されるまで続けます。
- **要素の検索**: 同じハッシュ関数の順序で検索し、ターゲット要素が見つかって返されるまで、または空の位置に遭遇するかすべてのハッシュ関数が試されるまで続け、要素がハッシュ表にないことを示し、`None`を返します。
線形プローブと比較して、二重ハッシュ法はクラスタリングが起こりにくいですが、複数のハッシュ関数は追加の計算オーバーヘッドを導入します。
!!! tip
オープンアドレス法(線形プローブ、二次プローブ、二重ハッシュ)ハッシュ表はすべて「要素を直接削除できない」という問題があることに注意してください。
## 6.2.3 &nbsp; プログラミング言語の選択
異なるプログラミング言語は異なるハッシュ表実装戦略を採用しています。以下にいくつかの例を示します:
- Pythonはオープンアドレス法を使用します。`dict`辞書はプローブに疑似乱数を使用します。
- Javaは連鎖法を使用します。JDK 1.8以降、`HashMap`の配列長が64に達し、連結リストの長さが8に達すると、連結リストは検索性能を向上させるために赤黒木に変換されます。
- Goは連鎖法を使用します。Goは各バケットが最大8つのキー値ペアを格納できることを規定し、容量を超えた場合はオーバーフローバケットが連結されます。オーバーフローバケットが多すぎる場合、性能を確保するために特別な等容量リサイズ操作が実行されます。

View File

@@ -0,0 +1,902 @@
---
comments: true
---
# 6.1 &nbsp; ハッシュ表
<u>ハッシュ表</u>は<u>ハッシュマップ</u>とも呼ばれ、キーと値の間のマッピングを確立し、効率的な要素の取得を可能にするデータ構造です。具体的には、ハッシュ表に`key`を入力すると、$O(1)$の時間計算量で対応する`value`を取得できます。
下図に示すように、$n$人の学生がいて、各学生には「名前」と「学籍番号」の2つのデータフィールドがあるとします。学籍番号を入力として対応する名前を返すクエリ機能を実装したい場合、下図に示すハッシュ表を使用できます。
![ハッシュ表の抽象的な表現](hash_map.assets/hash_table_lookup.png){ class="animation-figure" }
<p align="center"> 図 6-1 &nbsp; ハッシュ表の抽象的な表現 </p>
ハッシュ表に加えて、配列や連結リストもクエリ機能の実装に使用できますが、時間計算量が異なります。効率は以下の表で比較されています:
- **要素の挿入**: 配列(または連結リスト)の末尾に要素を追加するだけです。この操作の時間計算量は$O(1)$です。
- **要素の検索**: 配列(または連結リスト)がソートされていないため、要素を検索するにはすべての要素を走査する必要があります。この操作の時間計算量は$O(n)$です。
- **要素の削除**: 要素を削除するには、まずその要素を見つけてから、配列(または連結リスト)から削除します。この操作の時間計算量は$O(n)$です。
<p align="center"> 表 6-1 &nbsp; 一般的な操作の時間効率の比較 </p>
<div class="center-table" markdown>
| | 配列 | 連結リスト | ハッシュ表 |
| -------------- | ------ | ----------- | ---------- |
| 要素の検索 | $O(n)$ | $O(n)$ | $O(1)$ |
| 要素の挿入 | $O(1)$ | $O(1)$ | $O(1)$ |
| 要素の削除 | $O(n)$ | $O(n)$ | $O(1)$ |
</div>
観察されるように、**ハッシュ表における操作(挿入、削除、検索、変更)の時間計算量は$O(1)$**で、非常に効率的です。
## 6.1.1 &nbsp; ハッシュ表の一般的な操作
ハッシュ表の一般的な操作には、初期化、クエリ、キー値ペアの追加、キー値ペアの削除があります。以下はコード例です:
=== "Python"
```python title="hash_map.py"
# ハッシュ表を初期化
hmap: dict = {}
# 追加操作
# ハッシュ表にキー値ペア (key, value) を追加
hmap[12836] = "小哈"
hmap[15937] = "小啰"
hmap[16750] = "小算"
hmap[13276] = "小法"
hmap[10583] = "小鸭"
# クエリ操作
# ハッシュ表にキーを入力し、値を取得
name: str = hmap[15937]
# 削除操作
# ハッシュ表からキー値ペア (key, value) を削除
hmap.pop(10583)
```
=== "C++"
```cpp title="hash_map.cpp"
/* ハッシュ表を初期化 */
unordered_map<int, string> map;
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
map[12836] = "小哈";
map[15937] = "小啰";
map[16750] = "小算";
map[13276] = "小法";
map[10583] = "小鸭";
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
string name = map[15937];
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
map.erase(10583);
```
=== "Java"
```java title="hash_map.java"
/* ハッシュ表を初期化 */
Map<Integer, String> map = new HashMap<>();
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
map.put(12836, "小哈");
map.put(15937, "小啰");
map.put(16750, "小算");
map.put(13276, "小法");
map.put(10583, "小鸭");
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
String name = map.get(15937);
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
map.remove(10583);
```
=== "C#"
```csharp title="hash_map.cs"
/* ハッシュ表を初期化 */
Dictionary<int, string> map = new() {
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
{ 12836, "小哈" },
{ 15937, "小啰" },
{ 16750, "小算" },
{ 13276, "小法" },
{ 10583, "小鸭" }
};
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
string name = map[15937];
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
map.Remove(10583);
```
=== "Go"
```go title="hash_map_test.go"
/* ハッシュ表を初期化 */
hmap := make(map[int]string)
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
hmap[12836] = "小哈"
hmap[15937] = "小啰"
hmap[16750] = "小算"
hmap[13276] = "小法"
hmap[10583] = "小鸭"
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
name := hmap[15937]
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
delete(hmap, 10583)
```
=== "Swift"
```swift title="hash_map.swift"
/* ハッシュ表を初期化 */
var map: [Int: String] = [:]
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
map[12836] = "小哈"
map[15937] = "小啰"
map[16750] = "小算"
map[13276] = "小法"
map[10583] = "小鸭"
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
let name = map[15937]!
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
map.removeValue(forKey: 10583)
```
=== "JS"
```javascript title="hash_map.js"
/* ハッシュ表を初期化 */
const map = new Map();
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
map.set(12836, '小哈');
map.set(15937, '小啰');
map.set(16750, '小算');
map.set(13276, '小法');
map.set(10583, '小鸭');
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
let name = map.get(15937);
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
map.delete(10583);
```
=== "TS"
```typescript title="hash_map.ts"
/* ハッシュ表を初期化 */
const map = new Map<number, string>();
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
map.set(12836, '小哈');
map.set(15937, '小啰');
map.set(16750, '小算');
map.set(13276, '小法');
map.set(10583, '小鸭');
console.info('\n追加後、ハッシュ表は\nKey -> Value');
console.info(map);
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
let name = map.get(15937);
console.info('\n学籍番号15937を入力、名前を問い合わせ ' + name);
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
map.delete(10583);
console.info('\n10583を削除後、ハッシュ表は\nKey -> Value');
console.info(map);
```
=== "Dart"
```dart title="hash_map.dart"
/* ハッシュ表を初期化 */
Map<int, String> map = {};
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
map[12836] = "小哈";
map[15937] = "小啰";
map[16750] = "小算";
map[13276] = "小法";
map[10583] = "小鸭";
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
String name = map[15937];
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
map.remove(10583);
```
=== "Rust"
```rust title="hash_map.rs"
use std::collections::HashMap;
/* ハッシュ表を初期化 */
let mut map: HashMap<i32, String> = HashMap::new();
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
map.insert(12836, "小哈".to_string());
map.insert(15937, "小啰".to_string());
map.insert(16750, "小算".to_string());
map.insert(13279, "小法".to_string());
map.insert(10583, "小鸭".to_string());
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
let _name: Option<&String> = map.get(&15937);
/* 削除操作 */
// ハッシュ表からキー値ペア (key, value) を削除
let _removed_value: Option<String> = map.remove(&10583);
```
=== "C"
```c title="hash_map.c"
// Cには組み込みのハッシュ表が提供されていません
```
=== "Kotlin"
```kotlin title="hash_map.kt"
```
=== "Zig"
```zig title="hash_map.zig"
```
ハッシュ表を走査する一般的な方法は3つありますキー値ペアの走査、キーの走査、値の走査。以下はコード例です
=== "Python"
```python title="hash_map.py"
# ハッシュ表を走査
# キー値ペア key->value を走査
for key, value in hmap.items():
print(key, "->", value)
# キーのみを走査
for key in hmap.keys():
print(key)
# 値のみを走査
for value in hmap.values():
print(value)
```
=== "C++"
```cpp title="hash_map.cpp"
/* ハッシュ表を走査 */
// キー値ペア key->value を走査
for (auto kv: map) {
cout << kv.first << " -> " << kv.second << endl;
}
// イテレータを使用してキー値ペア key->value を走査
for (auto iter = map.begin(); iter != map.end(); iter++) {
cout << iter->first << "->" << iter->second << endl;
}
```
=== "Java"
```java title="hash_map.java"
/* ハッシュ表を走査 */
// キー値ペア key->value を走査
for (Map.Entry<Integer, String> kv: map.entrySet()) {
System.out.println(kv.getKey() + " -> " + kv.getValue());
}
// キーのみを走査
for (int key: map.keySet()) {
System.out.println(key);
}
// 値のみを走査
for (String val: map.values()) {
System.out.println(val);
}
```
=== "C#"
```csharp title="hash_map.cs"
/* ハッシュ表を走査 */
// キー値ペア Key->Value を走査
foreach (var kv in map) {
Console.WriteLine(kv.Key + " -> " + kv.Value);
}
// キーのみを走査
foreach (int key in map.Keys) {
Console.WriteLine(key);
}
// 値のみを走査
foreach (string val in map.Values) {
Console.WriteLine(val);
}
```
=== "Go"
```go title="hash_map_test.go"
/* ハッシュ表を走査 */
// キー値ペア key->value を走査
for key, value := range hmap {
fmt.Println(key, "->", value)
}
// キーのみを走査
for key := range hmap {
fmt.Println(key)
}
// 値のみを走査
for _, value := range hmap {
fmt.Println(value)
}
```
=== "Swift"
```swift title="hash_map.swift"
/* ハッシュ表を走査 */
// キー値ペア Key->Value を走査
for (key, value) in map {
print("\(key) -> \(value)")
}
// キーのみを走査
for key in map.keys {
print(key)
}
// 値のみを走査
for value in map.values {
print(value)
}
```
=== "JS"
```javascript title="hash_map.js"
/* ハッシュ表を走査 */
console.info('\nキー値ペア Key->Value を走査');
for (const [k, v] of map.entries()) {
console.info(k + ' -> ' + v);
}
console.info('\nキーのみを走査 Key');
for (const k of map.keys()) {
console.info(k);
}
console.info('\n値のみを走査 Value');
for (const v of map.values()) {
console.info(v);
}
```
=== "TS"
```typescript title="hash_map.ts"
/* ハッシュ表を走査 */
console.info('\nキー値ペア Key->Value を走査');
for (const [k, v] of map.entries()) {
console.info(k + ' -> ' + v);
}
console.info('\nキーのみを走査 Key');
for (const k of map.keys()) {
console.info(k);
}
console.info('\n値のみを走査 Value');
for (const v of map.values()) {
console.info(v);
}
```
=== "Dart"
```dart title="hash_map.dart"
/* ハッシュ表を走査 */
// キー値ペア Key->Value を走査
map.forEach((key, value) {
print('$key -> $value');
});
// キーのみを走査 Key
map.keys.forEach((key) {
print(key);
});
// 値のみを走査 Value
map.values.forEach((value) {
print(value);
});
```
=== "Rust"
```rust title="hash_map.rs"
/* ハッシュ表を走査 */
// キー値ペア Key->Value を走査
for (key, value) in &map {
println!("{key} -> {value}");
}
// キーのみを走査 Key
for key in map.keys() {
println!("{key}");
}
// 値のみを走査 Value
for value in map.values() {
println!("{value}");
}
```
=== "C"
```c title="hash_map.c"
// Cには組み込みのハッシュ表が提供されていません
```
=== "Kotlin"
```kotlin title="hash_map.kt"
```
=== "Zig"
```zig title="hash_map.zig"
// Zigの例は提供されていません
```
## 6.1.2 &nbsp; ハッシュ表の簡単な実装
まず、最も簡単なケースを考えてみましょう:**配列のみを使ってハッシュ表を実装すること**。ハッシュ表において、配列の各空きスロットは<u>バケット</u>と呼ばれ、各バケットはキー値ペアを格納できます。したがって、クエリ操作は`key`に対応するバケットを見つけ、そこから`value`を取得することになります。
では、`key`に基づいて対応するバケットをどのように特定するのでしょうか?これは<u>ハッシュ関数</u>によって実現されます。ハッシュ関数の役割は、より大きな入力空間をより小さな出力空間にマッピングすることです。ハッシュ表では、入力空間はすべてのキーで構成され、出力空間はすべてのバケット(配列インデックス)で構成されます。つまり、`key`が与えられた場合、**ハッシュ関数を使用して対応するキー値ペアの配列内の格納位置を決定できます**。
与えられた`key`に対して、ハッシュ関数の計算は2つのステップで構成されます
1. 特定のハッシュアルゴリズム`hash()`を使用してハッシュ値を計算します。
2. ハッシュ値をバケット数(配列長)`capacity`で剰余を取り、キーに対応する配列`index`を取得します。
```shell
index = hash(key) % capacity
```
その後、`index`を使用してハッシュ表内の対応するバケットにアクセスし、`value`を取得できます。
配列長が`capacity = 100`で、ハッシュアルゴリズムが`hash(key) = key`として定義されているとします。したがって、ハッシュ関数は`key % 100`として表現できます。以下の図は、`key`を学籍番号、`value`を名前として、ハッシュ関数の動作原理を示しています。
![ハッシュ関数の動作原理](hash_map.assets/hash_function.png){ class="animation-figure" }
<p align="center"> 図 6-2 &nbsp; ハッシュ関数の動作原理 </p>
以下のコードは簡単なハッシュ表を実装しています。ここでは、`key`と`value`を`Pair`クラスにカプセル化してキー値ペアを表現しています。
=== "Python"
```python title="array_hash_map.py"
class Pair:
"""キー値ペア"""
def __init__(self, key: int, val: str):
self.key = key
self.val = val
class ArrayHashMap:
"""配列実装に基づくハッシュテーブル"""
def __init__(self):
"""コンストラクタ"""
# 100個のバケットを含む配列を初期化
self.buckets: list[Pair | None] = [None] * 100
def hash_func(self, key: int) -> int:
"""ハッシュ関数"""
index = key % 100
return index
def get(self, key: int) -> str:
"""照会操作"""
index: int = self.hash_func(key)
pair: Pair = self.buckets[index]
if pair is None:
return None
return pair.val
def put(self, key: int, val: str):
"""追加操作"""
pair = Pair(key, val)
index: int = self.hash_func(key)
self.buckets[index] = pair
def remove(self, key: int):
"""削除操作"""
index: int = self.hash_func(key)
# None に設定し、削除を表現
self.buckets[index] = None
def entry_set(self) -> list[Pair]:
"""すべてのキー値ペアを取得"""
result: list[Pair] = []
for pair in self.buckets:
if pair is not None:
result.append(pair)
return result
def key_set(self) -> list[int]:
"""すべてのキーを取得"""
result = []
for pair in self.buckets:
if pair is not None:
result.append(pair.key)
return result
def value_set(self) -> list[str]:
"""すべての値を取得"""
result = []
for pair in self.buckets:
if pair is not None:
result.append(pair.val)
return result
def print(self):
"""ハッシュテーブルを出力"""
for pair in self.buckets:
if pair is not None:
print(pair.key, "->", pair.val)
```
=== "C++"
```cpp title="array_hash_map.cpp"
/* キー値ペア */
struct Pair {
public:
int key;
string val;
Pair(int key, string val) {
this->key = key;
this->val = val;
}
};
/* 配列実装に基づくハッシュテーブル */
class ArrayHashMap {
private:
vector<Pair *> buckets;
public:
ArrayHashMap() {
// 配列を初期化、100個のバケットを含む
buckets = vector<Pair *>(100);
}
~ArrayHashMap() {
// メモリを解放
for (const auto &bucket : buckets) {
delete bucket;
}
buckets.clear();
}
/* ハッシュ関数 */
int hashFunc(int key) {
int index = key % 100;
return index;
}
/* クエリ操作 */
string get(int key) {
int index = hashFunc(key);
Pair *pair = buckets[index];
if (pair == nullptr)
return "";
return pair->val;
}
/* 追加操作 */
void put(int key, string val) {
Pair *pair = new Pair(key, val);
int index = hashFunc(key);
buckets[index] = pair;
}
/* 削除操作 */
void remove(int key) {
int index = hashFunc(key);
// メモリを解放してnullptrに設定
delete buckets[index];
buckets[index] = nullptr;
}
/* すべてのキー値ペアを取得 */
vector<Pair *> pairSet() {
vector<Pair *> pairSet;
for (Pair *pair : buckets) {
if (pair != nullptr) {
pairSet.push_back(pair);
}
}
return pairSet;
}
/* すべてのキーを取得 */
vector<int> keySet() {
vector<int> keySet;
for (Pair *pair : buckets) {
if (pair != nullptr) {
keySet.push_back(pair->key);
}
}
return keySet;
}
/* すべての値を取得 */
vector<string> valueSet() {
vector<string> valueSet;
for (Pair *pair : buckets) {
if (pair != nullptr) {
valueSet.push_back(pair->val);
}
}
return valueSet;
}
/* ハッシュテーブルを印刷 */
void print() {
for (Pair *kv : pairSet()) {
cout << kv->key << " -> " << kv->val << endl;
}
}
};
```
=== "Java"
```java title="array_hash_map.java"
/* キー値ペア */
class Pair {
public int key;
public String val;
public Pair(int key, String val) {
this.key = key;
this.val = val;
}
}
/* 配列実装に基づくハッシュテーブル */
class ArrayHashMap {
private List<Pair> buckets;
public ArrayHashMap() {
// 100個のバケットを含む配列を初期化
buckets = new ArrayList<>();
for (int i = 0; i < 100; i++) {
buckets.add(null);
}
}
/* ハッシュ関数 */
private int hashFunc(int key) {
int index = key % 100;
return index;
}
/* クエリ操作 */
public String get(int key) {
int index = hashFunc(key);
Pair pair = buckets.get(index);
if (pair == null)
return null;
return pair.val;
}
/* 追加操作 */
public void put(int key, String val) {
Pair pair = new Pair(key, val);
int index = hashFunc(key);
buckets.set(index, pair);
}
/* 削除操作 */
public void remove(int key) {
int index = hashFunc(key);
// nullに設定して削除を示す
buckets.set(index, null);
}
/* すべてのキー値ペアを取得 */
public List<Pair> pairSet() {
List<Pair> pairSet = new ArrayList<>();
for (Pair pair : buckets) {
if (pair != null)
pairSet.add(pair);
}
return pairSet;
}
/* すべてのキーを取得 */
public List<Integer> keySet() {
List<Integer> keySet = new ArrayList<>();
for (Pair pair : buckets) {
if (pair != null)
keySet.add(pair.key);
}
return keySet;
}
/* すべての値を取得 */
public List<String> valueSet() {
List<String> valueSet = new ArrayList<>();
for (Pair pair : buckets) {
if (pair != null)
valueSet.add(pair.val);
}
return valueSet;
}
/* ハッシュテーブルを印刷 */
public void print() {
for (Pair kv : pairSet()) {
System.out.println(kv.key + " -> " + kv.val);
}
}
}
```
=== "C#"
```csharp title="array_hash_map.cs"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "Go"
```go title="array_hash_map.go"
[class]{pair}-[func]{}
[class]{arrayHashMap}-[func]{}
```
=== "Swift"
```swift title="array_hash_map.swift"
[file]{utils/pair.swift}-[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "JS"
```javascript title="array_hash_map.js"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "TS"
```typescript title="array_hash_map.ts"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "Dart"
```dart title="array_hash_map.dart"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "Rust"
```rust title="array_hash_map.rs"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "C"
```c title="array_hash_map.c"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "Kotlin"
```kotlin title="array_hash_map.kt"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "Ruby"
```ruby title="array_hash_map.rb"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
=== "Zig"
```zig title="array_hash_map.zig"
[class]{Pair}-[func]{}
[class]{ArrayHashMap}-[func]{}
```
## 6.1.3 &nbsp; ハッシュ衝突とリサイズ
本質的に、ハッシュ関数の役割は、すべてのキーの入力空間全体を、すべての配列インデックスの出力空間にマッピングすることです。しかし、入力空間は出力空間よりもはるかに大きいことがよくあります。したがって、**理論的には、「複数の入力が同じ出力に対応する」ケースが常に存在します**。
上記の例では、与えられたハッシュ関数で、入力`key`の下二桁が同じ場合、ハッシュ関数は同じ出力を生成します。例えば、学籍番号12836と20336の2人の学生をクエリすると、以下のことがわかります
```shell
12836 % 100 = 36
20336 % 100 = 36
```
下図に示すように、両方の学籍番号が同じ名前を指しており、これは明らかに間違っています。この複数の入力が同じ出力に対応する状況を<u>ハッシュ衝突</u>と呼びます。
![ハッシュ衝突の例](hash_map.assets/hash_collision.png){ class="animation-figure" }
<p align="center"> 図 6-3 &nbsp; ハッシュ衝突の例 </p>
ハッシュ表の容量$n$が増加するにつれて、複数のキーが同じバケットに割り当てられる確率が減少し、衝突が少なくなることは理解しやすいです。したがって、**ハッシュ表をリサイズすることでハッシュ衝突を減らすことができます**。
下図に示すように、リサイズ前は、キー値ペア`(136, A)`と`(236, D)`が衝突していました。しかし、リサイズ後は衝突が解決されています。
![ハッシュ表のリサイズ](hash_map.assets/hash_table_reshash.png){ class="animation-figure" }
<p align="center"> 図 6-4 &nbsp; ハッシュ表のリサイズ </p>
配列の拡張と同様に、ハッシュ表のリサイズにはすべてのキー値ペアを元のハッシュ表から新しいものに移行する必要があり、時間がかかります。さらに、ハッシュ表の`capacity`が変更されるため、ハッシュ関数を使用してすべてのキー値ペアの格納位置を再計算する必要があり、リサイズプロセスの計算オーバーヘッドがさらに増加します。したがって、プログラミング言語は頻繁なリサイズを防ぐために、ハッシュ表に十分大きな容量を割り当てることがよくあります。
<u>負荷率</u>はハッシュ表の重要な概念です。ハッシュ表内の要素数とバケット数の比率として定義されます。ハッシュ衝突の深刻度を測定するために使用され、**しばしばハッシュ表のリサイズのトリガーとしても機能します**。例えば、Javaでは、負荷率が$0.75$を超えると、システムはハッシュ表を元のサイズの2倍にリサイズします。

View File

@@ -0,0 +1,21 @@
---
comments: true
icon: material/table-search
---
# 第 6 章 &nbsp; ハッシュ表
![ハッシュ表](../assets/covers/chapter_hashing.jpg){ class="cover-image" }
!!! abstract
コンピューティングの世界において、ハッシュ表は賢い司書のようなものです。
インデックス番号の計算方法を理解し、目的の本を迅速に取得することを可能にします。
## 章の内容
- [6.1 &nbsp; ハッシュ表](hash_map.md)
- [6.2 &nbsp; ハッシュ衝突](hash_collision.md)
- [6.3 &nbsp; ハッシュアルゴリズム](hash_algorithm.md)
- [6.4 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,51 @@
---
comments: true
---
# 6.4 &nbsp; まとめ
### 1. &nbsp; 重要なポイント
- 入力`key`が与えられると、ハッシュ表は$O(1)$の時間で対応する`value`を取得でき、非常に効率的です。
- 一般的なハッシュ表の操作には、クエリ、キー値ペアの追加、キー値ペアの削除、ハッシュ表の走査があります。
- ハッシュ関数は`key`を配列インデックスにマッピングし、対応するバケットにアクセスして`value`を取得できるようにします。
- 2つの異なるキーがハッシュ化後に同じ配列インデックスになる場合があり、誤ったクエリ結果につながります。この現象はハッシュ衝突として知られています。
- ハッシュ表の容量が大きいほど、ハッシュ衝突の確率は低くなります。したがって、ハッシュ表のリサイズはハッシュ衝突を緩和できます。配列のリサイズと同様に、ハッシュ表のリサイズはコストが高いです。
- 要素数をバケット数で割った負荷率は、ハッシュ衝突の深刻度を反映し、しばしばハッシュ表リサイズのトリガー条件として使用されます。
- 連鎖法は各要素を連結リストに変換し、衝突するすべての要素を同じリストに格納することでハッシュ衝突に対処します。ただし、過度に長いリストはクエリ効率を低下させる可能性があり、リストを赤黒木に変換することで改善できます。
- オープンアドレス法は複数回のプローブを通してハッシュ衝突を処理します。線形プローブは固定ステップサイズを使用しますが、要素を削除できず、クラスタリングを起こしやすい傾向があります。多重ハッシュはプローブに複数のハッシュ関数を使用し、線形プローブと比較してクラスタリングを減らしますが、計算オーバーヘッドが増加します。
- 異なるプログラミング言語はさまざまなハッシュ表実装を採用しています。例えば、Javaの`HashMap`は連鎖法を使用し、Pythonの`dict`はオープンアドレス法を採用しています。
- ハッシュ表では、決定性、高効率、均等分散を持つハッシュアルゴリズムが望まれます。暗号化では、ハッシュアルゴリズムは衝突耐性と雪崩効果も持つべきです。
- ハッシュアルゴリズムは通常、ハッシュ値の均等分散を保証し、ハッシュ衝突を減らすために、大きな素数を剰余として使用します。
- 一般的なハッシュアルゴリズムには、MD5、SHA-1、SHA-2、SHA-3があります。MD5はファイル整合性チェックによく使用され、SHA-2は安全なアプリケーションとプロトコルで一般的に使用されます。
- プログラミング言語は通常、ハッシュ表のバケットインデックスを計算するために、データ型に対して組み込みのハッシュアルゴリズムを提供します。一般的に、不変オブジェクトのみがハッシュ可能です。
### 2. &nbsp; Q & A
**Q**: ハッシュ表の時間計算量が$O(n)$に悪化するのはいつですか?
ハッシュ表の時間計算量は、ハッシュ衝突が深刻な場合に$O(n)$に悪化する可能性があります。ハッシュ関数が適切に設計され、容量が適切に設定され、衝突が均等に分散されている場合、時間計算量は$O(1)$です。プログラミング言語の組み込みハッシュ表を使用する場合、通常は時間計算量を$O(1)$と考えます。
**Q**: なぜハッシュ関数$f(x) = x$を使用しないのですか?これなら衝突を排除できます。
ハッシュ関数$f(x) = x$では、各要素が一意のバケットインデックスに対応し、これは配列と同等です。しかし、入力空間は通常出力空間(配列長)よりもはるかに大きいため、ハッシュ関数の最後のステップは配列長の剰余を取ることがよくあります。言い換えると、ハッシュ表の目標は、$O(1)$のクエリ効率を提供しながら、より大きな状態空間をより小さなものにマッピングすることです。
**Q**: ハッシュ表がこれらの構造を使って実装されているにもかかわらず、なぜ配列、連結リスト、二分木よりも効率的になれるのですか?
まず、ハッシュ表は時間効率が高いですが、空間効率は低いです。ハッシュ表のメモリの大部分は未使用のままです。
次に、ハッシュ表は特定のユースケースでのみ時間効率が高いです。配列や連結リストを使用して同じ時間計算量で機能を実装できる場合、通常はハッシュ表を使用するよりも高速です。これは、ハッシュ関数の計算がオーバーヘッドを発生させ、時間計算量の定数因子が大きくなるためです。
最後に、ハッシュ表の時間計算量は悪化する可能性があります。例えば、連鎖法では、連結リストや赤黒木で検索操作を実行し、これは依然として$O(n)$時間に悪化するリスクがあります。
**Q**: 多重ハッシュにも要素を直接削除できないという欠陥がありますか?削除としてマークされた空間は再利用できますか?
多重ハッシュはオープンアドレス法の一形態であり、すべてのオープンアドレス法には要素を直接削除できないという欠点があります。要素を削除済みとしてマークする必要があります。マークされた空間は再利用できます。ハッシュ表に新しい要素を挿入する際、ハッシュ関数が削除済みとしてマークされた位置を指している場合、その位置は新しい要素によって使用できます。これにより、ハッシュ表のプローブシーケンスを維持しながら、空間の効率的な使用が保証されます。
**Q**: なぜ線形プローブの検索プロセス中にハッシュ衝突が発生するのですか?
検索プロセス中、ハッシュ関数は対応するバケットとキー値ペアを指します。`key`が一致しない場合、ハッシュ衝突を示します。したがって、線形プローブは正しいキー値ペアが見つかるか検索が失敗するまで、事前に決められたステップサイズで下方向に検索します。
**Q**: なぜハッシュ表のリサイズがハッシュ衝突を緩和できるのですか?
ハッシュ関数の最後のステップは、出力を配列インデックス範囲内に保つために、配列長$n$の剰余を取ることがよくあります。リサイズ時、配列長$n$が変化し、キーに対応するインデックスも変化する可能性があります。以前に同じバケットにマッピングされていたキーが、リサイズ後に複数のバケットに分散される可能性があり、それによってハッシュ衝突が緩和されます。

View File

@@ -0,0 +1,182 @@
---
comments: true
---
# 8.2 &nbsp; ヒープ構築操作
場合によっては、リストのすべての要素を使用してヒープを構築したいことがあり、このプロセスは「ヒープ構築操作」として知られています。
## 8.2.1 &nbsp; ヒープ挿入操作による実装
まず、空のヒープを作成し、次にリストを反復処理して、各要素に対して順番に「ヒープ挿入操作」を実行します。これは、要素をヒープの末尾に追加し、次に下から上に「ヒープ化」することを意味します。
ヒープに要素が追加されるたびに、ヒープの長さは1つずつ増加します。ードは二分木に上から下に追加されるため、ヒープは「上から下に」構築されます。
要素数を$n$とすると、各要素の挿入操作は$O(\log{n})$時間かかるため、このヒープ構築方法の時間計算量は$O(n \log n)$です。
## 8.2.2 &nbsp; 走査によるヒープ化の実装
実際には、2つのステップでより効率的なヒープ構築方法を実装できます。
1. リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。
2. ヒープを逆順(レベル順走査の逆)で走査し、各非葉ノードに対して「上から下のヒープ化」を実行します。
**ノードをヒープ化した後、そのノードを根とする部分木は有効な部分ヒープになります**。走査が逆順であるため、ヒープは「下から上に」構築されます。
逆走査を選択する理由は、現在のノードの下の部分木がすでに有効な部分ヒープであることを保証し、現在のノードのヒープ化を効果的にするためです。
言及する価値があるのは、**葉ノードは子を持たないため、自然に有効な部分ヒープを形成し、ヒープ化する必要がない**ということです。以下のコードに示すように、最後の非葉ノードは最後のノードの親です。そこから開始して逆順に走査してヒープ化を実行します:
=== "Python"
```python title="my_heap.py"
def __init__(self, nums: list[int]):
"""コンストラクタ、入力リストに基づいてヒープを構築"""
# すべてのリスト要素をヒープに追加
self.max_heap = nums
# 葉以外のすべてのノードをヒープ化
for i in range(self.parent(self.size() - 1), -1, -1):
self.sift_down(i)
```
=== "C++"
```cpp title="my_heap.cpp"
/* コンストラクタ、入力リストに基づいてヒープを構築 */
MaxHeap(vector<int> nums) {
// すべてのリスト要素をヒープに追加
maxHeap = nums;
// 葉以外のすべてのノードをヒープ化
for (int i = parent(size() - 1); i >= 0; i--) {
siftDown(i);
}
}
```
=== "Java"
```java title="my_heap.java"
/* コンストラクタ、入力リストに基づいてヒープを構築 */
MaxHeap(List<Integer> nums) {
// すべてのリスト要素をヒープに追加
maxHeap = new ArrayList<>(nums);
// 葉を除くすべてのノードをヒープ化
for (int i = parent(size() - 1); i >= 0; i--) {
siftDown(i);
}
}
```
=== "C#"
```csharp title="my_heap.cs"
[class]{MaxHeap}-[func]{MaxHeap}
```
=== "Go"
```go title="my_heap.go"
[class]{maxHeap}-[func]{newMaxHeap}
```
=== "Swift"
```swift title="my_heap.swift"
[class]{MaxHeap}-[func]{init}
```
=== "JS"
```javascript title="my_heap.js"
[class]{MaxHeap}-[func]{constructor}
```
=== "TS"
```typescript title="my_heap.ts"
[class]{MaxHeap}-[func]{constructor}
```
=== "Dart"
```dart title="my_heap.dart"
[class]{MaxHeap}-[func]{MaxHeap}
```
=== "Rust"
```rust title="my_heap.rs"
[class]{MaxHeap}-[func]{new}
```
=== "C"
```c title="my_heap.c"
[class]{MaxHeap}-[func]{newMaxHeap}
```
=== "Kotlin"
```kotlin title="my_heap.kt"
[class]{MaxHeap}-[func]{}
```
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{initialize}
```
=== "Zig"
```zig title="my_heap.zig"
[class]{MaxHeap}-[func]{init}
```
## 8.2.3 &nbsp; 計算量分析
次に、この第2のヒープ構築方法の時間計算量を計算してみましょう。
- 完備二分木のノード数を$n$と仮定すると、葉ノードの数は$(n + 1) / 2$です。ここで$/$ は整数除算です。したがって、ヒープ化が必要なノードの数は$(n - 1) / 2$です。
- 「上から下のヒープ化」のプロセスでは、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ$\log n$です。
この2つを掛け合わせると、ヒープ構築プロセスの時間計算量は$O(n \log n)$となります。**しかし、この推定は正確ではありません。二分木の下位レベルには上位よりもはるかに多くのノードがあるという性質を考慮していないからです。**
より正確な計算を行いましょう。計算を簡素化するため、$n$個のノードと高さ$h$を持つ「完全二分木」を仮定します。この仮定は結果の正確性に影響しません。
![完全二分木の各レベルのノード数](build_heap.assets/heapify_operations_count.png){ class="animation-figure" }
<p align="center"> 図 8-5 &nbsp; 完全二分木の各レベルのノード数 </p>
上図に示すように、ノードが「上から下にヒープ化される」最大反復回数は、そのノードから葉ノードまでの距離と等しく、これは正確に「ノードの高さ」です。したがって、各レベルで「ノード数×ノードの高さ」を合計して、**すべてのノードの総ヒープ化反復回数を得る**ことができます。
$$
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
$$
上記の方程式を簡素化するために、高校の数列の知識を使用する必要があります。まず$T(h)$に$2$を掛けて以下を得ます:
$$
\begin{aligned}
T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
2T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^h\times1 \newline
\end{aligned}
$$
変位法を使用して$2T(h)$から$T(h)$を減算すると、以下を得ます:
$$
2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h
$$
方程式を観察すると、$T(h)$は等比数列であり、和の公式を使用して直接計算でき、時間計算量は以下になります:
$$
\begin{aligned}
T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
& = 2^{h+1} - h - 2 \newline
& = O(2^h)
\end{aligned}
$$
さらに、高さ$h$の完全二分木は$n = 2^{h+1} - 1$個のノードを持つため、計算量は$O(2^h) = O(n)$です。この計算は、**リストを入力してヒープを構築する時間計算量が$O(n)$であり、非常に効率的である**ことを示しています。

1153
ja/docs/chapter_heap/heap.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
---
comments: true
icon: material/family-tree
---
# 第 8 章 &nbsp; ヒープ
![ヒープ](../assets/covers/chapter_heap.jpg){ class="cover-image" }
!!! abstract
ヒープは山とその険しい峰のように、層をなして起伏し、それぞれが独特の形を持っています。
各山の頂は散らばった高さで上下しますが、最も高いものが常に最初に注目を集めます。
## 章の内容
- [8.1 &nbsp; ヒープ](heap.md)
- [8.2 &nbsp; ヒープの構築](build_heap.md)
- [8.3 &nbsp; Top-k問題](top_k.md)
- [8.4 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,21 @@
---
comments: true
---
# 8.4 &nbsp; まとめ
### 1. &nbsp; 重要な復習
- ヒープは完備二分木で、その構築性質に基づいて最大ヒープまたは最小ヒープに分類できます。最大ヒープの先頭要素は最大で、最小ヒープの先頭要素は最小です。
- 優先度キューは、デキューの優先度を持つキューとして定義され、通常ヒープを使用して実装されます。
- ヒープの一般的な操作とそれに対応する時間計算量には以下があります:ヒープへの要素挿入$O(\log n)$、ヒープからの先頭要素削除$O(\log n)$、ヒープの先頭要素へのアクセス$O(1)$。
- 完備二分木は配列で表現するのに適しているため、ヒープは一般的に配列を使用して格納されます。
- ヒープ化操作はヒープの性質を維持するために使用され、ヒープの挿入操作と削除操作の両方で使用されます。
- $n$個の要素が入力として与えられた場合のヒープ構築の時間計算量は$O(n)$に最適化でき、これは非常に効率的です。
- Top-kは古典的なアルゴリズム問題で、ヒープデータ構造を使用して効率的に解決でき、時間計算量は$O(n \log k)$です。
### 2. &nbsp; Q & A
**Q**: データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか?
この2つは、どちらも「ヒープ」と呼ばれますが、同じ概念ではありません。コンピュータシステムメモリのヒープは動的メモリ割り当ての一部で、プログラムが実行中にデータを格納するために使用できます。プログラムは、オブジェクトや配列などの複雑な構造を格納するために、一定量のヒープメモリを要求できます。割り当てられたデータが不要になったときは、メモリリークを防ぐためにプログラムがこのメモリを解放する必要があります。スタックメモリと比較して、ヒープメモリの管理と使用にはより多くの注意が必要で、不適切な使用はメモリリークやダングリングポインタにつながる可能性があります。

View File

@@ -0,0 +1,234 @@
---
comments: true
---
# 8.3 &nbsp; Top-k問題
!!! question
長さ$n$の順序付けられていない配列`nums`が与えられたとき、配列内の最大$k$個の要素を返してください。
この問題について、まず2つの直接的な解法を紹介し、次により効率的なヒープベースの方法を説明します。
## 8.3.1 &nbsp; 方法1反復選択
下図に示すように、$k$回の反復を実行し、各回で$1$番目、$2$番目、$\dots$、$k$番目に大きい要素を抽出できます。時間計算量は$O(nk)$です。
この方法は$k \ll n$の場合にのみ適しています。$k$が$n$に近い場合、時間計算量は$O(n^2)$に近づき、非常に時間がかかります。
![最大k個の要素を反復的に見つける](top_k.assets/top_k_traversal.png){ class="animation-figure" }
<p align="center"> 図 8-6 &nbsp; 最大k個の要素を反復的に見つける </p>
!!! tip
$k = n$の場合、完全に順序付けられたシーケンスを得ることができ、これは「選択ソート」アルゴリズムと同等です。
## 8.3.2 &nbsp; 方法2ソート
下図に示すように、まず配列`nums`をソートし、次に最後の$k$個の要素を返すことができます。時間計算量は$O(n \log n)$です。
明らかに、この方法はタスクを「やりすぎ」ています。最大$k$個の要素を見つけるだけでよく、他の要素をソートする必要はありません。
![ソートによる最大k個の要素の発見](top_k.assets/top_k_sorting.png){ class="animation-figure" }
<p align="center"> 図 8-7 &nbsp; ソートによる最大k個の要素の発見 </p>
## 8.3.3 &nbsp; 方法3ヒープ
以下のプロセスに示すように、ヒープに基づいてTop-k問題をより効率的に解決できます。
1. 最小ヒープを初期化します。先頭要素が最小になります。
2. まず、配列の最初の$k$個の要素をヒープに挿入します。
3. $k + 1$番目の要素から開始し、現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに挿入します。
4. 走査を完了した後、ヒープには最大$k$個の要素が含まれています。
=== "<1>"
![ヒープに基づく最大k個の要素の発見](top_k.assets/top_k_heap_step1.png){ class="animation-figure" }
=== "<2>"
![top_k_heap_step2](top_k.assets/top_k_heap_step2.png){ class="animation-figure" }
=== "<3>"
![top_k_heap_step3](top_k.assets/top_k_heap_step3.png){ class="animation-figure" }
=== "<4>"
![top_k_heap_step4](top_k.assets/top_k_heap_step4.png){ class="animation-figure" }
=== "<5>"
![top_k_heap_step5](top_k.assets/top_k_heap_step5.png){ class="animation-figure" }
=== "<6>"
![top_k_heap_step6](top_k.assets/top_k_heap_step6.png){ class="animation-figure" }
=== "<7>"
![top_k_heap_step7](top_k.assets/top_k_heap_step7.png){ class="animation-figure" }
=== "<8>"
![top_k_heap_step8](top_k.assets/top_k_heap_step8.png){ class="animation-figure" }
=== "<9>"
![top_k_heap_step9](top_k.assets/top_k_heap_step9.png){ class="animation-figure" }
<p align="center"> 図 8-8 &nbsp; ヒープに基づく最大k個の要素の発見 </p>
サンプルコードは以下の通りです:
=== "Python"
```python title="top_k.py"
def top_k_heap(nums: list[int], k: int) -> list[int]:
"""ヒープを使用して配列内の最大k個の要素を見つける"""
# 最小ヒープを初期化
heap = []
# 配列の最初のk個の要素をヒープに入力
for i in range(k):
heapq.heappush(heap, nums[i])
# k+1番目の要素から、ヒープの長さをkに保つ
for i in range(k, len(nums)):
# 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力
if nums[i] > heap[0]:
heapq.heappop(heap)
heapq.heappush(heap, nums[i])
return heap
```
=== "C++"
```cpp title="top_k.cpp"
/* ヒープを使用して配列内の最大k個の要素を見つける */
priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {
// 最小ヒープを初期化
priority_queue<int, vector<int>, greater<int>> heap;
// 配列の最初のk個の要素をヒープに入力
for (int i = 0; i < k; i++) {
heap.push(nums[i]);
}
// k+1番目の要素から、ヒープの長さをkに保つ
for (int i = k; i < nums.size(); i++) {
// 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力
if (nums[i] > heap.top()) {
heap.pop();
heap.push(nums[i]);
}
}
return heap;
}
```
=== "Java"
```java title="top_k.java"
/* ヒープを使用して配列内の最大 k 個の要素を検索 */
Queue<Integer> topKHeap(int[] nums, int k) {
// 最小ヒープを初期化
Queue<Integer> heap = new PriorityQueue<Integer>();
// 配列の最初の k 個の要素をヒープに入力
for (int i = 0; i < k; i++) {
heap.offer(nums[i]);
}
// k+1 番目の要素から、ヒープの長さを k に保つ
for (int i = k; i < nums.length; i++) {
// 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力
if (nums[i] > heap.peek()) {
heap.poll();
heap.offer(nums[i]);
}
}
return heap;
}
```
=== "C#"
```csharp title="top_k.cs"
[class]{top_k}-[func]{TopKHeap}
```
=== "Go"
```go title="top_k.go"
[class]{}-[func]{topKHeap}
```
=== "Swift"
```swift title="top_k.swift"
[class]{}-[func]{topKHeap}
```
=== "JS"
```javascript title="top_k.js"
[class]{}-[func]{pushMinHeap}
[class]{}-[func]{popMinHeap}
[class]{}-[func]{peekMinHeap}
[class]{}-[func]{getMinHeap}
[class]{}-[func]{topKHeap}
```
=== "TS"
```typescript title="top_k.ts"
[class]{}-[func]{pushMinHeap}
[class]{}-[func]{popMinHeap}
[class]{}-[func]{peekMinHeap}
[class]{}-[func]{getMinHeap}
[class]{}-[func]{topKHeap}
```
=== "Dart"
```dart title="top_k.dart"
[class]{}-[func]{topKHeap}
```
=== "Rust"
```rust title="top_k.rs"
[class]{}-[func]{top_k_heap}
```
=== "C"
```c title="top_k.c"
[class]{}-[func]{pushMinHeap}
[class]{}-[func]{popMinHeap}
[class]{}-[func]{peekMinHeap}
[class]{}-[func]{getMinHeap}
[class]{}-[func]{topKHeap}
```
=== "Kotlin"
```kotlin title="top_k.kt"
[class]{}-[func]{topKHeap}
```
=== "Ruby"
```ruby title="top_k.rb"
[class]{}-[func]{top_k_heap}
```
=== "Zig"
```zig title="top_k.zig"
[class]{}-[func]{topKHeap}
```
合計$n$回のヒープ挿入と削除が実行され、最大ヒープサイズが$k$であるため、時間計算量は$O(n \log k)$です。この方法は非常に効率的で、$k$が小さい場合、時間計算量は$O(n)$に近づき、$k$が大きい場合でも、時間計算量は$O(n \log n)$を超えません。
さらに、この方法は動的データストリームのシナリオに適しています。データを継続的に追加することで、ヒープ内の要素を維持し、最大$k$個の要素の動的更新を実現できます。

View File

@@ -0,0 +1,30 @@
---
comments: true
icon: material/rocket-launch-outline
---
# はじめに
数年前、私はLeetCodeで「剣指Offer」の問題解答を共有し、多くの読者から励ましとサポートを受けました。読者とのやり取りの中で、最もよく聞かれた質問は「アルゴリズムの勉強をどう始めたらよいか」でした。次第に、私はこの質問に強い関心を抱くようになりました。
問題を直接解くことが最も人気のある方法のようです。これはシンプルで直接的で効果的です。しかし、問題解決はマインスイーパーをプレイするようなものです。自学自習の能力が高い人は、地雷を一つずつ回避していくことができますが、しっかりとした基礎がない人は、何度もつまずいて挫折しながら後退することになるかもしれません。教科書を読むことも一般的な方法ですが、就職活動中の人にとって、卒業論文の執筆、履歴書の提出、筆記試験や面接の準備が既にエネルギーの大部分を消費しており、分厚い本を読むことはしばしば困難な挑戦となります。
もしあなたが同様の悩みを抱えているなら、この本があなたを見つけることができて幸運です。この本は、この質問に対する私の答えです。これが最良の解決策ではないかもしれませんが、少なくとも積極的な試みです。この本があなたに直接内定をもたらすことはできませんが、データ構造とアルゴリズムの「知識地図」を探索する手引きとなり、さまざまな「地雷」の形、大きさ、位置を理解し、さまざまな「地雷除去方法」をマスターできるようお手伝いします。これらのスキルがあれば、より快適に問題を解き、文献を読むことができ、徐々に知識体系を構築できると信じています。
私は、ファインマン教授の言葉に深く同感します。「知識は無料ではありません。注意を払わなければならないのです。」この意味で、この本は完全に「無料」ではありません。この本に対するあなたの貴重な「注意」に応えるために、私は最善を尽くし、最大の「注意」を払ってこの本を書きます。
自分の限界を認識しており、この本の内容が時間をかけて洗練されたにもかかわらず、間違いは確実に残っていることを理解しています。先生方や学生の皆様からの批評と訂正を心から歓迎いたします。
![Hello Algo](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" }
<div style="text-align: center;">
<h2 style="margin-top: 0.8em; margin-bottom: 0.8em;">Hello, Algo!</h2>
</div>
コンピュータの出現は世界に大きな変化をもたらしました。高速な計算能力と優れたプログラム可能性により、コンピュータはアルゴリズムを実行しデータを処理するための理想的な媒体となりました。ビデオゲームのリアルなグラフィックス、自動運転の知的な判断、AlphaGoの見事な囲碁ゲーム、ChatGPTの自然な対話など、これらのアプリケーションはすべて、コンピュータ上で動作するアルゴリズムの精巧な実演です。
実際、コンピュータの出現以前から、アルゴリズムとデータ構造は世界の至る所に存在していました。初期のアルゴリズムは比較的シンプルで、古代の計数方法や道具作りの手順などがありました。文明が進歩するにつれて、アルゴリズムはより洗練され複雑になりました。職人の精巧な技術から、生産力を解放する工業製品、宇宙を支配する科学法則まで、ほぼすべての平凡または驚異的なことの背後には、アルゴリズムの巧妙な思考があります。
同様に、データ構造は至る所にあります。ソーシャルネットワークから地下鉄路線まで、多くのシステムは「グラフ」としてモデル化できます。国から家族まで、社会組織の主要な形態は「木」の特徴を示します。冬服は「スタック」のようで、最初に着たものが最後に脱がれます。バドミントンのシャトル筒は「キュー」に似ており、一方の端で挿入し、もう一方の端で取り出します。辞書は「ハッシュテーブル」のようで、目標エントリを素早く検索できます。
この本は、明確で理解しやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、プログラミングを通じてそれらを実装できるようになることを目指しています。この基盤の上で、この本は複雑な世界におけるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示すことに努めています。この本があなたのお役に立てることを願っています!

View File

@@ -0,0 +1,66 @@
---
comments: true
---
# 1.1 &nbsp; アルゴリズムは至る所にある
「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし、多くのアルゴリズムは複雑な数学を含まず、基本的な論理により多く依存しており、これは私たちの日常生活の至る所で見ることができます。
アルゴリズムについて正式に議論を始める前に、興味深い事実を共有する価値があります。**あなたは無意識のうちに多くのアルゴリズムを学び、日常生活でそれらを応用することに慣れています**。ここで、この点を証明するためにいくつかの具体的な例を挙げます。
**例1辞書の引き方**。英語の辞書では、単語がアルファベット順に並んでいます。$r$で始まる単語を探していると仮定すると、通常は以下の方法で行います:
1. 辞書を大体半分ぐらいのところで開き、そのページの最初の語彙を確認します。例えば$m$で始まる文字だとしましょう。
2. $r$はアルファベットで$m$の後に来るので、前半を無視して、探索空間を後半に絞ります。
3. $r$で始まる単語を見つけるまで、ステップ`1.``2.`を繰り返します。
=== "<1>"
![辞書を引く過程](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" }
=== "<2>"
![辞書での二分探索ステップ2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" }
=== "<3>"
![辞書での二分探索ステップ3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" }
=== "<4>"
![辞書での二分探索ステップ4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" }
=== "<5>"
![辞書での二分探索ステップ5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" }
<p align="center"> 図 1-1 &nbsp; 辞書を引く過程 </p>
辞書を引くことは、小学生にとって必須のスキルですが、実際には有名な「二分探索」アルゴリズムです。データ構造の観点から、辞書をソートされた「配列」と考えることができます。アルゴリズムの観点から、辞書で単語を探すために取られる一連の行動は、「二分探索」アルゴリズムと見なすことができます。
**例2トランプの整理**。トランプをプレイするとき、手札を昇順に並べる必要があります。以下の過程で示されます。
1. トランプを「整列済み」と「未整列」のセクションに分けます。最初は一番左のカードが既に整列していると仮定します。
2. 未整列セクションからカードを1枚取り出し、整列済みセクションの正しい位置に挿入します。この後、左端の2枚のカードが整列します。
3. すべてのカードが整列するまで、ステップ`2`を繰り返します。
![トランプの整理過程](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" }
<p align="center"> 図 1-2 &nbsp; トランプの整理過程 </p>
上記のトランプを整理する方法は、実質的に「挿入ソート」アルゴリズムであり、小さなデータセットに対して非常に効率的です。多くのプログラミング言語のソート関数には挿入ソートが含まれています。
**例3お釣りの計算**。スーパーマーケットで$69$の買い物をしたと仮定します。レジ係に$100$を渡すと、$31$のお釣りを提供する必要があります。この過程は以下の図で明確に理解できます。
1. 選択肢は$31$以下の価値のある通貨で、$1$、$5$、$10$、$20$が含まれます。
2. 選択肢から最大の$20$を取り出し、$31 - 20 = 11$が残ります。
3. 残りの選択肢から最大の$10$を取り出し、$11 - 10 = 1$が残ります。
4. 残りの選択肢から最大の$1$を取り出し、$1 - 1 = 0$が残ります。
5. お釣りの計算が完了し、解答は$20 + 10 + 1 = 31$です。
![お釣りの計算過程](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" }
<p align="center"> 図 1-3 &nbsp; お釣りの計算過程 </p>
記述されたステップでは、利用可能な最大の額面を使用して各段階で最良の選択肢を選ぶことで、効果的なお釣り計算戦略につながります。データ構造とアルゴリズムの観点から、このアプローチは「貪欲」アルゴリズムとして知られています。
料理の準備から宇宙旅行まで、ほぼすべての問題解決にはアルゴリズムが関わっています。コンピュータの出現により、メモリにデータ構造を格納し、CPUとGPUを呼び出してアルゴリズムを実行するコードを書くことができるようになりました。このようにして、現実世界の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できます。
!!! tip
データ構造、アルゴリズム、配列、二分探索などの概念についてまだ混乱している場合は、読み続けることをお勧めします。この本は、データ構造とアルゴリズムの理解の領域へと優しく導いてくれるでしょう。

View File

@@ -0,0 +1,20 @@
---
comments: true
icon: material/calculator-variant-outline
---
# 第 1 章 &nbsp; アルゴリズムとの出会い
![アルゴリズムとの出会い](../assets/covers/chapter_introduction.jpg){ class="cover-image" }
!!! abstract
優雅な乙女が踊ります。データと絡み合い、アルゴリズムのメロディーに合わせてスカートをなびかせながら。
彼女があなたをダンスに誘います。彼女のステップに従って、論理と美に満ちたアルゴリズムの世界に入りましょう。
## 章の内容
- [1.1 &nbsp; アルゴリズムはどこにでもある](algorithms_are_everywhere.md)
- [1.2 &nbsp; アルゴリズムとは何か](what_is_dsa.md)
- [1.3 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,26 @@
---
comments: true
---
# 1.3 &nbsp; まとめ
- アルゴリズムは日常生活にありふれており、思っているほどアクセスしにくく複雑なものではありません。実際、私たちは既に無意識のうちに多くのアルゴリズムを学び、生活の様々な問題を解決するために使用しています。
- 辞書で単語を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム概念を体現しています。
- トランプを整理する過程は挿入ソートアルゴリズムと非常に似ています。挿入ソートアルゴリズムは小さなデータセットのソートに適しています。
- 通貨でお釣りを計算するステップは本質的に貪欲アルゴリズムに従っており、各ステップでその時点での最良の選択をします。
- アルゴリズムは有限時間内で特定の問題を解決するための段階的な指示のセットですが、データ構造はコンピュータでのデータの組織化と保存方法を定義します。
- データ構造とアルゴリズムは密接に関連しています。データ構造はアルゴリズムの基礎であり、アルゴリズムはデータ構造の機能を活用するステージです。
- データ構造とアルゴリズムをブロックの組み立てに例えることができます。ブロックはデータを表し、ブロックの形状と接続方法はデータ構造を表し、ブロックを組み立てるステップはアルゴリズムに対応します。
### 1. &nbsp; Q & A
**Q**:プログラマーとして、日常の仕事でアルゴリズムを手動で実装する必要があることはめったにありません。最も一般的に使用されるアルゴリズムは、既にプログラミング言語とライブラリに組み込まれており、すぐに使用できます。これは、私たちが仕事で遭遇する問題が、カスタムアルゴリズム設計を必要とする複雑さのレベルにまだ達していないことを示唆していますか?
特定の仕事スキルが武術の「技」のようなものだとすれば、基礎科目は「内功」のようなものです。
アルゴリズム(およびその他の基礎科目)を学ぶ意義は、必ずしも仕事でそれらを一から実装することではなく、概念の確固たる理解に基づいて、より専門的な意思決定と問題解決を可能にし、それによって仕事の全体的な質を向上させることだと私は信じています。例えば、すべてのプログラミング言語には組み込みのソート関数があります:
- データ構造とアルゴリズムを学んでいない場合、どんなデータが与えられても、このソート関数に渡すだけかもしれません。スムーズに動作し、良いパフォーマンスを示し、問題がないように見えます。
- しかし、アルゴリズムを学んだことがあれば、組み込みのソート関数の時間複雑度は通常$O(n \log n)$であることを理解しています。さらに、データが固定桁数の整数学生IDなどで構成されている場合、基数ソートのようなより効率的なアプローチを適用でき、時間複雑度をO(nk)に削減できます。ここでkは桁数です。大量のデータを処理する際、節約された時間は重要な価値に変わります — コストの削減、ユーザーエクスペリエンスの向上、システムパフォーマンスの向上。
エンジニアリングでは、多くの問題を最適に解決することは困難です。ほとんどは「準最適」解決策で対処されます。問題の難しさは、その固有の複雑さだけでなく、それに取り組む人の知識と経験にも依存します。専門知識と経験が深いほど、分析がより徹底的になり、問題をより優雅に解決できます。

View File

@@ -0,0 +1,65 @@
---
comments: true
---
# 1.2 &nbsp; アルゴリズムとは何か
## 1.2.1 &nbsp; アルゴリズムの定義
<u>アルゴリズム</u>は、有限時間内で特定の問題を解決するための一連の指示またはステップです。以下の特徴があります:
- 問題が明確に定義されており、入力と出力の明確な定義が含まれています。
- アルゴリズムは実行可能で、有限の回数のステップ、時間、メモリ空間内で完了できることを意味します。
- 各ステップには明確な意味があります。同じ入力と条件の下で出力は一貫して同じです。
## 1.2.2 &nbsp; データ構造の定義
<u>データ構造</u>は、コンピュータ内でデータを組織し保存する方法で、以下の設計目標があります:
- コンピュータのメモリを節約するために空間占有を最小化する。
- データ操作を可能な限り高速にし、データのアクセス、追加、削除、更新などをカバーする。
- 効率的なアルゴリズム実行を可能にするために、簡潔なデータ表現と論理情報を提供する。
**データ構造の設計はバランスを取る行為であり、しばしばトレードオフが必要です**。一つの側面を改善したい場合、しばしば別の側面で妥協する必要があります。以下は2つの例です
- 配列と比較して、連結リストはデータの追加と削除においてより便利ですが、データアクセス速度を犠牲にします。
- 連結リストと比較して、グラフはより豊富な論理情報を提供しますが、より多くのメモリ空間が必要です。
## 1.2.3 &nbsp; データ構造とアルゴリズムの関係
以下の図に示すように、データ構造とアルゴリズムは高度に関連し、密接に統合されており、具体的には以下の3つの側面があります
- データ構造はアルゴリズムの基礎です。構造化されたデータ保存とアルゴリズムのためのデータ操作方法を提供します。
- アルゴリズムはデータ構造に活力を注入します。データ構造だけではデータ情報を保存するだけです。アルゴリズムの応用によって、特定の問題を解決できます。
- アルゴリズムは異なるデータ構造に基づいて実装できることが多いですが、実行効率は大きく異なることがあります。適切なデータ構造を選択することが鍵です。
![データ構造とアルゴリズムの関係](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" }
<p align="center"> 図 1-4 &nbsp; データ構造とアルゴリズムの関係 </p>
データ構造とアルゴリズムは、以下の図に示すように、ブロックのセットに例えることができます。ブロックセットには多数のピースが含まれ、詳細な組み立て説明書が付いています。これらの説明書に段階的に従うことで、複雑なブロックモデルを構築できます。
![ブロックの組み立て](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" }
<p align="center"> 図 1-5 &nbsp; ブロックの組み立て </p>
両者の詳細な対応関係は以下の表に示されています。
<p align="center"> 表 1-1 &nbsp; データ構造とアルゴリズムをブロックと比較 </p>
<div class="center-table" markdown>
| データ構造とアルゴリズム | ブロック |
| ------------------------ | ----------------------------------------------------- |
| 入力データ | 未組み立てのブロック |
| データ構造 | ブロックの組織、形状、サイズ、接続などを含む |
| アルゴリズム | ブロックを望ましい形状に組み立てる一連のステップ |
| 出力データ | 完成したブロックモデル |
</div>
データ構造とアルゴリズムはプログラミング言語から独立していることは注目に値します。この理由により、この本は複数のプログラミング言語での実装を提供できます。
!!! tip "慣習的な略語"
実生活の議論では、「データ構造とアルゴリズム」を単純に「アルゴリズム」と呼ぶことがよくあります。例えば、よく知られたLeetCodeアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識をテストしています。

View File

@@ -0,0 +1,58 @@
---
comments: true
---
# 0.1 &nbsp; この本について
このオープンソースプロジェクトは、データ構造とアルゴリズムに関する無料で初心者にやさしいクラッシュコースの作成を目指しています。
- アニメーション付きの図解、理解しやすい内容、滑らかな学習曲線により、初心者がデータ構造とアルゴリズムの「知識マップ」を探索するのに役立ちます。
- ワンクリックでコードを実行できるため、読者のプログラミングスキルの向上と、アルゴリズムの動作原理およびデータ構造の基礎実装の理解に役立ちます。
- 教えることによる学習を促進し、質問や洞察の共有を自由に行ってください。議論を通じて一緒に成長しましょう。
## 0.1.1 &nbsp; 対象読者
もしあなたがアルゴリズムに触れたばかりで経験が限られている場合、またはアルゴリズムである程度の経験を積んでいても、データ構造とアルゴリズムについて曖昧な理解しかなく、常に「分かった」と「うーん」の間を行き来している場合、この本はあなたのためのものです!
すでにある程度の問題解決経験を積んでおり、ほとんどのタイプの問題に精通している場合、この本はアルゴリズムの知識体系を復習し整理するのに役立ちます。リポジトリのソースコードは「問題解決ツールキット」や「アルゴリズムチートシート」として使用できます。
もしあなたがアルゴリズムの専門家であれば、貴重な提案をいただくか、[参加して協力](https://www.hello-algo.com/chapter_appendix/contribution/)していただければと思います。
!!! success "前提条件"
少なくとも一つのプログラミング言語で簡単なコードを書いて読むことができる必要があります。
## 0.1.2 &nbsp; 内容構成
本書の主な内容を下図に示します。
- **計算量解析**: データ構造とアルゴリズムを評価する側面と方法を探求します。時間計算量と空間計算量を導出する方法、および一般的なタイプと例を扱います。
- **データ構造**: 基本的なデータ型、分類方法、定義、長所と短所、一般的な操作、タイプ、応用、および配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の実装方法に焦点を当てます。
- **アルゴリズム**: アルゴリズムを定義し、その長所と短所、効率性、応用シナリオ、問題解決ステップについて議論し、検索、ソート、分割統治、バックトラッキング、動的プログラミング、貪欲アルゴリズムなど、さまざまなアルゴリズムのサンプル問題を含みます。
![本書の主な内容](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" }
<p align="center"> 図 0-1 &nbsp; 本書の主な内容 </p>
## 0.1.3 &nbsp; 謝辞
この本は、オープンソースコミュニティの多くの貢献者の共同努力により継続的に改善されています。時間とエネルギーを投資してくださった各執筆者に感謝いたします。GitHubで生成された順序で記載されています: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, msk397, gvenusleo, khoaxuantu, RiverTwilight, rongyi, gyt95, zhuoqinyue, K3v123, Zuoxun, mingXta, hello-ikun, FangYuan33, GN-Yu, yuelinxin, longsizhuo, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, QiLOL, pengchzn, Guanngxu, L-Super, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, longranger2, cy-by-side, xiongsp, JeffersonHuang, Transmigration-zhou, magentaqin, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, Shyam-Chen, nanlei, hongyun-robot, Phoenix0415, MolDuM, Nigh, he-weilai, junminhong, mgisr, iron-irax, yd-j, XiaChuerwu, XC-Zero, seven1240, SamJin98, wodray, reeswell, NI-SW, Horbin-Magician, Enlightenus, xjr7670, YangXuanyi, DullSword, boloboloda, iStig, qq909244296, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, liuxjerry, lucaswangdev, lyl625760, hts0000, gledfish, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, luluxia, xb534, bitsmi, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, steventimes, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, ZhongGuanbin, shanghai-Jerry, JackYang-hellobobo, Javesun99, lipusheng, BlindTerran, ShiMaRing, FreddieLi, FloranceYeh, iFleey, fanchenggang, gltianwen, goerll, Dr-XYZ, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, fanenr, eagleanurag, LifeGoesOnionOnionOnion, 52coder, foursevenlove, KorsChen, hezhizhen, linzeyan, ZJKung, GaochaoZhu, hopkings2008, yang-le, Evilrabbit520, Turing-1024-Lee, thomasq0, Suremotoo, Allen-Scai, Risuntsy, Richard-Zhang1019, qingpeng9802, primexiao, nidhoggfgg, 1ch0, MwumLi, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai and 0130w.
この本のコードレビュー作業は、coderonion, Gonglja, gvenusleo, hpstory, justintse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyiアルファベット順によって完了されました。彼らの時間と努力に感謝し、様々な言語でのコードの標準化と統一性を確保してくださいました。
この本の繁体字中国語版は Shyam-Chen と Dr-XYZ によってレビューされ、英語版は yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0, and magentaqin によってレビューされました。彼らの継続的な貢献により、この本がより広い読者に届き、役立つことができます。
この本の制作過程において、多くの方々から貴重な支援をいただきました。これらに限定されませんが:
- 会社でのメンター、李熙博士に感謝します。ある会話で「早く行動しろ」と励ましていただき、この本を書く決意を固めることができました。
- ガールフレンドのBubbleに感謝します。この本の最初の読者として、アルゴリズム初心者の視点から多くの貴重な提案をいただき、この本を初心者により適したものにしてくださいました。
- Tengbao、Qibao、Feibaoに感謝します。この本のクリエイティブな名前を考えてくださり、みんなが初めて「Hello World!」を書いた時の素晴らしい思い出を呼び起こしてくれました。
- Xiaoquanに感謝します。知的財産に関する専門的な支援を提供してくださり、このオープンソース本の開発において重要な役割を果たしてくださいました。
- Sutongに感謝します。この本の美しいカバーとロゴをデザインしてくださり、私の要求で何度も修正を辛抱強く行ってくださいました。
- @squidfunk に感謝します。執筆と組版の提案、および彼が開発したオープンソースドキュメントテーマ [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) を提供してくださいました。
執筆の過程で、データ構造とアルゴリズムに関する多数の教科書や記事を深く研究しました。これらの作品は模範的なモデルとして機能し、この本の内容の正確性と品質を確保してくださいました。先人の方々の貴重な貢献に感謝いたします!
この本は、理論と実践を組み合わせた学習を提唱しており、この点で ["Dive into Deep Learning"](https://github.com/d2l-ai/d2l-en) からインスピレーションを受けています。この優れた本をすべての読者に強くお勧めします。
**継続的な支援と励ましにより、この興味深い仕事をすることを可能にしてくださった両親に心から感謝いたします**

View File

@@ -0,0 +1,20 @@
---
comments: true
icon: material/book-open-outline
---
# 第 0 章 &nbsp; 序文
![序文](../assets/covers/chapter_preface.jpg){ class="cover-image" }
!!! abstract
アルゴリズムは美しい交響曲のようで、コードの一行一行がリズムのように流れています。
この本があなたの心の中で静かに響き、独特で深い旋律を残すことを願っています。
## 章の内容
- [0.1 &nbsp; この本について](about_the_book.md)
- [0.2 &nbsp; 本書の使い方](suggestions.md)
- [0.3 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,255 @@
---
comments: true
---
# 0.2 &nbsp; 読み方
!!! tip
最良の読書体験のために、このセクションを通読することをお勧めします。
## 0.2.1 &nbsp; 記述規則
- タイトルの後に「*」が付いた章は任意であり、比較的難易度の高い内容が含まれています。時間に制約がある場合は、これらをスキップすることをお勧めします。
- 技術用語は太字印刷版およびPDF版または下線Web版で表示されます。例えば、<u>配列</u>などです。技術文書をより良く理解するために、これらに慣れることをお勧めします。
- **太字のテキスト**は重要な内容や要約文を示し、特別な注意を払う価値があります。
- 特定の意味を持つ単語や句は「引用符」で示され、曖昧さを避けます。
- プログラミング言語間で一致しない用語については、この本はPythonに従います。例えば、`null`を意味するために`None`を使用します。
- この本は、よりコンパクトなコンテンツレイアウトと引き換えに、プログラミング言語のコメント規約を部分的に無視しています。コメントは主に3つのタイプで構成されていますタイトルコメント、内容コメント、複数行コメント。
=== "Python"
```python title=""
"""関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント"""
# 詳細を説明するためのコメント
"""
複数行
コメント
"""
```
=== "C++"
```cpp title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "Java"
```java title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "C#"
```csharp title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "Go"
```go title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "Swift"
```swift title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "JS"
```javascript title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "TS"
```typescript title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "Dart"
```dart title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "Rust"
```rust title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "C"
```c title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "Kotlin"
```kotlin title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
```
=== "Zig"
```zig title=""
// 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント
// 詳細を説明するためのコメント
// 複数行
// コメント
```
## 0.2.2 &nbsp; アニメーション図解による効率的学習
テキストと比較して、動画や画像は情報密度が高く、より構造化されており、理解しやすくなっています。この本では、**重要で難しい概念は主にアニメーションと図解を通じて提示され**、テキストは説明と補足として機能します。
下図に示すようなアニメーションや図解のある内容に遭遇した場合、**図の理解を優先し、テキストを補足として**、両方を統合して包括的な理解を得てください。
![アニメーション図解の例](../index.assets/animation.gif){ class="animation-figure" }
<p align="center"> 図 0-2 &nbsp; アニメーション図解の例 </p>
## 0.2.3 &nbsp; コーディング実践による理解の深化
この本のソースコードは[GitHubリポジトリ](https://github.com/krahets/hello-algo)でホストされています。下図に示すように、**ソースコードにはテスト例が付属しており、ワンクリックで実行できます**。
時間に余裕がある場合は、**自分でコードをタイプすることをお勧めします**。時間がない場合は、少なくともすべてのコードを読んで実行してください。
コードを読むだけと比較して、コードを書くことは多くの場合、より多くの学習をもたらします。**実践による学習こそが真の学習方法です。**
![コード実行例](../index.assets/running_code.gif){ class="animation-figure" }
<p align="center"> 図 0-3 &nbsp; コード実行例 </p>
コードを実行するための設定には、主に3つのステップが含まれます。
**ステップ1ローカルプログラミング環境をインストール**。付録の[チュートリアル](https://www.hello-algo.com/chapter_appendix/installation/)に従ってインストールするか、すでにインストールされている場合はこのステップをスキップしてください。
**ステップ2コードリポジトリをクローンまたはダウンロード**。[GitHubリポジトリ](https://github.com/krahets/hello-algo)を訪問してください。
[Git](https://git-scm.com/downloads)がインストールされている場合は、次のコマンドを使用してリポジトリをクローンします:
```shell
git clone https://github.com/krahets/hello-algo.git
```
または、下図に示す場所にある「Download ZIP」ボタンをクリックして、コードを圧縮ZIPファイルとして直接ダウンロードすることもできます。その後、ローカルで展開するだけです。
![リポジトリのクローンとコードのダウンロード](suggestions.assets/download_code.png){ class="animation-figure" }
<p align="center"> 図 0-4 &nbsp; リポジトリのクローンとコードのダウンロード </p>
**ステップ3ソースコードを実行**。下図に示すように、上部にファイル名が記載されたコードブロックについては、リポジトリの`codes`フォルダで対応するソースコードファイルを見つけることができます。これらのファイルはワンクリックで実行でき、不要なデバッグ時間を節約し、学習に集中できます。
![コードブロックと対応するソースコードファイル](suggestions.assets/code_md_to_repo.png){ class="animation-figure" }
<p align="center"> 図 0-5 &nbsp; コードブロックと対応するソースコードファイル </p>
## 0.2.4 &nbsp; 議論による共同学習
この本を読んでいる間、学べなかった点を飛ばさないでください。**コメントセクションで気軽に質問してください**。喜んでお答えし、通常2日以内に回答できます。
下図に示すように、各章の下部にコメントセクションがあります。これらのコメントに注意を払うことをお勧めします。他の人が遭遇した問題を知ることで、知識のギャップを特定し、より深い思索を促すだけでなく、仲間の読者の質問に答えたり、洞察を共有したり、相互の向上を促進したりすることで寛大に貢献することも招待します。
![コメントセクションの例](../index.assets/comment.gif){ class="animation-figure" }
<p align="center"> 図 0-6 &nbsp; コメントセクションの例 </p>
## 0.2.5 &nbsp; アルゴリズム学習パス
全体的に、データ構造とアルゴリズムをマスターする旅は3つの段階に分けることができます
1. **段階1アルゴリズムの入門**。さまざまなデータ構造の特性と使用法に慣れ、異なるアルゴリズムの原理、プロセス、用途、効率について学ぶ必要があります。
2. **段階2アルゴリズム問題の練習**。[Sword for Offer](https://leetcode.cn/studyplan/coding-interviews/)や[LeetCode Hot 100](https://leetcode.cn/studyplan/top-100-liked/)などの人気のある問題から始めることをお勧めし、少なくとも100問を蓄積して主流のアルゴリズム問題に慣れることです。練習を始めると忘却が課題になる可能性がありますが、これは正常なことですのでご安心ください。「エビングハウスの忘却曲線」に従って問題を復習することができ、通常3〜5回の反復の後、それらを覚えることができるでしょう。
3. **段階3知識体系の構築**。学習の面では、アルゴリズムコラム記事、解法フレームワーク、アルゴリズム教科書を読んで知識体系を継続的に豊かにすることができます。練習の面では、トピック別分類、一つの問題に対する複数の解法、複数の問題に対する一つの解法など、高度な戦略を試すことができます。これらの戦略に関する洞察は、さまざまなコミュニティで見つけることができます。
下図に示すように、この本は主に「段階1」をカバーしており、段階2と3により効率的に取り組むのに役立つことを目的としています。
![アルゴリズム学習パス](suggestions.assets/learning_route.png){ class="animation-figure" }
<p align="center"> 図 0-7 &nbsp; アルゴリズム学習パス </p>

View File

@@ -0,0 +1,12 @@
---
comments: true
---
# 0.3 &nbsp; まとめ
- この本の主な読者はアルゴリズムの初心者です。すでに基本的な知識をお持ちの場合、この本はアルゴリズムの知識を体系的に復習するのに役立ち、この本のソースコードは「コーディングツールキット」としても使用できます。
- この本は3つの主要なセクション、計算量解析、データ構造、アルゴリズムで構成されており、この分野のほとんどのトピックをカバーしています。
- アルゴリズムの初心者にとって、多くの回り道や一般的な落とし穴を避けるために、初期段階で入門書を読むことが重要です。
- 本書内のアニメーションと図は通常、重要なポイントと難しい知識を紹介するために使用されます。本を読む際にはこれらにより多くの注意を払う必要があります。
- 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、自分でコードをタイプすることを強くお勧めします。
- この本のWeb版の各章には議論セクションがあり、いつでも質問や洞察を共有することを歓迎します。

View File

@@ -0,0 +1,25 @@
---
icon: material/bookshelf
---
# 参考文献
[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).
[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).
[3] Robert Sedgewick, et al. Algorithms (4th Edition).
[4] Yan Weimin. Data Structures (C Language Version).
[5] Deng Junhui. Data Structures (C++ Language Version, Third Edition).
[6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).
[7] Cheng Jie. Speaking of Data Structures.
[8] Wang Zheng. The Beauty of Data Structures and Algorithms.
[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).
[10] Aston Zhang, et al. Dive into Deep Learning.

View File

@@ -0,0 +1,344 @@
---
comments: true
---
# 10.1 &nbsp; 二分探索
<u>二分探索</u>は分割統治戦略を用いる効率的な探索アルゴリズムです。配列内の要素の整列順序を利用し、各反復で探索区間を半分に減らしながら、目標要素が見つかるか探索区間が空になるまで続行します。
!!! question
長さ$n$の配列`nums`が与えられ、要素は重複なしで昇順に配列されています。この配列内の要素`target`のインデックスを見つけて返してください。配列に要素が含まれていない場合は$-1$を返してください。例を下図に示します。
![Binary search example data](binary_search.assets/binary_search_example.png){ class="animation-figure" }
<p align="center"> 図 10-1 &nbsp; Binary search example data </p>
下図に示すように、まず$i = 0$と$j = n - 1$でポインタを初期化し、それぞれ配列の最初と最後の要素を指します。これらはまた全体の探索区間$[0, n - 1]$を表します。角括弧は閉区間を示し、境界値自身も含むことに注意してください。
そして、以下の2つのステップをループで実行する可能性があります。
1. 中点インデックス$m = \lfloor {(i + j) / 2} \rfloor$を計算します。ここで$\lfloor \: \rfloor$は床関数を表します。
2. `nums[m]``target`の比較に基づいて、以下の3つのケースのうち1つを選択して実行します。
1. `nums[m] < target`の場合、`target`は区間$[m + 1, j]$にあることを示すため、$i = m + 1$とします。
2. `nums[m] > target`の場合、`target`は区間$[i, m - 1]$にあることを示すため、$j = m - 1$とします。
3. `nums[m] = target`の場合、`target`が見つかったことを示すため、インデックス$m$を返します。
配列に目標要素が含まれていない場合、探索区間は最終的に空になり、$-1$を返して終了します。
=== "<1>"
![Binary search process](binary_search.assets/binary_search_step1.png){ class="animation-figure" }
=== "<2>"
![binary_search_step2](binary_search.assets/binary_search_step2.png){ class="animation-figure" }
=== "<3>"
![binary_search_step3](binary_search.assets/binary_search_step3.png){ class="animation-figure" }
=== "<4>"
![binary_search_step4](binary_search.assets/binary_search_step4.png){ class="animation-figure" }
=== "<5>"
![binary_search_step5](binary_search.assets/binary_search_step5.png){ class="animation-figure" }
=== "<6>"
![binary_search_step6](binary_search.assets/binary_search_step6.png){ class="animation-figure" }
=== "<7>"
![binary_search_step7](binary_search.assets/binary_search_step7.png){ class="animation-figure" }
<p align="center"> 図 10-2 &nbsp; Binary search process </p>
$i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲を超える可能性がある**ことは注目に値します。大きな数のオーバーフローを避けるため、通常は式$m = \lfloor {i + (j - i) / 2} \rfloor$を使用して中点を計算します。
コードは以下の通りです:
=== "Python"
```python title="binary_search.py"
def binary_search(nums: list[int], target: int) -> int:
"""二分探索(両端閉区間)"""
# 両端閉区間 [0, n-1] を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素を指す
i, j = 0, len(nums) - 1
# 検索区間が空になるまでループi > j のとき空)
while i <= j:
# 理論的には、Pythonの数値は無限に大きくなることができるメモリサイズに依存ため、大きな数のオーバーフローを考慮する必要はない
m = i + (j - i) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # この場合、target は区間 [m+1, j] にあることを示す
elif nums[m] > target:
j = m - 1 # この場合、target は区間 [i, m-1] にあることを示す
else:
return m # ターゲット要素が見つかったため、そのインデックスを返す
return -1 # ターゲット要素が見つからなかったため、-1 を返す
```
=== "C++"
```cpp title="binary_search.cpp"
/* 二分探索(両端閉区間) */
int binarySearch(vector<int> &nums, int target) {
// 両端閉区間[0, n-1]を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素を指す
int i = 0, j = nums.size() - 1;
// 探索区間が空になるまでループi > jの時空になる
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックスmを計算
if (nums[m] < target) // この状況はtargetが区間[m+1, j]にあることを示す
i = m + 1;
else if (nums[m] > target) // この状況はtargetが区間[i, m-1]にあることを示す
j = m - 1;
else // ターゲット要素が見つかったため、そのインデックスを返す
return m;
}
// ターゲット要素が見つからなかったため、-1を返す
return -1;
}
```
=== "Java"
```java title="binary_search.java"
/* 二分探索(両端閉区間) */
int binarySearch(int[] nums, int target) {
// 両端閉区間 [0, n-1] を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素を指す
int i = 0, j = nums.length - 1;
// 探索区間が空になるまでループi > j のとき空)
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) // この状況は target が区間 [m+1, j] にあることを示す
i = m + 1;
else if (nums[m] > target) // この状況は target が区間 [i, m-1] にあることを示す
j = m - 1;
else // 目標要素を見つけたので、そのインデックスを返す
return m;
}
// 目標要素を見つけられなかったので、-1 を返す
return -1;
}
```
=== "C#"
```csharp title="binary_search.cs"
[class]{binary_search}-[func]{BinarySearch}
```
=== "Go"
```go title="binary_search.go"
[class]{}-[func]{binarySearch}
```
=== "Swift"
```swift title="binary_search.swift"
[class]{}-[func]{binarySearch}
```
=== "JS"
```javascript title="binary_search.js"
[class]{}-[func]{binarySearch}
```
=== "TS"
```typescript title="binary_search.ts"
[class]{}-[func]{binarySearch}
```
=== "Dart"
```dart title="binary_search.dart"
[class]{}-[func]{binarySearch}
```
=== "Rust"
```rust title="binary_search.rs"
[class]{}-[func]{binary_search}
```
=== "C"
```c title="binary_search.c"
[class]{}-[func]{binarySearch}
```
=== "Kotlin"
```kotlin title="binary_search.kt"
[class]{}-[func]{binarySearch}
```
=== "Ruby"
```ruby title="binary_search.rb"
[class]{}-[func]{binary_search}
```
=== "Zig"
```zig title="binary_search.zig"
[class]{}-[func]{binarySearch}
```
**時間計算量は$O(\log n)$です**:二分ループにおいて、区間は各ラウンドで半分に減少するため、反復回数は$\log_2 n$となります。
**空間計算量は$O(1)$です**:ポインタ$i$と$j$は定数サイズの空間を占有します。
## 10.1.1 &nbsp; 区間表現方法
上記の閉区間の他に、もう一つの一般的な区間表現は「左閉右開」区間で、$[0, n)$として定義され、左境界は自身を含み、右境界は含みません。この表現では、$i = j$のとき区間$[i, j)$は空になります。
この表現に基づいて同じ機能を持つ二分探索アルゴリズムを実装できます:
=== "Python"
```python title="binary_search.py"
def binary_search_lcro(nums: list[int], target: int) -> int:
"""二分探索(左閉右開区間)"""
# 左閉右開区間 [0, n) を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素+1を指す
i, j = 0, len(nums)
# 検索区間が空になるまでループi = j のとき空)
while i < j:
m = i + (j - i) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # この場合、target は区間 [m+1, j) にあることを示す
elif nums[m] > target:
j = m # この場合、target は区間 [i, m) にあることを示す
else:
return m # ターゲット要素が見つかったため、そのインデックスを返す
return -1 # ターゲット要素が見つからなかったため、-1 を返す
```
=== "C++"
```cpp title="binary_search.cpp"
/* 二分探索(左閉右開区間) */
int binarySearchLCRO(vector<int> &nums, int target) {
// 左閉右開区間[0, n)を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素+1を指す
int i = 0, j = nums.size();
// 探索区間が空になるまでループi = jの時空になる
while (i < j) {
int m = i + (j - i) / 2; // 中点インデックスmを計算
if (nums[m] < target) // この状況はtargetが区間[m+1, j)にあることを示す
i = m + 1;
else if (nums[m] > target) // この状況はtargetが区間[i, m)にあることを示す
j = m;
else // ターゲット要素が見つかったため、そのインデックスを返す
return m;
}
// ターゲット要素が見つからなかったため、-1を返す
return -1;
}
```
=== "Java"
```java title="binary_search.java"
/* 二分探索(左閉右開区間) */
int binarySearchLCRO(int[] nums, int target) {
// 左閉右開区間 [0, n) を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素+1を指す
int i = 0, j = nums.length;
// 探索区間が空になるまでループi = j のとき空)
while (i < j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) // この状況は target が区間 [m+1, j) にあることを示す
i = m + 1;
else if (nums[m] > target) // この状況は target が区間 [i, m) にあることを示す
j = m;
else // 目標要素を見つけたので、そのインデックスを返す
return m;
}
// 目標要素を見つけられなかったので、-1 を返す
return -1;
}
```
=== "C#"
```csharp title="binary_search.cs"
[class]{binary_search}-[func]{BinarySearchLCRO}
```
=== "Go"
```go title="binary_search.go"
[class]{}-[func]{binarySearchLCRO}
```
=== "Swift"
```swift title="binary_search.swift"
[class]{}-[func]{binarySearchLCRO}
```
=== "JS"
```javascript title="binary_search.js"
[class]{}-[func]{binarySearchLCRO}
```
=== "TS"
```typescript title="binary_search.ts"
[class]{}-[func]{binarySearchLCRO}
```
=== "Dart"
```dart title="binary_search.dart"
[class]{}-[func]{binarySearchLCRO}
```
=== "Rust"
```rust title="binary_search.rs"
[class]{}-[func]{binary_search_lcro}
```
=== "C"
```c title="binary_search.c"
[class]{}-[func]{binarySearchLCRO}
```
=== "Kotlin"
```kotlin title="binary_search.kt"
[class]{}-[func]{binarySearchLCRO}
```
=== "Ruby"
```ruby title="binary_search.rb"
[class]{}-[func]{binary_search_lcro}
```
=== "Zig"
```zig title="binary_search.zig"
[class]{}-[func]{binarySearchLCRO}
```
下図に示すように、2つの区間表現タイプにおいて、二分探索アルゴリズムの初期化、ループ条件、区間縮小操作が異なります。
「閉区間」表現では両方の境界が包含的であるため、ポインタ$i$と$j$による区間縮小操作も対称的です。これによりエラーが発生しにくくなるため、**一般的に「閉区間」アプローチの使用が推奨されます**。
![Two types of interval definitions](binary_search.assets/binary_search_ranges.png){ class="animation-figure" }
<p align="center"> 図 10-3 &nbsp; Two types of interval definitions </p>
## 10.1.2 &nbsp; 利点と制限
二分探索は時間と空間の両方の面で良好な性能を示します。
- 二分探索は時間効率が良いです。大きなデータセットでは、対数時間計算量が大きな利点を提供します。例えば、サイズ$n = 2^{20}$のデータセットが与えられた場合、線形探索は$2^{20} = 1048576$回の反復が必要ですが、二分探索は$\log_2 2^{20} = 20$回のループのみで済みます。
- 二分探索には追加の空間が必要ありません。追加の空間に依存する探索アルゴリズム(ハッシュ探索など)と比較して、二分探索はより空間効率的です。
しかし、二分探索は以下の懸念により、すべてのシナリオに適しているとは限りません。
- 二分探索はソート済みデータにのみ適用できます。未ソートのデータは二分探索を適用する前にソートする必要があり、ソートアルゴリズムは通常$O(n \log n)$の時間計算量を持つため、これは価値がないかもしれません。このコストは線形探索よりも高く、二分探索自体は言うまでもありません。頻繁な挿入があるシナリオでは、配列を順序に保つコストは非常に高く、特定の位置に新しい要素を挿入する時間計算量は$O(n)$です。
- 二分探索は配列のみを使用できます。二分探索には非連続(ジャンプ)要素アクセスが必要で、これは連結リストでは非効率的です。そのため、連結リストや連結リストに基づくデータ構造はこのアルゴリズムに適していない可能性があります。
- 線形探索は小さなデータセットでより良い性能を示します。線形探索では各反復で1つの判定操作のみが必要ですが、二分探索では1つの加算、1つの除算、1つから3つの判定操作、1つの加算減算を含み、合計4つから6つの操作が必要です。そのため、データサイズ$n$が小さい場合、線形探索は二分探索よりも高速です。

View File

@@ -0,0 +1,286 @@
---
comments: true
---
# 10.3 &nbsp; 二分探索の境界
## 10.3.1 &nbsp; 左境界を見つける
!!! question
重複要素を含む可能性がある長さ$n$のソート済み配列`nums`が与えられ、最も左の要素`target`のインデックスを返してください。要素が配列に存在しない場合は、$-1$を返してください。
挿入位置の二分探索方法を思い出すと、探索完了後、インデックス$i$は`target`の最も左の出現を指します。したがって、**挿入位置の探索は本質的に最も左の`target`のインデックスを見つけることと同じです**。
挿入位置を見つける関数を使用して`target`の左境界を見つけることができます。配列に`target`が含まれていない可能性があることに注意してください。これは以下の2つの結果につながる可能性があります
- 挿入位置のインデックス$i$が範囲外です。
- 要素`nums[i]``target`と等しくありません。
これらの場合、単に$-1$を返します。コードは以下の通りです:
=== "Python"
```python title="binary_search_edge.py"
def binary_search_left_edge(nums: list[int], target: int) -> int:
"""最左端のターゲットの二分探索"""
# ターゲットの挿入位置を見つけることと同等
i = binary_search_insertion(nums, target)
# ターゲットが見つからなかった場合、-1 を返す
if i == len(nums) or nums[i] != target:
return -1
# ターゲットが見つかった場合、インデックス i を返す
return i
```
=== "C++"
```cpp title="binary_search_edge.cpp"
/* 最左のターゲットの二分探索 */
int binarySearchLeftEdge(vector<int> &nums, int target) {
// targetの挿入ポイントを見つけることと等価
int i = binarySearchInsertion(nums, target);
// targetが見つからなかったため、-1を返す
if (i == nums.size() || nums[i] != target) {
return -1;
}
// targetが見つかったため、インデックスiを返す
return i;
}
```
=== "Java"
```java title="binary_search_edge.java"
/* 最も左の target を二分探索 */
int binarySearchLeftEdge(int[] nums, int target) {
// target の挿入点を見つけることと等価
int i = binary_search_insertion.binarySearchInsertion(nums, target);
// target を見つけられなかったので、-1 を返す
if (i == nums.length || nums[i] != target) {
return -1;
}
// target を見つけたので、インデックス i を返す
return i;
}
```
=== "C#"
```csharp title="binary_search_edge.cs"
[class]{binary_search_edge}-[func]{BinarySearchLeftEdge}
```
=== "Go"
```go title="binary_search_edge.go"
[class]{}-[func]{binarySearchLeftEdge}
```
=== "Swift"
```swift title="binary_search_edge.swift"
[class]{}-[func]{binarySearchLeftEdge}
```
=== "JS"
```javascript title="binary_search_edge.js"
[class]{}-[func]{binarySearchLeftEdge}
```
=== "TS"
```typescript title="binary_search_edge.ts"
[class]{}-[func]{binarySearchLeftEdge}
```
=== "Dart"
```dart title="binary_search_edge.dart"
[class]{}-[func]{binarySearchLeftEdge}
```
=== "Rust"
```rust title="binary_search_edge.rs"
[class]{}-[func]{binary_search_left_edge}
```
=== "C"
```c title="binary_search_edge.c"
[class]{}-[func]{binarySearchLeftEdge}
```
=== "Kotlin"
```kotlin title="binary_search_edge.kt"
[class]{}-[func]{binarySearchLeftEdge}
```
=== "Ruby"
```ruby title="binary_search_edge.rb"
[class]{}-[func]{binary_search_left_edge}
```
=== "Zig"
```zig title="binary_search_edge.zig"
[class]{}-[func]{binarySearchLeftEdge}
```
## 10.3.2 &nbsp; 右境界を見つける
`target`の最も右の出現をどのように見つけるでしょうか?最も直接的な方法は、`nums[m] == target`の場合に探索境界を調整する方法を変更して、従来の二分探索ロジックを修正することです。コードはここでは省略されています。興味がある場合は、自分でコードを実装してみてください。
以下では、さらに2つの巧妙な方法を紹介します。
### 1. &nbsp; 左境界探索を再利用する
`target`の最も右の出現を見つけるには、最も左の`target`を見つけるために使用された関数を再利用できます。具体的には、最も右のターゲットの探索を最も左のターゲット + 1の探索に変換します。
下図に示すように、探索完了後、ポインタ$i$は最も左の`target + 1`(存在する場合)を指し、ポインタ$j$は`target`の最も右の出現を指します。したがって、$j$を返すことで右境界が得られます。
![Transforming the search for the right boundary into the search for the left boundary](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" }
<p align="center"> 図 10-7 &nbsp; Transforming the search for the right boundary into the search for the left boundary </p>
返される挿入位置は$i$であることに注意してください。したがって、$j$を得るためには1を引く必要があります
=== "Python"
```python title="binary_search_edge.py"
def binary_search_right_edge(nums: list[int], target: int) -> int:
"""最右端のターゲットの二分探索"""
# 最左端のターゲット + 1 を見つけることに変換
i = binary_search_insertion(nums, target + 1)
# j は最右端のターゲットを指し、i はターゲットより大きい最初の要素を指す
j = i - 1
# ターゲットが見つからなかった場合、-1 を返す
if j == -1 or nums[j] != target:
return -1
# ターゲットが見つかった場合、インデックス j を返す
return j
```
=== "C++"
```cpp title="binary_search_edge.cpp"
/* 最右のターゲットの二分探索 */
int binarySearchRightEdge(vector<int> &nums, int target) {
// 最左のtarget + 1を見つけることに変換
int i = binarySearchInsertion(nums, target + 1);
// jは最右のターゲットを指し、iはtargetより大きい最初の要素を指す
int j = i - 1;
// targetが見つからなかったため、-1を返す
if (j == -1 || nums[j] != target) {
return -1;
}
// targetが見つかったため、インデックスjを返す
return j;
}
```
=== "Java"
```java title="binary_search_edge.java"
/* 最も右の target を二分探索 */
int binarySearchRightEdge(int[] nums, int target) {
// 最も左の target + 1 を見つけることに変換
int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);
// j は最も右の target を指し、i は target より大きい最初の要素を指す
int j = i - 1;
// target を見つけられなかったので、-1 を返す
if (j == -1 || nums[j] != target) {
return -1;
}
// target を見つけたので、インデックス j を返す
return j;
}
```
=== "C#"
```csharp title="binary_search_edge.cs"
[class]{binary_search_edge}-[func]{BinarySearchRightEdge}
```
=== "Go"
```go title="binary_search_edge.go"
[class]{}-[func]{binarySearchRightEdge}
```
=== "Swift"
```swift title="binary_search_edge.swift"
[class]{}-[func]{binarySearchRightEdge}
```
=== "JS"
```javascript title="binary_search_edge.js"
[class]{}-[func]{binarySearchRightEdge}
```
=== "TS"
```typescript title="binary_search_edge.ts"
[class]{}-[func]{binarySearchRightEdge}
```
=== "Dart"
```dart title="binary_search_edge.dart"
[class]{}-[func]{binarySearchRightEdge}
```
=== "Rust"
```rust title="binary_search_edge.rs"
[class]{}-[func]{binary_search_right_edge}
```
=== "C"
```c title="binary_search_edge.c"
[class]{}-[func]{binarySearchRightEdge}
```
=== "Kotlin"
```kotlin title="binary_search_edge.kt"
[class]{}-[func]{binarySearchRightEdge}
```
=== "Ruby"
```ruby title="binary_search_edge.rb"
[class]{}-[func]{binary_search_right_edge}
```
=== "Zig"
```zig title="binary_search_edge.zig"
[class]{}-[func]{binarySearchRightEdge}
```
### 2. &nbsp; 要素探索に変換する
配列に`target`が含まれていない場合、$i$と$j$は最終的に`target`より大きい最初の要素と小さい最初の要素をそれぞれ指します。
したがって、下図に示すように、配列に存在しない要素を構築して、左と右の境界を探索できます。
- 最も左の`target`を見つけるには:`target - 0.5`を探索することに変換でき、ポインタ$i$を返します。
- 最も右の`target`を見つけるには:`target + 0.5`を探索することに変換でき、ポインタ$j$を返します。
![Transforming the search for boundaries into the search for an element](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" }
<p align="center"> 図 10-8 &nbsp; Transforming the search for boundaries into the search for an element </p>
コードはここでは省略されていますが、このアプローチについて注意すべき2つの重要な点があります。
- 与えられた配列`nums`には小数が含まれていないため、等しい場合の処理は心配ありません。
- ただし、このアプローチで小数を導入するには、`target`変数を浮動小数点型に変更する必要がありますPythonでは変更は不要です

View File

@@ -0,0 +1,345 @@
---
comments: true
---
# 10.2 &nbsp; 二分探索による挿入
二分探索は目標要素を探索するだけでなく、目標要素の挿入位置を探索するなど、多くの変種問題を解決するためにも使用されます。
## 10.2.1 &nbsp; 重複要素がない場合
!!! question
一意の要素を持つ長さ$n$のソート済み配列`nums`と要素`target`が与えられ、ソート順を維持しながら`target``nums`に挿入します。`target`が配列にすでに存在する場合は、既存の要素の左側に挿入します。挿入後の配列における`target`のインデックスを返してください。下図に示す例を参照してください。
![Example data for binary search insertion point](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" }
<p align="center"> 図 10-4 &nbsp; Example data for binary search insertion point </p>
前のセクションの二分探索コードを再利用したい場合、以下の2つの質問に答える必要があります。
**質問1**:配列にすでに`target`が含まれている場合、挿入位置は既存要素のインデックスになりますか?
`target`を等しい要素の左側に挿入するという要件は、新しく挿入される`target`が元の`target`の位置を置き換えることを意味します。つまり、**配列に`target`が含まれている場合、挿入位置は確かにその`target`のインデックスです**。
**質問2**:配列に`target`が含まれていない場合、どのインデックスに挿入されますか?
二分探索プロセスをさらに考えてみましょう:`nums[m] < target`のとき、ポインタ$i$が移動します。これは、ポインタ$i$が`target`以上の要素に近づいていることを意味します。同様に、ポインタ$j$は常に`target`以下の要素に近づいています。
したがって、二分の終了時には確実に:$i$は`target`より大きい最初の要素を指し、$j$は`target`より小さい最初の要素を指します。**配列に`target`が含まれていない場合、挿入位置は$i$であることは明らかです**。コードは以下の通りです:
=== "Python"
```python title="binary_search_insertion.py"
def binary_search_insertion_simple(nums: list[int], target: int) -> int:
"""挿入位置の二分探索(重複要素なし)"""
i, j = 0, len(nums) - 1 # 両端閉区間 [0, n-1] を初期化
while i <= j:
m = i + (j - i) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # ターゲットは区間 [m+1, j] にある
elif nums[m] > target:
j = m - 1 # ターゲットは区間 [i, m-1] にある
else:
return m # ターゲットが見つかった場合、挿入位置 m を返す
# ターゲットが見つからなかった場合、挿入位置 i を返す
return i
```
=== "C++"
```cpp title="binary_search_insertion.cpp"
/* 挿入ポイントの二分探索(重複要素なし) */
int binarySearchInsertionSimple(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 両端閉区間[0, n-1]を初期化
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックスmを計算
if (nums[m] < target) {
i = m + 1; // ターゲットは区間[m+1, j]にある
} else if (nums[m] > target) {
j = m - 1; // ターゲットは区間[i, m-1]にある
} else {
return m; // ターゲットが見つかったため、挿入ポイントmを返す
}
}
// ターゲットが見つからなかったため、挿入ポイントiを返す
return i;
}
```
=== "Java"
```java title="binary_search_insertion.java"
/* 挿入点の二分探索(重複要素なし) */
int binarySearchInsertionSimple(int[] nums, int target) {
int i = 0, j = nums.length - 1; // 両端閉区間 [0, n-1] を初期化
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) {
i = m + 1; // target は区間 [m+1, j] にある
} else if (nums[m] > target) {
j = m - 1; // target は区間 [i, m-1] にある
} else {
return m; // target を見つけたので、挿入点 m を返す
}
}
// target を見つけられなかったので、挿入点 i を返す
return i;
}
```
=== "C#"
```csharp title="binary_search_insertion.cs"
[class]{binary_search_insertion}-[func]{BinarySearchInsertionSimple}
```
=== "Go"
```go title="binary_search_insertion.go"
[class]{}-[func]{binarySearchInsertionSimple}
```
=== "Swift"
```swift title="binary_search_insertion.swift"
[class]{}-[func]{binarySearchInsertionSimple}
```
=== "JS"
```javascript title="binary_search_insertion.js"
[class]{}-[func]{binarySearchInsertionSimple}
```
=== "TS"
```typescript title="binary_search_insertion.ts"
[class]{}-[func]{binarySearchInsertionSimple}
```
=== "Dart"
```dart title="binary_search_insertion.dart"
[class]{}-[func]{binarySearchInsertionSimple}
```
=== "Rust"
```rust title="binary_search_insertion.rs"
[class]{}-[func]{binary_search_insertion_simple}
```
=== "C"
```c title="binary_search_insertion.c"
[class]{}-[func]{binarySearchInsertionSimple}
```
=== "Kotlin"
```kotlin title="binary_search_insertion.kt"
[class]{}-[func]{binarySearchInsertionSimple}
```
=== "Ruby"
```ruby title="binary_search_insertion.rb"
[class]{}-[func]{binary_search_insertion_simple}
```
=== "Zig"
```zig title="binary_search_insertion.zig"
[class]{}-[func]{binarySearchInsertionSimple}
```
## 10.2.2 &nbsp; 重複要素がある場合
!!! question
前の質問に基づいて、配列に重複要素が含まれている可能性があると仮定し、他はすべて同じとします。
配列に`target`の複数の出現がある場合、通常の二分探索は`target`の1つの出現のインデックスのみを返すことができ、**その位置の左右に`target`の出現がいくつあるかを特定することはできません**。
問題では目標要素を最も左の位置に挿入することが要求されているため、**配列内の最も左の`target`のインデックスを見つける必要があります**。最初に下図に示すステップを通してこれを実装することを考えてみましょう。
1. 二分探索を実行して`target`の任意のインデックス、例えば$k$を見つけます。
2. インデックス$k$から開始して、最も左の`target`の出現が見つかるまで左に線形探索を行い、このインデックスを返します。
![Linear search for the insertion point of duplicate elements](binary_search_insertion.assets/binary_search_insertion_naive.png){ class="animation-figure" }
<p align="center"> 図 10-5 &nbsp; Linear search for the insertion point of duplicate elements </p>
この方法は実現可能ですが、線形探索を含むため、時間計算量は$O(n)$です。この方法は、配列に多くの重複する`target`が含まれている場合に非効率です。
今度は二分探索コードを拡張することを考えてみましょう。下図に示すように、全体的なプロセスは同じままです。各ラウンドで、まず中間インデックス$m$を計算し、次に`target`と`nums[m]`の値を比較して、以下のケースになります。
- `nums[m] < target`または`nums[m] > target`のとき、これは`target`がまだ見つかっていないことを意味するため、通常の二分探索を使用して探索範囲を狭め、**ポインタ$i$と$j$を`target`に近づけます**。
- `nums[m] == target`のとき、これは`target`より小さい要素が範囲$[i, m - 1]$にあることを示すため、$j = m - 1$を使用して範囲を狭め、**ポインタ$j$を`target`より小さい要素に近づけます**。
ループ後、$i$は最も左の`target`を指し、$j$は`target`より小さい最初の要素を指すため、**インデックス$i$が挿入位置です**。
=== "<1>"
![Steps for binary search insertion point of duplicate elements](binary_search_insertion.assets/binary_search_insertion_step1.png){ class="animation-figure" }
=== "<2>"
![binary_search_insertion_step2](binary_search_insertion.assets/binary_search_insertion_step2.png){ class="animation-figure" }
=== "<3>"
![binary_search_insertion_step3](binary_search_insertion.assets/binary_search_insertion_step3.png){ class="animation-figure" }
=== "<4>"
![binary_search_insertion_step4](binary_search_insertion.assets/binary_search_insertion_step4.png){ class="animation-figure" }
=== "<5>"
![binary_search_insertion_step5](binary_search_insertion.assets/binary_search_insertion_step5.png){ class="animation-figure" }
=== "<6>"
![binary_search_insertion_step6](binary_search_insertion.assets/binary_search_insertion_step6.png){ class="animation-figure" }
=== "<7>"
![binary_search_insertion_step7](binary_search_insertion.assets/binary_search_insertion_step7.png){ class="animation-figure" }
=== "<8>"
![binary_search_insertion_step8](binary_search_insertion.assets/binary_search_insertion_step8.png){ class="animation-figure" }
<p align="center"> 図 10-6 &nbsp; Steps for binary search insertion point of duplicate elements </p>
以下のコードを観察してください。分岐`nums[m] > target`と`nums[m] == target`の操作は同じであるため、これら2つの分岐をマージできます。
それでも、ロジックがより明確になり、可読性が向上するため、条件を展開したままにしておくことができます。
=== "Python"
```python title="binary_search_insertion.py"
def binary_search_insertion(nums: list[int], target: int) -> int:
"""挿入位置の二分探索(重複要素あり)"""
i, j = 0, len(nums) - 1 # 両端閉区間 [0, n-1] を初期化
while i <= j:
m = i + (j - i) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # ターゲットは区間 [m+1, j] にある
elif nums[m] > target:
j = m - 1 # ターゲットは区間 [i, m-1] にある
else:
j = m - 1 # ターゲット未満の最初の要素は区間 [i, m-1] にある
# 挿入位置 i を返す
return i
```
=== "C++"
```cpp title="binary_search_insertion.cpp"
/* 挿入ポイントの二分探索(重複要素あり) */
int binarySearchInsertion(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 両端閉区間[0, n-1]を初期化
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックスmを計算
if (nums[m] < target) {
i = m + 1; // ターゲットは区間[m+1, j]にある
} else if (nums[m] > target) {
j = m - 1; // ターゲットは区間[i, m-1]にある
} else {
j = m - 1; // ターゲット未満の最初の要素は区間[i, m-1]にある
}
}
// 挿入ポイントiを返す
return i;
}
```
=== "Java"
```java title="binary_search_insertion.java"
/* 挿入点の二分探索(重複要素あり) */
int binarySearchInsertion(int[] nums, int target) {
int i = 0, j = nums.length - 1; // 両端閉区間 [0, n-1] を初期化
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) {
i = m + 1; // target は区間 [m+1, j] にある
} else if (nums[m] > target) {
j = m - 1; // target は区間 [i, m-1] にある
} else {
j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある
}
}
// 挿入点 i を返す
return i;
}
```
=== "C#"
```csharp title="binary_search_insertion.cs"
[class]{binary_search_insertion}-[func]{BinarySearchInsertion}
```
=== "Go"
```go title="binary_search_insertion.go"
[class]{}-[func]{binarySearchInsertion}
```
=== "Swift"
```swift title="binary_search_insertion.swift"
[class]{}-[func]{binarySearchInsertion}
```
=== "JS"
```javascript title="binary_search_insertion.js"
[class]{}-[func]{binarySearchInsertion}
```
=== "TS"
```typescript title="binary_search_insertion.ts"
[class]{}-[func]{binarySearchInsertion}
```
=== "Dart"
```dart title="binary_search_insertion.dart"
[class]{}-[func]{binarySearchInsertion}
```
=== "Rust"
```rust title="binary_search_insertion.rs"
[class]{}-[func]{binary_search_insertion}
```
=== "C"
```c title="binary_search_insertion.c"
[class]{}-[func]{binarySearchInsertion}
```
=== "Kotlin"
```kotlin title="binary_search_insertion.kt"
[class]{}-[func]{binarySearchInsertion}
```
=== "Ruby"
```ruby title="binary_search_insertion.rb"
[class]{}-[func]{binary_search_insertion}
```
=== "Zig"
```zig title="binary_search_insertion.zig"
[class]{}-[func]{binarySearchInsertion}
```
!!! tip
このセクションのコードは「閉区間」を使用しています。「左閉右開」に興味がある場合は、自分でコードを実装してみてください。
要約すると、二分探索は本質的にポインタ$i$と$j$の探索目標を設定することです。これらの目標は特定の要素(`target`など)または要素の範囲(`target`より小さいものなど)である可能性があります。
二分探索の連続ループにおいて、ポインタ$i$と$j$は段階的に事前定義された目標に近づきます。最終的に、それらは答えを見つけるか、境界を越えた後に停止します。

View File

@@ -0,0 +1,23 @@
---
comments: true
icon: material/text-search
---
# 第 10 章 &nbsp; 探索
![Searching](../assets/covers/chapter_searching.jpg){ class="cover-image" }
!!! abstract
探索は未知への冒険です。神秘的な空間の隅々まで巡る必要があるかもしれませんし、あるいはすぐに目標を見つけることができるかもしれません。
この発見の旅において、それぞれの探査は予期しない答えで終わるかもしれません。
## 章の内容
- [10.1 &nbsp; 二分探索](binary_search.md)
- [10.2 &nbsp; 二分探索挿入点](binary_search_insertion.md)
- [10.3 &nbsp; 二分探索の境界](binary_search_edge.md)
- [10.4 &nbsp; ハッシュ最適化戦略](replace_linear_by_hashing.md)
- [10.5 &nbsp; 探索アルゴリズム再考](searching_algorithm_revisited.md)
- [10.6 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,281 @@
---
comments: true
---
# 10.4 &nbsp; ハッシュ最適化戦略
アルゴリズム問題において、**線形探索をハッシュベースの探索に置き換えることで、アルゴリズムの時間計算量を削減することがよくあります**。アルゴリズム問題を使用して理解を深めましょう。
!!! question
整数配列`nums`と目標要素`target`が与えられ、配列内で「和」が`target`に等しい2つの要素を探索し、それらの配列インデックスを返してください。任意の解が受け入れられます。
## 10.4.1 &nbsp; 線形探索:時間を空間と交換
すべての可能な組み合わせを直接横断することを考えてみます。下図に示すように、ネストしたループを開始し、各反復で2つの整数の和が`target`に等しいかどうかを判断します。そうであれば、それらのインデックスを返します。
![Linear search solution for two-sum problem](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" }
<p align="center"> 図 10-9 &nbsp; Linear search solution for two-sum problem </p>
コードは以下の通りです:
=== "Python"
```python title="two_sum.py"
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:
"""方法一:ブルートフォース列挙"""
# 二重ループ、時間計算量は O(n^2)
for i in range(len(nums) - 1):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
```
=== "C++"
```cpp title="two_sum.cpp"
/* 方法一:ブルートフォース列挙 */
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
int size = nums.size();
// 二重ループ、時間計算量はO(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return {i, j};
}
}
return {};
}
```
=== "Java"
```java title="two_sum.java"
/* 方法一: 暴力列挙 */
int[] twoSumBruteForce(int[] nums, int target) {
int size = nums.length;
// 二重ループ、時間計算量は O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return new int[] { i, j };
}
}
return new int[0];
}
```
=== "C#"
```csharp title="two_sum.cs"
[class]{two_sum}-[func]{TwoSumBruteForce}
```
=== "Go"
```go title="two_sum.go"
[class]{}-[func]{twoSumBruteForce}
```
=== "Swift"
```swift title="two_sum.swift"
[class]{}-[func]{twoSumBruteForce}
```
=== "JS"
```javascript title="two_sum.js"
[class]{}-[func]{twoSumBruteForce}
```
=== "TS"
```typescript title="two_sum.ts"
[class]{}-[func]{twoSumBruteForce}
```
=== "Dart"
```dart title="two_sum.dart"
[class]{}-[func]{twoSumBruteForce}
```
=== "Rust"
```rust title="two_sum.rs"
[class]{}-[func]{two_sum_brute_force}
```
=== "C"
```c title="two_sum.c"
[class]{}-[func]{twoSumBruteForce}
```
=== "Kotlin"
```kotlin title="two_sum.kt"
[class]{}-[func]{twoSumBruteForce}
```
=== "Ruby"
```ruby title="two_sum.rb"
[class]{}-[func]{two_sum_brute_force}
```
=== "Zig"
```zig title="two_sum.zig"
[class]{}-[func]{twoSumBruteForce}
```
この方法の時間計算量は$O(n^2)$、空間計算量は$O(1)$で、大容量データでは非常に時間がかかる可能性があります。
## 10.4.2 &nbsp; ハッシュ探索:空間を時間と交換
ハッシュテーブルの使用を考えてみましょう。キーと値のペアはそれぞれ配列要素とそのインデックスです。配列をループし、各反復中に下図に示すステップを実行します。
1. 数値`target - nums[i]`がハッシュテーブルにあるかどうかを確認します。ある場合は、これら2つの要素のインデックスを直接返します。
2. キーと値のペア`nums[i]`とインデックス`i`をハッシュテーブルに追加します。
=== "<1>"
![Help hash table solve two-sum](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png){ class="animation-figure" }
=== "<2>"
![two_sum_hashtable_step2](replace_linear_by_hashing.assets/two_sum_hashtable_step2.png){ class="animation-figure" }
=== "<3>"
![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png){ class="animation-figure" }
<p align="center"> 図 10-10 &nbsp; Help hash table solve two-sum </p>
実装コードは以下に示され、単一のループのみが必要です:
=== "Python"
```python title="two_sum.py"
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:
"""方法二:補助ハッシュテーブル"""
# 補助ハッシュテーブル、空間計算量は O(n)
dic = {}
# 単一ループ、時間計算量は O(n)
for i in range(len(nums)):
if target - nums[i] in dic:
return [dic[target - nums[i]], i]
dic[nums[i]] = i
return []
```
=== "C++"
```cpp title="two_sum.cpp"
/* 方法二:補助ハッシュテーブル */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
int size = nums.size();
// 補助ハッシュテーブル、空間計算量はO(n)
unordered_map<int, int> dic;
// 単層ループ、時間計算量はO(n)
for (int i = 0; i < size; i++) {
if (dic.find(target - nums[i]) != dic.end()) {
return {dic[target - nums[i]], i};
}
dic.emplace(nums[i], i);
}
return {};
}
```
=== "Java"
```java title="two_sum.java"
/* 方法二: 補助ハッシュテーブル */
int[] twoSumHashTable(int[] nums, int target) {
int size = nums.length;
// 補助ハッシュテーブル、空間計算量は O(n)
Map<Integer, Integer> dic = new HashMap<>();
// 単一層ループ、時間計算量は O(n)
for (int i = 0; i < size; i++) {
if (dic.containsKey(target - nums[i])) {
return new int[] { dic.get(target - nums[i]), i };
}
dic.put(nums[i], i);
}
return new int[0];
}
```
=== "C#"
```csharp title="two_sum.cs"
[class]{two_sum}-[func]{TwoSumHashTable}
```
=== "Go"
```go title="two_sum.go"
[class]{}-[func]{twoSumHashTable}
```
=== "Swift"
```swift title="two_sum.swift"
[class]{}-[func]{twoSumHashTable}
```
=== "JS"
```javascript title="two_sum.js"
[class]{}-[func]{twoSumHashTable}
```
=== "TS"
```typescript title="two_sum.ts"
[class]{}-[func]{twoSumHashTable}
```
=== "Dart"
```dart title="two_sum.dart"
[class]{}-[func]{twoSumHashTable}
```
=== "Rust"
```rust title="two_sum.rs"
[class]{}-[func]{two_sum_hash_table}
```
=== "C"
```c title="two_sum.c"
[class]{HashTable}-[func]{}
[class]{}-[func]{twoSumHashTable}
```
=== "Kotlin"
```kotlin title="two_sum.kt"
[class]{}-[func]{twoSumHashTable}
```
=== "Ruby"
```ruby title="two_sum.rb"
[class]{}-[func]{two_sum_hash_table}
```
=== "Zig"
```zig title="two_sum.zig"
[class]{}-[func]{twoSumHashTable}
```
この方法は、ハッシュ探索を使用することで時間計算量を$O(n^2)$から$O(n)$に削減し、実行時効率を大幅に向上させます。
追加のハッシュテーブルを維持する必要があるため、空間計算量は$O(n)$です。**それにもかかわらず、この方法は全体的により均衡のとれた時空間効率を持ち、この問題の最適解となります**。

View File

@@ -0,0 +1,94 @@
---
comments: true
---
# 10.5 &nbsp; 探索アルゴリズムの再検討
<u>探索アルゴリズム(検索アルゴリズム)</u>は、配列、連結リスト、木、グラフなどのデータ構造内で特定の基準を満たす1つ以上の要素を取得するために使用されます。
探索アルゴリズムは、そのアプローチに基づいて以下の2つのカテゴリに分けることができます。
- **データ構造を横断することで目標要素を特定する**:配列、連結リスト、木、グラフの横断など。
- **データの組織構造や既存のデータを使用して効率的な要素探索を実現する**:二分探索、ハッシュ探索、二分探索木探索など。
これらのトピックは前の章で紹介されたため、私たちには馴染みのないものではありません。このセクションでは、より体系的な観点から探索アルゴリズムを再検討します。
## 10.5.1 &nbsp; 総当たり探索
総当たり探索は、データ構造のすべての要素を横断することで目標要素を特定します。
- 「線形探索」は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から開始し、目標要素が見つかるか、目標要素を見つけることなく他端に到達するまで、各要素に一つずつアクセスします。
- 「幅優先探索」と「深さ優先探索」は、グラフと木の2つの横断戦略です。幅優先探索は初期ードから開始し、層ごと左から右へに探索し、近くから遠くのードにアクセスします。深さ優先探索は初期ードから開始し、パスの終端上から下へまで追跡し、その後バックトラックして他のパスを試し、データ構造全体が横断されるまで続行します。
総当たり探索の利点は、その単純さと汎用性であり、**データの前処理や追加のデータ構造の助けが不要**です。
ただし、**このタイプのアルゴリズムの時間計算量は$O(n)$**で、$n$は要素数であるため、大規模なデータセットでは性能が悪くなります。
## 10.5.2 &nbsp; 適応的探索
適応的探索は、データの固有の性質(順序など)を使用して探索プロセスを最適化し、それにより目標要素をより効率的に特定します。
- 「二分探索」はデータの整列性を使用して効率的な探索を実現し、配列にのみ適用可能です。
- 「ハッシュ探索」はハッシュテーブルを使用して探索データと目標データの間にキーと値のマッピングを確立し、それによりクエリ操作を実装します。
- 特定の木構造(二分探索木など)での「木探索」は、ノード値の比較に基づいてノードを迅速に除外し、それにより目標要素を特定します。
これらのアルゴリズムの利点は高効率であり、**時間計算量が$O(\log n)$または$O(1)$にまで達します**。
ただし、**これらのアルゴリズムを使用するには、多くの場合データの前処理が必要です**。例えば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索の両方で追加のデータ構造の助けが必要です。これらの構造を維持することも、時間と空間の面でより多くのオーバーヘッドが必要です。
!!! tip
適応的探索アルゴリズムは、多くの場合探索アルゴリズムと呼ばれ、**主に特定のデータ構造内で目標要素を迅速に取得するために使用されます**。
## 10.5.3 &nbsp; 探索方法の選択
サイズ$n$のデータセットが与えられた場合、線形探索、二分探索、木探索、ハッシュ探索、またはその他の方法を使用して目標要素を取得できます。これらの方法の動作原理を下図に示します。
![Various search strategies](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" }
<p align="center"> 図 10-11 &nbsp; Various search strategies </p>
前述の方法の特性と操作効率を以下の表に示します。
<p align="center"> 表 10-1 &nbsp; 探索アルゴリズム効率の比較 </p>
<div class="center-table" markdown>
| | 線形探索 | 二分探索 | 木探索 | ハッシュ探索 |
| ------------------ | ------------- | --------------------- | --------------------------- | -------------------------- |
| 要素探索 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ |
| 要素挿入 | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
| 要素削除 | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
| 追加空間 | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ |
| データ前処理 | / | ソート $O(n \log n)$ | 木構築 $O(n \log n)$ | ハッシュテーブル構築 $O(n)$ |
| データ順序性 | 無順序 | 順序 | 順序 | 無順序 |
</div>
探索アルゴリズムの選択は、データ量、探索性能要件、データクエリと更新の頻度などにも依存します。
**線形探索**
- 汎用性が良く、データ前処理操作が不要です。データを一度だけクエリする必要がある場合、他の3つの方法のデータ前処理時間は線形探索の時間よりも長くなります。
- 小容量のデータに適しており、時間計算量が効率に与える影響は小さいです。
- データ更新が非常に頻繁なシナリオに適しています。この方法はデータの追加メンテナンスを必要としないためです。
**二分探索**
- より大きなデータ量に適しており、安定した性能と最悪ケースの時間計算量$O(\log n)$を持ちます。
- ただし、データ量が大きすぎることはできません。配列の保存には連続したメモリ空間が必要だからです。
- 頻繁な追加と削除があるシナリオには適していません。順序付き配列の維持に多くのオーバーヘッドが発生するためです。
**ハッシュ探索**
- 高速クエリ性能が不可欠なシナリオに適しており、平均時間計算量は$O(1)$です。
- 順序付きデータや範囲探索が必要なシナリオには適していません。ハッシュテーブルはデータの順序性を維持できないためです。
- ハッシュ関数とハッシュ衝突処理戦略への依存度が高く、性能劣化のリスクが大きいです。
- 過度に大容量のデータには適していません。ハッシュテーブルは衝突を最小化し、良好なクエリ性能を提供するために追加の空間が必要だからです。
**木探索**
- 大容量データに適しています。木ノードはメモリ内に分散して保存されるためです。
- 順序付きデータの維持や範囲探索に適しています。
- ノードの継続的な追加と削除により、二分探索木は偏る可能性があり、時間計算量が$O(n)$に劣化する可能性があります。
- AVL木や赤黒木を使用する場合、操作は$O(\log n)$効率で安定して実行できますが、木のバランスを維持する操作により追加のオーバーヘッドが追加されます。

View File

@@ -0,0 +1,12 @@
---
comments: true
---
# 10.6 &nbsp; まとめ
- 二分探索はデータの順序に依存し、探索区間を反復的に半分にすることで探索を実行します。入力データがソート済みである必要があり、配列または配列ベースのデータ構造にのみ適用可能です。
- 無順序データセット内のエントリを見つけるには、総当たり探索が必要な場合があります。データ構造に基づいて異なる探索アルゴリズムを適用できます線形探索は配列と連結リストに適しており、幅優先探索BFSと深さ優先探索DFSはグラフと木に適しています。これらのアルゴリズムは非常に汎用性が高く、データの前処理が不要ですが、$O(n)$という高い時間計算量を持ちます。
- ハッシュ探索、木探索、二分探索は効率的な探索方法で、特定のデータ構造内で目標要素を迅速に特定できます。これらのアルゴリズムは非常に効率的で、時間計算量が$O(\log n)$または$O(1)$にまで達しますが、通常は追加のデータ構造を収容するために追加の空間が必要です。
- 実際には、データ量、探索性能要件、データクエリと更新頻度などの要因を分析して、適切な探索方法を選択する必要があります。
- 線形探索は小さなデータや頻繁に更新される(変動性の高い)データに理想的です。二分探索は大きくてソート済みのデータに適しています。ハッシュ探索は高いクエリ効率が必要で範囲クエリが不要なデータに適しています。木探索は順序を維持し、範囲クエリをサポートする必要がある大きな動的データに最も適しています。
- 線形探索をハッシュ探索に置き換えることは、実行時性能を最適化する一般的な戦略で、時間計算量を$O(n)$から$O(1)$に削減します。

View File

@@ -0,0 +1,311 @@
---
comments: true
---
# 11.3 &nbsp; バブルソート
<u>バブルソート</u>は、隣接する要素を継続的に比較し交換することで動作します。このプロセスは泡が底から上に上昇するようなものなので、「バブルソート」と名付けられました。
下図に示すように、バブリングプロセスは要素交換を使用してシミュレートできます:配列の左端から開始して右に移動し、隣接する要素の各ペアを比較します。左の要素が右の要素より大きい場合は、それらを交換します。横断後、最大要素は配列の右端にバブルアップします。
=== "<1>"
![Simulating bubble process using element swap](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
=== "<2>"
![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" }
=== "<3>"
![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png){ class="animation-figure" }
=== "<4>"
![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png){ class="animation-figure" }
=== "<5>"
![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png){ class="animation-figure" }
=== "<6>"
![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png){ class="animation-figure" }
=== "<7>"
![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" }
<p align="center"> 図 11-4 &nbsp; Simulating bubble process using element swap </p>
## 11.3.1 &nbsp; アルゴリズムプロセス
配列の長さを$n$とします。バブルソートのステップは下図に示されます:
1. まず、$n$個の要素に対して1回の「バブル」パスを実行し、**最大要素を正しい位置に交換します**。
2. 次に、残りの$n - 1$個の要素に対して「バブル」パスを実行し、**2番目に大きい要素を正しい位置に交換します**。
3. この方法で続行します;$n - 1$回のパスの後、**最大$n - 1$個の要素が正しい位置に移動されます**。
4. 残りの唯一の要素は**必ず**最小であるため、**さらなる**ソートは必要ありません。この時点で、配列はソートされます。
![Bubble sort process](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-5 &nbsp; Bubble sort process </p>
コード例は以下の通りです:
=== "Python"
```python title="bubble_sort.py"
def bubble_sort(nums: list[int]):
"""バブルソート"""
n = len(nums)
# 外側のループ:未ソート範囲は [0, i]
for i in range(n - 1, 0, -1):
# 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動
for j in range(i):
if nums[j] > nums[j + 1]:
# nums[j] と nums[j + 1] を交換
nums[j], nums[j + 1] = nums[j + 1], nums[j]
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* バブルソート */
void bubbleSort(vector<int> &nums) {
// 外側ループ:未ソート範囲は[0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// 内側ループ:未ソート範囲[0, i]内の最大要素を範囲の右端に交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j]とnums[j + 1]を交換
// ここではstdのswapを使用
swap(nums[j], nums[j + 1]);
}
}
}
}
```
=== "Java"
```java title="bubble_sort.java"
/* バブルソート */
void bubbleSort(int[] nums) {
// 外側ループ: 未ソート範囲は [0, i]
for (int i = nums.length - 1; i > 0; i--) {
// 内側ループ: 未ソート範囲 [0, i] の最大要素を範囲の右端に交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSort}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSort}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSort}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSort}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSort}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSort}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSort}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSort}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort}
```
=== "Zig"
```zig title="bubble_sort.zig"
[class]{}-[func]{bubbleSort}
```
## 11.3.2 &nbsp; 効率の最適化
「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。
この最適化があっても、バブルソートの最悪時間計算量と平均時間計算量は$O(n^2)$のままです。ただし、入力配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$まで低くなる可能性があります。
=== "Python"
```python title="bubble_sort.py"
def bubble_sort_with_flag(nums: list[int]):
"""バブルソート(フラグによる最適化)"""
n = len(nums)
# 外側のループ:未ソート範囲は [0, i]
for i in range(n - 1, 0, -1):
flag = False # フラグを初期化
# 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動
for j in range(i):
if nums[j] > nums[j + 1]:
# nums[j] と nums[j + 1] を交換
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True # 要素を交換したことを記録
if not flag:
break # この回の「バブリング」で要素が交換されなかった場合、終了
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* バブルソート(フラグ最適化版)*/
void bubbleSortWithFlag(vector<int> &nums) {
// 外側ループ:未ソート範囲は[0, i]
for (int i = nums.size() - 1; i > 0; i--) {
bool flag = false; // フラグを初期化
// 内側ループ:未ソート範囲[0, i]内の最大要素を範囲の右端に交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j]とnums[j + 1]を交換
// ここではstdのswapを使用
swap(nums[j], nums[j + 1]);
flag = true; // 交換された要素を記録
}
}
if (!flag)
break; // この回の「バブリング」で要素が交換されなかった場合、終了
}
}
```
=== "Java"
```java title="bubble_sort.java"
/* バブルソート(フラグによる最適化) */
void bubbleSortWithFlag(int[] nums) {
// 外側ループ: 未ソート範囲は [0, i]
for (int i = nums.length - 1; i > 0; i--) {
boolean flag = false; // フラグを初期化
// 内側ループ: 未ソート範囲 [0, i] の最大要素を範囲の右端に交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 交換された要素を記録
}
}
if (!flag)
break; // この「バブリング」ラウンドで要素が交換されなかった場合、終了
}
}
```
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSortWithFlag}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort_with_flag}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort_with_flag}
```
=== "Zig"
```zig title="bubble_sort.zig"
[class]{}-[func]{bubbleSortWithFlag}
```
## 11.3.3 &nbsp; アルゴリズムの特性
- **$O(n^2)$の時間計算量、適応ソート。** 各「バブリング」ラウンドは長さ$n - 1$、$n - 2$、$\dots$、$2$、$1$の配列セグメントを横断し、合計は$(n - 1) n / 2$となります。`flag`最適化により、配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$に達する可能性があります。
- **$O(1)$の空間計算量、インプレースソート。** ポインタ$i$と$j$によって定数量の追加空間のみが使用されます。
- **安定ソート。** 等しい要素は「バブリング」中に交換されないため、元の順序が保持され、これは安定ソートになります。

View File

@@ -0,0 +1,206 @@
---
comments: true
---
# 11.8 &nbsp; バケットソート
前述のソートアルゴリズムはすべて「比較ベースのソートアルゴリズム」で、値を比較することで要素をソートします。このようなソートアルゴリズムは $O(n \log n)$ より良い時間計算量を持つことはできません。次に、線形時間計算量を達成できるいくつかの「非比較ソートアルゴリズム」について議論します。
<u>バケットソート</u>は分割統治戦略の典型的な応用です。一連の順序付けられたバケットを設定し、各バケットがデータの範囲を含み、入力データをこれらのバケットに均等に分散させることで動作します。そして、各バケット内のデータを個別にソートします。最後に、すべてのバケットからのソート済みデータを順次マージして最終結果を生成します。
## 11.8.1 &nbsp; アルゴリズムの過程
長さ $n$ の配列で、$[0, 1)$ の範囲の浮動小数点数を考えてみます。バケットソートの過程は以下の図に示されています。
1. $k$ 個のバケットを初期化し、$n$ 個の要素をこれらの $k$ 個のバケットに分散させます。
2. 各バケットを個別にソートします(プログラミング言語の組み込みソート関数を使用)。
3. 最小から最大のバケットの順序で結果をマージします。
![バケットソートアルゴリズムの過程](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-13 &nbsp; バケットソートアルゴリズムの過程 </p>
コードは以下の通りです:
=== "Python"
```python title="bucket_sort.py"
def bucket_sort(nums: list[float]):
"""バケットソート"""
# k = n/2 個のバケットを初期化、各バケットに平均2個の要素を配置することを期待
k = len(nums) // 2
buckets = [[] for _ in range(k)]
# 1. 配列要素を各バケットに分散
for num in nums:
# 入力データ範囲は [0, 1)、num * k を使用してインデックス範囲 [0, k-1] にマッピング
i = int(num * k)
# num をバケット i に追加
buckets[i].append(num)
# 2. 各バケットをソート
for bucket in buckets:
# 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
bucket.sort()
# 3. バケットを走査して結果をマージ
i = 0
for bucket in buckets:
for num in bucket:
nums[i] = num
i += 1
```
=== "C++"
```cpp title="bucket_sort.cpp"
/* バケットソート */
void bucketSort(vector<float> &nums) {
// k = n/2個のバケットを初期化、各バケットに2つの要素を割り当てることを期待
int k = nums.size() / 2;
vector<vector<float>> buckets(k);
// 1. 配列要素を各バケットに分配
for (float num : nums) {
// 入力データ範囲は[0, 1)、num * kを使用してインデックス範囲[0, k-1]にマップ
int i = num * k;
// bucket_idxバケットに数値を追加
buckets[i].push_back(num);
}
// 2. 各バケットをソート
for (vector<float> &bucket : buckets) {
// 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
sort(bucket.begin(), bucket.end());
}
// 3. バケットを走査して結果をマージ
int i = 0;
for (vector<float> &bucket : buckets) {
for (float num : bucket) {
nums[i++] = num;
}
}
}
```
=== "Java"
```java title="bucket_sort.java"
/* バケットソート */
void bucketSort(float[] nums) {
// k = n/2 個のバケットを初期化、各バケットに期待される要素数は 2 個
int k = nums.length / 2;
List<List<Float>> buckets = new ArrayList<>();
for (int i = 0; i < k; i++) {
buckets.add(new ArrayList<>());
}
// 1. 配列要素を各バケットに分散
for (float num : nums) {
// 入力データ範囲は [0, 1)、num * k を使ってインデックス範囲 [0, k-1] にマッピング
int i = (int) (num * k);
// num をバケット i に追加
buckets.get(i).add(num);
}
// 2. 各バケットをソート
for (List<Float> bucket : buckets) {
// 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
Collections.sort(bucket);
}
// 3. バケットを走査して結果をマージ
int i = 0;
for (List<Float> bucket : buckets) {
for (float num : bucket) {
nums[i++] = num;
}
}
}
```
=== "C#"
```csharp title="bucket_sort.cs"
[class]{bucket_sort}-[func]{BucketSort}
```
=== "Go"
```go title="bucket_sort.go"
[class]{}-[func]{bucketSort}
```
=== "Swift"
```swift title="bucket_sort.swift"
[class]{}-[func]{bucketSort}
```
=== "JS"
```javascript title="bucket_sort.js"
[class]{}-[func]{bucketSort}
```
=== "TS"
```typescript title="bucket_sort.ts"
[class]{}-[func]{bucketSort}
```
=== "Dart"
```dart title="bucket_sort.dart"
[class]{}-[func]{bucketSort}
```
=== "Rust"
```rust title="bucket_sort.rs"
[class]{}-[func]{bucket_sort}
```
=== "C"
```c title="bucket_sort.c"
[class]{}-[func]{bucketSort}
```
=== "Kotlin"
```kotlin title="bucket_sort.kt"
[class]{}-[func]{bucketSort}
```
=== "Ruby"
```ruby title="bucket_sort.rb"
[class]{}-[func]{bucket_sort}
```
=== "Zig"
```zig title="bucket_sort.zig"
[class]{}-[func]{bucketSort}
```
## 11.8.2 &nbsp; アルゴリズムの特徴
バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに100万個の要素が含まれ、システムメモリの制限によりすべてのデータを同時にロードできない場合、データを1,000個のバケットに分割し、各バケットを個別にソートしてから結果をマージできます。
- **時間計算量は $O(n + k)$**:要素がバケット間で均等に分散されていると仮定すると、各バケット内の要素数は $n/k$ です。単一のバケットのソートに $O(n/k \log(n/k))$ 時間がかかると仮定すると、すべてのバケットのソートに $O(n \log(n/k))$ 時間がかかります。**バケット数 $k$ が比較的大きいとき、時間計算量は $O(n)$ に近づきます**。結果のマージには、すべてのバケットと要素を走査する必要があり、$O(n + k)$ 時間がかかります。最悪の場合、すべてのデータが単一のバケットに分散され、そのバケットのソートには $O(n^2)$ 時間がかかります。
- **空間計算量は $O(n + k)$、非インプレースソート**$k$ 個のバケットと合計 $n$ 個の要素のための追加スペースが必要です。
- バケットソートが安定かどうかは、各バケット内で使用されるソートアルゴリズムが安定かどうかに依存します。
## 11.8.3 &nbsp; 均等分散を達成する方法
バケットソートの理論的時間計算量は $O(n)$ に達することができます。**重要なことは、すべてのバケットに要素を均等に分散させることです**。実世界のデータはしばしば均一に分散されていないからです。例えば、eBayのすべての商品を価格範囲で10個のバケットに均等に分散させたいとします。しかし、商品価格の分散は均等でない可能性があり、100ドル未満の商品が多く、500ドル以上の商品が少ないかもしれません。価格範囲を均等に10分割すると、各バケットの商品数の差が大きくなります。
均等分散を達成するために、最初におおよその境界を設定して、データを3つのバケットに大まかに分割できます。**分散が完了した後、より多くのアイテムを持つバケットをさらに3つのバケットに分割し、すべてのバケットの要素数がほぼ等しくなるまで続けます**。
以下の図に示すように、この方法は本質的に再帰木を構築し、葉ードの要素数ができるだけ均等になることを目指します。もちろん、各ラウンドでデータを3つのバケットに分割する必要はありません - 分割戦略はデータの独特な特性に適応的に調整できます。
![バケットの再帰的分割](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }
<p align="center"> 図 11-14 &nbsp; バケットの再帰的分割 </p>
商品価格の確率分布を事前に知っている場合、**データの確率分布に基づいて各バケットの価格境界を設定できます**。データ分布を具体的に計算する必要は必ずしもなく、代わりに確率モデルを使用してデータ特性に基づいて近似できることに注意してください。
以下の図に示すように、商品価格が正規分布に従うと仮定すると、バケット間でアイテムの分散のバランスを取るために合理的な価格区間を定義できます。
![確率分布に基づくバケット分割](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" }
<p align="center"> 図 11-15 &nbsp; 確率分布に基づくバケット分割 </p>

View File

@@ -0,0 +1,397 @@
---
comments: true
---
# 11.9 &nbsp; 計数ソート
<u>計数ソート</u>は要素の数をカウントすることでソートを実現し、通常は整数配列に適用されます。
## 11.9.1 &nbsp; 簡単な実装
簡単な例から始めましょう。長さ $n$ の配列 `nums` が与えられ、すべての要素が「非負整数」である場合、計数ソートの全体的な過程は以下の図に示されています。
1. 配列を走査して最大数を見つけ、それを $m$ とし、長さ $m + 1$ の補助配列 `counter` を作成します。
2. **`counter` を使用して `nums` 内の各数の出現回数をカウントします**。ここで `counter[num]` は数 `num` の出現回数に対応します。カウント方法は簡単で、`nums` を走査し(現在の数を `num` とする)、各ラウンドで `counter[num]` を $1$ 増やします。
3. **`counter` のインデックスは自然に順序付けられているため、すべての数は本質的にすでにソートされています**。次に、`counter` を走査し、出現順に `nums` を昇順で埋めます。
![計数ソートの過程](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-16 &nbsp; 計数ソートの過程 </p>
コードは以下の通りです:
=== "Python"
```python title="counting_sort.py"
def counting_sort_naive(nums: list[int]):
"""計数ソート"""
# シンプルな実装、オブジェクトのソートには使用できない
# 1. 配列内の最大要素 m を統計
m = 0
for num in nums:
m = max(m, num)
# 2. 各数字の出現回数を統計
# counter[num] は num の出現回数を表す
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. counter を走査し、各要素を元の配列 nums に埋め戻し
i = 0
for num in range(m + 1):
for _ in range(counter[num]):
nums[i] = num
i += 1
```
=== "C++"
```cpp title="counting_sort.cpp"
/* カウントソート */
// 簡単な実装、オブジェクトのソートには使用できない
void countingSortNaive(vector<int> &nums) {
// 1. 配列の最大要素mを統計
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num]はnumの出現回数を表す
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. counterを走査し、各要素を元の配列numsに戻す
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Java"
```java title="counting_sort.java"
/* 計数ソート */
// 簡単な実装、オブジェクトのソートには使用できない
void countingSortNaive(int[] nums) {
// 1. 配列の最大要素 m を統計
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. counter を走査し、各要素を元の配列 nums に戻す
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSortNaive}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSortNaive}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSortNaive}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSortNaive}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSortNaive}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSortNaive}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort_naive}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSortNaive}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSortNaive}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort_naive}
```
=== "Zig"
```zig title="counting_sort.zig"
[class]{}-[func]{countingSortNaive}
```
!!! note "計数ソートとバケットソートの関係"
バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。
## 11.9.2 &nbsp; 完全な実装
注意深い読者は気付くかもしれませんが、**入力データがオブジェクトの場合、上記の手順 `3.` は無効です**。入力データが商品オブジェクトで、価格(クラスメンバ変数)で商品をソートしたいとします。しかし、上記のアルゴリズムは結果としてソート済みの価格のみを提供できます。
では、元のデータのソート結果をどのように取得できるでしょうか?まず、`counter` の「前置和」を計算します。名前が示すように、インデックス `i` での前置和 `prefix[i]` は、配列の最初の `i` 個の要素の和に等しいです:
$$
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
$$
**前置和には明確な意味があります。`prefix[num] - 1` は結果配列 `res` における要素 `num` の最後の出現のインデックスを表します**。この情報は重要で、各要素が結果配列のどこに現れるべきかを教えてくれます。次に、元の配列 `nums` の各要素 `num` を逆順で走査し、各反復で以下の2つの手順を実行します。
1. インデックス `prefix[num] - 1` で配列 `res` に `num` を埋めます。
2. 前置和 `prefix[num]` を $1$ 減らして、`num` を配置する次のインデックスを取得します。
走査後、配列 `res` にはソートされた結果が含まれ、最後に `res` が元の配列 `nums` を置き換えます。完全な計数ソートの過程は以下の図に示されています。
=== "<1>"
![計数ソートの過程](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
=== "<2>"
![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" }
=== "<3>"
![counting_sort_step3](counting_sort.assets/counting_sort_step3.png){ class="animation-figure" }
=== "<4>"
![counting_sort_step4](counting_sort.assets/counting_sort_step4.png){ class="animation-figure" }
=== "<5>"
![counting_sort_step5](counting_sort.assets/counting_sort_step5.png){ class="animation-figure" }
=== "<6>"
![counting_sort_step6](counting_sort.assets/counting_sort_step6.png){ class="animation-figure" }
=== "<7>"
![counting_sort_step7](counting_sort.assets/counting_sort_step7.png){ class="animation-figure" }
=== "<8>"
![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" }
<p align="center"> 図 11-17 &nbsp; 計数ソートの過程 </p>
計数ソートの実装コードは以下の通りです:
=== "Python"
```python title="counting_sort.py"
def counting_sort(nums: list[int]):
"""計数ソート"""
# 完全な実装、オブジェクトのソートが可能で、安定ソート
# 1. 配列内の最大要素 m を統計
m = max(nums)
# 2. 各数字の出現回数を統計
# counter[num] は num の出現回数を表す
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. counter の前置和を計算し、「出現回数」を「末尾インデックス」に変換
# counter[num]-1 は res において num が最後に出現するインデックス
for i in range(m):
counter[i + 1] += counter[i]
# 4. nums を逆順に走査し、各要素を結果配列 res に配置
# 結果を記録するための配列 res を初期化
n = len(nums)
res = [0] * n
for i in range(n - 1, -1, -1):
num = nums[i]
res[counter[num] - 1] = num # num を対応するインデックスに配置
counter[num] -= 1 # 前置和を1減らし、num を配置する次のインデックスを取得
# 結果配列 res を使用して元の配列 nums を上書き
for i in range(n):
nums[i] = res[i]
```
=== "C++"
```cpp title="counting_sort.cpp"
/* カウントソート */
// 完全な実装、オブジェクトのソートが可能で安定ソート
void countingSort(vector<int> &nums) {
// 1. 配列の最大要素mを統計
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num]はnumの出現回数を表す
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. counterの前缀和を計算し、「出現回数」を「末尾インデックス」に変換
// counter[num]-1はnumがresで現れる最後のインデックス
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. numsを逆順で走査し、各要素を結果配列resに配置
// 結果を記録する配列resを初期化
int n = nums.size();
vector<int> res(n);
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // numを対応するインデックスに配置
counter[num]--; // 前缀和を1減らし、numを配置する次のインデックスを取得
}
// 結果配列resで元の配列numsを上書き
nums = res;
}
```
=== "Java"
```java title="counting_sort.java"
/* 計数ソート */
// 完全な実装、オブジェクトをソートでき、安定ソート
void countingSort(int[] nums) {
// 1. 配列の最大要素 m を統計
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. counter の累積和を計算し、「出現回数」を「尻尾インデックス」に変換
// counter[num]-1 は res 内で num が出現する最後のインデックス
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. nums を逆順に走査し、各要素を結果配列 res に配置
// 結果を記録する配列 res を初期化
int n = nums.length;
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // num を対応するインデックスに配置
counter[num]--; // 累積和を 1 減算し、num を配置する次のインデックスを取得
}
// 結果配列 res を使って元の配列 nums を上書き
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSort}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSort}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSort}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSort}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSort}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSort}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSort}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSort}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort}
```
=== "Zig"
```zig title="counting_sort.zig"
[class]{}-[func]{countingSort}
```
## 11.9.3 &nbsp; アルゴリズムの特徴
- **時間計算量は $O(n + m)$、非適応ソート**`nums` と `counter` の走査が含まれ、どちらも線形時間を使用します。一般的に、$n \gg m$ であり、時間計算量は $O(n)$ に近づきます。
- **空間計算量は $O(n + m)$、非インプレースソート**:長さ $n$ の配列 `res` と長さ $m$ の配列 `counter` をそれぞれ使用します。
- **安定ソート**:要素が「右から左」の順序で `res` に埋められるため、`nums` の走査を逆順にすることで、等しい要素間の相対位置の変化を防ぎ、安定したソートを実現できます。実際、`nums` を順番に走査しても正しいソート結果を生成できますが、結果は不安定です。
## 11.9.4 &nbsp; 制限事項
今までに、計数ソートは非常に巧妙だと感じるかもしれません。単に量をカウントするだけで効率的なソートを実現できるからです。しかし、計数ソートを使用するための前提条件は比較的厳しいです。
**計数ソートは非負整数にのみ適用できます**。他のタイプのデータに適用したい場合、これらのデータが要素の元の順序を変更することなく非負整数に変換できることを保証する必要があります。例えば、負の整数を含む配列の場合、最初にすべての数に定数を加えて、すべてを正の数に変換し、ソート完了後に元に戻すことができます。
**計数ソートは値の範囲が小さい大きなデータセットに適しています**。例えば、上記の例では、$m$ は大きすぎるべきではありません。そうでなければ、あまりにも多くのスペースを占有してしまいます。そして $n \ll m$ の場合、計数ソートは $O(m)$ 時間を使用し、$O(n \log n)$ ソートアルゴリズムより遅い可能性があります。

View File

@@ -0,0 +1,283 @@
---
comments: true
---
# 11.7 &nbsp; ヒープソート
!!! tip
この節を読む前に、「ヒープ」の章を必ず完了させてください。
<u>ヒープソート</u>は、ヒープデータ構造に基づく効率的なソートアルゴリズムです。すでに学習した「ヒープの構築」と「要素の抽出」操作を使用してヒープソートを実装できます。
1. 配列を入力し、最小ヒープを構築します。ここで、最小要素がヒープの頂上に位置します。
2. 継続的に抽出操作を実行し、抽出された要素を順次記録して、最小から最大までのソート済みリストを取得します。
上記の方法は実現可能ですが、ポップされた要素を格納するための追加の配列が必要で、やや空間を消費します。実際には、通常、より優雅な実装を使用します。
## 11.7.1 &nbsp; アルゴリズムの流れ
配列の長さを $n$ とすると、ヒープソートの過程は以下の通りです。
1. 配列を入力し、最大ヒープを構築します。この手順の後、最大要素がヒープの頂上に位置します。
2. ヒープの頂上要素(最初の要素)とヒープの底部要素(最後の要素)を交換します。この交換の後、ヒープの長さを $1$ 減らし、ソート済み要素の数を $1$ 増やします。
3. ヒープの頂上から開始して、上から下へのsift-down操作を実行します。sift-downの後、ヒープの性質が復元されます。
4. 手順 `2.``3.` を繰り返します。$n - 1$ ラウンドループして、配列のソートを完了します。
!!! tip
実際、要素抽出操作も手順 `2.``3.` を含み、抽出された要素をヒープから削除する追加の手順があります。
=== "<1>"
![ヒープソートの過程](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" }
=== "<2>"
![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" }
=== "<3>"
![heap_sort_step3](heap_sort.assets/heap_sort_step3.png){ class="animation-figure" }
=== "<4>"
![heap_sort_step4](heap_sort.assets/heap_sort_step4.png){ class="animation-figure" }
=== "<5>"
![heap_sort_step5](heap_sort.assets/heap_sort_step5.png){ class="animation-figure" }
=== "<6>"
![heap_sort_step6](heap_sort.assets/heap_sort_step6.png){ class="animation-figure" }
=== "<7>"
![heap_sort_step7](heap_sort.assets/heap_sort_step7.png){ class="animation-figure" }
=== "<8>"
![heap_sort_step8](heap_sort.assets/heap_sort_step8.png){ class="animation-figure" }
=== "<9>"
![heap_sort_step9](heap_sort.assets/heap_sort_step9.png){ class="animation-figure" }
=== "<10>"
![heap_sort_step10](heap_sort.assets/heap_sort_step10.png){ class="animation-figure" }
=== "<11>"
![heap_sort_step11](heap_sort.assets/heap_sort_step11.png){ class="animation-figure" }
=== "<12>"
![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" }
<p align="center"> 図 11-12 &nbsp; ヒープソートの過程 </p>
コードの実装では、「ヒープ」の章からのsift-down関数 `sift_down()` を使用しました。最大要素が抽出されるにつれてヒープの長さが減少するため、`sift_down()` 関数に長さパラメータ $n$ を追加して、ヒープの現在の有効長を指定する必要があることに注意することが重要です。コードは以下の通りです:
=== "Python"
```python title="heap_sort.py"
def sift_down(nums: list[int], n: int, i: int):
"""ヒープの長さが n、ード i から上から下へヒープ化を開始"""
while True:
# i、l、r の中で最大のードを判定し、ma とする
l = 2 * i + 1
r = 2 * i + 2
ma = i
if l < n and nums[l] > nums[ma]:
ma = l
if r < n and nums[r] > nums[ma]:
ma = r
# ノード i が最大または l、r のインデックスが範囲外の場合、さらなるヒープ化は不要、ループを抜ける
if ma == i:
break
# 2つのードを交換
nums[i], nums[ma] = nums[ma], nums[i]
# 下向きにヒープ化をループ
i = ma
def heap_sort(nums: list[int]):
"""ヒープソート"""
# ヒープ構築操作:葉ノード以外のすべてのノードをヒープ化
for i in range(len(nums) // 2 - 1, -1, -1):
sift_down(nums, len(nums), i)
# ヒープから最大要素を抽出し、n-1 回繰り返す
for i in range(len(nums) - 1, 0, -1):
# ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換)
nums[0], nums[i] = nums[i], nums[0]
# ルートノードから上から下へヒープ化を開始
sift_down(nums, i, 0)
```
=== "C++"
```cpp title="heap_sort.cpp"
/* ヒープの長さはn、ードiから上から下へヒープ化を開始 */
void siftDown(vector<int> &nums, int n, int i) {
while (true) {
// i、l、r の中で最大のードを決定し、maとして記録
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// ードiが最大か、インデックスl、rが境界外の場合、それ以上のヒープ化は不要で終了
if (ma == i) {
break;
}
// 二つのノードを交換
swap(nums[i], nums[ma]);
// 下向きにヒープ化をループ
i = ma;
}
}
/* ヒープソート */
void heapSort(vector<int> &nums) {
// ヒープ構築操作:葉以外のすべてのノードをヒープ化
for (int i = nums.size() / 2 - 1; i >= 0; --i) {
siftDown(nums, nums.size(), i);
}
// ヒープから最大要素を抽出し、n-1回繰り返す
for (int i = nums.size() - 1; i > 0; --i) {
// ルートノードを最右葉ノードと交換(最初の要素を最後の要素と交換)
swap(nums[0], nums[i]);
// ルートノードから上から下へヒープ化を開始
siftDown(nums, i, 0);
}
}
```
=== "Java"
```java title="heap_sort.java"
/* ヒープの長さは n、ード i から上から下へヒープ化開始 */
void siftDown(int[] nums, int n, int i) {
while (true) {
// i, l, r の中で最大のードを判定し、ma とする
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// ノード i が最大、またはインデックス l, r が範囲外の場合、さらなるヒープ化は不要、ブレーク
if (ma == i)
break;
// 2つのードを交換
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// 下向きにヒープ化をループ
i = ma;
}
}
/* ヒープソート */
void heapSort(int[] nums) {
// ヒープ構築操作: 葉ノード以外のすべてのノードをヒープ化
for (int i = nums.length / 2 - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// ヒープから最大要素を抽出し、n-1 回繰り返し
for (int i = nums.length - 1; i > 0; i--) {
// ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換)
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// ルートノードから上から下へヒープ化開始
siftDown(nums, i, 0);
}
}
```
=== "C#"
```csharp title="heap_sort.cs"
[class]{heap_sort}-[func]{SiftDown}
[class]{heap_sort}-[func]{HeapSort}
```
=== "Go"
```go title="heap_sort.go"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Swift"
```swift title="heap_sort.swift"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "JS"
```javascript title="heap_sort.js"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "TS"
```typescript title="heap_sort.ts"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Dart"
```dart title="heap_sort.dart"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Rust"
```rust title="heap_sort.rs"
[class]{}-[func]{sift_down}
[class]{}-[func]{heap_sort}
```
=== "C"
```c title="heap_sort.c"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Kotlin"
```kotlin title="heap_sort.kt"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Ruby"
```ruby title="heap_sort.rb"
[class]{}-[func]{sift_down}
[class]{}-[func]{heap_sort}
```
=== "Zig"
```zig title="heap_sort.zig"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
## 11.7.2 &nbsp; アルゴリズムの特徴
- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープの構築は $O(n)$ 時間を使用します。ヒープから最大要素を抽出するには $O(\log n)$ 時間がかかり、$n - 1$ ラウンドループします。
- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が $O(1)$ 空間を使用します。要素の交換とヒープ化操作は元の配列で実行されます。
- **非安定ソート**:ヒープの頂上と底部要素の交換中に、等しい要素の相対位置が変わる可能性があります。

View File

@@ -0,0 +1,28 @@
---
comments: true
icon: material/sort-ascending
---
# 第 11 章 &nbsp; ソート
![Sorting](../assets/covers/chapter_sorting.jpg){ class="cover-image" }
!!! abstract
ソートは混沌を秩序に変える魔法の鍵のようなもので、データをより効率的に理解し処理することを可能にします。
単純な昇順であろうと複雑なカテゴリ配列であろうと、ソートはデータの調和美を明らかにします。
## 章の内容
- [11.1 &nbsp; ソートアルゴリズム](sorting_algorithm.md)
- [11.2 &nbsp; 選択ソート](selection_sort.md)
- [11.3 &nbsp; バブルソート](bubble_sort.md)
- [11.4 &nbsp; 挿入ソート](insertion_sort.md)
- [11.5 &nbsp; クイックソート](quick_sort.md)
- [11.6 &nbsp; マージソート](merge_sort.md)
- [11.7 &nbsp; ヒープソート](heap_sort.md)
- [11.8 &nbsp; バケットソート](bucket_sort.md)
- [11.9 &nbsp; 計数ソート](counting_sort.md)
- [11.10 &nbsp; 基数ソート](radix_sort.md)
- [11.11 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,168 @@
---
comments: true
---
# 11.4 &nbsp; 挿入ソート
<u>挿入ソート</u>は、トランプのデッキを手動でソートするプロセスによく似た動作をするシンプルなソートアルゴリズムです。
具体的には、未ソート区間からベース要素を選択し、その左側のソート済み区間の要素と比較して、要素を正しい位置に挿入します。
下図は、要素が配列に挿入される方法を示しています。ベース要素を`base`とすると、ターゲットインデックスから`base`までのすべての要素を右に1つずつシフトし、その後`base`をターゲットインデックスに割り当てる必要があります。
![Single insertion operation](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }
<p align="center"> 図 11-6 &nbsp; Single insertion operation </p>
## 11.4.1 &nbsp; アルゴリズムプロセス
挿入ソートの全体的なプロセスは下図に示されます。
1. 配列の最初の要素をソート済みとみなします。
2. 2番目の要素を`base`として選択し、正しい位置に挿入して、**最初の2つの要素をソート済みにします**。
3. 3番目の要素を`base`として選択し、正しい位置に挿入して、**最初の3つの要素をソート済みにします**。
4. この方法で続行し、最後の反復では、最後の要素を`base`として取り、正しい位置に挿入した後、**すべての要素がソートされます**。
![Insertion sort process](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-7 &nbsp; Insertion sort process </p>
コード例は以下の通りです:
=== "Python"
```python title="insertion_sort.py"
def insertion_sort(nums: list[int]):
"""挿入ソート"""
# 外側のループ:ソート済み範囲は [0, i-1]
for i in range(1, len(nums)):
base = nums[i]
j = i - 1
# 内側のループbase をソート済み範囲 [0, i-1] の正しい位置に挿入
while j >= 0 and nums[j] > base:
nums[j + 1] = nums[j] # nums[j] を右に1つ移動
j -= 1
nums[j + 1] = base # base を正しい位置に代入
```
=== "C++"
```cpp title="insertion_sort.cpp"
/* 挿入ソート */
void insertionSort(vector<int> &nums) {
// 外側ループ:ソート済み範囲は[0, i-1]
for (int i = 1; i < nums.size(); i++) {
int base = nums[i], j = i - 1;
// 内側ループbaseをソート済み範囲[0, i-1]内の正しい位置に挿入
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // nums[j]を一つ右に移動
j--;
}
nums[j + 1] = base; // baseを正しい位置に代入
}
}
```
=== "Java"
```java title="insertion_sort.java"
/* 挿入ソート */
void insertionSort(int[] nums) {
// 外側ループ: ソート済み範囲は [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// 内側ループ: base をソート済み範囲 [0, i-1] の正しい位置に挿入
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // nums[j] を右に1つ移動
j--;
}
nums[j + 1] = base; // base を正しい位置に代入
}
}
```
=== "C#"
```csharp title="insertion_sort.cs"
[class]{insertion_sort}-[func]{InsertionSort}
```
=== "Go"
```go title="insertion_sort.go"
[class]{}-[func]{insertionSort}
```
=== "Swift"
```swift title="insertion_sort.swift"
[class]{}-[func]{insertionSort}
```
=== "JS"
```javascript title="insertion_sort.js"
[class]{}-[func]{insertionSort}
```
=== "TS"
```typescript title="insertion_sort.ts"
[class]{}-[func]{insertionSort}
```
=== "Dart"
```dart title="insertion_sort.dart"
[class]{}-[func]{insertionSort}
```
=== "Rust"
```rust title="insertion_sort.rs"
[class]{}-[func]{insertion_sort}
```
=== "C"
```c title="insertion_sort.c"
[class]{}-[func]{insertionSort}
```
=== "Kotlin"
```kotlin title="insertion_sort.kt"
[class]{}-[func]{insertionSort}
```
=== "Ruby"
```ruby title="insertion_sort.rb"
[class]{}-[func]{insertion_sort}
```
=== "Zig"
```zig title="insertion_sort.zig"
[class]{}-[func]{insertionSort}
```
## 11.4.2 &nbsp; アルゴリズムの特性
- **時間計算量は$O(n^2)$、適応ソート**:最悪の場合、各挿入操作には$n - 1$、$n-2$、...、$2$、$1$のループが必要で、合計は$(n - 1) n / 2$となり、時間計算量は$O(n^2)$です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量$O(n)$を実現します。
- **空間計算量は$O(1)$、インプレースソート**:ポインタ$i$と$j$は定数量の追加空間を使用します。
- **安定ソート**:挿入操作中、等しい要素の右側に要素を挿入し、順序を変更しません。
## 11.4.3 &nbsp; 挿入ソートの利点
挿入ソートの時間計算量は$O(n^2)$で、次に学習するクイックソートの時間計算量は$O(n \log n)$です。挿入ソートはより高い時間計算量を持ちますが、**小さな入力サイズでは通常より高速です**。
この結論は線形探索と二分探索の結論と似ています。時間計算量が$O(n \log n)$で分割統治戦略に基づくクイックソートなどのアルゴリズムは、多くの場合より多くの単位操作を含みます。小さな入力サイズでは、$n^2$と$n \log n$の数値は近く、計算量が支配的でなく、ラウンドあたりの単位操作数が決定的な役割を果たします。
実際、多くのプログラミング言語Javaなどは、組み込みソート関数内で挿入ソートを使用しています。一般的なアプローチは長い配列に対しては、クイックソートなどの分割統治戦略に基づくソートアルゴリズムを使用し、短い配列に対しては挿入ソートを直接使用します。
バブルソート、選択ソート、挿入ソートはすべて時間計算量$O(n^2)$を持ちますが、実際には、**挿入ソートはバブルソートや選択ソートよりも一般的に使用されます**。主な理由は以下の通りです。
- バブルソートは要素交換に基づき、一時変数の使用が必要で、3つの単位操作を含みます挿入ソートは要素代入に基づき、1つの単位操作のみが必要です。したがって、**バブルソートの計算オーバーヘッドは一般的に挿入ソートよりも高くなります**。
- 選択ソートの時間計算量は常に$O(n^2)$です。**部分的に順序付けられたデータのセットが与えられた場合、挿入ソートは通常選択ソートよりも効率的です**。
- 選択ソートは不安定で、マルチレベルソートに適用できません。

View File

@@ -0,0 +1,298 @@
---
comments: true
---
# 11.6 &nbsp; マージソート
<u>マージソート</u>は分割統治戦略に基づくソートアルゴリズムで、下図に示す「分割」と「マージ」フェーズを含みます。
1. **分割フェーズ**:中点から配列を再帰的に分割し、長い配列のソート問題をより短い配列に変換します。
2. **マージフェーズ**サブ配列の長さが1になったときに分割を停止し、その後マージを開始します。2つの短いソート済み配列を連続的により長いソート済み配列にマージし、プロセスが完了するまで続行します。
![The divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-10 &nbsp; The divide and merge phases of merge sort </p>
## 11.6.1 &nbsp; アルゴリズムワークフロー
下図に示すように、「分割フェーズ」は中点から配列を上から下に2つのサブ配列に再帰的に分割します。
1. 中点`mid`を計算し、左サブ配列(区間`[left, mid]`)と右サブ配列(区間`[mid + 1, right]`)を再帰的に分割します。
2. サブ配列の長さが1になるまでステップ`1.`を再帰的に続行し、その後停止します。
「マージフェーズ」は左と右のサブ配列を下から上にソート済み配列に結合します。重要なのは、マージが長さ1のサブ配列から開始され、マージフェーズ中に各サブ配列がソートされることです。
=== "<1>"
![Merge sort process](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
=== "<2>"
![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" }
=== "<3>"
![merge_sort_step3](merge_sort.assets/merge_sort_step3.png){ class="animation-figure" }
=== "<4>"
![merge_sort_step4](merge_sort.assets/merge_sort_step4.png){ class="animation-figure" }
=== "<5>"
![merge_sort_step5](merge_sort.assets/merge_sort_step5.png){ class="animation-figure" }
=== "<6>"
![merge_sort_step6](merge_sort.assets/merge_sort_step6.png){ class="animation-figure" }
=== "<7>"
![merge_sort_step7](merge_sort.assets/merge_sort_step7.png){ class="animation-figure" }
=== "<8>"
![merge_sort_step8](merge_sort.assets/merge_sort_step8.png){ class="animation-figure" }
=== "<9>"
![merge_sort_step9](merge_sort.assets/merge_sort_step9.png){ class="animation-figure" }
=== "<10>"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" }
<p align="center"> 図 11-11 &nbsp; Merge sort process </p>
マージソートの再帰順序は二分木の後順横断と一致することが観察できます。
- **後順横断**:まず左のサブツリーを再帰的に横断し、次に右のサブツリーを横断し、最後にルートノードを処理します。
- **マージソート**:まず左のサブ配列を再帰的に処理し、次に右のサブ配列を処理し、最後にマージを実行します。
マージソートの実装は以下のコードに示されます。`nums`でマージされる区間は`[left, right]`で、`tmp`の対応する区間は`[0, right - left]`であることに注意してください。
=== "Python"
```python title="merge_sort.py"
def merge(nums: list[int], left: int, mid: int, right: int):
"""左サブ配列と右サブ配列をマージ"""
# 左サブ配列区間は [left, mid]、右サブ配列区間は [mid+1, right]
# 一時配列 tmp を作成してマージ結果を格納
tmp = [0] * (right - left + 1)
# 左右サブ配列の開始インデックスを初期化
i, j, k = left, mid + 1, 0
# 両方のサブ配列に要素が残っている間、より小さい要素を一時配列にコピー
while i <= mid and j <= right:
if nums[i] <= nums[j]:
tmp[k] = nums[i]
i += 1
else:
tmp[k] = nums[j]
j += 1
k += 1
# 残った左右サブ配列の要素を一時配列にコピー
while i <= mid:
tmp[k] = nums[i]
i += 1
k += 1
while j <= right:
tmp[k] = nums[j]
j += 1
k += 1
# 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック
for k in range(0, len(tmp)):
nums[left + k] = tmp[k]
def merge_sort(nums: list[int], left: int, right: int):
"""マージソート"""
# 終了条件
if left >= right:
return # サブ配列の長さが1のときに再帰を終了
# 分割段階
mid = left + (right - left) // 2 # 中点を計算
merge_sort(nums, left, mid) # 左サブ配列を再帰的に処理
merge_sort(nums, mid + 1, right) # 右サブ配列を再帰的に処理
# マージ段階
merge(nums, left, mid, right)
```
=== "C++"
```cpp title="merge_sort.cpp"
/* 左サブ配列と右サブ配列をマージ */
void merge(vector<int> &nums, int left, int mid, int right) {
// 左サブ配列の区間は[left, mid]、右サブ配列の区間は[mid+1, right]
// マージ結果を保存する一時配列tmpを作成
vector<int> tmp(right - left + 1);
// 左右サブ配列の開始インデックスを初期化
int i = left, j = mid + 1, k = 0;
// 両サブ配列に要素がある間、小さい方の要素を一時配列にコピー
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 左右サブ配列の残りの要素を一時配列にコピー
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 一時配列tmpの要素を元の配列numsの対応する区間にコピー
for (k = 0; k < tmp.size(); k++) {
nums[left + k] = tmp[k];
}
}
/* マージソート */
void mergeSort(vector<int> &nums, int left, int right) {
// 終了条件
if (left >= right)
return; // サブ配列の長さが1の時、再帰を終了
// 分割段階
int mid = left + (right - left) / 2; // 中点を計算
mergeSort(nums, left, mid); // 左サブ配列を再帰的に処理
mergeSort(nums, mid + 1, right); // 右サブ配列を再帰的に処理
// マージ段階
merge(nums, left, mid, right);
}
```
=== "Java"
```java title="merge_sort.java"
/* 左部分配列と右部分配列をマージ */
void merge(int[] nums, int left, int mid, int right) {
// 左部分配列区間は [left, mid]、右部分配列区間は [mid+1, right]
// 一時配列 tmp を作成してマージ結果を格納
int[] tmp = new int[right - left + 1];
// 左右部分配列の開始インデックスを初期化
int i = left, j = mid + 1, k = 0;
// 両部分配列にまだ要素がある間、比較してより小さい要素を一時配列にコピー
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 左右部分配列の残りの要素を一時配列にコピー
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
/* マージソート */
void mergeSort(int[] nums, int left, int right) {
// 終了条件
if (left >= right)
return; // 部分配列の長さが 1 のとき再帰を終了
// 分割段階
int mid = left + (right - left) / 2; // 中点を計算
mergeSort(nums, left, mid); // 左部分配列を再帰的に処理
mergeSort(nums, mid + 1, right); // 右部分配列を再帰的に処理
// マージ段階
merge(nums, left, mid, right);
}
```
=== "C#"
```csharp title="merge_sort.cs"
[class]{merge_sort}-[func]{Merge}
[class]{merge_sort}-[func]{MergeSort}
```
=== "Go"
```go title="merge_sort.go"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Swift"
```swift title="merge_sort.swift"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "JS"
```javascript title="merge_sort.js"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "TS"
```typescript title="merge_sort.ts"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Dart"
```dart title="merge_sort.dart"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Rust"
```rust title="merge_sort.rs"
[class]{}-[func]{merge}
[class]{}-[func]{merge_sort}
```
=== "C"
```c title="merge_sort.c"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Kotlin"
```kotlin title="merge_sort.kt"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Ruby"
```ruby title="merge_sort.rb"
[class]{}-[func]{merge}
[class]{}-[func]{merge_sort}
```
=== "Zig"
```zig title="merge_sort.zig"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
## 11.6.2 &nbsp; アルゴリズムの特性
- **$O(n \log n)$の時間計算量、非適応ソート**:分割により高さ$\log n$の再帰ツリーが作成され、各層で合計$n$回の操作をマージし、全体的な時間計算量は$O(n \log n)$となります。
- **$O(n)$の空間計算量、非インプレースソート**:再帰深度は$\log n$で、$O(\log n)$のスタックフレーム空間を使用します。マージ操作には補助配列が必要で、追加の$O(n)$空間を使用します。
- **安定ソート**:マージプロセス中、等しい要素の順序は変更されません。
## 11.6.3 &nbsp; 連結リストのソート
連結リストの場合、マージソートは他のソートアルゴリズムよりも大きな利点があります。**連結リストソートタスクの空間計算量を$O(1)$に最適化できます**。
- **分割フェーズ**:「再帰」の代わりに「反復」を使用して連結リスト分割作業を実行できるため、再帰で使用されるスタックフレーム空間を節約できます。
- **マージフェーズ**連結リストでは、ードの挿入と削除操作は参照ポインタを変更することで実現できるため、マージフェーズ2つの短い順序付きリストを1つの長い順序付きリストに結合中に追加のリストを作成する必要がありません。
実装の詳細は比較的複雑で、興味のある読者は関連資料を参照して学習してください。

View File

@@ -0,0 +1,662 @@
---
comments: true
---
# 11.5 &nbsp; クイックソート
<u>クイックソート</u>は分割統治戦略に基づくソートアルゴリズムで、その効率性と幅広い応用で知られています。
クイックソートのコア操作は「ピボット分割」で、配列から要素を「ピボット」として選択し、ピボットより小さいすべての要素をその左側に移動し、ピボットより大きいすべての要素をその右側に移動することを目的としています。具体的に、ピボット分割のプロセスは下図に示されます。
1. 配列の最も左の要素をピボットとして選択し、2つのポインタ`i``j`を初期化して配列の両端をそれぞれ指すようにします。
2. 各ラウンドで`i``j`を使用してピボットより大きい小さい最初の要素を探索し、次にこれら2つの要素を交換するループを設定します。
3. `i``j`が出会うまでステップ`2.`を繰り返し、最後にピボットを2つのサブ配列の境界に交換します。
=== "<1>"
![Pivot division process](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" }
=== "<2>"
![pivot_division_step2](quick_sort.assets/pivot_division_step2.png){ class="animation-figure" }
=== "<3>"
![pivot_division_step3](quick_sort.assets/pivot_division_step3.png){ class="animation-figure" }
=== "<4>"
![pivot_division_step4](quick_sort.assets/pivot_division_step4.png){ class="animation-figure" }
=== "<5>"
![pivot_division_step5](quick_sort.assets/pivot_division_step5.png){ class="animation-figure" }
=== "<6>"
![pivot_division_step6](quick_sort.assets/pivot_division_step6.png){ class="animation-figure" }
=== "<7>"
![pivot_division_step7](quick_sort.assets/pivot_division_step7.png){ class="animation-figure" }
=== "<8>"
![pivot_division_step8](quick_sort.assets/pivot_division_step8.png){ class="animation-figure" }
=== "<9>"
![pivot_division_step9](quick_sort.assets/pivot_division_step9.png){ class="animation-figure" }
<p align="center"> 図 11-8 &nbsp; Pivot division process </p>
ピボット分割後、元の配列は3つの部分に分割されます左サブ配列、ピボット、右サブ配列で、「左サブ配列の任意の要素 $\leq$ ピボット $\leq$ 右サブ配列の任意の要素」を満たします。したがって、これら2つのサブ配列のみをソートすればよいのです。
!!! note "クイックソートの分割統治戦略"
ピボット分割の本質は、より長い配列のソート問題をより短い2つの配列に簡素化することです。
=== "Python"
```python title="quick_sort.py"
def partition(self, nums: list[int], left: int, right: int) -> int:
"""分割"""
# nums[left] をピボットとして使用
i, j = left, right
while i < j:
while i < j and nums[j] >= nums[left]:
j -= 1 # 右から左へピボットより小さい最初の要素を探す
while i < j and nums[i] <= nums[left]:
i += 1 # 左から右へピボットより大きい最初の要素を探す
# 要素を交換
nums[i], nums[j] = nums[j], nums[i]
# ピボットを2つのサブ配列の境界に交換
nums[i], nums[left] = nums[left], nums[i]
return i # ピボットのインデックスを返す
```
=== "C++"
```cpp title="quick_sort.cpp"
/* 分割 */
int partition(vector<int> &nums, int left, int right) {
// nums[left]をピボットとして使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へピボットより小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へピボットより大きい最初の要素を検索
swap(nums, i, j); // これら二つの要素を交換
}
swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換
return i; // ピボットのインデックスを返す
}
```
=== "Java"
```java title="quick_sort.java"
/* 要素を交換 */
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/* 分割 */
int partition(int[] nums, int left, int right) {
// nums[left] を基準値として使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へ、基準値より小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へ、基準値より大きい最初の要素を検索
swap(nums, i, j); // これら2つの要素を交換
}
swap(nums, i, left); // 基準値を2つの部分配列の境界に交換
return i; // 基準値のインデックスを返す
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{quickSort}-[func]{Swap}
[class]{quickSort}-[func]{Partition}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSort}-[func]{partition}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{partition}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSort}-[func]{swap}
[class]{QuickSort}-[func]{partition}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSort}-[func]{swap}
[class]{QuickSort}-[func]{partition}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSort}-[func]{_swap}
[class]{QuickSort}-[func]{_partition}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSort}-[func]{partition}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{swap}
[class]{}-[func]{partition}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{swap}
[class]{}-[func]{partition}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSort}-[func]{partition}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSort}-[func]{swap}
[class]{QuickSort}-[func]{partition}
```
## 11.5.1 &nbsp; アルゴリズムプロセス
クイックソートの全体的なプロセスは下図に示されます。
1. まず、元の配列に対して「ピボット分割」を実行し、未ソートの左と右のサブ配列を取得します。
2. 次に、左と右のサブ配列に対してそれぞれ再帰的に「ピボット分割」を実行します。
3. サブ配列の長さが1になるまで再帰を続け、配列全体のソートを完了します。
![Quick sort process](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-9 &nbsp; Quick sort process </p>
=== "Python"
```python title="quick_sort.py"
def quick_sort(self, nums: list[int], left: int, right: int):
"""クイックソート"""
# サブ配列の長さが1のときに再帰を終了
if left >= right:
return
# 分割
pivot = self.partition(nums, left, right)
# 左サブ配列と右サブ配列を再帰的に処理
self.quick_sort(nums, left, pivot - 1)
self.quick_sort(nums, pivot + 1, right)
```
=== "C++"
```cpp title="quick_sort.cpp"
/* クイックソート */
void quickSort(vector<int> &nums, int left, int right) {
// サブ配列の長さが1の時、再帰を終了
if (left >= right)
return;
// 分割
int pivot = partition(nums, left, right);
// 左サブ配列と右サブ配列を再帰的に処理
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
```
=== "Java"
```java title="quick_sort.java"
/* クイックソート */
void quickSort(int[] nums, int left, int right) {
// 部分配列の長さが 1 のとき再帰を終了
if (left >= right)
return;
// 分割
int pivot = partition(nums, left, right);
// 左部分配列と右部分配列を再帰的に処理
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{quickSort}-[func]{QuickSort}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSort}-[func]{quickSort}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{quickSort}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSort}-[func]{quickSort}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSort}-[func]{quickSort}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSort}-[func]{quickSort}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSort}-[func]{quick_sort}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{quickSort}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{quickSort}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSort}-[func]{quick_sort}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSort}-[func]{quickSort}
```
## 11.5.2 &nbsp; アルゴリズムの特徴
- **$O(n \log n)$の時間計算量、非適応ソート**:平均的なケースでは、ピボット分割の再帰レベルは$\log n$で、レベルあたりのループの総数は$n$であり、全体で$O(n \log n)$の時間を使用します。最悪の場合、各ラウンドのピボット分割は長さ$n$の配列を長さ$0$と$n - 1$の2つのサブ配列に分割し、再帰レベル数が$n$に達すると、各レベルのループ数は$n$で、使用される総時間は$O(n^2)$です。
- **$O(n)$の空間計算量、インプレースソート**:入力配列が完全に逆順の場合、最悪の再帰深度は$n$に達し、$O(n)$のスタックフレーム空間を使用します。ソート操作は追加の配列の助けなしに元の配列で実行されます。
- **非安定ソート**:ピボット分割の最終ステップで、ピボットは等しい要素の右側に交換される可能性があります。
## 11.5.3 &nbsp; なぜクイックソートは高速なのか
名前が示すように、クイックソートは効率性の面で一定の利点を持つべきです。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、以下の理由で一般的により効率的です。
- **最悪ケースシナリオの低い確率**:クイックソートの最悪時間計算量は$O(n^2)$で、マージソートほど安定していませんが、ほとんどの場合、クイックソートは$O(n \log n)$の時間計算量で動作できます。
- **高いキャッシュ利用率**:ピボット分割操作中、システムはサブ配列全体をキャッシュにロードできるため、要素により効率的にアクセスできます。対照的に、「ヒープソート」などのアルゴリズムは要素にジャンプ方式でアクセスする必要があり、この特徴を欠いています。
- **計算量の小さな定数係数**上記3つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作の総数が最も少ないです。これは「挿入ソート」が「バブルソート」よりも高速な理由と似ています。
## 11.5.4 &nbsp; ピボット最適化
**クイックソートの時間効率は特定の入力で劣化する可能性があります**。例えば、入力配列が完全に逆順の場合、最も左の要素をピボットとして選択するため、ピボット分割後、ピボットは配列の右端に交換され、左サブ配列の長さが$n - 1$、右サブ配列の長さが$0$になります。この方法を続けると、各ラウンドのピボット分割でサブ配列の長さが$0$になり、分割統治戦略が失敗し、クイックソートは「バブルソート」に似た形に劣化します。
この状況を避けるため、**ピボット分割でピボット選択戦略を最適化できます**。例えば、要素をランダムに選択してピボットとすることができます。ただし、運が悪く、一貫して最適でないピボットを選択した場合、効率はまだ満足できません。
プログラミング言語は通常「疑似乱数」を生成することに注意することが重要です。疑似乱数シーケンスに対して特定のテストケースを構築すると、クイックソートの効率はまだ劣化する可能性があります。
さらなる改善のため、3つの候補要素通常は配列の最初、最後、中点の要素を選択し、**これら3つの候補要素の中央値をピボットとして使用**できます。この方法で、ピボットが「小さすぎず大きすぎない」確率が大幅に増加します。もちろん、さらに多くの候補要素を選択してアルゴリズムの堅牢性をさらに向上させることもできます。この方法により、時間計算量が$O(n^2)$に劣化する確率が大幅に削減されます。
サンプルコードは以下の通りです:
=== "Python"
```python title="quick_sort.py"
def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:
"""3つの候補要素の中央値を選択"""
l, m, r = nums[left], nums[mid], nums[right]
if (l <= m <= r) or (r <= m <= l):
return mid # m は l と r の間
if (m <= l <= r) or (r <= l <= m):
return left # l は m と r の間
return right
def partition(self, nums: list[int], left: int, right: int) -> int:
"""分割(三点中央値)"""
# nums[left] をピボットとして使用
med = self.median_three(nums, left, (left + right) // 2, right)
# 中央値を配列の最左端に交換
nums[left], nums[med] = nums[med], nums[left]
# nums[left] をピボットとして使用
i, j = left, right
while i < j:
while i < j and nums[j] >= nums[left]:
j -= 1 # 右から左へピボットより小さい最初の要素を探す
while i < j and nums[i] <= nums[left]:
i += 1 # 左から右へピボットより大きい最初の要素を探す
# 要素を交換
nums[i], nums[j] = nums[j], nums[i]
# ピボットを2つのサブ配列の境界に交換
nums[i], nums[left] = nums[left], nums[i]
return i # ピボットのインデックスを返す
```
=== "C++"
```cpp title="quick_sort.cpp"
/* 三つの候補要素の中央値を選択 */
int medianThree(vector<int> &nums, int left, int mid, int right) {
int l = nums[left], m = nums[mid], r = nums[right];
if ((l <= m && m <= r) || (r <= m && m <= l))
return mid; // mはlとrの間
if ((m <= l && l <= r) || (r <= l && l <= m))
return left; // lはmとrの間
return right;
}
/* 分割(三つの中央値) */
int partition(vector<int> &nums, int left, int right) {
// 三つの候補要素の中央値を選択
int med = medianThree(nums, left, (left + right) / 2, right);
// 中央値を配列の最左位置に交換
swap(nums, left, med);
// nums[left]をピボットとして使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へピボットより小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へピボットより大きい最初の要素を検索
swap(nums, i, j); // これら二つの要素を交換
}
swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換
return i; // ピボットのインデックスを返す
}
```
=== "Java"
```java title="quick_sort.java"
/* 3つの候補要素の中央値を選択 */
int medianThree(int[] nums, int left, int mid, int right) {
int l = nums[left], m = nums[mid], r = nums[right];
if ((l <= m && m <= r) || (r <= m && m <= l))
return mid; // m は l と r の間
if ((m <= l && l <= r) || (r <= l && l <= m))
return left; // l は m と r の間
return right;
}
/* 分割3つの中央値 */
int partition(int[] nums, int left, int right) {
// 3つの候補要素の中央値を選択
int med = medianThree(nums, left, (left + right) / 2, right);
// 中央値を配列の最左端の位置に交換
swap(nums, left, med);
// nums[left] を基準値として使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へ、基準値より小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へ、基準値より大きい最初の要素を検索
swap(nums, i, j); // これら2つの要素を交換
}
swap(nums, i, left); // 基準値を2つの部分配列の境界に交換
return i; // 基準値のインデックスを返す
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{QuickSortMedian}-[func]{MedianThree}
[class]{QuickSortMedian}-[func]{Partition}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSortMedian}-[func]{medianThree}
[class]{quickSortMedian}-[func]{partition}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{medianThree}
[class]{}-[func]{partitionMedian}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSortMedian}-[func]{medianThree}
[class]{QuickSortMedian}-[func]{partition}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSortMedian}-[func]{medianThree}
[class]{QuickSortMedian}-[func]{partition}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSortMedian}-[func]{_medianThree}
[class]{QuickSortMedian}-[func]{_partition}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSortMedian}-[func]{median_three}
[class]{QuickSortMedian}-[func]{partition}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{medianThree}
[class]{}-[func]{partitionMedian}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{medianThree}
[class]{}-[func]{partitionMedian}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSortMedian}-[func]{median_three}
[class]{QuickSortMedian}-[func]{partition}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSortMedian}-[func]{medianThree}
[class]{QuickSortMedian}-[func]{partition}
```
## 11.5.5 &nbsp; 末尾再帰最適化
**特定の入力では、クイックソートはより多くの空間を占有する可能性があります**。例えば、完全に順序付けられた入力配列を考えてみましょう。再帰でのサブ配列の長さを$m$とします。各ラウンドのピボット分割で、長さ$0$の左サブ配列と長さ$m - 1$の右サブ配列が生成されます。これは、再帰呼び出しごとに問題サイズが1つの要素のみ減少することを意味し、各レベルの再帰での削減が非常に小さくなります。
結果として、再帰ツリーの高さは$n 1$に達する可能性があり、これには$O(n)$のスタックフレーム空間が必要です。
スタックフレーム空間の蓄積を防ぐため、各ラウンドのピボットソート後に2つのサブ配列の長さを比較し、**より短いサブ配列のみを再帰的にソート**できます。より短いサブ配列の長さは$n / 2$を超えないため、この方法は再帰深度が$\log n$を超えないことを保証し、最悪空間計算量を$O(\log n)$に最適化します。コードは以下の通りです:
=== "Python"
```python title="quick_sort.py"
def quick_sort(self, nums: list[int], left: int, right: int):
"""クイックソート(末尾再帰最適化)"""
# サブ配列の長さが1のときに終了
while left < right:
# 分割操作
pivot = self.partition(nums, left, right)
# 2つのサブ配列のうち短い方に対してクイックソートを実行
if pivot - left < right - pivot:
self.quick_sort(nums, left, pivot - 1) # 左サブ配列を再帰的にソート
left = pivot + 1 # 残りの未ソート区間は [pivot + 1, right]
else:
self.quick_sort(nums, pivot + 1, right) # 右サブ配列を再帰的にソート
right = pivot - 1 # 残りの未ソート区間は [left, pivot - 1]
```
=== "C++"
```cpp title="quick_sort.cpp"
/* クイックソート(末尾再帰最適化) */
void quickSort(vector<int> &nums, int left, int right) {
// サブ配列の長さが1の時終了
while (left < right) {
// 分割操作
int pivot = partition(nums, left, right);
// 二つのサブ配列のうち短い方でクイックソートを実行
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 左サブ配列を再帰的にソート
left = pivot + 1; // 残りの未ソート区間は[pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 右サブ配列を再帰的にソート
right = pivot - 1; // 残りの未ソート区間は[left, pivot - 1]
}
}
}
```
=== "Java"
```java title="quick_sort.java"
/* クイックソート(末尾再帰最適化) */
void quickSort(int[] nums, int left, int right) {
// 部分配列の長さが 1 のとき終了
while (left < right) {
// 分割操作
int pivot = partition(nums, left, right);
// 2つの部分配列のうち短い方にクイックソートを実行
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート
left = pivot + 1; // 残りの未ソート区間は [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート
right = pivot - 1; // 残りの未ソート区間は [left, pivot - 1]
}
}
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{QuickSortTailCall}-[func]{QuickSort}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSortTailCall}-[func]{quickSort}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{quickSortTailCall}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSortTailCall}-[func]{quickSort}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSortTailCall}-[func]{quickSort}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSortTailCall}-[func]{quickSort}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSortTailCall}-[func]{quick_sort}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{quickSortTailCall}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{quickSortTailCall}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSortTailCall}-[func]{quick_sort}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSortTailCall}-[func]{quickSort}
```

View File

@@ -0,0 +1,303 @@
---
comments: true
---
# 11.10 &nbsp; 基数ソート
前の節では計数ソートを紹介しました。これは、データサイズ $n$ が大きいがデータ範囲 $m$ が小さいシナリオに適しています。$n = 10^6$ の学生IDをソートする必要があり、各IDが $8$ 桁の数字であるとします。これは、データ範囲 $m = 10^8$ が非常に大きいことを意味します。この場合、計数ソートを使用すると、大量のメモリスペースが必要になります。基数ソートはこの状況を回避できます。
<u>基数ソート</u>は計数ソートと同じ核心概念を共有し、要素の頻度をカウントすることでソートします。同時に、基数ソートは数字の桁間の漸進的関係を利用してこれを基盤としています。桁を一度に一つずつ処理してソートし、最終的なソート順序を達成します。
## 11.10.1 &nbsp; アルゴリズムの過程
学生IDデータを例として、最下位桁を $1$ 番目、最上位桁を $8$ 番目とすると、基数ソートの過程は以下の図に示されています。
1. 桁 $k = 1$ を初期化します。
2. 学生IDの $k$ 番目の桁に対して「計数ソート」を実行します。完了後、データは $k$ 番目の桁に基づいて最小から最大までソートされます。
3. $k$ を $1$ 増やし、手順 `2.` に戻って反復を続け、すべての桁がソートされるまで続けます。この時点で過程が終了します。
![基数ソートアルゴリズムの過程](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-18 &nbsp; 基数ソートアルゴリズムの過程 </p>
以下、コード実装を詳しく見てみます。基数 $d$ での数 $x$ に対して、その $k$ 番目の桁 $x_k$ を取得するには、以下の計算式を使用できます:
$$
x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
$$
ここで $\lfloor a \rfloor$ は浮動小数点数 $a$ の切り捨てを表し、$\bmod \: d$ は $d$ による剰余を表します。学生IDデータの場合、$d = 10$ で $k \in [1, 8]$ です。
さらに、$k$ 番目の桁に基づいてソートできるように、計数ソートのコードを少し修正する必要があります:
=== "Python"
```python title="radix_sort.py"
def digit(num: int, exp: int) -> int:
"""要素 num の k 番目の桁を取得、exp = 10^(k-1)"""
# k の代わりに exp を渡すことで、ここでコストの高い累乗計算を避けることができる
return (num // exp) % 10
def counting_sort_digit(nums: list[int], exp: int):
"""計数ソートnums の k 番目の桁に基づく)"""
# 10進数の桁の範囲は 0~9、したがって長さ10のバケット配列が必要
counter = [0] * 10
n = len(nums)
# 数字 0~9 の出現回数を統計
for i in range(n):
d = digit(nums[i], exp) # nums[i] の k 番目の桁を取得、d とする
counter[d] += 1 # 数字 d の出現回数を統計
# 前置和を計算し、「出現回数」を「配列インデックス」に変換
for i in range(1, 10):
counter[i] += counter[i - 1]
# 逆順に走査し、バケット統計に基づいて各要素を res に配置
res = [0] * n
for i in range(n - 1, -1, -1):
d = digit(nums[i], exp)
j = counter[d] - 1 # 配列内の d のインデックス j を取得
res[j] = nums[i] # 現在の要素をインデックス j に配置
counter[d] -= 1 # d の数を1減らす
# 結果を使用して元の配列 nums を上書き
for i in range(n):
nums[i] = res[i]
def radix_sort(nums: list[int]):
"""基数ソート"""
# 配列の最大要素を取得し、最大桁数を判定するために使用
m = max(nums)
# 最下位桁から最上位桁まで走査
exp = 1
while exp <= m:
# 配列要素の k 番目の桁に対して計数ソートを実行
# k = 1 -> exp = 1
# k = 2 -> exp = 10
# つまり、exp = 10^(k-1)
counting_sort_digit(nums, exp)
exp *= 10
```
=== "C++"
```cpp title="radix_sort.cpp"
/* 要素numのk番目の桁を取得、exp = 10^(k-1) */
int digit(int num, int exp) {
// kの代わりにexpを渡すことで、ここで繰り返される高価な冪乗計算を避けることができる
return (num / exp) % 10;
}
/* カウントソートnumsのk番目の桁に基づく */
void countingSortDigit(vector<int> &nums, int exp) {
// 10進数の桁範囲は0~9なので、長さ10のバケット配列が必要
vector<int> counter(10, 0);
int n = nums.size();
// 数字0~9の出現回数を統計
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // nums[i]のk番目の桁を取得、dとして記録
counter[d]++; // 数字dの出現回数を統計
}
// 前缀和を計算し、「出現回数」を「配列インデックス」に変換
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順で走査し、バケット統計に基づいて各要素をresに配置
vector<int> res(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // dが配列内にあるインデックスjを取得
res[j] = nums[i]; // 現在の要素をインデックスjに配置
counter[d]--; // dのカウントを1減らす
}
// 結果で元の配列numsを上書き
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基数ソート */
void radixSort(vector<int> &nums) {
// 配列の最大要素を取得、最大桁数を判定するために使用
int m = *max_element(nums.begin(), nums.end());
// 最下位桁から最上位桁まで走査
for (int exp = 1; exp <= m; exp *= 10)
// 配列要素のk番目の桁でカウントソートを実行
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり、exp = 10^(k-1)
countingSortDigit(nums, exp);
}
```
=== "Java"
```java title="radix_sort.java"
/* 要素 num の k 番目の桁を取得、exp = 10^(k-1) */
int digit(int num, int exp) {
// k の代わりに exp を渡すことで、ここでコストの高い累乗計算の繰り返しを避けることができる
return (num / exp) % 10;
}
/* 計数ソートnums の k 番目の桁に基づく) */
void countingSortDigit(int[] nums, int exp) {
// 10進数の桁の範囲は 0~9、したがって長さ 10 のバケット配列が必要
int[] counter = new int[10];
int n = nums.length;
// 桁 0~9 の出現回数を統計
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // nums[i] の k 番目の桁を取得、d とする
counter[d]++; // 桁 d の出現回数を統計
}
// 累積和を計算し、「出現回数」を「配列インデックス」に変換
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット統計に基づいて各要素を res に配置
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // 配列内での d のインデックス j を取得
res[j] = nums[i]; // 現在の要素をインデックス j に配置
counter[d]--; // d のカウントを 1 減らす
}
// 結果で元の配列 nums を上書き
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基数ソート */
void radixSort(int[] nums) {
// 配列の最大要素を取得し、最大桁数を判定するために使用
int m = Integer.MIN_VALUE;
for (int num : nums)
if (num > m)
m = num;
// 最下位桁から最上位桁まで走査
for (int exp = 1; exp <= m; exp *= 10) {
// 配列要素の k 番目の桁に対して計数ソートを実行
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// すなわち exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
```
=== "C#"
```csharp title="radix_sort.cs"
[class]{radix_sort}-[func]{Digit}
[class]{radix_sort}-[func]{CountingSortDigit}
[class]{radix_sort}-[func]{RadixSort}
```
=== "Go"
```go title="radix_sort.go"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Swift"
```swift title="radix_sort.swift"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "JS"
```javascript title="radix_sort.js"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "TS"
```typescript title="radix_sort.ts"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Dart"
```dart title="radix_sort.dart"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Rust"
```rust title="radix_sort.rs"
[class]{}-[func]{digit}
[class]{}-[func]{counting_sort_digit}
[class]{}-[func]{radix_sort}
```
=== "C"
```c title="radix_sort.c"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Kotlin"
```kotlin title="radix_sort.kt"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Ruby"
```ruby title="radix_sort.rb"
[class]{}-[func]{digit}
[class]{}-[func]{counting_sort_digit}
[class]{}-[func]{radix_sort}
```
=== "Zig"
```zig title="radix_sort.zig"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
!!! question "なぜ最下位桁から開始するのか?"
連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が $a < b$ で、2番目のラウンドが $a > b$ の場合、2番目のラウンドの結果が最初のラウンドの結果を置き換えます。上位桁は下位桁より優先されるため、上位桁の前に下位桁をソートすることが理にかなっています。
## 11.10.2 &nbsp; アルゴリズムの特徴
計数ソートと比較して、基数ソートはより大きな数値範囲に適していますが、**データが固定桁数で表現でき、桁数があまり大きくないことを前提としています**。例えば、浮動小数点数は桁数 $k$ が大きい可能性があり、時間計算量 $O(nk) \gg O(n^2)$ につながる可能性があるため、基数ソートには適していません。
- **時間計算量は $O(nk)$、非適応ソート**:データサイズを $n$、データが基数 $d$、最大桁数を $k$ とすると、単一桁のソートには $O(n + d)$ 時間がかかり、すべての $k$ 桁のソートには $O((n + d)k)$ 時間がかかります。一般的に、$d$ と $k$ はどちらも比較的小さく、時間計算量は $O(n)$ に近づきます。
- **空間計算量は $O(n + d)$、非インプレースソート**:計数ソートと同様に、基数ソートは長さ $n$ と $d$ の配列 `res` と `counter` にそれぞれ依存します。
- **安定ソート**:計数ソートが安定な場合、基数ソートも安定です。計数ソートが不安定な場合、基数ソートは正しいソート順序を保証できません。

View File

@@ -0,0 +1,187 @@
---
comments: true
---
# 11.2 &nbsp; 選択ソート
<u>選択ソート</u>は非常にシンプルな原理で動作します:各反復で未ソート区間から最小要素を選択し、ソート済みセクションの末尾に移動するループを使用します。
配列の長さを$n$とすると、選択ソートのステップは下図に示されます。
1. 最初に、すべての要素は未ソートで、つまり未ソート(インデックス)区間は$[0, n-1]$です。
2. 区間$[0, n-1]$の最小要素を選択し、インデックス$0$の要素と交換します。この後、配列の最初の要素がソートされます。
3. 区間$[1, n-1]$の最小要素を選択し、インデックス$1$の要素と交換します。この後、配列の最初の2つの要素がソートされます。
4. この方法で続行します。$n - 1$ラウンドの選択と交換の後、最初の$n - 1$個の要素がソートされます。
5. 残りの唯一の要素は結果的に最大要素であり、ソートする必要がないため、配列はソートされます。
=== "<1>"
![Selection sort process](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" }
=== "<2>"
![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" }
=== "<3>"
![selection_sort_step3](selection_sort.assets/selection_sort_step3.png){ class="animation-figure" }
=== "<4>"
![selection_sort_step4](selection_sort.assets/selection_sort_step4.png){ class="animation-figure" }
=== "<5>"
![selection_sort_step5](selection_sort.assets/selection_sort_step5.png){ class="animation-figure" }
=== "<6>"
![selection_sort_step6](selection_sort.assets/selection_sort_step6.png){ class="animation-figure" }
=== "<7>"
![selection_sort_step7](selection_sort.assets/selection_sort_step7.png){ class="animation-figure" }
=== "<8>"
![selection_sort_step8](selection_sort.assets/selection_sort_step8.png){ class="animation-figure" }
=== "<9>"
![selection_sort_step9](selection_sort.assets/selection_sort_step9.png){ class="animation-figure" }
=== "<10>"
![selection_sort_step10](selection_sort.assets/selection_sort_step10.png){ class="animation-figure" }
=== "<11>"
![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" }
<p align="center"> 図 11-2 &nbsp; Selection sort process </p>
コードでは、$k$を使用して未ソート区間内の最小要素を記録します:
=== "Python"
```python title="selection_sort.py"
def selection_sort(nums: list[int]):
"""選択ソート"""
n = len(nums)
# 外側のループ:未ソート範囲は [i, n-1]
for i in range(n - 1):
# 内側のループ:未ソート範囲内で最小要素を見つける
k = i
for j in range(i + 1, n):
if nums[j] < nums[k]:
k = j # 最小要素のインデックスを記録
# 最小要素を未ソート範囲の先頭要素と交換
nums[i], nums[k] = nums[k], nums[i]
```
=== "C++"
```cpp title="selection_sort.cpp"
/* 選択ソート */
void selectionSort(vector<int> &nums) {
int n = nums.size();
// 外側ループ:未ソート範囲は[i, n-1]
for (int i = 0; i < n - 1; i++) {
// 内側ループ:未ソート範囲内で最小要素を見つける
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 最小要素のインデックスを記録
}
// 最小要素を未ソート範囲の最初の要素と交換
swap(nums[i], nums[k]);
}
}
```
=== "Java"
```java title="selection_sort.java"
/* 選択ソート */
void selectionSort(int[] nums) {
int n = nums.length;
// 外側ループ: 未ソート範囲は [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 内側ループ: 未ソート範囲内で最小要素を見つける
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 最小要素のインデックスを記録
}
// 最小要素と未ソート範囲の最初の要素を交換
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
```
=== "C#"
```csharp title="selection_sort.cs"
[class]{selection_sort}-[func]{SelectionSort}
```
=== "Go"
```go title="selection_sort.go"
[class]{}-[func]{selectionSort}
```
=== "Swift"
```swift title="selection_sort.swift"
[class]{}-[func]{selectionSort}
```
=== "JS"
```javascript title="selection_sort.js"
[class]{}-[func]{selectionSort}
```
=== "TS"
```typescript title="selection_sort.ts"
[class]{}-[func]{selectionSort}
```
=== "Dart"
```dart title="selection_sort.dart"
[class]{}-[func]{selectionSort}
```
=== "Rust"
```rust title="selection_sort.rs"
[class]{}-[func]{selection_sort}
```
=== "C"
```c title="selection_sort.c"
[class]{}-[func]{selectionSort}
```
=== "Kotlin"
```kotlin title="selection_sort.kt"
[class]{}-[func]{selectionSort}
```
=== "Ruby"
```ruby title="selection_sort.rb"
[class]{}-[func]{selection_sort}
```
=== "Zig"
```zig title="selection_sort.zig"
[class]{}-[func]{selectionSort}
```
## 11.2.1 &nbsp; アルゴリズムの特性
- **$O(n^2)$の時間計算量、非適応ソート**:外側ループに$n - 1$回の反復があり、未ソートセクションの長さは最初の反復で$n$から始まり、最後の反復で$2$まで減少します。つまり、各外側ループ反復にはそれぞれ$n$、$n - 1$、$\dots$、$3$、$2$回の内側ループ反復が含まれ、合計は$\frac{(n - 1)(n + 2)}{2}$となります。
- **$O(1)$の空間計算量、インプレースソート**:ポインタ$i$と$j$で定数の追加空間を使用します。
- **非安定ソート**:下図に示すように、要素`nums[i]`は等しい要素の右側に交換される可能性があり、相対順序が変わる原因となります。
![Selection sort instability example](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" }
<p align="center"> 図 11-3 &nbsp; Selection sort instability example </p>

View File

@@ -0,0 +1,52 @@
---
comments: true
---
# 11.1 &nbsp; ソートアルゴリズム
<u>ソートアルゴリズム</u>は、データセットを特定の順序で配列するために使用されます。ソートアルゴリズムは、順序付けられたデータは通常、より効率的に探索、分析、処理できるため、幅広い応用があります。
下図に示すように、ソートアルゴリズムのデータ型は整数、浮動小数点数、文字、文字列などです。ソート基準は、数値サイズ、文字ASCII順序、またはカスタム基準など、必要に応じて設定できます。
![Data types and comparator examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" }
<p align="center"> 図 11-1 &nbsp; Data types and comparator examples </p>
## 11.1.1 &nbsp; 評価次元
**実行効率**:ソートアルゴリズムの時間計算量ができるだけ低いことを期待し、全体的な操作数も少ないこと(時間計算量の定数項を下げる)を望みます。大容量データでは、実行効率が特に重要です。
**インプレース性**:名前が示すとおり、<u>インプレースソート</u>は元の配列を直接操作することで実現され、追加のヘルパー配列が不要であるため、メモリを節約します。一般的に、インプレースソートはデータ移動操作が少なく、高速です。
**安定性**<u>安定ソート</u>は、ソート後に配列内の等しい要素の相対順序が変わらないことを保証します。
安定ソートは、マルチキーソートシナリオにおいて必要条件です。学生情報を格納するテーブルがあり、第1列と第2列がそれぞれ名前と年齢であるとします。この場合、<u>不安定ソート</u>は入力データの順序を失う可能性があります:
```shell
# 入力データは名前でソート済み
# (名前, 年齢)
('A', 19)
('B', 18)
('C', 21)
('D', 19)
('E', 23)
# 不安定ソートアルゴリズムを使用してリストを年齢でソートすると仮定すると、
# 結果は('D', 19)と('A', 19)の相対位置を変更し、
# 入力データが名前でソート済みであるという性質が失われる
('B', 18)
('D', 19)
('A', 19)
('C', 21)
('E', 23)
```
**適応性**<u>適応ソート</u>は入力データ内の既存の順序情報を活用して計算負荷を削減し、より最適な時間効率を実現します。適応ソートアルゴリズムの最良ケース時間計算量は、通常平均ケース時間計算量よりも優れています。
**比較ベースまたは非比較ベース**<u>比較ベースソート</u>は比較演算子($<$、$=$、$>$)に依存して要素の相対順序を決定し、配列全体をソートします。理論的最適時間計算量は$O(n \log n)$です。一方、<u>非比較ソート</u>は比較演算子を使用せず、$O(n)$の時間計算量を実現できますが、汎用性は比較的劣ります。
## 11.1.2 &nbsp; 理想的なソートアルゴリズム
**高速実行、インプレース、安定、適応、汎用**。明らかに、これらのすべての特徴を組み合わせたソートアルゴリズムは今日まで見つかっていません。したがって、ソートアルゴリズムを選択する際は、データの特定の特徴と問題の要件に基づいて決定する必要があります。
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価次元に基づいてそれぞれの利点と欠点を分析します。

View File

@@ -0,0 +1,53 @@
---
comments: true
---
# 11.11 &nbsp; まとめ
### 1. &nbsp; 重要な復習
- バブルソートは隣接する要素を交換することで動作します。フラグを追加して早期リターンを可能にすることで、バブルソートの最良ケースの時間計算量を $O(n)$ に最適化できます。
- 挿入ソートは、未ソート区間から要素を取り出してソート済み区間の正しい位置に挿入することで各ラウンドをソートします。挿入ソートの時間計算量は $O(n^2)$ ですが、単位あたりの操作が比較的少ないため、少量のデータのソートでは非常に人気があります。
- クイックソートは歩哨分割操作に基づいています。歩哨分割では、常に最悪のピボットを選ぶ可能性があり、時間計算量が $O(n^2)$ に劣化する可能性があります。中央値やランダムピボットを導入することで、そのような劣化の確率を減らすことができます。末尾再帰は再帰の深さを効果的に減らし、空間計算量を $O(\log n)$ に最適化します。
- マージソートには分割とマージの2つの段階があり、通常分割統治戦略を体現しています。マージソートでは、配列のソートには補助配列の作成が必要で、空間計算量は $O(n)$ になります。しかし、リストのソートの空間計算量は $O(1)$ に最適化できます。
- バケットソートは3つの手順から構成されますデータをバケットに分散、各バケット内でのソート、バケット順での結果のマージ。これも分割統治戦略を体現し、非常に大きなデータセットに適しています。バケットソートの鍵はデータの均等分散です。
- 計数ソートはバケットソートの変形で、各データポイントの出現回数をカウントすることでソートします。計数ソートは限られた範囲のデータを持つ大きなデータセットに適しており、データを正の整数に変換する必要があります。
- 基数ソートは桁ごとにソートすることでデータを処理し、データが固定長の数値として表現される必要があります。
- 全体的に、私たちは高効率、安定性、インプレース操作、適応性を持つソートアルゴリズムを求めています。しかし、他のデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たすソートアルゴリズムは存在しません。実際の応用では、データの特性に基づいて適切なソートアルゴリズムを選択する必要があります。
- 以下の図は、効率性、安定性、インプレース性、適応性の観点から主流のソートアルゴリズムを比較しています。
![ソートアルゴリズムの比較](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" }
<p align="center"> 図 11-19 &nbsp; ソートアルゴリズムの比較 </p>
### 2. &nbsp; Q & A
**Q**: ソートアルゴリズムの安定性はいつ必要ですか?
実際には、オブジェクトの一つの属性に基づいてソートする場合があります。例えば、学生は名前と身長の属性を持ち、多段階ソートを実装することを目指します:最初に名前で `(A, 180) (B, 185) (C, 170) (D, 170)` を取得し、次に身長で。ソートアルゴリズムが不安定なため、`(D, 170) (C, 170) (A, 180) (B, 185)` になってしまう可能性があります。
学生DとCの位置が交換され、名前の順序性が破られているのが分かります。これは望ましくありません。
**Q**: 歩哨分割での「右から左への検索」と「左から右への検索」の順序を交換できますか?
いいえ、最左要素をピボットとして使用する場合、最初に「右から左への検索」を行い、次に「左から右への検索」を行う必要があります。この結論はやや直観に反するので、理由を分析してみましょう。
歩哨分割 `partition()` の最後のステップは `nums[left]``nums[i]` を交換することです。交換後、ピボットの左側の要素はすべてピボット以下になります。**これには最後の交換前に `nums[left] >= nums[i]` が成り立つ必要があります**。「左から右への検索」を最初に行い、ピボットより大きい要素が見つからない場合、**`i == j` でループを終了し、`nums[j] == nums[i] > nums[left]` となる可能性があります**。つまり、最終交換操作はピボットより大きい要素を配列の左端に交換し、歩哨分割を失敗させます。
例えば、配列 `[0, 0, 0, 0, 1]` が与えられた場合、最初に「左から右への検索」を行うと、歩哨分割後の配列は `[1, 0, 0, 0, 0]` となり、これは正しくありません。
さらに考えると、`nums[right]` をピボットとして選択する場合、まったく逆で、最初に「左から右への検索」を行う必要があります。
**Q**: 末尾再帰最適化について、短い配列を選択することで再帰の深さが $\log n$ を超えないことを保証するのはなぜですか?
再帰の深さは現在リターンしていない再帰メソッドの数です。歩哨分割の各ラウンドは元の配列を2つの副配列に分割します。末尾再帰最適化により、再帰的に続行する副配列の長さは最大でも元の配列長の半分です。最悪の場合常に長さを半分にすると仮定すると、最終的な再帰の深さは $\log n$ になります。
元のクイックソートを見直すと、より大きな配列を継続的に再帰処理する可能性があり、最悪の場合 $n$、$n - 1$、...、$2$、$1$ で、再帰の深さは $n$ になります。末尾再帰最適化はこのシナリオを回避できます。
**Q**: 配列のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ ですか?この劣化ケースをどう処理すべきですか?
はい。この状況については、歩哨分割を使用して配列をピボットより小さい、等しい、大きいの3つの部分に分割することを検討してください。小さい部分と大きい部分のみを再帰的に進めます。この方法では、すべての入力要素が等しい配列を1ラウンドの歩哨分割だけでソートできます。
**Q**: なぜバケットソートの最悪ケース時間計算量は $O(n^2)$ ですか?
最悪の場合、すべての要素が同じバケットに配置されます。これらの要素をソートするために $O(n^2)$ アルゴリズムを使用する場合、時間計算量は $O(n^2)$ になります。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
---
comments: true
icon: material/stack-overflow
---
# 第 5 章 &nbsp; スタックとキュー
![スタックとキュー](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" }
!!! abstract
スタックは積み重ねられた猫のようなもので、キューは一列に並んだ猫のようなものです。
それらはそれぞれ、後入先出LIFOと先入先出FIFOの論理関係を表しています。
## 章の内容
- [5.1 &nbsp; スタック](stack.md)
- [5.2 &nbsp; キュー](queue.md)
- [5.3 &nbsp; 両端キュー](deque.md)
- [5.4 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,931 @@
---
comments: true
---
# 5.2 &nbsp; キュー
<u>キュー</u>は、先入先出FIFOルールに従う線形データ構造です。名前が示すように、キューは行列の現象をシミュレートし、新参者は列の後ろに並び、前の人が最初に列を離れます。
下図に示すように、キューの前面を「ヘッド」、後面を「テール」と呼びます。キューの後ろに要素を追加する操作を「エンキュー」、前から要素を削除する操作を「デキュー」と呼びます。
![キューの先入先出ルール](queue.assets/queue_operations.png){ class="animation-figure" }
<p align="center"> 図 5-4 &nbsp; キューの先入先出ルール </p>
## 5.2.1 &nbsp; キューの一般的な操作
キューの一般的な操作を下表に示します。メソッド名はプログラミング言語によって異なる場合があることに注意してください。ここでは、スタックで使用したのと同じ命名規則を使用します。
<p align="center"> 表 5-2 &nbsp; キュー操作の効率 </p>
<div class="center-table" markdown>
| メソッド名 | 説明 | 時間計算量 |
| ----------- | -------------------------------------- | --------------- |
| `push()` | 要素をエンキュー、テールに追加 | $O(1)$ |
| `pop()` | ヘッド要素をデキュー | $O(1)$ |
| `peek()` | ヘッド要素にアクセス | $O(1)$ |
</div>
プログラミング言語で用意されているキュークラスを直接使用できます:
=== "Python"
```python title="queue.py"
from collections import deque
# キューを初期化
# Pythonでは、一般的にdequeクラスをキューとして使用します
# queue.Queue()は純粋なキュークラスですが、使いにくいため推奨されません
que: deque[int] = deque()
# 要素をエンキュー
que.append(1)
que.append(3)
que.append(2)
que.append(5)
que.append(4)
# 最初の要素にアクセス
front: int = que[0]
# 要素をデキュー
pop: int = que.popleft()
# キューの長さを取得
size: int = len(que)
# キューが空かどうかチェック
is_empty: bool = len(que) == 0
```
=== "C++"
```cpp title="queue.cpp"
/* キューを初期化 */
queue<int> queue;
/* 要素をエンキュー */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* 最初の要素にアクセス */
int front = queue.front();
/* 要素をデキュー */
queue.pop();
/* キューの長さを取得 */
int size = queue.size();
/* キューが空かどうかチェック */
bool empty = queue.empty();
```
=== "Java"
```java title="queue.java"
/* キューを初期化 */
Queue<Integer> queue = new LinkedList<>();
/* 要素をエンキュー */
queue.offer(1);
queue.offer(3);
queue.offer(2);
queue.offer(5);
queue.offer(4);
/* 最初の要素にアクセス */
int peek = queue.peek();
/* 要素をデキュー */
int pop = queue.poll();
/* キューの長さを取得 */
int size = queue.size();
/* キューが空かどうかチェック */
boolean isEmpty = queue.isEmpty();
```
=== "C#"
```csharp title="queue.cs"
/* キューを初期化 */
Queue<int> queue = new();
/* 要素をエンキュー */
queue.Enqueue(1);
queue.Enqueue(3);
queue.Enqueue(2);
queue.Enqueue(5);
queue.Enqueue(4);
/* 最初の要素にアクセス */
int peek = queue.Peek();
/* 要素をデキュー */
int pop = queue.Dequeue();
/* キューの長さを取得 */
int size = queue.Count;
/* キューが空かどうかチェック */
bool isEmpty = queue.Count == 0;
```
=== "Go"
```go title="queue_test.go"
/* キューを初期化 */
// Goでは、listをキューとして使用
queue := list.New()
/* 要素をエンキュー */
queue.PushBack(1)
queue.PushBack(3)
queue.PushBack(2)
queue.PushBack(5)
queue.PushBack(4)
/* 最初の要素にアクセス */
peek := queue.Front()
/* 要素をデキュー */
pop := queue.Front()
queue.Remove(pop)
/* キューの長さを取得 */
size := queue.Len()
/* キューが空かどうかチェック */
isEmpty := queue.Len() == 0
```
=== "Swift"
```swift title="queue.swift"
/* キューを初期化 */
// Swiftには組み込みのキュークラスがないため、Arrayをキューとして使用
var queue: [Int] = []
/* 要素をエンキュー */
queue.append(1)
queue.append(3)
queue.append(2)
queue.append(5)
queue.append(4)
/* 最初の要素にアクセス */
let peek = queue.first!
/* 要素をデキュー */
// 配列なので、removeFirstの計算量はO(n)
let pool = queue.removeFirst()
/* キューの長さを取得 */
let size = queue.count
/* キューが空かどうかチェック */
let isEmpty = queue.isEmpty
```
=== "JS"
```javascript title="queue.js"
/* キューを初期化 */
// JavaScriptには組み込みのキューがないため、Arrayをキューとして使用
const queue = [];
/* 要素をエンキュー */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* 最初の要素にアクセス */
const peek = queue[0];
/* 要素をデキュー */
// 基礎構造が配列なので、shift()メソッドの時間計算量はO(n)
const pop = queue.shift();
/* キューの長さを取得 */
const size = queue.length;
/* キューが空かどうかチェック */
const empty = queue.length === 0;
```
=== "TS"
```typescript title="queue.ts"
/* キューを初期化 */
// TypeScriptには組み込みのキューがないため、Arrayをキューとして使用
const queue: number[] = [];
/* 要素をエンキュー */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* 最初の要素にアクセス */
const peek = queue[0];
/* 要素をデキュー */
// 基礎構造が配列なので、shift()メソッドの時間計算量はO(n)
const pop = queue.shift();
/* キューの長さを取得 */
const size = queue.length;
/* キューが空かどうかチェック */
const empty = queue.length === 0;
```
=== "Dart"
```dart title="queue.dart"
/* キューを初期化 */
// DartのQueueクラスは双方向キューですが、キューとして使用できます
Queue<int> queue = Queue();
/* 要素をエンキュー */
queue.add(1);
queue.add(3);
queue.add(2);
queue.add(5);
queue.add(4);
/* 最初の要素にアクセス */
int peek = queue.first;
/* 要素をデキュー */
int pop = queue.removeFirst();
/* キューの長さを取得 */
int size = queue.length;
/* キューが空かどうかチェック */
bool isEmpty = queue.isEmpty;
```
=== "Rust"
```rust title="queue.rs"
/* 双方向キューを初期化 */
// Rustでは、双方向キューを通常のキューとして使用
let mut deque: VecDeque<u32> = VecDeque::new();
/* 要素をエンキュー */
deque.push_back(1);
deque.push_back(3);
deque.push_back(2);
deque.push_back(5);
deque.push_back(4);
/* 最初の要素にアクセス */
if let Some(front) = deque.front() {
}
/* 要素をデキュー */
if let Some(pop) = deque.pop_front() {
}
/* キューの長さを取得 */
let size = deque.len();
/* キューが空かどうかチェック */
let is_empty = deque.is_empty();
```
=== "C"
```c title="queue.c"
// Cは組み込みのキューを提供していません
```
=== "Kotlin"
```kotlin title="queue.kt"
```
=== "Zig"
```zig title="queue.zig"
```
## 5.2.2 &nbsp; キューの実装
キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列の両方がこの要件を満たします。
### 1. &nbsp; 連結リストベースの実装
下図に示すように、連結リストの「ヘッドノード」と「テールノード」をそれぞれキューの「フロント」と「リア」と考えることができます。ノードは後ろでのみ追加でき、前でのみ削除できるように規定されています。
=== "LinkedListQueue"
![連結リストによるキュー実装のエンキューとデキュー操作](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" }
=== "push()"
![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" }
=== "pop()"
![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" }
<p align="center"> 図 5-5 &nbsp; 連結リストによるキュー実装のエンキューとデキュー操作 </p>
以下は、連結リストを使用してキューを実装するコードです:
=== "Python"
```python title="linkedlist_queue.py"
class LinkedListQueue:
"""連結リストベースのキュークラス"""
def __init__(self):
"""コンストラクタ"""
self._front: ListNode | None = None # ヘッドノード front
self._rear: ListNode | None = None # テールノード rear
self._size: int = 0
def size(self) -> int:
"""キューの長さを取得"""
return self._size
def is_empty(self) -> bool:
"""キューが空かどうかを判定"""
return self._size == 0
def push(self, num: int):
"""エンキュー"""
# テールノードの後ろに num を追加
node = ListNode(num)
# キューが空の場合、ヘッドとテールノードの両方をそのノードに向ける
if self._front is None:
self._front = node
self._rear = node
# キューが空でない場合、そのノードをテールノードの後ろに追加
else:
self._rear.next = node
self._rear = node
self._size += 1
def pop(self) -> int:
"""デキュー"""
num = self.peek()
# ヘッドノードを削除
self._front = self._front.next
self._size -= 1
return num
def peek(self) -> int:
"""フロント要素にアクセス"""
if self.is_empty():
raise IndexError("Queue is empty")
return self._front.val
def to_list(self) -> list[int]:
"""出力用のリストに変換"""
queue = []
temp = self._front
while temp:
queue.append(temp.val)
temp = temp.next
return queue
```
=== "C++"
```cpp title="linkedlist_queue.cpp"
/* 連結リストに基づくキュークラス */
class LinkedListQueue {
private:
ListNode *front, *rear; // 先頭ードfront、末尾ードrear
int queSize;
public:
LinkedListQueue() {
front = nullptr;
rear = nullptr;
queSize = 0;
}
~LinkedListQueue() {
// 連結リストを走査、ノードを削除、メモリを解放
freeMemoryLinkedList(front);
}
/* キューの長さを取得 */
int size() {
return queSize;
}
/* キューが空かどうかを判定 */
bool isEmpty() {
return queSize == 0;
}
/* エンキュー */
void push(int num) {
// 末尾ードの後ろにnumを追加
ListNode *node = new ListNode(num);
// キューが空の場合、先頭と末尾ノードの両方をそのノードに向ける
if (front == nullptr) {
front = node;
rear = node;
}
// キューが空でない場合、そのノードを末尾ノードの後ろに追加
else {
rear->next = node;
rear = node;
}
queSize++;
}
/* デキュー */
int pop() {
int num = peek();
// 先頭ノードを削除
ListNode *tmp = front;
front = front->next;
// メモリを解放
delete tmp;
queSize--;
return num;
}
/* 先頭要素にアクセス */
int peek() {
if (size() == 0)
throw out_of_range("Queue is empty");
return front->val;
}
/* 連結リストをVectorに変換して返却 */
vector<int> toVector() {
ListNode *node = front;
vector<int> res(size());
for (int i = 0; i < res.size(); i++) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
```
=== "Java"
```java title="linkedlist_queue.java"
/* 連結リストに基づくキュークラス */
class LinkedListQueue {
private ListNode front, rear; // 先頭ノード front、末尾ード rear
private int queSize = 0;
public LinkedListQueue() {
front = null;
rear = null;
}
/* キューの長さを取得 */
public int size() {
return queSize;
}
/* キューが空かどうかを判定 */
public boolean isEmpty() {
return size() == 0;
}
/* エンキュー */
public void push(int num) {
// 末尾ノードの後ろに num を追加
ListNode node = new ListNode(num);
// キューが空の場合、先頭と末尾ノードの両方をそのノードにポイント
if (front == null) {
front = node;
rear = node;
// キューが空でない場合、そのノードを末尾ノードの後ろに追加
} else {
rear.next = node;
rear = node;
}
queSize++;
}
/* デキュー */
public int pop() {
int num = peek();
// 先頭ノードを削除
front = front.next;
queSize--;
return num;
}
/* 先頭要素にアクセス */
public int peek() {
if (isEmpty())
throw new IndexOutOfBoundsException();
return front.val;
}
/* 連結リストを配列に変換して返す */
public int[] toArray() {
ListNode node = front;
int[] res = new int[size()];
for (int i = 0; i < res.length; i++) {
res[i] = node.val;
node = node.next;
}
return res;
}
}
```
=== "C#"
```csharp title="linkedlist_queue.cs"
[class]{LinkedListQueue}-[func]{}
```
=== "Go"
```go title="linkedlist_queue.go"
[class]{linkedListQueue}-[func]{}
```
=== "Swift"
```swift title="linkedlist_queue.swift"
[class]{LinkedListQueue}-[func]{}
```
=== "JS"
```javascript title="linkedlist_queue.js"
[class]{LinkedListQueue}-[func]{}
```
=== "TS"
```typescript title="linkedlist_queue.ts"
[class]{LinkedListQueue}-[func]{}
```
=== "Dart"
```dart title="linkedlist_queue.dart"
[class]{LinkedListQueue}-[func]{}
```
=== "Rust"
```rust title="linkedlist_queue.rs"
[class]{LinkedListQueue}-[func]{}
```
=== "C"
```c title="linkedlist_queue.c"
[class]{LinkedListQueue}-[func]{}
```
=== "Kotlin"
```kotlin title="linkedlist_queue.kt"
[class]{LinkedListQueue}-[func]{}
```
=== "Ruby"
```ruby title="linkedlist_queue.rb"
[class]{LinkedListQueue}-[func]{}
```
=== "Zig"
```zig title="linkedlist_queue.zig"
[class]{LinkedListQueue}-[func]{}
```
### 2. &nbsp; 配列ベースの実装
配列の最初の要素を削除する時間計算量は$O(n)$で、デキュー操作が非効率になります。しかし、この問題は以下のように巧妙に回避できます。
変数`front`を使用してフロント要素のインデックスを示し、変数`size`を維持してキューの長さを記録します。`rear = front + size`を定義し、これはテール要素の直後の位置を指します。
この設計により、**配列内の要素の有効な間隔は`[front, rear - 1]`です**。各操作の実装方法を下図に示します。
- エンキュー操作:入力要素を`rear`インデックスに割り当て、`size`を1増加させます。
- デキュー操作:単に`front`を1増加させ、`size`を1減少させます。
エンキューとデキュー操作は両方とも単一の操作のみを必要とし、それぞれの時間計算量は$O(1)$です。
=== "ArrayQueue"
![配列によるキュー実装のエンキューとデキュー操作](queue.assets/array_queue_step1.png){ class="animation-figure" }
=== "push()"
![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" }
=== "pop()"
![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" }
<p align="center"> 図 5-6 &nbsp; 配列によるキュー実装のエンキューとデキュー操作 </p>
問題に気づくかもしれません:エンキューとデキュー操作が継続的に実行されると、`front`と`rear`の両方が右に移動し、**最終的に配列の末尾に到達してそれ以上移動できなくなります**。これを解決するために、配列を「循環配列」として扱い、配列の末尾を先頭に接続します。
循環配列では、`front`または`rear`が末尾に到達すると、配列の先頭にループバックする必要があります。この循環パターンは、以下のコードに示すように「剰余演算」で実現できます:
=== "Python"
```python title="array_queue.py"
class ArrayQueue:
"""循環配列ベースのキュークラス"""
def __init__(self, size: int):
"""コンストラクタ"""
self._nums: list[int] = [0] * size # キュー要素を格納する配列
self._front: int = 0 # フロントポインタ、フロント要素を指す
self._size: int = 0 # キューの長さ
def capacity(self) -> int:
"""キューの容量を取得"""
return len(self._nums)
def size(self) -> int:
"""キューの長さを取得"""
return self._size
def is_empty(self) -> bool:
"""キューが空かどうかを判定"""
return self._size == 0
def push(self, num: int):
"""エンキュー"""
if self._size == self.capacity():
raise IndexError("Queue is full")
# リアポインタを計算、リアインデックス + 1 を指す
# モジュロ演算を使用してリアポインタを配列の末尾から先頭に戻す
rear: int = (self._front + self._size) % self.capacity()
# num をリアに追加
self._nums[rear] = num
self._size += 1
def pop(self) -> int:
"""デキュー"""
num: int = self.peek()
# フロントポインタを1つ後ろに移動、末尾を超えた場合は配列の先頭に戻る
self._front = (self._front + 1) % self.capacity()
self._size -= 1
return num
def peek(self) -> int:
"""フロント要素にアクセス"""
if self.is_empty():
raise IndexError("Queue is empty")
return self._nums[self._front]
def to_list(self) -> list[int]:
"""出力用の配列を返す"""
res = [0] * self.size()
j: int = self._front
for i in range(self.size()):
res[i] = self._nums[(j % self.capacity())]
j += 1
return res
```
=== "C++"
```cpp title="array_queue.cpp"
/* 循環配列に基づくキュークラス */
class ArrayQueue {
private:
int *nums; // キュー要素を格納する配列
int front; // 先頭ポインタ、先頭要素を指す
int queSize; // キューの長さ
int queCapacity; // キューの容量
public:
ArrayQueue(int capacity) {
// 配列を初期化
nums = new int[capacity];
queCapacity = capacity;
front = queSize = 0;
}
~ArrayQueue() {
delete[] nums;
}
/* キューの容量を取得 */
int capacity() {
return queCapacity;
}
/* キューの長さを取得 */
int size() {
return queSize;
}
/* キューが空かどうかを判定 */
bool isEmpty() {
return size() == 0;
}
/* エンキュー */
void push(int num) {
if (queSize == queCapacity) {
cout << "Queue is full" << endl;
return;
}
// 末尾ポインタを計算、末尾インデックス + 1を指す
// 剰余演算を使用して末尾ポインタが配列の末尾から先頭に戻るようにラップ
int rear = (front + queSize) % queCapacity;
// numを末尾に追加
nums[rear] = num;
queSize++;
}
/* デキュー */
int pop() {
int num = peek();
// 先頭ポインタを1つ後ろに移動、末尾を超えた場合は配列の先頭に戻る
front = (front + 1) % queCapacity;
queSize--;
return num;
}
/* 先頭要素にアクセス */
int peek() {
if (isEmpty())
throw out_of_range("Queue is empty");
return nums[front];
}
/* 配列をVectorに変換して返却 */
vector<int> toVector() {
// 有効な長さ範囲内の要素のみを変換
vector<int> arr(queSize);
for (int i = 0, j = front; i < queSize; i++, j++) {
arr[i] = nums[j % queCapacity];
}
return arr;
}
};
```
=== "Java"
```java title="array_queue.java"
/* 配列に基づくキュークラス */
class ArrayQueue {
private int[] nums; // 要素を格納する配列
private int front; // キューヘッドポインタ、最初の要素を指す
private int queSize; // キューの長さ
public ArrayQueue(int capacity) {
nums = new int[capacity];
front = queSize = 0;
}
/* キューの容量を取得 */
public int capacity() {
return nums.length;
}
/* キューの長さを取得 */
public int size() {
return queSize;
}
/* キューが空かどうかを判定 */
public boolean isEmpty() {
return queSize == 0;
}
/* エンキュー */
public void push(int num) {
if (queSize == capacity()) {
System.out.println("キューが満杯です");
return;
}
// リアポインタを計算front + queSize
// モジュロ操作により rear が配列の長さを超えることを回避
int rear = (front + queSize) % capacity();
// 要素をキューリアに追加
nums[rear] = num;
queSize++;
}
/* デキュー */
public int pop() {
int num = peek();
// キューヘッドポインタを後ろに1つ移動、モジュロ操作により範囲を超えることを回避
front = (front + 1) % capacity();
queSize--;
return num;
}
/* キューヘッド要素にアクセス */
public int peek() {
if (isEmpty())
throw new IndexOutOfBoundsException();
return nums[front];
}
/* 配列を返す */
public int[] toArray() {
// front から開始して queSize 個の要素のみをコピー
int[] res = new int[queSize];
for (int i = 0, j = front; i < queSize; i++, j++) {
res[i] = nums[j % capacity()];
}
return res;
}
}
```
=== "C#"
```csharp title="array_queue.cs"
[class]{ArrayQueue}-[func]{}
```
=== "Go"
```go title="array_queue.go"
[class]{arrayQueue}-[func]{}
```
=== "Swift"
```swift title="array_queue.swift"
[class]{ArrayQueue}-[func]{}
```
=== "JS"
```javascript title="array_queue.js"
[class]{ArrayQueue}-[func]{}
```
=== "TS"
```typescript title="array_queue.ts"
[class]{ArrayQueue}-[func]{}
```
=== "Dart"
```dart title="array_queue.dart"
[class]{ArrayQueue}-[func]{}
```
=== "Rust"
```rust title="array_queue.rs"
[class]{ArrayQueue}-[func]{}
```
=== "C"
```c title="array_queue.c"
[class]{ArrayQueue}-[func]{}
```
=== "Kotlin"
```kotlin title="array_queue.kt"
[class]{ArrayQueue}-[func]{}
```
=== "Ruby"
```ruby title="array_queue.rb"
[class]{ArrayQueue}-[func]{}
```
=== "Zig"
```zig title="array_queue.zig"
[class]{ArrayQueue}-[func]{}
```
上記のキュー実装にはまだ制限があります:長さが固定されています。しかし、この問題は解決が困難ではありません。配列を必要に応じて自動拡張できる動的配列に置き換えることができます。興味のある読者は自分で実装してみてください。
2つの実装の比較はスタックの場合と一貫しており、ここでは繰り返しません。
## 5.2.3 &nbsp; キューの典型的な応用
- **Amazonの注文**:買い物客が注文を行った後、これらの注文はキューに参加し、システムは順番に処理します。独身の日などのイベント中は、短時間で大量の注文が生成され、高い同時実行性がエンジニアにとって重要な課題となります。
- **様々なToDoリスト**:「先着順」機能が必要なシナリオ、例えばプリンターのタスクキューやレストランの配達キューなど、キューで処理順序を効果的に維持できます。

View File

@@ -0,0 +1,832 @@
---
comments: true
---
# 5.1 &nbsp; スタック
<u>スタック</u>は、後入先出LIFOの原則に従う線形データ構造です。
スタックをテーブル上の皿の山に例えることができます。底の皿にアクセスするには、まず上の皿を取り除く必要があります。皿を様々な種類の要素(整数、文字、オブジェクトなど)に置き換えることで、スタックと呼ばれるデータ構造を得ることができます。
下図に示すように、要素の山の上部を「スタックのトップ」、下部を「スタックのボトム」と呼びます。スタックのトップに要素を追加する操作を「プッシュ」、トップ要素を削除する操作を「ポップ」と呼びます。
![スタックの後入先出ルール](stack.assets/stack_operations.png){ class="animation-figure" }
<p align="center"> 図 5-1 &nbsp; スタックの後入先出ルール </p>
## 5.1.1 &nbsp; スタックの一般的な操作
スタックの一般的な操作を下表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、例として`push()``pop()``peek()`を使用します。
<p align="center"> 表 5-1 &nbsp; スタック操作の効率 </p>
<div class="center-table" markdown>
| メソッド | 説明 | 時間計算量 |
| -------- | ----------------------------------------------- | --------------- |
| `push()` | 要素をスタックにプッシュ(トップに追加) | $O(1)$ |
| `pop()` | スタックからトップ要素をポップ | $O(1)$ |
| `peek()` | スタックのトップ要素にアクセス | $O(1)$ |
</div>
通常、プログラミング言語に組み込まれているスタッククラスを直接使用できます。ただし、一部の言語では具体的にスタッククラスを提供していない場合があります。これらの場合、言語の「配列」または「連結リスト」をスタックとして使用し、プログラムでスタックロジックに関連しない操作を無視できます。
=== "Python"
```python title="stack.py"
# スタックを初期化
# Pythonには組み込みのスタッククラスがないため、listをスタックとして使用
stack: list[int] = []
# 要素をスタックにプッシュ
stack.append(1)
stack.append(3)
stack.append(2)
stack.append(5)
stack.append(4)
# スタックのトップ要素にアクセス
peek: int = stack[-1]
# スタックから要素をポップ
pop: int = stack.pop()
# スタックの長さを取得
size: int = len(stack)
# スタックが空かどうかチェック
is_empty: bool = len(stack) == 0
```
=== "C++"
```cpp title="stack.cpp"
/* スタックを初期化 */
stack<int> stack;
/* 要素をスタックにプッシュ */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* スタックのトップ要素にアクセス */
int top = stack.top();
/* スタックから要素をポップ */
stack.pop(); // 戻り値なし
/* スタックの長さを取得 */
int size = stack.size();
/* スタックが空かどうかチェック */
bool empty = stack.empty();
```
=== "Java"
```java title="stack.java"
/* スタックを初期化 */
Stack<Integer> stack = new Stack<>();
/* 要素をスタックにプッシュ */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* スタックのトップ要素にアクセス */
int peek = stack.peek();
/* スタックから要素をポップ */
int pop = stack.pop();
/* スタックの長さを取得 */
int size = stack.size();
/* スタックが空かどうかチェック */
boolean isEmpty = stack.isEmpty();
```
=== "C#"
```csharp title="stack.cs"
/* スタックを初期化 */
Stack<int> stack = new();
/* 要素をスタックにプッシュ */
stack.Push(1);
stack.Push(3);
stack.Push(2);
stack.Push(5);
stack.Push(4);
/* スタックのトップ要素にアクセス */
int peek = stack.Peek();
/* スタックから要素をポップ */
int pop = stack.Pop();
/* スタックの長さを取得 */
int size = stack.Count;
/* スタックが空かどうかチェック */
bool isEmpty = stack.Count == 0;
```
=== "Go"
```go title="stack_test.go"
/* スタックを初期化 */
// Goでは、Sliceをスタックとして使用することが推奨されます
var stack []int
/* 要素をスタックにプッシュ */
stack = append(stack, 1)
stack = append(stack, 3)
stack = append(stack, 2)
stack = append(stack, 5)
stack = append(stack, 4)
/* スタックのトップ要素にアクセス */
peek := stack[len(stack)-1]
/* スタックから要素をポップ */
pop := stack[len(stack)-1]
stack = stack[:len(stack)-1]
/* スタックの長さを取得 */
size := len(stack)
/* スタックが空かどうかチェック */
isEmpty := len(stack) == 0
```
=== "Swift"
```swift title="stack.swift"
/* スタックを初期化 */
// Swiftには組み込みのスタッククラスがないため、Arrayをスタックとして使用
var stack: [Int] = []
/* 要素をスタックにプッシュ */
stack.append(1)
stack.append(3)
stack.append(2)
stack.append(5)
stack.append(4)
/* スタックのトップ要素にアクセス */
let peek = stack.last!
/* スタックから要素をポップ */
let pop = stack.removeLast()
/* スタックの長さを取得 */
let size = stack.count
/* スタックが空かどうかチェック */
let isEmpty = stack.isEmpty
```
=== "JS"
```javascript title="stack.js"
/* スタックを初期化 */
// JavaScriptには組み込みのスタッククラスがないため、Arrayをスタックとして使用
const stack = [];
/* 要素をスタックにプッシュ */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* スタックのトップ要素にアクセス */
const peek = stack[stack.length-1];
/* スタックから要素をポップ */
const pop = stack.pop();
/* スタックの長さを取得 */
const size = stack.length;
/* スタックが空かどうかチェック */
const is_empty = stack.length === 0;
```
=== "TS"
```typescript title="stack.ts"
/* スタックを初期化 */
// TypeScriptには組み込みのスタッククラスがないため、Arrayをスタックとして使用
const stack: number[] = [];
/* 要素をスタックにプッシュ */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* スタックのトップ要素にアクセス */
const peek = stack[stack.length - 1];
/* スタックから要素をポップ */
const pop = stack.pop();
/* スタックの長さを取得 */
const size = stack.length;
/* スタックが空かどうかチェック */
const is_empty = stack.length === 0;
```
=== "Dart"
```dart title="stack.dart"
/* スタックを初期化 */
// Dartには組み込みのスタッククラスがないため、Listをスタックとして使用
List<int> stack = [];
/* 要素をスタックにプッシュ */
stack.add(1);
stack.add(3);
stack.add(2);
stack.add(5);
stack.add(4);
/* スタックのトップ要素にアクセス */
int peek = stack.last;
/* スタックから要素をポップ */
int pop = stack.removeLast();
/* スタックの長さを取得 */
int size = stack.length;
/* スタックが空かどうかチェック */
bool isEmpty = stack.isEmpty;
```
=== "Rust"
```rust title="stack.rs"
/* スタックを初期化 */
// Vecをスタックとして使用
let mut stack: Vec<i32> = Vec::new();
/* 要素をスタックにプッシュ */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* スタックのトップ要素にアクセス */
let top = stack.last().unwrap();
/* スタックから要素をポップ */
let pop = stack.pop().unwrap();
/* スタックの長さを取得 */
let size = stack.len();
/* スタックが空かどうかチェック */
let is_empty = stack.is_empty();
```
=== "C"
```c title="stack.c"
// Cは組み込みのスタックを提供していません
```
=== "Kotlin"
```kotlin title="stack.kt"
```
=== "Zig"
```zig title="stack.zig"
```
## 5.1.2 &nbsp; スタックの実装
スタックがどのように動作するかをより深く理解するために、自分でスタッククラスを実装してみましょう。
スタックは後入先出の原則に従うため、スタックのトップでのみ要素を追加または削除できます。しかし、配列と連結リストの両方は任意の位置で要素を追加・削除できるため、**スタックは制限された配列または連結リストと見なすことができます**。言い換えれば、配列や連結リストの特定の無関係な操作を「遮蔽」して、外部の動作をスタックの特性に合わせることができます。
### 1. &nbsp; 連結リストベースの実装
連結リストを使用してスタックを実装する場合、リストのヘッドノードをスタックのトップ、テールノードをスタックのボトムと考えることができます。
下図に示すように、プッシュ操作では、単に連結リストのヘッドに要素を挿入します。このノード挿入方法は「ヘッド挿入」として知られています。ポップ操作では、リストからヘッドノードを削除するだけです。
=== "LinkedListStack"
![連結リストによるスタック実装のプッシュとポップ操作](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" }
=== "push()"
![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" }
=== "pop()"
![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" }
<p align="center"> 図 5-2 &nbsp; 連結リストによるスタック実装のプッシュとポップ操作 </p>
以下は、連結リストに基づくスタック実装のサンプルコードです:
=== "Python"
```python title="linkedlist_stack.py"
class LinkedListStack:
"""連結リストベースのスタッククラス"""
def __init__(self):
"""コンストラクタ"""
self._peek: ListNode | None = None
self._size: int = 0
def size(self) -> int:
"""スタックの長さを取得"""
return self._size
def is_empty(self) -> bool:
"""スタックが空かどうかを判定"""
return self._size == 0
def push(self, val: int):
"""プッシュ"""
node = ListNode(val)
node.next = self._peek
self._peek = node
self._size += 1
def pop(self) -> int:
"""ポップ"""
num = self.peek()
self._peek = self._peek.next
self._size -= 1
return num
def peek(self) -> int:
"""スタックトップ要素にアクセス"""
if self.is_empty():
raise IndexError("Stack is empty")
return self._peek.val
def to_list(self) -> list[int]:
"""出力用のリストに変換"""
arr = []
node = self._peek
while node:
arr.append(node.val)
node = node.next
arr.reverse()
return arr
```
=== "C++"
```cpp title="linkedlist_stack.cpp"
/* 連結リストに基づくスタッククラス */
class LinkedListStack {
private:
ListNode *stackTop; // 先頭ノードをスタックトップとして使用
int stkSize; // スタックの長さ
public:
LinkedListStack() {
stackTop = nullptr;
stkSize = 0;
}
~LinkedListStack() {
// 連結リストを走査、ノードを削除、メモリを解放
freeMemoryLinkedList(stackTop);
}
/* スタックの長さを取得 */
int size() {
return stkSize;
}
/* スタックが空かどうかを判定 */
bool isEmpty() {
return size() == 0;
}
/* プッシュ */
void push(int num) {
ListNode *node = new ListNode(num);
node->next = stackTop;
stackTop = node;
stkSize++;
}
/* ポップ */
int pop() {
int num = top();
ListNode *tmp = stackTop;
stackTop = stackTop->next;
// メモリを解放
delete tmp;
stkSize--;
return num;
}
/* スタックトップ要素にアクセス */
int top() {
if (isEmpty())
throw out_of_range("Stack is empty");
return stackTop->val;
}
/* リストを配列に変換して返却 */
vector<int> toVector() {
ListNode *node = stackTop;
vector<int> res(size());
for (int i = res.size() - 1; i >= 0; i--) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
```
=== "Java"
```java title="linkedlist_stack.java"
/* 連結リストに基づくスタッククラス */
class LinkedListStack {
private ListNode stackPeek; // ヘッドノードをスタックトップとして使用
private int stkSize = 0; // スタックの長さ
public LinkedListStack() {
stackPeek = null;
}
/* スタックの長さを取得 */
public int size() {
return stkSize;
}
/* スタックが空かどうかを判定 */
public boolean isEmpty() {
return size() == 0;
}
/* プッシュ */
public void push(int num) {
ListNode node = new ListNode(num);
node.next = stackPeek;
stackPeek = node;
stkSize++;
}
/* ポップ */
public int pop() {
int num = peek();
stackPeek = stackPeek.next;
stkSize--;
return num;
}
/* スタックトップ要素にアクセス */
public int peek() {
if (isEmpty())
throw new IndexOutOfBoundsException();
return stackPeek.val;
}
/* List を Array に変換して返す */
public int[] toArray() {
ListNode node = stackPeek;
int[] res = new int[size()];
for (int i = res.length - 1; i >= 0; i--) {
res[i] = node.val;
node = node.next;
}
return res;
}
}
```
=== "C#"
```csharp title="linkedlist_stack.cs"
[class]{LinkedListStack}-[func]{}
```
=== "Go"
```go title="linkedlist_stack.go"
[class]{linkedListStack}-[func]{}
```
=== "Swift"
```swift title="linkedlist_stack.swift"
[class]{LinkedListStack}-[func]{}
```
=== "JS"
```javascript title="linkedlist_stack.js"
[class]{LinkedListStack}-[func]{}
```
=== "TS"
```typescript title="linkedlist_stack.ts"
[class]{LinkedListStack}-[func]{}
```
=== "Dart"
```dart title="linkedlist_stack.dart"
[class]{LinkedListStack}-[func]{}
```
=== "Rust"
```rust title="linkedlist_stack.rs"
[class]{LinkedListStack}-[func]{}
```
=== "C"
```c title="linkedlist_stack.c"
[class]{LinkedListStack}-[func]{}
```
=== "Kotlin"
```kotlin title="linkedlist_stack.kt"
[class]{LinkedListStack}-[func]{}
```
=== "Ruby"
```ruby title="linkedlist_stack.rb"
[class]{LinkedListStack}-[func]{}
```
=== "Zig"
```zig title="linkedlist_stack.zig"
[class]{LinkedListStack}-[func]{}
```
### 2. &nbsp; 配列ベースの実装
配列を使用してスタックを実装する場合、配列の末尾をスタックのトップと考えることができます。下図に示すように、プッシュとポップ操作は、それぞれ配列の末尾での要素の追加と削除に対応し、どちらも時間計算量$O(1)$です。
=== "ArrayStack"
![配列によるスタック実装のプッシュとポップ操作](stack.assets/array_stack_step1.png){ class="animation-figure" }
=== "push()"
![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" }
=== "pop()"
![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" }
<p align="center"> 図 5-3 &nbsp; 配列によるスタック実装のプッシュとポップ操作 </p>
スタックにプッシュされる要素が継続的に増加する可能性があるため、動的配列を使用でき、配列拡張を自分で処理する必要がありません。以下はサンプルコードです:
=== "Python"
```python title="array_stack.py"
class ArrayStack:
"""配列ベースのスタッククラス"""
def __init__(self):
"""コンストラクタ"""
self._stack: list[int] = []
def size(self) -> int:
"""スタックの長さを取得"""
return len(self._stack)
def is_empty(self) -> bool:
"""スタックが空かどうかを判定"""
return self.size() == 0
def push(self, item: int):
"""プッシュ"""
self._stack.append(item)
def pop(self) -> int:
"""ポップ"""
if self.is_empty():
raise IndexError("Stack is empty")
return self._stack.pop()
def peek(self) -> int:
"""スタックトップ要素にアクセス"""
if self.is_empty():
raise IndexError("Stack is empty")
return self._stack[-1]
def to_list(self) -> list[int]:
"""出力用の配列を返す"""
return self._stack
```
=== "C++"
```cpp title="array_stack.cpp"
/* 配列に基づくスタッククラス */
class ArrayStack {
private:
vector<int> stack;
public:
/* スタックの長さを取得 */
int size() {
return stack.size();
}
/* スタックが空かどうかを判定 */
bool isEmpty() {
return stack.size() == 0;
}
/* プッシュ */
void push(int num) {
stack.push_back(num);
}
/* ポップ */
int pop() {
int num = top();
stack.pop_back();
return num;
}
/* スタックトップ要素にアクセス */
int top() {
if (isEmpty())
throw out_of_range("Stack is empty");
return stack.back();
}
/* Vectorを返却 */
vector<int> toVector() {
return stack;
}
};
```
=== "Java"
```java title="array_stack.java"
/* 配列に基づくスタッククラス */
class ArrayStack {
private ArrayList<Integer> stack;
public ArrayStack() {
// リスト(動的配列)を初期化
stack = new ArrayList<>();
}
/* スタックの長さを取得 */
public int size() {
return stack.size();
}
/* スタックが空かどうかを判定 */
public boolean isEmpty() {
return size() == 0;
}
/* プッシュ */
public void push(int num) {
stack.add(num);
}
/* ポップ */
public int pop() {
if (isEmpty())
throw new IndexOutOfBoundsException();
return stack.remove(size() - 1);
}
/* スタックトップ要素にアクセス */
public int peek() {
if (isEmpty())
throw new IndexOutOfBoundsException();
return stack.get(size() - 1);
}
/* List を Array に変換して返す */
public Object[] toArray() {
return stack.toArray();
}
}
```
=== "C#"
```csharp title="array_stack.cs"
[class]{ArrayStack}-[func]{}
```
=== "Go"
```go title="array_stack.go"
[class]{arrayStack}-[func]{}
```
=== "Swift"
```swift title="array_stack.swift"
[class]{ArrayStack}-[func]{}
```
=== "JS"
```javascript title="array_stack.js"
[class]{ArrayStack}-[func]{}
```
=== "TS"
```typescript title="array_stack.ts"
[class]{ArrayStack}-[func]{}
```
=== "Dart"
```dart title="array_stack.dart"
[class]{ArrayStack}-[func]{}
```
=== "Rust"
```rust title="array_stack.rs"
[class]{ArrayStack}-[func]{}
```
=== "C"
```c title="array_stack.c"
[class]{ArrayStack}-[func]{}
```
=== "Kotlin"
```kotlin title="array_stack.kt"
[class]{ArrayStack}-[func]{}
```
=== "Ruby"
```ruby title="array_stack.rb"
[class]{ArrayStack}-[func]{}
```
=== "Zig"
```zig title="array_stack.zig"
[class]{ArrayStack}-[func]{}
```
## 5.1.3 &nbsp; 2つの実装の比較
**サポートされる操作**
両方の実装は、スタックで定義されたすべての操作をサポートします。配列実装はさらにランダムアクセスをサポートしますが、これはスタック定義の範囲を超えており、一般的には使用されません。
**時間効率**
配列ベースの実装では、プッシュとポップ操作の両方が事前に割り当てられた連続メモリで発生し、良好なキャッシュ局所性があるため効率が高くなります。しかし、プッシュ操作が配列容量を超える場合、リサイズメカニズムがトリガーされ、そのプッシュ操作の時間計算量は$O(n)$になります。
連結リスト実装では、リスト拡張は非常に柔軟で、配列拡張のような効率低下の問題はありません。しかし、プッシュ操作にはノードオブジェクトの初期化とポインタの変更が必要なため、効率は比較的低くなります。プッシュされる要素がすでにノードオブジェクトの場合、初期化ステップをスキップでき、効率が向上します。
したがって、プッシュとポップ操作の要素が`int`や`double`などの基本データ型の場合、以下の結論を導くことができます:
- 配列ベースのスタック実装は拡張時に効率が低下しますが、拡張は低頻度操作であるため、平均効率は高くなります。
- 連結リストベースのスタック実装はより安定した効率パフォーマンスを提供します。
**空間効率**
リストを初期化する際、システムは「初期容量」を割り当てますが、これは実際の必要量を超える可能性があります。さらに、拡張メカニズムは通常、特定の係数2倍などで容量を増加させ、これも実際の必要量を超える可能性があります。したがって、**配列ベースのスタックは一部の空間を無駄にする可能性があります**。
しかし、連結リストノードはポインタを格納するための追加空間が必要なため、**連結リストノードが占有する空間は比較的大きくなります**。
まとめると、どちらの実装がよりメモリ効率的かを単純に判断することはできません。特定の状況に基づく分析が必要です。
## 5.1.4 &nbsp; スタックの典型的な応用
- **ブラウザの戻ると進む、ソフトウェアの元に戻すとやり直し**。新しいWebページを開くたびに、ブラウザは前のページをスタックにプッシュし、戻る操作本質的にはポップ操作を通じて前のページに戻ることができます。戻ると進むの両方をサポートするには、2つのスタックが連携して動作する必要があります。
- **プログラムのメモリ管理**。関数が呼び出されるたびに、システムはスタックのトップにスタックフレームを追加して関数のコンテキスト情報を記録します。再帰関数では、下方向の再帰フェーズはスタックへのプッシュを続け、上方向のバックトラッキングフェーズはスタックからのポップを続けます。

View File

@@ -0,0 +1,35 @@
---
comments: true
---
# 5.4 &nbsp; まとめ
### 1. &nbsp; 重要なポイント
- スタックは後入れ先出しLIFOの原則に従うデータ構造で、配列または連結リストを使って実装できます。
- 時間効率の観点では、スタックの配列実装の方が平均的な効率が高いです。ただし、拡張時には単一のプッシュ操作の時間計算量が$O(n)$に悪化する可能性があります。対照的に、スタックの連結リスト実装はより安定した効率を提供します。
- 空間効率に関しては、スタックの配列実装は一定程度の空間の無駄につながる可能性があります。ただし、連結リストのノードが占有するメモリ空間は一般的に配列の要素よりも大きいことに注意することが重要です。
- キューは先入れ先出しFIFOの原則に従うデータ構造で、同様に配列または連結リストを使って実装できます。キューの時間と空間効率に関する結論は、スタックと似ています。
- 両端キューdequeはより柔軟なキューの種類で、両端での要素の追加と削除を可能にします。
### 2. &nbsp; Q & A
**Q**: ブラウザの進む・戻る機能は双方向連結リストで実装されているのですか?
ブラウザの進む・戻るナビゲーションは本質的に「スタック」概念の現れです。ユーザーが新しいページを訪問すると、そのページがスタックの先頭に追加されます。戻るボタンをクリックすると、ページがスタックの先頭からポップされます。両端キューdequeは、「両端キュー」の章で述べたように、いくつかの追加操作を便利に実装できます。
**Q**: スタックからポップした後、ポップされたノードのメモリを解放する必要がありますか?
ポップされたードが後で使用される場合は、そのメモリを解放する必要はありません。自動ガベージコレクションを持つJavaやPythonなどの言語では、手動のメモリ解放は必要ありません。CやC++では、手動のメモリ解放が必要です。
**Q**: 両端キューは2つのスタックを結合したもののように見えます。その用途は何ですか
両端キューは、スタックとキューの組み合わせまたは2つのスタックを結合したもので、スタックとキューの両方のロジックを示します。したがって、スタックとキューのすべてのアプリケーションを実装でき、より大きな柔軟性を提供します。
**Q**: 元に戻すとやり直しは具体的にどのように実装されるのですか?
元に戻すとやり直しの操作は2つのスタックを使って実装されます元に戻す用のスタック`A`とやり直し用のスタック`B`です。
1. ユーザーが操作を実行するたびに、それがスタック`A`にプッシュされ、スタック`B`がクリアされます。
2. ユーザーが「元に戻す」を実行すると、最新の操作がスタック`A`からポップされ、スタック`B`にプッシュされます。
3. ユーザーが「やり直し」を実行すると、最新の操作がスタック`B`からポップされ、スタック`A`に戻されます。

View File

@@ -0,0 +1,501 @@
---
comments: true
---
# 7.3 &nbsp; 二分木の配列表現
連結リスト表現では、二分木の格納単位はノード`TreeNode`であり、ノードはポインタによって接続されます。連結リスト表現での二分木の基本操作については前の節で紹介しました。
では、配列を使って二分木を表現することはできるでしょうか?答えはイエスです。
## 7.3.1 &nbsp; 完全二分木の表現
まず簡単なケースから分析してみましょう。完全二分木が与えられたとき、レベル順探索の順序に従ってすべてのノードを配列に格納し、各ノードは一意の配列インデックスに対応します。
レベル順探索の特性に基づいて、親ノードのインデックスとその子ノードの間の「マッピング公式」を導き出すことができます:**ノードのインデックスが$i$の場合、その左の子のインデックスは$2i + 1$、右の子のインデックスは$2i + 2$です**。下図は、さまざまなノードのインデックス間のマッピング関係を示しています。
![完全二分木の配列表現](array_representation_of_tree.assets/array_representation_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-12 &nbsp; 完全二分木の配列表現 </p>
**マッピング公式は、連結リストのノード参照(ポインタ)と同様の役割を果たします**。配列内の任意のノードが与えられたとき、マッピング公式を使用してその左(右)の子ノードにアクセスできます。
## 7.3.2 &nbsp; 任意の二分木の表現
完全二分木は特別なケースです。二分木の中間レベルには多くの`None`値が存在することがよくあります。レベル順探索のシーケンスにはこれらの`None`値が含まれないため、このシーケンスだけに依存して`None`値の数と分布を推測することはできません。**つまり、複数の二分木構造が同じレベル順探索シーケンスと一致する可能性があります**。
下図に示すように、完全でない二分木が与えられた場合、上記の配列表現方法は失敗します。
![レベル順探索シーケンスが複数の二分木の可能性に対応](array_representation_of_tree.assets/array_representation_without_empty.png){ class="animation-figure" }
<p align="center"> 図 7-13 &nbsp; レベル順探索シーケンスが複数の二分木の可能性に対応 </p>
この問題を解決するために、**レベル順探索シーケンスですべての`None`値を明示的に書き出すことを検討できます**。下図に示すように、この処理後、レベル順探索シーケンスは二分木を一意に表現できます。サンプルコードは以下の通りです:
=== "Python"
```python title=""
# 二分木の配列表現
# Noneを使用して空のスロットを表現
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
```
=== "C++"
```cpp title=""
/* 二分木の配列表現 */
// 最大整数値INT_MAXを使用して空のスロットをマーク
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
```
=== "Java"
```java title=""
/* 二分木の配列表現 */
// Integerラッパークラスを使用してnullで空のスロットをマーク
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
```
=== "C#"
```csharp title=""
/* 二分木の配列表現 */
// nullable int (int?)を使用してnullで空のスロットをマーク
int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Go"
```go title=""
/* 二分木の配列表現 */
// any型スライスを使用してnilで空のスロットをマーク
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
```
=== "Swift"
```swift title=""
/* 二分木の配列表現 */
// optional Int (Int?)を使用してnilで空のスロットをマーク
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
```
=== "JS"
```javascript title=""
/* 二分木の配列表現 */
// nullを使用して空のスロットを表現
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "TS"
```typescript title=""
/* 二分木の配列表現 */
// nullを使用して空のスロットを表現
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Dart"
```dart title=""
/* 二分木の配列表現 */
// nullable int (int?)を使用してnullで空のスロットをマーク
List<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Rust"
```rust title=""
/* 二分木の配列表現 */
// Noneを使用して空のスロットをマーク
let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];
```
=== "C"
```c title=""
/* 二分木の配列表現 */
// 最大int値を使用して空のスロットをマーク、したがってード値はINT_MAXであってはならない
int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
```
=== "Kotlin"
```kotlin title=""
/* 二分木の配列表現 */
// nullを使用して空のスロットを表現
val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
```
=== "Ruby"
```ruby title=""
```
=== "Zig"
```zig title=""
```
![任意の種類の二分木の配列表現](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" }
<p align="center"> 図 7-14 &nbsp; 任意の種類の二分木の配列表現 </p>
注目すべきは、**完備二分木は配列表現に非常に適している**ということです。完備二分木の定義を思い出すと、`None`は最下位レベルでのみ、かつ右側に向かって現れます。**つまり、すべての`None`値は確実にレベル順探索シーケンスの最後に現れます**。
これは、配列を使用して完備二分木を表現する際、すべての`None`値の格納を省略できることを意味し、非常に便利です。下図に例を示します。
![完備二分木の配列表現](array_representation_of_tree.assets/array_representation_complete_binary_tree.png){ class="animation-figure" }
<p align="center"> 図 7-15 &nbsp; 完備二分木の配列表現 </p>
以下のコードは、配列表現に基づく二分木を実装し、次の操作を含みます:
- ノードが与えられたとき、その値、左(右)の子ノード、および親ノードを取得する。
- 前順、中順、後順、およびレベル順探索シーケンスを取得する。
=== "Python"
```python title="array_binary_tree.py"
class ArrayBinaryTree:
"""配列ベースの二分木クラス"""
def __init__(self, arr: list[int | None]):
"""コンストラクタ"""
self._tree = list(arr)
def size(self):
"""リストの容量"""
return len(self._tree)
def val(self, i: int) -> int | None:
"""インデックスiのードの値を取得"""
# インデックスが範囲外の場合、Noneを返し、空席を表す
if i < 0 or i >= self.size():
return None
return self._tree[i]
def left(self, i: int) -> int | None:
"""インデックスiのードの左の子のインデックスを取得"""
return 2 * i + 1
def right(self, i: int) -> int | None:
"""インデックスiのードの右の子のインデックスを取得"""
return 2 * i + 2
def parent(self, i: int) -> int | None:
"""インデックスiのードの親のインデックスを取得"""
return (i - 1) // 2
def level_order(self) -> list[int]:
"""レベル順走査"""
self.res = []
# 配列を走査
for i in range(self.size()):
if self.val(i) is not None:
self.res.append(self.val(i))
return self.res
def dfs(self, i: int, order: str):
"""深さ優先走査"""
if self.val(i) is None:
return
# 前順走査
if order == "pre":
self.res.append(self.val(i))
self.dfs(self.left(i), order)
# 中順走査
if order == "in":
self.res.append(self.val(i))
self.dfs(self.right(i), order)
# 後順走査
if order == "post":
self.res.append(self.val(i))
def pre_order(self) -> list[int]:
"""前順走査"""
self.res = []
self.dfs(0, order="pre")
return self.res
def in_order(self) -> list[int]:
"""中順走査"""
self.res = []
self.dfs(0, order="in")
return self.res
def post_order(self) -> list[int]:
"""後順走査"""
self.res = []
self.dfs(0, order="post")
return self.res
```
=== "C++"
```cpp title="array_binary_tree.cpp"
/* 配列ベースの二分木クラス */
class ArrayBinaryTree {
public:
/* コンストラクタ */
ArrayBinaryTree(vector<int> arr) {
tree = arr;
}
/* リストの容量 */
int size() {
return tree.size();
}
/* インデックス i のノードの値を取得 */
int val(int i) {
// インデックスが範囲外の場合、INT_MAX を返すnull を表す)
if (i < 0 || i >= size())
return INT_MAX;
return tree[i];
}
/* インデックス i のノードの左の子のインデックスを取得 */
int left(int i) {
return 2 * i + 1;
}
/* インデックス i のノードの右の子のインデックスを取得 */
int right(int i) {
return 2 * i + 2;
}
/* インデックス i のノードの親のインデックスを取得 */
int parent(int i) {
return (i - 1) / 2;
}
/* レベル順走査 */
vector<int> levelOrder() {
vector<int> res;
// 配列を走査
for (int i = 0; i < size(); i++) {
if (val(i) != INT_MAX)
res.push_back(val(i));
}
return res;
}
/* 前順走査 */
vector<int> preOrder() {
vector<int> res;
dfs(0, "pre", res);
return res;
}
/* 中順走査 */
vector<int> inOrder() {
vector<int> res;
dfs(0, "in", res);
return res;
}
/* 後順走査 */
vector<int> postOrder() {
vector<int> res;
dfs(0, "post", res);
return res;
}
private:
vector<int> tree;
/* 深さ優先走査 */
void dfs(int i, string order, vector<int> &res) {
// 空の位置の場合、戻る
if (val(i) == INT_MAX)
return;
// 前順走査
if (order == "pre")
res.push_back(val(i));
dfs(left(i), order, res);
// 中順走査
if (order == "in")
res.push_back(val(i));
dfs(right(i), order, res);
// 後順走査
if (order == "post")
res.push_back(val(i));
}
};
```
=== "Java"
```java title="array_binary_tree.java"
/* 配列ベースの二分木クラス */
class ArrayBinaryTree {
private List<Integer> tree;
/* コンストラクタ */
public ArrayBinaryTree(List<Integer> arr) {
tree = new ArrayList<>(arr);
}
/* リストの容量 */
public int size() {
return tree.size();
}
/* インデックス i のノードの値を取得 */
public Integer val(int i) {
// インデックスが範囲外の場合、null を返す(空の位置を表す)
if (i < 0 || i >= size())
return null;
return tree.get(i);
}
/* インデックス i のノードの左の子のインデックスを取得 */
public Integer left(int i) {
return 2 * i + 1;
}
/* インデックス i のノードの右の子のインデックスを取得 */
public Integer right(int i) {
return 2 * i + 2;
}
/* インデックス i のノードの親のインデックスを取得 */
public Integer parent(int i) {
return (i - 1) / 2;
}
/* レベル順走査 */
public List<Integer> levelOrder() {
List<Integer> res = new ArrayList<>();
// 配列を走査
for (int i = 0; i < size(); i++) {
if (val(i) != null)
res.add(val(i));
}
return res;
}
/* 深さ優先走査 */
private void dfs(Integer i, String order, List<Integer> res) {
// 空の位置の場合、戻る
if (val(i) == null)
return;
// 前順走査
if ("pre".equals(order))
res.add(val(i));
dfs(left(i), order, res);
// 中順走査
if ("in".equals(order))
res.add(val(i));
dfs(right(i), order, res);
// 後順走査
if ("post".equals(order))
res.add(val(i));
}
/* 前順走査 */
public List<Integer> preOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "pre", res);
return res;
}
/* 中順走査 */
public List<Integer> inOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "in", res);
return res;
}
/* 後順走査 */
public List<Integer> postOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "post", res);
return res;
}
}
```
=== "C#"
```csharp title="array_binary_tree.cs"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Go"
```go title="array_binary_tree.go"
[class]{arrayBinaryTree}-[func]{}
```
=== "Swift"
```swift title="array_binary_tree.swift"
[class]{ArrayBinaryTree}-[func]{}
```
=== "JS"
```javascript title="array_binary_tree.js"
[class]{ArrayBinaryTree}-[func]{}
```
=== "TS"
```typescript title="array_binary_tree.ts"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Dart"
```dart title="array_binary_tree.dart"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Rust"
```rust title="array_binary_tree.rs"
[class]{ArrayBinaryTree}-[func]{}
```
=== "C"
```c title="array_binary_tree.c"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Kotlin"
```kotlin title="array_binary_tree.kt"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Ruby"
```ruby title="array_binary_tree.rb"
[class]{ArrayBinaryTree}-[func]{}
```
=== "Zig"
```zig title="array_binary_tree.zig"
[class]{ArrayBinaryTree}-[func]{}
```
## 7.3.3 &nbsp; 利点と制限
二分木の配列表現には以下の利点があります:
- 配列は連続したメモリ空間に格納されるため、キャッシュフレンドリーで、より高速なアクセスと探索が可能です。
- ポインタを格納する必要がないため、スペースを節約できます。
- ノードへのランダムアクセスが可能です。
しかし、配列表現にはいくつかの制限もあります:
- 配列格納には連続したメモリ空間が必要なため、大量のデータを持つ木の格納には適していません。
- ノードの追加や削除には配列の挿入や削除操作が必要で、効率が低くなります。
- 二分木に多くの`None`値がある場合、配列に含まれるノードデータの割合が低くなり、空間利用率が低下します。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,664 @@
---
comments: true
---
# 7.4 &nbsp; 二分探索木
下図に示すように、<u>二分探索木</u>は以下の条件を満たします。
1. 根ノードについて、左部分木のすべてのノードの値 $<$ 根ノードの値 $<$ 右部分木のすべてのノードの値。
2. 任意のノードの左と右の部分木も二分探索木です。つまり、条件`1.`も満たします。
![二分探索木](binary_search_tree.assets/binary_search_tree.png){ class="animation-figure" }
<p align="center"> 図 7-16 &nbsp; 二分探索木 </p>
## 7.4.1 &nbsp; 二分探索木の操作
二分探索木をクラス`BinarySearchTree`としてカプセル化し、木の根ノードを指すメンバー変数`root`を宣言します。
### 1. &nbsp; ノードの検索
ターゲットノード値`num`が与えられた場合、二分探索木の性質に従って検索できます。下図に示すように、ノード`cur`を宣言し、二分木の根ノード`root`から開始し、ノード値`cur.val``num`のサイズを比較するループを行います。
- `cur.val < num`の場合、ターゲットノードは`cur`の右部分木にあることを意味するため、`cur = cur.right`を実行します。
- `cur.val > num`の場合、ターゲットノードは`cur`の左部分木にあることを意味するため、`cur = cur.left`を実行します。
- `cur.val = num`の場合、ターゲットノードが見つかったことを意味するため、ループを終了してノードを返します。
=== "<1>"
![二分探索木でのノード検索例](binary_search_tree.assets/bst_search_step1.png){ class="animation-figure" }
=== "<2>"
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png){ class="animation-figure" }
=== "<3>"
![bst_search_step3](binary_search_tree.assets/bst_search_step3.png){ class="animation-figure" }
=== "<4>"
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png){ class="animation-figure" }
<p align="center"> 図 7-17 &nbsp; 二分探索木でのノード検索例 </p>
二分探索木での検索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドでケースの半分を排除します。ループ数は最大で二分木の高さです。二分木が平衡している場合、$O(\log n)$の時間を使用します。コード例は以下の通りです:
=== "Python"
```python title="binary_search_tree.py"
def search(self, num: int) -> TreeNode | None:
"""ノードを探索"""
cur = self._root
# ループで探索、葉ノードを通過した後にブレーク
while cur is not None:
# ターゲットードはcurの右部分木にある
if cur.val < num:
cur = cur.right
# ターゲットードはcurの左部分木にある
elif cur.val > num:
cur = cur.left
# ターゲットノードを発見、ループをブレーク
else:
break
return cur
```
=== "C++"
```cpp title="binary_search_tree.cpp"
/* ノードを検索 */
TreeNode *search(int num) {
TreeNode *cur = root;
// ループで検索、葉ノードを通り過ぎたら終了
while (cur != nullptr) {
// 目標ードはcurの右部分木にある
if (cur->val < num)
cur = cur->right;
// 目標ードはcurの左部分木にある
else if (cur->val > num)
cur = cur->left;
// 目標ノードを見つけた、ループを抜ける
else
break;
}
// 目標ノードを返す
return cur;
}
```
=== "Java"
```java title="binary_search_tree.java"
/* ノードを検索 */
TreeNode search(int num) {
TreeNode cur = root;
// ループで検索、葉ノードを通過後に終了
while (cur != null) {
// 対象ノードは cur の右部分木にある
if (cur.val < num)
cur = cur.right;
// 対象ノードは cur の左部分木にある
else if (cur.val > num)
cur = cur.left;
// 対象ノードを見つけた、ループを終了
else
break;
}
// 対象ノードを返す
return cur;
}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{Search}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{search}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{search}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{search}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{search}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{search}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{search}
```
=== "C"
```c title="binary_search_tree.c"
[class]{BinarySearchTree}-[func]{search}
```
=== "Kotlin"
```kotlin title="binary_search_tree.kt"
[class]{BinarySearchTree}-[func]{search}
```
=== "Ruby"
```ruby title="binary_search_tree.rb"
[class]{BinarySearchTree}-[func]{search}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{search}
```
### 2. &nbsp; ノードの挿入
挿入する要素`num`が与えられた場合、二分探索木の性質「左部分木 < 根ノード < 右部分木」を維持するため、挿入操作は下図に示すように進行します。
1. **挿入位置を見つける**: 検索操作と同様に、根ノードから開始し、現在のノード値と`num`のサイズ関係に従って下向きにループし、葉ノードを通過(`None`に走査)するまで、ループを終了します。
2. **この位置にノードを挿入**: ノード`num`を初期化し、`None`があった場所に配置します。
![二分探索木へのノード挿入](binary_search_tree.assets/bst_insert.png){ class="animation-figure" }
<p align="center"> 図 7-18 &nbsp; 二分探索木へのノード挿入 </p>
コード実装では、以下の2点に注意してください。
- 二分探索木は重複ノードの存在を許可しません。そうでなければ、その定義に違反します。したがって、挿入するノードが既に木に存在する場合、挿入は実行されず、ノードは直接戻ります。
- 挿入操作を実行するには、前のループからのノードを保存するためにノード`pre`を使用する必要があります。このようにして、`None`に走査したときに、その親ノードを取得でき、ノード挿入操作を完了できます。
=== "Python"
```python title="binary_search_tree.py"
def insert(self, num: int):
"""ノードを挿入"""
# 木が空の場合、ルートノードを初期化
if self._root is None:
self._root = TreeNode(num)
return
# ループで探索、葉ノードを通過した後にブレーク
cur, pre = self._root, None
while cur is not None:
# 重複ノードを発見したため、戻る
if cur.val == num:
return
pre = cur
# 挿入位置はcurの右部分木にある
if cur.val < num:
cur = cur.right
# 挿入位置はcurの左部分木にある
else:
cur = cur.left
# ノードを挿入
node = TreeNode(num)
if pre.val < num:
pre.right = node
else:
pre.left = node
```
=== "C++"
```cpp title="binary_search_tree.cpp"
/* ノードを挿入 */
void insert(int num) {
// 木が空の場合、ルートノードを初期化
if (root == nullptr) {
root = new TreeNode(num);
return;
}
TreeNode *cur = root, *pre = nullptr;
// ループで検索、葉ノードを通り過ぎたら終了
while (cur != nullptr) {
// 重複ノードを見つけた場合、戻る
if (cur->val == num)
return;
pre = cur;
// 挿入位置はcurの右部分木にある
if (cur->val < num)
cur = cur->right;
// 挿入位置はcurの左部分木にある
else
cur = cur->left;
}
// ノードを挿入
TreeNode *node = new TreeNode(num);
if (pre->val < num)
pre->right = node;
else
pre->left = node;
}
```
=== "Java"
```java title="binary_search_tree.java"
/* ノードを挿入 */
void insert(int num) {
// 木が空の場合、根ノードを初期化
if (root == null) {
root = new TreeNode(num);
return;
}
TreeNode cur = root, pre = null;
// ループで検索、葉ノードを通過後に終了
while (cur != null) {
// 重複ノードを見つけた場合、戻る
if (cur.val == num)
return;
pre = cur;
// 挿入位置は cur の右部分木にある
if (cur.val < num)
cur = cur.right;
// 挿入位置は cur の左部分木にある
else
cur = cur.left;
}
// ノードを挿入
TreeNode node = new TreeNode(num);
if (pre.val < num)
pre.right = node;
else
pre.left = node;
}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{Insert}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{insert}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{insert}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{insert}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{insert}
```
=== "C"
```c title="binary_search_tree.c"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Kotlin"
```kotlin title="binary_search_tree.kt"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Ruby"
```ruby title="binary_search_tree.rb"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{insert}
```
ノードの検索と同様に、ノードの挿入には$O(\log n)$の時間を使用します。
### 3. &nbsp; ノードの削除
まず、二分木でターゲットノードを見つけ、それを削除します。ノードの挿入と同様に、削除操作が完了した後も、二分探索木の性質「左部分木 < 根ノード < 右部分木」が満たされることを保証する必要があります。したがって、ターゲットードの子ード数に基づいて、0、1、2の3つのケースに分け、対応するード削除操作を実行します。
下図に示すように、削除するノードの次数が$0$の場合、そのノードは葉ノードであることを意味し、直接削除できます。
![二分探索木でのード削除次数0](binary_search_tree.assets/bst_remove_case1.png){ class="animation-figure" }
<p align="center"> 図 7-19 &nbsp; 二分探索木でのード削除次数0 </p>
下図に示すように、削除するノードの次数が$1$の場合、削除するノードをその子ノードで置き換えるだけで十分です。
![二分探索木でのード削除次数1](binary_search_tree.assets/bst_remove_case2.png){ class="animation-figure" }
<p align="center"> 図 7-20 &nbsp; 二分探索木でのード削除次数1 </p>
削除するノードの次数が$2$の場合、直接削除することはできませんが、ノードを使用して置き換える必要があります。二分探索木の性質「左部分木 $<$ 根ノード $<$ 右部分木」を維持するため、**このノードは右部分木の最小ノードまたは左部分木の最大ノードのいずれかです**。
右部分木の最小ノード(中順走査での次のノード)を選択すると仮定すると、削除操作は下図に示すように進行します。
1. 削除するノードの「中順走査シーケンス」での次のノードを見つけ、`tmp`として示します。
2. 削除するノードの値を`tmp`の値で置き換え、木内でノード`tmp`を再帰的に削除します。
=== "<1>"
![二分探索木でのード削除次数2](binary_search_tree.assets/bst_remove_case3_step1.png){ class="animation-figure" }
=== "<2>"
![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png){ class="animation-figure" }
=== "<3>"
![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png){ class="animation-figure" }
=== "<4>"
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png){ class="animation-figure" }
<p align="center"> 図 7-21 &nbsp; 二分探索木でのード削除次数2 </p>
ノードを削除する操作も$O(\log n)$の時間を使用します。削除するノードを見つけるのに$O(\log n)$の時間が必要で、中順走査の後継ノードを取得するのに$O(\log n)$の時間が必要です。コード例は以下の通りです:
=== "Python"
```python title="binary_search_tree.py"
def remove(self, num: int):
"""ノードを削除"""
# 木が空の場合、戻る
if self._root is None:
return
# ループで探索、葉ノードを通過した後にブレーク
cur, pre = self._root, None
while cur is not None:
# 削除するノードを発見、ループをブレーク
if cur.val == num:
break
pre = cur
# 削除するードはcurの右部分木にある
if cur.val < num:
cur = cur.right
# 削除するードはcurの左部分木にある
else:
cur = cur.left
# 削除するノードが存在しない場合、戻る
if cur is None:
return
# 子ノード数 = 0 または 1
if cur.left is None or cur.right is None:
# 子ノード数 = 0/1の場合、child = null/その子ノード
child = cur.left or cur.right
# ードcurを削除
if cur != self._root:
if pre.left == cur:
pre.left = child
else:
pre.right = child
else:
# 削除されるノードがルートの場合、ルートを再割り当て
self._root = child
# 子ノード数 = 2
else:
# curの中順走査の次のードを取得
tmp: TreeNode = cur.right
while tmp.left is not None:
tmp = tmp.left
# 再帰的にードtmpを削除
self.remove(tmp.val)
# curをtmpで置き換え
cur.val = tmp.val
```
=== "C++"
```cpp title="binary_search_tree.cpp"
/* ノードを削除 */
void remove(int num) {
// 木が空の場合、戻る
if (root == nullptr)
return;
TreeNode *cur = root, *pre = nullptr;
// ループで検索、葉ノードを通り過ぎたら終了
while (cur != nullptr) {
// 削除するノードを見つけた、ループを抜ける
if (cur->val == num)
break;
pre = cur;
// 削除するードはcurの右部分木にある
if (cur->val < num)
cur = cur->right;
// 削除するードはcurの左部分木にある
else
cur = cur->left;
}
// 削除するノードがない場合、戻る
if (cur == nullptr)
return;
// 子ノード数 = 0 または 1
if (cur->left == nullptr || cur->right == nullptr) {
// 子ノード数 = 0 / 1の場合、child = nullptr / その子ノード
TreeNode *child = cur->left != nullptr ? cur->left : cur->right;
// ードcurを削除
if (cur != root) {
if (pre->left == cur)
pre->left = child;
else
pre->right = child;
} else {
// 削除されるノードがルートの場合、ルートを再割り当て
root = child;
}
// メモリを解放
delete cur;
}
// 子ノード数 = 2
else {
// curの中順走査の次のードを取得
TreeNode *tmp = cur->right;
while (tmp->left != nullptr) {
tmp = tmp->left;
}
int tmpVal = tmp->val;
// ードtmpを再帰的に削除
remove(tmp->val);
// curをtmpで置き換え
cur->val = tmpVal;
}
}
```
=== "Java"
```java title="binary_search_tree.java"
/* ノードを削除 */
void remove(int num) {
// 木が空の場合、戻る
if (root == null)
return;
TreeNode cur = root, pre = null;
// ループで検索、葉ノードを通過後に終了
while (cur != null) {
// 削除するノードを見つけた、ループを終了
if (cur.val == num)
break;
pre = cur;
// 削除するノードは cur の右部分木にある
if (cur.val < num)
cur = cur.right;
// 削除するノードは cur の左部分木にある
else
cur = cur.left;
}
// 削除するノードがない場合、戻る
if (cur == null)
return;
// 子ノード数 = 0 または 1
if (cur.left == null || cur.right == null) {
// 子ノード数 = 0/1 の場合、child = null/その子ノード
TreeNode child = cur.left != null ? cur.left : cur.right;
// ノード cur を削除
if (cur != root) {
if (pre.left == cur)
pre.left = child;
else
pre.right = child;
} else {
// 削除されるノードが根の場合、根を再割り当て
root = child;
}
}
// 子ノード数 = 2
else {
// cur の中順走査の次のノードを取得
TreeNode tmp = cur.right;
while (tmp.left != null) {
tmp = tmp.left;
}
// 再帰的にノード tmp を削除
remove(tmp.val);
// cur を tmp で置き換える
cur.val = tmp.val;
}
}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{Remove}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{remove}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{remove}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{remove}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{remove}
```
=== "C"
```c title="binary_search_tree.c"
[class]{BinarySearchTree}-[func]{removeItem}
```
=== "Kotlin"
```kotlin title="binary_search_tree.kt"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Ruby"
```ruby title="binary_search_tree.rb"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{remove}
```
### 4. &nbsp; 中順走査は順序付けされている
下図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の走査順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」のサイズ関係を満たします。
これは、二分探索木で中順走査を実行するときに、常に次に小さいノードが最初に走査されることを意味し、重要な性質につながります:**二分探索木の中順走査のシーケンスは昇順です**。
中順走査の昇順性質を使用して、二分探索木で順序付けされたデータを取得するには$O(n)$の時間のみが必要で、追加のソート操作は不要であり、非常に効率的です。
![二分探索木の中順走査シーケンス](binary_search_tree.assets/bst_inorder_traversal.png){ class="animation-figure" }
<p align="center"> 図 7-22 &nbsp; 二分探索木の中順走査シーケンス </p>
## 7.4.2 &nbsp; 二分探索木の効率
データのセットが与えられた場合、配列または二分探索木を使用して格納することを検討します。下の表を観察すると、二分探索木のすべての操作は対数時間計算量を持ち、安定して効率的です。配列は、頻繁な追加と検索や削除の頻度が少ないシナリオでのみ、二分探索木よりも効率的です。
<p align="center"> 表 7-2 &nbsp; 配列と探索木の効率比較 </p>
<div class="center-table" markdown>
| | 未ソート配列 | 二分探索木 |
| -------------- | -------------- | ------------------ |
| 要素の検索 | $O(n)$ | $O(\log n)$ |
| 要素の挿入 | $O(1)$ | $O(\log n)$ |
| 要素の削除 | $O(n)$ | $O(\log n)$ |
</div>
理想的には、二分探索木は「平衡」しており、任意のノードを$\log n$ループ内で見つけることができます。
しかし、二分探索木で継続的にノードを挿入および削除すると、下図に示すように連結リストに退化する可能性があり、さまざまな操作の時間計算量も$O(n)$に悪化します。
![二分探索木の退化](binary_search_tree.assets/bst_degradation.png){ class="animation-figure" }
<p align="center"> 図 7-23 &nbsp; 二分探索木の退化 </p>
## 7.4.3 &nbsp; 二分探索木の一般的な応用
- システムでの多レベルインデックスとして使用され、効率的な検索、挿入、削除操作を実装します。
- 特定の検索アルゴリズムの基盤となるデータ構造として機能します。
- データストリームを格納して、その順序付けされた状態を維持するために使用されます。

Some files were not shown because too many files have changed in this diff Show More