This commit is contained in:
krahets
2026-04-03 18:46:15 +08:00
parent 377736b1bd
commit 9d21ca86b0
352 changed files with 46563 additions and 11262 deletions
+21 -21
View File
@@ -2,51 +2,51 @@
comments: true
---
# 16.2   コントリビューション
# 16.2   一緒に制作に参加しましょう
著者の力に限りがあるため、本書にはいくつかの省略や誤りが避けられません。ご理解をお願いします。誤字、リンク切れ、内容の欠落、文章の曖昧さ、説明の不明さ、または不合理な文章構造を発見された場合は、読者により良質な学習リソースを提供するため、修正にご協力ください
著者の力に限りがあるため、本書にはどうしても一部の漏れや誤りが含まれる可能性があります。ご了承ください。誤字、リンク切れ、内容の欠落、表現の曖昧さ、説明の不明さ、文章構成の不適切さなどの問題を見つけた場合は、ぜひ修正にご協力ください。読者により良学習リソースを提供できます。
すべての[コントリビューター](https://github.com/krahets/hello-algo/graphs/contributors)のGitHub IDは、本書のリポジトリ、ウェブ、PDFバージョンのホームページに表示され、オープンソースコミュニティへの無私の貢献に感謝いたします。
すべての[寄稿者](https://github.com/krahets/hello-algo/graphs/contributors)の GitHub ID は、本書のリポジトリ、Web 版、PDF 版のホームページに掲載され、オープンソースコミュニティへの惜しみない貢献に感謝を表します。
!!! success "オープンソースの魅力"
紙の本の2つの印刷版の間隔はしばしば長く、内容更新非常に不便です。
しかし、このオープンソースの本では、内容更新サイクルは数日、さらには数時間に短縮されます。
紙の書籍では、2 回の増刷の間隔が長くなりがちで、内容更新非常に不便です。
一方、このオープンソース書籍では、内容更新サイクルは数日、場合によっては数時間にまで短縮されています。
### 1.   内容の微調整
下の図に示すように、各ページの右上に「編集アイコン」があります。以下の手順に従ってテキストやコードを修正できます。
下の図ように、各ページの右上に「編集アイコン」があります。の手順で本文やコードを修正できます。
1. 「編集アイコン」をクリックします。「このリポジトリをフォークします」とされた場合は、同意してください。
2. Markdownソースファイルの内容を修正し、内容の正確性を確認し、フォーマットの一貫性を保つようにしてください。
3. ページ下部修正説明を記入し、「Propose file change」ボタンをクリックします。ページがリダイレクトされた後、「Create pull request」ボタンをクリックしてプルリクエストを開始します。
1. 「編集アイコン」をクリックし「このリポジトリを Fork する必要があります」と表示された場合は、その操作を承認してください。
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)を作成するか、問題を説明するコメントが必要です。できるだけ早く図を再描画して置き換えます。
画像は直接修正できないため、新しい [Issue](https://github.com/krahets/hello-algo/issues) を作成するかコメントで問題を説明してください。できるだけ早く描き直して差し替えます。
### 2. &nbsp; 内容の作成
### 2. &nbsp; コンテンツ制作
このオープンソースプロジェクトへの参加に興味がある場合、コードを他のプログラミング言語翻訳したり、記事内容を拡張したりすることを含めて、以下のプルリクエストワークフローを実装する必要があります
コードを他のプログラミング言語翻訳することや、記事内容を拡充することなど、このオープンソースプロジェクトへの参加に興味がある場合は、以下の Pull Request ワークフローに従ってください
1. GitHubにログインし、本書の[コードリポジトリ](https://github.com/krahets/hello-algo)を個人アカウントにフォークします。
2. フォークしたリポジトリのウェブページに移動し`git clone`コマンドを使用してリポジトリをローカルマシンにクローンします。
3. ローカルで内容を作成し、完全なテストを実行してコードの正確性を検証します。
4. ローカルで行った変更をコミットし、リモートリポジトリにプッシュします。
5. リポジトリのウェブページを更新し、「Create pull request」ボタンをクリックしてプルリクエストを開始します。
1. GitHub にログインし、本書の[コードリポジトリ](https://github.com/krahets/hello-algo)を個人アカウントに Fork します。
2. Fork したリポジトリのページに入り`git clone` コマンドを使てリポジトリをローカルにクローンします。
3. ローカルでコンテンツを作成し、完全なテストを行ってコードの正しさを確認します。
4. ローカルで行った変更を Commit し、その後リモートリポジトリへ Push します。
5. リポジトリのページを更新し、「Create pull request」ボタンをクリックするとプルリクエストを作成できます。
### 3. &nbsp; Dockerデプロイメント
### 3. &nbsp; Docker デプロイ
`hello-algo`ルートディレクトリで以下のDockerスクリプトを実行して`http://localhost:8000`プロジェクトにアクセスます
`hello-algo`ルートディレクトリで以下の Docker スクリプトを実行すると、`http://localhost:8000` で本プロジェクトにアクセスできます
```shell
docker-compose up -d
```
以下のコマンドを使用してデプロイメントを削除ます
以下のコマンドデプロイを削除できます
```shell
docker-compose down
+1 -1
View File
@@ -10,5 +10,5 @@ icon: material/help-circle-outline
## 章の内容
- [16.1 &nbsp; プログラミング環境のインストール](installation.md)
- [16.2 &nbsp; 一緒に作に参加する](contribution.md)
- [16.2 &nbsp; 一緒に作に参加しましょう](contribution.md)
- [16.3 &nbsp; 用語集](terminology.md)
+43 -43
View File
@@ -2,75 +2,75 @@
comments: true
---
# 16.1 &nbsp; インストール
# 16.1 &nbsp; プログラミング環境のインストール
## 16.1.1 &nbsp; IDEのインストール
## 16.1.1 &nbsp; IDE のインストール
ローカルの統合開発環境(IDE)として、オープンソースで軽量なVS Codeを使用することをお勧めします。[VS Code公式ウェブサイト](https://code.visualstudio.com/)にアクセスし、お使いのオペレーティングシステムに適したVS Codeのバージョンを選択してダウンロードしインストールしてください。
オープンソースで軽量な VS Code をローカルの統合開発環境(IDE)として使用することを推奨します。[VS Code 公式サイト](https://code.visualstudio.com/) にアクセスし、使用している OS に応じたバージョンの VS Code をダウンロードしインストールしてください。
![公式ウェブサイトからVS Codeをダウンロード](installation.assets/vscode_installation.png){ class="animation-figure" }
![公式サイトから VS Code をダウンロード](installation.assets/vscode_installation.png){ class="animation-figure" }
<p align="center"> 図 16-1 &nbsp; 公式ウェブサイトからVS Codeをダウンロード </p>
<p align="center"> 図 16-1 &nbsp; 公式サイトから VS Code をダウンロード </p>
VS Codeには強力な拡張機能エコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。例えば、「Python Extension Pack」をインストールした後、Pythonコードをデバッグできます。インストール手順を下の図に示します。
VS Code には強力な拡張機能エコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。Python を例にすると、「Python Extension Pack」拡張機能をインストールした後、Python コードをデバッグできるようになります。インストール手順を下に示します。
![VS Code拡張機能パックのインストール](installation.assets/vscode_extension_installation.png){ class="animation-figure" }
![VS Code 拡張機能のインストール](installation.assets/vscode_extension_installation.png){ class="animation-figure" }
<p align="center"> 図 16-2 &nbsp; VS Code拡張機能パックのインストール </p>
<p align="center"> 図 16-2 &nbsp; VS Code 拡張機能のインストール </p>
## 16.1.2 &nbsp; 言語環境のインストール
### 1. &nbsp; Python環境
### 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`を入力し、コードフォーマッティングツールをインストールします。
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++環境
### 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 }`に設定します。
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. 任意)Settings ページを開き、コード整形オプション `Clang_format_fallback Style` を検索し`{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }` に設定します。
### 3. &nbsp; Java環境
### 3. &nbsp; Java 環境
1. [OpenJDK](https://jdk.java.net/18/)をダウンロードしてインストールします(バージョンはJDK 9より新しい必要があります)。
2. VS Code拡張機能マーケットプレイスで`java`を検索し、Extension Pack for Javaをインストールします。
1. [OpenJDK](https://jdk.java.net/18/) をダウンロードしてインストールします(バージョンは JDK 9 より新しい必要があります)。
2. VS Code拡張機能マーケットプレイスで `java` を検索し、Extension Pack for Java をインストールします。
### 4. &nbsp; C#環境
### 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))。
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環境
### 5. &nbsp; Go 環境
1. [go](https://go.dev/dl/)をダウンロードしてインストールします。
2. VS Code拡張機能マーケットプレイスで`go`を検索し、Goをインストールします。
3. `Ctrl + Shift + P`を押してコマンドバーを呼び出し、goと入力し`Go: Install/Update Tools`を選択し、すべてを選択してインストールします。
1. [go](https://go.dev/dl/) をダウンロードしてインストールします。
2. VS Code拡張機能マーケットプレイスで `go` を検索し、Go をインストールします。
3. ショートカットキー `Ctrl + Shift + P` を押してコマンドパレットを開き、go と入力し`Go: Install/Update Tools` を選択し、すべてにチェックを入れてインストールします。
### 6. &nbsp; Swift環境
### 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)をインストールします。
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環境
### 7. &nbsp; JavaScript 環境
1. [Node.js](https://nodejs.org/en/)をダウンロードしてインストールします。
2. オプション)VS Code拡張機能マーケットプレイスで`Prettier`を検索し、コードフォーマッティングツールをインストールします。
1. [Node.js](https://nodejs.org/en/) をダウンロードしてインストールします。
2. 任意VS Code拡張機能マーケットプレイスで `Prettier` を検索し、コード整形ツールをインストールします。
### 8. &nbsp; TypeScript環境
### 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)をインストールします。
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環境
### 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)をインストールします。
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環境
### 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)をインストールします。
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) をインストールします。
+131 -131
View File
@@ -4,142 +4,142 @@ 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 | 貪欲アルゴリズム | 贪心算法 | 貪婪演算法 |
| 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 | ビッグオー記法 | ビッグオー記法 |
| asymptotic upper bound | 漸近上界 | 漸近上界 |
| sign-magnitude | 符号絶対値表現 | 符号絶対値表現 |
| 1s complement | 1の補数 | 1の補数 |
| 2s complement | 2の補数 | 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 |
| 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$ 問題 |
| 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$ クイーン問題 |
| dynamic programming | 動的計画法 | 動的計画法 |
| initial state | 初期状態 | 初期状態 |
| state-transition equation | 状態遷移方程式 | 状態遷移方程 |
| knapsack problem | ナップサック問題 | ナップサック問題 |
| edit distance problem | 編集距離問題 | 編距離問題 |
| greedy algorithm | 貪欲 | 貪欲法 |
</div>
File diff suppressed because it is too large Load Diff
@@ -9,9 +9,9 @@ icon: material/view-list-outline
!!! abstract
データ構造の世界は頑丈なレンガの壁に似ています。
データ構造の世界は、まるで重厚なれんがの壁のようです。
配列では、レンガがぴったりと整列し、それぞれが次のものと継ぎ目なく隣り合って、統一された形成を作っている姿を想像してください。一方、連結リストでは、これらのレンガが自由に散らばり、それらの間を優雅に編み込む蔦に抱かれています。
配列のれんがは整然と並び、一つひとつがぴったりと接しています。連結リストのれんがはあちこちに分散し、それらをつなぐつるがれんがのすき間を自由に行き交います。
## 章の内容
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,80 +4,80 @@ comments: true
# 4.4 &nbsp; メモリとキャッシュ *
この章の最初の2つのセクションでは、「連続格納」と「分散格納」をそれぞれ表現する2つの基本的なデータ構造である配列と連結リストを探究しました
本章の前二節では、配列と連結リストという二つの基礎的かつ重要なデータ構造を扱いました。これらはそれぞれ「連続格納」と「分散格納」という二つの物理構造を表しています
実際、**物理構造はプログラムメモリとキャッシュをどの程度効率的に利用するかを大きく決定し**、これがアルゴリズムの全体的なパフォーマンスに影響を与えます。
実際には、**物理構造はプログラムにおけるメモリとキャッシュの利用効率を大きく左右し**、ひいてはアルゴリズムプログラム全体の性能に影響します。
## 4.4.1 &nbsp; コンピュータ記憶装置
## 4.4.1 &nbsp; コンピュータ記憶装置
コンピュータには3種類の記憶装置があります<u>ハードディスク</u>、<u>ランダムアクセスメモリ(RAM</u>、および<u>キャッシュメモリ</u>です。以下の表は、コンピュータシステムにおけるそれぞれの役割とパフォーマンス特性を示しています。
コンピュータには種類の記憶装置があります<u>ハードディスクhard disk</u>、<u>メモリ(random-access memory, RAM</u>、<u>キャッシュcache memory</u>です。以下の表は、これらがコンピュータシステムで担う役割と性能上の特徴を示しています。
<p align="center"> 表 4-2 &nbsp; コンピュータ記憶装置 </p>
<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と一緒に価格設定 |
| | ハードディスク | メモリ | キャッシュ |
| -------------- | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
| 用途 | OS、プログラム、ファイルなど長期保存 | 実行中のプログラム処理中のデータ一時保存 | 頻繁にアクセスされるデータ命令を保存し、CPU のメモリアクセス数を減らす |
| 揮発性 | 電源後もデータは失われない | 電源後にデータは失われる | 電源後にデータは失われる |
| 容量 | 大きい、TB 級 | 小さい、GB 級 | 非常に小さい、MB 級 |
| 速度 | 遅い、数百数千 MB/s | 速い、数十 GB/s | 非常に速、数十数百 GB/s |
| 価格(人民元 | 比較的安価、数角〜数元 / GB | 比較的高価、数十〜数百元 / GB | 非常に高価、CPU と一体で価格設定される |
</div>
コンピュータ記憶システムは、下図に示すようピラミッドとして視覚化できます。ピラミッドの上部にある記憶装置ほど高速で、容量小さく、より高価です。このマルチレベル設計は偶然ではなく、コンピュータ科学者エンジニアによる慎重な検討の結果です。
コンピュータ記憶システムは、下図ようピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者エンジニアによる熟慮の末の設計です。
- **ハードディスクメモリ置き換えるのは困難です**。第一に、メモリ内のデータは電源オフ後に失われるため、長期データ保存には適していません。第二に、メモリはハードディスクよりも大幅に高価で、消費者市場での広範囲な使用の実現可能性を制限しています。
- **キャッシュは大容量と高速のトレードオフに直面しています**。L1、L2、L3キャッシュの容量が増加するにつれて、その物理サイズ大きくなり、CPUコアからの距離が増加します。これによりデータ転送時間が長くなり、アクセス遅延が高くなります。現在の技術では、マルチレベルキャッシュ構造が容量、速度、コストの間の最適なバランスを提供します。
- **ハードディスクメモリ置き換えにくい**。まず、メモリ内のデータは電源後に失われるため、長期保存には向きません。に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。
- **キャッシュは大容量と高速性を両立しにくい**。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズ大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセス遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスす。
![コンピュータ記憶システム](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" }
![コンピュータ記憶システム](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" }
<p align="center"> 図 4-9 &nbsp; コンピュータ記憶システム </p>
<p align="center"> 図 4-9 &nbsp; コンピュータ記憶システム </p>
!!! tip
コンピュータの記憶階層は、速度、容量、コストの間の慎重なバランスを反映しています。このタイプのトレードオフは様々な業界で一般的であり、利益と制限の間の最適なバランスを見つけることが重要です。
コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。
全体的に、**ハードディスクは大量データの長期保存を提供し、メモリはプログラム実行中に処理されるデータの一時保存として機能し、キャッシュは頻繁にアクセスされるデータ命令保存して実行効率を向上させます**。それらは一緒になってコンピュータシステムの効率的な動作を保証します。
要するに、**ハードディスクは大量データの長期保存、メモリはプログラム実行中に処理しているデータの一時保存、キャッシュは頻繁にアクセスされるデータ命令保存に用いられ**、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの効率な運用を支えています。
図に示すように、プログラム実行中、データハードディスクからメモリ読み込まれ、CPU計算が行われます。CPUの拡張として機能するキャッシュは、**メモリからインテリジェントにデータを先読み**、CPUのより高速なデータアクセスを可能にします。これによりプログラム実行効率が大幅に向上し、低速なメモリへの依存が減少します。
次の図に示すように、プログラム実行時にはデータハードディスクからメモリ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、**メモリからデータを賢く読み込むことで**、CPU高速なデータ読み出しを提供し、プログラム実行効率を大きく高め、低速なメモリへの依存を減らします。
![ハードディスク、メモリ、キャッシュ間のデータフロー](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" }
![ハードディスク、メモリ、キャッシュ間のデータの流れ](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" }
<p align="center"> 図 4-10 &nbsp; ハードディスク、メモリ、キャッシュ間のデータフロー </p>
<p align="center"> 図 4-10 &nbsp; ハードディスク、メモリ、キャッシュ間のデータの流れ </p>
## 4.4.2 &nbsp; データ構造のメモリ効率
メモリ空間利用の観点から、配列と連結リストにはそれぞれ利点と制があります。
メモリ空間利用という観点では、配列と連結リストにはそれぞれ利点と制があります。
一方で、**メモリは限られており、複数のプログラムで共有できない**ため、データ構造での空間使用の最適化は重要です。配列要素が密接にパックされており、連結リストのように参照(ポインタ)のための追加メモリを必要としないため、空間効率的です。しかし、配列は連続したメモリブロックを事前に割り当てる必要があり、割り当てられた空間が実際の必要量を超える場合、無駄につながる可能性があります。配列の拡張も追加の時間と空間のオーバーヘッドを伴います。対照的に、連結リストはノードに対してメモリを動的に割り当て・解放し、ポインタのための追加メモリのコストでより大きな柔軟性を提供します。
一方で、**メモリは有限であり、同じメモリ領域を複数のプログラムで共有することはできません**。そのため、データ構造にはできるだけ効率よく空間を使うことが求められます。配列要素は密に並んでおり、連結リストのノード間参照(ポインタ)を保持する追加領域が不要なため、空間効率は高くなります。しかし、配列は十分な連続メモリを一度に確保する必要があり、メモリ浪費を招くことがありますし、拡張時にも追加の時間と空間コストがかかります。これに対して連結リストはノード」単位で動的にメモリを割り当て・解放でき、より高い柔軟性を備えています。
方で、プログラム実行中、**繰り返されるメモリの割り当てと解放はメモリの断片化を増加させ**、メモリ利用効率低下させます。配列は連続記憶方式により、メモリ断片化を引き起こす可能性が比較的低いです。対照的に、連結リスト要素を非連続の場所に保存し、頻繁な挿入削除はメモリ断片化を悪化させる可能性があります。
方で、プログラム実行中には、**メモリの確保と解放を繰り返すにつれて、空きメモリの断片化はますます進み**、メモリ利用効率低下を招きます。配列は連続した格納方式を取るため、比較的メモリ断片化を起こしにくい構造です。反対に、連結リスト要素は分散して格納されるため、頻繁な挿入削除を行うと、より断片化を招きやすくなります。
## 4.4.3 &nbsp; データ構造のキャッシュ効率
キャッシュはメモリよりも空間容量がはるかに小さいですが、はるかに高速で、プログラム実行速度において重要な役割を果たします。限られた容量のため、キャッシュは頻繁にアクセスされるデータのサブセットのみを保存できます。CPUがキャッシュに存在しないデータにアクセスしようとすると、<u>キャッシュミス</u>が発生し、CPUは低速なメモリから必要なデータを取得する必要があり、パフォーマンスに影響を与える可能性があります
キャッシュは容量こそメモリよりはるかに小さいものの、速度はメモリよりずっと速く、プログラム実行速度において極めて重要な役割を果たします。キャッシュ容量には限りがあり、頻繁にアクセスされる一部のデータしか保持できません。そのため、CPU がアクセスしようとするデータがキャッシュに存在しない場合、<u>キャッシュミス(cache miss</u>が発生し、CPU は低速なメモリから必要なデータを読み込まなければなりません
明らかに、**キャッシュミスが少ないほど、CPUのデータ読み書き効率高く**、プログラムパフォーマンスが向上します。CPUがキャッシュからデータを正常に取得する割合<u>キャッシュヒット率</u>と呼ばれ、キャッシュ効率を測定するためによく使用される指標です。
当然ながら、**キャッシュミスが少ないほど、CPU のデータ読み書き効率高くなり**、プログラム性能も向上します。CPU がキャッシュからデータを正常に取得できた割合<u>キャッシュヒット率cache hit rate</u>と呼び、この指標は通常、キャッシュ効率の評価に用いられます。
より高い効率を達成するため、キャッシュは以下のデータロードメカニズムを採用します。
できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。
- **キャッシュライン**:キャッシュは個々のバイトではなく、キャッシュラインと呼ばれる単位でデータを保存・ロードして動作します。このアプローチは、一度により大きなデータブロックを転送することで効率を向上させます。
- **先読みメカニズム**:プロセッサはデータアクセスパターン(例:連続または固定ストライドアクセス)を予測し、これらのパターンに基づいてデータをキャッシュに先読みして、キャッシュヒット率を向上させます。
- **空間的局所性**特定のデータがアクセスされると、近くのデータもまもなくアクセスされる可能性があります。これを活用するため、キャッシュは要求されたデータと一緒に隣接するデータをロードし、ヒット率を向上させます。
- **時間的局所性**:データがアクセスされた場合、近い将来に再びアクセスされる可能性があります。キャッシュはこの原理を使用し、最近アクセスされたデータを保持してヒット率を向上させます。
- **キャッシュライン**:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。
- **プリフェッチ機構**:プロセッサはデータアクセスパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、のパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。
- **空間的局所性**あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。
- **時間的局所性**あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスたデータを保持することでヒット率を高めます。
実際、**配列と連結リストは異なるキャッシュ利用効率を持ち**、これは主に以下の側面に反映されます。
実際には、**配列と連結リストはキャッシュ利用効率が異なり**、主に次の点に表れます。
- **占有空間**:連結リスト要素は配列要素より多くの空間を占有するため、キャッシュに保持される有効データ少なくなります。
- **キャッシュライン**:連結リストデータはメモリ全体に散在し、キャッシュは「行単位でロード」されるため、ロードされる無効データの割合が高くなります。
- **先読みメカニズム**:配列のデータアクセスパターンは連結リストよりも「予測可能」で、つまりシステムがこれからロードされるデータを推測しやすいです。
- **空間的局所性**:配列は連続したメモリ空間に保存されるため、ロードされているデータの近くデータまもなくアクセスされる可能性が高くなります。
- **使用空間**:連結リスト要素は配列要素より多くの空間を占るため、キャッシュに収まる有効データ量は少なくなります。
- **キャッシュライン**:連結リストデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。
- **プリフェッチ機構**:配列のほうが連結リストよりもデータアクセスパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。
- **空間的局所性**:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。
全体的に、**配列はより高いキャッシュヒット率を持ち、一般的に連結リストよりも操作効率が高いです**。これにより、配列に基づくデータ構造はアルゴリズム問題の解決において人気があります。
全体として、**配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています**。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。
**高いキャッシュ効率が配列が常に連結リストより優れているという意味ではない**ことに注意すべきです。データ構造の選択は特定のアプリケーション要件に依存すべきです。えば、配列と連結リストの両方が「スタック」データ構造を実装できますが(次章で詳細説明)、それらは異なるシナリオに適しています。
注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。
- アルゴリズム問題では、より高い操作効率とランダムアクセス機能を提供するため、配列に基づくスタックを選択する傾向があります。唯一のコストは配列に対して一定量のメモリ空間を事前に割り当てる必要があることです。
- データ量が非常に大きく、高度に動的で、スタックの想サイズを推定するのが困難な場合、連結リストに基づくスタックがより良い選択です。連結リスト大量のデータをメモリの異なる部分に分散でき、配列拡張の追加オーバーヘッドを回避できます。
- アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。
- データ量が非常に大きく、動的性が高く、スタックの想サイズを見積もりにくい場合、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。
+49 -44
View File
@@ -4,82 +4,87 @@ comments: true
# 4.5 &nbsp; まとめ
### 1. &nbsp; 重要な復習
### 1. &nbsp; 要点の振り返り
- 配列と連結リストは2つの基本的なデータ構造であり、コンピュータメモリにおける2つの格納方法を表しています:連続空間格納と非連続空間格納です。それらの特性は互いに補完し合います
- 配列はランダムアクセスをサポートし、使用するメモリ少ない一方で、要素の挿入と削除は非効率的で、初期化後長さが固定されています
- 連結リストは参照(ポインタ)変更によって効率的なノードの挿入と削除を実装し、長さ柔軟に調整できますが、ノードアクセス効率低く、より多くのメモリを消費します
- 連結リストの一般的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあり、それぞれに独自の応用シナリオがあります
- リストは要素の順序付けられたコレクションで、追加、削除、変更をサポートし、通常は動的配列に基づいて実装され、配列の利点を保持しながら柔軟な長さ調整を可能にします
- リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります
- プログラム実行中、データは主にメモリに格納されます。配列はより高いメモリ空間効率を提供し、連結リストはメモリ使用においてより柔軟です
- キャッシュは、キャッシュライン、先読み、空間的局所性、時間的局所性などのメカニズムを通じてCPUに高速データアクセスを提供し、プログラム実行効率を大幅に向上させます
- より高いキャッシュヒット率により、配列は一般的に連結リストよりも効率的です。データ構造を選択する際は、特定のニーズとシナリオに基づいて適切な選択をすべきです。
- 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である
- 配列はランダムアクセスをサポートし、使用メモリ少ない一方で、要素の挿入と削除の効率は低く、初期化後長さを変更できない
- 連結リストは参照(ポインタ)変更することでノードの挿入と削除を効率的に行え、長さ柔軟に調整できる。一方で、ノードへのアクセス効率低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある
- リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる
- リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある
- プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である
- キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて CPU に高速なデータアクセスを提供し、プログラムの実行効率を大きく向上させる
- 配列はキャッシュヒット率が高いため、通常は連結リストよりも高効率である。データ構造を選択する際は、具体的な要件や場面に応じて適切に選ぶべきである
### 2. &nbsp; Q & A
**Q**:配列をスタックに格納するヒープに格納するは、時間と空間効率に影響ますか?
**Q**:配列をスタックに格納する場合とヒープに格納する場合では、時間効率と空間効率に影響がありますか?
スタックとヒープの両方に格納される配列は連続したメモリ空間に格納され、データ操作効率は本的に同じです。しかし、スタックとヒープには独自の特性があり、以下の違いが生じます
スタックとヒープ上の配列はいずれも連続したメモリ領域に格納されるため、データ操作効率は本的に同じである。ただし、スタックとヒープにはそれぞれ特徴があり、以下の違いが生じ
1. 割り当てと解放効率:スタックはより小さなメモリブロックで、コンパイラによって自動的に割り当てられます。ヒープメモリは比較的大きく、コードで動的に割り当てることができ、断片化しやすいです。したがって、ヒープでの割り当てと解放操作は一般的にスタックよりも遅くなります
2. サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般に利用可能メモリによって制限されます。したがって、ヒープは大きな配列の格納により適しています
3. 柔軟性:スタック上の配列サイズはコンパイル時に決定される必要がありますが、ヒープ上の配列サイズは実行時に動的に決定できます
1. 確保と解放効率:スタックは比較的小さなメモリ領域で、確保はコンパイラによって自動的に行われる。一方、ヒープメモリは相対的に大きく、コードで動的に確保できる反面、断片化しやすい。そのため、ヒープでの確保と解放は通常スタックより遅い
2. サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般に利用可能メモリに制限される。そのため、ヒープは大きな配列の格納により適してい
3. 柔軟性:スタック上の配列サイズはコンパイル時に確定している必要があが、ヒープ上の配列サイズは実行時に動的に決定でき
**Q**:なぜ配列は同じ型の要素を必要とし、連結リストは同じ型の要素を強調しないのですか?
**Q**:なぜ配列は同じ型の要素が求められるのに、連結リストは同じ型であることが強調されないのですか?
連結リストは参照(ポインタ)によって接続されたノードで構成され、各ノードはint、doublestringobjectなど、異なる型のデータを格納できます
連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには `int``double``string``object` など、異なる型のデータを格納でき
対照的に、配列要素は同じ型である必要があり、これにより対応する要素位置にアクセスするためのオフセットを計算できます。例えば、intとlong型の両方を含む配列で、単一要素がそれぞれ4バイトと8バイトを占有する場合、配列に2つの異なる長さの要素が含まれているため、以下の式を使用してオフセットを計算できません
これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に `int``long` の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占る場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない
```shell
# 要素メモリアドレス = 配列メモリアドレス + 要素長 * 要素インデックス
# 要素メモリアドレス = 配列メモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス
```
**Q**:ノードを削除した後、`P.next``None`に設定する必要ありますか?
**Q**:ノード `P` を削除した後、`P.next``None` に設定する必要ありますか?
`P.next`を変更しなくても問題ありません。連結リストの観点から、ヘッドノードからテールノードまでの巡回で`P`に遭遇することはもうありません。これは、ノード`P`がリストから効果的に削除されたことを意味し、`P`が指す場所はもはやリストに影響しません
`P.next` を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや `P` に出会うことはない。つまり、ノード `P` はすでに連結リストから削除されており、この時点で `P` がどこを指していても、この連結リストに影響しない
ガベージコレクションの観点から、Java、Python、Goなどの自動ガベージコレクションメカニズムを持つ言語では、ノード`P`が収集されるかどうかは、それを指す参照がまだあるかどうかに依存し、`P.next`の値には依存しません。CやC++などの言語では、ノードのメモリを手動で解放する必要があります
データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある
**Q**:連結リストでは、挿入と削除操作の時間計算量は`O(1)`です。しかし、挿入や削除前の要素検索には`O(n)`時間がかかるので、なぜ時間計算量は`O(n)`ではないのですか?
**Q**:連結リストで挿入と削除の時間計算量は $O(1)$ です。しかし、追加や削除の前には要素を探すのに $O(n)$ の時間が必要です。では、なぜ時間計算量は $O(n)$ ではないのですか?
要素を最初に検索してから削除する場合、時間計算量は確かに`O(n)`です。しかし、連結リストの挿入と削除における`O(1)`の利点は他のアプリケーションで実現できます。例えば、連結リストを使用した両端キューの実装では、常にヘッドとテールノードを指すポインタを維持、各挿入削除操作`O(1)`にします
要素を先に探してから削除するのであれば、時間計算量が $O(n)$ であるのは確かである。しかし、連結リストの $O(1)$ での追加・削除という利点は、ほかの用途で生かせる。たとえば、両端キューは連結リストで実装するのに適しており、先頭ノードと末尾ノードを常に指すポインタ変数を維持すれば、各挿入削除操作はどれも $O(1)$ になる
**Q**:「連結リストの定義と格納方法」の図で、薄青色の格納ノードは単一のメモリアドレスを占有しますか、それともノード値と半分を共有しますか?
**Q**「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか?
図は単なる定性的な表現であり、定量的分析は特定の状況に依存します
この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある
- 異なる型のノード値は異なる量の空間を占有します。例えば、int、long、double、オブジェクトインスタンスです
- ポインタ変数によって占有されるメモリ空間は、使用されるオペレーティングシステムとコンパイル環境に依存し、通常8バイトまたは4バイトで
- ノード値が占める領域は型によって異なり、たとえば `int``long``double`、インスタンスオブジェクトなどがある
- ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである
**Q**:リストの末尾への要素追加は常に`O(1)`ですか?
**Q**:リストの末尾への要素追加は常に $O(1)$ ですか?
要素を追加することでリスト長を超える場合、リストは最初に拡張される必要があります。システムは新しいメモリブロックを要求し、元のリストのすべての要素を移動するため、この場合の時間計算量は`O(n)`になります
要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要があ。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は $O(n)$ になる
**Q**:「リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間無駄につながる可能性があります」というは、容量、長さ、拡張係数などの追加変数によって占有されるメモリを指していますか?
**Q**:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間無駄にる可能性があ」というは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか?
ここで空間の無駄は主に2つの側面を指します:一方で、リストは初期長設定されますが、常に必要とは限りません。他方で、頻繁な拡張を防ぐため、拡張は通常$\times 1.5$などの係数で乗算されます。これにより多くの空きスロットが生まれ、通常完全に埋めることできません
ここでいう空間の無駄は主に 2 つの意味がある。一方で、リストは初期長設定されるが、必ずしもそれだけ必要とは限らない。もう一方で、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば $\times 1.5$ を掛ける。このため、多くの空きスロットが生、通常それらを完全に埋めることできない
**Q**Python`n = [1, 2, 3]`を初期化した後、これら3つの要素のアドレスは連続していますが、`m = [2, 1, 3]`を初期化すると、各要素の`id`は連続していないが`n`のものと同一です。これらの要素のアドレスが連続していない場合`m`はまだ配列ですか?
**Q**Python`n = [1, 2, 3]` を初期化した後、この 3 つの要素のアドレスは連続しています。しかし `m = [2, 1, 3]` を初期化すると、各要素の id は連続しておらず、それぞれ `n` 内の同じ値と一致していることがわかります。これらの要素のアドレスが連続していないなら`m` も配列なのですか?
リスト要素を連結リストノード`n = [n1, n2, n3, n4, n5]`に置き換える場合、これら5つのノードオブジェクトも通常メモリ全体に分散しています。しかし、リストインデックスが与えられれば、`O(1)`時間でノードのメモリアドレスにアクセスでき、対応するノードにアクセスできます。これは、配列がノード自体ではなく、ノードへの参照を格納するためです
仮にリスト要素を連結リストノード `n = [n1, n2, n3, n4, n5]` に置き換えたとしても、通常この 5 つのノードオブジェクトもメモリ上の各所に分散して格納される。それでも、与えられたリストインデックスに対して、私たちは依然として $O(1)$ 時間でノードのメモリアドレスを取得し、対応するノードにアクセスでき。これは、配列に格納されているのがノードそのものではなく、ノードへの参照だからである
多くの言語と異なり、Pythonでは数値もオブジェクトとしてラップされ、リストは数値自体ではなく、これらの数値への参照を格納します。したがって、2つの配列の同じ数値が同`id`を持ち、これらの数値のメモリアドレスは連続である必要がないことがわかります
多くの言語と異なり、Python では数値もオブジェクトとしてラップされており、リストに格納されているのは数値そのものではなく、数値への参照である。そのため、2 つの配列の同じ数値が同一の id を持つことがあり、しかもそれらの数値のメモリアドレスは連続している必要がない。
**Q**C++ STL`std::list`はすでに双方向連結リストを実装していますが、一部のアルゴリズム書籍では直接使用していないようです。何か制がありますか?
**Q**C++ STL`std::list` はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制があるのでしょうか?
一方で、アルゴリズム実装する際は配列を使用することを好み、必要な場合のみ連結リストを使用します。主に2つの理由があります
一方では、私たちは多くの場合、アルゴリズム実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある
- 空間オーバーヘッド:各要素に2つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list`は通常`std::vector`より多くの空間を占有します
- キャッシュ非友好的:データが連続して格納されていないため、`std::list`はキャッシュ利用率が低くなります。一般に、`std::vector`の方がパフォーマンスが優れています
- 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、`std::list` は通常 `std::vector` より多くの空間を消費する
- キャッシュ非効率:データが連続して格納されていないため、`std::list` はキャッシュ利用率が低。一般に`std::vector` のほうが性能がよい
方で、連結リストは主に二分木とグラフに必要です。スタックキューは、連結リストではなく、プログラミング言語の`stack``queue`クラスを使用して実装されることが多いです
もう一方で、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックキューについては、連結リストではなく、たいてい言語が提供する `stack``queue` を使う
**Q**リスト`res = [0] * self.size()`を初期化すると、`res`の各要素は同じアドレスを参照しますか?
**Q**`res = [[0]] * n` という操作で 2 次元リストを生成した場合、それぞれの `[0]` は独立していますか?
いいえ。しかし、この問題は二次元配列で発生します。例えば、二次元リスト`res = [[0]] * self.size()`を初期化すると、同じリスト`[0]`を複数回参照することになります
独立していない。この 2 次元リストでは、すべての `[0]` は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる
**Q**:ノードを削除する際、その後続ノードへの参照を断つ必要がありますか?
2 次元リスト内の各 `[0]` を独立させたい場合は、`res = [[0] for _ in range(n)]` を使って実現できる。この方式の原理は、独立した `[0]` リストオブジェクトを $n$ 個初期化していることにある。
データ構造とアルゴリズム(問題解決)の観点から、プログラムのロジックが正しい限り、リンクを断たなくても問題ありません。標準ライブラリの観点から、リンクを断つ方が安全で論理的に明確です。リンクを断たず、削除されたノードが適切にリサイクルされない場合、後続ノードのメモリのリサイクルに影響を与える可能性があります。
**Q**`res = [0] * n` という操作で生成されたリストでは、それぞれの整数 0 は独立していますか?
このリストでは、すべての整数 0 が同一オブジェクトへの参照である。これは、Python が小さな整数(通常は -5 から 256)に対してキャッシュプール機構を採用し、オブジェクトの再利用を最大化して性能を向上させているためである。
それらは同じオブジェクトを指しているが、それでもリスト内の各要素は独立して変更できる。これは、Python の整数が「イミュータブルオブジェクト」だからである。ある要素を変更するとき、実際には別のオブジェクトへの参照に切り替わるのであって、元のオブジェクトそのものを変更しているわけではない。
しかし、リスト要素が「ミュータブルオブジェクト」(たとえばリスト、辞書、クラスインスタンスなど)である場合は、ある要素を変更するとそのオブジェクト自体が直接変更され、そのオブジェクトを参照しているすべての要素に同じ変化が生じる。
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -9,14 +9,14 @@ icon: material/map-marker-path
!!! 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.3 &nbsp; 部分和問題](subset_sum_problem.md)
- [13.4 &nbsp; n クイーン問題](n_queens_problem.md)
- [13.5 &nbsp; まとめ](summary.md)
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+15 -15
View File
@@ -4,24 +4,24 @@ comments: true
# 13.5 &nbsp; まとめ
### 1. &nbsp; 重要な復習
### 1. &nbsp; 重要なポイントの振り返り
- バックトラッキングアルゴリズムの本質は全探索です。解空間深さ優先走査を実行することで条件を満たす解を求めます。検索中に満足のいく解が見つかった場合、それを記録し、すべての解見つるか走査が完了するまで続けます。
- バックトラッキングアルゴリズムの検索プロセスには試行と後退が含まれます。深さ優先探索を使用して様々な選択を探索し、選択が制約を満たさない場合、前の選択を取り消します。そして前の状態に戻って他のオプションを試し続けます。試行と後退は反対方向の操作です。
- バックトラッキング問題には通常複数の制約が含まれます。これらの制約は剪定操作を実行するために使用できます。剪定は不要な索分岐を事前に終了し、検索効率を大幅に向上させることができます。
- バックトラッキングアルゴリズムは主に索問題と制約足問題解決するために使用されます。組み合わせ最適化問題バックトラッキングを使用して解決できますが、多くの場合、より効率または効果的な解決方法が利用可能です
- 順列問題は、与えられた集合要素のすべての可能な順列を検索することを目的とします。各要素が選択されたかどうかを記録するために配列を使用し、同じ要素重複選択を避けます。これにより、各要素が度だけ選択されることが保証されます。
- 順列問題では、集合に重複要素が含まれている場合、最終結果に重複順列が含まれます。同一要素が各ラウンドで一度だけ選択できるように制する必要があり、これは通常ハッシュセットを使用して実装されます。
- 部分集合和問題は、与えられた集合でターゲット値に合計する全ての部分集合を見つけることを目的とします。集合は要素順序を区別しませんが、検索プロセスでは重複する部分集合が生成される可能性があります。これは、アルゴリズムが異なる要素順序を独特のパスとして探索するために発生します。バックトラッキング前にデータをソートし、各ラウンドの走査開始を示す変数を設定します。これにより、重複する部分集合を生成する索分岐を剪定できます。
- 部分集合和問題では、配列の等しい要素重複集合を生成する可能性があります。配列がすでにソートされているという前提条件を使用し、隣接する要素が等しいかどうかを判定することで剪定を行います。これにより、等しい要素がラウンドごとに一度だけ選択されることが保証されます。
- $n$ クイーン問題は、2つのクイーンが互いに攻撃できないように $n \times n$ のチェスボードに $n$ 個のクイーンを配置する方を見つけることを目的とします。問題の制約には行制約、列制約、および主対角線副対角線制約が含まれます。行制約を満たすため、行ごとに1つのクイーンを配置する戦略を採用し、各行に1つのクイーンが配置されることを保証します。
- 列制約と対角線制約の処理は似ています。列制約については、各列にクイーンがるかどうかを記録する配列を使用し、選択されたセルが合法かどうかをします。対角線制約については、2つの配列を使用して主対角線と副対角線それぞれクイーン存在記録します。課題は、同じ主対角線または副対角線上のセルの行と列のインデックス間の関係を決定することです。
- バックトラッキングアルゴリズムの本質は全探索法であり、解空間深さ優先走査することで条件を満たす解を探索します。探索の過程で条件を満たす解に出会ったら記録し、すべての解見つるか探索が完了するまで続けます。
- バックトラッキングアルゴリズムの探索過程は、試行と戻るという 2 つの部分から成ります。深さ優先探索によってさまざまな選択を試し、制約条件を満たさない状況に遭遇した場合は直前の選択を取り消して前の状態に戻り、ほかの選択を引き続き試します。試行と戻るは互いに逆方向の操作です。
- バックトラッキング問題には通常複数の制約条件が含まれており、それらを枝刈りに利用できます。枝刈りによって不要な索分岐を早期に打ち切り、探索効率を大幅に高められます。
- バックトラッキングアルゴリズムは主に索問題と制約足問題解決に用いられます。組せ最適化問題バックトラッキングで解けますが、より効率またはより適した解法が存在することが少なくありません
- 順列問題の目的は、与えられた集合要素のすべての可能な並べ方を探索することです。各要素が選択済みかどうかを配列で記録し、同じ要素重複して選ぶ探索分岐を刈り取ることで、各要素が 1 度だけ選ばれるようにします。
- 順列問題では、集合に重複要素があると最終結果に重複した順列がれます。各ラウンドで等しい要素は 1 回しか選べないように制する必要があり、通常ハッシュ集合を用いて実現します。
- 部分和問題の目標は、与えられた集合の中から和が目標値となるすべての部分集合を見つけることす。集合は要素順序を区別しませんが、探索過程では順序違いの結果も出力されるため、重複部分集合が生じます。そこで、バックトラッキング前にデータをソートし、各ラウンドの走査開始位置を示す変数を設定することで、重複部分集合を生成する索分岐を枝刈りします。
- 部分和問題では、配列の等しい要素重複集合を生みます。配列がソート済みであるという前提を利用し、隣接要素が等しいかどうかを判定して枝刈りすることで、等しい要素がラウンドで 1 回しか選ばれないようにします。
- $n$ クイーン問題の目的は、$n \times n$ の盤面に $n$ 個のクイーンを配置する方を見つけることであり、どの 2 つのクイーンも互いに攻撃できないことが条件です。この問題の制約には行制約、列制約、主対角線制約、副対角線制約があります。行制約を満たすため、行ごとに配置する戦略を採用し、各行に 1 個のクイーンを置くことを保証します。
- 列制約と対角線制約の扱い方は似ています。列制約については、各列にクイーンが存在するかどうかを配列で記録し、選択したマスが有効かどうかを判定します。対角線制約については、主対角線と副対角線それぞれクイーン存在するかを 2 つの配列で記録します。難点は、同じ主対角線または副対角線上にあるマスが満たす行列インデックスの規則を見つけることにあります。
### 2. &nbsp; Q & A
**Q**: バックトラッキングと再帰の関係どのように理解すればよいですか?
**Q**バックトラッキングと再帰の関係どのように理解すればよいですか?
全体的に、バックトラッキングは「アルゴリズム戦略」であり、再帰はより「ツール」です。
全体として見ると、バックトラッキングは「アルゴリズム戦略」の一種であり、再帰はむしろ「道具」に近いものです。
- バックトラッキングアルゴリズムは通常再帰に基づいてます。しかし、バックトラッキングは再帰の応用シナリオの一つであり、特に検索問題においてです。
- 再帰の構造は「部分問題分解」問題解決パラダイムを反映します。分割統治、バックトラッキング、動的プログラミング(メモ化再帰)を含む問題の解決でよく使用されます。
- バックトラッキングアルゴリズムは通常再帰に基づいて実装されます。ただし、バックトラッキングは再帰の応用場面の 1 つであり、索問題における再帰の応用です。
- 再帰の構造は「部分問題への分解」という問題解決パラダイムを表しており、分割統治、バックトラッキング、動的計画法(メモ化再帰)などの問題によく用いられます。
@@ -3,19 +3,19 @@ comments: true
icon: material/timer-sand
---
# 第 2 章 &nbsp; 複雑度解析
# 第 2 章 &nbsp; 計算量解析
![Complexity analysis](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" }
![計算量解析](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" }
!!! abstract
複雑度解析は、アルゴリズムの広大な宇宙における時空のナビゲーターのようなものです。
時間と空間の次元をより深く探求し、より優雅な解決策を求めるためのガイドとなります。
計算量解析は、広大なアルゴリズム宇宙における時空の案内人のようなものです。
それは、時間と空間という二つの次元で私たちをより深く探求へ導き、より洗練された解決策を見つけ出します。
## 章の内容
- [2.1 &nbsp; アルゴリズム効率評価](performance_evaluation.md)
- [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)
File diff suppressed because it is too large Load Diff
@@ -2,52 +2,52 @@
comments: true
---
# 2.1 &nbsp; アルゴリズム効率評価
# 2.1 &nbsp; アルゴリズム効率評価
アルゴリズム設計において、私たちは順序に従って以下の2つの目標を追求します。
アルゴリズム設計では、次の 2 つのレベルの目標を順に追求します。
1. **問題の解決策を見つける**: アルゴリズムは、指定された入力範囲内で確実に正しい解を見つけることができるべきです。
2. **最適解を求め**: 同じ問題に対して複数の解決策が存在する場合があり、私たちは可能な限り最も効率的なアルゴリズムを見つけることを目指します。
1. **問題の解を見つける**アルゴリズムは、定められた入力範囲内で問題の正しい解を確実に求められる必要があります。
2. **最適な解法を追求す**同じ問題に対して複数の解が存在する場合があり、私たちはできるだけ効率的なアルゴリズムを見つけたいと考えます。
つまり、問題を解決できることを前提として、アルゴリズム効率がアルゴリズムを評価する主要な基準となっており、これには以下の2つの次元が含まれます。
つまり、問題を解ることを前提として、アルゴリズム効率はその良し悪しを測る主要な評価指標となっており、次の 2 つの観点を含みます。
- **時間効率**: アルゴリズム実行される速度
- **空間効率**: アルゴリズムが占有するメモリ空間のサイズ
- **時間効率**アルゴリズム実行時間の長さ。
- **空間効率**アルゴリズムが使用するメモリ空間の大きさ
要するに、**私たちの目標は高速でメモリ効率の良いデータ構造とアルゴリズムを設計することです**。アルゴリズム効率を効果的に評価することは重要です。なぜなら、そうすることで初めて様々なアルゴリズムを比較し、アルゴリズム設計と最適化プロセスを導くことができるからです。
簡単に言えば、**私たちの目標は高速でメモリ」なデータ構造とアルゴリズムを設計すること**です。そして、アルゴリズム効率を効果的に評価することは非常に重要です。そうすることで初めて、さまざまなアルゴリズムを比較し、さらにアルゴリズム設計と最適化の過程を導けるからです。
効率評価は主に2つの方法があります:実際のテストと理論的推定です。
効率評価方法は主に 2 種類に分けられます。実測と理論的な見積もりです。
## 2.1.1 &nbsp; 実際のテスト
## 2.1.1 &nbsp; 実
アルゴリズム`A``B`があり、どちらも同じ問題を解決でき、それらの効率を比較する必要があるとします。最も直接的な方法はコンピュータを使用してこれら2つのアルゴリズムを実行し、実行時間とメモリ使用量を監視記録することです。この評価方法は実際の状況を反映ますが、大きな制限があります。
いまアルゴリズム `A` とアルゴリズム `B` があり、どちらも同じ問題を解けるとします。この 2 つのアルゴリズムの効率を比較する必要がある場合、最も直接的な方法は 1 台のコンピュータで両者を実行し、その実行時間とメモリ使用量を監視して記録することです。この評価方法は実際の状況を反映できますが、大きな制約もあります。
一方で、**テスト環境からの干渉を排除することは困難です**。ハードウェア構成はアルゴリズムの性能に影響を与える可能性があります。えば、並列度の高いアルゴリズムはマルチコアCPUでの実行により適していますし、集約的なメモリ操作を含むアルゴリズムは高性能メモリでより良い性能を発揮します。アルゴリズムのテスト結果は、異なるマシン間で変わる可能性があります。これは、平均効率を計算するために複数のマシンでテストすることが実用的でないことを意味します
一方で、**テスト環境による干渉要因を排除しにくい**という問題があります。ハードウェア構成はアルゴリズムの性能に影響ます。たとえば、並列度の高いアルゴリズムはマルチコア CPU での実行により適しており、メモリアクセスが集中的なアルゴリズムは高性能メモリでより良い性能をします。つまり、異なるマシンでのテスト結果は一致しない可能性があります。これは、さまざまなマシンでテストして平均効率を統計的に求める必要があることを意味しますが、それは現実的ではありません
方で、**完全なテストを実施することは非常にリソース集約的です**。アルゴリズムの効率は入力データサイズによって変わります。えば、データ量が少ない場合はアルゴリズム`A``B`より速く実行される可能性がありますが、データ量が多い場合はテスト結果が逆になる可能性があります。したがって、説得力のある結論を導くためには、幅広い入力データサイズをテストする必要があり、れには過度な計算リソースが必要になります。
方で、**完全なテストを実施するは非常に多くの資源が必要**です。入力データ量が変化すると、アルゴリズムは異なる効率を示します。たとえば、入力データ量が小さいときはアルゴリズム `A` の実行時間がアルゴリズム `B` より短くても、入力データ量が大きいときには結果がちょうど逆になるかもしれません。そのため、説得力のある結論を得るには、さまざまな規模の入力データテストする必要があり、れには大量の計算資源を要します。
## 2.1.2 &nbsp; 理論的推定
## 2.1.2 &nbsp; 理論的な見積もり
際のテストの大きな制限により、計算のみでアルゴリズムの効率を評価することを検討できます。この推定方法は<u>漸近的複雑度解析</u>、または単に<u>複雑度解析</u>として知られています。
測には大きな制約があるため、いくつかの計算だけによってアルゴリズムの効率を評価することを考えられます。この見積もり方法は<u>漸近計算量解析(asymptotic complexity analysis</u>と呼ばれ、略して<u>計算量解析</u>といます。
複雑度解析は、アルゴリズムの実行に必要な時間と空間リソースと入力データのサイズとの関係を反映します。**これは、入力データのサイズが増加するにつれて、アルゴリズムに必要な時間と空間の増加傾向を記述します**。この定義は複雑に聞こえるかもしれませんが、より良く理解するために3つの重要なポイントに分解できます。
計算量解析は、アルゴリズムの実行に必要な時間資源と空間資源が入力データ規模とどのような関係にあるかを表します。**これは、入力データ規模が増加するにつれて、アルゴリズムの実行に必要な時間と空間がどのように増加するかという傾向を記述するものです**。この定義はややわかりにくいので、次の 3 つのポイントに分けて理解できます。
- 「時間と空間リソース」は、それぞれ<u>時間計算量</u>と<u>空間計算量</u>に対応します。
- 「入力データのサイズが増加するにつれて」は、複雑度がアルゴリズムの効率と入力データとの関係を反映ることを意味します。
- 「時間と空間の増加傾向」は、複雑度解析が実行時間や占有空間の具体的な値ではなく、時間や空間増加する「率」に焦点を当てることを示します。
- 「時間資源と空間資源」は、それぞれ<u>時間計算量time complexity</u>と<u>空間計算量(space complexity</u>に対応します。
- 「入力データ規模が増加するにつれて」は、計算量がアルゴリズムの実行効率と入力データ規模との関係を反映していることを意味します。
- 「時間と空間の増加傾向」は、計算量解析が注目するのは実行時間や使用空間の具体的な値ではなく、時間や空間増加の「速さ」であることを示します。
**複雑度解析は実際のテスト方法の欠点を克服します**これは以下の側面で反映されます
**計算量解析は実測という方法の欠点を克服しています**その点は次のように表れます
- 実際にコードを実行する必要がないため、より環境にしくエネルギー効率が良いです。
- テスト環境に依存せず、すべての動作プラットフォームに適用できます。
- 異なるデータ量でのアルゴリズム効率を反映でき、特に大量データでのアルゴリズムの性能を示します。
- 実際にコードを動かす必要がな、より環境にやさしくエネルギーです。
- テスト環境から独立しており、解析結果はすべての実行プラットフォームに適用できます。
- 異なるデータ量におけるアルゴリズム効率を表せ、とくに大規模データでの性能を反映できます。
!!! tip
複雑度の概念についてまだ混乱している場合でも、心配しないでください。以降の章で詳しく取り上げます。
それでも計算量の概念がまだわかりにくくても、心配はいりません。後続の章で詳しく説明します。
複雑度解析は、アルゴリズム効率を評価する「ものさし」を提供し、実行に必要な時間と空間リソースを測定し、異なるアルゴリズムの効率を比較することを可能にします。
計算量解析は、アルゴリズム効率を評価するための「物差し」を私たちに与えてくれます。これにより、あるアルゴリズムの実行に必要な時間資源と空間資源を測り、異なるアルゴリズム同士の効率を比較できます。
複雑度は数学的概念であり、初者には抽象的で困難かもしれません。この観点から、複雑度解析は最初に紹介するのに最も適したトピックではないかもしれません。しかし、特定のデータ構造やアルゴリズムの特性について議論するとき、その速度空間使用量を分析することを避けるのは困難です
計算量は数学的概念であり、初者にとってはやや抽象的で、学習の難度も比較的高いかもしれません。この観点から見ると、計算量解析は最初に紹介する内容としてはあまり適していない可能性があります。しかし、あるデータ構造やアルゴリズムの特徴を議論する際には、その実行速度空間使用状況の分析を避けることはできません
要約すると、データ構造とアルゴリズム深く入る前に複雑度解析の基本的な理解を身につけることをお勧めします。**これにより、簡単なアルゴリズムで複雑度解析を実行できるようになります**
以上を踏まえると、データ構造とアルゴリズム深く学ぶ前に、**まず計算量解析について初歩的な理解を持ち、簡単なアルゴリズムの計算量解析ができるようにしておくこと**を勧めます
File diff suppressed because it is too large Load Diff
@@ -4,50 +4,56 @@ comments: true
# 2.5 &nbsp; まとめ
### 1. &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$ で表され、関数の漸近上界に対応し、$n$ が正の無限大に近づくときの操作回数 $T(n)$ の増加の度合いを表します。
- 時間計算量の推定は二段階に分かれ、まず操作数を数え、次に漸近上界を判断します。
- 一般的な時間計算量を低い順から並べると、$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)$などが含まれます。
- 空間計算量の役割は時間計算量に似ており、アルゴリズムが使用するメモリ空間がデータ量の増加に伴ってどう変化するかを測ります。
- アルゴリズム実行中に関係するメモリ空間は、入力空間、一時空間、出力空間に分けられます。通常、入力空間は空間計算量の計算に含ません。一時空間は一時データ、スタックフレーム空間、命令空間に分けられ、このうちスタックフレーム空間は通常、再帰関数でのみ空間計算量に影響します。
- 私たちは通常最悪空間計算量のみに注目し、最悪の入力データと最悪の実行時点における空間計算量を数えます。
- 一般的な空間計算量を低い順から並べると、$O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$、$O(2^n)$ などがあります。
### 2. &nbsp; Q & A
**Q**: 末尾再帰の空間計算量は$O(1)$ですか?
**Q**尾再帰の空間計算量は $O(1)$ ですか?
理論的には、末尾再帰関数の空間計算量は$O(1)$最適化できます。しかし、ほとんどのプログラミング言語(Java、Python、C++、Go、C#など)は尾再帰の自動最適化をサポートしていないため、一般的に空間計算量$O(n)$と考えられています。
理論上、尾再帰関数の空間計算量は $O(1)$ まで最適化できます。ただし、ほとんどのプログラミング言語(Java、Python、C++、Go、C# など)は尾再帰の自動最適化をサポートしていないため、通常は空間計算量$O(n)$ と見なします。
**Q**: 「関数」と「メソッドという用語の違いは何ですか?
**Q**:関数とメソッドという二つの用語の違いは何ですか?
<u>関数</u>は独立して実行でき、すべてのパラメータが明示的に渡されます。<u>メソッド</u>はオブジェクトに関連付けられ、それを呼び出すオブジェクト暗黙的に渡され、クラスのインスタンスに含まれるデータを操作できます。
<u>関数function</u>は独立して実行でき、すべての引数は明示的に渡されます。<u>メソッドmethod</u>はオブジェクトに関連付けられ、それを呼び出すオブジェクト暗黙的に渡され、クラスのインスタンスに含まれるデータを操作できます。
一般的なプログラミング言語からの例をいくつか示します
以下では、いくつかの一般的なプログラミング言語を例に説明します
- Cは手続き型プログラミング言語で、オブジェクト指向の概念がないため、関数のみがあります。しかし、構造体(struct)を作成することでオブジェクト指向プログラミングをシミュレートでき、これらの構造体に関連付けられた関数は他のプログラミング言語のメソッドと同等です。
- JavaとC#はオブジェクト指向プログラミング言語で、コードブロック(メソッド)は通常クラスの一部です。静的メソッドはクラスにバインドされ、特定のインスタンス変数にアクセスできないため、関数のように動作します
- C++Python手続き型プログラミング(関数)オブジェクト指向プログラミング(メソッド)の両方をサポートしています。
- C 言語は手続き型プログラミング言語であり、オブジェクト指向の概念がないため、関数しかありません。ただし、構造体(struct)を作成してオブジェクト指向プログラミングを模倣でき、構造体に関連付けられた関数は他のプログラミング言語におけるメソッドに相当します。
- Java と C# はオブジェクト指向プログラミング言語であり、コードブロック(メソッド)は通常あるクラスの一部です。静的メソッドの振る舞いは関数に似ており、クラスに束縛され、特定のインスタンス変数にアクセスできません
- C++Python は、手続き型プログラミング(関数)にもオブジェクト指向プログラミング(メソッド)にも対応しています。
**Q**: 「空間計算量の一般的な種類」の図は、占有空間の絶対サイズを反映していますか?
**Q**:「一般的な空間計算量の種類」の図が表しているのは、使用空間の絶対量ですか?
いいえ、図は空間計算量を示しており、これは増加傾向を反映するものであり、占有空間の絶対サイズではありません。
いいえ。この図が示しているのは空間計算量であり、表しているのは増加傾向であって、使用空間の絶対ではありません。
$n = 8$を取ると、各曲線の値がその関数に対応していないことに気づくかもしれません。これは、各曲線に定数項が含まれているためで、値の範囲を視覚的に快適な範囲圧縮することを意図しています。
$n = 8$ と仮定すると、各曲線の値が対応する関数と一致していないように見えるかもしれません。これは、各曲線に定数項が含まれており、値の範囲を視覚的に見やすい範囲圧縮しているためです。
実際には、通常は各メソッドの「定数項」複雑度を知らないため、複雑度のみに基づいて$n = 8$の最良ソリューションを選択することは一般的に不可能です。しかし、$n = 8^5$の場合、増加傾向が支配的になるため、選択がはるかに容易になります。
実際には、各手法の「定数項」複雑度がどれほどか通常は分からないため、一般に複雑度だけを根拠に $n = 8$ 以下で最適解を選ぶことはできません。ただし、$n = 8^5$ であれば選びやすく、このときは増加傾向がすでに支配的になっています。
**Q** 実際の利用場面に応じて、時間(または空間)を犠牲にしてアルゴリズムを設計することはありますか?
実際の応用では、多くの場合、空間を犠牲にして時間を得る選択をします。たとえばデータベースのインデックスでは、通常 B+ 木やハッシュインデックスを構築し、大量のメモリ空間を使う代わりに、$O(\log n)$ あるいは $O(1)$ の高速な検索を実現します。
空間資源が貴重な場面では、時間を犠牲にして空間を得ることもあります。たとえば組み込み開発では、デバイスのメモリが非常に貴重なため、エンジニアはハッシュテーブルの使用をやめ、配列による順次探索を選んでメモリ使用量を節約することがあります。その代償として探索は遅くなります。
File diff suppressed because it is too large Load Diff
@@ -4,71 +4,71 @@ comments: true
# 3.2 &nbsp; 基本データ型
コンピュータ内のデータについて考える、テキスト、画像、動画、音声、3Dモデルなど、様々な形式が思い浮かます。これらの組織的な形式は異なりますが、すべて様々な基本データ型から構成されています。
コンピュータ内のデータについて考えるとき、テキスト、画像、動画、音声、3D モデルなど、さまざまな形態を思い浮かます。これらのデータの構成形式はそれぞれ異なりますが、いずれも各種の基本データ型によって成り立っています。
**基本データ型とは、CPUが直接操作できるもの**であり、アルゴリズムで直接使用されます。主に以下が含まれます。
**基本データ型CPU が直接演算できる**であり、アルゴリズムの中で直接使れます。主なものは次のとおりです。
- 整数型`byte``short``int``long`
- 浮動小数点型:`float``double`小数を表現するために使
- 文字型`char`、様々な言語の文字、句読点、さらには絵文字を表現するために使用
- ブール型:`bool`、「はい」または「いいえ」の判を表現するために使
- 整数型 `byte``short``int``long`
- 浮動小数点数型 `float``double` 小数を表ために使います。
- 文字型 `char` ,各言語の文字、句読点、さらには絵文字などを表すために使います。
- 真偽値型 `bool` ,真か偽かの判を表ために使います。
**基本データ型はコンピュータ内で二進形式で格納されます**。1つの二進桁は1ビットです。ほとんどの現代的なオペレーティングシステムでは、1バイトは8ビットで構成されています。
**基本データ型はコンピュータ内で 2 進数の形で格納されます**。1 つの二進桁は $1$ ビットです。現代のほとんどのオペレーティングシステムでは、$1$ バイト(byte)は $8$ ビット(bitで構成されます。
基本データ型の値の範囲は、それらが占める空間のサイズに依存します。以下ではJavaを例に説明します。
基本データ型の値は、その型が占める領域の大きさによって決まります。以下では Java を例に取ります。
- 整数型`byte`は1バイト = 8ビットを占め、$2^8$個の数を表現できます。
- 整数型`int`は4バイト = 32ビットを占め、$2^{32}$個の数を表現できます。
- 整数型 `byte` は $1$ バイト = $8$ ビットを占め、$2^{8}$ 個の数を表ます。
- 整数型 `int` は $4$ バイト = $32$ ビットを占め、$2^{32}$ 個の数を表ます。
以下の表は、Javaにおける様々な基本データ型が占める空間、値の範囲、デフォルト値を示しています。この表を暗記する必要はありませんが、一般的な理解を持ち、必要時に参照することをお勧めします
表は、Java における各種基本データ型の使用領域、値域、デフォルト値を示したものです。この表を暗記する必要はなく、大まかに理解しておけば十分であり、必要になったときに参照すればかまいません
<p align="center"> 表 3-1 &nbsp; 基本データ型が占める空間と値の範囲 </p>
<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}$ |
| 型 | 記号 | 使用領域 | 最小値 | 最大値 | デフォルト値 |
| ------ | -------- | -------- | ------------------------ | ----------------------- | -------------- |
| 整数 | `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の基本データ型に特有であることにご注意ください。すべてのプログラミング言語には独自のデータ型定義があり、占有空間、値の範囲、デフォルト値異なる場合があります。
注意してください。上表は Java の基本データ型に対するものです。各プログラミング言語にはそれぞれ独自のデータ型定義があり、使用領域、値域、デフォルト値異なる場合があります。
- Pythonでは、整数型`int`は任意のサイズになることができ、利用可能なメモリによってのみ制限されます。浮動小数点`float`は倍精度64ビットです。`char`型は存在せず、単一文字は実際には長さ1の文字列`str`です。
- CおよびC++では基本データ型のサイズが指定されておらず、実装プラットフォームによって異なります。上記の表はLP64[データモデル](https://en.cppreference.com/w/cpp/language/types#Properties)に従っており、LinuxmacOSを含むUnix 64ビットオペレーティングシステムで使用されています。
- CおよびC++における`char`のサイズは1バイトですが、ほとんどのプログラミング言語では、特定の文字エンコーディング方法に依存し、詳細は「文字エンコーディング」の章で説明されています
- ブール値の表現には1ビット(0または1)のみが必要ですが、通常はメモリ内に1バイトとして格納されます。これは、現代のコンピュータCPUが通常1バイトを最小のアドレス可能なメモリ単位として使用するためです。
- Python では、整数型 `int` は利用可能なメモリに制限されるだけで任意の大きさを取れます。浮動小数点`float` は倍精度 64 ビットです。`char` 型はなく、1 文字は実際には長さ 1 の文字列 `str` です。
- CC++ では基本データ型の大きさは明確に規定されておらず、実装プラットフォームによって異なります。上表は LP64 [データモデル](https://en.cppreference.com/w/cpp/language/types#Properties) に従っており、LinuxmacOS を含む Unix 64 ビット OS で用いられています。
- `char` の大きさは C と C++ では 1 バイトですが、多くのプログラミング言語では採用する文字エンコーディング方式によって決まります。詳しくは「文字エンコーディング」の章を参照してください
- 真偽値を表すのに必要なのは 1 ビット($0$ または $1$)だけですが、メモリ上では通常 1 バイトとして格納されます。これは、現代のコンピュータ CPU が通常 1 バイトを最小のアドレス指定可能なメモリ単位としてるためです。
では、基本データ型とデータ構造の関係は何でしょうかデータ構造とは、コンピュータ内でデータを組織し格納する方法であることを知っています。ここでの焦点は「データ」ではなく「構造」です。
では、基本データ型とデータ構造の間にはどのような関係があるのでしょうかデータ構造とは、コンピュータ内でデータを組織し格納する方法のことです。この言葉で主役なのは「データ」ではなく「構造」です。
「数値の列」を表したい場合、自然に配列の使用を考えます。これは、配列の線形構造が数値の隣接性と順序性を表現できるためですが、格納される内容が整数`int`、小数`float`、文字`char`のいずれであっても、「データ構造」とは関係です
「数字の並び」を表したいなら、自然に配列の使用を思い浮かべるでしょう。これは、配列の線形構造が数字どうしの隣接関係や順序関係を表せるからです。しかし、格納る内容が整数 `int` なのか、小数 `float` なのか、文字 `char` なのかは、「データ構造」とは関係ありません
言い換えると、**基本データ型はデータの「内容型」を提供し、データ構造はデータの「組織方法」を提供します**。例えば、以下のコードでは、同じデータ構造(配列)を使用して、`int``float``char``bool`など異なる基本データ型を格納表現しています。
言い換えると、**基本データ型はデータの「内容型」を提供し、データ構造はデータの「組織方法」を提供します**。たとえば次のコードでは、同じデータ構造(配列)を使って `int``float``char``bool` など異なる基本データ型を格納表現しています。
=== "Python"
```python title=""
# 様々な基本データ型を使用して配列を初期化
# さまざまな基本データ型配列を初期化する
numbers: list[int] = [0] * 5
decimals: list[float] = [0.0] * 5
# Pythonの文字は実際には長さ1の文字列
# Python の文字は実際には長さ 1 の文字列
characters: list[str] = ['0'] * 5
bools: list[bool] = [False] * 5
# Pythonのリストは様々な基本データ型とオブジェクト参照を自由に格納可能
# Python のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる
data = [0, 0.0, 'a', False, ListNode(0)]
```
=== "C++"
```cpp title=""
// 様々な基本データ型を使用して配列を初期化
// さまざまな基本データ型配列を初期化する
int numbers[5];
float decimals[5];
char characters[5];
@@ -78,7 +78,7 @@ comments: true
=== "Java"
```java title=""
// 様々な基本データ型を使用して配列を初期化
// さまざまな基本データ型配列を初期化する
int[] numbers = new int[5];
float[] decimals = new float[5];
char[] characters = new char[5];
@@ -88,7 +88,7 @@ comments: true
=== "C#"
```csharp title=""
// 様々な基本データ型を使用して配列を初期化
// さまざまな基本データ型配列を初期化する
int[] numbers = new int[5];
float[] decimals = new float[5];
char[] characters = new char[5];
@@ -98,7 +98,7 @@ comments: true
=== "Go"
```go title=""
// 様々な基本データ型を使用して配列を初期化
// さまざまな基本データ型配列を初期化する
var numbers = [5]int{}
var decimals = [5]float64{}
var characters = [5]byte{}
@@ -108,7 +108,7 @@ comments: true
=== "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)
@@ -118,14 +118,14 @@ comments: true
=== "JS"
```javascript title=""
// JavaScriptの配列は様々な基本データ型とオブジェクトを自由に格納可能
// JavaScript の配列はさまざまな基本データ型とオブジェクトを自由に格納できる
const array = [0, 0.0, 'a', false];
```
=== "TS"
```typescript title=""
// 様々な基本データ型を使用して配列を初期化
// さまざまな基本データ型配列を初期化する
const numbers: number[] = [];
const characters: string[] = [];
const bools: boolean[] = [];
@@ -134,7 +134,7 @@ comments: true
=== "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');
@@ -144,9 +144,9 @@ comments: true
=== "Rust"
```rust title=""
// 様々な基本データ型を使用して配列を初期化
// さまざまな基本データ型配列を初期化する
let numbers: Vec<i32> = vec![0; 5];
let decimals: Vec<f32> = vec![0.0, 5];
let decimals: Vec<f32> = vec![0.0; 5];
let characters: Vec<char> = vec!['0'; 5];
let bools: Vec<bool> = vec![false; 5];
```
@@ -154,7 +154,7 @@ comments: true
=== "C"
```c title=""
// 様々な基本データ型を使用して配列を初期化
// さまざまな基本データ型配列を初期化する
int numbers[10];
float decimals[10];
char characters[10];
@@ -164,6 +164,20 @@ comments: true
=== "Kotlin"
```kotlin title=""
// さまざまな基本データ型で配列を初期化する
val numbers = IntArray(5)
val decinals = FloatArray(5)
val characters = CharArray(5)
val bools = BooleanArray(5)
```
=== "Ruby"
```ruby title=""
# Ruby のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる
data = [0, 0.0, 'a', false, ListNode(0)]
```
??? pythontutor "実行の可視化"
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
@@ -4,94 +4,94 @@ comments: true
# 3.4 &nbsp; 文字エンコーディング *
コンピュータシステムでは、すべてのデータ二進形式で格納され、`char`も例外ではありません。文字を表現するために、各文字と二進数の一対一のマッピングを定義する「文字セット」を開発する必要があります。文字セットがあれば、コンピュータは表を参照して二進数を文字に変換できます。
コンピュータでは、すべてのデータ二進数の形で保存されており、文字 `char` も例外ではありません。文字を表すためには、「文字セット」を定義し、各文字と二進数の間の一対一の対応関係を定める必要があります。文字セットがあれば、コンピュータは対応表を参照して二進数から文字への変換を行えます。
## 3.4.1 &nbsp; ASCII文字セット
## 3.4.1 &nbsp; ASCII 文字セット
<u>ASCIIコード</u>は最も初期の文字セットの一つで、正式にはAmerican Standard Code for Information Interchangeとして知られています。7つの二進1バイトの下位7ビット)を使用して文字を表し、最大128種類の異なる文字を表現できます。以下の図に示すように、ASCIIには英の大文字と小文字、0〜9の数字、様々な句読点、特定の制御文字(改行やタブなど)が含まれています。
<u>ASCII コード</u>は最も早く登場した文字セットで、その正式名称は American Standard Code for Information Interchange(米国標準情報交換コード)です。これは 7 ビットの二進1 バイトの下位 7 ビット)で 1 文字を表し、最大128 種類の異なる文字を表現できます。下図のように、ASCII コードには英の大文字と小文字、数字 0 ~ 9、いくつかの句読点、そしていくつかの制御文字(改行やタブなど)が含まれます。
![ASCIIコード](character_encoding.assets/ascii_table.png){ class="animation-figure" }
![ASCII コード](character_encoding.assets/ascii_table.png){ class="animation-figure" }
<p align="center"> 図 3-6 &nbsp; ASCIIコード </p>
<p align="center"> 図 3-6 &nbsp; ASCII コード </p>
しかし、**ASCIIは英語の文字のみを表現できます**。コンピュータのグローバル化に伴い、より多くの言語を表現するために<u>EASCII</u>と呼ばれる文字セットが開発されました。ASCIIの7ビット構造から8ビット拡張し、256文字表現を可能にしました
しかし、**ASCII コードで表現できるのは英語だけです**。コンピュータのグローバル化に伴い、より多くの言語を表せる <u>EASCII</u> 文字セットが生まれました。これは ASCII の 7 ビットを 8 ビット拡張したもので、256 種類の異なる文字表現できます
世界的に、様々な地域固有のEASCII文字セットが導入されました。これらのセットの最初の128文字はASCIIと一致していますが、残りの128文字は異なる言語の要件に対応するために異なって定義されています。
世界では、さまざまな地域に適した EASCII 文字セットが次々に登場しました。これらの文字セットでは、前半の 128 文字は ASCII コードで統一され、後半の 128 文字は言語の要件に合わせて個別に定義されています。
## 3.4.2 &nbsp; GBK文字セット
## 3.4.2 &nbsp; GBK 文字セット
後に、**EASCIIでも多くの言語の文字要件を満たすことができない**ことが判明しました。例えば、中国語には約10万の漢字があり、そのうち数千が定期的に使用されています。1980年、中国標準化委員会は6763の中国語文字を含む<u>GB2312</u>文字セットを発表し、中国語のコンピュータ処理ニーズを本質的に満たしました。
その後、人々は**EASCII コードでも多くの言語に必要な文字数を満たせない**ことに気づきました。たとえば漢字は 10 万字近くあり、日常的に使うものだけでも数千字あります。中国国家標準総局は 1980 年に <u>GB2312</u> 文字セットを公開し、6763 字の漢字を収録して、漢字のコンピュータ処理の基本的な需要を満たしました。
しかし、GB2312は一部の稀少文字や繁体字を処理できませんでした。<u>GBK</u>文字セットはGB2312を拡張し、21886の中国語文字を含んでいます。GBKエンコーディングスキームでは、ASCII文字は1バイトで表現され、中国語文字は2バイトを使用します。
しかし、GB2312は一部の珍しい字や繁体字を扱えません。<u>GBK</u> 文字セットは GB2312 を基に拡張されたもので、合計 21886 字の漢字を収録しています。GBKエンコーディング方式では、ASCII 文字は 1 バイト、漢字は 2 バイトで表されます。
## 3.4.3 &nbsp; Unicode文字セット
## 3.4.3 &nbsp; Unicode 文字セット
コンピュータ技術急速発展と多数の文字セットおよびエンコーディング標準により、数多くの問題が発生しました。一方では、これらの文字セットは一般的に特定の言語の文字のみを定義し、多言語環境では適切に機能できませんでした。方では、同じ言語に対する複数の文字セット標準の存在により、異なるエンコーディング標準を使用するコンピュータ間で情報交換を行う際に文字化けが発生しました。
コンピュータ技術急速発展するにつれて、文字セットと符号化規格は百花繚乱の状態となり、それに伴って多くの問題も生じました。一方では、これらの文字セットは通常、特定の言語の文字しか定義しておらず、多言語環境では正常に動作できませんでした。もう一方では、同じ言語に複数の文字セット規格が存在し、2 台のコンピュータが異なる符号化規格を使っていると、情報伝達の際に文字化けが発生しました。
当時の研究者たちは考えました**世界のすべての言語と記号を含む包括的な文字セットが開発されれば、言語横断環境文字化けに関連する問題を解決できるのではないでしょうか?** このアイデアにインスパイアされて、広範囲な文字セットであるUnicodeが誕生しました。
当時の研究者たちはこう考えました**十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、言語環境文字化け問題を解決できるのではない**この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。
<u>Unicode</u>中国語で「统一码」(統一コード)と呼ばれ、理論的に100万文字以上を収容できます。世界中のすべての文字を単一のセットに組み込み、様々な言語の処理表示のための汎用文字セットを提供し、異なるエンコーディング標準による文字化けの問題を減らすことを目指しています。
<u>Unicode</u>中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。
1991年のリリース以来、Unicodeは新しい言語と文字を含むよう継続的に拡張されています。2022年9月現在、Unicodeには149,186文字が含まれており、様々な言語の文字、記号、さらには絵文字も含まれています。大なUnicode文字セットでは、一般的に使用される文字は2バイトを占有し、一部の稀少な文字は3バイトまたは4バイトを占有する場合があります。
1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。大な Unicode 文字セットでは、よく使われる文字は 2 バイトを占、一部の珍しい文字は 3 バイト、さらには 4 バイトを占めます。
Unicodeは各文字に数値(「コードポイント」と呼ばれる)を割り当てる汎用文字セットですが、**れらの文字コードポイントコンピュータシステムにどのように格納されるべきかは指定していません**。疑問が生じるかもしれません:システムはテキスト内の異なる長さのUnicodeコードポイントをどのように解釈するのでしょうか?例えば、2バイトのコードが与えられた場合、システムはそれが単一の2バイト文字を表すのか、2つの1バイト文字を表すのかをどのように判断するのでしょうか
Unicode は汎用文字セットであり、本質的には各文字に番号(「コードポイント」)を割り当てるものですが、**れらのコードポイントコンピュータ内でどのように保存するかまでは規定していません**。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか
**この問題に対する簡単な解決策は、すべての文字を等長エンコーディングとして格納することです**。以下の図に示すように、「Hello」の各文字は1バイトを占有し、「算法」(アルゴリズムの各文字は2バイトを占有します。上位ビットをゼロで埋めることで、「Hello 算法」のすべての文字を2バイトとしてエンコードできます。この方法により、システムは2バイトごとに文字を解釈し、フレーズの内容を復元できます。
この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズムの各文字は 2 バイトを占ます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。
![Unicodeエンコーディング例](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" }
![Unicode エンコーディング](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" }
<p align="center"> 図 3-7 &nbsp; Unicodeエンコーディング例 </p>
<p align="center"> 図 3-7 &nbsp; Unicode エンコーディング例 </p>
しかしASCIIが示したように、英語のエンコーディングには1バイトのみが必要です。上記のアプローチを使用すると、英語テキストが占有する空間ASCIIエンコーディングと比較して2倍になり、メモリ空間の無駄になります。したがって、より効率的なUnicodeエンコーディング方が必要です。
しかし ASCII コードはすでに、英語の符号化には 1 バイトで十分であることを示しています。上記の方式を採用すると、英語テキストが占る空間ASCII エンコーディング時の 2 倍になり、メモリ空間の浪費が大きくなります。そのため、より効率的な Unicode エンコーディング方が必要です。
## 3.4.4 &nbsp; UTF-8エンコーディング
## 3.4.4 &nbsp; UTF-8 エンコーディング
現在、UTF-8は国際的に最も広く使用されているUnicodeエンコーディング方になっています。**これは可変長エンコーディング**で、文字の複雑さに応じて1〜4バイトを使用して文字を表現します。ASCII文字は1バイトのみが必要で、ラテン文字とギリシャ文字は2バイト、一般的に使用される中国語文字は3バイト、その他の稀少な文字は4バイト必要です。
現在、UTF-8 は国際的に最も広く使れている Unicode エンコーディング方になっています。**これは可変長エンコーディング**であり、1 文字を 1 〜 4 バイトで表し、文字の複雑さに応じて長さが変わります。ASCII 文字は 1 バイト、ラテン文字とギリシャ文字は 2 バイト、一般的な漢字は 3 バイト、そのほかの一部の珍しい文字は 4 バイト必要です。
UTF-8のエンコーディング規則は複雑ではなく、2つのケースに分けることができます
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コードポイントを埋めるために使用されます。
- 長さ 1 バイト文字では、最上位ビットを $0$ にし、残りの 7 ビットを Unicode コードポイントに設定します。ここで注意すべきなのは、ASCII 文字が Unicode 文字セットの先頭 128 個のコードポイントを占めていることです。つまり、**UTF-8 エンコーディングASCII コードと下位互換性があります**。このため、UTF-8 を使て古い ASCII コードのテキストを解析できます。
- 長さ $n$ バイトの文字(ただし $n > 1$では、先頭バイトの上位 $n$ ビットをすべて $1$ にし、第 $n + 1$ ビットを $0$ に設定します。2 バイト目以降では、各バイトの上位 2 ビットをいずれも $10$ にし、残りのすべてのビットで文字の Unicode コードポイントを埋めます。
以下の図は「Hello算法」のUTF-8エンコーディングを示しています。上位$n$ビットが$1$に設定されているため、システムは最上位ビットで$1$に設定されたビット数を数えることで文字の長さ$n$として決定できることが観察できます。
図は「Helloアルゴリズム」に対応する UTF-8 エンコーディングを示しています。観察すると、上位 $n$ ビットがすべて $1$ に設定されているため、システムは先頭から連続する $1$ の個数を読むことで、その文字の長さ$n$ であると解析できます。
しかし、なぜ残りのバイトの上位2ビットを$10$に設定するのでしょうか?実際、この$10$は一種のチェックサムとして機能します。システムが間違ったバイトからテキスト解析を開始した場合、バイト先頭の$10$によりシステムは異常を迅速に検出できます。
では、なぜ残りのすべてのバイトの上位 2 ビットを $10$ にするのでしょうか。実は、この $10$ は検査用の印として機能します。システムがったバイト位置からテキスト解析し始めたとしても、バイト先頭の $10$ によって異常を素早く判定できます。
$10$をチェックサムとして使用する理由は、UTF-8エンコーディング規則の下では、文字の最上位2ビットが$10$になることは不可能だからです。これは矛盾により証明できます文字の上位2ビットが$10$の場合、文字の長さ$1$であることを示し、これはASCIIに対応します。しかしASCII文字の最上位ビットは$0$であるべきで、これは仮定と矛盾します。
この $10$ を検査用の印とする理由は、UTF-8 の符号化規則では上位 2 ビットが $10$ になる文字は存在しないからです。この結論は背理法で証明できます。ある文字の上位 2 ビットが $10$ だと仮定すると、その文字の長さ$1$ であり、ASCII コードに対応することになります。しかし ASCII コードの最上位ビットは $0$ であるはずなので、仮定と矛盾します。
![UTF-8エンコーディング例](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" }
![UTF-8 エンコーディング](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" }
<p align="center"> 図 3-8 &nbsp; UTF-8エンコーディング例 </p>
<p align="center"> 図 3-8 &nbsp; UTF-8 エンコーディング例 </p>
UTF-8以外にも、他の一般的なエンコーディング方法には以下があります
UTF-8 以外にも、一般的なエンコーディング方式として次の 2 つがあります
- **UTF-16エンコーディング**2または4バイトを使用して文字を表現します。すべてのASCII文字と一般的に使用される非英語文字は2バイトで表現され、少数の文字は4バイト必要す。2バイト文字の場合、UTF-16エンコーディングはUnicodeコードポイントと等しくなります。
- **UTF-32エンコーディング**すべての文字が4バイトを使用します。これは、UTF-32UTF-8UTF-16よりも多くの空間を占有することを意味し、特にASCII文字の割合が高いテキストで顕著です。
- **UTF-16 エンコーディング**1 文字を 2 バイトまたは 4 バイトで表します。すべての ASCII 文字と一般的非英語文字は 2 バイトで表し、一部の文字だけが 4 バイト必要とします。2 バイト文字については、UTF-16 エンコーディングは Unicode コードポイントと等しくなります。
- **UTF-32 エンコーディング**各文字を必ず 4 バイトで表します。つまり UTF-32UTF-8UTF-16 よりも多くの領域を消費し、とくに ASCII 文字の比率が高いテキストでその傾向が顕著です。
ストレージ空間の観点から、UTF-8を使用して英語文字表現することは1バイトのみが必要なため非常に効率的です。UTF-16を使用して一部の非英語文字(中国語など)をエンコードすることは、2バイトのみが必要なためより効率的になる場合があります。一方、UTF-8では3バイト必要になる場合があります。
記憶領域の使用量という観点では、UTF-8英語文字表現に非常に効率的で、必要なのは 1 バイトだけです。一方、UTF-16一部の非英語文字(たとえば中国語の文字)の符号化でより効率的になることがあり、必要なのは 2 バイトだけで、UTF-8 では 3 バイト必要になる場合があります。
互換性の観点から、UTF-8は最も汎用性があり、多くのツールライブラリがUTF-8を優先的にサポートしています。
互換性という観点では、UTF-8汎用性が最も高く、多くのツールライブラリが UTF-8 を優先的にサポートしています。
## 3.4.5 &nbsp; プログラミング言語における文字エンコーディング
## 3.4.5 &nbsp; プログラミング言語文字エンコーディング
歴史的に、多くのプログラミング言語はプログラム実行中の文字列処理にUTF-16UTF-32などの固定長エンコーディングを利用していました。これにより文字列を配列として処理でき、いくつかの利点があります
従来の多くのプログラミング言語では、実行中の文字列に UTF-16UTF-32 のような固定長エンコーディングが使われています。固定長エンコーディングでは、文字列を配列のように扱えるため、次のような利点があります
- **ランダムアクセス**UTF-16でエンコードされた文字列は簡単にランダムアクセスできます。可変長エンコーディングであるUTF-8の場合、$i^{th}$文字の位置を特定するには文字列の開始から$i^{th}$位置まで走査する必要があり、$O(n)$時間がかかります。
- **文字数カウント**:ランダムアクセスと同様に、UTF-16でエンコードされた文字列の文字数をカウントすることは$O(1)$操作です。しかし、UTF-8でエンコードされた文字列の文字数をカウントするには文字列全体を走査する必要があります。
- **文字列操作**分割、連結、挿入、削除などの多くの文字列操作は、UTF-16でエンコードされた文字列で簡単です。これらの操作は一般的に、UTF-8エンコーディングの有効性を確保するためにUTF-8でエンコードされた文字列で追加計算が必要です。
- **ランダムアクセス**UTF-16 で符号化された文字列はランダムアクセスが容易です。UTF-8 は可変長エンコーディングなので、第 $i$ 文字を見つけるには文字列の先頭から第 $i$ 文字まで走査する必要があり、$O(n)$時間がかかります。
- **文字数の計算**:ランダムアクセスと同様に、UTF-16 で符号化された文字列の長さを計算するのも $O(1)$操作です。しかし、UTF-8 で符号化された文字列の長さを計算するには文字列全体を走査する必要があります。
- **文字列操作**UTF-16 で符号化された文字列では、多くの文字列操作(分割、連結、挿入、削除など)をより簡単に行えます。UTF-8 で符号化された文字列では、これらの操作を行う際に、無効な UTF-8 エンコーディングを生じさせないための追加計算が通常必要になります。
プログラミング言語における文字エンコーディングスキームの設計は、様々な要因を含む興味深いトピックです:
実際、プログラミング言語における文字エンコーディング方式の設計は、とても興味深い話題であり、多くの要因が関わっています。
- Java`String`型はUTF-16エンコーディングを使用し、各文字が2バイトを占有します。これは16ビットがすべての可能な文字を表現するのに十分であるという初期の信念に基づいており、後に間違いであることが証明されました。Unicode標準が16ビットを超えて拡張されると、Javaの文字は「サロゲートペア」として知られる16ビット値のペアで表される場合があります。
- JavaScriptTypeScriptは、Javaと同様の理由でUTF-16エンコーディングを使用します。JavaScriptが1995年にNetscapeによって最初に導入されたとき、Unicodeはまだ初期段階にあり、16ビットエンコーディングはすべてのUnicode文字を表現するのに十分でした。
- C#はUTF-16エンコーディングを使用し、これは主にMicrosoftによって設計された.NETプラットフォーム、および多くのMicrosoft技術(Windowsオペレーティングシステムを含む)UTF-16エンコーディングを広範囲に使用しているためです。
- Java`String` 型は UTF-16 エンコーディングを使用し、各文字は 2 バイトを占ます。これは Java 言語の設計当初、人々が 16 ビットあればあらゆる文字を表現するのに十分だと考えていたためです。しかし、これは誤った判断でした。その後 Unicode 規格は 16 ビットを超える範囲へ拡張されたため、現在の Java では 1 文字が 16 ビット値の組(「サロゲートペア」)で表されることがあります。
- JavaScriptTypeScript の文字列が UTF-16 エンコーディングを使う理由も Java と似ています。1995 年に Netscape 社が初めて JavaScript 言語を公開した当時、Unicode はまだ発展初期にあり、16 ビットの符号化で十分すべての Unicode 文字を表せると考えられていました。
- C# が UTF-16 エンコーディングを使う主な理由は、.NET プラットフォームが Microsoft によって設計されMicrosoft の多くの技術(Windows オペレーティングシステムを含む)UTF-16 エンコーディングが広く使われているためです。
文字数過小評価により、これらの言語は16ビットを超えるUnicode文字を表現するために「サロゲートペア」を使用する必要がありました。このアプローチには欠点があります:サロゲートペアを含む文字列は2バイトまたは4バイトを占有する文字を持つ場合があり、固定長エンコーディングの利点を失います。さらに、サロゲートペアの処理はプログラミング複雑さとデバッグの困難さを追加します。
以上のプログラミング言語は文字数過小評価していたため、16 ビットを超える長さの Unicode 文字を表ために「サロゲートペア」を採用せざるを得ませんでした。これはやむを得ない妥協策です。一方では、サロゲートペアを含む文字列では、1 文字が 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`型も提供します。
- Python`str` は Unicode エンコーディングを使用し、柔軟な文字列表現を採用しています。保存される文字の長さは、その文字列中で最大の Unicode コードポイントに依存します。文字列がすべて ASCII 文字であれば各文字は 1 バイト、ASCII の範囲を超える文字があってもすべてが基本多言語面(BMP)内であれば各文字は 2 バイト、BMP を超える文字があれば各文字は 4 バイトを占ます。
- Go 言語の `string` 型は内部UTF-8 エンコーディングを使用します。Go 言語には単一の Unicode コードポイントを表`rune` 型も用意されています。
- Rust 言語の `str``String` 型は内部UTF-8 エンコーディングを使用します。Rust にも単一の Unicode コードポイントを表す `char` 型があります。
上記の議論は、プログラミング言語での文字列の格納方法に関するものであり、**ファイルでの文字列の格納方法やネットワーク上での送信方法とは異なる**ことに注意することが重要です。ファイルストレージやネットワーク送では、文字列は通常、最適な互換性と空間効率のためにUTF-8形式エンコードされます。
注意すべきなのは、ここまでの議論はすべて、プログラミング言語での文字列の保存方法についてであり、**文字列をファイルに保存したりネットワークで転送したりする方法とは別の問題である**ということです。ファイル保存やネットワーク送では、通常、互換性と空間効率を最適化するために文字列を UTF-8 形式エンコードます。
@@ -4,55 +4,55 @@ comments: true
# 3.1 &nbsp; データ構造の分類
一般的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュ、木、ヒープ、グラフがあります。これらは「論理構造」と「物理構造」分類できます。
代表的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフがありこれらは「論理構造」と「物理構造」の 2 つの観点から分類できます。
## 3.1.1 &nbsp; 論理構造:線形と非線形
**論理構造はデータ要素間の論理的関係を明らかにします**。配列と連結リストでは、データは定の順序で配置され、データ間の線形関係を示しています。一方、木ではデータは上から下へ階層的に配置され、「祖先」と「子孫」の派生関係を示しています。そして、グラフはノードとエッジから構成され、複雑なネットワーク関係を反映しています。
**論理構造はデータ要素間の論理的関係をします**。配列と連結リストでは、データは定の順序で並び、データ間の線形関係を表します。一方、木ではデータは上から下へ階層的に並び、「祖先」と「子孫」の派生関係を示します。グラフはノードと辺で構成され、複雑なネットワーク関係を反映します。
下図に示されているように、論理構造は「線形」と「非線形」の2つの主要カテゴリに分けることができます。線形構造はより直感的で、データが論理関係において線形に配置されていることを示しています。非線形構造は、逆に非線形に配置されています。
以下の図に示すように、論理構造は「線形」と「非線形」の 2 つに大別できます。線形構造は比較的直感的で、データが論理関係において線形に並ぶことを指します。非線形構造はその逆で、非線形に配置されます。
- **線形データ構造**:配列、連結リスト、スタック、キュー、ハッシュ表。要素が一対一の順関係を持ちます。
- **非線形データ構造**:木、ヒープ、グラフ、ハッシュ
- **線形データ構造**:配列、連結リスト、スタック、キュー、ハッシュテーブルであり、要素間は 1 対 1 の順関係す。
- **非線形データ構造**:木、ヒープ、グラフ、ハッシュテーブル
非線形データ構造は、さらに木構造とネットワーク構造に分けることができます。
非線形データ構造は、さらに木構造と網状構造に分けられます。
- **木構造**:木、ヒープ、ハッシュ表。要素が一対多の関係を持ちます。
- **ネットワーク構造**:グラフ。要素が多対多の関係を持ちます。
- **木構造**:木、ヒープ、ハッシュテーブルであり、要素間は 1 対多の関係す。
- **網状構造**:グラフであり、要素間は多対多の関係す。
![Linear and non-linear data structures](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" }
![線形データ構造と非線形データ構造](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>
<p align="center"> 図 3-1 &nbsp; 線形データ構造と非線形データ構造 </p>
## 3.1.2 &nbsp; 物理構造:連続と分散
**アルゴリズムの実行中、処理されるデータはメモリに格納されます**。下図はコンピュータのメモリスティックを示しており、各黒い正方形は物理メモリ空間です。メモリ巨大なExcelスプレッドシートと考えることができ、各セルは一定量のデータを格納できます。
**アルゴリズムのプログラムが実行されるとき、処理中のデータは主にメモリに格納されます**。下図はコンピュータのメモリモジュールを示しており、各黒い四角はそれぞれ 1 つのメモリ空間を表しています。メモリ巨大な Excel の表のようなものだと考えることができ、各セルは一定量のデータを格納できます。
**システムはメモリアドレスによって目標位置のデータアクセスします**。下図に示されているように、コンピュータは特定のルールに従って表の各セルに一意の識別子を割り当て、各メモリ空間が一意のメモリアドレスを持つことを保証します。これらのアドレスにより、プログラムはメモリに格納されたデータにアクセスできます。
**システムはメモリアドレスを通じて目的の位置にあるデータアクセスします**。下図に示ように、コンピュータは特定の規則に従って表の各セルに番号を割り当て、各メモリ空間が一意のメモリアドレスを持つようにします。これらのアドレスがあれば、プログラムはメモリ内のデータにアクセスできます。
![Memory stick, memory spaces, memory addresses](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" }
![メモリモジュール、メモリ空間、メモリアドレス](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>
<p align="center"> 図 3-2 &nbsp; メモリモジュール、メモリ空間、メモリアドレス </p>
!!! tip
メモリをExcelスプレッドシートに比較することは簡略化された類推であることに注意してください。メモリの実際の動作メカニズムはより複雑で、アドレス空間、メモリ管理、キャッシュメカニズム、仮想メモリ、物理メモリなどの概念が関係しています。
補足すると、メモリを Excel の表にたとえるのは単純化した比喩であり、実際のメモリの動作機構はより複雑で、アドレス空間、メモリ管理、キャッシュ機構、仮想メモリ、物理メモリなどの概念が関わります。
メモリはすべてのプログラム共有リソースです。あるメモリブロックが1つのプログラムによって占有されると、他のプログラムが同時に使用することはできません。**したがって、メモリリソースはデータ構造とアルゴリズムの設計における重要な考慮事項です**。えば、アルゴリズムのピークメモリ使用量は、システムの残り空きメモリを超えてはいけません。連続したメモリブロックが不足している場合は、非連続メモリブロックに格納できるデータ構造を選択する必要があります。
メモリはすべてのプログラム共有される資源であり、あるメモリ領域が 1 つのプログラムに占有されると、通常は他のプログラムが同時に利用できません。**したがって、データ構造とアルゴリズムの設計では、メモリ資源は重要な考慮要素です**。たとえば、アルゴリズムが使用するメモリ使用量のピークは、システムに残っている空きメモリを超えてはなりません。大きな連続メモリ領域が不足している場合、選択するデータ構造は分散したメモリ空間に格納できる必要があります。
下図に示されているように、**物理構造はコンピュータメモリにおけるデータの格納方法を反映し**、連続空間格納(配列)と非連続空間格納(連結リスト)に分けることができます。2つのタイプの物理構造は時間効率と空間効率の観点で補完的な特性を示します。
下図に示ように、**物理構造はデータがコンピュータメモリ内にどのように格納されるかを表します**。これは連続空間への格納(配列)と分散空間への格納(連結リスト)に分けられます。物理構造は低レベルでデータのアクセス、更新、追加、削除などの操作方法を決定し、2 種類の物理構造は時間効率と空間効率の面で相補的な特徴を持ちます。
![Contiguous space storage and dispersed space storage](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" }
![連続空間格納と分散空間格納](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>
<p align="center"> 図 3-3 &nbsp; 連続空間格納と分散空間格納 </p>
**すべてのデータ構造は配列、連結リスト、またはその組み合わせに基づいて実装されていることに注意してください**。例えば、スタックとキューは配列または連結リストのどちらでも実装できます。ハッシュの実装には配列と連結リストの両方が関係する場合があります。
補足すると、**すべてのデータ構造は配列、連結リスト、またはその両者の組み合わせに基づいて実装されます**。たとえば、スタックとキューは配列でも連結リストでも実装できます。一方、ハッシュテーブルの実装には配列と連結リストの両方が含まれる場合があります。
- **配列ベースの実装**:スタック、キュー、ハッシュ、木、ヒープ、グラフ、行列、テンソル(次元$\geq 3$の配列)。
- **連結リストベースの実装**:スタック、キュー、ハッシュ、木、ヒープ、グラフなど。
- **配列に基づいて実装可能**:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフ、行列、テンソル(次元 $\geq 3$ の配列)など
- **連結リストに基づいて実装可能**:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなど。
配列に基づいて実装されたデータ構造は「静的データ構造」とも呼ばれ初期化後に長さを変更できないことを意味します。逆に、連結リストに基づいたものは「動的データ構造」と呼ばれ、プログラム実行中にサイズを調整できます。
連結リストは初期化後も、プログラムの実行中に長さを調整できるため、「動的データ構造」とも呼ばれます。配列は初期化後に長さを変更できないため、「静的データ構造」と呼ばれます。なお、配列もメモリを再割り当てすることで長さを変更でき、ある程度の「動的性」を持たせることができます。
!!! tip
物理構造理解するのが困難な場合は、次の章「配列と連結リスト」を読んでから、この節に戻ることを勧めます。
物理構造理解が難しいと感じる場合は、先に次の章を読んでから本節を振り返ることを勧めます。
+6 -6
View File
@@ -5,18 +5,18 @@ icon: material/shape-outline
# 第 3 章 &nbsp; データ構造
![Data structures](../assets/covers/chapter_data_structure.jpg){ class="cover-image" }
![データ構造](../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.3 &nbsp; 数値エンコーディング *](number_encoding.md)
- [3.4 &nbsp; 文字エンコーディング *](character_encoding.md)
- [3.5 &nbsp; まとめ](summary.md)
@@ -6,25 +6,25 @@ comments: true
!!! tip
本書では、アスタリスク「*」が付いた章は任意読書です。時間が不足している場合や難しいと感じる場合は、最初はこれらをスキップして、必須の章を完了した後に戻ることができます
本書では、タイトルに * 記号が付いている章は選読です。時間が限られている場合や理解が難しいと感じる場合は、いったん読み飛ばし、必読章を終えてから個別に取り組んでください
## 3.3.1 &nbsp; 整数エンコーディング
## 3.3.1 &nbsp; 符号付き絶対値表現、1 の補数、2 の補数
節の表、すべての整数型は正数よりも1つ多い負の数を表現できることを観察しました。例えば`byte`の範囲は$[-128, 127]$です。この現象は直感に反するように見え、その根本的な理由には符号絶対値、1の補数、2の補数エンコーディングの知識が関与しています。
前節の表を見ると、すべての整数型で表せる負数の個数は正数より 1 つ多く、たとえば `byte` の値域は $[-128, 127]$ です。この現象は直感に反するように見えますが、その背景には符号付き絶対値表現、1 の補数、2 の補数に関する知識があります。
まず重要なことは、**数値はコンピュータ内で2の補数形式で格納される**ということです。なぜそうなのかを分析する前に、これら3つのエンコーディング方法を定義しましょう:
まず押さえておくべきなのは、**数値はコンピュータ内で「2 の補数」の形で保存される**ということです。その理由を説明する前に、まずはこの 3 つの定義を示します。
- **符号絶対値**:数値の二進表現の最上位ビットを符号ビットとし、$0$は正数、$1$は負数を表します。残りのビット数値の値を表します。
- **1の補数**:正数の1の補数は符号絶対値と同じです。負の数の場合、符号ビット以外のすべてのビットを反転して得られます。
- **2の補数**:正数の2の補数は符号絶対値と同じです。負の数の場合、その1の補数に$1$を加えて得られます。
- **符号付き絶対値表現**:数値の二進表現の最上位ビットを符号ビットとみなし、$0$ は正数、$1$ は負数を表し残りのビット数値の値を表します。
- **1 の補数**:正数の 1 の補数は符号付き絶対値表現と同じで、負数の 1 の補数は符号ビットを除くすべてのビットを反転したものです。
- **2 の補数**:正数の 2 の補数は符号付き絶対値表現と同じで、負数の 2 の補数は 1 の補数に $1$ を加えたものです。
以下の図は、符号絶対値、1の補数、2の補数の変換を示しています
図は、符号付き絶対値表現、1 の補数、2 の補数の変換方法を示しています
![符号絶対値、1の補数、2の補数の変換](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" }
![符号付き絶対値表現、1 の補数、2 の補数の相互変換](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" }
<p align="center"> 図 3-4 &nbsp; 符号絶対値、1の補数、2の補数の変換 </p>
<p align="center"> 図 3-4 &nbsp; 符号付き絶対値表現、1 の補数、2 の補数の相互変換 </p>
<u>符号絶対値</u>は最も直感的ですが、制限があります。一つには、**符号絶対値の負の数は計算で直接使用できません**。えば符号絶対値$1 + (-2)$を計算すると$-3$になり、これは正しくありません
<u>符号付き絶対値表現(sign-magnitude</u>は最も直感的ですが、いくつかの制約があります。まず、**負数の符号付き絶対値表現はそのまま演算に使えません**。たとえば符号付き絶対値表現で $1 + (-2)$ を計算すると、結果は $-3$ になってしまい、これは明らかに誤りです
$$
\begin{aligned}
@@ -35,20 +35,20 @@ $$
\end{aligned}
$$
この問題に対処するため、コンピュータは<u>1の補数</u>導入ました。1の補数に変換して$1 + (-2)$を計算し、結果を符号絶対値に戻すと、正しい結果$-1$が得られます。
この問題を解決するため、コンピュータは<u>1 の補数1's complement</u>導入されました。まず符号付き絶対値表現を 1 の補数に変換し、1 の補数で $1 + (-2)$ を計算してから、結果を 1 の補数から符号付き絶対値表現へ戻すと、正しい結果 $-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 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を区別しないと、正しくない結果になる可能性があります。この曖昧さに対処するには追加のチェックが必要で、計算効率が低下する可能性があります。
一方、**数値 0 の符号付き絶対値表現には $+0$ と $-0$ の 2 つの表し方があります**。つまり、数値 0 に対して異なる 2 つの二進コードが対応しており、これは曖昧さの原因になります。たとえば条件判定で正のゼロと負のゼロを区別しないと、誤った判定結果になる可能性があります。また、この曖昧さを解消しようとすると追加の判定処理が必要になり、計算効率が下がるおそれがあります。
$$
\begin{aligned}
@@ -57,106 +57,106 @@ $$
\end{aligned}
$$
符号絶対値と同様に、1の補数も正負の0の曖昧さに悩まされます。そのため、コンピュータはさらに<u>2の補数</u>を導入しました。符号絶対値、1の補数、2の補数における負の0の変換過程を観察してみましょう
符号付き絶対値表現と同様に、1 の補数も正負のゼロの曖昧さがあります。そこでコンピュータはさらに<u>2 の補数2's complement</u>を導入しました。まずは負のゼロについて、符号付き絶対値表現、1 の補数、2 の補数の変換を見てみましょう
$$
\begin{aligned}
-0 \rightarrow \; & 1000 \; 0000 \; \text{(符号絶対値)} \newline
= \; & 1111 \; 1111 \; \text{(1の補数)} \newline
= 1 \; & 0000 \; 0000 \; \text{(2の補数)} \newline
-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と同じになり、曖昧さが解決されます。
負のゼロの 1 の補数に $1$ を加えると桁上がりが発生しますが、`byte`の長さは 8 ビットしかないため、第 9 ビットへあふれた $1$ は捨てられます。つまり、**負のゼロの 2 の補数は $0000 \; 0000$ であり、正のゼロの 2 の補数と同じです**。そのため、2 の補数表現ではゼロは 1 つしか存在せず、正負のゼロの曖昧さは解消されます。
最後の謎は、`byte`$[-128, 127]$の範囲で、追加の負の数$-128$があることです。$[-127, +127]$の区間では、すべての整数に対応する符号絶対値、1の補数、2の補数があり、相互変換が可能であることを観察します。
最後にもう 1 つ疑問が残ります。`byte` 型の値域は $[-128, 127]$ ですが、余分にある負数 $-128$ はどのように得られるのでしょうか。区間 $[-127, +127]$ にあるすべての整数には、それぞれ対応する符号付き絶対値表現、1 の補数、2 の補数があり、符号付き絶対値表現と 2 の補数の間は相互変換できます。
しかし、**2の補数$1000 \; 0000$対応する符号絶対値を持たない例外です**。変換方法によると、その符号絶対値は$0000 \; 0000$で、0を示します。これは矛盾を示しています。なぜなら、その2の補数は自分自身を表すべきだからです。コンピュータは、この特別な2の補数$1000 \; 0000$$-128$を表すものとして指定しています。実際、2の補数での$(-1) + (-127)$の計算結果は$-128$になります。
しかし、**2 の補数 $1000 \; 0000$ だけは例外で、対応する符号付き絶対値表現を持ちません**。変換規則に従うと、この 2 の補数に対応する符号付き絶対値表現は $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 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)$に変換でき、乗算除算は複数の加算または減算に変換できます。
ただし、これはコンピュータが加算しかできないという意味ではありません。**加算といくつかの基本的な論理演算を組み合わせることで、コンピュータはさまざまな数学演算を実できます**。たとえば減算 $a - b$ は加算 $a + (-b)$ に変換できますし、乗算除算も繰り返しの加算または減算に変換できます。
コンピュータで2の補数を使用する理由をまとめることができます:2の補数表現により、コンピュータは同じ回路と演算を使用して正と負数の加算を処理でき、減算用の特別なハードウェア回路の必要性を排除し、正負の0の曖昧さを回避できます。これによりハードウェア設計大幅に簡化され、算効率向上します。
これで、コンピュータが 2 の補数を使理由をまとめられます。2 の補数表現に基づけば、コンピュータは同じ回路と操作で正数と負数の加算を扱うことができ、減算用の特別なハードウェア回路を設計する必要がなく、正負のゼロの曖昧さも特別に処理しなくて済みます。これによりハードウェア設計大幅に簡化され、算効率向上します。
2の補数の設計は非常に巧妙で、スペースの制約により、ここで停止します。興味のある読者はさらに探求することを奨励します
2 の補数の設計は非常に巧妙ですが、紙幅の都合上ここまでにします。興味のある読者はさらに深く調べてみてください
## 3.3.2 &nbsp; 浮動小数点数エンコーディング
## 3.3.2 &nbsp; 浮動小数点数エンコーディング
興味深いことに気づいたかもしれません:同じ4バイトの長さにもかかわらず、なぜ`float``int`と比較してはるかに大きい値の範囲を持つのでしょうかこれは直感に反するように見えます。`float`は分数を表現する必要があるため、範囲が縮小すると予想されるからです。
注意深い人なら気づくかもしれません。`int``float` はどちらも長さが 4 バイトで同じなのに、なぜ `float` の値域は `int` よりはるかに広いのでしょうかこれはかなり直感に反します。というのも、`float` は小数を表す必要があるので、本来なら値域は狭くなるはずだからです。
実際、**これは浮動小数点数`float`)で使用される異なる表現方法によるものです**。32ビットの二進数を次のように考えてみましょう:
実際には、**これは浮動小数点数 `float` が異なる表現方法を採用しているためです**。32 ビットの二進数を次のように表します。
$$
b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
$$
IEEE 754標準によると、32ビットの`float`は次の3つの部分構成されます
IEEE 754 標準によれば、32-bit 長の `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$に対応します。
- 符号$\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`数の値は次のように計算されます
二進`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)^{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})
\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}]
(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} 2^{-i}) \subset [1, 2 - 2^{-23}]
\end{aligned}
$$
![IEEE 754標準での浮動小数点数の計算例](number_encoding.assets/ieee_754_float.png){ class="animation-figure" }
![IEEE 754 標準における float の計算例](number_encoding.assets/ieee_754_float.png){ class="animation-figure" }
<p align="center"> 図 3-5 &nbsp; IEEE 754標準での浮動小数点数の計算例 </p>
<p align="center"> 図 3-5 &nbsp; IEEE 754 標準における float の計算例 </p>
図を観察すると、例のデータ$\mathrm{S} = 0$$\mathrm{E} = 124$、$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$が与えられた場合
上図をると、例として $\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
\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` よりはるかに広い**のです。上の計算より`float` が表せる最大の正数は $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$ であり、符号ビットを切り替えれば最小の負数が得られます。
**しかし、`float`の拡張された範囲のトレードオフは精度犠牲**。整数型`int`32ビットすべてを数値表現に使用し、値は均等に分布していますが、指数ビットのため、`float`値が大きほど、隣接する数値間の差大きくなります。
**浮動小数点数 `float` は値域を広げる一方で、その代償として精度犠牲にしていま**。整数型 `int`32 ビットすべてを数値表現に使うため、数値は一様に分布します。しかし指数部があるため、浮動小数点数 `float`値が大きくなるほど、隣り合う 2 つの数の差大きくなる傾向があります。
以下の表に示すように、指数ビット$\mathrm{E} = 0$$\mathrm{E} = 255$は特別な意味を持ち、**0、無限大、$\mathrm{NaN}$などを表現するために使用されます**。
次の表のとおり、指数部 $\mathrm{E} = 0$$\mathrm{E} = 255$は特別な意味があり、**ゼロ、無限大、$\mathrm{NaN}$ などを表ために使れます**。
<p align="center"> 表 3-2 &nbsp; 指数ビットの意味 </p>
<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}$ | |
| 指数部 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}$です。
なお、非正規化数によって浮動小数点数の精度は大きく向上します。最小の正の正規化数は $2^{-126}$ であり、最小の正の非正規化数は $2^{-126} \times 2^{-23}$ です。
倍精度`double``float`と同様の表現方法を使用しますが、簡潔さのためここでは詳述しません。
倍精度 `double``float` と同様の表現方法を用しているためここでは詳述しません。
+31 -31
View File
@@ -4,67 +4,67 @@ comments: true
# 3.5 &nbsp; まとめ
### 1. &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-16UTF-32は固定長エンコーディング方法です。中国語文字をエンコードする際、UTF-16UTF-8よりも少ない空間を使用します。JavaやC#などのプログラミング言語はデフォルトでUTF-16エンコーディングを使用します。
- データ構造は論理構造と物理構造という 2 つの観点から分類できます。論理構造はデータ要素間の論理的関係を記述し、物理構造はデータのコンピュータメモリ上での格納方法を記述します。
- 代表的な論理構造には、線形、木構造、網状構造などがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)の 2 種類に分類します。ハッシュテーブルの実装には、線形データ構造と非線形データ構造が同時に含まれる場合があります。
- プログラム実行、データはコンピュータメモリに格納されます。各メモリ空間には対応するメモリアドレスがあり、プログラムはれらのメモリアドレスを通じてデータにアクセスします。
- 物理構造は主に連続領域への格納(配列)と分散領域への格納(連結リスト)に分けられます。すべてのデータ構造は配列、連結リスト、またはその両方の組み合わせによって実装されます。
- コンピュータにおける基本データ型には、整数 `byte``short``int``long`、浮動小数点数 `float``double`、文字 `char`、真偽値 `bool` があります。これらの値域は、使用する記憶領域の大きさと表現方式によって決まります。
- 符号付き絶対値表現、1 の補数、2 の補数は、コンピュータで数値を符号化する 3 つの方法であり、相互に変換できます。整数の符号付き絶対値表現では最上位ビット符号ビットで、残りのビット数値の値す。
- 整数はコンピュータ内では 2 の補数の形式で格納されます。2 の補数表現では、コンピュータは正数と負数の加算を同じように扱うことができ、減算のために特別なハードウェア回路を別途設計する必要がなく、さらに正負のゼロが重複する問題もありません
- 浮動小数点数の符号化は、1 ビットの符号部、8 ビットの指数部、23 ビットの仮数部で構成されます。指数部があるため、浮動小数点数の値域は整数よりはるかにくなりますが、その代償として精度犠牲になります。
- ASCII コードは最も早く登場した英字文字集合で、長さは 1 バイト、収録文字数は 127 です。GBK 文字集合はよく使われる中国語文字集合で、2 万字以上の漢字を収録しています。Unicode は完全な文字集合標準を提供することを目指しており、世界中のさまざまな言語の文字を収録することで、文字コード方式の不一致によって生じる文字化け問題を解決します。
- UTF-8 は最も広く使われている Unicode の符号化方式で、汎用性が非常に高いです。可変長の符号化方式であり、拡張性に優れ、記憶領域の利用効率を効果的に高めます。UTF-16UTF-32 は固定長の符号化方式です。中国語を符号化する場合、UTF-16UTF-8 よりも使用領域が小さくなります。Java や C# などのプログラミング言語はデフォルトで UTF-16 を使用します。
### 2. &nbsp; Q & A
**Q**: なぜハッシュ表は線形と非線形の両方のデータ構造を含むのですか?
**Q**なぜハッシュテーブルには線形データ構造と非線形データ構造が同時に含まれるのですか?
ハッシュ表の基礎構造は配列です。ハッシュ衝突を解決するために「チェイン法」を使用する場合があります(後の節「ハッシュ衝突」で説明):配列の各バケットは連結リストを指し、その長さが特定の閾値より大きくなると木(通常は赤黒木)に変換される可能性があります。
格納の観点から、ハッシュ表の基礎構造は配列で、各バケットには値、連結リスト、または木が含まれる場合があります。したがって、ハッシュ表は線形データ構造(配列、連結リスト)と非線形データ構造(木)の両方を含む場合があります。
ハッシュテーブルの基盤は配列であり、ハッシュ衝突を解決するために「チェイン法」(後続の「ハッシュ衝突」の章で説明します)を使うことがあります。配列の各バケットは 1 つの連結リストを指し、その連結リストの長さがある閾値を超えると木(通常は赤黒木)に変換されることもあります。
**Q**: `char`型の長さは1バイトですか?
格納の観点から見ると、ハッシュテーブルの基盤は配列であり、各バケットスロットには値が入ることもあれば、連結リストや木が入ることもあります。したがって、ハッシュテーブルには線形データ構造(配列、連結リスト)と非線形データ構造(木)が同時に含まれる場合があります。
`char`型の長さは、プログラミング言語のエンコーディング方法によって決まります。例えば、Java、JavaScript、TypeScript、C#はすべてUTF-16エンコーディング(Unicodeコードポイントを保存するため)を使用するため、`char`型の長さは2バイトです
**Q**`char` 型の長さは 1 バイトですか?
**Q**: 配列ベースのデータ構造を「静的データ構造」と呼ぶことに曖昧さはありませんか?スタックもプッシュやポップなどの「動的」操作を実行できます。
`char` 型の長さは、プログラミング言語が採用する符号化方式によって決まります。たとえば、Java、JavaScript、TypeScript、C# はいずれも UTF-16 符号化(Unicode コードポイントを保持)を採用しているため、`char` 型の長さは 2 バイトです。
スタックは動的なデータ操作を実装できますが、データ構造は依然として「静的」です(長さが固定)。配列ベースのデータ構造は動的に要素を追加または削除できますが、その容量は固定されています。スタックサイズが事前に割り当てられたサイズを超える場合、古い配列は新しく作成されたより大きな配列にコピーされます。
**Q**:配列ベースで実装されたデータ構造を「静的データ構造」と呼ぶのは曖昧ではありませんか? スタックも push や pop などの操作ができ、これらの操作はどれも「動的」です。
**Q**: スタック(キュー)を構築する際、そのサイズが指定されていないのに、なぜ「静的データ構造」なのですか?
スタックは確かに動的なデータ操作を実現できますが、データ構造自体は依然として「静的」(長さが不変)です。配列ベースのデータ構造でも要素を動的に追加または削除できますが、その容量は固定です。データ量が事前に確保した大きさを超えた場合は、より大きな新しい配列を作成し、古い配列の内容を新しい配列にコピーする必要があります。
高級プログラミング言語では、スタック(キュー)の初期容量を手動で指定する必要はありません。このタスクはクラス内で自動的に完了されます。例えば、Javaの`ArrayList`の初期容量は通常10です。さらに、拡張操作も自動的に完了されます。詳細については、後続の「リスト」の章を参照してください。
**Q**:スタック(キュー)を構築するときにサイズを指定していないのに、なぜそれらは「静的データ構造」なのですか?
**Q**: 符号絶対値を2の補数に変換する方法は「最初に否定してから1を加える」ですので、2の補数を符号絶対値に変換することはその逆操作「最初に1を減算してから否定する」であるべきです
しかし、2の補数も「最初に否定してから1を加える」を通じて符号絶対値に変換できます。なぜですか?
高水準プログラミング言語では、スタック(キュー)の初期容量を人手で指定する必要はなく、この作業はクラス内部で自動的に行われます。たとえば、Java の `ArrayList` の初期容量は通常 10 です。また、容量拡張も自動的に実装されています。詳しくは後続の「リスト」の章を参照してください
**A**: これは、符号絶対値と2の補数間の相互変換が「補数」の計算と等価だからです。まず補数を定義します:$a + b = c$と仮定すると、$a$は$b$の$c$に対する補数と言い、逆に$b$は$a$の$c$に対する補数と言います。
**Q**:符号付き絶対値表現から 2 の補数への変換方法は「先にビット反転してから 1 を加える」ですが、2 の補数から符号付き絶対値表現への変換は逆演算である「先に 1 を引いてからビット反転する」べきなのに、同じく「先にビット反転してから 1 を加える」でも求められます。これはなぜですか?
長さ$n = 4$の二進数$0010$が与えられた場合、この数が符号絶対値(符号ビットを無視)の場合、その2の補数は「最初に否定してから1を加える」ことで得られます
これは、符号付き絶対値表現と 2 の補数の相互変換が、実際には「補数」を計算する過程だからです。まず補数の定義を示します。$a + b = c$ とすると、$a$ を $b$ から $c$ への補数と呼び、逆に $b$ も $a$ から $c$ への補数と呼びます
長さ $n = 4$ ビットの 2 進数 $0010$ が与えられたとします。この数を符号付き絶対値表現(符号ビットは考慮しない)とみなすと、その 2 の補数は「先にビット反転してから 1 を加える」ことで得られます。
$$
0010 \rightarrow 1101 \rightarrow 1110
$$
符号絶対値と2の補数の和$0010 + 1110 = 10000$であることを観察します。つまり、2の補数$1110$は符号絶対値$0010$$10000$に対する「補数」です。**これは、上記の「最初に否定してから1を加える」が$10000$に対する補数計算と等価であることを意味します**
ここで、符号付き絶対値表現と 2 の補数の和$0010 + 1110 = 10000$ となります。つまり、2 の補数 $1110$ は符号付き絶対値表現 $0010$ から $10000$ への「補数」です。**これは、上記の「先にビット反転してから 1 を加える」が、実際には $10000$ への補数計算する過程であることを意味します**
では、$1110$$10000$に対する「補数」はでしょうか?「最初に否定してから1を加える」ことで計算できます
では、2 の補数 $1110$ から $10000$ への「補数」はいくつでしょうか。これもやはり「先にビット反転してから 1 を加える」ことで求められます
$$
1110 \rightarrow 0001 \rightarrow 0010
$$
言い換えると、符号絶対値と2の補数は互いに$10000$に対する「補数」であるため、「符号絶対値から2の補数」と「2の補数から符号絶対値」は同じ操作(最初に否定してから1を加える)で実できます。
言い換えると、符号付き絶対値表現と 2 の補数は互いに相手から $10000$ への「補数」なので、「符号付き絶対値表現から 2 の補数への変換」と「2 の補数から符号付き絶対値表現への変換」は同じ操作(先にビット反転してから 1 を加える)で実できます。
もちろん、「最初に否定してから1を加える」の逆操作を使用して2の補数$1110$の符号絶対値を求めることもできます。つまり、「最初に1を減算してから否定する」:
もちろん、逆演算を用いて 2 の補数 $1110$ の符号付き絶対値表現を求めることもでき、その場合は「先に 1 を引いてからビット反転する」ことになります。
$$
1110 \rightarrow 1101 \rightarrow 0010
$$
要約すると、「最初に否定してから1を加える」と「最初に1を減算してから否定する」は両方とも$10000$に対する補数を計算しており、等価です。
まとめると、「先にビット反転してから 1 を加える」と「先に 1 を引いてからビット反転する」の 2 つの演算は、どちらも $10000$ への補数を計算しており、等価です。
本質的に、「否定」操作は実際には$1111$に対する補数を求めることです(`符号絶対値 + 1の補数 = 1111`常に成り立つため)。そして1の補数に1を加えることは$10000$に対する2の補数と等しくなります。
本質的に、「ビット反転」という操作は実際には $1111$ への補数を求めています(常に `符号付き絶対値表現 + 1 の補数 = 1111` が成り立つため)。そして、1 の補数にさらに 1 を加えて得られる 2 の補数が、$10000$ への補数です。
上記では$n = 4$を例に取りましたが、任意の桁数の任意の二進数に一般化できます。
上記では $n = 4$ を例にましたが、この考え方は任意のビット長の 2 進数に一般化できます。
@@ -2,68 +2,68 @@
comments: true
---
# 12.2 &nbsp; 分割統治索戦略
# 12.2 &nbsp; 分割統治索戦略
私たちは検索アルゴリズムが主に2つのカテゴリに分類されることを学びました
私たちはすでに学んだように、探索アルゴリズムは大きく二つに分けられる
- **総当たり検索**:データ構造を走査することで実され、時間計算量は $O(n)$ で
- **適応索**独特なデータ組織形式や事前情報を利用し、時間計算量は $O(\log n)$ または $O(1)$ に達することができます
- **力ずく探索**:データ構造を走査することで実され、時間計算量は $O(n)$ である
- **適応的探索**固有のデータ構造や事前情報を利用し、時間計算量は $O(\log n)$ 、さらには $O(1)$ に達しうる
実際、**時間計算量が $O(\log n)$ の索アルゴリズムは通常分割統治戦略に基づいています**。えば二分探索や木などです
実際、**時間計算量が $O(\log n)$ の索アルゴリズムは通常分割統治戦略に基づいて実装される**。たとえば二分探索や木構造である
- 二分探索の各ステップは、問題(配列内でターゲット要素を索する)をより小さな問題(配列の半分でターゲット要素を索する)に分し、配列が空になるかターゲット要素が見つかるまで続けます
- 木は分割統治のアイデアを表現し、二分探索木、AVL木、ヒープなどのデータ構造では、様々な操作の時間計算量は $O(\log n)$ で
- 二分探索の各ステップは、問題(配列内で目標要素を索すること)を小さな問題(配列の半分で目標要素を索すること)に分し、この過程は配列が空になるか目標要素が見つかるまで続
-構造は分割統治の考え方を代表するものであり、二分探索木、AVL 木、ヒープなどのデータ構造では、さまざまな操作の時間計算量はいずれも $O(\log n)$ である
二分探索の分割統治戦略は以下の通りです
二分探索の分割統治戦略は以下のとおりである
- **問題を分割できる**:二分探索は元の問題(配列内での検索)を部分問題(配列の半分での検索)に再帰的に分割し、中間要素とターゲット要素を比較することで実現されます
- **部分問題は独立している**:二分探索では、各ラウンドで一つの部分問題を処理し、の部分問題影響されません
- **部分問題の解をマージする必要ない**:二分探索は特定の要素を見つけることを目的としているため、部分問題の解をマージする必要がありません。部分問題が解決されると、元の問題も解決されます
- **問題は分解できる**:二分探索は元の問題(配列内で探索すること)を部分問題(配列の半分で探索すること)へ再帰的に分解する。これは中央要素と目標要素を比較することで実現され
- **部分問題は独立している**:二分探索では、各ラウンドで一つの部分問題だけを処理し、ほかの部分問題影響を受けない
- **部分問題の解を統合する必要ない**:二分探索は特定の要素を探すことを目的としているため、部分問題の解を統合する必要がない。部分問題が解決されると、元の問題も同時に解決され
分割統治は検索効率を向上させることができます。なぜなら、総当たり検索はラウンドごとに1つの選択肢しか除できませんが、**分割統治は選択肢の半分を除できるからです**
分割統治が探索効率を高められる本質的な理由は、力ずく探索では各ラウンドで一つの候補しか除できないのに対し、**分割統治による探索では各ラウンドで候補の半分を除できる**からである
### 1. &nbsp; 分割統治に基づく二分探索の実装
### 1. &nbsp; 分割統治に基づく二分探索
前の章では、二分探索反復に基づいて実装されました。今度は、分割統治(再帰)に基づいて実装します
前の章では、二分探索を漸化式(反復に基づいて実装した。ここでは分割統治(再帰)に基づいてこれを実装する
!!! question
長さ $n$ の順序付けられた配列 `nums` が与えられ、すべての要素一意である場合、要素 `target`見つけてください
長さ $n$ の順配列 `nums` が与えられ、そのすべての要素一意である要素 `target`探索せよ
分割統治の観点から、索区間 $[i, j]$ に対応する部分問題を $f(i, j)$ と表します。
分割統治の観点から、索区間 $[i, j]$ に対応する部分問題を $f(i, j)$ とす。
元の問題 $f(0, n-1)$ から開始して、以下のステップで二分探索を実行します
元の問題 $f(0, n-1)$ を出発点として、次の手順で二分探索を行う
1. 索区間 $[i, j]$ の中点 $m$ を計算し、それを使用して検索区間の半分を除去します
2. 半分のサイズに縮小された部分問題を再帰的に解決します。これは $f(i, m-1)$ または $f(m+1, j)$ になる可能性があります
3. `target` が見つかるか区間が空になってリターンするまで、ステップ `1.``2.` を繰り返します。
1. 索区間 $[i, j]$ の中点 $m$ を計算し、それに基づいて探索区間の半分を除外する
2. 規模が半分に縮小された部分問題を再帰的に解く。候補は $f(i, m-1)$ または $f(m+1, j)$ である
3. `1.``2.` の手順を繰り返し、`target` が見つかるか区間が空になったら返す。
以下の図は、配列内で要素 $6$ を探す二分探索分割統治過程を示しています
の図は、配列内で要素 $6$ を二分探索する分割統治過程を示してい
![二分探索の分割統治過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" }
![二分探索の分割統治過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" }
<p align="center"> 図 12-4 &nbsp; 二分探索の分割統治過程 </p>
<p align="center"> 図 12-4 &nbsp; 二分探索の分割統治過程 </p>
実装コードでは、問題 $f(i, j)$ を解決するために再帰関数 `dfs()` を宣言します:
実装コードでは、再帰関数 `dfs()` を宣言して問題 $f(i, j)$ を解く。
=== "Python"
```python title="binary_search_recur.py"
def dfs(nums: list[int], target: int, i: int, j: int) -> int:
"""二分探索:問題 f(i, j)"""
# 区間が空の場合、対象要素がないことを示すため、-1 を返す
# 区間が空なら対象要素は存在しないので -1 を返す
if i > j:
return -1
# 中点インデックス m を計算
m = (i + j) // 2
if nums[m] < target:
# 再帰部分問題 f(m+1, j)
# 部分問題 f(m+1, j) を再帰的に解く
return dfs(nums, target, m + 1, j)
elif nums[m] > target:
# 再帰部分問題 f(i, m-1)
# 部分問題 f(i, m-1) を再帰的に解く
return dfs(nums, target, i, m - 1)
else:
# 対象要素を発見したため、そのインデックスを返す
# 目標要素が見つかったらそのインデックスを返す
return m
def binary_search(nums: list[int], target: int) -> int:
@@ -78,20 +78,20 @@ comments: true
```cpp title="binary_search_recur.cpp"
/* 二分探索:問題 f(i, j) */
int dfs(vector<int> &nums, int target, int i, int j) {
// 区間が空の場合、対象要素存在しないことを示すため、-1 を返す
// 区間が空なら対象要素存在しないので -1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
int m = i + (j - i) / 2;
int m = (i + j) / 2;
if (nums[m] < target) {
// 再帰的な部分問題 f(m+1, j)
// 部分問題 f(m+1, j) を再帰的に解く
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 再帰的な部分問題 f(i, m-1)
// 部分問題 f(i, m-1) を再帰的に解く
return dfs(nums, target, i, m - 1);
} else {
// 対象要素が見つかったため、そのインデックスを返す
// 目標要素が見つかったそのインデックスを返す
return m;
}
}
@@ -109,20 +109,20 @@ comments: true
```java title="binary_search_recur.java"
/* 二分探索:問題 f(i, j) */
int dfs(int[] nums, int target, int i, int j) {
// 区間が空の場合、対象要素存在しないことを示すため、-1 を返す
// 区間が空なら対象要素存在しないので -1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
int m = i + (j - i) / 2;
int m = (i + j) / 2;
if (nums[m] < target) {
// 再帰的な部分問題 f(m+1, j)
// 部分問題 f(m+1, j) を再帰的に解く
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 再帰的な部分問題 f(i, m-1)
// 部分問題 f(i, m-1) を再帰的に解く
return dfs(nums, target, i, m - 1);
} else {
// 対象要素が見つかったため、そのインデックスを返す
// 目標要素が見つかったそのインデックスを返す
return m;
}
}
@@ -138,79 +138,319 @@ comments: true
=== "C#"
```csharp title="binary_search_recur.cs"
[class]{binary_search_recur}-[func]{DFS}
/* 二分探索:問題 f(i, j) */
int DFS(int[] nums, int target, int i, int j) {
// 区間が空なら対象要素は存在しないので -1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
int m = (i + j) / 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;
}
}
[class]{binary_search_recur}-[func]{BinarySearch}
/* 二分探索 */
int BinarySearch(int[] nums, int target) {
int n = nums.Length;
// 問題 f(0, n-1) を解く
return DFS(nums, target, 0, n - 1);
}
```
=== "Go"
```go title="binary_search_recur.go"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
func dfs(nums []int, target, i, j int) int {
// 区間が空なら対象要素は存在しないため、-1 を返す
if i > j {
return -1
}
// 中点インデックスを計算する
m := i + ((j - i) >> 1)
// 中点の要素と目標要素の大小を判定する
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
}
}
[class]{}-[func]{binarySearch}
/* 二分探索 */
func binarySearch(nums []int, target int) int {
n := len(nums)
return dfs(nums, target, 0, n-1)
}
```
=== "Swift"
```swift title="binary_search_recur.swift"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
func dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {
// 区間が空なら対象要素は存在しないので -1 を返す
if i > j {
return -1
}
// 中点インデックス m を計算
let m = (i + j) / 2
if nums[m] < target {
// 部分問題 f(m+1, j) を再帰的に解く
return dfs(nums: nums, target: target, i: m + 1, j: j)
} else if nums[m] > target {
// 部分問題 f(i, m-1) を再帰的に解く
return dfs(nums: nums, target: target, i: i, j: m - 1)
} else {
// 目標要素が見つかったらそのインデックスを返す
return m
}
}
[class]{}-[func]{binarySearch}
/* 二分探索 */
func binarySearch(nums: [Int], target: Int) -> Int {
// 問題 f(0, n-1) を解く
dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)
}
```
=== "JS"
```javascript title="binary_search_recur.js"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
function dfs(nums, target, i, j) {
// 区間が空なら対象要素は存在しないので -1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
const m = i + ((j - i) >> 1);
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;
}
}
[class]{}-[func]{binarySearch}
/* 二分探索 */
function binarySearch(nums, target) {
const n = nums.length;
// 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1);
}
```
=== "TS"
```typescript title="binary_search_recur.ts"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
function dfs(nums: number[], target: number, i: number, j: number): number {
// 区間が空なら対象要素は存在しないので -1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
const m = i + ((j - i) >> 1);
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;
}
}
[class]{}-[func]{binarySearch}
/* 二分探索 */
function binarySearch(nums: number[], target: number): number {
const n = nums.length;
// 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1);
}
```
=== "Dart"
```dart title="binary_search_recur.dart"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
int dfs(List<int> nums, int target, int i, int j) {
// 区間が空なら対象要素は存在しないので -1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
int m = (i + j) ~/ 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;
}
}
[class]{}-[func]{binarySearch}
/* 二分探索 */
int binarySearch(List<int> nums, int target) {
int n = nums.length;
// 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1);
}
```
=== "Rust"
```rust title="binary_search_recur.rs"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {
// 区間が空なら対象要素は存在しないので -1 を返す
if i > j {
return -1;
}
let m: i32 = i + (j - i) / 2;
if nums[m as usize] < target {
// 部分問題 f(m+1, j) を再帰的に解く
return dfs(nums, target, m + 1, j);
} else if nums[m as usize] > target {
// 部分問題 f(i, m-1) を再帰的に解く
return dfs(nums, target, i, m - 1);
} else {
// 目標要素が見つかったらそのインデックスを返す
return m;
}
}
[class]{}-[func]{binary_search}
/* 二分探索 */
fn binary_search(nums: &[i32], target: i32) -> i32 {
let n = nums.len() as i32;
// 問題 f(0, n-1) を解く
dfs(nums, target, 0, n - 1)
}
```
=== "C"
```c title="binary_search_recur.c"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
int dfs(int nums[], int target, int i, int j) {
// 区間が空なら対象要素は存在しないので -1 を返す
if (i > j) {
return -1;
}
// 中点インデックス m を計算
int m = (i + j) / 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;
}
}
[class]{}-[func]{binarySearch}
/* 二分探索 */
int binarySearch(int nums[], int target, int numsSize) {
int n = numsSize;
// 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1);
}
```
=== "Kotlin"
```kotlin title="binary_search_recur.kt"
[class]{}-[func]{dfs}
/* 二分探索:問題 f(i, j) */
fun dfs(
nums: IntArray,
target: Int,
i: Int,
j: Int
): Int {
// 区間が空なら対象要素は存在しないので -1 を返す
if (i > j) {
return -1
}
// 中点インデックス m を計算
val m = (i + j) / 2
return if (nums[m] < target) {
// 部分問題 f(m+1, j) を再帰的に解く
dfs(nums, target, m + 1, j)
} else if (nums[m] > target) {
// 部分問題 f(i, m-1) を再帰的に解く
dfs(nums, target, i, m - 1)
} else {
// 目標要素が見つかったらそのインデックスを返す
m
}
}
[class]{}-[func]{binarySearch}
/* 二分探索 */
fun binarySearch(nums: IntArray, target: Int): Int {
val n = nums.size
// 問題 f(0, n-1) を解く
return dfs(nums, target, 0, n - 1)
}
```
=== "Ruby"
```ruby title="binary_search_recur.rb"
[class]{}-[func]{dfs}
### 二分探索: 問題 f(i, j) ###
def dfs(nums, target, i, j)
# 区間が空なら対象要素は存在しないので -1 を返す
return -1 if i > j
# 中点インデックス m を計算
m = (i + j) / 2
[class]{}-[func]{binary_search}
if nums[m] < target
# 部分問題 f(m+1, j) を再帰的に解く
return dfs(nums, target, m + 1, j)
elsif nums[m] > target
# 部分問題 f(i, m-1) を再帰的に解く
return dfs(nums, target, i, m - 1)
else
# 目標要素が見つかったらそのインデックスを返す
return m
end
end
### 二分探索 ###
def binary_search(nums, target)
n = nums.length
# 問題 f(0, n-1) を解く
dfs(nums, target, 0, n - 1)
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%9A%E5%95%8F%E9%A1%8C%20f%28i%2C%20j%29%22%22%22%0A%20%20%20%20%23%20%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AA%E3%82%89%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%E3%81%AF%E5%AD%98%E5%9C%A8%E3%81%97%E3%81%AA%E3%81%84%E3%81%AE%E3%81%A7%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28m%2B1%2C%20j%29%20%E3%82%92%E5%86%8D%E5%B8%B0%E7%9A%84%E3%81%AB%E8%A7%A3%E3%81%8F%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20m%20%2B%201%2C%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28i%2C%20m-1%29%20%E3%82%92%E5%86%8D%E5%B8%B0%E7%9A%84%E3%81%AB%E8%A7%A3%E3%81%8F%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20i%2C%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%81%9D%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%95%8F%E9%A1%8C%20f%280%2C%20n-1%29%20%E3%82%92%E8%A7%A3%E3%81%8F%0A%20%20%20%20return%20dfs%28nums%2C%20target%2C%200%2C%20n%20-%201%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%206%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20%3D%20%22%2C%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%9A%E5%95%8F%E9%A1%8C%20f%28i%2C%20j%29%22%22%22%0A%20%20%20%20%23%20%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AA%E3%82%89%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%E3%81%AF%E5%AD%98%E5%9C%A8%E3%81%97%E3%81%AA%E3%81%84%E3%81%AE%E3%81%A7%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28m%2B1%2C%20j%29%20%E3%82%92%E5%86%8D%E5%B8%B0%E7%9A%84%E3%81%AB%E8%A7%A3%E3%81%8F%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20m%20%2B%201%2C%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28i%2C%20m-1%29%20%E3%82%92%E5%86%8D%E5%B8%B0%E7%9A%84%E3%81%AB%E8%A7%A3%E3%81%8F%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20i%2C%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%81%9D%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%95%8F%E9%A1%8C%20f%280%2C%20n-1%29%20%E3%82%92%E8%A7%A3%E3%81%8F%0A%20%20%20%20return%20dfs%28nums%2C%20target%2C%200%2C%20n%20-%201%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%206%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20%3D%20%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
@@ -2,74 +2,74 @@
comments: true
---
# 12.3 &nbsp; 二分木構築問題
# 12.3 &nbsp; 二分木構築問題
!!! question
二分木の前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスが与えられた場合、二分木を構築しそのルートノードを返してください。二分木に重複するノード値がないと仮定します(以下の図に示すように)。
二分木の前順走査 `preorder` と中順走査 `inorder` が与えられたとき、これらから二分木を構築しそのノードを返してください。二分木には値が重複するノードが存在しないものとします(下図のとおり)。
![二分木構築のサンプルデータ](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" }
![二分木構築する例のデータ](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" }
<p align="center"> 図 12-5 &nbsp; 二分木構築のサンプルデータ </p>
<p align="center"> 図 12-5 &nbsp; 二分木構築する例のデータ </p>
### 1. &nbsp; 分割統治問題かどうかの判定
### 1. &nbsp; 分割統治問題かどうかを判断する
`preorder``inorder` シーケンスから二分木を構築する元の問題は、典型的な分割統治問題です。
元の問題は `preorder``inorder` から二分木を構築することであり、典型的な分割統治問題です。
- **問題分解できる**:分割統治の観点から、元の問題を2つの部分問題(左の部分木の構築と右部分木の構築)とルートノード初期化という1つの操作に分割できます。各部分木(部分問題)について、同じアプローチを継続的に適用しより小さな部分木(部分問題)に分割し、最小の部分問題(空部分木)に到達するまで続けます。
- **部分問題は独立している**:左と右部分木は重複しません。左部分木を構築する際、左の部分木に対応する中順走査と前順走査のセグメントのみが必要です。右部分木も同じアプローチが適用されます。
- **部分問題の解を組み合わせることができる**:左と右部分木(部分問題の解)を構築したら、それらをルートノードに接続して元の問題の解を取得できます。
- **問題分解できる**:分割統治の観点から見ると、元の問題は 2 つの部分問題、すなわち左部分木の構築と右部分木の構築に分けられ、さらに根ノード初期化する 1 ステップが加わります。各部分木(部分問題)に対しても、同じ分割方法を再利用しより小さな部分木(部分問題)へと分けていき、最小の部分問題(空部分木)に達した時点で終了します。
- **部分問題は独立している**:左部分木と右部分木は互いに独立しており、両者の間に重なりはありません。左部分木を構築するときは、中順走査と前順走査のうち左部分木に対応する部分だけを見れば十分です。右部分木も同様です。
- **部分問題の解は統合できる**:左部分木と右部分木(部分問題の解)が得られたら、それらをノードに接続することで元の問題の解を得られます。
### 2. &nbsp; 部分木の分割方法
### 2. &nbsp; 部分木をどのように分割するか
の分析に基づいて、この問題は分割統治を使用して解決できます。**しかし、前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスを使用して左と右部分木をどのように分割すればよいでしょうか**
上の分析より、この問題は分割統治で解けます。**では、前順走査 `preorder` と中順走査 `inorder` を使って左部分木と右部分木をどのように分割すればよいでしょうか**
定義により`preorder``inorder` シーケンスの両方を3つの部分に分割できます
定義に従うと`preorder``inorder` はいずれも 3 つの部分に分けられます
- 前順走査:`[ ルート | 左部分木 | 右部分木 ]`例えば、図では、木は `[ 3 | 9 | 2 1 7 ]` に対応します。
- 中順走査:`[ 左部分木 | ルート |部分木 ]`例えば、図では、木は `[ 9 | 3 | 1 2 7 ]` に対応します。
- 前順走査:`[ 根ノード | 左部分木 | 右部分木 ]` 例えば上図の木は `[ 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 ]`対応して分割できます。
1. 前順走査の先頭要素 3 が根ノードの値です。
2. 根ノード 3 の `inorder` におけるインデックスを探すと、そのインデックスを用い`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" }
![前順走査と中順走査で部分木分割する](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" }
<p align="center"> 図 12-6 &nbsp; 前順走査と中順走査で部分木分割 </p>
<p align="center"> 図 12-6 &nbsp; 前順走査と中順走査で部分木分割する </p>
### 3. &nbsp; 変数に基づく部分木範囲の記述
### 3. &nbsp; 変数を用いて部分木区間を記述する
の分割方法に基づいて、**`preorder``inorder` シーケンスにおけるルート、左の部分木、右の部分木のインデックス範囲を取得しました**。これらのインデックス範囲を記述するために、いくつかのポインタ変数を使用します。
上の分割方法により、**根ノード、左部分木、右部分木が `preorder``inorder` の中で占めるインデックス区間**が得られました。これらのインデックス区間を表すために、いくつかのポインタ変数を導入します。
- 現在の木のルートノード `preorder` シーケンスでのインデックスを $i$ とします。
- 現在の木のルートノード `inorder` シーケンスでのインデックスを $m$ とします。
- 現在の木 `inorder` シーケンスでのインデックス範囲を $[l, r]$ とします。
- 現在の木のノード `preorder` に現れるインデックスを $i$ とします。
- 現在の木のノード `inorder` に現れるインデックスを $m$ とします。
- 現在の木 `inorder` において占めるインデックス区間を $[l, r]$ とします。
以下の表に示すように、これらの変数 `preorder` シーケンスでのルートノードのインデックスと `inorder` シーケンスでの部分木のインデックス範囲を表ます。
次の表のように、これらの変数を用いれば根ノードの `preorder` におけるインデックスと、部分木の `inorder` におけるインデックス区間を表ます。
<p align="center"> 表 12-1 &nbsp; 前順走査と中順走査でのルートノードと部分木のインデックス </p>
<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]$ |
| | 根ノードの `preorder` におけるインデックス | 部分木の `inorder` におけるインデックス区間 |
| ------ | ---------------------------- | ----------------------------- |
| 現在の木 | $i$ | $[l, r]$ |
| 左部分木 | $i + 1$ | $[l, m-1]$ |
| 右部分木 | $i + 1 + (m - l)$ | $[m+1, r]$ |
</div>
部分木のルートインデックス $(m-l)$ は「左部分木のノード数」を表すことに注意してください。より明確な理解のために、以下の図を参照することが役立つ場合があります。
右部分木の根ノードのインデックスにある $(m-l)$ は「左部分木のノード数」を意味します。下図と合わせて理解することを勧めます。
![ルートノードと左右部分木のインデックス](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" }
![ノードと左右部分木のインデックス区間の表し方](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" }
<p align="center"> 図 12-7 &nbsp; ルートノードと左右部分木のインデックス </p>
<p align="center"> 図 12-7 &nbsp; ノードと左右部分木のインデックス区間の表し方 </p>
### 4. &nbsp; コード実装
### 4. &nbsp; コード実装
$m$ の問い合わせの効率を向上させるために、ハッシュテーブル `hmap`使用して `inorder` シーケンスの要素からそのインデックスへのマッピングを格納します
$m$ の検索効率を高めるために、ハッシュテーブル `hmap`用いて配列 `inorder` の要素からインデックスへの対応を保存します
=== "Python"
@@ -81,24 +81,24 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー
l: int,
r: int,
) -> TreeNode | None:
"""二分木構築:分割統治"""
# 部分木区間が空のとき終了
"""二分木構築:分割統治"""
# 部分木区間が空なら終了する
if r - l < 0:
return None
# ルートノードを初期化
# ルートノードを初期化する
root = TreeNode(preorder[i])
# m をクエリして左部分木と右部分木を分割
# 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 の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
inorder_map = {val: i for i, val in enumerate(inorder)}
root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)
return root
@@ -107,26 +107,26 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー
=== "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 を問い合わせて左右部分木を分割
// 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) {
// ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
@@ -139,26 +139,26 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー
=== "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 を問い合わせて左右部分木を分割
// 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) {
// ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
Map<Integer, Integer> inorderMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
@@ -171,87 +171,356 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー
=== "C#"
```csharp title="build_tree.cs"
[class]{build_tree}-[func]{DFS}
/* 二分木を構築:分割統治 */
TreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {
// 部分木区間が空なら終了する
if (r - l < 0)
return null;
// ルートノードを初期化する
TreeNode root = new(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;
}
[class]{build_tree}-[func]{BuildTree}
/* 二分木を構築 */
TreeNode? BuildTree(int[] preorder, int[] inorder) {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
Dictionary<int, int> inorderMap = [];
for (int i = 0; i < inorder.Length; i++) {
inorderMap.TryAdd(inorder[i], i);
}
TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);
return root;
}
```
=== "Go"
```go title="build_tree.go"
[class]{}-[func]{dfsBuildTree}
/* 二分木を構築:分割統治 */
func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {
// 部分木区間が空なら終了する
if r-l < 0 {
return nil
}
// ルートノードを初期化する
root := NewTreeNode(preorder[i])
// m を求めて左右部分木を分割する
m := inorderMap[preorder[i]]
// 部分問題:左部分木を構築する
root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)
// 部分問題:右部分木を構築する
root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)
// 根ノードを返す
return root
}
[class]{}-[func]{buildTree}
/* 二分木を構築 */
func buildTree(preorder, inorder []int) *TreeNode {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
inorderMap := make(map[int]int, len(inorder))
for i := 0; i < len(inorder); i++ {
inorderMap[inorder[i]] = i
}
root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)
return root
}
```
=== "Swift"
```swift title="build_tree.swift"
[class]{}-[func]{dfs}
/* 二分木を構築:分割統治 */
func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {
// 部分木区間が空なら終了する
if r - l < 0 {
return nil
}
// ルートノードを初期化する
let root = TreeNode(x: preorder[i])
// m を求めて左右部分木を分割する
let m = inorderMap[preorder[i]]!
// 部分問題:左部分木を構築する
root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)
// 部分問題:右部分木を構築する
root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)
// 根ノードを返す
return root
}
[class]{}-[func]{buildTree}
/* 二分木を構築 */
func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)
}
```
=== "JS"
```javascript title="build_tree.js"
[class]{}-[func]{dfs}
/* 二分木を構築:分割統治 */
function dfs(preorder, inorderMap, i, l, r) {
// 部分木区間が空なら終了する
if (r - l < 0) return null;
// ルートノードを初期化する
const root = new TreeNode(preorder[i]);
// m を求めて左右部分木を分割する
const 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;
}
[class]{}-[func]{buildTree}
/* 二分木を構築 */
function buildTree(preorder, inorder) {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
let inorderMap = new Map();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "TS"
```typescript title="build_tree.ts"
[class]{}-[func]{dfs}
/* 二分木を構築:分割統治 */
function dfs(
preorder: number[],
inorderMap: Map<number, number>,
i: number,
l: number,
r: number
): TreeNode | null {
// 部分木区間が空なら終了する
if (r - l < 0) return null;
// ルートノードを初期化する
const root: TreeNode = new TreeNode(preorder[i]);
// m を求めて左右部分木を分割する
const 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;
}
[class]{}-[func]{buildTree}
/* 二分木を構築 */
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
let inorderMap = new Map<number, number>();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "Dart"
```dart title="build_tree.dart"
[class]{}-[func]{dfs}
/* 二分木を構築:分割統治 */
TreeNode? dfs(
List<int> preorder,
Map<int, int> inorderMap,
int i,
int l,
int r,
) {
// 部分木区間が空なら終了する
if (r - l < 0) {
return null;
}
// ルートノードを初期化する
TreeNode? root = 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;
}
[class]{}-[func]{buildTree}
/* 二分木を構築 */
TreeNode? buildTree(List<int> preorder, List<int> inorder) {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
Map<int, int> inorderMap = {};
for (int i = 0; i < inorder.length; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "Rust"
```rust title="build_tree.rs"
[class]{}-[func]{dfs}
/* 二分木を構築:分割統治 */
fn dfs(
preorder: &[i32],
inorder_map: &HashMap<i32, i32>,
i: i32,
l: i32,
r: i32,
) -> Option<Rc<RefCell<TreeNode>>> {
// 部分木区間が空なら終了する
if r - l < 0 {
return None;
}
// ルートノードを初期化する
let root = TreeNode::new(preorder[i as usize]);
// m を求めて左右部分木を分割する
let m = inorder_map.get(&preorder[i as usize]).unwrap();
// 部分問題:左部分木を構築する
root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);
// 部分問題:右部分木を構築する
root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);
// 根ノードを返す
Some(root)
}
[class]{}-[func]{build_tree}
/* 二分木を構築 */
fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
let mut inorder_map: HashMap<i32, i32> = HashMap::new();
for i in 0..inorder.len() {
inorder_map.insert(inorder[i], i as i32);
}
let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);
root
}
```
=== "C"
```c title="build_tree.c"
[class]{}-[func]{dfs}
/* 二分木を構築:分割統治 */
TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {
// 部分木区間が空なら終了する
if (r - l < 0)
return NULL;
// ルートノードを初期化する
TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));
root->val = preorder[i];
root->left = NULL;
root->right = NULL;
// m を求めて左右部分木を分割する
int m = inorderMap[preorder[i]];
// 部分問題:左部分木を構築する
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);
// 部分問題:右部分木を構築する
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);
// 根ノードを返す
return root;
}
[class]{}-[func]{buildTree}
/* 二分木を構築 */
TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);
for (int i = 0; i < inorderSize; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);
free(inorderMap);
return root;
}
```
=== "Kotlin"
```kotlin title="build_tree.kt"
[class]{}-[func]{dfs}
/* 二分木を構築:分割統治 */
fun dfs(
preorder: IntArray,
inorderMap: Map<Int?, Int?>,
i: Int,
l: Int,
r: Int
): TreeNode? {
// 部分木区間が空なら終了する
if (r - l < 0) return null
// ルートノードを初期化する
val root = TreeNode(preorder[i])
// m を求めて左右部分木を分割する
val 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
}
[class]{}-[func]{buildTree}
/* 二分木を構築 */
fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
// inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
val inorderMap = HashMap<Int?, Int?>()
for (i in inorder.indices) {
inorderMap[inorder[i]] = i
}
val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)
return root
}
```
=== "Ruby"
```ruby title="build_tree.rb"
[class]{}-[func]{dfs}
### 二分木を構築:分割統治 ###
def dfs(preorder, inorder_map, i, l, r)
# 部分木区間が空なら終了する
return if r - l < 0
[class]{}-[func]{build_tree}
# ルートノードを初期化する
root = TreeNode.new(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)
# 根ノードを返す
root
end
### 二分木を構築 ###
def build_tree(preorder, inorder)
# inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する
inorder_map = {}
inorder.each_with_index { |val, i| inorder_map[val] = i }
dfs(preorder, inorder_map, 0, 0, inorder.length - 1)
end
```
以下の図は、二分木を構築する再帰過程を示しています。各ノードは再帰の「下降」段階で作成され、各エッジ(参照)は「上昇」段階で形成されます。
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9C%A8%E3%83%8E%E3%83%BC%E3%83%89%E3%82%AF%E3%83%A9%E3%82%B9%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%E5%80%A4%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E3%83%8E%E3%83%BC%E3%83%89%E3%81%B8%E3%81%AE%E5%8F%82%E7%85%A7%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E3%83%8E%E3%83%BC%E3%83%89%E3%81%B8%E3%81%AE%E5%8F%82%E7%85%A7%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D%2C%0A%20%20%20%20inorder_map%3A%20dict%5Bint%2C%20int%5D%2C%0A%20%20%20%20i%3A%20int%2C%0A%20%20%20%20l%3A%20int%2C%0A%20%20%20%20r%3A%20int%2C%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%EF%BC%9A%E5%88%86%E5%89%B2%E7%B5%B1%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E6%9C%A8%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AA%E3%82%89%E7%B5%82%E4%BA%86%E3%81%99%E3%82%8B%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%88%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20m%20%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6%E5%B7%A6%E5%8F%B3%E9%83%A8%E5%88%86%E6%9C%A8%E3%82%92%E5%88%86%E5%89%B2%E3%81%99%E3%82%8B%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%EF%BC%9A%E5%B7%A6%E9%83%A8%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B%0A%20%20%20%20root.left%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%2C%20l%2C%20m%20-%201%29%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%EF%BC%9A%E5%8F%B3%E9%83%A8%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B%0A%20%20%20%20root.right%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%20%2B%20m%20-%20l%2C%20m%20%2B%201%2C%20r%29%0A%20%20%20%20%23%20%E6%A0%B9%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D%2C%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%22%22%22%0A%20%20%20%20%23%20inorder%20%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%8B%E3%82%89%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%B8%E3%81%AE%E5%AF%BE%E5%BF%9C%E3%82%92%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i%2C%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder%2C%20inorder_map%2C%200%2C%200%2C%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3%2C%209%2C%202%2C%201%2C%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9%2C%203%2C%201%2C%202%2C%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E9%A0%86%E8%B5%B0%E6%9F%BB%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E9%A0%86%E8%B5%B0%E6%9F%BB%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder%2C%20inorder%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9C%A8%E3%83%8E%E3%83%BC%E3%83%89%E3%82%AF%E3%83%A9%E3%82%B9%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%E5%80%A4%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E3%83%8E%E3%83%BC%E3%83%89%E3%81%B8%E3%81%AE%E5%8F%82%E7%85%A7%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E3%83%8E%E3%83%BC%E3%83%89%E3%81%B8%E3%81%AE%E5%8F%82%E7%85%A7%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D%2C%0A%20%20%20%20inorder_map%3A%20dict%5Bint%2C%20int%5D%2C%0A%20%20%20%20i%3A%20int%2C%0A%20%20%20%20l%3A%20int%2C%0A%20%20%20%20r%3A%20int%2C%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%EF%BC%9A%E5%88%86%E5%89%B2%E7%B5%B1%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E6%9C%A8%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AA%E3%82%89%E7%B5%82%E4%BA%86%E3%81%99%E3%82%8B%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%88%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20m%20%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6%E5%B7%A6%E5%8F%B3%E9%83%A8%E5%88%86%E6%9C%A8%E3%82%92%E5%88%86%E5%89%B2%E3%81%99%E3%82%8B%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%EF%BC%9A%E5%B7%A6%E9%83%A8%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B%0A%20%20%20%20root.left%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%2C%20l%2C%20m%20-%201%29%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%EF%BC%9A%E5%8F%B3%E9%83%A8%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B%0A%20%20%20%20root.right%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%20%2B%20m%20-%20l%2C%20m%20%2B%201%2C%20r%29%0A%20%20%20%20%23%20%E6%A0%B9%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D%2C%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9C%A8%E3%82%92%E6%A7%8B%E7%AF%89%22%22%22%0A%20%20%20%20%23%20inorder%20%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%8B%E3%82%89%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%B8%E3%81%AE%E5%AF%BE%E5%BF%9C%E3%82%92%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i%2C%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder%2C%20inorder_map%2C%200%2C%200%2C%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3%2C%209%2C%202%2C%201%2C%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9%2C%203%2C%201%2C%202%2C%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E9%A0%86%E8%B5%B0%E6%9F%BB%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E9%A0%86%E8%B5%B0%E6%9F%BB%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder%2C%20inorder%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
下図は二分木を構築する再帰過程を示しています。各ノードは下向きに「再帰していく」過程で生成され、各辺(参照)は上向きに「戻る」過程で張られます。
=== "<1>"
![二分木構築再帰過程](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" }
![二分木構築する再帰過程](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" }
@@ -277,14 +546,14 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー
=== "<9>"
![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png){ class="animation-figure" }
<p align="center"> 図 12-8 &nbsp; 二分木構築再帰過程 </p>
<p align="center"> 図 12-8 &nbsp; 二分木構築する再帰過程 </p>
各再帰関数 `preorder` と `inorder` シーケンスの分割は以下の図に示されています。
各再帰関数における前順走査 `preorder` と中順走査 `inorder` の分割結果を下図に示します。
![各再帰関数での分割](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" }
![各再帰関数での分割結果](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" }
<p align="center"> 図 12-9 &nbsp; 各再帰関数での分割 </p>
<p align="center"> 図 12-9 &nbsp; 各再帰関数での分割結果 </p>
二分木が $n$ 個のノードを持つと仮定すると、各ノードの初期化(再帰関数 `dfs()` の呼び出し)には $O(1)$ 時間かかります。**したがって、全体の時間計算量は $O(n)$ です**
木のノード数を $n$ とすると、各ノードの初期化(再帰関数 `dfs()` の 1 回の実行)には $O(1)$ 時間かかります。**したがって、全体の時間計算量は $O(n)$** です。
ハッシュテーブルは `inorder` 要素からそのインデックスへのマッピングを格納するため、$O(n)$ スペースが必要です。最悪の場合、二分木が連結リストに退化すると、再帰の深さは $n$ に達し、$O(n)$ のスタックスペースを消費する可能性があります。**したがって、全体の空間計算量は $O(n)$ です**
ハッシュテーブルは `inorder` 要素からインデックスへの対応を保存するため、空間計算量は $O(n)$ です。最悪の場合、すなわち二分木が連結リストに退化すると、再帰の深さは $n$ に達し、$O(n)$ のスタックフレーム空間を使用します。**したがって、全体の空間計算量は $O(n)$** です。
@@ -2,45 +2,45 @@
comments: true
---
# 12.1 &nbsp; 分割統治アルゴリズム
# 12.1 &nbsp; 分割統治
<u>分割統治</u>は重要で人気のあるアルゴリズム戦略です。名前が示すように、アルゴリズムは通常再帰に実装され、「分」と「治」の2つのステップから構成されます。
<u>分割統治法(divide and conquer</u>は、問題を分けて統べるという意味であり、非常に重要で一般的なアルゴリズム戦略です。分割統治法は通常再帰に基づいて実装され、「分」と「治」の 2 つのステップから構成されます。
1. **分(分割段階)**:元の問題を再帰的に2つ以上の小さな部分問題に分解し、最小の部分問題に到達するまで続けます。
2. **治(マージ段階)**:解決方法が既知の最小の部分問題から開始し、部分問題の解をボトムアップ方式でマージして元の問題の解を構築します。
1. **分(分割段階)**:元の問題を 2 つ以上の部分問題へ再帰的に分解し、最小の部分問題に到達した時点で停止します。
2. **治(統合段階)**:解が既知である最小の部分問題から始めて、部分問題の解を下から上へ統合し、元の問題の解を構築します。
以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。
1. **分**:元の配列(元の問題)を再帰的に2つの副配列(部分問題)に分割し、副配列が1つの要素のみになるまで(最小の部分問題)続けます。
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; 分割統治問題を特定する方法
## 12.1.1 &nbsp; 分割統治法の問題を見極めるには
問題が分割統治解決に適しているかどうかは、通常以下の基準に基づいて決定できます。
ある問題が分割統治法で解くのに適しているかどうかは、通常、次の判断基準を参考にできます。
1. **問題をより小さなものに分解できる**:元の問題より小さく類似した部分問題に分でき、そのような過程を同じ方法で再帰的に実行できます。
2. **部分問題は独立している**:部分問題に重複がなく、独立しており、個別に解決できます。
3. **部分問題の解をマージできる**:元の問題の解は、部分問題の解を組み合わせることで導出されます。
1. **問題分解できる**:元の問題は、より小さく類似した部分問題に分でき、同じ方法で再帰的に分割できます。
2. **部分問題は独立している**:部分問題同士に重複がなく、相互依存もないため、独立して解決できます。
3. **部分問題の解は統合できる**:元の問題の解は、部分問題の解を統合することで得られます。
明らかに、マージソートはこれら3つの基準を満たしています。
明らかに、マージソートは以上の 3 つの判断基準を満たしています。
1. **問題をより小さなものに分解できる**:配列(元の問題)を再帰的に2つの副配列(部分問題)に分割します。
2. **部分問題は独立している**:各配列は独立してソートできます(部分問題は独立して解決できます)。
3. **部分問題の解をマージできる**2つの順序付けられた副配列(部分問題の解)を1つの順序付けられた配列(元の問題の解)にマージできます。
1. **問題分解できる**:配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割します。
2. **部分問題は独立している**:各部分配列は独立ソートできます(部分問題は独立に解けます)。
3. **部分問題の解は統合できる**2 つの整列済み部分配列(部分問題の解)は、1 つの整列済み配列(元の問題の解)に統合できます。
## 12.1.2 &nbsp; 分割統治による効率の向上
## 12.1.2 &nbsp; 分割統治法で効率を高める
**分割統治戦略はアルゴリズム問題を効果的に解決するだけでなく、しばしば効率を向上させます**。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートは、分割統治戦略を適用しているため、選択ソート、バブルソート、挿入ソートより高速です。
**分割統治はアルゴリズム問題を効果的に解るだけでなく、多くの場合アルゴリズムの効率も高められます**。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソート選択ソート、バブルソート、挿入ソートより高速ですが、これは分割統治戦略を適用しているためです
私たちの心には疑問があるかもしれません:**なぜ分割統治はアルゴリズム効率を向上させることができ、その根本的な論理は何ですか?** つまり、問題を部分問題に分解し、それらを解決し、それらの解を組み合わせて元の問題に対処することが、元の問題を直接解決するより効率的である理由は何ですか?この質問は2つの側面から分析できます:操作数と並列計算
ここで次の疑問が生じます。**なぜ分割統治はアルゴリズム効率を高められるのでしょうか。その根本的な仕組みは何でしょうか**?言い換えると、大きな問題を複数の部分問題に分解し、部分問題を解き、それらの解を統合して元の問題の解にするという手順は、なぜ元の問題を直接解より効率的なのでしょうか。この問題は、操作回数と並列計算の 2 つの観点から議論できます
### 1. &nbsp; 操作数の最適化
### 1. &nbsp; 操作数の最適化
「バブルソート」を例にると、長さ $n$ の配列を処理するのに $O(n^2)$ 時間が必要です。以下の図に示すように、配列を中点から2つの副配列に分割するとします。そのような分割には $O(n)$ 時間が必要です。各副配列のソートには $O((n / 2)^2)$ 時間が必要です。そして2つの副配列のマージには $O(n)$ 時間が必要です。したがって、全体の時間計算量は:
「バブルソート」を例にると、長さ $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)
@@ -50,7 +50,7 @@ $$
<p align="center"> 図 12-2 &nbsp; 配列分割前後のバブルソート </p>
以下の不等式を計算してみましょう。左側は分割前の総操作数を表し、右側は分割後の操作数をそれぞれ表します:
次に、以下の不等式を計算します。左辺と右辺はそれぞれ、分割前と分割後の操作総数です:
$$
\begin{aligned}
@@ -60,42 +60,42 @@ n(n - 4) & > 0
\end{aligned}
$$
**これは $n > 4$ の場合、分割後の操作数が少なく、より良いパフォーマンスにつながることを意味します**。分割後の時間計算量は依然として二次 $O(n^2)$ ですが、計算量の定数係数が減少していることに注意してください
**これは$n > 4$ のときに分割後の操作回数の方が少なくなり、ソート効率が高くなることを意味します**ただし、分割後の時間計算量は依然として 2 次の $O(n^2)$ であり、計算量の定数項が小さくなっただけです
さらに進むことができます。**副配列をその中点からさらに2つの副配列に分割し続けて、副配列が1つの要素のみになるまで続けたらどうでしょうか?** このアイデアは実際には「マージソート」で、時間計算量は $O(n \log n)$ です。
さらに考えると、**部分配列を中央からさらに 2 つの部分配列へと分割し続け**、部分配列に要素が 1 つだけ残るまで分割を止めないとしたらどうでしょうか。この考え方がまさに「マージソート」であり、時間計算量は $O(n \log n)$ です。
少し違うことを試してみましょう。**2つではなく、より多くの分割に分割したらどうでしょうか?** 例えば、元の配列を $k$ 個の副配列に均等に分割しますか?このアプローチは「バケットソート」と非常に似ており、大量データのソートに非常に適しています。理論的には、時間計算量は $O(n + k)$ に達することができます。
さらに、**分割点をいくつか増やして**、元の配列を平均的に $k$ 個の部分配列に分けるとしたらどうでしょうか。この状況は「バケットソート」と非常によく似ており、大量データのソートに非常に適しています。理論上の時間計算量は $O(n + k)$ に達ます。
### 2. &nbsp; 並列計算による最適化
### 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; 分割統治の一般的な応用
## 12.1.3 &nbsp; 分割統治法の代表的な応用
分割統治は多くの古典的なアルゴリズム問題を解決するために使用できます。
一方では、分割統治は多くの古典的なアルゴリズム問題を解くのに使えます。
- **最近点対の発見**:このアルゴリズムは点の集合を2つの半分に分割することで動作します。そして各半分で再帰的に最近点対を見つけます。最後に、2つの半分にまたがるペアを考慮して、全体の最近点対を見つけます。
- **大整数乗算**一つのアルゴリズムはKaratsubaと呼ばれます。大整数の乗算をいくつかの小さな整数の乗算と加算に分解します。
- **行列乗算**一例はStrassenアルゴリズムです。大きな行列の乗算を複数の小さな行列の乗算と加算に分解します。
- **ハノイの塔問題**:ハノイの塔問題は再帰的に解決でき、分割統治戦略の典型的な応用です。
- **転倒対の解決**:シーケンスで、前の数が後の数より大きい場合、これら2つの数は転対を構成します。転倒対問題の解決は、マージソートの助けを借りて、分割統治のアイデアを利用できます。
- **最近点対探索**:このアルゴリズムは、まず点集合を 2 つに分け、それぞれの部分における最近点対を求め、最後に 2 つの部分をまたぐ最近点対を求めます。
- **大整数乗算**たとえば Karatsuba 法では、大整数の乗算を、より小さな整数どうしのいくつかの乗算と加算に分解します。
- **行列乗算**たとえば Strassen 法では、大きな行列の乗算を複数の小さな行列の乗算と加算に分解します。
- **ハノイの塔問題**:ハノイの塔問題は再帰によって解くことができ、これは典型的な分割統治戦略の応用です。
- **反転対の計算**:ある数列で前の数が後の数より大きい場合、その 2 つの数は転対を構成します。反転対の問題は、分割統治の考え方を利用し、マージソートを用いて解けます。
分割統治はアルゴリズムとデータ構造の設計にも広く応用されています。
他方で、分割統治はアルゴリズムとデータ構造の設計にも非常に広く応用されています。
- **二分探索**:二分探索は、ソート済み配列を中インデックスから2つの半分に分割します。そして、ターゲット値と中要素の比較結果に基づいて、一方の半分が破棄されます。同じプロセスで残りの半分で検索が続行され、ターゲットが見つかるか残りの要素がなくなるまで続きます。
- **マージソート**この節の冒頭ですでに紹介したため、さらなる詳述は不要です
- **クイックソート**:クイックソートはピボット値を選択して配列を2つの副配列に分割し、一方はピボットより小さい要素、もう一方はピボットより大きい要素を持ちます。このプロセスは、これら2つの副配列のそれぞれに対して、1つの要素のみを保持するまで続ます。
- **バケットソート**:バケットソートの基本的なアイデアは、データを複数のバケットに分散させることです。各バケット内の要素をソートした後、バケットから順序よく要素を取得して順序付けられた配列を取得します。
- **木**えば二分探索木、AVL木、赤黒木、B木、B+木など。その操作(検索、挿入削除)はすべて分割統治戦略の応用と見なすことができます。
- **ヒープ**:ヒープは特別なタイプの完全二分木です。その様々な操作(挿入、削除、ヒープ化は、実際に分割統治のアイデアを含意しています。
- **ハッシュテーブル**:ハッシュテーブルは直接分割統治を適用しませんが、一部のハッシュ衝突解決ソリューションは間接的にこの戦略を適用します。えば、チェイン法の長いリストは、クエリ効率を向上させるために赤黒木変換される場合があります。
- **二分探索**:二分探索は、整列済み配列を中央のインデックスで 2 つに分け、目標値と中要素の比較結果に基づいてどちらの半区間を除外するかを決め、残った区間で同じ二分操作を行います。
- **マージソート**節の冒頭で紹介したため、ここでは繰り返しません
- **クイックソート**:クイックソートは基準値を 1 つ選び、配列を、基準値より小さい要素の部分配列と、基準値より大きい要素の部分配列に分け、その後それぞれに対して同じ分割操作を行い、部分配列に要素が 1 つだけ残るまで続ます。
- **バケットソート**:バケットソートの基本的な考え方は、データを複数のバケットに分散し、各バケット内の要素をソートしたうえで、各バケットの要素を順に取り出して整列済み配列を得ることです。
- **木構造**たとえば二分探索木、AVL 木、赤黒木、B 木、B+ 木などでは、探索・挿入削除などの操作をいずれも分割統治戦略の応用とみなせます。
- **ヒープ**:ヒープは特殊な完全二分木であり、挿入、削除、ヒープ化などの各種操作には、実際に分割統治の考え方が含まれています。
- **ハッシュテーブル**:ハッシュテーブル自体は分割統治を直接適用しているわけではありませんが、いくつかのハッシュ衝突解決法では間接的に分割統治戦略が使われています。たとえば、連鎖アドレス法における長い連結リストは、検索効率を高めるために赤黒木変換されます。
**分割統治は巧妙に浸透するアルゴリズムアイデア**であり、様々なアルゴリズムデータ構造に組み込まれていることが分かります。
このように、**分割統治法は「静かに物を潤す」ようなアルゴリズム思想**であり、さまざまなアルゴリズムデータ構造の中に潜んでいます。
@@ -2,44 +2,44 @@
comments: true
---
# 12.4 &nbsp; ハノイの塔問題
# 12.4 &nbsp; ハノイの塔問題
マージソート二分木構築の両方で、元の問題を2つの部分問題に分解し、それぞれが元の問題のサイズの半分でした。しかし、ハノイの塔では、異なる分解戦略を採用します。
マージソート二分木構築では、いずれも元の問題を元問題の半分の規模をもつ 2 つの部分問題に分解していました。しかし、ハノイの塔の問題では、異なる分解戦略を採用します。
!!! question
3の柱があり、それぞれ `A``B``C`表記されます。最初、柱 `A` $n$ 枚の円盤がり、上から下に向かって昇順のサイズで配置されています。私たちのタスクは、これらの $n$ 枚の円盤を柱 `C` に移し、元の順序を維持することです(以下の図に示すように)。移動中には以下のルールが適用されます
3の柱があり、それぞれ `A``B``C`します。初期状態では、柱 `A` に $n$ 枚の円盤が通されており、上から下へ小さい順に並んでいます。私たちの課題は、この $n$ 枚の円盤を柱 `C` に移し、元の順序を保つことです(以下の図のとおり)。円盤を移動する際には、次のルールに従う必要があります
1. 円盤は 1 本の柱の頂上から取り出し、別の柱の頂上に置くことしかできません。
2. 1 回に移動できる円盤は 1 枚だけです。
3. 小さい円盤は常に大きい円盤の上になければなりません。
1. 円盤は柱の上部からのみ取り除くことができ、別の柱の上部に置く必要があります。
2. 一度に移動できるのは1枚の円盤のみです。
3. 小さい円盤は常に大きい円盤の上にある必要があります。
![ハノイの塔の問題の例](hanota_problem.assets/hanota_example.png){ class="animation-figure" }
![ハノイの塔の例](hanota_problem.assets/hanota_example.png){ class="animation-figure" }
<p align="center"> 図 12-10 &nbsp; ハノイの塔の問題の例 </p>
<p align="center"> 図 12-10 &nbsp; ハノイの塔の例 </p>
**サイズ $i$ のハノイの塔問題を $f(i)$ と表記します**。例えば、$f(3)$ は3枚の円盤を柱 `A` から柱 `C` に移動することを表します。
**規模が $i$ のハノイの塔の問題を $f(i)$ と表します** 。たとえば $f(3)$ は、$3$ 枚の円盤を `A` から `C` へ移動するハノイの塔の問題を表します。
### 1. &nbsp; 基本ケースを考える
以下の図に示すように、問題 $f(1)$(円盤が1枚のみ)については、`A` から `C` 直接移動できます。
以下の図に示すように、問題 $f(1)$ 、すなわち円盤が 1 枚だけの場合は、それを `A` から `C` 直接移動すれば済みます。
=== "<1>"
![サイズ1の問題の解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" }
![規模 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>
<p align="center"> 図 12-11 &nbsp; 規模 1 の問題の解 </p>
$f(2)$(円盤が2枚)については、**柱 `B` の助けを借りて小さい円盤大きい円盤の上に保つ**必要があります。以下の図に示すように:
以下の図に示すように、問題 $f(2)$ 、すなわち円盤が 2 枚ある場合は、**小さい円盤が常に大きい円盤の上にある条件を満たすため、`B` を借りて移動を行う必要があります**。
1. まず小さい円盤を `A` から `B` に移動します。
2. 次に大きい円盤を `A` から `C` に移動します。
3. 最後に小さい円盤を `B` から `C` に移動します。
1. まず上の小さい円盤を `A` から `B` へ移します。
2. 次に大きい円盤を `A` から `C` へ移します。
3. 最後に小さい円盤を `B` から `C` へ移します。
=== "<1>"
![サイズ2の問題の解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" }
![規模 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" }
@@ -50,22 +50,22 @@ $f(2)$(円盤が2枚)については、**柱 `B` の助けを借りて小さ
=== "<4>"
![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png){ class="animation-figure" }
<p align="center"> 図 12-12 &nbsp; サイズ2の問題の解 </p>
<p align="center"> 図 12-12 &nbsp; 規模 2 の問題の解 </p>
$f(2)$ を解決する過程は次のように要約できます:**`B` の助けを借りて2枚の円盤を `A` から `C` に移動する**。ここで`C`ターゲット柱、`B`バッファ柱と呼びます。
問題 $f(2)$ を解く過程は、**2 枚の円盤を `B` を介して `A` から `C` へ移す**と要約できます。このとき`C`目標の柱、`B`補助の柱と呼びます。
### 2. &nbsp; 部分問題の分解
### 2. &nbsp; 部分問題の分解
問題 $f(3)$(つまり、円盤が3枚の場合)については、状況やや複雑になります。
問題 $f(3)$ 、すなわち円盤が 3 枚ある場合になると、状況やや複雑になります。
すでに $f(1)$ と $f(2)$ の解が分かっているので、分割統治の観点を採用し、**`A` の上の2枚の円盤を1つの単位として扱い**、以下の図に示すステップを実行できます。これにより、3枚の円盤を `A` から `C` に正常に移動できます。
$f(1)$ と $f(2)$ の解が既知なので、分割統治の観点から、**`A` の上部にある 2 枚の円盤をひとまとまりとみなして**、次の図の手順を実行できます。こうして 3 枚の円盤を `A` から `C` へ順調に移動できます。
1. `B`ターゲット柱、`C`バッファ柱として、2枚の円盤を `A` から `B` に移動します。
2. 残りの円盤を `A` から直接 `C` 移動します。
3. `C`ターゲット柱、`A`バッファ柱として、2枚の円盤を `B` から `C` に移動します。
1. `B`目標の柱、`C`補助の柱として、2 枚の円盤を `A` から `B` へ移します。
2. `A` に残った 1 枚の円盤を `A` から `C` へ直接移動します。
3. `C`目標の柱、`A`補助の柱として、2 枚の円盤を `B` から `C` へ移します。
=== "<1>"
![サイズ3の問題の解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" }
![規模 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" }
@@ -76,87 +76,87 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助
=== "<4>"
![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png){ class="animation-figure" }
<p align="center"> 図 12-13 &nbsp; サイズ3の問題の解 </p>
<p align="center"> 図 12-13 &nbsp; 規模 3 の問題の解 </p>
本質的に、**$f(3)$ を2つの $f(2)$ 部分問題と1つの $f(1)$ 部分問題に分解します**。これら3つの部分問題を順次解決することで、元の問題解決され、部分問題が独立しており、それらの解をマージできることを示しています。
本質的に、**問題 $f(3)$ を 2 つの部分問題 $f(2)$ と 1 つの部分問題 $f(1)$ に分けています** 。この 3 つの部分問題を順に解けば、元の問題解決されます。これは、部分問題が独立しており、解を組み合わせられることを示しています。
ここから、以下の図に示すハノイの塔の分割統治戦略を要約できます。元の問題 $f(n)$ を2つの部分問題 $f(n-1)$ と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` に移動します。
1. $n-1$ 枚の円盤を `C` を介して `A` から `B` へ移します。
2. 残り $1$ 枚の円盤を `A` から `C` へ直接移します。
3. $n-1$ 枚の円盤を `A` を介して `B` から `C` へ移します。
各 $f(n-1)$ 部分問題について、**同じ再帰分割を適用でき**最小の部分問題 $f(1)$ に到達するまで続けます。$f(1)$ は単一の移動のみが必要であることがすでに分かっているため、解決するのは簡単です。
この 2 つの部分問題 $f(n-1)$ は、**同じ方法で再帰的に分割できます**最小の部分問題 $f(1)$ に到達するまでこれを続けます。一方、$f(1)$ の解は既知であり、1 回の移動操作だけで済みます。
![ハノイの塔を解決するための分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" }
![ハノイの塔の問題を解く分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" }
<p align="center"> 図 12-14 &nbsp; ハノイの塔を解決するための分割統治戦略 </p>
<p align="center"> 図 12-14 &nbsp; ハノイの塔の問題を解く分割統治戦略 </p>
### 3. &nbsp; コード実装
### 3. &nbsp; コード実装
コードでは、再帰関数 `dfs(i, src, buf, tar)` を定義します。これは`src` から上の $i$ 枚の円盤を`tar` に移動し、柱 `buf` をバッファとして使用します:
コードでは、再帰関数 `dfs(i, src, buf, tar)` を定義します。その役割は、`src`上部にある $i$ 枚の円盤を、補助の柱 `buf` を使って目標の柱 `tar` へ移動することです:
=== "Python"
```python title="hanota.py"
def move(src: list[int], tar: list[int]):
"""円盤を移動"""
# src の上から円盤を取り出す
"""円盤を 1 枚移動"""
# src の上から円盤を1枚取り出す
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 に移動
"""ハノイの塔問題 f(i) を解く"""
# src に円盤が 1 だけ残っている場合、そのまま tar へ移す
if i == 1:
move(src, tar)
return
# 部分問題 f(i-1)tar の助けを借りて src の上 i-1 の円盤を buf に移動
# 部分問題 f(i-1)src の上 i-1 の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf)
# 部分問題 f(1):残りの 1 の円盤を src から tar に移
# 部分問題 f(1):src に残る 1 の円盤を tar に移
move(src, tar)
# 部分問題 f(i-1)src の助けを借りて buf の上 i-1 の円盤を tar に移動
# 部分問題 f(i-1)buf の上 i-1 の円盤を src を補助にして 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 に移動
# A の上から n の円盤を B を介して C へ移す
dfs(n, A, B, C)
```
=== "C++"
```cpp title="hanota.cpp"
/* 円盤を移動 */
/* 円盤を 1 枚移動 */
void move(vector<int> &src, vector<int> &tar) {
// src の最上部から円盤を取り出す
// src のから円盤を1枚取り出す
int pan = src.back();
src.pop_back();
// 円盤を tar の最上部に配置
// 円盤を tar の上に置く
tar.push_back(pan);
}
/* ハノイの塔問題 f(i) を解く */
/* ハノイの塔問題 f(i) を解く */
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
// src に円盤が1つだけ残っている場合、それを tar に移動
// src に円盤が 1 枚だけ残っている場合、そのまま tar へ移す
if (i == 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1)tar の助けを借りて、上位 i-1 の円盤を src から buf に移動
// 部分問題 f(i-1)src の上部 i-1 の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf);
// 部分問題 f(1)残りの1つの円盤を src から tar に移
// 部分問題 f(1)src に残る 1 枚の円盤を tar に移
move(src, tar);
// 部分問題 f(i-1)src の助けを借りて、上位 i-1 の円盤を buf から tar に移動
// 部分問題 f(i-1)buf の上部 i-1 の円盤を src を補助にして 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 に移動
// A上から n の円盤を B を介して C へ移す
dfs(n, A, B, C);
}
```
@@ -164,33 +164,33 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助
=== "Java"
```java title="hanota.java"
/* 円盤を移動 */
/* 円盤を 1 枚移動 */
void move(List<Integer> src, List<Integer> tar) {
// src の最上部から円盤を取り出す
// src のから円盤を1枚取り出す
Integer pan = src.remove(src.size() - 1);
// 円盤を tar の最上部に配置
// 円盤を tar の上に置く
tar.add(pan);
}
/* ハノイの塔問題 f(i) を解く */
/* ハノイの塔問題 f(i) を解く */
void dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {
// src に円盤が1つだけ残っている場合、それを tar に移動
// src に円盤が 1 枚だけ残っている場合、そのまま tar へ移す
if (i == 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1)tar の助けを借りて、上位 i-1 の円盤を src から buf に移動
// 部分問題 f(i-1)src の上部 i-1 の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf);
// 部分問題 f(1)残りの1つの円盤を src から tar に移
// 部分問題 f(1)src に残る 1 枚の円盤を tar に移
move(src, tar);
// 部分問題 f(i-1)src の助けを借りて、上位 i-1 の円盤を buf から tar に移動
// 部分問題 f(i-1)buf の上部 i-1 の円盤を src を補助にして 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 に移動
// A上から n の円盤を B を介して C へ移す
dfs(n, A, B, C);
}
```
@@ -198,111 +198,363 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助
=== "C#"
```csharp title="hanota.cs"
[class]{hanota}-[func]{Move}
/* 円盤を 1 枚移動 */
void Move(List<int> src, List<int> tar) {
// src の上から円盤を1枚取り出す
int pan = src[^1];
src.RemoveAt(src.Count - 1);
// 円盤を tar の上に置く
tar.Add(pan);
}
[class]{hanota}-[func]{DFS}
/* ハノイの塔の問題 f(i) を解く */
void DFS(int i, List<int> src, List<int> buf, List<int> tar) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if (i == 1) {
Move(src, tar);
return;
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
DFS(i - 1, src, tar, buf);
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
Move(src, tar);
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
DFS(i - 1, buf, src, tar);
}
[class]{hanota}-[func]{SolveHanota}
/* ハノイの塔を解く */
void SolveHanota(List<int> A, List<int> B, List<int> C) {
int n = A.Count;
// A の上から n 枚の円盤を B を介して C へ移す
DFS(n, A, B, C);
}
```
=== "Go"
```go title="hanota.go"
[class]{}-[func]{move}
/* 円盤を 1 枚移動 */
func move(src, tar *list.List) {
// src の上から円盤を1枚取り出す
pan := src.Back()
// 円盤を tar の上に置く
tar.PushBack(pan.Value)
// `src` の最上部の円盤を取り外す
src.Remove(pan)
}
[class]{}-[func]{dfsHanota}
/* ハノイの塔の問題 f(i) を解く */
func dfsHanota(i int, src, buf, tar *list.List) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if i == 1 {
move(src, tar)
return
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfsHanota(i-1, src, tar, buf)
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src, tar)
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfsHanota(i-1, buf, src, tar)
}
[class]{}-[func]{solveHanota}
/* ハノイの塔を解く */
func solveHanota(A, B, C *list.List) {
n := A.Len()
// A の上から n 枚の円盤を B を介して C へ移す
dfsHanota(n, A, B, C)
}
```
=== "Swift"
```swift title="hanota.swift"
[class]{}-[func]{move}
/* 円盤を 1 枚移動 */
func move(src: inout [Int], tar: inout [Int]) {
// src の上から円盤を1枚取り出す
let pan = src.popLast()!
// 円盤を tar の上に置く
tar.append(pan)
}
[class]{}-[func]{dfs}
/* ハノイの塔の問題 f(i) を解く */
func dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if i == 1 {
move(src: &src, tar: &tar)
return
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src: &src, tar: &tar)
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)
}
[class]{}-[func]{solveHanota}
/* ハノイの塔を解く */
func solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {
let n = A.count
// リストの末尾が柱の上端に対応する
// src の上から n 個の円盤を、B を介して C に移動する
dfs(i: n, src: &A, buf: &B, tar: &C)
}
```
=== "JS"
```javascript title="hanota.js"
[class]{}-[func]{move}
/* 円盤を 1 枚移動 */
function move(src, tar) {
// src の上から円盤を1枚取り出す
const pan = src.pop();
// 円盤を tar の上に置く
tar.push(pan);
}
[class]{}-[func]{dfs}
/* ハノイの塔の問題 f(i) を解く */
function dfs(i, src, buf, tar) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if (i === 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf);
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src, tar);
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i - 1, buf, src, tar);
}
[class]{}-[func]{solveHanota}
/* ハノイの塔を解く */
function solveHanota(A, B, C) {
const n = A.length;
// A の上から n 枚の円盤を B を介して C へ移す
dfs(n, A, B, C);
}
```
=== "TS"
```typescript title="hanota.ts"
[class]{}-[func]{move}
/* 円盤を 1 枚移動 */
function move(src: number[], tar: number[]): void {
// src の上から円盤を1枚取り出す
const pan = src.pop();
// 円盤を tar の上に置く
tar.push(pan);
}
[class]{}-[func]{dfs}
/* ハノイの塔の問題 f(i) を解く */
function dfs(i: number, src: number[], buf: number[], tar: number[]): void {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if (i === 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf);
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src, tar);
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i - 1, buf, src, tar);
}
[class]{}-[func]{solveHanota}
/* ハノイの塔を解く */
function solveHanota(A: number[], B: number[], C: number[]): void {
const n = A.length;
// A の上から n 枚の円盤を B を介して C へ移す
dfs(n, A, B, C);
}
```
=== "Dart"
```dart title="hanota.dart"
[class]{}-[func]{move}
/* 円盤を 1 枚移動 */
void move(List<int> src, List<int> tar) {
// src の上から円盤を1枚取り出す
int pan = src.removeLast();
// 円盤を tar の上に置く
tar.add(pan);
}
[class]{}-[func]{dfs}
/* ハノイの塔の問題 f(i) を解く */
void dfs(int i, List<int> src, List<int> buf, List<int> tar) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if (i == 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf);
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src, tar);
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i - 1, buf, src, tar);
}
[class]{}-[func]{solveHanota}
/* ハノイの塔を解く */
void solveHanota(List<int> A, List<int> B, List<int> C) {
int n = A.length;
// A の上から n 枚の円盤を B を介して C へ移す
dfs(n, A, B, C);
}
```
=== "Rust"
```rust title="hanota.rs"
[class]{}-[func]{move_pan}
/* 円盤を 1 枚移動 */
fn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {
// src の上から円盤を1枚取り出す
let pan = src.pop().unwrap();
// 円盤を tar の上に置く
tar.push(pan);
}
[class]{}-[func]{dfs}
/* ハノイの塔の問題 f(i) を解く */
fn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if i == 1 {
move_pan(src, tar);
return;
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf);
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move_pan(src, tar);
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i - 1, buf, src, tar);
}
[class]{}-[func]{solve_hanota}
/* ハノイの塔を解く */
fn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {
let n = A.len() as i32;
// A の上から n 枚の円盤を B を介して C へ移す
dfs(n, A, B, C);
}
```
=== "C"
```c title="hanota.c"
[class]{}-[func]{move}
/* 円盤を 1 枚移動 */
void move(int *src, int *srcSize, int *tar, int *tarSize) {
// src の上から円盤を1枚取り出す
int pan = src[*srcSize - 1];
src[*srcSize - 1] = 0;
(*srcSize)--;
// 円盤を tar の上に置く
tar[*tarSize] = pan;
(*tarSize)++;
}
[class]{}-[func]{dfs}
/* ハノイの塔の問題 f(i) を解く */
void dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if (i == 1) {
move(src, srcSize, tar, tarSize);
return;
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src, srcSize, tar, tarSize);
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);
}
[class]{}-[func]{solveHanota}
/* ハノイの塔を解く */
void solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {
// A の上から n 枚の円盤を B を介して C へ移す
dfs(*ASize, A, ASize, B, BSize, C, CSize);
}
```
=== "Kotlin"
```kotlin title="hanota.kt"
[class]{}-[func]{move}
/* 円盤を 1 枚移動 */
fun move(src: MutableList<Int>, tar: MutableList<Int>) {
// src の上から円盤を1枚取り出す
val pan = src.removeAt(src.size - 1)
// 円盤を tar の上に置く
tar.add(pan)
}
[class]{}-[func]{dfs}
/* ハノイの塔の問題 f(i) を解く */
fun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {
// src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if (i == 1) {
move(src, tar)
return
}
// 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf)
// 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src, tar)
// 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i - 1, buf, src, tar)
}
[class]{}-[func]{solveHanota}
/* ハノイの塔を解く */
fun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {
val n = A.size
// A の上から n 枚の円盤を B を介して C へ移す
dfs(n, A, B, C)
}
```
=== "Ruby"
```ruby title="hanota.rb"
[class]{}-[func]{move}
### 円盤を1枚移動 ###
def move(src, tar)
# src の上から円盤を1枚取り出す
pan = src.pop
# 円盤を tar の上に置く
tar << pan
end
[class]{}-[func]{dfs}
### ハノイの塔 f(i) を解く ###
def dfs(i, src, buf, tar)
# src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す
if i == 1
move(src, tar)
return
end
[class]{}-[func]{solve_hanota}
# 部分問題 f(i-1)src の上部 i-1 枚の円盤を tar を補助にして buf へ移す
dfs(i - 1, src, tar, buf)
# 部分問題 f(1):src に残る 1 枚の円盤を tar に移す
move(src, tar)
# 部分問題 f(i-1)buf の上部 i-1 枚の円盤を src を補助にして tar へ移す
dfs(i - 1, buf, src, tar)
end
### ハノイの塔を解く ###
def solve_hanota(_A, _B, _C)
n = _A.length
# A の上から n 枚の円盤を B を介して C へ移す
dfs(n, _A, _B, _C)
end
```
以下の図に示すように、ハノイの塔問題は高さ $n$ の再帰木として視覚化できます。各ノードは部分問題を表し、`dfs()` の呼び出しに対応します。**したがって、時間計算量は $O(2^n)$、空間計算量は $O(n)$ です。**
??? pythontutor "コードの可視化"
![ハノイの塔の再帰木](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" }
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%86%86%E7%9B%A4%E3%82%92%201%20%E6%9E%9A%E7%A7%BB%E5%8B%95%22%22%22%0A%20%20%20%20%23%20src%20%E3%81%AE%E4%B8%8A%E3%81%8B%E3%82%89%E5%86%86%E7%9B%A4%E3%82%921%E6%9E%9A%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%99%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%86%86%E7%9B%A4%E3%82%92%20tar%20%E3%81%AE%E4%B8%8A%E3%81%AB%E7%BD%AE%E3%81%8F%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int%2C%20src%3A%20list%5Bint%5D%2C%20buf%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%8F%E3%83%8E%E3%82%A4%E3%81%AE%E5%A1%94%E3%81%AE%E5%95%8F%E9%A1%8C%20f%28i%29%20%E3%82%92%E8%A7%A3%E3%81%8F%22%22%22%0A%20%20%20%20%23%20src%20%E3%81%AB%E5%86%86%E7%9B%A4%E3%81%8C%201%20%E6%9E%9A%E3%81%A0%E3%81%91%E6%AE%8B%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%20tar%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28i-1%29%EF%BC%9Asrc%20%E3%81%AE%E4%B8%8A%E9%83%A8%20i-1%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20tar%20%E3%82%92%E8%A3%9C%E5%8A%A9%E3%81%AB%E3%81%97%E3%81%A6%20buf%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20dfs%28i%20-%201%2C%20src%2C%20tar%2C%20buf%29%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%281%29%EF%BC%9Asrc%20%E3%81%AB%E6%AE%8B%E3%82%8B%201%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20tar%20%E3%81%AB%E7%A7%BB%E3%81%99%0A%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28i-1%29%EF%BC%9Abuf%20%E3%81%AE%E4%B8%8A%E9%83%A8%20i-1%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20src%20%E3%82%92%E8%A3%9C%E5%8A%A9%E3%81%AB%E3%81%97%E3%81%A6%20tar%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20dfs%28i%20-%201%2C%20buf%2C%20src%2C%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D%2C%20B%3A%20list%5Bint%5D%2C%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%8F%E3%83%8E%E3%82%A4%E3%81%AE%E5%A1%94%E3%82%92%E8%A7%A3%E3%81%8F%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20A%20%E3%81%AE%E4%B8%8A%E3%81%8B%E3%82%89%20n%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20B%20%E3%82%92%E4%BB%8B%E3%81%97%E3%81%A6%20C%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20dfs%28n%2C%20A%2C%20B%2C%20C%29%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E6%9C%AB%E5%B0%BE%E3%81%8C%E6%9F%B1%E3%81%AE%E9%A0%82%E4%B8%8A%0A%20%20%20%20A%20%3D%20%5B5%2C%204%2C%203%2C%202%2C%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E6%9C%9F%E7%8A%B6%E6%85%8B%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A%2C%20B%2C%20C%29%0A%0A%20%20%20%20print%28%22%E5%86%86%E7%9B%A4%E3%81%AE%E7%A7%BB%E5%8B%95%E5%AE%8C%E4%BA%86%E5%BE%8C%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%86%86%E7%9B%A4%E3%82%92%201%20%E6%9E%9A%E7%A7%BB%E5%8B%95%22%22%22%0A%20%20%20%20%23%20src%20%E3%81%AE%E4%B8%8A%E3%81%8B%E3%82%89%E5%86%86%E7%9B%A4%E3%82%921%E6%9E%9A%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%99%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%86%86%E7%9B%A4%E3%82%92%20tar%20%E3%81%AE%E4%B8%8A%E3%81%AB%E7%BD%AE%E3%81%8F%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int%2C%20src%3A%20list%5Bint%5D%2C%20buf%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%8F%E3%83%8E%E3%82%A4%E3%81%AE%E5%A1%94%E3%81%AE%E5%95%8F%E9%A1%8C%20f%28i%29%20%E3%82%92%E8%A7%A3%E3%81%8F%22%22%22%0A%20%20%20%20%23%20src%20%E3%81%AB%E5%86%86%E7%9B%A4%E3%81%8C%201%20%E6%9E%9A%E3%81%A0%E3%81%91%E6%AE%8B%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%20tar%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28i-1%29%EF%BC%9Asrc%20%E3%81%AE%E4%B8%8A%E9%83%A8%20i-1%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20tar%20%E3%82%92%E8%A3%9C%E5%8A%A9%E3%81%AB%E3%81%97%E3%81%A6%20buf%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20dfs%28i%20-%201%2C%20src%2C%20tar%2C%20buf%29%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%281%29%EF%BC%9Asrc%20%E3%81%AB%E6%AE%8B%E3%82%8B%201%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20tar%20%E3%81%AB%E7%A7%BB%E3%81%99%0A%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%23%20%E9%83%A8%E5%88%86%E5%95%8F%E9%A1%8C%20f%28i-1%29%EF%BC%9Abuf%20%E3%81%AE%E4%B8%8A%E9%83%A8%20i-1%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20src%20%E3%82%92%E8%A3%9C%E5%8A%A9%E3%81%AB%E3%81%97%E3%81%A6%20tar%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20dfs%28i%20-%201%2C%20buf%2C%20src%2C%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D%2C%20B%3A%20list%5Bint%5D%2C%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%8F%E3%83%8E%E3%82%A4%E3%81%AE%E5%A1%94%E3%82%92%E8%A7%A3%E3%81%8F%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20A%20%E3%81%AE%E4%B8%8A%E3%81%8B%E3%82%89%20n%20%E6%9E%9A%E3%81%AE%E5%86%86%E7%9B%A4%E3%82%92%20B%20%E3%82%92%E4%BB%8B%E3%81%97%E3%81%A6%20C%20%E3%81%B8%E7%A7%BB%E3%81%99%0A%20%20%20%20dfs%28n%2C%20A%2C%20B%2C%20C%29%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E3%83%AA%E3%82%B9%E3%83%88%E6%9C%AB%E5%B0%BE%E3%81%8C%E6%9F%B1%E3%81%AE%E9%A0%82%E4%B8%8A%0A%20%20%20%20A%20%3D%20%5B5%2C%204%2C%203%2C%202%2C%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E6%9C%9F%E7%8A%B6%E6%85%8B%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A%2C%20B%2C%20C%29%0A%0A%20%20%20%20print%28%22%E5%86%86%E7%9B%A4%E3%81%AE%E7%A7%BB%E5%8B%95%E5%AE%8C%E4%BA%86%E5%BE%8C%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
<p align="center"> 図 12-15 &nbsp; ハノイの塔の再帰木 </p>
以下の図に示すように、ハノイの塔の問題は高さ $n$ の再帰木を形成し、各ノードは 1 つの部分問題、すなわち 1 つ起動された `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$ 枚の金の円盤を持っていました。彼らは、最後の円盤が正しく置かれたとき、世界が終わると信じていました。
ハノイの塔の問題は古い伝説に由来します。古代インドのある寺院で、僧侶たちは 3 本の高いダイヤモンドの柱と、$64$ 枚の大きさの異なる金の円盤を持っていました。僧侶たちは絶えず円盤を動かし、最後の 1 枚が正しく置かれた瞬間に世界が終わると信じていました。
しかし、僧侶たちが1秒に1枚の円盤を移動したとしても、約 $2^{64} \approx 1.84×10^{19}$ —約5850億年—かかり、宇宙の年齢の現在の推定をはるかに超えています。したがって、この伝説が真実であれば、世界の終わりについて心配する必要はおそらくないでしょう
しかし、たとえ僧侶たちが 1 秒に 1 回移動するとしても、合計でおよそ $2^{64} \approx 1.84×10^{19}$ 秒、約 $5850$ 億年が必要で、現在推定されている宇宙の年齢をはるかに上回ります。したがって、この伝説が本当だったとしても、世界の終わり心配する必要はなさそうです
+6 -6
View File
@@ -9,14 +9,14 @@ icon: material/set-split
!!! abstract
困難な問題は層を重ねて分解され、各分解によってより単純になります
分割統治は深い真理を明らかにします:単純さから始めれば、複雑さは解決される
難題は段階的に分解され、そのたびにより単純になっていく
分割統治は一つの重要な事実を示している。単純なことから始めれば、すべてはもはや複雑ではない
## 章の内容
- [12.1 &nbsp; 分割統治アルゴリズム](divide_and_conquer.md)
- [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.3 &nbsp; 二分木の構築問題](build_binary_tree_problem.md)
- [12.4 &nbsp; ハノイの塔問題](hanota_problem.md)
- [12.5 &nbsp; まとめ](summary.md)
+11 -9
View File
@@ -4,12 +4,14 @@ comments: true
# 12.5 &nbsp; まとめ
- 分割統治は一般的なアルゴリズム設計戦略で、分割(分割)と統治(マージ)の2つの段階から構成され、一般的に再帰を使用して実装されます。
- 問題が分割統治アプローチに適しているかどうかを判断するために、問題が分解可能かどうか、部分問題が独立しているかどうか、部分問題をマージできるかどうかを確認します。
- マージソートは分割統治戦略の典型的な例です。配列を再帰的に2つの等しい長さの副配列に分割し、1つの要素のみが残るまで続け、次にこれらの副配列を層ごとにマージしてソートを完了します。
- 分割統治戦略の導入は、しばしばアルゴリズムの効率を向上させます。一方では操作数を減らし、他方では分割後のシステムの並列最適化を促進します。
- 分割統治は多数のアルゴリズム問題に適用でき、データ構造とアルゴリズム設計で広く使用され、多くのシナリオに現れます。
- 総当たり検索と比較して、適応検索はより効率的です。時間計算量が $O(\log n)$ の検索アルゴリズムは、通常分割統治戦略に基づいています。
- 二分探索は分割統治戦略のもう一つの古典的な応用です。部分問題の解のマージを含まず、再帰的な分割統治アプローチで実装できます。
- 二分木構築問題では、木の構築(元の問題)を左の部分木と右の部分木の構築(部分問題)に分割できます。これは前順走査と中順走査のインデックス範囲を分割することで実現できます。
- ハノイの塔問題では、サイズ $n$ の問題をサイズ $n-1$ の2つの部分問題とサイズ $1$ の1つの部分問題に分解できます。これら3つの部分問題を順次解決することで、元の問題が解決されます。
### 1. &nbsp; 要点の振り返り
- 分割統治法は一般的なアルゴリズム設計戦略であり、分(分割)と治(統合)の 2 つの段階からなり、通常は再帰に基づいて実装されます。
- それが分割統治法の問題かどうかを判断する基準には、問題を分解できるか、部分問題が独立しているか、部分問題を統合できるかが含まれます。
- マージソートは分割統治法の典型的な応用であり、配列を再帰的に同じ長さの 2 つの部分配列に分割し、要素が 1 つだけになるまで続け、その後で各層を順に統合してソートを完了します。
- 分割統治法を導入すると、多くの場合アルゴリズムの効率を高められます。一方では操作回数が減り、他方では分割後にシステムの並列最適化を行いやすくなります。
- 分割統治法は多くのアルゴリズム問題を解決できるだけでなく、データ構造やアルゴリズム設計にも広く応用されており、至る所でその姿を見ることができます。
- 総当たり探索と比べて、適応的な探索のほうが効率的です。時間計算量が $O(\log n)$ の探索アルゴリズムは、通常は分割統治法に基づいて実装されます。
- 二分探索は分割統治法のもう 1 つの典型的な応用であり、部分問題の解を統合する手順を含みません。再帰的な分割統治によって二分探索を実現できます。
- 二分木を構築する問題では、木の構築(元の問題)を左部分木と右部分木の構築(部分問題)に分けられます。これは、先行順序走査と中間順序走査のインデックス区間を分割することで実現できます。
- ハノイの塔の問題では、規模 $n$ の問題を、規模 $n-1$ の 2 つの部分問題と規模 $1$ の 1 つの部分問題に分けられます。これら 3 つの部分問題を順に解くと、元の問題も解決されます。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -3,22 +3,22 @@ comments: true
icon: material/table-pivot
---
# 第 14 章 &nbsp; 動的プログラミング
# 第 14 章 &nbsp; 動的計画法
![動的プログラミング](../assets/covers/chapter_dynamic_programming.jpg){ class="cover-image" }
![動的計画法](../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.1 &nbsp; 動的計画法入門](intro_to_dynamic_programming.md)
- [14.2 &nbsp; 動的計画法の問題特性](dp_problem_features.md)
- [14.3 &nbsp; 動的計画法の問題解決の考え方](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)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17 -15
View File
@@ -4,24 +4,26 @@ comments: true
# 14.7 &nbsp; まとめ
- 動的プログラミングは問題を分解し、部分問題の解を保存することで冗長な計算を避け、計算効率を向上させます。
- 時間を考慮しなければ、すべての動的プログラミング問題はバックトラッキング(力任せ探索)を使用して解決できますが、再帰木には多くの重複する部分問題があり、効率が非常に低くなります。記憶化リストを導入することで、計算されたすべての部分問題の解を保存し、重複する部分問題が一度だけ計算されることを保証できます。
- 記憶化探索はトップダウンの再帰解法であり、動的プログラミングはボトムアップの反復アプローチに対応し、「表を埋める」ことに似ています。現在の状態は特定の局所状態のみに依存するため、dpテーブルの1次元を削除して空間計算量を削減できます。
- 部分問題の分解は汎用的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングで特徴が異なります。
- 動的プログラミング問題には3つの主要な特徴があります:重複する部分問題、最適部分構造、無記憶性
- 元の問題の最適解がその部分問題の最適解から構築できる場合、最適部分構造を持ちます。
- 無記憶性とは、状態の将来の発展が現在の状態のみに依存し、過去に経験したすべての状態に依存しないことを意味します。多くの組み合わせ最適化問題にはこの特性がなく、動的プログラミングを使用して迅速に解決することはできません
### 1. &nbsp; 要点の振り返り
- 動的計画法は問題を分解し、部分問題の解を保存することで重複計算を避け、計算効率を高めます。
- 時間を考慮しなければ、すべての動的計画法の問題はバックトラッキング(総当たり探索)で解けますが、再帰木には大量の重複部分問題が存在するため、効率はきわめて低くなります。メモ化配列を導入すると、計算済みのすべての部分問題の解を保存でき、重複部分問題が 1 回だけ計算されることを保証できます。
- メモ化探索はトップダウンの再帰的解法であり、それに対応する動的計画法はボトムアップの漸化式による解法で、ちょうど「表を埋める」ようなものです。現在の状態は一部の局所状態にのみ依存するため、$dp$ 表の 1 次元を削減して空間計算量を下げることができます
- 部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治、動的計画法、バックトラッキングではそれぞれ異なる性質を持ちます。
- 動的計画法の問題には 3 つの大きな特徴があります。重複部分問題、最適部分構造、無後効性です
- 元の問題の最適解が部分問題の最適解から構築できるなら、その問題は最適部分構造を持ちます。
- 無後効性とは、ある状態の将来の発展がその状態のみに関係し、過去に経たすべての状態とは無関係であることを指します。多くの組合せ最適化問題は無後効性を持たず、動的計画法で高速に解くことはできません。
**ナップサック問題**
- ナップサック問題は最も典型的な動的プログラミング問題の1つで、0-1ナップサック、無制限ナップサック、複数ナップサックなどの変種があります。
- 0-1ナップサックの状態定義は、最初の $i$ 個のアイテムを含む容量 $c$ のナップサックでの最大値です。アイテムをナップサックに入れないまたは入れるという決定に基づいて、最適部分構造を特定し、状態遷移方程式を構築できます。空間最適化では、各状態が直接上と左上の状態に依存するため、左上の状態上書きを避けるためにリストを逆順走査する必要があります。
- 無制限ナップサック問題では、各種類のアイテムを選択できる数に制限がないため、アイテムを含める状態遷移は0-1ナップサック異なります。状態が直接上と左の状態に依存するため、空間最適化では前方走査を含める必要があります。
- コイン交換問題は無制限ナップサック問題の変種で「最大」値を求めることから「最小」コイン数を求めることに変わり、状態遷移方程式 $\max()$ $\min()$ に変更する必要があります。ナップサック容量を「超えない」ことを追求することから、正確に目標金額を求めることに変わり、「目標金額を構成できない」無効解を表すため$amt + 1$ を使用します。
- コイン交換問題IIは「最小コイン数」を求めることから「コインの組み合わせ数」を求めることに変わり、状態遷移方程式 $\min()$ から和演算子に変更します。
- ナップサック問題は最も典型的な動的計画法の問題の 1 つであり、0-1 ナップサック、完全ナップサック、多重ナップサックなどの派生があります。
- 0-1 ナップサックの状態は、容量 $c$ のナップサックに対して、前 $i$ 個の品物で得られる最大価値として定義されます。ナップサックに入れない場合と入れる場合の 2 つの判断から最適部分構造を得て、状態遷移方程式を構築できます。空間最適化では、各状態が上と左上の状態に依存するため、左上の状態上書きされるのを避けるために配列を逆順走査する必要があります。
- 完全ナップサック問題では各品物の選択数に制限がないため、品物を入れる場合の状態遷移は 0-1 ナップサック問題とは異なります。状態は真上と左の状態に依存するので、空間最適化では順方向に走査するべきです。
- コイン両替問題は完全ナップサック問題の変種です。「最大」値を求める問題から「最小」の硬貨枚数を求める問題へ変わるため、状態遷移方程式 $\max()$ $\min()$ に置き換える必要があります。また、ナップサック容量を「超えない」ことを目指すのではなく、目標金額を「ちょうど」作ることを目指すため$amt + 1$ を「目標金額を作れない」無効解の表現として用います。
- コイン両替問題 II では、「最少硬貨枚数」を求める問題から「硬貨の組合せ数」を求める問題へ変わるため、状態遷移方程式 $\min()$ から和演算子へ対応して変わります。
**編集距離問題**
- 編集距離(レーベンシュタイン距離)は2つの文字列間の類似度を測定し、一つの文字列を別の文字列に変更するために必要な最小編集ステップ数として定義され編集操作には追加、削除、置換が含まれます。
- 編集距離問題の状態定義は、$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字変更するために必要な最小編集ステップ数です。$s[i] \ne t[j]$ の場合、追加、削除、置換の3つの決定があり、それぞれに対応する残部分問題があります。これから最適部分構造を特定し、状態遷移方程式を構築できます。$s[i] = t[j]$ の場合、現在の文字編集必要ありません。
- 編集距離では、状態が直接上、左、左上の状態に依存します。したがって、空間最適化後、前方走査も逆走査も正しく状態遷移を実行できません。これに対処するため、変数を使用して左上の状態を一時的に保存し、無制限ナップサック問題の状況と同等にし、空間最適化後に前方走査を可能にします。
- 編集距離(Levenshtein 距離)は 2 つの文字列間の類似度を測るために用いられ、ある文字列を別の文字列へ変換するための最小編集回数として定義されます。編集操作には追加、削除、置換が含まれます。
- 編集距離問題の状態は、$s$ の $i$ 文字を $t$ の $j$ 文字変更するに必要な最小編集回数として定義されます。$s[i] \ne t[j]$ のときは、追加、削除、置換の 3 つの判断があり、それぞれに対応する残りの部分問題があります。これにより最適部分構造を見いだし、状態遷移方程式を構築できます。一方、$s[i] = t[j]$ のときは現在の文字編集する必要ありません。
- 編集距離では、状態は真上、左、左上の状態に依存します。そのため、空間最適化後は順方向でも逆方向でも正しく状態遷移できません。そこで、変数を 1 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。
File diff suppressed because it is too large Load Diff
+35 -35
View File
@@ -4,7 +4,7 @@ comments: true
# 9.1 &nbsp; グラフ
<u>グラフ</u>は非線形データ構造の一種で、<u>頂点</u>と<u>辺</u>構成されす。グラフ$G$は、頂点集合$V$と辺集合$E$の組み合わせとして抽象的に表現できます。以下の例は、5の頂点と7つの辺を含むグラフを示しています。
<u>グラフgraph</u>は、<u>頂点(vertex</u>と<u>辺edge</u>から構成される非線形データ構造です。グラフ $G$ は、頂点集合 $V$ と辺集合 $E$ からなる集合として抽象的に表ます。以下の例は、5の頂点と 7 本の辺を含むグラフを示しています。
$$
\begin{aligned}
@@ -14,7 +14,7 @@ G & = \{ V, E \} \newline
\end{aligned}
$$
頂点をノード、辺をノードを接続する参照(ポインタ)と見なすと、グラフは連結リストから拡張されたデータ構造として見ることができます。図に示すように、**線形関係(連結リスト)や分割統治関係(木)と比較して、ネットワーク関係(グラフ)は自由度が高いため、より複雑です**
頂点をノード、辺をノードをつなぐ参照(ポインタ)とみなせば、グラフは連結リストを拡張したデータ構造の一種と捉えられます。次の図に示すように、**線形関係(連結リスト)や分治関係(木)と比て、ネットワーク関係(グラフ)は自由度が高く**、そのぶん複雑です。
![連結リスト、木、グラフの関係](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" }
@@ -22,82 +22,82 @@ $$
## 9.1.1 &nbsp; グラフの一般的な種類と用語
グラフは、辺に方向があるかどうかによって<u>無向グラフ</u>と<u>有向グラフ</u>に分けることができます(下図参照)
辺が方向性を持つかどうかに応じて、<u>無向グラフundirected graph</u>と<u>有向グラフ(directed graph</u>に分けられます。次の図のとおりです
- 無向グラフでは、辺は2つの頂点間の「双方向」接続を表します。例えば、Facebookの「友達」関係です。
- 有向グラフでは、辺方向性があります。つまり、辺$A \rightarrow B$$A \leftarrow B$は互いに独立しています。例えば、InstagramやTikTokの「フォロー」と「フォロワー」の関係です。
- 無向グラフでは、辺は 2 つの頂点間の「双方向」接続関係を表します。例えば WeChat や QQ における「友だち関係です。
- 有向グラフでは、辺方向性を持ち、すなわち $A \rightarrow B$$A \leftarrow B$ の 2 方向の辺は互いに独立す。例えば Weibo や Douyin における「フォロー」と「フォロワー」の関係です。
![有向グラフと無向グラフ](graph.assets/directed_graph.png){ class="animation-figure" }
<p align="center"> 図 9-2 &nbsp; 有向グラフと無向グラフ </p>
すべての頂点が接続されているかどうかによって、グラフは<u>連結グラフ</u>と<u>非連結グラフ</u>に分けることができます(下図参照)
すべての頂点が連結しているかどうかに応じて、<u>連結グラフ(connected graph</u>と<u>非連結グラフ(disconnected graph</u>に分けられます。次の図のとおりです
- 連結グラフでは、任意の頂点から開始して他の任意の頂点に到達することが可能です。
- 非連結グラフでは、任意の開始頂点から到達できない頂点が少なくとも1つ存在します
- 連結グラフでは、ある頂点から出発すると、ほかの任意の頂点に到達できます。
- 非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません
![連結グラフと非連結グラフ](graph.assets/connected_graph.png){ class="animation-figure" }
<p align="center"> 図 9-3 &nbsp; 連結グラフと非連結グラフ </p>
辺に重み変数を追加することもでき、その結果として<u>重み付きグラフ</u>が生まれます(下図参照)。例えば、Instagramでは、システムがあなたと他のユーザーとの間の相互作用レベル(いいね、閲覧、コメントなど)によってフォロワーとフォロー中のリストをソートします。このような相互作用ネットワークは重み付きグラフで表現できます。
辺に重み」の変数を追加すると、次の図に示すような<u>重み付きグラフ(weighted graph</u>が得られます。例えば『Honor of Kings』のようなモバイルゲームでは、システムが共にプレイした時間に基づいてプレイヤー間の「親密度」を計算します。この親密度ネットワークは重み付きグラフで表ます。
![重み付きグラフと重みなしグラフ](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>はその頂点から出る辺の数をします。
- <u>隣接adjacency</u>2 つの頂点の間に辺が存在するとき、この 2 つの頂点は「隣接している」といいます。上図では、頂点 1 に隣接する頂点は 2、3、5 です。
- <u>経路(path</u>:頂点 A から頂点 B までに通過する辺で構成された列を、A から B への「経路」と呼びます。上図では、辺の1-5-2-4 は頂点 1 から頂点 4 への 1 本の経路です。
- <u>次数degree</u>ある頂点が持つ辺の数です。有向グラフでは、<u>入次数in-degree</u>はその頂点に向かう辺の本数を表し、<u>出次数(out-degree</u>はその頂点から出る辺の数をします。
## 9.1.2 &nbsp; グラフの表現
グラフの一般的な表現には「隣接行列」と「隣接リスト」があります。以下の例では無向グラフを使用します。
グラフの一般的な表現方法には「隣接行列」と「隣接リスト」があります。以下では無向グラフを例に説明します。
### 1. &nbsp; 隣接行列
グラフの頂点数を$n$とすると、<u>隣接行列</u>は$n \times n$の行列を使用してグラフを表します。各行(列)は頂点を表し、行列要素は辺を表し、2つの頂点間に辺があるかどうかを$1$または$0$で示します。
グラフの頂点数を $n$ とすると、<u>隣接行列adjacency matrix</u>は $n \times n$ の行列を用いてグラフを表します。各行(列)は 1 つの頂点を表し、行列要素は辺を表します。$1$ または $0$ を用いて、2 つの頂点間に辺があるかどうかを示します。
下図に示すように、隣接行列を$M$、頂点リストを$V$とすると、行列要素$M[i, j] = 1$は頂点$V[i]$頂点$V[j]$の間に辺があることをし、逆に$M[i, j] = 0$は2つの頂点間に辺がないことをします。
次の図のように、隣接行列を $M$、頂点リストを $V$ とすると、行列要素 $M[i, j] = 1$ は頂点 $V[i]$ から頂点 $V[j]$ への辺が存在することをし、逆に $M[i, j] = 0$ は 2 つの頂点間に辺がないことをします。
![隣接行列によるグラフの表現](graph.assets/adjacency_matrix.png){ class="animation-figure" }
![グラフの隣接行列による表現](graph.assets/adjacency_matrix.png){ class="animation-figure" }
<p align="center"> 図 9-5 &nbsp; 隣接行列によるグラフの表現 </p>
<p align="center"> 図 9-5 &nbsp; グラフの隣接行列による表現 </p>
隣接行列には以下の特があります。
隣接行列にはの特があります。
- 頂点は自分自身に接続することはできないため、隣接行列の主対角線上の要素は意味がありません。
- 無向グラフの場合、両方向の辺は等価であるため、隣接行列は主対角線に関して対称です。
- 隣接行列の要素を$1$$0$から重みに置き換えることで、重み付きグラフを表現できます。
- 単純グラフでは、頂点は自分自身とは接続できないため、このとき隣接行列の主対角線上の要素は意味がありません。
- 無向グラフでは、2 方向の辺は等価であるため、このとき隣接行列は主対角線に関して対称です。
- 隣接行列の要素を $1$$0$ から重みに置き換える、重み付きグラフを表ます。
隣接行列でグラフを表現する場合、行列要素に直接アクセスして辺を取得できるため、追加削除検索、変更の操作効率的で、すべて時間計算量$O(1)$です。ただし、行列の空間計算量は$O(n^2)$で、より多くのメモリを消費します。
隣接行列でグラフを表場合、行列要素に直接アクセスして辺を取得できるため、追加削除検索・更新の操作効率は高く、時間計算量はいずれも $O(1)$ です。しかし、行列の空間計算量は $O(n^2)$ であり、メモリ使用量は多くなります。
### 2. &nbsp; 隣接リスト
<u>隣接リスト</u>は$n$の連結リストを使用してグラフを表現し、各連結リストノードは頂点を表します。$i$番目の連結リストは頂点$i$に対応し、すべての隣接頂点(その頂点接続された頂点)を含みます。図は隣接リストを使用して格納されたグラフの例を示しています。
<u>隣接リストadjacency list</u>は$n$の連結リストを使てグラフを表します。連結リストノードは頂点を表します。$i$の連結リストは頂点 $i$ に対応し、その頂点に隣接するすべての頂点(その頂点接続された頂点)を格納します。次の図は隣接リストで保存したグラフの例です。
![隣接リストによるグラフの表現](graph.assets/adjacency_list.png){ class="animation-figure" }
![グラフの隣接リストによる表現](graph.assets/adjacency_list.png){ class="animation-figure" }
<p align="center"> 図 9-6 &nbsp; 隣接リストによるグラフの表現 </p>
<p align="center"> 図 9-6 &nbsp; グラフの隣接リストによる表現 </p>
隣接リストは実際の辺のみを格納し、辺の総数は$n^2$よりはるかに少ないことが多く、より空間効率的です。ただし、隣接リストで辺を見つけるに連結リストを走査する必要があるため、その時間効率は隣接行列ほど良くありません。
隣接リストは実際に存在する辺だけを格納し、辺の総数は通常 $n^2$ よりはるかに小さいため、より省スペースです。しかし、隣接リストで辺を見つけるために連結リストを走査する必要があるため、時間効率は隣接行列に及びません。
上図を観察すると、**隣接リストの構造はハッシュテーブルの「チェイン法」と非常に似ているため、同様の方法を使用して効率を最適化できます**。例えば、連結リストが長い場合、それをAVL木や赤黒木に変換して時間効率を$O(n)$から$O(\log n)$に最適化できます。連結リストをハッシュテーブルに変換することで、時間計算量を$O(1)$に削減することもできます。
上図をると、**隣接リストの構造はハッシュテーブルにおける「連鎖アドレス法」と非常によく似ているため、同様の方法効率を最適化できます**。例えば、連結リストが長い場合AVL 木や赤黒木に変換して時間効率を $O(n)$ から $O(\log n)$ に改善できます。さらに、連結リストをハッシュテーブルに変換すれば、時間計算量を $O(1)$ まで下げられます。
## 9.1.3 &nbsp; グラフの一般的な応用
下表に示すように、多くの現実世界のシステムはグラフでモデル化でき、対応する問題グラフ計算問題に削減できます。
次の表のように、多くの現実のシステムはグラフでモデル化でき、対応する問題グラフ計算問題に帰着できます。
<p align="center"> 表 9-1 &nbsp; 現実生活の一般的なグラフ </p>
<p align="center"> 表 9-1 &nbsp; 現実世界でよく見られるグラフ </p>
<div class="center-table" markdown>
| | 頂点 | 辺 | グラフ計算問題 |
| -------------- | -------------- | -------------------------------- | --------------------------- |
| ソーシャルネットワーク | ユーザー | フォロー / フォロワー関係 | 潜在的フォロー推薦 |
| 地下鉄路線 | 駅 | 駅間の接続性 | 最短ルート推薦 |
| 太陽系 | 天体 | 天体間の重力 | 惑星軌道計算 |
| | 頂点 | 辺 | グラフ計算問題 |
| -------- | ---- | -------------------- | ------------ |
| ソーシャルネットワーク | ユーザー | 友だち関係 | 潜在的な友だちの推薦 |
| 地下鉄路線 | 駅 | 駅間の接続性 | 最短経路の推薦 |
| 太陽系 | 天体 | 天体間の万有引力作用 | 惑星軌道計算 |
</div>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -9,9 +9,9 @@ icon: material/graphql
!!! abstract
人生の旅路において、私たちの一人一人はノードであり、無数の見えない辺結ばれています。
一つ一つの出会いと別れ、この大な人生のグラフに独特の印を残していきます。
人生の旅路において、私たちはそれぞれ一つひとつのノードのようなものであり、無数の見えない辺によって結ばれています。
出会いと別れのたびに、この大なネットワークグラフの中に固有の足跡が刻まれます。
## 章の内容
+19 -19
View File
@@ -4,32 +4,32 @@ comments: true
# 9.4 &nbsp; まとめ
### 1. &nbsp; 重要な復習
### 1. &nbsp; 重要なポイントの振り返り
- グラフは頂点と辺構成されます。頂点の集合と辺の集合として記述できます。
- 線形関係(連結リストなど)や階層関係(木など)と比較して、ネットワーク関係(グラフ)はより大きな柔軟性を提供し、より複雑になります。
- 有向グラフでは、辺に方向があります。連結グラフでは、任意の頂点から他の任意の頂点に到達できます。重み付きグラフでは、各辺に関連する重み変数があります。
- 隣接行列は行列(2次元配列)を使用してグラフを表現する方法です。行と列は頂点を表します。行列要素の値は、2つの頂点間に辺があるかどうかを示し、辺がある場合は$1$、ない場合は$0$を使用します。隣接行列は辺の追加削除、チェックなどの操作に非常に効率的ですが、より多くのスペースが必要です。
- 隣接リストは連結リストの集合を使用してグラフを表現するもう一つの一般的な方法です。グラフ内の各頂点には、その隣接するすべての頂点を含むリストがあります。$i$番目のリストは頂点$i$を表します。隣接リストは隣接行列と比較してより少ないスペースを使用します。ただし、辺を見つけるためにリストを走査する必要があるため、時間効率は低くなります。
- 隣接リストの連結リストが十分に長くなったとき、ルックアップ効率を向上させるために赤黒木やハッシュテーブルに変換できます。
- アルゴリズム設計の観点から、隣接行列は「空間時間のトレードオフ」の概念を反映し、隣接リストは「時間空間のトレードオフ」を反映します。
- グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実世界のシステムをモデル化するために使用できます。
- 木はグラフの特別なケースであり、木の走査もグラフ走査の特別なケースです。
- グラフの幅優先走査は、近くから遠くへと層ごとに拡張する探索方法で、通常キューを使します。
- グラフの深さ優先走査は、それ以上のパスがない場合にバックトラックする前に、まず終端に到達することを優先する探索方法です。しばしば再帰を使用して実装されます。
- グラフは頂点と辺から構成され、一組の頂点と一組の辺からなる集合として表せます。
- 線形関係(連結リスト)や分治関係(木)と比て、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。
- 有向グラフの辺は方向性を持ち、連結グラフでは任意の頂点に到達でき重み付きグラフの各辺は重み変数を含みます。
- 隣接行列は行列を用いてグラフを表し、各行(列)が 1 つの頂点を表し行列要素が辺を表します。$1$ または $0$ を用いて、2 つの頂点間に辺があるかないかを示します。隣接行列は追加削除・検索・更新の操作効率が高い一方で、より多くの空間を消費します。
- 隣接リストは複数の連結リストを使ってグラフを表し、第 $i$ 個の連結リストが頂点 $i$ に対応し、その頂点に隣接するすべての頂点を格納します。隣接リストは隣接行列よりも省スペースですが、辺を探すために連結リストを走査する必要があるため、時間効率は低くなります。
- 隣接リストの連結リストが長くなりすぎた場合は、赤黒木やハッシュテーブルに変換することで、検索効率を高められます。
- アルゴリズムの考え方という観点では、隣接行列は「空間時間と引き換えにする」ことを体現し、隣接リストは「時間空間と引き換えにする」ことを体現します。
- グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実のシステムをモデル化するために使ます。
- 木はグラフの特殊な一例であり、木の走査もグラフ走査の特殊な一例です。
- グラフの幅優先探索は、近いところから遠いところへ、層ごとに広がっていく探索方法であり、通常キューを使って実装します。
- グラフの深さ優先探索は、まず行けるところまで進み、進めなくなったらバックトラックする探索方法であり、通常は再帰に基づいて実装ます。
### 2. &nbsp; Q & A
**Q**: パスは頂点のシーケンスとして定義されますか、それとも辺のシーケンスとして定義されますか?
**Q**:経路の定義は頂点列ですか、それとも辺列ですか?
グラフ理論では、グラフ内のパスは頂点のシーケンスを結ぶ有限または無限の辺のシーケンスです。
Wikipedia では言語版ごとに定義が一致していません。英語版では「経路は辺の列」であり、中国語版では「経路は頂点の列」です。以下は英語版の原文です:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.
この文書では、パスは頂点のシーケンスではなく、辺のシーケンスと考えられます。これは、2つの頂点を結ぶ複数の辺がる可能性があり、その場合各辺がパスに対応するためです。
書では、経路を頂点列ではなく辺列とみなします。これは、2 つの頂点の間に複数の辺が存在する可能性があり、その場合各辺がそれぞれ 1 本の経路に対応するためです。
**Q**: 非連結グラフでは、走査できない点がありますか?
**Q**非連結グラフには到達できない点がありますか?
非連結グラフでは、特定の点から到達できない頂点が少なくとも1つあります。非連結グラフを走査するには、グラフのすべての連結成分を走査するために複数の始点を設定する必要があります。
非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。非連結グラフ全体を走査するには、グラフのすべての連結成分をたどれるように複数の始点を設定する必要があります。
**Q**: 隣接リスト、「その頂点に接続されたすべての頂点」の順序は重要ですか?
**Q**隣接リストにおいて、「その頂点に接続されたすべての頂点」の順序に決まりはありますか?
任意の順序で構いません。ただし実際のアプリケーションでは、頂点追加された順序や頂点値の順など、特定のルールに従ってそれらをソートする必要がある場合があります。これにより、特定の極値を持つ頂点を素早く見つけることができます。
順序は任意でかまいません。ただし実際の応用では、頂点追加た順序や頂点値の大小順など、特定の規則に従って並べ替える必要がある場合があります。そうすることで、「ある種の極値を持つ頂点をすばやく見つけやすくなります。
@@ -6,30 +6,30 @@ comments: true
!!! question
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重は $wgt[i-1]$値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できますが、**アイテムの一部を選択することでき、その値は選択された重量の割合に基づいて計算されます**。限られた容量の下でナップサック内のアイテムの最大値は何ですか?例を下の図に示します。
$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$ を含み、ナップサックの限られた容量内で値を最大化することを目的としています
分数ナップサック問題は 0-1 ナップサック問題と全体として非常によく似ており、状態には現在の品物 $i$ と容量 $c$ が含まれ、目標は容量制限下での最大価値を求めることである
違いは、この問題ではアイテムの一部のみを選択できることです。下の図に示すように、**アイテムを任意に分割し、重量の割合に基づいて対応する値を計算できます**。
異なる点は、本問では品物の一部だけを選べることである。以下に示すように、**品物は任意に分割でき、対応する価値は重量の割合に応じて計算される**。
1. アイテム $i$ について、その単位重量あたりの値は $val[i-1] / wgt[i-1]$ で単位値と呼ばれます
2. 重量 $w$ のアイテム $i$ の一部をナップサックに入れるとすると、ナップサックに追加される値は $w \times val[i-1] / wgt[i-1]$ です
1. 品物 $i$ について、単位重量あたりの値は $val[i-1] / wgt[i-1]$ であり、これを単位値と呼
2. 品物 $i$ の一部を重さ $w$ だけ入れると、ナップサックに増える価値は $w \times val[i-1] / wgt[i-1]$ となる
![アイテムの単位重量あたりの値](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" }
![品物の単位重量あたりの](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" }
<p align="center"> 図 15-4 &nbsp; アイテムの単位重量あたりの値 </p>
<p align="center"> 図 15-4 &nbsp; 品物の単位重量あたりの値 </p>
### 1. &nbsp; 貪欲戦略の決定
ナップサック内のアイテムの総値を最大化することは、**本質的に単位重量あたりの値を最大化することを意味します**。これから、下の図に示す貪欲戦略を導出できます
ナップサック内の品物の総値を最大化することは、**本質的に単位重量あたりの品物価値を最大化すること**である。そこから、以下に示す貪欲戦略を導ける
1. アイテムを単位値の高い順から低い順にソートします。
2. すべてのアイテムを反復し、**各ラウンドで最も高い単位値を持つアイテムを貪欲に選択**します。
3. ナップサックの残り容量が不十分な場合、現在のアイテムの一部を使用してナップサックを満たします。
1. 品物を単位値の高い順にソートす
2. すべての品物を走査し、**各回で単位価値が最も高い品物を貪欲に選択する**
3. 残りのナップサック容量が足りない場合、現在の品物の一部を使てナップサックを満たす。
![分数ナップサック問題の貪欲戦略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" }
@@ -37,35 +37,35 @@ comments: true
### 2. &nbsp; コード実装
アイテムを単位値でソートするために `Item` クラスを作成しました。ナップサックが満たされるまでループして貪欲な選択を行い、その後終了して解を返します:
品物を単位値でソートできるように、`Item` クラスを定義する。貪欲選択を繰り返し、ナップサックが満杯になったら終了して解を返す。
=== "Python"
```python title="fractional_knapsack.py"
class Item:
"""アイテム"""
"""品物"""
def __init__(self, w: int, v: int):
self.w = w # アイテムの重
self.v = v # アイテムの価値
self.w = w # 品物の重
self.v = v # 品物の価値
def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
"""分数ナップサック:貪欲法"""
# アイテムリストを作成、2 つの属性を含む:重量、価値
# 重さと価値の 2 属性を持つ品物リストを作成
items = [Item(w, v) for w, v in zip(wgt, val)]
# 単位価値 item.v / item.w 高い順にソート
# 単位価値 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
```
@@ -73,11 +73,11 @@ comments: true
=== "C++"
```cpp title="fractional_knapsack.cpp"
/* アイテム */
/* 品物 */
class Item {
public:
int w; // アイテムの重
int v; // アイテムの価値
int w; // 品物の重
int v; // 品物の価値
Item(int w, int v) : w(w), v(v) {
}
@@ -85,24 +85,24 @@ comments: true
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
// アイテムリストを作成、2つの属性を含む:重量、価値
// 重さと価値の 2 属性を持つ品物リストを作成
vector<Item> items;
for (int i = 0; i < wgt.size(); i++) {
items.push_back(Item(wgt[i], val[i]));
}
// 単位価値 item.v / item.w 高い順にソート
// 単位価値 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;
}
}
@@ -113,10 +113,10 @@ comments: true
=== "Java"
```java title="fractional_knapsack.java"
/* アイテム */
/* 品物 */
class Item {
int w; // アイテムの重
int v; // アイテムの価値
int w; // 品物の重
int v; // 品物の価値
public Item(int w, int v) {
this.w = w;
@@ -126,24 +126,24 @@ comments: true
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(int[] wgt, int[] val, int cap) {
// アイテムリストを作成、2つの属性を含む:重量、価値
// 重さと価値の 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 高い順にソート
// 単位価値 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;
}
}
@@ -154,96 +154,403 @@ comments: true
=== "C#"
```csharp title="fractional_knapsack.cs"
[class]{Item}-[func]{}
/* 品物 */
class Item(int w, int v) {
public int w = w; // 品物の重さ
public int v = v; // 品物の価値
}
[class]{fractional_knapsack}-[func]{FractionalKnapsack}
/* 分数ナップサック:貪欲法 */
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 の高い順にソートする
Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));
// 貪欲選択を繰り返す
double res = 0;
foreach (Item item in items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (double)item.v / item.w * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "Go"
```go title="fractional_knapsack.go"
[class]{Item}-[func]{}
/* 品物 */
type Item struct {
w int // 品物の重さ
v int // 品物の価値
}
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
func fractionalKnapsack(wgt []int, val []int, cap int) float64 {
// 重さと価値の 2 属性を持つ品物リストを作成
items := make([]Item, len(wgt))
for i := 0; i < len(wgt); i++ {
items[i] = Item{wgt[i], val[i]}
}
// 単位価値 item.v / item.w の高い順にソートする
sort.Slice(items, func(i, j int) bool {
return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)
})
// 貪欲選択を繰り返す
res := 0.0
for _, item := range items {
if item.w <= cap {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += float64(item.v)
cap -= item.w
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += float64(item.v) / float64(item.w) * float64(cap)
// 残り容量がないため、ループを抜ける
break
}
}
return res
}
```
=== "Swift"
```swift title="fractional_knapsack.swift"
[class]{Item}-[func]{}
/* 品物 */
class Item {
var w: Int // 品物の重さ
var v: Int // 品物の価値
[class]{}-[func]{fractionalKnapsack}
init(w: Int, v: Int) {
self.w = w
self.v = v
}
}
/* 分数ナップサック:貪欲法 */
func fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {
// 重さと価値の 2 属性を持つ品物リストを作成
var items = zip(wgt, val).map { Item(w: $0, v: $1) }
// 単位価値 item.v / item.w の高い順にソートする
items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }
// 貪欲選択を繰り返す
var res = 0.0
var cap = cap
for item in items {
if item.w <= cap {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += Double(item.v)
cap -= item.w
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += Double(item.v) / Double(item.w) * Double(cap)
// 残り容量がないため、ループを抜ける
break
}
}
return res
}
```
=== "JS"
```javascript title="fractional_knapsack.js"
[class]{Item}-[func]{}
/* 品物 */
class Item {
constructor(w, v) {
this.w = w; // 品物の重さ
this.v = v; // 品物の価値
}
}
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
function fractionalKnapsack(wgt, val, cap) {
// 重さと価値の 2 属性を持つ品物リストを作成
const items = wgt.map((w, i) => new Item(w, val[i]));
// 単位価値 item.v / item.w の高い順にソートする
items.sort((a, b) => b.v / b.w - a.v / a.w);
// 貪欲選択を繰り返す
let res = 0;
for (const item of items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (item.v / item.w) * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "TS"
```typescript title="fractional_knapsack.ts"
[class]{Item}-[func]{}
/* 品物 */
class Item {
w: number; // 品物の重さ
v: number; // 品物の価値
[class]{}-[func]{fractionalKnapsack}
constructor(w: number, v: number) {
this.w = w;
this.v = v;
}
}
/* 分数ナップサック:貪欲法 */
function fractionalKnapsack(wgt: number[], val: number[], cap: number): number {
// 重さと価値の 2 属性を持つ品物リストを作成
const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));
// 単位価値 item.v / item.w の高い順にソートする
items.sort((a, b) => b.v / b.w - a.v / a.w);
// 貪欲選択を繰り返す
let res = 0;
for (const item of items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (item.v / item.w) * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "Dart"
```dart title="fractional_knapsack.dart"
[class]{Item}-[func]{}
/* 品物 */
class Item {
int w; // 品物の重さ
int v; // 品物の価値
[class]{}-[func]{fractionalKnapsack}
Item(this.w, this.v);
}
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(List<int> wgt, List<int> val, int cap) {
// 重さと価値の 2 属性を持つ品物リストを作成
List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));
// 単位価値 item.v / item.w の高い順にソートする
items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));
// 貪欲選択を繰り返す
double res = 0;
for (Item item in items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += item.v / item.w * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "Rust"
```rust title="fractional_knapsack.rs"
[class]{Item}-[func]{}
/* 品物 */
struct Item {
w: i32, // 品物の重さ
v: i32, // 品物の価値
}
[class]{}-[func]{fractional_knapsack}
impl Item {
fn new(w: i32, v: i32) -> Self {
Self { w, v }
}
}
/* 分数ナップサック:貪欲法 */
fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {
// 重さと価値の 2 属性を持つ品物リストを作成
let mut items = wgt
.iter()
.zip(val.iter())
.map(|(&w, &v)| Item::new(w, v))
.collect::<Vec<Item>>();
// 単位価値 item.v / item.w の高い順にソートする
items.sort_by(|a, b| {
(b.v as f64 / b.w as f64)
.partial_cmp(&(a.v as f64 / a.w as f64))
.unwrap()
});
// 貪欲選択を繰り返す
let mut res = 0.0;
for item in &items {
if item.w <= cap {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v as f64;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += item.v as f64 / item.w as f64 * cap as f64;
// 残り容量がないため、ループを抜ける
break;
}
}
res
}
```
=== "C"
```c title="fractional_knapsack.c"
[class]{Item}-[func]{}
/* 品物 */
typedef struct {
int w; // 品物の重さ
int v; // 品物の価値
} Item;
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
float fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {
// 重さと価値の 2 属性を持つ品物リストを作成
Item *items = malloc(sizeof(Item) * itemCount);
for (int i = 0; i < itemCount; i++) {
items[i] = (Item){.w = wgt[i], .v = val[i]};
}
// 単位価値 item.v / item.w の高い順にソートする
qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);
// 貪欲選択を繰り返す
float res = 0.0;
for (int i = 0; i < itemCount; i++) {
if (items[i].w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += items[i].v;
cap -= items[i].w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (float)cap / items[i].w * items[i].v;
cap = 0;
break;
}
}
free(items);
return res;
}
```
=== "Kotlin"
```kotlin title="fractional_knapsack.kt"
[class]{Item}-[func]{}
/* 品物 */
class Item(
val w: Int, // 品物
val v: Int // 品物の価値
)
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
fun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {
// 重さと価値の 2 属性を持つ品物リストを作成
var cap = c
val items = arrayOfNulls<Item>(wgt.size)
for (i in wgt.indices) {
items[i] = Item(wgt[i], _val[i])
}
// 単位価値 item.v / item.w の高い順にソートする
items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }
// 貪欲選択を繰り返す
var res = 0.0
for (item in items) {
if (item!!.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v
cap -= item.w
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += item.v.toDouble() / item.w * cap
// 残り容量がないため、ループを抜ける
break
}
}
return res
}
```
=== "Ruby"
```ruby title="fractional_knapsack.rb"
[class]{Item}-[func]{}
### アイテム ###
class Item
attr_accessor :w # 品物の重さ
attr_accessor :v # 品物の価値
[class]{}-[func]{fractional_knapsack}
def initialize(w, v)
@w = w
@v = v
end
end
### 分数ナップサック:貪欲法 ###
def fractional_knapsack(wgt, val, cap)
# 重さと価値の 2 属性を持つ品物リストを作成する
items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }
# 単位価値 item.v / item.w の高い順にソートする
items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }
# 貪欲選択を繰り返す
res = 0
for item in items
if item.w <= cap
# 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v
cap -= item.w
else
# 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (item.v.to_f / item.w) * cap
# 残り容量がないため、ループを抜ける
break
end
end
res
end
```
ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、**時間計算量は $O(n)$** です。ここで $n$ はアイテムの数です。
??? pythontutor "コードの可視化"
`Item` オブジェクトリストが初期化されるため、**空間計算量は $O(n)$** です。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E5%93%81%E7%89%A9%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20w%3A%20int%2C%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E9%87%8D%E3%81%95%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E4%BE%A1%E5%80%A4%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20%E9%87%8D%E3%81%95%E3%81%A8%E4%BE%A1%E5%80%A4%E3%81%AE%202%20%E5%B1%9E%E6%80%A7%E3%82%92%E6%8C%81%E3%81%A4%E5%93%81%E7%89%A9%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%0A%20%20%20%20items%20%3D%20%5BItem%28w%2C%20v%29%20for%20w%2C%20v%20in%20zip%28wgt%2C%20val%29%5D%0A%20%20%20%20%23%20%E5%8D%98%E4%BD%8D%E4%BE%A1%E5%80%A4%20item.v%20%2F%20item.w%20%E3%81%AE%E9%AB%98%E3%81%84%E9%A0%86%E3%81%AB%E3%82%BD%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20%2F%20item.w%2C%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E5%8D%81%E5%88%86%E3%81%AA%E3%82%89%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%82%92%E4%B8%B8%E3%81%94%E3%81%A8%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E8%B6%B3%E3%82%8A%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%81%AE%E4%B8%80%E9%83%A8%E3%81%A0%E3%81%91%E3%82%92%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20%2F%20item.w%29%20%2A%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%80%81%E3%83%AB%E3%83%BC%E3%83%97%E3%82%92%E6%8A%9C%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt%2C%20val%2C%20cap%29%0A%20%20%20%20print%28f%22%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E5%AE%B9%E9%87%8F%E3%82%92%E8%B6%85%E3%81%88%E3%81%AA%E3%81%84%E6%9C%80%E5%A4%A7%E4%BE%A1%E5%80%A4%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E5%93%81%E7%89%A9%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20w%3A%20int%2C%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E9%87%8D%E3%81%95%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E4%BE%A1%E5%80%A4%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20%E9%87%8D%E3%81%95%E3%81%A8%E4%BE%A1%E5%80%A4%E3%81%AE%202%20%E5%B1%9E%E6%80%A7%E3%82%92%E6%8C%81%E3%81%A4%E5%93%81%E7%89%A9%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%0A%20%20%20%20items%20%3D%20%5BItem%28w%2C%20v%29%20for%20w%2C%20v%20in%20zip%28wgt%2C%20val%29%5D%0A%20%20%20%20%23%20%E5%8D%98%E4%BD%8D%E4%BE%A1%E5%80%A4%20item.v%20%2F%20item.w%20%E3%81%AE%E9%AB%98%E3%81%84%E9%A0%86%E3%81%AB%E3%82%BD%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20%2F%20item.w%2C%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E5%8D%81%E5%88%86%E3%81%AA%E3%82%89%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%82%92%E4%B8%B8%E3%81%94%E3%81%A8%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E8%B6%B3%E3%82%8A%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%81%AE%E4%B8%80%E9%83%A8%E3%81%A0%E3%81%91%E3%82%92%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20%2F%20item.w%29%20%2A%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%80%81%E3%83%AB%E3%83%BC%E3%83%97%E3%82%92%E6%8A%9C%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt%2C%20val%2C%20cap%29%0A%20%20%20%20print%28f%22%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E5%AE%B9%E9%87%8F%E3%82%92%E8%B6%85%E3%81%88%E3%81%AA%E3%81%84%E6%9C%80%E5%A4%A7%E4%BE%A1%E5%80%A4%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
組み込みのソートアルゴリズムの時間計算量は通常 $O(\log n)$、空間計算量は通常 $O(\log n)$ または $O(n)$ であり、具体的な値はプログラミング言語の実装に依存する。
ソートを除けば、最悪の場合は品物リスト全体を走査する必要があるため、**時間計算量は $O(n)$** であり、ここで $n$ は品物数である。
`Item` オブジェクトのリストを初期化しているため、**空間計算量は $O(n)$** である。
### 3. &nbsp; 正しさの証明
背理法を使用します。アイテム $x$ が最高の単位値を持ち、あるアルゴリズムが最大値 `res` を生成するが、解にアイテム $x$ が含まれていないと仮定します。
背理法を用いる。品物 $x$ が単位価値最大の品物であり、あるアルゴリズムで得られた最大価値を `res` するが、その解には品物 $x$ が含まれていないと仮定す
今、ナップサックから任意のアイテムの単位重量を取り除き、アイテム $x$ の単位重量で置き換えます。アイテム $x$ の単位値が最であるため、置き換え後の総値は確実に `res` より大きくなります。**これは `res` が最適解であるという仮定と矛盾し、最適解には必ずアイテム $x$ が含まれることを証明します**。
ここでナップサックから単位重量の任意の品物を取り出し、単位重量の品物 $x$ に置き換える。品物 $x$ の単位値が最であるため、置き換え後の総値は必ず `res` より大きくな。**これは `res` が最適解であることに矛盾し、最適解には必ず品物 $x$ が含まれなければならないことを示す**。
この解の他のアイテムについても、上記の矛盾を構できます。全体的に、**単位値がより大きいアイテムは常により良い選択**であり、貪欲戦略が効果的であることを証明します
この解に含まれる他の品物についても、同様の矛盾を構できる。要するに、**単位値がより大きい品物は常により良い選択である**。これは貪欲戦略が効であることを示している
の図に示すように、アイテムの重量と単位値をそれぞれ二次元チャートの横軸と縦軸となすと、分数ナップサック問題は「限られた横軸範囲内で囲まれる最大面積を求める」ことに変換できます。この類は、幾何学的観点から貪欲戦略の効果を理解するのに役立ちます
下に示すように、品物の重さと品物の単位値をそれぞれ二次元グラフの横軸と縦軸となすと、分数ナップサック問題は「有限な横軸区間で囲まれる最大面積を求める問題」に変換でき。この類は、幾何学的観点から貪欲戦略の有効性を理解する助けになる
![分数ナップサック問題の幾何学的表現](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" }
+263 -79
View File
@@ -2,68 +2,68 @@
comments: true
---
# 15.1 &nbsp; 貪欲アルゴリズム
# 15.1 &nbsp; 貪欲
<u>貪欲アルゴリズム</u>は最適化問題を解決するための一般的なアルゴリズムで、基本的に問題の各意思決定段階で最も良い選択をすること、つまり局所的に最適な決定を貪欲に行い、グローバルに最適解を見つけることを望みます。貪欲アルゴリズムは簡潔で効率的であり、多くの実用的な問題で広く使用されています。
<u>貪欲法(greedy algorithm</u>は最適化問題を解ための一般的なアルゴリズムです。その基本的な考え方は、問題の各意思決定段階において、その時点で最善に見える選択を行い、すなわち貪欲に局所最適な決定を下すことで、大域最適解を得ようとするものです。貪欲法は簡潔で効率的であり、多くの実際の問題で広く用いられています。
貪欲アルゴリズムと動的プログラミングは、どちらも最適化問題を解決するためによく使用されます。両者最適部分構造の性質に依存するなど、いくつかの類似点を共有していますが、動作方法が異なります。
貪欲法と動的計画法は、どちらも最適化問題を解く際によく用いられます。両者には、最適部分構造に依存するなどの共通点がありますが、その動作原理は異なります。
- 動的プログラミングは現在の決定段階ですべての以前の決定を考慮し、過去の部分問題の解を使用して現在の部分問題の解を構築します。
- 貪欲アルゴリズムは過去の決定を考慮せず、代わりに貪欲な選択を続け、問題が解決されるまで問題の範囲を継続的に狭めます。
- 動的計画法は、前の段階までのすべての決定に基づいて現在の決定を考、過去の部分問題の解を用いて現在の部分問題の解を構築します。
- 貪欲は過去の決定を考慮せず、ひたすら前に進みながら貪欲な選択を行い、問題の範囲を縮小し続けて、最終的に問題を解決します。
まず、「完全ナップサック問題」の章で紹介された「コイン交換」の例を通じて貪欲アルゴリズムの動作原理を理解しましょう。すでによく知っていると思います。
まずは例題「コイン両替」を通して、貪欲法の仕組みを理解しましょう。この問題はすでに「完全ナップサック問題」の節で紹介しているので、見覚えがあるはずです。
!!! question
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ 、目標金額は $amt$ です。各種類のコインは無制限に利用可能で、目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください
$n$ 種類の硬貨が与えられ、$i$ 番目の硬貨の額面は $coins[i - 1]$ 、目標金額は $amt$ です。各硬貨は何度でも選べるとき、目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は $-1$ を返します
この問題で採用される貪欲戦略を下の図に示します。目標金額が与えられたとき、**それに最も近く、それを超えないコインを貪欲に選択**し、目標金額が満たされるまでこのステップを繰り返します。
この問題で採用る貪欲戦略は下図のとおりです。目標金額が与えられた、**それを超えず、かつ最も近い硬貨を貪欲に選択**、この手順を目標金額を作り切るまで繰り返します。
![コイン交換の貪欲戦略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" }
![コイン両替の貪欲戦略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" }
<p align="center"> 図 15-1 &nbsp; コイン交換の貪欲戦略 </p>
<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 リストソートされていると仮定
"""コイン交換:貪欲法"""
# coins リストソート済みと仮定する
i = len(coins) - 1
count = 0
# 残り金額がなくなるまで貪欲選択をループ
# 残額がなくなるまで貪欲選択を繰り返す
while amt > 0:
# 残り金額に最も近く、それより小さい硬貨を見つける
# 残額以下で最も近い硬貨を見つける
while i > 0 and coins[i] > amt:
i -= 1
# coins[i] を選択
# coins[i] を選択する
amt -= coins[i]
count += 1
# 実行可能な解が見つからない場合、-1 を返す
# 実行可能な解が見つからなければ -1 を返す
return count if amt == 0 else -1
```
=== "C++"
```cpp title="coin_change_greedy.cpp"
/* 硬貨両替:貪欲法 */
/* コイン交換:貪欲法 */
int coinChangeGreedy(vector<int> &coins, int amt) {
// 硬貨リストが順序付けされていると仮定
// coins リストはソート済みと仮定する
int i = coins.size() - 1;
int count = 0;
// 残り金額がなくなるまで貪欲選択をループ
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残り金額に近く、それ以下の最小硬貨を見つける
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからない場合、-1 を返す
// 実行可能な解が見つからなければ -1 を返す
return amt == 0 ? count : -1;
}
```
@@ -71,22 +71,22 @@ comments: true
=== "Java"
```java title="coin_change_greedy.java"
/* 硬貨両替:貪欲法 */
/* コイン交換:貪欲法 */
int coinChangeGreedy(int[] coins, int amt) {
// 硬貨リストが順序付けされていると仮定
// coins リストはソート済みと仮定する
int i = coins.length - 1;
int count = 0;
// 残り金額がなくなるまで貪欲選択をループ
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残り金額に近く、それ以下の最小硬貨を見つける
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからない場合、-1 を返す
// 実行可能な解が見つからなければ -1 を返す
return amt == 0 ? count : -1;
}
```
@@ -94,131 +94,315 @@ comments: true
=== "C#"
```csharp title="coin_change_greedy.cs"
[class]{coin_change_greedy}-[func]{CoinChangeGreedy}
/* コイン交換:貪欲法 */
int CoinChangeGreedy(int[] coins, int amt) {
// coins リストはソート済みと仮定する
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;
}
```
=== "Go"
```go title="coin_change_greedy.go"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
func coinChangeGreedy(coins []int, amt int) int {
// coins リストはソート済みと仮定する
i := len(coins) - 1
count := 0
// 残額がなくなるまで貪欲選択を繰り返す
for amt > 0 {
// 残額以下で最も近い硬貨を見つける
for i > 0 && coins[i] > amt {
i--
}
// coins[i] を選択する
amt -= coins[i]
count++
}
// 実行可能な解が見つからなければ -1 を返す
if amt != 0 {
return -1
}
return count
}
```
=== "Swift"
```swift title="coin_change_greedy.swift"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
func coinChangeGreedy(coins: [Int], amt: Int) -> Int {
// coins リストはソート済みと仮定する
var i = coins.count - 1
var count = 0
var amt = amt
// 残額がなくなるまで貪欲選択を繰り返す
while amt > 0 {
// 残額以下で最も近い硬貨を見つける
while i > 0 && coins[i] > amt {
i -= 1
}
// coins[i] を選択する
amt -= coins[i]
count += 1
}
// 実行可能な解が見つからなければ -1 を返す
return amt == 0 ? count : -1
}
```
=== "JS"
```javascript title="coin_change_greedy.js"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
function coinChangeGreedy(coins, amt) {
// coins 配列はソート済みと仮定する
let i = coins.length - 1;
let count = 0;
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからなければ -1 を返す
return amt === 0 ? count : -1;
}
```
=== "TS"
```typescript title="coin_change_greedy.ts"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
function coinChangeGreedy(coins: number[], amt: number): number {
// coins 配列はソート済みと仮定する
let i = coins.length - 1;
let count = 0;
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからなければ -1 を返す
return amt === 0 ? count : -1;
}
```
=== "Dart"
```dart title="coin_change_greedy.dart"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
int coinChangeGreedy(List<int> coins, int amt) {
// coins リストはソート済みと仮定する
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;
}
```
=== "Rust"
```rust title="coin_change_greedy.rs"
[class]{}-[func]{coin_change_greedy}
/* コイン交換:貪欲法 */
fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {
// coins リストはソート済みと仮定する
let mut i = coins.len() - 1;
let mut count = 0;
// 残額がなくなるまで貪欲選択を繰り返す
while amt > 0 {
// 残額以下で最も近い硬貨を見つける
while i > 0 && coins[i] > amt {
i -= 1;
}
// coins[i] を選択する
amt -= coins[i];
count += 1;
}
// 実行可能な解が見つからなければ -1 を返す
if amt == 0 {
count
} else {
-1
}
}
```
=== "C"
```c title="coin_change_greedy.c"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
int coinChangeGreedy(int *coins, int size, int amt) {
// coins リストはソート済みと仮定する
int i = 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;
}
```
=== "Kotlin"
```kotlin title="coin_change_greedy.kt"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
fun coinChangeGreedy(coins: IntArray, amt: Int): Int {
// coins リストはソート済みと仮定する
var am = amt
var i = coins.size - 1
var count = 0
// 残額がなくなるまで貪欲選択を繰り返す
while (am > 0) {
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > am) {
i--
}
// coins[i] を選択する
am -= coins[i]
count++
}
// 実行可能な解が見つからなければ -1 を返す
return if (am == 0) count else -1
}
```
=== "Ruby"
```ruby title="coin_change_greedy.rb"
[class]{}-[func]{coin_change_greedy}
### コイン両替:貪欲法 ###
def coin_change_greedy(coins, amt)
# coins リストはソート済みと仮定する
i = coins.length - 1
count = 0
# 残額がなくなるまで貪欲選択を繰り返す
while amt > 0
# 残額以下で最も近い硬貨を見つける
while i > 0 && coins[i] > amt
i -= 1
end
# coins[i] を選択する
amt -= coins[i]
count += 1
end
# 実行可能な解が見つからなければ `-1` を返す
amt == 0 ? count : -1
end
```
感嘆するかもしれません:なんて簡潔なんだ!貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。
??? pythontutor "コードの可視化"
## 15.1.1 &nbsp; 貪欲アルゴリズムの利点と制限
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D%2C%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E3%82%B3%E3%82%A4%E3%83%B3%E4%BA%A4%E6%8F%9B%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20coins%20%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AF%E3%82%BD%E3%83%BC%E3%83%88%E6%B8%88%E3%81%BF%E3%81%A8%E4%BB%AE%E5%AE%9A%E3%81%99%E3%82%8B%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E3%81%8C%E3%81%AA%E3%81%8F%E3%81%AA%E3%82%8B%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E4%BB%A5%E4%B8%8B%E3%81%A7%E6%9C%80%E3%82%82%E8%BF%91%E3%81%84%E7%A1%AC%E8%B2%A8%E3%82%92%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20coins%5Bi%5D%20%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD%E3%81%AA%E8%A7%A3%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%82%8B%0A%20%20%20%20coins%20%3D%20%5B1%2C%205%2C%2010%2C%2020%2C%2050%2C%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%0A%20%20%20%20coins%20%3D%20%5B1%2C%2020%2C%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E6%9E%9A%E6%95%B0%E3%81%AF%203%20%EF%BC%8C%E3%81%A4%E3%81%BE%E3%82%8A%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D%2C%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E3%82%B3%E3%82%A4%E3%83%B3%E4%BA%A4%E6%8F%9B%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20coins%20%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AF%E3%82%BD%E3%83%BC%E3%83%88%E6%B8%88%E3%81%BF%E3%81%A8%E4%BB%AE%E5%AE%9A%E3%81%99%E3%82%8B%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E3%81%8C%E3%81%AA%E3%81%8F%E3%81%AA%E3%82%8B%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E4%BB%A5%E4%B8%8B%E3%81%A7%E6%9C%80%E3%82%82%E8%BF%91%E3%81%84%E7%A1%AC%E8%B2%A8%E3%82%92%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20coins%5Bi%5D%20%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD%E3%81%AA%E8%A7%A3%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%82%8B%0A%20%20%20%20coins%20%3D%20%5B1%2C%205%2C%2010%2C%2020%2C%2050%2C%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%0A%20%20%20%20coins%20%3D%20%5B1%2C%2020%2C%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E6%9E%9A%E6%95%B0%E3%81%AF%203%20%EF%BC%8C%E3%81%A4%E3%81%BE%E3%82%8A%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
**貪欲アルゴリズムは直接的で実装が簡単であるだけでなく、通常非常に効率的でもあります**。上記のコードで、最小のコイン額面を $\min(coins)$ とすると、貪欲な選択ループは最大 $amt / \min(coins)$ 回実行され、時間計算量は $O(amt / \min(coins))$ になります。これは動的プログラミング解法の時間計算量 $O(n \times amt)$ よりも一桁小さいです。
思わずこう言いたくなるかもしれません。So clean!貪欲法はわずか十行ほどのコードでコイン両替問題を解いてしまいます。
しかし、**一部のコイン額面の組み合わせでは、貪欲アルゴリズムは最適解を見つけることができません**。下の図は2つの例を示しています。
## 15.1.1 &nbsp; 貪欲法の利点と限界
- **正の例 $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枚のコインのみが必要です。
**貪欲法は操作が直接的で実装が簡単なだけでなく、通常は効率も高い**です。上のコードでは、硬貨の最小額面を $\min(coins)$ とすると、貪欲選択のループ回数は高々 $amt / \min(coins)$ 回であり、時間計算量は $O(amt / \min(coins))$ です。これは動的計画法による解法の時間計算量 $O(n \times amt)$ より 1 桁小さいオーダーです。
![貪欲アルゴリズムが最適解を見つけられない例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" }
しかし、**硬貨の額面の組み合わせによっては、貪欲法では最適解を見つけられません**。下図に 2 つの例を示します。
<p align="center"> 図 15-2 &nbsp; 貪欲アルゴリズムが最適解を見つけられない例 </p>
- **正例 $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" }
一般的に、貪欲アルゴリズムの適用性は2つのカテゴリに分類されます。
<p align="center"> 図 15-2 &nbsp; 貪欲法では最適解を見つけられない例 </p>
1. **最適解を見つけることが保証される**:これらの場合、貪欲アルゴリズムはしばしば最良の選択で、バックトラッキングや動的プログラミングよりも効率的である傾向があります。
2. **準最適解を見つけることができる**:貪欲アルゴリズムはここでも適用可能です。多くの複雑な問題では、グローバル最適解を見つけることは非常に困難であり、高効率の準最適解を見つけることも非常に価値があります。
つまり、コイン両替問題に対して、貪欲法は大域最適解を保証できず、非常に悪い解を見つけてしまうこともあります。この問題は動的計画法で解くほうが適しています。
## 15.1.2 &nbsp; 貪欲アルゴリズムの特徴
一般に、貪欲法が適用できる状況は次の 2 つに分けられます。
それでは、どのような問題が貪欲アルゴリズムで解決するのに適しているのでしょうか?言い換えれば、どのような条件下で貪欲アルゴリズムは最適解を見つけることを保証できるのでしょうか?
1. **最適解を保証できる場合**:この場合、貪欲法はしばしば最良の選択です。多くの場合、バックトラッキングや動的計画法より効率的だからです。
2. **近似最適解を見つけられる場合**:この場合も貪欲法は有効です。多くの複雑な問題では、大域最適解を求めること自体が非常に難しく、より高い効率で準最適解を得られるだけでも十分価値があります。
動的プログラミングと比較して、貪欲アルゴリズムはより厳しい使用条件を持ち、主に問題の2つの性質に焦点を当てています。
## 15.1.2 &nbsp; 貪欲法の特性
- **貪欲選択性**:局所的に最適な選択が常にグローバルに最適な解に導くことができる場合のみ、貪欲アルゴリズムは最適解を得ることを保証できます
- **最適部分構造**:元の問題の最適解はその部分問題の最適解を含みます。
では、どのような問題が貪欲法に適しているのでしょうか。言い換えると、貪欲法はどのような場合に最適解を保証できるのでしょうか
最適部分構造は「動的プログラミング」の章ですでに紹介されているため、ここではこれ以上議論しません。一部の問題には明らかな最適部分構造がありませんが、それでも貪欲アルゴリズムを使用して解決できることに注意することが重要です。
動的計画法と比べると、貪欲法の適用条件はより厳しく、主に次の 2 つの性質に注目します。
主に貪欲選択性を決定する方法を探索します。その記述は単純に見えますが、**実際には、多くの問題の貪欲選択性を証明することは容易ではありません**
- **貪欲選択性**:局所最適な選択が常に大域最適解につながる場合にのみ、貪欲法は最適解を保証できます
- **最適部分構造**:元の問題の最適解が、部分問題の最適解を含むことです。
例えば、コイン交換問題では、貪欲選択性を反証するために反例を簡単に挙げることができますが、それを証明することははるかに困難です。**コインの組み合わせが貪欲アルゴリズムを使用して解決できるためには、どのような条件を満たす必要があるか**と尋ねられた場合、厳密な数学的証明を提供することが困難であるため、しばしば直感や例に頼って曖昧な答えを提供しなければなりません
最適部分構造については「動的計画法」の節ですでに紹介したので、ここでは繰り返しません。なお、問題によっては最適部分構造が明確でなくても、貪欲法で解ける場合があります
ここでは主に、貪欲選択性をどのように判定するかを考えます。説明だけを見ると単純そうですが、**実際には多くの問題で、貪欲選択性を証明するのは容易ではありません**。
たとえばコイン両替問題では、反例を挙げて貪欲選択性が成り立たないことを示すのは簡単ですが、成り立つことを証明するのは難しいです。もし、**どのような条件を満たす硬貨の組み合わせなら貪欲法で解けるのか**と問われると、直感や例示に頼った曖昧な答えしか出せず、厳密な数学的証明を与えるのは困難です。
!!! quote
ある論文では、コインの組み合わせ任意の金額に対して貪欲アルゴリズムを使用して最適解を見つけることができるかどうかを判定するための時間計算量 $O(n^3)$ のアルゴリズムが示されています。
ある論文では、ある硬貨の組み合わせについて、任意の金額に対する最適解を貪欲法で求められるかどうかを判定する時間計算量 $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; 貪欲アルゴリズムによる問題解決のステップ
## 15.1.3 &nbsp; 貪欲法の問題解決手順
貪欲問題の問題解決プロセスは、一般的に以下の3つのステップに分けることができます。
貪欲法による問題解決の流れは、おおむね次の 3 段階に分けられます。
1. **問題分析**問題の特徴を整理し理解する。状態定義、最適化目標、制約などを含みます。このステップはバックトラッキングや動的プログラミングでも関与します。
2. **貪欲戦略の決定**:各ステップで貪欲選択をする方法を決定する。この戦略各ステップで問題規模を縮小し、最終的に問題全体を解決できます。
3. **正確性の証明**:通常問題が貪欲選択性と最適部分構造の両方を持つことを証明する必要があります。このステップには、帰納法や背理法などの数学的証明が必要な場合があります。
1. **問題分析**:状態定義、最適化目標、制約条件などを整理し、問題の性質を理解します。この段階はバックトラッキングや動的計画法でも共通して現れます。
2. **貪欲戦略の決定**:各ステップでどのように貪欲選択を行うかを定めます。この戦略により各ステップで問題規模を縮小し、最終的に問題全体を解決ます。
3. **正しさの証明**:通常は、その問題が貪欲選択性と最適部分構造を持つことを示す必要があります。この段階では、帰納法や背理法などの数学的証明が必要になることがあります。
貪欲戦略の決定は問題解決の核心ステップですが、実装は容易ではない場合があります。主な理由は以下の通りです。
貪欲戦略を定めることは問題解決の核心ですが、実際には簡単ではないことも多く、主な理由は次のとおりです。
- **異なる問題間で貪欲戦略は大きく異なる**。多くの問題では貪欲戦略はかなり直接的で、一般的な思考と試行を通じて思いつくことができます。しかし、一部の複雑な問題では、貪欲戦略非常に見つけにくく、これは個人の問題解決経験アルゴリズム能力の真のテストです。
- **一部の貪欲戦略は非常に誤解を招く**。自信を持って貪欲戦略を設計し、コードを書いてテストに提出したとき、一部のテストケースが通らない可能性が高いです。これは設計された貪欲戦略が「部分的に正しい」だけであるためで、上記のコイン交換の例で説明した通りです。
- **問題ごとに貪欲戦略の差が大きい**。多くの問題では貪欲戦略は比較的わかりやすく、おおまかな考察や試行だけで見つけられます。しかし複雑な問題では、貪欲戦略非常に見えにくいことがあり、その場合は解法経験アルゴリズム力が大きく問われます。
- **一見もっともらしい貪欲戦略もある**。自信を持って貪欲戦略を設計し、コードを書いて提出しても、一部のテストケースを通過できないことがあります。これは、その貪欲戦略が「部分的にしか正しくない」ためであり、先ほどのコイン両替は典型例です。
確性を確保するために、貪欲戦略に対して厳密な数学的証明を提供すべきで、**通常は背理法や数学的帰納法を含みます**。
しさを保証するために、貪欲戦略に対して厳密な数学的証明を行うべきであり、**通常は背理法や数学的帰納法が必要になります**。
しかし、正確性を証明することは容易な作業ではない場合があります。途方に暮れた場合、通常はテストケースに基づいてコードをデバッグし、貪欲戦略を段階的に修正し検証することを選択します。
しかし、正しさの証明もまた簡単とは限りません。手がかりがない場合には、テストケースを使ってコードをデバッグしながら、貪欲戦略を少しずつ修正し検証していくことがよくあります。
## 15.1.4 &nbsp; 貪欲アルゴリズムで解決される典型的な問題
## 15.1.4 &nbsp; 貪欲法の典型問題
貪欲アルゴリズムは、貪欲選択と最適部分構造の性質を満たす最適化問題によく適用されます。以下典型的な貪欲アルゴリズム問題いくつかす。
貪欲は、貪欲選択と最適部分構造を満たす最適化問題によく用いられます。以下典型的な貪欲法の問題いくつか挙げます。
- **コイン交換問題**:一部のコインの組み合わせでは、貪欲アルゴリズムは常に最適解を提供します。
- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれが一定期間にわたって行われるとします。目標はできるだけ多くのタスクを完了することです。常に最も早く終了するタスクを選択すると、貪欲アルゴリズムは最適解を達成できます。
- **分数ナップサック問題**アイテムのセットと運搬容量が与えられ、目標は総重量が運搬容量を超えず、総価値が最大化されるようなアイテムのセットを選択することです。常に最高の価値対重量比(価値/重量)のアイテムを選択すると、貪欲アルゴリズムは一部のケースで最適解を達成できます。
- **株式取引問題**:株価の履歴のセットが与えられ、複数回の取引を行うことができますが、すでに株式を所有している場合は売却後でないと再度購入できません。目標は最大利益を達成することです。
- **ハフマン符号化**:ハフマン符号化は無損失データ圧縮に使用される貪欲アルゴリズムです。ハフマン木を構築することにより、常に最低頻度の2つのノードを統合し、最小重み付きパス長(符号長)のハフマン木を生成します。
- **ダイクストラのアルゴリズム**これは与えられたソース頂点から他のすべての頂点への最短経路問題を解決するための貪欲アルゴリズムです。
- **硬貨のお釣り問題**:ある種の硬貨の組み合わせでは、貪欲法で常に最適解が得られます。
- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれがある時間区間で実行されるとします。できるだけ多くのタスクを完了することが目標で、毎回終了時刻が最も早いタスクを選ぶなら、貪欲法で最適解を得られます。
- **分数ナップサック問題**一群の品物と積載容量が与えられたとき、総重量が容量を超えず、かつ総価値が最大になるように品物を選ぶ問題です。毎回、価値対重量比(価値 / 重量)が最も高い品物を選ぶなら、ある条件下で貪欲法は最適解を得られます。
- **株式売買問題**:株価の履歴が与えられ、複数回の売買が可能ですが、すでに株を保有している場合は売却前に再度購入することはできません。目標は最大利益をることです。
- **ハフマン符号化**:ハフマン符号化は、可逆データ圧縮に用いられる貪欲です。ハフマン木を構築する際、毎回出現頻度が最も低い 2 つのノードを選んで併合すると、最終的に得られるハフマン木の重み付きパス長(符号長)は最小になります。
- **Dijkstra アルゴリズム**:与えられた点から他の頂点への最短経路問題を解く貪欲法です。
+3 -3
View File
@@ -9,13 +9,13 @@ icon: material/head-heart-outline
!!! abstract
ひまわりは太陽の方を向き、常に自分にとって最大の成長を求めます
ヒマワリは太陽に向かって回り、自らが最も大きく成長できる可能性を常に追い求める
貪欲戦略は、一の単純な選択を通じて、段階的に最良の答えへと導きます
貪欲戦略は、一回ごとの単純な選択を通じて、徐々に最適な答えへと導
## 章の内容
- [15.1 &nbsp; 貪欲アルゴリズム](greedy_algorithm.md)
- [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)
+261 -59
View File
@@ -6,59 +6,59 @@ comments: true
!!! question
配列 $ht$ を入力します。各要素は垂直仕切りの高さを表します。配列内の任意の2つの仕切りと、それらの間のスペースによってコンテナを形成できます。
配列 $ht$ が与えられ、各要素は垂直仕切りの高さを表します。配列内の任意の 2 枚の仕切りと、その間の空間で容器を構成できます。
容器の容量は高さと幅の積(面積)に等しく、高さは短い方の仕切り板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。
配列から 2 枚の仕切り板を選び、構成される容器の容量が最大となるようにしてください。最大容量を返します。例を以下の図に示します。
コンテナの容量は高さと幅の積(面積)で、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。
![最大容量問題のサンプルデータ](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" }
コンテナの容量を最大化する2つの仕切りを配列から選択し、この最大容量を返してください。例を下の図に示します。
<p align="center"> 図 15-7 &nbsp; 最大容量問題のサンプルデータ </p>
![最大容量問題の例データ](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" }
容器は任意の 2 枚の仕切り板で囲まれるため、**本問の状態は 2 枚の仕切り板のインデックスで表され、$[i, j]$ と記します**。
<p align="center"> 図 15-7 &nbsp; 最大容量問題の例データ </p>
コンテナは任意の2つの仕切りによって形成されるため、**この問題の状態は2つの仕切りのインデックスで表現され、$[i, j]$ と表記されます**。
問題の記述によれば、容量は高さと幅の積に等しく、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。容量 $cap[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)$ になります。
配列の長さを $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$ は高い仕切りです。
この問題にはさらに効率的な解法があります。下の図ように、状態 $[i, j]$ を 1 つ選び、インデックス $i < j$ かつ高さ $ht[i] < ht[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$ を短い $i$ に近づけると、容量は必ず小さくなります**。
これは、高い仕切り $j$ を移動すると、幅 $j-i$ が確実に減少するためです。高さは短い仕切りによって決定されるため、高さは同じまま($i$ が短い仕切りのまま)か減少(移動した $j$ が短い仕切りになる)しかありません。
これは、長い板 $j$ を動かした後は幅 $j-i$ が必ず小さくなるためです。また、高さは短い板で決まるので、高さは変わらない( $i$ が依然として短い板)か、小さくなる(移動後の $j$ が短いになる)ことしかありません。
![高い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" }
![長い板を内側へ動かした後の状態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" }
<p align="center"> 図 15-9 &nbsp; 高い仕切りを内側に移動した後の状態 </p>
<p align="center"> 図 15-9 &nbsp; 長い板を内側へ動かした後の状態 </p>
逆に、**短い仕切り $i$ を内側に移動することによってのみ容量を増加させることが可能です**。幅は確実に減少しますが、**高さが増加する可能性があります**(移動した短い仕切り $i$ が高くなる場合)。例えば、下の図では、短い仕切りを移動した後に面積が増加しています。
逆に考えると、**短い $i$ を内側へ縮めた場合にのみ容量が大きくなる可能性があります**。幅は必ず小さくなりますが、**高さは大きくなる可能性がある**からです(移動後の短い板 $i$ がより長くなる可能性があります)。たとえば次の図では、短い板を動かした後に面積が大きくなっています。
![短い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" }
![短い板を内側へ動かした後の状態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" }
<p align="center"> 図 15-10 &nbsp; 短い仕切りを内側に移動した後の状態 </p>
<p align="center"> 図 15-10 &nbsp; 短い板を内側へ動かした後の状態 </p>
これにより、この問題の貪欲戦略が導かれます:コンテナの両端に2つのポインタを初期化し、各ラウンドで短い仕切りに対応するポインタを内側に移動し、2つのポインタが出会うまで続けます。
以上から、本問の貪欲戦略を導けます。2 本のポインタを初期化して容器の両端に置き、各ラウンドで短いに対応するポインタを内側へ縮め、2 本のポインタが出会うまで続けます。
下の図は貪欲戦略の実行を示しています。
下の図は貪欲戦略の実行過程を示しています。
1. 最初に、ポインタ $i$ と $j$ 配列の両端に配置されます。
1. 初期状態では、ポインタ $i$ と $j$ 配列の両端にあります。
2. 現在の状態の容量 $cap[i, j]$ を計算し、最大容量を更新します。
3. 仕切り $i$ と $j$ の高さを比較し、短い仕切りを1ステップ内側に移動します。
4. $i$ と $j$ が出会うまでステップ `2.``3.` を繰り返します。
3. $i$ と $j$ の高さを比較し、短い板を内側へ 1 マス移動します。
4. `2.``3.` を繰り返し実行し、$i$ と $j$ が出会ったら終了します。
=== "<1>"
![最大容量問題の貪欲プロセス](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" }
![最大容量問題の貪欲な過程](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" }
@@ -84,29 +84,29 @@ $$
=== "<9>"
![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png){ class="animation-figure" }
<p align="center"> 図 15-11 &nbsp; 最大容量問題の貪欲プロセス </p>
<p align="center"> 図 15-11 &nbsp; 最大容量問題の貪欲な過程 </p>
### 2. &nbsp; 実装
### 2. &nbsp; コード実装
コードは最大 $n$ 回ループするため、**時間計算量は $O(n)$** です。
コードのループ回数は最大でも $n$ 回であるため、**時間計算量は $O(n)$** です。
変数 $i$、$j$、$res$ は一定量の追加スペースを使用するため、**空間計算量は $O(1)$** です。
変数 $i$、$j$、$res$ が使う追加領域は定数サイズなので、**空間計算量は $O(1)$** です。
=== "Python"
```python title="max_capacity.py"
def max_capacity(ht: list[int]) -> int:
"""最大容量:貪欲法"""
# ij を初期化、配列の両端で分割させる
# i, j を初期化し、それぞれ配列の両端に置く
i, j = 0, len(ht) - 1
# 初期最大容量は 0
# 初期最大容量は 0
res = 0
# 2 の板が出会うまで貪欲選択をループ
# 2 の板が出会うまで貪欲選択を繰り返す
while i < j:
# 最大容量を更新
# 最大容量を更新する
cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
# 短いを内側に移動
# 短いを内側へ動かす
if ht[i] < ht[j]:
i += 1
else:
@@ -119,16 +119,16 @@ $$
```cpp title="max_capacity.cpp"
/* 最大容量:貪欲法 */
int maxCapacity(vector<int> &ht) {
// ij を初期化し、配列の両端で分割させる
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0, j = ht.size() - 1;
// 初期最大容量は 0
// 初期最大容量は 0
int res = 0;
// 2の板が出会うまで貪欲選択をループ
// 2の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新
// 最大容量を更新する
int cap = min(ht[i], ht[j]) * (j - i);
res = max(res, cap);
// より短いを内側に移動
// 短いを内側へ動かす
if (ht[i] < ht[j]) {
i++;
} else {
@@ -144,16 +144,16 @@ $$
```java title="max_capacity.java"
/* 最大容量:貪欲法 */
int maxCapacity(int[] ht) {
// ij を初期化し、配列の両端で分割させる
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0, j = ht.length - 1;
// 初期最大容量は 0
// 初期最大容量は 0
int res = 0;
// 2の板が出会うまで貪欲選択をループ
// 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 {
@@ -167,77 +167,279 @@ $$
=== "C#"
```csharp title="max_capacity.cs"
[class]{max_capacity}-[func]{MaxCapacity}
/* 最大容量:貪欲法 */
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;
}
```
=== "Go"
```go title="max_capacity.go"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
func maxCapacity(ht []int) int {
// i, j を初期化し、それぞれ配列の両端に置く
i, j := 0, len(ht)-1
// 初期の最大容量は 0
res := 0
// 2 枚の板が出会うまで貪欲選択を繰り返す
for i < j {
// 最大容量を更新する
capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)
res = int(math.Max(float64(res), float64(capacity)))
// 短い方を内側へ動かす
if ht[i] < ht[j] {
i++
} else {
j--
}
}
return res
}
```
=== "Swift"
```swift title="max_capacity.swift"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
func maxCapacity(ht: [Int]) -> Int {
// i, j を初期化し、それぞれ配列の両端に置く
var i = ht.startIndex, j = ht.endIndex - 1
// 初期の最大容量は 0
var res = 0
// 2 枚の板が出会うまで貪欲選択を繰り返す
while i < j {
// 最大容量を更新する
let cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
// 短い方を内側へ動かす
if ht[i] < ht[j] {
i += 1
} else {
j -= 1
}
}
return res
}
```
=== "JS"
```javascript title="max_capacity.js"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
function maxCapacity(ht) {
// i, j を初期化し、それぞれ配列の両端に置く
let i = 0,
j = ht.length - 1;
// 初期の最大容量は 0
let res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
const cap = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i += 1;
} else {
j -= 1;
}
}
return res;
}
```
=== "TS"
```typescript title="max_capacity.ts"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
function maxCapacity(ht: number[]): number {
// i, j を初期化し、それぞれ配列の両端に置く
let i = 0,
j = ht.length - 1;
// 初期の最大容量は 0
let res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
const cap: number = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i += 1;
} else {
j -= 1;
}
}
return res;
}
```
=== "Dart"
```dart title="max_capacity.dart"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
int maxCapacity(List<int> ht) {
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0, j = ht.length - 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;
}
```
=== "Rust"
```rust title="max_capacity.rs"
[class]{}-[func]{max_capacity}
/* 最大容量:貪欲法 */
fn max_capacity(ht: &[i32]) -> i32 {
// i, j を初期化し、それぞれ配列の両端に置く
let mut i = 0;
let mut j = ht.len() - 1;
// 初期の最大容量は 0
let mut res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while i < j {
// 最大容量を更新する
let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;
res = std::cmp::max(res, cap);
// 短い方を内側へ動かす
if ht[i] < ht[j] {
i += 1;
} else {
j -= 1;
}
}
res
}
```
=== "C"
```c title="max_capacity.c"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
int maxCapacity(int ht[], int htLength) {
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0;
int j = htLength - 1;
// 初期の最大容量は 0
int res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
int capacity = myMin(ht[i], ht[j]) * (j - i);
res = myMax(res, capacity);
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Kotlin"
```kotlin title="max_capacity.kt"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
fun maxCapacity(ht: IntArray): Int {
// i, j を初期化し、それぞれ配列の両端に置く
var i = 0
var j = ht.size - 1
// 初期の最大容量は 0
var res = 0
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
val cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i++
} else {
j--
}
}
return res
}
```
=== "Ruby"
```ruby title="max_capacity.rb"
[class]{}-[func]{max_capacity}
### 最大容量:貪欲法 ###
def max_capacity(ht)
# i, j を初期化し、それぞれ配列の両端に置く
i, j = 0, ht.length - 1
# 初期の最大容量は 0
res = 0
# 2 枚の板が出会うまで貪欲選択を繰り返す
while i < j
# 最大容量を更新する
cap = [ht[i], ht[j]].min * (j - i)
res = [res, cap].max
# 短い方を内側へ動かす
if ht[i] < ht[j]
i += 1
else
j -= 1
end
end
res
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20i%2C%20j%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%97%E3%80%81%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E4%B8%A1%E7%AB%AF%E3%81%AB%E7%BD%AE%E3%81%8F%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E6%9C%9F%E3%81%AE%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%202%20%E6%9E%9A%E3%81%AE%E6%9D%BF%E3%81%8C%E5%87%BA%E4%BC%9A%E3%81%86%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D%2C%20ht%5Bj%5D%29%20%2A%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res%2C%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E7%9F%AD%E3%81%84%E6%96%B9%E3%82%92%E5%86%85%E5%81%B4%E3%81%B8%E5%8B%95%E3%81%8B%E3%81%99%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3%2C%208%2C%205%2C%202%2C%207%2C%207%2C%203%2C%204%5D%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20i%2C%20j%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%97%E3%80%81%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E4%B8%A1%E7%AB%AF%E3%81%AB%E7%BD%AE%E3%81%8F%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E6%9C%9F%E3%81%AE%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%202%20%E6%9E%9A%E3%81%AE%E6%9D%BF%E3%81%8C%E5%87%BA%E4%BC%9A%E3%81%86%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D%2C%20ht%5Bj%5D%29%20%2A%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res%2C%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E7%9F%AD%E3%81%84%E6%96%B9%E3%82%92%E5%86%85%E5%81%B4%E3%81%B8%E5%8B%95%E3%81%8B%E3%81%99%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3%2C%208%2C%205%2C%202%2C%207%2C%207%2C%203%2C%204%5D%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
### 3. &nbsp; 正しさの証明
貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。
貪欲法が総当たりより速いのは、各ラウンドの貪欲選択がいくつかの状態を「スキップ」するためです。
例えば、$i$ が短い仕切りで $j$ が高い仕切りである状態 $cap[i, j]$ の下で、短い仕切り $i$ を貪欲に1ステップ内側に移動すると、の図に示す「スキップされた」状態につながります。**これは、れらの状態の容量を後で検証できないことを意味します**。
たとえば状態 $cap[i, j]$ において、$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" }
![短いの移動によってスキップされる状態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" }
<p align="center"> 図 15-12 &nbsp; 短い仕切りの移動によってスキップされる状態 </p>
<p align="center"> 図 15-12 &nbsp; 短いの移動によってスキップされる状態 </p>
観察すると、**これらのスキップされた状態は実際には高い仕切り $j$ 内側に移動したすべての状態**です。高い仕切りを内側に移動すると容量が確実に減少することをすでに証明しました。したがって、スキップされた状態は最適解である可能性がなく、**それらをスキップしても最適解を逃すことはありません**。
観察すると、**これらのスキップされた状態は実際には長い板 $j$ 内側へ動かしたすべての状態そのものです**。前述のとおり、長い板を内側へ動かすと容量は必ず小さくなります。つまり、スキップされた状態はいずれも最適解にはなりえず、**それらを飛ばしても最適解を逃すことはありません**。
分析により、短い仕切りを移動する操作は「安全」であり、貪欲戦略が効果的であることが示されます。
以上の分析から、短い板を動かす操作は「安全」であり、貪欲戦略は有効であると分かります。
@@ -2,33 +2,33 @@
comments: true
---
# 15.4 &nbsp; 最大積切断問題
# 15.4 &nbsp; 最大積分割問題
!!! question
整数 $n$ が与えられたとき、それを合計が $n$ になる少なくとも2つの正整数に分割し、これらの整数の最大を求めてください。下図に示すとおりです
正整数 $n$ が与えられたとき、それを少なくとも 2 つの正整数の和に分割し、分割後のすべての整数の積の最大を求め。下図に示す。
![最大積切断問題の定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" }
![最大積分割問題の定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" }
<p align="center"> 図 15-13 &nbsp; 最大積切断問題の定義 </p>
<p align="center"> 図 15-13 &nbsp; 最大積分割問題の定義 </p>
$n$ を $m$ 個の整数因子に分割すると仮定し、$i$ 番目の因子を $n_i$ と記すと、
仮に $n$ を $m$ 個の整数因子に分割し、そのうち第 $i$ の因子を $n_i$ と記すと、
$$
n = \sum_{i=1}^{m}n_i
$$
この問題の目は、すべての整数因子の最大積を見つけることです。すなわち
問題の目は、すべての整数因子の積の最大値を求めることであり、すなわち
$$
\max(\prod_{i=1}^{m}n_i)
$$
慮すべき点:分割数 $m$ はどの程度大きくすべきか、各 $n_i$ は何であるべきか?
えるべきことは、分割数 $m$ をいくつにすべきか、各 $n_i$ をいくつにすべきかである。
### 1. &nbsp; 貪欲戦略の決定
経験的に、2つの整数の積は多くの場合その和より大きくなります。$n$ から因子 $2$ を分割すると仮定すると、その積は $2(n-2)$ です。この積を $n$ と比較します:
経験的に、2 つの整数の積はその和より大きくなることが多い。$n$ から因子 $2$ を 1 つ切り出すと、それらの積は $2(n-2)$ となる。この積を $n$ と比較すると、
$$
\begin{aligned}
@@ -38,82 +38,82 @@ n & \geq 4
\end{aligned}
$$
下の図に示すように、$n \geq 4$ のとき、$2$ を分割すると積が増加します。**これは4以上の整数分割すべきであることを示しています**。
のように、$n \geq 4$ のとき、$2$ を 1 つ切り出すと積は大きくなる。**これは、$4$ 以上の整数はすべて分割すべきことを意味する**。
**貪欲戦略1**:分割スキームが $\geq 4$ の因子を含む場合、それはさらに分割されるべきで。最終的な分割因子 $1$、$2$、$3$ のみを含むべきです
**貪欲戦略**:分割方法に $\geq 4$ の因子が含まれるなら、それはさらに分割べきである。最終的な分割方法に現れる因子 $1$、$2$、$3$ の 3 種類だけである
![分割による積の増加](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" }
![分割により積が大きくな](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" }
<p align="center"> 図 15-14 &nbsp; 分割による積の増加 </p>
<p align="center"> 図 15-14 &nbsp; 分割により積が大きくなる </p>
次に、どの因子が最適かを考慮します。因子 $1$、$2$、$3$ の中で、明らかに $1$ が最悪です。$1 \times (n-1) < n$ 常に成り立つため、$1$ を分割すると実際に積が減少します
次に、どの因子が最適かを考える。$1$、$2$、$3$ の 3 つの因子のうち、明らかに $1$ が最も悪い。なぜなら $1 \times (n-1) < n$ 常に成り立、$1$ を切り出すとかえって積が小さくなるからである
下の図に示すように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ です。**これは $3$ を分割する方が $2$ を分割するよりも良いことを意味します**。
のように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ が成り立つ。**これは、$2$ を切り出すより $3$ を切り出すほうが有利であることを意味す**。
**貪欲戦略2**:分割スキームには最大で2つの $2$ があるべきです。3つの $2$ は常に2つの $3$ に置き換えてより高い積を得ることができるからで
**貪欲戦略**:分割方法の中に存在してよい $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$ に置き換えるべきで
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$ を取得できます。したがって:
図のように、ループで整数を分割する必要はなく、切り捨て除算によって $3$ の数 $a$ を、剰余演算によって余り $b$ を得られる。このとき、
$$
n = 3a + b
n = 3 a + b
$$
$n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積は $1 \times (n - 1)$ であることに注意してください
なお、$n \leq 3$ の境界ケースでは、必ず $1$ を 1 つ分割する必要があり、積は $1 \times (n - 1)$ となる
=== "Python"
```python title="max_product_cutting.py"
def max_product_cutting(n: int) -> int:
"""切断の最大積:貪欲法"""
# n <= 3 の場合、1 を切り出す必要がある
"""最大切断積:貪欲法"""
# n <= 3 のときは、必ず 1 を切り出す
if n <= 3:
return 1 * (n - 1)
# 貪欲に 3 を切り出、a 3 の個数、b 余り
# 貪欲に 3 を切り出、a 3 の個数、b 余りとする
a, b = n // 3, n % 3
if b == 1:
# 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変
# 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return int(math.pow(3, a - 1)) * 2 * 2
if b == 2:
# 余りが 2 の場合、何もしない
# 余りが 2 のときは、そのままにする
return int(math.pow(3, a)) * 2
# 余りが 0 の場合、何もしない
# 余りが 0 のときは、そのままにする
return int(math.pow(3, a))
```
=== "C++"
```cpp title="max_product_cutting.cpp"
/* 最大切断:貪欲法 */
/* 最大切断:貪欲法 */
int maxProductCutting(int n) {
// n <= 3 の場合、1 を切り出す必要がある
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出す。a 3 の個数、b 余り
// 貪欲に 3 を切り出し、a 3 の個数、b 余りとする
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return (int)pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 の場合、何もしない
// 余りが 2 のときは、そのままにする
return (int)pow(3, a) * 2;
}
// 余りが 0 の場合、何もしない
// 余りが 0 のときは、そのままにする
return (int)pow(3, a);
}
```
@@ -121,24 +121,24 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積
=== "Java"
```java title="max_product_cutting.java"
/* 最大切断:貪欲法 */
/* 最大切断:貪欲法 */
int maxProductCutting(int n) {
// n <= 3 の場合、1 を切り出す必要がある
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出す。a 3 の個数、b 余り
// 貪欲に 3 を切り出し、a 3 の個数、b 余りとする
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return (int) Math.pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 の場合、何もしない
// 余りが 2 のときは、そのままにする
return (int) Math.pow(3, a) * 2;
}
// 余りが 0 の場合、何もしない
// 余りが 0 のときは、そのままにする
return (int) Math.pow(3, a);
}
```
@@ -146,78 +146,266 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積
=== "C#"
```csharp title="max_product_cutting.cs"
[class]{max_product_cutting}-[func]{MaxProductCutting}
/* 最大切断積:貪欲法 */
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);
}
```
=== "Go"
```go title="max_product_cutting.go"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
func maxProductCutting(n int) int {
// n <= 3 のときは、必ず 1 を切り出す
if n <= 3 {
return 1 * (n - 1)
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
a := n / 3
b := n % 3
if b == 1 {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return int(math.Pow(3, float64(a-1))) * 2 * 2
}
if b == 2 {
// 余りが 2 のときは、そのままにする
return int(math.Pow(3, float64(a))) * 2
}
// 余りが 0 のときは、そのままにする
return int(math.Pow(3, float64(a)))
}
```
=== "Swift"
```swift title="max_product_cutting.swift"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
func maxProductCutting(n: Int) -> Int {
// n <= 3 のときは、必ず 1 を切り出す
if n <= 3 {
return 1 * (n - 1)
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a = n / 3
let b = n % 3
if b == 1 {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return pow(3, a - 1) * 2 * 2
}
if b == 2 {
// 余りが 2 のときは、そのままにする
return pow(3, a) * 2
}
// 余りが 0 のときは、そのままにする
return pow(3, a)
}
```
=== "JS"
```javascript title="max_product_cutting.js"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
function maxProductCutting(n) {
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a = Math.floor(n / 3);
let b = n % 3;
if (b === 1) {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return Math.pow(3, a - 1) * 2 * 2;
}
if (b === 2) {
// 余りが 2 のときは、そのままにする
return Math.pow(3, a) * 2;
}
// 余りが 0 のときは、そのままにする
return Math.pow(3, a);
}
```
=== "TS"
```typescript title="max_product_cutting.ts"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
function maxProductCutting(n: number): number {
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a: number = Math.floor(n / 3);
let b: number = n % 3;
if (b === 1) {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return Math.pow(3, a - 1) * 2 * 2;
}
if (b === 2) {
// 余りが 2 のときは、そのままにする
return Math.pow(3, a) * 2;
}
// 余りが 0 のときは、そのままにする
return Math.pow(3, a);
}
```
=== "Dart"
```dart title="max_product_cutting.dart"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
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 (pow(3, a - 1) * 2 * 2).toInt();
}
if (b == 2) {
// 余りが 2 のときは、そのままにする
return (pow(3, a) * 2).toInt();
}
// 余りが 0 のときは、そのままにする
return pow(3, a).toInt();
}
```
=== "Rust"
```rust title="max_product_cutting.rs"
[class]{}-[func]{max_product_cutting}
/* 最大切断積:貪欲法 */
fn max_product_cutting(n: i32) -> i32 {
// n <= 3 のときは、必ず 1 を切り出す
if n <= 3 {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a = n / 3;
let b = n % 3;
if b == 1 {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
3_i32.pow(a as u32 - 1) * 2 * 2
} else if b == 2 {
// 余りが 2 のときは、そのままにする
3_i32.pow(a as u32) * 2
} else {
// 余りが 0 のときは、そのままにする
3_i32.pow(a as u32)
}
}
```
=== "C"
```c title="max_product_cutting.c"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
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 pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 のときは、そのままにする
return pow(3, a) * 2;
}
// 余りが 0 のときは、そのままにする
return pow(3, a);
}
```
=== "Kotlin"
```kotlin title="max_product_cutting.kt"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
fun maxProductCutting(n: Int): Int {
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1)
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
val a = n / 3
val b = n % 3
if (b == 1) {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return 3.0.pow((a - 1)).toInt() * 2 * 2
}
if (b == 2) {
// 余りが 2 のときは、そのままにする
return 3.0.pow(a).toInt() * 2 * 2
}
// 余りが 0 のときは、そのままにする
return 3.0.pow(a).toInt()
}
```
=== "Ruby"
```ruby title="max_product_cutting.rb"
[class]{}-[func]{max_product_cutting}
### 最大分割積:貪欲法 ###
def max_product_cutting(n)
# n <= 3 のときは、必ず 1 を切り出す
return 1 * (n - 1) if n <= 3
# 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
a, b = n / 3, n % 3
# 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return (3.pow(a - 1) * 2 * 2).to_i if b == 1
# 余りが 2 のときは、そのままにする
return (3.pow(a) * 2).to_i if b == 2
# 余りが 0 のときは、そのままにする
3.pow(a).to_i
end
```
![切断後の最大積の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" }
??? pythontutor "コードの可視化"
<p align="center"> 図 15-16 &nbsp; 切断後の最大積の計算方法 </p>
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E6%96%AD%E7%A9%8D%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20n%20%3C%3D%203%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E5%BF%85%E3%81%9A%201%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%99%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20%2A%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E3%81%AB%203%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%97%E3%80%81a%20%E3%82%92%203%20%E3%81%AE%E5%80%8B%E6%95%B0%E3%80%81b%20%E3%82%92%E4%BD%99%E3%82%8A%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20a%2C%20b%20%3D%20n%20%2F%2F%203%2C%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%201%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%811%20%2A%203%20%E3%82%92%202%20%2A%202%20%E3%81%AB%E5%A4%89%E3%81%88%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%20-%201%29%29%20%2A%202%20%2A%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%202%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%20%2A%202%0A%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%200%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%86%E5%89%B2%E7%A9%8D%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E6%96%AD%E7%A9%8D%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20n%20%3C%3D%203%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E5%BF%85%E3%81%9A%201%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%99%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20%2A%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E3%81%AB%203%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%97%E3%80%81a%20%E3%82%92%203%20%E3%81%AE%E5%80%8B%E6%95%B0%E3%80%81b%20%E3%82%92%E4%BD%99%E3%82%8A%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20a%2C%20b%20%3D%20n%20%2F%2F%203%2C%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%201%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%811%20%2A%203%20%E3%82%92%202%20%2A%202%20%E3%81%AB%E5%A4%89%E3%81%88%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%20-%201%29%29%20%2A%202%20%2A%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%202%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%20%2A%202%0A%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%200%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%86%E5%89%B2%E7%A9%8D%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
**時間計算量はプログラミング言語のべき乗演算の実装に依存します**。Pythonでは、よく使用されるべき乗計算関数は3種類あります:
![最大積分割の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" }
- 演算子 `**` と関数 `pow()` の両方の時間計算量は $O(\log⁡ a)$ です。
- `math.pow()` 関数は内部でC言語ライブラリの `pow()` 関数を呼び出し、浮動小数点べき乗を実行し、時間計算量は $O(1)$ です。
<p align="center"> 図 15-16 &nbsp; 最大積分割の計算方法 </p>
変数 $a$ と $b$ は一定サイズの追加スペースを使用するため、**空間計算量は $O(1)$** です
**時間計算量は、プログラミング言語におけるべき乗演算の実装方法に依存する**。Python を例に取ると、よく使われるべき乗計算関数は 3 種類ある
- 演算子 `**` と関数 `pow()` の時間計算量はいずれも $O(\log⁡ a)$ である。
- 関数 `math.pow()` は内部で C 言語ライブラリの `pow()` 関数を呼び出し、浮動小数点のべき乗を実行するため、時間計算量は $O(1)$ である。
変数 $a$ と $b$ が使う追加領域は定数サイズであり、**したがって空間計算量は $O(1)$ である**。
### 3. &nbsp; 正しさの証明
背理法を使用し、$n \geq 3$ のケースのみを分析します
背理法を用い、$n \geq 4$ の場合のみを考える
1. **すべての因子 $\leq 3$**:最適分割スキームが因子 $x \geq 4$ を含むと仮定すると、それを確実に $2(x-2)$ にさらに分割でき、より大きな積を得られます。これは仮定矛盾します。
2. **分割スキームに $1$ 含まれない**:最適分割スキームが因子 $1$ を含むと仮定すると、それを確実に別の因子と結合してより大き積を得られます。これは仮定矛盾します。
3. **分割スキームには最大で2つの $2$ が含まれる**:最適分割スキームが3つの $2$ を含むと仮定すると、それらを確実に2つの $3$ に置き換えて、より高い積を達成できます。これは仮定矛盾します。
1. **すべての因子 $\leq 3$** :最適分割方法に $\geq 4$ の因子 $x$ が存在すると仮定すると、それは必ずさらに $2(x-2)$ に分割でき、より大きい(または等しい)積が得られ。これは仮定矛盾す
2. **分割方法に $1$ 含まれない** :最適分割方法に因子 $1$ が 1 つ存在すると仮定すると、それは必ず別の因子に併合でき、より大き積を得られ。これは仮定矛盾す
3. **分割方法に含まれる $2$ は高々 2 つ** :最適分割方法に 3 つの $2$ が含まれると仮定すると、それは必ず 2 つの $3$ に置き換えられ、積はより大きくなる。これは仮定矛盾す
+12 -10
View File
@@ -4,13 +4,15 @@ comments: true
# 15.5 &nbsp; まとめ
- 貪欲アルゴリズムは最適化問題を解決するためによく使用され、原理は各決定段階で局所的に最適な決定を行い、グローバルに最適な解を達成することです。
- 貪欲アルゴリズムは貪欲な選択を次々と反復的に行い、各ラウンドで問題をより小さな部分問題に変換し、問題が解決されるまで続けます。
- 貪欲アルゴリズムは実装が簡単なだけでなく、問題解決効率も高いです。動的プログラミングと比較して、貪欲アルゴリズムは一般的により低い時間計算量を持ちます
- コイン交換問題において、貪欲アルゴリズムは特定のコインの組み合わせに対して最適解を保証できますが、他の組み合わせでは貪欲アルゴリズムが非常に悪い解を見つける可能性があります
- 貪欲アルゴリズム解法に適した問題は2つの主要な性質を持ちます:貪欲選択性と最適部分構造。貪欲選択性は貪欲戦略の効果を表します
- 一部の複雑な問題では、貪欲選択性を証明することは簡単ではありません。逆に、無効性を証明することはしばしばより容易で、コイン交換問題などがその例です
- 貪欲問題の解決は主に3つのステップから構成されます:問題分析、貪欲戦略の決定、正しさの証明。このうち、貪欲戦略の決定が重要なステップであり、正しさの証明がしばしば挑戦となります
- 分数ナップサック問題は0-1ナップサック問題に基づいてアイテムの一部の選択を可能にし、したがって貪欲アルゴリズムを使用して解決できます。貪欲戦略の正しさは背理法によって証明できます
- 最大容量問題は全探索法で解決でき、時間計算量は $O(n^2)$ です。貪欲戦略を設計することで、各ラウンドで短い板を内側に移動し、時間計算量を $O(n)$ に最適化します
- 切断後の最大積問題において、2つの貪欲戦略を導出します:$\geq 4$ の整数は継続的に切断されるべきで、最適な切断因子は $3$ です。コードにはべき乗演算が含まれ、時間計算量はべき乗演算の実装方法に依存し、一般的に $O(1)$ または $O(\log n)$ です
### 1. &nbsp; 重要な振り返り
- 貪欲法は通常、最適化問題を解くために用いられ、その原理は各意思決定段階で局所最適な決定を行い、全体最適解を得ることを目指すというものである
- 貪欲法は反復的に次々と貪欲な選択を行い、各ラウンドで問題をより小さな部分問題へと変換し、最終的に問題を解決する
- 貪欲法は実装が簡単であるだけでなく、問題を解く効率も高い。動的計画法と比べると、貪欲法の時間計算量は通常より低い
- 硬貨両替問題では、ある種の硬貨の組み合わせに対しては貪欲法で最適解を保証できるが、別の組み合わせではそうではなく、非常に悪い解を見つけてしまう可能性がある
- 貪欲法による解法に適した問題は、貪欲選択性と最適部分構造という 2 つの性質を備えている。貪欲選択性は、貪欲戦略の有効性を表している
- 一部の複雑な問題では、貪欲選択性を証明するのは容易ではない。相対的には、反例による否定のほうが簡単であり、硬貨両替問題がその一例である
- 貪欲法の問題を解く流れは主に 3 段階に分かれる。すなわち、問題分析、貪欲戦略の決定、正しさの証明である。このうち、貪欲戦略の決定が中核であり、正しさの証明はしばしば難所となる
- 分数ナップサック問題は 0-1 ナップサックを基に、品物の一部を選ぶことを許しているため、貪欲法で解くことができる。貪欲戦略の正しさは背理法で証明できる
- 最大容量問題は全探索で解くことができ、時間計算量は $O(n^2)$ である。貪欲戦略を設計し、各ラウンドで短い板を内側へ動かすことで、時間計算量を $O(n)$ に最適化できる。
- 最大分割積問題では、2 つの貪欲戦略を順に導いた。すなわち、$\geq 4$ の整数はすべてさらに分割すべきであり、最適な分割因子は $3$ である。コードにはべき乗演算が含まれており、時間計算量はその実装方法に依存し、通常は $O(1)$ または $O(\log n)$ である。
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -3,19 +3,19 @@ comments: true
icon: material/table-search
---
# 第 6 章 &nbsp; ハッシュ
# 第 6 章 &nbsp; ハッシュテーブル
![ハッシュ](../assets/covers/chapter_hashing.jpg){ class="cover-image" }
![ハッシュテーブル](../assets/covers/chapter_hashing.jpg){ class="cover-image" }
!!! abstract
コンピューティングの世界において、ハッシュ表は賢い司書のようなものです。
インデックス番号の計算方法を理解し、目的の本を迅速に取得することを可能にします。
コンピュータの世界では、ハッシュテーブルは聡明な図書館員のような存在です。
彼は請求記号の計算方法を知っており、そのため目的の本を素早く見つけられます。
## 章の内容
- [6.1 &nbsp; ハッシュ](hash_map.md)
- [6.1 &nbsp; ハッシュテーブル](hash_map.md)
- [6.2 &nbsp; ハッシュ衝突](hash_collision.md)
- [6.3 &nbsp; ハッシュアルゴリズム](hash_algorithm.md)
- [6.4 &nbsp; まとめ](summary.md)
+32 -28
View File
@@ -4,48 +4,52 @@ comments: true
# 6.4 &nbsp; まとめ
### 1. &nbsp; 重要ポイント
### 1. &nbsp; 重要ポイントの振り返り
- 入力`key`が与えられると、ハッシュ表は$O(1)$時間で対応する`value`を取得でき、非常に効率的です
- 一般的なハッシュの操作には、クエリ、キー値ペアの追加、キーペアの削除、ハッシュ表の走査があります
- ハッシュ関数は`key`を配列インデックスにマッピングし、対応するバケットにアクセスして`value`を取得できるようにします。
- 2つの異なるキーがハッシュ化後に同じ配列インデックスになる場合があり、誤ったクエリ結果につながります。この現象ハッシュ衝突として知られています
- ハッシュの容量が大きいほど、ハッシュ衝突の確率は低くなります。したがって、ハッシュ表のリサイズはハッシュ衝突を緩和できます。配列のリサイズと同様に、ハッシュ表のリサイズはコストが高いです
- 要素数をバケット数で割った負荷率は、ハッシュ衝突の深刻を反映し、しばしばハッシュ表リサイズのトリガー条件として使用されます
- 連鎖法は各要素を連結リストに変換し、衝突するすべての要素を同じリストに格納することでハッシュ衝突に対処します。ただし、過度に長いリストはクエリ効率低下させる可能性があり、リストを赤黒木に変換することで改善できます
- オープンアドレス法は複数回のプローブを通してハッシュ衝突を処理します。線形プローブは固定ステップサイズを使用しますが、要素を削除できず、クラスタリングを起こしやすい傾向があります。多重ハッシュはプローブに複数のハッシュ関数を使用し、線形プローブと比較してクラスタリングを減らしますが、計算オーバーヘッドが増加します
- 異なるプログラミング言語はさまざまなハッシュ表実装採用しています。例えば、Java`HashMap`は連鎖を使用し、Python`dict`はオープンアドレス法を採用しています
- ハッシュ表では、決定性、高効率、均等分散を持つハッシュアルゴリズムが望まれます。暗号では、ハッシュアルゴリズムは衝突性と雪崩効果も持つべきで
- ハッシュアルゴリズムは通常、ハッシュ値の均等分散を保証しハッシュ衝突を減らすために、大きな素数を剰余として使用します
- 一般的なハッシュアルゴリズムにはMD5、SHA-1、SHA-2、SHA-3があります。MD5はファイル整合性チェックによく使用され、SHA-2は安全なアプリケーションとプロトコルで一般的に使用されます
- プログラミング言語は通常、ハッシュ表のバケットインデックスを計算するために、データ型に対して組み込みのハッシュアルゴリズムを提供します。一般的に、不変オブジェクトのみがハッシュ可能です
- `key` を入力すると、ハッシュテーブルは $O(1)$ 時間で `value` を検索でき、非常に効率である
- 一般的なハッシュテーブルの操作には、検索、キーと値のペアの追加、キーと値のペアの削除、ハッシュテーブルの走査などがある
- ハッシュ関数は `key` を配列インデックスに写像し、それによって対応するバケットにアクセスして `value` を取得す
- 異なる 2 つの `key` が、ハッシュ関数を通した後に同じ配列インデックスになることがあり、検索結果の誤りを引き起こす。この現象ハッシュ衝突と呼ぶ
- ハッシュテーブルの容量が大きいほど、ハッシュ衝突の確率は低くなる。そのため、ハッシュテーブルを拡張することでハッシュ衝突を緩和でき。配列の拡張と同様に、ハッシュテーブルの拡張操作のコストは大きい
- 負荷率は、ハッシュテーブル内の要素数をバケット数で割ったものと定義され、ハッシュ衝突の深刻を反映する。ハッシュテーブル拡張を発動する条件としてよく用いられる
- 連鎖方式では、単一要素を連結リストに変換し、衝突したすべての要素を同じ連結リストに格納する。しかし、連結リストが長すぎると検索効率低下するため、さらに連結リストを赤黒木に変換して効率を高めることができる
- オープンアドレス法は複数回の探索によってハッシュ衝突を処理する。線形探索は固定ステップ幅を用いるが、要素を削除できず、クラスタリングが発生しやすいという欠点がある。二重ハッシュは複数のハッシュ関数を用いて探索するため、線形探索に比べてクラスタリングが起きにくいが、複数のハッシュ関数によって計算量が増える
- プログラミング言語ごとに、異なるハッシュテーブル実装採用されている。たとえば、Java`HashMap` は連鎖方式を使用し、Python`Dict` はオープンアドレス法を採用してい
- ハッシュテーブルでは、ハッシュアルゴリズムに決定性、高効率、均一分布という特徴が求められる。暗号では、ハッシュアルゴリズムはさらに耐衝突性とアバランシェ効果も備えるべきである
- ハッシュアルゴリズムは通常、大きな素数を法として用い、ハッシュ値の均一分布を最大限に保証しハッシュ衝突を減らす。
- 一般的なハッシュアルゴリズムには MD5、SHA-1、SHA-2、SHA-3 などがある。MD5 はファイル完全性の検証によく用いられ、SHA-2 はセキュリティ用途やプロトコルでよく用いられる
- プログラミング言語は通常、データ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックスの計算に用いる。通常、ハッシュ可能なのは不変オブジェクトだけである
### 2. &nbsp; Q & A
**Q**: ハッシュの時間計算量が$O(n)$に悪化するのはいつですか?
**Q**ハッシュテーブルの時間計算量が $O(n)$ になるのはどのような場合ですか?
ハッシュ表の時間計算量は、ハッシュ衝突が深刻な場合に$O(n)$に悪化する可能性があります。ハッシュ関数が適切に設計され、容量が適切に設定され、衝突が均等に分散されている場合、時間計算量は$O(1)$です。プログラミング言語組み込みハッシュ表を使用する場合、通常は時間計算量を$O(1)$と考えます。
ハッシュ衝突が深刻な場合、ハッシュテーブルの時間計算量は $O(n)$ に劣化する。ハッシュ関数の設計が適切で、容量設定が合理的で、衝突が比較的均等な場合、時間計算量は $O(1)$ である。プログラミング言語組み込みハッシュテーブルを使うとき、通常は時間計算量を $O(1)$ とみなす。
**Q**: なぜハッシュ関数$f(x) = x$を使用しないのですか?これなら衝突を排除できます
**Q**なぜハッシュ関数 $f(x) = x$ を使ないのですか? そうすれば衝突は起きません
ハッシュ関数$f(x) = x$では、各要素一意のバケットインデックスに対応し、これは配列と同等です。しかし、入力空間は通常出力空間(配列長)よりはるかに大きいため、ハッシュ関数の最後のステップは配列長の剰余を取ることがよくあります。言い換えると、ハッシュの目は、$O(1)$のクエリ効率を提供しながら、より大きな状態空間をより小さなものにマッピングすることで
$f(x) = x$ というハッシュ関数では、各要素一意のバケットインデックスに対応し、これは配列と等価である。しかし、入力空間は通常出力空間(配列長)よりはるかに大きいため、ハッシュ関数の最後のステップはたいてい配列長の剰余になる。言い換えると、ハッシュテーブルの目は、大きな状態空間をより小さな空間に写像し、$O(1)$ の検索効率を提供することである
**Q**: ハッシュ表がこれらの構造を使って実装されているにもかかわらず、なぜ配列、連結リスト、二分木より効率になるのですか?
**Q**ハッシュテーブルの基礎実装は配列、連結リスト、二分木なのに、なぜそれらより効率になり得るのですか?
まず、ハッシュは時間効率が高いですが、空間効率は低いです。ハッシュ表のメモリの大部分未使用のままです
まず、ハッシュテーブルは時間効率が高くなる一方で、空間効率は低くなる。ハッシュテーブルには、かなりの部分未使用のメモリが存在する
次に、ハッシュ表は特定のユースケースでのみ時間効率が高いです。配列や連結リストを使用して同じ時間計算量で機能を実装できる場合、通常はハッシュ表を使用するよりも高速です。これは、ハッシュ関数の計算がオーバーヘッドを発生させ、時間計算量の定数因子が大きくなるためです
次に、時間効率が高くなるのは特定の利用場面に限られる。ある機能が同じ時間計算量で配列や連結リストによって実装できるなら、通常はハッシュテーブルより速い。これは、ハッシュ関数の計算にコストがかかり、時間計算量の定数項がより大きいからである
最後に、ハッシュの時間計算量は化する可能性があります。例えば連鎖では、連結リストや赤黒木で検索操作を実行し、これは依然として$O(n)$時間に化するリスクがあります
最後に、ハッシュテーブルの時間計算量は化する可能性がある。たとえば連鎖方式では、連結リストや赤黒木で検索操作を行うため、なお $O(n)$ 時間に化するリスクがあ
**Q**: 多重ハッシュにも要素を直接削除できないという欠陥がありますか?削除としてマークされた空間は再利用できますか?
**Q**:二重ハッシュにも要素を直接削除できない欠点がありますか? 削除済みとマークした領域は再利用できますか?
重ハッシュはオープンアドレス法の一形態であり、すべてのオープンアドレス法は要素を直接削除できないという欠点があります。要素を削除済みとしてマークする必要があります。マークされた空間は再利用できます。ハッシュ表に新しい要素を挿入する際、ハッシュ関数削除済みとしてマークされた位置を指している場合、その位置は新しい要素によって使用できます。これにより、ハッシュ表のプローブシーケンスを維持しながら、空間の効率的な使用が保証されます
重ハッシュはオープンアドレス法の一であり、オープンアドレス法はいずれも要素を直接削除できないという欠点があるため、削除のマーク付けが必要になる。削除済みとマークされた領域は再利用できる。新しい要素をハッシュテーブルに挿入し、ハッシュ関数によって削除済みとマークされた位置を見つけた場合、その位置は新しい要素に使用できる。こうすることで、ハッシュテーブルの探索系列を変えずに保ちつつ、空間利用率も確保できる
**Q**: なぜ線形プローブの検索プロセス中にハッシュ衝突が発生するのですか?
**Q**なぜ線形探索では、要素を探すときにハッシュ衝突が発生するのですか?
検索プロセス中、ハッシュ関数対応するバケットとキー値ペアを指します。`key`が一致しない場合、ハッシュ衝突を示します。したがって、線形プローブは正しいキーペア見つるか検索が失敗するまで、事前に決められたステップサイズで下方向に検索します
探索時には、ハッシュ関数対応するバケットとキーと値のペアを見つけ、`key` が一致しないことが分かると、それはハッシュ衝突を意味する。そのため、線形探索法では事前に設定したステップ幅に従って順に探索し、正しいキーと値のペア見つるか、見つからずに終了するまで続ける
**Q**: なぜハッシュ表のリサイズがハッシュ衝突を緩和できるのですか?
**Q**なぜハッシュテーブルの拡張でハッシュ衝突を緩和できるのですか?
ハッシュ関数の最後のステップは、出力を配列インデックス範囲内に保つために、配列長$n$の剰余を取ることがよくあります。リサイズ時、配列長$n$が変化し、キーに対応するインデックスも変化する可能性があります。以前に同じバケットにマッピングされていたキーが、リサイズ後に複数のバケットに分散される可能性があり、それによってハッシュ衝突が緩和されます
ハッシュ関数の最後のステップは、たいてい配列長 $n$ での剰余を取り、出力を配列インデックス範囲内に収めることである。拡張後は配列長 $n$ が変化し、`key` に対応するインデックスも変化する可能性がある。もともと同じバケットに入っていた複数の `key` は、拡張後に複数のバケットに割り当てられる可能性があり、それによってハッシュ衝突が緩和され
**Q**:高効率な読み書きのためなら、配列を直接使えばよいのではないですか?
データの `key` が連続した小範囲の整数であれば、配列を直接使えばよく、単純で高効率である。しかし `key` が別の型(たとえば文字列)の場合は、ハッシュ関数を用いて `key` を配列インデックスに写像し、さらにバケット配列を通じて要素を格納する必要がある。このような構造がハッシュテーブルである。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -9,13 +9,13 @@ icon: material/family-tree
!!! abstract
ヒープは山とその険しい峰のように、層をなして起伏し、それぞれが独特の形を持っています
各山の頂は散らばった高さで上下しますが、最も高いものが常に最初に注目を集めます
ヒープは連なる山々の峰のように、幾重にも重なり、さまざまな形をしている
いくつもの山の高さはまちまちだが、最も高い峰がいつも最初に目に入る
## 章の内容
- [8.1 &nbsp; ヒープ](heap.md)
- [8.2 &nbsp; ヒープ構築操作](build_heap.md)
- [8.2 &nbsp; ヒープ構築](build_heap.md)
- [8.3 &nbsp; Top-k 問題](top_k.md)
- [8.4 &nbsp; まとめ](summary.md)
+10 -10
View File
@@ -4,18 +4,18 @@ comments: true
# 8.4 &nbsp; まとめ
### 1. &nbsp; 重要な復習
### 1. &nbsp; 重要なポイントの振り返り
- ヒープは完二分木で、その構築性質に基づいて最大ヒープまたは最小ヒープに分類できます。最大ヒープの先頭要素は最大で、最小ヒープの先頭要素は最小です
- 優先度キューは、デキューの優先度を持つキューとして定義され、通常ヒープを使用して実装されます
- ヒープの一般的な操作とそれに対応する時間計算量には以下があります:ヒープへの要素挿入$O(\log n)$、ヒープからの先頭要素削除$O(\log n)$、ヒープの先頭要素へのアクセス$O(1)$。
-二分木は配列で表現するのに適しているため、ヒープは一般的に配列を使用して格納されます
- ヒープ化操作はヒープの性質を維持するために使用され、ヒープの挿入操作と削除操作の両方で使用されます
- $n$個の要素入力して与えられた場合のヒープ構築時間計算量は$O(n)$最適化でき、これは非常に効率的で
- Top-kは古典的なアルゴリズム問題で、ヒープデータ構造を使用して効率的に解でき、時間計算量は$O(n \log k)$です
- ヒープは完二分木であり、条件の違いによって最大ヒープ最小ヒープに分けられる。最大(最小)ヒープの根の要素は最大値(最小値)である
- 優先度付きキューは、取り出し時に優先度が考慮されるキューであり、通常ヒープを用いて実装され
- ヒープの代表的な操作とそれに対応する時間計算量には要素挿入 $O(\log n)$、根の要素削除 $O(\log n)$、根の要素へのアクセス $O(1)$ などがある
-二分木は配列で表現するのに非常に適しているため、通常は配列を使ってヒープを格納する
- ヒープ化操作はヒープの性質を保つために用いられ、挿入操作と削除操作の両方で使用され
- $n$ 個の要素入力してヒープ構築する時間計算量は $O(n)$ まで最適化でき、非常に効率的である
- Top-k は古典的なアルゴリズム問題であり、ヒープ構造を用いることで効率的に解くことができ、時間計算量は $O(n \log k)$ である
### 2. &nbsp; Q & A
**Q**: データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか?
**Q**データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか?
この2つは、どちらも「ヒープ」と呼ばれますが、同じ概念ではありません。コンピュータシステムメモリヒープは動的メモリ割り当ての一部で、プログラム実行中にデータを格納するために使用できます。プログラムは、オブジェクトや配列などの複雑な構造を格納するために、一定量のヒープメモリを要求できます。割り当てられたデータが不要になったときは、メモリリークを防ぐためにプログラムがこのメモリを解放する必要があります。スタックメモリと比較して、ヒープメモリの管理と使用にはより多くの注意が必要で、不適切な使用はメモリリークやダングリングポインタにつながる可能性があります
両者は同じ概念ではなく、たまたまどちらも「ヒープ」と呼ばれているだけである。コンピュータシステムメモリにおけるヒープは動的メモリ割り当ての一部であり、プログラム実行時にこれを使ってデータを格納できる。プログラムは一定量のヒープメモリを要求し、オブジェクトや配列などの複雑な構造を保存できる。これらのデータが不要になったときは、メモリリークを防ぐためにのメモリを解放する必要があ。スタックメモリと比べると、ヒープメモリの管理と使用にはより慎重さが求められ、不適切に扱うとメモリリークやダングリングポインタなどの問題を引き起こす可能性があ
+302 -59
View File
@@ -2,49 +2,49 @@
comments: true
---
# 8.3 &nbsp; Top-k問題
# 8.3 &nbsp; Top-k 問題
!!! question
長さ$n$の順序付けられていない配列`nums`が与えられたとき、配列内最大$k$個の要素を返してください。
長さ $n$ の未整列配列 `nums` が与えられたとき、配列内最大$k$ 個の要素を返してください。
この問題について、まず2つの直接的な解法を紹介し、次により効率的なヒープベースの方法を説明します。
この問題について、まずは発想が比較的直接的な 2 つの解法を紹介し、その後でより効率の高いヒープ解法を紹介します。
## 8.3.1 &nbsp; 方法1:反復選択
## 8.3.1 &nbsp; 方法一:走査による選択
図に示すように$k$回の反復を実行し、各回で$1$番目、$2$番目、$\dots$、$k$番目に大きい要素を抽出できます。時間計算量は$O(nk)$です。
以下の図に示すように $k$ 回の走査を行い、各ラウンドでそれぞれ第 $1$、$2$、$\dots$、$k$ 位の要素を取り出すことができます。時間計算量は $O(nk)$ です。
この方法は$k \ll n$の場合にのみ適していま。$k$が$n$に近い場合、時間計算量は$O(n^2)$に近づき、非常に時間がかかります。
この方法は $k \ll n$ の場合にしか適していません。$k$ が $n$ にかなり近いと、時間計算量は $O(n^2)$ に近づき、非常に時間がかかるためです。
![最大k個の要素を反復的に見つける](top_k.assets/top_k_traversal.png){ class="animation-figure" }
![走査によって最大の k 個の要素を探す](top_k.assets/top_k_traversal.png){ class="animation-figure" }
<p align="center"> 図 8-6 &nbsp; 最大k個の要素を反復的に見つける </p>
<p align="center"> 図 8-6 &nbsp; 走査によって最大の k 個の要素を探す </p>
!!! tip
$k = n$の場合、完全に順序付けられたシーケンスを得ることができ、こは「選択ソート」アルゴリズムと同等です。
$k = n$ のとき、完全な昇順列を得ることができ、この場合は「選択ソート」アルゴリズムと等価になります。
## 8.3.2 &nbsp; 方法2:ソート
## 8.3.2 &nbsp; 方法:ソート
図に示すように、まず配列`nums`をソートし、次に最後の$k$個の要素を返すことができます。時間計算量は$O(n \log n)$です。
以下の図に示すように、まず配列 `nums` をソートし、その後で右端の $k$ 個の要素を返すことができます。時間計算量は $O(n \log n)$ です。
明らかに、この方法はタスクを「やりすぎ」ています。最大$k$個の要素を見つけるだけでよく、他の要素をソートする必要はありません
明らかに、この方法は必要以上の処理を行っています。なぜなら、必要なのは最大の $k$ 個の要素を見つけることだけであり、他の要素をソートする必要はないからです
![ソートによる最大k個の要素の発見](top_k.assets/top_k_sorting.png){ class="animation-figure" }
![ソートによって最大の k 個の要素を探す](top_k.assets/top_k_sorting.png){ class="animation-figure" }
<p align="center"> 図 8-7 &nbsp; ソートによる最大k個の要素の発見 </p>
<p align="center"> 図 8-7 &nbsp; ソートによって最大の k 個の要素を探す </p>
## 8.3.3 &nbsp; 方法3:ヒープ
## 8.3.3 &nbsp; 方法:ヒープ
以下のプロセスに示すように、ヒープに基づいてTop-k問題をより効率的に解決できます。
ヒープを用いることで、Top-k 問題をより効率的に解くことができます。手順は以下の図のとおりです。
1. 最小ヒープを初期化します。先頭要素が最小になります。
2. まず配列の最初の$k$個の要素をヒープ挿入します。
3. $k + 1$番目の要素から開始し、現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープ挿入します。
4. 走査完了した後、ヒープには最大$k$個の要素が含まれています。
1. 最小ヒープを初期化し、そのヒープ頂点の要素が最小となるようにします。
2. まず配列の先頭 $k$ 個の要素を順にヒープ挿入します。
3. $k + 1$ 番目の要素から開始し、現在の要素がヒープ頂点の要素より大きければ、ヒープ頂点の要素を取り出し、現在の要素をヒープ挿入します。
4. 走査完了した後、ヒープに保持されているのが最大の $k$ 個の要素す。
=== "<1>"
![ヒープに基づく最大k個の要素の発見](top_k.assets/top_k_heap_step1.png){ class="animation-figure" }
![ヒープに基づいて最大の 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" }
@@ -70,23 +70,23 @@ comments: true
=== "<9>"
![top_k_heap_step9](top_k.assets/top_k_heap_step9.png){ class="animation-figure" }
<p align="center"> 図 8-8 &nbsp; ヒープに基づく最大k個の要素の発見 </p>
<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個の要素を見つける"""
"""ヒープに基づいて配列の最大の k 個の要素を探す"""
# 最小ヒープを初期化
heap = []
# 配列の最初のk個の要素をヒープに入力
# 配列の先頭 k 個の要素をヒープに追加
for i in range(k):
heapq.heappush(heap, nums[i])
# k+1番目の要素から、ヒープの長さをkに保つ
# k+1 番目の要素から開始し、ヒープ長を k に保つ
for i in range(k, len(nums)):
# 現在の要素がヒープ先頭要素より大きい場合、ヒープ先頭要素を削除し、現在の要素をヒープに入力
# 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if nums[i] > heap[0]:
heapq.heappop(heap)
heapq.heappush(heap, nums[i])
@@ -96,17 +96,17 @@ comments: true
=== "C++"
```cpp title="top_k.cpp"
/* ヒープを使用して配列の最大k個の要素を見つける */
/* ヒープに基づいて配列の最大の k 個の要素を探す */
priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {
// 最小ヒープを初期化
priority_queue<int, vector<int>, greater<int>> heap;
// 配列の最初のk個の要素をヒープに入力
// 配列の先頭 k 個の要素をヒープに追加
for (int i = 0; i < k; i++) {
heap.push(nums[i]);
}
// k+1番目の要素から、ヒープの長さをkに保つ
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for (int i = k; i < nums.size(); i++) {
// 現在の要素がヒープ先頭要素より大きい場合、ヒープ先頭要素を削除し、現在の要素をヒープに入力
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > heap.top()) {
heap.pop();
heap.push(nums[i]);
@@ -119,17 +119,17 @@ comments: true
=== "Java"
```java title="top_k.java"
/* ヒープを使用して配列の最大 k 個の要素を検索 */
/* ヒープに基づいて配列の最大 k 個の要素を探す */
Queue<Integer> topKHeap(int[] nums, int k) {
// 最小ヒープを初期化
Queue<Integer> heap = new PriorityQueue<Integer>();
// 配列の最初の k 個の要素をヒープに入力
// 配列の先頭 k 個の要素をヒープに追加
for (int i = 0; i < k; i++) {
heap.offer(nums[i]);
}
// k+1 番目の要素から、ヒープの長さを k に保つ
// k+1 番目の要素から開始し、ヒープを k に保つ
for (int i = k; i < nums.length; i++) {
// 現在の要素がヒープ先頭要素より大きい場合、ヒープ先頭要素を削除し、現在の要素をヒープに入力
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > heap.peek()) {
heap.poll();
heap.offer(nums[i]);
@@ -142,87 +142,330 @@ comments: true
=== "C#"
```csharp title="top_k.cs"
[class]{top_k}-[func]{TopKHeap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
PriorityQueue<int, int> TopKHeap(int[] nums, int k) {
// 最小ヒープを初期化
PriorityQueue<int, int> heap = new();
// 配列の先頭 k 個の要素をヒープに追加
for (int i = 0; i < k; i++) {
heap.Enqueue(nums[i], nums[i]);
}
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for (int i = k; i < nums.Length; i++) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > heap.Peek()) {
heap.Dequeue();
heap.Enqueue(nums[i], nums[i]);
}
}
return heap;
}
```
=== "Go"
```go title="top_k.go"
[class]{}-[func]{topKHeap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
func topKHeap(nums []int, k int) *minHeap {
// 最小ヒープを初期化
h := &minHeap{}
heap.Init(h)
// 配列の先頭 k 個の要素をヒープに追加
for i := 0; i < k; i++ {
heap.Push(h, nums[i])
}
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for i := k; i < len(nums); i++ {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if nums[i] > h.Top().(int) {
heap.Pop(h)
heap.Push(h, nums[i])
}
}
return h
}
```
=== "Swift"
```swift title="top_k.swift"
[class]{}-[func]{topKHeap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
func topKHeap(nums: [Int], k: Int) -> [Int] {
// 最小ヒープを初期化し、先頭 k 個の要素でヒープを構築する
var heap = Heap(nums.prefix(k))
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for i in nums.indices.dropFirst(k) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if nums[i] > heap.min()! {
_ = heap.removeMin()
heap.insert(nums[i])
}
}
return heap.unordered
}
```
=== "JS"
```javascript title="top_k.js"
[class]{}-[func]{pushMinHeap}
/* 要素をヒープに追加 */
function pushMinHeap(maxHeap, val) {
// 要素を反転する
maxHeap.push(-val);
}
[class]{}-[func]{popMinHeap}
/* 要素をヒープから取り出す */
function popMinHeap(maxHeap) {
// 要素を反転する
return -maxHeap.pop();
}
[class]{}-[func]{peekMinHeap}
/* ヒープ先頭要素にアクセス */
function peekMinHeap(maxHeap) {
// 要素を反転する
return -maxHeap.peek();
}
[class]{}-[func]{getMinHeap}
/* ヒープから要素を取り出す */
function getMinHeap(maxHeap) {
// 要素を反転する
return maxHeap.getMaxHeap().map((num) => -num);
}
[class]{}-[func]{topKHeap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
function topKHeap(nums, k) {
// 最小ヒープを初期化する
// 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする
const maxHeap = new MaxHeap([]);
// 配列の先頭 k 個の要素をヒープに追加
for (let i = 0; i < k; i++) {
pushMinHeap(maxHeap, nums[i]);
}
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for (let i = k; i < nums.length; i++) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > peekMinHeap(maxHeap)) {
popMinHeap(maxHeap);
pushMinHeap(maxHeap, nums[i]);
}
}
// ヒープ内の要素を返す
return getMinHeap(maxHeap);
}
```
=== "TS"
```typescript title="top_k.ts"
[class]{}-[func]{pushMinHeap}
/* 要素をヒープに追加 */
function pushMinHeap(maxHeap: MaxHeap, val: number): void {
// 要素を反転する
maxHeap.push(-val);
}
[class]{}-[func]{popMinHeap}
/* 要素をヒープから取り出す */
function popMinHeap(maxHeap: MaxHeap): number {
// 要素を反転する
return -maxHeap.pop();
}
[class]{}-[func]{peekMinHeap}
/* ヒープ先頭要素にアクセス */
function peekMinHeap(maxHeap: MaxHeap): number {
// 要素を反転する
return -maxHeap.peek();
}
[class]{}-[func]{getMinHeap}
/* ヒープから要素を取り出す */
function getMinHeap(maxHeap: MaxHeap): number[] {
// 要素を反転する
return maxHeap.getMaxHeap().map((num: number) => -num);
}
[class]{}-[func]{topKHeap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
function topKHeap(nums: number[], k: number): number[] {
// 最小ヒープを初期化する
// 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする
const maxHeap = new MaxHeap([]);
// 配列の先頭 k 個の要素をヒープに追加
for (let i = 0; i < k; i++) {
pushMinHeap(maxHeap, nums[i]);
}
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for (let i = k; i < nums.length; i++) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > peekMinHeap(maxHeap)) {
popMinHeap(maxHeap);
pushMinHeap(maxHeap, nums[i]);
}
}
// ヒープ内の要素を返す
return getMinHeap(maxHeap);
}
```
=== "Dart"
```dart title="top_k.dart"
[class]{}-[func]{topKHeap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
MinHeap topKHeap(List<int> nums, int k) {
// 最小ヒープを初期化し、配列の先頭 k 個の要素をヒープに入れる
MinHeap heap = MinHeap(nums.sublist(0, k));
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for (int i = k; i < nums.length; i++) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > heap.peek()) {
heap.pop();
heap.push(nums[i]);
}
}
return heap;
}
```
=== "Rust"
```rust title="top_k.rs"
[class]{}-[func]{top_k_heap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
fn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {
// BinaryHeap は最大ヒープであり、Reverse で要素の順序を反転することで最小ヒープを実現する
let mut heap = BinaryHeap::<Reverse<i32>>::new();
// 配列の先頭 k 個の要素をヒープに追加
for &num in nums.iter().take(k) {
heap.push(Reverse(num));
}
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for &num in nums.iter().skip(k) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if num > heap.peek().unwrap().0 {
heap.pop();
heap.push(Reverse(num));
}
}
heap
}
```
=== "C"
```c title="top_k.c"
[class]{}-[func]{pushMinHeap}
/* 要素をヒープに追加 */
void pushMinHeap(MaxHeap *maxHeap, int val) {
// 要素を反転する
push(maxHeap, -val);
}
[class]{}-[func]{popMinHeap}
/* 要素をヒープから取り出す */
int popMinHeap(MaxHeap *maxHeap) {
// 要素を反転する
return -pop(maxHeap);
}
[class]{}-[func]{peekMinHeap}
/* ヒープ先頭要素にアクセス */
int peekMinHeap(MaxHeap *maxHeap) {
// 要素を反転する
return -peek(maxHeap);
}
[class]{}-[func]{getMinHeap}
/* ヒープから要素を取り出す */
int *getMinHeap(MaxHeap *maxHeap) {
// ヒープ内のすべての要素を反転して res 配列に格納
int *res = (int *)malloc(maxHeap->size * sizeof(int));
for (int i = 0; i < maxHeap->size; i++) {
res[i] = -maxHeap->data[i];
}
return res;
}
[class]{}-[func]{topKHeap}
/* ヒープから要素を取り出す */
int *getMinHeap(MaxHeap *maxHeap) {
// ヒープ内のすべての要素を反転して res 配列に格納
int *res = (int *)malloc(maxHeap->size * sizeof(int));
for (int i = 0; i < maxHeap->size; i++) {
res[i] = -maxHeap->data[i];
}
return res;
}
// ヒープに基づいて配列中の最大の k 個の要素を求める関数
int *topKHeap(int *nums, int sizeNums, int k) {
// 最小ヒープを初期化する
// 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする
int *empty = (int *)malloc(0);
MaxHeap *maxHeap = newMaxHeap(empty, 0);
// 配列の先頭 k 個の要素をヒープに追加
for (int i = 0; i < k; i++) {
pushMinHeap(maxHeap, nums[i]);
}
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for (int i = k; i < sizeNums; i++) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > peekMinHeap(maxHeap)) {
popMinHeap(maxHeap);
pushMinHeap(maxHeap, nums[i]);
}
}
int *res = getMinHeap(maxHeap);
// メモリを解放する
delMaxHeap(maxHeap);
return res;
}
```
=== "Kotlin"
```kotlin title="top_k.kt"
[class]{}-[func]{topKHeap}
/* ヒープに基づいて配列中の最大の k 個の要素を探す */
fun topKHeap(nums: IntArray, k: Int): Queue<Int> {
// 最小ヒープを初期化
val heap = PriorityQueue<Int>()
// 配列の先頭 k 個の要素をヒープに追加
for (i in 0..<k) {
heap.offer(nums[i])
}
// k+1 番目の要素から開始し、ヒープ長を k に保つ
for (i in k..<nums.size) {
// 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if (nums[i] > heap.peek()) {
heap.poll()
heap.offer(nums[i])
}
}
return heap
}
```
=== "Ruby"
```ruby title="top_k.rb"
[class]{}-[func]{top_k_heap}
### ヒープに基づいて配列中の最大 k 個の要素を探す ###
def top_k_heap(nums, k)
# 最小ヒープを初期化する
# 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする
max_heap = MaxHeap.new([])
# 配列の先頭 k 個の要素をヒープに追加
for i in 0...k
push_min_heap(max_heap, nums[i])
end
# k+1 番目の要素から開始し、ヒープ長を k に保つ
for i in k...nums.length
# 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する
if nums[i] > peek_min_heap(max_heap)
pop_min_heap(max_heap)
push_min_heap(max_heap, nums[i])
end
end
get_min_heap(max_heap)
end
```
合計$n$回のヒープ挿入と削除が実行され、最大ヒープサイズが$k$であるため、時間計算量は$O(n \log k)$です。この方法は非常に効率的で、$k$が小さい場合、時間計算量は$O(n)$に近づき、$k$が大きい場合でも、時間計算量は$O(n \log n)$を超えません。
??? pythontutor "コードの可視化"
さらに、この方法は動的データストリームのシナリオに適しています。データを継続的に追加することで、ヒープ内の要素を維持し、最大$k$個の要素の動的更新を実現できます。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20heapq%0A%0Adef%20top_k_heap%28nums%3A%20list%5Bint%5D%2C%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E3%83%92%E3%83%BC%E3%83%97%E3%81%AB%E5%9F%BA%E3%81%A5%E3%81%84%E3%81%A6%E9%85%8D%E5%88%97%E4%B8%AD%E3%81%AE%E6%9C%80%E5%A4%A7%E3%81%AE%20k%20%E5%80%8B%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E6%8E%A2%E3%81%99%22%22%22%0A%20%20%20%20%23%20%E6%9C%80%E5%B0%8F%E3%83%92%E3%83%BC%E3%83%97%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E9%85%8D%E5%88%97%E3%81%AE%E5%85%88%E9%A0%AD%20k%20%E5%80%8B%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E3%83%92%E3%83%BC%E3%83%97%E3%81%AB%E8%BF%BD%E5%8A%A0%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20%23%20k%2B1%20%E7%95%AA%E7%9B%AE%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%8B%E3%82%89%E9%96%8B%E5%A7%8B%E3%81%97%E3%80%81%E3%83%92%E3%83%BC%E3%83%97%E9%95%B7%E3%82%92%20k%20%E3%81%AB%E4%BF%9D%E3%81%A4%0A%20%20%20%20for%20i%20in%20range%28k%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8F%BE%E5%9C%A8%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%8C%E3%83%92%E3%83%BC%E3%83%97%E5%85%88%E9%A0%AD%E3%82%88%E3%82%8A%E5%A4%A7%E3%81%8D%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81%E3%83%92%E3%83%BC%E3%83%97%E5%85%88%E9%A0%AD%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%97%E3%81%A6%E7%8F%BE%E5%9C%A8%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%207%2C%206%2C%203%2C%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums%2C%20k%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20heapq%0A%0Adef%20top_k_heap%28nums%3A%20list%5Bint%5D%2C%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E3%83%92%E3%83%BC%E3%83%97%E3%81%AB%E5%9F%BA%E3%81%A5%E3%81%84%E3%81%A6%E9%85%8D%E5%88%97%E4%B8%AD%E3%81%AE%E6%9C%80%E5%A4%A7%E3%81%AE%20k%20%E5%80%8B%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E6%8E%A2%E3%81%99%22%22%22%0A%20%20%20%20%23%20%E6%9C%80%E5%B0%8F%E3%83%92%E3%83%BC%E3%83%97%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E9%85%8D%E5%88%97%E3%81%AE%E5%85%88%E9%A0%AD%20k%20%E5%80%8B%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E3%83%92%E3%83%BC%E3%83%97%E3%81%AB%E8%BF%BD%E5%8A%A0%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20%23%20k%2B1%20%E7%95%AA%E7%9B%AE%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%8B%E3%82%89%E9%96%8B%E5%A7%8B%E3%81%97%E3%80%81%E3%83%92%E3%83%BC%E3%83%97%E9%95%B7%E3%82%92%20k%20%E3%81%AB%E4%BF%9D%E3%81%A4%0A%20%20%20%20for%20i%20in%20range%28k%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8F%BE%E5%9C%A8%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%8C%E3%83%92%E3%83%BC%E3%83%97%E5%85%88%E9%A0%AD%E3%82%88%E3%82%8A%E5%A4%A7%E3%81%8D%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81%E3%83%92%E3%83%BC%E3%83%97%E5%85%88%E9%A0%AD%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%97%E3%81%A6%E7%8F%BE%E5%9C%A8%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%207%2C%206%2C%203%2C%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums%2C%20k%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
合計で $n$ 回のヒープ挿入と取り出しを行い、ヒープの最大長は $k$ であるため、時間計算量は $O(n \log k)$ です。この方法は非常に効率が高く、$k$ が小さいときは時間計算量が $O(n)$ に近づき、$k$ が大きいときでも $O(n \log n)$ を超えることはありません。
さらに、この方法は動的データストリームの利用シーンにも適しています。データが継続的に追加される場合でも、ヒープ内の要素を保ち続けることで、最大の $k$ 個の要素を動的に更新できます。
+11 -11
View File
@@ -5,26 +5,26 @@ icon: material/rocket-launch-outline
# はじめに
数年前、私はLeetCodeで「剣指Offer」の問題解答を共有し、多くの読者から励ましとサポートを受けました。読者とのやり取りの中で、最もよく聞かれた質問「アルゴリズムの勉強をどう始めたらよいか」でした。次第に、私はこの問に強い関心を抱くようになりました。
数年前、私は LeetCode 中国版で「剣指 Offer」シリーズの解説を共有し、多くの読者から励ましと支持をいただきました。読者との交流の中で、最もよく聞かれた質問の一つが「アルゴリズムをどう学び始めればよいか」でした。次第に、私はこの問に強い関心を抱くようになりました。
問題を直接解くこと最も人気のある方法のようです。これはシンプルで直接的で効果的です。しかし問題解決はマインスイーパーをプレイするようなものです。自学自習の能力が高い人は地雷を一つずつ回避していくことができますが、しっかりとした基礎がない人は、何度もつまずいて挫折しながら後退することになるかもしれません。教科書を読むことも一般的な方法ですが、就職活動中の人にとって、卒業論文の執筆、履歴書の提出、筆記試験や面接の準備が既にエネルギーの大部分を消費しており、分厚い本を読むことはしばしば困難な挑戦となります。
手探りでひたすら問題を解くことは、最も人気のある方法のようです。単純で、直接的で、しかも効果的です。しかし問題演習は「マインスイーパー」を遊ぶことに似ており、独学力の高い人は地雷を一つずつうまく取り除ける一方で、基礎が十分でない人は大きな痛手を受け、挫折の中で少しずつ後退してしまいがちです。教材を通読するのもよくある方法ですが、就職を目指す人にとって、卒業論文、履歴書の提出、筆記試験や面接の準備ですでに大半の力を使い果たしており、分厚い本を読み込むことはしばしば困難な挑戦になってしまいます。
もしあなたが同様の悩みを抱えているなら、この本があなたを見つけることができて幸運です。この本は、この質問に対する私の答えです。これが最良の解決策ではないかもしれませんが、少なくとも積極的な試みです。この本があなたに直接内定をもたらすことはできませんが、データ構造とアルゴリズムの「知識地図」を探索する手引きとなり、さまざまな「地雷」の形大きさ、位置を理解し、さまざまな「地雷除去方法」をマスターできるようお手伝いします。これらのスキルがあれば、より快適に問題を解き、文献を読むことができ、徐々に知識体系を構築できると信じています。
もしあなたも同じような悩みを抱えているなら、この本があなたのもとに“たどり着いた”のは幸運なことです。本書は、この問いに対して私が示す答えです。たとえ最良の解ではなくても、少なくとも前向きな試みではあります。本書だけで直接内定を得られるわけではありませんが、データ構造とアルゴリズムの「知識地図」を探る手助けをし、さまざまな「地雷」の形大きさ、分布を理解し、いろいろな「地雷除去方法」を身につけられるよう導きます。こうした力があれば、問題演習や文献読解をより自在に進め、やがて完整な知識体系を築いていけると信じています。
私はファインマン教授の言葉に深く同感します。「知識は無料ではありません。注意を払わなければならないのです。」この意味で、この本は完全に「無料」ではありません。この本に対するあなたの貴重な「注意」に応えるため、私は最善を尽くし、最大の「注意」を払ってこの本を書きます。
私はファインマン教授の言葉に深く賛同しています。「Knowledge isn't free. You have to pay attention.」この意味において、本書は完全に「無料」ではありません。本書のためにあなたが払ってくれる貴重な「注意」に応えるため、私はできる限りの力を尽くし、最大の「注意」を注いで本書を書き上げます。
自分の限界を認識しており、この本の内容が時間をかけて洗練されたにもかかわらず、間違いは確実に残っていることを理解しています。先生方や学生の皆様からの批評と訂正を心から歓迎いたします。
私は自らの学識と力量の浅さをよく承知しています。本書の内容はある程度磨きをかけてきたものの、なお多くの誤りが残っているはずです。先生方、そして学習者の皆さまからのご批判とご指摘を心よりお願いいたします。
![Hello Algo](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" }
![Hello アルゴリズム](../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>
<h2 style="margin-top: 0.8em; margin-bottom: 0.8em;">Hello、アルゴリズム!</h2>
</div>
コンピュータの出現は世界に大きな変をもたらしました。高速な計算能力と優れたプログラム可能性により、コンピュータはアルゴリズム実行データ処理するための理想的な媒体となりました。ビデオゲームのリアルなグラフィックス、自動運転の知的な判断、AlphaGoの見事な囲碁ゲーム、ChatGPTの自然な対話など、これらのアプリケーションはすべて、コンピュータ上で動作するアルゴリズムの精巧な実演です。
コンピュータの登場は世界に大きな変をもたらしました。高速な計算能力と優れたプログラム可能性によって、アルゴリズム実行データ処理の理想的な媒体となったのです。ビデオゲームのリアルな映像、自動運転の知的な意思決定、AlphaGo の見事な対局、ChatGPT の自然な対話に至るまで、これらの応用はいずれもコンピュータ上でアルゴリズムが巧みに表現されたものです。
実際、コンピュータの出現以前から、アルゴリズムとデータ構造は世界の至る所に存在していました。初期のアルゴリズムは比較的シンプルで、古代の計数方法や道具作りの手順などがありました。文明進歩するにつれて、アルゴリズムはより洗練され複雑になりました。職人の精巧な技から、生産力を解放する工業製品、宇宙を支配する科学法則まで、ほぼすべての平凡または驚異的なことの背後には、アルゴリズムの巧妙な思考があります。
実際には、コンピュータが誕生する以前から、アルゴリズムとデータ構造は世界のいたるところに存在していました。初期のアルゴリズムは比較的単純で、たとえば古代の数え方や道具作りの手順などがそれに当たります。文明進歩とともに、アルゴリズムは次第により精緻で複雑なものになっていきました。匠の巧みな技から、生産力を解放する工業製品、さらには宇宙の運行を支配する科学法則に至るまで、ほとんどあらゆる平凡なもの、あるいは驚嘆すべきものの背後には、精妙なアルゴリズムの思想が潜んでいます。
同様に、データ構造至る所にあります。ソーシャルネットワークから地下鉄路線まで、多くのシステムは「グラフ」としてモデル化できます。国から家族まで、社会組織の主要な形態は「木」の特徴を示します。冬服は「スタック」のよう、最初に着たものが最後に脱がれます。バドミントンシャトル筒は「キュー」に似ており、一方の端で挿入し、もう一方の端で取り出します。辞書は「ハッシュテーブル」のようで、目標エントリを素早く検索できます。
同様に、データ構造至るところに存在します。社会ネットワークのような大きなものから地下鉄路線のような小さなものまで、多くのシステムは「グラフ」としてモデル化できます。国家のような大きな単位から家庭のような小さな単位まで、社会の主な組織形態は「木」の特徴があります。冬服は「スタック」のよう、最初に着たものが最後に脱がれます。バドミントンシャトル筒は「キュー」のように、一方から入れて他方から取り出します。辞書は「ハッシュテーブル」のようなもので、目的の見出し語を素早く探せます。
この本は、明確で理解しやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、プログラミングを通じてそれらを実装できるようになることを目指しています。この基盤の上で、この本は複雑な世界におけるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示すことに努めています。この本があなたのお役に立てることを願っています!
本書は、わかりやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、さらにプログラミングによってそれらを実装できるようになることを目指しています。そのうえで、本書は複雑な世界の中にあるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示そうとしています。本があなたの助けになれば幸いです。
@@ -2,65 +2,65 @@
comments: true
---
# 1.1 &nbsp; アルゴリズムは至るにある
# 1.1 &nbsp; アルゴリズムは至るところにある
「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし、多くのアルゴリズムは複雑な数学を含まず、基本的な論理により多く依存しており、これは私たちの日常生活の至る所で見ることができます。
「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし実際には、多くのアルゴリズムは複雑な数学を必要とせず、むしろ基本的な論理に依存しており、その論理は私たちの日常生活のいたるところで見られます。
アルゴリズムについて正式に議論を始める前に、興味深い事実を共有する価値があります。**あなたは無意識のうちに多くのアルゴリズムを学び、日常生活でそれらを応用することに慣れていす**。ここで、この点を証明するためにいくつかの具体的な例を挙げます。
アルゴリズムを本格的に議論する前に、ひとつ面白い事実を共有しておきます。**あなたはすでに知らず知らずのうちに多くのアルゴリズムを身につけ、それらを日常生活に応用することに慣れているのです**。以下では、いくつかの具体例を挙げてこれを示します。
**例1:辞書の引き方**英語の辞書では、単語がアルファベット順に並んでいます。$r$で始まる単語を探していると仮定すると、通常は以下の方法で行います
**例1:辞書を引く**。辞書では、各漢字に対応するピンインがあり、辞書はピンインのアルファベット順に並んでいます。ピンインの先頭文字が $r$ の字を探すと仮定すると、通常は次の図のような方法で行います
1. 辞書を大体半分ぐらいのところで開き、そのページの最初の語彙を確認します。例えば$m$で始まる文字だとしましょう
2. $r$はアルファベット$m$の後に来るので、前半を無視して、探索空間を後半に絞ります。
3. $r$で始まる単語を見つけるまで、ステップ`1.``2.`を繰り返します。
1. 辞書をおよそ半分のところで開き、そのページの先頭文字を確認し、先頭文字が $m$ だとしま
2. ピンインのアルファベット表では $r$ は $m$ の後にあるため、辞書の前半を除外し、探索範囲を後半に絞ります。
3. ピンインの先頭文字が $r$ のページを見つけるまで、手順 `1.` と手順 `2.` を繰り返します。
=== "<1>"
![辞書を引く過程](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" }
![辞書を引く手順](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" }
![binary_search_dictionary_step2](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" }
![binary_search_dictionary_step3](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" }
![binary_search_dictionary_step4](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" }
![binary_search_dictionary_step5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" }
<p align="center"> 図 1-1 &nbsp; 辞書を引く過程 </p>
<p align="center"> 図 1-1 &nbsp; 辞書を引く手順 </p>
辞書を引くことは、小学生にとって必須スキルですが、実際には有名な「二分探索」アルゴリズムです。データ構造の観点から、辞書をソートされた「配列」と考えることができます。アルゴリズムの観点から、辞書で単語を探すために取られる一連の行動は、「二分探索」アルゴリズムと見なすことができます。
辞書を引くという小学生の必須スキルは、実は有名な「二分探索」アルゴリズムそのものです。データ構造の観点では、辞書を整列済みの「配列」とみなせます。アルゴリズムの観点では、上記の一連の辞書引きの操作を「二分探索」とみなせます。
**例2:トランプ整理**トランプをプレイするとき、手札を昇順に並べる必要があります。以下の過程で示されます。
**例2:トランプ整理する**カードゲームをするとき、毎回手札のトランプを小さい順に並べ替える必要があります。その流れは次の図のとおりです。
1. トランプを「整列済み」と「未整列」のセクションに分けます。最初は一番左のカードが既に整列していると仮定します。
2. 未整列セクションからカードを1枚取り出し、整列済みセクションの正しい位置に挿入します。この後、左端の2枚のカードが整列します。
3. すべてのカードが整列するまで、ステップ`2`を繰り返します。
1. トランプを「整列済み」と「未整列」の2つの部分に分け、初期状態では一番左の1枚がすでに整列済みだとします。
2. 未整列部分から1枚のトランプを取り出し、整列済み部分の正しい位置に挿入します。完了すると、左端の2枚は整列済みになります。
3. 手順 `2.` を繰り返し、各ラウンドで未整列部分から1枚を整列済み部分へ挿入し、すべてのトランプが整列済みになるまで続けます。
![トランプの整理過程](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" }
![トランプを並べ替える手順](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" }
<p align="center"> 図 1-2 &nbsp; トランプの整理過程 </p>
<p align="center"> 図 1-2 &nbsp; トランプを並べ替える手順 </p>
上記のトランプ整理する方法は、質的に「挿入ソート」アルゴリズムであり、小さなデータセットに対して非常に効率的です。多くのプログラミング言語のソート関数に挿入ソートが含まれています。
上記のトランプ整理方法は、質的に「挿入ソート」アルゴリズムです。これは小規模なデータ集合を処理する際に非常に効率的で多くのプログラミング言語のソートライブラリ関数に挿入ソートが使われています。
**例3:お釣りの計算**。スーパーマーケットで$69$の買い物をしたと仮定します。レジ係に$100$を渡すと、$31$のお釣りを提供する必要があります。この過程は以下の図で明確に理解できます。
**例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$です。
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" }
![お釣りの過程](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" }
<p align="center"> 図 1-3 &nbsp; お釣りの計算過程 </p>
<p align="center"> 図 1-3 &nbsp; お釣りの過程 </p>
記述されたステップでは、利用可能な最大の額面を使用して各段階で最良の選択肢を選ぶことで、効果的なお釣り計算戦略につながります。データ構造とアルゴリズムの観点から、このアプローチは「貪欲」アルゴリズムとして知られています。
以上の手順では、各ステップでその時点で最善と思われる選択肢を取っています。つまり、できるだけ額面の大きい貨幣を使い、最終的に実行可能なお釣りの方案を得ています。データ構造とアルゴリズムの観点から見ると、この方法は本質的に「貪欲法」です。
料理の準備から宇宙旅行まで、ほぼすべての問題解決にアルゴリズムが関わっています。コンピュータの出現により、メモリにデータ構造を格納し、CPUとGPUを呼び出してアルゴリズムを実行するコードを書くことができるようになりました。このようにして、現実世界の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できす。
料理を一品作ることから星間航行に至るまで、ほとんどあらゆる問題解決にアルゴリズムは欠かせません。コンピュータの登場によって、プログラミングを通じてデータ構造をメモリに格納し、さらにコードを書いて CPU や GPU にアルゴリズムを実行させることが可能になりました。こうして、生活の中の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できるのです。
!!! tip
データ構造、アルゴリズム、配列、二分探索などの概念についてまだ混乱している場合は、読み続けることをお勧めします。この本は、データ構造とアルゴリズムの理解の領域へと優しく導いてくれるでしょう
データ構造、アルゴリズム、配列、二分探索といった概念がまだ少し曖昧でも、そのまま読み進めてください。本書がデータ構造とアルゴリズムの知識の世界へと案内します
+7 -7
View File
@@ -3,18 +3,18 @@ comments: true
icon: material/calculator-variant-outline
---
# 第 1 章 &nbsp; アルゴリズムとの出会い
# 第 1 章 &nbsp; アルゴリズム入門
![アルゴリズムとの出会い](../assets/covers/chapter_introduction.jpg){ class="cover-image" }
![アルゴリズム入門](../assets/covers/chapter_introduction.jpg){ class="cover-image" }
!!! abstract
優雅な乙女が踊ります。データと絡み合い、アルゴリズムのメロディーに合わせてスカートをなびかせながら
彼女あなたをダンスに誘います。彼女のステップに従って、論理と美に満ちたアルゴリズムの世界入りましょう。
一人の少女が軽やかに舞い、データと織り重なり合いながら、スカートの裾にはアルゴリズムの旋律がたなびいています
彼女あなたをこの舞いへと誘います。その足取りに続いて、論理と美しさに満ちたアルゴリズムの世界へ踏み入りましょう。
## 章の内容
- [1.1 &nbsp; アルゴリズムはどこにでもある](algorithms_are_everywhere.md)
- [1.2 &nbsp; アルゴリズムとは何か](what_is_dsa.md)
- [1.1 &nbsp; アルゴリズムは至るところにある](algorithms_are_everywhere.md)
- [1.2 &nbsp; アルゴリズムとは](what_is_dsa.md)
- [1.3 &nbsp; まとめ](summary.md)
+16 -14
View File
@@ -4,23 +4,25 @@ comments: true
# 1.3 &nbsp; まとめ
- アルゴリズムは日常生活にありふれており、思っているほどアクセスしにくく複雑なものではありません。実際、私たちは既に無意識のうちに多くのアルゴリズムを学び、生活の様々な問題を解決するために使用しています。
- 辞書で単語を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム概念を体現しています。
- トランプを整理する過程は挿入ソートアルゴリズムと非常に似ています。挿入ソートアルゴリズムは小さなデータセットのソートに適しています。
- 通貨でお釣りを計算するステップは本質的に貪欲アルゴリズムに従っており、各ステップでその時点での最良の選択をします。
- アルゴリズムは有限時間内で特定の問題を解決するための段階的な指示のセットですが、データ構造はコンピュータでのデータの組織化と保存方法を定義します。
- データ構造とアルゴリズムは密接に関連しています。データ構造はアルゴリズムの基礎であり、アルゴリズムはデータ構造の機能を活用するステージです。
- データ構造とアルゴリズムをブロックの組み立てに例えることができます。ブロックはデータを表し、ブロックの形状と接続方法はデータ構造を表し、ブロックを組み立てるステップはアルゴリズムに対応します。
### 1. &nbsp; 要点の振り返り
### 1. &nbsp; Q & A
- アルゴリズムは日常生活の至る所にあり、決して手の届かない難解な知識ではありません。実際、私たちは気づかないうちに多くのアルゴリズムを身につけ、生活のさまざまな問題を解決しています。
- 辞書を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム思想を体現しています。
- トランプを整理する過程は挿入ソートアルゴリズムと非常によく似ています。挿入ソートアルゴリズムは小規模なデータ集合のソートに適しています。
- 貨幣の釣り銭を求める手順の本質は貪欲アルゴリズムであり、各ステップでその時点で最善と思われる選択を取ります。
- アルゴリズムとは、限られた時間内に特定の問題を解決するための一連の命令または操作手順であり、データ構造とは、コンピュータ内でデータを組織し保存する方法です。
- データ構造とアルゴリズムは密接に結びついています。データ構造はアルゴリズムの土台であり、アルゴリズムはデータ構造に生命を吹き込みます。
- データ構造とアルゴリズムは積み木の組み立てにたとえることができます。積み木はデータを表し、積み木の形や接続方法などはデータ構造を表し、積み木を組み立てる手順がアルゴリズムに対応します。
**Q**:プログラマーとして、日常の仕事でアルゴリズムを手動で実装する必要があることはめったにありません。最も一般的に使用されるアルゴリズムは、既にプログラミング言語とライブラリに組み込まれており、すぐに使用できます。これは、私たちが仕事で遭遇する問題が、カスタムアルゴリズム設計を必要とする複雑さのレベルにまだ達していないことを示唆していますか?
### 2. &nbsp; Q & A
特定の仕事スキルが武術の「技」のようなものだとすれば、基礎科目は「内功」のようなものです。
**Q**:プログラマーとして、私は日常業務でアルゴリズムを使って問題を解決したことがありません。よく使うアルゴリズムはプログラミング言語にすべてカプセル化されており、そのまま使えばよいです。これは、仕事上の問題がまだアルゴリズムを必要とする段階に達していないことを意味するのでしょうか?
アルゴリズム(およびその他の基礎科目)を学ぶ意義は、必ずしも仕事でそれらを一から実装することではなく、概念の確固たる理解に基づいて、より専門的な意思決定と問題解決を可能にし、それによって仕事の全体的な質を向上させることだと私は信じています。例えば、すべてのプログラミング言語には組み込みのソート関数があります:
具体的な仕事のスキルを武術の「型」にたとえるなら、基礎科目はむしろ「内功」に近いものです。
- データ構造とアルゴリズムを学んでいない場合、どんなデータが与えられても、このソート関数に渡すだけかもしれません。スムーズに動作し、良いパフォーマンスを示し、問題がないように見えます。
- しかし、アルゴリズムを学んだことがあれば、組み込みのソート関数の時間複雑度は通常$O(n \log n)$であることを理解しています。さらに、データが固定桁数の整数(学生IDなど)で構成されている場合、基数ソートのようなより効率的なアプローチを適用でき、時間複雑度をO(nk)に削減できます。ここでkは桁数です。大量のデータを処理する際、節約された時間は重要な価値に変わります — コストの削減、ユーザーエクスペリエンスの向上、システムパフォーマンスの向上。
私は、アルゴリズム(およびその他の基礎科目)を学ぶ意義は、仕事でそれをゼロから実装することではなく、学んだ知識に基づいて問題解決の際に専門的な反応や判断を下せるようになり、その結果として仕事全体の品質を高めることにあると考えています。簡単な例を挙げると、どのプログラミング言語にもソート関数が組み込まれています。
エンジニアリングでは、多くの問題を最適に解決することは困難です。ほとんどは「準最適」解決策で対処されます。問題の難しさは、その固有の複雑さだけでなく、それに取り組む人の知識と経験にも依存します。専門知識と経験が深いほど、分析がより徹底的になり、問題をより優雅に解決できます
- もしデータ構造とアルゴリズムを学んでいなければ、どんなデータが与えられても、そのソート関数に任せてしまうかもしれません。問題なく動き、性能も悪くなく、一見すると特に問題はありません
- しかしアルゴリズムを学んでいれば、組み込みのソート関数の時間計算量が $O(n \log n)$ であることを知っています。さらに、与えられたデータが固定桁数の整数(例えば学籍番号)であれば、より効率の高い「基数ソート」を使って、時間計算量を $O(nk)$ に下げることができます。ここで $k$ は桁数です。データ量が非常に大きい場合、節約できた実行時間は大きな価値を生みます(コスト削減、体験向上など)。
工学分野では、多くの問題で最適解に到達することは難しく、少なくない問題は「だいたい」解決されているにすぎません。問題の難しさは、一方では問題そのものの性質に依存し、他方ではそれを観測する人の知識の蓄積にも依存します。知識が充実し、経験が豊富であるほど、問題分析はより深くなり、問題はより洗練された形で解決できるようになります。
+30 -30
View File
@@ -2,64 +2,64 @@
comments: true
---
# 1.2 &nbsp; アルゴリズムとは何か
# 1.2 &nbsp; アルゴリズムとは
## 1.2.1 &nbsp; アルゴリズムの定義
<u>アルゴリズム</u>は、限時間内特定の問題を解決するための一連の指示またはステップです。以下の特徴があります
<u>アルゴリズムalgorithm</u>は、限られた時間内特定の問題を解決するための一連の命令または操作手順であり、次のような特徴を持ちます
- 問題が明確に定義されており、入力と出力の明確な定義が含まれています。
- アルゴリズムは実行可能で、有限の回数のステップ、時間、メモリ空間で完了できることを意味します。
- 各ステップには明確な意味があります。同じ入力と条件の下で出力は一貫して同じです。
- 問題が明確であり、入力と出力の定義がはっきりしています。
- 実行可能であり、有限の手順、時間、メモリ空間で完了できます。
- 各手順の意味が確定しており、同じ入力と実行条件では常に同じ出力になります。
## 1.2.2 &nbsp; データ構造の定義
<u>データ構造</u>は、コンピュータ内でデータを組織し保存する方法で、以下の設計目標があります
<u>データ構造data structure</u>は、データを整理して保存する方式であり、データの内容、データ間の関係、データの操作方法を含み、次のような設計目標があります
- コンピュータのメモリを節約するために空間占有を最小化する
- データ操作を可能な限り高速にし、データのアクセス、追加、削除、更新などをカバーする
- 効率的なアルゴリズム実行を可能にするために、簡潔なデータ表現と論理情報を提供する
- 使用する空間をできるだけ少なくし、コンピュータのメモリを節約します
- データ操作をできるだけ高速にし、アクセス、追加、削除、更新などを含みます
- 簡潔なデータ表現と論理情報を提供し、アルゴリズムが効率よく動作できるようにします
**データ構造の設計はバランスを取る行為であり、しばしばトレードオフが必要です**。一つの側面を改善したい場合、しばしば別の面で妥協する必要があります。以下は2つの例です:
**データ構造の設計はトレードオフに満ちた過程です**。ある面を改善したい場合、別の面で妥協が必要になることがよくあります。以下に 2 つの例を示します。
- 配列と比較して、連結リストはデータの追加削除においてより便利ですが、データアクセス速度を犠牲にします。
- 連結リストと比較して、グラフはより豊富な論理情報を提供しますが、より多くのメモリ空間必要す。
- 連結リストは配列に比べてデータの追加削除がしやすい一方で、データアクセス速度を犠牲にしています。
- グラフは連結リストに比べてより豊富な論理情報を提供しますが、より大きなメモリ空間必要とします。
## 1.2.3 &nbsp; データ構造とアルゴリズムの関係
以下の図に示すように、データ構造とアルゴリズムは高度に関連し、密接に統合されており、具体的には以下の3つの側面があります
以下の図ように、データ構造とアルゴリズムは高度に関連し、密接に結び付いており、具体的には次の 3 つの点に表れます
- データ構造はアルゴリズムの基礎です。構造化されたデータ保存とアルゴリズムのためのデータ操作方法を提供します。
- アルゴリズムはデータ構造に活力を注入します。データ構造だけではデータ情報を保存するだけです。アルゴリズムの応用によって、特定の問題を解決できます。
- アルゴリズムは異なるデータ構造に基づいて実装できることが多いですが、実行効率大きく異なることがあります。適切なデータ構造を選択することがです。
- データ構造はアルゴリズムの土台です。データ構造はアルゴリズムに対して、構造化して格納されたデータと、そのデータ操作する方法を提供します。
- アルゴリズムはデータ構造に命を吹き込みます。データ構造そのものはデータ情報を保存するだけであり、アルゴリズムと組み合わせて初めて特定の問題を解決できます。
- アルゴリズムは通常、異なるデータ構造に基づいて実装できすが、実行効率大きく異なる場合があり適切なデータ構造を選ことが重要です。
![データ構造とアルゴリズムの関係](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" }
<p align="center"> 図 1-4 &nbsp; データ構造とアルゴリズムの関係 </p>
データ構造とアルゴリズムは、以下の図に示すように、ブロックのセットに例えることができます。ブロックセットには多数のピースが含まれ、詳細な組み立て説明書付いています。これらの説明書に段階的に従うことで、複雑なブロックモデルを構築できます。
データ構造とアルゴリズムは、以下の図に示す組み立てブロックのようなものです。1 セットのブロックには多くの部品が含まれるだけでなく、詳しい組み立て説明書付いています。説明書に従って一歩ずつ操作すれば、精巧なブロック模型を組み立てられます。
![ブロックの組み立て](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" }
![組み立てブロック](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" }
<p align="center"> 図 1-5 &nbsp; ブロックの組み立て </p>
<p align="center"> 図 1-5 &nbsp; 組み立てブロック </p>
両者の詳細な対応関係は以下の表に示されています。
両者の詳細な対応関係を次の表に示ます。
<p align="center"> 表 1-1 &nbsp; データ構造とアルゴリズムをブロックと比較 </p>
<p align="center"> 表 1-1 &nbsp; データ構造とアルゴリズムを組み立てブロックにたとえる </p>
<div class="center-table" markdown>
| データ構造とアルゴリズム | ブロック |
| ------------------------ | ----------------------------------------------------- |
| 入力データ | 未組み立てのブロック |
| データ構造 | ブロックの組織、形状、サイズ、接続などを含む |
| アルゴリズム | ブロックを望ましい形状に組み立てる一連のステップ |
| 出力データ | 完成したブロックモデル |
| データ構造とアルゴリズム | 組み立てブロック |
| -------------- | ---------------------------------------- |
| 入力データ | まだ組み立てていないブロック |
| データ構造 | ブロックの構成形式。形状、大きさ、接続方法などを含む |
| アルゴリズム | ブロックを目標の形に組み上げる一連の操作手順 |
| 出力データ | ブロック模型 |
</div>
データ構造とアルゴリズムはプログラミング言語から独立していることは注目に値します。この理由により、この本は複数のプログラミング言語での実装を提供できます。
特筆すべき点として、データ構造とアルゴリズムはプログラミング言語から独立しています。だからこそ、本書では複数のプログラミング言語に基づく実装を提供できます。
!!! tip "慣習的な略"
!!! tip "慣習的な略"
生活の議論では、「データ構造とアルゴリズム」を単純に「アルゴリズム」と呼ぶことがよくあります。例えば、よく知られたLeetCodeアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識をテストしています。
の議論では、私たちは通常「データ構造とアルゴリズム」を略して「アルゴリズム」と呼びます。たとえば広く知られている LeetCodeアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識を同時に問うています。
+68
View File
@@ -0,0 +1,68 @@
---
comments: true
icon: material/book-open-page-variant
status: new
---
# 紙の書籍
長い時間をかけて磨き上げた『Hello アルゴリズム』の紙の書籍が、ついに発売されました!今の気持ちは、次の一節で表せます:
<p align="center">風を追い月を追って立ち止まるな、草原の果てには春の山がある。</p>
![](index.assets/paper_book_overview.jpg){ class="animation-figure" }
以下の動画では紙の書籍を紹介しており、私の考えもいくつか含まれています:
- データ構造とアルゴリズムを学ぶ重要性。
- なぜ紙の書籍で Python を選んだのか。
- 知識共有に対する理解。
> 新人 UP 主ですので、ぜひ応援と高評価・チャンネル登録をお願いします~ありがとうございます!
<div class="video-container">
<iframe src="//player.bilibili.com/player.html?aid=1051597767&bvid=BV1QH4y157uC&cid=1462564112&p=1&autoplay=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
</div>
紙の書籍のスナップショット:
![](index.assets/paper_book_chapter_heap.jpg){ class="animation-figure" }
![](index.assets/paper_book_avl_tree.jpg){ class="animation-figure" }
## 長所と短所
紙の書籍ならではの魅力を、簡単にまとめると次のとおりです:
- フルカラー印刷を採用し、本書の「アニメーション図解」の強みをそのまま活かせます。
- 紙の素材にもこだわり、色彩を高い精度で再現しつつ、紙の書籍ならではの質感も残しています。
- 紙の書籍版は Web 版よりも書式が整っており、たとえば図中の数式には斜体を用いています。
- 価格を上げずに、マインドマップの折り込みページやしおりも付属します。
- 紙の書籍、Web 版、PDF 版で内容は同期しており、自由に切り替えて読めます。
!!! tip
紙の書籍と Web 版を同期させるのは難しいため、細かな違いが生じる場合があります。ご了承ください!
もちろん、購入前に検討しておくべき点もいくつかあります:
- Python 言語を使用しているため、あなたの主言語と合わない可能性があります(Python は疑似コードと捉え、考え方の理解を重視してください)。
- フルカラー印刷は図解やコードの読みやすさを大きく高める一方で、白黒印刷より価格はやや高くなります。
!!! tip
「印刷品質」と「価格」は、アルゴリズムにおける「時間効率」と「空間効率」のようなもので、両立は容易ではありません。そして私は、「印刷品質」は「時間効率」に当たるため、より重視すべきだと考えています。
## 購入リンク
紙の書籍に興味があれば、ぜひ一冊ご検討ください。新刊の 5 割引を用意していただきましたので、[こちらのリンク](https://3.cn/1X-qmTD3)をご覧いただくか、以下の QR コードをスキャンしてください:
![](index.assets/book_jd_link.jpg){ class="animation-figure" }
## あとがき
当初、私は紙の書籍出版に必要な作業量を甘く見ていて、オープンソースプロジェクトをきちんと保守していれば、紙の書籍版も何らかの自動化手段で生成できると思っていました。実際には、紙の書籍の制作フローとオープンソースプロジェクトの更新の仕組みには大きな違いがあり、その間をつなぐには多くの追加作業が必要でした。
一冊の本の初稿と出版基準を満たす完成稿との間には、なお大きな隔たりがあります。出版社(企画、編集、デザイン、マーケティングなど)と著者が力を合わせ、長い時間をかけて磨き上げていく必要があります。ここで、図霊の企画編集者である王軍花さん、そして人民郵電出版社と図霊コミュニティで本書の出版工程に携わってくださったすべての皆さまに感謝いたします!
この本があなたの助けになれば幸いです!
+28 -26
View File
@@ -2,33 +2,33 @@
comments: true
---
# 0.1 &nbsp; この本について
# 0.1 &nbsp; 本について
このオープンソースプロジェクトは、データ構造とアルゴリズムに関する無料で初心者にやさしいクラッシュコースの作成を目指しています。
本プロジェクトは、オープンソースで無料、かつ初心者にやさしいデータ構造とアルゴリズムの入門書を作ることを目的としています。
- アニメーション付きの図解、理解しやすい内容、滑らかな学習曲線により、初心者がデータ構造とアルゴリズムの知識マップ」を探索するのに役立ちます。
- ワンクリックでコードを実行できるため、読者のプログラミングスキルの向上と、アルゴリズムの動作原理およびデータ構造の基礎実装理解に役立ちます。
- 教えることによる学習を促進し、質問や洞察の共有を自由に行ってください。議論を通じて一緒に成長しましょう
- 全編でアニメーション付きの図解を採用し、内容は明快で理解しやすく、学習曲線もなだらかで、初心者がデータ構造とアルゴリズムの知識地図を探求できるよう導きます。
- ソースコードはワンクリックで実行でき、読者が演習を通じてプログラミング能力を高め、アルゴリズムの動作原理データ構造の内部実装理解する助けとなります。
- 読者どうしの助け合いによる学習を推奨しており、コメント欄で質問や見解を共有し、対話と議論を通じてともに成長していくことを歓迎します
## 0.1.1 &nbsp; 対象読者
もしあなたがアルゴリズムに触れたばかりで経験が限られている場合、またはアルゴリズムである程度の経験を積んでいても、データ構造とアルゴリズムについて曖昧な理解しかなく、常に「分かった」と「うーん」の間を行き来している場合、この本はあなたのためのものです!
もしあなたがアルゴリズム初心者で、これまでアルゴリズムに触れたことがない、あるいはすでに多少の問題演習の経験はあるものの、データ構造とアルゴリズムについてはまだ曖昧な理解にとどまり、できるかできないかの間を行き来しているなら、本書はまさにあなたのために作られています!
すでにある程度の問題解決経験を積んでおり、ほとんどのタイプの問題に精通している場合、この本はアルゴリズム知識体系復習整理するのに役立ちます。リポジトリのソースコードは「問題解決ツールキット」や「アルゴリズムチートシート」として使用できます。
もしすでに一定量の問題演習を積み、ほとんどの問題パターンに慣れているなら、本書はアルゴリズム知識体系復習整理に役立ちます。リポジトリのソースコードは「問題演習ツール」や「アルゴリズム辞典」として用できます。
もしあなたがアルゴリズムの専門家であれば、貴重な提案をいただくか、[参加して協力](https://www.hello-algo.com/chapter_appendix/contribution/)していただければと思います
もしあなたがアルゴリズムの「達人」なら、貴重な提案をいただけることを楽しみにしています。あるいは[一緒に執筆に参加](https://www.hello-algo.com/chapter_appendix/contribution/)してください。
!!! success "前提条件"
少なくとも一つのプログラミング言語で簡単なコードを書いて読むことができる必要があります。
少なくともいずれか一つの言語でのプログラミング基礎があり、簡単なコードを読んだり書いたりできる必要があります。
## 0.1.2 &nbsp; 内容構成
本書の主な内容を下図に示します。
本書の主な内容は以下の図のとおりです。
- **計算量解析**: データ構造とアルゴリズムを評価する側面と方法を探求します。時間計算量と空間計算量を導出する方法、および一般的なタイプと例を扱います
- **データ構造**: 基本的なデータ型、分類方法、定義、長所と短所、一般的な操作、タイプ、応用、および配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の実装方法に焦点を当てます
- **アルゴリズム**: アルゴリズムを定義し、その長所と短所、効率性、応用シナリオ、問題解決ステップについて議論し、検索、ソート、分割統治、バックトラッキング、動的プログラミング、貪欲アルゴリズムなど、さまざまなアルゴリズムのサンプル問題を含みます
- **計算量解析**データ構造とアルゴリズムを評価する観点と方法。時間計算量と空間計算量の求め方、代表的な種類、例など
- **データ構造**:基本データ型とデータ構造の分類方法。配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の定義、長所と短所、基本操作、代表的な種類、典型的な応用、実装方法など
- **アルゴリズム**:探索、ソート、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムの定義、長所と短所、効率、適用場面、問題を解く手順、例題など
![本書の主な内容](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" }
@@ -36,23 +36,25 @@ comments: true
## 0.1.3 &nbsp; 謝辞
本書は、オープンソースコミュニティの多の貢献者による共同の努力のもと、継続的に改善されています。時間と労力を注いで執筆に携わってくださったすべての方々に感謝します。貢献者は以下のとおりです(GitHub により自動生成された順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、KeynmanKeiichiKasai、KawaiiAsh
本書は、オープンソースコミュニティの多の貢献者による共同の努力のもと、継続的に改善されています。時間と労力を注いでくださったすべての執筆者の皆さんに感謝します。お名前は次のとおりです(GitHub により自動生成された順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、pengchzn、QiLOL、Cathay-Chen、guowei-gong、xBLACKICEx、IsChristina、JoseHung、qualifier1024、hello-ikun、magentaqin、Guanngxu、thomasq0、sunshinesDL、L-Super、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、theNefelibatas、Shyam-Chen、sangxiaai、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、Nigh、Dr-XYZ、MolDuM、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、xjr7670、beatrix-chan、DullSword、qq909244296、iStig、boloboloda、hts0000、gledfish、fbigm、echo1937、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、JTCPOWI、KawaiiAsh、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、llql1211、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Senrian、Allen-Scai、19santosh99、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、codetypess、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Kunchen-Luo、KeynmanKeiichiKasai。
この本のコードレビュー作業は、coderonion, Gonglja, gvenusleo, hpstory, justintse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi(アルファベット順)によって完了されました。彼らの時間と力に感謝し、様々な言語のコードの標準化と統一性を確保してくださいました
のコードレビューcoderonion、curtishd、Gongljagvenusleohpstoryjustin-tsekhoaxuantukrahetsnight-cruisenuomi1Reanon rongyi によって行われました(アルファベット順)。彼らが費やしてくれた時間と力に感謝します。各言語のコードの規範性と統一性が保たれているのは、まさに彼らのおかげです
本書の繁体字中国語版は Shyam-Chen および Dr-XYZ によってレビューされ、英語版は yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchznthomasq0、magentaqin によってレビューされ、日本語版は eltociear によってレビューされました。彼らの継続的な貢献のおかげで、本書はより幅広い読者層に提供することができています。ここに深く感謝いたします。
本書の英語版は yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchznthomasq0レビュー、日本語版は eltociear がレビューし、ロシア語版は И. А. Шевкун と Yuyan Huang がレビューし、繁体字中国語版は Shyam-Chen と Dr-XYZ がレビューました。彼らの貢献があってこそ、本書はより幅広い読者に届けられています。感謝いたします。
この本の制作過程において、多くの方々から貴重な支援をいただきました。これらに限定されませんが:
本書の ePub 電子書籍生成ツールは zhongfq によって開発されました。彼の貢献に感謝します。読者により柔軟な読書方法を提供してくれました。
- 会社でのメンター、李熙博士に感謝します。ある会話で「早く行動しろ」と励ましていただき、この本を書く決意を固めることができました。
- ガールフレンドのBubbleに感謝します。この本の最初の読者として、アルゴリズム初心者の視点から多くの貴重な提案をいただき、この本を初心者により適したものにしてくださいました。
- Tengbao、Qibao、Feibaoに感謝します。この本のクリエイティブな名前を考えてくださり、みんなが初めて「Hello World!」を書いた時の素晴らしい思い出を呼び起こしてくれました。
- Xiaoquanに感謝します。知的財産に関する専門的な支援を提供してくださり、このオープンソース本の開発において重要な役割を果たしてくださいました。
- Sutongに感謝します。この本の美しいカバーとロゴをデザインしてくださり、私の要求で何度も修正を辛抱強く行ってくださいました。
- @squidfunk に感謝します。執筆と組版の提案、および彼が開発したオープンソースドキュメントテーマ [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) を提供してくださいました。
本書の執筆過程で、私は多くの方々の助けを得ました。
執筆の過程で、データ構造とアルゴリズムに関する多数の教科書や記事を深く研究しました。これらの作品は模範的なモデルとして機能し、この本の内容の正確性と品質を確保してくださいました。先人の方々の貴重な貢献に感謝いたします!
- 会社での私の指導教員である李汐博士に感謝します。ある対話の中で「すぐに行動しよう」と励ましてくださり、この本を書く決意を固めることができました;
- 私の恋人であり、本書の最初の読者でもある泡泡に感謝します。アルゴリズム初心者の視点から多くの貴重な提案をしてくれたおかげで、本書はより初心者に適したものになりました;
- 腾宝、琦宝、飞宝が本書に創造性あふれる名前を付けてくれたことに感謝します。みんなが最初のコード行「Hello World!」を書いた美しい記憶を呼び起こしてくれました;
- 校铨が知的財産の面で専門的な支援をしてくれたことに感謝します。これは本オープンソース書籍の改善に重要な役割を果たしました;
- 苏潼が本書の美しい表紙と logo をデザインし、私の完璧主義につき合って何度も辛抱強く修正してくれたことに感謝します;
- @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material) に感謝します。
この本は、理論と実践を組み合わせた学習を提唱しており、この点で ["Dive into Deep Learning"](https://github.com/d2l-ai/d2l-en) からインスピレーションを受けています。この優れた本をすべての読者に強くお勧めします
執筆の過程で、私はデータ構造とアルゴリズムに関する多くの教材や記事を読みました。これらの作品は本書に優れた手本を与え、本書の内容の正確性と品質を支えてくれました。ここに、すべての先生方と先人たちの卓越した貢献に感謝します
**継続的な支援と励ましにより、この興味深い仕事をすることを可能にしてくださった両親に心から感謝いたします**
本書は手と頭を同時に使う学習方法を提唱しています。この点で私は[『手を動かして学ぶ深層学習』](https://github.com/d2l-ai/d2l-zh)から大きな啓発を受けました。ここで読者の皆さんにこの優れた著作を強くお勧めします。
**心から両親に感謝します。いつも支え励ましてくれたからこそ、私はこの興味深いことに取り組む機会を得ることができました**。
+5 -5
View File
@@ -3,15 +3,15 @@ comments: true
icon: material/book-open-outline
---
# 第 0 章 &nbsp; 序文
# 第 0 章 &nbsp; はじめに
![序文](../assets/covers/chapter_preface.jpg){ class="cover-image" }
![はじめに](../assets/covers/chapter_preface.jpg){ class="cover-image" }
!!! abstract
アルゴリズムは美しい交響曲のようで、コードの一行一行がリズムのように流れています。
この本があなたの心の中で静かに響き、独で深い旋律を残ことを願っています。
アルゴリズムは美しい交響曲のようであり、コードの一行一行が旋律のように流れています。
この本があなたの心の中でそっと響き、独で深い旋律を残してくれることを願っています。
## 章の内容
+100 -87
View File
@@ -2,28 +2,28 @@
comments: true
---
# 0.2 &nbsp; 読み
# 0.2 &nbsp; 本書の使い
!!! tip
良の読書体験ために、このセクションを通読することをおめします。
適な読書体験を得るために、本節の内容を一通り読むことをおすすめします。
## 0.2.1 &nbsp; 記述規則
## 0.2.1 &nbsp; 文章スタイルの約束
- タイトルの後に「*」が付いた章は任意であり、比較的難易度の高い内容が含まれています。時間に制約がある場合は、これらをスキップすることをお勧めします
- 技術用語は太字(印刷版およびPDF版)または下線(Web版)で表示されます。えば<u>配列</u>などです。技術文書をより良く理解するために、これらに慣れることをおめします。
- **太字のテキスト**は重要な内容や要約文を示し、特別な注意を払う価値があります
- 特定の意味を持つ単語や句は「引用符」で示され、曖昧さを避けます。
- プログラミング言語間で一致しない用語については、この本はPythonに従います。えば、`null`を意味するために`None`を使用します。
- この本は、よりコンパクトなコンテンツレイアウトと引き換えに、プログラミング言語のコメント規約を部分的に無視しています。コメントは主に3つのタイプで構成されていますタイトルコメント、内容コメント、複数行コメント。
- 見出しの後に `*` が付いているのは選読章で、内容は比較的難しめです。時間が限られている場合は、先に読み飛ばしてもかまいません
- 専門用語は太字(紙書籍版と PDF 版)または下線付きWeb 版)で示します。たとえば<u>配列array</u>のようなものです。文献を読む際に役立つため、覚えておくことをおすすめします。
- 重要な内容やまとめの文は **太字** で示します。これらの文章には特に注意してください
- 特定の意味を持つ語句には“引用符”を付け、曖昧さを避けます。
- プログラミング言語ごとに用語が一致しない場合、本書では Python を基準とします。たとえば、“空”を表すのに `None` を使ます。
- 本書では、よりコンパクトなレイアウトのために、言語ごとのコメント規約を一部省略しています。コメントは主に3種類ありますタイトルコメント、内容コメント、複数行コメントです
=== "Python"
```python title=""
"""関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント"""
# 詳細を説明するためのコメント
"""タイトルコメント。関数、クラス、テストケースなどを示すために使います"""
# 内容コメント。コードを詳しく説明するために使います
"""
複数行
コメント
@@ -33,10 +33,10 @@ comments: true
=== "C++"
```cpp title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -46,10 +46,10 @@ comments: true
=== "Java"
```java title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -59,10 +59,10 @@ comments: true
=== "C#"
```csharp title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -72,10 +72,10 @@ comments: true
=== "Go"
```go title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -85,10 +85,10 @@ comments: true
=== "Swift"
```swift title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -98,10 +98,10 @@ comments: true
=== "JS"
```javascript title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -111,10 +111,10 @@ comments: true
=== "TS"
```typescript title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -124,10 +124,10 @@ comments: true
=== "Dart"
```dart title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -137,23 +137,21 @@ comments: true
=== "Rust"
```rust title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 詳細を説明するためのコメント
/**
* 複数行
* コメント
*/
// 内容コメント。コードを詳しく説明するために使います
// 複数行
// コメント
```
=== "C"
```c title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
@@ -163,82 +161,97 @@ comments: true
=== "Kotlin"
```kotlin title=""
/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */
// 詳細を説明するためのコメント
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */
// 内容コメント。コードを詳しく説明するために使います
/**
* 複数行
* コメント
*/
```
## 0.2.2 &nbsp; アニメーション図解による効率的学習
=== "Ruby"
テキストと比較して、動画や画像は情報密度が高く、より構造化されており、理解しやすくなっています。この本では、**重要で難しい概念は主にアニメーションと図解を通じて提示され**、テキストは説明と補足として機能します。
```ruby title=""
### タイトルコメント。関数、クラス、テストケースなどを示すために使います ###
下図に示すようなアニメーションや図解のある内容に遭遇した場合、**図の理解を優先し、テキストを補足として**、両方を統合して包括的な理解を得てください。
# 内容コメント。コードを詳しく説明するために使います
# 複数行
# コメント
```
## 0.2.2 &nbsp; アニメーション図解で効率よく学ぶ
文字と比べて、動画や画像は情報密度と構造化の度合いが高く、理解しやすいものです。本書では、**重要かつ難解な知識は主にアニメーションによる図解で示し**、文章は説明と補足を担います。
本書を読んでいて、ある内容に以下の図のようなアニメーション図解がある場合は、**図を主、文章を従として**、両方を合わせて理解してください。
![アニメーション図解の例](../index.assets/animation.gif){ class="animation-figure" }
<p align="center"> 図 0-2 &nbsp; アニメーション図解の例 </p>
## 0.2.3 &nbsp; コーディング実践による理解の深化
## 0.2.3 &nbsp; コード実践で理解を深める
この本のソースコードは[GitHubリポジトリ](https://github.com/krahets/hello-algo)でホストされています。下図に示すように、**ソースコードにはテスト例が付属しており、ワンクリックで実行できます**。
本書のサンプルコードは [GitHub リポジトリ](https://github.com/krahets/hello-algo) で管理されています。以下の図のように、**ソースコードにはテストケースが付いており、ワンクリックで実行できます**。
時間に余裕がある場合は、**自分でコードをタイプすることをおめします**。時間がない場合は、少なくともすべてのコードを読んで実行してください。
時間に余裕があれば、**コードを見ながら自分で一度書いてみることをおすすめします**。学習時間が限られている場合でも、少なくともすべてのコードに目を通し、実行してください。
コードを読むだけと比較して、コードを書くことは多くの場合、より多くの学習をもたらします。**実践による学習こそが真の学習方法です。**
コードを読むのに比べて、書く過程のほうが得られるものは多いものです。**手を動かしてこそ、本当に学んだことになります**
![コード実行例](../index.assets/running_code.gif){ class="animation-figure" }
<p align="center"> 図 0-3 &nbsp; コード実行例 </p>
コードを実行するための設定には、主に3つのステップが含まれます。
コードを実行する前準備は主に3ステップす。
**ステップ1:ローカルプログラミング環境をインストール**。付録の[チュートリアル](https://www.hello-algo.com/chapter_appendix/installation/)に従ってインストールするか、すでにインストールされている場合はこのステップをスキップしてください
**第1ステップ:ローカルプログラミング環境をインストールする**。付録の[チュートリアル](https://www.hello-algo.com/chapter_appendix/installation/)を参照してインストールしてください。すでにインストール済みであれば、この手順は省略できます
**ステップ2:コードリポジトリをクローンまたはダウンロード**。[GitHubリポジトリ](https://github.com/krahets/hello-algo)を訪問してください。
[Git](https://git-scm.com/downloads)がインストールされている場合は、次のコマンドを使用してリポジトリをクローンします:
**第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ファイルとして直接ダウンロードすることもできます。その後、ローカルで展開するだけです。
もちろん、以下の図に示す場所で“Download ZIPボタンをクリックし、コード圧縮ファイル直接ダウンロードしてローカルで展開することもできます。
![リポジトリのクローンとコードのダウンロード](suggestions.assets/download_code.png){ class="animation-figure" }
<p align="center"> 図 0-4 &nbsp; リポジトリのクローンとコードのダウンロード </p>
**ステップ3:ソースコードを実行**。下図に示すように、上部にファイル名が記載されたコードブロックについては、リポジトリの`codes`フォルダ対応するソースコードファイルを見つけることができます。これらのファイルはワンクリックで実行でき、不要なデバッグ時間を節約し、学習に集中できます。
**第3ステップ:ソースコードを実行する**。以下の図のように、上部にファイル名が表示されているコードブロックについては、リポジトリの `codes` フォルダ内に対応するソースコードファイルがあります。ソースコードファイルはワンクリックで実行できるため、不要なデバッグ時間を減らし、学習内容に集中できます。
![コードブロックと対応するソースコードファイル](suggestions.assets/code_md_to_repo.png){ class="animation-figure" }
<p align="center"> 図 0-5 &nbsp; コードブロックと対応するソースコードファイル </p>
## 0.2.4 &nbsp; 議論による共同学習
ローカルでコードを実行するだけでなく、**Web 版では Python コードの可視化実行にも対応しています**([pythontutor](https://pythontutor.com/) を利用)。以下の図のように、コードブロックの下にある“可視化実行”をクリックすると表示を展開し、アルゴリズムコードの実行過程を観察できます。また、“全画面表示”をクリックすると、より見やすい閲覧体験が得られます。
この本を読んでいる間、学べなかった点を飛ばさないでください。**コメントセクションで気軽に質問してください**。喜んでお答えし、通常2日以内に回答できます。
![Python コードの可視化実行](suggestions.assets/pythontutor_example.png){ class="animation-figure" }
下図に示すように、各章の下部にコメントセクションがあります。これらのコメントに注意を払うことをお勧めします。他の人が遭遇した問題を知ることで、知識のギャップを特定し、より深い思索を促すだけでなく、仲間の読者の質問に答えたり、洞察を共有したり、相互の向上を促進したりすることで寛大に貢献することも招待します。
<p align="center"> 図 0-6 &nbsp; Python コードの可視化実行 </p>
![コメントセクションの例](../index.assets/comment.gif){ class="animation-figure" }
## 0.2.4 &nbsp; 質問と議論を通じてともに成長する
<p align="center"> 図 0-6 &nbsp; コメントセクションの例 </p>
本書を読んでいて、理解できていない知識点を安易に読み飛ばさないでください。**コメント欄で気軽に質問してください**。私と仲間たちが誠意をもって回答し、通常は 2 日以内に返信します。
## 0.2.5 &nbsp; アルゴリズム学習パス
以下の図のように、Web 版では各章の下部にコメント欄があります。ぜひコメント欄の内容にも目を通してください。一方では、みんなが直面した問題を知ることで知識の抜けを補い、より深い思考を促せます。もう一方では、ほかの仲間の質問にも積極的に答え、見解を共有し、互いの成長を助けてほしいと思います。
全体的に、データ構造とアルゴリズムをマスターする旅は3つの段階に分けることができます:
![コメント欄の例](../index.assets/comment.gif){ class="animation-figure" }
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:知識体系の構築**。学習の面では、アルゴリズムコラム記事、解法フレームワーク、アルゴリズム教科書を読んで知識体系を継続的に豊かにすることができます。練習の面では、トピック別分類、一つの問題に対する複数の解法、複数の問題に対する一つの解法など、高度な戦略を試すことができます。これらの戦略に関する洞察は、さまざまなコミュニティで見つけることができます。
<p align="center"> 図 0-7 &nbsp; コメント欄の例 </p>
下図に示すように、この本は主に「段階1」をカバーしており、段階2と3により効率的に取り組むのに役立つことを目的としています。
## 0.2.5 &nbsp; アルゴリズム学習ロードマップ
![アルゴリズム学習パス](suggestions.assets/learning_route.png){ class="animation-figure" }
全体として見ると、データ構造とアルゴリズム学習過程は 3 つの段階に分けられます。
<p align="center"> 図 0-7 &nbsp; アルゴリズム学習パス </p>
1. **第 1 段階:アルゴリズム入門**。さまざまなデータ構造の特徴と使い方に慣れ、異なるアルゴリズムの原理、流れ、用途、効率などを学ぶ必要があります。
2. **第 2 段階:アルゴリズム問題を解く**。まずは人気の高い問題から取り組み、少なくとも 100 問は蓄積して、主流のアルゴリズム問題に慣れることをおすすめします。最初のうちは、“知識の忘却”が課題になるかもしれませんが、心配はいりません。これはごく自然なことです。“エビングハウスの忘却曲線”に沿って問題を復習すれば、通常は 3~5 回繰り返すことでしっかり記憶に定着します。おすすめの問題リストと学習計画は、この [GitHub リポジトリ](https://github.com/krahets/LeetCode-Book) を参照してください。
3. **第 3 段階:知識体系を構築する**。学習面では、アルゴリズムの連載記事、解法フレームワーク、教材などを読むことで、知識体系を継続的に充実させられます。問題演習の面では、トピック別分類、1 問多解、1 解多題といった発展的な戦略も試せます。関連する学習ノウハウは各コミュニティで見つけられます。
以下の図のように、本書の内容は主に“第 1 段階”を扱っており、第 2 段階と第 3 段階の学習をより効率的に進める助けとなることを目的としています。
![アルゴリズム学習ロードマップ](suggestions.assets/learning_route.png){ class="animation-figure" }
<p align="center"> 図 0-8 &nbsp; アルゴリズム学習ロードマップ </p>
+8 -6
View File
@@ -4,9 +4,11 @@ comments: true
# 0.3 &nbsp; まとめ
- この本の主な読者はアルゴリズムの初心者です。すでに基本的な知識をお持ちの場合、この本はアルゴリズムの知識を体系的に復習するのに役立ち、この本のソースコードは「コーディングツールキット」としても使用できます。
- この本は3つの主要なセクション、計算量解析、データ構造、アルゴリズムで構成されており、この分野のほとんどのトピックをカバーしています。
- アルゴリズムの初心者にとって、多くの回り道や一般的な落とし穴を避けるために、初期段階で入門書を読むことが重要です。
- 本書内のアニメーションと図は通常、重要なポイントと難しい知識を紹介するために使用されます。本を読む際にはこれらにより多くの注意を払う必要があります。
- 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、自分でコードをタイプすることを強くお勧めします。
- この本のWeb版の各章には議論セクションがあり、いつでも質問や洞察を共有することを歓迎します。
### 1. &nbsp; 重要ポイントの振り返り
- 本書の主な対象読者はアルゴリズム初学者です。すでにある程度の基礎がある場合でも、本書はアルゴリズム知識を体系的に振り返る助けとなり、書中のソースコードは「問題演習用ツール集」としても利用できます。
- 本書の内容は主に計算量解析、データ構造、アルゴリズムの三部からなり、この分野の大部分のテーマを網羅しています。
- アルゴリズム初心者にとって、学習初期の段階で入門書を読むことは非常に重要であり、多くの遠回りを避けられます。
- 本書のアニメーション図解は通常、重要な知識や難しい知識を紹介するために用いられます。本書を読む際は、これらの内容により多く注意を払うべきです。
- 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、実際に自分でコードを書くことを強く勧めます。
- 本書のWeb版の各章にはコメント欄が設けられており、疑問や見解をいつでも共有することを歓迎します。
+5 -5
View File
@@ -10,15 +10,15 @@ icon: material/bookshelf
[3] Robert Sedgewick, et al. Algorithms (4th Edition).
[4] Yan Weimin. Data Structures (C Language Version).
[4] 严蔚敏. データ構造(C 言語版).
[5] Deng Junhui. Data Structures (C++ Language Version, Third Edition).
[5] 邓俊辉. データ構造(C++ 言語版、第3版).
[6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).
[6] マーク・アレン・ワイス著,陈越訳. データ構造とアルゴリズム解析:Java言語による記述(第3版).
[7] Cheng Jie. Speaking of Data Structures.
[7] 程杰. データ構造の話.
[8] Wang Zheng. The Beauty of Data Structures and Algorithms.
[8] 王争. データ構造とアルゴリズムの美.
[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).
+478 -97
View File
@@ -4,30 +4,30 @@ comments: true
# 10.1 &nbsp; 二分探索
<u>二分探索</u>は分割統治戦略を用いる効率的な探索アルゴリズムです。配列内の要素の整列順序を利用し、各反復で探索区間を半分に減らしながら、目標要素見つるか探索区間が空になるまで続行します。
<u>二分探索binary search</u>は分割統治法に基づく効率的な探索アルゴリズムです。データが整列済みである性質を利用し、各ラウンドで探索範囲を半分に縮小し、目標要素見つるか探索区間が空になるまで続ます。
!!! question
長さ$n$の配列`nums`が与えられ、要素は重複なしで昇順に配列されています。この配列内の要素`target`のインデックスを見つけて返してください。配列に要素が含まれていない場合は$-1$を返してください。例を図に示します。
長さ $n$ の配列 `nums` が与えられます。要素は小さい順に並んでおり、重複しません。要素 `target` がこの配列内にある場合はそのインデックスを返し、含まれない場合は $-1$ を返してください。例を次の図に示します。
![Binary search example data](binary_search.assets/binary_search_example.png){ class="animation-figure" }
![二分探索の例](binary_search.assets/binary_search_example.png){ class="animation-figure" }
<p align="center"> 図 10-1 &nbsp; Binary search example data </p>
<p align="center"> 図 10-1 &nbsp; 二分探索の例 </p>
図に示すように、まず$i = 0$$j = n - 1$でポインタを初期化し、それぞれ配列の最初と最後の要素を指します。これらはまた全体の探索区間$[0, n - 1]$を表します。角括弧は閉区間をし、境界値自身も含むことに注意してください。
次の図に示すように、まずポインタ $i = 0$$j = n - 1$ を初期化し、それぞれ配列の先頭要素と末尾要素を指すようにして、探索区間 $[0, n - 1]$ を表します。角括弧は閉区間をし、境界値自体を含むことに注意してください。
そして、以下の2つのステップをループで実行する可能性があります。
次に、以下の 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. 中央のインデックス $m = \lfloor {(i + j) / 2} \rfloor$ を計算します。ここで $\lfloor \: \rfloor$ は切り捨てを表します。
2. `nums[m]``target` の大小関係を判定し、次の 3 つの場合に分かれます。
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$ を返します。
=== "<1>"
![Binary search process](binary_search.assets/binary_search_step1.png){ class="animation-figure" }
![二分探索の流れ](binary_search.assets/binary_search_step1.png){ class="animation-figure" }
=== "<2>"
![binary_search_step2](binary_search.assets/binary_search_step2.png){ class="animation-figure" }
@@ -47,50 +47,50 @@ comments: true
=== "<7>"
![binary_search_step7](binary_search.assets/binary_search_step7.png){ class="animation-figure" }
<p align="center"> 図 10-2 &nbsp; Binary search process </p>
<p align="center"> 図 10-2 &nbsp; 二分探索の流れ </p>
$i$と$j$が両方とも`int`型であるため、**$i + j$`int`型の範囲を超える可能性がある**ことは注目に値します。大きな数オーバーフローを避けるため、通常は式$m = \lfloor {i + (j - i) / 2} \rfloor$を使用して中点を計算します。
注意すべき点として、$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 はそれぞれ配列の最初の要素と最後の要素を指す
"""二分探索(両閉区間)"""
# 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
i, j = 0, len(nums) - 1
# 索区間が空になるまでループi > j のとき空)
# ループし、探索区間が空になったら終了する(i > j 空)
while i <= j:
# 理論的には、Pythonの数値は無限に大きくなることができる(メモリサイズに依存)ため、大きな数のオーバーフローを考慮する必要はない
m = i + (j - i) // 2 # 中点インデックス m を計算
# 理論、Python の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない
m = (i + j) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # この場合、target は区間 [m+1, j] にあることを示す
i = m + 1 # この場合、target は区間 [m+1, j] にある
elif nums[m] > target:
j = m - 1 # この場合、target は区間 [i, m-1] にあることを示す
j = m - 1 # この場合、target は区間 [i, m-1] にある
else:
return m # ターゲット要素が見つかったため、そのインデックスを返す
return -1 # ターゲット要素が見つからなかったため、-1 を返す
return m # 目標要素が見つかったそのインデックスを返す
return -1 # 目標要素が見つからなければ -1 を返す
```
=== "C++"
```cpp title="binary_search.cpp"
/* 二分探索(両閉区間) */
/* 二分探索(両閉区間) */
int binarySearch(vector<int> &nums, int target) {
// 両閉区間[0, n-1]を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素を指す
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
int i = 0, j = nums.size() - 1;
// 探索区間が空になるまでループ(i > jの時空になる
// ループし、探索区間が空になったら終了する(i > j で空
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックスmを計算
if (nums[m] < target) // この状況はtarget区間[m+1, 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]にあることを示す
else if (nums[m] > target) // この場合、target区間 [i, m-1] にある
j = m - 1;
else // ターゲット要素が見つかったため、そのインデックスを返す
else // 目標要素が見つかったそのインデックスを返す
return m;
}
// ターゲット要素が見つからなかったため、-1を返す
// 目標要素が見つからなければ -1 を返す
return -1;
}
```
@@ -98,21 +98,21 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲
=== "Java"
```java title="binary_search.java"
/* 二分探索(両閉区間) */
/* 二分探索(両閉区間) */
int binarySearch(int[] nums, int target) {
// 両閉区間 [0, n-1] を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素を指す
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
int i = 0, j = nums.length - 1;
// 探索区間が空になるまでループi > j のとき空)
// ループし、探索区間が空になったら終了する(i > j 空)
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) // この状況は target 区間 [m+1, j] にあることを示す
if (nums[m] < target) // この場合、target 区間 [m+1, j] にある
i = m + 1;
else if (nums[m] > target) // この状況は target 区間 [i, m-1] にあることを示す
else if (nums[m] > target) // この場合、target 区間 [i, m-1] にある
j = m - 1;
else // 目標要素見つけたので、そのインデックスを返す
else // 目標要素見つかったらそのインデックスを返す
return m;
}
// 目標要素見つけられなかったので、-1 を返す
// 目標要素見つからなければ -1 を返す
return -1;
}
```
@@ -120,90 +120,280 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲
=== "C#"
```csharp title="binary_search.cs"
[class]{binary_search}-[func]{BinarySearch}
/* 二分探索(両閉区間) */
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;
}
```
=== "Go"
```go title="binary_search.go"
[class]{}-[func]{binarySearch}
/* 二分探索(両閉区間) */
func binarySearch(nums []int, target int) int {
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
i, j := 0, len(nums)-1
// ループし、探索区間が空になったら終了する(i > j で空)
for i <= j {
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
}
```
=== "Swift"
```swift title="binary_search.swift"
[class]{}-[func]{binarySearch}
/* 二分探索(両閉区間) */
func binarySearch(nums: [Int], target: Int) -> Int {
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
var i = nums.startIndex
var j = nums.endIndex - 1
// ループし、探索区間が空になったら終了する(i > j で空)
while i <= j {
let 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
}
```
=== "JS"
```javascript title="binary_search.js"
[class]{}-[func]{binarySearch}
/* 二分探索(両閉区間) */
function binarySearch(nums, target) {
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
let i = 0,
j = nums.length - 1;
// ループし、探索区間が空になったら終了する(i > j で空)
while (i <= j) {
// 中点インデックス `m` を計算し、`parseInt()` で切り捨てる
const m = parseInt(i + (j - i) / 2);
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;
}
```
=== "TS"
```typescript title="binary_search.ts"
[class]{}-[func]{binarySearch}
/* 二分探索(両閉区間) */
function binarySearch(nums: number[], target: number): number {
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
let i = 0,
j = nums.length - 1;
// ループし、探索区間が空になったら終了する(i > j で空)
while (i <= j) {
// 中点インデックス m を計算
const m = Math.floor(i + (j - i) / 2);
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;
}
}
return -1; // 目標要素が見つからなければ -1 を返す
}
```
=== "Dart"
```dart title="binary_search.dart"
[class]{}-[func]{binarySearch}
/* 二分探索(両閉区間) */
int binarySearch(List<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;
}
```
=== "Rust"
```rust title="binary_search.rs"
[class]{}-[func]{binary_search}
/* 二分探索(両閉区間) */
fn binary_search(nums: &[i32], target: i32) -> i32 {
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
let mut i = 0;
let mut j = nums.len() as i32 - 1;
// ループし、探索区間が空になったら終了する(i > j で空)
while i <= j {
let m = i + (j - i) / 2; // 中点インデックス m を計算
if nums[m as usize] < target {
// この場合、target は区間 [m+1, j] にある
i = m + 1;
} else if nums[m as usize] > target {
// この場合、target は区間 [i, m-1] にある
j = m - 1;
} else {
// 目標要素が見つかったらそのインデックスを返す
return m;
}
}
// 目標要素が見つからなければ -1 を返す
return -1;
}
```
=== "C"
```c title="binary_search.c"
[class]{}-[func]{binarySearch}
/* 二分探索(両閉区間) */
int binarySearch(int *nums, int len, int target) {
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
int i = 0, j = len - 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;
}
```
=== "Kotlin"
```kotlin title="binary_search.kt"
[class]{}-[func]{binarySearch}
/* 二分探索(両閉区間) */
fun binarySearch(nums: IntArray, target: Int): Int {
// 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
var i = 0
var j = nums.size - 1
// ループし、探索区間が空になったら終了する(i > j で空)
while (i <= j) {
val 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
}
```
=== "Ruby"
```ruby title="binary_search.rb"
[class]{}-[func]{binary_search}
### 二分探索(両閉区間) ###
def binary_search(nums, target)
# 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す
i, j = 0, nums.length - 1
# ループし、探索区間が空になったら終了する(i > j で空)
while i <= j
# 理論上、Ruby の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない
m = (i + j) / 2 # 中点インデックス m を計算
if nums[m] < target
i = m + 1 # この場合、target は区間 [m+1, j] にある
elsif nums[m] > target
j = m - 1 # この場合、target は区間 [i, m-1] にある
else
return m # 目標要素が見つかったらそのインデックスを返す
end
end
-1 # 目標要素が見つからなければ -1 を返す
end
```
**時間計算量は$O(\log n)$です**:二分ループにおいて、区間は各ラウンドで半分に減少するため、反復回数は$\log_2 n$となります。
??? pythontutor "コードの可視化"
**空間計算量は$O(1)$です**:ポインタ$i$と$j$は定数サイズの空間を占有します。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%E3%80%82%E3%81%A4%E3%81%BE%E3%82%8A%20i%2C%20j%20%E3%81%AF%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E6%9C%AB%E5%B0%BE%E8%A6%81%E7%B4%A0%E3%82%92%E6%8C%87%E3%81%99%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%0A%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%97%E3%81%97%E3%80%81%E6%8E%A2%E7%B4%A2%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%89%E7%B5%82%E4%BA%86%E3%81%99%E3%82%8B%EF%BC%88i%20%3E%20j%20%E3%81%A7%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%90%86%E8%AB%96%E4%B8%8A%E3%80%81Python%20%E3%81%AE%E6%95%B0%E5%80%A4%E3%81%AF%E7%84%A1%E9%99%90%E3%81%AB%E5%A4%A7%E3%81%8D%E3%81%8F%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%9F%E3%82%81%EF%BC%88%E3%83%A1%E3%83%A2%E3%83%AA%E5%AE%B9%E9%87%8F%E3%81%AB%E4%BE%9D%E5%AD%98%EF%BC%89%E3%80%81%E5%A4%A7%E3%81%8D%E3%81%AA%E6%95%B0%E3%81%AE%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E8%80%83%E6%85%AE%E3%81%99%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%AF%E3%81%AA%E3%81%84%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%81%9D%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20-1%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%206%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20%3D%20%22%2C%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%E3%80%82%E3%81%A4%E3%81%BE%E3%82%8A%20i%2C%20j%20%E3%81%AF%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E6%9C%AB%E5%B0%BE%E8%A6%81%E7%B4%A0%E3%82%92%E6%8C%87%E3%81%99%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%0A%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%97%E3%81%97%E3%80%81%E6%8E%A2%E7%B4%A2%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%89%E7%B5%82%E4%BA%86%E3%81%99%E3%82%8B%EF%BC%88i%20%3E%20j%20%E3%81%A7%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%90%86%E8%AB%96%E4%B8%8A%E3%80%81Python%20%E3%81%AE%E6%95%B0%E5%80%A4%E3%81%AF%E7%84%A1%E9%99%90%E3%81%AB%E5%A4%A7%E3%81%8D%E3%81%8F%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%9F%E3%82%81%EF%BC%88%E3%83%A1%E3%83%A2%E3%83%AA%E5%AE%B9%E9%87%8F%E3%81%AB%E4%BE%9D%E5%AD%98%EF%BC%89%E3%80%81%E5%A4%A7%E3%81%8D%E3%81%AA%E6%95%B0%E3%81%AE%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E8%80%83%E6%85%AE%E3%81%99%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%AF%E3%81%AA%E3%81%84%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%81%9D%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20-1%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%206%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20%3D%20%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 10.1.1 &nbsp; 区間表現方法
**時間計算量は $O(\log n)$** :二分探索のループでは各ラウンドで区間が半分になるため、ループ回数は $\log_2 n$ です。
上記の閉区間の他に、もう一つの一般的な区間表現は「左閉右開」区間で、$[0, n)$として定義され、左境界は自身を含み、右境界は含みません。この表現では、$i = j$のとき区間$[i, j)$は空になります。
**空間計算量は $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を指す
# 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
i, j = 0, len(nums)
# 索区間が空になるまでループi = j のとき空)
# ループし、探索区間が空になったら終了する(i = j 空)
while i < j:
m = i + (j - i) // 2 # 中点インデックス m を計算
m = (i + j) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # この場合、target は区間 [m+1, j) にあることを示す
i = m + 1 # この場合、target は区間 [m+1, j) にある
elif nums[m] > target:
j = m # この場合、target は区間 [i, m) にあることを示す
j = m # この場合、target は区間 [i, m) にある
else:
return m # ターゲット要素が見つかったため、そのインデックスを返す
return -1 # ターゲット要素が見つからなかったため、-1 を返す
return m # 目標要素が見つかったそのインデックスを返す
return -1 # 目標要素が見つからなければ -1 を返す
```
=== "C++"
@@ -211,19 +401,19 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲
```cpp title="binary_search.cpp"
/* 二分探索(左閉右開区間) */
int binarySearchLCRO(vector<int> &nums, int target) {
// 左閉右開区間[0, n)を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素+1を指す
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
int i = 0, j = nums.size();
// 探索区間が空になるまでループ(i = jの時空になる
// ループし、探索区間が空になったら終了する(i = j で空
while (i < j) {
int m = i + (j - i) / 2; // 中点インデックスmを計算
if (nums[m] < target) // この状況はtarget区間[m+1, 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)にあることを示す
else if (nums[m] > target) // この場合、target区間 [i, m) にある
j = m;
else // ターゲット要素が見つかったため、そのインデックスを返す
else // 目標要素が見つかったそのインデックスを返す
return m;
}
// ターゲット要素が見つからなかったため、-1を返す
// 目標要素が見つからなければ -1 を返す
return -1;
}
```
@@ -233,19 +423,19 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲
```java title="binary_search.java"
/* 二分探索(左閉右開区間) */
int binarySearchLCRO(int[] nums, int target) {
// 左閉右開区間 [0, n) を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素+1を指す
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
int i = 0, j = nums.length;
// 探索区間が空になるまでループi = j のとき空)
// ループし、探索区間が空になったら終了する(i = j 空)
while (i < j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) // この状況は target 区間 [m+1, j) にあることを示す
if (nums[m] < target) // この場合、target 区間 [m+1, j) にある
i = m + 1;
else if (nums[m] > target) // この状況は target 区間 [i, m) にあることを示す
else if (nums[m] > target) // この場合、target 区間 [i, m) にある
j = m;
else // 目標要素見つけたので、そのインデックスを返す
else // 目標要素見つかったらそのインデックスを返す
return m;
}
// 目標要素見つけられなかったので、-1 を返す
// 目標要素見つからなければ -1 を返す
return -1;
}
```
@@ -253,80 +443,271 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲
=== "C#"
```csharp title="binary_search.cs"
[class]{binary_search}-[func]{BinarySearchLCRO}
/* 二分探索(左閉右開区間) */
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;
}
```
=== "Go"
```go title="binary_search.go"
[class]{}-[func]{binarySearchLCRO}
/* 二分探索(左閉右開区間) */
func binarySearchLCRO(nums []int, target int) int {
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
i, j := 0, len(nums)
// ループし、探索区間が空になったら終了する(i = j で空)
for i < j {
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
}
```
=== "Swift"
```swift title="binary_search.swift"
[class]{}-[func]{binarySearchLCRO}
/* 二分探索(左閉右開区間) */
func binarySearchLCRO(nums: [Int], target: Int) -> Int {
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
var i = nums.startIndex
var j = nums.endIndex
// ループし、探索区間が空になったら終了する(i = j で空)
while i < j {
let 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
}
```
=== "JS"
```javascript title="binary_search.js"
[class]{}-[func]{binarySearchLCRO}
/* 二分探索(左閉右開区間) */
function binarySearchLCRO(nums, target) {
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
let i = 0,
j = nums.length;
// ループし、探索区間が空になったら終了する(i = j で空)
while (i < j) {
// 中点インデックス `m` を計算し、`parseInt()` で切り捨てる
const m = parseInt(i + (j - i) / 2);
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;
}
```
=== "TS"
```typescript title="binary_search.ts"
[class]{}-[func]{binarySearchLCRO}
/* 二分探索(左閉右開区間) */
function binarySearchLCRO(nums: number[], target: number): number {
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
let i = 0,
j = nums.length;
// ループし、探索区間が空になったら終了する(i = j で空)
while (i < j) {
// 中点インデックス m を計算
const m = Math.floor(i + (j - i) / 2);
if (nums[m] < target) {
// この場合、target は区間 [m+1, j) にある
i = m + 1;
} else if (nums[m] > target) {
// この場合、target は区間 [i, m) にある
j = m;
} else {
// 目標要素が見つかったらそのインデックスを返す
return m;
}
}
return -1; // 目標要素が見つからなければ -1 を返す
}
```
=== "Dart"
```dart title="binary_search.dart"
[class]{}-[func]{binarySearchLCRO}
/* 二分探索(左閉右開区間) */
int binarySearchLCRO(List<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;
}
```
=== "Rust"
```rust title="binary_search.rs"
[class]{}-[func]{binary_search_lcro}
/* 二分探索(左閉右開区間) */
fn binary_search_lcro(nums: &[i32], target: i32) -> i32 {
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
let mut i = 0;
let mut j = nums.len() as i32;
// ループし、探索区間が空になったら終了する(i = j で空)
while i < j {
let m = i + (j - i) / 2; // 中点インデックス m を計算
if nums[m as usize] < target {
// この場合、target は区間 [m+1, j) にある
i = m + 1;
} else if nums[m as usize] > target {
// この場合、target は区間 [i, m) にある
j = m;
} else {
// 目標要素が見つかったらそのインデックスを返す
return m;
}
}
// 目標要素が見つからなければ -1 を返す
return -1;
}
```
=== "C"
```c title="binary_search.c"
[class]{}-[func]{binarySearchLCRO}
/* 二分探索(左閉右開区間) */
int binarySearchLCRO(int *nums, int len, int target) {
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
int i = 0, j = len;
// ループし、探索区間が空になったら終了する(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;
}
```
=== "Kotlin"
```kotlin title="binary_search.kt"
[class]{}-[func]{binarySearchLCRO}
/* 二分探索(左閉右開区間) */
fun binarySearchLCRO(nums: IntArray, target: Int): Int {
// 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
var i = 0
var j = nums.size
// ループし、探索区間が空になったら終了する(i = j で空)
while (i < j) {
val 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
}
```
=== "Ruby"
```ruby title="binary_search.rb"
[class]{}-[func]{binary_search_lcro}
### 二分探索(左閉右開区間) ###
def binary_search_lcro(nums, target)
# 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す
i, j = 0, nums.length
# ループし、探索区間が空になったら終了する(i = j で空)
while i < j
# 中点インデックス m を計算
m = (i + j) / 2
if nums[m] < target
i = m + 1 # この場合、target は区間 [m+1, j) にある
elsif nums[m] > target
j = m - 1 # この場合、target は区間 [i, m) にある
else
return m # 目標要素が見つかったらそのインデックスを返す
end
end
-1 # 目標要素が見つからなければ -1 を返す
end
```
下図に示すように、2つの区間表現タイプにおいて、二分探索アルゴリズムの初期化、ループ条件、区間縮小操作が異なります。
??? pythontutor "コードの可視化"
「閉区間」表現では両方の境界が包含的であるため、ポインタ$i$と$j$による区間縮小操作も対称的です。これによりエラーが発生しにくくなるため、**一般的に「閉区間」アプローチの使用が推奨されます**。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_lcro%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8C%BA%E9%96%93%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8C%BA%E9%96%93%20%5B0%2C%20n%29%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%E3%80%82%E3%81%A4%E3%81%BE%E3%82%8A%20i%2C%20j%20%E3%81%AF%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E6%9C%AB%E5%B0%BE%E8%A6%81%E7%B4%A0%2B1%E3%82%92%E6%8C%87%E3%81%99%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%0A%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%97%E3%81%97%E3%80%81%E6%8E%A2%E7%B4%A2%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%89%E7%B5%82%E4%BA%86%E3%81%99%E3%82%8B%EF%BC%88i%20%3D%20j%20%E3%81%A7%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%29%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m%29%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%81%9D%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20-1%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8C%BA%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search_lcro%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%206%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20%3D%20%22%2C%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_lcro%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8C%BA%E9%96%93%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8C%BA%E9%96%93%20%5B0%2C%20n%29%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%E3%80%82%E3%81%A4%E3%81%BE%E3%82%8A%20i%2C%20j%20%E3%81%AF%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E6%9C%AB%E5%B0%BE%E8%A6%81%E7%B4%A0%2B1%E3%82%92%E6%8C%87%E3%81%99%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%0A%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%97%E3%81%97%E3%80%81%E6%8E%A2%E7%B4%A2%E5%8C%BA%E9%96%93%E3%81%8C%E7%A9%BA%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%89%E7%B5%82%E4%BA%86%E3%81%99%E3%82%8B%EF%BC%88i%20%3D%20j%20%E3%81%A7%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%29%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20%20%23%20%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m%29%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%81%9D%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20-1%20%20%23%20%E7%9B%AE%E6%A8%99%E8%A6%81%E7%B4%A0%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8C%BA%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search_lcro%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E5%AF%BE%E8%B1%A1%E8%A6%81%E7%B4%A0%206%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20%3D%20%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
![Two types of interval definitions](binary_search.assets/binary_search_ranges.png){ class="animation-figure" }
次の図に示すように、2 種類の区間表現では、二分探索アルゴリズムの初期化、ループ条件、区間の縮小操作がそれぞれ異なります。
<p align="center"> 図 10-3 &nbsp; Two types of interval definitions </p>
「両閉区間」の表現では左右の境界がどちらも閉区間として定義されるため、ポインタ $i$ とポインタ $j$ による区間縮小の操作も対称になります。このほうがミスをしにくいため、**一般には「両閉区間」の書き方を推奨します**。
## 10.1.2 &nbsp; 利点と制限
![2 種類の区間定義](binary_search.assets/binary_search_ranges.png){ class="animation-figure" }
二分探索は時間と空間の両方の面で良好な性能を示します。
<p align="center"> 図 10-3 &nbsp; 2 種類の区間定義 </p>
- 二分探索は時間効率が良いです。大きなデータセットでは、対数時間計算量が大きな利点を提供します。例えば、サイズ$n = 2^{20}$のデータセットが与えられた場合、線形探索は$2^{20} = 1048576$回の反復が必要ですが、二分探索は$\log_2 2^{20} = 20$回のループのみで済みます。
- 二分探索には追加の空間が必要ありません。追加の空間に依存する探索アルゴリズム(ハッシュ探索など)と比較して、二分探索はより空間効率的です。
## 10.1.2 &nbsp; 利点と限界
しかし、二分探索は以下の懸念により、すべてのシナリオに適しているとは限りません
二分探索は時間と空間の両面で優れた性能を持ちます
- 二分探索はソート済みデータにのみ適用できます。未ソートのデータは二分探索を適用する前にソートする必要があり、ソートアルゴリズムは通常$O(n \log n)$の時間計算量を持つため、これは価値がないかもしれません。このコストは線形探索よりも高く、二分探索自体は言うまでもありません。頻繁な挿入があるシナリオでは、配列を順序に保つコストは非常に高く、特定の位置に新しい要素を挿入する時間計算量は$O(n)$です。
- 二分探索は配列のみを使用できます。二分探索には非連続(ジャンプ)要素アクセスが必要で、これは連結リストでは非効率的です。そのため、連結リストや連結リストに基づくデータ構造はこのアルゴリズムに適していない可能性があります。
- 線形探索は小さなデータセットでより良い性能を示します。線形探索では各反復で1つの判定操作のみが必要ですが、二分探索では1つの加算、1つの除算、1つから3つの判定操作、1つの加算(減算)を含み、合計4つから6つの操作が必要です。そのため、データサイズ$n$が小さい場合、線形探索は二分探索よりも高速です。
- 二分探索は時間効率が高いです。データ量が大きい場合、対数時間計算量は大きな優位性を持ちます。たとえば、データサイズ $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$ が小さいときは、線形探索のほうがかえって速くなります。
+298 -69
View File
@@ -4,47 +4,47 @@ comments: true
# 10.3 &nbsp; 二分探索の境界
## 10.3.1 &nbsp; 左境界を見つける
## 10.3.1 &nbsp; 左境界を探す
!!! question
重複要素を含む可能性がある長さ$n$のソート済み配列`nums`が与えられ、最も左の要素`target`のインデックスを返してください。要素が配列に存在しない場合は、$-1$を返してください
長さ $n$ のソート済み配列 `nums` が与えられ、その中には重複要素が含まれる可能性があります。配列内で最も左にある要素 `target` のインデックスを返してください。配列にこの要素が含まれない場合は、$-1$ を返します
挿入位置の二分探索方法を思い出すと、探索完了後、インデックス$i$は`target`の最も左の出現を指します。したがって、**挿入位置の探索は本質的に最も左の`target`のインデックスを見つけることと同じです**。
二分探索で挿入位置を求める方法を思い出すと、探索完了後に $i$ は最も左にある `target` を指します。**したがって、挿入位置を探すことの本質は、最も左にある `target` のインデックスを探すことです**。
挿入位置を見つける関数を使用して`target`の左境界を見つけることができます。配列に`target`が含まれていない可能性があることに注意してください。これは以下の2つの結果につながる可能性があります
挿入位置を探す関数を使って左端境界を求めることを考えます。なお、配列に `target` が含まれない場合があり、そのときは次の 2 つの結果が起こりえます
- 挿入位置のインデックス$i$が範囲外です
- 要素`nums[i]``target`と等しくありません
- 挿入位置のインデックス $i$ が範囲外になる
- 要素 `nums[i]``target` と等しくない
これらの場合、単に$-1$を返します。コードは以下のりです:
上の 2 つの状況に当てはまる場合は、直接 $-1$ を返せば十分です。コードは以下のとおりです:
=== "Python"
```python title="binary_search_edge.py"
def binary_search_left_edge(nums: list[int], target: int) -> int:
"""最左端のターゲットの二分探索"""
# ターゲットの挿入位置を見つけることと同等
"""最も左の target を二分探索"""
# target の挿入位置を探すのと等価
i = binary_search_insertion(nums, target)
# ターゲットが見つからなかった場合、-1 を返す
# target が見つからなければ、-1 を返す
if i == len(nums) or nums[i] != target:
return -1
# ターゲットが見つかった場合、インデックス i を返す
# target が見つかった、インデックス i を返す
return i
```
=== "C++"
```cpp title="binary_search_edge.cpp"
/* 最左のターゲットの二分探索 */
/* 最左の target を二分探索 */
int binarySearchLeftEdge(vector<int> &nums, int target) {
// targetの挿入ポイントを見つけることと等価
// target の挿入位置を探すのと等価
int i = binarySearchInsertion(nums, target);
// targetが見つからなかったため、-1を返す
// target が見つからなければ、-1 を返す
if (i == nums.size() || nums[i] != target) {
return -1;
}
// targetが見つかったため、インデックスiを返す
// target が見つかった、インデックス i を返す
return i;
}
```
@@ -54,13 +54,13 @@ comments: true
```java title="binary_search_edge.java"
/* 最も左の target を二分探索 */
int binarySearchLeftEdge(int[] nums, int target) {
// target の挿入点を見つけることと等価
// target の挿入位置を探すのと等価
int i = binary_search_insertion.binarySearchInsertion(nums, target);
// target 見つけられなかったので、-1 を返す
// target 見つからなければ、-1 を返す
if (i == nums.length || nums[i] != target) {
return -1;
}
// target 見つけたので、インデックス i を返す
// target 見つかったら、インデックス i を返す
return i;
}
```
@@ -68,111 +68,215 @@ comments: true
=== "C#"
```csharp title="binary_search_edge.cs"
[class]{binary_search_edge}-[func]{BinarySearchLeftEdge}
/* 最も左の 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;
}
```
=== "Go"
```go title="binary_search_edge.go"
[class]{}-[func]{binarySearchLeftEdge}
/* 最も左の target を二分探索 */
func binarySearchLeftEdge(nums []int, target int) int {
// target の挿入位置を探すのと等価
i := binarySearchInsertion(nums, target)
// target が見つからなければ、-1 を返す
if i == len(nums) || nums[i] != target {
return -1
}
// target が見つかったら、インデックス i を返す
return i
}
```
=== "Swift"
```swift title="binary_search_edge.swift"
[class]{}-[func]{binarySearchLeftEdge}
/* 最も左の target を二分探索 */
func binarySearchLeftEdge(nums: [Int], target: Int) -> Int {
// target の挿入位置を探すのと等価
let i = binarySearchInsertion(nums: nums, target: target)
// target が見つからなければ、-1 を返す
if i == nums.endIndex || nums[i] != target {
return -1
}
// target が見つかったら、インデックス i を返す
return i
}
```
=== "JS"
```javascript title="binary_search_edge.js"
[class]{}-[func]{binarySearchLeftEdge}
/* 最も左の target を二分探索 */
function binarySearchLeftEdge(nums, target) {
// target の挿入位置を探すのと等価
const i = binarySearchInsertion(nums, target);
// target が見つからなければ、-1 を返す
if (i === nums.length || nums[i] !== target) {
return -1;
}
// target が見つかったら、インデックス i を返す
return i;
}
```
=== "TS"
```typescript title="binary_search_edge.ts"
[class]{}-[func]{binarySearchLeftEdge}
/* 最も左の target を二分探索 */
function binarySearchLeftEdge(nums: Array<number>, target: number): number {
// target の挿入位置を探すのと等価
const i = binarySearchInsertion(nums, target);
// target が見つからなければ、-1 を返す
if (i === nums.length || nums[i] !== target) {
return -1;
}
// target が見つかったら、インデックス i を返す
return i;
}
```
=== "Dart"
```dart title="binary_search_edge.dart"
[class]{}-[func]{binarySearchLeftEdge}
/* 最も左の target を二分探索 */
int binarySearchLeftEdge(List<int> nums, int target) {
// target の挿入位置を探すのと等価
int i = binarySearchInsertion(nums, target);
// target が見つからなければ、-1 を返す
if (i == nums.length || nums[i] != target) {
return -1;
}
// target が見つかったら、インデックス i を返す
return i;
}
```
=== "Rust"
```rust title="binary_search_edge.rs"
[class]{}-[func]{binary_search_left_edge}
/* 最も左の target を二分探索 */
fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {
// target の挿入位置を探すのと等価
let i = binary_search_insertion(nums, target);
// target が見つからなければ、-1 を返す
if i == nums.len() as i32 || nums[i as usize] != target {
return -1;
}
// target が見つかったら、インデックス i を返す
i
}
```
=== "C"
```c title="binary_search_edge.c"
[class]{}-[func]{binarySearchLeftEdge}
/* 最も左の target を二分探索 */
int binarySearchLeftEdge(int *nums, int numSize, int target) {
// target の挿入位置を探すのと等価
int i = binarySearchInsertion(nums, numSize, target);
// target が見つからなければ、-1 を返す
if (i == numSize || nums[i] != target) {
return -1;
}
// target が見つかったら、インデックス i を返す
return i;
}
```
=== "Kotlin"
```kotlin title="binary_search_edge.kt"
[class]{}-[func]{binarySearchLeftEdge}
/* 最も左の target を二分探索 */
fun binarySearchLeftEdge(nums: IntArray, target: Int): Int {
// target の挿入位置を探すのと等価
val i = binarySearchInsertion(nums, target)
// target が見つからなければ、-1 を返す
if (i == nums.size || nums[i] != target) {
return -1
}
// target が見つかったら、インデックス i を返す
return i
}
```
=== "Ruby"
```ruby title="binary_search_edge.rb"
[class]{}-[func]{binary_search_left_edge}
### target の最左位置を二分探索 ###
def binary_search_left_edge(nums, target)
# target の挿入位置を探すのと等価
i = binary_search_insertion(nums, target)
# target が見つからなければ、-1 を返す
return -1 if i == nums.length || nums[i] != target
i # target が見つかったら、インデックス i を返す
end
```
## 10.3.2 &nbsp; 右境界を見つける
??? pythontutor "コードの可視化"
`target`の最も右の出現をどのように見つけるでしょうか?最も直接的な方法は、`nums[m] == target`の場合に探索境界を調整する方法を変更して、従来の二分探索ロジックを修正することです。コードはここでは省略されています。興味がある場合は、自分でコードを実装してみてください。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%82%E3%82%8A%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%82%88%E3%82%8A%E5%B0%8F%E3%81%95%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%23%20%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_left_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E3%82%82%E5%B7%A6%E3%81%AE%20target%20%E3%82%92%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%22%22%22%0A%20%20%20%20%23%20target%20%E3%81%AE%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%E3%81%AE%E3%81%A8%E7%AD%89%E4%BE%A1%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%20target%29%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20if%20i%20%3D%3D%20len%28nums%29%20or%20nums%5Bi%5D%20%21%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%80%81%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%AB%E3%82%80%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E5%B7%A6%E7%AB%AF%E3%81%A8%E5%8F%B3%E7%AB%AF%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_left_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%B7%A6%E7%AB%AF%E3%81%AE%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%82%E3%82%8A%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%82%88%E3%82%8A%E5%B0%8F%E3%81%95%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%23%20%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_left_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E3%82%82%E5%B7%A6%E3%81%AE%20target%20%E3%82%92%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%22%22%22%0A%20%20%20%20%23%20target%20%E3%81%AE%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%E3%81%AE%E3%81%A8%E7%AD%89%E4%BE%A1%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%20target%29%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20if%20i%20%3D%3D%20len%28nums%29%20or%20nums%5Bi%5D%20%21%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%80%81%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%AB%E3%82%80%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E5%B7%A6%E7%AB%AF%E3%81%A8%E5%8F%B3%E7%AB%AF%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_left_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%B7%A6%E7%AB%AF%E3%81%AE%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
以下では、さらに2つの巧妙な方法を紹介します。
## 10.3.2 &nbsp; 右端境界を探す
### 1. &nbsp; 左境界探索を再利用する
では、最も右にある `target` はどのように探せるでしょうか。最も直接的な方法はコードを修正し、`nums[m] == target` の場合のポインタの縮小操作を置き換えることです。ここではコードを省略するので、興味があれば自分で実装してみてください。
`target`の最も右の出現を見つけるには、最も左の`target`を見つけるために使用された関数を再利用できます。具体的には、最も右のターゲットの探索を最も左のターゲット + 1の探索に変換します。
ここでは、より巧妙な 2 つの方法を紹介します。
下図に示すように、探索完了後、ポインタ$i$は最も左の`target + 1`(存在する場合)を指し、ポインタ$j$は`target`の最も右の出現を指します。したがって、$j$を返すことで右境界が得られます。
### 1. &nbsp; 左端境界探索を再利用する
![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" }
実際には、最も左の要素を探す関数を利用して最も右の要素を探せます。具体的には、**最も右にある `target` を探すことを、最も左にある `target + 1` を探すことに変換します**。
<p align="center"> 図 10-7 &nbsp; Transforming the search for the right boundary into the search for the left boundary </p>
下図のように、探索完了後、ポインタ $i$ は最も左にある `target + 1`(存在する場合)を指し、$j$ は最も右にある `target` を指します。**したがって $j$ を返せばよいです**。
返される挿入位置は$i$であることに注意してください。したがって、$j$を得るためには1を引く必要があります:
![右端境界の探索を左端境界の探索に変換する](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" }
<p align="center"> 図 10-7 &nbsp; 右端境界の探索を左端境界の探索に変換する </p>
返される挿入位置は $i$ なので、そこから $1$ を引いて $j$ を得る必要があることに注意してください:
=== "Python"
```python title="binary_search_edge.py"
def binary_search_right_edge(nums: list[int], target: int) -> int:
"""最右端のターゲットの二分探索"""
# 最左端のターゲット + 1 を見つけることに変換
"""最も右の target を二分探索"""
# 最左の target + 1 を探す問題に変換する
i = binary_search_insertion(nums, target + 1)
# j は最右端のターゲットを指し、i はターゲットより大きい最初の要素を指す
# j は最も右の target を指し、i は target より大きい最初の要素を指す
j = i - 1
# ターゲットが見つからなかった場合、-1 を返す
# target が見つからなければ、-1 を返す
if j == -1 or nums[j] != target:
return -1
# ターゲットが見つかった場合、インデックス j を返す
# target が見つかった、インデックス j を返す
return j
```
=== "C++"
```cpp title="binary_search_edge.cpp"
/* 最右のターゲットの二分探索 */
/* 最右の target を二分探索 */
int binarySearchRightEdge(vector<int> &nums, int target) {
// 最左のtarget + 1を見つけることに変換
// 最左の target + 1 を探す問題に変換する
int i = binarySearchInsertion(nums, target + 1);
// jは最右のターゲットを指し、itargetより大きい最初の要素を指す
// j は最右の target を指し、itarget より大きい最初の要素を指す
int j = i - 1;
// targetが見つからなかったため、-1を返す
// target が見つからなければ、-1 を返す
if (j == -1 || nums[j] != target) {
return -1;
}
// targetが見つかったため、インデックスjを返す
// target が見つかった、インデックス j を返す
return j;
}
```
@@ -182,15 +286,15 @@ comments: true
```java title="binary_search_edge.java"
/* 最も右の target を二分探索 */
int binarySearchRightEdge(int[] nums, int target) {
// 最左の target + 1 を見つけることに変換
// 最左の target + 1 を探す問題に変換する
int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);
// j は最も右の target を指し、i は target より大きい最初の要素を指す
int j = i - 1;
// target 見つけられなかったので、-1 を返す
// target 見つからなければ、-1 を返す
if (j == -1 || nums[j] != target) {
return -1;
}
// target 見つけたので、インデックス j を返す
// target 見つかったら、インデックス j を返す
return j;
}
```
@@ -198,77 +302,202 @@ comments: true
=== "C#"
```csharp title="binary_search_edge.cs"
[class]{binary_search_edge}-[func]{BinarySearchRightEdge}
/* 最も右の 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;
}
```
=== "Go"
```go title="binary_search_edge.go"
[class]{}-[func]{binarySearchRightEdge}
/* 最も右の target を二分探索 */
func binarySearchRightEdge(nums []int, target int) int {
// 最左の target + 1 を探す問題に変換する
i := binarySearchInsertion(nums, target+1)
// j は最も右の target を指し、i は target より大きい最初の要素を指す
j := i - 1
// target が見つからなければ、-1 を返す
if j == -1 || nums[j] != target {
return -1
}
// target が見つかったら、インデックス j を返す
return j
}
```
=== "Swift"
```swift title="binary_search_edge.swift"
[class]{}-[func]{binarySearchRightEdge}
/* 最も右の target を二分探索 */
func binarySearchRightEdge(nums: [Int], target: Int) -> Int {
// 最左の target + 1 を探す問題に変換する
let i = binarySearchInsertion(nums: nums, target: target + 1)
// j は最も右の target を指し、i は target より大きい最初の要素を指す
let j = i - 1
// target が見つからなければ、-1 を返す
if j == -1 || nums[j] != target {
return -1
}
// target が見つかったら、インデックス j を返す
return j
}
```
=== "JS"
```javascript title="binary_search_edge.js"
[class]{}-[func]{binarySearchRightEdge}
/* 最も右の target を二分探索 */
function binarySearchRightEdge(nums, target) {
// 最左の target + 1 を探す問題に変換する
const i = binarySearchInsertion(nums, target + 1);
// j は最も右の target を指し、i は target より大きい最初の要素を指す
const j = i - 1;
// target が見つからなければ、-1 を返す
if (j === -1 || nums[j] !== target) {
return -1;
}
// target が見つかったら、インデックス j を返す
return j;
}
```
=== "TS"
```typescript title="binary_search_edge.ts"
[class]{}-[func]{binarySearchRightEdge}
/* 最も右の target を二分探索 */
function binarySearchRightEdge(nums: Array<number>, target: number): number {
// 最左の target + 1 を探す問題に変換する
const i = binarySearchInsertion(nums, target + 1);
// j は最も右の target を指し、i は target より大きい最初の要素を指す
const j = i - 1;
// target が見つからなければ、-1 を返す
if (j === -1 || nums[j] !== target) {
return -1;
}
// target が見つかったら、インデックス j を返す
return j;
}
```
=== "Dart"
```dart title="binary_search_edge.dart"
[class]{}-[func]{binarySearchRightEdge}
/* 最も右の target を二分探索 */
int binarySearchRightEdge(List<int> nums, int target) {
// 最左の target + 1 を探す問題に変換する
int i = 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;
}
```
=== "Rust"
```rust title="binary_search_edge.rs"
[class]{}-[func]{binary_search_right_edge}
/* 最も右の target を二分探索 */
fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {
// 最左の target + 1 を探す問題に変換する
let i = binary_search_insertion(nums, target + 1);
// j は最も右の target を指し、i は target より大きい最初の要素を指す
let j = i - 1;
// target が見つからなければ、-1 を返す
if j == -1 || nums[j as usize] != target {
return -1;
}
// target が見つかったら、インデックス j を返す
j
}
```
=== "C"
```c title="binary_search_edge.c"
[class]{}-[func]{binarySearchRightEdge}
/* 最も右の target を二分探索 */
int binarySearchRightEdge(int *nums, int numSize, int target) {
// 最左の target + 1 を探す問題に変換する
int i = binarySearchInsertion(nums, numSize, target + 1);
// j は最も右の target を指し、i は target より大きい最初の要素を指す
int j = i - 1;
// target が見つからなければ、-1 を返す
if (j == -1 || nums[j] != target) {
return -1;
}
// target が見つかったら、インデックス j を返す
return j;
}
```
=== "Kotlin"
```kotlin title="binary_search_edge.kt"
[class]{}-[func]{binarySearchRightEdge}
/* 最も右の target を二分探索 */
fun binarySearchRightEdge(nums: IntArray, target: Int): Int {
// 最左の target + 1 を探す問題に変換する
val i = binarySearchInsertion(nums, target + 1)
// j は最も右の target を指し、i は target より大きい最初の要素を指す
val j = i - 1
// target が見つからなければ、-1 を返す
if (j == -1 || nums[j] != target) {
return -1
}
// target が見つかったら、インデックス j を返す
return j
}
```
=== "Ruby"
```ruby title="binary_search_edge.rb"
[class]{}-[func]{binary_search_right_edge}
### target の最右位置を二分探索 ###
def binary_search_right_edge(nums, target)
# 最左の target + 1 を探す問題に変換する
i = binary_search_insertion(nums, target + 1)
# j は最も右の target を指し、i は target より大きい最初の要素を指す
j = i - 1
# target が見つからなければ、-1 を返す
return -1 if j == -1 || nums[j] != target
j # target が見つかったら、インデックス j を返す
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%82%E3%82%8A%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%82%88%E3%82%8A%E5%B0%8F%E3%81%95%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%23%20%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_right_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E3%82%82%E5%8F%B3%E3%81%AE%20target%20%E3%82%92%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%22%22%22%0A%20%20%20%20%23%20%E6%9C%80%E5%B7%A6%E3%81%AE%20target%20%2B%201%20%E3%82%92%E6%8E%A2%E3%81%99%E5%95%8F%E9%A1%8C%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%20target%20%2B%201%29%0A%20%20%20%20%23%20j%20%E3%81%AF%E6%9C%80%E3%82%82%E5%8F%B3%E3%81%AE%20target%20%E3%82%92%E6%8C%87%E3%81%97%E3%80%81i%20%E3%81%AF%20target%20%E3%82%88%E3%82%8A%E5%A4%A7%E3%81%8D%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E6%8C%87%E3%81%99%0A%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20if%20j%20%3D%3D%20-1%20or%20nums%5Bj%5D%20%21%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%80%81%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20j%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20j%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%AB%E3%82%80%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E5%B7%A6%E7%AB%AF%E3%81%A8%E5%8F%B3%E7%AB%AF%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_right_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%8F%B3%E7%AB%AF%E3%81%AE%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%82%E3%82%8A%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%82%88%E3%82%8A%E5%B0%8F%E3%81%95%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%23%20%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_right_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E3%82%82%E5%8F%B3%E3%81%AE%20target%20%E3%82%92%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%22%22%22%0A%20%20%20%20%23%20%E6%9C%80%E5%B7%A6%E3%81%AE%20target%20%2B%201%20%E3%82%92%E6%8E%A2%E3%81%99%E5%95%8F%E9%A1%8C%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%20target%20%2B%201%29%0A%20%20%20%20%23%20j%20%E3%81%AF%E6%9C%80%E3%82%82%E5%8F%B3%E3%81%AE%20target%20%E3%82%92%E6%8C%87%E3%81%97%E3%80%81i%20%E3%81%AF%20target%20%E3%82%88%E3%82%8A%E5%A4%A7%E3%81%8D%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E6%8C%87%E3%81%99%0A%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20if%20j%20%3D%3D%20-1%20or%20nums%5Bj%5D%20%21%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%80%81%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20j%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20j%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%AB%E3%82%80%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E5%B7%A6%E7%AB%AF%E3%81%A8%E5%8F%B3%E7%AB%AF%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_right_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%8F%B3%E7%AB%AF%E3%81%AE%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
### 2. &nbsp; 要素探索に変換する
配列に`target`が含まれていない場合、$i$と$j$は最終的に`target`より大きい最初の要素と小さい最初の要素をそれぞれ指します。
配列に `target` が含まれない場合、最終的に $i$ と $j$ はそれぞれ `target` より大きい最初の要素と`target` より小さい最初の要素を指すことになります。
したがって、下図に示すように、配列に存在しない要素を構して、左右の境界を探索できます。
したがって、下図ように、配列に存在しない要素を構して、それを使って左右の境界を探ます。
- 最も左`target`を見つけるには`target - 0.5`を探索することに変換でき、ポインタ$i$を返します。
- 最も右`target`を見つけるには`target + 0.5`を探索することに変換でき、ポインタ$j$を返します。
- 最も左にある `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" }
![境界の探索を要素の探索に変換する](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>
<p align="center"> 図 10-8 &nbsp; 境界の探索を要素の探索に変換する </p>
コードはここでは省略されていますが、このアプローチについて注意すべき2つの重要な点があります。
ここではコードを省略しますが、次の 2 点に注意が必要です。
- 与えられた配列`nums`には小数が含まれていないため、等しい場合の処理は心配ありません。
- ただし、このアプローチで小数を導入するには、`target`変数を浮動小数点型に変更する必要があります(Pythonは変更不要です)。
- 与えられた配列には小数が含まれないため、等しい場合をどう処理するかを気にする必要はありません。
- この方法では小数を導入するため、関数内の変数 `target` を浮動小数点型に変更する必要があります(Python は変更不要です)。
@@ -2,77 +2,56 @@
comments: true
---
# 10.2 &nbsp; 二分探索による挿入
# 10.2 &nbsp; 二分探索の挿入位置
二分探索は目標要素を探索するだけでなく、目標要素の挿入位置を探索するなど、多くの変種問題解決するためにも使用されます。
二分探索は目標要素の検索だけでなく、目標要素の挿入位置を探など、多くの派生問題解決にも利用できます。
## 10.2.1 &nbsp; 重複要素がない場合
!!! question
一意の要素を持つ長さ$n$のソート済み配列`nums`と要素`target`が与えられ、ソート順を維持しながら`target``nums`に挿入します。`target`配列にすでに存在する場合は、既存の要素の左側に挿入します。挿入後の配列における`target`のインデックスを返してください。下図に示す例を参照してください
長さ $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" }
![二分探索の挿入位置の例データ](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>
<p align="center"> 図 10-4 &nbsp; 二分探索の挿入位置の例データ </p>
のセクションの二分探索コードを再利用したい場合、以下の2つの質問に答える必要があります。
の二分探索コードを再利用したい場合は、次の二つの問題に答える必要があります。
**問1**:配列にすでに`target`が含まれている場合、挿入位置は既存要素のインデックスになりますか?
**問1**:配列に `target` が含まれる場合、挿入位置のインデックスはその要素のインデックスすか?
`target`を等しい要素の左側に挿入するという要件は、新しく挿入され`target`元の`target`の位置を置き換えることを意味します。つまり、**配列に`target`が含まれている場合、挿入位置は確かにその`target`のインデックスです**。
問題では `target` を等しい要素の左側に挿入するよう求めているため、新しく挿入され`target`元の `target` の位置に入ります。つまり、**配列に `target` が含まれる場合、挿入位置のインデックスはその `target` のインデックスです**。
**問2**:配列に`target`が含まれていない場合、どのインデックスに挿入されますか?
**問2**:配列に `target` が存在しない場合、挿入位置はどの要素のインデックスすか?
二分探索プロセスをさらに考えてみましょう:`nums[m] < target`のとき、ポインタ$i$が移動します。これは、ポインタ$i$`target`以上の要素近づいていることを意味します。同様に、ポインタ$j$は常に`target`以下の要素近づいています。
二分探索の過程をさらに考えると、`nums[m] < target` のとき$i$ が移動します。これは、ポインタ $i$`target` 以上の要素近づいていることを意味します。同様に、ポインタ $j$ は常に `target` 以下の要素近づいています。
したがって二分の終了時には確実に:$i$は`target`より大きい最初の要素を指し、$j$`target`より小さい最初の要素を指します。**配列に`target`が含まれていない場合、挿入位置は$i$であることは明らかです**。コードは以下の通りです:
したがって二分探索の終了時には、$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] を初期化
"""二分探索で挿入位置を探す(重複要素なし)"""
i, j = 0, len(nums) - 1 # 両閉区間 [0, n-1] を初期化
while i <= j:
m = i + (j - i) // 2 # 中点インデックス m を計算
m = (i + j) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # ターゲットは区間 [m+1, j] にある
i = m + 1 # target は区間 [m+1, j] にある
elif nums[m] > target:
j = m - 1 # ターゲットは区間 [i, m-1] にある
j = m - 1 # target は区間 [i, m-1] にある
else:
return m # ターゲットが見つかった場合、挿入位置 m を返す
# ターゲットが見つからなかった場合、挿入位置 i を返す
return m # target が見つかった、挿入位置 m を返す
# target が見つからなければ、挿入位置 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] を初期化
int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) {
@@ -80,10 +59,31 @@ comments: true
} else if (nums[m] > target) {
j = m - 1; // target は区間 [i, m-1] にある
} else {
return m; // target 見つけたので、挿入 m を返す
return m; // target 見つかったら、挿入位置 m を返す
}
}
// target 見つけられなかったので、挿入 i を返す
// target 見つからなければ、挿入位置 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;
}
```
@@ -91,91 +91,263 @@ comments: true
=== "C#"
```csharp title="binary_search_insertion.cs"
[class]{binary_search_insertion}-[func]{BinarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
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;
}
```
=== "Go"
```go title="binary_search_insertion.go"
[class]{}-[func]{binarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
func binarySearchInsertionSimple(nums []int, target int) int {
// 両閉区間 [0, n-1] を初期化
i, j := 0, len(nums)-1
for i <= j {
// 中点インデックス m を計算
m := i + (j-i)/2
if nums[m] < target {
// target は区間 [m+1, j] にある
i = m + 1
} else if nums[m] > target {
// target は区間 [i, m-1] にある
j = m - 1
} else {
// target が見つかったら、挿入位置 m を返す
return m
}
}
// target が見つからなければ、挿入位置 i を返す
return i
}
```
=== "Swift"
```swift title="binary_search_insertion.swift"
[class]{}-[func]{binarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
func binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {
// 両閉区間 [0, n-1] を初期化
var i = nums.startIndex
var j = nums.endIndex - 1
while i <= j {
let 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
}
```
=== "JS"
```javascript title="binary_search_insertion.js"
[class]{}-[func]{binarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
function binarySearchInsertionSimple(nums, target) {
let i = 0,
j = nums.length - 1; // 両閉区間 [0, n-1] を初期化
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる
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;
}
```
=== "TS"
```typescript title="binary_search_insertion.ts"
[class]{}-[func]{binarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
function binarySearchInsertionSimple(
nums: Array<number>,
target: number
): number {
let i = 0,
j = nums.length - 1; // 両閉区間 [0, n-1] を初期化
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる
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;
}
```
=== "Dart"
```dart title="binary_search_insertion.dart"
[class]{}-[func]{binarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
int binarySearchInsertionSimple(List<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;
}
```
=== "Rust"
```rust title="binary_search_insertion.rs"
[class]{}-[func]{binary_search_insertion_simple}
/* 二分探索で挿入位置を探す(重複要素なし) */
fn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {
let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化
while i <= j {
let m = i + (j - i) / 2; // 中点インデックス m を計算
if nums[m as usize] < target {
i = m + 1; // target は区間 [m+1, j] にある
} else if nums[m as usize] > target {
j = m - 1; // target は区間 [i, m-1] にある
} else {
return m;
}
}
// target が見つからなければ、挿入位置 i を返す
i
}
```
=== "C"
```c title="binary_search_insertion.c"
[class]{}-[func]{binarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
int binarySearchInsertionSimple(int *nums, int numSize, int target) {
int i = 0, j = numSize - 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;
}
```
=== "Kotlin"
```kotlin title="binary_search_insertion.kt"
[class]{}-[func]{binarySearchInsertionSimple}
/* 二分探索で挿入位置を探す(重複要素なし) */
fun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {
var i = 0
var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化
while (i <= j) {
val 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
}
```
=== "Ruby"
```ruby title="binary_search_insertion.rb"
[class]{}-[func]{binary_search_insertion_simple}
### 二分探索の挿入位置(重複要素なし) ###
def binary_search_insertion_simple(nums, target)
# 両閉区間 [0, n-1] を初期化
i, j = 0, nums.length - 1
while i <= j
# 中点インデックス m を計算
m = (i + j) / 2
if nums[m] < target
i = m + 1 # target は区間 [m+1, j] にある
elsif nums[m] > target
j = m - 1 # target は区間 [i, m-1] にある
else
return m # target が見つかったら、挿入位置 m を返す
end
end
i # target が見つからなければ、挿入位置 i を返す
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion_simple%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%AA%E3%81%97%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%80%81%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20m%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%AE%E3%81%AA%E3%81%84%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion_simple%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion_simple%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%AA%E3%81%97%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%89%E3%80%81%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20m%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20%23%20target%20%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%80%81%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%AE%E3%81%AA%E3%81%84%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion_simple%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 10.2.2 &nbsp; 重複要素がある場合
!!! question
の質問に基づいて、配列に重複要素が含まれている可能性があると仮定し、他はすべて同じとします
問を踏まえ、配列に重複要素が含まれる可能性があるものとし、それ以外の条件は変わりません
配列`target`の複数の出現がある場合、通常の二分探索`target`の1つの出現のインデックスのみを返すことができ、**その位置の左右に`target`の出現がいくつあるかを特定することはできません**。
配列中に複数の `target` が存在する場合、通常の二分探索ではそのうち一つの `target` のインデックスしか返せず、**その要素の左側と右側にあといくつ `target` があるかは分かりません**。
問題では目標要素を最も左の位置に挿入することが要求されているため、**配列内の最も左`target`のインデックスを見つける必要があります**。最初に下図に示すステップを通してこれを実装することを考えてみましょう
問題では目標要素を最も左に挿入する必要があるため、**配列中で最も左にある `target` のインデックスを探す必要があります**。まずは以下の図に示す手順で実現することを考えます
1. 二分探索を実行し`target`の任意のインデックス、例えば$k$を見つけます。
2. インデックス$k$から開始して、最も左の`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" }
![線形探索による重複要素の挿入位置](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>
<p align="center"> 図 10-5 &nbsp; 線形探索による重複要素の挿入位置 </p>
この方法は実現可能ですが、線形探索を含むため、時間計算量は$O(n)$です。この方法は、配列に多くの重複する`target`が含まれている場合に非効率です。
この方法は使用できますが、線形探索を含むため、時間計算量は $O(n)$ です。配列中に重複した `target` が多い場合、この方法の効率は低くなります。
今度は二分探索コードを拡張することを考えてみましょう。下図に示すように、全体的なプロセスは同じままです。各ラウンドで、まず中インデックス$m$を計算し、次に`target``nums[m]`の値を比較して、以下のケースになります。
次に、二分探索コードを拡張することを考えます。以下の図に示すように、全体の流れは変えず、各反復でまず中インデックス $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`より小さい要素に近づけます**。
- `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$が挿入位置です**。
ループ終了後、$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" }
![重複要素に対する二分探索の挿入位置の手順](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" }
@@ -198,26 +370,26 @@ comments: true
=== "<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>
<p align="center"> 図 10-6 &nbsp; 重複要素に対する二分探索の挿入位置の手順 </p>
以下のコードを観察してください。分岐`nums[m] > target``nums[m] == target`の操作は同じであるため、これら2つの分岐をマージできます。
以下のコードを観察すると、分岐 `nums[m] > target``nums[m] == target` の処理は同じであるため、両者はまとめることができます。
それでも、ロジックがより明確になり、可読性が向上するため、条件を展開したままにしておくことができます。
それでも、判定条件を分けたままにしておくことは可能であり、そのほうがロジックがより明確で、可読性も高くなります。
=== "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] を初期化
"""二分探索で挿入位置を探す(重複要素あり)"""
i, j = 0, len(nums) - 1 # 両閉区間 [0, n-1] を初期化
while i <= j:
m = i + (j - i) // 2 # 中点インデックス m を計算
m = (i + j) // 2 # 中点インデックス m を計算
if nums[m] < target:
i = m + 1 # ターゲットは区間 [m+1, j] にある
i = m + 1 # target は区間 [m+1, j] にある
elif nums[m] > target:
j = m - 1 # ターゲットは区間 [i, m-1] にある
j = m - 1 # target は区間 [i, m-1] にある
else:
j = m - 1 # ターゲット未満の最初の要素は区間 [i, m-1] にある
j = m - 1 # target より小さい最初の要素は区間 [i, m-1] にある
# 挿入位置 i を返す
return i
```
@@ -225,30 +397,9 @@ comments: true
=== "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] を初期化
int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化
while (i <= j) {
int m = i + (j - i) / 2; // 中点インデックス m を計算
if (nums[m] < target) {
@@ -259,7 +410,28 @@ comments: true
j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある
}
}
// 挿入 i を返す
// 挿入位置 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;
}
```
@@ -267,67 +439,236 @@ comments: true
=== "C#"
```csharp title="binary_search_insertion.cs"
[class]{binary_search_insertion}-[func]{BinarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
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;
}
```
=== "Go"
```go title="binary_search_insertion.go"
[class]{}-[func]{binarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
func binarySearchInsertion(nums []int, target int) int {
// 両閉区間 [0, n-1] を初期化
i, j := 0, len(nums)-1
for i <= j {
// 中点インデックス m を計算
m := i + (j-i)/2
if nums[m] < target {
// target は区間 [m+1, j] にある
i = m + 1
} else if nums[m] > target {
// target は区間 [i, m-1] にある
j = m - 1
} else {
// target より小さい最初の要素は区間 [i, m-1] にある
j = m - 1
}
}
// 挿入位置 i を返す
return i
}
```
=== "Swift"
```swift title="binary_search_insertion.swift"
[class]{}-[func]{binarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
func binarySearchInsertion(nums: [Int], target: Int) -> Int {
// 両閉区間 [0, n-1] を初期化
var i = nums.startIndex
var j = nums.endIndex - 1
while i <= j {
let 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
}
```
=== "JS"
```javascript title="binary_search_insertion.js"
[class]{}-[func]{binarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
function binarySearchInsertion(nums, target) {
let i = 0,
j = nums.length - 1; // 両閉区間 [0, n-1] を初期化
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる
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;
}
```
=== "TS"
```typescript title="binary_search_insertion.ts"
[class]{}-[func]{binarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
function binarySearchInsertion(nums: Array<number>, target: number): number {
let i = 0,
j = nums.length - 1; // 両閉区間 [0, n-1] を初期化
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる
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;
}
```
=== "Dart"
```dart title="binary_search_insertion.dart"
[class]{}-[func]{binarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
int binarySearchInsertion(List<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;
}
```
=== "Rust"
```rust title="binary_search_insertion.rs"
[class]{}-[func]{binary_search_insertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
pub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {
let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化
while i <= j {
let m = i + (j - i) / 2; // 中点インデックス m を計算
if nums[m as usize] < target {
i = m + 1; // target は区間 [m+1, j] にある
} else if nums[m as usize] > target {
j = m - 1; // target は区間 [i, m-1] にある
} else {
j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある
}
}
// 挿入位置 i を返す
i
}
```
=== "C"
```c title="binary_search_insertion.c"
[class]{}-[func]{binarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
int binarySearchInsertion(int *nums, int numSize, int target) {
int i = 0, j = numSize - 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;
}
```
=== "Kotlin"
```kotlin title="binary_search_insertion.kt"
[class]{}-[func]{binarySearchInsertion}
/* 二分探索で挿入位置を探す(重複要素あり) */
fun binarySearchInsertion(nums: IntArray, target: Int): Int {
var i = 0
var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化
while (i <= j) {
val 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
}
```
=== "Ruby"
```ruby title="binary_search_insertion.rb"
[class]{}-[func]{binary_search_insertion}
### 二分探索の挿入位置(重複要素あり) ###
def binary_search_insertion(nums, target)
# 両閉区間 [0, n-1] を初期化
i, j = 0, nums.length - 1
while i <= j
# 中点インデックス m を計算
m = (i + j) / 2
if nums[m] < target
i = m + 1 # target は区間 [m+1, j] にある
elsif nums[m] > target
j = m - 1 # target は区間 [i, m-1] にある
else
j = m - 1 # target より小さい最初の要素は区間 [i, m-1] にある
end
end
i # 挿入位置 i を返す
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%82%E3%82%8A%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%82%88%E3%82%8A%E5%B0%8F%E3%81%95%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%23%20%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%AB%E3%82%80%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%EF%BC%88%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%81%82%E3%82%8A%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E4%B8%A1%E9%96%89%E5%8C%BA%E9%96%93%20%5B0%2C%20n-1%5D%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20m%20%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bm%2B1%2C%20j%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E3%82%88%E3%82%8A%E5%B0%8F%E3%81%95%E3%81%84%E6%9C%80%E5%88%9D%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AF%E5%8C%BA%E9%96%93%20%5Bi%2C%20m-1%5D%20%E3%81%AB%E3%81%82%E3%82%8B%0A%20%20%20%20%23%20%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%20i%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20i%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E9%87%8D%E8%A4%87%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%AB%E3%82%80%E9%85%8D%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E3%81%A7%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%82%92%E6%8E%A2%E3%81%99%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E8%A6%81%E7%B4%A0%20%7Btarget%7D%20%E3%81%AE%E6%8C%BF%E5%85%A5%E4%BD%8D%E7%BD%AE%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
!!! tip
このセクションのコードは「閉区間」を使用しています。「左閉右開」に興味がある場合は、自分でコードを実装してみてください。
本節のコードはすべて「両閉区間」の書き方です。興味のある読者は「左閉右開」の書き方を自分で実装してみてください。
する、二分探索は本質的にポインタ$i$と$j$の探索目標を設定することです。これらの目標は特定の要素(`target`など)または要素の範囲(`target`より小さいものなど)である可能性があります。
要する、二分探索はポインタ $i$ と $j$ にそれぞれ探索目標を設定することにほかなりません。目標は具体的な要素(たとえば `target`)である場合もあれば、要素の範囲(たとえば `target` より小さい要素)である場合もあります。
二分探索の連続ループにおいて、ポインタ$i$と$j$は段階的に事前定義された目標に近づきます。最終的に、それらは答えを見つけるか、境界を越えた後に停止します。
繰り返される二分のループの中で、ポインタ $i$ と $j$ はどちらも事前に定めた目標へ徐々に近づいていきます。最終的に、それらは答えを見つけるか、境界を越えたところで停止します。
+7 -7
View File
@@ -5,19 +5,19 @@ icon: material/text-search
# 第 10 章 &nbsp; 探索
![Searching](../assets/covers/chapter_searching.jpg){ class="cover-image" }
![探索](../assets/covers/chapter_searching.jpg){ class="cover-image" }
!!! abstract
探索は未知の冒険です。神秘的な空間の隅々までる必要があるかもしれませんし、あるいはすぐに目標を見つけることができるかもしれません。
この発見の旅において、それぞれの探査は予期しない答えで終わるかもしれません。
探索は未知の冒険であり、私たちは神秘的な空間の隅々まで歩き回る必要があるかもしれ、あるいは素早く目標を特定できるかもしれません。
この探索の旅において、すべての探求が思いもよらなかった答えをもたらすかもしれません。
## 章の内容
- [10.1 &nbsp; 二分探索](binary_search.md)
- [10.2 &nbsp; 二分探索の挿入](binary_search_insertion.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.4 &nbsp; ハッシュによる最適化戦略](replace_linear_by_hashing.md)
- [10.5 &nbsp; 探索アルゴリズム再考](searching_algorithm_revisited.md)
- [10.6 &nbsp; まとめ](summary.md)
@@ -2,30 +2,30 @@
comments: true
---
# 10.4 &nbsp; ハッシュ最適化戦略
# 10.4 &nbsp; ハッシュによる最適化戦略
アルゴリズム問題において、**線形探索をハッシュベースの探索に置き換えることでアルゴリズムの時間計算量を削減することがよくあります**。アルゴリズム問題を使用して理解を深めましょう。
アルゴリズム問題では,**線形探索をハッシュ探索に置き換えることでアルゴリズムの時間計算量を下げることがよくあります**。ここでは,あるアルゴリズム問題を通じて理解を深めましょう。
!!! question
整数配列`nums`と目標要素`target`が与えられ、配列内で「和」が`target`に等しい2つの要素を探索しそれらの配列インデックスを返してください。任意の解が受け入れられます。
整数配列 `nums` と目標要素 `target` が与えられたとき,配列内から和が `target` となる 2 つの要素を探索しそれらの配列インデックスを返してください。任意の 1 つの解を返せば十分です。
## 10.4.1 &nbsp; 線形探索:時間を空間と交換
## 10.4.1 &nbsp; 線形探索:時間と引き換えに空間を節約
すべての可能な組み合わせを直接横断することを考えてみます。図に示すように、ネストしたループを開始し、各反復で2つの整数の和が`target`に等しいかどうかを判します。そうであればそれらのインデックスを返します。
考えられるすべての組み合わせを直接走査することを考えます。次の図に示すように2 重ループを開始し,各ラウンドで 2 つの整数の和が `target` であるかを判します。そうであればそれらのインデックスを返します。
![Linear search solution for two-sum problem](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" }
![線形探索で 2 数の和を求める](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>
<p align="center"> 図 10-9 &nbsp; 線形探索で 2 数の和を求める </p>
コードは以下の通りです:
コードは次のとおりです:
=== "Python"
```python title="two_sum.py"
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:
"""方法一:ブルートフォース列挙"""
# 重ループ、時間計算量は O(n^2)
"""方法 1:総当たり列挙"""
# 2重ループのため、時間計算量は O(n^2)
for i in range(len(nums) - 1):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
@@ -36,10 +36,10 @@ comments: true
=== "C++"
```cpp title="two_sum.cpp"
/* 方法一:ブルートフォース列挙 */
/* 方法 1:総当たり列挙 */
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
int size = nums.size();
// 重ループ、時間計算量はO(n^2)
// 2重ループのため、時間計算量は 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)
@@ -53,10 +53,10 @@ comments: true
=== "Java"
```java title="two_sum.java"
/* 方法一: 暴力列挙 */
/* 方法 1:総当たり列挙 */
int[] twoSumBruteForce(int[] nums, int target) {
int size = nums.length;
// 重ループ、時間計算量は O(n^2)
// 2重ループのため、時間計算量は 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)
@@ -70,74 +70,193 @@ comments: true
=== "C#"
```csharp title="two_sum.cs"
[class]{two_sum}-[func]{TwoSumBruteForce}
/* 方法 1:総当たり列挙 */
int[] TwoSumBruteForce(int[] nums, int target) {
int size = nums.Length;
// 2重ループのため、時間計算量は 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 [];
}
```
=== "Go"
```go title="two_sum.go"
[class]{}-[func]{twoSumBruteForce}
/* 方法 1:総当たり列挙 */
func twoSumBruteForce(nums []int, target int) []int {
size := len(nums)
// 2重ループのため、時間計算量は O(n^2)
for i := 0; i < size-1; i++ {
for j := i + 1; j < size; j++ {
if nums[i]+nums[j] == target {
return []int{i, j}
}
}
}
return nil
}
```
=== "Swift"
```swift title="two_sum.swift"
[class]{}-[func]{twoSumBruteForce}
/* 方法 1:総当たり列挙 */
func twoSumBruteForce(nums: [Int], target: Int) -> [Int] {
// 2重ループのため、時間計算量は O(n^2)
for i in nums.indices.dropLast() {
for j in nums.indices.dropFirst(i + 1) {
if nums[i] + nums[j] == target {
return [i, j]
}
}
}
return [0]
}
```
=== "JS"
```javascript title="two_sum.js"
[class]{}-[func]{twoSumBruteForce}
/* 方法 1:総当たり列挙 */
function twoSumBruteForce(nums, target) {
const n = nums.length;
// 2重ループのため、時間計算量は O(n^2)
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (nums[i] + nums[j] === target) {
return [i, j];
}
}
}
return [];
}
```
=== "TS"
```typescript title="two_sum.ts"
[class]{}-[func]{twoSumBruteForce}
/* 方法 1:総当たり列挙 */
function twoSumBruteForce(nums: number[], target: number): number[] {
const n = nums.length;
// 2重ループのため、時間計算量は O(n^2)
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (nums[i] + nums[j] === target) {
return [i, j];
}
}
}
return [];
}
```
=== "Dart"
```dart title="two_sum.dart"
[class]{}-[func]{twoSumBruteForce}
/* 方法1: 総当たり列挙 */
List<int> twoSumBruteForce(List<int> nums, int target) {
int size = nums.length;
// 2重ループのため、時間計算量は O(n^2)
for (var i = 0; i < size - 1; i++) {
for (var j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target) return [i, j];
}
}
return [0];
}
```
=== "Rust"
```rust title="two_sum.rs"
[class]{}-[func]{two_sum_brute_force}
/* 方法 1:総当たり列挙 */
pub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
let size = nums.len();
// 2重ループのため、時間計算量は O(n^2)
for i in 0..size - 1 {
for j in i + 1..size {
if nums[i] + nums[j] == target {
return Some(vec![i as i32, j as i32]);
}
}
}
None
}
```
=== "C"
```c title="two_sum.c"
[class]{}-[func]{twoSumBruteForce}
/* 方法 1:総当たり列挙 */
int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {
for (int i = 0; i < numsSize; ++i) {
for (int j = i + 1; j < numsSize; ++j) {
if (nums[i] + nums[j] == target) {
int *res = malloc(sizeof(int) * 2);
res[0] = i, res[1] = j;
*returnSize = 2;
return res;
}
}
}
*returnSize = 0;
return NULL;
}
```
=== "Kotlin"
```kotlin title="two_sum.kt"
[class]{}-[func]{twoSumBruteForce}
/* 方法 1:総当たり列挙 */
fun twoSumBruteForce(nums: IntArray, target: Int): IntArray {
val size = nums.size
// 2重ループのため、時間計算量は O(n^2)
for (i in 0..<size - 1) {
for (j in i + 1..<size) {
if (nums[i] + nums[j] == target) return intArrayOf(i, j)
}
}
return IntArray(0)
}
```
=== "Ruby"
```ruby title="two_sum.rb"
[class]{}-[func]{two_sum_brute_force}
### 方法1:総当たり列挙 ###
def two_sum_brute_force(nums, target)
# 2重ループのため、時間計算量は O(n^2)
for i in 0...(nums.length - 1)
for j in (i + 1)...nums.length
return [i, j] if nums[i] + nums[j] == target
end
end
[]
end
```
この方法の時間計算量は$O(n^2)$、空間計算量は$O(1)$で、大容量データでは非常に時間がかかる可能性があります。
??? pythontutor "コードの可視化"
## 10.4.2 &nbsp; ハッシュ探索:空間を時間と交換
<div style="height: 423px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_brute_force%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%201%EF%BC%9A%E7%B7%8F%E5%BD%93%E3%81%9F%E3%82%8A%E5%88%97%E6%8C%99%22%22%22%0A%20%20%20%20%23%202%E9%87%8D%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E3%81%9F%E3%82%81%E3%80%81%E6%99%82%E9%96%93%E8%A8%88%E7%AE%97%E9%87%8F%E3%81%AF%20O%28n%5E2%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%2B%20nums%5Bj%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bi%2C%20j%5D%0A%20%20%20%20return%20%5B%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_brute_force%28nums%2C%20target%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_brute_force%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%201%EF%BC%9A%E7%B7%8F%E5%BD%93%E3%81%9F%E3%82%8A%E5%88%97%E6%8C%99%22%22%22%0A%20%20%20%20%23%202%E9%87%8D%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E3%81%9F%E3%82%81%E3%80%81%E6%99%82%E9%96%93%E8%A8%88%E7%AE%97%E9%87%8F%E3%81%AF%20O%28n%5E2%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%2B%20nums%5Bj%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bi%2C%20j%5D%0A%20%20%20%20return%20%5B%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_brute_force%28nums%2C%20target%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
ハッシュテーブルの使用を考えてみましょう。キーと値のペアはそれぞれ配列要素とそのインデックスです。配列をループし、各反復中に下図に示すステップを実行します。
この方法の時間計算量は $O(n^2)$ ,空間計算量は $O(1)$ であり,大規模データでは非常に時間がかかります。
1. 数値`target - nums[i]`がハッシュテーブルにあるかどうかを確認します。ある場合は、これら2つの要素のインデックスを直接返します。
2. キーと値のペア`nums[i]`とインデックス`i`をハッシュテーブルに追加します。
## 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 数の和を求める](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" }
@@ -145,18 +264,18 @@ comments: true
=== "<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>
<p align="center"> 図 10-10 &nbsp; 補助ハッシュテーブルで 2 数の和を求める </p>
実装コードは以下に示され、単一ループのみが必要です:
実装コードは次のとおりで,単一ループだけで済みます:
=== "Python"
```python title="two_sum.py"
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:
"""方法:補助ハッシュテーブル"""
# 補助ハッシュテーブル、空間計算量は O(n)
"""方法 2:補助ハッシュテーブル"""
# 補助ハッシュテーブルを使用し、空間計算量は O(n)
dic = {}
# 単一ループ、時間計算量は O(n)
# 単一ループ、時間計算量は O(n)
for i in range(len(nums)):
if target - nums[i] in dic:
return [dic[target - nums[i]], i]
@@ -167,12 +286,12 @@ comments: true
=== "C++"
```cpp title="two_sum.cpp"
/* 方法:補助ハッシュテーブル */
/* 方法 2:補助ハッシュテーブル */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
int size = nums.size();
// 補助ハッシュテーブル、空間計算量はO(n)
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
unordered_map<int, int> dic;
// 単ループ、時間計算量はO(n)
// 単ループ、時間計算量は O(n)
for (int i = 0; i < size; i++) {
if (dic.find(target - nums[i]) != dic.end()) {
return {dic[target - nums[i]], i};
@@ -186,12 +305,12 @@ comments: true
=== "Java"
```java title="two_sum.java"
/* 方法二: 補助ハッシュテーブル */
/* 方法 2補助ハッシュテーブル */
int[] twoSumHashTable(int[] nums, int target) {
int size = nums.length;
// 補助ハッシュテーブル、空間計算量は O(n)
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
Map<Integer, Integer> dic = new HashMap<>();
// 単一ループ、時間計算量は O(n)
// 単一ループ、時間計算量は O(n)
for (int i = 0; i < size; i++) {
if (dic.containsKey(target - nums[i])) {
return new int[] { dic.get(target - nums[i]), i };
@@ -205,65 +324,223 @@ comments: true
=== "C#"
```csharp title="two_sum.cs"
[class]{two_sum}-[func]{TwoSumHashTable}
/* 方法 2:補助ハッシュテーブル */
int[] TwoSumHashTable(int[] nums, int target) {
int size = nums.Length;
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
Dictionary<int, int> dic = [];
// 単一ループで、時間計算量は O(n)
for (int i = 0; i < size; i++) {
if (dic.ContainsKey(target - nums[i])) {
return [dic[target - nums[i]], i];
}
dic.Add(nums[i], i);
}
return [];
}
```
=== "Go"
```go title="two_sum.go"
[class]{}-[func]{twoSumHashTable}
/* 方法 2:補助ハッシュテーブル */
func twoSumHashTable(nums []int, target int) []int {
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
hashTable := map[int]int{}
// 単一ループで、時間計算量は O(n)
for idx, val := range nums {
if preIdx, ok := hashTable[target-val]; ok {
return []int{preIdx, idx}
}
hashTable[val] = idx
}
return nil
}
```
=== "Swift"
```swift title="two_sum.swift"
[class]{}-[func]{twoSumHashTable}
/* 方法 2:補助ハッシュテーブル */
func twoSumHashTable(nums: [Int], target: Int) -> [Int] {
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
var dic: [Int: Int] = [:]
// 単一ループで、時間計算量は O(n)
for i in nums.indices {
if let j = dic[target - nums[i]] {
return [j, i]
}
dic[nums[i]] = i
}
return [0]
}
```
=== "JS"
```javascript title="two_sum.js"
[class]{}-[func]{twoSumHashTable}
/* 方法 2:補助ハッシュテーブル */
function twoSumHashTable(nums, target) {
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
let m = {};
// 単一ループで、時間計算量は O(n)
for (let i = 0; i < nums.length; i++) {
if (m[target - nums[i]] !== undefined) {
return [m[target - nums[i]], i];
} else {
m[nums[i]] = i;
}
}
return [];
}
```
=== "TS"
```typescript title="two_sum.ts"
[class]{}-[func]{twoSumHashTable}
/* 方法 2:補助ハッシュテーブル */
function twoSumHashTable(nums: number[], target: number): number[] {
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
let m: Map<number, number> = new Map();
// 単一ループで、時間計算量は O(n)
for (let i = 0; i < nums.length; i++) {
let index = m.get(target - nums[i]);
if (index !== undefined) {
return [index, i];
} else {
m.set(nums[i], i);
}
}
return [];
}
```
=== "Dart"
```dart title="two_sum.dart"
[class]{}-[func]{twoSumHashTable}
/* 方法2: 補助ハッシュテーブル */
List<int> twoSumHashTable(List<int> nums, int target) {
int size = nums.length;
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
Map<int, int> dic = HashMap();
// 単一ループで、時間計算量は O(n)
for (var i = 0; i < size; i++) {
if (dic.containsKey(target - nums[i])) {
return [dic[target - nums[i]]!, i];
}
dic.putIfAbsent(nums[i], () => i);
}
return [0];
}
```
=== "Rust"
```rust title="two_sum.rs"
[class]{}-[func]{two_sum_hash_table}
/* 方法 2:補助ハッシュテーブル */
pub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
let mut dic = HashMap::new();
// 単一ループで、時間計算量は O(n)
for (i, num) in nums.iter().enumerate() {
match dic.get(&(target - num)) {
Some(v) => return Some(vec![*v as i32, i as i32]),
None => dic.insert(num, i as i32),
};
}
None
}
```
=== "C"
```c title="two_sum.c"
[class]{HashTable}-[func]{}
/* ハッシュテーブル */
typedef struct {
int key;
int val;
UT_hash_handle hh; // uthash.h を用いて実装
} HashTable;
[class]{}-[func]{twoSumHashTable}
/* ハッシュテーブルを検索する */
HashTable *find(HashTable *h, int key) {
HashTable *tmp;
HASH_FIND_INT(h, &key, tmp);
return tmp;
}
/* ハッシュテーブルに要素を挿入する */
void insert(HashTable **h, int key, int val) {
HashTable *t = find(*h, key);
if (t == NULL) {
HashTable *tmp = malloc(sizeof(HashTable));
tmp->key = key, tmp->val = val;
HASH_ADD_INT(*h, key, tmp);
} else {
t->val = val;
}
}
/* 方法 2:補助ハッシュテーブル */
int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {
HashTable *hashtable = NULL;
for (int i = 0; i < numsSize; i++) {
HashTable *t = find(hashtable, target - nums[i]);
if (t != NULL) {
int *res = malloc(sizeof(int) * 2);
res[0] = t->val, res[1] = i;
*returnSize = 2;
return res;
}
insert(&hashtable, nums[i], i);
}
*returnSize = 0;
return NULL;
}
```
=== "Kotlin"
```kotlin title="two_sum.kt"
[class]{}-[func]{twoSumHashTable}
/* 方法 2:補助ハッシュテーブル */
fun twoSumHashTable(nums: IntArray, target: Int): IntArray {
val size = nums.size
// 補助ハッシュテーブルを使用し、空間計算量は O(n)
val dic = HashMap<Int, Int>()
// 単一ループで、時間計算量は O(n)
for (i in 0..<size) {
if (dic.containsKey(target - nums[i])) {
return intArrayOf(dic[target - nums[i]]!!, i)
}
dic[nums[i]] = i
}
return IntArray(0)
}
```
=== "Ruby"
```ruby title="two_sum.rb"
[class]{}-[func]{two_sum_hash_table}
### 方法2:補助ハッシュテーブル ###
def two_sum_hash_table(nums, target)
# 補助ハッシュテーブルを使用し、空間計算量は O(n)
dic = {}
# 単一ループで、時間計算量は O(n)
for i in 0...nums.length
return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])
dic[nums[i]] = i
end
[]
end
```
この方法は、ハッシュ探索を使用することで時間計算量を$O(n^2)$から$O(n)$に削減し、実行時効率を大幅に向上させます。
??? pythontutor "コードの可視化"
追加のハッシュテーブルを維持する必要があるため、空間計算量は$O(n)$です。**それにもかかわらず、この方法は全体的により均衡のとれた時空間効率を持ち、この問題の最適解となります**。
<div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_hash_table%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%202%EF%BC%9A%E8%A3%9C%E5%8A%A9%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%22%22%22%0A%20%20%20%20%23%20%E8%A3%9C%E5%8A%A9%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%80%81%E7%A9%BA%E9%96%93%E8%A8%88%E7%AE%97%E9%87%8F%E3%81%AF%20O%28n%29%0A%20%20%20%20dic%20%3D%20%7B%7D%0A%20%20%20%20%23%20%E5%8D%98%E4%B8%80%E3%83%AB%E3%83%BC%E3%83%97%E3%81%A7%E3%80%81%E6%99%82%E9%96%93%E8%A8%88%E7%AE%97%E9%87%8F%E3%81%AF%20O%28n%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20if%20target%20-%20nums%5Bi%5D%20in%20dic%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bdic%5Btarget%20-%20nums%5Bi%5D%5D%2C%20i%5D%0A%20%20%20%20%20%20%20%20dic%5Bnums%5Bi%5D%5D%20%3D%20i%0A%20%20%20%20return%20%5B%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_hash_table%28nums%2C%20target%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_hash_table%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%202%EF%BC%9A%E8%A3%9C%E5%8A%A9%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%22%22%22%0A%20%20%20%20%23%20%E8%A3%9C%E5%8A%A9%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%80%81%E7%A9%BA%E9%96%93%E8%A8%88%E7%AE%97%E9%87%8F%E3%81%AF%20O%28n%29%0A%20%20%20%20dic%20%3D%20%7B%7D%0A%20%20%20%20%23%20%E5%8D%98%E4%B8%80%E3%83%AB%E3%83%BC%E3%83%97%E3%81%A7%E3%80%81%E6%99%82%E9%96%93%E8%A8%88%E7%AE%97%E9%87%8F%E3%81%AF%20O%28n%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20if%20target%20-%20nums%5Bi%5D%20in%20dic%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bdic%5Btarget%20-%20nums%5Bi%5D%5D%2C%20i%5D%0A%20%20%20%20%20%20%20%20dic%5Bnums%5Bi%5D%5D%20%3D%20i%0A%20%20%20%20return%20%5B%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_hash_table%28nums%2C%20target%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
この方法ではハッシュ探索によって時間計算量を $O(n^2)$ から $O(n)$ に下げ,実行効率を大幅に向上させます。
追加のハッシュテーブルを維持する必要があるため,空間計算量は $O(n)$ です。**それでも,この方法は全体として時間と空間の効率のバランスがより良く,本問の最適解です**。
@@ -2,93 +2,93 @@
comments: true
---
# 10.5 &nbsp; 探索アルゴリズムの再検討
# 10.5 &nbsp; 探索アルゴリズム再考
<u>探索アルゴリズム(検索アルゴリズム)</u>は、配列、連結リスト、木、グラフなどのデータ構造内で特定の基準を満たす1つ以上の要素を取得するために使用されます。
<u>探索アルゴリズム(searching algorithm</u>は、データ構造(配列、連結リスト、木、グラフなど)の中から、特定の条件を満たす 1 つまたは複数の要素を探索するために用いられます。
探索アルゴリズムは、そのアプローチに基づいて以下の2つのカテゴリに分けることができます。
探索アルゴリズムは、実装の考え方に応じて次の 2 種類に分けられます。
- **データ構造を横断することで目標要素を特定する**配列、連結リスト、木、グラフの横断など
- **データの組織構造や既存のデータを使用して効率的な要素探索を実現する**二分探索、ハッシュ探索、二分探索木探索など。
- **データ構造を走査して目標要素を特定する方法**配列、連結リスト、木、グラフの走査などがこれに当たります
- **データの構成やデータに含まれる事前情報を利用して、要素を効率よく探す方法**二分探索、ハッシュ探索、二分探索木による探索などがこれに当たります
これらのトピックは前の章で紹介されたため、私たちには馴染みのないものではありません。このセクションでは、より体系的な点から探索アルゴリズムを再検討します。
これらのトピックはすでに前の章で扱っているため、探索アルゴリズムは私たちにとって見慣れたものです。本節では、より体系的な点から探索アルゴリズムをあらためて見直します。
## 10.5.1 &nbsp; 総当たり探索
総当たり探索は、データ構造のすべての要素を横断することで目標要素を特定します。
総当たり探索は、データ構造の各要素を順に調べて目標要素を特定します。
- 線形探索は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から開始し、目標要素が見つかるか、目標要素を見つけることなく他端に到達するまで、各要素に一つずつアクセスします。
- 幅優先探索」と「深さ優先探索は、グラフと木の2つの横断戦略です。幅優先探索は初期ノードから開始し、層ごと(左から右へ)に探索し、近くから遠くのノードにアクセスします。深さ優先探索は初期ノードから開始し、パスの終端(上から下へ)まで追跡し、その後バックトラックして他のパスを試し、データ構造全体が横断されるまで続行します。
- 線形探索は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から始めて、要素を 1 つずつ調べ、目標要素が見つかるか、もう一方の端に達しても見つからないまで続けます。
- 幅優先探索”と“深さ優先探索は、グラフと木における 2 つの走査戦略です。幅優先探索は初期ノードから始めて層ごとに探索し、近いところから遠いところへ各ノードを訪れます。深さ優先探索は初期ノードから始めて 1 本の経路を最後までたどり、その後バックトラックしてほかの経路を試し、データ構造全体を走査し終えるまで続ます。
総当たり探索の利点は、その単純さと汎用性であり、**データの前処理や追加のデータ構造の助けが不要**です。
総当たり探索の利点は、単純で汎用性が高く、**データの前処理や追加のデータ構造を必要としない**ことです。
ただし、**このタイプのアルゴリズムの時間計算量は$O(n)$**で、$n$は要素数であるため、大規模なデータセットでは性能がくなります。
しかし、**こののアルゴリズムの時間計算量は $O(n)$ です**。ここで $n$ は要素数であり、そのためデータ量が大きい場合は性能がくなります。
## 10.5.2 &nbsp; 適応的探索
## 10.5.2 &nbsp; 適応的探索
適応的探索は、データ固有の性質(順序など)を使用して探索プロセスを最適化し、それにより目標要素をより効率的に特定します。
適応的探索は、データが持つ固有の性質(整列性など)を用して探索過程を最適化し、目標要素をより効率よく特定します。
- 二分探索」はデータの整列性を使用して効率的な探索を実現し、配列にのみ適用可能です
- ハッシュ探索」はハッシュテーブルを使用して探索データと目標データの間にキーと値のマッピングを確立し、それによりクエリ操作を実します。
- 特定の木構造(二分探索木など)での「木探索」は、ノード値の比較に基づいてノードを迅速に除外し、それにより目標要素を特定します。
- 二分探索”は、データの順序性を用して効率的な探索を行う方法で、配列にしか適用できません
- ハッシュ探索”は、ハッシュ表を用いて探索対象のデータと目標データキーと値の対応にし、問い合わせ操作を実します。
- “木探索”は、特定の木構造(たとえば二分探索木)の中で、ノード値の比較に基づいて不要なノードをすばやく除外し、目標要素を特定します。
れらのアルゴリズムの利点は効率であり、**時間計算量が$O(\log n)$または$O(1)$にまで達します**
の種のアルゴリズムの利点は効率が高く、**時間計算量が $O(\log n)$ あるいは $O(1)$ に達する**ことです
ただし、**これらのアルゴリズムを使用するには、多くの場合データの前処理が必要です**。えば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索の両方で追加のデータ構造の助けが必要です。これらの構造を維持することも、時間と空間の面でより多くのオーバーヘッドが必要です。
しかし、**これらのアルゴリズムを使うには、たいていデータの前処理が必要です**。たとえば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索で追加のデータ構造が必要です。これらのデータ構造を維持するにも、追加の時間と空間のコストがかかります。
!!! tip
適応的探索アルゴリズムは、多くの場合探索アルゴリズムと呼ばれ、**主に特定のデータ構造で目標要素を速に取得するために使用されます**。
適応的探索アルゴリズムは、しばしば検索アルゴリズムと呼ばれ、**主に特定のデータ構造の中で目標要素を速に取得するために用いられます**。
## 10.5.3 &nbsp; 探索法の選択
## 10.5.3 &nbsp; 探索法の選択
サイズ$n$のデータセットが与えられた場合、線形探索、二分探索、木探索、ハッシュ探索、またはその他の方法を使用して目標要素を取得できます。これらの方法の動作原理を下図に示します。
大きさ $n$ のデータ集合が与えられたとき、線形探索、二分探索、木探索、ハッシュ探索など、さまざまな方法で目標要素を探索できます。各手法の動作原理を下図に示します。
![Various search strategies](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" }
![複数の探索戦略](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" }
<p align="center"> 図 10-11 &nbsp; Various search strategies </p>
<p align="center"> 図 10-11 &nbsp; 複数の探索戦略 </p>
前述の方法の特性と操作効率を以下の表に示します。
上記のいくつかの手法について、操作効率と特性を次の表に示します。
<p align="center"> 表 10-1 &nbsp; 探索アルゴリズム効率比較 </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)$ |
| データ順序性 | 無順序 | 順序 | 順序 | 無順序 |
| | 線形探索 | 二分探索 | 木探索 | ハッシュ探索 |
| ------------ | -------- | ------------------ | ------------------ | --------------- |
| 要素探索 | $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つの方法のデータ前処理時間は線形探索の時間より長くなります。
- 小容量のデータに適しており、時間計算量が効率に与える影響は小さいです。
- データ更新が非常に頻繁なシナリオに適しています。この方法はデータの追加メンテナンスを必要としないためです。
- 汎用性がく、データ前処理をまったく必要としません。データを 1 回だけ問い合わせればよい場合、ほか 3 つの手法では前処理にかかる時間のほうが、線形探索そのものより長くなることがあります。
- 規模の小さいデータに適しています。この場合、時間計算量が効率に与える影響は比較的小さいです。
- データ更新頻度が高い場面に適しています。この手法では、データに対する追加の保守が不要だからです。
**二分探索**
- より大きなデータに適しており、安定した性能と最悪ケースの時間計算量$O(\log n)$を持ちます。
- ただし、データ量が大きすぎることはできません。配列の保存には連続したメモリ空間が必要だからです。
- 頻繁な追加と削除があるシナリオには適していません。順序付き配列維持に多くのオーバーヘッドが発生するためです。
- 大規模データに適しており、効率が安定しています。最悪時間計算量$O(\log n)$す。
- データ量が大きすぎる場合には向きません。配列の格納には連続したメモリ領域が必要だからです。
- 頻繁な挿入・削除がある場面には向きません。整列配列維持するコストが高いためです。
**ハッシュ探索**
- 高速クエリ性能が不可欠なシナリオに適しており、平均時間計算量は$O(1)$です。
- 順序付きデータや範囲探索が必要なシナリオには適していません。ハッシュテーブルはデータの順序性を維持できないためです。
- 問い合わせ性能への要求が高い場面に適しており、平均時間計算量は $O(1)$ です。
- 順序付きデータや範囲探索が必要な場面には向きません。ハッシュ表ではデータの順序性を維持できないからです。
- ハッシュ関数とハッシュ衝突処理戦略への依存度が高く、性能劣化のリスクが大きいです。
- 過度に大容量のデータには適していません。ハッシュテーブルは衝突を最小化し、良好なクエリ性能を提供するために追加の空間必要からです。
- データ量が大きすぎる場合には向きません。ハッシュ表は衝突をできるだけ減らして良好な問い合わせ性能を出すために追加の空間必要とするからです。
**木探索**
- 大容量データに適しています。木ノードはメモリに分散して保存されるためです。
- 順序付きデータの維持や範囲探索に適しています。
- ノードの継続的な追加と削除により、二分探索木は偏る可能性があり、時間計算量$O(n)$劣化する可能性があります。
- AVL木や赤黒木を使用する場合、操作は$O(\log n)$効率で安定して実行できますが、木のバランスを維持する操作によ追加のオーバーヘッドが追加されます。
- 大データに適しています。木ノードはメモリに分散して格納されるためです。
- 順序付きデータの維持や範囲探索が必要な場面に適しています。
- ノードの挿入・削除を続ける過程で、二分探索木は偏ることがあり、時間計算量$O(n)$ まで劣化する可能性があります。
- AVL 木や赤黒木を使えば、各種操作を $O(\log n)$効率で安定して実行できますが、木の平衡を保つ処理によ追加コストが発生します。
+8 -6
View File
@@ -4,9 +4,11 @@ comments: true
# 10.6 &nbsp; まとめ
- 二分探索はデータの順序に依存し、探索区間を反復的に半分にすることで探索を実行します。入力データがソート済みである必要があり、配列または配列ベースのデータ構造にのみ適用可能です。
- 無順序データセット内のエントリを見つけるには、総当たり探索が必要な場合があります。データ構造に基づいて異なる探索アルゴリズムを適用できます:線形探索は配列と連結リストに適しており、幅優先探索(BFS)と深さ優先探索(DFS)はグラフと木に適しています。これらのアルゴリズムは非常に汎用性が高く、データの前処理が不要ですが、$O(n)$という高い時間計算量を持ちます。
- ハッシュ探索、木探索、二分探索は効率的な探索方法で、特定のデータ構造内で目標要素を迅速に特定できます。これらのアルゴリズムは非常に効率的で、時間計算量が$O(\log n)$または$O(1)$にまで達しますが、通常は追加のデータ構造を収容するために追加の空間が必要です
- 実際には、データ量、探索性能要件、データクエリと更新頻度などの要因を分析して、適切な探索方法を選択する必要があります
- 線形探索は小さなデータや頻繁に更新される(変動性の高い)データに理想的です。二分探索は大きくてソート済みのデータに適しています。ハッシュ探索は高いクエリ効率が必要で範囲クエリが不要なデータに適しています。木探索は順序を維持し、範囲クエリをサポートする必要がある大きな動的データに最も適しています
- 線形探索をハッシュ探索に置き換えることは、実行時性能を最適化する一般的な戦略で、時間計算量を$O(n)$から$O(1)$に削減します
### 1. &nbsp; 要点の振り返り
- 二分探索はデータの順序性に依存し、ループによって探索区間を半分ずつ縮小しながら探索を行う。入力データがソート済みであることを前提とし、配列または配列ベースで実装されたデータ構造にのみ適用できる
- 総当たり探索はデータ構造を走査してデータを特定する。線形探索は配列と連結リストに適しており、幅優先探索と深さ優先探索はグラフと木に適している。この種のアルゴリズムは汎用性が高く、データの前処理を必要としないが、時間計算量 $O(n)$ は高い
- ハッシュ探索、木探索、二分探索は高効率な探索手法であり、特定のデータ構造内で目的の要素を高速に特定できる。この種のアルゴリズムは効率が高く、時間計算量は $O(\log n)$ あるいは $O(1)$ に達するが、通常は追加のデータ構造を必要とする
- 実際には、データ規模、探索性能の要件、データの問い合わせ頻度や更新頻度などの要因を具体的に分析し、そのうえで適切な探索手法を選択する必要がある
- 線形探索は小規模または頻繁に更新されるデータに適している。二分探索は大規模でソート済みのデータに適している。ハッシュ探索は問い合わせ効率への要求が高く、範囲検索を必要としないデータに適している。木探索は順序の維持と範囲検索のサポートが必要な大規模動的データに適している。
- ハッシュ探索で線形探索を置き換えることは、実行時間を最適化するための一般的な戦略であり、時間計算量を $O(n)$ から $O(1)$ へと下げられる。
+390 -67
View File
@@ -4,12 +4,12 @@ comments: true
# 11.3 &nbsp; バブルソート
<u>バブルソート</u>は、隣接する要素を継続的に比較し交換することで動作します。このプロセスは泡が底から上に上昇するようなものなので、「バブルソート」と名付けられました
<u>バブルソートbubble sort</u>は、隣接する要素を繰り返し比較し交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます
図に示すように、バブリングプロセスは要素交換を使用してシミュレートできます配列の左端から開始して右に移動し、隣接する要素の各ペアを比較します。左の要素が右の要素より大きい場合は、それらを交換します。横断後、最大要素は配列の右端にバブルアップします。
次の図に示すように、バブル処理は要素交換操作によってシミュレートできます配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大要素は配列の最も右端へ移動します。
=== "<1>"
![Simulating bubble process using element swap](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
![要素の交換操作でバブル処理をシミュレート](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
=== "<2>"
![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" }
@@ -29,22 +29,22 @@ comments: true
=== "<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>
<p align="center"> 図 11-4 &nbsp; 要素の交換操作でバブル処理をシミュレート </p>
## 11.3.1 &nbsp; アルゴリズムプロセス
## 11.3.1 &nbsp; アルゴリズムの流れ
配列の長さを$n$とします。バブルソートのステップは下図に示されます:
配列の長さを $n$ とすると、バブルソートの手順は次の図のとおりです。
1. まず、$n$個の要素に対して1回の「バブル」パスを実行し、**最大要素を正しい位置交換します**。
2. 次に、残りの$n - 1$個の要素に対して「バブル」パスを実行し、**2番目に大きい要素を正しい位置交換します**。
3. この方法で続行します;$n - 1$回のパスの後、**最大$n - 1$個の要素が正しい位置に移動されます**。
4. 残りの唯一の要素は**必ず**最小であるため、**さらなる**ソートは必要ありません。この時点で、配列ソートされます。
1. まず、$n$ 個の要素に対して「バブル処理」を行い、**配列中の最大要素を正しい位置交換します**。
2. 次に、残りの $n - 1$ 個の要素に対して「バブル処理」を行い、**2 番目に大きい要素を正しい位置交換します**。
3. このようにして、$n - 1$ 回の「バブル処理」を終えると、**大きいほうから $n - 1$ 個の要素がすべて正しい位置へ交換されます**。
4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列ソートが完了します。
![Bubble sort process](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
![バブルソートの流れ](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-5 &nbsp; Bubble sort process </p>
<p align="center"> 図 11-5 &nbsp; バブルソートの流れ </p>
コード例は以下の通りです
コード例は次のとおりです
=== "Python"
@@ -52,9 +52,9 @@ comments: true
def bubble_sort(nums: list[int]):
"""バブルソート"""
n = len(nums)
# 外側のループ:未ソート範囲は [0, i]
# 外側のループ:未ソート区間は [0, i]
for i in range(n - 1, 0, -1):
# 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動
# 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j in range(i):
if nums[j] > nums[j + 1]:
# nums[j] と nums[j + 1] を交換
@@ -66,13 +66,13 @@ comments: true
```cpp title="bubble_sort.cpp"
/* バブルソート */
void bubbleSort(vector<int> &nums) {
// 外側ループ:未ソート範囲は[0, i]
// 外側ループ:未ソート区間は [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// 内側ループ:未ソート範囲[0, i]の最大要素を範囲の右端交換
// 内側ループ:未ソート区間 [0, i] の最大要素をその区間の最右端交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j]nums[j + 1]を交換
// ここではstdswapを使用
// nums[j]nums[j + 1] を交換する
// ここでは std::swap() 関数を使用する
swap(nums[j], nums[j + 1]);
}
}
@@ -85,9 +85,9 @@ comments: true
```java title="bubble_sort.java"
/* バブルソート */
void bubbleSort(int[] nums) {
// 外側ループ: 未ソート範囲は [0, i]
// 外側ループ未ソート区間は [0, i]
for (int i = nums.length - 1; i > 0; i--) {
// 内側ループ: 未ソート範囲 [0, i] の最大要素を範囲の右端交換
// 内側ループ未ソート区間 [0, i] の最大要素をその区間の最右端交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
@@ -103,107 +103,242 @@ comments: true
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSort}
/* バブルソート */
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] を交換
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
}
}
}
}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSort}
/* バブルソート */
func bubbleSort(nums []int) {
// 外側のループ:未ソート区間は [0, i]
for i := len(nums) - 1; i > 0; i-- {
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j := 0; j < i; j++ {
if nums[j] > nums[j+1] {
// nums[j] と nums[j + 1] を交換
nums[j], nums[j+1] = nums[j+1], nums[j]
}
}
}
}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSort}
/* バブルソート */
func bubbleSort(nums: inout [Int]) {
// 外側のループ:未ソート区間は [0, i]
for i in nums.indices.dropFirst().reversed() {
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j in 0 ..< i {
if nums[j] > nums[j + 1] {
// nums[j] と nums[j + 1] を交換
nums.swapAt(j, j + 1)
}
}
}
}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSort}
/* バブルソート */
function bubbleSort(nums) {
// 外側のループ:未ソート区間は [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSort}
/* バブルソート */
function bubbleSort(nums: number[]): void {
// 外側のループ:未ソート区間は [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSort}
/* バブルソート */
void bubbleSort(List<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;
}
}
}
}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort}
/* バブルソート */
fn bubble_sort(nums: &mut [i32]) {
// 外側のループ:未ソート区間は [0, i]
for i in (1..nums.len()).rev() {
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j in 0..i {
if nums[j] > nums[j + 1] {
// nums[j] と nums[j + 1] を交換
nums.swap(j, j + 1);
}
}
}
}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSort}
/* バブルソート */
void bubbleSort(int nums[], int size) {
// 外側のループ:未ソート区間は [0, i]
for (int i = size - 1; i > 0; i--) {
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSort}
/* バブルソート */
fun bubbleSort(nums: IntArray) {
// 外側のループ:未ソート区間は [0, i]
for (i in nums.size - 1 downTo 1) {
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (j in 0..<i) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
val temp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = temp
}
}
}
}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort}
### バブルソート ###
def bubble_sort(nums)
n = nums.length
# 外側のループ:未ソート区間は [0, i]
for i in (n - 1).downto(1)
# 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j in 0...i
if nums[j] > nums[j + 1]
# nums[j] と nums[j + 1] を交換
nums[j], nums[j + 1] = nums[j + 1], nums[j]
end
end
end
end
```
??? pythontutor "コードの可視化"
<div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%E3%81%AF%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%20%5B0%2C%20i%5D%20%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%E3%82%92%E3%81%9D%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AE%E6%9C%80%E5%8F%B3%E7%AB%AF%E3%81%B8%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20nums%5Bj%5D%20%E3%81%A8%20nums%5Bj%20%2B%201%5D%20%E3%82%92%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%20nums%5Bj%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%E3%81%AF%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%20%5B0%2C%20i%5D%20%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%E3%82%92%E3%81%9D%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AE%E6%9C%80%E5%8F%B3%E7%AB%AF%E3%81%B8%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20nums%5Bj%5D%20%E3%81%A8%20nums%5Bj%20%2B%201%5D%20%E3%82%92%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%20nums%5Bj%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 11.3.2 &nbsp; 効率の最適化
「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。
ある回の「バブル処理」で交換操作が一度も行われなければ、配列はすでにソート済みであり、結果をそのまま返せることがわかります。したがって、この状況を検出するためのフラグ `flag` を追加し、発生した時点で直ちに返すようにできます。
この最適化があっても、バブルソートの最悪時間計算量と平均時間計算量は$O(n^2)$のままです。ただし、入力配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$まで低くなる可能性があります。
最適化も、バブルソートの最悪時間計算量と平均時間計算量は依然として $O(n^2)$ です。ただし、入力配列が完全に整列済みであれば、最良時間計算量は $O(n)$ に達します。
=== "Python"
```python title="bubble_sort.py"
def bubble_sort_with_flag(nums: list[int]):
"""バブルソート(フラグによる最適化)"""
"""バブルソート(フラグ最適化)"""
n = len(nums)
# 外側のループ:未ソート範囲は [0, i]
# 外側のループ:未ソート区間は [0, i]
for i in range(n - 1, 0, -1):
flag = False # フラグを初期化
# 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動
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 # 要素を交換したことを記録
flag = True # 交換する要素を記録
if not flag:
break # この回の「バブリング」で要素交換されなかった場合、終了
break # このバブル処理で要素交換が一度もなければそのまま終了
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* バブルソート(フラグ最適化*/
/* バブルソート(フラグ最適化) */
void bubbleSortWithFlag(vector<int> &nums) {
// 外側ループ:未ソート範囲は[0, i]
// 外側ループ:未ソート区間は [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
bool flag = false; // フラグを初期化
// 内側ループ:未ソート範囲[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]を交換
// ここではstdswapを使用
// nums[j]nums[j + 1] を交換する
// ここでは std::swap() 関数を使用する
swap(nums[j], nums[j + 1]);
flag = true; // 交換された要素を記録
flag = true; // 交換する要素を記録
}
}
if (!flag)
break; // この回の「バブリング」で要素交換されなかった場合、終了
break; // このバブル処理で要素交換が一度もなければそのまま終了
}
}
```
@@ -211,23 +346,23 @@ comments: true
=== "Java"
```java title="bubble_sort.java"
/* バブルソート(フラグによる最適化) */
/* バブルソート(フラグ最適化) */
void bubbleSortWithFlag(int[] nums) {
// 外側ループ: 未ソート範囲は [0, i]
// 外側ループ未ソート区間は [0, i]
for (int i = nums.length - 1; i > 0; i--) {
boolean flag = false; // フラグを初期化
// 内側ループ: 未ソート範囲 [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; // 交換された要素を記録
flag = true; // 交換する要素を記録
}
}
if (!flag)
break; // このバブリング」ラウンドで要素交換されなかった場合、終了
break; // このバブル処理で要素交換が一度もなければそのまま終了
}
}
```
@@ -235,65 +370,253 @@ comments: true
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
void BubbleSortWithFlag(int[] nums) {
// 外側のループ:未ソート区間は [0, i]
for (int i = nums.Length - 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] を交換
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
flag = true; // 交換する要素を記録
}
}
if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了
}
}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
func bubbleSortWithFlag(nums []int) {
// 外側のループ:未ソート区間は [0, i]
for i := len(nums) - 1; i > 0; i-- {
flag := false // フラグを初期化する
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j := 0; j < i; j++ {
if nums[j] > nums[j+1] {
// nums[j] と nums[j + 1] を交換
nums[j], nums[j+1] = nums[j+1], nums[j]
flag = true // 交換する要素を記録
}
}
if flag == false { // このバブル処理で要素交換が一度もなければそのまま終了
break
}
}
}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
func bubbleSortWithFlag(nums: inout [Int]) {
// 外側のループ:未ソート区間は [0, i]
for i in nums.indices.dropFirst().reversed() {
var flag = false // フラグを初期化する
for j in 0 ..< i {
if nums[j] > nums[j + 1] {
// nums[j] と nums[j + 1] を交換
nums.swapAt(j, j + 1)
flag = true // 交換する要素を記録
}
}
if !flag { // このバブル処理で要素交換が一度もなければそのまま終了
break
}
}
}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
function bubbleSortWithFlag(nums) {
// 外側のループ:未ソート区間は [0, i]
for (let i = nums.length - 1; i > 0; i--) {
let flag = false; // フラグを初期化する
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 交換する要素を記録
}
}
if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了
}
}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
function bubbleSortWithFlag(nums: number[]): void {
// 外側のループ:未ソート区間は [0, i]
for (let i = nums.length - 1; i > 0; i--) {
let flag = false; // フラグを初期化する
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 交換する要素を記録
}
}
if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了
}
}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
void bubbleSortWithFlag(List<int> nums) {
// 外側のループ:未ソート区間は [0, i]
for (int i = nums.length - 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] を交換
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 交換する要素を記録
}
}
if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了
}
}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort_with_flag}
/* バブルソート(フラグ最適化) */
fn bubble_sort_with_flag(nums: &mut [i32]) {
// 外側のループ:未ソート区間は [0, i]
for i in (1..nums.len()).rev() {
let mut flag = false; // フラグを初期化する
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j in 0..i {
if nums[j] > nums[j + 1] {
// nums[j] と nums[j + 1] を交換
nums.swap(j, j + 1);
flag = true; // 交換する要素を記録
}
}
if !flag {
break; // このバブル処理で要素交換が一度もなければそのまま終了
};
}
}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
void bubbleSortWithFlag(int nums[], int size) {
// 外側のループ:未ソート区間は [0, i]
for (int i = size - 1; i > 0; i--) {
bool flag = false;
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
flag = true;
}
}
if (!flag)
break;
}
}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSortWithFlag}
/* バブルソート(フラグ最適化) */
fun bubbleSortWithFlag(nums: IntArray) {
// 外側のループ:未ソート区間は [0, i]
for (i in nums.size - 1 downTo 1) {
var flag = false // フラグを初期化する
// 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for (j in 0..<i) {
if (nums[j] > nums[j + 1]) {
// nums[j] と nums[j + 1] を交換
val temp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = temp
flag = true // 交換する要素を記録
}
}
if (!flag) break // このバブル処理で要素交換が一度もなければそのまま終了
}
}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort_with_flag}
### バブルソート ###
def bubble_sort(nums)
n = nums.length
# 外側のループ:未ソート区間は [0, i]
for i in (n - 1).downto(1)
# 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j in 0...i
if nums[j] > nums[j + 1]
# nums[j] と nums[j + 1] を交換
nums[j], nums[j + 1] = nums[j + 1], nums[j]
end
end
end
end
# ## バブルソート(フラグ最適化)###
def bubble_sort_with_flag(nums)
n = nums.length
# 外側のループ:未ソート区間は [0, i]
for i in (n - 1).downto(1)
flag = false # フラグを初期化する
# 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換
for j in 0...i
if nums[j] > nums[j + 1]
# nums[j] と nums[j + 1] を交換
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = true # 交換する要素を記録
end
end
break unless flag # このバブル処理で要素交換が一度もなければそのまま終了
end
end
```
## 11.3.3 &nbsp; アルゴリズムの特性
??? pythontutor "コードの可視化"
- **$O(n^2)$の時間計算量、適応ソート。** 各「バブリング」ラウンドは長さ$n - 1$、$n - 2$、$\dots$、$2$、$1$の配列セグメントを横断し、合計は$(n - 1) n / 2$となります。`flag`最適化により、配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$に達する可能性があります。
- **$O(1)$の空間計算量、インプレースソート。** ポインタ$i$と$j$によって定数量の追加空間のみが使用されます。
- **安定ソート。** 等しい要素は「バブリング」中に交換されないため、元の順序が保持され、これは安定ソートになります。
<div style="height: 531px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort_with_flag%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%EF%BC%88%E3%83%95%E3%83%A9%E3%82%B0%E6%9C%80%E9%81%A9%E5%8C%96%EF%BC%89%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%E3%81%AF%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20flag%20%3D%20False%20%20%23%20%E3%83%95%E3%83%A9%E3%82%B0%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%20%5B0%2C%20i%5D%20%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%E3%82%92%E3%81%9D%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AE%E6%9C%80%E5%8F%B3%E7%AB%AF%E3%81%B8%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20nums%5Bj%5D%20%E3%81%A8%20nums%5Bj%20%2B%201%5D%20%E3%82%92%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20flag%20%3D%20True%20%20%23%20%E4%BA%A4%E6%8F%9B%E3%81%99%E3%82%8B%E8%A6%81%E7%B4%A0%E3%82%92%E8%A8%98%E9%8C%B2%0A%20%20%20%20%20%20%20%20if%20not%20flag%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%20%20%23%20%E3%81%93%E3%81%AE%E3%83%90%E3%83%96%E3%83%AB%E5%87%A6%E7%90%86%E3%81%A7%E8%A6%81%E7%B4%A0%E4%BA%A4%E6%8F%9B%E3%81%8C%E4%B8%80%E5%BA%A6%E3%82%82%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E7%B5%82%E4%BA%86%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort_with_flag%28nums%29%0A%20%20%20%20print%28%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort_with_flag%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%EF%BC%88%E3%83%95%E3%83%A9%E3%82%B0%E6%9C%80%E9%81%A9%E5%8C%96%EF%BC%89%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%E3%81%AF%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20flag%20%3D%20False%20%20%23%20%E3%83%95%E3%83%A9%E3%82%B0%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%20%5B0%2C%20i%5D%20%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%E3%82%92%E3%81%9D%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AE%E6%9C%80%E5%8F%B3%E7%AB%AF%E3%81%B8%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20nums%5Bj%5D%20%E3%81%A8%20nums%5Bj%20%2B%201%5D%20%E3%82%92%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20flag%20%3D%20True%20%20%23%20%E4%BA%A4%E6%8F%9B%E3%81%99%E3%82%8B%E8%A6%81%E7%B4%A0%E3%82%92%E8%A8%98%E9%8C%B2%0A%20%20%20%20%20%20%20%20if%20not%20flag%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%20%20%23%20%E3%81%93%E3%81%AE%E3%83%90%E3%83%96%E3%83%AB%E5%87%A6%E7%90%86%E3%81%A7%E8%A6%81%E7%B4%A0%E4%BA%A4%E6%8F%9B%E3%81%8C%E4%B8%80%E5%BA%A6%E3%82%82%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E7%B5%82%E4%BA%86%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort_with_flag%28nums%29%0A%20%20%20%20print%28%22%E3%83%90%E3%83%96%E3%83%AB%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 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$ は定数サイズの追加領域しか使用しません。
- **安定ソート**:「バブル処理」では等しい要素に出会っても交換しないためです。
+325 -54
View File
@@ -4,43 +4,43 @@ comments: true
# 11.8 &nbsp; バケットソート
前述のソートアルゴリズムはすべて「比較ベースのソートアルゴリズム」で、値を比較することで要素をソートします。このようなソートアルゴリズムは $O(n \log n)$ より良い時間計算量を持つことはできません。次に、線形時間計算量を達成できるいくつかの「非比較ソートアルゴリズム」について議論します。
前述のいくつかのソートアルゴリズムは、いずれも「比較ベースのソートアルゴリズム」に属し、要素間の大小を比較することで整列を実現します。この種のソートアルゴリズムの時間計算量は $O(n \log n)$ を超えられません。続いて、時間計算量が線形オーダーに達しうる「非比較ソートアルゴリズム」をいくつか見ていきます。
<u>バケットソート</u>は分割統治戦略の典型的な応用です。一連の順序付けられたバケットを設定し、各バケットがデータ範囲を含み、入力データをこれらのバケット均等に分散させることで動作します。そして、各バケット内のデータを個別にソートします。最後に、すべてのバケットからのソート済みデータを順次マージして最終結果を生成します。
<u>バケットソートbucket sort</u>は分割統治戦略の典型的な応用です。大小関係をもつ複数のバケットを用意し、各バケットがあるデータ範囲に対応するようにして、データを各バケット均等に分します。その後、各バケット内でそれぞれソートを行い、最後にバケットの順序に従ってすべてのデータを結合します。
## 11.8.1 &nbsp; アルゴリズムの過程
## 11.8.1 &nbsp; アルゴリズムの流れ
長さ $n$ の配列で、$[0, 1)$ の範囲の浮動小数点数を考えてみます。バケットソートの過程は以下の図に示されています。
長さ $n$ の配列を考え、その要素は範囲 $[0, 1)$ の浮動小数点数であるとします。バケットソートの流れを以下の図に示ます。
1. $k$ 個のバケットを初期化し、$n$ 個の要素をこれらの $k$ 個のバケットに分散させます。
2. 各バケットを個別にソートします(プログラミング言語の組み込みソート関数を使用)。
3. 最小から最大のバケットの順序で結果をマージします。
1. $k$ 個のバケットを初期化し、$n$ 個の要素を $k$ 個のバケットに分配します。
2. 各バケットに対してそれぞれソートを実行します(ここではプログラミング言語の組み込みソート関数を用います)。
3. バケットを小さい順にたどって結果を結合します。
![バケットソートアルゴリズムの過程](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" }
![バケットソートの流れ](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-13 &nbsp; バケットソートアルゴリズムの過程 </p>
<p align="center"> 図 11-13 &nbsp; バケットソートの流れ </p>
コードは以下のりです:
コードは以下のとおりです:
=== "Python"
```python title="bucket_sort.py"
def bucket_sort(nums: list[float]):
"""バケットソート"""
# k = n/2 個のバケットを初期化、各バケットに平均2個の要素を配置することを期待
# k = n/2 個のバケットを初期化、各バケットに 2 要素ずつ割り当てる想定とする
k = len(nums) // 2
buckets = [[] for _ in range(k)]
# 1. 配列要素を各バケットに分散
# 1. 配列要素を各バケットに振り分ける
for num in nums:
# 入力データ範囲は [0, 1)、num * k を使用してインデックス範囲 [0, k-1] にマッピング
# 入力データ範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
i = int(num * k)
# num をバケット i に追加
buckets[i].append(num)
# 2. 各バケットをソート
# 2. 各バケットをソートする
for bucket in buckets:
# 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
# 組み込みソート関数を使う。他のソートアルゴリズムに置き換えてもよい
bucket.sort()
# 3. バケットを走査して結果をマージ
# 3. バケットを走査して結果を結合
i = 0
for bucket in buckets:
for num in bucket:
@@ -53,22 +53,22 @@ comments: true
```cpp title="bucket_sort.cpp"
/* バケットソート */
void bucketSort(vector<float> &nums) {
// k = n/2個のバケットを初期化、各バケットに2つの要素を割り当てることを期待
// k = n/2 個のバケットを初期化、各バケットに 2 要素ずつ割り当てる想定とする
int k = nums.size() / 2;
vector<vector<float>> buckets(k);
// 1. 配列要素を各バケットに分配
// 1. 配列要素を各バケットに振り分ける
for (float num : nums) {
// 入力データ範囲は[0, 1)、num * kを使用してインデックス範囲[0, k-1]にマップ
// 入力データ範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
int i = num * k;
// bucket_idxバケットに数値を追加
// num をバケット bucket_idx追加
buckets[i].push_back(num);
}
// 2. 各バケットをソート
// 2. 各バケットをソートする
for (vector<float> &bucket : buckets) {
// 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
// 組み込みソート関数を使う。他のソートアルゴリズムに置き換えてもよい
sort(bucket.begin(), bucket.end());
}
// 3. バケットを走査して結果をマージ
// 3. バケットを走査して結果を結合
int i = 0;
for (vector<float> &bucket : buckets) {
for (float num : bucket) {
@@ -83,25 +83,25 @@ comments: true
```java title="bucket_sort.java"
/* バケットソート */
void bucketSort(float[] nums) {
// k = n/2 個のバケットを初期化、各バケットに期待される要素数は 2 個
// 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. 配列要素を各バケットに分散
// 1. 配列要素を各バケットに振り分ける
for (float num : nums) {
// 入力データ範囲は [0, 1)、num * k を使ってインデックス範囲 [0, k-1] にマッピング
// 入力データ範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
int i = (int) (num * k);
// num をバケット i に追加
buckets.get(i).add(num);
}
// 2. 各バケットをソート
// 2. 各バケットをソートする
for (List<Float> bucket : buckets) {
// 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
// 組み込みソート関数を使う。他のソートアルゴリズムに置き換えてもよい
Collections.sort(bucket);
}
// 3. バケットを走査して結果をマージ
// 3. バケットを走査して結果を結合
int i = 0;
for (List<Float> bucket : buckets) {
for (float num : bucket) {
@@ -114,87 +114,358 @@ comments: true
=== "C#"
```csharp title="bucket_sort.cs"
[class]{bucket_sort}-[func]{BucketSort}
/* バケットソート */
void BucketSort(float[] nums) {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
int k = nums.Length / 2;
List<List<float>> buckets = [];
for (int i = 0; i < k; i++) {
buckets.Add([]);
}
// 1. 配列要素を各バケットに振り分ける
foreach (float num in nums) {
// 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
int i = (int)(num * k);
// num をバケット i に追加
buckets[i].Add(num);
}
// 2. 各バケットをソートする
foreach (List<float> bucket in buckets) {
// 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい
bucket.Sort();
}
// 3. バケットを走査して結果を結合
int j = 0;
foreach (List<float> bucket in buckets) {
foreach (float num in bucket) {
nums[j++] = num;
}
}
}
```
=== "Go"
```go title="bucket_sort.go"
[class]{}-[func]{bucketSort}
/* バケットソート */
func bucketSort(nums []float64) {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
k := len(nums) / 2
buckets := make([][]float64, k)
for i := 0; i < k; i++ {
buckets[i] = make([]float64, 0)
}
// 1. 配列要素を各バケットに振り分ける
for _, num := range nums {
// 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
i := int(num * float64(k))
// num をバケット i に追加
buckets[i] = append(buckets[i], num)
}
// 2. 各バケットをソートする
for i := 0; i < k; i++ {
// 組み込みのスライスソート関数を使う。ほかのソートアルゴリズムに置き換えてもよい
sort.Float64s(buckets[i])
}
// 3. バケットを走査して結果を結合
i := 0
for _, bucket := range buckets {
for _, num := range bucket {
nums[i] = num
i++
}
}
}
```
=== "Swift"
```swift title="bucket_sort.swift"
[class]{}-[func]{bucketSort}
/* バケットソート */
func bucketSort(nums: inout [Double]) {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
let k = nums.count / 2
var buckets = (0 ..< k).map { _ in [Double]() }
// 1. 配列要素を各バケットに振り分ける
for num in nums {
// 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
let i = Int(num * Double(k))
// num をバケット i に追加
buckets[i].append(num)
}
// 2. 各バケットをソートする
for i in buckets.indices {
// 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい
buckets[i].sort()
}
// 3. バケットを走査して結果を結合
var i = nums.startIndex
for bucket in buckets {
for num in bucket {
nums[i] = num
i += 1
}
}
}
```
=== "JS"
```javascript title="bucket_sort.js"
[class]{}-[func]{bucketSort}
/* バケットソート */
function bucketSort(nums) {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
const k = nums.length / 2;
const buckets = [];
for (let i = 0; i < k; i++) {
buckets.push([]);
}
// 1. 配列要素を各バケットに振り分ける
for (const num of nums) {
// 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
const i = Math.floor(num * k);
// num をバケット i に追加
buckets[i].push(num);
}
// 2. 各バケットをソートする
for (const bucket of buckets) {
// 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい
bucket.sort((a, b) => a - b);
}
// 3. バケットを走査して結果を結合
let i = 0;
for (const bucket of buckets) {
for (const num of bucket) {
nums[i++] = num;
}
}
}
```
=== "TS"
```typescript title="bucket_sort.ts"
[class]{}-[func]{bucketSort}
/* バケットソート */
function bucketSort(nums: number[]): void {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
const k = nums.length / 2;
const buckets: number[][] = [];
for (let i = 0; i < k; i++) {
buckets.push([]);
}
// 1. 配列要素を各バケットに振り分ける
for (const num of nums) {
// 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
const i = Math.floor(num * k);
// num をバケット i に追加
buckets[i].push(num);
}
// 2. 各バケットをソートする
for (const bucket of buckets) {
// 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい
bucket.sort((a, b) => a - b);
}
// 3. バケットを走査して結果を結合
let i = 0;
for (const bucket of buckets) {
for (const num of bucket) {
nums[i++] = num;
}
}
}
```
=== "Dart"
```dart title="bucket_sort.dart"
[class]{}-[func]{bucketSort}
/* バケットソート */
void bucketSort(List<double> nums) {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
int k = nums.length ~/ 2;
List<List<double>> buckets = List.generate(k, (index) => []);
// 1. 配列要素を各バケットに振り分ける
for (double _num in nums) {
// 入力データの範囲は [0, 1) であり、_num * k を用いてインデックス範囲 [0, k-1] に写像する
int i = (_num * k).toInt();
// _num をバケット bucket_idx に追加
buckets[i].add(_num);
}
// 2. 各バケットをソートする
for (List<double> bucket in buckets) {
bucket.sort();
}
// 3. バケットを走査して結果を結合
int i = 0;
for (List<double> bucket in buckets) {
for (double _num in bucket) {
nums[i++] = _num;
}
}
}
```
=== "Rust"
```rust title="bucket_sort.rs"
[class]{}-[func]{bucket_sort}
/* バケットソート */
fn bucket_sort(nums: &mut [f64]) {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
let k = nums.len() / 2;
let mut buckets = vec![vec![]; k];
// 1. 配列要素を各バケットに振り分ける
for &num in nums.iter() {
// 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
let i = (num * k as f64) as usize;
// num をバケット i に追加
buckets[i].push(num);
}
// 2. 各バケットをソートする
for bucket in &mut buckets {
// 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい
bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());
}
// 3. バケットを走査して結果を結合
let mut i = 0;
for bucket in buckets.iter() {
for &num in bucket.iter() {
nums[i] = num;
i += 1;
}
}
}
```
=== "C"
```c title="bucket_sort.c"
[class]{}-[func]{bucketSort}
/* バケットソート */
void bucketSort(float nums[], int n) {
int k = n / 2; // k = n/2 個のバケットを初期化する
int *sizes = malloc(k * sizeof(int)); // 各バケットのサイズを記録する
float **buckets = malloc(k * sizeof(float *)); // 動的配列の配列(バケット)
// 各バケットに十分な容量を事前確保する
for (int i = 0; i < k; ++i) {
buckets[i] = (float *)malloc(n * sizeof(float));
sizes[i] = 0;
}
// 1. 配列要素を各バケットに振り分ける
for (int i = 0; i < n; ++i) {
int idx = (int)(nums[i] * k);
buckets[idx][sizes[idx]++] = nums[i];
}
// 2. 各バケットをソートする
for (int i = 0; i < k; ++i) {
qsort(buckets[i], sizes[i], sizeof(float), compare);
}
// 3. ソート済みのバケットを結合する
int idx = 0;
for (int i = 0; i < k; ++i) {
for (int j = 0; j < sizes[i]; ++j) {
nums[idx++] = buckets[i][j];
}
// メモリを解放する
free(buckets[i]);
}
}
```
=== "Kotlin"
```kotlin title="bucket_sort.kt"
[class]{}-[func]{bucketSort}
/* バケットソート */
fun bucketSort(nums: FloatArray) {
// k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
val k = nums.size / 2
val buckets = mutableListOf<MutableList<Float>>()
for (i in 0..<k) {
buckets.add(mutableListOf())
}
// 1. 配列要素を各バケットに振り分ける
for (num in nums) {
// 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
val i = (num * k).toInt()
// num をバケット i に追加
buckets[i].add(num)
}
// 2. 各バケットをソートする
for (bucket in buckets) {
// 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい
bucket.sort()
}
// 3. バケットを走査して結果を結合
var i = 0
for (bucket in buckets) {
for (num in bucket) {
nums[i++] = num
}
}
}
```
=== "Ruby"
```ruby title="bucket_sort.rb"
[class]{}-[func]{bucket_sort}
### バケットソート ###
def bucket_sort(nums)
# k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする
k = nums.length / 2
buckets = Array.new(k) { [] }
# 1. 配列要素を各バケットに振り分ける
nums.each do |num|
# 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する
i = (num * k).to_i
# num をバケット i に追加
buckets[i] << num
end
# 2. 各バケットをソートする
buckets.each do |bucket|
# 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい
bucket.sort!
end
# 3. バケットを走査して結果を結合
i = 0
buckets.each do |bucket|
bucket.each do |num|
nums[i] = num
i += 1
end
end
end
```
## 11.8.2 &nbsp; アルゴリズムの特徴
??? pythontutor "コードの可視化"
バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに100万個の要素が含まれ、システムメモリの制限によりすべてのデータを同時にロードできない場合、データを1,000個のバケットに分割し、各バケットを個別にソートしてから結果をマージできます。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20bucket_sort%28nums%3A%20list%5Bfloat%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20k%20%3D%20n%2F2%20%E5%80%8B%E3%81%AE%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%97%E3%80%81%E5%90%84%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%81%AB%202%20%E8%A6%81%E7%B4%A0%E3%81%9A%E3%81%A4%E5%89%B2%E3%82%8A%E5%BD%93%E3%81%A6%E3%82%8B%E6%83%B3%E5%AE%9A%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20k%20%3D%20len%28nums%29%20%2F%2F%202%0A%20%20%20%20buckets%20%3D%20%5B%5B%5D%20for%20_%20in%20range%28k%29%5D%0A%20%20%20%20%23%201.%20%E9%85%8D%E5%88%97%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%84%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%81%AB%E6%8C%AF%E3%82%8A%E5%88%86%E3%81%91%E3%82%8B%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A5%E5%8A%9B%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E7%AF%84%E5%9B%B2%E3%81%AF%20%5B0%2C%201%29%20%E3%81%A7%E3%81%82%E3%82%8A%E3%80%81num%20%2A%20k%20%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E7%AF%84%E5%9B%B2%20%5B0%2C%20k-1%5D%20%E3%81%AB%E5%86%99%E5%83%8F%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20i%20%3D%20int%28num%20%2A%20k%29%0A%20%20%20%20%20%20%20%20%23%20num%20%E3%82%92%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%20i%20%E3%81%AB%E8%BF%BD%E5%8A%A0%0A%20%20%20%20%20%20%20%20buckets%5Bi%5D.append%28num%29%0A%20%20%20%20%23%202.%20%E5%90%84%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%AE%E3%82%BD%E3%83%BC%E3%83%88%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%BF%E3%81%86%E3%80%82%E4%BB%96%E3%81%AE%E3%82%BD%E3%83%BC%E3%83%88%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%AB%E7%BD%AE%E3%81%8D%E6%8F%9B%E3%81%88%E3%81%A6%E3%82%82%E3%82%88%E3%81%84%0A%20%20%20%20%20%20%20%20bucket.sort%28%29%0A%20%20%20%20%23%203.%20%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92%E8%B5%B0%E6%9F%BB%E3%81%97%E3%81%A6%E7%B5%90%E6%9E%9C%E3%82%92%E7%B5%90%E5%90%88%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20for%20num%20in%20bucket%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%85%A5%E5%8A%9B%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AF%E7%AF%84%E5%9B%B2%20%5B0%2C%201%29%20%E3%81%AE%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20nums%20%3D%20%5B0.49%2C%200.96%2C%200.82%2C%200.09%2C%200.57%2C%200.43%2C%200.91%2C%200.75%2C%200.15%2C%200.37%5D%0A%20%20%20%20bucket_sort%28nums%29%0A%20%20%20%20print%28%22%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bucket_sort%28nums%3A%20list%5Bfloat%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20k%20%3D%20n%2F2%20%E5%80%8B%E3%81%AE%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%97%E3%80%81%E5%90%84%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%81%AB%202%20%E8%A6%81%E7%B4%A0%E3%81%9A%E3%81%A4%E5%89%B2%E3%82%8A%E5%BD%93%E3%81%A6%E3%82%8B%E6%83%B3%E5%AE%9A%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20k%20%3D%20len%28nums%29%20%2F%2F%202%0A%20%20%20%20buckets%20%3D%20%5B%5B%5D%20for%20_%20in%20range%28k%29%5D%0A%20%20%20%20%23%201.%20%E9%85%8D%E5%88%97%E8%A6%81%E7%B4%A0%E3%82%92%E5%90%84%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%81%AB%E6%8C%AF%E3%82%8A%E5%88%86%E3%81%91%E3%82%8B%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A5%E5%8A%9B%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E7%AF%84%E5%9B%B2%E3%81%AF%20%5B0%2C%201%29%20%E3%81%A7%E3%81%82%E3%82%8A%E3%80%81num%20%2A%20k%20%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E7%AF%84%E5%9B%B2%20%5B0%2C%20k-1%5D%20%E3%81%AB%E5%86%99%E5%83%8F%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20i%20%3D%20int%28num%20%2A%20k%29%0A%20%20%20%20%20%20%20%20%23%20num%20%E3%82%92%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%20i%20%E3%81%AB%E8%BF%BD%E5%8A%A0%0A%20%20%20%20%20%20%20%20buckets%5Bi%5D.append%28num%29%0A%20%20%20%20%23%202.%20%E5%90%84%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%AE%E3%82%BD%E3%83%BC%E3%83%88%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%BF%E3%81%86%E3%80%82%E4%BB%96%E3%81%AE%E3%82%BD%E3%83%BC%E3%83%88%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%AB%E7%BD%AE%E3%81%8D%E6%8F%9B%E3%81%88%E3%81%A6%E3%82%82%E3%82%88%E3%81%84%0A%20%20%20%20%20%20%20%20bucket.sort%28%29%0A%20%20%20%20%23%203.%20%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92%E8%B5%B0%E6%9F%BB%E3%81%97%E3%81%A6%E7%B5%90%E6%9E%9C%E3%82%92%E7%B5%90%E5%90%88%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20for%20num%20in%20bucket%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%85%A5%E5%8A%9B%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AF%E7%AF%84%E5%9B%B2%20%5B0%2C%201%29%20%E3%81%AE%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20nums%20%3D%20%5B0.49%2C%200.96%2C%200.82%2C%200.09%2C%200.57%2C%200.43%2C%200.91%2C%200.75%2C%200.15%2C%200.37%5D%0A%20%20%20%20bucket_sort%28nums%29%0A%20%20%20%20print%28%22%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
- **時間計算量は $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.2 &nbsp; アルゴリズムの特性
## 11.8.3 &nbsp; 均等分散を達成する方法
バケットソートは、非常に大規模なデータの処理に適しています。たとえば、入力データに 100 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。
バケットソートの理論的時間計算量は $O(n)$ に達することができます。**重要なことは、すべてのバケットに要素を均等に分散させることです**。実世界のデータはしばしば均一に分散されていないからです。例えば、eBayのすべての商品を価格範囲で10個のバケットに均等に分散させたいとします。しかし、商品価格の分散は均等でない可能性があり、100ドル未満の商品が多く、500ドル以上の商品が少ないかもしれません。価格範囲を均等に10分割すると、各バケットの商品数の差が大きくなります。
- **時間計算量は $O(n + k)$** :要素が各バケット内に平均的に分布していると仮定すると、各バケット内の要素数は $\frac{n}{k}$ です。1 つのバケットをソートするのに $O(\frac{n}{k} \log\frac{n}{k})$ の時間がかかるなら、すべてのバケットのソートには $O(n \log\frac{n}{k})$ の時間がかかります。**バケット数 $k$ が十分大きいとき、時間計算量は $O(n)$ に近づきます**。結果を結合する際には、すべてのバケットと要素を走査する必要があり、$O(n + k)$ の時間を要します。最悪の場合、すべてのデータが 1 つのバケットに割り当てられ、そのバケットのソートに $O(n^2)$ の時間がかかります。
- **空間計算量は $O(n + k)$、非インプレースソート**:$k$ 個のバケットと合計 $n$ 個の要素ぶんの追加領域が必要です。
- バケットソートが安定かどうかは、バケット内要素のソートに用いるアルゴリズムが安定かどうかに依存します。
均等分散を達成するために、最初におおよその境界を設定して、データを3つのバケットに大まかに分割できます。**分散が完了した後、より多くのアイテムを持つバケットをさらに3つのバケットに分割し、すべてのバケットの要素数がほぼ等しくなるまで続けます**。
## 11.8.3 &nbsp; 均等な分配を実現するには
以下の図に示すように、この方法は本質的に再帰木を構築し、葉ノードの要素数ができるだけ均等になることを目指します。もちろん、各ラウンドでデータを3つのバケットに分割する必要はありません - 分割戦略はデータの独特な特性に適応的に調整できます。
バケットソートの時間計算量は理論上 $O(n)$ に達しますが、**鍵は要素を各バケットへ均等に分配すること** にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。
![バケットの再帰的分割](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }
均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。**分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます**。
<p align="center"> 図 11-14 &nbsp; バケットの再帰的分割 </p>
以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 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>
以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。
![確率分布に基づいてバケットを分割](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" }
<p align="center"> 図 11-15 &nbsp; 確率分布に基づいてバケットを分割 </p>
+605 -93
View File
@@ -4,38 +4,36 @@ comments: true
# 11.9 &nbsp; 計数ソート
<u>計数ソート</u>は要素数をカウントすることでソートを実現し、通常は整数配列に適用されます。
<u>計数ソートcounting sort</u>は要素数を集計することでソートを実現し、通常は整数配列に適用されます。
## 11.9.1 &nbsp; 単な実装
## 11.9.1 &nbsp; 単な実装
簡単な例から始めましょう。長さ $n$ の配列 `nums` が与えられ、すべての要素が「非負整数」である場合、計数ソートの全体的な過程は以下の図に示されています。
まず簡単な例を見てみましょう。長さ $n$ の配列 `nums` が与えられ、その要素はすべて「非負整数」であるとします。計数ソートの全体的な流れを以下の図に示ます。
1. 配列を走査し最大を見つけ、それを $m$ とし、長さ $m + 1$ の補助配列 `counter` を作成します。
2. **`counter`使用し`nums` 内の各数の出現回数をカウントします**。ここで `counter[num]` は数 `num` の出現回数に対応します。カウント方法は簡単で、`nums` を走査し(現在の数を `num` とする)、各ラウンド`counter[num]` を $1$ 増やします。
3. **`counter` のインデックスは自然に順序けられているため、すべての数は本質的にすでにソートされています**。次に、`counter` を走査し、出現順に `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" }
![計数ソートの流れ](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-16 &nbsp; 計数ソートの過程 </p>
<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. 各数字の出現回数を統計
# 簡易版。オブジェクトのソートには使ない
# 1. 配列の最大要素 m を求める
m = max(nums)
# 2. 各数値の出現回数を数える
# counter[num] は num の出現回数を表す
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. counter を走査し、各要素を元の配列 nums に埋め戻し
# 3. counter を走査し、各要素を元の配列 nums に書き戻す
i = 0
for num in range(m + 1):
for _ in range(counter[num]):
@@ -46,21 +44,21 @@ comments: true
=== "C++"
```cpp title="counting_sort.cpp"
/* カウントソート */
// 簡単な実装、オブジェクトのソートには使用できない
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使ない
void countingSortNaive(vector<int> &nums) {
// 1. 配列の最大要素mを統計
// 1. 配列の最大要素 m を求める
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 各数の出現回数を統計
// counter[num]numの出現回数を表す
// 2. 各数の出現回数を数える
// counter[num]num の出現回数を表す
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. counterを走査し、各要素を元の配列nums戻す
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
@@ -74,20 +72,20 @@ comments: true
```java title="counting_sort.java"
/* 計数ソート */
// 簡単な実装、オブジェクトのソートには使用できない
// 簡易実装のため、オブジェクトのソートには使ない
void countingSortNaive(int[] nums) {
// 1. 配列の最大要素 m を統計
// 1. 配列の最大要素 m を求める
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 各数の出現回数を統計
// 2. 各数の出現回数を数える
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. counter を走査し、各要素を元の配列 nums に戻す
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
@@ -100,86 +98,297 @@ comments: true
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
void CountingSortNaive(int[] nums) {
// 1. 配列の最大要素 m を求める
int m = 0;
foreach (int num in nums) {
m = Math.Max(m, num);
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
foreach (int num in 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;
}
}
}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
func countingSortNaive(nums []int) {
// 1. 配列の最大要素 m を求める
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
for i, num := 0, 0; num < m+1; num++ {
for j := 0; j < counter[num]; j++ {
nums[i] = num
i++
}
}
}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
func countingSortNaive(nums: inout [Int]) {
// 1. 配列の最大要素 m を求める
let m = nums.max()!
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
var counter = Array(repeating: 0, count: m + 1)
for num in nums {
counter[num] += 1
}
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
var i = 0
for num in 0 ..< m + 1 {
for _ in 0 ..< counter[num] {
nums[i] = num
i += 1
}
}
}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
function countingSortNaive(nums) {
// 1. 配列の最大要素 m を求める
let m = Math.max(...nums);
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
const counter = new Array(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
let i = 0;
for (let num = 0; num < m + 1; num++) {
for (let j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
function countingSortNaive(nums: number[]): void {
// 1. 配列の最大要素 m を求める
let m: number = Math.max(...nums);
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
let i = 0;
for (let num = 0; num < m + 1; num++) {
for (let j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
void countingSortNaive(List<int> nums) {
// 1. 配列の最大要素 m を求める
int m = 0;
for (int _num in nums) {
m = max(m, _num);
}
// 2. 各数値の出現回数を数える
// counter[_num] は _num の出現回数を表す
List<int> counter = List.filled(m + 1, 0);
for (int _num in 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;
}
}
}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort_naive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
fn counting_sort_naive(nums: &mut [i32]) {
// 1. 配列の最大要素 m を求める
let m = *nums.iter().max().unwrap();
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
let mut counter = vec![0; m as usize + 1];
for &num in nums.iter() {
counter[num as usize] += 1;
}
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
let mut i = 0;
for num in 0..m + 1 {
for _ in 0..counter[num as usize] {
nums[i] = num;
i += 1;
}
}
}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
void countingSortNaive(int nums[], int size) {
// 1. 配列の最大要素 m を求める
int m = 0;
for (int i = 0; i < size; i++) {
if (nums[i] > m) {
m = nums[i];
}
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
int *counter = calloc(m + 1, sizeof(int));
for (int i = 0; i < size; i++) {
counter[nums[i]]++;
}
// 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;
}
}
// 4. メモリを解放する
free(counter);
}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSortNaive}
/* 計数ソート */
// 簡易実装のため、オブジェクトのソートには使えない
fun countingSortNaive(nums: IntArray) {
// 1. 配列の最大要素 m を求める
var m = 0
for (num in nums) {
m = max(m, num)
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
val counter = IntArray(m + 1)
for (num in nums) {
counter[num]++
}
// 3. counter を走査し、各要素を元の配列 nums に書き戻す
var i = 0
for (num in 0..<m + 1) {
var j = 0
while (j < counter[num]) {
nums[i] = num
j++
i++
}
}
}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort_naive}
### 計数ソート ###
def counting_sort_naive(nums)
# 簡易版。オブジェクトのソートには使えない
# 1. 配列の最大要素 m を求める
m = 0
nums.each { |num| m = [m, num].max }
# 2. 各数値の出現回数を数える
# counter[num] は num の出現回数を表す
counter = Array.new(m + 1, 0)
nums.each { |num| counter[num] += 1 }
# 3. counter を走査し、各要素を元の配列 nums に書き戻す
i = 0
for num in 0...(m + 1)
(0...counter[num]).each do
nums[i] = num
i += 1
end
end
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort_naive%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%A8%88%E6%95%B0%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E7%B0%A1%E6%98%93%E7%89%88%E3%80%82%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%82%BD%E3%83%BC%E3%83%88%E3%81%AB%E3%81%AF%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84%0A%20%20%20%20%23%201.%20%E9%85%8D%E5%88%97%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%20m%20%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B%0A%20%20%20%20m%20%3D%200%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20max%28m%2C%20num%29%0A%20%20%20%20%23%202.%20%E5%90%84%E6%95%B0%E5%80%A4%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E6%95%B0%E3%81%88%E3%82%8B%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E3%81%AF%20num%20%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E8%A1%A8%E3%81%99%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20counter%20%E3%82%92%E8%B5%B0%E6%9F%BB%E3%81%97%E3%80%81%E5%90%84%E8%A6%81%E7%B4%A0%E3%82%92%E5%85%83%E3%81%AE%E9%85%8D%E5%88%97%20nums%20%E3%81%AB%E6%9B%B8%E3%81%8D%E6%88%BB%E3%81%99%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20num%20in%20range%28m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20_%20in%20range%28counter%5Bnum%5D%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort_naive%28nums%29%0A%20%20%20%20print%28f%22%E8%A8%88%E6%95%B0%E3%82%BD%E3%83%BC%E3%83%88%EF%BC%88%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AF%E3%82%BD%E3%83%BC%E3%83%88%E4%B8%8D%E5%8F%AF%EF%BC%89%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort_naive%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%A8%88%E6%95%B0%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E7%B0%A1%E6%98%93%E7%89%88%E3%80%82%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%82%BD%E3%83%BC%E3%83%88%E3%81%AB%E3%81%AF%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84%0A%20%20%20%20%23%201.%20%E9%85%8D%E5%88%97%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%20m%20%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B%0A%20%20%20%20m%20%3D%200%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20max%28m%2C%20num%29%0A%20%20%20%20%23%202.%20%E5%90%84%E6%95%B0%E5%80%A4%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E6%95%B0%E3%81%88%E3%82%8B%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E3%81%AF%20num%20%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E8%A1%A8%E3%81%99%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20counter%20%E3%82%92%E8%B5%B0%E6%9F%BB%E3%81%97%E3%80%81%E5%90%84%E8%A6%81%E7%B4%A0%E3%82%92%E5%85%83%E3%81%AE%E9%85%8D%E5%88%97%20nums%20%E3%81%AB%E6%9B%B8%E3%81%8D%E6%88%BB%E3%81%99%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20num%20in%20range%28m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20_%20in%20range%28counter%5Bnum%5D%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort_naive%28nums%29%0A%20%20%20%20print%28f%22%E8%A8%88%E6%95%B0%E3%82%BD%E3%83%BC%E3%83%88%EF%BC%88%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AF%E3%82%BD%E3%83%BC%E3%83%88%E4%B8%8D%E5%8F%AF%EF%BC%89%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
!!! note "計数ソートとバケットソートの関係"
バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。
バケットソートの観点から見ると、計数ソートにおける計数配列 `counter` の各インデックスを 1 つのバケットとみなし、個数を数える過程を要素を対応するバケットへ振り分ける操作とみなせます。本質的に、計数ソートは整数データにおけるバケットソートの特殊な一例です。
## 11.9.2 &nbsp; 完全な実装
注意深い読者は気付くかもしれませんが、**入力データがオブジェクト場合、上記の手順 `3.`無効です**。入力データが商品オブジェクトで価格(クラスメンバ変数)商品をソートしたいとします。しかし上記のアルゴリズムは結果としてソート済みの価格のみを提供できます。
注意深い読者なら、**入力データがオブジェクトである場合、上記の手順 `3.`機能しない**ことに気づくかもしれません。入力データが商品オブジェクトであり、商品価格(クラスメンバ変数)に基づいて商品をソートしたいとします。しかし上記のアルゴリズムが返せるのは価格のソート結果だけです。
では、元のデータのソート結果をどのように取得できるでしょうかまず`counter` の「前置和」を計算します。名前が示すように、インデックス `i` での前置`prefix[i]` は、配列の最初の `i` の要素の和に等しいです:
では、元のデータのソート結果を得るにはどうすればよいのでしょうかまず `counter` の「累積和」を計算します。名前のとおり、インデックス `i` における累積`prefix[i]` は、配列の先頭から `i` 番目までの要素の和に等しくなります:
$$
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
$$
**前置和には明確な意味があります。`prefix[num] - 1` は結果配列 `res`おける要素 `num` の最後の出現のインデックスを表します**。この情報は重要で、各要素が結果配列のどに現れるべきかを教えてくれます。次に、元の配列 `nums` 各要素 `num` を逆順で走査し、各反復で以下の2つの手順を実行します。
**累積和には明確な意味があり`prefix[num] - 1`要素 `num`結果配列 `res`最後に現れるインデックスを表します**。この情報は非常に重要で、各要素が結果配列のどの位置に現れるべきかを示してくれます。続いて元の配列 `nums` を逆順に走査し、各要素 `num` に対して各反復で次の 2 つの手順を行います。
1. インデックス `prefix[num] - 1` で配列 `res``num` を埋めます。
2. 前置`prefix[num]` を $1$ 減らし`num` を配置する次のインデックスを取得します。
1. `num` を配列 `res`インデックス `prefix[num] - 1` に格納します。
2. 累積`prefix[num]` を $1$ 減らし、次に `num` を配置するインデックスをます。
走査、配列 `res`ソートされた結果が含まれ、最後に `res` 元の配列 `nums`置き換えます。完全な計数ソートの過程は以下の図に示されています。
走査が完了すると、配列 `res` にソート済みの結果が格納されます。最後に `res` 元の配列 `nums`上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。
=== "<1>"
![計数ソートの過程](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
![計数ソートの手順](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
=== "<2>"
![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" }
@@ -202,36 +411,36 @@ $$
=== "<8>"
![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" }
<p align="center"> 図 11-17 &nbsp; 計数ソートの過程 </p>
<p align="center"> 図 11-17 &nbsp; 計数ソートの手順 </p>
計数ソートの実装コードは以下のりです:
計数ソートの実装コードは以下のとおりです:
=== "Python"
```python title="counting_sort.py"
def counting_sort(nums: list[int]):
"""計数ソート"""
# 完全な実装、オブジェクトソートが可能で、安定ソート
# 1. 配列の最大要素 m を統計
# 完全版。オブジェクトソートでき、かつ安定ソートである
# 1. 配列の最大要素 m を求める
m = max(nums)
# 2. 各数の出現回数を統計
# 2. 各数の出現回数を数える
# counter[num] は num の出現回数を表す
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. counter の前置和を計算し、「出現回数」を「末尾インデックス」に変換
# counter[num]-1 は res において num が最後に出現するインデックス
# 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
# つまり counter[num]-1 は、num が res に最後に現れるインデックス
for i in range(m):
counter[i + 1] += counter[i]
# 4. nums を逆順に走査し、各要素を結果配列 res に配置
# 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 を上書き
counter[num] -= 1 # 累積和を 1 減らして、次に num を配置するインデックスを得
# 結果配列 res 元の配列 nums を上書きする
for i in range(n):
nums[i] = res[i]
```
@@ -239,35 +448,35 @@ $$
=== "C++"
```cpp title="counting_sort.cpp"
/* カウントソート */
// 完全な実装、オブジェクトソートが可能で安定ソート
/* 計数ソート */
// 完全な実装、オブジェクトソートでき、かつ安定ソートである
void countingSort(vector<int> &nums) {
// 1. 配列の最大要素mを統計
// 1. 配列の最大要素 m を求める
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 各数の出現回数を統計
// counter[num]numの出現回数を表す
// 2. 各数の出現回数を数える
// counter[num]num の出現回数を表す
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. counterの前缀和を計算し、「出現回数」を「末尾インデックス」に変換
// counter[num]-1はnumがresで現れる最後のインデックス
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. numsを逆順走査し、各要素を結果配列resに配置
// 結果を記録する配列resを初期化
// 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[counter[num] - 1] = num; // num を対応するインデックスに配置
counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得
}
// 結果配列resで元の配列numsを上書き
// 結果配列 res で元の配列 nums を上書きする
nums = res;
}
```
@@ -276,34 +485,34 @@ $$
```java title="counting_sort.java"
/* 計数ソート */
// 完全な実装、オブジェクトをソートでき、安定ソート
// 完全な実装、オブジェクトをソートでき、かつ安定ソートである
void countingSort(int[] nums) {
// 1. 配列の最大要素 m を統計
// 1. 配列の最大要素 m を求める
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 各数の出現回数を統計
// 2. 各数の出現回数を数える
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. counter の累積和を計算し、「出現回数」を「尾インデックス」に変換
// counter[num]-1 は res 内で num が出現する最後のインデックス
// 3. counter の累積和を求めて、「出現回数」を「尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. nums を逆順に走査し、各要素を結果配列 res に配置
// 結果を記録する配列 res を初期化
// 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 を配置する次のインデックスを
counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得
}
// 結果配列 res を使って元の配列 nums を上書き
// 結果配列 res 元の配列 nums を上書きする
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
@@ -313,73 +522,376 @@ $$
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
void CountingSort(int[] nums) {
// 1. 配列の最大要素 m を求める
int m = 0;
foreach (int num in nums) {
m = Math.Max(m, num);
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
foreach (int num in 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.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];
}
}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
func countingSort(nums []int) {
// 1. 配列の最大要素 m を求める
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for i := 0; i < m; i++ {
counter[i+1] += counter[i]
}
// 4. nums を逆順に走査し、各要素を結果配列 res に格納する
// 結果を記録するための配列 res を初期化
n := len(nums)
res := make([]int, n)
for i := n - 1; i >= 0; i-- {
num := nums[i]
// num を対応するインデックスに配置
res[counter[num]-1] = num
// 累積和を 1 減らして、次に num を配置するインデックスを得る
counter[num]--
}
// 結果配列 res で元の配列 nums を上書きする
copy(nums, res)
}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
func countingSort(nums: inout [Int]) {
// 1. 配列の最大要素 m を求める
let m = nums.max()!
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
var counter = Array(repeating: 0, count: m + 1)
for num in nums {
counter[num] += 1
}
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for i in 0 ..< m {
counter[i + 1] += counter[i]
}
// 4. nums を逆順に走査し、各要素を結果配列 res に格納する
// 結果を記録するための配列 res を初期化
var res = Array(repeating: 0, count: nums.count)
for i in nums.indices.reversed() {
let num = nums[i]
res[counter[num] - 1] = num // num を対応するインデックスに配置
counter[num] -= 1 // 累積和を 1 減らして、次に num を配置するインデックスを得る
}
// 結果配列 res で元の配列 nums を上書きする
for i in nums.indices {
nums[i] = res[i]
}
}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
function countingSort(nums) {
// 1. 配列の最大要素 m を求める
let m = Math.max(...nums);
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
const counter = new Array(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for (let i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. nums を逆順に走査し、各要素を結果配列 res に格納する
// 結果を記録するための配列 res を初期化
const n = nums.length;
const res = new Array(n);
for (let i = n - 1; i >= 0; i--) {
const num = nums[i];
res[counter[num] - 1] = num; // num を対応するインデックスに配置
counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る
}
// 結果配列 res で元の配列 nums を上書きする
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
function countingSort(nums: number[]): void {
// 1. 配列の最大要素 m を求める
let m: number = Math.max(...nums);
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for (let i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. nums を逆順に走査し、各要素を結果配列 res に格納する
// 結果を記録するための配列 res を初期化
const n = nums.length;
const res: number[] = new Array<number>(n);
for (let i = n - 1; i >= 0; i--) {
const num = nums[i];
res[counter[num] - 1] = num; // num を対応するインデックスに配置
counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る
}
// 結果配列 res で元の配列 nums を上書きする
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
void countingSort(List<int> nums) {
// 1. 配列の最大要素 m を求める
int m = 0;
for (int _num in nums) {
m = max(m, _num);
}
// 2. 各数値の出現回数を数える
// counter[_num] は _num の出現回数を表す
List<int> counter = List.filled(m + 1, 0);
for (int _num in 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;
List<int> res = List.filled(n, 0);
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.setAll(0, res);
}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
fn counting_sort(nums: &mut [i32]) {
// 1. 配列の最大要素 m を求める
let m = *nums.iter().max().unwrap() as usize;
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
let mut counter = vec![0; m + 1];
for &num in nums.iter() {
counter[num as usize] += 1;
}
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for i in 0..m {
counter[i + 1] += counter[i];
}
// 4. nums を逆順に走査し、各要素を結果配列 res に格納する
// 結果を記録するための配列 res を初期化
let n = nums.len();
let mut res = vec![0; n];
for i in (0..n).rev() {
let num = nums[i];
res[counter[num as usize] - 1] = num; // num を対応するインデックスに配置
counter[num as usize] -= 1; // 累積和を 1 減らして、次に num を配置するインデックスを得る
}
// 結果配列 res で元の配列 nums を上書きする
nums.copy_from_slice(&res)
}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
void countingSort(int nums[], int size) {
// 1. 配列の最大要素 m を求める
int m = 0;
for (int i = 0; i < size; i++) {
if (nums[i] > m) {
m = nums[i];
}
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
int *counter = calloc(m, sizeof(int));
for (int i = 0; i < size; i++) {
counter[nums[i]]++;
}
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. nums を逆順に走査し、各要素を結果配列 res に格納する
// 結果を記録するための配列 res を初期化
int *res = malloc(sizeof(int) * size);
for (int i = size - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // num を対応するインデックスに配置
counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る
}
// 結果配列 res で元の配列 nums を上書きする
memcpy(nums, res, size * sizeof(int));
// 5. メモリを解放する
free(res);
free(counter);
}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSort}
/* 計数ソート */
// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである
fun countingSort(nums: IntArray) {
// 1. 配列の最大要素 m を求める
var m = 0
for (num in nums) {
m = max(m, num)
}
// 2. 各数値の出現回数を数える
// counter[num] は num の出現回数を表す
val counter = IntArray(m + 1)
for (num in nums) {
counter[num]++
}
// 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
// つまり counter[num]-1 は、num が res に最後に現れるインデックス
for (i in 0..<m) {
counter[i + 1] += counter[i]
}
// 4. nums を逆順に走査し、各要素を結果配列 res に格納する
// 結果を記録するための配列 res を初期化
val n = nums.size
val res = IntArray(n)
for (i in n - 1 downTo 0) {
val num = nums[i]
res[counter[num] - 1] = num // num を対応するインデックスに配置
counter[num]-- // 累積和を 1 減らして、次に num を配置するインデックスを得る
}
// 結果配列 res で元の配列 nums を上書きする
for (i in 0..<n) {
nums[i] = res[i]
}
}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort}
### 計数ソート ###
def counting_sort(nums)
# 完全版。オブジェクトをソートでき、かつ安定ソートである
# 1. 配列の最大要素 m を求める
m = nums.max
# 2. 各数値の出現回数を数える
# counter[num] は num の出現回数を表す
counter = Array.new(m + 1, 0)
nums.each { |num| counter[num] += 1 }
# 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する
# つまり counter[num]-1 は、num が res に最後に現れるインデックス
(0...m).each { |i| counter[i + 1] += counter[i] }
# 4. nums を逆順に走査し、各要素を結果配列 res に格納する
# 結果を記録するための配列 res を初期化する
n = nums.length
res = Array.new(n, 0)
(n - 1).downto(0).each do |i|
num = nums[i]
res[counter[num] - 1] = num # num を対応するインデックスに配置
counter[num] -= 1 # 累積和を 1 減らして、次に num を配置するインデックスを得る
end
# 結果配列 res で元の配列 nums を上書きする
(0...n).each { |i| nums[i] = res[i] }
end
```
## 11.9.3 &nbsp; アルゴリズムの特徴
??? pythontutor "コードの可視化"
- **時間計算量は $O(n + m)$、非適応ソート**`nums``counter` の走査が含まれ、どちらも線形時間を使用します。一般的に、$n \gg m$ であり、時間計算量は $O(n)$ に近づきます。
- **空間計算量は $O(n + m)$、非インプレースソート**:長さ $n$ の配列 `res` と長さ $m$ の配列 `counter` をそれぞれ使用します。
- **安定ソート**:要素が「右から左」の順序で `res` に埋められるため、`nums` の走査を逆順にすることで、等しい要素間の相対位置の変化を防ぎ、安定したソートを実現できます。実際、`nums` を順番に走査しても正しいソート結果を生成できますが、結果は不安定です。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%A8%88%E6%95%B0%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E5%AE%8C%E5%85%A8%E7%89%88%E3%80%82%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88%E3%81%A7%E3%81%8D%E3%80%81%E3%81%8B%E3%81%A4%E5%AE%89%E5%AE%9A%E3%82%BD%E3%83%BC%E3%83%88%E3%81%A7%E3%81%82%E3%82%8B%0A%20%20%20%20%23%201.%20%E9%85%8D%E5%88%97%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%20m%20%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B%0A%20%20%20%20m%20%3D%20max%28nums%29%0A%20%20%20%20%23%202.%20%E5%90%84%E6%95%B0%E5%80%A4%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E6%95%B0%E3%81%88%E3%82%8B%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E3%81%AF%20num%20%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E8%A1%A8%E3%81%99%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20counter%20%E3%81%AE%E7%B4%AF%E7%A9%8D%E5%92%8C%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6%E3%80%81%E3%80%8C%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%80%8D%E3%82%92%E3%80%8C%E6%9C%AB%E5%B0%BE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%80%8D%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%0A%20%20%20%20%23%20%E3%81%A4%E3%81%BE%E3%82%8A%20counter%5Bnum%5D-1%20%E3%81%AF%E3%80%81num%20%E3%81%8C%20res%20%E3%81%AB%E6%9C%80%E5%BE%8C%E3%81%AB%E7%8F%BE%E3%82%8C%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%0A%20%20%20%20for%20i%20in%20range%28m%29%3A%0A%20%20%20%20%20%20%20%20counter%5Bi%20%2B%201%5D%20%2B%3D%20counter%5Bi%5D%0A%20%20%20%20%23%204.%20nums%20%E3%82%92%E9%80%86%E9%A0%86%E3%81%AB%E8%B5%B0%E6%9F%BB%E3%81%97%E3%80%81%E5%90%84%E8%A6%81%E7%B4%A0%E3%82%92%E7%B5%90%E6%9E%9C%E9%85%8D%E5%88%97%20res%20%E3%81%AB%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%0A%20%20%20%20%23%20%E7%B5%90%E6%9E%9C%E3%82%92%E8%A8%98%E9%8C%B2%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E9%85%8D%E5%88%97%20res%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20res%20%3D%20%5B0%5D%20%2A%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20num%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20res%5Bcounter%5Bnum%5D%20-%201%5D%20%3D%20num%20%20%23%20num%20%E3%82%92%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AB%E9%85%8D%E7%BD%AE%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20-%3D%201%20%20%23%20%E7%B4%AF%E7%A9%8D%E5%92%8C%E3%82%92%201%20%E6%B8%9B%E3%82%89%E3%81%97%E3%81%A6%E3%80%81%E6%AC%A1%E3%81%AB%20num%20%E3%82%92%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E5%BE%97%E3%82%8B%0A%20%20%20%20%23%20%E7%B5%90%E6%9E%9C%E9%85%8D%E5%88%97%20res%20%E3%81%A7%E5%85%83%E3%81%AE%E9%85%8D%E5%88%97%20nums%20%E3%82%92%E4%B8%8A%E6%9B%B8%E3%81%8D%E3%81%99%E3%82%8B%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20res%5Bi%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort%28nums%29%0A%20%20%20%20print%28f%22%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%A8%88%E6%95%B0%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E5%AE%8C%E5%85%A8%E7%89%88%E3%80%82%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88%E3%81%A7%E3%81%8D%E3%80%81%E3%81%8B%E3%81%A4%E5%AE%89%E5%AE%9A%E3%82%BD%E3%83%BC%E3%83%88%E3%81%A7%E3%81%82%E3%82%8B%0A%20%20%20%20%23%201.%20%E9%85%8D%E5%88%97%E3%81%AE%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%20m%20%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B%0A%20%20%20%20m%20%3D%20max%28nums%29%0A%20%20%20%20%23%202.%20%E5%90%84%E6%95%B0%E5%80%A4%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E6%95%B0%E3%81%88%E3%82%8B%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E3%81%AF%20num%20%E3%81%AE%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%82%92%E8%A1%A8%E3%81%99%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20counter%20%E3%81%AE%E7%B4%AF%E7%A9%8D%E5%92%8C%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6%E3%80%81%E3%80%8C%E5%87%BA%E7%8F%BE%E5%9B%9E%E6%95%B0%E3%80%8D%E3%82%92%E3%80%8C%E6%9C%AB%E5%B0%BE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%80%8D%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%0A%20%20%20%20%23%20%E3%81%A4%E3%81%BE%E3%82%8A%20counter%5Bnum%5D-1%20%E3%81%AF%E3%80%81num%20%E3%81%8C%20res%20%E3%81%AB%E6%9C%80%E5%BE%8C%E3%81%AB%E7%8F%BE%E3%82%8C%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%0A%20%20%20%20for%20i%20in%20range%28m%29%3A%0A%20%20%20%20%20%20%20%20counter%5Bi%20%2B%201%5D%20%2B%3D%20counter%5Bi%5D%0A%20%20%20%20%23%204.%20nums%20%E3%82%92%E9%80%86%E9%A0%86%E3%81%AB%E8%B5%B0%E6%9F%BB%E3%81%97%E3%80%81%E5%90%84%E8%A6%81%E7%B4%A0%E3%82%92%E7%B5%90%E6%9E%9C%E9%85%8D%E5%88%97%20res%20%E3%81%AB%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%0A%20%20%20%20%23%20%E7%B5%90%E6%9E%9C%E3%82%92%E8%A8%98%E9%8C%B2%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E9%85%8D%E5%88%97%20res%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20res%20%3D%20%5B0%5D%20%2A%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20num%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20res%5Bcounter%5Bnum%5D%20-%201%5D%20%3D%20num%20%20%23%20num%20%E3%82%92%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AB%E9%85%8D%E7%BD%AE%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20-%3D%201%20%20%23%20%E7%B4%AF%E7%A9%8D%E5%92%8C%E3%82%92%201%20%E6%B8%9B%E3%82%89%E3%81%97%E3%81%A6%E3%80%81%E6%AC%A1%E3%81%AB%20num%20%E3%82%92%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E5%BE%97%E3%82%8B%0A%20%20%20%20%23%20%E7%B5%90%E6%9E%9C%E9%85%8D%E5%88%97%20res%20%E3%81%A7%E5%85%83%E3%81%AE%E9%85%8D%E5%88%97%20nums%20%E3%82%92%E4%B8%8A%E6%9B%B8%E3%81%8D%E3%81%99%E3%82%8B%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20res%5Bi%5D%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort%28nums%29%0A%20%20%20%20print%28f%22%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 11.9.4 &nbsp; 制限事項
## 11.9.3 &nbsp; アルゴリズムの特性
今までに、計数ソートは非常に巧妙だと感じるかもしれません。単に量をカウントするだけで効率的なソートを実現できるからです。しかし、計数ソートを使用するための前提条件は比較的厳しいです。
- **時間計算量は $O(n + m)$、非適応ソート** `nums` の走査と `counter` の走査が含まれ、いずれも線形時間です。一般には $n \gg m$ であり、時間計算量は $O(n)$ に近づきます。
- **空間計算量は $O(n + m)$、非インプレースソート**:長さがそれぞれ $n$ と $m$ の配列 `res``counter` を利用します。
- **安定ソート**`res` に要素を埋める順序が「右から左」であるため、`nums` を逆順に走査することで等しい要素どうしの相対位置が変化するのを防ぎ、安定ソートを実現できます。実際には、`nums` を順方向に走査しても正しいソート結果は得られますが、その結果は安定ではありません。
**計数ソートは非負整数にのみ適用できます**。他のタイプのデータに適用したい場合、これらのデータが要素の元の順序を変更することなく非負整数に変換できることを保証する必要があります。例えば、負の整数を含む配列の場合、最初にすべての数に定数を加えて、すべてを正の数に変換し、ソート完了後に元に戻すことができます。
## 11.9.4 &nbsp; 制約
**計数ソートは値の範囲が小さい大きなデータセットに適しています**。例えば、上記の例では、$m$ は大きすぎるべきではありません。そうでなければ、あまりにも多くのスペースを占有してしまいます。そして $n \ll m$ の場合、計数ソートは $O(m)$ 時間を使用し、$O(n \log n)$ ソートアルゴリズムより遅い可能性があります。
ここまで読むと、計数ソートは非常に巧妙で、個数を数えるだけで効率的なソートを実現できると感じるかもしれません。しかし、計数ソートを利用するための前提条件は比較的厳格です。
**計数ソートは非負整数にしか適用できません**。ほかの型のデータに適用したい場合は、それらのデータを非負整数に変換でき、かつ変換の過程で要素間の相対的な大小関係が変わらないことを保証する必要があります。たとえば、負数を含む整数配列に対しては、すべての数値に定数を加えて正数へ変換し、ソート後に元へ戻すことができます。
**計数ソートはデータ量が多く、値域が小さい場合に適しています**。たとえば上記の例では $m$ が大きすぎてはならず、そうでないと過剰な空間を消費します。また、$n \ll m$ のとき、計数ソートは $O(m)$ 時間を要するため、$O(n \log n)$ のソートアルゴリズムより遅くなる可能性があります。
+414 -65
View File
@@ -6,30 +6,30 @@ comments: true
!!! tip
この節を読む前に、「ヒープ」の章を必ず完了させてください。
節を読む前に、「ヒープ」の章を学習済みであることを確認してください。
<u>ヒープソート</u>は、ヒープデータ構造に基づ効率的なソートアルゴリズムです。すでに学習した「ヒープ構築」と「要素の抽出」操作を使用してヒープソートを実できます。
<u>ヒープソートheap sort</u>は、ヒープデータ構造に基づいて実装される効率的なソートアルゴリズムです。すでに学んだ「ヒープ構築操作」と「要素の取り出し操作」を利用してヒープソートを実できます。
1. 配列を入力し最小ヒープを構築します。ここで、最小要素ヒープの頂上に位置します。
2. 継続的に抽出操作を実行し、出された要素を順記録して、最小から最大までのソート済みリストを取得します。
1. 配列を入力し最小ヒープを構築すると、このとき最小要素ヒープの頂点にあります。
2. 取り出し操作を繰り返し実行し、取り出された要素を順記録すれば、昇順に並んだ列が得られます。
の方法は実現可能ですが、ポップされた要素を格納するため追加の配列が必要で、やや空間を消費します。実際には、通常より優雅な実装を使用します。
上の方法でも実行できますが、取り出した要素を保存するため追加の配列が必要となり、空間をやや無駄にします。実際には、通常より洗練された実装方法を用います。
## 11.7.1 &nbsp; アルゴリズムの流れ
配列の長さを $n$ とすると、ヒープソートの過程は以下の通りです。
配列の長さを $n$ とすると、ヒープソートの流れは次図のとおりです。
1. 配列を入力し最大ヒープを構築します。この手順の後、最大要素ヒープの頂上に位置します。
2. ヒープの頂上要素(最初の要素)とヒープの底部要素(最後の要素)を交換します。この交換後、ヒープの長さ $1$ 減し、ソート済み要素の数を $1$ 増します。
3. ヒープの頂上から開始して、上から下へのsift-down操作を実行します。sift-downの後、ヒープの性質が復元されます。
4. 手順 `2.` `3.` を繰り返します。$n - 1$ ラウンドループして、配列のソートを完了します。
1. 配列を入力し最大ヒープを構築します。完了後、最大要素ヒープの頂点にあります。
2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さ $1$ 減し、整列済み要素数は $1$ 増します。
3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift downを実行します。ヒープ化が完了すると、ヒープの性質が回復します。
4. `2.` ステップと第 `3.` ステップを繰り返し実行します。これを $n - 1$ 回繰り返すと、配列の整列が完了します。
!!! tip
実際、要素抽出操作も手順 `2.` `3.` を含み、抽出された要素をヒープから削除する追加の手順があります。
実際には、要素の取り出し操作にも第 `2.` ステップと第 `3.` ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。
=== "<1>"
![ヒープソートの過程](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" }
![ヒープソートの手順](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" }
=== "<2>"
![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" }
@@ -64,17 +64,17 @@ comments: true
=== "<12>"
![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" }
<p align="center"> 図 11-12 &nbsp; ヒープソートの過程 </p>
<p align="center"> 図 11-12 &nbsp; ヒープソートの手順 </p>
コード実装では、「ヒープ」の章からのsift-down関数 `sift_down()` を使用しました。最大要素が抽出されるにつれてヒープの長さが減少するため、`sift_down()` 関数に長さパラメータ $n$ を追加し、ヒープの現在の有効を指定する必要があることに注意することが重要です。コードは以下のりです
コード実装では、「ヒープ」の章と同じ上から下へのヒープ化 `sift_down()` 関数を使用します。注意すべき点として、ヒープの長さは最大要素を取り出すたびに短くなるため、`sift_down()` 関数に長さパラメータ $n$ を追加し、ヒープの現在の有効な長さを指定する必要があります。コードは以下のとおりです
=== "Python"
```python title="heap_sort.py"
def sift_down(nums: list[int], n: int, i: int):
"""ヒープの長さ nノード i から上から下へヒープ化を開始"""
"""ヒープの長さ nノード i から下方向にヒープ化"""
while True:
# i、l、r の中で最大のノードを判定し、ma とする
# ノード i, l, r のうち値が最大のノードを ma とする
l = 2 * i + 1
r = 2 * i + 2
ma = i
@@ -82,34 +82,34 @@ comments: true
ma = l
if r < n and nums[r] > nums[ma]:
ma = r
# ノード i が最大または l、r のインデックスが範囲外の場合、さらなるヒープ化は不要、ループを抜ける
# ノード i が最大またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける
if ma == i:
break
# 2つのノードを交換
# 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 回繰り返す
# ヒープから最大要素を取り出し、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から上から下へヒープ化を開始 */
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
void siftDown(vector<int> &nums, int n, int i) {
while (true) {
// i、l、r の中で最大のノードを決定し、maとして記録
// ノード i, l, r のうち値が最大のノードを ma とする
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
@@ -117,28 +117,28 @@ comments: true
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// ノードiが最大か、インデックスl、rが境界外の場合、それ以上のヒープ化は不要で終了
// ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける
if (ma == i) {
break;
}
// つのノードを交換
// 2 つのノードを交換
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回繰り返す
// ヒープから最大要素を取り出し、n-1 回繰り返す
for (int i = nums.size() - 1; i > 0; --i) {
// ルートノードを最右葉ノード交換(最初の要素を最後の要素交換)
// 根ノードと最も右の葉ノード交換(先頭要素と末尾要素交換)
swap(nums[0], nums[i]);
// ルートノードから上から下へヒープ化を開始
// 根ノードを起点に、上から下へヒープ化
siftDown(nums, i, 0);
}
}
@@ -147,10 +147,10 @@ comments: true
=== "Java"
```java title="heap_sort.java"
/* ヒープの長さは nノード i から上から下へヒープ化開始 */
/* ヒープの長さは nノード i から下方向にヒープ化 */
void siftDown(int[] nums, int n, int i) {
while (true) {
// i, l, r の中で最大のノードを判定し、ma とする
// ノード i, l, r のうち値が最大のノードを ma とする
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
@@ -158,31 +158,31 @@ comments: true
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// ノード i が最大、またはインデックス l, r が範囲外の場合、さらなるヒープ化は不要、ブレーク
// ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける
if (ma == i)
break;
// 2つのノードを交換
// 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 回繰り返
// ヒープから最大要素を取り出し、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);
}
}
@@ -191,85 +191,434 @@ comments: true
=== "C#"
```csharp title="heap_sort.cs"
[class]{heap_sort}-[func]{SiftDown}
/* ヒープの長さは 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 つのノードを交換
(nums[ma], nums[i]) = (nums[i], nums[ma]);
// ループで上から下へヒープ化
i = ma;
}
}
[class]{heap_sort}-[func]{HeapSort}
/* ヒープソート */
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--) {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
(nums[i], nums[0]) = (nums[0], nums[i]);
// 根ノードを起点に、上から下へヒープ化
SiftDown(nums, i, 0);
}
}
```
=== "Go"
```go title="heap_sort.go"
[class]{}-[func]{siftDown}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
func siftDown(nums *[]int, n, i int) {
for true {
// ノード i, l, r のうち値が最大のノードを ma とする
l := 2*i + 1
r := 2*i + 2
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 つのノードを交換
(*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]
// ループで上から下へヒープ化
i = ma
}
}
[class]{}-[func]{heapSort}
/* ヒープソート */
func heapSort(nums *[]int) {
// ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
for i := len(*nums)/2 - 1; i >= 0; i-- {
siftDown(nums, len(*nums), i)
}
// ヒープから最大要素を取り出し、n-1 回繰り返す
for i := len(*nums) - 1; i > 0; i-- {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
(*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]
// 根ノードを起点に、上から下へヒープ化
siftDown(nums, i, 0)
}
}
```
=== "Swift"
```swift title="heap_sort.swift"
[class]{}-[func]{siftDown}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
func siftDown(nums: inout [Int], n: Int, i: Int) {
var i = i
while true {
// ノード i, l, r のうち値が最大のノードを ma とする
let l = 2 * i + 1
let r = 2 * i + 2
var 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 つのノードを交換
nums.swapAt(i, ma)
// ループで上から下へヒープ化
i = ma
}
}
[class]{}-[func]{heapSort}
/* ヒープソート */
func heapSort(nums: inout [Int]) {
// ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {
siftDown(nums: &nums, n: nums.count, i: i)
}
// ヒープから最大要素を取り出し、n-1 回繰り返す
for i in nums.indices.dropFirst().reversed() {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
nums.swapAt(0, i)
// 根ノードを起点に、上から下へヒープ化
siftDown(nums: &nums, n: i, i: 0)
}
}
```
=== "JS"
```javascript title="heap_sort.js"
[class]{}-[func]{siftDown}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
function siftDown(nums, n, i) {
while (true) {
// ノード i, l, r のうち値が最大のノードを ma とする
let l = 2 * i + 1;
let r = 2 * i + 2;
let 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 つのノードを交換
[nums[i], nums[ma]] = [nums[ma], nums[i]];
// ループで上から下へヒープ化
i = ma;
}
}
[class]{}-[func]{heapSort}
/* ヒープソート */
function heapSort(nums) {
// ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// ヒープから最大要素を取り出し、n-1 回繰り返す
for (let i = nums.length - 1; i > 0; i--) {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
[nums[0], nums[i]] = [nums[i], nums[0]];
// 根ノードを起点に、上から下へヒープ化
siftDown(nums, i, 0);
}
}
```
=== "TS"
```typescript title="heap_sort.ts"
[class]{}-[func]{siftDown}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
function siftDown(nums: number[], n: number, i: number): void {
while (true) {
// ノード i, l, r のうち値が最大のノードを ma とする
let l = 2 * i + 1;
let r = 2 * i + 2;
let 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 つのノードを交換
[nums[i], nums[ma]] = [nums[ma], nums[i]];
// ループで上から下へヒープ化
i = ma;
}
}
[class]{}-[func]{heapSort}
/* ヒープソート */
function heapSort(nums: number[]): void {
// ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// ヒープから最大要素を取り出し、n-1 回繰り返す
for (let i = nums.length - 1; i > 0; i--) {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
[nums[0], nums[i]] = [nums[i], nums[0]];
// 根ノードを起点に、上から下へヒープ化
siftDown(nums, i, 0);
}
}
```
=== "Dart"
```dart title="heap_sort.dart"
[class]{}-[func]{siftDown}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
void siftDown(List<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;
}
}
[class]{}-[func]{heapSort}
/* ヒープソート */
void heapSort(List<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);
}
}
```
=== "Rust"
```rust title="heap_sort.rs"
[class]{}-[func]{sift_down}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
fn sift_down(nums: &mut [i32], n: usize, mut i: usize) {
loop {
// ノード i, l, r のうち値が最大のノードを ma とする
let l = 2 * i + 1;
let r = 2 * i + 2;
let mut 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 つのノードを交換
nums.swap(i, ma);
// ループで上から下へヒープ化
i = ma;
}
}
[class]{}-[func]{heap_sort}
/* ヒープソート */
fn heap_sort(nums: &mut [i32]) {
// ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
for i in (0..nums.len() / 2).rev() {
sift_down(nums, nums.len(), i);
}
// ヒープから最大要素を取り出し、n-1 回繰り返す
for i in (1..nums.len()).rev() {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
nums.swap(0, i);
// 根ノードを起点に、上から下へヒープ化
sift_down(nums, i, 0);
}
}
```
=== "C"
```c title="heap_sort.c"
[class]{}-[func]{siftDown}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
void siftDown(int nums[], int n, int i) {
while (1) {
// ノード 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;
}
}
[class]{}-[func]{heapSort}
/* ヒープソート */
void heapSort(int nums[], int n) {
// ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
for (int i = n / 2 - 1; i >= 0; --i) {
siftDown(nums, n, i);
}
// ヒープから最大要素を取り出し、n-1 回繰り返す
for (int i = n - 1; i > 0; --i) {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// 根ノードを起点に、上から下へヒープ化
siftDown(nums, i, 0);
}
}
```
=== "Kotlin"
```kotlin title="heap_sort.kt"
[class]{}-[func]{siftDown}
/* ヒープの長さは n。ノード i から下方向にヒープ化 */
fun siftDown(nums: IntArray, n: Int, li: Int) {
var i = li
while (true) {
// ノード i, l, r のうち値が最大のノードを ma とする
val l = 2 * i + 1
val r = 2 * i + 2
var 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 つのノードを交換
val temp = nums[i]
nums[i] = nums[ma]
nums[ma] = temp
// ループで上から下へヒープ化
i = ma
}
}
[class]{}-[func]{heapSort}
/* ヒープソート */
fun heapSort(nums: IntArray) {
// ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
for (i in nums.size / 2 - 1 downTo 0) {
siftDown(nums, nums.size, i)
}
// ヒープから最大要素を取り出し、n-1 回繰り返す
for (i in nums.size - 1 downTo 1) {
// 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
val temp = nums[0]
nums[0] = nums[i]
nums[i] = temp
// 根ノードを起点に、上から下へヒープ化
siftDown(nums, i, 0)
}
}
```
=== "Ruby"
```ruby title="heap_sort.rb"
[class]{}-[func]{sift_down}
### ヒープ長 n で、ノード i から上から下へヒープ化 ###
def sift_down(nums, n, i)
while true
# ノード i, l, r のうち値が最大のノードを ma とする
l = 2 * i + 1
r = 2 * i + 2
ma = i
ma = l if l < n && nums[l] > nums[ma]
ma = r if r < n && nums[r] > nums[ma]
# ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける
break if ma == i
# 2 つのノードを交換
nums[i], nums[ma] = nums[ma], nums[i]
# ループで上から下へヒープ化
i = ma
end
end
[class]{}-[func]{heap_sort}
### ヒープソート ###
def heap_sort(nums)
# ヒープ構築:葉ノード以外のすべてのノードをヒープ化する
(nums.length / 2 - 1).downto(0) do |i|
sift_down(nums, nums.length, i)
end
# ヒープから最大要素を取り出し、n-1 回繰り返す
(nums.length - 1).downto(1) do |i|
# 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)
nums[0], nums[i] = nums[i], nums[0]
# 根ノードを起点に、上から下へヒープ化
sift_down(nums, i, 0)
end
end
```
## 11.7.2 &nbsp; アルゴリズムの特徴
??? pythontutor "コードの可視化"
- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープの構築は $O(n)$ 時間を使用します。ヒープから最大要素を抽出するには $O(\log n)$ 時間がかかり、$n - 1$ ラウンドループします。
- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が $O(1)$ 空間を使用します。要素の交換とヒープ化操作は元の配列で実行されます。
- **非安定ソート**:ヒープの頂上と底部要素の交換中に、等しい要素の相対位置が変わる可能性があります。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20sift_down%28nums%3A%20list%5Bint%5D%2C%20n%3A%20int%2C%20i%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E3%83%92%E3%83%BC%E3%83%97%E3%81%AE%E9%95%B7%E3%81%95%E3%81%AF%20n%E3%80%82%E3%83%8E%E3%83%BC%E3%83%89%20i%20%E3%81%8B%E3%82%89%E4%B8%8B%E6%96%B9%E5%90%91%E3%81%AB%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%22%22%22%0A%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%20i%2C%20l%2C%20r%20%E3%81%AE%E3%81%86%E3%81%A1%E5%80%A4%E3%81%8C%E6%9C%80%E5%A4%A7%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%20ma%20%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20l%20%3D%202%20%2A%20i%20%2B%201%0A%20%20%20%20%20%20%20%20r%20%3D%202%20%2A%20i%20%2B%202%0A%20%20%20%20%20%20%20%20ma%20%3D%20i%0A%20%20%20%20%20%20%20%20if%20l%20%3C%20n%20and%20nums%5Bl%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20if%20r%20%3C%20n%20and%20nums%5Br%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%20i%20%E3%81%8C%E6%9C%80%E5%A4%A7%E3%80%81%E3%81%BE%E3%81%9F%E3%81%AF%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20l%2C%20r%20%E3%81%8C%E7%AF%84%E5%9B%B2%E5%A4%96%E3%81%AA%E3%82%89%E3%80%81%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%E3%81%AF%E4%B8%8D%E8%A6%81%E3%81%AA%E3%81%AE%E3%81%A7%E6%8A%9C%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%23%202%20%E3%81%A4%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bma%5D%20%3D%20nums%5Bma%5D%2C%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%97%E3%81%A7%E4%B8%8A%E3%81%8B%E3%82%89%E4%B8%8B%E3%81%B8%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%0A%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0Adef%20heap_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%92%E3%83%BC%E3%83%97%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E3%83%92%E3%83%BC%E3%83%97%E6%A7%8B%E7%AF%89%EF%BC%9A%E8%91%89%E3%83%8E%E3%83%BC%E3%83%89%E4%BB%A5%E5%A4%96%E3%81%AE%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20%2F%2F%202%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20len%28nums%29%2C%20i%29%0A%20%20%20%20%23%20%E3%83%92%E3%83%BC%E3%83%97%E3%81%8B%E3%82%89%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%97%E3%80%81n-1%20%E5%9B%9E%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%A0%B9%E3%83%8E%E3%83%BC%E3%83%89%E3%81%A8%E6%9C%80%E3%82%82%E5%8F%B3%E3%81%AE%E8%91%89%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E4%BA%A4%E6%8F%9B%EF%BC%88%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E6%9C%AB%E5%B0%BE%E8%A6%81%E7%B4%A0%E3%82%92%E4%BA%A4%E6%8F%9B%EF%BC%89%0A%20%20%20%20%20%20%20%20nums%5B0%5D%2C%20nums%5Bi%5D%20%3D%20nums%5Bi%5D%2C%20nums%5B0%5D%0A%20%20%20%20%20%20%20%20%23%20%E6%A0%B9%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E8%B5%B7%E7%82%B9%E3%81%AB%E3%80%81%E4%B8%8A%E3%81%8B%E3%82%89%E4%B8%8B%E3%81%B8%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20i%2C%200%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20heap_sort%28nums%29%0A%20%20%20%20print%28%22%E3%83%92%E3%83%BC%E3%83%97%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20sift_down%28nums%3A%20list%5Bint%5D%2C%20n%3A%20int%2C%20i%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E3%83%92%E3%83%BC%E3%83%97%E3%81%AE%E9%95%B7%E3%81%95%E3%81%AF%20n%E3%80%82%E3%83%8E%E3%83%BC%E3%83%89%20i%20%E3%81%8B%E3%82%89%E4%B8%8B%E6%96%B9%E5%90%91%E3%81%AB%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%22%22%22%0A%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%20i%2C%20l%2C%20r%20%E3%81%AE%E3%81%86%E3%81%A1%E5%80%A4%E3%81%8C%E6%9C%80%E5%A4%A7%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%20ma%20%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20l%20%3D%202%20%2A%20i%20%2B%201%0A%20%20%20%20%20%20%20%20r%20%3D%202%20%2A%20i%20%2B%202%0A%20%20%20%20%20%20%20%20ma%20%3D%20i%0A%20%20%20%20%20%20%20%20if%20l%20%3C%20n%20and%20nums%5Bl%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20if%20r%20%3C%20n%20and%20nums%5Br%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%23%20%E3%83%8E%E3%83%BC%E3%83%89%20i%20%E3%81%8C%E6%9C%80%E5%A4%A7%E3%80%81%E3%81%BE%E3%81%9F%E3%81%AF%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%20l%2C%20r%20%E3%81%8C%E7%AF%84%E5%9B%B2%E5%A4%96%E3%81%AA%E3%82%89%E3%80%81%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%E3%81%AF%E4%B8%8D%E8%A6%81%E3%81%AA%E3%81%AE%E3%81%A7%E6%8A%9C%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%23%202%20%E3%81%A4%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bma%5D%20%3D%20nums%5Bma%5D%2C%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%23%20%E3%83%AB%E3%83%BC%E3%83%97%E3%81%A7%E4%B8%8A%E3%81%8B%E3%82%89%E4%B8%8B%E3%81%B8%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%0A%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0Adef%20heap_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E3%83%92%E3%83%BC%E3%83%97%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E3%83%92%E3%83%BC%E3%83%97%E6%A7%8B%E7%AF%89%EF%BC%9A%E8%91%89%E3%83%8E%E3%83%BC%E3%83%89%E4%BB%A5%E5%A4%96%E3%81%AE%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20%2F%2F%202%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20len%28nums%29%2C%20i%29%0A%20%20%20%20%23%20%E3%83%92%E3%83%BC%E3%83%97%E3%81%8B%E3%82%89%E6%9C%80%E5%A4%A7%E8%A6%81%E7%B4%A0%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%97%E3%80%81n-1%20%E5%9B%9E%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%A0%B9%E3%83%8E%E3%83%BC%E3%83%89%E3%81%A8%E6%9C%80%E3%82%82%E5%8F%B3%E3%81%AE%E8%91%89%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E4%BA%A4%E6%8F%9B%EF%BC%88%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E6%9C%AB%E5%B0%BE%E8%A6%81%E7%B4%A0%E3%82%92%E4%BA%A4%E6%8F%9B%EF%BC%89%0A%20%20%20%20%20%20%20%20nums%5B0%5D%2C%20nums%5Bi%5D%20%3D%20nums%5Bi%5D%2C%20nums%5B0%5D%0A%20%20%20%20%20%20%20%20%23%20%E6%A0%B9%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E8%B5%B7%E7%82%B9%E3%81%AB%E3%80%81%E4%B8%8A%E3%81%8B%E3%82%89%E4%B8%8B%E3%81%B8%E3%83%92%E3%83%BC%E3%83%97%E5%8C%96%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20i%2C%200%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20heap_sort%28nums%29%0A%20%20%20%20print%28%22%E3%83%92%E3%83%BC%E3%83%97%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 11.7.2 &nbsp; アルゴリズムの特性
- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープ構築操作には $O(n)$ の時間がかかります。ヒープから最大要素を取り出す時間計算量は $O(\log n)$ であり、これを合計 $n - 1$ 回繰り返します。
- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が使う空間は $O(1)$ です。要素の交換とヒープ化操作はいずれも元の配列上で行われます。
- **非安定ソート**:ヒープ頂点の要素とヒープ末尾の要素を交換する際、等しい要素どうしの相対位置が変化する可能性があります。
+3 -3
View File
@@ -5,13 +5,13 @@ icon: material/sort-ascending
# 第 11 章 &nbsp; ソート
![Sorting](../assets/covers/chapter_sorting.jpg){ class="cover-image" }
![ソート](../assets/covers/chapter_sorting.jpg){ class="cover-image" }
!!! abstract
ソートは混沌を秩序変える魔法の鍵のようなもので、データをより効率的に理解し処理することを可能にします。
ソートは混沌を秩序へと変える魔法の鍵のようなものであり、データをより効率的に理解し処理することを可能にします。
単純な昇順であろうと複雑なカテゴリ配列であろうと、ソートはデータの調和美を明らかにします。
単純な昇順であれ、複雑な分類配列であ、ソートはデータの調和のとれた美しさを私たちに示してくれます。
## 章の内容
+181 -47
View File
@@ -4,45 +4,45 @@ comments: true
# 11.4 &nbsp; 挿入ソート
<u>挿入ソート</u>は、トランプのデッキを手動でソートするプロセスによく似た動作をするシンプルなソートアルゴリズムです。
<u>挿入ソートinsertion sort</u>は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。
具体的には、未ソート区間からベース要素を選択し、その左側のソート済み区間の要素と比較して、要素を正しい位置に挿入します。
具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。
下図は、要素が配列に挿入される方法を示しています。ベース要素を`base`とすると、ターゲットインデックスから`base`までのすべての要素を右に1つずつシフトし、その後`base`をターゲットインデックスに割り当てる必要があります。
以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を `base` とすると、目的のインデックスから `base` までのすべての要素を 1 つずつ右に移動し、その後 `base` を目的のインデックスに代入する必要があります。
![Single insertion operation](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }
![1 回の挿入操作](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }
<p align="center"> 図 11-6 &nbsp; Single insertion operation </p>
<p align="center"> 図 11-6 &nbsp; 1 回の挿入操作 </p>
## 11.4.1 &nbsp; アルゴリズムプロセス
## 11.4.1 &nbsp; アルゴリズムの流れ
挿入ソート全体的なプロセスは下図に示されます。
挿入ソート全体の流れを以下の図に示ます。
1. 配列の最初の要素をソート済みとみなします。
2. 2番目の要素を`base`として選択し、正しい位置に挿入して、**最初の2つの要素をソート済みにします**
3. 3番目の要素を`base`として選択し、正しい位置に挿入して、**最初の3つの要素をソート済みにします**
4. この方法で続行し、最後の反復では、最後の要素を`base`として取り、正しい位置に挿入した後、**すべての要素がソートされます**
1. 初期状態では、配列の 1 番目の要素はすでに整列済みです。
2. 配列の 2 番目の要素を `base` として選、正しい位置に挿入すると、**配列の先頭 2 要素が整列済み**になります
3. 3 番目の要素を `base` として選、正しい位置に挿入すると、**配列の先頭 3 要素が整列済み**になります
4. このように繰り返し、最後のラウンドで最後の要素を `base` として選んで正しい位置に挿入すると、**すべての要素が整列済み**になります
![Insertion sort process](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
![挿入ソートの流れ](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-7 &nbsp; Insertion sort process </p>
<p align="center"> 図 11-7 &nbsp; 挿入ソートの流れ </p>
コード例は以下のりです
コード例は以下のとおりです
=== "Python"
```python title="insertion_sort.py"
def insertion_sort(nums: list[int]):
"""挿入ソート"""
# 外側ループ:ソート済み範囲は [0, i-1]
# 外側ループ:整列済み区間は [0, i-1]
for i in range(1, len(nums)):
base = nums[i]
j = i - 1
# 内側ループbase をソート済み範囲 [0, i-1] の正しい位置に挿入
# 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
while j >= 0 and nums[j] > base:
nums[j + 1] = nums[j] # nums[j] を右に1つ移動
nums[j + 1] = nums[j] # nums[j] を 1 つ右へ移動する
j -= 1
nums[j + 1] = base # base を正しい位置に代入
nums[j + 1] = base # base を正しい位置に配置する
```
=== "C++"
@@ -50,15 +50,15 @@ comments: true
```cpp title="insertion_sort.cpp"
/* 挿入ソート */
void insertionSort(vector<int> &nums) {
// 外側ループ:ソート済み範囲は[0, i-1]
// 外側ループ:整列済み区間は [0, i-1]
for (int i = 1; i < nums.size(); i++) {
int base = nums[i], j = i - 1;
// 内側ループbaseをソート済み範囲[0, i-1]の正しい位置に挿入
// 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // nums[j]を一つ右移動
nums[j + 1] = nums[j]; // nums[j] を 1 つ右移動する
j--;
}
nums[j + 1] = base; // baseを正しい位置に代入
nums[j + 1] = base; // base を正しい位置に配置する
}
}
```
@@ -68,15 +68,15 @@ comments: true
```java title="insertion_sort.java"
/* 挿入ソート */
void insertionSort(int[] nums) {
// 外側ループ: ソート済み範囲は [0, i-1]
// 外側ループ:整列済み区間は [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// 内側ループ: base をソート済み範囲 [0, i-1] の正しい位置に挿入
// 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // nums[j] を右に1つ移動
nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する
j--;
}
nums[j + 1] = base; // base を正しい位置に代入
nums[j + 1] = base; // base を正しい位置に配置する
}
}
```
@@ -84,79 +84,213 @@ comments: true
=== "C#"
```csharp title="insertion_sort.cs"
[class]{insertion_sort}-[func]{InsertionSort}
/* 挿入ソート */
void InsertionSort(int[] nums) {
// 外側ループ:整列済み区間は [0, i-1]
for (int i = 1; i < nums.Length; i++) {
int bas = nums[i], j = i - 1;
// 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
while (j >= 0 && nums[j] > bas) {
nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する
j--;
}
nums[j + 1] = bas; // base を正しい位置に配置する
}
}
```
=== "Go"
```go title="insertion_sort.go"
[class]{}-[func]{insertionSort}
/* 挿入ソート */
func insertionSort(nums []int) {
// 外側ループ:整列済み区間は [0, i-1]
for i := 1; i < len(nums); i++ {
base := nums[i]
j := i - 1
// 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
for j >= 0 && nums[j] > base {
nums[j+1] = nums[j] // nums[j] を 1 つ右へ移動する
j--
}
nums[j+1] = base // base を正しい位置に配置する
}
}
```
=== "Swift"
```swift title="insertion_sort.swift"
[class]{}-[func]{insertionSort}
/* 挿入ソート */
func insertionSort(nums: inout [Int]) {
// 外側ループ:整列済み区間は [0, i-1]
for i in nums.indices.dropFirst() {
let base = nums[i]
var j = i - 1
// 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
while j >= 0, nums[j] > base {
nums[j + 1] = nums[j] // nums[j] を 1 つ右へ移動する
j -= 1
}
nums[j + 1] = base // base を正しい位置に配置する
}
}
```
=== "JS"
```javascript title="insertion_sort.js"
[class]{}-[func]{insertionSort}
/* 挿入ソート */
function insertionSort(nums) {
// 外側ループ:整列済み区間は [0, i-1]
for (let i = 1; i < nums.length; i++) {
let 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 を正しい位置に配置する
}
}
```
=== "TS"
```typescript title="insertion_sort.ts"
[class]{}-[func]{insertionSort}
/* 挿入ソート */
function insertionSort(nums: number[]): void {
// 外側ループ:整列済み区間は [0, i-1]
for (let i = 1; i < nums.length; i++) {
const base = nums[i];
let 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 を正しい位置に配置する
}
}
```
=== "Dart"
```dart title="insertion_sort.dart"
[class]{}-[func]{insertionSort}
/* 挿入ソート */
void insertionSort(List<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 を正しい位置に配置する
}
}
```
=== "Rust"
```rust title="insertion_sort.rs"
[class]{}-[func]{insertion_sort}
/* 挿入ソート */
fn insertion_sort(nums: &mut [i32]) {
// 外側ループ:整列済み区間は [0, i-1]
for i in 1..nums.len() {
let (base, mut j) = (nums[i], (i - 1) as i32);
// 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
while j >= 0 && nums[j as usize] > base {
nums[(j + 1) as usize] = nums[j as usize]; // nums[j] を 1 つ右へ移動する
j -= 1;
}
nums[(j + 1) as usize] = base; // base を正しい位置に配置する
}
}
```
=== "C"
```c title="insertion_sort.c"
[class]{}-[func]{insertionSort}
/* 挿入ソート */
void insertionSort(int nums[], int size) {
// 外側ループ:整列済み区間は [0, i-1]
for (int i = 1; i < size; i++) {
int base = nums[i], j = i - 1;
// 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する
while (j >= 0 && nums[j] > base) {
// nums[j] を 1 つ右へ移動する
nums[j + 1] = nums[j];
j--;
}
// base を正しい位置に配置する
nums[j + 1] = base;
}
}
```
=== "Kotlin"
```kotlin title="insertion_sort.kt"
[class]{}-[func]{insertionSort}
/* 挿入ソート */
fun insertionSort(nums: IntArray) {
// 外側ループ: ソート済み要素は 1, 2, ..., n
for (i in nums.indices) {
val base = nums[i]
var 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 を正しい位置に配置する
}
}
```
=== "Ruby"
```ruby title="insertion_sort.rb"
[class]{}-[func]{insertion_sort}
### 挿入ソート ###
def insertion_sort(nums)
n = nums.length
# 外側ループ:整列済み区間は [0, i-1]
for i in 1...n
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 -= 1
end
nums[j + 1] = base # base を正しい位置に配置する
end
end
```
## 11.4.2 &nbsp; アルゴリズムの特性
??? pythontutor "コードの可視化"
- **時間計算量は$O(n^2)$、適応ソート**:最悪の場合、各挿入操作には$n - 1$、$n-2$、...、$2$、$1$のループが必要で、合計は$(n - 1) n / 2$となり、時間計算量は$O(n^2)$です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量$O(n)$を実現します。
- **空間計算量は$O(1)$、インプレースソート**:ポインタ$i$と$j$は定数量の追加空間を使用します。
- **安定ソート**:挿入操作中、等しい要素の右側に要素を挿入し、順序を変更しません。
<div style="height: 495px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20insertion_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%8C%BF%E5%85%A5%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%95%B4%E5%88%97%E6%B8%88%E3%81%BF%E5%8C%BA%E9%96%93%E3%81%AF%20%5B0%2C%20i-1%5D%0A%20%20%20%20for%20i%20in%20range%281%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20base%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%83%AB%E3%83%BC%E3%83%97%3A%20base%20%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88%E6%B8%88%E3%81%BF%E5%8C%BA%E9%96%93%20%5B0%2C%20i-1%5D%20%E3%81%AE%E6%AD%A3%E3%81%97%E3%81%84%E4%BD%8D%E7%BD%AE%E3%81%AB%E6%8C%BF%E5%85%A5%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20while%20j%20%3E%3D%200%20and%20nums%5Bj%5D%20%3E%20base%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%5D%20%20%23%20nums%5Bj%5D%20%E3%82%92%201%20%E3%81%A4%E5%8F%B3%E3%81%B8%E7%A7%BB%E5%8B%95%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20base%20%20%23%20base%20%E3%82%92%E6%AD%A3%E3%81%97%E3%81%84%E4%BD%8D%E7%BD%AE%E3%81%AB%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20insertion_sort%28nums%29%0A%20%20%20%20print%28%22%E6%8C%BF%E5%85%A5%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20insertion_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%8C%BF%E5%85%A5%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%95%B4%E5%88%97%E6%B8%88%E3%81%BF%E5%8C%BA%E9%96%93%E3%81%AF%20%5B0%2C%20i-1%5D%0A%20%20%20%20for%20i%20in%20range%281%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20base%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%83%AB%E3%83%BC%E3%83%97%3A%20base%20%E3%82%92%E3%82%BD%E3%83%BC%E3%83%88%E6%B8%88%E3%81%BF%E5%8C%BA%E9%96%93%20%5B0%2C%20i-1%5D%20%E3%81%AE%E6%AD%A3%E3%81%97%E3%81%84%E4%BD%8D%E7%BD%AE%E3%81%AB%E6%8C%BF%E5%85%A5%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20while%20j%20%3E%3D%200%20and%20nums%5Bj%5D%20%3E%20base%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%5D%20%20%23%20nums%5Bj%5D%20%E3%82%92%201%20%E3%81%A4%E5%8F%B3%E3%81%B8%E7%A7%BB%E5%8B%95%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20base%20%20%23%20base%20%E3%82%92%E6%AD%A3%E3%81%97%E3%81%84%E4%BD%8D%E7%BD%AE%E3%81%AB%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20insertion_sort%28nums%29%0A%20%20%20%20print%28%22%E6%8C%BF%E5%85%A5%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 11.4.2 &nbsp; アルゴリズムの特徴
- **計算量は $O(n^2)$、適応的ソート**:最悪の場合、各挿入操作ではそれぞれ $n - 1$、$n-2$、$\dots$、$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^2)$ であり、これから学ぶクイックソートの時間計算量は $O(n \log n)$ です。挿入ソートの時間計算量のほうが大きいにもかかわらず、**データ量が小さい場合には、挿入ソートのほうが通常は高速**です
この結論は線形探索と二分探索の結論と似ています。時間計算量が$O(n \log n)$分割統治戦略に基づくクイックソートなどのアルゴリズムは、多くの場合より多くの単位操作を含みます。小さな入力サイズでは、$n^2$$n \log n$の数値は近く、計算量が支配的でなく、ラウンドあたりの単位操作数が決定的な役割を果たします。
この結論は線形探索と二分探索の適用条件に関する結論と似ています。クイックソートのような $O(n \log n)$ のアルゴリズムは分割統治に基づくソートアルゴリズムであり、一般により多くの基本演算を含みます。一方、データ量が小さい場合は、$n^2$$n \log n$ の値は比較的近く、計算量が支配的でなくなり、各ラウンドにおける基本演算の回数が決定的な役割を果たします。
実際、多くのプログラミング言語(Javaなど)は、組み込みソート関数で挿入ソートを使用しています。一般的なアプローチは:長い配列に対しては、クイックソートなどの分割統治戦略に基づくソートアルゴリズムを使用し、短い配列に対しては挿入ソートを直接使用します。
実際、多くのプログラミング言語(たとえば Java)の組み込みソート関数で挿入ソートが採用されており、その大まかな考え方は次のとおりです。長い配列にはクイックソートなどの分割統治に基づくソートアルゴリズムを使、短い配列には直接挿入ソートを使います。
バブルソート、選択ソート、挿入ソートはすべて時間計算量$O(n^2)$を持ちますが、実際には、**挿入ソートはバブルソートや選択ソートよりも一般的に使用されます**。主な理由は以下の通りです。
バブルソート、選択ソート、挿入ソートはいずれも時間計算量$O(n^2)$すが、実際には、**挿入ソートはバブルソートや選択ソートよりもはるかに高い頻度で使われます**。主な理由は次のとおりです。
- バブルソートは要素交換に基づき、一時変数の使用が必要で、3つの単位操作を含みます;挿入ソートは要素代入に基づき、1つの単位操作のみが必要です。したがって、**バブルソートの計算オーバーヘッドは一般的に挿入ソートより高くなります**。
- 選択ソートの時間計算量は常に$O(n^2)$です。**部分的に順序付けられたデータのセットが与えられた場合、挿入ソートは通常選択ソートより効率的です**
- 選択ソートは安定で、マルチレベルソートに適用できません。
- バブルソートは要素交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素代入に基づいており、必要な基本演算は 1 回だけです。したがって、**バブルソートの計算コストは通常、挿入ソートより高くなります**。
- 選択ソートの時間計算量はどのような場合でも $O(n^2)$ です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常選択ソートより効率的**です。
- 選択ソートは安定ではないため、多段ソートに適用できません。
+489 -79
View File
@@ -4,26 +4,26 @@ comments: true
# 11.6 &nbsp; マージソート
<u>マージソート</u>は分割統治戦略に基づくソートアルゴリズムで、下図に示す「分割」と「マージ」フェーズを含みます。
<u>マージソートmerge sort</u>は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。
1. **分割フェーズ**中点から配列を再帰的に分割し、長い配列のソート問題をより短い配列変換します。
2. **マージフェーズ**サブ配列の長さが1になったときに分割を停止し、その後マージを開始します。2つの短いソート済み配列を連続的により長いソート済み配列にマージし、プロセスが完了するまで続行します。
1. **分割段階**再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。
2. **マージ段階**部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。
![The divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" }
![マージソートの分割とマージの段階](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>
<p align="center"> 図 11-10 &nbsp; マージソートの分割とマージの段階 </p>
## 11.6.1 &nbsp; アルゴリズムワークフロー
## 11.6.1 &nbsp; アルゴリズムの流れ
図に示すように、「分割フェーズ」は中点から配列を上から下に2つのサブ配列に再帰的に分割します。
以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 2 つの部分配列へ分割します。
1. 中点`mid`を計算し、左サブ配列(区間`[left, mid]`)と右サブ配列(区間`[mid + 1, right]`)を再帰的に分割します。
2. サブ配列の長さが1になるまでステップ`1.`を再帰的に行し、その後停止します。
1. 配列の中点 `mid` を計算し、左部分配列(区間 `[left, mid]` )と右部分配列(区間 `[mid + 1, right]` )を再帰的に分割します。
2. 手順 `1.` を再帰的に行し、部分配列区間の長さが 1 になった時点で終了します。
「マージフェーズ」は左と右のサブ配列を下から上にソート済み配列に結合します。重要なのは、マージが長さ1のサブ配列から開始され、マージフェーズ中に各サブ配列がソートされることです。
「マージ段階」では左部分配列と右部分配列を下から上へとマージし、1 つのソート済み配列にします。長さ 1 の部分配列からマージを始めるため、この段階の各部分配列はすでに整列されています。
=== "<1>"
![Merge sort process](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
![マージソートの手順](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
=== "<2>"
![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" }
@@ -52,26 +52,26 @@ comments: true
=== "<10>"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" }
<p align="center"> 図 11-11 &nbsp; Merge sort process </p>
<p align="center"> 図 11-11 &nbsp; マージソートの手順 </p>
マージソートの再帰順序は二分木の後順横断と一致ることが観察できます。
観察すると、マージソートの再帰順序は二分木の後順走査と一致していることがわかります。
- **後順横断**:まず左のサブツリーを再帰的に横断し、次に右のサブツリーを横断し、最後にルートノードを処理します。
- **マージソート**:まず左のサブ配列を再帰的に処理し、次に右のサブ配列を処理し、最後にマージを実行します。
- **後順走査**:まず左部分木を再帰し、次に右部分木を再帰し、最後にノードを処理します。
- **マージソート**:まず左部分配列を再帰し、次に右部分配列を再帰し、最後にマージを処理します。
マージソートの実装以下のコードに示されます。`nums`マージされる区間は`[left, right]``tmp`の対応する区間は`[0, right - left]`であることに注意してください
マージソートの実装以下のコードに示ます。注意として、`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 を作成してマージ結果を格納
"""左部分配列と右部分配列をマージ"""
# 左部分配列区間は [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]
@@ -80,7 +80,7 @@ comments: true
tmp[k] = nums[j]
j += 1
k += 1
# 残った左右サブ配列の要素を一時配列にコピー
# 左右の部分配列の残り要素を一時配列にコピーする
while i <= mid:
tmp[k] = nums[i]
i += 1
@@ -89,7 +89,7 @@ comments: true
tmp[k] = nums[j]
j += 1
k += 1
# 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック
# 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする
for k in range(0, len(tmp)):
nums[left + k] = tmp[k]
@@ -97,40 +97,40 @@ comments: true
"""マージソート"""
# 終了条件
if left >= right:
return # サブ配列の長さが1のときに再帰を終了
# 分割段階
mid = left + (right - left) // 2 # 中点を計算
merge_sort(nums, left, mid) # 左サブ配列を再帰的に処理
merge_sort(nums, mid + 1, right) # 右サブ配列を再帰的に処理
# マージ段階
return # 部分配列の長さが 1 になったら再帰を終了
# 分割フェーズ
mid = (left + right) // 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を作成
// 左部分配列の区間は [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の対応する区間にコピー
// 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする
for (k = 0; k < tmp.size(); k++) {
nums[left + k] = tmp[k];
}
@@ -140,12 +140,12 @@ comments: true
void mergeSort(vector<int> &nums, int left, int right) {
// 終了条件
if (left >= right)
return; // サブ配列の長さが1の時、再帰を終了
// 分割段階
return; // 部分配列の長さが 1 になったら再帰を終了
// 分割フェーズ
int mid = left + (right - left) / 2; // 中点を計算
mergeSort(nums, left, mid); // 左サブ配列を再帰的に処理
mergeSort(nums, mid + 1, right); // 右サブ配列を再帰的に処理
// マージ段階
mergeSort(nums, left, mid); // 左部分配列を再帰処理
mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理
// マージフェーズ
merge(nums, left, mid, right);
}
```
@@ -155,26 +155,26 @@ comments: true
```java title="merge_sort.java"
/* 左部分配列と右部分配列をマージ */
void merge(int[] nums, int left, int mid, int right) {
// 左部分配列区間は [left, mid]、右部分配列区間は [mid+1, right]
// 一時配列 tmp を作成してマージ結果を格納
// 左部分配列区間は [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 の対応する区間にコピーバック
// 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
@@ -184,12 +184,12 @@ comments: true
void mergeSort(int[] nums, int left, int right) {
// 終了条件
if (left >= right)
return; // 部分配列の長さが 1 のとき再帰を終了
// 分割段階
return; // 部分配列の長さが 1 になったら再帰を終了
// 分割フェーズ
int mid = left + (right - left) / 2; // 中点を計算
mergeSort(nums, left, mid); // 左部分配列を再帰的に処理
mergeSort(nums, mid + 1, right); // 右部分配列を再帰的に処理
// マージ段階
mergeSort(nums, left, mid); // 左部分配列を再帰処理
mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理
// マージフェーズ
merge(nums, left, mid, right);
}
```
@@ -197,94 +197,504 @@ comments: true
=== "C#"
```csharp title="merge_sort.cs"
[class]{merge_sort}-[func]{Merge}
/* 左部分配列と右部分配列をマージ */
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];
}
}
[class]{merge_sort}-[func]{MergeSort}
/* マージソート */
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);
}
```
=== "Go"
```go title="merge_sort.go"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
func merge(nums []int, left, mid, right int) {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
tmp := make([]int, right-left+1)
// 左右の部分配列の開始インデックスを初期化する
i, j, k := left, mid+1, 0
// 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする
for i <= mid && j <= right {
if nums[i] <= nums[j] {
tmp[k] = nums[i]
i++
} else {
tmp[k] = nums[j]
j++
}
k++
}
// 左右の部分配列の残り要素を一時配列にコピーする
for i <= mid {
tmp[k] = nums[i]
i++
k++
}
for j <= right {
tmp[k] = nums[j]
j++
k++
}
// 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする
for k := 0; k < len(tmp); k++ {
nums[left+k] = tmp[k]
}
}
[class]{}-[func]{mergeSort}
/* マージソート */
func mergeSort(nums []int, left, right int) {
// 終了条件
if left >= right {
return
}
// 分割フェーズ
mid := left + (right - left) / 2
mergeSort(nums, left, mid)
mergeSort(nums, mid+1, right)
// マージフェーズ
merge(nums, left, mid, right)
}
```
=== "Swift"
```swift title="merge_sort.swift"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
func merge(nums: inout [Int], left: Int, mid: Int, right: Int) {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
var tmp = Array(repeating: 0, count: right - left + 1)
// 左右の部分配列の開始インデックスを初期化する
var i = left, j = mid + 1, k = 0
// 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする
while i <= mid, 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 tmp.indices {
nums[left + k] = tmp[k]
}
}
[class]{}-[func]{mergeSort}
/* マージソート */
func mergeSort(nums: inout [Int], left: Int, right: Int) {
// 終了条件
if left >= right { // 部分配列の長さが 1 になったら再帰を終了
return
}
// 分割フェーズ
let mid = left + (right - left) / 2 // 中点を計算
mergeSort(nums: &nums, left: left, right: mid) // 左部分配列を再帰処理
mergeSort(nums: &nums, left: mid + 1, right: right) // 右部分配列を再帰処理
// マージフェーズ
merge(nums: &nums, left: left, mid: mid, right: right)
}
```
=== "JS"
```javascript title="merge_sort.js"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
function merge(nums, left, mid, right) {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
const tmp = new Array(right - left + 1);
// 左右の部分配列の開始インデックスを初期化する
let 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];
}
}
[class]{}-[func]{mergeSort}
/* マージソート */
function mergeSort(nums, left, right) {
// 終了条件
if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了
// 分割フェーズ
let mid = Math.floor(left + (right - left) / 2); // 中点を計算
mergeSort(nums, left, mid); // 左部分配列を再帰処理
mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理
// マージフェーズ
merge(nums, left, mid, right);
}
```
=== "TS"
```typescript title="merge_sort.ts"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
function merge(nums: number[], left: number, mid: number, right: number): void {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
const tmp = new Array(right - left + 1);
// 左右の部分配列の開始インデックスを初期化する
let 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];
}
}
[class]{}-[func]{mergeSort}
/* マージソート */
function mergeSort(nums: number[], left: number, right: number): void {
// 終了条件
if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了
// 分割フェーズ
let mid = Math.floor(left + (right - left) / 2); // 中点を計算
mergeSort(nums, left, mid); // 左部分配列を再帰処理
mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理
// マージフェーズ
merge(nums, left, mid, right);
}
```
=== "Dart"
```dart title="merge_sort.dart"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
void merge(List<int> nums, int left, int mid, int right) {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
List<int> tmp = List.filled(right - left + 1, 0);
// 左右の部分配列の開始インデックスを初期化する
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];
}
}
[class]{}-[func]{mergeSort}
/* マージソート */
void mergeSort(List<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);
}
```
=== "Rust"
```rust title="merge_sort.rs"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
let tmp_size = right - left + 1;
let mut tmp = vec![0; tmp_size];
// 左右の部分配列の開始インデックスを初期化する
let (mut i, mut j, mut k) = (left, mid + 1, 0);
// 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする
while i <= mid && 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];
k += 1;
i += 1;
}
while j <= right {
tmp[k] = nums[j];
k += 1;
j += 1;
}
// 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする
for k in 0..tmp_size {
nums[left + k] = tmp[k];
}
}
[class]{}-[func]{merge_sort}
/* マージソート */
fn merge_sort(nums: &mut [i32], left: usize, right: usize) {
// 終了条件
if left >= right {
return; // 部分配列の長さが 1 になったら再帰を終了
}
// 分割フェーズ
let mid = left + (right - left) / 2; // 中点を計算
merge_sort(nums, left, mid); // 左部分配列を再帰処理
merge_sort(nums, mid + 1, right); // 右部分配列を再帰処理
// マージフェーズ
merge(nums, left, mid, right);
}
```
=== "C"
```c title="merge_sort.c"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
void merge(int *nums, int left, int mid, int right) {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
int tmpSize = right - left + 1;
int *tmp = (int *)malloc(tmpSize * sizeof(int));
// 左右の部分配列の開始インデックスを初期化する
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 < tmpSize; ++k) {
nums[left + k] = tmp[k];
}
// メモリを解放する
free(tmp);
}
[class]{}-[func]{mergeSort}
/* マージソート */
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);
}
```
=== "Kotlin"
```kotlin title="merge_sort.kt"
[class]{}-[func]{merge}
/* 左部分配列と右部分配列をマージ */
fun merge(nums: IntArray, left: Int, mid: Int, right: Int) {
// 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
// マージ結果を格納する一時配列 tmp を作成
val tmp = IntArray(right - left + 1)
// 左右の部分配列の開始インデックスを初期化する
var i = left
var j = mid + 1
var 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 (l in tmp.indices) {
nums[left + l] = tmp[l]
}
}
[class]{}-[func]{mergeSort}
/* マージソート */
fun mergeSort(nums: IntArray, left: Int, right: Int) {
// 終了条件
if (left >= right) return // 部分配列の長さが 1 になったら再帰を終了
// 分割フェーズ
val mid = left + (right - left) / 2 // 中点を計算
mergeSort(nums, left, mid) // 左部分配列を再帰処理
mergeSort(nums, mid + 1, right) // 右部分配列を再帰処理
// マージフェーズ
merge(nums, left, mid, right)
}
```
=== "Ruby"
```ruby title="merge_sort.rb"
[class]{}-[func]{merge}
### 左部分配列と右部分配列をマージ ###
def merge(nums, left, mid, right)
# 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]
# マージ結果を格納する一時配列 tmp を作成
tmp = Array.new(right - left + 1, 0)
# 左右の部分配列の開始インデックスを初期化する
i, j, k = left, mid + 1, 0
# 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする
while i <= mid && j <= right
if nums[i] <= nums[j]
tmp[k] = nums[i]
i += 1
else
tmp[k] = nums[j]
j += 1
end
k += 1
end
# 左右の部分配列の残り要素を一時配列にコピーする
while i <= mid
tmp[k] = nums[i]
i += 1
k += 1
end
while j <= right
tmp[k] = nums[j]
j += 1
k += 1
end
# 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする
(0...tmp.length).each do |k|
nums[left + k] = tmp[k]
end
end
[class]{}-[func]{merge_sort}
### マージソート ###
def merge_sort(nums, left, right)
# 終了条件
# 部分配列の長さが 1 になったら再帰を終了する
return if left >= right
# 分割フェーズ
mid = left + (right - left) / 2 # 中点を計算
merge_sort(nums, left, mid) # 左部分配列を再帰処理
merge_sort(nums, mid + 1, right) # 右部分配列を再帰処理
# マージフェーズ
merge(nums, left, mid, right)
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20merge%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20mid%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%B7%A6%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%A8%E5%8F%B3%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%82%92%E3%83%9E%E3%83%BC%E3%82%B8%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AF%20%5Bleft%2C%20mid%5D%E3%80%81%E5%8F%B3%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AF%20%5Bmid%2B1%2C%20right%5D%0A%20%20%20%20%23%20%E3%83%9E%E3%83%BC%E3%82%B8%E7%B5%90%E6%9E%9C%E3%82%92%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%20tmp%20%E3%82%92%E4%BD%9C%E6%88%90%0A%20%20%20%20tmp%20%3D%20%5B0%5D%20%2A%20%28right%20-%20left%20%2B%201%29%0A%20%20%20%20%23%20%E5%B7%A6%E5%8F%B3%E3%81%AE%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E9%96%8B%E5%A7%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20i%2C%20j%2C%20k%20%3D%20left%2C%20mid%20%2B%201%2C%200%0A%20%20%20%20%23%20%E5%B7%A6%E5%8F%B3%E3%81%AE%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AB%E3%81%BE%E3%81%A0%E8%A6%81%E7%B4%A0%E3%81%8C%E3%81%82%E3%82%8B%E9%96%93%E3%81%AF%E6%AF%94%E8%BC%83%E3%81%97%E3%80%81%E5%B0%8F%E3%81%95%E3%81%84%E3%81%BB%E3%81%86%E3%82%92%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B%0A%20%20%20%20while%20i%20%3C%3D%20mid%20and%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3C%3D%20nums%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E5%B7%A6%E5%8F%B3%E3%81%AE%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E6%AE%8B%E3%82%8A%E8%A6%81%E7%B4%A0%E3%82%92%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B%0A%20%20%20%20while%20i%20%3C%3D%20mid%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20while%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%20tmp%20%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E5%85%83%E3%81%AE%E9%85%8D%E5%88%97%20nums%20%E3%81%AE%E5%AF%BE%E5%BF%9C%E5%8C%BA%E9%96%93%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B%0A%20%20%20%20for%20k%20in%20range%280%2C%20len%28tmp%29%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bleft%20%2B%20k%5D%20%3D%20tmp%5Bk%5D%0A%0A%0Adef%20merge_sort%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E3%83%9E%E3%83%BC%E3%82%B8%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E7%B5%82%E4%BA%86%E6%9D%A1%E4%BB%B6%0A%20%20%20%20if%20left%20%3E%3D%20right%3A%0A%20%20%20%20%20%20%20%20return%20%20%23%20%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E9%95%B7%E3%81%95%E3%81%8C%201%20%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%89%E5%86%8D%E5%B8%B0%E3%82%92%E7%B5%82%E4%BA%86%0A%20%20%20%20%23%20%E5%88%86%E5%89%B2%E3%83%95%E3%82%A7%E3%83%BC%E3%82%BA%0A%20%20%20%20mid%20%3D%20%28left%20%2B%20right%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20merge_sort%28nums%2C%20left%2C%20mid%29%20%20%23%20%E5%B7%A6%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%82%92%E5%86%8D%E5%B8%B0%E5%87%A6%E7%90%86%0A%20%20%20%20merge_sort%28nums%2C%20mid%20%2B%201%2C%20right%29%20%20%23%20%E5%8F%B3%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%82%92%E5%86%8D%E5%B8%B0%E5%87%A6%E7%90%86%0A%20%20%20%20%23%20%E3%83%9E%E3%83%BC%E3%82%B8%E3%83%95%E3%82%A7%E3%83%BC%E3%82%BA%0A%20%20%20%20merge%28nums%2C%20left%2C%20mid%2C%20right%29%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B7%2C%203%2C%202%2C%206%2C%200%2C%201%2C%205%2C%204%5D%0A%20%20%20%20merge_sort%28nums%2C%200%2C%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E3%83%9E%E3%83%BC%E3%82%B8%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20merge%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20mid%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%B7%A6%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%A8%E5%8F%B3%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%82%92%E3%83%9E%E3%83%BC%E3%82%B8%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AF%20%5Bleft%2C%20mid%5D%E3%80%81%E5%8F%B3%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E5%8C%BA%E9%96%93%E3%81%AF%20%5Bmid%2B1%2C%20right%5D%0A%20%20%20%20%23%20%E3%83%9E%E3%83%BC%E3%82%B8%E7%B5%90%E6%9E%9C%E3%82%92%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%20tmp%20%E3%82%92%E4%BD%9C%E6%88%90%0A%20%20%20%20tmp%20%3D%20%5B0%5D%20%2A%20%28right%20-%20left%20%2B%201%29%0A%20%20%20%20%23%20%E5%B7%A6%E5%8F%B3%E3%81%AE%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E9%96%8B%E5%A7%8B%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%0A%20%20%20%20i%2C%20j%2C%20k%20%3D%20left%2C%20mid%20%2B%201%2C%200%0A%20%20%20%20%23%20%E5%B7%A6%E5%8F%B3%E3%81%AE%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AB%E3%81%BE%E3%81%A0%E8%A6%81%E7%B4%A0%E3%81%8C%E3%81%82%E3%82%8B%E9%96%93%E3%81%AF%E6%AF%94%E8%BC%83%E3%81%97%E3%80%81%E5%B0%8F%E3%81%95%E3%81%84%E3%81%BB%E3%81%86%E3%82%92%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B%0A%20%20%20%20while%20i%20%3C%3D%20mid%20and%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3C%3D%20nums%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E5%B7%A6%E5%8F%B3%E3%81%AE%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E6%AE%8B%E3%82%8A%E8%A6%81%E7%B4%A0%E3%82%92%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B%0A%20%20%20%20while%20i%20%3C%3D%20mid%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20while%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E4%B8%80%E6%99%82%E9%85%8D%E5%88%97%20tmp%20%E3%81%AE%E8%A6%81%E7%B4%A0%E3%82%92%E5%85%83%E3%81%AE%E9%85%8D%E5%88%97%20nums%20%E3%81%AE%E5%AF%BE%E5%BF%9C%E5%8C%BA%E9%96%93%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B%0A%20%20%20%20for%20k%20in%20range%280%2C%20len%28tmp%29%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bleft%20%2B%20k%5D%20%3D%20tmp%5Bk%5D%0A%0A%0Adef%20merge_sort%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E3%83%9E%E3%83%BC%E3%82%B8%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20%23%20%E7%B5%82%E4%BA%86%E6%9D%A1%E4%BB%B6%0A%20%20%20%20if%20left%20%3E%3D%20right%3A%0A%20%20%20%20%20%20%20%20return%20%20%23%20%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%81%AE%E9%95%B7%E3%81%95%E3%81%8C%201%20%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%89%E5%86%8D%E5%B8%B0%E3%82%92%E7%B5%82%E4%BA%86%0A%20%20%20%20%23%20%E5%88%86%E5%89%B2%E3%83%95%E3%82%A7%E3%83%BC%E3%82%BA%0A%20%20%20%20mid%20%3D%20%28left%20%2B%20right%29%20%2F%2F%202%20%20%23%20%E4%B8%AD%E7%82%B9%E3%82%92%E8%A8%88%E7%AE%97%0A%20%20%20%20merge_sort%28nums%2C%20left%2C%20mid%29%20%20%23%20%E5%B7%A6%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%82%92%E5%86%8D%E5%B8%B0%E5%87%A6%E7%90%86%0A%20%20%20%20merge_sort%28nums%2C%20mid%20%2B%201%2C%20right%29%20%20%23%20%E5%8F%B3%E9%83%A8%E5%88%86%E9%85%8D%E5%88%97%E3%82%92%E5%86%8D%E5%B8%B0%E5%87%A6%E7%90%86%0A%20%20%20%20%23%20%E3%83%9E%E3%83%BC%E3%82%B8%E3%83%95%E3%82%A7%E3%83%BC%E3%82%BA%0A%20%20%20%20merge%28nums%2C%20left%2C%20mid%2C%20right%29%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B7%2C%203%2C%202%2C%206%2C%200%2C%201%2C%205%2C%204%5D%0A%20%20%20%20merge_sort%28nums%2C%200%2C%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E3%83%9E%E3%83%BC%E3%82%B8%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
## 11.6.2 &nbsp; アルゴリズムの特性
- **$O(n \log n)$の時間計算量、非適応ソート**:分割によ高さ$\log n$の再帰ツリーが作成され、各層で合計$n$回の操作をマージし、全体的な時間計算量は$O(n \log n)$となります。
- **$O(n)$の空間計算量、非インプレースソート**:再帰深度は$\log n$で、$O(\log n)$のスタックフレーム空間を使用します。マージ操作は補助配列が必要で、追加の$O(n)$空間を使用します。
- **安定ソート**:マージプロセス中、等しい要素の順序は変更されません。
- **時間計算量は $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)$最適化できます**。
連結リストに対しては、マージソートは他のソートアルゴリズムと比べて顕著な利点があり**連結リストソート問題の空間計算量を $O(1)$ まで最適化できます**
- **分割フェーズ**:「再帰」の代わりに「反復」を使用して連結リスト分割作業を実行できるため、再帰で使用されるスタックフレーム空間を節約できます。
- **マージフェーズ**:連結リストでは、ノードの挿入と削除操作は参照(ポインタ)を変更することで実現できるため、マージフェーズ2つの短い順序付きリストを1つの長い順序付きリストに結合)中に追加のリストを作成する必要がありません。
- **分割段階**連結リストの分割は「再帰」の代わりに「反復」で実装できるため、再帰で使用るスタックフレーム領域を省けます。
- **マージ段階**:連結リストでは、ノードの追加や削除は参照(ポインタ)を変更するだけで実現できるため、マージ段階2 つの短いソート済み連結リストを 1 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。
実装の詳細は比較的複雑で、興味のある読者は関連資料を参照して学習してください。
具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+194 -35
View File
@@ -4,18 +4,18 @@ comments: true
# 11.2 &nbsp; 選択ソート
<u>選択ソート</u>は非常にシンプルな原理で動作します:各反復で未ソート区間から最小要素を選択し、ソート済みセクションの末尾に移動するループを使用します。
<u>選択ソートselection sort</u>の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小要素を選び、整列済み区間の末尾に配置します。
配列の長さを$n$とすると、選択ソートのステップは下図に示されます。
配列の長さを $n$ とすると、選択ソートの手順は次の図のようになります。
1. 最初に、すべての要素未ソートで、つまり未ソート(インデックス)区間は$[0, n-1]$です。
2. 区間$[0, n-1]$の最小要素を選択し、インデックス$0$の要素と交換します。この後、配列の最初の要素がソートされます。
3. 区間$[1, n-1]$の最小要素を選択し、インデックス$1$の要素と交換します。この後、配列の最初の2つの要素がソートされます。
4. この方法で続行します。$n - 1$ラウンドの選択と交換の後、最初の$n - 1$個の要素がソートされます。
5. 残りの唯一の要素は結果的に最大要素であり、ソートする必要がないため、配列ソートされます。
1. 初期状態では、すべての要素未ソートであり、未ソート(インデックス)区間は $[0, n-1]$ です。
2. 区間 $[0, n-1]$の最小要素を選、インデックス $0$ の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。
3. 区間 $[1, n-1]$の最小要素を選、インデックス $1$ の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。
4. これを繰り返します。$n - 1$の選択と交換を経ると、配列の先頭 $n - 1$ 要素が整列済みになります。
5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列ソートは完了します。
=== "<1>"
![Selection sort process](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" }
![選択ソートの手順](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" }
=== "<2>"
![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" }
@@ -47,9 +47,9 @@ comments: true
=== "<11>"
![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" }
<p align="center"> 図 11-2 &nbsp; Selection sort process </p>
<p align="center"> 図 11-2 &nbsp; 選択ソートの手順 </p>
コードでは、$k$を使用して未ソート区間内の最小要素を記録します
コードでは、$k$ を用いて未ソート区間内の最小要素を記録します
=== "Python"
@@ -57,14 +57,14 @@ comments: true
def selection_sort(nums: list[int]):
"""選択ソート"""
n = len(nums)
# 外側ループ:未ソート範囲は [i, n-1]
# 外側ループ:未整列区間は [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]
```
@@ -74,15 +74,15 @@ comments: true
/* 選択ソート */
void selectionSort(vector<int> &nums) {
int n = nums.size();
// 外側ループ:未ソート範囲は[i, n-1]
// 外側ループ:未整列区間は [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]);
}
}
@@ -94,15 +94,15 @@ comments: true
/* 選択ソート */
void selectionSort(int[] nums) {
int n = nums.length;
// 外側ループ: 未ソート範囲は [i, n-1]
// 外側ループ:未整列区間は [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;
@@ -113,69 +113,228 @@ comments: true
=== "C#"
```csharp title="selection_sort.cs"
[class]{selection_sort}-[func]{SelectionSort}
/* 選択ソート */
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; // 最小要素のインデックスを記録
}
// その最小要素を未整列区間の先頭要素と交換する
(nums[k], nums[i]) = (nums[i], nums[k]);
}
}
```
=== "Go"
```go title="selection_sort.go"
[class]{}-[func]{selectionSort}
/* 選択ソート */
func selectionSort(nums []int) {
n := len(nums)
// 外側ループ:未整列区間は [i, n-1]
for i := 0; i < n-1; i++ {
// 内側のループ:未ソート区間の最小要素を見つける
k := i
for j := i + 1; j < n; j++ {
if nums[j] < nums[k] {
// 最小要素のインデックスを記録
k = j
}
}
// その最小要素を未整列区間の先頭要素と交換する
nums[i], nums[k] = nums[k], nums[i]
}
}
```
=== "Swift"
```swift title="selection_sort.swift"
[class]{}-[func]{selectionSort}
/* 選択ソート */
func selectionSort(nums: inout [Int]) {
// 外側ループ:未整列区間は [i, n-1]
for i in nums.indices.dropLast() {
// 内側のループ:未ソート区間の最小要素を見つける
var k = i
for j in nums.indices.dropFirst(i + 1) {
if nums[j] < nums[k] {
k = j // 最小要素のインデックスを記録
}
}
// その最小要素を未整列区間の先頭要素と交換する
nums.swapAt(i, k)
}
}
```
=== "JS"
```javascript title="selection_sort.js"
[class]{}-[func]{selectionSort}
/* 選択ソート */
function selectionSort(nums) {
let n = nums.length;
// 外側ループ:未整列区間は [i, n-1]
for (let i = 0; i < n - 1; i++) {
// 内側のループ:未ソート区間の最小要素を見つける
let k = i;
for (let j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) {
k = j; // 最小要素のインデックスを記録
}
}
// その最小要素を未整列区間の先頭要素と交換する
[nums[i], nums[k]] = [nums[k], nums[i]];
}
}
```
=== "TS"
```typescript title="selection_sort.ts"
[class]{}-[func]{selectionSort}
/* 選択ソート */
function selectionSort(nums: number[]): void {
let n = nums.length;
// 外側ループ:未整列区間は [i, n-1]
for (let i = 0; i < n - 1; i++) {
// 内側のループ:未ソート区間の最小要素を見つける
let k = i;
for (let j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) {
k = j; // 最小要素のインデックスを記録
}
}
// その最小要素を未整列区間の先頭要素と交換する
[nums[i], nums[k]] = [nums[k], nums[i]];
}
}
```
=== "Dart"
```dart title="selection_sort.dart"
[class]{}-[func]{selectionSort}
/* 選択ソート */
void selectionSort(List<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;
}
}
```
=== "Rust"
```rust title="selection_sort.rs"
[class]{}-[func]{selection_sort}
/* 選択ソート */
fn selection_sort(nums: &mut [i32]) {
if nums.is_empty() {
return;
}
let n = nums.len();
// 外側ループ:未整列区間は [i, n-1]
for i in 0..n - 1 {
// 内側のループ:未ソート区間の最小要素を見つける
let mut k = i;
for j in i + 1..n {
if nums[j] < nums[k] {
k = j; // 最小要素のインデックスを記録
}
}
// その最小要素を未整列区間の先頭要素と交換する
nums.swap(i, k);
}
}
```
=== "C"
```c title="selection_sort.c"
[class]{}-[func]{selectionSort}
/* 選択ソート */
void selectionSort(int nums[], int n) {
// 外側ループ:未整列区間は [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;
}
}
```
=== "Kotlin"
```kotlin title="selection_sort.kt"
[class]{}-[func]{selectionSort}
/* 選択ソート */
fun selectionSort(nums: IntArray) {
val n = nums.size
// 外側ループ:未整列区間は [i, n-1]
for (i in 0..<n - 1) {
var k = i
// 内側のループ:未ソート区間の最小要素を見つける
for (j in i + 1..<n) {
if (nums[j] < nums[k])
k = j // 最小要素のインデックスを記録
}
// その最小要素を未整列区間の先頭要素と交換する
val temp = nums[i]
nums[i] = nums[k]
nums[k] = temp
}
}
```
=== "Ruby"
```ruby title="selection_sort.rb"
[class]{}-[func]{selection_sort}
### 選択ソート ###
def selection_sort(nums)
n = nums.length
# 外側ループ:未整列区間は [i, n-1]
for i in 0...(n - 1)
# 内側のループ:未ソート区間の最小要素を見つける
k = i
for j in (i + 1)...n
if nums[j] < nums[k]
k = j # 最小要素のインデックスを記録
end
end
# その最小要素を未整列区間の先頭要素と交換する
nums[i], nums[k] = nums[k], nums[i]
end
end
```
## 11.2.1 &nbsp; アルゴリズムの特性
??? pythontutor "コードの可視化"
- **$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]`は等しい要素の右側に交換される可能性があり、相対順序が変わる原因となります。
<div style="height: 513px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20selection_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E9%81%B8%E6%8A%9E%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E6%95%B4%E5%88%97%E5%8C%BA%E9%96%93%E3%81%AF%20%5Bi%2C%20n-1%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%E3%81%AE%E6%9C%80%E5%B0%8F%E8%A6%81%E7%B4%A0%E3%82%92%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20k%20%3D%20i%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201%2C%20n%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3C%20nums%5Bk%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20k%20%3D%20j%20%20%23%20%E6%9C%80%E5%B0%8F%E8%A6%81%E7%B4%A0%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%A8%98%E9%8C%B2%0A%20%20%20%20%20%20%20%20%23%20%E3%81%9D%E3%81%AE%E6%9C%80%E5%B0%8F%E8%A6%81%E7%B4%A0%E3%82%92%E6%9C%AA%E6%95%B4%E5%88%97%E5%8C%BA%E9%96%93%E3%81%AE%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E4%BA%A4%E6%8F%9B%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bk%5D%20%3D%20nums%5Bk%5D%2C%20nums%5Bi%5D%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20selection_sort%28nums%29%0A%20%20%20%20print%28%22%E9%81%B8%E6%8A%9E%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20selection_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E9%81%B8%E6%8A%9E%E3%82%BD%E3%83%BC%E3%83%88%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%81%B4%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E6%95%B4%E5%88%97%E5%8C%BA%E9%96%93%E3%81%AF%20%5Bi%2C%20n-1%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%81%B4%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%97%EF%BC%9A%E6%9C%AA%E3%82%BD%E3%83%BC%E3%83%88%E5%8C%BA%E9%96%93%E3%81%AE%E6%9C%80%E5%B0%8F%E8%A6%81%E7%B4%A0%E3%82%92%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20k%20%3D%20i%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201%2C%20n%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3C%20nums%5Bk%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20k%20%3D%20j%20%20%23%20%E6%9C%80%E5%B0%8F%E8%A6%81%E7%B4%A0%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%A8%98%E9%8C%B2%0A%20%20%20%20%20%20%20%20%23%20%E3%81%9D%E3%81%AE%E6%9C%80%E5%B0%8F%E8%A6%81%E7%B4%A0%E3%82%92%E6%9C%AA%E6%95%B4%E5%88%97%E5%8C%BA%E9%96%93%E3%81%AE%E5%85%88%E9%A0%AD%E8%A6%81%E7%B4%A0%E3%81%A8%E4%BA%A4%E6%8F%9B%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bk%5D%20%3D%20nums%5Bk%5D%2C%20nums%5Bi%5D%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20selection_sort%28nums%29%0A%20%20%20%20print%28%22%E9%81%B8%E6%8A%9E%E3%82%BD%E3%83%BC%E3%83%88%E5%AE%8C%E4%BA%86%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
![Selection sort instability example](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" }
## 11.2.1 &nbsp; アルゴリズムの特徴
<p align="center"> 図 11-3 &nbsp; Selection sort instability example </p>
- **時間計算量は $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.assets/selection_sort_instability.png){ class="animation-figure" }
<p align="center"> 図 11-3 &nbsp; 選択ソートの不安定な例 </p>
+18 -18
View File
@@ -4,36 +4,36 @@ comments: true
# 11.1 &nbsp; ソートアルゴリズム
<u>ソートアルゴリズム</u>は、データセットを特定の順序で配列するために使用されます。ソートアルゴリズムは、順序付けられたデータは通常、より効率的に索、分析、処理できるため、幅広い応用があります。
<u>ソートアルゴリズムsorting algorithm</u>は、データの集合を特定の順序に従って並べ替えるために用いられます。ソートアルゴリズムは幅広く応用されており、整列済みデータは通常、より効率的に索、分析、処理できるためす。
下図に示すように、ソートアルゴリズムデータ型は整数、浮動小数点数、文字、文字列などです。ソート基準は、数値サイズ、文字ASCII順、またはカスタム基準など、要に応じて設定できます。
下図に示すように、ソートアルゴリズムにおけるデータ型は整数、浮動小数点数、文字、文字列などです。ソートの判定規則は、数値の大小、文字ASCII コード順、またはカスタムルールなど、要に応じて設定できます。
![Data types and comparator examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" }
![データ型と判定規則の例](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" }
<p align="center"> 図 11-1 &nbsp; Data types and comparator examples </p>
<p align="center"> 図 11-1 &nbsp; データ型と判定規則の例 </p>
## 11.1.1 &nbsp; 評価次元
## 11.1.1 &nbsp; 評価
**実行効率**:ソートアルゴリズムの時間計算量できるだけ低いことを期待し、全体的な操作数も少ないこと(時間計算量の定数項を下げる)を望みます。大量データでは、実行効率が特に重要です。
**実行効率**:ソートアルゴリズムの時間計算量できるだけ低く、かつ全体の操作数も少ないこと(時間計算量における定数項が小さいこと)が望まれます。大量データの場合、実行効率はとりわけ重要です。
**インプレース性**名前が示すとおり、<u>インプレースソート</u>は元の配列を直接操作することで実現され、追加のヘルパー配列が不要であるため、メモリを節約ます。一般的に、インプレースソートはデータ移動操作が少なく、高速です。
**インプレース性**その名のとおり、<u>インプレースソート</u>は元の配列を直接操作して並べ替えを行うため、追加の補助配列を必要とせず、メモリを節約できます。通常、インプレースソートはデータ移動操作が少なく、実行速度もより高速です。
**安定性**<u>安定ソート</u>は、ソート後に配列内の等しい要素の相対順序が変わらないことを保証します
**安定性**<u>安定ソート</u>は、並べ替え完了後も、等しい要素の配列内での相対順序が変化しません
安定ソートは、マルチキーソートシナリオにおいて必要条件です。学生情報を格納するテーブルがあり、第1列と第2列がそれぞれ名と年齢であるとします。この場合、<u>不安定ソート</u>入力データの順序を失う可能性があります
安定ソートは多段ソートの場面で必要条件となります。学生情報を保存した表があり、第 1 列と第 2 列がそれぞれ名と年齢であると仮定します。この場合、<u>不安定ソート</u>によって入力データの順序性が失われる可能性があります
```shell
# 入力データは名前でソート済み
# (名前, 年齢)
# 入力データは氏名順にソートされている
# (name, age)
('A', 19)
('B', 18)
('C', 21)
('D', 19)
('E', 23)
# 不安定ソートアルゴリズムを使用してリストを年齢でソートすると仮定すると、
# 結果('D', 19)('A', 19)の相対位置を変更し
# 入力データが名前でソート済みであるという性質が失われる
# 不安定ソートアルゴリズムで年齢順にリストを並べ替えると仮定すると、
# 結果では ('D', 19)('A', 19) の相対位置が変わり
# 入力データが氏名順である性質が失われる
('B', 18)
('D', 19)
('A', 19)
@@ -41,12 +41,12 @@ comments: true
('E', 23)
```
**適応性**<u>適応ソート</u>は入力データ内の既存の順序情報を用して計算負荷を削減し、より最適な時間効率を実現ます。適応ソートアルゴリズムの最良ケース時間計算量は、通常平均ケース時間計算量より優れています。
**適応性**<u>適応ソート</u>は入力データに既に存在する順序情報を用して計算量を減らし、より優れた時間効率を実現できます。適応ソートアルゴリズムの最良時間計算量は、通常平均時間計算量より優れています。
**比較ベースまたは非比較ベース**<u>比較ベースソート</u>は比較演算子($<$、$=$、$>$)に依存して要素の相対順序を定し、配列全体をソートします。理論的最適時間計算量は$O(n \log n)$です。一方、<u>非比較ソート</u>は比較演算子を使用せず、$O(n)$の時間計算量を実現できますが、汎用性は比較的劣ります。
**比較ベースかどうか**<u>比較ベースソート</u>は比較演算子($<$、$=$、$>$)に依存して要素の相対順序を定し、それによって配列全体をソートします。理論上の最良時間計算量は $O(n \log n)$ です。一方、<u>非比較ソート</u>は比較演算子を使用せず、時間計算量は $O(n)$ に達しますが、汎用性は相対的に低くなります。
## 11.1.2 &nbsp; 理想的なソートアルゴリズム
**高速実行、インプレース、安定、適応汎用**。明らかに、これらのすべての特徴を組み合わせたソートアルゴリズムは今日まで見つかっていません。したがって、ソートアルゴリズムを選択する際は、データの特定の特徴と問題の要件に基づいて決定する必要があります。
**高速、インプレース、安定、適応的、高い汎用**。明らかに、これまでのところ、以上のすべての特性を兼ね備えたソートアルゴリズムはまだ見つかっていません。そのため、ソートアルゴリズムを選択する際は、具体的なデータの特徴と問題の要件に応じて判断する必要があります。
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価次元に基づいてそれぞれの利点と欠点を分析します。
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価に基づいて各ソートアルゴリズムの長所と短所を分析していきます。
+25 -25
View File
@@ -4,17 +4,17 @@ comments: true
# 11.11 &nbsp; まとめ
### 1. &nbsp; 重要な復習
### 1. &nbsp; 重要なポイントの振り返り
- バブルソートは隣接する要素を交換することで動作します。フラグを追加して早期リターンを可能にすることで、バブルソートの最良ケースの時間計算量を $O(n)$ に最適化できます。
- 挿入ソートは、未ソート区間から要素を取り出してソート済み区間の正しい位置に挿入することで各ラウンドをソートします。挿入ソートの時間計算量は $O(n^2)$ ですが、単位あたりの操作が比較的少ないため、少量のデータのソートで非常に人気があります。
- クイックソートは歩哨分割操作に基づいています。歩哨分割では、常に最悪のピボットを選ぶ可能性があり、時間計算量 $O(n^2)$ 劣化する可能性があります。中央値やランダムピボットを導入することで、そのような劣化の確率を減らすことができます。末尾再帰は再帰の深さを効果的に減らし、空間計算量を $O(\log n)$ に最適化ます。
- マージソートは分割とマージの2つの段階があり、通常分割統治戦略を体現しています。マージソートでは配列のソートには補助配列の作成が必要で、空間計算量は $O(n)$ になります。しかし、リストのソートの空間計算量は $O(1)$ 最適化できます。
- バケットソートは3つの手順から構成されます:データバケットに分散、各バケット内でのソート、バケット順での結果のマージ。これも分割統治戦略を体現し、非常に大きなデータセットに適しています。バケットソートの鍵はデータの均等分散です。
- 計数ソートはバケットソートの変形で、各データポイントの出現回数をカウントすることでソートします。計数ソートは限られた範囲のデータを持つ大きなデータセットに適しており、データを正の整数に変換する必要があります。
- 基数ソートは桁ごとにソートすることでデータを処理し、データが固定の数値として表現される必要があります。
- 全体的に、私たちは高効率、安定、インプレース操作、適応性を持つソートアルゴリズムを求めています。しかし、のデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たソートアルゴリズムは存在しません。実際の応用では、データの特性に基づいて適切なソートアルゴリズムを選択する必要があります。
- 以下の図は、効率、安定性、インプレース性、適応性の観点から主流のソートアルゴリズムを比較しています。
- バブルソートは隣接する要素を交換することで整列を行います。フラグを追加して早期リターンを可能にする、バブルソートの最良時間計算量を $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" }
@@ -22,32 +22,32 @@ comments: true
### 2. &nbsp; Q & A
**Q**: ソートアルゴリズムの安定性はいつ必要ですか?
**Q**ソートアルゴリズムの安定性は、どのような場合に必須ですか?
には、オブジェクトの一つの属性に基づいてソートする場合があります。えば、学生は名前と身長の属性を持ち、多段階ソートを実装することを目指します:最初に名前で `(A, 180) (B, 185) (C, 170) (D, 170)`取得し、次に身長で。ソートアルゴリズムが不安定なため、`(D, 170) (C, 170) (A, 180) (B, 185)` になってしまう可能性があります。
実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階ソートを行いたいとします。まず氏名で整列して `(A, 180) (B, 185) (C, 170) (D, 170)`得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は `(D, 170) (C, 170) (A, 180) (B, 185)` にな可能性があります。
学生DとCの位置が交換され、名前の順序性が破られているのが分かります。これは望ましくありません。
このように、学生 D と C の位置が入れ替わり、氏名に関する順序性が壊れてしまいます。これは望ましくありません。
**Q**: 歩哨分割での「右から左への検索」と「左から右への検索」の順序を交換できますか?
**Q**:番兵分割において、「右から左へ探索する」順序と「左から右へ探索する」順序は入れ替えられますか?
いいえ、最左要素をピボットとして使用する場合、最初に「右から左への検索」を行い、に「左から右への検索」を行う必要があります。この結論はやや直に反するので、理由を分析してみましょう。
できません。最も左端の要素を基準値とする場合は、必ず先に「右から左へ探索する」を行い、その後に「左から右へ探索する」を行う必要があります。この結論はやや直に反するので、理由を分析してみましょう。
歩哨分割 `partition()` の最後のステップは `nums[left]``nums[i]` を交換することです。交換後、ピボットの左側の要素はすべてピボット以下になります。**これには最後の交換前に `nums[left] >= nums[i]` が成り立つ必要があります**。「左から右への検索」を最初に行い、ピボットより大きい要素が見つからない場合、**`i == j` でループを終了し、`nums[j] == nums[i] > nums[left]` となる可能性があります**。つまり、最終交換操作はピボットより大きい要素配列の左端に交換し、歩哨分割失敗させます。
番兵分割 `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]` なり、これは正しくありません
たとえば、配列 `[0, 0, 0, 0, 1]` が与えられたとき、先に「左から右へ探索する」を行うと、番兵分割後の配列は `[1, 0, 0, 0, 0]` なります。これは誤った結果です
さらに考えると、`nums[right]`ピボットとして選択する場合、まったく逆で、最初に「左から右への検索」を行う必要があります。
さらに考えると、`nums[right]`基準値に選ぶ場合はちょうど逆になり、必ず先に「左から右へ探索する」を行う必要があります。
**Q**: 末尾再帰最適化について、短い配列を選択することで再帰の深さが $\log n$ を超えないことを保証するのはなぜですか?
**Q**:クイックソートの再帰深度最適化について、短い配列を選ぶとなぜ再帰深度が $\log n$ を超えないと保証できるのですか?
再帰の深さは現在リターンしていない再帰メソッドの数です。歩哨分割の各ラウンドは元の配列を2つの副配列に分割します。末尾再帰最適化により、再帰的に続行する副配列の長さは最大でも元の配列長の半分です。最悪の場合常に長さを半分にすると仮定すると、最終的な再帰の深さは $\log n$ になります。
再帰深度とは、現在まだ戻っていない再帰呼び出しの数のことです。各ラウンドの番兵分割では、元の配列を 2 つの部分配列に分ます。再帰深度の最適化後は、下方向に再帰する部分配列の長さは最大でも元の配列長の半分です。最悪の場合でも毎回半分の長さになると仮定すれば、最終的な再帰深度は $\log n$ になります。
元のクイックソートを見直すと、より大きな配列を継続的に再帰処理する可能性があり、最悪の場合 $n$、$n - 1$、...、$2$、$1$ で、再帰の深さは $n$ になります。末尾再帰最適化はこのシナリオを回避できます。
元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合 $n$、$n - 1$、$\dots$、$2$、$1$ と続き、再帰深度は $n$ になります。再帰深度の最適化により、このような状況を避けられます。
**Q**: 配列のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ すか?この劣化ケースをどう処理すべきですか?
**Q**配列のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ になりますか?このような退化はどう処理すべきですか?
はい。この状況については、歩哨分割を使用して配列をピボットより小さい、等しい、大きいの3つの部分に分割することを検討してください。小さい部分と大きい部分のみを再帰的に進めます。この方法では、すべての入力要素が等しい配列を1ラウンドの歩哨分割だけでソートできます。
はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。
**Q**: なぜバケットソートの最悪ケース時間計算量 $O(n^2)$ ですか?
**Q**バケットソートの最悪時間計算量 $O(n^2)$ なのはなぜですか?
最悪の場合、すべての要素が同じバケットに配置されます。これらの要素をソートするために $O(n^2)$ アルゴリズムを使用する場合、時間計算量は $O(n^2)$ になります。
最悪の場合、すべての要素が同じバケットに振り分けられます。その要素群を整列するのに $O(n^2)$ アルゴリズムを使えば、時間計算量は $O(n^2)$ になります。
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -9,9 +9,9 @@ icon: material/stack-overflow
!!! abstract
スタックは積み重ねられた猫のようなもので、キューは列に並んだ猫のようなものです。
それらはそれぞれ、後入先出LIFOと先入先出FIFOの論理関係を表しています。
スタックは猫を積み重ねようなもので、キューは猫が列に並ようなものです。
両者はそれぞれ、後入先出と先入先出の論理関係を表します。
## 章の内容
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+17 -17
View File
@@ -4,32 +4,32 @@ comments: true
# 5.4 &nbsp; まとめ
### 1. &nbsp; 重要なポイント
### 1. &nbsp; 要点の振り返り
- スタックは後入れ先出しLIFOの原則に従うデータ構造で、配列または連結リストを使って実装できます。
- 時間効率の観点では、スタックの配列実装の方が平均的な効率が高いです。ただし、拡張時には単一のプッシュ操作の時間計算量が$O(n)$に悪化する可能性があります。対照的に、スタックの連結リスト実装はより安定した効率を提供します。
- 空間効率に関しては、スタックの配列実装は一定程度の空間の無駄につながる可能性があります。ただし、連結リストのノードが占有するメモリ空間は一般的に配列要素よりも大きいことに注意することが重要です。
- キューは先入れ先出しFIFOの原則に従うデータ構造で、同様に配列または連結リストを使って実装できます。キューの時間と空間効率に関する結論は、スタックと似ています。
- 両端キュー(deque)はより柔軟なキューの種類で、両端で要素の追加と削除を可能にします。
- スタックは後入れ先出しの原則に従うデータ構造であり、配列または連結リスト実装できます。
- 時間効率のでは、スタックの配列実装は平均効率が高い一方、拡張時には 1 回のプッシュ操作の時間計算量が $O(n)$ まで悪化します。これに対して、スタックの連結リスト実装はより安定した効率をします。
- 空間効率の面では、スタックの配列実装はある程度の領域の無駄を生む可能性があります。ただし、連結リストのノードが占有するメモリ配列要素よりも大きいに注意が必要です。
- キューは先入れ先出しの原則に従うデータ構造であり、同様に配列または連結リスト実装できます。時間効率と空間効率の比較における結論は、前述のスタックの場合と似ています。
- 両端キューはより高い自由度を持つキューであり、両端で要素の追加と削除を行えます。
### 2. &nbsp; Q & A
**Q**: ブラウザの進む・戻る機能は双方向連結リストで実装されているのですか?
**Q**ブラウザの進む・戻るは双方向連結リストで実装されているのですか?
ブラウザの進む・戻るナビゲーションは本質的に「スタック」概念の現れです。ユーザーが新しいページを訪問すると、そのページスタックの先頭に追加されます。戻るボタンをクリックすると、ページスタックの先頭からポップされます。両端キュー(deque)は、「両端キュー」の章で述べたように、いくつかの追加操作を便利に実装できます。
ブラウザの進む・戻る機能の本質は「スタック」の表れです。ユーザーが新しいページにアクセスすると、そのページスタックの先頭に追加されます。ユーザーが戻るボタンをクリックすると、そのページスタックの先頭から取り出されます。両端キューを使うといくつかの追加操作を簡単に実装でき、この点は「両端キュー」の章で触れています。
**Q**: スタックからポップした後、ポップされたノードのメモリを解放する必要ありますか?
**Q**ポップした後、そのノードのメモリを解放する必要ありますか?
ポップされたノードが後で使用される場合は、そのメモリを解放する必要はありません。自動ガベージコレクションを持つJavaやPythonなどの言語では、手動メモリ解放必要ありません。CやC++では手動メモリ解放が必要です。
後で取り出したノードを引き続き使うのであれば、メモリを解放する必要はありません。以降そのノードを使わない場合でも、`Java``Python` などの言語には自動ガベージコレクション機構があるため、手動メモリ解放する必要ありません。一方、`C``C++` では手動メモリ解放する必要があります。
**Q**: 両端キューは2つのスタックを結合したもののように見えます。その用途は何ですか?
**Q**両端キューは 2 つのスタックをつなげたように見えますが、用途は何ですか?
両端キューは、スタックとキューの組み合わせまたは2つのスタックを結合したもので、スタックキューの両方のロジックを示します。したがって、スタックとキューのすべてのアプリケーションを実でき、より大きな柔軟性を提供します。
両端キューは、スタックとキューの組み合わせ、あるいは 2 つのスタックをつなげたもののような構造です。表しているのはスタック + キューのロジックなので、スタックとキューのすべての応用を実でき、しかもより柔軟です。
**Q**: 元に戻すとやり直しは具体的にどのように実装されるのですか?
**Q**:取り消し(undo)とやり直し(redo)は具体的にどのように実装されすか?
元に戻すとやり直しの操作は2つのスタックを使って実装されます:元に戻す用のスタック`A`とやり直し用スタック`B`す。
2 つのスタックを使い、スタック `A` を取り消し用スタック `B` をやり直し用に使います。
1. ユーザーが操作を実行するたびに、それがスタック`A`にプッシュされ、スタック`B`がクリアされます。
2. ユーザーが「元に戻す」を実行すると、最新の操作がスタック`A`からポップされ、スタック`B`にプッシュされます。
3. ユーザーが「やり直し」を実行すると、最新の操作がスタック`B`からポップされ、スタック`A`に戻されます。
1. ユーザーが操作を 1 つ実行するたびに、その操作をスタック `A` にプッシュ、スタック `B` を空にします。
2. ユーザーが「取り消し」を実行したときは、スタック `A` から直近の操作をポップし、それをスタック `B` にプッシュます。
3. ユーザーが「やり直し」を実行したときは、スタック `B` から直近の操作をポップし、それをスタック `A` にプッシュします。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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