mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-05 12:14:20 +00:00
1 line
1.2 MiB
Plaintext
1 line
1.2 MiB
Plaintext
{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"第 16 章 付録","text":"","path":["第 16 章 付録"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"章の内容","text":"<ul> <li>16.1 プログラミング環境のインストール</li> <li>16.2 一緒に創作に参加する</li> <li>16.3 用語集</li> </ul>","path":["第 16 章 付録"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2 コントリビューション","text":"<p>著者の能力に限りがあるため、本書にはいくつかの省略や誤りが避けられません。ご理解をお願いします。誤字、リンク切れ、内容の欠落、文章の曖昧さ、説明の不明確さ、または不合理な文章構造を発見された場合は、読者により良質な学習リソースを提供するため、修正にご協力ください。</p> <p>すべてのコントリビューターのGitHub IDは、本書のリポジトリ、ウェブ、PDFバージョンのホームページに表示され、オープンソースコミュニティへの無私の貢献に感謝いたします。</p> <p>オープンソースの魅力</p> <p>紙の本の2つの印刷版の間隔はしばしば長く、内容の更新が非常に不便です。</p> <p>しかし、このオープンソースの本では、内容の更新サイクルは数日、さらには数時間に短縮されます。</p>","path":["第 16 章 付録","16.2 コントリビューション"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1. 内容の微調整","text":"<p>下の図に示すように、各ページの右上角に「編集アイコン」があります。以下の手順に従ってテキストやコードを修正できます。</p> <ol> <li>「編集アイコン」をクリックします。「このリポジトリをフォークしますか」と促された場合は、同意してください。</li> <li>Markdownソースファイルの内容を修正し、内容の正確性を確認し、フォーマットの一貫性を保つようにしてください。</li> <li>ページの下部で修正説明を記入し、「Propose file change」ボタンをクリックします。ページがリダイレクトされた後、「Create pull request」ボタンをクリックしてプルリクエストを開始します。</li> </ol> <p></p> <p> 図 16-3 ページ編集ボタン </p> <p>図は直接修正できないため、新しいIssueを作成するか、問題を説明するコメントが必要です。できるだけ早く図を再描画して置き換えます。</p>","path":["第 16 章 付録","16.2 コントリビューション"],"tags":[]},{"location":"chapter_appendix/contribution/#2","level":3,"title":"2. 内容の作成","text":"<p>このオープンソースプロジェクトへの参加に興味がある場合、コードを他のプログラミング言語に翻訳したり、記事の内容を拡張したりすることを含めて、以下のプルリクエストワークフローを実装する必要があります。</p> <ol> <li>GitHubにログインし、本書のコードリポジトリを個人アカウントにフォークします。</li> <li>フォークしたリポジトリのウェブページに移動し、<code>git clone</code>コマンドを使用してリポジトリをローカルマシンにクローンします。</li> <li>ローカルで内容を作成し、完全なテストを実行してコードの正確性を検証します。</li> <li>ローカルで行った変更をコミットし、リモートリポジトリにプッシュします。</li> <li>リポジトリのウェブページを更新し、「Create pull request」ボタンをクリックしてプルリクエストを開始します。</li> </ol>","path":["第 16 章 付録","16.2 コントリビューション"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker","level":3,"title":"3. Dockerデプロイメント","text":"<p><code>hello-algo</code>ルートディレクトリで、以下のDockerスクリプトを実行して<code>http://localhost:8000</code>でプロジェクトにアクセスします:</p> <pre><code>docker-compose up -d\n</code></pre> <p>以下のコマンドを使用してデプロイメントを削除します:</p> <pre><code>docker-compose down\n</code></pre>","path":["第 16 章 付録","16.2 コントリビューション"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1 インストール","text":"","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#1611-ide","level":2,"title":"16.1.1 IDEのインストール","text":"<p>ローカルの統合開発環境(IDE)として、オープンソースで軽量なVS Codeを使用することをお勧めします。VS Code公式ウェブサイトにアクセスし、お使いのオペレーティングシステムに適したVS Codeのバージョンを選択してダウンロードし、インストールしてください。</p> <p></p> <p> 図 16-1 公式ウェブサイトからVS Codeをダウンロード </p> <p>VS Codeには強力な拡張機能エコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。例えば、「Python Extension Pack」をインストールした後、Pythonコードをデバッグできます。インストール手順を下の図に示します。</p> <p></p> <p> 図 16-2 VS Code拡張機能パックのインストール </p>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#1612","level":2,"title":"16.1.2 言語環境のインストール","text":"","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#1-python","level":3,"title":"1. Python環境","text":"<ol> <li>Miniconda3をダウンロードしてインストールします。Python 3.10以降が必要です。</li> <li>VS Code拡張機能マーケットプレイスで<code>python</code>を検索し、Python Extension Packをインストールします。</li> <li>(オプション)コマンドラインで<code>pip install black</code>を入力して、コードフォーマッティングツールをインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc","level":3,"title":"2. C/C++環境","text":"<ol> <li>WindowsシステムではMinGWをインストールする必要があります(設定チュートリアル)。MacOSにはClangが付属しているため、インストールは不要です。</li> <li>VS Code拡張機能マーケットプレイスで<code>c++</code>を検索し、C/C++ Extension Packをインストールします。</li> <li>(オプション)設定ページを開き、<code>Clang_format_fallback Style</code>コードフォーマッティングオプションを検索し、<code>{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }</code>に設定します。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#3-java","level":3,"title":"3. Java環境","text":"<ol> <li>OpenJDKをダウンロードしてインストールします(バージョンはJDK 9より新しい必要があります)。</li> <li>VS Code拡張機能マーケットプレイスで<code>java</code>を検索し、Extension Pack for Javaをインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#4-c","level":3,"title":"4. C#環境","text":"<ol> <li>.Net 8.0をダウンロードしてインストールします。</li> <li>VS Code拡張機能マーケットプレイスで<code>C# Dev Kit</code>を検索し、C# Dev Kitをインストールします(設定チュートリアル)。</li> <li>Visual Studioを使用することもできます(インストールチュートリアル)。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#5-go","level":3,"title":"5. Go環境","text":"<ol> <li>goをダウンロードしてインストールします。</li> <li>VS Code拡張機能マーケットプレイスで<code>go</code>を検索し、Goをインストールします。</li> <li><code>Ctrl + Shift + P</code>を押してコマンドバーを呼び出し、goと入力し、<code>Go: Install/Update Tools</code>を選択し、すべてを選択してインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift","level":3,"title":"6. Swift環境","text":"<ol> <li>Swiftをダウンロードしてインストールします。</li> <li>VS Code拡張機能マーケットプレイスで<code>swift</code>を検索し、Swift for Visual Studio Codeをインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript","level":3,"title":"7. JavaScript環境","text":"<ol> <li>Node.jsをダウンロードしてインストールします。</li> <li>(オプション)VS Code拡張機能マーケットプレイスで<code>Prettier</code>を検索し、コードフォーマッティングツールをインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript","level":3,"title":"8. TypeScript環境","text":"<ol> <li>JavaScript環境と同じインストール手順に従います。</li> <li>TypeScript Execute (tsx)をインストールします。</li> <li>VS Code拡張機能マーケットプレイスで<code>typescript</code>を検索し、Pretty TypeScript Errorsをインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart","level":3,"title":"9. Dart環境","text":"<ol> <li>Dartをダウンロードしてインストールします。</li> <li>VS Code拡張機能マーケットプレイスで<code>dart</code>を検索し、Dartをインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust","level":3,"title":"10. Rust環境","text":"<ol> <li>Rustをダウンロードしてインストールします。</li> <li>VS Code拡張機能マーケットプレイスで<code>rust</code>を検索し、rust-analyzerをインストールします。</li> </ol>","path":["第 16 章 付録","16.1 インストール"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3 用語集","text":"<p>下の表は本書に登場する重要な用語をリストアップしており、以下の点に注意する価値があります。</p> <ul> <li>英語文献を読みやすくするため、用語の英語名を覚えることをお勧めします。</li> <li>一部の用語は簡体字中国語と繁体字中国語で異なる名前を持ちます。</li> </ul> <p> 表 16-1 データ構造とアルゴリズムの重要用語 </p> English 日本語 简体中文 繁体中文 algorithm アルゴリズム 算法 演算法 data structure データ構造 数据结构 資料結構 code コード 代码 程式碼 file ファイル 文件 檔案 function 関数 函数 函式 method メソッド 方法 方法 variable 変数 变量 變數 asymptotic complexity analysis 漸近計算量解析 渐近复杂度分析 漸近複雜度分析 time complexity 時間計算量 时间复杂度 時間複雜度 space complexity 空間計算量 空间复杂度 空間複雜度 loop ループ 循环 迴圈 iteration 反復 迭代 迭代 recursion 再帰 递归 遞迴 tail recursion 末尾再帰 尾递归 尾遞迴 recursion tree 再帰木 递归树 遞迴樹 big-\\(O\\) notation ビッグO記法 大 \\(O\\) 记号 大 \\(O\\) 記號 asymptotic upper bound 漸近上界 渐近上界 漸近上界 sign-magnitude 符号と絶対値 原码 原碼 1's complement 1の補数 反码 一補數 2's complement 2の補数 补码 二補數 array 配列 数组 陣列 index インデックス 索引 索引 linked list 連結リスト 链表 鏈結串列 linked list node, list node 連結リストノード 链表节点 鏈結串列節點 head node 先頭ノード 头节点 頭節點 tail node 末尾ノード 尾节点 尾節點 list リスト 列表 串列 dynamic array 動的配列 动态数组 動態陣列 hard disk ハードディスク 硬盘 硬碟 random-access memory (RAM) メモリ 内存 記憶體 cache memory キャッシュメモリ 缓存 快取 cache miss キャッシュミス 缓存未命中 快取未命中 cache hit rate キャッシュヒット率 缓存命中率 快取命中率 stack スタック 栈 堆疊 top of the stack スタックトップ 栈顶 堆疊頂 bottom of the stack スタックボトム 栈底 堆疊底 queue キュー 队列 佇列 double-ended queue 両端キュー 双向队列 雙向佇列 front of the queue キューの先頭 队首 佇列首 rear of the queue キューの末尾 队尾 佇列尾 hash table ハッシュテーブル 哈希表 雜湊表 hash set ハッシュセット 哈希集合 雜湊集合 bucket バケット 桶 桶 hash function ハッシュ関数 哈希函数 雜湊函式 hash collision ハッシュ衝突 哈希冲突 雜湊衝突 load factor 負荷率 负载因子 負載因子 separate chaining チェイン法 链式地址 鏈結位址 open addressing オープンアドレス法 开放寻址 開放定址 linear probing 線形プローブ法 线性探测 線性探查 lazy deletion 遅延削除 懒删除 懶刪除 binary tree 二分木 二叉树 二元樹 tree node 木のノード 树节点 樹節點 left-child node 左の子ノード 左子节点 左子節點 right-child node 右の子ノード 右子节点 右子節點 parent node 親ノード 父节点 父節點 left subtree 左の部分木 左子树 左子樹 right subtree 右の部分木 右子树 右子樹 root node ルートノード 根节点 根節點 leaf node 葉ノード 叶节点 葉節點 edge エッジ 边 邊 level レベル 层 層 degree 次数 度 度 height 高さ 高度 高度 depth 深さ 深度 深度 perfect binary tree 完全二分木 完美二叉树 完美二元樹 complete binary tree 完全二分木 完全二叉树 完全二元樹 full binary tree 満二分木 完满二叉树 完滿二元樹 balanced binary tree 平衡二分木 平衡二叉树 平衡二元樹 binary search tree 二分探索木 二叉搜索树 二元搜尋樹 AVL tree AVL木 AVL 树 AVL 樹 red-black tree 赤黒木 红黑树 紅黑樹 level-order traversal レベル順走査 层序遍历 層序走訪 breadth-first traversal 幅優先走査 广度优先遍历 廣度優先走訪 depth-first traversal 深さ優先走査 深度优先遍历 深度優先走訪 binary search tree 二分探索木 二叉搜索树 二元搜尋樹 balanced binary search tree 平衡二分探索木 平衡二叉搜索树 平衡二元搜尋樹 balance factor 平衡因子 平衡因子 平衡因子 heap ヒープ 堆 堆積 max heap 最大ヒープ 大顶堆 大頂堆積 min heap 最小ヒープ 小顶堆 小頂堆積 priority queue 優先度キュー 优先队列 優先佇列 heapify ヒープ化 堆化 堆積化 top-\\(k\\) problem Top-\\(k\\) 問題 Top-\\(k\\) 问题 Top-\\(k\\) 問題 graph グラフ 图 圖 vertex 頂点 顶点 頂點 undirected graph 無向グラフ 无向图 無向圖 directed graph 有向グラフ 有向图 有向圖 connected graph 連結グラフ 连通图 連通圖 disconnected graph 非連結グラフ 非连通图 非連通圖 weighted graph 重み付きグラフ 有权图 有權圖 adjacency 隣接 邻接 鄰接 path パス 路径 路徑 in-degree 入次数 入度 入度 out-degree 出次数 出度 出度 adjacency matrix 隣接行列 邻接矩阵 鄰接矩陣 adjacency list 隣接リスト 邻接表 鄰接表 breadth-first search 幅優先探索 广度优先搜索 廣度優先搜尋 depth-first search 深さ優先探索 深度优先搜索 深度優先搜尋 binary search 二分探索 二分查找 二分搜尋 searching algorithm 探索アルゴリズム 搜索算法 搜尋演算法 sorting algorithm ソートアルゴリズム 排序算法 排序演算法 selection sort 選択ソート 选择排序 選擇排序 bubble sort バブルソート 冒泡排序 泡沫排序 insertion sort 挿入ソート 插入排序 插入排序 quick sort クイックソート 快速排序 快速排序 merge sort マージソート 归并排序 合併排序 heap sort ヒープソート 堆排序 堆積排序 bucket sort バケットソート 桶排序 桶排序 counting sort 計数ソート 计数排序 計數排序 radix sort 基数ソート 基数排序 基數排序 divide and conquer 分割統治法 分治 分治 hanota problem ハノイの塔問題 汉诺塔问题 河內塔問題 backtracking algorithm バックトラッキング 回溯算法 回溯演算法 constraint 制約 约束 約束 solution 解 解 解 state 状態 状态 狀態 pruning 枝刈り 剪枝 剪枝 permutations problem 順列問題 全排列问题 全排列問題 subset-sum problem 部分集合和問題 子集和问题 子集合問題 \\(n\\)-queens problem \\(n\\) クイーン問題 \\(n\\) 皇后问题 \\(n\\) 皇后問題 dynamic programming 動的プログラミング 动态规划 動態規劃 initial state 初期状態 初始状态 初始狀態 state-transition equation 状態遷移方程式 状态转移方程 狀態轉移方程 knapsack problem ナップサック問題 背包问题 背包問題 edit distance problem 編集距離問題 编辑距离问题 編輯距離問題 greedy algorithm 貪欲アルゴリズム 贪心算法 貪婪演算法","path":["第 16 章 付録","16.3 用語集"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"第 4 章 配列と連結リスト","text":"<p>Abstract</p> <p>データ構造の世界は頑丈なレンガの壁に似ています。</p> <p>配列では、レンガがぴったりと整列し、それぞれが次のものと継ぎ目なく隣り合って、統一された形成を作っている姿を想像してください。一方、連結リストでは、これらのレンガが自由に散らばり、それらの間を優雅に編み込む蔦に抱かれています。</p>","path":["第 4 章 配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"章の内容","text":"<ul> <li>4.1 配列</li> <li>4.2 連結リスト</li> <li>4.3 リスト</li> <li>4.4 メモリとキャッシュ *</li> <li>4.5 まとめ</li> </ul>","path":["第 4 章 配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1 配列","text":"<p>配列は線形データ構造で、同じような項目が並んでいるようなもので、コンピュータのメモリ内の連続した空間に一緒に格納されます。これは整理された格納を維持するシーケンスのようなものです。この並びの各項目には、インデックスとして知られる独自の「位置」があります。以下の図を参照して、配列の動作を観察し、これらの重要な用語を理解してください。</p> <p></p> <p> 図 4-1 配列の定義と格納方法 </p>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411","level":2,"title":"4.1.1 配列の一般的な操作","text":"","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1","level":3,"title":"1. 配列の初期化","text":"<p>配列は必要に応じて2つの方法で初期化できます:初期値なしまたは指定された初期値付きです。初期値が指定されていない場合、ほとんどのプログラミング言語は配列要素を\\(0\\)に設定します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin array.py<pre><code># 配列を初期化\narr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n</code></pre> array.cpp<pre><code>/* 配列を初期化 */\n// スタックに格納\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// ヒープに格納(手動でのメモリ解放が必要)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n</code></pre> array.java<pre><code>/* 配列を初期化 */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n</code></pre> array.cs<pre><code>/* 配列を初期化 */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n</code></pre> array.go<pre><code>/* 配列を初期化 */\nvar arr [5]int\n// Goでは、長さを指定([5]int)すると配列を示し、指定しない([]int)とスライスを示します。\n// Goの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。\n// extend()メソッドの実装の便宜上、ここではSliceを配列として扱います。\nnums := []int{1, 3, 2, 5, 4}\n</code></pre> array.swift<pre><code>/* 配列を初期化 */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n</code></pre> array.js<pre><code>/* 配列を初期化 */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n</code></pre> array.ts<pre><code>/* 配列を初期化 */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n</code></pre> array.dart<pre><code>/* 配列を初期化 */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n</code></pre> array.rs<pre><code>/* 配列を初期化 */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// Rustでは、長さを指定([i32; 5])すると配列を示し、指定しない(&[i32])とスライスを示します。\n// Rustの配列はコンパイル時に固定長を持つよう設計されているため、長さの指定には定数のみ使用できます。\n// 一般的にRustでは動的配列としてVectorが使用されます。\n// extend()メソッドの実装の便宜上、ここではベクターを配列として扱います。\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n</code></pre> array.c<pre><code>/* 配列を初期化 */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n</code></pre> array.kt<pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2","level":3,"title":"2. 要素へのアクセス","text":"<p>配列内の要素は連続したメモリ空間に格納されるため、各要素のメモリアドレスを計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。</p> <p></p> <p> 図 4-2 配列要素のメモリアドレス計算 </p> <p>上の図で観察されるように、配列のインデックスは慣例的に\\(0\\)から始まります。これは直感に反するように見えるかもしれません。数を数えるのは通常\\(1\\)から始まるためですが、アドレス計算公式内では、**インデックスは本質的にメモリアドレスからのオフセット**です。最初の要素のアドレスでは、このオフセットは\\(0\\)で、そのインデックスが\\(0\\)であることを検証しています。</p> <p>配列内の要素へのアクセスは非常に効率的で、\\(O(1)\\)時間で任意の要素にランダムアクセスできます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def random_access(nums: list[int]) -> int:\n \"\"\"要素へのランダムアクセス\"\"\"\n # 区間 [0, len(nums)-1] から数値をランダムに選択\n random_index = random.randint(0, len(nums) - 1)\n # ランダムな要素を取得して返す\n random_num = nums[random_index]\n return random_num\n</code></pre> array.cpp<pre><code>/* 要素への乱数アクセス */\nint randomAccess(int *nums, int size) {\n // [0, size)の範囲で乱数を選択\n int randomIndex = rand() % size;\n // 乱数要素を取得して返却\n int randomNum = nums[randomIndex];\n return randomNum;\n}\n</code></pre> array.java<pre><code>/* 要素へのランダムアクセス */\nint randomAccess(int[] nums) {\n // 区間 [0, nums.length) からランダムに数を選択\n int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n // ランダム要素を取得して返す\n int randomNum = nums[randomIndex];\n return randomNum;\n}\n</code></pre> array.cs<pre><code>[class]{array}-[func]{RandomAccess}\n</code></pre> array.go<pre><code>[class]{}-[func]{randomAccess}\n</code></pre> array.swift<pre><code>[class]{}-[func]{randomAccess}\n</code></pre> array.js<pre><code>[class]{}-[func]{randomAccess}\n</code></pre> array.ts<pre><code>[class]{}-[func]{randomAccess}\n</code></pre> array.dart<pre><code>[class]{}-[func]{randomAccess}\n</code></pre> array.rs<pre><code>[class]{}-[func]{random_access}\n</code></pre> array.c<pre><code>[class]{}-[func]{randomAccess}\n</code></pre> array.kt<pre><code>[class]{}-[func]{randomAccess}\n</code></pre> array.rb<pre><code>[class]{}-[func]{random_access}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3","level":3,"title":"3. 要素の挿入","text":"<p>配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中央に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。</p> <p></p> <p> 図 4-3 配列要素挿入の例 </p> <p>配列の長さが固定されているため、要素を挿入すると必然的に配列の最後の要素が失われることに注意することが重要です。この問題を解決する方法は「リスト」の章で探求されます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def insert(nums: list[int], num: int, index: int):\n \"\"\"インデックス index に要素 num を挿入\"\"\"\n # インデックス index より後のすべての要素を1つ後ろに移動\n for i in range(len(nums) - 1, index, -1):\n nums[i] = nums[i - 1]\n # num を index の位置の要素に代入\n nums[index] = num\n</code></pre> array.cpp<pre><code>/* `index`に要素numを挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n // `index`より後のすべての要素を1つ後ろに移動\n for (int i = size - 1; i > index; i--) {\n nums[i] = nums[i - 1];\n }\n // indexの位置にnumを代入\n nums[index] = num;\n}\n</code></pre> array.java<pre><code>/* `index` に要素 num を挿入 */\nvoid insert(int[] nums, int num, int index) {\n // `index` より後のすべての要素を1つ後ろに移動\n for (int i = nums.length - 1; i > index; i--) {\n nums[i] = nums[i - 1];\n }\n // index の要素に num を代入\n nums[index] = num;\n}\n</code></pre> array.cs<pre><code>[class]{array}-[func]{Insert}\n</code></pre> array.go<pre><code>[class]{}-[func]{insert}\n</code></pre> array.swift<pre><code>[class]{}-[func]{insert}\n</code></pre> array.js<pre><code>[class]{}-[func]{insert}\n</code></pre> array.ts<pre><code>[class]{}-[func]{insert}\n</code></pre> array.dart<pre><code>[class]{}-[func]{insert}\n</code></pre> array.rs<pre><code>[class]{}-[func]{insert}\n</code></pre> array.c<pre><code>[class]{}-[func]{insert}\n</code></pre> array.kt<pre><code>[class]{}-[func]{insert}\n</code></pre> array.rb<pre><code>[class]{}-[func]{insert}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4. 要素の削除","text":"<p>同様に、以下の図に示すように、インデックス\\(i\\)の要素を削除するには、インデックス\\(i\\)に続くすべての要素を1つずつ前に移動する必要があります。</p> <p></p> <p> 図 4-4 配列要素削除の例 </p> <p>削除後、元の最後の要素は「意味がない」ものになるため、特定の修正は必要ないことに注意してください。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def remove(nums: list[int], index: int):\n \"\"\"インデックス index の要素を削除\"\"\"\n # インデックス index より後のすべての要素を1つ前に移動\n for i in range(index, len(nums) - 1):\n nums[i] = nums[i + 1]\n</code></pre> array.cpp<pre><code>/* `index`の要素を削除 */\nvoid remove(int *nums, int size, int index) {\n // `index`より後のすべての要素を1つ前に移動\n for (int i = index; i < size - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.java<pre><code>/* `index` の要素を削除 */\nvoid remove(int[] nums, int index) {\n // `index` より後のすべての要素を1つ前に移動\n for (int i = index; i < nums.length - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.cs<pre><code>[class]{array}-[func]{Remove}\n</code></pre> array.go<pre><code>[class]{}-[func]{remove}\n</code></pre> array.swift<pre><code>[class]{}-[func]{remove}\n</code></pre> array.js<pre><code>[class]{}-[func]{remove}\n</code></pre> array.ts<pre><code>[class]{}-[func]{remove}\n</code></pre> array.dart<pre><code>[class]{}-[func]{remove}\n</code></pre> array.rs<pre><code>[class]{}-[func]{remove}\n</code></pre> array.c<pre><code>[class]{}-[func]{removeItem}\n</code></pre> array.kt<pre><code>[class]{}-[func]{remove}\n</code></pre> array.rb<pre><code>[class]{}-[func]{remove}\n</code></pre> <p>要約すると、配列の挿入と削除操作には以下の欠点があります:</p> <ul> <li>高い時間計算量:配列の挿入と削除の両方の平均時間計算量は\\(O(n)\\)で、ここで\\(n\\)は配列の長さです。</li> <li>要素の損失:配列の長さが固定されているため、挿入時に配列の容量を超える要素は失われます。</li> <li>メモリの無駄:より長い配列を初期化して前部分のみを利用すると、挿入時に「意味のない」末尾要素が生じ、メモリ空間の無駄につながります。</li> </ul>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5","level":3,"title":"5. 配列の走査","text":"<p>ほとんどのプログラミング言語では、インデックスを使用するか、各要素を直接反復することで配列を走査できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def traverse(nums: list[int]):\n \"\"\"配列の走査\"\"\"\n count = 0\n # インデックスによる配列の走査\n for i in range(len(nums)):\n count += nums[i]\n # 配列要素の走査\n for num in nums:\n count += num\n # データのインデックスと要素の両方を走査\n for i, num in enumerate(nums):\n count += nums[i]\n count += num\n</code></pre> array.cpp<pre><code>/* 配列の走査 */\nvoid traverse(int *nums, int size) {\n int count = 0;\n // インデックスによる配列の走査\n for (int i = 0; i < size; i++) {\n count += nums[i];\n }\n}\n</code></pre> array.java<pre><code>/* 配列を走査 */\nvoid traverse(int[] nums) {\n int count = 0;\n // インデックスによる配列の走査\n for (int i = 0; i < nums.length; i++) {\n count += nums[i];\n }\n // 配列要素の走査\n for (int num : nums) {\n count += num;\n }\n}\n</code></pre> array.cs<pre><code>[class]{array}-[func]{Traverse}\n</code></pre> array.go<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.swift<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.js<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.ts<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.dart<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.rs<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.c<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.kt<pre><code>[class]{}-[func]{traverse}\n</code></pre> array.rb<pre><code>[class]{}-[func]{traverse}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6","level":3,"title":"6. 要素の検索","text":"<p>配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。</p> <p>配列は線形データ構造であるため、この操作は一般的に「線形探索」と呼ばれます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def find(nums: list[int], target: int) -> int:\n \"\"\"配列内の指定された要素を検索\"\"\"\n for i in range(len(nums)):\n if nums[i] == target:\n return i\n return -1\n</code></pre> array.cpp<pre><code>/* 配列内の指定要素を検索 */\nint find(int *nums, int size, int target) {\n for (int i = 0; i < size; i++) {\n if (nums[i] == target)\n return i;\n }\n return -1;\n}\n</code></pre> array.java<pre><code>/* 配列内で指定された要素を検索 */\nint find(int[] nums, int target) {\n for (int i = 0; i < nums.length; i++) {\n if (nums[i] == target)\n return i;\n }\n return -1;\n}\n</code></pre> array.cs<pre><code>[class]{array}-[func]{Find}\n</code></pre> array.go<pre><code>[class]{}-[func]{find}\n</code></pre> array.swift<pre><code>[class]{}-[func]{find}\n</code></pre> array.js<pre><code>[class]{}-[func]{find}\n</code></pre> array.ts<pre><code>[class]{}-[func]{find}\n</code></pre> array.dart<pre><code>[class]{}-[func]{find}\n</code></pre> array.rs<pre><code>[class]{}-[func]{find}\n</code></pre> array.c<pre><code>[class]{}-[func]{find}\n</code></pre> array.kt<pre><code>[class]{}-[func]{find}\n</code></pre> array.rb<pre><code>[class]{}-[func]{find}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7","level":3,"title":"7. 配列の拡張","text":"<p>複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。</p> <p>配列を拡張するには、より大きな配列を作成し、元の配列から要素をコピーする必要があります。この操作の時間計算量は\\(O(n)\\)で、大きな配列では時間がかかる可能性があります。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def extend(nums: list[int], enlarge: int) -> list[int]:\n \"\"\"配列の長さを拡張\"\"\"\n # 拡張された長さの配列を初期化\n res = [0] * (len(nums) + enlarge)\n # 元の配列のすべての要素を新しい配列にコピー\n for i in range(len(nums)):\n res[i] = nums[i]\n # 拡張後の新しい配列を返す\n return res\n</code></pre> array.cpp<pre><code>/* 配列長の拡張 */\nint *extend(int *nums, int size, int enlarge) {\n // 拡張された長さの配列を初期化\n int *res = new int[size + enlarge];\n // 元の配列の全要素を新しい配列にコピー\n for (int i = 0; i < size; i++) {\n res[i] = nums[i];\n }\n // メモリを解放\n delete[] nums;\n // 拡張後の新しい配列を返却\n return res;\n}\n</code></pre> array.java<pre><code>/* 配列長の拡張 */\nint[] extend(int[] nums, int enlarge) {\n // 拡張された長さの配列を初期化\n int[] res = new int[nums.length + enlarge];\n // 元の配列のすべての要素を新しい配列にコピー\n for (int i = 0; i < nums.length; i++) {\n res[i] = nums[i];\n }\n // 拡張後の新しい配列を返す\n return res;\n}\n</code></pre> array.cs<pre><code>[class]{array}-[func]{Extend}\n</code></pre> array.go<pre><code>[class]{}-[func]{extend}\n</code></pre> array.swift<pre><code>[class]{}-[func]{extend}\n</code></pre> array.js<pre><code>[class]{}-[func]{extend}\n</code></pre> array.ts<pre><code>[class]{}-[func]{extend}\n</code></pre> array.dart<pre><code>[class]{}-[func]{extend}\n</code></pre> array.rs<pre><code>[class]{}-[func]{extend}\n</code></pre> array.c<pre><code>[class]{}-[func]{extend}\n</code></pre> array.kt<pre><code>[class]{}-[func]{extend}\n</code></pre> array.rb<pre><code>[class]{}-[func]{extend}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412","level":2,"title":"4.1.2 配列の利点と制限","text":"<p>配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作の効率を最適化するために活用できる実質的な事前情報を提供します。</p> <ul> <li>高い空間効率:配列はデータのための連続したメモリブロックを割り当て、追加の構造的オーバーヘッドの必要性を排除します。</li> <li>ランダムアクセスのサポート:配列は任意の要素への\\(O(1)\\)時間アクセスを可能にします。</li> <li>キャッシュ局所性:配列要素にアクセスするとき、コンピュータはそれらを読み込むだけでなく、周囲のデータもキャッシュし、高速キャッシュを利用して後続の操作速度を向上させます。</li> </ul> <p>しかし、連続空間格納は諸刃の剣で、以下の制限があります:</p> <ul> <li>挿入と削除の効率が低い:配列に多くの要素が蓄積されると、要素の挿入や削除には大量の要素をシフトする必要があります。</li> <li>固定長:配列の長さは初期化後に固定されます。配列を拡張するには、すべてのデータを新しい配列にコピーする必要があり、大きなコストがかかります。</li> <li>空間の無駄:割り当てられた配列サイズが必要以上に大きい場合、余分な空間が無駄になります。</li> </ul>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413","level":2,"title":"4.1.3 配列の典型的な応用","text":"<p>配列は基本的で広く使用されるデータ構造です。様々なアルゴリズムで頻繁に応用され、複雑なデータ構造の実装に役立ちます。</p> <ul> <li>ランダムアクセス:配列はランダムサンプリングが必要なときのデータ格納に理想的です。インデックスに基づいてランダムシーケンスを生成することで、効率的にランダムサンプリングを実現できます。</li> <li>ソートと検索:配列はソートと検索アルゴリズムで最も一般的に使用されるデータ構造です。クイックソート、マージソート、二分探索などの技術は主に配列で動作します。</li> <li>ルックアップテーブル:配列は迅速な要素や関係の取得のための効率的なルックアップテーブルとして機能します。例えば、文字をASCIIコードにマッピングすることは、ASCIIコード値をインデックスとして使用し、対応する要素を配列に格納することで簡単になります。</li> <li>機械学習:ニューラルネットワークの領域では、配列はベクトル、行列、テンソルを含む重要な線形代数演算の実行において重要な役割を果たします。配列はニューラルネットワークプログラミングにおいて主要かつ最も広範囲に使用されるデータ構造として機能します。</li> <li>データ構造の実装:配列は、スタック、キュー、ハッシュ表、ヒープ、グラフなど、様々なデータ構造を実装するための構成要素として機能します。例えば、グラフの隣接行列表現は本質的に二次元配列です。</li> </ul>","path":["第 4 章 配列と連結リスト","4.1 配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2 連結リスト","text":"<p>メモリ空間は、すべてのプログラム間で共有されるリソースです。複雑なシステム環境では、使用可能なメモリがメモリ空間全体に分散している可能性があります。配列に割り当てられるメモリは連続している必要があることを理解していますが、非常に大きな配列の場合、十分な大きさの連続メモリ空間を見つけるのは困難な場合があります。ここで、連結リストの柔軟な利点が明らかになります。</p> <p>連結リストは線形データ構造であり、各要素はノードオブジェクトで、ノードは「参照」を通じて相互接続されています。これらの参照は後続ノードのメモリアドレスを保持し、1つのノードから次のノードへのナビゲーションを可能にします。</p> <p>連結リストの設計では、ノードを連続するメモリアドレスを必要とせずに、メモリ位置全体に分散配置することができます。</p> <p></p> <p> 図 4-5 連結リストの定義と格納方法 </p> <p>上図に示すように、連結リストの基本的な構成要素はノードオブジェクトです。各ノードは2つの主要なコンポーネントで構成されています:ノードの「値」と次のノードへの「参照」です。</p> <ul> <li>連結リストの最初のノードは「ヘッドノード」、最後のノードは「テールノード」です。</li> <li>テールノードは「null」を指し、Javaでは<code>null</code>、C++では<code>nullptr</code>、Pythonでは<code>None</code>として指定されます。</li> <li>C、C++、Go、Rustなどのポインタをサポートする言語では、この「参照」は通常「ポインタ」として実装されます。</li> </ul> <p>以下のコードが示すように、連結リストの<code>ListNode</code>は値を保持するだけでなく、追加の参照(またはポインタ)も維持する必要があります。したがって、連結リストは同じ量のデータを格納する場合、配列よりも多くのメモリ空間を占有します。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>class ListNode:\n \"\"\"連結リストノードクラス\"\"\"\n def __init__(self, val: int):\n self.val: int = val # ノード値\n self.next: ListNode | None = None # 次のノードへの参照\n</code></pre> <pre><code>/* 連結リストノード構造体 */\nstruct ListNode {\n int val; // ノード値\n ListNode *next; // 次のノードへのポインタ\n ListNode(int x) : val(x), next(nullptr) {} // コンストラクタ\n};\n</code></pre> <pre><code>/* 連結リストノードクラス */\nclass ListNode {\n int val; // ノード値\n ListNode next; // 次のノードへの参照\n ListNode(int x) { val = x; } // コンストラクタ\n}\n</code></pre> <pre><code>/* 連結リストノードクラス */\nclass ListNode(int x) { // コンストラクタ\n int val = x; // ノード値\n ListNode? next; // 次のノードへの参照\n}\n</code></pre> <pre><code>/* 連結リストノード構造体 */\ntype ListNode struct {\n Val int // ノード値\n Next *ListNode // 次のノードへのポインタ\n}\n\n// NewListNode コンストラクタ、新しい連結リストを作成\nfunc NewListNode(val int) *ListNode {\n return &ListNode{\n Val: val,\n Next: nil,\n }\n}\n</code></pre> <pre><code>/* 連結リストノードクラス */\nclass ListNode {\n var val: Int // ノード値\n var next: ListNode? // 次のノードへの参照\n\n init(x: Int) { // コンストラクタ\n val = x\n }\n}\n</code></pre> <pre><code>/* 連結リストノードクラス */\nclass ListNode {\n constructor(val, next) {\n this.val = (val === undefined ? 0 : val); // ノード値\n this.next = (next === undefined ? null : next); // 次のノードへの参照\n }\n}\n</code></pre> <pre><code>/* 連結リストノードクラス */\nclass ListNode {\n val: number;\n next: ListNode | null;\n constructor(val?: number, next?: ListNode | null) {\n this.val = val === undefined ? 0 : val; // ノード値\n this.next = next === undefined ? null : next; // 次のノードへの参照\n }\n}\n</code></pre> <pre><code>/* 連結リストノードクラス */\nclass ListNode {\n int val; // ノード値\n ListNode? next; // 次のノードへの参照\n ListNode(this.val, [this.next]); // コンストラクタ\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n/* 連結リストノードクラス */\n#[derive(Debug)]\nstruct ListNode {\n val: i32, // ノード値\n next: Option<Rc<RefCell<ListNode>>>, // 次のノードへのポインタ\n}\n</code></pre> <pre><code>/* 連結リストノード構造体 */\ntypedef struct ListNode {\n int val; // ノード値\n struct ListNode *next; // 次のノードへのポインタ\n} ListNode;\n\n/* コンストラクタ */\nListNode *newListNode(int val) {\n ListNode *node;\n node = (ListNode *) malloc(sizeof(ListNode));\n node->val = val;\n node->next = NULL;\n return node;\n}\n</code></pre> <pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421","level":2,"title":"4.2.1 連結リストの一般的な操作","text":"","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1","level":3,"title":"1. 連結リストの初期化","text":"<p>連結リストの構築は2段階のプロセスです:まず各ノードオブジェクトを初期化し、次にノード間の参照リンクを形成します。初期化後、ヘッドノードから<code>next</code>参照をたどってすべてのノードを順次巡回できます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin linked_list.py<pre><code># 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4\n# 各ノードを初期化\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n</code></pre> linked_list.cpp<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// ノード間の参照を構築\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n</code></pre> linked_list.java<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.cs<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.go<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// ノード間の参照を構築\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n</code></pre> linked_list.swift<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n</code></pre> linked_list.js<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.ts<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.dart<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.rs<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// ノード間の参照を構築\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n</code></pre> linked_list.c<pre><code>/* 連結リストを初期化: 1 -> 3 -> 2 -> 5 -> 4 */\n// 各ノードを初期化\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// ノード間の参照を構築\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n</code></pre> linked_list.kt<pre><code>\n</code></pre> <p>配列全体は1つの変数です。例えば、配列<code>nums</code>には<code>nums[0]</code>、<code>nums[1]</code>などの要素が含まれますが、連結リストは複数の異なるノードオブジェクトで構成されています。通常、連結リストはそのヘッドノードで参照されます。例えば、前のコードスニペットの連結リストは<code>n0</code>として参照されます。</p>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2. ノードの挿入","text":"<p>連結リストにノードを挿入するのは非常に簡単です。下図に示すように、隣接する2つのノード<code>n0</code>と<code>n1</code>の間に新しいノード<code>P</code>を挿入することを目指すとします。これは2つのノード参照(ポインタ)を変更するだけで実現でき、時間計算量は\\(O(1)\\)です。</p> <p>比較すると、配列に要素を挿入する時間計算量は\\(O(n)\\)であり、大量のデータを扱う場合には効率が悪くなります。</p> <p></p> <p> 図 4-6 連結リストノード挿入の例 </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def insert(n0: ListNode, P: ListNode):\n \"\"\"連結リストのノード n0 の後にノード P を挿入\"\"\"\n n1 = n0.next\n P.next = n1\n n0.next = P\n</code></pre> linked_list.cpp<pre><code>/* 連結リストのノードn0の後にノードPを挿入 */\nvoid insert(ListNode *n0, ListNode *P) {\n ListNode *n1 = n0->next;\n P->next = n1;\n n0->next = P;\n}\n</code></pre> linked_list.java<pre><code>/* 連結リストでノード n0 の後にノード P を挿入 */\nvoid insert(ListNode n0, ListNode P) {\n ListNode n1 = n0.next;\n P.next = n1;\n n0.next = P;\n}\n</code></pre> linked_list.cs<pre><code>[class]{linked_list}-[func]{Insert}\n</code></pre> linked_list.go<pre><code>[class]{}-[func]{insertNode}\n</code></pre> linked_list.swift<pre><code>[class]{}-[func]{insert}\n</code></pre> linked_list.js<pre><code>[class]{}-[func]{insert}\n</code></pre> linked_list.ts<pre><code>[class]{}-[func]{insert}\n</code></pre> linked_list.dart<pre><code>[class]{}-[func]{insert}\n</code></pre> linked_list.rs<pre><code>[class]{}-[func]{insert}\n</code></pre> linked_list.c<pre><code>[class]{}-[func]{insert}\n</code></pre> linked_list.kt<pre><code>[class]{}-[func]{insert}\n</code></pre> linked_list.rb<pre><code>[class]{}-[func]{insert}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3","level":3,"title":"3. ノードの削除","text":"<p>下図に示すように、連結リストからノードを削除することも非常に簡単で、1つのノードの参照(ポインタ)を変更するだけです。</p> <p>重要な点は、ノード<code>P</code>が削除された後も<code>n1</code>を指し続けていることですが、連結リストの巡回中にはアクセスできなくなることです。これは事実上、<code>P</code>が連結リストの一部ではなくなったことを意味します。</p> <p></p> <p> 図 4-7 連結リストノードの削除 </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def remove(n0: ListNode):\n \"\"\"連結リストのノード n0 の後の最初のノードを削除\"\"\"\n if not n0.next:\n return\n # n0 -> P -> n1\n P = n0.next\n n1 = P.next\n n0.next = n1\n</code></pre> linked_list.cpp<pre><code>/* 連結リストのノードn0の後の最初のノードを削除 */\nvoid remove(ListNode *n0) {\n if (n0->next == nullptr)\n return;\n // n0 -> P -> n1\n ListNode *P = n0->next;\n ListNode *n1 = P->next;\n n0->next = n1;\n // メモリを解放\n delete P;\n}\n</code></pre> linked_list.java<pre><code>/* 連結リストでノード n0 の後の最初のノードを削除 */\nvoid remove(ListNode n0) {\n if (n0.next == null)\n return;\n // n0 -> P -> n1\n ListNode P = n0.next;\n ListNode n1 = P.next;\n n0.next = n1;\n}\n</code></pre> linked_list.cs<pre><code>[class]{linked_list}-[func]{Remove}\n</code></pre> linked_list.go<pre><code>[class]{}-[func]{removeItem}\n</code></pre> linked_list.swift<pre><code>[class]{}-[func]{remove}\n</code></pre> linked_list.js<pre><code>[class]{}-[func]{remove}\n</code></pre> linked_list.ts<pre><code>[class]{}-[func]{remove}\n</code></pre> linked_list.dart<pre><code>[class]{}-[func]{remove}\n</code></pre> linked_list.rs<pre><code>[class]{}-[func]{remove}\n</code></pre> linked_list.c<pre><code>[class]{}-[func]{removeItem}\n</code></pre> linked_list.kt<pre><code>[class]{}-[func]{remove}\n</code></pre> linked_list.rb<pre><code>[class]{}-[func]{remove}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4","level":3,"title":"4. ノードへのアクセス","text":"<p>連結リストでのノードへのアクセスは効率が悪いです。前述したように、配列の任意の要素には\\(O(1)\\)時間でアクセスできます。対照的に、連結リストでは、プログラムはヘッドノードから開始して目的のノードが見つかるまで順次ノードを巡回する必要があります。つまり、連結リストの\\(i\\)番目のノードにアクセスするには、プログラムは\\(i - 1\\)個のノードを反復処理する必要があり、時間計算量は\\(O(n)\\)になります。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def access(head: ListNode, index: int) -> ListNode | None:\n \"\"\"連結リストのインデックス index のノードにアクセス\"\"\"\n for _ in range(index):\n if not head:\n return None\n head = head.next\n return head\n</code></pre> linked_list.cpp<pre><code>/* 連結リストの`index`番目のノードにアクセス */\nListNode *access(ListNode *head, int index) {\n for (int i = 0; i < index; i++) {\n if (head == nullptr)\n return nullptr;\n head = head->next;\n }\n return head;\n}\n</code></pre> linked_list.java<pre><code>/* 連結リストの `index` のノードにアクセス */\nListNode access(ListNode head, int index) {\n for (int i = 0; i < index; i++) {\n if (head == null)\n return null;\n head = head.next;\n }\n return head;\n}\n</code></pre> linked_list.cs<pre><code>[class]{linked_list}-[func]{Access}\n</code></pre> linked_list.go<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.swift<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.js<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.ts<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.dart<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.rs<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.c<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.kt<pre><code>[class]{}-[func]{access}\n</code></pre> linked_list.rb<pre><code>[class]{}-[func]{access}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5","level":3,"title":"5. ノードの検索","text":"<p>連結リストを巡回して、値が<code>target</code>に一致するノードを見つけ、連結リスト内でのそのノードのインデックスを出力します。この手順も線形検索の例です。対応するコードは以下のとおりです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def find(head: ListNode, target: int) -> int:\n \"\"\"連結リストで値 target を持つ最初のノードを検索\"\"\"\n index = 0\n while head:\n if head.val == target:\n return index\n head = head.next\n index += 1\n return -1\n</code></pre> linked_list.cpp<pre><code>/* 連結リストで値がtargetの最初のノードを検索 */\nint find(ListNode *head, int target) {\n int index = 0;\n while (head != nullptr) {\n if (head->val == target)\n return index;\n head = head->next;\n index++;\n }\n return -1;\n}\n</code></pre> linked_list.java<pre><code>/* 連結リストで値 target を持つ最初のノードを検索 */\nint find(ListNode head, int target) {\n int index = 0;\n while (head != null) {\n if (head.val == target)\n return index;\n head = head.next;\n index++;\n }\n return -1;\n}\n</code></pre> linked_list.cs<pre><code>[class]{linked_list}-[func]{Find}\n</code></pre> linked_list.go<pre><code>[class]{}-[func]{findNode}\n</code></pre> linked_list.swift<pre><code>[class]{}-[func]{find}\n</code></pre> linked_list.js<pre><code>[class]{}-[func]{find}\n</code></pre> linked_list.ts<pre><code>[class]{}-[func]{find}\n</code></pre> linked_list.dart<pre><code>[class]{}-[func]{find}\n</code></pre> linked_list.rs<pre><code>[class]{}-[func]{find}\n</code></pre> linked_list.c<pre><code>[class]{}-[func]{find}\n</code></pre> linked_list.kt<pre><code>[class]{}-[func]{find}\n</code></pre> linked_list.rb<pre><code>[class]{}-[func]{find}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-vs","level":2,"title":"4.2.2 配列 vs. 連結リスト","text":"<p>下表は配列と連結リストの特性をまとめ、様々な操作における効率も比較しています。それぞれが対照的な格納戦略を使用するため、それぞれの特性と操作効率は明確に対比されています。</p> <p> 表 4-1 配列と連結リストの効率比較 </p> 配列 連結リスト 格納方式 連続メモリ空間 分散メモリ空間 容量拡張 固定長 柔軟な拡張 メモリ効率 要素あたりのメモリ少、潜在的な空間の無駄 要素あたりのメモリ多 要素へのアクセス \\(O(1)\\) \\(O(n)\\) 要素の追加 \\(O(n)\\) \\(O(1)\\) 要素の削除 \\(O(n)\\) \\(O(1)\\)","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423","level":2,"title":"4.2.3 連結リストの一般的な種類","text":"<p>下図に示すように、連結リストには3つの一般的な種類があります。</p> <ul> <li>単方向連結リスト:これは前述した標準的な連結リストです。単方向連結リストのノードには値と次のノードへの参照が含まれます。最初のノードはヘッドノードと呼ばれ、null(<code>None</code>)を指す最後のノードはテールノードです。</li> <li>循環連結リスト:これは単方向連結リストのテールノードがヘッドノードを指してループを作ることで形成されます。循環連結リストでは、任意のノードがヘッドノードとして機能できます。</li> <li>双方向連結リスト:単方向連結リストとは対照的に、双方向連結リストは2つの方向で参照を維持します。各ノードには後続者(次のノード)と前任者(前のノード)の両方への参照(ポインタ)が含まれます。双方向連結リストはどちらの方向にも巡回できるより多くの柔軟性を提供しますが、より多くのメモリ空間も消費します。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>class ListNode:\n \"\"\"双方向連結リストノードクラス\"\"\"\n def __init__(self, val: int):\n self.val: int = val # ノード値\n self.next: ListNode | None = None # 後続ノードへの参照\n self.prev: ListNode | None = None # 前任ノードへの参照\n</code></pre> <pre><code>/* 双方向連結リストノード構造体 */\nstruct ListNode {\n int val; // ノード値\n ListNode *next; // 後続ノードへのポインタ\n ListNode *prev; // 前任ノードへのポインタ\n ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // コンストラクタ\n};\n</code></pre> <pre><code>/* 双方向連結リストノードクラス */\nclass ListNode {\n int val; // ノード値\n ListNode next; // 次のノードへの参照\n ListNode prev; // 前任ノードへの参照\n ListNode(int x) { val = x; } // コンストラクタ\n}\n</code></pre> <pre><code>/* 双方向連結リストノードクラス */\nclass ListNode(int x) { // コンストラクタ\n int val = x; // ノード値\n ListNode next; // 次のノードへの参照\n ListNode prev; // 前任ノードへの参照\n}\n</code></pre> <pre><code>/* 双方向連結リストノード構造体 */\ntype DoublyListNode struct {\n Val int // ノード値\n Next *DoublyListNode // 後続ノードへのポインタ\n Prev *DoublyListNode // 前任ノードへのポインタ\n}\n\n// NewDoublyListNode 初期化\nfunc NewDoublyListNode(val int) *DoublyListNode {\n return &DoublyListNode{\n Val: val,\n Next: nil,\n Prev: nil,\n }\n}\n</code></pre> <pre><code>/* 双方向連結リストノードクラス */\nclass ListNode {\n var val: Int // ノード値\n var next: ListNode? // 次のノードへの参照\n var prev: ListNode? // 前任ノードへの参照\n\n init(x: Int) { // コンストラクタ\n val = x\n }\n}\n</code></pre> <pre><code>/* 双方向連結リストノードクラス */\nclass ListNode {\n constructor(val, next, prev) {\n this.val = val === undefined ? 0 : val; // ノード値\n this.next = next === undefined ? null : next; // 後続ノードへの参照\n this.prev = prev === undefined ? null : prev; // 前任ノードへの参照\n }\n}\n</code></pre> <pre><code>/* 双方向連結リストノードクラス */\nclass ListNode {\n val: number;\n next: ListNode | null;\n prev: ListNode | null;\n constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n this.val = val === undefined ? 0 : val; // ノード値\n this.next = next === undefined ? null : next; // 後続ノードへの参照\n this.prev = prev === undefined ? null : prev; // 前任ノードへの参照\n }\n}\n</code></pre> <pre><code>/* 双方向連結リストノードクラス */\nclass ListNode {\n int val; // ノード値\n ListNode next; // 次のノードへの参照\n ListNode prev; // 前任ノードへの参照\n ListNode(this.val, [this.next, this.prev]); // コンストラクタ\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 双方向連結リストノード型 */\n#[derive(Debug)]\nstruct ListNode {\n val: i32, // ノード値\n next: Option<Rc<RefCell<ListNode>>>, // 後続ノードへのポインタ\n prev: Option<Rc<RefCell<ListNode>>>, // 前任ノードへのポインタ\n}\n\n/* コンストラクタ */\nimpl ListNode {\n fn new(val: i32) -> Self {\n ListNode {\n val,\n next: None,\n prev: None,\n }\n }\n}\n</code></pre> <pre><code>/* 双方向連結リストノード構造体 */\ntypedef struct ListNode {\n int val; // ノード値\n struct ListNode *next; // 後続ノードへのポインタ\n struct ListNode *prev; // 前任ノードへのポインタ\n} ListNode;\n\n/* コンストラクタ */\nListNode *newListNode(int val) {\n ListNode *node, *next;\n node = (ListNode *) malloc(sizeof(ListNode));\n node->val = val;\n node->next = NULL;\n node->prev = NULL;\n return node;\n}\n</code></pre> <pre><code>\n</code></pre> <p></p> <p> 図 4-8 連結リストの一般的な種類 </p>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424","level":2,"title":"4.2.4 連結リストの典型的な応用","text":"<p>単方向連結リストは、スタック、キュー、ハッシュ表、グラフの実装によく使用されます。</p> <ul> <li>スタックとキュー:単方向連結リストで、挿入と削除が同じ端で行われる場合、スタック(後入先出)のように動作します。逆に、挿入が一方の端で、削除がもう一方の端で行われる場合、キュー(先入先出)のように機能します。</li> <li>ハッシュ表:連結リストは、ハッシュ衝突を解決する人気の方法である連鎖法で使用されます。ここでは、すべての衝突した要素が連結リストにグループ化されます。</li> <li>グラフ:グラフ表現の標準的な方法である隣接リストは、各グラフ頂点を連結リストに関連付けます。このリストには、対応する頂点に接続された頂点を表す要素が含まれます。</li> </ul> <p>双方向連結リストは、前後の要素への高速アクセスが必要なシナリオに最適です。</p> <ul> <li>高度なデータ構造:赤黒木やB木などの構造では、ノードの親へのアクセスが重要です。これは各ノードに親ノードへの参照を組み込むことで実現され、双方向連結リストに似ています。</li> <li>ブラウザ履歴:Webブラウザでは、双方向連結リストにより、ユーザーが前進または後退ボタンをクリックしたときの訪問ページの履歴ナビゲーションが容易になります。</li> <li>LRUアルゴリズム:双方向連結リストは、最近最少使用(LRU)キャッシュ削除アルゴリズムに適しており、最近最少使用データの迅速な識別と、高速なノード追加・削除を可能にします。</li> </ul> <p>循環連結リストは、オペレーティングシステムでのリソーススケジューリングなど、周期的な操作が必要なアプリケーションに最適です。</p> <ul> <li>ラウンドロビンスケジューリングアルゴリズム:オペレーティングシステムでは、ラウンドロビンスケジューリングアルゴリズムは一般的なCPUスケジューリング方法であり、プロセスのグループを循環する必要があります。各プロセスにはタイムスライスが割り当てられ、期限切れになるとCPUは次のプロセスに回転します。この循環操作は循環連結リストを使用して効率的に実現でき、すべてのプロセス間で公平かつ時分割システムを可能にします。</li> <li>データバッファ:循環連結リストは、オーディオやビデオプレーヤーなどのデータバッファでも使用され、データストリームが複数のバッファブロックに分割され、シームレスな再生のために循環方式で配置されます。</li> </ul>","path":["第 4 章 配列と連結リスト","4.2 連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3 リスト","text":"<p>リストは、要素へのアクセス、変更、追加、削除、走査などの操作をサポートする、順序付けられた要素のコレクションを表す抽象的なデータ構造の概念であり、ユーザーが容量制限を考慮する必要がありません。リストは連結リストまたは配列に基づいて実装できます。</p> <ul> <li>連結リストは本質的にリストとして機能し、要素の追加、削除、検索、変更の操作をサポートし、サイズを動的に調整する柔軟性があります。</li> <li>配列もこれらの操作をサポートしますが、長さが不変であるため、長さ制限のあるリストと考えることができます。</li> </ul> <p>配列を使用してリストを実装する場合、長さの不変性によりリストの実用性が低下します。これは、事前に格納するデータ量を予測することが困難な場合が多く、適切なリスト長を選択することが困難であるためです。長さが小さすぎると要件を満たさない可能性があり、大きすぎるとメモリ空間を無駄にする可能性があります。</p> <p>この問題を解決するために、動的配列を使用してリストを実装できます。これは配列の利点を継承し、プログラム実行中に動的に拡張できます。</p> <p>実際、多くのプログラミング言語の標準ライブラリは動的配列を使用してリストを実装しています。例えば、Pythonの<code>list</code>、Javaの<code>ArrayList</code>、C++の<code>vector</code>、C#の<code>List</code>などです。以下の議論では、「リスト」と「動的配列」を同義の概念として扱います。</p>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431","level":2,"title":"4.3.1 リストの一般的な操作","text":"","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1","level":3,"title":"1. リストの初期化","text":"<p>通常、「初期値なし」と「初期値あり」の2つの初期化方法を使用します。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin list.py<pre><code># リストを初期化\n# 初期値なし\nnums1: list[int] = []\n# 初期値あり\nnums: list[int] = [1, 3, 2, 5, 4]\n</code></pre> list.cpp<pre><code>/* リストを初期化 */\n// 注意: C++では、vectorがここで説明されているnumsに相当します\n// 初期値なし\nvector<int> nums1;\n// 初期値あり\nvector<int> nums = { 1, 3, 2, 5, 4 };\n</code></pre> list.java<pre><code>/* リストを初期化 */\n// 初期値なし\nList<Integer> nums1 = new ArrayList<>();\n// 初期値あり(要素型はint[]のラッパークラスInteger[]である必要があります)\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n</code></pre> list.cs<pre><code>/* リストを初期化 */\n// 初期値なし\nList<int> nums1 = [];\n// 初期値あり\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n</code></pre> list_test.go<pre><code>/* リストを初期化 */\n// 初期値なし\nnums1 := []int{}\n// 初期値あり\nnums := []int{1, 3, 2, 5, 4}\n</code></pre> list.swift<pre><code>/* リストを初期化 */\n// 初期値なし\nlet nums1: [Int] = []\n// 初期値あり\nvar nums = [1, 3, 2, 5, 4]\n</code></pre> list.js<pre><code>/* リストを初期化 */\n// 初期値なし\nconst nums1 = [];\n// 初期値あり\nconst nums = [1, 3, 2, 5, 4];\n</code></pre> list.ts<pre><code>/* リストを初期化 */\n// 初期値なし\nconst nums1: number[] = [];\n// 初期値あり\nconst nums: number[] = [1, 3, 2, 5, 4];\n</code></pre> list.dart<pre><code>/* リストを初期化 */\n// 初期値なし\nList<int> nums1 = [];\n// 初期値あり\nList<int> nums = [1, 3, 2, 5, 4];\n</code></pre> list.rs<pre><code>/* リストを初期化 */\n// 初期値なし\nlet nums1: Vec<i32> = Vec::new();\n// 初期値あり\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n</code></pre> list.c<pre><code>// Cは組み込みの動的配列を提供していません\n</code></pre> list.kt<pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2","level":3,"title":"2. 要素へのアクセス","text":"<p>リストは本質的に配列であるため、\\(O(1)\\)時間で要素にアクセスし更新することができ、非常に効率的です。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin list.py<pre><code># 要素にアクセス\nnum: int = nums[1] # インデックス1の要素にアクセス\n\n# 要素を更新\nnums[1] = 0 # インデックス1の要素を0に更新\n</code></pre> list.cpp<pre><code>/* 要素にアクセス */\nint num = nums[1]; // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0; // インデックス1の要素を0に更新\n</code></pre> list.java<pre><code>/* 要素にアクセス */\nint num = nums.get(1); // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums.set(1, 0); // インデックス1の要素を0に更新\n</code></pre> list.cs<pre><code>/* 要素にアクセス */\nint num = nums[1]; // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0; // インデックス1の要素を0に更新\n</code></pre> list_test.go<pre><code>/* 要素にアクセス */\nnum := nums[1] // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0 // インデックス1の要素を0に更新\n</code></pre> list.swift<pre><code>/* 要素にアクセス */\nlet num = nums[1] // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0 // インデックス1の要素を0に更新\n</code></pre> list.js<pre><code>/* 要素にアクセス */\nconst num = nums[1]; // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0; // インデックス1の要素を0に更新\n</code></pre> list.ts<pre><code>/* 要素にアクセス */\nconst num: number = nums[1]; // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0; // インデックス1の要素を0に更新\n</code></pre> list.dart<pre><code>/* 要素にアクセス */\nint num = nums[1]; // インデックス1の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0; // インデックス1の要素を0に更新\n</code></pre> list.rs<pre><code>/* 要素にアクセス */\nlet num: i32 = nums[1]; // インデックス1の要素にアクセス\n/* 要素を更新 */\nnums[1] = 0; // インデックス1の要素を0に更新\n</code></pre> list.c<pre><code>// Cは組み込みの動的配列を提供していません\n</code></pre> list.kt<pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3","level":3,"title":"3. 要素の挿入と削除","text":"<p>配列と比較して、リストは要素の追加と削除においてより柔軟性を提供します。リストの末尾への要素追加は\\(O(1)\\)操作ですが、リストの他の場所での要素の挿入と削除の効率は配列と同じままで、時間計算量は\\(O(n)\\)です。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin list.py<pre><code># リストをクリア\nnums.clear()\n\n# 末尾に要素を追加\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# 中間に要素を挿入\nnums.insert(3, 6) # インデックス3に数値6を挿入\n\n# 要素を削除\nnums.pop(3) # インデックス3の要素を削除\n</code></pre> list.cpp<pre><code>/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* 中間に要素を挿入 */\nnums.insert(nums.begin() + 3, 6); // インデックス3に数値6を挿入\n\n/* 要素を削除 */\nnums.erase(nums.begin() + 3); // インデックス3の要素を削除\n</code></pre> list.java<pre><code>/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 中間に要素を挿入 */\nnums.add(3, 6); // インデックス3に数値6を挿入\n\n/* 要素を削除 */\nnums.remove(3); // インデックス3の要素を削除\n</code></pre> list.cs<pre><code>/* リストをクリア */\nnums.Clear();\n\n/* 末尾に要素を追加 */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* 中間に要素を挿入 */\nnums.Insert(3, 6);\n\n/* 要素を削除 */\nnums.RemoveAt(3);\n</code></pre> list_test.go<pre><code>/* リストをクリア */\nnums = nil\n\n/* 末尾に要素を追加 */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* 中間に要素を挿入 */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス3に数値6を挿入\n\n/* 要素を削除 */\nnums = append(nums[:3], nums[4:]...) // インデックス3の要素を削除\n</code></pre> list.swift<pre><code>/* リストをクリア */\nnums.removeAll()\n\n/* 末尾に要素を追加 */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* 中間に要素を挿入 */\nnums.insert(6, at: 3) // インデックス3に数値6を挿入\n\n/* 要素を削除 */\nnums.remove(at: 3) // インデックス3の要素を削除\n</code></pre> list.js<pre><code>/* リストをクリア */\nnums.length = 0;\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 中間に要素を挿入 */\nnums.splice(3, 0, 6);\n\n/* 要素を削除 */\nnums.splice(3, 1);\n</code></pre> list.ts<pre><code>/* リストをクリア */\nnums.length = 0;\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 中間に要素を挿入 */\nnums.splice(3, 0, 6);\n\n/* 要素を削除 */\nnums.splice(3, 1);\n</code></pre> list.dart<pre><code>/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 中間に要素を挿入 */\nnums.insert(3, 6); // インデックス3に数値6を挿入\n\n/* 要素を削除 */\nnums.removeAt(3); // インデックス3の要素を削除\n</code></pre> list.rs<pre><code>/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 中間に要素を挿入 */\nnums.insert(3, 6); // インデックス3に数値6を挿入\n\n/* 要素を削除 */\nnums.remove(3); // インデックス3の要素を削除\n</code></pre> list.c<pre><code>// Cは組み込みの動的配列を提供していません\n</code></pre> list.kt<pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4","level":3,"title":"4. リストの反復","text":"<p>配列と同様に、リストはインデックスを使用して反復することも、各要素を直接反復することもできます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin list.py<pre><code># インデックスでリストを反復\ncount = 0\nfor i in range(len(nums)):\n count += nums[i]\n\n# リスト要素を直接反復\nfor num in nums:\n count += num\n</code></pre> list.cpp<pre><code>/* インデックスでリストを反復 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n count += nums[i];\n}\n\n/* リスト要素を直接反復 */\ncount = 0;\nfor (int num : nums) {\n count += num;\n}\n</code></pre> list.java<pre><code>/* インデックスでリストを反復 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n count += nums.get(i);\n}\n\n/* リスト要素を直接反復 */\nfor (int num : nums) {\n count += num;\n}\n</code></pre> list.cs<pre><code>/* インデックスでリストを反復 */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n count += nums[i];\n}\n\n/* リスト要素を直接反復 */\ncount = 0;\nforeach (int num in nums) {\n count += num;\n}\n</code></pre> list_test.go<pre><code>/* インデックスでリストを反復 */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n count += nums[i]\n}\n\n/* リスト要素を直接反復 */\ncount = 0\nfor _, num := range nums {\n count += num\n}\n</code></pre> list.swift<pre><code>/* インデックスでリストを反復 */\nvar count = 0\nfor i in nums.indices {\n count += nums[i]\n}\n\n/* リスト要素を直接反復 */\ncount = 0\nfor num in nums {\n count += num\n}\n</code></pre> list.js<pre><code>/* インデックスでリストを反復 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n count += nums[i];\n}\n\n/* リスト要素を直接反復 */\ncount = 0;\nfor (const num of nums) {\n count += num;\n}\n</code></pre> list.ts<pre><code>/* インデックスでリストを反復 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n count += nums[i];\n}\n\n/* リスト要素を直接反復 */\ncount = 0;\nfor (const num of nums) {\n count += num;\n}\n</code></pre> list.dart<pre><code>/* インデックスでリストを反復 */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n count += nums[i];\n}\n\n/* リスト要素を直接反復 */\ncount = 0;\nfor (var num in nums) {\n count += num;\n}\n</code></pre> list.rs<pre><code>// インデックスでリストを反復\nlet mut _count = 0;\nfor i in 0..nums.len() {\n _count += nums[i];\n}\n\n// リスト要素を直接反復\n_count = 0;\nfor num in &nums {\n _count += num;\n}\n</code></pre> list.c<pre><code>// Cは組み込みの動的配列を提供していません\n</code></pre> list.kt<pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5","level":3,"title":"5. リストの連結","text":"<p>新しいリスト<code>nums1</code>が与えられたとき、それを元のリストの末尾に追加できます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin list.py<pre><code># 2つのリストを連結\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1 # nums1をnumsの末尾に連結\n</code></pre> list.cpp<pre><code>/* 2つのリストを連結 */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// nums1をnumsの末尾に連結\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n</code></pre> list.java<pre><code>/* 2つのリストを連結 */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1); // nums1をnumsの末尾に連結\n</code></pre> list.cs<pre><code>/* 2つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1); // nums1をnumsの末尾に連結\n</code></pre> list_test.go<pre><code>/* 2つのリストを連結 */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...) // nums1をnumsの末尾に連結\n</code></pre> list.swift<pre><code>/* 2つのリストを連結 */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // nums1をnumsの末尾に連結\n</code></pre> list.js<pre><code>/* 2つのリストを連結 */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1); // nums1をnumsの末尾に連結\n</code></pre> list.ts<pre><code>/* 2つのリストを連結 */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1); // nums1をnumsの末尾に連結\n</code></pre> list.dart<pre><code>/* 2つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1); // nums1をnumsの末尾に連結\n</code></pre> list.rs<pre><code>/* 2つのリストを連結 */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n</code></pre> list.c<pre><code>// Cは組み込みの動的配列を提供していません\n</code></pre> list.kt<pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6","level":3,"title":"6. リストのソート","text":"<p>リストがソートされると、「二分探索」や「双ポインタ」アルゴリズムなど、配列関連のアルゴリズム問題でよく使用されるアルゴリズムを使用できます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin list.py<pre><code># リストをソート\nnums.sort() # ソート後、リスト要素は昇順になります\n</code></pre> list.cpp<pre><code>/* リストをソート */\nsort(nums.begin(), nums.end()); // ソート後、リスト要素は昇順になります\n</code></pre> list.java<pre><code>/* リストをソート */\nCollections.sort(nums); // ソート後、リスト要素は昇順になります\n</code></pre> list.cs<pre><code>/* リストをソート */\nnums.Sort(); // ソート後、リスト要素は昇順になります\n</code></pre> list_test.go<pre><code>/* リストをソート */\nsort.Ints(nums) // ソート後、リスト要素は昇順になります\n</code></pre> list.swift<pre><code>/* リストをソート */\nnums.sort() // ソート後、リスト要素は昇順になります\n</code></pre> list.js<pre><code>/* リストをソート */\nnums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります\n</code></pre> list.ts<pre><code>/* リストをソート */\nnums.sort((a, b) => a - b); // ソート後、リスト要素は昇順になります\n</code></pre> list.dart<pre><code>/* リストをソート */\nnums.sort(); // ソート後、リスト要素は昇順になります\n</code></pre> list.rs<pre><code>/* リストをソート */\nnums.sort(); // ソート後、リスト要素は昇順になります\n</code></pre> list.c<pre><code>// Cは組み込みの動的配列を提供していません\n</code></pre> list.kt<pre><code>\n</code></pre>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432","level":2,"title":"4.3.2 リストの実装","text":"<p>多くのプログラミング言語には、Java、C++、Pythonなどを含む組み込みリストが付属しています。それらの実装は、初期容量や拡張係数などの様々なパラメータを慎重に考慮した設定で、複雑になりがちです。興味のある読者は、さらなる学習のためにソースコードを調べることができます。</p> <p>リストがどのように動作するかの理解を深めるために、3つの重要な設計側面に焦点を当てて、簡略化されたリストの実装を試みます:</p> <ul> <li>初期容量:配列に合理的な初期容量を選択します。この例では、初期容量として10を選択します。</li> <li>サイズ記録:リスト内の現在の要素数を記録する変数<code>size</code>を宣言し、要素の挿入と削除でリアルタイムに更新します。この変数により、リストの末尾を特定し、拡張が必要かどうかを判断できます。</li> <li>拡張メカニズム:要素挿入時にリストが満杯に達した場合、拡張プロセスが必要です。これには拡張係数に基づいてより大きな配列を作成し、現在の配列からすべての要素を新しい配列に転送することが含まれます。この例では、拡張のたびに配列サイズを2倍にすることを規定します。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py<pre><code>class MyList:\n \"\"\"リストクラス\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n self._capacity: int = 10 # リストの容量\n self._arr: list[int] = [0] * self._capacity # 配列(リスト要素を格納)\n self._size: int = 0 # リストの長さ(現在の要素数)\n self._extend_ratio: int = 2 # 各リスト拡張の倍数\n\n def size(self) -> int:\n \"\"\"リストの長さ(現在の要素数)を取得\"\"\"\n return self._size\n\n def capacity(self) -> int:\n \"\"\"リストの容量を取得\"\"\"\n return self._capacity\n\n def get(self, index: int) -> int:\n \"\"\"要素にアクセス\"\"\"\n # インデックスが範囲外の場合、以下のように例外をスロー\n if index < 0 or index >= self._size:\n raise IndexError(\"Index out of bounds\")\n return self._arr[index]\n\n def set(self, num: int, index: int):\n \"\"\"要素を更新\"\"\"\n if index < 0 or index >= self._size:\n raise IndexError(\"Index out of bounds\")\n self._arr[index] = num\n\n def add(self, num: int):\n \"\"\"末尾に要素を追加\"\"\"\n # 要素数が容量を超える場合、拡張メカニズムをトリガー\n if self.size() == self.capacity():\n self.extend_capacity()\n self._arr[self._size] = num\n self._size += 1\n\n def insert(self, num: int, index: int):\n \"\"\"中間に要素を挿入\"\"\"\n if index < 0 or index >= self._size:\n raise IndexError(\"Index out of bounds\")\n # 要素数が容量を超える場合、拡張メカニズムをトリガー\n if self._size == self.capacity():\n self.extend_capacity()\n # インデックス index より後のすべての要素を1つ後ろに移動\n for j in range(self._size - 1, index - 1, -1):\n self._arr[j + 1] = self._arr[j]\n self._arr[index] = num\n # 要素数を更新\n self._size += 1\n\n def remove(self, index: int) -> int:\n \"\"\"要素を削除\"\"\"\n if index < 0 or index >= self._size:\n raise IndexError(\"Index out of bounds\")\n num = self._arr[index]\n # インデックス index より後のすべての要素を1つ前に移動\n for j in range(index, self._size - 1):\n self._arr[j] = self._arr[j + 1]\n # 要素数を更新\n self._size -= 1\n # 削除された要素を返す\n return num\n\n def extend_capacity(self):\n \"\"\"リストを拡張\"\"\"\n # 元の配列の _extend_ratio 倍の長さの新しい配列を作成し、元の配列を新しい配列にコピー\n self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n # リストの容量を更新\n self._capacity = len(self._arr)\n\n def to_array(self) -> list[int]:\n \"\"\"有効な長さのリストを返す\"\"\"\n return self._arr[: self._size]\n</code></pre> my_list.cpp<pre><code>/* リストクラス */\nclass MyList {\n private:\n int *arr; // 配列(リスト要素を格納)\n int arrCapacity = 10; // リストの容量\n int arrSize = 0; // リストの長さ(現在の要素数)\n int extendRatio = 2; // リスト拡張時の倍率\n\n public:\n /* コンストラクタ */\n MyList() {\n arr = new int[arrCapacity];\n }\n\n /* デストラクタ */\n ~MyList() {\n delete[] arr;\n }\n\n /* リストの長さを取得(現在の要素数)*/\n int size() {\n return arrSize;\n }\n\n /* リストの容量を取得 */\n int capacity() {\n return arrCapacity;\n }\n\n /* 要素にアクセス */\n int get(int index) {\n // インデックスが範囲外の場合、例外をスロー(以下同様)\n if (index < 0 || index >= size())\n throw out_of_range(\"Index out of bounds\");\n return arr[index];\n }\n\n /* 要素を更新 */\n void set(int index, int num) {\n if (index < 0 || index >= size())\n throw out_of_range(\"Index out of bounds\");\n arr[index] = num;\n }\n\n /* 末尾に要素を追加 */\n void add(int num) {\n // 要素数が容量を超えた場合、拡張メカニズムをトリガー\n if (size() == capacity())\n extendCapacity();\n arr[size()] = num;\n // 要素数を更新\n arrSize++;\n }\n\n /* 中間に要素を挿入 */\n void insert(int index, int num) {\n if (index < 0 || index >= size())\n throw out_of_range(\"Index out of bounds\");\n // 要素数が容量を超えた場合、拡張メカニズムをトリガー\n if (size() == capacity())\n extendCapacity();\n // `index`より後のすべての要素を1つ後ろに移動\n for (int j = size() - 1; j >= index; j--) {\n arr[j + 1] = arr[j];\n }\n arr[index] = num;\n // 要素数を更新\n arrSize++;\n }\n\n /* 要素を削除 */\n int remove(int index) {\n if (index < 0 || index >= size())\n throw out_of_range(\"Index out of bounds\");\n int num = arr[index];\n // `index`より後のすべての要素を1つ前に移動\n for (int j = index; j < size() - 1; j++) {\n arr[j] = arr[j + 1];\n }\n // 要素数を更新\n arrSize--;\n // 削除された要素を返却\n return num;\n }\n\n /* リストを拡張 */\n void extendCapacity() {\n // 元の配列のextendRatio倍の長さで新しい配列を作成\n int newCapacity = capacity() * extendRatio;\n int *tmp = arr;\n arr = new int[newCapacity];\n // 元の配列のすべての要素を新しい配列にコピー\n for (int i = 0; i < size(); i++) {\n arr[i] = tmp[i];\n }\n // メモリを解放\n delete[] tmp;\n arrCapacity = newCapacity;\n }\n\n /* リストをVectorに変換して印刷用に使用 */\n vector<int> toVector() {\n // 有効な長さ範囲内の要素のみを変換\n vector<int> vec(size());\n for (int i = 0; i < size(); i++) {\n vec[i] = arr[i];\n }\n return vec;\n }\n};\n</code></pre> my_list.java<pre><code>/* リストクラス */\nclass MyList {\n private int[] arr; // 配列(リスト要素を格納)\n private int capacity = 10; // リスト容量\n private int size = 0; // リスト長(現在の要素数)\n private int extendRatio = 2; // リストの各拡張倍率\n\n /* コンストラクタ */\n public MyList() {\n arr = new int[capacity];\n }\n\n /* リスト長を取得(現在の要素数) */\n public int size() {\n return size;\n }\n\n /* リスト容量を取得 */\n public int capacity() {\n return capacity;\n }\n\n /* 要素へのアクセス */\n public int get(int index) {\n // インデックスが範囲外の場合、以下のように例外をスロー\n if (index < 0 || index >= size)\n throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n return arr[index];\n }\n\n /* 要素の更新 */\n public void set(int index, int num) {\n if (index < 0 || index >= size)\n throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n arr[index] = num;\n }\n\n /* 末尾に要素を追加 */\n public void add(int num) {\n // 要素数が容量を超える場合、拡張メカニズムを実行\n if (size == capacity())\n extendCapacity();\n arr[size] = num;\n // 要素数を更新\n size++;\n }\n\n /* 中間に要素を挿入 */\n public void insert(int index, int num) {\n if (index < 0 || index >= size)\n throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n // 要素数が容量を超える場合、拡張メカニズムを実行\n if (size == capacity())\n extendCapacity();\n // `index` より後のすべての要素を1つ後ろに移動\n for (int j = size - 1; j >= index; j--) {\n arr[j + 1] = arr[j];\n }\n arr[index] = num;\n // 要素数を更新\n size++;\n }\n\n /* 要素の削除 */\n public int remove(int index) {\n if (index < 0 || index >= size)\n throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n int num = arr[index];\n // `index` より後のすべての要素を1つ前に移動\n for (int j = index; j < size - 1; j++) {\n arr[j] = arr[j + 1];\n }\n // 要素数を更新\n size--;\n // 削除された要素を返す\n return num;\n }\n\n /* リストを拡張 */\n public void extendCapacity() {\n // 元の配列の長さを extendRatio 倍した新しい配列を作成し、元の配列を新しい配列にコピー\n arr = Arrays.copyOf(arr, capacity() * extendRatio);\n // リスト容量を更新\n capacity = arr.length;\n }\n\n /* リストを配列に変換 */\n public int[] toArray() {\n int size = size();\n // 有効な長さ範囲内の要素のみを変換\n int[] arr = new int[size];\n for (int i = 0; i < size; i++) {\n arr[i] = get(i);\n }\n return arr;\n }\n}\n</code></pre> my_list.cs<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.go<pre><code>[class]{myList}-[func]{}\n</code></pre> my_list.swift<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.js<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.ts<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.dart<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.rs<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.c<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.kt<pre><code>[class]{MyList}-[func]{}\n</code></pre> my_list.rb<pre><code>[class]{MyList}-[func]{}\n</code></pre>","path":["第 4 章 配列と連結リスト","4.3 リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4 メモリとキャッシュ *","text":"<p>この章の最初の2つのセクションでは、「連続格納」と「分散格納」をそれぞれ表現する2つの基本的なデータ構造である配列と連結リストを探究しました。</p> <p>実際、物理構造はプログラムがメモリとキャッシュをどの程度効率的に利用するかを大きく決定し、これがアルゴリズムの全体的なパフォーマンスに影響を与えます。</p>","path":["第 4 章 配列と連結リスト","4.4 メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441","level":2,"title":"4.4.1 コンピュータ記憶装置","text":"<p>コンピュータには3種類の記憶装置があります:ハードディスク、ランダムアクセスメモリ(RAM)、およびキャッシュメモリです。以下の表は、コンピュータシステムにおけるそれぞれの役割とパフォーマンス特性を示しています。</p> <p> 表 4-2 コンピュータ記憶装置 </p> ハードディスク メモリ キャッシュ 用途 OS、プログラム、ファイルなどのデータの長期保存 現在実行中のプログラムと処理中のデータの一時保存 頻繁にアクセスされるデータと命令を保存し、CPUのメモリへのアクセス数を削減 揮発性 電源オフ後もデータは失われない 電源オフ後にデータは失われる 電源オフ後にデータは失われる 容量 より大きい、TBレベル より小さい、GBレベル 非常に小さい、MBレベル 速度 より遅い、数百から数千MB/s より高速、数十GB/s 非常に高速、数十から数百GB/s 価格(USD) より安価、数セント/GB より高価、数ドル/GB 非常に高価、CPUと一緒に価格設定 <p>コンピュータ記憶システムは、下図に示すようにピラミッドとして視覚化できます。ピラミッドの上部にある記憶装置ほど高速で、容量が小さく、より高価です。このマルチレベル設計は偶然ではなく、コンピュータ科学者とエンジニアによる慎重な検討の結果です。</p> <ul> <li>ハードディスクをメモリに置き換えるのは困難です。第一に、メモリ内のデータは電源オフ後に失われるため、長期データ保存には適していません。第二に、メモリはハードディスクよりも大幅に高価で、消費者市場での広範囲な使用の実現可能性を制限しています。</li> <li>キャッシュは大容量と高速のトレードオフに直面しています。L1、L2、L3キャッシュの容量が増加するにつれて、その物理サイズが大きくなり、CPUコアからの距離が増加します。これによりデータ転送時間が長くなり、アクセス遅延が高くなります。現在の技術では、マルチレベルキャッシュ構造が容量、速度、コストの間の最適なバランスを提供します。</li> </ul> <p></p> <p> 図 4-9 コンピュータ記憶システム </p> <p>Tip</p> <p>コンピュータの記憶階層は、速度、容量、コストの間の慎重なバランスを反映しています。このタイプのトレードオフは様々な業界で一般的であり、利益と制限の間の最適なバランスを見つけることが重要です。</p> <p>全体的に、ハードディスクは大量のデータの長期保存を提供し、メモリはプログラム実行中に処理されるデータの一時保存として機能し、キャッシュは頻繁にアクセスされるデータと命令を保存して実行効率を向上させます。それらは一緒になってコンピュータシステムの効率的な動作を保証します。</p> <p>下図に示すように、プログラム実行中、データはハードディスクからメモリに読み込まれ、CPU計算が行われます。CPUの拡張として機能するキャッシュは、**メモリからインテリジェントにデータを先読み**し、CPUのより高速なデータアクセスを可能にします。これによりプログラム実行効率が大幅に向上し、低速なメモリへの依存が減少します。</p> <p></p> <p> 図 4-10 ハードディスク、メモリ、キャッシュ間のデータフロー </p>","path":["第 4 章 配列と連結リスト","4.4 メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442","level":2,"title":"4.4.2 データ構造のメモリ効率","text":"<p>メモリ空間利用の観点から、配列と連結リストにはそれぞれ利点と制限があります。</p> <p>一方で、**メモリは限られており、複数のプログラム間で共有できない**ため、データ構造での空間使用の最適化は重要です。配列は要素が密接にパックされており、連結リストのように参照(ポインタ)のための追加メモリを必要としないため、空間効率的です。しかし、配列は連続したメモリブロックを事前に割り当てる必要があり、割り当てられた空間が実際の必要量を超える場合、無駄につながる可能性があります。配列の拡張も追加の時間と空間のオーバーヘッドを伴います。対照的に、連結リストは各ノードに対してメモリを動的に割り当て・解放し、ポインタのための追加メモリのコストでより大きな柔軟性を提供します。</p> <p>一方で、プログラム実行中、繰り返されるメモリの割り当てと解放はメモリの断片化を増加させ、メモリ利用効率を低下させます。配列は連続記憶方式により、メモリ断片化を引き起こす可能性が比較的低いです。対照的に、連結リストは要素を非連続の場所に保存し、頻繁な挿入と削除はメモリ断片化を悪化させる可能性があります。</p>","path":["第 4 章 配列と連結リスト","4.4 メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443","level":2,"title":"4.4.3 データ構造のキャッシュ効率","text":"<p>キャッシュはメモリよりも空間容量がはるかに小さいですが、はるかに高速で、プログラム実行速度において重要な役割を果たします。限られた容量のため、キャッシュは頻繁にアクセスされるデータのサブセットのみを保存できます。CPUがキャッシュに存在しないデータにアクセスしようとすると、キャッシュミスが発生し、CPUは低速なメモリから必要なデータを取得する必要があり、パフォーマンスに影響を与える可能性があります。</p> <p>明らかに、キャッシュミスが少ないほど、CPUのデータ読み書き効率が高く、プログラムパフォーマンスが向上します。CPUがキャッシュからデータを正常に取得する割合はキャッシュヒット率と呼ばれ、キャッシュ効率を測定するためによく使用される指標です。</p> <p>より高い効率を達成するために、キャッシュは以下のデータロードメカニズムを採用します。</p> <ul> <li>キャッシュライン:キャッシュは個々のバイトではなく、キャッシュラインと呼ばれる単位でデータを保存・ロードして動作します。このアプローチは、一度により大きなデータブロックを転送することで効率を向上させます。</li> <li>先読みメカニズム:プロセッサはデータアクセスパターン(例:連続または固定ストライドアクセス)を予測し、これらのパターンに基づいてデータをキャッシュに先読みして、キャッシュヒット率を向上させます。</li> <li>空間的局所性:特定のデータがアクセスされると、近くのデータもまもなくアクセスされる可能性があります。これを活用するために、キャッシュは要求されたデータと一緒に隣接するデータをロードし、ヒット率を向上させます。</li> <li>時間的局所性:データがアクセスされた場合、近い将来に再びアクセスされる可能性があります。キャッシュはこの原理を使用して、最近アクセスされたデータを保持してヒット率を向上させます。</li> </ul> <p>実際、配列と連結リストは異なるキャッシュ利用効率を持ち、これは主に以下の側面に反映されます。</p> <ul> <li>占有空間:連結リスト要素は配列要素よりも多くの空間を占有するため、キャッシュに保持される有効データが少なくなります。</li> <li>キャッシュライン:連結リストデータはメモリ全体に散在し、キャッシュは「行単位でロード」されるため、ロードされる無効データの割合が高くなります。</li> <li>先読みメカニズム:配列のデータアクセスパターンは連結リストよりも「予測可能」で、つまりシステムがこれからロードされるデータを推測しやすいです。</li> <li>空間的局所性:配列は連続したメモリ空間に保存されるため、ロードされているデータの近くのデータがまもなくアクセスされる可能性が高くなります。</li> </ul> <p>全体的に、配列はより高いキャッシュヒット率を持ち、一般的に連結リストよりも操作効率が高いです。これにより、配列に基づくデータ構造はアルゴリズム問題の解決において人気があります。</p> <p>**高いキャッシュ効率が配列が常に連結リストより優れているという意味ではない**ことに注意すべきです。データ構造の選択は特定のアプリケーション要件に依存すべきです。例えば、配列と連結リストの両方が「スタック」データ構造を実装できますが(次章で詳細説明)、それらは異なるシナリオに適しています。</p> <ul> <li>アルゴリズム問題では、より高い操作効率とランダムアクセス機能を提供するため、配列に基づくスタックを選択する傾向があります。唯一のコストは配列に対して一定量のメモリ空間を事前に割り当てる必要があることです。</li> <li>データ量が非常に大きく、高度に動的で、スタックの予想サイズを推定するのが困難な場合、連結リストに基づくスタックがより良い選択です。連結リストは大量のデータをメモリの異なる部分に分散でき、配列拡張の追加オーバーヘッドを回避できます。</li> </ul>","path":["第 4 章 配列と連結リスト","4.4 メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5 まとめ","text":"","path":["第 4 章 配列と連結リスト","4.5 まとめ"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1","level":3,"title":"1. 重要な復習","text":"<ul> <li>配列と連結リストは2つの基本的なデータ構造であり、コンピュータメモリにおける2つの格納方法を表しています:連続空間格納と非連続空間格納です。それらの特性は互いに補完し合います。</li> <li>配列はランダムアクセスをサポートし、使用するメモリが少ない一方で、要素の挿入と削除は非効率的で、初期化後の長さが固定されています。</li> <li>連結リストは参照(ポインタ)の変更によって効率的なノードの挿入と削除を実装し、長さを柔軟に調整できますが、ノードアクセス効率が低く、より多くのメモリを消費します。</li> <li>連結リストの一般的な種類には、単方向連結リスト、循環連結リスト、双方向連結リストがあり、それぞれに独自の応用シナリオがあります。</li> <li>リストは要素の順序付けられたコレクションで、追加、削除、変更をサポートし、通常は動的配列に基づいて実装され、配列の利点を保持しながら柔軟な長さ調整を可能にします。</li> <li>リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります。</li> <li>プログラム実行中、データは主にメモリに格納されます。配列はより高いメモリ空間効率を提供し、連結リストはメモリ使用においてより柔軟です。</li> <li>キャッシュは、キャッシュライン、先読み、空間的局所性、時間的局所性などのメカニズムを通じてCPUに高速データアクセスを提供し、プログラム実行効率を大幅に向上させます。</li> <li>より高いキャッシュヒット率により、配列は一般的に連結リストよりも効率的です。データ構造を選択する際は、特定のニーズとシナリオに基づいて適切な選択をすべきです。</li> </ul>","path":["第 4 章 配列と連結リスト","4.5 まとめ"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q:配列をスタックに格納するかヒープに格納するかは、時間と空間効率に影響しますか?</p> <p>スタックとヒープの両方に格納される配列は連続したメモリ空間に格納され、データ操作効率は本質的に同じです。しかし、スタックとヒープには独自の特性があり、以下の違いが生じます。</p> <ol> <li>割り当てと解放効率:スタックはより小さなメモリブロックで、コンパイラによって自動的に割り当てられます。ヒープメモリは比較的大きく、コードで動的に割り当てることができ、断片化しやすいです。したがって、ヒープでの割り当てと解放操作は一般的にスタックよりも遅くなります。</li> <li>サイズ制限:スタックメモリは比較的小さく、ヒープサイズは一般的に利用可能なメモリによって制限されます。したがって、ヒープは大きな配列の格納により適しています。</li> <li>柔軟性:スタック上の配列のサイズはコンパイル時に決定される必要がありますが、ヒープ上の配列のサイズは実行時に動的に決定できます。</li> </ol> <p>Q:なぜ配列は同じ型の要素を必要とし、連結リストは同じ型の要素を強調しないのですか?</p> <p>連結リストは参照(ポインタ)によって接続されたノードで構成され、各ノードはint、double、string、objectなど、異なる型のデータを格納できます。</p> <p>対照的に、配列要素は同じ型である必要があり、これにより対応する要素位置にアクセスするためのオフセットを計算できます。例えば、intとlong型の両方を含む配列で、単一要素がそれぞれ4バイトと8バイトを占有する場合、配列に2つの異なる長さの要素が含まれているため、以下の式を使用してオフセットを計算できません。</p> <pre><code># 要素メモリアドレス = 配列メモリアドレス + 要素長 * 要素インデックス\n</code></pre> <p>Q:ノードを削除した後、<code>P.next</code>を<code>None</code>に設定する必要がありますか?</p> <p><code>P.next</code>を変更しなくても問題ありません。連結リストの観点から、ヘッドノードからテールノードまでの巡回で<code>P</code>に遭遇することはもうありません。これは、ノード<code>P</code>がリストから効果的に削除されたことを意味し、<code>P</code>が指す場所はもはやリストに影響しません。</p> <p>ガベージコレクションの観点から、Java、Python、Goなどの自動ガベージコレクションメカニズムを持つ言語では、ノード<code>P</code>が収集されるかどうかは、それを指す参照がまだあるかどうかに依存し、<code>P.next</code>の値には依存しません。CやC++などの言語では、ノードのメモリを手動で解放する必要があります。</p> <p>Q:連結リストでは、挿入と削除操作の時間計算量は<code>O(1)</code>です。しかし、挿入や削除前の要素検索には<code>O(n)</code>時間がかかるので、なぜ時間計算量は<code>O(n)</code>ではないのですか?</p> <p>要素を最初に検索してから削除する場合、時間計算量は確かに<code>O(n)</code>です。しかし、連結リストの挿入と削除における<code>O(1)</code>の利点は他のアプリケーションで実現できます。例えば、連結リストを使用した両端キューの実装では、常にヘッドとテールノードを指すポインタを維持し、各挿入と削除操作を<code>O(1)</code>にします。</p> <p>Q:「連結リストの定義と格納方法」の図で、薄青色の格納ノードは単一のメモリアドレスを占有しますか、それともノード値と半分を共有しますか?</p> <p>図は単なる定性的な表現であり、定量的分析は特定の状況に依存します。</p> <ul> <li>異なる型のノード値は異なる量の空間を占有します。例えば、int、long、double、オブジェクトインスタンスです。</li> <li>ポインタ変数によって占有されるメモリ空間は、使用されるオペレーティングシステムとコンパイル環境に依存し、通常8バイトまたは4バイトです。</li> </ul> <p>Q:リストの末尾への要素追加は常に<code>O(1)</code>ですか?</p> <p>要素を追加することでリスト長を超える場合、リストは最初に拡張される必要があります。システムは新しいメモリブロックを要求し、元のリストのすべての要素を移動するため、この場合の時間計算量は<code>O(n)</code>になります。</p> <p>Q:「リストの出現により配列の実用性が大幅に向上しましたが、一部のメモリ空間の無駄につながる可能性があります」という文は、容量、長さ、拡張係数などの追加変数によって占有されるメモリを指していますか?</p> <p>ここでの空間の無駄は主に2つの側面を指します:一方で、リストは初期長で設定されますが、常に必要とは限りません。他方で、頻繁な拡張を防ぐため、拡張は通常\\(\\times 1.5\\)などの係数で乗算されます。これにより多くの空きスロットが生まれ、通常は完全に埋めることができません。</p> <p>Q:Pythonで<code>n = [1, 2, 3]</code>を初期化した後、これら3つの要素のアドレスは連続していますが、<code>m = [2, 1, 3]</code>を初期化すると、各要素の<code>id</code>は連続していないが<code>n</code>のものと同一です。これらの要素のアドレスが連続していない場合、<code>m</code>はまだ配列ですか?</p> <p>リスト要素を連結リストノード<code>n = [n1, n2, n3, n4, n5]</code>に置き換える場合、これら5つのノードオブジェクトも通常メモリ全体に分散しています。しかし、リストインデックスが与えられれば、<code>O(1)</code>時間でノードのメモリアドレスにアクセスでき、対応するノードにアクセスできます。これは、配列がノード自体ではなく、ノードへの参照を格納するためです。</p> <p>多くの言語とは異なり、Pythonでは数値もオブジェクトとしてラップされ、リストは数値自体ではなく、これらの数値への参照を格納します。したがって、2つの配列の同じ数値が同じ<code>id</code>を持ち、これらの数値のメモリアドレスは連続である必要がないことがわかります。</p> <p>Q:C++ STLの<code>std::list</code>はすでに双方向連結リストを実装していますが、一部のアルゴリズム書籍では直接使用していないようです。何か制限がありますか?</p> <p>一方で、アルゴリズムを実装する際は配列を使用することを好み、必要な場合のみ連結リストを使用します。主に2つの理由があります。</p> <ul> <li>空間オーバーヘッド:各要素に2つの追加ポインタ(前の要素用と次の要素用)が必要なため、<code>std::list</code>は通常<code>std::vector</code>よりも多くの空間を占有します。</li> <li>キャッシュ非友好的:データが連続して格納されていないため、<code>std::list</code>はキャッシュ利用率が低くなります。一般的に、<code>std::vector</code>の方がパフォーマンスが優れています。</li> </ul> <p>他方で、連結リストは主に二分木とグラフに必要です。スタックとキューは、連結リストではなく、プログラミング言語の<code>stack</code>と<code>queue</code>クラスを使用して実装されることが多いです。</p> <p>Q:リスト<code>res = [0] * self.size()</code>を初期化すると、<code>res</code>の各要素は同じアドレスを参照しますか?</p> <p>いいえ。しかし、この問題は二次元配列で発生します。例えば、二次元リスト<code>res = [[0]] * self.size()</code>を初期化すると、同じリスト<code>[0]</code>を複数回参照することになります。</p> <p>Q:ノードを削除する際、その後続ノードへの参照を断つ必要がありますか?</p> <p>データ構造とアルゴリズム(問題解決)の観点から、プログラムのロジックが正しい限り、リンクを断たなくても問題ありません。標準ライブラリの観点から、リンクを断つ方が安全で論理的に明確です。リンクを断たず、削除されたノードが適切にリサイクルされない場合、後続ノードのメモリのリサイクルに影響を与える可能性があります。</p>","path":["第 4 章 配列と連結リスト","4.5 まとめ"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"第 13 章 バックトラッキング","text":"<p>Abstract</p> <p>迷路の探検家のように、私たちは前進する道で障害に遭遇することがあります。</p> <p>バックトラッキングの力は、私たちに新しく始めること、試し続けること、そして最終的に光への出口を見つけることを可能にします。</p>","path":["第 13 章 バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"章の内容","text":"<ul> <li>13.1 バックトラッキングアルゴリズム</li> <li>13.2 全順列問題</li> <li>13.3 部分集合和問題</li> <li>13.4 Nクイーン問題</li> <li>13.5 まとめ</li> </ul>","path":["第 13 章 バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1 バックトラッキングアルゴリズム","text":"<p>バックトラッキングアルゴリズムは全数探索によって問題を解決する方法です。その核心概念は、初期状態から開始してすべての可能な解を総当たりで探索することです。アルゴリズムは正しいものを記録し、解が見つかるか、すべての可能な解が試されたが解が見つからないまで続けます。</p> <p>バックトラッキングは通常「深さ優先探索」を使用して解空間を走査します。「二分木」の章で、前順、中順、後順走査はすべて深さ優先探索であることを述べました。次に、前順走査を使用してバックトラッキング問題を解決し、アルゴリズムの動作を段階的に理解していきます。</p> <p>例1</p> <p>二分木が与えられた場合、値が \\(7\\) のすべてのノードを検索して記録し、リストで返してください。</p> <p>この問題を解決するために、この木を前順で走査し、現在のノードの値が \\(7\\) かどうかを確認します。そうであれば、ノードの値を結果リスト <code>res</code> に追加します。プロセスは以下の図に示されています:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py<pre><code>def pre_order(root: TreeNode):\n \"\"\"前順走査:例一\"\"\"\n if root is None:\n return\n if root.val == 7:\n # 解を記録\n res.append(root)\n pre_order(root.left)\n pre_order(root.right)\n</code></pre> preorder_traversal_i_compact.cpp<pre><code>/* 前順走査:例1 */\nvoid preOrder(TreeNode *root) {\n if (root == nullptr) {\n return;\n }\n if (root->val == 7) {\n // 解を記録\n res.push_back(root);\n }\n preOrder(root->left);\n preOrder(root->right);\n}\n</code></pre> preorder_traversal_i_compact.java<pre><code>/* 前順走査:例1 */\nvoid preOrder(TreeNode root) {\n if (root == null) {\n return;\n }\n if (root.val == 7) {\n // 解を記録\n res.add(root);\n }\n preOrder(root.left);\n preOrder(root.right);\n}\n</code></pre> preorder_traversal_i_compact.cs<pre><code>[class]{preorder_traversal_i_compact}-[func]{PreOrder}\n</code></pre> preorder_traversal_i_compact.go<pre><code>[class]{}-[func]{preOrderI}\n</code></pre> preorder_traversal_i_compact.swift<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_i_compact.js<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_i_compact.ts<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_i_compact.dart<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_i_compact.rs<pre><code>[class]{}-[func]{pre_order}\n</code></pre> preorder_traversal_i_compact.c<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_i_compact.kt<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_i_compact.rb<pre><code>[class]{}-[func]{pre_order}\n</code></pre> <p></p> <p> 図 13-1 前順走査でのノード検索 </p>","path":["第 13 章 バックトラッキング","13.1 バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311","level":2,"title":"13.1.1 試行と後退","text":"<p>解空間を探索する際に「試行」と「後退」戦略を使用するため、バックトラッキングアルゴリズムと呼ばれます。探索中、満足のいく解を得るためにもはや進めない状態に遭遇するたびに、前の選択を取り消して前の状態に戻り、次の試行のために他の可能な選択を選択できるようにします。</p> <p>例1では、各ノードの訪問が「試行」を開始します。そして葉ノードを通過するか、<code>return</code> 文で親ノードに戻ることが「後退」を示唆します。</p> <p>後退は単に関数の戻り値ではないことに注意してください。例1の問題を少し拡張して、それが何を意味するかを説明します。</p> <p>例2</p> <p>二分木で、値が \\(7\\) のすべてのノードを検索し、すべてのマッチングノードについて、ルートノードからそのノードまでのパスを返してください。</p> <p>例1のコードに基づいて、訪問したノードパスを記録するために <code>path</code> というリストを使用する必要があります。値が \\(7\\) のノードに到達すると、<code>path</code> をコピーして結果リスト <code>res</code> に追加します。走査後、<code>res</code> にはすべての解が保持されます。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py<pre><code>def pre_order(root: TreeNode):\n \"\"\"前順走査:例二\"\"\"\n if root is None:\n return\n # 試行\n path.append(root)\n if root.val == 7:\n # 解を記録\n res.append(list(path))\n pre_order(root.left)\n pre_order(root.right)\n # 撤回\n path.pop()\n</code></pre> preorder_traversal_ii_compact.cpp<pre><code>/* 前順走査:例2 */\nvoid preOrder(TreeNode *root) {\n if (root == nullptr) {\n return;\n }\n // 試行\n path.push_back(root);\n if (root->val == 7) {\n // 解を記録\n res.push_back(path);\n }\n preOrder(root->left);\n preOrder(root->right);\n // 回退\n path.pop_back();\n}\n</code></pre> preorder_traversal_ii_compact.java<pre><code>/* 前順走査:例2 */\nvoid preOrder(TreeNode root) {\n if (root == null) {\n return;\n }\n // 試行\n path.add(root);\n if (root.val == 7) {\n // 解を記録\n res.add(new ArrayList<>(path));\n }\n preOrder(root.left);\n preOrder(root.right);\n // 回退\n path.remove(path.size() - 1);\n}\n</code></pre> preorder_traversal_ii_compact.cs<pre><code>[class]{preorder_traversal_ii_compact}-[func]{PreOrder}\n</code></pre> preorder_traversal_ii_compact.go<pre><code>[class]{}-[func]{preOrderII}\n</code></pre> preorder_traversal_ii_compact.swift<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_ii_compact.js<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_ii_compact.ts<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_ii_compact.dart<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_ii_compact.rs<pre><code>[class]{}-[func]{pre_order}\n</code></pre> preorder_traversal_ii_compact.c<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_ii_compact.kt<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_ii_compact.rb<pre><code>[class]{}-[func]{pre_order}\n</code></pre> <p>各「試行」で、現在のノードを <code>path</code> に追加することでパスを記録します。「後退」が必要なときはいつでも、<code>path</code> からノードをポップして**この失敗した試行前の状態を復元します**。</p> <p>以下の図に示すプロセスを観察することで、試行は「前進」のようで、後退は「元に戻す」のようです。後者のペアは、対応するものに対する逆操作と見なすことができます。</p> <1><2><3><4><5><6><7><8><9><10><11> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 13-2 試行と後退 </p>","path":["第 13 章 バックトラッキング","13.1 バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312","level":2,"title":"13.1.2 剪定","text":"<p>複雑なバックトラッキング問題は通常1つ以上の制約を含み、これらは「剪定」によく使用されます。</p> <p>例3</p> <p>二分木で、値が \\(7\\) のすべてのノードを検索し、ルートからこれらのノードまでのパスを返してください。ただし、パスには値が \\(3\\) のノードを含まないという制限があります。</p> <p>上記の制約を満たすために、剪定操作を追加する必要があります:検索プロセス中に、値が \\(3\\) のノードに遭遇した場合、そのパスを通じてさらに検索することを即座に中止します。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py<pre><code>def pre_order(root: TreeNode):\n \"\"\"前順走査:例三\"\"\"\n # 枝刈り\n if root is None or root.val == 3:\n return\n # 試行\n path.append(root)\n if root.val == 7:\n # 解を記録\n res.append(list(path))\n pre_order(root.left)\n pre_order(root.right)\n # 撤回\n path.pop()\n</code></pre> preorder_traversal_iii_compact.cpp<pre><code>/* 前順走査:例3 */\nvoid preOrder(TreeNode *root) {\n // 剪定\n if (root == nullptr || root->val == 3) {\n return;\n }\n // 試行\n path.push_back(root);\n if (root->val == 7) {\n // 解を記録\n res.push_back(path);\n }\n preOrder(root->left);\n preOrder(root->right);\n // 回退\n path.pop_back();\n}\n</code></pre> preorder_traversal_iii_compact.java<pre><code>/* 前順走査:例3 */\nvoid preOrder(TreeNode root) {\n // 剪定\n if (root == null || root.val == 3) {\n return;\n }\n // 試行\n path.add(root);\n if (root.val == 7) {\n // 解を記録\n res.add(new ArrayList<>(path));\n }\n preOrder(root.left);\n preOrder(root.right);\n // 回退\n path.remove(path.size() - 1);\n}\n</code></pre> preorder_traversal_iii_compact.cs<pre><code>[class]{preorder_traversal_iii_compact}-[func]{PreOrder}\n</code></pre> preorder_traversal_iii_compact.go<pre><code>[class]{}-[func]{preOrderIII}\n</code></pre> preorder_traversal_iii_compact.swift<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_iii_compact.js<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_iii_compact.ts<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_iii_compact.dart<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_iii_compact.rs<pre><code>[class]{}-[func]{pre_order}\n</code></pre> preorder_traversal_iii_compact.c<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_iii_compact.kt<pre><code>[class]{}-[func]{preOrder}\n</code></pre> preorder_traversal_iii_compact.rb<pre><code>[class]{}-[func]{pre_order}\n</code></pre> <p>「剪定」は非常に生き生きとした名詞です。以下の図に示すように、検索プロセスで、制約を満たさない検索分岐を「切り取り」ます。さらなる不要な試行を避け、検索効率を向上させます。</p> <p></p> <p> 図 13-3 制約に基づく剪定 </p>","path":["第 13 章 バックトラッキング","13.1 バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313","level":2,"title":"13.1.3 フレームワークコード","text":"<p>今度は、バックトラッキングから「試行、後退、剪定」の主要なフレームワークを抽出して、コードの汎用性を向上させてみましょう。</p> <p>以下のフレームワークコードでは、<code>state</code> は問題の現在の状態を表し、<code>choices</code> は現在の状態で利用可能な選択肢を表します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>def backtrack(state: State, choices: list[choice], res: list[state]):\n \"\"\"バックトラッキングアルゴリズムフレームワーク\"\"\"\n # 解かどうかを確認\n if is_solution(state):\n # 解を記録\n record_solution(state, res)\n # 検索を停止\n return\n # すべての選択肢を反復\n for choice in choices:\n # 剪定:選択肢が有効かどうかを確認\n if is_valid(state, choice):\n # 試行:選択を行い、状態を更新\n make_choice(state, choice)\n backtrack(state, choices, res)\n # 後退:選択を取り消し、前の状態に戻す\n undo_choice(state, choice)\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n // 解かどうかを確認\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n for (Choice choice : choices) {\n // 剪定:選択肢が有効かどうかを確認\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n // 解かどうかを確認\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n for (Choice choice : choices) {\n // 剪定:選択肢が有効かどうかを確認\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n // 解かどうかを確認\n if (IsSolution(state)) {\n // 解を記録\n RecordSolution(state, res);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n foreach (Choice choice in choices) {\n // 剪定:選択肢が有効かどうかを確認\n if (IsValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n MakeChoice(state, choice);\n Backtrack(state, choices, res);\n // 後退:選択を取り消し、前の状態に戻す\n UndoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n // 解かどうかを確認\n if isSolution(state) {\n // 解を記録\n recordSolution(state, res)\n // 検索を停止\n return\n }\n // すべての選択肢を反復\n for _, choice := range choices {\n // 剪定:選択肢が有効かどうかを確認\n if isValid(state, choice) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice)\n backtrack(state, choices, res)\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, choice)\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n // 解かどうかを確認\n if isSolution(state: state) {\n // 解を記録\n recordSolution(state: state, res: &res)\n // 検索を停止\n return\n }\n // すべての選択肢を反復\n for choice in choices {\n // 剪定:選択肢が有効かどうかを確認\n if isValid(state: state, choice: choice) {\n // 試行:選択を行い、状態を更新\n makeChoice(state: &state, choice: choice)\n backtrack(state: &state, choices: choices, res: &res)\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state: &state, choice: choice)\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nfunction backtrack(state, choices, res) {\n // 解かどうかを確認\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n for (let choice of choices) {\n // 剪定:選択肢が有効かどうかを確認\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n // 解かどうかを確認\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n for (let choice of choices) {\n // 剪定:選択肢が有効かどうかを確認\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n // 解かどうかを確認\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n for (Choice choice in choices) {\n // 剪定:選択肢が有効かどうかを確認\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n // 解かどうかを確認\n if is_solution(state) {\n // 解を記録\n record_solution(state, res);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n for choice in choices {\n // 剪定:選択肢が有効かどうかを確認\n if is_valid(state, choice) {\n // 試行:選択を行い、状態を更新\n make_choice(state, choice);\n backtrack(state, choices, res);\n // 後退:選択を取り消し、前の状態に戻す\n undo_choice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n // 解かどうかを確認\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res, numRes);\n // 検索を停止\n return;\n }\n // すべての選択肢を反復\n for (int i = 0; i < numChoices; i++) {\n // 剪定:選択肢が有効かどうかを確認\n if (isValid(state, &choices[i])) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, &choices[i]);\n backtrack(state, choices, numChoices, res, numRes);\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, &choices[i]);\n }\n }\n}\n</code></pre> <pre><code>/* バックトラッキングアルゴリズムフレームワーク */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n // 解かどうかを確認\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res)\n // 検索を停止\n return\n }\n // すべての選択肢を反復\n for (choice in choices) {\n // 剪定:選択肢が有効かどうかを確認\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice)\n backtrack(state, choices, res)\n // 後退:選択を取り消し、前の状態に戻す\n undoChoice(state, choice)\n }\n }\n}\n</code></pre> <pre><code>### バックトラッキングアルゴリズムフレームワーク ###\ndef backtrack(state, choices, res)\n # 解かどうかを確認\n if is_solution?(state)\n # 解を記録\n record_solution(state, res)\n return\n end\n\n # すべての選択肢を反復\n for choice in choices\n # 剪定:選択肢が有効かどうかを確認\n if is_valid?(state, choice)\n # 試行:選択を行い、状態を更新\n make_choice(state, choice)\n backtrack(state, choices, res)\n # 後退:選択を取り消し、前の状態に戻す\n undo_choice(state, choice)\n end\n end\nend\n</code></pre> <p>次に、フレームワークコードに基づいて例題 3 を解きます。状態 <code>state</code> はノードの走査経路を表し、選択肢 <code>choices</code> は現在ノードの左子ノードと右子ノード、結果 <code>res</code> は経路リストです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py<pre><code>def is_solution(state: list[TreeNode]) -> bool:\n \"\"\"現在の状態が解かどうかを判定\"\"\"\n return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n \"\"\"解を記録\"\"\"\n res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n \"\"\"現在の状態下で選択が合法かどうかを判定\"\"\"\n return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n \"\"\"状態を更新\"\"\"\n state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n \"\"\"状態を復元\"\"\"\n state.pop()\n\ndef backtrack(\n state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n \"\"\"バックトラッキングアルゴリズム:例三\"\"\"\n # 解かどうかをチェック\n if is_solution(state):\n # 解を記録\n record_solution(state, res)\n # すべての選択肢を走査\n for choice in choices:\n # 枝刈り:選択が合法かどうかをチェック\n if is_valid(state, choice):\n # 試行:選択を行い、状態を更新\n make_choice(state, choice)\n # 次の選択ラウンドに進む\n backtrack(state, [choice.left, choice.right], res)\n # 撤回:選択を取り消し、前の状態に復元\n undo_choice(state, choice)\n</code></pre> preorder_traversal_iii_template.cpp<pre><code>/* 現在の状態が解かどうかを判定 */\nbool isSolution(vector<TreeNode *> &state) {\n return !state.empty() && state.back()->val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n res.push_back(state);\n}\n\n/* 現在の状態下で選択が合法かどうかを判定 */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n return choice != nullptr && choice->val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n state.push_back(choice);\n}\n\n/* 状態を復元 */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n state.pop_back();\n}\n\n/* バックトラッキングアルゴリズム:例3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n // 解かどうかをチェック\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res);\n }\n // すべての選択肢を走査\n for (TreeNode *choice : choices) {\n // 剪定:選択が合法かどうかをチェック\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice);\n // 次のラウンドの選択に進む\n vector<TreeNode *> nextChoices{choice->left, choice->right};\n backtrack(state, nextChoices, res);\n // 回退:選択を取り消し、前の状態に復元\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.java<pre><code>/* 現在の状態が解かどうかを判定 */\nboolean isSolution(List<TreeNode> state) {\n return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n res.add(new ArrayList<>(state));\n}\n\n/* 現在の状態下で選択が合法かどうかを判定 */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n return choice != null && choice.val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n state.add(choice);\n}\n\n/* 状態を復元 */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n state.remove(state.size() - 1);\n}\n\n/* バックトラッキングアルゴリズム:例3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n // 解かどうかをチェック\n if (isSolution(state)) {\n // 解を記録\n recordSolution(state, res);\n }\n // すべての選択肢を走査\n for (TreeNode choice : choices) {\n // 剪定:選択が合法かどうかをチェック\n if (isValid(state, choice)) {\n // 試行:選択を行い、状態を更新\n makeChoice(state, choice);\n // 次のラウンドの選択に進む\n backtrack(state, Arrays.asList(choice.left, choice.right), res);\n // 回退:選択を取り消し、前の状態に復元\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.cs<pre><code>[class]{preorder_traversal_iii_template}-[func]{IsSolution}\n\n[class]{preorder_traversal_iii_template}-[func]{RecordSolution}\n\n[class]{preorder_traversal_iii_template}-[func]{IsValid}\n\n[class]{preorder_traversal_iii_template}-[func]{MakeChoice}\n\n[class]{preorder_traversal_iii_template}-[func]{UndoChoice}\n\n[class]{preorder_traversal_iii_template}-[func]{Backtrack}\n</code></pre> preorder_traversal_iii_template.go<pre><code>[class]{}-[func]{isSolution}\n\n[class]{}-[func]{recordSolution}\n\n[class]{}-[func]{isValid}\n\n[class]{}-[func]{makeChoice}\n\n[class]{}-[func]{undoChoice}\n\n[class]{}-[func]{backtrackIII}\n</code></pre> preorder_traversal_iii_template.swift<pre><code>[class]{}-[func]{isSolution}\n\n[class]{}-[func]{recordSolution}\n\n[class]{}-[func]{isValid}\n\n[class]{}-[func]{makeChoice}\n\n[class]{}-[func]{undoChoice}\n\n[class]{}-[func]{backtrack}\n</code></pre> preorder_traversal_iii_template.js<pre><code>[class]{}-[func]{isSolution}\n\n[class]{}-[func]{recordSolution}\n\n[class]{}-[func]{isValid}\n\n[class]{}-[func]{makeChoice}\n\n[class]{}-[func]{undoChoice}\n\n[class]{}-[func]{backtrack}\n</code></pre> preorder_traversal_iii_template.ts<pre><code>[class]{}-[func]{isSolution}\n\n[class]{}-[func]{recordSolution}\n\n[class]{}-[func]{isValid}\n\n[class]{}-[func]{makeChoice}\n\n[class]{}-[func]{undoChoice}\n\n[class]{}-[func]{backtrack}\n</code></pre> preorder_traversal_iii_template.dart<pre><code>[class]{}-[func]{isSolution}\n\n[class]{}-[func]{recordSolution}\n\n[class]{}-[func]{isValid}\n\n[class]{}-[func]{makeChoice}\n\n[class]{}-[func]{undoChoice}\n\n[class]{}-[func]{backtrack}\n</code></pre> preorder_traversal_iii_template.rs<pre><code>[class]{}-[func]{is_solution}\n\n[class]{}-[func]{record_solution}\n\n[class]{}-[func]{is_valid}\n\n[class]{}-[func]{make_choice}\n\n[class]{}-[func]{undo_choice}\n\n[class]{}-[func]{backtrack}\n</code></pre> preorder_traversal_iii_template.c<pre><code>[class]{}-[func]{isSolution}\n\n[class]{}-[func]{recordSolution}\n\n[class]{}-[func]{isValid}\n\n[class]{}-[func]{makeChoice}\n\n[class]{}-[func]{undoChoice}\n\n[class]{}-[func]{backtrack}\n</code></pre> preorder_traversal_iii_template.kt<pre><code>[class]{}-[func]{isSolution}\n\n[class]{}-[func]{recordSolution}\n\n[class]{}-[func]{isValid}\n\n[class]{}-[func]{makeChoice}\n\n[class]{}-[func]{undoChoice}\n\n[class]{}-[func]{backtrack}\n</code></pre> preorder_traversal_iii_template.rb<pre><code>[class]{}-[func]{is_solution}\n\n[class]{}-[func]{record_solution}\n\n[class]{}-[func]{is_valid}\n\n[class]{}-[func]{make_choice}\n\n[class]{}-[func]{undo_choice}\n\n[class]{}-[func]{backtrack}\n</code></pre> <p>問題文の意味に従い、値が \\(7\\) のノードを見つけた後も探索を続ける必要があります。したがって、解を記録した後の <code>return</code> 文を削除する必要があります。次の図は、<code>return</code> 文を保持する場合と削除する場合の探索過程の比較です。</p> <p></p> <p> 図 13-4 returnを保持する場合と削除する場合の探索過程の比較 </p> <p>前順走査に基づくコード実装と比べると、バックトラッキングアルゴリズムのフレームワークに基づく実装はやや冗長に見えますが、汎用性はより高いです。実際、多くのバックトラッキング問題はこのフレームワークの下で解くことができます。具体的な問題に応じて <code>state</code> と <code>choices</code> を定義し、フレームワーク内の各メソッドを実装すればよいのです。</p>","path":["第 13 章 バックトラッキング","13.1 バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4 よく使われる用語","text":"<p>アルゴリズム問題をより明確に分析するために、バックトラッキングアルゴリズムでよく使われる用語の意味をまとめ、例題 3 の対応例を以下の表に示します。</p> <p> 表 13-1 バックトラッキングアルゴリズムでよく使われる用語 </p> 名称 定義 例題 3 解(solution) 解は問題の特定条件を満たす答えであり、1 つまたは複数存在する可能性がある 根ノードからノード \\(7\\) までの制約条件を満たすすべての経路 制約条件(constraint) 制約条件は、解の実現可能性を制限する条件であり、通常は枝刈りに使用される 経路にノード \\(3\\) を含まない 状態(state) 状態は、ある時点での問題の状況を表し、これまでに行った選択を含む 現在訪問したノード経路、すなわち <code>path</code> ノードリスト 試行(attempt) 試行は、利用可能な選択肢に基づいて解空間を探索する過程であり、選択を行い、状態を更新し、解かどうかを確認する 左(右)子ノードを再帰的に訪問し、ノードを <code>path</code> に追加し、ノードの値が \\(7\\) かを確認する バックトラック(backtracking) 制約条件を満たさない状態に遭遇した場合、以前の選択を取り消して前の状態に戻ること 葉ノードを越えたとき、探索終了、値が \\(3\\) のノードに遭遇したとき探索を終了し、関数が戻る 枝刈り(pruning) 問題の特性や制約条件に基づき、無意味な探索経路を避ける方法であり、探索効率を向上させる 値が \\(3\\) のノードに遭遇した場合、それ以上探索しない <p>Tip</p> <p>問題、解、状態などの概念は一般的なものであり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも関係します。</p>","path":["第 13 章 バックトラッキング","13.1 バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315","level":2,"title":"13.1.5 長所と限界","text":"<p>バックトラッキングアルゴリズムは本質的に深さ優先探索(DFS)アルゴリズムの一種であり、条件を満たす解を見つけるまであらゆる可能な解を試みます。この方法の利点は、すべての可能な解を見つけられる点であり、適切な枝刈りを行えば効率が高いことです。</p> <p>しかし、大規模または複雑な問題を扱う場合、バックトラッキングアルゴリズムの実行効率は許容できないほど低下する可能性があります。</p> <ul> <li>時間:バックトラッキングアルゴリズムは通常、状態空間のすべての可能性を探索する必要があり、時間計算量は指数オーダーまたは階乗オーダーに達する可能性があります。</li> <li>空間:再帰呼び出し中に現在の状態(例:経路、枝刈り用の補助変数など)を保存する必要があり、深さが大きい場合、空間の使用量が増加します。</li> </ul> <p>それでもなお、バックトラッキングアルゴリズムは特定の探索問題や制約満足問題の最良の解法であることが多いです。これらの問題では、どの選択が有効な解を生成するかを予測できないため、すべての可能な選択を試す必要があります。このような場合、**効率の最適化が鍵**となります。一般的な最適化手法は次の 2 つです。</p> <ul> <li>枝刈り:解を生成しないことが確実な経路を避けることで、時間と空間を節約します。</li> <li>ヒューリスティック探索:探索中に戦略や評価値を導入し、有効な解を生成する可能性が高い経路を優先的に探索します。</li> </ul>","path":["第 13 章 バックトラッキング","13.1 バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6 バックトラッキングの典型的な例題","text":"<p>バックトラッキングアルゴリズムは、多くの探索問題、制約満足問題、組合せ最適化問題を解くのに使用できます。</p> <p>探索問題:この種の問題の目標は、特定の条件を満たす解を見つけることです。</p> <ul> <li>全順列問題:与えられた集合のすべての可能な順列を求める。</li> <li>部分和問題:与えられた集合と目標和に対して、和が目標値になるすべての部分集合を求める。</li> <li>ハノイの塔:3 本の柱と異なるサイズの円盤があり、すべての円盤を 1 本の柱から別の柱に移す。1 回に 1 枚しか動かせず、大きな円盤を小さい円盤の上に置くことはできない。</li> </ul> <p>制約満足問題:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。</p> <ul> <li>\\(n\\) クイーン問題:\\(n imes n\\) のチェス盤に \\(n\\) 個のクイーンを配置し、互いに攻撃しないようにする。</li> <li>数独:\\(9 imes 9\\) のグリッドに数字 \\(1\\) ~ \\(9\\) を入力し、各行、列、\\(3 imes 3\\) のサブグリッドに重複がないようにする。</li> <li>グラフ彩色問題:与えられた無向グラフに対し、隣接頂点が異なる色になるように最小限の色で彩色する。</li> </ul> <p>組合せ最適化問題:この種の問題の目標は、組合せ空間内で特定の条件を満たす最適解を見つけることです。</p> <ul> <li>0-1 ナップサック問題:与えられた物品群とバックパックがあり、各物品には価値と重さが設定されている。バックパックの容量制限内で、総価値を最大化する物品の選択を求める。</li> <li>旅行セールスマン問題:グラフ上で、1 つの点から出発し、すべての他の点を 1 回ずつ訪問して出発点に戻る最短経路を求める。</li> <li>最大クリーク問題:与えられた無向グラフの中で、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。</li> </ul> <p>注意すべきは、多くの組合せ最適化問題に対して、バックトラッキングが最適解法ではないということです。</p> <ul> <li>0-1 ナップサック問題は、時間効率を高めるために動的計画法がよく使用されます。</li> <li>旅行セールスマン問題は有名な NP-Hard 問題であり、遺伝的アルゴリズムやアントコロニーアルゴリズムなどの手法がよく使われます。</li> <li>最大クリーク問題はグラフ理論の古典的な問題であり、貪欲法などのヒューリスティックアルゴリズムで解くことができます。</li> </ul>","path":["第 13 章 バックトラッキング","13.1 バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4 Nクイーン問題","text":"<p>Question</p> <p>チェスのルールによると、クイーンは同じ行、列、または対角線上の駒を攻撃できます。\\(n\\) 個のクイーンと \\(n \\times n\\) のチェスボードが与えられた場合、2つのクイーンが互いに攻撃できない配置を見つけてください。</p> <p>以下の図に示すように、\\(n = 4\\) の場合、2つの解があります。バックトラッキングアルゴリズムの観点から、\\(n \\times n\\) のチェスボードには \\(n^2\\) 個のマスがあり、すべての可能な選択肢 <code>choices</code> を示しています。チェスボードの状態 <code>state</code> は、各クイーンが配置されるにつれて継続的に変化します。</p> <p></p> <p> 図 13-15 4クイーン問題の解 </p> <p>以下の図は、この問題の3つの制約を示しています:複数のクイーンは同じ行、列、または対角線を占有できません。対角線は主対角線 <code>\\</code> と副対角線 <code>/</code> に分かれることに注意することが重要です。</p> <p></p> <p> 図 13-16 Nクイーン問題の制約 </p>","path":["第 13 章 バックトラッキング","13.4 Nクイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1. 行ごとの配置戦略","text":"<p>クイーンの数がチェスボードの行数と等しく、どちらも \\(n\\) であるため、**チェスボードの各行には1つのクイーンのみが配置できることが**容易に結論付けられます。</p> <p>これは、行ごとの配置戦略を採用できることを意味します:最初の行から開始して、最後の行に到達するまで行ごとに1つのクイーンを配置します。</p> <p>以下の図は、4クイーン問題の行ごとの配置プロセスを示しています。スペースの制限により、図は最初の行の1つの検索分岐のみを展開し、列と対角線の制約を満たさない配置を剪定します。</p> <p></p> <p> 図 13-17 行ごとの配置戦略 </p> <p>本質的に、行ごとの配置戦略は剪定関数として機能し、同じ行に複数のクイーンを配置するすべての検索分岐を除去します。</p>","path":["第 13 章 バックトラッキング","13.4 Nクイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2. 列と対角線の剪定","text":"<p>列の制約を満たすために、長さ \\(n\\) のブール配列 <code>cols</code> を使用して、各列にクイーンが占有されているかどうかを追跡できます。各配置決定の前に、<code>cols</code> を使用してすでにクイーンがある列を剪定し、バックトラッキング中に動的に更新されます。</p> <p>Tip</p> <p>行列の原点は左上隅にあり、行インデックスは上から下に増加し、列インデックスは左から右に増加することに注意してください。</p> <p>対角線の制約はどうでしょうか?チェスボード上の特定のセルの行と列のインデックスを \\((row, col)\\) とします。特定の主対角線を選択することで、その対角線上のすべてのセルで差 \\(row - col\\) が同じであることに気付きます。つまり、\\(row - col\\) は主対角線上で定数値です。</p> <p>言い換えると、2つのセルが \\(row_1 - col_1 = row_2 - col_2\\) を満たす場合、それらは確実に同じ主対角線上にあります。このパターンを使用して、以下の図に示す配列 <code>diags1</code> を利用して、クイーンが主対角線上にあるかどうかを追跡できます。</p> <p>同様に、\\(row + col\\) の和は副対角線上のすべてのセルで定数値です。配列 <code>diags2</code> を使用して副対角線の制約も処理できます。</p> <p></p> <p> 図 13-18 列と対角線の制約の処理 </p>","path":["第 13 章 バックトラッキング","13.4 Nクイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3","level":3,"title":"3. コード実装","text":"<p>\\(n\\) 次元の正方行列では、\\(row - col\\) の範囲は \\([-n + 1, n - 1]\\) で、\\(row + col\\) の範囲は \\([0, 2n - 2]\\) であることに注意してください。したがって、主対角線と副対角線の数はどちらも \\(2n - 1\\) で、配列 <code>diags1</code> と <code>diags2</code> の長さは \\(2n - 1\\) です。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py<pre><code>def backtrack(\n row: int,\n n: int,\n state: list[list[str]],\n res: list[list[list[str]]],\n cols: list[bool],\n diags1: list[bool],\n diags2: list[bool],\n):\n \"\"\"バックトラッキングアルゴリズム:n クイーン\"\"\"\n # すべての行が配置されたら、解を記録\n if row == n:\n res.append([list(row) for row in state])\n return\n # すべての列を走査\n for col in range(n):\n # セルに対応する主対角線と副対角線を計算\n diag1 = row - col + n - 1\n diag2 = row + col\n # 枝刈り:セルの列、主対角線、副対角線にクイーンを配置しない\n if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n # 試行:セルにクイーンを配置\n state[row][col] = \"Q\"\n cols[col] = diags1[diag1] = diags2[diag2] = True\n # 次の行を配置\n backtrack(row + 1, n, state, res, cols, diags1, diags2)\n # 撤回:セルを空のスポットに復元\n state[row][col] = \"#\"\n cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n \"\"\"n クイーンを解く\"\"\"\n # n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す\n state = [[\"#\" for _ in range(n)] for _ in range(n)]\n cols = [False] * n # クイーンがある列を記録\n diags1 = [False] * (2 * n - 1) # クイーンがある主対角線を記録\n diags2 = [False] * (2 * n - 1) # クイーンがある副対角線を記録\n res = []\n backtrack(0, n, state, res, cols, diags1, diags2)\n\n return res\n</code></pre> n_queens.cpp<pre><code>/* バックトラッキングアルゴリズム:n クイーン */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n vector<bool> &diags1, vector<bool> &diags2) {\n // すべての行が配置されたら、解を記録\n if (row == n) {\n res.push_back(state);\n return;\n }\n // すべての列を走査\n for (int col = 0; col < n; col++) {\n // セルに対応する主対角線と副対角線を計算\n int diag1 = row - col + n - 1;\n int diag2 = row + col;\n // 剪定:セルの列、主対角線、副対角線にクイーンを配置することを許可しない\n if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n // 試行:セルにクイーンを配置\n state[row][col] = \"Q\";\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 次の行を配置\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 回退:セルを空のスポットに復元\n state[row][col] = \"#\";\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n クイーンを解く */\nvector<vector<vector<string>>> nQueens(int n) {\n // n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す\n vector<vector<string>> state(n, vector<string>(n, \"#\"));\n vector<bool> cols(n, false); // クイーンのある列を記録\n vector<bool> diags1(2 * n - 1, false); // クイーンのある主対角線を記録\n vector<bool> diags2(2 * n - 1, false); // クイーンのある副対角線を記録\n vector<vector<vector<string>>> res;\n\n backtrack(0, n, state, res, cols, diags1, diags2);\n\n return res;\n}\n</code></pre> n_queens.java<pre><code>/* バックトラッキングアルゴリズム:n クイーン */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n boolean[] cols, boolean[] diags1, boolean[] diags2) {\n // すべての行が配置されたら、解を記録\n if (row == n) {\n List<List<String>> copyState = new ArrayList<>();\n for (List<String> sRow : state) {\n copyState.add(new ArrayList<>(sRow));\n }\n res.add(copyState);\n return;\n }\n // すべての列を走査\n for (int col = 0; col < n; col++) {\n // セルに対応する主対角線と副対角線を計算\n int diag1 = row - col + n - 1;\n int diag2 = row + col;\n // 剪定:セルの列、主対角線、副対角線にクイーンを配置することを許可しない\n if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n // 試行:セルにクイーンを配置\n state.get(row).set(col, \"Q\");\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 次の行を配置\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 回退:セルを空のスポットに復元\n state.get(row).set(col, \"#\");\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n クイーンを解く */\nList<List<List<String>>> nQueens(int n) {\n // n*n サイズのチェスボードを初期化、'Q' はクイーンを表し、'#' は空のスポットを表す\n List<List<String>> state = new ArrayList<>();\n for (int i = 0; i < n; i++) {\n List<String> row = new ArrayList<>();\n for (int j = 0; j < n; j++) {\n row.add(\"#\");\n }\n state.add(row);\n }\n boolean[] cols = new boolean[n]; // クイーンのある列を記録\n boolean[] diags1 = new boolean[2 * n - 1]; // クイーンのある主対角線を記録\n boolean[] diags2 = new boolean[2 * n - 1]; // クイーンのある副対角線を記録\n List<List<List<String>>> res = new ArrayList<>();\n\n backtrack(0, n, state, res, cols, diags1, diags2);\n\n return res;\n}\n</code></pre> n_queens.cs<pre><code>[class]{n_queens}-[func]{Backtrack}\n\n[class]{n_queens}-[func]{NQueens}\n</code></pre> n_queens.go<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{nQueens}\n</code></pre> n_queens.swift<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{nQueens}\n</code></pre> n_queens.js<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{nQueens}\n</code></pre> n_queens.ts<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{nQueens}\n</code></pre> n_queens.dart<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{nQueens}\n</code></pre> n_queens.rs<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{n_queens}\n</code></pre> n_queens.c<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{nQueens}\n</code></pre> n_queens.kt<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{nQueens}\n</code></pre> n_queens.rb<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{n_queens}\n</code></pre> <p>\\(n\\) 個のクイーンを行ごとに配置し、列の制約を考慮して、最初の行から最後の行まで、\\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) の選択肢があり、\\(O(n!)\\) 時間を使用します。解を記録する際、行列 <code>state</code> をコピーして <code>res</code> に追加する必要があり、コピー操作は \\(O(n^2)\\) 時間を使用します。したがって、全体の時間計算量は \\(O(n! \\cdot n^2)\\) です。実際には、対角線制約に基づく剪定により検索空間を大幅に削減できるため、多くの場合、検索効率は上記の時間計算量よりも優れています。</p> <p>配列 <code>state</code> は \\(O(n^2)\\) 空間を使用し、配列 <code>cols</code>、<code>diags1</code>、<code>diags2</code> はそれぞれ \\(O(n)\\) 空間を使用します。最大再帰深度は \\(n\\) で、\\(O(n)\\) のスタックフレーム空間を使用します。したがって、空間計算量は \\(O(n^2)\\) です。</p>","path":["第 13 章 バックトラッキング","13.4 Nクイーン問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2 順列問題","text":"<p>順列問題は、バックトラッキングアルゴリズムの典型的な応用です。これは、配列や文字列などの与えられた集合から要素のすべての可能な配置(順列)を見つけることを含みます。</p> <p>以下の表は、入力配列とその対応する順列を含むいくつかの例を示しています。</p> <p> 表 13-2 順列の例 </p> 入力配列 順列 \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321","level":2,"title":"13.2.1 重複要素がない場合","text":"<p>Question</p> <p>重複要素のない整数配列が与えられた場合、すべての可能な順列を返してください。</p> <p>バックトラッキングの観点から、順列を生成するプロセスを一連の選択として見ることができます。 入力配列が \\([1, 2, 3]\\) だとします。最初に \\(1\\) を選択し、次に \\(3\\)、最後に \\(2\\) を選択すると、順列 \\([1, 3, 2]\\) が得られます。「バックトラッキング」は前の選択を取り消して、代替オプションを探索することを意味します。</p> <p>コーディングの観点から、候補集合 <code>choices</code> は入力配列のすべての要素で構成され、<code>state</code> はこれまでに選択された要素を保持します。各要素は一度だけ選択できるため、<code>state</code> のすべての要素は一意である必要があります。</p> <p>以下の図に示すように、検索プロセスを再帰木に展開できます。各ノードは現在の <code>state</code> を表します。ルートノードから開始して、3回の選択の後、葉ノードに到達します—それぞれが順列に対応します。</p> <p></p> <p> 図 13-5 順列の再帰木 </p>","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1. 重複選択の剪定","text":"<p>各要素が一度だけ選択されることを保証するために、ブール配列 <code>selected</code> を導入します。ここで <code>selected[i]</code> は <code>choices[i]</code> が選択されたかどうかを示します。次に、この配列に基づいて剪定ステップを実行します:</p> <ul> <li><code>choice[i]</code> を選択した後、<code>selected[i]</code> を \\(\\text{True}\\) に設定して選択されたとマークします。</li> <li><code>choices</code> を反復処理する際、選択されたとマークされたすべての要素をスキップします(つまり、それらの分岐を剪定します)。</li> </ul> <p>以下の図に示すように、最初のラウンドで1を選択し、2番目のラウンドで3を選択し、最後のラウンドで2を選択するとします。2番目のラウンドで要素1の分岐と、3番目のラウンドで要素1と3の分岐を剪定する必要があります。</p> <p></p> <p> 図 13-6 順列の剪定例 </p> <p>図から、この剪定プロセスが検索空間を \\(O(n^n)\\) から \\(O(n!)\\) に削減することがわかります。</p>","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2","level":3,"title":"2. コード実装","text":"<p>この理解により、フレームワークコードの「空欄を埋める」ことができます。全体のコードを簡潔に保つため、フレームワークの各部分を個別に実装せず、代わりに <code>backtrack()</code> 関数ですべてを展開します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py<pre><code>def backtrack(\n state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n \"\"\"バックトラッキングアルゴリズム:順列 I\"\"\"\n # 状態の長さが要素数と等しいとき、解を記録\n if len(state) == len(choices):\n res.append(list(state))\n return\n # すべての選択肢を走査\n for i, choice in enumerate(choices):\n # 枝刈り:要素の重複選択を許可しない\n if not selected[i]:\n # 試行:選択を行い、状態を更新\n selected[i] = True\n state.append(choice)\n # 次の選択ラウンドに進む\n backtrack(state, choices, selected, res)\n # 撤回:選択を取り消し、前の状態に復元\n selected[i] = False\n state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n \"\"\"順列 I\"\"\"\n res = []\n backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n return res\n</code></pre> permutations_i.cpp<pre><code>/* バックトラッキングアルゴリズム:順列 I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n // 状態の長さが要素数と等しくなったら、解を記録\n if (state.size() == choices.size()) {\n res.push_back(state);\n return;\n }\n // すべての選択肢を走査\n for (int i = 0; i < choices.size(); i++) {\n int choice = choices[i];\n // 剪定:要素の重複選択を許可しない\n if (!selected[i]) {\n // 試行:選択を行い、状態を更新\n selected[i] = true;\n state.push_back(choice);\n // 次のラウンドの選択に進む\n backtrack(state, choices, selected, res);\n // 回退:選択を取り消し、前の状態に復元\n selected[i] = false;\n state.pop_back();\n }\n }\n}\n\n/* 順列 I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n vector<int> state;\n vector<bool> selected(nums.size(), false);\n vector<vector<int>> res;\n backtrack(state, nums, selected, res);\n return res;\n}\n</code></pre> permutations_i.java<pre><code>/* バックトラッキングアルゴリズム:順列 I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n // 状態の長さが要素数と等しくなったら、解を記録\n if (state.size() == choices.length) {\n res.add(new ArrayList<Integer>(state));\n return;\n }\n // すべての選択肢を走査\n for (int i = 0; i < choices.length; i++) {\n int choice = choices[i];\n // 剪定:要素の重複選択を許可しない\n if (!selected[i]) {\n // 試行:選択を行い、状態を更新\n selected[i] = true;\n state.add(choice);\n // 次のラウンドの選択に進む\n backtrack(state, choices, selected, res);\n // 回退:選択を取り消し、前の状態に復元\n selected[i] = false;\n state.remove(state.size() - 1);\n }\n }\n}\n\n/* 順列 I */\nList<List<Integer>> permutationsI(int[] nums) {\n List<List<Integer>> res = new ArrayList<List<Integer>>();\n backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n return res;\n}\n</code></pre> permutations_i.cs<pre><code>[class]{permutations_i}-[func]{Backtrack}\n\n[class]{permutations_i}-[func]{PermutationsI}\n</code></pre> permutations_i.go<pre><code>[class]{}-[func]{backtrackI}\n\n[class]{}-[func]{permutationsI}\n</code></pre> permutations_i.swift<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsI}\n</code></pre> permutations_i.js<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsI}\n</code></pre> permutations_i.ts<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsI}\n</code></pre> permutations_i.dart<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsI}\n</code></pre> permutations_i.rs<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutations_i}\n</code></pre> permutations_i.c<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsI}\n</code></pre> permutations_i.kt<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsI}\n</code></pre> permutations_i.rb<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutations_i}\n</code></pre>","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322","level":2,"title":"13.2.2 重複要素を考慮する場合","text":"<p>Question</p> <p>**重複要素を含む可能性のある**整数配列が与えられた場合、すべての一意の順列を返してください。</p> <p>入力配列が \\([1, 1, 2]\\) だとします。2つの同一要素 \\(1\\) を区別するために、2番目を \\(\\hat{1}\\) とラベル付けします。</p> <p>以下の図に示すように、この方法で生成される順列の半分は重複です:</p> <p></p> <p> 図 13-7 重複順列 </p> <p>では、これらの重複順列をどのように除去できるでしょうか?一つの直接的なアプローチは、すべての順列を生成した後にハッシュセットを使用して重複を除去することです。しかし、これはあまり優雅ではありません。重複を生成する分岐は本来不要であり、事前に剪定されるべきだからです、これによりアルゴリズムの効率が向上します。</p>","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1. 等値要素の剪定","text":"<p>以下の図を見ると、最初のラウンドで \\(1\\) または \\(\\hat{1}\\) を選択すると同じ順列につながるため、\\(\\hat{1}\\) を剪定します。</p> <p>同様に、最初のラウンドで \\(2\\) を選択した後、2番目のラウンドで \\(1\\) または \\(\\hat{1}\\) を選択しても重複分岐につながるため、その時も \\(\\hat{1}\\) を剪定します。</p> <p>本質的に、私たちの目標は、複数の同一要素が選択の各ラウンドで一度だけ選択されることを保証することです。</p> <p></p> <p> 図 13-8 重複順列の剪定 </p>","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2. コード実装","text":"<p>前の問題のコードに基づいて、各ラウンドでハッシュセット <code>duplicated</code> を導入します。このセットは、すでに試行した要素を追跡し、重複を剪定できるようにします:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py<pre><code>def backtrack(\n state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n \"\"\"バックトラッキングアルゴリズム:順列 II\"\"\"\n # 状態の長さが要素数と等しいとき、解を記録\n if len(state) == len(choices):\n res.append(list(state))\n return\n # すべての選択肢を走査\n duplicated = set[int]()\n for i, choice in enumerate(choices):\n # 枝刈り:要素の重複選択を許可せず、等しい要素の重複選択も許可しない\n if not selected[i] and choice not in duplicated:\n # 試行:選択を行い、状態を更新\n duplicated.add(choice) # 選択された要素値を記録\n selected[i] = True\n state.append(choice)\n # 次の選択ラウンドに進む\n backtrack(state, choices, selected, res)\n # 撤回:選択を取り消し、前の状態に復元\n selected[i] = False\n state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n \"\"\"順列 II\"\"\"\n res = []\n backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n return res\n</code></pre> permutations_ii.cpp<pre><code>/* バックトラッキングアルゴリズム:順列 II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n // 状態の長さが要素数と等しくなったら、解を記録\n if (state.size() == choices.size()) {\n res.push_back(state);\n return;\n }\n // すべての選択肢を走査\n unordered_set<int> duplicated;\n for (int i = 0; i < choices.size(); i++) {\n int choice = choices[i];\n // 剪定:要素の重複選択を許可せず、等しい要素の重複選択も許可しない\n if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n // 試行:選択を行い、状態を更新\n duplicated.emplace(choice); // 選択された要素値を記録\n selected[i] = true;\n state.push_back(choice);\n // 次のラウンドの選択に進む\n backtrack(state, choices, selected, res);\n // 回退:選択を取り消し、前の状態に復元\n selected[i] = false;\n state.pop_back();\n }\n }\n}\n\n/* 順列 II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n vector<int> state;\n vector<bool> selected(nums.size(), false);\n vector<vector<int>> res;\n backtrack(state, nums, selected, res);\n return res;\n}\n</code></pre> permutations_ii.java<pre><code>/* バックトラッキングアルゴリズム:順列 II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n // 状態の長さが要素数と等しくなったら、解を記録\n if (state.size() == choices.length) {\n res.add(new ArrayList<Integer>(state));\n return;\n }\n // すべての選択肢を走査\n Set<Integer> duplicated = new HashSet<Integer>();\n for (int i = 0; i < choices.length; i++) {\n int choice = choices[i];\n // 剪定:要素の重複選択を許可せず、等しい要素の重複選択も許可しない\n if (!selected[i] && !duplicated.contains(choice)) {\n // 試行:選択を行い、状態を更新\n duplicated.add(choice); // 選択された要素値を記録\n selected[i] = true;\n state.add(choice);\n // 次のラウンドの選択に進む\n backtrack(state, choices, selected, res);\n // 回退:選択を取り消し、前の状態に復元\n selected[i] = false;\n state.remove(state.size() - 1);\n }\n }\n}\n\n/* 順列 II */\nList<List<Integer>> permutationsII(int[] nums) {\n List<List<Integer>> res = new ArrayList<List<Integer>>();\n backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n return res;\n}\n</code></pre> permutations_ii.cs<pre><code>[class]{permutations_ii}-[func]{Backtrack}\n\n[class]{permutations_ii}-[func]{PermutationsII}\n</code></pre> permutations_ii.go<pre><code>[class]{}-[func]{backtrackII}\n\n[class]{}-[func]{permutationsII}\n</code></pre> permutations_ii.swift<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsII}\n</code></pre> permutations_ii.js<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsII}\n</code></pre> permutations_ii.ts<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsII}\n</code></pre> permutations_ii.dart<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsII}\n</code></pre> permutations_ii.rs<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutations_ii}\n</code></pre> permutations_ii.c<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsII}\n</code></pre> permutations_ii.kt<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutationsII}\n</code></pre> permutations_ii.rb<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{permutations_ii}\n</code></pre> <p>すべての要素が異なると仮定すると、\\(n\\) 個の要素の順列は \\(n!\\) (階乗)個あります。各結果を記録するには長さ \\(n\\) のリストをコピーする必要があり、これには \\(O(n)\\) 時間がかかります。したがって、総時間計算量は \\(O(n!n)\\) です。</p> <p>最大再帰深度は \\(n\\) で、\\(O(n)\\) のスタック空間を使用します。<code>selected</code> 配列も \\(O(n)\\) 空間が必要です。一度に最大 \\(n\\) 個の個別の <code>duplicated</code> セットが存在する可能性があるため、それらは集合的に \\(O(n^2)\\) 空間を占有します。したがって、空間計算量は \\(O(n^2)\\) です。</p>","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-2","level":3,"title":"3. 2つの剪定方法の比較","text":"<p><code>selected</code> と <code>duplicated</code> はどちらも剪定メカニズムとして機能しますが、異なる問題をターゲットにしています:</p> <ul> <li>重複選択の剪定(<code>selected</code> 経由):検索全体に単一の <code>selected</code> 配列があり、現在の状態にすでにある要素を示します。これにより、同じ要素が <code>state</code> に複数回現れることを防ぎます。</li> <li>等値要素の剪定(<code>duplicated</code> 経由):<code>backtrack</code> 関数の各呼び出しは独自の <code>duplicated</code> セットを使用し、その特定の反復(<code>for</code> ループ)ですでに選択された要素を記録します。これにより、等しい要素が選択の各ラウンドで一度だけ選択されることを保証します。</li> </ul> <p>以下の図は、これら2つの剪定戦略の範囲を示しています。木の各ノードは選択を表します。ルートから任意の葉への経路は、1つの完全な順列に対応します。</p> <p></p> <p> 図 13-9 2つの剪定条件の範囲 </p>","path":["第 13 章 バックトラッキング","13.2 順列問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3 部分集合和問題","text":"","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331","level":2,"title":"13.3.1 重複要素がない場合","text":"<p>Question</p> <p>正の整数の配列 <code>nums</code> とターゲット正整数 <code>target</code> が与えられた場合、組み合わせ内の要素の和が <code>target</code> に等しくなるようなすべての可能な組み合わせを見つけてください。与えられた配列には重複要素がなく、各要素は複数回選択できます。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。</p> <p>例えば、入力集合 \\(\\{3, 4, 5\\}\\) とターゲット整数 \\(9\\) の場合、解は \\(\\{3, 3, 3\\}, \\{4, 5\\}\\) です。以下の2点に注意してください。</p> <ul> <li>入力集合の要素は無制限に選択できます。</li> <li>部分集合は要素の順序を区別しません。例えば \\(\\{4, 5\\}\\) と \\(\\{5, 4\\}\\) は同じ部分集合です。</li> </ul>","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1","level":3,"title":"1. 順列解法の参考","text":"<p>順列問題と同様に、部分集合の生成を一連の選択として想像でき、選択プロセス中に「要素和」をリアルタイムで更新できます。要素和が <code>target</code> に等しくなったとき、部分集合を結果リストに記録します。</p> <p>順列問題とは異なり、この問題では要素は無制限に選択できるため、要素が選択されたかどうかを記録するための <code>selected</code> ブール配列を使用する必要がありません。順列コードに軽微な修正を加えて、最初に問題を解決できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py<pre><code>def backtrack(\n state: list[int],\n target: int,\n total: int,\n choices: list[int],\n res: list[list[int]],\n):\n \"\"\"バックトラッキングアルゴリズム:部分集合の和 I\"\"\"\n # 部分集合の和が target と等しいとき、解を記録\n if total == target:\n res.append(list(state))\n return\n # すべての選択肢を走査\n for i in range(len(choices)):\n # 枝刈り:部分集合の和が target を超える場合、その選択をスキップ\n if total + choices[i] > target:\n continue\n # 試行:選択を行い、要素と total を更新\n state.append(choices[i])\n # 次の選択ラウンドに進む\n backtrack(state, target, total + choices[i], choices, res)\n # 撤回:選択を取り消し、前の状態に復元\n state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n \"\"\"部分集合の和 I を解く(重複する部分集合を含む)\"\"\"\n state = [] # 状態(部分集合)\n total = 0 # 部分集合の和\n res = [] # 結果リスト(部分集合リスト)\n backtrack(state, target, total, nums, res)\n return res\n</code></pre> subset_sum_i_naive.cpp<pre><code>/* バックトラッキングアルゴリズム:部分集合和 I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n // 部分集合の和がtargetと等しいとき、解を記録\n if (total == target) {\n res.push_back(state);\n return;\n }\n // すべての選択肢を走査\n for (int i = 0; i < choices.size(); i++) {\n // 剪定:部分集合の和がtargetを超えた場合、その選択をスキップ\n if (total + choices[i] > target) {\n continue;\n }\n // 試行:選択を行い、要素とtotalを更新\n state.push_back(choices[i]);\n // 次のラウンドの選択に進む\n backtrack(state, target, total + choices[i], choices, res);\n // 回退:選択を取り消し、前の状態に復元\n state.pop_back();\n }\n}\n\n/* 部分集合和 I を解く(重複する部分集合を含む) */\nvector<vector<int>> subsetSumINaive(vector<int> nums, int target) {\n vector<int> state; // 状態(部分集合)\n int total = 0; // 部分集合の和\n vector<vector<int>> res; // 結果リスト(部分集合リスト)\n backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.java<pre><code>/* バックトラッキングアルゴリズム:部分集合和 I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n // 部分集合の和がtargetと等しいとき、解を記録\n if (total == target) {\n res.add(new ArrayList<>(state));\n return;\n }\n // すべての選択肢を走査\n for (int i = 0; i < choices.length; i++) {\n // 剪定:部分集合の和がtargetを超えた場合、その選択をスキップ\n if (total + choices[i] > target) {\n continue;\n }\n // 試行:選択を行い、要素とtotalを更新\n state.add(choices[i]);\n // 次のラウンドの選択に進む\n backtrack(state, target, total + choices[i], choices, res);\n // 回退:選択を取り消し、前の状態に復元\n state.remove(state.size() - 1);\n }\n}\n\n/* 部分集合和 I を解く(重複する部分集合を含む) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n int total = 0; // 部分集合の和\n List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)\n backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.cs<pre><code>[class]{subset_sum_i_naive}-[func]{Backtrack}\n\n[class]{subset_sum_i_naive}-[func]{SubsetSumINaive}\n</code></pre> subset_sum_i_naive.go<pre><code>[class]{}-[func]{backtrackSubsetSumINaive}\n\n[class]{}-[func]{subsetSumINaive}\n</code></pre> subset_sum_i_naive.swift<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumINaive}\n</code></pre> subset_sum_i_naive.js<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumINaive}\n</code></pre> subset_sum_i_naive.ts<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumINaive}\n</code></pre> subset_sum_i_naive.dart<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumINaive}\n</code></pre> subset_sum_i_naive.rs<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subset_sum_i_naive}\n</code></pre> subset_sum_i_naive.c<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumINaive}\n</code></pre> subset_sum_i_naive.kt<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumINaive}\n</code></pre> subset_sum_i_naive.rb<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subset_sum_i_naive}\n</code></pre> <p>配列 \\([3, 4, 5]\\) とターゲット要素 \\(9\\) を上記のコードに入力すると、結果 \\([3, 3, 3], [4, 5], [5, 4]\\) が得られます。和が \\(9\\) のすべての部分集合を正常に見つけましたが、重複する部分集合 \\([4, 5]\\) と \\([5, 4]\\) が含まれています。</p> <p>これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、\\(5\\) の前に \\(4\\) を選択することと \\(4\\) の前に \\(5\\) を選択することは異なる分岐ですが、同じ部分集合に対応します。</p> <p></p> <p> 図 13-10 部分集合の検索と境界外の剪定 </p> <p>重複する部分集合を除去するために、直接的なアイデアは結果リストを重複除去することです。しかし、この方法は2つの理由で非常に非効率的です。</p> <ul> <li>配列要素が多い場合、特に <code>target</code> が大きい場合、検索プロセスで大量の重複する部分集合が生成されます。</li> <li>部分集合(配列)の差異を比較することは非常に時間がかかり、まず配列をソートし、次に配列の各要素の差異を比較する必要があります。</li> </ul>","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2. 重複部分集合の剪定","text":"<p>剪定を通じて検索プロセス中に重複除去を検討します。以下の図を観察すると、異なる順序で配列要素を選択するときに重複する部分集合が生成されます。例えば、以下の状況です。</p> <ol> <li>最初のラウンドで \\(3\\) を選択し、2番目のラウンドで \\(4\\) を選択すると、これら2つの要素を含むすべての部分集合が生成され、\\([3, 4, \\dots]\\) と表記されます。</li> <li>後で、最初のラウンドで \\(4\\) が選択されたとき、2番目のラウンドは \\(3\\) をスキップすべきです。この選択によって生成される部分集合 \\([4, 3, \\dots]\\) はステップ <code>1.</code> の部分集合と完全に重複するからです。</li> </ol> <p>検索プロセスでは、各層の選択が左から右に一つずつ試行されるため、右側の分岐ほどより多く剪定されます。</p> <ol> <li>最初の2ラウンドで \\(3\\) と \\(5\\) を選択し、部分集合 \\([3, 5, \\dots]\\) を生成します。</li> <li>最初の2ラウンドで \\(4\\) と \\(5\\) を選択し、部分集合 \\([4, 5, \\dots]\\) を生成します。</li> <li>最初のラウンドで \\(5\\) が選択された場合、2番目のラウンドは \\(3\\) と \\(4\\) をスキップすべきです。部分集合 \\([5, 3, \\dots]\\) と \\([5, 4, \\dots]\\) はステップ <code>1.</code> と <code>2.</code> で記述された部分集合と完全に重複するからです。</li> </ol> <p></p> <p> 図 13-11 異なる選択順序による重複部分集合 </p> <p>要約すると、入力配列 \\([x_1, x_2, \\dots, x_n]\\) が与えられた場合、検索プロセスでの選択シーケンスは \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\) であるべきで、\\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) を満たす必要があります。この条件を満たさない選択シーケンスは重複を引き起こし、剪定されるべきです。</p>","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3","level":3,"title":"3. コード実装","text":"<p>この剪定を実装するために、変数 <code>start</code> を初期化し、これは走査の開始点を示します。選択 \\(x_{i}\\) を行った後、次のラウンドをインデックス \\(i\\) から開始するように設定します。これにより、選択シーケンスが \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) を満たすことが保証され、部分集合の一意性が保証されます。</p> <p>さらに、コードに以下の2つの最適化を行いました。</p> <ul> <li>検索を開始する前に、配列 <code>nums</code> をソートします。すべての選択の走査で、部分集合和が <code>target</code> を超えたときにループを直接終了します。後続の要素はより大きく、それらの部分集合和は確実に <code>target</code> を超えるからです。</li> <li>要素和変数 <code>total</code> を除去し、<code>target</code> に対して減算を実行して要素和をカウントします。<code>target</code> が \\(0\\) に等しくなったとき、解を記録します。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py<pre><code>def backtrack(\n state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n \"\"\"バックトラッキングアルゴリズム:部分集合の和 I\"\"\"\n # 部分集合の和が target と等しいとき、解を記録\n if target == 0:\n res.append(list(state))\n return\n # すべての選択肢を走査\n # 枝刈り二:start から走査を開始して重複する部分集合の生成を避ける\n for i in range(start, len(choices)):\n # 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了\n # これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため\n if target - choices[i] < 0:\n break\n # 試行:選択を行い、target、start を更新\n state.append(choices[i])\n # 次の選択ラウンドに進む\n backtrack(state, target - choices[i], choices, i, res)\n # 撤回:選択を取り消し、前の状態に復元\n state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n \"\"\"部分集合の和 I を解く\"\"\"\n state = [] # 状態(部分集合)\n nums.sort() # nums をソート\n start = 0 # 走査の開始点\n res = [] # 結果リスト(部分集合リスト)\n backtrack(state, target, nums, start, res)\n return res\n</code></pre> subset_sum_i.cpp<pre><code>/* バックトラッキングアルゴリズム:部分集合和 I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n // 部分集合の和がtargetと等しいとき、解を記録\n if (target == 0) {\n res.push_back(state);\n return;\n }\n // すべての選択肢を走査\n // 剪定二:startから走査を開始し、重複する部分集合の生成を回避\n for (int i = start; i < choices.size(); i++) {\n // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了\n // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える\n if (target - choices[i] < 0) {\n break;\n }\n // 試行:選択を行い、target、startを更新\n state.push_back(choices[i]);\n // 次のラウンドの選択に進む\n backtrack(state, target - choices[i], choices, i, res);\n // 回退:選択を取り消し、前の状態に復元\n state.pop_back();\n }\n}\n\n/* 部分集合和 I を解く */\nvector<vector<int>> subsetSumI(vector<int> nums, int target) {\n vector<int> state; // 状態(部分集合)\n sort(nums.begin(), nums.end()); // nums をソート\n int start = 0; // 走査の開始点\n vector<vector<int>> res; // 結果リスト(部分集合リスト)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.java<pre><code>/* バックトラッキングアルゴリズム:部分集合和 I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n // 部分集合の和がtargetと等しいとき、解を記録\n if (target == 0) {\n res.add(new ArrayList<>(state));\n return;\n }\n // すべての選択肢を走査\n // 剪定二:startから走査を開始し、重複する部分集合の生成を回避\n for (int i = start; i < choices.length; i++) {\n // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了\n // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える\n if (target - choices[i] < 0) {\n break;\n }\n // 試行:選択を行い、target、startを更新\n state.add(choices[i]);\n // 次のラウンドの選択に進む\n backtrack(state, target - choices[i], choices, i, res);\n // 回退:選択を取り消し、前の状態に復元\n state.remove(state.size() - 1);\n }\n}\n\n/* 部分集合和 I を解く */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n Arrays.sort(nums); // nums をソート\n int start = 0; // 走査の開始点\n List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.cs<pre><code>[class]{subset_sum_i}-[func]{Backtrack}\n\n[class]{subset_sum_i}-[func]{SubsetSumI}\n</code></pre> subset_sum_i.go<pre><code>[class]{}-[func]{backtrackSubsetSumI}\n\n[class]{}-[func]{subsetSumI}\n</code></pre> subset_sum_i.swift<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumI}\n</code></pre> subset_sum_i.js<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumI}\n</code></pre> subset_sum_i.ts<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumI}\n</code></pre> subset_sum_i.dart<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumI}\n</code></pre> subset_sum_i.rs<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subset_sum_i}\n</code></pre> subset_sum_i.c<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumI}\n</code></pre> subset_sum_i.kt<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumI}\n</code></pre> subset_sum_i.rb<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subset_sum_i}\n</code></pre> <p>以下の図は、配列 \\([3, 4, 5]\\) とターゲット要素 \\(9\\) を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。</p> <p></p> <p> 図 13-12 部分集合和 I のバックトラッキングプロセス </p>","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332","level":2,"title":"13.3.2 重複要素がある場合を考慮","text":"<p>Question</p> <p>正の整数の配列 <code>nums</code> とターゲット正整数 <code>target</code> が与えられた場合、組み合わせ内の要素の和が <code>target</code> に等しくなるようなすべての可能な組み合わせを見つけてください。与えられた配列には重複要素が含まれる可能性があり、各要素は一度だけ選択できます。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。</p> <p>前の問題と比較して、この問題の入力配列には重複要素が含まれる可能性があり、新しい問題が導入されます。例えば、配列 \\([4, \\hat{4}, 5]\\) とターゲット要素 \\(9\\) が与えられた場合、既存のコードの出力結果は \\([4, 5], [\\hat{4}, 5]\\) となり、重複する部分集合が生成されます。</p> <p>この重複の理由は、特定のラウンドで等しい要素が複数回選択されることです。以下の図では、最初のラウンドに3つの選択肢があり、そのうち2つが \\(4\\) であり、2つの重複する検索分岐を生成し、重複する部分集合を出力します。同様に、2番目のラウンドの2つの \\(4\\) も重複する部分集合を生成します。</p> <p></p> <p> 図 13-13 等しい要素による重複部分集合 </p>","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1. 等値要素の剪定","text":"<p>この問題を解決するために、等しい要素がラウンドごとに一度だけ選択されるように制限する必要があります。実装は非常に巧妙です:配列がソートされているため、等しい要素は隣接しています。これは、特定のラウンドの選択で、現在の要素がその左側の要素と等しい場合、それはすでに選択されていることを意味するため、現在の要素を直接スキップします。</p> <p>同時に、この問題では各配列要素は一度だけ選択できると規定されています。幸い、変数 <code>start</code> を使用してこの制約も満たすことができます:選択 \\(x_{i}\\) を行った後、次のラウンドをインデックス \\(i + 1\\) から前方に開始するように設定します。これにより、重複する部分集合が除去されるだけでなく、要素の重複選択も回避されます。</p>","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2_1","level":3,"title":"2. コード実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py<pre><code>def backtrack(\n state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n \"\"\"バックトラッキングアルゴリズム:部分集合の和 II\"\"\"\n # 部分集合の和が target と等しいとき、解を記録\n if target == 0:\n res.append(list(state))\n return\n # すべての選択肢を走査\n # 枝刈り二:start から走査を開始して重複する部分集合の生成を避ける\n # 枝刈り三:start から走査を開始して同じ要素の重複選択を避ける\n for i in range(start, len(choices)):\n # 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了\n # これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため\n if target - choices[i] < 0:\n break\n # 枝刈り四:要素が左の要素と等しい場合、検索分岐が重複していることを示すため、スキップ\n if i > start and choices[i] == choices[i - 1]:\n continue\n # 試行:選択を行い、target、start を更新\n state.append(choices[i])\n # 次の選択ラウンドに進む\n backtrack(state, target - choices[i], choices, i + 1, res)\n # 撤回:選択を取り消し、前の状態に復元\n state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n \"\"\"部分集合の和 II を解く\"\"\"\n state = [] # 状態(部分集合)\n nums.sort() # nums をソート\n start = 0 # 走査の開始点\n res = [] # 結果リスト(部分集合リスト)\n backtrack(state, target, nums, start, res)\n return res\n</code></pre> subset_sum_ii.cpp<pre><code>/* バックトラッキングアルゴリズム:部分集合和 II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n // 部分集合の和がtargetと等しいとき、解を記録\n if (target == 0) {\n res.push_back(state);\n return;\n }\n // すべての選択肢を走査\n // 剪定二:startから走査を開始し、重複する部分集合の生成を回避\n // 剪定三:startから走査を開始し、同じ要素の繰り返し選択を回避\n for (int i = start; i < choices.size(); i++) {\n // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了\n // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える\n if (target - choices[i] < 0) {\n break;\n }\n // 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ\n if (i > start && choices[i] == choices[i - 1]) {\n continue;\n }\n // 試行:選択を行い、target、startを更新\n state.push_back(choices[i]);\n // 次のラウンドの選択に進む\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 回退:選択を取り消し、前の状態に復元\n state.pop_back();\n }\n}\n\n/* 部分集合和 II を解く */\nvector<vector<int>> subsetSumII(vector<int> nums, int target) {\n vector<int> state; // 状態(部分集合)\n sort(nums.begin(), nums.end()); // nums をソート\n int start = 0; // 走査の開始点\n vector<vector<int>> res; // 結果リスト(部分集合リスト)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.java<pre><code>/* バックトラッキングアルゴリズム:部分集合和 II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n // 部分集合の和がtargetと等しいとき、解を記録\n if (target == 0) {\n res.add(new ArrayList<>(state));\n return;\n }\n // すべての選択肢を走査\n // 剪定二:startから走査を開始し、重複する部分集合の生成を回避\n // 剪定三:startから走査を開始し、同じ要素の繰り返し選択を回避\n for (int i = start; i < choices.length; i++) {\n // 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了\n // 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える\n if (target - choices[i] < 0) {\n break;\n }\n // 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ\n if (i > start && choices[i] == choices[i - 1]) {\n continue;\n }\n // 試行:選択を行い、target、startを更新\n state.add(choices[i]);\n // 次のラウンドの選択に進む\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 回退:選択を取り消し、前の状態に復元\n state.remove(state.size() - 1);\n }\n}\n\n/* 部分集合和 II を解く */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n Arrays.sort(nums); // nums をソート\n int start = 0; // 走査の開始点\n List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.cs<pre><code>[class]{subset_sum_ii}-[func]{Backtrack}\n\n[class]{subset_sum_ii}-[func]{SubsetSumII}\n</code></pre> subset_sum_ii.go<pre><code>[class]{}-[func]{backtrackSubsetSumII}\n\n[class]{}-[func]{subsetSumII}\n</code></pre> subset_sum_ii.swift<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumII}\n</code></pre> subset_sum_ii.js<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumII}\n</code></pre> subset_sum_ii.ts<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumII}\n</code></pre> subset_sum_ii.dart<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumII}\n</code></pre> subset_sum_ii.rs<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subset_sum_ii}\n</code></pre> subset_sum_ii.c<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumII}\n</code></pre> subset_sum_ii.kt<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subsetSumII}\n</code></pre> subset_sum_ii.rb<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{subset_sum_ii}\n</code></pre> <p>以下の図は、配列 \\([4, 4, 5]\\) とターゲット要素 \\(9\\) のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。</p> <p></p> <p> 図 13-14 部分集合和 II のバックトラッキングプロセス </p>","path":["第 13 章 バックトラッキング","13.3 部分集合和問題"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5 まとめ","text":"","path":["第 13 章 バックトラッキング","13.5 まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#1","level":3,"title":"1. 重要な復習","text":"<ul> <li>バックトラッキングアルゴリズムの本質は全数探索です。解空間の深さ優先走査を実行することで条件を満たす解を求めます。検索中に満足のいく解が見つかった場合、それを記録し、すべての解が見つかるか走査が完了するまで続けます。</li> <li>バックトラッキングアルゴリズムの検索プロセスには試行と後退が含まれます。深さ優先探索を使用して様々な選択を探索し、選択が制約を満たさない場合、前の選択を取り消します。そして前の状態に戻って他のオプションを試し続けます。試行と後退は反対方向の操作です。</li> <li>バックトラッキング問題には通常複数の制約が含まれます。これらの制約は剪定操作を実行するために使用できます。剪定は不要な検索分岐を事前に終了し、検索効率を大幅に向上させることができます。</li> <li>バックトラッキングアルゴリズムは主に検索問題と制約満足問題を解決するために使用されます。組み合わせ最適化問題はバックトラッキングを使用して解決できますが、多くの場合、より効率的または効果的な解決方法が利用可能です。</li> <li>順列問題は、与えられた集合の要素のすべての可能な順列を検索することを目的とします。各要素が選択されたかどうかを記録するために配列を使用し、同じ要素の重複選択を避けます。これにより、各要素が一度だけ選択されることが保証されます。</li> <li>順列問題では、集合に重複要素が含まれている場合、最終結果に重複順列が含まれます。同一要素が各ラウンドで一度だけ選択できるように制限する必要があり、これは通常ハッシュセットを使用して実装されます。</li> <li>部分集合和問題は、与えられた集合でターゲット値に合計する全ての部分集合を見つけることを目的とします。集合は要素の順序を区別しませんが、検索プロセスでは重複する部分集合が生成される可能性があります。これは、アルゴリズムが異なる要素順序を独特のパスとして探索するために発生します。バックトラッキングの前に、データをソートし、各ラウンドの走査の開始点を示す変数を設定します。これにより、重複する部分集合を生成する検索分岐を剪定できます。</li> <li>部分集合和問題では、配列内の等しい要素は重複集合を生成する可能性があります。配列がすでにソートされているという前提条件を使用して、隣接する要素が等しいかどうかを判定することで剪定を行います。これにより、等しい要素がラウンドごとに一度だけ選択されることが保証されます。</li> <li>\\(n\\) クイーン問題は、2つのクイーンが互いに攻撃できないように \\(n \\times n\\) のチェスボードに \\(n\\) 個のクイーンを配置する方案を見つけることを目的とします。問題の制約には行制約、列制約、および主対角線と副対角線の制約が含まれます。行制約を満たすために、行ごとに1つのクイーンを配置する戦略を採用し、各行に1つのクイーンが配置されることを保証します。</li> <li>列制約と対角線制約の処理は似ています。列制約については、各列にクイーンがあるかどうかを記録する配列を使用し、選択されたセルが合法かどうかを示します。対角線制約については、2つの配列を使用して主対角線と副対角線にそれぞれクイーンの存在を記録します。課題は、同じ主対角線または副対角線上のセルの行と列のインデックス間の関係を決定することです。</li> </ul>","path":["第 13 章 バックトラッキング","13.5 まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: バックトラッキングと再帰の関係をどのように理解すればよいですか?</p> <p>全体的に、バックトラッキングは「アルゴリズム戦略」であり、再帰はより「ツール」です。</p> <ul> <li>バックトラッキングアルゴリズムは通常再帰に基づいています。しかし、バックトラッキングは再帰の応用シナリオの一つであり、特に検索問題においてです。</li> <li>再帰の構造は「部分問題分解」の問題解決パラダイムを反映します。分割統治、バックトラッキング、動的プログラミング(メモ化再帰)を含む問題の解決でよく使用されます。</li> </ul>","path":["第 13 章 バックトラッキング","13.5 まとめ"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章 複雑度解析","text":"<p>Abstract</p> <p>複雑度解析は、アルゴリズムの広大な宇宙における時空のナビゲーターのようなものです。</p> <p>時間と空間の次元をより深く探求し、より優雅な解決策を求めるためのガイドとなります。</p>","path":["第 2 章 計算量解析","第 2 章 複雑度解析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"章の内容","text":"<ul> <li>2.1 アルゴリズムの効率評価</li> <li>2.2 反復と再帰</li> <li>2.3 時間計算量</li> <li>2.4 空間計算量</li> <li>2.5 まとめ</li> </ul>","path":["第 2 章 計算量解析","第 2 章 複雑度解析"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2 反復と再帰","text":"<p>アルゴリズムにおいて、タスクの繰り返し実行は非常に一般的であり、複雑度の分析と密接に関係しています。したがって、時間計算量と空間計算量の概念を詳しく学ぶ前に、まずプログラミングで繰り返しタスクを実装する方法を探究しましょう。これには、2つの基本的なプログラミング制御構造である反復と再帰の理解が含まれます。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221","level":2,"title":"2.2.1 反復","text":"<p>反復は、タスクを繰り返し実行するための制御構造です。反復では、プログラムは特定の条件が満たされている限りコードブロックを繰り返し実行し、この条件が満たされなくなるまで続けます。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for","level":3,"title":"1. forループ","text":"<p><code>for</code>ループは反復の最も一般的な形式の1つであり、反復回数が事前に分かっている場合に特に適しています。</p> <p>以下の関数は<code>for</code>ループを使用して\\(1 + 2 + \\dots + n\\)の合計を実行し、合計を変数<code>res</code>に格納します。Pythonでは、<code>range(a, b)</code>は<code>a</code>を含み<code>b</code>を除く区間を作成することに注意してください。つまり、\\(a\\)から\\(b−1\\)までの範囲で反復します。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def for_loop(n: int) -> int:\n \"\"\"forループ\"\"\"\n res = 0\n # 1, 2, ..., n-1, n の合計をループ\n for i in range(1, n + 1):\n res += i\n return res\n</code></pre> iteration.cpp<pre><code>/* for ループ */\nint forLoop(int n) {\n int res = 0;\n // 1, 2, ..., n-1, n の合計をループ計算\n for (int i = 1; i <= n; ++i) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.java<pre><code>/* for ループ */\nint forLoop(int n) {\n int res = 0;\n // 1, 2, ..., n-1, n の合計をループ計算\n for (int i = 1; i <= n; i++) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.cs<pre><code>[class]{iteration}-[func]{ForLoop}\n</code></pre> iteration.go<pre><code>[class]{}-[func]{forLoop}\n</code></pre> iteration.swift<pre><code>[class]{}-[func]{forLoop}\n</code></pre> iteration.js<pre><code>[class]{}-[func]{forLoop}\n</code></pre> iteration.ts<pre><code>[class]{}-[func]{forLoop}\n</code></pre> iteration.dart<pre><code>[class]{}-[func]{forLoop}\n</code></pre> iteration.rs<pre><code>[class]{}-[func]{for_loop}\n</code></pre> iteration.c<pre><code>[class]{}-[func]{forLoop}\n</code></pre> iteration.kt<pre><code>[class]{}-[func]{forLoop}\n</code></pre> iteration.rb<pre><code>[class]{}-[func]{for_loop}\n</code></pre> <p>以下の図はこの合計関数を表しています。</p> <p></p> <p> 図 2-1 Flowchart of the sum function </p> <p>この合計関数での操作数は入力データのサイズ\\(n\\)に比例する、つまり線形関係があります。この「線形関係」こそが時間計算量が記述するものです。このトピックについては次のセクションで詳しく説明します。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while","level":3,"title":"2. whileループ","text":"<p><code>for</code>ループと同様に、<code>while</code>ループは反復を実装するためのもう1つのアプローチです。<code>while</code>ループでは、プログラムは各反復の開始時に条件をチェックし、条件が真の場合は実行を継続し、そうでなければループを終了します。</p> <p>以下では<code>while</code>ループを使用して合計\\(1 + 2 + \\dots + n\\)を実装します。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def while_loop(n: int) -> int:\n \"\"\"whileループ\"\"\"\n res = 0\n i = 1 # 条件変数を初期化\n # 1, 2, ..., n-1, n の合計をループ\n while i <= n:\n res += i\n i += 1 # 条件変数を更新\n return res\n</code></pre> iteration.cpp<pre><code>/* while ループ */\nint whileLoop(int n) {\n int res = 0;\n int i = 1; // 条件変数を初期化\n // 1, 2, ..., n-1, n の合計をループ計算\n while (i <= n) {\n res += i;\n i++; // 条件変数を更新\n }\n return res;\n}\n</code></pre> iteration.java<pre><code>/* while ループ */\nint whileLoop(int n) {\n int res = 0;\n int i = 1; // 条件変数を初期化\n // 1, 2, ..., n-1, n の合計をループ計算\n while (i <= n) {\n res += i;\n i++; // 条件変数を更新\n }\n return res;\n}\n</code></pre> iteration.cs<pre><code>[class]{iteration}-[func]{WhileLoop}\n</code></pre> iteration.go<pre><code>[class]{}-[func]{whileLoop}\n</code></pre> iteration.swift<pre><code>[class]{}-[func]{whileLoop}\n</code></pre> iteration.js<pre><code>[class]{}-[func]{whileLoop}\n</code></pre> iteration.ts<pre><code>[class]{}-[func]{whileLoop}\n</code></pre> iteration.dart<pre><code>[class]{}-[func]{whileLoop}\n</code></pre> iteration.rs<pre><code>[class]{}-[func]{while_loop}\n</code></pre> iteration.c<pre><code>[class]{}-[func]{whileLoop}\n</code></pre> iteration.kt<pre><code>[class]{}-[func]{whileLoop}\n</code></pre> iteration.rb<pre><code>[class]{}-[func]{while_loop}\n</code></pre> <p><code>while</code>ループは<code>for</code>ループよりも柔軟性を提供します。特に、条件変数のカスタム初期化と各ステップでの変更が可能です。</p> <p>例えば、以下のコードでは、条件変数\\(i\\)が各ラウンドで2回更新されますが、これは<code>for</code>ループでは実装が不便です。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def while_loop_ii(n: int) -> int:\n \"\"\"whileループ(2つの更新)\"\"\"\n res = 0\n i = 1 # 条件変数を初期化\n # 1, 4, 10, ... の合計をループ\n while i <= n:\n res += i\n # 条件変数を更新\n i += 1\n i *= 2\n return res\n</code></pre> iteration.cpp<pre><code>/* while ループ(2つの更新) */\nint whileLoopII(int n) {\n int res = 0;\n int i = 1; // 条件変数を初期化\n // 1, 4, 10, ... の合計をループ計算\n while (i <= n) {\n res += i;\n // 条件変数を更新\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.java<pre><code>/* while ループ(2つの更新) */\nint whileLoopII(int n) {\n int res = 0;\n int i = 1; // 条件変数を初期化\n // 1, 4, 10, ... の合計をループ計算\n while (i <= n) {\n res += i;\n // 条件変数を更新\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.cs<pre><code>[class]{iteration}-[func]{WhileLoopII}\n</code></pre> iteration.go<pre><code>[class]{}-[func]{whileLoopII}\n</code></pre> iteration.swift<pre><code>[class]{}-[func]{whileLoopII}\n</code></pre> iteration.js<pre><code>[class]{}-[func]{whileLoopII}\n</code></pre> iteration.ts<pre><code>[class]{}-[func]{whileLoopII}\n</code></pre> iteration.dart<pre><code>[class]{}-[func]{whileLoopII}\n</code></pre> iteration.rs<pre><code>[class]{}-[func]{while_loop_ii}\n</code></pre> iteration.c<pre><code>[class]{}-[func]{whileLoopII}\n</code></pre> iteration.kt<pre><code>[class]{}-[func]{whileLoopII}\n</code></pre> iteration.rb<pre><code>[class]{}-[func]{while_loop_ii}\n</code></pre> <p>全体的に、<code>for</code>ループはより簡潔で、<code>while</code>ループはより柔軟です。どちらも反復構造を実装できます。どちらを使用するかは、問題の具体的な要件に基づいて決定する必要があります。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3","level":3,"title":"3. ネストしたループ","text":"<p>1つのループ構造を別のループ構造内にネストできます。以下は<code>for</code>ループを使用した例です:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def nested_for_loop(n: int) -> str:\n \"\"\"二重forループ\"\"\"\n res = \"\"\n # i = 1, 2, ..., n-1, n をループ\n for i in range(1, n + 1):\n # j = 1, 2, ..., n-1, n をループ\n for j in range(1, n + 1):\n res += f\"({i}, {j}), \"\n return res\n</code></pre> iteration.cpp<pre><code>/* 2重 for ループ */\nstring nestedForLoop(int n) {\n ostringstream res;\n // ループ i = 1, 2, ..., n-1, n\n for (int i = 1; i <= n; ++i) {\n // ループ j = 1, 2, ..., n-1, n\n for (int j = 1; j <= n; ++j) {\n res << \"(\" << i << \", \" << j << \"), \";\n }\n }\n return res.str();\n}\n</code></pre> iteration.java<pre><code>/* 2重 for ループ */\nString nestedForLoop(int n) {\n StringBuilder res = new StringBuilder();\n // ループ i = 1, 2, ..., n-1, n\n for (int i = 1; i <= n; i++) {\n // ループ j = 1, 2, ..., n-1, n\n for (int j = 1; j <= n; j++) {\n res.append(\"(\" + i + \", \" + j + \"), \");\n }\n }\n return res.toString();\n}\n</code></pre> iteration.cs<pre><code>[class]{iteration}-[func]{NestedForLoop}\n</code></pre> iteration.go<pre><code>[class]{}-[func]{nestedForLoop}\n</code></pre> iteration.swift<pre><code>[class]{}-[func]{nestedForLoop}\n</code></pre> iteration.js<pre><code>[class]{}-[func]{nestedForLoop}\n</code></pre> iteration.ts<pre><code>[class]{}-[func]{nestedForLoop}\n</code></pre> iteration.dart<pre><code>[class]{}-[func]{nestedForLoop}\n</code></pre> iteration.rs<pre><code>[class]{}-[func]{nested_for_loop}\n</code></pre> iteration.c<pre><code>[class]{}-[func]{nestedForLoop}\n</code></pre> iteration.kt<pre><code>[class]{}-[func]{nestedForLoop}\n</code></pre> iteration.rb<pre><code>[class]{}-[func]{nested_for_loop}\n</code></pre> <p>以下の図はこのネストしたループを表しています。</p> <p></p> <p> 図 2-2 Flowchart of the nested loop </p> <p>このような場合、関数の操作数は\\(n^2\\)に比例します。つまり、アルゴリズムの実行時間と入力データのサイズ\\(n\\)には「二次関係」があります。</p> <p>さらにネストしたループを追加することで複雑度を高めることができ、各レベルのネストは事実上「次元を増加」させ、時間計算量を「三次」、「四次」などに引き上げます。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2 再帰","text":"<p>再帰は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に2つのフェーズが含まれます:</p> <ol> <li>呼び出し: プログラムが自分自身を繰り返し呼び出し、しばしばより小さいまたはより単純な引数で、「終了条件」に向かって進みます。</li> <li>返却: 「終了条件」がトリガーされると、プログラムは最も深い再帰関数から返り始め、各レイヤーの結果を集約します。</li> </ol> <p>実装の観点から、再帰コードは主に3つの要素を含みます。</p> <ol> <li>終了条件: 「呼び出し」から「返却」にいつ切り替えるかを決定します。</li> <li>再帰呼び出し: 「呼び出し」に対応し、関数が自分自身を呼び出し、通常はより小さいまたはより単純化されたパラメータで行います。</li> <li>結果の返却: 「返却」に対応し、現在の再帰レベルの結果が前のレイヤーに返されます。</li> </ol> <p>以下のコードを観察してください。単純に関数<code>recur(n)</code>を呼び出すだけで\\(1 + 2 + \\dots + n\\)の合計を計算できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def recur(n: int) -> int:\n \"\"\"再帰\"\"\"\n # 終了条件\n if n == 1:\n return 1\n # 再帰:再帰呼び出し\n res = recur(n - 1)\n # 復帰:結果を返す\n return n + res\n</code></pre> recursion.cpp<pre><code>/* 再帰 */\nint recur(int n) {\n // 終了条件\n if (n == 1)\n return 1;\n // 再帰:再帰呼び出し\n int res = recur(n - 1);\n // 戻り値:結果を返す\n return n + res;\n}\n</code></pre> recursion.java<pre><code>/* 再帰 */\nint recur(int n) {\n // 終了条件\n if (n == 1)\n return 1;\n // 再帰:再帰呼び出し\n int res = recur(n - 1);\n // 戻り値:結果を返す\n return n + res;\n}\n</code></pre> recursion.cs<pre><code>[class]{recursion}-[func]{Recur}\n</code></pre> recursion.go<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.swift<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.js<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.ts<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.dart<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.rs<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.c<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.kt<pre><code>[class]{}-[func]{recur}\n</code></pre> recursion.rb<pre><code>[class]{}-[func]{recur}\n</code></pre> <p>以下の図はこの関数の再帰プロセスを示しています。</p> <p></p> <p> 図 2-3 Recursive process of the sum function </p> <p>反復と再帰は計算の観点から同じ結果を達成できますが、それらは思考と問題解決の全く異なるパラダイムを表します。</p> <ul> <li>反復: 「ボトムアップ」で問題を解決します。最も基本的なステップから始まり、タスクが完了するまでこれらのステップを繰り返し追加または累積します。</li> <li>再帰: 「トップダウン」で問題を解決します。元の問題をより小さなサブ問題に分解し、各サブ問題は元の問題と同じ形式を持ちます。これらのサブ問題は、解が分かっているベースケースで停止するまで、さらに小さなサブ問題に分解されます。</li> </ul> <p>先ほどの合計関数の例を取ってみましょう。\\(f(n) = 1 + 2 + \\dots + n\\)として定義されます。</p> <ul> <li>反復: このアプローチでは、ループ内で合計プロセスをシミュレートします。\\(1\\)から始まり\\(n\\)まで横断し、各反復で合計操作を実行して最終的に\\(f(n)\\)を計算します。</li> <li>再帰: ここでは、問題はサブ問題に分解されます:\\(f(n) = n + f(n-1)\\)。この分解は、ベースケースの\\(f(1) = 1\\)に到達するまで再帰的に続き、そこで再帰が終了します。</li> </ul>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1","level":3,"title":"1. 呼び出しスタック","text":"<p>再帰関数が自分自身を呼び出すたびに、システムは新しく開始された関数にメモリを割り当てて、ローカル変数、戻りアドレス、その他の関連情報を格納します。これは2つの主要な結果をもたらします。</p> <ul> <li>関数のコンテキストデータは「スタックフレーム空間」と呼ばれるメモリ領域に格納され、関数が返された後にのみ解放されます。したがって、再帰は一般的に反復よりも多くのメモリ空間を消費します。</li> <li>再帰呼び出しは追加のオーバーヘッドを導入します。したがって、再帰は通常ループよりも時間効率が劣ります。</li> </ul> <p>以下の図に示されているように、終了条件がトリガーされる前に\\(n\\)個の未返却の再帰関数があり、再帰の深さが\\(n\\)であることを示しています。</p> <p></p> <p> 図 2-4 Recursion call depth </p> <p>実際には、プログラミング言語で許可される再帰の深さは通常制限されており、過度に深い再帰はスタックオーバーフローエラーを引き起こす可能性があります。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2","level":3,"title":"2. 末尾再帰","text":"<p>興味深いことに、関数が返す直前の最後のステップとして再帰呼び出しを実行する場合、コンパイラまたはインタープリターによって反復と同じ空間効率になるように最適化できます。このシナリオは末尾再帰として知られています。</p> <ul> <li>通常の再帰: 標準的な再帰では、関数が前のレベルに戻ったとき、さらにコードを実行し続けるため、システムは前の呼び出しのコンテキストを保存する必要があります。</li> <li>末尾再帰: ここでは、再帰呼び出しは関数が返す前の最終操作です。これは、前のレベルに戻った際に、さらなるアクションが必要ないことを意味するため、システムは前のレベルのコンテキストを保存する必要がありません。</li> </ul> <p>例えば、\\(1 + 2 + \\dots + n\\)の計算では、結果変数<code>res</code>を関数のパラメータにすることで、末尾再帰を実現できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def tail_recur(n, res):\n \"\"\"末尾再帰\"\"\"\n # 終了条件\n if n == 0:\n return res\n # 末尾再帰呼び出し\n return tail_recur(n - 1, res + n)\n</code></pre> recursion.cpp<pre><code>/* 末尾再帰 */\nint tailRecur(int n, int res) {\n // 終了条件\n if (n == 0)\n return res;\n // 末尾再帰呼び出し\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.java<pre><code>/* 末尾再帰 */\nint tailRecur(int n, int res) {\n // 終了条件\n if (n == 0)\n return res;\n // 末尾再帰呼び出し\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.cs<pre><code>[class]{recursion}-[func]{TailRecur}\n</code></pre> recursion.go<pre><code>[class]{}-[func]{tailRecur}\n</code></pre> recursion.swift<pre><code>[class]{}-[func]{tailRecur}\n</code></pre> recursion.js<pre><code>[class]{}-[func]{tailRecur}\n</code></pre> recursion.ts<pre><code>[class]{}-[func]{tailRecur}\n</code></pre> recursion.dart<pre><code>[class]{}-[func]{tailRecur}\n</code></pre> recursion.rs<pre><code>[class]{}-[func]{tail_recur}\n</code></pre> recursion.c<pre><code>[class]{}-[func]{tailRecur}\n</code></pre> recursion.kt<pre><code>[class]{}-[func]{tailRecur}\n</code></pre> recursion.rb<pre><code>[class]{}-[func]{tail_recur}\n</code></pre> <p>末尾再帰の実行プロセスは以下の図に示されています。通常の再帰と末尾再帰を比較すると、合計操作のポイントが異なります。</p> <ul> <li>通常の再帰: 合計操作は「返却」フェーズで発生し、各レイヤーが返った後にもう一度合計が必要です。</li> <li>末尾再帰: 合計操作は「呼び出し」フェーズで発生し、「返却」フェーズは各レイヤーを通じて返すだけです。</li> </ul> <p></p> <p> 図 2-5 Tail recursion process </p> <p>Tip</p> <p>多くのコンパイラやインタープリターは末尾再帰最適化をサポートしていないことに注意してください。例えば、Pythonはデフォルトで末尾再帰最適化をサポートしていないため、関数が末尾再帰の形式であっても、スタックオーバーフローの問題に遭遇する可能性があります。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3_1","level":3,"title":"3. 再帰木","text":"<p>「分割統治」に関連するアルゴリズムを扱う際、再帰は反復よりもしばしばより直感的なアプローチとより読みやすいコードを提供します。「フィボナッチ数列」を例に取ってみましょう。</p> <p>Question</p> <p>フィボナッチ数列\\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\)が与えられた場合、数列の\\(n\\)番目の数を求めなさい。</p> <p>フィボナッチ数列の\\(n\\)番目の数を\\(f(n)\\)とすると、2つの結論を簡単に導き出せます:</p> <ul> <li>数列の最初の2つの数は\\(f(1) = 0\\)と\\(f(2) = 1\\)です。</li> <li>数列の各数は前の2つの数の合計です。つまり、\\(f(n) = f(n - 1) + f(n - 2)\\)です。</li> </ul> <p>再帰関係を使用し、最初の2つの数を終了条件として考慮すると、再帰コードを書けます。<code>fib(n)</code>を呼び出すとフィボナッチ数列の\\(n\\)番目の数が得られます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def fib(n: int) -> int:\n \"\"\"フィボナッチ数列:再帰\"\"\"\n # 終了条件 f(1) = 0, f(2) = 1\n if n == 1 or n == 2:\n return n - 1\n # 再帰呼び出し f(n) = f(n-1) + f(n-2)\n res = fib(n - 1) + fib(n - 2)\n # 結果 f(n) を返す\n return res\n</code></pre> recursion.cpp<pre><code>/* フィボナッチ数列:再帰 */\nint fib(int n) {\n // 終了条件 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2)\n return n - 1;\n // 再帰呼び出し f(n) = f(n-1) + f(n-2)\n int res = fib(n - 1) + fib(n - 2);\n // 結果 f(n) を返す\n return res;\n}\n</code></pre> recursion.java<pre><code>/* フィボナッチ数列:再帰 */\nint fib(int n) {\n // 終了条件 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2)\n return n - 1;\n // 再帰呼び出し f(n) = f(n-1) + f(n-2)\n int res = fib(n - 1) + fib(n - 2);\n // 結果 f(n) を返す\n return res;\n}\n</code></pre> recursion.cs<pre><code>[class]{recursion}-[func]{Fib}\n</code></pre> recursion.go<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.swift<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.js<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.ts<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.dart<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.rs<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.c<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.kt<pre><code>[class]{}-[func]{fib}\n</code></pre> recursion.rb<pre><code>[class]{}-[func]{fib}\n</code></pre> <p>上記のコードを観察すると、それ自体の中で2つの関数を再帰的に呼び出していることがわかります。つまり、1回の呼び出しで2つの分岐呼び出しが生成されます。以下の図に示されているように、この継続的な再帰呼び出しは最終的に深さ\\(n\\)の再帰木を作成します。</p> <p></p> <p> 図 2-6 Fibonacci sequence recursion tree </p> <p>基本的に、再帰は「問題をより小さなサブ問題に分解する」パラダイムを体現しています。この分割統治戦略は重要です。</p> <ul> <li>アルゴリズムの観点から、探索、ソート、バックトラッキング、分割統治、動的プログラミングなどの多くの重要な戦略は、直接的または間接的にこの思考方法を使用しています。</li> <li>データ構造の観点から、再帰は連結リスト、木、グラフを扱うのに自然に適しており、これらは分割統治アプローチを使用した分析に適しているためです。</li> </ul>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3 比較","text":"<p>上記の内容をまとめると、以下の表は実装、性能、適用性の観点から反復と再帰の違いを示しています。</p> <p> 表: 反復と再帰の特性の比較 </p> 反復 再帰 アプローチ ループ構造 関数が自分自身を呼び出す 時間効率 一般的により高い効率、関数呼び出しのオーバーヘッドなし 各関数呼び出しがオーバーヘッドを生成 メモリ使用量 通常は固定サイズのメモリ空間を使用 累積的な関数呼び出しが大量のスタックフレーム空間を使用する可能性 適用可能な問題 単純なループタスクに適している、直感的で読みやすいコード 問題の分解に適している(木、グラフ、分割統治、バックトラッキングなど)、簡潔で明確なコード構造 <p>Tip</p> <p>以下の内容が理解しにくい場合は、「スタック」の章を読んだ後に再び訪れることを検討してください。</p> <p>それでは、反復と再帰の本質的な関連は何でしょうか?上記の再帰関数を例に取ると、合計操作は再帰の「返却」フェーズで発生します。これは、最初に呼び出された関数が最後に合計操作を完了することを意味し、スタックの「後入れ先出し」原理を反映しています。</p> <p>「呼び出しスタック」や「スタックフレーム空間」などの再帰用語は、再帰とスタックの密接な関係を示しています。</p> <ol> <li>呼び出し: 関数が呼び出されると、システムは「呼び出しスタック」上にその関数用の新しいスタックフレームを割り当て、ローカル変数、パラメータ、戻りアドレス、その他のデータを格納します。</li> <li>返却: 関数が実行を完了して返ると、対応するスタックフレームが「呼び出しスタック」から削除され、前の関数の実行環境が復元されます。</li> </ol> <p>したがって、明示的なスタックを使用して呼び出しスタックの動作をシミュレートできます。これにより再帰を反復形式に変換できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def for_loop_recur(n: int) -> int:\n \"\"\"反復で再帰をシミュレート\"\"\"\n # 明示的なスタックを使用してシステムコールスタックをシミュレート\n stack = []\n res = 0\n # 再帰:再帰呼び出し\n for i in range(n, 0, -1):\n # 「スタックへのプッシュ」で「再帰」をシミュレート\n stack.append(i)\n # 復帰:結果を返す\n while stack:\n # 「スタックからのポップ」で「復帰」をシミュレート\n res += stack.pop()\n # res = 1+2+3+...+n\n return res\n</code></pre> recursion.cpp<pre><code>/* 反復で再帰をシミュレート */\nint forLoopRecur(int n) {\n // 明示的なスタックを使用してシステムコールスタックをシミュレート\n stack<int> stack;\n int res = 0;\n // 再帰:再帰呼び出し\n for (int i = n; i > 0; i--) {\n // 「スタックへのプッシュ」で「再帰」をシミュレート\n stack.push(i);\n }\n // 戻り値:結果を返す\n while (!stack.empty()) {\n // 「スタックからのポップ」で「戻り値」をシミュレート\n res += stack.top();\n stack.pop();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.java<pre><code>/* 反復で再帰をシミュレート */\nint forLoopRecur(int n) {\n // 明示的なスタックを使用してシステムコールスタックをシミュレート\n Stack<Integer> stack = new Stack<>();\n int res = 0;\n // 再帰:再帰呼び出し\n for (int i = n; i > 0; i--) {\n // 「スタックへのプッシュ」で「再帰」をシミュレート\n stack.push(i);\n }\n // 戻り値:結果を返す\n while (!stack.isEmpty()) {\n // 「スタックからのポップ」で「戻り値」をシミュレート\n res += stack.pop();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.cs<pre><code>[class]{recursion}-[func]{ForLoopRecur}\n</code></pre> recursion.go<pre><code>[class]{}-[func]{forLoopRecur}\n</code></pre> recursion.swift<pre><code>[class]{}-[func]{forLoopRecur}\n</code></pre> recursion.js<pre><code>[class]{}-[func]{forLoopRecur}\n</code></pre> recursion.ts<pre><code>[class]{}-[func]{forLoopRecur}\n</code></pre> recursion.dart<pre><code>[class]{}-[func]{forLoopRecur}\n</code></pre> recursion.rs<pre><code>[class]{}-[func]{for_loop_recur}\n</code></pre> recursion.c<pre><code>[class]{}-[func]{forLoopRecur}\n</code></pre> recursion.kt<pre><code>[class]{}-[func]{forLoopRecur}\n</code></pre> recursion.rb<pre><code>[class]{}-[func]{for_loop_recur}\n</code></pre> <p>上記のコードを観察すると、再帰が反復に変換されたとき、コードはより複雑になります。反復と再帰はしばしば相互に変換できますが、2つの理由でそうすることが常に推奨されるわけではありません:</p> <ul> <li>変換されたコードは理解がより困難になり、読みにくくなる可能性があります。</li> <li>一部の複雑な問題では、システムの呼び出しスタックの動作をシミュレートすることは非常に困難です。</li> </ul> <p>結論として、反復または再帰を選択するかは問題の具体的な性質によります。プログラミングの実践では、両方の長所と短所を比較検討し、手元の状況に最も適したアプローチを選択することが重要です。</p>","path":["第 2 章 計算量解析","2.2 反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1 アルゴリズムの効率評価","text":"<p>アルゴリズム設計において、私たちは順序に従って以下の2つの目標を追求します。</p> <ol> <li>問題の解決策を見つける: アルゴリズムは、指定された入力範囲内で確実に正しい解を見つけることができるべきです。</li> <li>最適解を求める: 同じ問題に対して複数の解決策が存在する場合があり、私たちは可能な限り最も効率的なアルゴリズムを見つけることを目指します。</li> </ol> <p>つまり、問題を解決できることを前提として、アルゴリズムの効率がアルゴリズムを評価する主要な基準となっており、これには以下の2つの次元が含まれます。</p> <ul> <li>時間効率: アルゴリズムが実行される速度。</li> <li>空間効率: アルゴリズムが占有するメモリ空間のサイズ。</li> </ul> <p>要するに、私たちの目標は、高速でメモリ効率の良いデータ構造とアルゴリズムを設計することです。アルゴリズムの効率を効果的に評価することは重要です。なぜなら、そうすることで初めて様々なアルゴリズムを比較し、アルゴリズムの設計と最適化プロセスを導くことができるからです。</p> <p>効率評価には主に2つの方法があります:実際のテストと理論的推定です。</p>","path":["第 2 章 計算量解析","2.1 アルゴリズムの効率評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1 実際のテスト","text":"<p>アルゴリズム<code>A</code>と<code>B</code>があり、どちらも同じ問題を解決でき、それらの効率を比較する必要があるとします。最も直接的な方法は、コンピュータを使用してこれら2つのアルゴリズムを実行し、実行時間とメモリ使用量を監視・記録することです。この評価方法は実際の状況を反映しますが、大きな制限があります。</p> <p>一方で、テスト環境からの干渉を排除することは困難です。ハードウェア構成はアルゴリズムの性能に影響を与える可能性があります。例えば、並列度の高いアルゴリズムはマルチコアCPUでの実行により適していますし、集約的なメモリ操作を含むアルゴリズムは高性能メモリでより良い性能を発揮します。アルゴリズムのテスト結果は、異なるマシン間で変わる可能性があります。これは、平均効率を計算するために複数のマシンでテストすることが実用的でないことを意味します。</p> <p>一方で、完全なテストを実施することは非常にリソース集約的です。アルゴリズムの効率は入力データサイズによって変わります。例えば、データ量が少ない場合はアルゴリズム<code>A</code>が<code>B</code>より速く実行される可能性がありますが、データ量が多い場合はテスト結果が逆になる可能性があります。したがって、説得力のある結論を導くためには、幅広い入力データサイズをテストする必要があり、これには過度な計算リソースが必要になります。</p>","path":["第 2 章 計算量解析","2.1 アルゴリズムの効率評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212","level":2,"title":"2.1.2 理論的推定","text":"<p>実際のテストの大きな制限により、計算のみでアルゴリズムの効率を評価することを検討できます。この推定方法は漸近的複雑度解析、または単に複雑度解析として知られています。</p> <p>複雑度解析は、アルゴリズムの実行に必要な時間と空間リソースと入力データのサイズとの関係を反映します。これは、入力データのサイズが増加するにつれて、アルゴリズムに必要な時間と空間の増加傾向を記述します。この定義は複雑に聞こえるかもしれませんが、より良く理解するために3つの重要なポイントに分解できます。</p> <ul> <li>「時間と空間リソース」は、それぞれ時間計算量と空間計算量に対応します。</li> <li>「入力データのサイズが増加するにつれて」は、複雑度がアルゴリズムの効率と入力データ量との関係を反映することを意味します。</li> <li>「時間と空間の増加傾向」は、複雑度解析が実行時間や占有空間の具体的な値ではなく、時間や空間が増加する「率」に焦点を当てることを示します。</li> </ul> <p>複雑度解析は実際のテスト方法の欠点を克服します。これは以下の側面で反映されます:</p> <ul> <li>実際にコードを実行する必要がないため、より環境に優しく、エネルギー効率が良いです。</li> <li>テスト環境に依存せず、すべての動作プラットフォームに適用できます。</li> <li>異なるデータ量でのアルゴリズムの効率を反映でき、特に大量データでのアルゴリズムの性能を示します。</li> </ul> <p>Tip</p> <p>複雑度の概念についてまだ混乱している場合でも、心配しないでください。以降の章で詳しく取り上げます。</p> <p>複雑度解析は、アルゴリズムの効率を評価する「ものさし」を提供し、実行に必要な時間と空間リソースを測定し、異なるアルゴリズムの効率を比較することを可能にします。</p> <p>複雑度は数学的概念であり、初心者には抽象的で困難かもしれません。この観点から、複雑度解析は最初に紹介するのに最も適したトピックではないかもしれません。しかし、特定のデータ構造やアルゴリズムの特性について議論するとき、その速度と空間使用量を分析することを避けるのは困難です。</p> <p>要約すると、データ構造とアルゴリズムに深く入る前に複雑度解析の基本的な理解を身につけることをお勧めします。これにより、簡単なアルゴリズムで複雑度解析を実行できるようになります。</p>","path":["第 2 章 計算量解析","2.1 アルゴリズムの効率評価"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4 空間計算量","text":"<p>空間計算量は、データ量が増加するにつれてアルゴリズムが占有するメモリ空間の増加傾向を測定するために使用されます。この概念は時間計算量と非常に似ていますが、「実行時間」が「占有メモリ空間」に置き換えられています。</p>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241","level":2,"title":"2.4.1 アルゴリズムに関連する空間","text":"<p>アルゴリズムが実行中に使用するメモリ空間には、主に以下の種類があります。</p> <ul> <li>入力空間: アルゴリズムの入力データを格納するために使用されます。</li> <li>一時空間: アルゴリズムの実行中に変数、オブジェクト、関数コンテキスト、その他のデータを格納するために使用されます。</li> <li>出力空間: アルゴリズムの出力データを格納するために使用されます。</li> </ul> <p>一般的に、空間計算量の統計範囲には「一時空間」と「出力空間」の両方が含まれます。</p> <p>一時空間はさらに3つの部分に分けることができます。</p> <ul> <li>一時データ: アルゴリズムの実行中に様々な定数、変数、オブジェクトなどを保存するために使用されます。</li> <li>スタックフレーム空間: 呼び出された関数のコンテキストデータを保存するために使用されます。システムは関数が呼び出されるたびにスタックの頂上にスタックフレームを作成し、関数が返された後にスタックフレーム空間を解放します。</li> <li>命令空間: コンパイル済みプログラム命令を格納するために使用され、実際の統計では通常無視できます。</li> </ul> <p>プログラムの空間計算量を分析する際、通常は一時データ、スタックフレーム空間、出力データをカウントします。以下の図に示されています。</p> <p></p> <p> 図 2-15 Space types used in algorithms </p> <p>関連するコードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>class Node:\n \"\"\"クラス\"\"\"\n def __init__(self, x: int):\n self.val: int = x # ノード値\n self.next: Node | None = None # 次のノードへの参照\n\ndef function() -> int:\n \"\"\"関数\"\"\"\n # 特定の操作を実行...\n return 0\n\ndef algorithm(n) -> int: # 入力データ\n A = 0 # 一時データ(定数、通常大文字)\n b = 0 # 一時データ(変数)\n node = Node(0) # 一時データ(オブジェクト)\n c = function() # スタックフレーム空間(関数呼び出し)\n return A + b + c # 出力データ\n</code></pre> <pre><code>/* 構造体 */\nstruct Node {\n int val;\n Node *next;\n Node(int x) : val(x), next(nullptr) {}\n};\n\n/* 関数 */\nint func() {\n // 特定の操作を実行...\n return 0;\n}\n\nint algorithm(int n) { // 入力データ\n const int a = 0; // 一時データ(定数)\n int b = 0; // 一時データ(変数)\n Node* node = new Node(0); // 一時データ(オブジェクト)\n int c = func(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>/* クラス */\nclass Node {\n int val;\n Node next;\n Node(int x) { val = x; }\n}\n\n/* 関数 */\nint function() {\n // 特定の操作を実行...\n return 0;\n}\n\nint algorithm(int n) { // 入力データ\n final int a = 0; // 一時データ(定数)\n int b = 0; // 一時データ(変数)\n Node node = new Node(0); // 一時データ(オブジェクト)\n int c = function(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>/* クラス */\nclass Node {\n int val;\n Node next;\n Node(int x) { val = x; }\n}\n\n/* 関数 */\nint Function() {\n // 特定の操作を実行...\n return 0;\n}\n\nint Algorithm(int n) { // 入力データ\n const int a = 0; // 一時データ(定数)\n int b = 0; // 一時データ(変数)\n Node node = new(0); // 一時データ(オブジェクト)\n int c = Function(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>/* 構造体 */\ntype node struct {\n val int\n next *node\n}\n\n/* ノード構造体を作成 */\nfunc newNode(val int) *node {\n return &node{val: val}\n}\n\n/* 関数 */\nfunc function() int {\n // 特定の操作を実行...\n return 0\n}\n\nfunc algorithm(n int) int { // 入力データ\n const a = 0 // 一時データ(定数)\n b := 0 // 一時データ(変数)\n newNode(0) // 一時データ(オブジェクト)\n c := function() // スタックフレーム空間(関数呼び出し)\n return a + b + c // 出力データ\n}\n</code></pre> <pre><code>/* クラス */\nclass Node {\n var val: Int\n var next: Node?\n\n init(x: Int) {\n val = x\n }\n}\n\n/* 関数 */\nfunc function() -> Int {\n // 特定の操作を実行...\n return 0\n}\n\nfunc algorithm(n: Int) -> Int { // 入力データ\n let a = 0 // 一時データ(定数)\n var b = 0 // 一時データ(変数)\n let node = Node(x: 0) // 一時データ(オブジェクト)\n let c = function() // スタックフレーム空間(関数呼び出し)\n return a + b + c // 出力データ\n}\n</code></pre> <pre><code>/* クラス */\nclass Node {\n val;\n next;\n constructor(val) {\n this.val = val === undefined ? 0 : val; // ノード値\n this.next = null; // 次のノードへの参照\n }\n}\n\n/* 関数 */\nfunction constFunc() {\n // 特定の操作を実行\n return 0;\n}\n\nfunction algorithm(n) { // 入力データ\n const a = 0; // 一時データ(定数)\n let b = 0; // 一時データ(変数)\n const node = new Node(0); // 一時データ(オブジェクト)\n const c = constFunc(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>/* クラス */\nclass Node {\n val: number;\n next: Node | null;\n constructor(val?: number) {\n this.val = val === undefined ? 0 : val; // ノード値\n this.next = null; // 次のノードへの参照\n }\n}\n\n/* 関数 */\nfunction constFunc(): number {\n // 特定の操作を実行\n return 0;\n}\n\nfunction algorithm(n: number): number { // 入力データ\n const a = 0; // 一時データ(定数)\n let b = 0; // 一時データ(変数)\n const node = new Node(0); // 一時データ(オブジェクト)\n const c = constFunc(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>/* クラス */\nclass Node {\n int val;\n Node next;\n Node(this.val, [this.next]);\n}\n\n/* 関数 */\nint function() {\n // 特定の操作を実行...\n return 0;\n}\n\nint algorithm(int n) { // 入力データ\n const int a = 0; // 一時データ(定数)\n int b = 0; // 一時データ(変数)\n Node node = Node(0); // 一時データ(オブジェクト)\n int c = function(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 構造体 */\nstruct Node {\n val: i32,\n next: Option<Rc<RefCell<Node>>>,\n}\n\n/* コンストラクタ */\nimpl Node {\n fn new(val: i32) -> Self {\n Self { val: val, next: None }\n }\n}\n\n/* 関数 */\nfn function() -> i32 {\n // 特定の操作を実行...\n return 0;\n}\n\nfn algorithm(n: i32) -> i32 { // 入力データ\n const a: i32 = 0; // 一時データ(定数)\n let mut b = 0; // 一時データ(変数)\n let node = Node::new(0); // 一時データ(オブジェクト)\n let c = function(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>/* 関数 */\nint func() {\n // 特定の操作を実行...\n return 0;\n}\n\nint algorithm(int n) { // 入力データ\n const int a = 0; // 一時データ(定数)\n int b = 0; // 一時データ(変数)\n int c = func(); // スタックフレーム空間(関数呼び出し)\n return a + b + c; // 出力データ\n}\n</code></pre> <pre><code>\n</code></pre>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242","level":2,"title":"2.4.2 計算方法","text":"<p>空間計算量を計算する方法は時間計算量とほぼ同様で、統計対象を「操作数」から「使用空間のサイズ」に変更するだけです。</p> <p>しかし、時間計算量とは異なり、通常は最悪ケース空間計算量のみに焦点を当てます。これは、メモリ空間がハード要件であり、すべての入力データの下で十分なメモリ空間が確保されていることを保証する必要があるためです。</p> <p>以下のコードを考えてみましょう。最悪ケース空間計算量の「最悪ケース」という用語には2つの意味があります。</p> <ol> <li>最悪の入力データに基づく: \\(n < 10\\)の場合、空間計算量は\\(O(1)\\)ですが、\\(n > 10\\)の場合、初期化された配列<code>nums</code>が\\(O(n)\\)の空間を占有するため、最悪ケース空間計算量は\\(O(n)\\)です。</li> <li>アルゴリズムの実行中に使用されるピークメモリに基づく: 例えば、最後の行を実行する前、プログラムは\\(O(1)\\)の空間を占有します。配列<code>nums</code>を初期化する際、プログラムは\\(O(n)\\)の空間を占有するため、最悪ケース空間計算量は\\(O(n)\\)です。</li> </ol> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>def algorithm(n: int):\n a = 0 # O(1)\n b = [0] * 10000 # O(1)\n if n > 10:\n nums = [0] * n # O(n)\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n vector<int> b(10000); // O(1)\n if (n > 10)\n vector<int> nums(n); // O(n)\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n int[] b = new int[10000]; // O(1)\n if (n > 10)\n int[] nums = new int[n]; // O(n)\n}\n</code></pre> <pre><code>void Algorithm(int n) {\n int a = 0; // O(1)\n int[] b = new int[10000]; // O(1)\n if (n > 10) {\n int[] nums = new int[n]; // O(n)\n }\n}\n</code></pre> <pre><code>func algorithm(n int) {\n a := 0 // O(1)\n b := make([]int, 10000) // O(1)\n var nums []int\n if n > 10 {\n nums := make([]int, n) // O(n)\n }\n fmt.Println(a, b, nums)\n}\n</code></pre> <pre><code>func algorithm(n: Int) {\n let a = 0 // O(1)\n let b = Array(repeating: 0, count: 10000) // O(1)\n if n > 10 {\n let nums = Array(repeating: 0, count: n) // O(n)\n }\n}\n</code></pre> <pre><code>function algorithm(n) {\n const a = 0; // O(1)\n const b = new Array(10000); // O(1)\n if (n > 10) {\n const nums = new Array(n); // O(n)\n }\n}\n</code></pre> <pre><code>function algorithm(n: number): void {\n const a = 0; // O(1)\n const b = new Array(10000); // O(1)\n if (n > 10) {\n const nums = new Array(n); // O(n)\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n List<int> b = List.filled(10000, 0); // O(1)\n if (n > 10) {\n List<int> nums = List.filled(n, 0); // O(n)\n }\n}\n</code></pre> <pre><code>fn algorithm(n: i32) {\n let a = 0; // O(1)\n let b = [0; 10000]; // O(1)\n if n > 10 {\n let nums = vec![0; n as usize]; // O(n)\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n int b[10000]; // O(1)\n if (n > 10)\n int nums[n] = {0}; // O(n)\n}\n</code></pre> <pre><code>\n</code></pre> <p>再帰関数では、スタックフレーム空間を考慮に入れる必要があります。以下のコードを考えてみましょう:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>def function() -> int:\n # 特定の操作を実行\n return 0\n\ndef loop(n: int):\n \"\"\"ループ O(1)\"\"\"\n for _ in range(n):\n function()\n\ndef recur(n: int):\n \"\"\"再帰 O(n)\"\"\"\n if n == 1:\n return\n return recur(n - 1)\n</code></pre> <pre><code>int func() {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nvoid loop(int n) {\n for (int i = 0; i < n; i++) {\n func();\n }\n}\n/* 再帰 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>int function() {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nvoid loop(int n) {\n for (int i = 0; i < n; i++) {\n function();\n }\n}\n/* 再帰 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>int Function() {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nvoid Loop(int n) {\n for (int i = 0; i < n; i++) {\n Function();\n }\n}\n/* 再帰 O(n) */\nint Recur(int n) {\n if (n == 1) return 1;\n return Recur(n - 1);\n}\n</code></pre> <pre><code>func function() int {\n // 特定の操作を実行\n return 0\n}\n\n/* サイクル O(1) */\nfunc loop(n int) {\n for i := 0; i < n; i++ {\n function()\n }\n}\n\n/* 再帰 O(n) */\nfunc recur(n int) {\n if n == 1 {\n return\n }\n recur(n - 1)\n}\n</code></pre> <pre><code>@discardableResult\nfunc function() -> Int {\n // 特定の操作を実行\n return 0\n}\n\n/* サイクル O(1) */\nfunc loop(n: Int) {\n for _ in 0 ..< n {\n function()\n }\n}\n\n/* 再帰 O(n) */\nfunc recur(n: Int) {\n if n == 1 {\n return\n }\n recur(n: n - 1)\n}\n</code></pre> <pre><code>function constFunc() {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nfunction loop(n) {\n for (let i = 0; i < n; i++) {\n constFunc();\n }\n}\n/* 再帰 O(n) */\nfunction recur(n) {\n if (n === 1) return;\n return recur(n - 1);\n}\n</code></pre> <pre><code>function constFunc(): number {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nfunction loop(n: number): void {\n for (let i = 0; i < n; i++) {\n constFunc();\n }\n}\n/* 再帰 O(n) */\nfunction recur(n: number): void {\n if (n === 1) return;\n return recur(n - 1);\n}\n</code></pre> <pre><code>int function() {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nvoid loop(int n) {\n for (int i = 0; i < n; i++) {\n function();\n }\n}\n/* 再帰 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>fn function() -> i32 {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nfn loop(n: i32) {\n for i in 0..n {\n function();\n }\n}\n/* 再帰 O(n) */\nvoid recur(n: i32) {\n if n == 1 {\n return;\n }\n recur(n - 1);\n}\n</code></pre> <pre><code>int func() {\n // 特定の操作を実行\n return 0;\n}\n/* サイクル O(1) */\nvoid loop(int n) {\n for (int i = 0; i < n; i++) {\n func();\n }\n}\n/* 再帰 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>\n</code></pre> <p><code>loop()</code>関数と<code>recur()</code>関数の時間計算量は両方とも\\(O(n)\\)ですが、それらの空間計算量は異なります。</p> <ul> <li><code>loop()</code>関数はループ内で<code>function()</code>を\\(n\\)回呼び出し、各反復の<code>function()</code>は返ってそのスタックフレーム空間を解放するため、空間計算量は\\(O(1)\\)のままです。</li> <li>再帰関数<code>recur()</code>は実行中に\\(n\\)個の未返却の<code>recur()</code>インスタンスが同時に存在するため、\\(O(n)\\)のスタックフレーム空間を占有します。</li> </ul>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3 一般的な種類","text":"<p>入力データのサイズを\\(n\\)とすると、下図は一般的な空間計算量の種類を示しています(低いものから高いものへと並べられています)。</p> \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline & \\text{定数} < \\text{対数} < \\text{線形} < \\text{二次} < \\text{指数} \\end{aligned} \\] <p></p> <p> 図 2-16 Common types of space complexity </p>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-o1","level":3,"title":"1. 定数オーダー \\(O(1)\\)","text":"<p>定数オーダーは、入力データサイズ\\(n\\)とは無関係な定数、変数、オブジェクトで一般的です。</p> <p>ループで変数を初期化したり関数を呼び出したりするために占有されるメモリは、次のサイクルに入る際に解放され、空間上で累積されないため、空間計算量は\\(O(1)\\)のままです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def function() -> int:\n \"\"\"関数\"\"\"\n # 何らかの操作を実行\n return 0\n\ndef constant(n: int):\n \"\"\"定数複雑度\"\"\"\n # 定数、変数、オブジェクトは O(1) のスペースを占有\n a = 0\n nums = [0] * 10000\n node = ListNode(0)\n # ループ内の変数は O(1) のスペースを占有\n for _ in range(n):\n c = 0\n # ループ内の関数は O(1) のスペースを占有\n for _ in range(n):\n function()\n</code></pre> space_complexity.cpp<pre><code>/* 関数 */\nint func() {\n // 何らかの操作を実行\n return 0;\n}\n\n/* 定数計算量 */\nvoid constant(int n) {\n // 定数、変数、オブジェクトは O(1) 空間を占める\n const int a = 0;\n int b = 0;\n vector<int> nums(10000);\n ListNode node(0);\n // ループ内の変数は O(1) 空間を占める\n for (int i = 0; i < n; i++) {\n int c = 0;\n }\n // ループ内の関数は O(1) 空間を占める\n for (int i = 0; i < n; i++) {\n func();\n }\n}\n</code></pre> space_complexity.java<pre><code>/* 関数 */\nint function() {\n // 何らかの操作を実行\n return 0;\n}\n\n/* 定数計算量 */\nvoid constant(int n) {\n // 定数、変数、オブジェクトは O(1) 空間を占める\n final int a = 0;\n int b = 0;\n int[] nums = new int[10000];\n ListNode node = new ListNode(0);\n // ループ内の変数は O(1) 空間を占める\n for (int i = 0; i < n; i++) {\n int c = 0;\n }\n // ループ内の関数は O(1) 空間を占める\n for (int i = 0; i < n; i++) {\n function();\n }\n}\n</code></pre> space_complexity.cs<pre><code>[class]{space_complexity}-[func]{Function}\n\n[class]{space_complexity}-[func]{Constant}\n</code></pre> space_complexity.go<pre><code>[class]{}-[func]{function}\n\n[class]{}-[func]{spaceConstant}\n</code></pre> space_complexity.swift<pre><code>[class]{}-[func]{function}\n\n[class]{}-[func]{constant}\n</code></pre> space_complexity.js<pre><code>[class]{}-[func]{constFunc}\n\n[class]{}-[func]{constant}\n</code></pre> space_complexity.ts<pre><code>[class]{}-[func]{constFunc}\n\n[class]{}-[func]{constant}\n</code></pre> space_complexity.dart<pre><code>[class]{}-[func]{function}\n\n[class]{}-[func]{constant}\n</code></pre> space_complexity.rs<pre><code>[class]{}-[func]{function}\n\n[class]{}-[func]{constant}\n</code></pre> space_complexity.c<pre><code>[class]{}-[func]{func}\n\n[class]{}-[func]{constant}\n</code></pre> space_complexity.kt<pre><code>[class]{}-[func]{function}\n\n[class]{}-[func]{constant}\n</code></pre> space_complexity.rb<pre><code>[class]{}-[func]{function}\n\n[class]{}-[func]{constant}\n</code></pre>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-on","level":3,"title":"2. 線形オーダー \\(O(n)\\)","text":"<p>線形オーダーは配列、連結リスト、スタック、キューなどで一般的で、要素数は\\(n\\)に比例します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def linear(n: int):\n \"\"\"線形複雑度\"\"\"\n # 長さ n のリストは O(n) のスペースを占有\n nums = [0] * n\n # 長さ n のハッシュマップは O(n) のスペースを占有\n hmap = dict[int, str]()\n for i in range(n):\n hmap[i] = str(i)\n</code></pre> space_complexity.cpp<pre><code>/* 線形計算量 */\nvoid linear(int n) {\n // 長さ n の配列は O(n) 空間を占める\n vector<int> nums(n);\n // 長さ n のリストは O(n) 空間を占める\n vector<ListNode> nodes;\n for (int i = 0; i < n; i++) {\n nodes.push_back(ListNode(i));\n }\n // 長さ n のハッシュテーブルは O(n) 空間を占める\n unordered_map<int, string> map;\n for (int i = 0; i < n; i++) {\n map[i] = to_string(i);\n }\n}\n</code></pre> space_complexity.java<pre><code>/* 線形計算量 */\nvoid linear(int n) {\n // 長さ n の配列は O(n) 空間を占める\n int[] nums = new int[n];\n // 長さ n のリストは O(n) 空間を占める\n List<ListNode> nodes = new ArrayList<>();\n for (int i = 0; i < n; i++) {\n nodes.add(new ListNode(i));\n }\n // 長さ n のハッシュテーブルは O(n) 空間を占める\n Map<Integer, String> map = new HashMap<>();\n for (int i = 0; i < n; i++) {\n map.put(i, String.valueOf(i));\n }\n}\n</code></pre> space_complexity.cs<pre><code>[class]{space_complexity}-[func]{Linear}\n</code></pre> space_complexity.go<pre><code>[class]{}-[func]{spaceLinear}\n</code></pre> space_complexity.swift<pre><code>[class]{}-[func]{linear}\n</code></pre> space_complexity.js<pre><code>[class]{}-[func]{linear}\n</code></pre> space_complexity.ts<pre><code>[class]{}-[func]{linear}\n</code></pre> space_complexity.dart<pre><code>[class]{}-[func]{linear}\n</code></pre> space_complexity.rs<pre><code>[class]{}-[func]{linear}\n</code></pre> space_complexity.c<pre><code>[class]{HashTable}-[func]{}\n\n[class]{}-[func]{linear}\n</code></pre> space_complexity.kt<pre><code>[class]{}-[func]{linear}\n</code></pre> space_complexity.rb<pre><code>[class]{}-[func]{linear}\n</code></pre> <p>下図に示されているように、この関数の再帰深度は\\(n\\)で、\\(n\\)個の未返却の<code>linear_recur()</code>関数インスタンスがあり、\\(O(n)\\)サイズのスタックフレーム空間を使用します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def linear_recur(n: int):\n \"\"\"線形複雑度(再帰実装)\"\"\"\n print(\"再帰 n =\", n)\n if n == 1:\n return\n linear_recur(n - 1)\n</code></pre> space_complexity.cpp<pre><code>/* 線形計算量(再帰実装) */\nvoid linearRecur(int n) {\n cout << \"再帰 n = \" << n << endl;\n if (n == 1)\n return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.java<pre><code>/* 線形計算量(再帰実装) */\nvoid linearRecur(int n) {\n System.out.println(\"再帰 n = \" + n);\n if (n == 1)\n return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.cs<pre><code>[class]{space_complexity}-[func]{LinearRecur}\n</code></pre> space_complexity.go<pre><code>[class]{}-[func]{spaceLinearRecur}\n</code></pre> space_complexity.swift<pre><code>[class]{}-[func]{linearRecur}\n</code></pre> space_complexity.js<pre><code>[class]{}-[func]{linearRecur}\n</code></pre> space_complexity.ts<pre><code>[class]{}-[func]{linearRecur}\n</code></pre> space_complexity.dart<pre><code>[class]{}-[func]{linearRecur}\n</code></pre> space_complexity.rs<pre><code>[class]{}-[func]{linear_recur}\n</code></pre> space_complexity.c<pre><code>[class]{}-[func]{linearRecur}\n</code></pre> space_complexity.kt<pre><code>[class]{}-[func]{linearRecur}\n</code></pre> space_complexity.rb<pre><code>[class]{}-[func]{linear_recur}\n</code></pre> <p></p> <p> 図 2-17 Recursive function generating linear order space complexity </p>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-on2","level":3,"title":"3. 二次オーダー \\(O(n^2)\\)","text":"<p>二次オーダーは行列やグラフで一般的で、要素数は\\(n\\)の二乗に比例します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def quadratic(n: int):\n \"\"\"平方複雑度\"\"\"\n # 二次元リストは O(n^2) のスペースを占有\n num_matrix = [[0] * n for _ in range(n)]\n</code></pre> space_complexity.cpp<pre><code>/* 二次計算量 */\nvoid quadratic(int n) {\n // 二次元リストは O(n^2) 空間を占める\n vector<vector<int>> numMatrix;\n for (int i = 0; i < n; i++) {\n vector<int> tmp;\n for (int j = 0; j < n; j++) {\n tmp.push_back(0);\n }\n numMatrix.push_back(tmp);\n }\n}\n</code></pre> space_complexity.java<pre><code>/* 二次計算量 */\nvoid quadratic(int n) {\n // 行列は O(n^2) 空間を占める\n int[][] numMatrix = new int[n][n];\n // 二次元リストは O(n^2) 空間を占める\n List<List<Integer>> numList = new ArrayList<>();\n for (int i = 0; i < n; i++) {\n List<Integer> tmp = new ArrayList<>();\n for (int j = 0; j < n; j++) {\n tmp.add(0);\n }\n numList.add(tmp);\n }\n}\n</code></pre> space_complexity.cs<pre><code>[class]{space_complexity}-[func]{Quadratic}\n</code></pre> space_complexity.go<pre><code>[class]{}-[func]{spaceQuadratic}\n</code></pre> space_complexity.swift<pre><code>[class]{}-[func]{quadratic}\n</code></pre> space_complexity.js<pre><code>[class]{}-[func]{quadratic}\n</code></pre> space_complexity.ts<pre><code>[class]{}-[func]{quadratic}\n</code></pre> space_complexity.dart<pre><code>[class]{}-[func]{quadratic}\n</code></pre> space_complexity.rs<pre><code>[class]{}-[func]{quadratic}\n</code></pre> space_complexity.c<pre><code>[class]{}-[func]{quadratic}\n</code></pre> space_complexity.kt<pre><code>[class]{}-[func]{quadratic}\n</code></pre> space_complexity.rb<pre><code>[class]{}-[func]{quadratic}\n</code></pre> <p>下図に示されているように、この関数の再帰深度は\\(n\\)で、各再帰呼び出しで長さ\\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\)の配列が初期化され、平均\\(n/2\\)となり、全体として\\(O(n^2)\\)の空間を占有します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def quadratic_recur(n: int) -> int:\n \"\"\"平方複雑度(再帰実装)\"\"\"\n if n <= 0:\n return 0\n nums = [0] * n\n print(f\"再帰 n = {n} の中で配列の長さ = {len(nums)}\")\n return quadratic_recur(n - 1)\n</code></pre> space_complexity.cpp<pre><code>/* 二次計算量(再帰実装) */\nint quadraticRecur(int n) {\n if (n <= 0)\n return 0;\n vector<int> nums(n);\n cout << \"再帰 n = \" << n << \", nums の長さ = \" << nums.size() << endl;\n return quadraticRecur(n - 1);\n}\n</code></pre> space_complexity.java<pre><code>/* 二次計算量(再帰実装) */\nint quadraticRecur(int n) {\n if (n <= 0)\n return 0;\n // 配列 nums の長さ = n, n-1, ..., 2, 1\n int[] nums = new int[n];\n System.out.println(\"再帰 n = \" + n + \" の nums の長さ = \" + nums.length);\n return quadraticRecur(n - 1);\n}\n</code></pre> space_complexity.cs<pre><code>[class]{space_complexity}-[func]{QuadraticRecur}\n</code></pre> space_complexity.go<pre><code>[class]{}-[func]{spaceQuadraticRecur}\n</code></pre> space_complexity.swift<pre><code>[class]{}-[func]{quadraticRecur}\n</code></pre> space_complexity.js<pre><code>[class]{}-[func]{quadraticRecur}\n</code></pre> space_complexity.ts<pre><code>[class]{}-[func]{quadraticRecur}\n</code></pre> space_complexity.dart<pre><code>[class]{}-[func]{quadraticRecur}\n</code></pre> space_complexity.rs<pre><code>[class]{}-[func]{quadratic_recur}\n</code></pre> space_complexity.c<pre><code>[class]{}-[func]{quadraticRecur}\n</code></pre> space_complexity.kt<pre><code>[class]{}-[func]{quadraticRecur}\n</code></pre> space_complexity.rb<pre><code>[class]{}-[func]{quadratic_recur}\n</code></pre> <p></p> <p> 図 2-18 Recursive function generating quadratic order space complexity </p>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-o2n","level":3,"title":"4. 指数オーダー \\(O(2^n)\\)","text":"<p>指数オーダーは二分木で一般的です。下図を観察すると、\\(n\\)レベルの「完全二分木」は\\(2^n - 1\\)個のノードを持ち、\\(O(2^n)\\)の空間を占有します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def build_tree(n: int) -> TreeNode | None:\n \"\"\"指数複雑度(完全二分木の構築)\"\"\"\n if n == 0:\n return None\n root = TreeNode(0)\n root.left = build_tree(n - 1)\n root.right = build_tree(n - 1)\n return root\n</code></pre> space_complexity.cpp<pre><code>/* 指数計算量(完全二分木の構築) */\nTreeNode *buildTree(int n) {\n if (n == 0)\n return nullptr;\n TreeNode *root = new TreeNode(0);\n root->left = buildTree(n - 1);\n root->right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.java<pre><code>/* 指数計算量(完全二分木の構築) */\nTreeNode buildTree(int n) {\n if (n == 0)\n return null;\n TreeNode root = new TreeNode(0);\n root.left = buildTree(n - 1);\n root.right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.cs<pre><code>[class]{space_complexity}-[func]{BuildTree}\n</code></pre> space_complexity.go<pre><code>[class]{}-[func]{buildTree}\n</code></pre> space_complexity.swift<pre><code>[class]{}-[func]{buildTree}\n</code></pre> space_complexity.js<pre><code>[class]{}-[func]{buildTree}\n</code></pre> space_complexity.ts<pre><code>[class]{}-[func]{buildTree}\n</code></pre> space_complexity.dart<pre><code>[class]{}-[func]{buildTree}\n</code></pre> space_complexity.rs<pre><code>[class]{}-[func]{build_tree}\n</code></pre> space_complexity.c<pre><code>[class]{}-[func]{buildTree}\n</code></pre> space_complexity.kt<pre><code>[class]{}-[func]{buildTree}\n</code></pre> space_complexity.rb<pre><code>[class]{}-[func]{build_tree}\n</code></pre> <p></p> <p> 図 2-19 Full binary tree generating exponential order space complexity </p>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-olog-n","level":3,"title":"5. 対数オーダー \\(O(\\log n)\\)","text":"<p>対数オーダーは分割統治アルゴリズムで一般的です。例えば、マージソートでは、長さ\\(n\\)の配列が各ラウンドで再帰的に半分に分割され、高さ\\(\\log n\\)の再帰木を形成し、\\(O(\\log n)\\)のスタックフレーム空間を使用します。</p> <p>別の例は、数値を文字列に変換することです。正の整数\\(n\\)が与えられた場合、その桁数は\\(\\log_{10} n + 1\\)で、文字列の長さに対応するため、空間計算量は\\(O(\\log_{10} n + 1) = O(\\log n)\\)です。</p>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244","level":2,"title":"2.4.4 時間と空間のバランス","text":"<p>理想的には、時間計算量と空間計算量の両方が最適であることを目指します。しかし、実際には両方を同時に最適化することはしばしば困難です。</p> <p>時間計算量を下げることは通常、空間計算量の増加を代償とし、その逆も同様です。アルゴリズムの速度を向上させるためにメモリ空間を犠牲にするアプローチは「時空トレードオフ」として知られ、その逆は「空時トレードオフ」として知られています。</p> <p>選択は、どちらの側面をより重視するかに依存します。ほとんどの場合、時間は空間よりも貴重であるため、「時空トレードオフ」がより一般的な戦略です。もちろん、大量のデータを扱う際は空間計算量を制御することも非常に重要です。</p>","path":["第 2 章 計算量解析","2.4 空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5 まとめ","text":"","path":["第 2 章 計算量解析","2.5 まとめ"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1","level":3,"title":"1. 重要なレビュー","text":"<p>アルゴリズム効率評価</p> <ul> <li>時間効率と空間効率は、アルゴリズムの優劣を評価する2つの主要な基準です。</li> <li>実際のテストによってアルゴリズムの効率を評価できますが、テスト環境の影響を排除することは困難で、大量の計算リソースを消費します。</li> <li>複雑度分析は実際のテストの欠点を克服できます。その結果はすべての動作プラットフォームに適用でき、異なるデータスケールでのアルゴリズムの効率を明らかにできます。</li> </ul> <p>時間計算量</p> <ul> <li>時間計算量は、データ量の増加に伴うアルゴリズムの実行時間の傾向を測定し、アルゴリズムの効率を効果的に評価します。しかし、入力データ量が少ない場合や時間計算量が同じ場合など、特定のケースでは失敗することがあり、アルゴリズムの効率を正確に比較することが困難になります。</li> <li>最悪ケース時間計算量はビッグ\\(O\\)記法を使用して表記され、漸近上限を表し、\\(n\\)が無限大に近づくにつれての操作数\\(T(n)\\)の増加レベルを反映します。</li> <li>時間計算量の計算には2つのステップが含まれます:まず操作数をカウントし、次に漸近上限を決定します。</li> <li>一般的な時間計算量は、低いものから高いものへと並べると、\\(O(1)\\)、\\(O(\\log n)\\)、\\(O(n)\\)、\\(O(n \\log n)\\)、\\(O(n^2)\\)、\\(O(2^n)\\)、\\(O(n!)\\)などが含まれます。</li> <li>一部のアルゴリズムの時間計算量は固定されておらず、入力データの分布に依存します。時間計算量は最悪、最良、平均のケースに分けられます。最良ケースは、入力データが最良ケースを達成するために厳格な条件を満たす必要があるため、ほとんど使用されません。</li> <li>平均時間計算量は、ランダムデータ入力下でのアルゴリズムの効率を反映し、実際のアプリケーションでのアルゴリズムの性能に密接に類似しています。平均時間計算量の計算には、入力データの分布とその後の数学的期待値を考慮する必要があります。</li> </ul> <p>空間計算量</p> <ul> <li>空間計算量は、時間計算量と同様に、データ量の増加に伴うアルゴリズムが占有するメモリ空間の傾向を測定します。</li> <li>アルゴリズムの実行中に使用される関連メモリ空間は、入力空間、一時空間、出力空間に分けることができます。一般的に、入力空間は空間計算量の計算に含まれません。一時空間は一時データ、スタックフレーム空間、命令空間に分けることができ、スタックフレーム空間は通常、再帰関数でのみ空間計算量に影響します。</li> <li>通常は最悪ケース空間計算量のみに焦点を当てます。これは、最悪の入力データと操作の最悪の瞬間でのアルゴリズムの空間計算量を計算することを意味します。</li> <li>一般的な空間計算量は、低いものから高いものへと並べると、\\(O(1)\\)、\\(O(\\log n)\\)、\\(O(n)\\)、\\(O(n^2)\\)、\\(O(2^n)\\)などが含まれます。</li> </ul>","path":["第 2 章 計算量解析","2.5 まとめ"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: 末尾再帰の空間計算量は\\(O(1)\\)ですか?</p> <p>理論的には、末尾再帰関数の空間計算量は\\(O(1)\\)に最適化できます。しかし、ほとんどのプログラミング言語(Java、Python、C++、Go、C#など)は末尾再帰の自動最適化をサポートしていないため、一般的に空間計算量は\\(O(n)\\)と考えられています。</p> <p>Q: 「関数」と「メソッド」という用語の違いは何ですか?</p> <p>関数は独立して実行でき、すべてのパラメータが明示的に渡されます。メソッドはオブジェクトに関連付けられ、それを呼び出すオブジェクトに暗黙的に渡され、クラスのインスタンス内に含まれるデータを操作できます。</p> <p>一般的なプログラミング言語からの例をいくつか示します:</p> <ul> <li>Cは手続き型プログラミング言語で、オブジェクト指向の概念がないため、関数のみがあります。しかし、構造体(struct)を作成することでオブジェクト指向プログラミングをシミュレートでき、これらの構造体に関連付けられた関数は他のプログラミング言語のメソッドと同等です。</li> <li>JavaとC#はオブジェクト指向プログラミング言語で、コードブロック(メソッド)は通常クラスの一部です。静的メソッドはクラスにバインドされ、特定のインスタンス変数にアクセスできないため、関数のように動作します。</li> <li>C++とPythonは手続き型プログラミング(関数)とオブジェクト指向プログラミング(メソッド)の両方をサポートしています。</li> </ul> <p>Q: 「空間計算量の一般的な種類」の図は、占有空間の絶対サイズを反映していますか?</p> <p>いいえ、図は空間計算量を示しており、これは増加傾向を反映するものであり、占有空間の絶対サイズではありません。</p> <p>\\(n = 8\\)を取ると、各曲線の値がその関数に対応していないことに気づくかもしれません。これは、各曲線に定数項が含まれているためで、値の範囲を視覚的に快適な範囲に圧縮することを意図しています。</p> <p>実際には、通常は各メソッドの「定数項」複雑度を知らないため、複雑度のみに基づいて\\(n = 8\\)の最良ソリューションを選択することは一般的に不可能です。しかし、\\(n = 8^5\\)の場合、増加傾向が支配的になるため、選択がはるかに容易になります。</p>","path":["第 2 章 計算量解析","2.5 まとめ"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3 時間計算量","text":"<p>実行時間は、アルゴリズムの効率を直感的に評価できます。アルゴリズムの実行時間を正確に推定するにはどうすればよいでしょうか?</p> <ol> <li>実行プラットフォームの決定: これには、ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらすべてがコードの実行効率に影響する可能性があります。</li> <li>様々な計算操作の実行時間の評価: 例えば、加算操作<code>+</code>は1 ns、乗算操作<code>*</code>は10 ns、印刷操作<code>print()</code>は5 nsなどかかる可能性があります。</li> <li>コード内のすべての計算操作をカウント: これらすべての操作の実行時間を合計すると、総実行時間が得られます。</li> </ol> <p>例えば、入力サイズが\\(n\\)の以下のコードを考えてみましょう:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code># 特定の操作プラットフォーム下で\ndef algorithm(n: int):\n a = 2 # 1 ns\n a = a + 1 # 1 ns\n a = a * 2 # 10 ns\n # n回ループ\n for _ in range(n): # 1 ns\n print(0) # 5 ns\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される\n cout << 0 << endl; // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される\n System.out.println(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nvoid Algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される\n Console.WriteLine(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nfunc algorithm(n int) {\n a := 2 // 1 ns\n a = a + 1 // 1 ns\n a = a * 2 // 10 ns\n // n回ループ\n for i := 0; i < n; i++ { // 1 ns\n fmt.Println(a) // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nfunc algorithm(n: Int) {\n var a = 2 // 1 ns\n a = a + 1 // 1 ns\n a = a * 2 // 10 ns\n // n回ループ\n for _ in 0 ..< n { // 1 ns\n print(0) // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nfunction algorithm(n) {\n var a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for(let i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される\n console.log(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nfunction algorithm(n: number): void {\n var a: number = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for(let i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される\n console.log(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される\n print(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nfn algorithm(n: i32) {\n let mut a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for _ in 0..n { // 毎回i++で1 ns\n println!(\"{}\", 0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 特定の操作プラットフォーム下で\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n回ループ\n for (int i = 0; i < n; i++) { // 1 ns, 毎回i++が実行される\n printf(\"%d\", 0); // 5 ns\n }\n}\n</code></pre> <pre><code>\n</code></pre> <p>上記の方法を使用すると、アルゴリズムの実行時間は\\((6n + 12)\\) nsとして計算できます:</p> \\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\] <p>しかし、実際には、アルゴリズムの実行時間をカウントすることは実用的でも合理的でもありません。第一に、推定時間を実行プラットフォームに結び付けたくありません。アルゴリズムは様々なプラットフォームで実行される必要があるからです。第二に、各種操作の実行時間を知ることは困難であり、推定プロセスを難しくします。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231","level":2,"title":"2.3.1 時間増加傾向の評価","text":"<p>時間計算量分析は、アルゴリズムの実行時間をカウントするのではなく、**データ量が増加するにつれての実行時間の増加傾向**を分析します。</p> <p>この「時間増加傾向」の概念を例で理解しましょう。入力データサイズを\\(n\\)とし、3つのアルゴリズム<code>A</code>、<code>B</code>、<code>C</code>を考えてみます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code># アルゴリズムAの時間計算量:定数オーダー\ndef algorithm_A(n: int):\n print(0)\n# アルゴリズムBの時間計算量:線形オーダー\ndef algorithm_B(n: int):\n for _ in range(n):\n print(0)\n# アルゴリズムCの時間計算量:定数オーダー\ndef algorithm_C(n: int):\n for _ in range(1000000):\n print(0)\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nvoid algorithm_A(int n) {\n cout << 0 << endl;\n}\n// アルゴリズムBの時間計算量:線形オーダー\nvoid algorithm_B(int n) {\n for (int i = 0; i < n; i++) {\n cout << 0 << endl;\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nvoid algorithm_C(int n) {\n for (int i = 0; i < 1000000; i++) {\n cout << 0 << endl;\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nvoid algorithm_A(int n) {\n System.out.println(0);\n}\n// アルゴリズムBの時間計算量:線形オーダー\nvoid algorithm_B(int n) {\n for (int i = 0; i < n; i++) {\n System.out.println(0);\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nvoid algorithm_C(int n) {\n for (int i = 0; i < 1000000; i++) {\n System.out.println(0);\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nvoid AlgorithmA(int n) {\n Console.WriteLine(0);\n}\n// アルゴリズムBの時間計算量:線形オーダー\nvoid AlgorithmB(int n) {\n for (int i = 0; i < n; i++) {\n Console.WriteLine(0);\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nvoid AlgorithmC(int n) {\n for (int i = 0; i < 1000000; i++) {\n Console.WriteLine(0);\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nfunc algorithm_A(n int) {\n fmt.Println(0)\n}\n// アルゴリズムBの時間計算量:線形オーダー\nfunc algorithm_B(n int) {\n for i := 0; i < n; i++ {\n fmt.Println(0)\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nfunc algorithm_C(n int) {\n for i := 0; i < 1000000; i++ {\n fmt.Println(0)\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nfunc algorithmA(n: Int) {\n print(0)\n}\n\n// アルゴリズムBの時間計算量:線形オーダー\nfunc algorithmB(n: Int) {\n for _ in 0 ..< n {\n print(0)\n }\n}\n\n// アルゴリズムCの時間計算量:定数オーダー\nfunc algorithmC(n: Int) {\n for _ in 0 ..< 1_000_000 {\n print(0)\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nfunction algorithm_A(n) {\n console.log(0);\n}\n// アルゴリズムBの時間計算量:線形オーダー\nfunction algorithm_B(n) {\n for (let i = 0; i < n; i++) {\n console.log(0);\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nfunction algorithm_C(n) {\n for (let i = 0; i < 1000000; i++) {\n console.log(0);\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nfunction algorithm_A(n: number): void {\n console.log(0);\n}\n// アルゴリズムBの時間計算量:線形オーダー\nfunction algorithm_B(n: number): void {\n for (let i = 0; i < n; i++) {\n console.log(0);\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nfunction algorithm_C(n: number): void {\n for (let i = 0; i < 1000000; i++) {\n console.log(0);\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nvoid algorithmA(int n) {\n print(0);\n}\n// アルゴリズムBの時間計算量:線形オーダー\nvoid algorithmB(int n) {\n for (int i = 0; i < n; i++) {\n print(0);\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nvoid algorithmC(int n) {\n for (int i = 0; i < 1000000; i++) {\n print(0);\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nfn algorithm_A(n: i32) {\n println!(\"{}\", 0);\n}\n// アルゴリズムBの時間計算量:線形オーダー\nfn algorithm_B(n: i32) {\n for _ in 0..n {\n println!(\"{}\", 0);\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nfn algorithm_C(n: i32) {\n for _ in 0..1000000 {\n println!(\"{}\", 0);\n }\n}\n</code></pre> <pre><code>// アルゴリズムAの時間計算量:定数オーダー\nvoid algorithm_A(int n) {\n printf(\"%d\", 0);\n}\n// アルゴリズムBの時間計算量:線形オーダー\nvoid algorithm_B(int n) {\n for (int i = 0; i < n; i++) {\n printf(\"%d\", 0);\n }\n}\n// アルゴリズムCの時間計算量:定数オーダー\nvoid algorithm_C(int n) {\n for (int i = 0; i < 1000000; i++) {\n printf(\"%d\", 0);\n }\n}\n</code></pre> <pre><code>\n</code></pre> <p>下図はこれら3つのアルゴリズムの時間計算量を示しています。</p> <ul> <li>アルゴリズム<code>A</code>には1つの印刷操作のみがあり、その実行時間は\\(n\\)とともに増加しません。その時間計算量は「定数オーダー」と考えられます。</li> <li>アルゴリズム<code>B</code>には\\(n\\)回ループする印刷操作があり、その実行時間は\\(n\\)と線形に増加します。その時間計算量は「線形オーダー」です。</li> <li>アルゴリズム<code>C</code>には1,000,000回ループする印刷操作があります。時間はかかりますが、入力データサイズ\\(n\\)とは無関係です。したがって、<code>C</code>の時間計算量は<code>A</code>と同じ「定数オーダー」です。</li> </ul> <p></p> <p> 図 2-7 Time growth trend of algorithms a, b, and c </p> <p>アルゴリズムの実行時間を直接カウントすることと比較して、時間計算量分析の特徴は何でしょうか?</p> <ul> <li>時間計算量はアルゴリズムの効率を効果的に評価します。例えば、アルゴリズム<code>B</code>は線形に増加する実行時間を持ち、\\(n > 1\\)の時はアルゴリズム<code>A</code>より遅く、\\(n > 1,000,000\\)の時は<code>C</code>より遅くなります。実際、入力データサイズ\\(n\\)が十分に大きい限り、「定数オーダー」複雑度アルゴリズムは常に「線形オーダー」よりも優れており、時間増加傾向の本質を示しています。</li> <li>時間計算量分析はより直感的です。明らかに、実行プラットフォームと計算操作の種類は実行時間増加の傾向とは無関係です。したがって、時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」として扱うことができ、「計算操作実行時間カウント」を「計算操作カウント」に単純化できます。これにより推定の複雑さが大幅に軽減されます。</li> <li>時間計算量には制限があります。例えば、アルゴリズム<code>A</code>と<code>C</code>は同じ時間計算量を持ちますが、実際の実行時間は大きく異なる場合があります。同様に、アルゴリズム<code>B</code>は<code>C</code>よりも高い時間計算量を持ちますが、入力データサイズ\\(n\\)が小さい場合は明らかに優れています。これらの場合、時間計算量のみに基づいてアルゴリズムの効率を判断することは困難です。しかし、これらの問題にもかかわらず、複雑度分析はアルゴリズムの効率を評価するための最も効果的で一般的に使用される方法です。</li> </ul>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232","level":2,"title":"2.3.2 漸近上限","text":"<p>入力サイズが\\(n\\)の関数を考えてみましょう:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>def algorithm(n: int):\n a = 1 # +1\n a = a + 1 # +1\n a = a * 2 # +1\n # n回ループ\n for i in range(n): # +1\n print(0) # +1\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n回ループ\n for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される)\n cout << 0 << endl; // +1\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n回ループ\n for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される)\n System.out.println(0); // +1\n }\n}\n</code></pre> <pre><code>void Algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n回ループ\n for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される)\n Console.WriteLine(0); // +1\n }\n}\n</code></pre> <pre><code>func algorithm(n int) {\n a := 1 // +1\n a = a + 1 // +1\n a = a * 2 // +1\n // n回ループ\n for i := 0; i < n; i++ { // +1\n fmt.Println(a) // +1\n }\n}\n</code></pre> <pre><code>func algorithm(n: Int) {\n var a = 1 // +1\n a = a + 1 // +1\n a = a * 2 // +1\n // n回ループ\n for _ in 0 ..< n { // +1\n print(0) // +1\n }\n}\n</code></pre> <pre><code>function algorithm(n) {\n var a = 1; // +1\n a += 1; // +1\n a *= 2; // +1\n // n回ループ\n for(let i = 0; i < n; i++){ // +1 (毎回i++が実行される)\n console.log(0); // +1\n }\n}\n</code></pre> <pre><code>function algorithm(n: number): void{\n var a: number = 1; // +1\n a += 1; // +1\n a *= 2; // +1\n // n回ループ\n for(let i = 0; i < n; i++){ // +1 (毎回i++が実行される)\n console.log(0); // +1\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n回ループ\n for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される)\n print(0); // +1\n }\n}\n</code></pre> <pre><code>fn algorithm(n: i32) {\n let mut a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n\n // n回ループ\n for _ in 0..n { // +1 (毎回i++が実行される)\n println!(\"{}\", 0); // +1\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n回ループ\n for (int i = 0; i < n; i++) { // +1 (毎回i++が実行される)\n printf(\"%d\", 0); // +1\n }\n}\n</code></pre> <pre><code>\n</code></pre> <p>アルゴリズムの操作数を入力サイズ\\(n\\)の関数として表す関数を\\(T(n)\\)とすると、以下の例を考えてみましょう:</p> \\[ T(n) = 3 + 2n \\] <p>\\(T(n)\\)は線形関数であるため、その増加傾向は線形であり、したがって、その時間計算量は線形オーダーで、\\(O(n)\\)と表記されます。この数学記法はビッグO記法として知られ、関数\\(T(n)\\)の漸近上限を表します。</p> <p>本質的に、時間計算量分析は「操作数\\(T(n)\\)」の漸近上限を見つけることです。それには正確な数学的定義があります。</p> <p>漸近上限</p> <p>すべての\\(n > n_0\\)に対して\\(T(n) \\leq c \\cdot f(n)\\)となるような正の実数\\(c\\)と\\(n_0\\)が存在する場合、\\(f(n)\\)は\\(T(n)\\)の漸近上限とみなされ、\\(T(n) = O(f(n))\\)と表記されます。</p> <p>下図に示されているように、漸近上限の計算では、\\(n\\)が無限大に近づくにつれて、\\(T(n)\\)と\\(f(n)\\)が同じ増加オーダーを持ち、定数因子\\(c\\)のみが異なるような関数\\(f(n)\\)を見つけることが含まれます。</p> <p></p> <p> 図 2-8 Asymptotic upper bound of a function </p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233","level":2,"title":"2.3.3 計算方法","text":"<p>漸近上限の概念は数学的に濃密に見えるかもしれませんが、今すぐ完全に理解する必要はありません。まず計算方法を理解し、時間をかけて練習し理解しましょう。</p> <p>\\(f(n)\\)が決まれば、時間計算量\\(O(f(n))\\)が得られます。しかし、漸近上限\\(f(n)\\)をどのように決定するのでしょうか?このプロセスには一般的に2つのステップが含まれます:操作数のカウントと漸近上限の決定です。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1. ステップ1: 操作数のカウント","text":"<p>このステップでは、コードを行ごとに確認します。しかし、\\(c \\cdot f(n)\\)の定数\\(c\\)の存在により、\\(T(n)\\)のすべての係数と定数項は無視できます。この原理により、操作をカウントする際の簡略化技法が可能になります。</p> <ol> <li>\\(T(n)\\)の定数項を無視します。これらは\\(n\\)とは無関係であるため、時間計算量に影響しません。</li> <li>すべての係数を省略します。例えば、\\(2n\\)、\\(5n + 1\\)回などのループは、\\(n\\)の前の係数が時間計算量に影響しないため、\\(n\\)回に簡略化できます。</li> <li>ネストしたループには乗算を使用します。総操作数は各ループの操作数の積であり、ポイント1と2の簡略化技法を各ループレベルに適用します。</li> </ol> <p>関数が与えられた場合、これらの技法を使用して操作をカウントできます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>def algorithm(n: int):\n a = 1 # +0 (技法1)\n a = a + n # +0 (技法1)\n # +n (技法2)\n for i in range(5 * n + 1):\n print(0)\n # +n*n (技法3)\n for i in range(2 * n):\n for j in range(n + 1):\n print(0)\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n // +n (技法2)\n for (int i = 0; i < 5 * n + 1; i++) {\n cout << 0 << endl;\n }\n // +n*n (技法3)\n for (int i = 0; i < 2 * n; i++) {\n for (int j = 0; j < n + 1; j++) {\n cout << 0 << endl;\n }\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n // +n (技法2)\n for (int i = 0; i < 5 * n + 1; i++) {\n System.out.println(0);\n }\n // +n*n (技法3)\n for (int i = 0; i < 2 * n; i++) {\n for (int j = 0; j < n + 1; j++) {\n System.out.println(0);\n }\n }\n}\n</code></pre> <pre><code>void Algorithm(int n) {\n int a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n // +n (技法2)\n for (int i = 0; i < 5 * n + 1; i++) {\n Console.WriteLine(0);\n }\n // +n*n (技法3)\n for (int i = 0; i < 2 * n; i++) {\n for (int j = 0; j < n + 1; j++) {\n Console.WriteLine(0);\n }\n }\n}\n</code></pre> <pre><code>func algorithm(n int) {\n a := 1 // +0 (技法1)\n a = a + n // +0 (技法1)\n // +n (技法2)\n for i := 0; i < 5 * n + 1; i++ {\n fmt.Println(0)\n }\n // +n*n (技法3)\n for i := 0; i < 2 * n; i++ {\n for j := 0; j < n + 1; j++ {\n fmt.Println(0)\n }\n }\n}\n</code></pre> <pre><code>func algorithm(n: Int) {\n var a = 1 // +0 (技法1)\n a = a + n // +0 (技法1)\n // +n (技法2)\n for _ in 0 ..< (5 * n + 1) {\n print(0)\n }\n // +n*n (技法3)\n for _ in 0 ..< (2 * n) {\n for _ in 0 ..< (n + 1) {\n print(0)\n }\n }\n}\n</code></pre> <pre><code>function algorithm(n) {\n let a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n // +n (技法2)\n for (let i = 0; i < 5 * n + 1; i++) {\n console.log(0);\n }\n // +n*n (技法3)\n for (let i = 0; i < 2 * n; i++) {\n for (let j = 0; j < n + 1; j++) {\n console.log(0);\n }\n }\n}\n</code></pre> <pre><code>function algorithm(n: number): void {\n let a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n // +n (技法2)\n for (let i = 0; i < 5 * n + 1; i++) {\n console.log(0);\n }\n // +n*n (技法3)\n for (let i = 0; i < 2 * n; i++) {\n for (let j = 0; j < n + 1; j++) {\n console.log(0);\n }\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n // +n (技法2)\n for (int i = 0; i < 5 * n + 1; i++) {\n print(0);\n }\n // +n*n (技法3)\n for (int i = 0; i < 2 * n; i++) {\n for (int j = 0; j < n + 1; j++) {\n print(0);\n }\n }\n}\n</code></pre> <pre><code>fn algorithm(n: i32) {\n let mut a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n\n // +n (技法2)\n for i in 0..(5 * n + 1) {\n println!(\"{}\", 0);\n }\n\n // +n*n (技法3)\n for i in 0..(2 * n) {\n for j in 0..(n + 1) {\n println!(\"{}\", 0);\n }\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0 (技法1)\n a = a + n; // +0 (技法1)\n // +n (技法2)\n for (int i = 0; i < 5 * n + 1; i++) {\n printf(\"%d\", 0);\n }\n // +n*n (技法3)\n for (int i = 0; i < 2 * n; i++) {\n for (int j = 0; j < n + 1; j++) {\n printf(\"%d\", 0);\n }\n }\n}\n</code></pre> <pre><code>\n</code></pre> <p>以下の式は、簡略化前後のカウント結果を示しており、どちらも\\(O(n^2)\\)の時間計算量に導きます:</p> \\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{完全カウント (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{簡略化カウント (o.O)} \\end{aligned} \\]","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-2","level":3,"title":"2. ステップ2: 漸近上限の決定","text":"<p>時間計算量は\\(T(n)\\)の最高次項によって決定されます。これは、\\(n\\)が無限大に近づくにつれて、最高次項が支配的になり、他の項の影響は無視できるようになるためです。</p> <p>以下の表は、異なる操作カウントとそれに対応する時間計算量の例を示しています。係数が増加オーダーを変更できないことを強調するために、誇張された値が使用されています。\\(n\\)が非常に大きくなると、これらの定数は重要でなくなります。</p> <p> 表: 異なる操作カウントに対する時間計算量 </p> 操作カウント \\(T(n)\\) 時間計算量 \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234","level":2,"title":"2.3.4 一般的な時間計算量の種類","text":"<p>入力データサイズを\\(n\\)としましょう。一般的な時間計算量の種類を下図に示し、低いものから高いものへと並べています:</p> \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline & \\text{定数} < \\text{対数} < \\text{線形} < \\text{線形対数} < \\text{二次} < \\text{指数} < \\text{階乗} \\end{aligned} \\] <p></p> <p> 図 2-9 Common types of time complexity </p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-o1","level":3,"title":"1. 定数オーダー \\(O(1)\\)","text":"<p>定数オーダーは、操作数が入力データサイズ\\(n\\)とは無関係であることを意味します。以下の関数では、操作数<code>size</code>が大きい場合でも、\\(n\\)とは無関係であるため、時間計算量は\\(O(1)\\)のままです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def constant(n: int) -> int:\n \"\"\"定数複雑度\"\"\"\n count = 0\n size = 100000\n for _ in range(size):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 定数計算量 */\nint constant(int n) {\n int count = 0;\n int size = 100000;\n for (int i = 0; i < size; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 定数計算量 */\nint constant(int n) {\n int count = 0;\n int size = 100000;\n for (int i = 0; i < size; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{Constant}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{constant}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{constant}\n</code></pre>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-on","level":3,"title":"2. 線形オーダー \\(O(n)\\)","text":"<p>線形オーダーは、操作数が入力データサイズ\\(n\\)と線形に増加することを示します。線形オーダーは一般的に単一ループ構造で現れます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def linear(n: int) -> int:\n \"\"\"線形複雑度\"\"\"\n count = 0\n for _ in range(n):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 線形計算量 */\nint linear(int n) {\n int count = 0;\n for (int i = 0; i < n; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 線形計算量 */\nint linear(int n) {\n int count = 0;\n for (int i = 0; i < n; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{Linear}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{linear}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{linear}\n</code></pre> <p>配列の走査や連結リストの走査などの操作は時間計算量が\\(O(n)\\)で、\\(n\\)は配列またはリストの長さです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def array_traversal(nums: list[int]) -> int:\n \"\"\"線形複雑度(配列の走査)\"\"\"\n count = 0\n # ループ回数は配列の長さに比例する\n for num in nums:\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 線形計算量(配列の走査) */\nint arrayTraversal(vector<int> &nums) {\n int count = 0;\n // ループ回数は配列の長さに比例\n for (int num : nums) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 線形計算量(配列の走査) */\nint arrayTraversal(int[] nums) {\n int count = 0;\n // ループ回数は配列の長さに比例\n for (int num : nums) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{ArrayTraversal}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{arrayTraversal}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{arrayTraversal}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{arrayTraversal}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{arrayTraversal}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{arrayTraversal}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{array_traversal}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{arrayTraversal}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{arrayTraversal}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{array_traversal}\n</code></pre> <p>入力データサイズ\\(n\\)は入力データの種類に基づいて決定する必要があります。例えば、最初の例では、\\(n\\)は入力データサイズを表し、2番目の例では、配列の長さ\\(n\\)がデータサイズです。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-on2","level":3,"title":"3. 二次オーダー \\(O(n^2)\\)","text":"<p>二次オーダーは、操作数が入力データサイズ\\(n\\)の二乗に比例して増加することを意味します。二次オーダーは通常ネストしたループで現れ、外側と内側のループの両方が時間計算量\\(O(n)\\)を持ち、全体の複雑度は\\(O(n^2)\\)になります:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def quadratic(n: int) -> int:\n \"\"\"二次複雑度\"\"\"\n count = 0\n # ループ回数はデータサイズnの二乗に比例する\n for i in range(n):\n for j in range(n):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 二次計算量 */\nint quadratic(int n) {\n int count = 0;\n // ループ回数はデータサイズ n の二乗に比例\n for (int i = 0; i < n; i++) {\n for (int j = 0; j < n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 二次計算量 */\nint quadratic(int n) {\n int count = 0;\n // ループ回数はデータサイズ n の二乗に比例\n for (int i = 0; i < n; i++) {\n for (int j = 0; j < n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{Quadratic}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{quadratic}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{quadratic}\n</code></pre> <p>下図は定数オーダー、線形オーダー、二次オーダーの時間計算量を比較しています。</p> <p></p> <p> 図 2-10 Constant, linear, and quadratic order time complexities </p> <p>例えば、バブルソートでは、外側のループが\\(n - 1\\)回実行され、内側のループが\\(n-1\\)、\\(n-2\\)、...、\\(2\\)、\\(1\\)回実行され、平均\\(n / 2\\)回となり、時間計算量は\\(O((n - 1) n / 2) = O(n^2)\\)になります:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def bubble_sort(nums: list[int]) -> int:\n \"\"\"二次複雑度(バブルソート)\"\"\"\n count = 0 # カウンタ\n # 外側のループ: 未ソート範囲は [0, i]\n for i in range(len(nums) - 1, 0, -1):\n # 内側のループ: 未ソート範囲 [0, i] の最大要素を右端にスワップ\n for j in range(i):\n if nums[j] > nums[j + 1]:\n # nums[j] と nums[j + 1] をスワップ\n tmp: int = nums[j]\n nums[j] = nums[j + 1]\n nums[j + 1] = tmp\n count += 3 # 要素のスワップは3つの個別操作を含む\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 二次計算量(バブルソート) */\nint bubbleSort(vector<int> &nums) {\n int count = 0; // カウンター\n // 外側ループ:未ソート範囲は [0, i]\n for (int i = nums.size() - 1; i > 0; i--) {\n // 内側ループ:未ソート範囲 [0, i] の最大要素を範囲の右端にスワップ\n for (int j = 0; j < i; j++) {\n if (nums[j] > nums[j + 1]) {\n // nums[j] と nums[j + 1] をスワップ\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 要素のスワップには3つの個別操作が含まれる\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 二次計算量(バブルソート) */\nint bubbleSort(int[] nums) {\n int count = 0; // カウンター\n // 外側ループ:未ソート範囲は [0, i]\n for (int i = nums.length - 1; i > 0; i--) {\n // 内側ループ:未ソート範囲 [0, i] の最大要素を範囲の右端にスワップ\n for (int j = 0; j < i; j++) {\n if (nums[j] > nums[j + 1]) {\n // nums[j] と nums[j + 1] をスワップ\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 要素のスワップには3つの個別操作が含まれる\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{BubbleSort}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{bubble_sort}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{bubble_sort}\n</code></pre>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-o2n","level":3,"title":"4. 指数オーダー \\(O(2^n)\\)","text":"<p>生物学的「細胞分裂」は指数オーダー増加の典型例です:1つの細胞から始まり、1回の分裂後に2つ、2回の分裂後に4つとなり、\\(n\\)回の分裂後に\\(2^n\\)個の細胞になります。</p> <p>下図とコードは細胞分裂プロセスをシミュレートし、時間計算量は\\(O(2^n)\\)です:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def exponential(n: int) -> int:\n \"\"\"指数複雑度(ループ実装)\"\"\"\n count = 0\n base = 1\n # セルは毎回2つに分裂し、1, 2, 4, 8, ..., 2^(n-1) の数列を形成する\n for _ in range(n):\n for _ in range(base):\n count += 1\n base *= 2\n # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 指数計算量(ループ実装) */\nint exponential(int n) {\n int count = 0, base = 1;\n // セルは毎ラウンド2つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成\n for (int i = 0; i < n; i++) {\n for (int j = 0; j < base; j++) {\n count++;\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 指数計算量(ループ実装) */\nint exponential(int n) {\n int count = 0, base = 1;\n // セルは毎ラウンド2つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成\n for (int i = 0; i < n; i++) {\n for (int j = 0; j < base; j++) {\n count++;\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{Exponential}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{exponential}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{exponential}\n</code></pre> <p></p> <p> 図 2-11 Exponential order time complexity </p> <p>実際には、指数オーダーは再帰関数でよく現れます。例えば、以下のコードでは、再帰的に2つの半分に分割し、\\(n\\)回の分割後に停止します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def exp_recur(n: int) -> int:\n \"\"\"指数複雑度(再帰実装)\"\"\"\n if n == 1:\n return 1\n return exp_recur(n - 1) + exp_recur(n - 1) + 1\n</code></pre> time_complexity.cpp<pre><code>/* 指数計算量(再帰実装) */\nint expRecur(int n) {\n if (n == 1)\n return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.java<pre><code>/* 指数計算量(再帰実装) */\nint expRecur(int n) {\n if (n == 1)\n return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{ExpRecur}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{expRecur}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{expRecur}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{expRecur}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{expRecur}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{expRecur}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{exp_recur}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{expRecur}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{expRecur}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{exp_recur}\n</code></pre> <p>指数オーダーの増加は極めて急速で、全数探索法(ブルートフォース、バックトラッキングなど)でよく見られます。大規模問題では、指数オーダーは受け入れられず、しばしば動的プログラミングや貪欲アルゴリズムが解決策として必要になります。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-olog-n","level":3,"title":"5. 対数オーダー \\(O(\\log n)\\)","text":"<p>指数オーダーとは対照的に、対数オーダーは「各ラウンドでサイズが半分になる」状況を反映します。入力データサイズが\\(n\\)の場合、各ラウンドでサイズが半分になるため、反復回数は\\(\\log_2 n\\)で、これは\\(2^n\\)の逆関数です。</p> <p>下図とコードは「各ラウンドで半分にする」プロセスをシミュレートし、時間計算量は\\(O(\\log_2 n)\\)で、一般的に\\(O(\\log n)\\)と省略されます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def logarithmic(n: int) -> int:\n \"\"\"対数複雑度(ループ実装)\"\"\"\n count = 0\n while n > 1:\n n = n / 2\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 対数計算量(ループ実装) */\nint logarithmic(int n) {\n int count = 0;\n while (n > 1) {\n n = n / 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 対数計算量(ループ実装) */\nint logarithmic(int n) {\n int count = 0;\n while (n > 1) {\n n = n / 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{Logarithmic}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{logarithmic}\n</code></pre> <p></p> <p> 図 2-12 Logarithmic order time complexity </p> <p>指数オーダーと同様に、対数オーダーも再帰関数で頻繁に現れます。以下のコードは高さ\\(\\log_2 n\\)の再帰木を形成します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def log_recur(n: int) -> int:\n \"\"\"対数複雑度(再帰実装)\"\"\"\n if n <= 1:\n return 0\n return log_recur(n / 2) + 1\n</code></pre> time_complexity.cpp<pre><code>/* 対数計算量(再帰実装) */\nint logRecur(int n) {\n if (n <= 1)\n return 0;\n return logRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.java<pre><code>/* 対数計算量(再帰実装) */\nint logRecur(int n) {\n if (n <= 1)\n return 0;\n return logRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{LogRecur}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{logRecur}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{logRecur}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{logRecur}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{logRecur}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{logRecur}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{log_recur}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{logRecur}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{logRecur}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{log_recur}\n</code></pre> <p>対数オーダーは分割統治戦略に基づくアルゴリズムの典型で、「多くに分割」と「複雑な問題を単純化」するアプローチを体現しています。増加が遅く、定数オーダーの次に最も理想的な時間計算量です。</p> <p>\\(O(\\log n)\\)の底は何ですか?</p> <p>技術的には、「\\(m\\)に分割」は時間計算量\\(O(\\log_m n)\\)に対応します。対数の底変更公式を使用すると、異なる対数複雑度を等価にできます:</p> \\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\] <p>これは、底\\(m\\)を複雑度に影響を与えることなく変更できることを意味します。したがって、しばしば底\\(m\\)を省略し、単に対数オーダーを\\(O(\\log n)\\)と表記します。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-on-log-n","level":3,"title":"6. 線形対数オーダー \\(O(n \\log n)\\)","text":"<p>線形対数オーダーはネストしたループでよく現れ、2つのループの複雑度がそれぞれ\\(O(\\log n)\\)と\\(O(n)\\)です。関連するコードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def linear_log_recur(n: int) -> int:\n \"\"\"線形対数複雑度\"\"\"\n if n <= 1:\n return 1\n count: int = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n for _ in range(n):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 線形対数計算量 */\nint linearLogRecur(int n) {\n if (n <= 1)\n return 1;\n int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n for (int i = 0; i < n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 線形対数計算量 */\nint linearLogRecur(int n) {\n if (n <= 1)\n return 1;\n int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n for (int i = 0; i < n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{LinearLogRecur}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{linearLogRecur}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{linearLogRecur}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{linearLogRecur}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{linearLogRecur}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{linearLogRecur}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{linear_log_recur}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{linearLogRecur}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{linearLogRecur}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{linear_log_recur}\n</code></pre> <p>下図は線形対数オーダーがどのように生成されるかを示しています。二分木の各レベルには\\(n\\)個の操作があり、木には\\(\\log_2 n + 1\\)レベルがあり、時間計算量は\\(O(n \\log n)\\)になります。</p> <p></p> <p> 図 2-13 Linear-logarithmic order time complexity </p> <p>主流のソートアルゴリズムは通常\\(O(n \\log n)\\)の時間計算量を持ち、クイックソート、マージソート、ヒープソートなどがあります。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-on","level":3,"title":"7. 階乗オーダー \\(O(n!)\\)","text":"<p>階乗オーダーは「全順列」の数学問題に対応します。\\(n\\)個の異なる要素が与えられた場合、可能な順列の総数は:</p> \\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\] <p>階乗は通常再帰を使用して実装されます。以下のコードと図に示されているように、第1レベルは\\(n\\)個の分岐に分割され、第2レベルは\\(n - 1\\)個の分岐に分割され、第\\(n\\)レベル後に停止します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def factorial_recur(n: int) -> int:\n \"\"\"階乗複雑度(再帰実装)\"\"\"\n if n == 0:\n return 1\n count = 0\n # 1つからnに分岐\n for _ in range(n):\n count += factorial_recur(n - 1)\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 階乗計算量(再帰実装) */\nint factorialRecur(int n) {\n if (n == 0)\n return 1;\n int count = 0;\n // 1から n に分裂\n for (int i = 0; i < n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 階乗計算量(再帰実装) */\nint factorialRecur(int n) {\n if (n == 0)\n return 1;\n int count = 0;\n // 1から n に分裂\n for (int i = 0; i < n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>[class]{time_complexity}-[func]{FactorialRecur}\n</code></pre> time_complexity.go<pre><code>[class]{}-[func]{factorialRecur}\n</code></pre> time_complexity.swift<pre><code>[class]{}-[func]{factorialRecur}\n</code></pre> time_complexity.js<pre><code>[class]{}-[func]{factorialRecur}\n</code></pre> time_complexity.ts<pre><code>[class]{}-[func]{factorialRecur}\n</code></pre> time_complexity.dart<pre><code>[class]{}-[func]{factorialRecur}\n</code></pre> time_complexity.rs<pre><code>[class]{}-[func]{factorial_recur}\n</code></pre> time_complexity.c<pre><code>[class]{}-[func]{factorialRecur}\n</code></pre> time_complexity.kt<pre><code>[class]{}-[func]{factorialRecur}\n</code></pre> time_complexity.rb<pre><code>[class]{}-[func]{factorial_recur}\n</code></pre> <p></p> <p> 図 2-14 Factorial order time complexity </p> <p>階乗オーダーは指数オーダーよりもさらに速く増加することに注意してください。より大きな\\(n\\)値では受け入れられません。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235","level":2,"title":"2.3.5 最悪、最良、平均時間計算量","text":"<p>アルゴリズムの時間効率は固定されていないことが多く、入力データの分布に依存します。長さ\\(n\\)の配列<code>nums</code>があり、\\(1\\)から\\(n\\)までの数で構成され、それぞれが一度だけ現れますが、ランダムにシャッフルされた順序であるとします。タスクは要素\\(1\\)のインデックスを返すことです。以下の結論を導けます:</p> <ul> <li><code>nums = [?, ?, ..., 1]</code>の場合、つまり最後の要素が\\(1\\)の場合、配列の完全な走査が必要で、最悪ケース時間計算量\\(O(n)\\)を達成します。</li> <li><code>nums = [1, ?, ?, ...]</code>の場合、つまり最初の要素が\\(1\\)の場合、配列の長さに関係なく、さらなる走査は不要で、最良ケース時間計算量\\(\\Omega(1)\\)を達成します。</li> </ul> <p>「最悪ケース時間計算量」は漸近上限に対応し、大きな\\(O\\)記法で表されます。対応して、「最良ケース時間計算量」は漸近下限に対応し、\\(\\Omega\\)で表されます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py<pre><code>def random_numbers(n: int) -> list[int]:\n \"\"\"要素 1, 2, ..., n を含む配列を生成、順序はシャッフル\"\"\"\n # 配列 nums = 1, 2, 3, ..., n を生成\n nums = [i for i in range(1, n + 1)]\n # 配列要素をランダムにシャッフル\n random.shuffle(nums)\n return nums\n\ndef find_one(nums: list[int]) -> int:\n \"\"\"配列 nums で数値 1 のインデックスを検索\"\"\"\n for i in range(len(nums)):\n # 要素 1 が配列の最初にある場合、最良時間計算量 O(1) を達成\n # 要素 1 が配列の最後にある場合、最悪時間計算量 O(n) を達成\n if nums[i] == 1:\n return i\n return -1\n</code></pre> worst_best_time_complexity.cpp<pre><code>/* 要素 {1, 2, ..., n} をランダムにシャッフルした配列を生成 */\nvector<int> randomNumbers(int n) {\n vector<int> nums(n);\n // 配列 nums = { 1, 2, 3, ..., n } を生成\n for (int i = 0; i < n; i++) {\n nums[i] = i + 1;\n }\n // システム時刻を使用してランダムシードを生成\n unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n // 配列要素をランダムにシャッフル\n shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n return nums;\n}\n\n/* 配列 nums で数値1のインデックスを見つける */\nint findOne(vector<int> &nums) {\n for (int i = 0; i < nums.size(); i++) {\n // 要素1が配列の先頭にある場合、最良時間計算量 O(1) を達成\n // 要素1が配列の末尾にある場合、最悪時間計算量 O(n) を達成\n if (nums[i] == 1)\n return i;\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.java<pre><code>/* 要素 {1, 2, ..., n} をランダムにシャッフルした配列を生成 */\nint[] randomNumbers(int n) {\n Integer[] nums = new Integer[n];\n // 配列 nums = { 1, 2, 3, ..., n } を生成\n for (int i = 0; i < n; i++) {\n nums[i] = i + 1;\n }\n // 配列要素をランダムにシャッフル\n Collections.shuffle(Arrays.asList(nums));\n // Integer[] -> int[]\n int[] res = new int[n];\n for (int i = 0; i < n; i++) {\n res[i] = nums[i];\n }\n return res;\n}\n\n/* 配列 nums で数値1のインデックスを見つける */\nint findOne(int[] nums) {\n for (int i = 0; i < nums.length; i++) {\n // 要素1が配列の先頭にある場合、最良時間計算量 O(1) を達成\n // 要素1が配列の末尾にある場合、最悪時間計算量 O(n) を達成\n if (nums[i] == 1)\n return i;\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.cs<pre><code>[class]{worst_best_time_complexity}-[func]{RandomNumbers}\n\n[class]{worst_best_time_complexity}-[func]{FindOne}\n</code></pre> worst_best_time_complexity.go<pre><code>[class]{}-[func]{randomNumbers}\n\n[class]{}-[func]{findOne}\n</code></pre> worst_best_time_complexity.swift<pre><code>[class]{}-[func]{randomNumbers}\n\n[class]{}-[func]{findOne}\n</code></pre> worst_best_time_complexity.js<pre><code>[class]{}-[func]{randomNumbers}\n\n[class]{}-[func]{findOne}\n</code></pre> worst_best_time_complexity.ts<pre><code>[class]{}-[func]{randomNumbers}\n\n[class]{}-[func]{findOne}\n</code></pre> worst_best_time_complexity.dart<pre><code>[class]{}-[func]{randomNumbers}\n\n[class]{}-[func]{findOne}\n</code></pre> worst_best_time_complexity.rs<pre><code>[class]{}-[func]{random_numbers}\n\n[class]{}-[func]{find_one}\n</code></pre> worst_best_time_complexity.c<pre><code>[class]{}-[func]{randomNumbers}\n\n[class]{}-[func]{findOne}\n</code></pre> worst_best_time_complexity.kt<pre><code>[class]{}-[func]{randomNumbers}\n\n[class]{}-[func]{findOne}\n</code></pre> worst_best_time_complexity.rb<pre><code>[class]{}-[func]{random_numbers}\n\n[class]{}-[func]{find_one}\n</code></pre> <p>最良ケース時間計算量は実際にはほとんど使用されないことに注意してください。通常は非常に低い確率でのみ達成可能で、誤解を招く可能性があるからです。最悪ケース時間計算量はより実用的で、効率の安全値を提供し、アルゴリズムを自信を持って使用できるようにします。</p> <p>上記の例から、最悪ケースと最良ケースの時間計算量は両方とも「特殊なデータ分布」下でのみ発生し、発生確率が小さく、アルゴリズムの実行効率を正確に反映しない可能性があることが明らかです。対照的に、平均時間計算量はランダム入力データ下でのアルゴリズムの効率を反映でき、\\(\\Theta\\)記法で表されます。</p> <p>一部のアルゴリズムでは、ランダムデータ分布下での平均ケースを簡単に推定できます。例えば、前述の例では、入力配列がシャッフルされているため、要素\\(1\\)が任意のインデックスに現れる確率は等しいです。したがって、アルゴリズムの平均ループ数は配列長さの半分\\(n / 2\\)で、平均時間計算量は\\(\\Theta(n / 2) = \\Theta(n)\\)です。</p> <p>しかし、より複雑なアルゴリズムの平均時間計算量を計算することは非常に困難です。データ分布下での全体的な数学的期待値を分析することが困難だからです。そのような場合、通常はアルゴリズムの効率を判断する基準として最悪ケース時間計算量を使用します。</p> <p>\\(\\Theta\\)記号はなぜほとんど見られないのですか?</p> <p>おそらく\\(O\\)記法がより一般的に話されるため、平均時間計算量を表すためによく使用されます。しかし、厳密に言えば、この実践は正確ではありません。この本や他の資料で「平均時間計算量\\(O(n)\\)」のような表現に遭遇した場合は、直接\\(\\Theta(n)\\)として理解してください。</p>","path":["第 2 章 計算量解析","2.3 時間計算量"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"第 3 章 データ構造","text":"<p>Abstract</p> <p>データ構造は堅牢で多様なフレームワークとして機能します。</p> <p>データの整然とした組織化のための設計図を提供し、その上でアルゴリズムが生き生きと動き出します。</p>","path":["第 3 章 データ構造"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"章の内容","text":"<ul> <li>3.1 データ構造の分類</li> <li>3.2 基本データ型</li> <li>3.3 数値の符号化 *</li> <li>3.4 文字の符号化 *</li> <li>3.5 まとめ</li> </ul>","path":["第 3 章 データ構造"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2 基本データ型","text":"<p>コンピュータ内のデータについて考える際、テキスト、画像、動画、音声、3Dモデルなど、様々な形式が思い浮かびます。これらの組織的な形式は異なりますが、すべて様々な基本データ型から構成されています。</p> <p>**基本データ型とは、CPUが直接操作できるもの**であり、アルゴリズムで直接使用されます。主に以下が含まれます。</p> <ul> <li>整数型:<code>byte</code>、<code>short</code>、<code>int</code>、<code>long</code></li> <li>浮動小数点型:<code>float</code>、<code>double</code>、小数を表現するために使用</li> <li>文字型:<code>char</code>、様々な言語の文字、句読点、さらには絵文字を表現するために使用</li> <li>ブール型:<code>bool</code>、「はい」または「いいえ」の判断を表現するために使用</li> </ul> <p>基本データ型は、コンピュータ内で二進形式で格納されます。1つの二進桁は1ビットです。ほとんどの現代的なオペレーティングシステムでは、1バイトは8ビットで構成されています。</p> <p>基本データ型の値の範囲は、それらが占める空間のサイズに依存します。以下では、Javaを例に説明します。</p> <ul> <li>整数型<code>byte</code>は1バイト = 8ビットを占め、\\(2^8\\)個の数値を表現できます。</li> <li>整数型<code>int</code>は4バイト = 32ビットを占め、\\(2^{32}\\)個の数値を表現できます。</li> </ul> <p>以下の表は、Javaにおける様々な基本データ型が占める空間、値の範囲、デフォルト値を示しています。この表を暗記する必要はありませんが、一般的な理解を持ち、必要時に参照することをお勧めします。</p> <p> 表 3-1 基本データ型が占める空間と値の範囲 </p> 型 シンボル 占有空間 最小値 最大値 デフォルト値 整数 <code>byte</code> 1バイト \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) 0 <code>short</code> 2バイト \\(-2^{15}\\) \\(2^{15} - 1\\) 0 <code>int</code> 4バイト \\(-2^{31}\\) \\(2^{31} - 1\\) 0 <code>long</code> 8バイト \\(-2^{63}\\) \\(2^{63} - 1\\) 0 浮動小数点 <code>float</code> 4バイト \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) <code>double</code> 8バイト \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) 0.0 文字 <code>char</code> 2バイト 0 \\(2^{16} - 1\\) 0 ブール <code>bool</code> 1バイト \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\) <p>上記の表はJavaの基本データ型に特有であることにご注意ください。すべてのプログラミング言語には独自のデータ型定義があり、占有空間、値の範囲、デフォルト値が異なる場合があります。</p> <ul> <li>Pythonでは、整数型<code>int</code>は任意のサイズになることができ、利用可能なメモリによってのみ制限されます。浮動小数点<code>float</code>は倍精度64ビットです。<code>char</code>型は存在せず、単一文字は実際には長さ1の文字列<code>str</code>です。</li> <li>CおよびC++では基本データ型のサイズが指定されておらず、実装とプラットフォームによって異なります。上記の表はLP64データモデルに従っており、LinuxやmacOSを含むUnix 64ビットオペレーティングシステムで使用されています。</li> <li>CおよびC++における<code>char</code>のサイズは1バイトですが、ほとんどのプログラミング言語では、特定の文字エンコーディング方法に依存し、詳細は「文字エンコーディング」の章で説明されています。</li> <li>ブール値の表現には1ビット(0または1)のみが必要ですが、通常はメモリ内に1バイトとして格納されます。これは、現代のコンピュータCPUが通常1バイトを最小のアドレス可能なメモリ単位として使用するためです。</li> </ul> <p>では、基本データ型とデータ構造の関係は何でしょうか?データ構造とは、コンピュータ内でデータを組織化し格納する方法であることを知っています。ここでの焦点は「データ」ではなく「構造」です。</p> <p>「数値の列」を表現したい場合、自然に配列の使用を考えます。これは、配列の線形構造が数値の隣接性と順序性を表現できるためですが、格納される内容が整数<code>int</code>、小数<code>float</code>、文字<code>char</code>のいずれであっても、「データ構造」とは無関係です。</p> <p>言い換えると、基本データ型はデータの「内容型」を提供し、データ構造はデータの「組織化方法」を提供します。例えば、以下のコードでは、同じデータ構造(配列)を使用して、<code>int</code>、<code>float</code>、<code>char</code>、<code>bool</code>などの異なる基本データ型を格納し表現しています。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code># 様々な基本データ型を使用して配列を初期化\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# Pythonの文字は実際には長さ1の文字列\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Pythonのリストは様々な基本データ型とオブジェクト参照を自由に格納可能\ndata = [0, 0.0, 'a', False, ListNode(0)]\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n</code></pre> <pre><code>// JavaScriptの配列は様々な基本データ型とオブジェクトを自由に格納可能\nconst array = [0, 0.0, 'a', false];\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0, 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n</code></pre> <pre><code>// 様々な基本データ型を使用して配列を初期化\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n</code></pre> <pre><code>\n</code></pre>","path":["第 3 章 データ構造","3.2 基本データ型"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4 文字エンコーディング *","text":"<p>コンピュータシステムでは、すべてのデータが二進形式で格納され、<code>char</code>も例外ではありません。文字を表現するために、各文字と二進数の一対一のマッピングを定義する「文字セット」を開発する必要があります。文字セットがあれば、コンピュータは表を参照して二進数を文字に変換できます。</p>","path":["第 3 章 データ構造","3.4 文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii","level":2,"title":"3.4.1 ASCII文字セット","text":"<p>ASCIIコードは最も初期の文字セットの一つで、正式にはAmerican Standard Code for Information Interchangeとして知られています。7つの二進桁(1バイトの下位7ビット)を使用して文字を表現し、最大128種類の異なる文字を表現できます。以下の図に示すように、ASCIIには英語の大文字と小文字、0〜9の数字、様々な句読点、特定の制御文字(改行やタブなど)が含まれています。</p> <p></p> <p> 図 3-6 ASCIIコード </p> <p>しかし、ASCIIは英語の文字のみを表現できます。コンピュータのグローバル化に伴い、より多くの言語を表現するためにEASCIIと呼ばれる文字セットが開発されました。ASCIIの7ビット構造から8ビットに拡張し、256文字の表現を可能にしました。</p> <p>世界的に、様々な地域固有のEASCII文字セットが導入されました。これらのセットの最初の128文字はASCIIと一致していますが、残りの128文字は異なる言語の要件に対応するために異なって定義されています。</p>","path":["第 3 章 データ構造","3.4 文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk","level":2,"title":"3.4.2 GBK文字セット","text":"<p>後に、**EASCIIでも多くの言語の文字要件を満たすことができない**ことが判明しました。例えば、中国語には約10万の漢字があり、そのうち数千が定期的に使用されています。1980年、中国標準化委員会は6763の中国語文字を含むGB2312文字セットを発表し、中国語のコンピュータ処理ニーズを本質的に満たしました。</p> <p>しかし、GB2312は一部の稀少文字や繁体字を処理できませんでした。GBK文字セットはGB2312を拡張し、21886の中国語文字を含んでいます。GBKエンコーディングスキームでは、ASCII文字は1バイトで表現され、中国語文字は2バイトを使用します。</p>","path":["第 3 章 データ構造","3.4 文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3 Unicode文字セット","text":"<p>コンピュータ技術の急速な発展と多数の文字セットおよびエンコーディング標準により、数多くの問題が発生しました。一方では、これらの文字セットは一般的に特定の言語の文字のみを定義し、多言語環境では適切に機能できませんでした。他方では、同じ言語に対する複数の文字セット標準の存在により、異なるエンコーディング標準を使用するコンピュータ間で情報交換を行う際に文字化けが発生しました。</p> <p>当時の研究者たちは考えました:世界のすべての言語と記号を含む包括的な文字セットが開発されれば、言語横断環境と文字化けに関連する問題を解決できるのではないでしょうか? このアイデアにインスパイアされて、広範囲な文字セットであるUnicodeが誕生しました。</p> <p>Unicodeは中国語で「统一码」(統一コード)と呼ばれ、理論的に100万文字以上を収容できます。世界中のすべての文字を単一のセットに組み込み、様々な言語の処理と表示のための汎用文字セットを提供し、異なるエンコーディング標準による文字化けの問題を減らすことを目指しています。</p> <p>1991年のリリース以来、Unicodeは新しい言語と文字を含むよう継続的に拡張されています。2022年9月現在、Unicodeには149,186文字が含まれており、様々な言語の文字、記号、さらには絵文字も含まれています。広大なUnicode文字セットでは、一般的に使用される文字は2バイトを占有し、一部の稀少な文字は3バイトまたは4バイトを占有する場合があります。</p> <p>Unicodeは各文字に数値(「コードポイント」と呼ばれる)を割り当てる汎用文字セットですが、これらの文字コードポイントがコンピュータシステムにどのように格納されるべきかは指定していません。疑問が生じるかもしれません:システムはテキスト内の異なる長さのUnicodeコードポイントをどのように解釈するのでしょうか?例えば、2バイトのコードが与えられた場合、システムはそれが単一の2バイト文字を表すのか、2つの1バイト文字を表すのかをどのように判断するのでしょうか?</p> <p>この問題に対する簡単な解決策は、すべての文字を等長エンコーディングとして格納することです。以下の図に示すように、「Hello」の各文字は1バイトを占有し、「算法」(アルゴリズム)の各文字は2バイトを占有します。上位ビットをゼロで埋めることで、「Hello 算法」のすべての文字を2バイトとしてエンコードできます。この方法により、システムは2バイトごとに文字を解釈し、フレーズの内容を復元できます。</p> <p></p> <p> 図 3-7 Unicodeエンコーディング例 </p> <p>しかし、ASCIIが示したように、英語のエンコーディングには1バイトのみが必要です。上記のアプローチを使用すると、英語テキストが占有する空間がASCIIエンコーディングと比較して2倍になり、メモリ空間の無駄になります。したがって、より効率的なUnicodeエンコーディング方法が必要です。</p>","path":["第 3 章 データ構造","3.4 文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8","level":2,"title":"3.4.4 UTF-8エンコーディング","text":"<p>現在、UTF-8は国際的に最も広く使用されているUnicodeエンコーディング方法になっています。**これは可変長エンコーディング**で、文字の複雑さに応じて1〜4バイトを使用して文字を表現します。ASCII文字は1バイトのみが必要で、ラテン文字とギリシャ文字は2バイト、一般的に使用される中国語文字は3バイト、その他の稀少な文字は4バイトが必要です。</p> <p>UTF-8のエンコーディング規則は複雑ではなく、2つのケースに分けることができます:</p> <ul> <li>1バイト文字の場合、最上位ビットを\\(0\\)に設定し、残りの7ビットをUnicodeコードポイントに設定します。注目すべきは、ASCII文字がUnicodeセットの最初の128コードポイントを占有することです。これは**UTF-8エンコーディングがASCIIと後方互換性がある**ことを意味します。これは、UTF-8を使用して古いASCIIテキストを解析できることを意味します。</li> <li>長さ\\(n\\)バイトの文字(\\(n > 1\\))の場合、最初のバイトの最上位\\(n\\)ビットを\\(1\\)に設定し、\\((n + 1)^{\\text{th}}\\)ビットを\\(0\\)に設定します。2番目のバイトから、各バイトの最上位2ビットを\\(10\\)に設定します。残りのビットはUnicodeコードポイントを埋めるために使用されます。</li> </ul> <p>以下の図は「Hello算法」のUTF-8エンコーディングを示しています。最上位\\(n\\)ビットが\\(1\\)に設定されているため、システムは最上位ビットで\\(1\\)に設定されたビット数を数えることで文字の長さを\\(n\\)として決定できることが観察できます。</p> <p>しかし、なぜ残りのバイトの最上位2ビットを\\(10\\)に設定するのでしょうか?実際、この\\(10\\)は一種のチェックサムとして機能します。システムが間違ったバイトからテキストの解析を開始した場合、バイトの先頭の\\(10\\)によりシステムは異常を迅速に検出できます。</p> <p>\\(10\\)をチェックサムとして使用する理由は、UTF-8エンコーディング規則の下では、文字の最上位2ビットが\\(10\\)になることは不可能だからです。これは矛盾により証明できます:文字の最上位2ビットが\\(10\\)の場合、文字の長さが\\(1\\)であることを示し、これはASCIIに対応します。しかし、ASCII文字の最上位ビットは\\(0\\)であるべきで、これは仮定と矛盾します。</p> <p></p> <p> 図 3-8 UTF-8エンコーディング例 </p> <p>UTF-8以外にも、他の一般的なエンコーディング方法には以下があります:</p> <ul> <li>UTF-16エンコーディング:2または4バイトを使用して文字を表現します。すべてのASCII文字と一般的に使用される非英語文字は2バイトで表現され、少数の文字は4バイトが必要です。2バイト文字の場合、UTF-16エンコーディングはUnicodeコードポイントと等しくなります。</li> <li>UTF-32エンコーディング:すべての文字が4バイトを使用します。これは、UTF-32がUTF-8やUTF-16よりも多くの空間を占有することを意味し、特にASCII文字の割合が高いテキストでは顕著です。</li> </ul> <p>ストレージ空間の観点から、UTF-8を使用して英語文字を表現することは1バイトのみが必要なため非常に効率的です。UTF-16を使用して一部の非英語文字(中国語など)をエンコードすることは、2バイトのみが必要なためより効率的になる場合があります。一方、UTF-8では3バイトが必要になる場合があります。</p> <p>互換性の観点から、UTF-8は最も汎用性があり、多くのツールとライブラリがUTF-8を優先的にサポートしています。</p>","path":["第 3 章 データ構造","3.4 文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345","level":2,"title":"3.4.5 プログラミング言語における文字エンコーディング","text":"<p>歴史的に、多くのプログラミング言語はプログラム実行中の文字列処理にUTF-16やUTF-32などの固定長エンコーディングを利用していました。これにより文字列を配列として処理でき、いくつかの利点があります:</p> <ul> <li>ランダムアクセス:UTF-16でエンコードされた文字列は簡単にランダムアクセスできます。可変長エンコーディングであるUTF-8の場合、\\(i^{th}\\)文字の位置を特定するには文字列の開始から\\(i^{th}\\)位置まで走査する必要があり、\\(O(n)\\)時間がかかります。</li> <li>文字数カウント:ランダムアクセスと同様に、UTF-16でエンコードされた文字列の文字数をカウントすることは\\(O(1)\\)操作です。しかし、UTF-8でエンコードされた文字列の文字数をカウントするには文字列全体を走査する必要があります。</li> <li>文字列操作:分割、連結、挿入、削除などの多くの文字列操作は、UTF-16でエンコードされた文字列で簡単です。これらの操作は一般的に、UTF-8エンコーディングの有効性を確保するためにUTF-8でエンコードされた文字列で追加の計算が必要です。</li> </ul> <p>プログラミング言語における文字エンコーディングスキームの設計は、様々な要因を含む興味深いトピックです:</p> <ul> <li>Javaの<code>String</code>型はUTF-16エンコーディングを使用し、各文字が2バイトを占有します。これは、16ビットがすべての可能な文字を表現するのに十分であるという初期の信念に基づいており、後に間違いであることが証明されました。Unicode標準が16ビットを超えて拡張されると、Javaの文字は「サロゲートペア」として知られる16ビット値のペアで表現される場合があります。</li> <li>JavaScriptとTypeScriptは、Javaと同様の理由でUTF-16エンコーディングを使用します。JavaScriptが1995年にNetscapeによって最初に導入されたとき、Unicodeはまだ初期段階にあり、16ビットエンコーディングはすべてのUnicode文字を表現するのに十分でした。</li> <li>C#はUTF-16エンコーディングを使用し、これは主にMicrosoftによって設計された.NETプラットフォーム、および多くのMicrosoft技術(Windowsオペレーティングシステムを含む)がUTF-16エンコーディングを広範囲に使用しているためです。</li> </ul> <p>文字数の過小評価により、これらの言語は16ビットを超えるUnicode文字を表現するために「サロゲートペア」を使用する必要がありました。このアプローチには欠点があります:サロゲートペアを含む文字列は2バイトまたは4バイトを占有する文字を持つ場合があり、固定長エンコーディングの利点を失います。さらに、サロゲートペアの処理はプログラミングに複雑さとデバッグの困難さを追加します。</p> <p>これらの課題に対処するため、一部の言語は代替エンコーディング戦略を採用しています:</p> <ul> <li>Pythonの<code>str</code>型は、文字のストレージ長が文字列内の最大のUnicodeコードポイントに依存する柔軟な表現でUnicodeエンコーディングを使用します。すべての文字がASCIIの場合、各文字は1バイトを占有し、基本多言語面(BMP)内の文字は2バイト、BMPを超える文字は4バイトを占有します。</li> <li>Goの<code>string</code>型は内部的にUTF-8エンコーディングを使用します。Goは個別のUnicodeコードポイントを表現するための<code>rune</code>型も提供します。</li> <li>Rustの<code>str</code>と<code>String</code>型は内部的にUTF-8エンコーディングを使用します。Rustは個別のUnicodeコードポイント用の<code>char</code>型も提供します。</li> </ul> <p>上記の議論は、プログラミング言語での文字列の格納方法に関するものであり、**ファイルでの文字列の格納方法やネットワーク上での送信方法とは異なる**ことに注意することが重要です。ファイルストレージやネットワーク送信では、文字列は通常、最適な互換性と空間効率のためにUTF-8形式でエンコードされます。</p>","path":["第 3 章 データ構造","3.4 文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1 データ構造の分類","text":"<p>一般的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュ表、木、ヒープ、グラフがあります。これらは「論理構造」と「物理構造」に分類できます。</p>","path":["第 3 章 データ構造","3.1 データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1 論理構造:線形と非線形","text":"<p>論理構造はデータ要素間の論理的関係を明らかにします。配列と連結リストでは、データは特定の順序で配置され、データ間の線形関係を示しています。一方、木では、データは上から下へ階層的に配置され、「祖先」と「子孫」間の派生関係を示しています。そして、グラフはノードとエッジから構成され、複雑なネットワーク関係を反映しています。</p> <p>下図に示されているように、論理構造は「線形」と「非線形」の2つの主要カテゴリに分けることができます。線形構造はより直感的で、データが論理関係において線形に配置されていることを示しています。非線形構造は、逆に非線形に配置されています。</p> <ul> <li>線形データ構造:配列、連結リスト、スタック、キュー、ハッシュ表。要素が一対一の順次関係を持ちます。</li> <li>非線形データ構造:木、ヒープ、グラフ、ハッシュ表。</li> </ul> <p>非線形データ構造は、さらに木構造とネットワーク構造に分けることができます。</p> <ul> <li>木構造:木、ヒープ、ハッシュ表。要素が一対多の関係を持ちます。</li> <li>ネットワーク構造:グラフ。要素が多対多の関係を持ちます。</li> </ul> <p></p> <p> 図 3-1 Linear and non-linear data structures </p>","path":["第 3 章 データ構造","3.1 データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2 物理構造:連続と分散","text":"<p>アルゴリズムの実行中、処理されるデータはメモリに格納されます。下図はコンピュータのメモリスティックを示しており、各黒い正方形は物理メモリ空間です。メモリを巨大なExcelスプレッドシートと考えることができ、各セルは一定量のデータを格納できます。</p> <p>システムはメモリアドレスによって目標位置のデータにアクセスします。下図に示されているように、コンピュータは特定のルールに従って表の各セルに一意の識別子を割り当て、各メモリ空間が一意のメモリアドレスを持つことを保証します。これらのアドレスにより、プログラムはメモリに格納されたデータにアクセスできます。</p> <p></p> <p> 図 3-2 Memory stick, memory spaces, memory addresses </p> <p>Tip</p> <p>メモリをExcelスプレッドシートに比較することは簡略化された類推であることに注意してください。メモリの実際の動作メカニズムはより複雑で、アドレス空間、メモリ管理、キャッシュメカニズム、仮想メモリ、物理メモリなどの概念が関係しています。</p> <p>メモリはすべてのプログラムの共有リソースです。あるメモリブロックが1つのプログラムによって占有されると、他のプログラムが同時に使用することはできません。したがって、メモリリソースはデータ構造とアルゴリズムの設計における重要な考慮事項です。例えば、アルゴリズムのピークメモリ使用量は、システムの残り空きメモリを超えてはいけません。連続したメモリブロックが不足している場合は、非連続メモリブロックに格納できるデータ構造を選択する必要があります。</p> <p>下図に示されているように、物理構造はコンピュータメモリにおけるデータの格納方法を反映し、連続空間格納(配列)と非連続空間格納(連結リスト)に分けることができます。2つのタイプの物理構造は、時間効率と空間効率の観点で補完的な特性を示します。</p> <p></p> <p> 図 3-3 Contiguous space storage and dispersed space storage </p> <p>すべてのデータ構造は配列、連結リスト、またはその組み合わせに基づいて実装されていることに注意してください。例えば、スタックとキューは配列または連結リストのどちらでも実装できます。ハッシュ表の実装には配列と連結リストの両方が関係する場合があります。</p> <ul> <li>配列ベースの実装:スタック、キュー、ハッシュ表、木、ヒープ、グラフ、行列、テンソル(次元\\(\\geq 3\\)の配列)。</li> <li>連結リストベースの実装:スタック、キュー、ハッシュ表、木、ヒープ、グラフなど。</li> </ul> <p>配列に基づいて実装されたデータ構造は「静的データ構造」とも呼ばれ、初期化後に長さを変更できないことを意味します。逆に、連結リストに基づいたものは「動的データ構造」と呼ばれ、プログラム実行中にサイズを調整できます。</p> <p>Tip</p> <p>物理構造を理解するのが困難な場合は、次の章「配列と連結リスト」を読んでから、この節に戻ることをお勧めします。</p>","path":["第 3 章 データ構造","3.1 データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3 数値エンコーディング *","text":"<p>Tip</p> <p>本書では、アスタリスク「*」が付いた章は任意読書です。時間が不足している場合や難しいと感じる場合は、最初はこれらをスキップして、必須の章を完了した後に戻ることができます。</p>","path":["第 3 章 データ構造","3.3 数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331","level":2,"title":"3.3.1 整数エンコーディング","text":"<p>前の節の表で、すべての整数型は正の数よりも1つ多い負の数を表現できることを観察しました。例えば、<code>byte</code>の範囲は\\([-128, 127]\\)です。この現象は直感に反するように見え、その根本的な理由には符号絶対値、1の補数、2の補数エンコーディングの知識が関与しています。</p> <p>まず重要なことは、**数値はコンピュータ内で2の補数形式で格納される**ということです。なぜそうなのかを分析する前に、これら3つのエンコーディング方法を定義しましょう:</p> <ul> <li>符号絶対値:数値の二進表現の最上位ビットを符号ビットとし、\\(0\\)は正の数、\\(1\\)は負の数を表します。残りのビットは数値の値を表します。</li> <li>1の補数:正の数の1の補数は符号絶対値と同じです。負の数の場合、符号ビット以外のすべてのビットを反転して得られます。</li> <li>2の補数:正の数の2の補数は符号絶対値と同じです。負の数の場合、その1の補数に\\(1\\)を加えて得られます。</li> </ul> <p>以下の図は、符号絶対値、1の補数、2の補数間の変換を示しています:</p> <p></p> <p> 図 3-4 符号絶対値、1の補数、2の補数間の変換 </p> <p>符号絶対値は最も直感的ですが、制限があります。一つには、符号絶対値の負の数は計算で直接使用できません。例えば、符号絶対値で\\(1 + (-2)\\)を計算すると\\(-3\\)になり、これは正しくありません。</p> \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\] <p>この問題に対処するため、コンピュータは1の補数を導入しました。1の補数に変換して\\(1 + (-2)\\)を計算し、結果を符号絶対値に戻すと、正しい結果\\(-1\\)が得られます。</p> \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(符号絶対値)} + 1000 \\; 0010 \\; \\text{(符号絶対値)} \\newline & = 0000 \\; 0001 \\; \\text{(1の補数)} + 1111 \\; 1101 \\; \\text{(1の補数)} \\newline & = 1111 \\; 1110 \\; \\text{(1の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(符号絶対値)} \\newline & \\rightarrow -1 \\end{aligned} \\] <p>また、符号絶対値では0に2つの表現があります:\\(+0\\)と\\(-0\\)です。これは0に対して2つの異なる二進エンコーディングがあることを意味し、曖昧さを引き起こす可能性があります。例えば、条件チェックで正と負の0を区別しないと、正しくない結果になる可能性があります。この曖昧さに対処するには追加のチェックが必要で、計算効率が低下する可能性があります。</p> \\[ \\begin{aligned} +0 & \\rightarrow 0000 \\; 0000 \\newline -0 & \\rightarrow 1000 \\; 0000 \\end{aligned} \\] <p>符号絶対値と同様に、1の補数も正と負の0の曖昧さに悩まされます。そのため、コンピュータはさらに2の補数を導入しました。符号絶対値、1の補数、2の補数における負の0の変換過程を観察してみましょう:</p> \\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(符号絶対値)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1の補数)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2の補数)} \\newline \\end{aligned} \\] <p>負の0の1の補数に\\(1\\)を加えると桁上がりが発生しますが、<code>byte</code>の長さは8ビットのみのため、9番目のビットへの桁上がり\\(1\\)は破棄されます。したがって、**負の0の2の補数は\\(0000 \\; 0000\\)**で、正の0と同じになり、曖昧さが解決されます。</p> <p>最後の謎は、<code>byte</code>の\\([-128, 127]\\)の範囲で、追加の負の数\\(-128\\)があることです。\\([-127, +127]\\)の区間では、すべての整数に対応する符号絶対値、1の補数、2の補数があり、相互変換が可能であることを観察します。</p> <p>しかし、2の補数\\(1000 \\; 0000\\)は対応する符号絶対値を持たない例外です。変換方法によると、その符号絶対値は\\(0000 \\; 0000\\)で、0を示します。これは矛盾を示しています。なぜなら、その2の補数は自分自身を表すべきだからです。コンピュータは、この特別な2の補数\\(1000 \\; 0000\\)を\\(-128\\)を表すものとして指定しています。実際、2の補数での\\((-1) + (-127)\\)の計算結果は\\(-128\\)になります。</p> \\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(符号絶対値)} + 1000 \\; 0001 \\; \\text{(符号絶対値)} \\newline & = 1000 \\; 0000 \\; \\text{(1の補数)} + 1111 \\; 1110 \\; \\text{(1の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(2の補数)} + 1111 \\; 1111 \\; \\text{(2の補数)} \\newline & = 1000 \\; 0000 \\; \\text{(2の補数)} \\newline & \\rightarrow -128 \\end{aligned} \\] <p>お気づきかもしれませんが、これらの計算はすべて加算であり、重要な事実を示唆しています:コンピュータの内部ハードウェア回路は主に加算演算を中心に設計されています。これは、加算が乗算、除算、減算などの他の演算と比較してハードウェアで実装しやすく、並列化が容易で高速計算が可能だからです。</p> <p>これはコンピュータが加算のみを実行できることを意味するものではありません。加算と基本的な論理演算を組み合わせることで、コンピュータは様々な他の数学演算を実行できます。例えば、減算\\(a - b\\)は\\(a + (-b)\\)に変換でき、乗算と除算は複数の加算または減算に変換できます。</p> <p>コンピュータで2の補数を使用する理由をまとめることができます:2の補数表現により、コンピュータは同じ回路と演算を使用して正と負の数の加算を処理でき、減算用の特別なハードウェア回路の必要性を排除し、正と負の0の曖昧さを回避できます。これによりハードウェア設計が大幅に簡素化され、計算効率が向上します。</p> <p>2の補数の設計は非常に巧妙で、スペースの制約により、ここで停止します。興味のある読者はさらに探求することを奨励します。</p>","path":["第 3 章 データ構造","3.3 数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2 浮動小数点数エンコーディング","text":"<p>興味深いことに気づいたかもしれません:同じ4バイトの長さにもかかわらず、なぜ<code>float</code>は<code>int</code>と比較してはるかに大きい値の範囲を持つのでしょうか?これは直感に反するように見えます。<code>float</code>は分数を表現する必要があるため、範囲が縮小すると予想されるからです。</p> <p>実際、これは浮動小数点数(<code>float</code>)で使用される異なる表現方法によるものです。32ビットの二進数を次のように考えてみましょう:</p> \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\] <p>IEEE 754標準によると、32ビットの<code>float</code>は次の3つの部分で構成されます:</p> <ul> <li>符号ビット\\(\\mathrm{S}\\):1ビットを占有し、\\(b_{31}\\)に対応します。</li> <li>指数ビット\\(\\mathrm{E}\\):8ビットを占有し、\\(b_{30} b_{29} \\ldots b_{23}\\)に対応します。</li> <li>仮数ビット\\(\\mathrm{N}\\):23ビットを占有し、\\(b_{22} b_{21} \\ldots b_0\\)に対応します。</li> </ul> <p>二進<code>float</code>数の値は次のように計算されます:</p> \\[ \\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 \\] <p>十進公式に変換すると、次のようになります:</p> \\[ \\text{val} = (-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} - 127} \\times (1 + \\mathrm{N}) \\] <p>各成分の範囲は:</p> \\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} \\times 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\] <p></p> <p> 図 3-5 IEEE 754標準での浮動小数点数の計算例 </p> <p>上の図を観察すると、例のデータ\\(\\mathrm{S} = 0\\)、\\(\\mathrm{E} = 124\\)、\\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\)が与えられた場合:</p> \\[ \\text{val} = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\] <p>これで最初の質問に答えることができます:<code>float</code>の表現には指数ビットが含まれているため、<code>int</code>よりもはるかに大きい範囲を持ちます。上記の計算に基づくと、<code>float</code>で表現可能な最大正の数は約\\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\)で、最小負の数は符号ビットを切り替えることで得られます。</p> <p>しかし、<code>float</code>の拡張された範囲のトレードオフは精度の犠牲です。整数型<code>int</code>は32ビットすべてを数値表現に使用し、値は均等に分布していますが、指数ビットのため、<code>float</code>の値が大きいほど、隣接する数値間の差が大きくなります。</p> <p>以下の表に示すように、指数ビット\\(\\mathrm{E} = 0\\)と\\(\\mathrm{E} = 255\\)は特別な意味を持ち、0、無限大、\\(\\mathrm{NaN}\\)などを表現するために使用されます。</p> <p> 表 3-2 指数ビットの意味 </p> 指数ビット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}\\) <p>非正規化数は浮動小数点数の精度を大幅に向上させることは注目に値します。最小の正の正規化数は\\(2^{-126}\\)で、最小の正の非正規化数は\\(2^{-126} \\times 2^{-23}\\)です。</p> <p>倍精度<code>double</code>も<code>float</code>と同様の表現方法を使用しますが、簡潔さのためここでは詳述しません。</p>","path":["第 3 章 データ構造","3.3 数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5 まとめ","text":"","path":["第 3 章 データ構造","3.5 まとめ"],"tags":[]},{"location":"chapter_data_structure/summary/#1","level":3,"title":"1. 重要なポイント","text":"<ul> <li>データ構造は論理構造と物理構造の2つの観点から分類できます。論理構造はデータ間の論理的関係を記述し、物理構造はデータがメモリにどのように格納されるかを記述します。</li> <li>よく使用される論理構造には、線形構造、木、ネットワークがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)に分けます。ハッシュ表の実装は線形と非線形の両方のデータ構造を含む場合があります。</li> <li>プログラムが実行中の際、データはメモリに格納されます。各メモリ空間には対応するアドレスがあり、プログラムはこれらのアドレスを通じてデータにアクセスします。</li> <li>物理構造は連続空間格納(配列)と離散空間格納(連結リスト)に分けることができます。すべてのデータ構造は配列、連結リスト、またはその両方の組み合わせを使用して実装されます。</li> <li>コンピュータの基本データ型には、整数(<code>byte</code>、<code>short</code>、<code>int</code>、<code>long</code>)、浮動小数点数(<code>float</code>、<code>double</code>)、文字(<code>char</code>)、ブール値(<code>bool</code>)が含まれます。データ型の値の範囲は、そのサイズと表現に依存します。</li> <li>符号絶対値、1の補数、2の補数は、コンピュータで整数をエンコードする3つの方法であり、相互に変換することができます。符号絶対値の最上位ビットは符号ビットで、残りのビットは数値の値を表します。</li> <li>整数はコンピュータで2の補数によってエンコードされます。この表現の利点には、(i)コンピュータが正と負の整数の加算を統一できる、(ii)減算用の特別なハードウェア回路を設計する必要がない、(iii)正と負の0の曖昧さがない、があります。</li> <li>浮動小数点数のエンコーディングは、1つの符号ビット、8つの指数ビット、23の仮数ビットで構成されます。指数ビットのため、浮動小数点数の範囲は整数よりもはるかに大きくなりますが、精度を犠牲にします。</li> <li>ASCIIは最初期の英語文字セットで、1バイトの長さで計127文字です。GBKは人気のある中国語文字セットで、2万文字以上の中国語文字を含みます。Unicodeは世界の様々な言語の文字を含む完全な文字セット標準を提供することを目的とし、文字エンコーディング方法の不一致による文字化け問題を解決します。</li> <li>UTF-8は最も人気があり一般的なUnicodeエンコーディング方法です。これは可変長エンコーディング方法で、優れた拡張性と空間効率を持ちます。UTF-16とUTF-32は固定長エンコーディング方法です。中国語文字をエンコードする際、UTF-16はUTF-8よりも少ない空間を使用します。JavaやC#などのプログラミング言語はデフォルトでUTF-16エンコーディングを使用します。</li> </ul>","path":["第 3 章 データ構造","3.5 まとめ"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: なぜハッシュ表は線形と非線形の両方のデータ構造を含むのですか?</p> <p>ハッシュ表の基礎構造は配列です。ハッシュ衝突を解決するために、「チェイン法」を使用する場合があります(後の節「ハッシュ衝突」で説明):配列の各バケットは連結リストを指し、その長さが特定の閾値より大きくなると木(通常は赤黒木)に変換される可能性があります。 格納の観点から、ハッシュ表の基礎構造は配列で、各バケットには値、連結リスト、または木が含まれる場合があります。したがって、ハッシュ表は線形データ構造(配列、連結リスト)と非線形データ構造(木)の両方を含む場合があります。</p> <p>Q: <code>char</code>型の長さは1バイトですか?</p> <p><code>char</code>型の長さは、プログラミング言語のエンコーディング方法によって決まります。例えば、Java、JavaScript、TypeScript、C#はすべてUTF-16エンコーディング(Unicodeコードポイントを保存するため)を使用するため、<code>char</code>型の長さは2バイトです。</p> <p>Q: 配列ベースのデータ構造を「静的データ構造」と呼ぶことに曖昧さはありませんか?スタックもプッシュやポップなどの「動的」操作を実行できます。</p> <p>スタックは動的なデータ操作を実装できますが、データ構造は依然として「静的」です(長さが固定)。配列ベースのデータ構造は動的に要素を追加または削除できますが、その容量は固定されています。スタックサイズが事前に割り当てられたサイズを超える場合、古い配列は新しく作成されたより大きな配列にコピーされます。</p> <p>Q: スタック(キュー)を構築する際、そのサイズが指定されていないのに、なぜ「静的データ構造」なのですか?</p> <p>高級プログラミング言語では、スタック(キュー)の初期容量を手動で指定する必要はありません。このタスクはクラス内で自動的に完了されます。例えば、Javaの<code>ArrayList</code>の初期容量は通常10です。さらに、拡張操作も自動的に完了されます。詳細については、後続の「リスト」の章を参照してください。</p> <p>Q: 符号絶対値を2の補数に変換する方法は「最初に否定してから1を加える」ですので、2の補数を符号絶対値に変換することはその逆操作「最初に1を減算してから否定する」であるべきです。 しかし、2の補数も「最初に否定してから1を加える」を通じて符号絶対値に変換できます。なぜですか?</p> <p>A: これは、符号絶対値と2の補数間の相互変換が「補数」の計算と等価だからです。まず補数を定義します:\\(a + b = c\\)と仮定すると、\\(a\\)は\\(b\\)の\\(c\\)に対する補数と言い、逆に\\(b\\)は\\(a\\)の\\(c\\)に対する補数と言います。</p> <p>長さ\\(n = 4\\)の二進数\\(0010\\)が与えられた場合、この数が符号絶対値(符号ビットを無視)の場合、その2の補数は「最初に否定してから1を加える」ことで得られます:</p> \\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\] <p>符号絶対値と2の補数の和が\\(0010 + 1110 = 10000\\)であることを観察します。つまり、2の補数\\(1110\\)は符号絶対値\\(0010\\)の\\(10000\\)に対する「補数」です。これは、上記の「最初に否定してから1を加える」が\\(10000\\)に対する補数の計算と等価であることを意味します。</p> <p>では、\\(1110\\)の\\(10000\\)に対する「補数」は何でしょうか?「最初に否定してから1を加える」ことで計算できます:</p> \\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\] <p>言い換えると、符号絶対値と2の補数は互いに\\(10000\\)に対する「補数」であるため、「符号絶対値から2の補数」と「2の補数から符号絶対値」は同じ操作(最初に否定してから1を加える)で実装できます。</p> <p>もちろん、「最初に否定してから1を加える」の逆操作を使用して2の補数\\(1110\\)の符号絶対値を求めることもできます。つまり、「最初に1を減算してから否定する」:</p> \\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\] <p>要約すると、「最初に否定してから1を加える」と「最初に1を減算してから否定する」は両方とも\\(10000\\)に対する補数を計算しており、等価です。</p> <p>本質的に、「否定」操作は実際には\\(1111\\)に対する補数を求めることです(<code>符号絶対値 + 1の補数 = 1111</code>が常に成り立つため)。そして1の補数に1を加えることは\\(10000\\)に対する2の補数と等しくなります。</p> <p>上記では\\(n = 4\\)を例に取りましたが、任意の桁数の任意の二進数に一般化できます。</p>","path":["第 3 章 データ構造","3.5 まとめ"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"第 12 章 分割統治","text":"<p>Abstract</p> <p>困難な問題は層を重ねて分解され、各分解によってより単純になります。</p> <p>分割統治は深い真理を明らかにします:単純さから始めれば、複雑さは解決される。</p>","path":["第 12 章 分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"章の内容","text":"<ul> <li>12.1 分割統治アルゴリズム</li> <li>12.2 分割統治探索戦略</li> <li>12.3 木の構築問題</li> <li>12.4 ハノイの塔問題</li> <li>12.5 まとめ</li> </ul>","path":["第 12 章 分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2 分割統治検索戦略","text":"<p>私たちは検索アルゴリズムが主に2つのカテゴリに分類されることを学びました。</p> <ul> <li>総当たり検索:データ構造を走査することで実装され、時間計算量は \\(O(n)\\) です。</li> <li>適応検索:独特なデータ組織形式や事前情報を利用し、時間計算量は \\(O(\\log n)\\) または \\(O(1)\\) に達することができます。</li> </ul> <p>実際、時間計算量が \\(O(\\log n)\\) の検索アルゴリズムは通常分割統治戦略に基づいています。例えば、二分探索や木などです。</p> <ul> <li>二分探索の各ステップは、問題(配列内でターゲット要素を検索する)をより小さな問題(配列の半分でターゲット要素を検索する)に分割し、配列が空になるかターゲット要素が見つかるまで続けます。</li> <li>木は分割統治のアイデアを表現し、二分探索木、AVL木、ヒープなどのデータ構造では、様々な操作の時間計算量は \\(O(\\log n)\\) です。</li> </ul> <p>二分探索の分割統治戦略は以下の通りです。</p> <ul> <li>問題を分割できる:二分探索は元の問題(配列内での検索)を部分問題(配列の半分での検索)に再帰的に分割し、中間要素とターゲット要素を比較することで実現されます。</li> <li>部分問題は独立している:二分探索では、各ラウンドで一つの部分問題を処理し、他の部分問題に影響されません。</li> <li>部分問題の解をマージする必要がない:二分探索は特定の要素を見つけることを目的としているため、部分問題の解をマージする必要がありません。部分問題が解決されると、元の問題も解決されます。</li> </ul> <p>分割統治は検索効率を向上させることができます。なぜなら、総当たり検索はラウンドごとに1つの選択肢しか除去できませんが、分割統治は選択肢の半分を除去できるからです。</p>","path":["第 12 章 分割統治","12.2 分割統治検索戦略"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1","level":3,"title":"1. 分割統治に基づく二分探索の実装","text":"<p>前の章では、二分探索は反復に基づいて実装されました。今度は、分割統治(再帰)に基づいて実装します。</p> <p>Question</p> <p>長さ \\(n\\) の順序付けられた配列 <code>nums</code> が与えられ、すべての要素が一意である場合、要素 <code>target</code> を見つけてください。</p> <p>分割統治の観点から、検索区間 \\([i, j]\\) に対応する部分問題を \\(f(i, j)\\) と表します。</p> <p>元の問題 \\(f(0, n-1)\\) から開始して、以下のステップで二分探索を実行します。</p> <ol> <li>検索区間 \\([i, j]\\) の中点 \\(m\\) を計算し、それを使用して検索区間の半分を除去します。</li> <li>半分のサイズに縮小された部分問題を再帰的に解決します。これは \\(f(i, m-1)\\) または \\(f(m+1, j)\\) になる可能性があります。</li> <li><code>target</code> が見つかるか区間が空になってリターンするまで、ステップ <code>1.</code> と <code>2.</code> を繰り返します。</li> </ol> <p>以下の図は、配列内で要素 \\(6\\) を探す二分探索の分割統治過程を示しています。</p> <p></p> <p> 図 12-4 二分探索の分割統治過程 </p> <p>実装コードでは、問題 \\(f(i, j)\\) を解決するために再帰関数 <code>dfs()</code> を宣言します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py<pre><code>def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n \"\"\"二分探索:問題 f(i, j)\"\"\"\n # 区間が空の場合、対象要素がないことを示すため、-1 を返す\n if i > j:\n return -1\n # 中点インデックス m を計算\n m = (i + j) // 2\n if nums[m] < target:\n # 再帰部分問題 f(m+1, j)\n return dfs(nums, target, m + 1, j)\n elif nums[m] > target:\n # 再帰部分問題 f(i, m-1)\n return dfs(nums, target, i, m - 1)\n else:\n # 対象要素を発見したため、そのインデックスを返す\n return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n \"\"\"二分探索\"\"\"\n n = len(nums)\n # 問題 f(0, n-1) を解く\n return dfs(nums, target, 0, n - 1)\n</code></pre> binary_search_recur.cpp<pre><code>/* 二分探索:問題 f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n // 区間が空の場合、対象要素が存在しないことを示すため、-1 を返す\n if (i > j) {\n return -1;\n }\n // 中点インデックス m を計算\n int m = i + (j - i) / 2;\n if (nums[m] < target) {\n // 再帰的な部分問題 f(m+1, j)\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] > target) {\n // 再帰的な部分問題 f(i, m-1)\n return dfs(nums, target, i, m - 1);\n } else {\n // 対象要素が見つかったため、そのインデックスを返す\n return m;\n }\n}\n\n/* 二分探索 */\nint binarySearch(vector<int> &nums, int target) {\n int n = nums.size();\n // 問題 f(0, n-1) を解く\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.java<pre><code>/* 二分探索:問題 f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n // 区間が空の場合、対象要素が存在しないことを示すため、-1 を返す\n if (i > j) {\n return -1;\n }\n // 中点インデックス m を計算\n int m = i + (j - i) / 2;\n if (nums[m] < target) {\n // 再帰的な部分問題 f(m+1, j)\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] > target) {\n // 再帰的な部分問題 f(i, m-1)\n return dfs(nums, target, i, m - 1);\n } else {\n // 対象要素が見つかったため、そのインデックスを返す\n return m;\n }\n}\n\n/* 二分探索 */\nint binarySearch(int[] nums, int target) {\n int n = nums.length;\n // 問題 f(0, n-1) を解く\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.cs<pre><code>[class]{binary_search_recur}-[func]{DFS}\n\n[class]{binary_search_recur}-[func]{BinarySearch}\n</code></pre> binary_search_recur.go<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binarySearch}\n</code></pre> binary_search_recur.swift<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binarySearch}\n</code></pre> binary_search_recur.js<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binarySearch}\n</code></pre> binary_search_recur.ts<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binarySearch}\n</code></pre> binary_search_recur.dart<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binarySearch}\n</code></pre> binary_search_recur.rs<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binary_search}\n</code></pre> binary_search_recur.c<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binarySearch}\n</code></pre> binary_search_recur.kt<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binarySearch}\n</code></pre> binary_search_recur.rb<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{binary_search}\n</code></pre>","path":["第 12 章 分割統治","12.2 分割統治検索戦略"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3 二分木構築問題","text":"<p>Question</p> <p>二分木の前順走査 <code>preorder</code> シーケンスと中順走査 <code>inorder</code> シーケンスが与えられた場合、二分木を構築してそのルートノードを返してください。二分木に重複するノード値がないと仮定します(以下の図に示すように)。</p> <p></p> <p> 図 12-5 二分木構築のサンプルデータ </p>","path":["第 12 章 分割統治","12.3 二分木構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1. 分割統治問題かどうかの判定","text":"<p><code>preorder</code> と <code>inorder</code> シーケンスから二分木を構築する元の問題は、典型的な分割統治問題です。</p> <ul> <li>問題を分解できる:分割統治の観点から、元の問題を2つの部分問題(左の部分木の構築と右の部分木の構築)とルートノードの初期化という1つの操作に分割できます。各部分木(部分問題)について、同じアプローチを継続的に適用し、より小さな部分木(部分問題)に分割し、最小の部分問題(空の部分木)に到達するまで続けます。</li> <li>部分問題は独立している:左と右の部分木は重複しません。左の部分木を構築する際、左の部分木に対応する中順走査と前順走査のセグメントのみが必要です。右の部分木にも同じアプローチが適用されます。</li> <li>部分問題の解を組み合わせることができる:左と右の部分木(部分問題の解)を構築したら、それらをルートノードに接続して元の問題の解を取得できます。</li> </ul>","path":["第 12 章 分割統治","12.3 二分木構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2. 部分木の分割方法","text":"<p>上記の分析に基づいて、この問題は分割統治を使用して解決できます。しかし、前順走査 <code>preorder</code> シーケンスと中順走査 <code>inorder</code> シーケンスを使用して左と右の部分木をどのように分割すればよいでしょうか?</p> <p>定義により、<code>preorder</code> と <code>inorder</code> シーケンスの両方を3つの部分に分割できます:</p> <ul> <li>前順走査:<code>[ ルート | 左の部分木 | 右の部分木 ]</code>。例えば、図では、木は <code>[ 3 | 9 | 2 1 7 ]</code> に対応します。</li> <li>中順走査:<code>[ 左の部分木 | ルート | 右の部分木 ]</code>。例えば、図では、木は <code>[ 9 | 3 | 1 2 7 ]</code> に対応します。</li> </ul> <p>前の図のデータを使用して、次の図に示すステップに従って分割結果を取得できます:</p> <ol> <li>前順走査の最初の要素3がルートノードの値です。</li> <li><code>inorder</code> シーケンス内でルートノード3のインデックスを見つけ、このインデックスを使用して <code>inorder</code> を <code>[ 9 | 3 | 1 2 7 ]</code> に分割します。</li> <li><code>inorder</code> シーケンスの分割に従って、左と右の部分木がそれぞれ1個と3個のノードを含むことが簡単に決定できるため、<code>preorder</code> シーケンスを <code>[ 3 | 9 | 2 1 7 ]</code> に対応して分割できます。</li> </ol> <p></p> <p> 図 12-6 前順走査と中順走査での部分木の分割 </p>","path":["第 12 章 分割統治","12.3 二分木構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3","level":3,"title":"3. 変数に基づく部分木範囲の記述","text":"<p>上記の分割方法に基づいて、<code>preorder</code> と <code>inorder</code> シーケンスにおけるルート、左の部分木、右の部分木のインデックス範囲を取得しました。これらのインデックス範囲を記述するために、いくつかのポインタ変数を使用します。</p> <ul> <li>現在の木のルートノードの <code>preorder</code> シーケンスでのインデックスを \\(i\\) とします。</li> <li>現在の木のルートノードの <code>inorder</code> シーケンスでのインデックスを \\(m\\) とします。</li> <li>現在の木の <code>inorder</code> シーケンスでのインデックス範囲を \\([l, r]\\) とします。</li> </ul> <p>以下の表に示すように、これらの変数は <code>preorder</code> シーケンスでのルートノードのインデックスと <code>inorder</code> シーケンスでの部分木のインデックス範囲を表します。</p> <p> 表 12-1 前順走査と中順走査でのルートノードと部分木のインデックス </p> <code>preorder</code> でのルートノードインデックス <code>inorder</code> での部分木インデックス範囲 現在の木 \\(i\\) \\([l, r]\\) 左の部分木 \\(i + 1\\) \\([l, m-1]\\) 右の部分木 \\(i + 1 + (m - l)\\) \\([m+1, r]\\) <p>右の部分木のルートインデックスの \\((m-l)\\) は「左の部分木のノード数」を表すことに注意してください。より明確な理解のために、以下の図を参照することが役立つ場合があります。</p> <p></p> <p> 図 12-7 ルートノードと左右の部分木のインデックス </p>","path":["第 12 章 分割統治","12.3 二分木構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4","level":3,"title":"4. コード実装","text":"<p>\\(m\\) の問い合わせの効率を向上させるために、ハッシュテーブル <code>hmap</code> を使用して <code>inorder</code> シーケンスの要素からそのインデックスへのマッピングを格納します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py<pre><code>def dfs(\n preorder: list[int],\n inorder_map: dict[int, int],\n i: int,\n l: int,\n r: int,\n) -> TreeNode | None:\n \"\"\"二分木の構築:分割統治\"\"\"\n # 部分木の区間が空のとき終了\n if r - l < 0:\n return None\n # ルートノードを初期化\n root = TreeNode(preorder[i])\n # m をクエリして左部分木と右部分木を分割\n m = inorder_map[preorder[i]]\n # 部分問題:左部分木を構築\n root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n # 部分問題:右部分木を構築\n root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n # ルートノードを返す\n return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n \"\"\"二分木を構築\"\"\"\n # ハッシュテーブルを初期化、中順走査の要素からインデックスへのマッピングを保存\n inorder_map = {val: i for i, val in enumerate(inorder)}\n root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n return root\n</code></pre> build_tree.cpp<pre><code>/* 二分木の構築:分割統治 */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n // 部分木の区間が空の場合に終了\n if (r - l < 0)\n return NULL;\n // ルートノードを初期化\n TreeNode *root = new TreeNode(preorder[i]);\n // m を問い合わせて左右の部分木を分割\n int m = inorderMap[preorder[i]];\n // 部分問題:左の部分木を構築\n root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n // 部分問題:右の部分木を構築\n root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // ルートノードを返す\n return root;\n}\n\n/* 二分木の構築 */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n // ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納\n unordered_map<int, int> inorderMap;\n for (int i = 0; i < inorder.size(); i++) {\n inorderMap[inorder[i]] = i;\n }\n TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n return root;\n}\n</code></pre> build_tree.java<pre><code>/* 二分木の構築:分割統治 */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n // 部分木の区間が空の場合に終了\n if (r - l < 0)\n return null;\n // ルートノードを初期化\n TreeNode root = new TreeNode(preorder[i]);\n // m を問い合わせて左右の部分木を分割\n int m = inorderMap.get(preorder[i]);\n // 部分問題:左の部分木を構築\n root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n // 部分問題:右の部分木を構築\n root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // ルートノードを返す\n return root;\n}\n\n/* 二分木の構築 */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n // ハッシュテーブルを初期化し、中間順序の要素からインデックスへのマッピングを格納\n Map<Integer, Integer> inorderMap = new HashMap<>();\n for (int i = 0; i < inorder.length; i++) {\n inorderMap.put(inorder[i], i);\n }\n TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n return root;\n}\n</code></pre> build_tree.cs<pre><code>[class]{build_tree}-[func]{DFS}\n\n[class]{build_tree}-[func]{BuildTree}\n</code></pre> build_tree.go<pre><code>[class]{}-[func]{dfsBuildTree}\n\n[class]{}-[func]{buildTree}\n</code></pre> build_tree.swift<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{buildTree}\n</code></pre> build_tree.js<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{buildTree}\n</code></pre> build_tree.ts<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{buildTree}\n</code></pre> build_tree.dart<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{buildTree}\n</code></pre> build_tree.rs<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{build_tree}\n</code></pre> build_tree.c<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{buildTree}\n</code></pre> build_tree.kt<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{buildTree}\n</code></pre> build_tree.rb<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{build_tree}\n</code></pre> <p>以下の図は、二分木を構築する再帰過程を示しています。各ノードは再帰の「下降」段階で作成され、各エッジ(参照)は「上昇」段階で形成されます。</p> <1><2><3><4><5><6><7><8><9> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 12-8 二分木構築の再帰過程 </p> <p>各再帰関数の <code>preorder</code> と <code>inorder</code> シーケンスの分割は以下の図に示されています。</p> <p></p> <p> 図 12-9 各再帰関数での分割 </p> <p>二分木が \\(n\\) 個のノードを持つと仮定すると、各ノードの初期化(再帰関数 <code>dfs()</code> の呼び出し)には \\(O(1)\\) 時間がかかります。したがって、全体の時間計算量は \\(O(n)\\) です。</p> <p>ハッシュテーブルは <code>inorder</code> 要素からそのインデックスへのマッピングを格納するため、\\(O(n)\\) スペースが必要です。最悪の場合、二分木が連結リストに退化すると、再帰の深さは \\(n\\) に達し、\\(O(n)\\) のスタックスペースを消費する可能性があります。したがって、全体の空間計算量は \\(O(n)\\) です。</p>","path":["第 12 章 分割統治","12.3 二分木構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1 分割統治アルゴリズム","text":"<p>分割統治は重要で人気のあるアルゴリズム戦略です。名前が示すように、アルゴリズムは通常再帰的に実装され、「分割」と「統治」の2つのステップから構成されます。</p> <ol> <li>分割(分割段階):元の問題を再帰的に2つ以上の小さな部分問題に分解し、最小の部分問題に到達するまで続けます。</li> <li>統治(マージ段階):解決方法が既知の最小の部分問題から開始し、部分問題の解をボトムアップ方式でマージして元の問題の解を構築します。</li> </ol> <p>以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。</p> <ol> <li>分割:元の配列(元の問題)を再帰的に2つの副配列(部分問題)に分割し、副配列が1つの要素のみになるまで(最小の部分問題)続けます。</li> <li>統治:順序付けられた副配列(部分問題の解)をボトムアップでマージして、順序付けられた元の配列(元の問題の解)を取得します。</li> </ol> <p></p> <p> 図 12-1 マージソートの分割統治戦略 </p>","path":["第 12 章 分割統治","12.1 分割統治アルゴリズム"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211","level":2,"title":"12.1.1 分割統治問題を特定する方法","text":"<p>問題が分割統治解決に適しているかどうかは、通常以下の基準に基づいて決定できます。</p> <ol> <li>問題をより小さなものに分解できる:元の問題をより小さく類似した部分問題に分割でき、そのような過程を同じ方法で再帰的に実行できます。</li> <li>部分問題は独立している:部分問題間に重複がなく、独立しており、個別に解決できます。</li> <li>部分問題の解をマージできる:元の問題の解は、部分問題の解を組み合わせることで導出されます。</li> </ol> <p>明らかに、マージソートはこれら3つの基準を満たしています。</p> <ol> <li>問題をより小さなものに分解できる:配列(元の問題)を再帰的に2つの副配列(部分問題)に分割します。</li> <li>部分問題は独立している:各副配列は独立してソートできます(部分問題は独立して解決できます)。</li> <li>部分問題の解をマージできる:2つの順序付けられた副配列(部分問題の解)を1つの順序付けられた配列(元の問題の解)にマージできます。</li> </ol>","path":["第 12 章 分割統治","12.1 分割統治アルゴリズム"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2 分割統治による効率の向上","text":"<p>分割統治戦略はアルゴリズム問題を効果的に解決するだけでなく、しばしば効率を向上させます。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートは、分割統治戦略を適用しているため、選択ソート、バブルソート、挿入ソートよりも高速です。</p> <p>私たちの心には疑問があるかもしれません:なぜ分割統治はアルゴリズムの効率を向上させることができ、その根本的な論理は何ですか? つまり、問題を部分問題に分解し、それらを解決し、それらの解を組み合わせて元の問題に対処することが、元の問題を直接解決するよりも効率的である理由は何ですか?この質問は2つの側面から分析できます:操作数と並列計算。</p>","path":["第 12 章 分割統治","12.1 分割統治アルゴリズム"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1. 操作数の最適化","text":"<p>「バブルソート」を例にとると、長さ \\(n\\) の配列を処理するのに \\(O(n^2)\\) 時間が必要です。以下の図に示すように、配列を中点から2つの副配列に分割するとします。そのような分割には \\(O(n)\\) 時間が必要です。各副配列のソートには \\(O((n / 2)^2)\\) 時間が必要です。そして2つの副配列のマージには \\(O(n)\\) 時間が必要です。したがって、全体の時間計算量は:</p> \\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\] <p></p> <p> 図 12-2 配列分割前後のバブルソート </p> <p>以下の不等式を計算してみましょう。左側は分割前の総操作数を表し、右側は分割後の総操作数をそれぞれ表します:</p> \\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\] <p>これは \\(n > 4\\) の場合、分割後の操作数が少なく、より良いパフォーマンスにつながることを意味します。分割後の時間計算量は依然として二次 \\(O(n^2)\\) ですが、計算量の定数係数が減少していることに注意してください。</p> <p>さらに進むことができます。副配列をその中点からさらに2つの副配列に分割し続けて、副配列が1つの要素のみになるまで続けたらどうでしょうか? このアイデアは実際には「マージソート」で、時間計算量は \\(O(n \\log n)\\) です。</p> <p>少し違うことを試してみましょう。2つではなく、より多くの分割に分割したらどうでしょうか? 例えば、元の配列を \\(k\\) 個の副配列に均等に分割しますか?このアプローチは「バケットソート」と非常に似ており、大量のデータのソートに非常に適しています。理論的には、時間計算量は \\(O(n + k)\\) に達することができます。</p>","path":["第 12 章 分割統治","12.1 分割統治アルゴリズム"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2","level":3,"title":"2. 並列計算による最適化","text":"<p>分割統治によって生成される部分問題は互いに独立していることが分かっています。これは、それらを並列で解決できることを意味します。 その結果、分割統治はアルゴリズムの時間計算量を減らすだけでなく、現代のオペレーティングシステムによる並列最適化も促進します。</p> <p>並列最適化は、複数のコアやプロセッサを持つ環境で特に効果的です。システムが複数の部分問題を同時に処理できるため、計算リソースを完全に活用し、全体的な実行時間が大幅に短縮されます。</p> <p>例えば、以下の図に示す「バケットソート」では、大量のデータを様々なバケットに均等に分解します。各バケットのソート作業は、利用可能な計算ユニットに割り当てることができます。すべての作業が完了すると、すべてのソートされたバケットがマージされて最終結果が生成されます。</p> <p></p> <p> 図 12-3 バケットソートの並列計算 </p>","path":["第 12 章 分割統治","12.1 分割統治アルゴリズム"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213","level":2,"title":"12.1.3 分割統治の一般的な応用","text":"<p>分割統治は多くの古典的なアルゴリズム問題を解決するために使用できます。</p> <ul> <li>最近点対の発見:このアルゴリズムは点の集合を2つの半分に分割することで動作します。そして各半分で再帰的に最近点対を見つけます。最後に、2つの半分にまたがるペアを考慮して、全体の最近点対を見つけます。</li> <li>大整数の乗算:一つのアルゴリズムはKaratsubaと呼ばれます。大整数の乗算をいくつかの小さな整数の乗算と加算に分解します。</li> <li>行列の乗算:一例はStrassenアルゴリズムです。大きな行列の乗算を複数の小さな行列の乗算と加算に分解します。</li> <li>ハノイの塔問題:ハノイの塔問題は再帰的に解決でき、分割統治戦略の典型的な応用です。</li> <li>転倒対の解決:シーケンスで、前の数が後の数より大きい場合、これら2つの数は転倒対を構成します。転倒対問題の解決は、マージソートの助けを借りて、分割統治のアイデアを利用できます。</li> </ul> <p>分割統治はアルゴリズムとデータ構造の設計にも広く応用されています。</p> <ul> <li>二分探索:二分探索は、ソート済み配列を中点インデックスから2つの半分に分割します。そして、ターゲット値と中間要素値の比較結果に基づいて、一方の半分が破棄されます。同じプロセスで残りの半分で検索が続行され、ターゲットが見つかるか残りの要素がなくなるまで続きます。</li> <li>マージソート:この節の冒頭ですでに紹介したため、さらなる詳述は不要です。</li> <li>クイックソート:クイックソートはピボット値を選択して配列を2つの副配列に分割し、一方はピボットより小さい要素、もう一方はピボットより大きい要素を持ちます。このプロセスは、これら2つの副配列のそれぞれに対して、1つの要素のみを保持するまで続きます。</li> <li>バケットソート:バケットソートの基本的なアイデアは、データを複数のバケットに分散させることです。各バケット内の要素をソートした後、バケットから順序よく要素を取得して順序付けられた配列を取得します。</li> <li>木:例えば、二分探索木、AVL木、赤黒木、B木、B+木など。その操作(検索、挿入、削除)はすべて分割統治戦略の応用と見なすことができます。</li> <li>ヒープ:ヒープは特別なタイプの完全二分木です。その様々な操作(挿入、削除、ヒープ化)は、実際に分割統治のアイデアを含意しています。</li> <li>ハッシュテーブル:ハッシュテーブルは直接分割統治を適用しませんが、一部のハッシュ衝突解決ソリューションは間接的にこの戦略を適用します。例えば、チェイン法の長いリストは、クエリ効率を向上させるために赤黒木に変換される場合があります。</li> </ul> <p>**分割統治は巧妙に浸透するアルゴリズムアイデア**であり、様々なアルゴリズムとデータ構造に組み込まれていることが分かります。</p>","path":["第 12 章 分割統治","12.1 分割統治アルゴリズム"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4 ハノイの塔問題","text":"<p>マージソートと二分木構築の両方で、元の問題を2つの部分問題に分解し、それぞれが元の問題のサイズの半分でした。しかし、ハノイの塔では、異なる分解戦略を採用します。</p> <p>Question</p> <p>3つの柱があり、それぞれ <code>A</code>、<code>B</code>、<code>C</code> と表記されます。最初、柱 <code>A</code> には \\(n\\) 枚の円盤があり、上から下に向かって昇順のサイズで配置されています。私たちのタスクは、これらの \\(n\\) 枚の円盤を柱 <code>C</code> に移動し、元の順序を維持することです(以下の図に示すように)。移動中には以下のルールが適用されます:</p> <ol> <li>円盤は柱の上部からのみ取り除くことができ、別の柱の上部に置く必要があります。</li> <li>一度に移動できるのは1枚の円盤のみです。</li> <li>小さい円盤は常に大きい円盤の上にある必要があります。</li> </ol> <p></p> <p> 図 12-10 ハノイの塔の例 </p> <p>サイズ \\(i\\) のハノイの塔問題を \\(f(i)\\) と表記します。例えば、\\(f(3)\\) は3枚の円盤を柱 <code>A</code> から柱 <code>C</code> に移動することを表します。</p>","path":["第 12 章 分割統治","12.4 ハノイの塔問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1","level":3,"title":"1. 基本ケースを考える","text":"<p>以下の図に示すように、問題 \\(f(1)\\)(円盤が1枚のみ)については、<code>A</code> から <code>C</code> に直接移動できます。</p> <1><2> <p></p> <p></p> <p> 図 12-11 サイズ1の問題の解 </p> <p>\\(f(2)\\)(円盤が2枚)については、**柱 <code>B</code> の助けを借りて小さい円盤を大きい円盤の上に保つ**必要があります。以下の図に示すように:</p> <ol> <li>まず、小さい円盤を <code>A</code> から <code>B</code> に移動します。</li> <li>次に、大きい円盤を <code>A</code> から <code>C</code> に移動します。</li> <li>最後に、小さい円盤を <code>B</code> から <code>C</code> に移動します。</li> </ol> <1><2><3><4> <p></p> <p></p> <p></p> <p></p> <p> 図 12-12 サイズ2の問題の解 </p> <p>\\(f(2)\\) を解決する過程は次のように要約できます:<code>B</code> の助けを借りて2枚の円盤を <code>A</code> から <code>C</code> に移動する。ここで、<code>C</code> をターゲット柱、<code>B</code> をバッファ柱と呼びます。</p>","path":["第 12 章 分割統治","12.4 ハノイの塔問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2. 部分問題の分解","text":"<p>問題 \\(f(3)\\)(つまり、円盤が3枚の場合)については、状況がやや複雑になります。</p> <p>すでに \\(f(1)\\) と \\(f(2)\\) の解が分かっているので、分割統治の観点を採用し、<code>A</code> の上の2枚の円盤を1つの単位として扱い、以下の図に示すステップを実行できます。これにより、3枚の円盤を <code>A</code> から <code>C</code> に正常に移動できます。</p> <ol> <li><code>B</code> をターゲット柱、<code>C</code> をバッファ柱として、2枚の円盤を <code>A</code> から <code>B</code> に移動します。</li> <li>残りの円盤を <code>A</code> から直接 <code>C</code> に移動します。</li> <li><code>C</code> をターゲット柱、<code>A</code> をバッファ柱として、2枚の円盤を <code>B</code> から <code>C</code> に移動します。</li> </ol> <1><2><3><4> <p></p> <p></p> <p></p> <p></p> <p> 図 12-13 サイズ3の問題の解 </p> <p>本質的に、\\(f(3)\\) を2つの \\(f(2)\\) 部分問題と1つの \\(f(1)\\) 部分問題に分解します。これら3つの部分問題を順次解決することで、元の問題が解決され、部分問題が独立しており、それらの解をマージできることを示しています。</p> <p>ここから、以下の図に示すハノイの塔の分割統治戦略を要約できます。元の問題 \\(f(n)\\) を2つの部分問題 \\(f(n-1)\\) と1つの部分問題 \\(f(1)\\) に分割し、以下の順序でこれら3つの部分問題を解決します:</p> <ol> <li><code>C</code> をバッファとして使用し、\\(n-1\\) 枚の円盤を <code>A</code> から <code>B</code> に移動します。</li> <li>残りの円盤を <code>A</code> から直接 <code>C</code> に移動します。</li> <li><code>A</code> をバッファとして使用し、\\(n-1\\) 枚の円盤を <code>B</code> から <code>C</code> に移動します。</li> </ol> <p>各 \\(f(n-1)\\) 部分問題について、同じ再帰分割を適用でき、最小の部分問題 \\(f(1)\\) に到達するまで続けます。\\(f(1)\\) は単一の移動のみが必要であることがすでに分かっているため、解決するのは簡単です。</p> <p></p> <p> 図 12-14 ハノイの塔を解決するための分割統治戦略 </p>","path":["第 12 章 分割統治","12.4 ハノイの塔問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3","level":3,"title":"3. コード実装","text":"<p>コードでは、再帰関数 <code>dfs(i, src, buf, tar)</code> を定義します。これは柱 <code>src</code> から上の \\(i\\) 枚の円盤を柱 <code>tar</code> に移動し、柱 <code>buf</code> をバッファとして使用します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py<pre><code>def move(src: list[int], tar: list[int]):\n \"\"\"円盤を移動\"\"\"\n # src の上から円盤を取り出す\n pan = src.pop()\n # 円盤を tar の上に置く\n tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n \"\"\"ハノイの塔問題 f(i) を解く\"\"\"\n # src に円盤が 1 つだけ残っている場合、それを tar に移動\n if i == 1:\n move(src, tar)\n return\n # 部分問題 f(i-1):tar の助けを借りて src の上の i-1 個の円盤を buf に移動\n dfs(i - 1, src, tar, buf)\n # 部分問題 f(1):残りの 1 個の円盤を src から tar に移動\n move(src, tar)\n # 部分問題 f(i-1):src の助けを借りて buf の上の i-1 個の円盤を tar に移動\n dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n \"\"\"ハノイの塔問題を解く\"\"\"\n n = len(A)\n # B の助けを借りて A の上の n 個の円盤を C に移動\n dfs(n, A, B, C)\n</code></pre> hanota.cpp<pre><code>/* 円盤を移動 */\nvoid move(vector<int> &src, vector<int> &tar) {\n // src の最上部から円盤を取り出す\n int pan = src.back();\n src.pop_back();\n // 円盤を tar の最上部に配置\n tar.push_back(pan);\n}\n\n/* ハノイの塔問題 f(i) を解く */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n // src に円盤が1つだけ残っている場合、それを tar に移動\n if (i == 1) {\n move(src, tar);\n return;\n }\n // 部分問題 f(i-1):tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動\n dfs(i - 1, src, tar, buf);\n // 部分問題 f(1):残りの1つの円盤を src から tar に移動\n move(src, tar);\n // 部分問題 f(i-1):src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動\n dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔問題を解く */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n int n = A.size();\n // B の助けを借りて、上位 n 個の円盤を A から C に移動\n dfs(n, A, B, C);\n}\n</code></pre> hanota.java<pre><code>/* 円盤を移動 */\nvoid move(List<Integer> src, List<Integer> tar) {\n // src の最上部から円盤を取り出す\n Integer pan = src.remove(src.size() - 1);\n // 円盤を tar の最上部に配置\n tar.add(pan);\n}\n\n/* ハノイの塔問題 f(i) を解く */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n // src に円盤が1つだけ残っている場合、それを tar に移動\n if (i == 1) {\n move(src, tar);\n return;\n }\n // 部分問題 f(i-1):tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動\n dfs(i - 1, src, tar, buf);\n // 部分問題 f(1):残りの1つの円盤を src から tar に移動\n move(src, tar);\n // 部分問題 f(i-1):src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動\n dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔問題を解く */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n int n = A.size();\n // B の助けを借りて、上位 n 個の円盤を A から C に移動\n dfs(n, A, B, C);\n}\n</code></pre> hanota.cs<pre><code>[class]{hanota}-[func]{Move}\n\n[class]{hanota}-[func]{DFS}\n\n[class]{hanota}-[func]{SolveHanota}\n</code></pre> hanota.go<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfsHanota}\n\n[class]{}-[func]{solveHanota}\n</code></pre> hanota.swift<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solveHanota}\n</code></pre> hanota.js<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solveHanota}\n</code></pre> hanota.ts<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solveHanota}\n</code></pre> hanota.dart<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solveHanota}\n</code></pre> hanota.rs<pre><code>[class]{}-[func]{move_pan}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solve_hanota}\n</code></pre> hanota.c<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solveHanota}\n</code></pre> hanota.kt<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solveHanota}\n</code></pre> hanota.rb<pre><code>[class]{}-[func]{move}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{solve_hanota}\n</code></pre> <p>以下の図に示すように、ハノイの塔問題は高さ \\(n\\) の再帰木として視覚化できます。各ノードは部分問題を表し、<code>dfs()</code> の呼び出しに対応します。したがって、時間計算量は \\(O(2^n)\\)、空間計算量は \\(O(n)\\) です。</p> <p></p> <p> 図 12-15 ハノイの塔の再帰木 </p> <p>Quote</p> <p>ハノイの塔は古代の伝説に由来します。古代インドの寺院で、僧侶たちは3本の高いダイヤモンドの柱と、異なるサイズの \\(64\\) 枚の金の円盤を持っていました。彼らは、最後の円盤が正しく置かれたとき、世界が終わると信じていました。</p> <p>しかし、僧侶たちが1秒に1枚の円盤を移動したとしても、約 \\(2^{64} \\approx 1.84×10^{19}\\) —約5850億年—かかり、宇宙の年齢の現在の推定をはるかに超えています。したがって、この伝説が真実であれば、世界の終わりについて心配する必要はおそらくないでしょう。</p>","path":["第 12 章 分割統治","12.4 ハノイの塔問題"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5 まとめ","text":"<ul> <li>分割統治は一般的なアルゴリズム設計戦略で、分割(分割)と統治(マージ)の2つの段階から構成され、一般的に再帰を使用して実装されます。</li> <li>問題が分割統治アプローチに適しているかどうかを判断するために、問題が分解可能かどうか、部分問題が独立しているかどうか、部分問題をマージできるかどうかを確認します。</li> <li>マージソートは分割統治戦略の典型的な例です。配列を再帰的に2つの等しい長さの副配列に分割し、1つの要素のみが残るまで続け、次にこれらの副配列を層ごとにマージしてソートを完了します。</li> <li>分割統治戦略の導入は、しばしばアルゴリズムの効率を向上させます。一方では操作数を減らし、他方では分割後のシステムの並列最適化を促進します。</li> <li>分割統治は多数のアルゴリズム問題に適用でき、データ構造とアルゴリズム設計で広く使用され、多くのシナリオに現れます。</li> <li>総当たり検索と比較して、適応検索はより効率的です。時間計算量が \\(O(\\log n)\\) の検索アルゴリズムは、通常分割統治戦略に基づいています。</li> <li>二分探索は分割統治戦略のもう一つの古典的な応用です。部分問題の解のマージを含まず、再帰的な分割統治アプローチで実装できます。</li> <li>二分木構築問題では、木の構築(元の問題)を左の部分木と右の部分木の構築(部分問題)に分割できます。これは前順走査と中順走査のインデックス範囲を分割することで実現できます。</li> <li>ハノイの塔問題では、サイズ \\(n\\) の問題をサイズ \\(n-1\\) の2つの部分問題とサイズ \\(1\\) の1つの部分問題に分解できます。これら3つの部分問題を順次解決することで、元の問題が解決されます。</li> </ul>","path":["第 12 章 分割統治","12.5 まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"第 14 章 動的プログラミング","text":"<p>Abstract</p> <p>川が流れて海に注ぐように、</p> <p>動的プログラミングは小さな問題の解を織り合わせて、より大きな問題の解へと導きます。一歩一歩進んで、最終的な答えが待つ彼岸へと向かいます。</p>","path":["第 14 章 動的計画法","第 14 章 動的プログラミング"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"章の内容","text":"<ul> <li>14.1 動的計画法の初歩</li> <li>14.2 DP 問題の特性</li> <li>14.3 DP の解法の考え方</li> <li>14.4 0-1ナップサック問題</li> <li>14.5 完全ナップサック問題</li> <li>14.6 編集距離問題</li> <li>14.7 まとめ</li> </ul>","path":["第 14 章 動的計画法","第 14 章 動的プログラミング"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2 動的プログラミング問題の特徴","text":"<p>前のセクションでは、動的プログラミングが問題を部分問題に分解することで元の問題を解決する方法を学びました。実際、部分問題の分解は一般的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングでは異なる重点があります。</p> <ul> <li>分割統治法アルゴリズムは元の問題を複数の独立した部分問題に再帰的に分割し、最小の部分問題に到達するまで続け、バックトラッキング時に部分問題の解を組み合わせて最終的に元の問題の解を得ます。</li> <li>動的プログラミングも問題を再帰的に分解しますが、分割統治法アルゴリズムとの主な違いは、動的プログラミングの部分問題が相互依存的であり、分解プロセス中に多くの重複する部分問題が現れることです。</li> <li>バックトラッキングアルゴリズムは試行錯誤によってすべての可能な解を網羅し、枝刈りによって不必要な探索分岐を避けます。元の問題の解は一連の決定ステップから構成され、各決定ステップ前の各部分シーケンスを部分問題として考えることができます。</li> </ul> <p>実際、動的プログラミングは最適化問題を解決するためによく使用され、これらは重複する部分問題を含むだけでなく、他に2つの主要な特徴があります:最適部分構造と無記憶性です。</p>","path":["第 14 章 動的計画法","14.2 動的プログラミング問題の特徴"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421","level":2,"title":"14.2.1 最適部分構造","text":"<p>階段登り問題を少し修正して、最適部分構造の概念を実証するのにより適したものにします。</p> <p>階段登りの最小コスト</p> <p>階段があり、一度に1段または2段上ることができ、階段の各段にはその段で支払う必要があるコストを表す非負の整数があります。非負の整数配列 \\(cost\\) が与えられ、\\(cost[i]\\) は \\(i\\) 段目で支払う必要があるコストを表し、\\(cost[0]\\) は地面(開始点)です。頂上に到達するために必要な最小コストは何ですか?</p> <p>下の図に示すように、1段目、2段目、3段目のコストがそれぞれ \\(1\\)、\\(10\\)、\\(1\\) の場合、地面から3段目に登る最小コストは \\(2\\) です。</p> <p></p> <p> 図 14-6 3段目に登る最小コスト </p> <p>\\(dp[i]\\) を \\(i\\) 段目に登る累積コストとします。\\(i\\) 段目は \\(i-1\\) 段目または \\(i-2\\) 段目からのみ来ることができるため、\\(dp[i]\\) は \\(dp[i-1] + cost[i]\\) または \\(dp[i-2] + cost[i]\\) のいずれかしかありえません。コストを最小化するために、2つのうち小さい方を選択すべきです:</p> \\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\] <p>これにより最適部分構造の意味がわかります:元の問題の最適解は部分問題の最適解から構築される。</p> <p>この問題は明らかに最適部分構造を持っています:2つの部分問題 \\(dp[i-1]\\) と \\(dp[i-2]\\) の最適解からより良い方を選択し、それを使用して元の問題 \\(dp[i]\\) の最適解を構築します。</p> <p>では、前のセクションの階段登り問題は最適部分構造を持っているでしょうか?その目標は解の数を求めることで、これは数え上げ問題のようですが、別の方法で尋ねてみましょう:「解の最大数を求める」。驚くことに、**問題が変わったにもかかわらず、最適部分構造が現れた**ことがわかります:\\(n\\) 段目での解の最大数は、\\(n-1\\) 段目と \\(n-2\\) 段目での解の最大数の和に等しいです。したがって、最適部分構造の解釈は非常に柔軟で、異なる問題では異なる意味を持ちます。</p> <p>状態遷移方程式と初期状態 \\(dp[1] = cost[1]\\) および \\(dp[2] = cost[2]\\) に従って、動的プログラミングコードを得ることができます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py<pre><code>def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n \"\"\"最小コスト階段登り:動的プログラミング\"\"\"\n n = len(cost) - 1\n if n == 1 or n == 2:\n return cost[n]\n # dp テーブルを初期化、部分問題の解を格納するために使用\n dp = [0] * (n + 1)\n # 初期状態:最小の部分問題の解を事前設定\n dp[1], dp[2] = cost[1], cost[2]\n # 状態遷移:小さい部分問題から大きい部分問題を段階的に解く\n for i in range(3, n + 1):\n dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n return dp[n]\n</code></pre> min_cost_climbing_stairs_dp.cpp<pre><code>/* 最小コスト階段登り:動的プログラミング */\nint minCostClimbingStairsDP(vector<int> &cost) {\n int n = cost.size() - 1;\n if (n == 1 || n == 2)\n return cost[n];\n // DPテーブルを初期化し、部分問題の解を格納するために使用\n vector<int> dp(n + 1);\n // 初期状態:最小の部分問題の解を事前設定\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 状態遷移:小さな問題から大きな部分問題を段階的に解く\n for (int i = 3; i <= n; i++) {\n dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.java<pre><code>/* 最小コスト階段登り:動的プログラミング */\nint minCostClimbingStairsDP(int[] cost) {\n int n = cost.length - 1;\n if (n == 1 || n == 2)\n return cost[n];\n // DPテーブルを初期化し、部分問題の解を格納するために使用\n int[] dp = new int[n + 1];\n // 初期状態:最小の部分問題の解を事前設定\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 状態遷移:小さな問題から大きな部分問題を段階的に解く\n for (int i = 3; i <= n; i++) {\n dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.cs<pre><code>[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.go<pre><code>[class]{}-[func]{minCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.swift<pre><code>[class]{}-[func]{minCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.js<pre><code>[class]{}-[func]{minCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.ts<pre><code>[class]{}-[func]{minCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.dart<pre><code>[class]{}-[func]{minCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.rs<pre><code>[class]{}-[func]{min_cost_climbing_stairs_dp}\n</code></pre> min_cost_climbing_stairs_dp.c<pre><code>[class]{}-[func]{minCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.kt<pre><code>[class]{}-[func]{minCostClimbingStairsDP}\n</code></pre> min_cost_climbing_stairs_dp.rb<pre><code>[class]{}-[func]{min_cost_climbing_stairs_dp}\n</code></pre> <p>下の図は上記コードの動的プログラミングプロセスを示しています。</p> <p></p> <p> 図 14-7 階段登りの最小コストの動的プログラミングプロセス </p> <p>この問題も空間最適化が可能で、1次元を0に圧縮し、空間計算量を \\(O(n)\\) から \\(O(1)\\) に削減できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py<pre><code>def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n \"\"\"最小コスト階段登り:空間最適化動的プログラミング\"\"\"\n n = len(cost) - 1\n if n == 1 or n == 2:\n return cost[n]\n a, b = cost[1], cost[2]\n for i in range(3, n + 1):\n a, b = b, min(a, b) + cost[i]\n return b\n</code></pre> min_cost_climbing_stairs_dp.cpp<pre><code>/* 最小コスト階段登り:空間最適化動的プログラミング */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n int n = cost.size() - 1;\n if (n == 1 || n == 2)\n return cost[n];\n int a = cost[1], b = cost[2];\n for (int i = 3; i <= n; i++) {\n int tmp = b;\n b = min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.java<pre><code>/* 最小コスト階段登り:空間最適化動的プログラミング */\nint minCostClimbingStairsDPComp(int[] cost) {\n int n = cost.length - 1;\n if (n == 1 || n == 2)\n return cost[n];\n int a = cost[1], b = cost[2];\n for (int i = 3; i <= n; i++) {\n int tmp = b;\n b = Math.min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.cs<pre><code>[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.go<pre><code>[class]{}-[func]{minCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.swift<pre><code>[class]{}-[func]{minCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.js<pre><code>[class]{}-[func]{minCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.ts<pre><code>[class]{}-[func]{minCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.dart<pre><code>[class]{}-[func]{minCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.rs<pre><code>[class]{}-[func]{min_cost_climbing_stairs_dp_comp}\n</code></pre> min_cost_climbing_stairs_dp.c<pre><code>[class]{}-[func]{minCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.kt<pre><code>[class]{}-[func]{minCostClimbingStairsDPComp}\n</code></pre> min_cost_climbing_stairs_dp.rb<pre><code>[class]{}-[func]{min_cost_climbing_stairs_dp_comp}\n</code></pre>","path":["第 14 章 動的計画法","14.2 動的プログラミング問題の特徴"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422","level":2,"title":"14.2.2 無記憶性","text":"<p>無記憶性は動的プログラミングが問題解決に効果的であることを可能にする重要な特徴の1つです。その定義は:特定の状態が与えられたとき、その将来の発展は現在の状態のみに関連し、過去に経験したすべての状態とは無関係である。</p> <p>階段登り問題を例に取ると、状態 \\(i\\) が与えられたとき、それは状態 \\(i+1\\) と \\(i+2\\) に発展し、それぞれ1段ジャンプと2段ジャンプに対応します。これら2つの選択をするとき、状態 \\(i\\) より前の状態を考慮する必要はありません。なぜなら、それらは状態 \\(i\\) の将来に影響しないからです。</p> <p>しかし、階段登り問題に制約を追加すると、状況が変わります。</p> <p>制約付き階段登り</p> <p>\\(n\\) 段の階段があり、毎回1段または2段上ることができますが、1段を2回連続でジャンプすることはできません。頂上に登る方法は何通りありますか?</p> <p>下の図に示すように、3段目に登る実行可能な選択肢は2つだけで、1段を3回連続でジャンプする選択肢は制約条件を満たさないため破棄されます。</p> <p></p> <p> 図 14-8 制約付きで3段目に登る実行可能な選択肢の数 </p> <p>この問題では、前回が1段ジャンプだった場合、次回は必ず2段ジャンプでなければなりません。これは**次のステップの選択が現在の状態(現在の階段段数)だけでは独立して決定できず、前の状態(前回の階段段数)にも依存する**ことを意味します。</p> <p>この問題がもはや無記憶性を満たさず、状態遷移方程式 \\(dp[i] = dp[i-1] + dp[i-2]\\) も失敗することは容易にわかります。なぜなら \\(dp[i-1]\\) は今回の1段ジャンプを表しますが、多くの「前回が1段ジャンプだった」選択肢を含んでおり、制約を満たすためにはこれらを直接 \\(dp[i]\\) に含めることはできません。</p> <p>このため、状態定義を拡張する必要があります:状態 \\([i, j]\\) は \\(i\\) 段目にいて、前回が \\(j\\) 段ジャンプだったことを表す。ここで \\(j \\in \\{1, 2\\}\\) です。この状態定義は前回が1段ジャンプだったか2段ジャンプだったかを効果的に区別し、現在の状態がどこから来たかを適切に判断できます。</p> <ul> <li>前回が1段ジャンプだった場合、前々回は必ず2段ジャンプを選択していたはずです。つまり、\\(dp[i, 1]\\) は \\(dp[i-1, 2]\\) からのみ遷移できます。</li> <li>前回が2段ジャンプだった場合、前々回は1段ジャンプまたは2段ジャンプを選択できました。つまり、\\(dp[i, 2]\\) は \\(dp[i-2, 1]\\) または \\(dp[i-2, 2]\\) から遷移できます。</li> </ul> <p>下の図に示すように、\\(dp[i, j]\\) は状態 \\([i, j]\\) の解の数を表します。この時点で、状態遷移方程式は次のようになります:</p> \\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\] <p></p> <p> 図 14-9 制約を考慮した再帰関係 </p> <p>最終的に、\\(dp[n, 1] + dp[n, 2]\\) を返せばよく、この2つの合計が \\(n\\) 段目に登る解の総数を表します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py<pre><code>def climbing_stairs_constraint_dp(n: int) -> int:\n \"\"\"制約付き階段登り:動的プログラミング\"\"\"\n if n == 1 or n == 2:\n return 1\n # dp テーブルを初期化、部分問題の解を格納するために使用\n dp = [[0] * 3 for _ in range(n + 1)]\n # 初期状態:最小の部分問題の解を事前設定\n dp[1][1], dp[1][2] = 1, 0\n dp[2][1], dp[2][2] = 0, 1\n # 状態遷移:小さい部分問題から大きい部分問題を段階的に解く\n for i in range(3, n + 1):\n dp[i][1] = dp[i - 1][2]\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n return dp[n][1] + dp[n][2]\n</code></pre> climbing_stairs_constraint_dp.cpp<pre><code>/* 制約付き階段登り:動的プログラミング */\nint climbingStairsConstraintDP(int n) {\n if (n == 1 || n == 2) {\n return 1;\n }\n // DPテーブルを初期化し、部分問題の解を格納するために使用\n vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n // 初期状態:最小の部分問題の解を事前設定\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 状態遷移:小さな問題から大きな部分問題を段階的に解く\n for (int i = 3; i <= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n return dp[n][1] + dp[n][2];\n}\n</code></pre> climbing_stairs_constraint_dp.java<pre><code>/* 制約付き階段登り:動的プログラミング */\nint climbingStairsConstraintDP(int n) {\n if (n == 1 || n == 2) {\n return 1;\n }\n // DPテーブルを初期化し、部分問題の解を格納するために使用\n int[][] dp = new int[n + 1][3];\n // 初期状態:最小の部分問題の解を事前設定\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 状態遷移:小さな問題から大きな部分問題を段階的に解く\n for (int i = 3; i <= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n return dp[n][1] + dp[n][2];\n}\n</code></pre> climbing_stairs_constraint_dp.cs<pre><code>[class]{climbing_stairs_constraint_dp}-[func]{ClimbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.go<pre><code>[class]{}-[func]{climbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.swift<pre><code>[class]{}-[func]{climbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.js<pre><code>[class]{}-[func]{climbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.ts<pre><code>[class]{}-[func]{climbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.dart<pre><code>[class]{}-[func]{climbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.rs<pre><code>[class]{}-[func]{climbing_stairs_constraint_dp}\n</code></pre> climbing_stairs_constraint_dp.c<pre><code>[class]{}-[func]{climbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.kt<pre><code>[class]{}-[func]{climbingStairsConstraintDP}\n</code></pre> climbing_stairs_constraint_dp.rb<pre><code>[class]{}-[func]{climbing_stairs_constraint_dp}\n</code></pre> <p>上記のケースでは、前の状態のみを考慮すればよいため、状態定義を拡張することで依然として無記憶性を満たすことができます。しかし、一部の問題では非常に深刻な「状態効果」があります。</p> <p>障害物生成付き階段登り</p> <p>\\(n\\) 段の階段があり、毎回1段または2段上ることができます。**\\(i\\) 段目に登ったとき、システムが自動的に \\(2i\\) 段目に障害物を置き、その後のすべてのラウンドで \\(2i\\) 段目にジャンプすることが禁止される**と規定されています。例えば、最初の2ラウンドで2段目と3段目にジャンプした場合、その後は4段目と6段目にジャンプできません。頂上に登る方法は何通りありますか?</p> <p>この問題では、次のジャンプはすべての過去の状態に依存します。各ジャンプがより高い段に障害物を置き、将来のジャンプに影響するからです。このような問題では、動的プログラミングはしばしば解決に苦労します。</p> <p>実際、多くの複雑な組み合わせ最適化問題(巡回セールスマン問題など)は無記憶性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を選択して、限られた時間内に使用可能な局所最適解を得ます。</p>","path":["第 14 章 動的計画法","14.2 動的プログラミング問題の特徴"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3 動的プログラミング問題解決アプローチ","text":"<p>前の2つのセクションでは、動的プログラミング問題の主要な特徴を紹介しました。次に、より実用的な2つの問題を一緒に探索しましょう。</p> <ol> <li>問題が動的プログラミング問題かどうかをどのように判断するか?</li> <li>動的プログラミング問題を解決する完全なステップは何か?</li> </ol>","path":["第 14 章 動的計画法","14.3 動的プログラミング問題解決アプローチ"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1 問題の判定","text":"<p>一般的に言えば、問題が重複する部分問題、最適部分構造を含み、無記憶性を示す場合、通常動的プログラミング解法に適しています。しかし、問題の説明から直接これらの特徴を抽出することはしばしば困難です。したがって、通常は条件を緩和し、**まず問題がバックトラッキング(全探索)を使用した解決に適しているかどうかを観察**します。</p> <p>**バックトラッキングに適した問題は通常「決定木モデル」に適合**し、これは木構造を使用して記述でき、各ノードは決定を表し、各パスは決定のシーケンスを表します。</p> <p>言い換えると、問題が明示的な決定概念を含み、解が一連の決定を通じて生成される場合、それは決定木モデルに適合し、通常バックトラッキングを使用して解決できます。</p> <p>この基礎の上で、動的プログラミング問題を判定するための「ボーナスポイント」があります。</p> <ul> <li>問題に最大化(最小化)または最も(最も少ない)最適な解を見つけるという記述が含まれている。</li> <li>問題の状態がリスト、多次元行列、または木を使用して表現でき、状態がその周囲の状態と再帰関係を持っている。</li> </ul> <p>対応して、「ペナルティポイント」もあります。</p> <ul> <li>問題の目標は最適解だけでなく、すべての可能な解を見つけることである。</li> <li>問題の説明に順列と組み合わせの明らかな特徴があり、特定の複数の解を返す必要がある。</li> </ul> <p>問題が決定木モデルに適合し、比較的明らかな「ボーナスポイント」を持つ場合、それが動的プログラミング問題であると仮定し、解決プロセス中に検証できます。</p>","path":["第 14 章 動的計画法","14.3 動的プログラミング問題解決アプローチ"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2 問題解決ステップ","text":"<p>動的プログラミング問題解決プロセスは問題の性質と難易度によって異なりますが、一般的に次のステップに従います:決定の記述、状態の定義、\\(dp\\) テーブルの確立、状態遷移方程式の導出、境界条件の決定など。</p> <p>問題解決ステップをより具体的に説明するために、古典的な問題「最小経路和」を例として使用します。</p> <p>Question</p> <p>\\(n \\times m\\) の二次元グリッド <code>grid</code> が与えられ、グリッドの各セルには負でない整数が含まれ、そのセルのコストを表します。ロボットは左上のセルから始まり、各ステップで下または右にのみ移動でき、右下のセルに到達するまで続けます。左上から右下への最小経路和を返してください。</p> <p>下の図は例を示しており、与えられたグリッドの最小経路和は \\(13\\) です。</p> <p></p> <p> 図 14-10 最小経路和の例データ </p> <p>第1ステップ:各ラウンドの決定を考え、状態を定義し、それにより \\(dp\\) テーブルを得る</p> <p>この問題の各ラウンドの決定は、現在のセルから下または右に1ステップ移動することです。現在のセルの行と列のインデックスが \\([i, j]\\) であると仮定すると、下または右に移動した後、インデックスは \\([i+1, j]\\) または \\([i, j+1]\\) になります。したがって、状態には2つの変数が含まれるべきです:行インデックスと列インデックス、\\([i, j]\\) と表記されます。</p> <p>状態 \\([i, j]\\) は部分問題に対応します:開始点 \\([0, 0]\\) から \\([i, j]\\) への最小経路和、\\(dp[i, j]\\) と表記されます。</p> <p>このようにして、下の図に示す二次元 \\(dp\\) 行列を得ます。そのサイズは入力グリッド \\(grid\\) と同じです。</p> <p></p> <p> 図 14-11 状態定義とDPテーブル </p> <p>Note</p> <p>動的プログラミングとバックトラッキングは決定のシーケンスとして記述でき、状態はすべての決定変数から構成されます。問題解決の進行を記述するすべての変数を含むべきで、次の状態を導出するのに十分な情報を含んでいる必要があります。</p> <p>各状態は部分問題に対応し、すべての部分問題の解を保存するための \\(dp\\) テーブルを定義します。状態の各独立変数は \\(dp\\) テーブルの次元です。本質的に、\\(dp\\) テーブルは状態と部分問題の解の間のマッピングです。</p> <p>第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する</p> <p>状態 \\([i, j]\\) について、それは上のセル \\([i-1, j]\\) または左のセル \\([i, j-1]\\) からのみ導出できます。したがって、最適部分構造は:\\([i, j]\\) に到達する最小経路和は、\\([i, j-1]\\) と \\([i-1, j]\\) の最小経路和の小さい方によって決定されます。</p> <p>上記の分析に基づいて、下の図に示す状態遷移方程式を導出できます:</p> \\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\] <p></p> <p> 図 14-12 最適部分構造と状態遷移方程式 </p> <p>Note</p> <p>定義された \\(dp\\) テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解をどのように構築するか、つまり最適部分構造を見つけます。</p> <p>最適部分構造を特定したら、それを使用して状態遷移方程式を構築できます。</p> <p>第3ステップ:境界条件と状態遷移順序を決定する</p> <p>この問題では、最初の行の状態は左の状態からのみ来ることができ、最初の列の状態は上の状態からのみ来ることができるため、最初の行 \\(i = 0\\) と最初の列 \\(j = 0\\) が境界条件です。</p> <p>下の図に示すように、各セルは左のセルと上のセルから導出されるため、ループを使用して行列を走査し、外側のループは行を反復し、内側のループは列を反復します。</p> <p></p> <p> 図 14-13 境界条件と状態遷移順序 </p> <p>Note</p> <p>境界条件は動的プログラミングで \\(dp\\) テーブルを初期化するために使用され、探索では枝刈りに使用されます。</p> <p>状態遷移順序の核心は、現在の問題の解を計算するとき、それが依存するすべての小さな部分問題が既に正しく計算されていることを確保することです。</p> <p>上記の分析に基づいて、動的プログラミングコードを直接書くことができます。しかし、部分問題の分解はトップダウンアプローチであるため、「力任せ探索 → メモ化探索 → 動的プログラミング」の順序で実装することが習慣的な思考により適合します。</p>","path":["第 14 章 動的計画法","14.3 動的プログラミング問題解決アプローチ"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-1","level":3,"title":"1. 方法1:力任せ探索","text":"<p>状態 \\([i, j]\\) から探索を開始し、それを常により小さな状態 \\([i-1, j]\\) と \\([i, j-1]\\) に分解します。再帰関数には以下の要素が含まれます。</p> <ul> <li>再帰パラメータ:状態 \\([i, j]\\)。</li> <li>戻り値:\\([0, 0]\\) から \\([i, j]\\) への最小経路和 \\(dp[i, j]\\)。</li> <li>終了条件:\\(i = 0\\) かつ \\(j = 0\\) のとき、コスト \\(grid[0, 0]\\) を返す。</li> <li>枝刈り:\\(i < 0\\) または \\(j < 0\\) でインデックスが範囲外のとき、コスト \\(+\\infty\\) を返し、実行不可能性を表す。</li> </ul> <p>実装コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n \"\"\"最小パス和:ブルートフォース探索\"\"\"\n # 左上のセルの場合、探索を終了\n if i == 0 and j == 0:\n return grid[0][0]\n # 行または列のインデックスが範囲外の場合、+∞ コストを返す\n if i < 0 or j < 0:\n return inf\n # 左上から (i-1, j) と (i, j-1) への最小パスコストを計算\n up = min_path_sum_dfs(grid, i - 1, j)\n left = min_path_sum_dfs(grid, i, j - 1)\n # 左上から (i, j) への最小パスコストを返す\n return min(left, up) + grid[i][j]\n</code></pre> min_path_sum.cpp<pre><code>/* 最小パス和:ブルートフォース探索 */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n // 左上のセルの場合、探索を終了\n if (i == 0 && j == 0) {\n return grid[0][0];\n }\n // 行または列のインデックスが範囲外の場合、+∞ のコストを返す\n if (i < 0 || j < 0) {\n return INT_MAX;\n }\n // 左上から (i-1, j) と (i, j-1) への最小パスコストを計算\n int up = minPathSumDFS(grid, i - 1, j);\n int left = minPathSumDFS(grid, i, j - 1);\n // 左上から (i, j) への最小パスコストを返す\n return min(left, up) + grid[i][j];\n}\n</code></pre> min_path_sum.java<pre><code>/* 最小パス和:ブルートフォース探索 */\nint minPathSumDFS(int[][] grid, int i, int j) {\n // 左上のセルの場合、探索を終了\n if (i == 0 && j == 0) {\n return grid[0][0];\n }\n // 行または列のインデックスが範囲外の場合、+∞ のコストを返す\n if (i < 0 || j < 0) {\n return Integer.MAX_VALUE;\n }\n // 左上から (i-1, j) と (i, j-1) への最小パスコストを計算\n int up = minPathSumDFS(grid, i - 1, j);\n int left = minPathSumDFS(grid, i, j - 1);\n // 左上から (i, j) への最小パスコストを返す\n return Math.min(left, up) + grid[i][j];\n}\n</code></pre> min_path_sum.cs<pre><code>[class]{min_path_sum}-[func]{MinPathSumDFS}\n</code></pre> min_path_sum.go<pre><code>[class]{}-[func]{minPathSumDFS}\n</code></pre> min_path_sum.swift<pre><code>[class]{}-[func]{minPathSumDFS}\n</code></pre> min_path_sum.js<pre><code>[class]{}-[func]{minPathSumDFS}\n</code></pre> min_path_sum.ts<pre><code>[class]{}-[func]{minPathSumDFS}\n</code></pre> min_path_sum.dart<pre><code>[class]{}-[func]{minPathSumDFS}\n</code></pre> min_path_sum.rs<pre><code>[class]{}-[func]{min_path_sum_dfs}\n</code></pre> min_path_sum.c<pre><code>[class]{}-[func]{minPathSumDFS}\n</code></pre> min_path_sum.kt<pre><code>[class]{}-[func]{minPathSumDFS}\n</code></pre> min_path_sum.rb<pre><code>[class]{}-[func]{min_path_sum_dfs}\n</code></pre> <p>下の図は \\(dp[2, 1]\\) を根とする再帰木を示しており、いくつかの重複する部分問題を含み、その数はグリッド <code>grid</code> のサイズが増加すると急激に増加します。</p> <p>本質的に、重複する部分問題の理由は:**左上隅から特定のセルに到達する複数のパスが存在する**ことです。</p> <p></p> <p> 図 14-14 力任せ探索の再帰木 </p> <p>各状態には下と右の2つの選択があるため、左上隅から右下隅までの総ステップ数は \\(m + n - 2\\) で、最悪時間計算量は \\(O(2^{m + n})\\) です。この計算方法はグリッドエッジ近くの状況を考慮していないことに注意してください。ネットワークエッジに到達したとき、選択肢が1つしか残らないため、実際のパス数はより少なくなります。</p>","path":["第 14 章 動的計画法","14.3 動的プログラミング問題解決アプローチ"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2. 方法2:メモ化探索","text":"<p>グリッド <code>grid</code> と同じサイズのメモリスト <code>mem</code> を導入し、様々な部分問題の解を記録し、重複する部分問題を枝刈りします:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dfs_mem(\n grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n \"\"\"最小パス和:記憶化探索\"\"\"\n # 左上のセルの場合、探索を終了\n if i == 0 and j == 0:\n return grid[0][0]\n # 行または列のインデックスが範囲外の場合、+∞ コストを返す\n if i < 0 or j < 0:\n return inf\n # 記録がある場合、それを返す\n if mem[i][j] != -1:\n return mem[i][j]\n # 左と上のセルからの最小パスコスト\n up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n # 左上から (i, j) への最小パスコストを記録して返す\n mem[i][j] = min(left, up) + grid[i][j]\n return mem[i][j]\n</code></pre> min_path_sum.cpp<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.java<pre><code>/* 最小パス和:メモ化探索 */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n // 左上のセルの場合、探索を終了\n if (i == 0 && j == 0) {\n return grid[0][0];\n }\n // 行または列のインデックスが範囲外の場合、+∞ のコストを返す\n if (i < 0 || j < 0) {\n return Integer.MAX_VALUE;\n }\n // 記録がある場合、それを返す\n if (mem[i][j] != -1) {\n return mem[i][j];\n }\n // 左と上のセルからの最小パスコスト\n int up = minPathSumDFSMem(grid, mem, i - 1, j);\n int left = minPathSumDFSMem(grid, mem, i, j - 1);\n // 左上から (i, j) への最小パスコストを記録して返す\n mem[i][j] = Math.min(left, up) + grid[i][j];\n return mem[i][j];\n}\n</code></pre> min_path_sum.cs<pre><code>[class]{min_path_sum}-[func]{MinPathSumDFSMem}\n</code></pre> min_path_sum.go<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.swift<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.js<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.ts<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.dart<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.rs<pre><code>[class]{}-[func]{min_path_sum_dfs_mem}\n</code></pre> min_path_sum.c<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.kt<pre><code>[class]{}-[func]{minPathSumDFSMem}\n</code></pre> min_path_sum.rb<pre><code>[class]{}-[func]{min_path_sum_dfs_mem}\n</code></pre> <p>下の図に示すように、メモ化を導入した後、すべての部分問題の解は一度だけ計算される必要があるため、時間計算量は状態の総数、つまりグリッドサイズ \\(O(nm)\\) に依存します。</p> <p></p> <p> 図 14-15 メモ化探索の再帰木 </p>","path":["第 14 章 動的計画法","14.3 動的プログラミング問題解決アプローチ"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-3","level":3,"title":"3. 方法3:動的プログラミング","text":"<p>動的プログラミング解法を反復的に実装します。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dp(grid: list[list[int]]) -> int:\n \"\"\"最小パス和:動的プログラミング\"\"\"\n n, m = len(grid), len(grid[0])\n # dp テーブルを初期化\n dp = [[0] * m for _ in range(n)]\n dp[0][0] = grid[0][0]\n # 状態遷移:最初の行\n for j in range(1, m):\n dp[0][j] = dp[0][j - 1] + grid[0][j]\n # 状態遷移:最初の列\n for i in range(1, n):\n dp[i][0] = dp[i - 1][0] + grid[i][0]\n # 状態遷移:残りの行と列\n for i in range(1, n):\n for j in range(1, m):\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n return dp[n - 1][m - 1]\n</code></pre> min_path_sum.cpp<pre><code>/* 最小パス和:動的プログラミング */\nint minPathSumDP(vector<vector<int>> &grid) {\n int n = grid.size(), m = grid[0].size();\n // DPテーブルを初期化\n vector<vector<int>> dp(n, vector<int>(m));\n dp[0][0] = grid[0][0];\n // 状態遷移:最初の行\n for (int j = 1; j < m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 状態遷移:最初の列\n for (int i = 1; i < n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 状態遷移:残りの行と列\n for (int i = 1; i < n; i++) {\n for (int j = 1; j < m; j++) {\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n return dp[n - 1][m - 1];\n}\n</code></pre> min_path_sum.java<pre><code>/* 最小パス和:動的プログラミング */\nint minPathSumDP(int[][] grid) {\n int n = grid.length, m = grid[0].length;\n // DPテーブルを初期化\n int[][] dp = new int[n][m];\n dp[0][0] = grid[0][0];\n // 状態遷移:最初の行\n for (int j = 1; j < m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 状態遷移:最初の列\n for (int i = 1; i < n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 状態遷移:残りの行と列\n for (int i = 1; i < n; i++) {\n for (int j = 1; j < m; j++) {\n dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n return dp[n - 1][m - 1];\n}\n</code></pre> min_path_sum.cs<pre><code>[class]{min_path_sum}-[func]{MinPathSumDP}\n</code></pre> min_path_sum.go<pre><code>[class]{}-[func]{minPathSumDP}\n</code></pre> min_path_sum.swift<pre><code>[class]{}-[func]{minPathSumDP}\n</code></pre> min_path_sum.js<pre><code>[class]{}-[func]{minPathSumDP}\n</code></pre> min_path_sum.ts<pre><code>[class]{}-[func]{minPathSumDP}\n</code></pre> min_path_sum.dart<pre><code>[class]{}-[func]{minPathSumDP}\n</code></pre> min_path_sum.rs<pre><code>[class]{}-[func]{min_path_sum_dp}\n</code></pre> min_path_sum.c<pre><code>[class]{}-[func]{minPathSumDP}\n</code></pre> min_path_sum.kt<pre><code>[class]{}-[func]{minPathSumDP}\n</code></pre> min_path_sum.rb<pre><code>[class]{}-[func]{min_path_sum_dp}\n</code></pre> <p>下の図は最小経路和の状態遷移プロセスを示し、グリッド全体を走査するため、時間計算量は \\(O(nm)\\) です。</p> <p>配列 <code>dp</code> のサイズは \\(n \\times m\\) であるため、空間計算量は \\(O(nm)\\) です。</p> <1><2><3><4><5><6><7><8><9><10><11><12> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 14-16 最小経路和の動的プログラミングプロセス </p>","path":["第 14 章 動的計画法","14.3 動的プログラミング問題解決アプローチ"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4","level":3,"title":"4. 空間最適化","text":"<p>各セルは左と上のセルのみに関連するため、単一行配列を使用して \\(dp\\) テーブルを実装できます。</p> <p>配列 <code>dp</code> は1行の状態のみを表現できるため、最初の列の状態を事前に初期化できず、各行を走査するときに更新することに注意してください:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n \"\"\"最小パス和:空間最適化動的プログラミング\"\"\"\n n, m = len(grid), len(grid[0])\n # dp テーブルを初期化\n dp = [0] * m\n # 状態遷移:最初の行\n dp[0] = grid[0][0]\n for j in range(1, m):\n dp[j] = dp[j - 1] + grid[0][j]\n # 状態遷移:残りの行\n for i in range(1, n):\n # 状態遷移:最初の列\n dp[0] = dp[0] + grid[i][0]\n # 状態遷移:残りの列\n for j in range(1, m):\n dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n return dp[m - 1]\n</code></pre> min_path_sum.cpp<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.java<pre><code>/* 最小パス和:空間最適化動的プログラミング */\nint minPathSumDPComp(int[][] grid) {\n int n = grid.length, m = grid[0].length;\n // DPテーブルを初期化\n int[] dp = new int[m];\n // 状態遷移:最初の行\n dp[0] = grid[0][0];\n for (int j = 1; j < m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 状態遷移:残りの行\n for (int i = 1; i < n; i++) {\n // 状態遷移:最初の列\n dp[0] = dp[0] + grid[i][0];\n // 状態遷移:残りの列\n for (int j = 1; j < m; j++) {\n dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n return dp[m - 1];\n}\n</code></pre> min_path_sum.cs<pre><code>[class]{min_path_sum}-[func]{MinPathSumDPComp}\n</code></pre> min_path_sum.go<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.swift<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.js<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.ts<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.dart<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.rs<pre><code>[class]{}-[func]{min_path_sum_dp_comp}\n</code></pre> min_path_sum.c<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.kt<pre><code>[class]{}-[func]{minPathSumDPComp}\n</code></pre> min_path_sum.rb<pre><code>[class]{}-[func]{min_path_sum_dp_comp}\n</code></pre>","path":["第 14 章 動的計画法","14.3 動的プログラミング問題解決アプローチ"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6 編集距離問題","text":"<p>編集距離は、レーベンシュタイン距離とも呼ばれ、一つの文字列を別の文字列に変換するために必要な最小修正回数を指し、情報検索や自然言語処理で2つのシーケンス間の類似度を測定するためによく使用されます。</p> <p>Question</p> <p>2つの文字列 \\(s\\) と \\(t\\) が与えられたとき、\\(s\\) を \\(t\\) に変換するために必要な最小編集回数を返してください。</p> <p>文字列に対して3種類の編集を実行できます:文字の挿入、文字の削除、または文字を他の任意の文字に置換。</p> <p>下の図に示すように、<code>kitten</code> を <code>sitting</code> に変換するには3回の編集が必要で、2回の置換と1回の挿入を含みます。<code>hello</code> を <code>algo</code> に変換するには3ステップが必要で、2回の置換と1回の削除を含みます。</p> <p></p> <p> 図 14-27 編集距離の例データ </p> <p>編集距離問題は決定木モデルで自然に説明できます。文字列は木のノードに対応し、1ラウンドの決定(編集操作)は木のエッジに対応します。</p> <p>下の図に示すように、操作に制限がない場合、各ノードは多くのエッジを導出でき、それぞれが1つの操作に対応するため、<code>hello</code> を <code>algo</code> に変換する可能な経路は多数あります。</p> <p>決定木の観点から、この問題の目標は、ノード <code>hello</code> とノード <code>algo</code> の間の最短経路を見つけることです。</p> <p></p> <p> 図 14-28 決定木モデルに基づいて表現された編集距離問題 </p>","path":["第 14 章 動的計画法","14.6 編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1","level":3,"title":"1. 動的プログラミングアプローチ","text":"<p>ステップ1:各ラウンドの決定を考え、状態を定義し、それにより \\(dp\\) テーブルを得る</p> <p>各ラウンドの決定は、文字列 \\(s\\) に対して1つの編集操作を実行することを含みます。</p> <p>編集プロセス中に問題のサイズを段階的に縮小することを目指し、これにより部分問題を構築できます。文字列 \\(s\\) と \\(t\\) の長さをそれぞれ \\(n\\) と \\(m\\) とします。まず、両方の文字列の末尾文字 \\(s[n-1]\\) と \\(t[m-1]\\) を考慮します。</p> <ul> <li>\\(s[n-1]\\) と \\(t[m-1]\\) が同じ場合、それらをスキップして直接 \\(s[n-2]\\) と \\(t[m-2]\\) を考慮できます。</li> <li>\\(s[n-1]\\) と \\(t[m-1]\\) が異なる場合、\\(s\\) に対して1つの編集(挿入、削除、置換)を実行して、2つの文字列の末尾文字を一致させ、それらをスキップしてより小規模な問題を考慮できるようにする必要があります。</li> </ul> <p>したがって、文字列 \\(s\\) での各ラウンドの決定(編集操作)は、\\(s\\) と \\(t\\) でマッチされる残りの文字を変更します。したがって、状態は \\(s\\) と \\(t\\) で現在考慮されている \\(i\\) 番目と \\(j\\) 番目の文字であり、\\([i, j]\\) と表記されます。</p> <p>状態 \\([i, j]\\) は部分問題に対応します:\\(s\\) の最初の \\(i\\) 文字を \\(t\\) の最初の \\(j\\) 文字に変更するために必要な最小編集回数。</p> <p>これから、サイズ \\((i+1) \\times (j+1)\\) の二次元 \\(dp\\) テーブルを得ます。</p> <p>ステップ2:最適部分構造を特定し、状態遷移方程式を導出する</p> <p>部分問題 \\(dp[i, j]\\) を考慮すると、これに対応する2つの文字列の末尾文字は \\(s[i-1]\\) と \\(t[j-1]\\) であり、下の図に示すように3つのシナリオに分けることができます。</p> <ol> <li>\\(s[i-1]\\) の後に \\(t[j-1]\\) を追加すると、残りの部分問題は \\(dp[i, j-1]\\) です。</li> <li>\\(s[i-1]\\) を削除すると、残りの部分問題は \\(dp[i-1, j]\\) です。</li> <li>\\(s[i-1]\\) を \\(t[j-1]\\) に置換すると、残りの部分問題は \\(dp[i-1, j-1]\\) です。</li> </ol> <p></p> <p> 図 14-29 編集距離の状態遷移 </p> <p>上記の分析に基づいて、最適部分構造を決定できます:\\(dp[i, j]\\) の最小編集回数は、\\(dp[i, j-1]\\)、\\(dp[i-1, j]\\)、\\(dp[i-1, j-1]\\) の中の最小値に編集ステップ \\(1\\) を加えたものです。対応する状態遷移方程式は:</p> \\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\] <p>注意してください。\\(s[i-1]\\) と \\(t[j-1]\\) が同じ場合、現在の文字に対して編集は必要ありません。この場合、状態遷移方程式は:</p> \\[ dp[i, j] = dp[i-1, j-1] \\] <p>ステップ3:境界条件と状態遷移の順序を決定する</p> <p>両方の文字列が空の場合、編集回数は \\(0\\) です。つまり、\\(dp[0, 0] = 0\\) です。\\(s\\) が空で \\(t\\) が空でない場合、最小編集回数は \\(t\\) の長さに等しく、つまり最初の行 \\(dp[0, j] = j\\) です。\\(s\\) が空でなく \\(t\\) が空の場合、最小編集回数は \\(s\\) の長さに等しく、つまり最初の列 \\(dp[i, 0] = i\\) です。</p> <p>状態遷移方程式を観察すると、\\(dp[i, j]\\) の解決は左、上、左上の解に依存するため、二重ループを使用して正しい順序で \\(dp\\) テーブル全体を走査できます。</p>","path":["第 14 章 動的計画法","14.6 編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2","level":3,"title":"2. コード実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py<pre><code>def edit_distance_dp(s: str, t: str) -> int:\n \"\"\"編集距離:動的プログラミング\"\"\"\n n, m = len(s), len(t)\n dp = [[0] * (m + 1) for _ in range(n + 1)]\n # 状態遷移:最初の行と最初の列\n for i in range(1, n + 1):\n dp[i][0] = i\n for j in range(1, m + 1):\n dp[0][j] = j\n # 状態遷移:残りの行と列\n for i in range(1, n + 1):\n for j in range(1, m + 1):\n if s[i - 1] == t[j - 1]:\n # 2 つの文字が等しい場合、これら 2 つの文字をスキップ\n dp[i][j] = dp[i - 1][j - 1]\n else:\n # 最小編集数 = 3 つの操作(挿入、削除、置換)からの最小編集数 + 1\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n return dp[n][m]\n</code></pre> edit_distance.cpp<pre><code>/* 編集距離:動的プログラミング */\nint editDistanceDP(string s, string t) {\n int n = s.length(), m = t.length();\n vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n // 状態遷移:最初の行と最初の列\n for (int i = 1; i <= n; i++) {\n dp[i][0] = i;\n }\n for (int j = 1; j <= m; j++) {\n dp[0][j] = j;\n }\n // 状態遷移:残りの行と列\n for (int i = 1; i <= n; i++) {\n for (int j = 1; j <= m; j++) {\n if (s[i - 1] == t[j - 1]) {\n // 2つの文字が等しい場合、これら2つの文字をスキップ\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 最小編集数 = 3つの操作(挿入、削除、置換)からの最小編集数 + 1\n dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n }\n }\n }\n return dp[n][m];\n}\n</code></pre> edit_distance.java<pre><code>/* 編集距離:動的プログラミング */\nint editDistanceDP(String s, String t) {\n int n = s.length(), m = t.length();\n int[][] dp = new int[n + 1][m + 1];\n // 状態遷移:最初の行と最初の列\n for (int i = 1; i <= n; i++) {\n dp[i][0] = i;\n }\n for (int j = 1; j <= m; j++) {\n dp[0][j] = j;\n }\n // 状態遷移:残りの行と列\n for (int i = 1; i <= n; i++) {\n for (int j = 1; j <= m; j++) {\n if (s.charAt(i - 1) == t.charAt(j - 1)) {\n // 2つの文字が等しい場合、これら2つの文字をスキップ\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 最小編集数 = 3つの操作(挿入、削除、置換)からの最小編集数 + 1\n dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n }\n }\n }\n return dp[n][m];\n}\n</code></pre> edit_distance.cs<pre><code>[class]{edit_distance}-[func]{EditDistanceDP}\n</code></pre> edit_distance.go<pre><code>[class]{}-[func]{editDistanceDP}\n</code></pre> edit_distance.swift<pre><code>[class]{}-[func]{editDistanceDP}\n</code></pre> edit_distance.js<pre><code>[class]{}-[func]{editDistanceDP}\n</code></pre> edit_distance.ts<pre><code>[class]{}-[func]{editDistanceDP}\n</code></pre> edit_distance.dart<pre><code>[class]{}-[func]{editDistanceDP}\n</code></pre> edit_distance.rs<pre><code>[class]{}-[func]{edit_distance_dp}\n</code></pre> edit_distance.c<pre><code>[class]{}-[func]{editDistanceDP}\n</code></pre> edit_distance.kt<pre><code>[class]{}-[func]{editDistanceDP}\n</code></pre> edit_distance.rb<pre><code>[class]{}-[func]{edit_distance_dp}\n</code></pre> <p>下の図に示すように、編集距離問題の状態遷移プロセスはナップサック問題と非常に似ており、二次元グリッドを埋めることと見なすことができます。</p> <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 14-30 編集距離の動的プログラミングプロセス </p>","path":["第 14 章 動的計画法","14.6 編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3","level":3,"title":"3. 空間最適化","text":"<p>\\(dp[i, j]\\) は上の \\(dp[i-1, j]\\)、左の \\(dp[i, j-1]\\)、左上の \\(dp[i-1, j-1]\\) の解から導出され、直接走査では左上の解 \\(dp[i-1, j-1]\\) が失われ、逆走査では事前に \\(dp[i, j-1]\\) を構築できないため、どちらの走査順序も実行可能ではありません。</p> <p>この理由で、変数 <code>leftup</code> を使用して左上の \\(dp[i-1, j-1]\\) からの解を一時的に保存し、左と上の解のみを考慮すればよくなります。この状況は無制限ナップサック問題と似ており、直接走査が可能です。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py<pre><code>def edit_distance_dp_comp(s: str, t: str) -> int:\n \"\"\"編集距離:空間最適化動的プログラミング\"\"\"\n n, m = len(s), len(t)\n dp = [0] * (m + 1)\n # 状態遷移:最初の行\n for j in range(1, m + 1):\n dp[j] = j\n # 状態遷移:残りの行\n for i in range(1, n + 1):\n # 状態遷移:最初の列\n leftup = dp[0] # dp[i-1, j-1] を一時的に保存\n dp[0] += 1\n # 状態遷移:残りの列\n for j in range(1, m + 1):\n temp = dp[j]\n if s[i - 1] == t[j - 1]:\n # 2 つの文字が等しい場合、これら 2 つの文字をスキップ\n dp[j] = leftup\n else:\n # 最小編集数 = 3 つの操作(挿入、削除、置換)からの最小編集数 + 1\n dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n leftup = temp # 次の dp[i-1, j-1] のために更新\n return dp[m]\n</code></pre> edit_distance.cpp<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.java<pre><code>/* 編集距離:空間最適化動的プログラミング */\nint editDistanceDPComp(String s, String t) {\n int n = s.length(), m = t.length();\n int[] dp = new int[m + 1];\n // 状態遷移:最初の行\n for (int j = 1; j <= m; j++) {\n dp[j] = j;\n }\n // 状態遷移:残りの行\n for (int i = 1; i <= n; i++) {\n // 状態遷移:最初の列\n int leftup = dp[0]; // dp[i-1, j-1] を一時的に格納\n dp[0] = i;\n // 状態遷移:残りの列\n for (int j = 1; j <= m; j++) {\n int temp = dp[j];\n if (s.charAt(i - 1) == t.charAt(j - 1)) {\n // 2つの文字が等しい場合、これら2つの文字をスキップ\n dp[j] = leftup;\n } else {\n // 最小編集数 = 3つの操作(挿入、削除、置換)からの最小編集数 + 1\n dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n }\n leftup = temp; // 次のラウンドの dp[i-1, j-1] のために更新\n }\n }\n return dp[m];\n}\n</code></pre> edit_distance.cs<pre><code>[class]{edit_distance}-[func]{EditDistanceDPComp}\n</code></pre> edit_distance.go<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.swift<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.js<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.ts<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.dart<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.rs<pre><code>[class]{}-[func]{edit_distance_dp_comp}\n</code></pre> edit_distance.c<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.kt<pre><code>[class]{}-[func]{editDistanceDPComp}\n</code></pre> edit_distance.rb<pre><code>[class]{}-[func]{edit_distance_dp_comp}\n</code></pre>","path":["第 14 章 動的計画法","14.6 編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1 動的プログラミングの紹介","text":"<p>動的プログラミングは重要なアルゴリズムパラダイムであり、問題を一連の小さな部分問題に分解し、これらの部分問題の解を保存することで冗長な計算を避け、時間効率を大幅に向上させます。</p> <p>このセクションでは、古典的な問題から始めて、まず力任せの探索法による解法を提示し、重複する部分問題を特定してから、より効率的な動的プログラミング解法を段階的に導出します。</p> <p>階段登り</p> <p>\\(n\\) 段の階段があり、一度に \\(1\\) 段または \\(2\\) 段上ることができます。頂上に到達する方法は何通りありますか?</p> <p>下の図に示すように、\\(3\\) 段の階段の頂上に到達する方法は \\(3\\) 通りあります。</p> <p></p> <p> 図 14-1 3段目に到達する方法の数 </p> <p>この問題は**バックトラッキングを用いてすべての可能性を網羅**することで方法の数を計算することを目的としています。具体的には、階段登りの問題を多段階選択プロセスとして考えます:地面から始めて、毎回 \\(1\\) 段または \\(2\\) 段上るかを選択し、階段の頂上に到達したら方法の数をカウントし、頂上を超えた場合はプルーニング(枝刈り)を行います。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py<pre><code>def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n \"\"\"バックトラッキング\"\"\"\n # n 段目に登ったとき、解の数に 1 を加える\n if state == n:\n res[0] += 1\n # すべての選択肢を走査\n for choice in choices:\n # 枝刈り:n 段を超えて登ることを許可しない\n if state + choice > n:\n continue\n # 試行:選択を行い、状態を更新\n backtrack(choices, state + choice, n, res)\n # 撤回\n\ndef climbing_stairs_backtrack(n: int) -> int:\n \"\"\"階段登り:バックトラッキング\"\"\"\n choices = [1, 2] # 1 段または 2 段登ることを選択可能\n state = 0 # 0 段目から登り始める\n res = [0] # res[0] を使用して解の数を記録\n backtrack(choices, state, n, res)\n return res[0]\n</code></pre> climbing_stairs_backtrack.cpp<pre><code>/* バックトラッキング */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n // n段目に到達したとき、解の数に1を加える\n if (state == n)\n res[0]++;\n // すべての選択肢を走査\n for (auto &choice : choices) {\n // 剪定:n段を超えて登ることを許可しない\n if (state + choice > n)\n continue;\n // 試行:選択を行い、状態を更新\n backtrack(choices, state + choice, n, res);\n // 撤回\n }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n vector<int> choices = {1, 2}; // 1段または2段登ることを選択可能\n int state = 0; // 0段目から登り始める\n vector<int> res = {0}; // res[0] を使用して解の数を記録\n backtrack(choices, state, n, res);\n return res[0];\n}\n</code></pre> climbing_stairs_backtrack.java<pre><code>/* バックトラッキング */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n // n段目に到達したとき、解の数に1を加える\n if (state == n)\n res.set(0, res.get(0) + 1);\n // すべての選択肢を走査\n for (Integer choice : choices) {\n // 剪定:n段を超えて登ることを許可しない\n if (state + choice > n)\n continue;\n // 試行:選択を行い、状態を更新\n backtrack(choices, state + choice, n, res);\n // 撤回\n }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n List<Integer> choices = Arrays.asList(1, 2); // 1段または2段登ることを選択可能\n int state = 0; // 0段目から登り始める\n List<Integer> res = new ArrayList<>();\n res.add(0); // res[0] を使用して解の数を記録\n backtrack(choices, state, n, res);\n return res.get(0);\n}\n</code></pre> climbing_stairs_backtrack.cs<pre><code>[class]{climbing_stairs_backtrack}-[func]{Backtrack}\n\n[class]{climbing_stairs_backtrack}-[func]{ClimbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.go<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.swift<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.js<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.ts<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.dart<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.rs<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbing_stairs_backtrack}\n</code></pre> climbing_stairs_backtrack.c<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.kt<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbingStairsBacktrack}\n</code></pre> climbing_stairs_backtrack.rb<pre><code>[class]{}-[func]{backtrack}\n\n[class]{}-[func]{climbing_stairs_backtrack}\n</code></pre>","path":["第 14 章 動的計画法","14.1 動的プログラミングの紹介"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-1","level":2,"title":"14.1.1 方法1:力任せ探索","text":"<p>バックトラッキングアルゴリズムは問題を明示的に部分問題に分解しません。代わりに、問題を一連の決定ステップとして扱い、試行と枝刈りを通じてすべての可能性を探索します。</p> <p>この問題を分解アプローチを使って分析できます。\\(dp[i]\\) を \\(i\\) 段目に到達する方法の数とします。この場合、\\(dp[i]\\) が元の問題であり、その部分問題は次のようになります:</p> \\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\] <p>各移動は \\(1\\) 段または \\(2\\) 段しか進めないため、\\(i\\) 段目に立っているとき、前のステップは \\(i-1\\) 段目または \\(i-2\\) 段目のいずれかにいたはずです。つまり、\\(i\\) 段目には \\(i-1\\) 段目または \\(i-2\\) 段目からしか到達できません。</p> <p>これにより重要な結論が得られます:\\(i-1\\) 段目に到達する方法の数に \\(i-2\\) 段目に到達する方法の数を加えたものが、\\(i\\) 段目に到達する方法の数に等しい。式は以下の通りです:</p> \\[ dp[i] = dp[i-1] + dp[i-2] \\] <p>これは、階段登り問題において部分問題間に再帰関係があることを意味し、元の問題の解は部分問題の解から構築できます。下の図はこの再帰関係を示しています。</p> <p></p> <p> 図 14-2 解の数の再帰関係 </p> <p>再帰式に従って力任せ探索解法を得ることができます。\\(dp[n]\\) から始めて、**より大きな問題を再帰的に2つの小さな部分問題の和に分解**し、解が既知の最小の部分問題 \\(dp[1]\\) と \\(dp[2]\\) に到達するまで続けます。\\(dp[1] = 1\\) と \\(dp[2] = 2\\) で、それぞれ1段目と2段目に登る方法が \\(1\\) 通りと \\(2\\) 通りあることを表します。</p> <p>以下のコードを観察すると、標準的なバックトラッキングコードと同様に深さ優先探索に属しますが、より簡潔です:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py<pre><code>def dfs(i: int) -> int:\n \"\"\"探索\"\"\"\n # 既知の dp[1] と dp[2] は、それらを返す\n if i == 1 or i == 2:\n return i\n # dp[i] = dp[i-1] + dp[i-2]\n count = dfs(i - 1) + dfs(i - 2)\n return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n \"\"\"階段登り:探索\"\"\"\n return dfs(n)\n</code></pre> climbing_stairs_dfs.cpp<pre><code>/* 探索 */\nint dfs(int i) {\n // 既知の dp[1] と dp[2] を返す\n if (i == 1 || i == 2)\n return i;\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.java<pre><code>/* 探索 */\nint dfs(int i) {\n // 既知の dp[1] と dp[2] を返す\n if (i == 1 || i == 2)\n return i;\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.cs<pre><code>[class]{climbing_stairs_dfs}-[func]{DFS}\n\n[class]{climbing_stairs_dfs}-[func]{ClimbingStairsDFS}\n</code></pre> climbing_stairs_dfs.go<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFS}\n</code></pre> climbing_stairs_dfs.swift<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFS}\n</code></pre> climbing_stairs_dfs.js<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFS}\n</code></pre> climbing_stairs_dfs.ts<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFS}\n</code></pre> climbing_stairs_dfs.dart<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFS}\n</code></pre> climbing_stairs_dfs.rs<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbing_stairs_dfs}\n</code></pre> climbing_stairs_dfs.c<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFS}\n</code></pre> climbing_stairs_dfs.kt<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFS}\n</code></pre> climbing_stairs_dfs.rb<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbing_stairs_dfs}\n</code></pre> <p>下の図は力任せ探索によって形成される再帰木を示しています。問題 \\(dp[n]\\) について、その再帰木の深さは \\(n\\) で、時間計算量は \\(O(2^n)\\) です。この指数的増加により、\\(n\\) が大きいとプログラムの実行がはるかに遅くなり、長い待機時間が生じます。</p> <p></p> <p> 図 14-3 階段登りの再帰木 </p> <p>上の図を観察すると、**指数時間計算量は「重複する部分問題」によって引き起こされる**ことがわかります。例えば、\\(dp[9]\\) は \\(dp[8]\\) と \\(dp[7]\\) に分解され、\\(dp[8]\\) はさらに \\(dp[7]\\) と \\(dp[6]\\) に分解され、両方とも部分問題 \\(dp[7]\\) を含んでいます。</p> <p>このように、部分問題にはさらに小さな重複する部分問題が含まれ、これは無限に続きます。計算リソースの大部分がこれらの重複する部分問題に浪費されています。</p>","path":["第 14 章 動的計画法","14.1 動的プログラミングの紹介"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-2","level":2,"title":"14.1.2 方法2:メモ化探索","text":"<p>アルゴリズムの効率を向上させるため、**すべての重複する部分問題を一度だけ計算したい**と考えます。この目的のため、各部分問題の解を記録する配列 <code>mem</code> を宣言し、探索プロセス中に重複する部分問題を枝刈りします。</p> <ol> <li>\\(dp[i]\\) が初めて計算されるとき、後で使用するために <code>mem[i]</code> に記録します。</li> <li>\\(dp[i]\\) を再度計算する必要があるとき、<code>mem[i]</code> から直接結果を取得でき、その部分問題の冗長な計算を避けられます。</li> </ol> <p>コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py<pre><code>def dfs(i: int, mem: list[int]) -> int:\n \"\"\"記憶化探索\"\"\"\n # 既知の dp[1] と dp[2] は、それらを返す\n if i == 1 or i == 2:\n return i\n # dp[i] の記録がある場合、それを返す\n if mem[i] != -1:\n return mem[i]\n # dp[i] = dp[i-1] + dp[i-2]\n count = dfs(i - 1, mem) + dfs(i - 2, mem)\n # dp[i] を記録\n mem[i] = count\n return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n \"\"\"階段登り:記憶化探索\"\"\"\n # mem[i] は i 段目に登る解の総数を記録、-1 は記録なしを意味する\n mem = [-1] * (n + 1)\n return dfs(n, mem)\n</code></pre> climbing_stairs_dfs_mem.cpp<pre><code>/* メモ化探索 */\nint dfs(int i, vector<int> &mem) {\n // 既知の dp[1] と dp[2] を返す\n if (i == 1 || i == 2)\n return i;\n // dp[i] の記録がある場合、それを返す\n if (mem[i] != -1)\n return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i] を記録\n mem[i] = count;\n return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n // mem[i] は i 段目に登る総解数を記録、-1 は記録なしを意味する\n vector<int> mem(n + 1, -1);\n return dfs(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.java<pre><code>/* メモ化探索 */\nint dfs(int i, int[] mem) {\n // 既知の dp[1] と dp[2] を返す\n if (i == 1 || i == 2)\n return i;\n // dp[i] の記録がある場合、それを返す\n if (mem[i] != -1)\n return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i] を記録\n mem[i] = count;\n return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n // mem[i] は i 段目に登る総解数を記録、-1 は記録なしを意味する\n int[] mem = new int[n + 1];\n Arrays.fill(mem, -1);\n return dfs(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.cs<pre><code>[class]{climbing_stairs_dfs_mem}-[func]{DFS}\n\n[class]{climbing_stairs_dfs_mem}-[func]{ClimbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.go<pre><code>[class]{}-[func]{dfsMem}\n\n[class]{}-[func]{climbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.swift<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.js<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.ts<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.dart<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.rs<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbing_stairs_dfs_mem}\n</code></pre> climbing_stairs_dfs_mem.c<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.kt<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbingStairsDFSMem}\n</code></pre> climbing_stairs_dfs_mem.rb<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{climbing_stairs_dfs_mem}\n</code></pre> <p>下の図を観察すると、**メモ化後、すべての重複する部分問題は一度だけ計算される必要があり、時間計算量を \\(O(n)\\) に最適化**します。これは大幅な改善です。</p> <p></p> <p> 図 14-4 メモ化探索による再帰木 </p>","path":["第 14 章 動的計画法","14.1 動的プログラミングの紹介"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-3","level":2,"title":"14.1.3 方法3:動的プログラミング","text":"<p>**メモ化探索は「トップダウン」方式**です:元の問題(根ノード)から始めて、より大きな部分問題をより小さなものに再帰的に分解し、最小の既知の部分問題(葉ノード)の解に到達するまで続けます。その後、バックトラッキングにより部分問題の解を収集し、元の問題の解を構築します。</p> <p>一方、**動的プログラミングは「ボトムアップ」方式**です:最小の部分問題の解から始めて、元の問題が解決されるまで、より大きな部分問題の解を反復的に構築します。</p> <p>動的プログラミングはバックトラッキングを必要としないため、ループを使った反復のみが必要で、再帰は不要です。以下のコードでは、配列 <code>dp</code> を初期化して部分問題の解を保存し、メモ化探索の配列 <code>mem</code> と同じ記録機能を果たします:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py<pre><code>def climbing_stairs_dp(n: int) -> int:\n \"\"\"階段登り:動的プログラミング\"\"\"\n if n == 1 or n == 2:\n return n\n # dp テーブルを初期化、部分問題の解を格納するため使用\n dp = [0] * (n + 1)\n # 初期状態:最小の部分問題の解を事前設定\n dp[1], dp[2] = 1, 2\n # 状態遷移:小さい部分問題から大きい部分問題を段階的に解く\n for i in range(3, n + 1):\n dp[i] = dp[i - 1] + dp[i - 2]\n return dp[n]\n</code></pre> climbing_stairs_dp.cpp<pre><code>/* 階段登り:動的プログラミング */\nint climbingStairsDP(int n) {\n if (n == 1 || n == 2)\n return n;\n // DPテーブルを初期化し、部分問題の解を格納するために使用\n vector<int> dp(n + 1);\n // 初期状態:最小の部分問題の解を事前設定\n dp[1] = 1;\n dp[2] = 2;\n // 状態遷移:小さな問題から大きな部分問題を段階的に解く\n for (int i = 3; i <= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.java<pre><code>/* 階段登り:動的プログラミング */\nint climbingStairsDP(int n) {\n if (n == 1 || n == 2)\n return n;\n // DPテーブルを初期化し、部分問題の解を格納するために使用\n int[] dp = new int[n + 1];\n // 初期状態:最小の部分問題の解を事前設定\n dp[1] = 1;\n dp[2] = 2;\n // 状態遷移:小さな問題から大きな部分問題を段階的に解く\n for (int i = 3; i <= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.cs<pre><code>[class]{climbing_stairs_dp}-[func]{ClimbingStairsDP}\n</code></pre> climbing_stairs_dp.go<pre><code>[class]{}-[func]{climbingStairsDP}\n</code></pre> climbing_stairs_dp.swift<pre><code>[class]{}-[func]{climbingStairsDP}\n</code></pre> climbing_stairs_dp.js<pre><code>[class]{}-[func]{climbingStairsDP}\n</code></pre> climbing_stairs_dp.ts<pre><code>[class]{}-[func]{climbingStairsDP}\n</code></pre> climbing_stairs_dp.dart<pre><code>[class]{}-[func]{climbingStairsDP}\n</code></pre> climbing_stairs_dp.rs<pre><code>[class]{}-[func]{climbing_stairs_dp}\n</code></pre> climbing_stairs_dp.c<pre><code>[class]{}-[func]{climbingStairsDP}\n</code></pre> climbing_stairs_dp.kt<pre><code>[class]{}-[func]{climbingStairsDP}\n</code></pre> climbing_stairs_dp.rb<pre><code>[class]{}-[func]{climbing_stairs_dp}\n</code></pre> <p>下の図は上記コードの実行プロセスをシミュレートしています。</p> <p></p> <p> 図 14-5 階段登りの動的プログラミングプロセス </p> <p>バックトラッキングアルゴリズムと同様に、動的プログラミングも「状態」の概念を使用して問題解決の特定の段階を表現し、各状態は部分問題とその局所最適解に対応します。例えば、階段登り問題の状態は現在のステップ番号 \\(i\\) として定義されます。</p> <p>上記の内容に基づいて、動的プログラミングでよく使用される用語をまとめることができます。</p> <ul> <li>配列 <code>dp</code> はDPテーブルと呼ばれ、\\(dp[i]\\) は状態 \\(i\\) に対応する部分問題の解を表します。</li> <li>最小の部分問題(ステップ \\(1\\) と \\(2\\))に対応する状態は初期状態と呼ばれます。</li> <li>再帰式 \\(dp[i] = dp[i-1] + dp[i-2]\\) は状態遷移方程式と呼ばれます。</li> </ul>","path":["第 14 章 動的計画法","14.1 動的プログラミングの紹介"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414","level":2,"title":"14.1.4 空間最適化","text":"<p>注意深い読者は**\\(dp[i]\\) は \\(dp[i-1]\\) と \\(dp[i-2]\\) のみに関連するため、すべての部分問題の解を保存するために配列 <code>dp</code> を使用する必要がない**ことに気づくでしょう。単に2つの変数を使って反復的に進めることができます。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py<pre><code>def climbing_stairs_dp_comp(n: int) -> int:\n \"\"\"階段登り:空間最適化動的プログラミング\"\"\"\n if n == 1 or n == 2:\n return n\n a, b = 1, 2\n for _ in range(3, n + 1):\n a, b = b, a + b\n return b\n</code></pre> climbing_stairs_dp.cpp<pre><code>/* 階段登り:空間最適化動的プログラミング */\nint climbingStairsDPComp(int n) {\n if (n == 1 || n == 2)\n return n;\n int a = 1, b = 2;\n for (int i = 3; i <= n; i++) {\n int tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.java<pre><code>/* 階段登り:空間最適化動的プログラミング */\nint climbingStairsDPComp(int n) {\n if (n == 1 || n == 2)\n return n;\n int a = 1, b = 2;\n for (int i = 3; i <= n; i++) {\n int tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.cs<pre><code>[class]{climbing_stairs_dp}-[func]{ClimbingStairsDPComp}\n</code></pre> climbing_stairs_dp.go<pre><code>[class]{}-[func]{climbingStairsDPComp}\n</code></pre> climbing_stairs_dp.swift<pre><code>[class]{}-[func]{climbingStairsDPComp}\n</code></pre> climbing_stairs_dp.js<pre><code>[class]{}-[func]{climbingStairsDPComp}\n</code></pre> climbing_stairs_dp.ts<pre><code>[class]{}-[func]{climbingStairsDPComp}\n</code></pre> climbing_stairs_dp.dart<pre><code>[class]{}-[func]{climbingStairsDPComp}\n</code></pre> climbing_stairs_dp.rs<pre><code>[class]{}-[func]{climbing_stairs_dp_comp}\n</code></pre> climbing_stairs_dp.c<pre><code>[class]{}-[func]{climbingStairsDPComp}\n</code></pre> climbing_stairs_dp.kt<pre><code>[class]{}-[func]{climbingStairsDPComp}\n</code></pre> climbing_stairs_dp.rb<pre><code>[class]{}-[func]{climbing_stairs_dp_comp}\n</code></pre> <p>上記のコードを観察すると、配列 <code>dp</code> が占有していた空間が削除されるため、空間計算量は \\(O(n)\\) から \\(O(1)\\) に削減されます。</p> <p>多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています。</p>","path":["第 14 章 動的計画法","14.1 動的プログラミングの紹介"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4 0-1ナップサック問題","text":"<p>ナップサック問題は動的プログラミングの優れた入門問題であり、動的プログラミングで最も一般的な問題タイプです。0-1ナップサック問題、無制限ナップサック問題、複数ナップサック問題など、多くの変種があります。</p> <p>このセクションでは、まず最も一般的な0-1ナップサック問題を解決します。</p> <p>Question</p> <p>\\(n\\) 個のアイテムが与えられ、\\(i\\) 番目のアイテムの重量は \\(wgt[i-1]\\) で値は \\(val[i-1]\\) です。容量が \\(cap\\) のナップサックがあります。各アイテムは1回のみ選択できます。容量制限下でナップサックに入れることができるアイテムの最大値は何ですか?</p> <p>下の図を観察すると、アイテム番号 \\(i\\) は1から数え始め、配列インデックスは0から始まるため、アイテム \\(i\\) の重量は \\(wgt[i-1]\\) に対応し、値は \\(val[i-1]\\) に対応します。</p> <p></p> <p> 図 14-17 0-1ナップサックの例データ </p> <p>0-1ナップサック問題を \\(n\\) ラウンドの決定から構成されるプロセスとして考えることができます。各アイテムについて入れない、または入れるという2つの決定があり、したがって問題は決定木モデルに適合します。</p> <p>この問題の目的は「限られた容量の下でナップサックに入れることができるアイテムの値を最大化する」ことであり、動的プログラミング問題である可能性が高いです。</p> <p>第1ステップ:各ラウンドの決定を考え、状態を定義し、それにより \\(dp\\) テーブルを得る</p> <p>各アイテムについて、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。これから状態定義を得ることができます:現在のアイテム番号 \\(i\\) とナップサック容量 \\(c\\)、\\([i, c]\\) と表記されます。</p> <p>状態 \\([i, c]\\) は部分問題に対応します:容量 \\(c\\) のナップサックでの最初の \\(i\\) 個のアイテムの最大値、\\(dp[i, c]\\) と表記されます。</p> <p>探している解は \\(dp[n, cap]\\) であるため、サイズ \\((n+1) \\times (cap+1)\\) の二次元 \\(dp\\) テーブルが必要です。</p> <p>第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する</p> <p>アイテム \\(i\\) の決定を行った後、残るのは最初の \\(i-1\\) 個のアイテムの決定の部分問題であり、これは2つのケースに分けることができます。</p> <ul> <li>アイテム \\(i\\) を入れない:ナップサック容量は変わらず、状態は \\([i-1, c]\\) に変わります。</li> <li>アイテム \\(i\\) を入れる:ナップサック容量は \\(wgt[i-1]\\) だけ減少し、値は \\(val[i-1]\\) だけ増加し、状態は \\([i-1, c-wgt[i-1]]\\) に変わります。</li> </ul> <p>上記の分析により、この問題の最適部分構造が明らかになります:最大値 \\(dp[i, c]\\) は、アイテム \\(i\\) を入れない方案とアイテム \\(i\\) を入れる方案の2つのうち、より大きな値に等しい。これから状態遷移方程式を導出できます:</p> \\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\] <p>現在のアイテムの重量 \\(wgt[i - 1]\\) が残りのナップサック容量 \\(c\\) を超える場合、唯一の選択肢はナップサックに入れないことであることに注意することが重要です。</p> <p>第3ステップ:境界条件と状態遷移の順序を決定する</p> <p>アイテムがない場合またはナップサック容量が \\(0\\) の場合、最大値は \\(0\\) です。つまり、最初の列 \\(dp[i, 0]\\) と最初の行 \\(dp[0, c]\\) はどちらも \\(0\\) に等しいです。</p> <p>現在の状態 \\([i, c]\\) は直接上の状態 \\([i-1, c]\\) と左上の状態 \\([i-1, c-wgt[i-1]]\\) から遷移するため、2層のループを通じて \\(dp\\) テーブル全体を順序通りに走査します。</p> <p>上記の分析に従って、次に力任せ探索、メモ化探索、動的プログラミングの順序で解法を実装します。</p>","path":["第 14 章 動的計画法","14.4 0-1ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-1","level":3,"title":"1. 方法1:力任せ探索","text":"<p>探索コードには以下の要素が含まれます。</p> <ul> <li>再帰パラメータ:状態 \\([i, c]\\)。</li> <li>戻り値:部分問題 \\(dp[i, c]\\) の解。</li> <li>終了条件:アイテム番号が範囲外 \\(i = 0\\) またはナップサックの残り容量が \\(0\\) のとき、再帰を終了し値 \\(0\\) を返す。</li> <li>枝刈り:現在のアイテムの重量がナップサックの残り容量を超える場合、唯一の選択肢はナップサックに入れないことです。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n \"\"\"0-1 ナップサック:ブルートフォース探索\"\"\"\n # すべてのアイテムが選択されたかナップサックに残り容量がない場合、値 0 を返す\n if i == 0 or c == 0:\n return 0\n # ナップサック容量を超える場合、ナップサックに入れないことしか選択できない\n if wgt[i - 1] > c:\n return knapsack_dfs(wgt, val, i - 1, c)\n # アイテム i を入れないのと入れるのとの最大値を計算\n no = knapsack_dfs(wgt, val, i - 1, c)\n yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n # 2 つの選択肢のうち大きい値を返す\n return max(no, yes)\n</code></pre> knapsack.cpp<pre><code>/* 0-1 ナップサック:ブルートフォース探索 */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n // すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す\n if (i == 0 || c == 0) {\n return 0;\n }\n // ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない\n if (wgt[i - 1] > c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // アイテム i を入れない場合と入れる場合の最大値を計算\n int no = knapsackDFS(wgt, val, i - 1, c);\n int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 2つの選択肢のより大きい値を返す\n return max(no, yes);\n}\n</code></pre> knapsack.java<pre><code>/* 0-1 ナップサック:ブルートフォース探索 */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n // すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す\n if (i == 0 || c == 0) {\n return 0;\n }\n // ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない\n if (wgt[i - 1] > c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // アイテム i を入れない場合と入れる場合の最大値を計算\n int no = knapsackDFS(wgt, val, i - 1, c);\n int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 2つの選択肢のより大きい値を返す\n return Math.max(no, yes);\n}\n</code></pre> knapsack.cs<pre><code>[class]{knapsack}-[func]{KnapsackDFS}\n</code></pre> knapsack.go<pre><code>[class]{}-[func]{knapsackDFS}\n</code></pre> knapsack.swift<pre><code>[class]{}-[func]{knapsackDFS}\n</code></pre> knapsack.js<pre><code>[class]{}-[func]{knapsackDFS}\n</code></pre> knapsack.ts<pre><code>[class]{}-[func]{knapsackDFS}\n</code></pre> knapsack.dart<pre><code>[class]{}-[func]{knapsackDFS}\n</code></pre> knapsack.rs<pre><code>[class]{}-[func]{knapsack_dfs}\n</code></pre> knapsack.c<pre><code>[class]{}-[func]{knapsackDFS}\n</code></pre> knapsack.kt<pre><code>[class]{}-[func]{knapsackDFS}\n</code></pre> knapsack.rb<pre><code>[class]{}-[func]{knapsack_dfs}\n</code></pre> <p>下の図に示すように、各アイテムは選択しないと選択するという2つの探索分岐を生成するため、時間計算量は \\(O(2^n)\\) です。</p> <p>再帰木を観察すると、\\(dp[1, 10]\\) などの重複する部分問題があることが容易にわかります。アイテムが多く、ナップサック容量が大きい場合、特に同じ重量のアイテムが多い場合、重複する部分問題の数は大幅に増加します。</p> <p></p> <p> 図 14-18 0-1ナップサック問題の力任せ探索再帰木 </p>","path":["第 14 章 動的計画法","14.4 0-1ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-2","level":3,"title":"2. 方法2:メモ化探索","text":"<p>重複する部分問題が一度だけ計算されることを確保するために、部分問題の解を記録するメモ化リスト <code>mem</code> を使用します。ここで <code>mem[i][c]</code> は \\(dp[i, c]\\) に対応します。</p> <p>メモ化を導入した後、**時間計算量は部分問題の数に依存**し、\\(O(n \\times cap)\\) になります。実装コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dfs_mem(\n wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n \"\"\"0-1 ナップサック:記憶化探索\"\"\"\n # すべてのアイテムが選択されたかナップサックに残り容量がない場合、値 0 を返す\n if i == 0 or c == 0:\n return 0\n # 記録がある場合、それを返す\n if mem[i][c] != -1:\n return mem[i][c]\n # ナップサック容量を超える場合、ナップサックに入れないことしか選択できない\n if wgt[i - 1] > c:\n return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n # アイテム i を入れないのと入れるのとの最大値を計算\n no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n # 2 つの選択肢のうち大きい値を記録して返す\n mem[i][c] = max(no, yes)\n return mem[i][c]\n</code></pre> knapsack.cpp<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.java<pre><code>/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n // すべてのアイテムが選択されたか、ナップサックに残り容量がない場合、値 0 を返す\n if (i == 0 || c == 0) {\n return 0;\n }\n // 記録がある場合、それを返す\n if (mem[i][c] != -1) {\n return mem[i][c];\n }\n // ナップサックの容量を超える場合、ナップサックに入れないことしか選択できない\n if (wgt[i - 1] > c) {\n return knapsackDFSMem(wgt, val, mem, i - 1, c);\n }\n // アイテム i を入れない場合と入れる場合の最大値を計算\n int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 2つの選択肢のより大きい値を記録して返す\n mem[i][c] = Math.max(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.cs<pre><code>[class]{knapsack}-[func]{KnapsackDFSMem}\n</code></pre> knapsack.go<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.swift<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.js<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.ts<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.dart<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.rs<pre><code>[class]{}-[func]{knapsack_dfs_mem}\n</code></pre> knapsack.c<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.kt<pre><code>[class]{}-[func]{knapsackDFSMem}\n</code></pre> knapsack.rb<pre><code>[class]{}-[func]{knapsack_dfs_mem}\n</code></pre> <p>下の図はメモ化探索で枝刈りされる探索分岐を示しています。</p> <p></p> <p> 図 14-19 0-1ナップサック問題のメモ化探索再帰木 </p>","path":["第 14 章 動的計画法","14.4 0-1ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-3","level":3,"title":"3. 方法3:動的プログラミング","text":"<p>動的プログラミングは本質的に状態遷移中に \\(dp\\) テーブルを埋めることを含みます。コードは下の図に示されています:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n \"\"\"0-1 ナップサック:動的プログラミング\"\"\"\n n = len(wgt)\n # dp テーブルを初期化\n dp = [[0] * (cap + 1) for _ in range(n + 1)]\n # 状態遷移\n for i in range(1, n + 1):\n for c in range(1, cap + 1):\n if wgt[i - 1] > c:\n # ナップサック容量を超える場合、アイテム i を選択しない\n dp[i][c] = dp[i - 1][c]\n else:\n # アイテム i を選択しないのと選択するのとで大きい値\n dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n return dp[n][cap]\n</code></pre> knapsack.cpp<pre><code>/* 0-1 ナップサック:動的プログラミング */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n int n = wgt.size();\n // DPテーブルを初期化\n vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int c = 1; c <= cap; c++) {\n if (wgt[i - 1] > c) {\n // ナップサックの容量を超える場合、アイテム i を選択しない\n dp[i][c] = dp[i - 1][c];\n } else {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> knapsack.java<pre><code>/* 0-1 ナップサック:動的プログラミング */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // DPテーブルを初期化\n int[][] dp = new int[n + 1][cap + 1];\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int c = 1; c <= cap; c++) {\n if (wgt[i - 1] > c) {\n // ナップサックの容量を超える場合、アイテム i を選択しない\n dp[i][c] = dp[i - 1][c];\n } else {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> knapsack.cs<pre><code>[class]{knapsack}-[func]{KnapsackDP}\n</code></pre> knapsack.go<pre><code>[class]{}-[func]{knapsackDP}\n</code></pre> knapsack.swift<pre><code>[class]{}-[func]{knapsackDP}\n</code></pre> knapsack.js<pre><code>[class]{}-[func]{knapsackDP}\n</code></pre> knapsack.ts<pre><code>[class]{}-[func]{knapsackDP}\n</code></pre> knapsack.dart<pre><code>[class]{}-[func]{knapsackDP}\n</code></pre> knapsack.rs<pre><code>[class]{}-[func]{knapsack_dp}\n</code></pre> knapsack.c<pre><code>[class]{}-[func]{knapsackDP}\n</code></pre> knapsack.kt<pre><code>[class]{}-[func]{knapsackDP}\n</code></pre> knapsack.rb<pre><code>[class]{}-[func]{knapsack_dp}\n</code></pre> <p>下の図に示すように、時間計算量と空間計算量の両方が配列 <code>dp</code> のサイズ、つまり \\(O(n \\times cap)\\) によって決定されます。</p> <1><2><3><4><5><6><7><8><9><10><11><12><13><14> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 14-20 0-1ナップサック問題の動的プログラミングプロセス </p>","path":["第 14 章 動的計画法","14.4 0-1ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4","level":3,"title":"4. 空間最適化","text":"<p>各状態は上の行の状態のみに関連するため、2つの配列を使用してローリング前進させ、空間計算量を \\(O(n^2)\\) から \\(O(n)\\) に削減できます。</p> <p>さらに考えてみると、1つの配列だけで空間最適化を達成できるでしょうか?各状態が直接上のセルまたは左上のセルから遷移することが観察できます。配列が1つしかない場合、\\(i\\) 行目の走査を開始するとき、その配列はまだ \\(i-1\\) 行目の状態を保存しています。</p> <ul> <li>通常の順序で走査する場合、\\(dp[i, j]\\) に走査したとき、左上の \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) の値がすでに上書きされている可能性があり、正しい状態遷移結果を得ることができません。</li> <li>逆順で走査する場合、上書き問題はなく、状態遷移を正しく実行できます。</li> </ul> <p>下の図は単一配列での \\(i = 1\\) 行目から \\(i = 2\\) 行目への遷移プロセスを示しています。通常順序走査と逆順走査の違いについて考えてみてください。</p> <1><2><3><4><5><6> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 14-21 0-1ナップサックの空間最適化動的プログラミングプロセス </p> <p>コード実装では、配列 <code>dp</code> の最初の次元 \\(i\\) を削除し、内側のループを逆走査に変更するだけです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n \"\"\"0-1 ナップサック:空間最適化動的プログラミング\"\"\"\n n = len(wgt)\n # dp テーブルを初期化\n dp = [0] * (cap + 1)\n # 状態遷移\n for i in range(1, n + 1):\n # 逆順で走査\n for c in range(cap, 0, -1):\n if wgt[i - 1] > c:\n # ナップサック容量を超える場合、アイテム i を選択しない\n dp[c] = dp[c]\n else:\n # アイテム i を選択しないのと選択するのとで大きい値\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n return dp[cap]\n</code></pre> knapsack.cpp<pre><code>/* 0-1 ナップサック:空間最適化動的プログラミング */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n int n = wgt.size();\n // DPテーブルを初期化\n vector<int> dp(cap + 1, 0);\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n // 逆順で走査\n for (int c = cap; c >= 1; c--) {\n if (wgt[i - 1] <= c) {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.java<pre><code>/* 0-1 ナップサック:空間最適化動的プログラミング */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // DPテーブルを初期化\n int[] dp = new int[cap + 1];\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n // 逆順で走査\n for (int c = cap; c >= 1; c--) {\n if (wgt[i - 1] <= c) {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.cs<pre><code>[class]{knapsack}-[func]{KnapsackDPComp}\n</code></pre> knapsack.go<pre><code>[class]{}-[func]{knapsackDPComp}\n</code></pre> knapsack.swift<pre><code>[class]{}-[func]{knapsackDPComp}\n</code></pre> knapsack.js<pre><code>[class]{}-[func]{knapsackDPComp}\n</code></pre> knapsack.ts<pre><code>[class]{}-[func]{knapsackDPComp}\n</code></pre> knapsack.dart<pre><code>[class]{}-[func]{knapsackDPComp}\n</code></pre> knapsack.rs<pre><code>[class]{}-[func]{knapsack_dp_comp}\n</code></pre> knapsack.c<pre><code>[class]{}-[func]{knapsackDPComp}\n</code></pre> knapsack.kt<pre><code>[class]{}-[func]{knapsackDPComp}\n</code></pre> knapsack.rb<pre><code>[class]{}-[func]{knapsack_dp_comp}\n</code></pre>","path":["第 14 章 動的計画法","14.4 0-1ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7 まとめ","text":"<ul> <li>動的プログラミングは問題を分解し、部分問題の解を保存することで冗長な計算を避け、計算効率を向上させます。</li> <li>時間を考慮しなければ、すべての動的プログラミング問題はバックトラッキング(力任せ探索)を使用して解決できますが、再帰木には多くの重複する部分問題があり、効率が非常に低くなります。記憶化リストを導入することで、計算されたすべての部分問題の解を保存し、重複する部分問題が一度だけ計算されることを保証できます。</li> <li>記憶化探索はトップダウンの再帰解法であり、動的プログラミングはボトムアップの反復アプローチに対応し、「表を埋める」ことに似ています。現在の状態は特定の局所状態のみに依存するため、dpテーブルの1次元を削除して空間計算量を削減できます。</li> <li>部分問題の分解は汎用的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングで特徴が異なります。</li> <li>動的プログラミング問題には3つの主要な特徴があります:重複する部分問題、最適部分構造、無記憶性。</li> <li>元の問題の最適解がその部分問題の最適解から構築できる場合、最適部分構造を持ちます。</li> <li>無記憶性とは、状態の将来の発展が現在の状態のみに依存し、過去に経験したすべての状態に依存しないことを意味します。多くの組み合わせ最適化問題にはこの特性がなく、動的プログラミングを使用して迅速に解決することはできません。</li> </ul> <p>ナップサック問題</p> <ul> <li>ナップサック問題は最も典型的な動的プログラミング問題の1つで、0-1ナップサック、無制限ナップサック、複数ナップサックなどの変種があります。</li> <li>0-1ナップサックの状態定義は、最初の \\(i\\) 個のアイテムを含む容量 \\(c\\) のナップサックでの最大値です。アイテムをナップサックに入れないまたは入れるという決定に基づいて、最適部分構造を特定し、状態遷移方程式を構築できます。空間最適化では、各状態が直接上と左上の状態に依存するため、左上の状態の上書きを避けるためにリストを逆順で走査する必要があります。</li> <li>無制限ナップサック問題では、各種類のアイテムを選択できる数に制限がないため、アイテムを含める状態遷移は0-1ナップサックと異なります。状態が直接上と左の状態に依存するため、空間最適化では前方走査を含める必要があります。</li> <li>コイン交換問題は無制限ナップサック問題の変種で、「最大」値を求めることから「最小」コイン数を求めることに変わり、状態遷移方程式は \\(\\max()\\) を \\(\\min()\\) に変更する必要があります。ナップサックの容量を「超えない」ことを追求することから、正確に目標金額を求めることに変わり、「目標金額を構成できない」無効解を表すために \\(amt + 1\\) を使用します。</li> <li>コイン交換問題IIは「最小コイン数」を求めることから「コインの組み合わせ数」を求めることに変わり、状態遷移方程式を \\(\\min()\\) から和算演算子に変更します。</li> </ul> <p>編集距離問題</p> <ul> <li>編集距離(レーベンシュタイン距離)は2つの文字列間の類似度を測定し、一つの文字列を別の文字列に変更するために必要な最小編集ステップ数として定義され、編集操作には追加、削除、置換が含まれます。</li> <li>編集距離問題の状態定義は、\\(s\\) の最初の \\(i\\) 文字を \\(t\\) の最初の \\(j\\) 文字に変更するために必要な最小編集ステップ数です。\\(s[i] \\ne t[j]\\) の場合、追加、削除、置換の3つの決定があり、それぞれに対応する残余部分問題があります。これから最適部分構造を特定し、状態遷移方程式を構築できます。\\(s[i] = t[j]\\) の場合、現在の文字の編集は必要ありません。</li> <li>編集距離では、状態が直接上、左、左上の状態に依存します。したがって、空間最適化後、前方走査も逆走査も正しく状態遷移を実行できません。これに対処するため、変数を使用して左上の状態を一時的に保存し、無制限ナップサック問題の状況と同等にし、空間最適化後に前方走査を可能にします。</li> </ul>","path":["第 14 章 動的計画法","14.7 まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5 無制限ナップサック問題","text":"<p>このセクションでは、まず別の一般的なナップサック問題である無制限ナップサックを解決し、次にその特殊ケースであるコイン交換問題を探索します。</p>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451","level":2,"title":"14.5.1 無制限ナップサック問題","text":"<p>Question</p> <p>\\(n\\) 個のアイテムが与えられ、\\(i\\) 番目のアイテムの重量は \\(wgt[i-1]\\) で値は \\(val[i-1]\\) です。容量が \\(cap\\) のバックパックがあります。各アイテムは複数回選択できます。容量を超えることなくバックパックに入れることができるアイテムの最大値は何ですか?以下の例を参照してください。</p> <p></p> <p> 図 14-22 無制限ナップサック問題の例データ </p>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1. 動的プログラミングアプローチ","text":"<p>無制限ナップサック問題は0-1ナップサック問題と非常に似ており、唯一の違いはアイテムを選択できる回数に制限がないことです。</p> <ul> <li>0-1ナップサック問題では、各アイテムは1つしかないため、アイテム \\(i\\) をバックパックに入れた後は、前の \\(i-1\\) 個のアイテムからのみ選択できます。</li> <li>無制限ナップサック問題では、各アイテムの数量は無制限であるため、アイテム \\(i\\) をバックパックに入れた後も、前の \\(i\\) 個のアイテムから引き続き選択できます。</li> </ul> <p>無制限ナップサック問題のルールの下で、状態 \\([i, c]\\) は2つの方法で変化できます。</p> <ul> <li>アイテム \\(i\\) を入れない:0-1ナップサック問題と同様に、\\([i-1, c]\\) に遷移します。</li> <li>アイテム \\(i\\) を入れる:0-1ナップサック問題とは異なり、\\([i, c-wgt[i-1]]\\) に遷移します。</li> </ul> <p>したがって、状態遷移方程式は次のようになります:</p> \\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2","level":3,"title":"2. コード実装","text":"<p>2つの問題のコードを比較すると、状態遷移が \\(i-1\\) から \\(i\\) に変わり、残りは完全に同一です:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py<pre><code>def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n \"\"\"完全ナップサック:動的プログラミング\"\"\"\n n = len(wgt)\n # dp テーブルを初期化\n dp = [[0] * (cap + 1) for _ in range(n + 1)]\n # 状態遷移\n for i in range(1, n + 1):\n for c in range(1, cap + 1):\n if wgt[i - 1] > c:\n # ナップサック容量を超える場合、アイテム i を選択しない\n dp[i][c] = dp[i - 1][c]\n else:\n # アイテム i を選択しないのと選択するのとで大きい値\n dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n return dp[n][cap]\n</code></pre> unbounded_knapsack.cpp<pre><code>/* 完全ナップサック:動的プログラミング */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n int n = wgt.size();\n // DPテーブルを初期化\n vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int c = 1; c <= cap; c++) {\n if (wgt[i - 1] > c) {\n // ナップサックの容量を超える場合、アイテム i を選択しない\n dp[i][c] = dp[i - 1][c];\n } else {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.java<pre><code>/* 完全ナップサック:動的プログラミング */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // DPテーブルを初期化\n int[][] dp = new int[n + 1][cap + 1];\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int c = 1; c <= cap; c++) {\n if (wgt[i - 1] > c) {\n // ナップサックの容量を超える場合、アイテム i を選択しない\n dp[i][c] = dp[i - 1][c];\n } else {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.cs<pre><code>[class]{unbounded_knapsack}-[func]{UnboundedKnapsackDP}\n</code></pre> unbounded_knapsack.go<pre><code>[class]{}-[func]{unboundedKnapsackDP}\n</code></pre> unbounded_knapsack.swift<pre><code>[class]{}-[func]{unboundedKnapsackDP}\n</code></pre> unbounded_knapsack.js<pre><code>[class]{}-[func]{unboundedKnapsackDP}\n</code></pre> unbounded_knapsack.ts<pre><code>[class]{}-[func]{unboundedKnapsackDP}\n</code></pre> unbounded_knapsack.dart<pre><code>[class]{}-[func]{unboundedKnapsackDP}\n</code></pre> unbounded_knapsack.rs<pre><code>[class]{}-[func]{unbounded_knapsack_dp}\n</code></pre> unbounded_knapsack.c<pre><code>[class]{}-[func]{unboundedKnapsackDP}\n</code></pre> unbounded_knapsack.kt<pre><code>[class]{}-[func]{unboundedKnapsackDP}\n</code></pre> unbounded_knapsack.rb<pre><code>[class]{}-[func]{unbounded_knapsack_dp}\n</code></pre>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3","level":3,"title":"3. 空間最適化","text":"<p>現在の状態は左と上の状態から来るため、空間最適化解法は \\(dp\\) テーブルの各行に対して前方走査を実行する必要があります。</p> <p>この走査順序は0-1ナップサックの場合とは逆です。違いを理解するために下の図を参照してください。</p> <1><2><3><4><5><6> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 14-23 空間最適化後の無制限ナップサック問題の動的プログラミングプロセス </p> <p>コード実装は非常に簡単で、配列 <code>dp</code> の最初の次元を削除するだけです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py<pre><code>def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n \"\"\"完全ナップサック:空間最適化動的プログラミング\"\"\"\n n = len(wgt)\n # dp テーブルを初期化\n dp = [0] * (cap + 1)\n # 状態遷移\n for i in range(1, n + 1):\n # 順序で走査\n for c in range(1, cap + 1):\n if wgt[i - 1] > c:\n # ナップサック容量を超える場合、アイテム i を選択しない\n dp[c] = dp[c]\n else:\n # アイテム i を選択しないのと選択するのとで大きい値\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n return dp[cap]\n</code></pre> unbounded_knapsack.cpp<pre><code>/* 完全ナップサック:空間最適化動的プログラミング */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n int n = wgt.size();\n // DPテーブルを初期化\n vector<int> dp(cap + 1, 0);\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int c = 1; c <= cap; c++) {\n if (wgt[i - 1] > c) {\n // ナップサックの容量を超える場合、アイテム i を選択しない\n dp[c] = dp[c];\n } else {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.java<pre><code>/* 完全ナップサック:空間最適化動的プログラミング */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // DPテーブルを初期化\n int[] dp = new int[cap + 1];\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int c = 1; c <= cap; c++) {\n if (wgt[i - 1] > c) {\n // ナップサックの容量を超える場合、アイテム i を選択しない\n dp[c] = dp[c];\n } else {\n // 選択しない場合とアイテム i を選択する場合のより大きい値\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.cs<pre><code>[class]{unbounded_knapsack}-[func]{UnboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.go<pre><code>[class]{}-[func]{unboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.swift<pre><code>[class]{}-[func]{unboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.js<pre><code>[class]{}-[func]{unboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.ts<pre><code>[class]{}-[func]{unboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.dart<pre><code>[class]{}-[func]{unboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.rs<pre><code>[class]{}-[func]{unbounded_knapsack_dp_comp}\n</code></pre> unbounded_knapsack.c<pre><code>[class]{}-[func]{unboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.kt<pre><code>[class]{}-[func]{unboundedKnapsackDPComp}\n</code></pre> unbounded_knapsack.rb<pre><code>[class]{}-[func]{unbounded_knapsack_dp_comp}\n</code></pre>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452","level":2,"title":"14.5.2 コイン交換問題","text":"<p>ナップサック問題は動的プログラミング問題の大きなクラスの代表であり、コイン交換問題など多くの変種があります。</p> <p>Question</p> <p>\\(n\\) 種類のコインが与えられ、\\(i\\) 番目の種類のコインの額面は \\(coins[i - 1]\\) で、目標金額は \\(amt\\) です。各種類のコインは複数回選択できます。目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は \\(-1\\) を返してください。以下の例を参照してください。</p> <p></p> <p> 図 14-24 コイン交換問題の例データ </p>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1. 動的プログラミングアプローチ","text":"<p>コイン交換は無制限ナップサック問題の特殊ケースと見なすことができ、以下の類似点と相違点を共有しています。</p> <ul> <li>2つの問題は互いに変換できます:「アイテム」は「コイン」に対応し、「アイテムの重量」は「コインの額面」に対応し、「バックパックの容量」は「目標金額」に対応します。</li> <li>最適化目標は逆です:無制限ナップサック問題はアイテムの値を最大化することを目的とし、コイン交換問題はコインの数を最小化することを目的とします。</li> <li>無制限ナップサック問題はバックパック容量を「超えない」解を求め、コイン交換は目標金額を「正確に」構成する解を求めます。</li> </ul> <p>第1ステップ:各ラウンドの意思決定を考え、状態を定義し、それにより \\(dp\\) テーブルを導出する</p> <p>状態 \\([i, a]\\) は部分問題に対応します:最初の \\(i\\) 種類のコインを使用して金額 \\(a\\) を構成できる最小コイン数、\\(dp[i, a]\\) と表記されます。</p> <p>二次元 \\(dp\\) テーブルのサイズは \\((n+1) \\times (amt+1)\\) です。</p> <p>第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する</p> <p>この問題は状態遷移方程式の2つの側面で無制限ナップサック問題と異なります。</p> <ul> <li>この問題は最小値を求めるため、演算子 \\(\\max()\\) を \\(\\min()\\) に変更する必要があります。</li> <li>最適化はコインの数に焦点を当てているため、コインが選択されたときに単純に \\(+1\\) を追加します。</li> </ul> \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\] <p>第3ステップ:境界条件と状態遷移順序を定義する</p> <p>目標金額が \\(0\\) の場合、それを構成するのに必要な最小コイン数は \\(0\\) であるため、最初の列のすべての \\(dp[i, 0]\\) は \\(0\\) です。</p> <p>コインがない場合、**任意の金額 >0 を構成することは不可能**であり、これは無効な解です。状態遷移方程式の \\(\\min()\\) 関数が無効な解を認識してフィルタリングできるように、\\(+\\infty\\) を使用してそれらを表現することを検討し、つまり最初の行のすべての \\(dp[0, a]\\) を \\(+\\infty\\) に設定します。</p>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_1","level":3,"title":"2. コード実装","text":"<p>ほとんどのプログラミング言語は \\(+\\infty\\) 変数を提供しておらず、整数 <code>int</code> の最大値のみを代替として使用できます。これによりオーバーフローが発生する可能性があります:状態遷移方程式の \\(+1\\) 演算がオーバーフローする可能性があります。</p> <p>この理由で、数値 \\(amt + 1\\) を使用して無効な解を表します。なぜなら、\\(amt\\) を構成するのに必要な最大コイン数は最大でも \\(amt\\) だからです。結果を返す前に、\\(dp[n, amt]\\) が \\(amt + 1\\) に等しいかどうかを確認し、そうであれば \\(-1\\) を返し、目標金額を構成できないことを示します。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py<pre><code>def coin_change_dp(coins: list[int], amt: int) -> int:\n \"\"\"硬貨交換:動的プログラミング\"\"\"\n n = len(coins)\n MAX = amt + 1\n # dp テーブルを初期化\n dp = [[0] * (amt + 1) for _ in range(n + 1)]\n # 状態遷移:最初の行と最初の列\n for a in range(1, amt + 1):\n dp[0][a] = MAX\n # 状態遷移:残りの行と列\n for i in range(1, n + 1):\n for a in range(1, amt + 1):\n if coins[i - 1] > a:\n # 目標金額を超える場合、硬貨 i を選択しない\n dp[i][a] = dp[i - 1][a]\n else:\n # 硬貨 i を選択しないのと選択するのとで小さい値\n dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n return dp[n][amt] if dp[n][amt] != MAX else -1\n</code></pre> coin_change.cpp<pre><code>/* 硬貨両替:動的プログラミング */\nint coinChangeDP(vector<int> &coins, int amt) {\n int n = coins.size();\n int MAX = amt + 1;\n // DPテーブルを初期化\n vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n // 状態遷移:最初の行と最初の列\n for (int a = 1; a <= amt; a++) {\n dp[0][a] = MAX;\n }\n // 状態遷移:残りの行と列\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[i][a] = dp[i - 1][a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合のより小さい値\n dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n</code></pre> coin_change.java<pre><code>/* 硬貨両替:動的プログラミング */\nint coinChangeDP(int[] coins, int amt) {\n int n = coins.length;\n int MAX = amt + 1;\n // DPテーブルを初期化\n int[][] dp = new int[n + 1][amt + 1];\n // 状態遷移:最初の行と最初の列\n for (int a = 1; a <= amt; a++) {\n dp[0][a] = MAX;\n }\n // 状態遷移:残りの行と列\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[i][a] = dp[i - 1][a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合のより小さい値\n dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n</code></pre> coin_change.cs<pre><code>[class]{coin_change}-[func]{CoinChangeDP}\n</code></pre> coin_change.go<pre><code>[class]{}-[func]{coinChangeDP}\n</code></pre> coin_change.swift<pre><code>[class]{}-[func]{coinChangeDP}\n</code></pre> coin_change.js<pre><code>[class]{}-[func]{coinChangeDP}\n</code></pre> coin_change.ts<pre><code>[class]{}-[func]{coinChangeDP}\n</code></pre> coin_change.dart<pre><code>[class]{}-[func]{coinChangeDP}\n</code></pre> coin_change.rs<pre><code>[class]{}-[func]{coin_change_dp}\n</code></pre> coin_change.c<pre><code>[class]{}-[func]{coinChangeDP}\n</code></pre> coin_change.kt<pre><code>[class]{}-[func]{coinChangeDP}\n</code></pre> coin_change.rb<pre><code>[class]{}-[func]{coin_change_dp}\n</code></pre> <p>下の図はコイン交換問題の動的プログラミングプロセスを示しており、無制限ナップサック問題と非常に似ています。</p> <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 14-25 コイン交換問題の動的プログラミングプロセス </p>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_1","level":3,"title":"3. 空間最適化","text":"<p>コイン交換問題の空間最適化は無制限ナップサック問題と同じ方法で処理されます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py<pre><code>def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n \"\"\"硬貨交換:空間最適化動的プログラミング\"\"\"\n n = len(coins)\n MAX = amt + 1\n # dp テーブルを初期化\n dp = [MAX] * (amt + 1)\n dp[0] = 0\n # 状態遷移\n for i in range(1, n + 1):\n # 順序で走査\n for a in range(1, amt + 1):\n if coins[i - 1] > a:\n # 目標金額を超える場合、硬貨 i を選択しない\n dp[a] = dp[a]\n else:\n # 硬貨 i を選択しないのと選択するのとで小さい値\n dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n return dp[amt] if dp[amt] != MAX else -1\n</code></pre> coin_change.cpp<pre><code>/* 硬貨両替:空間最適化動的プログラミング */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n int n = coins.size();\n int MAX = amt + 1;\n // DPテーブルを初期化\n vector<int> dp(amt + 1, MAX);\n dp[0] = 0;\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[a] = dp[a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合のより小さい値\n dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] != MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.java<pre><code>/* 硬貨両替:空間最適化動的プログラミング */\nint coinChangeDPComp(int[] coins, int amt) {\n int n = coins.length;\n int MAX = amt + 1;\n // DPテーブルを初期化\n int[] dp = new int[amt + 1];\n Arrays.fill(dp, MAX);\n dp[0] = 0;\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[a] = dp[a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合のより小さい値\n dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] != MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.cs<pre><code>[class]{coin_change}-[func]{CoinChangeDPComp}\n</code></pre> coin_change.go<pre><code>[class]{}-[func]{coinChangeDPComp}\n</code></pre> coin_change.swift<pre><code>[class]{}-[func]{coinChangeDPComp}\n</code></pre> coin_change.js<pre><code>[class]{}-[func]{coinChangeDPComp}\n</code></pre> coin_change.ts<pre><code>[class]{}-[func]{coinChangeDPComp}\n</code></pre> coin_change.dart<pre><code>[class]{}-[func]{coinChangeDPComp}\n</code></pre> coin_change.rs<pre><code>[class]{}-[func]{coin_change_dp_comp}\n</code></pre> coin_change.c<pre><code>[class]{}-[func]{coinChangeDPComp}\n</code></pre> coin_change.kt<pre><code>[class]{}-[func]{coinChangeDPComp}\n</code></pre> coin_change.rb<pre><code>[class]{}-[func]{coin_change_dp_comp}\n</code></pre>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-ii","level":2,"title":"14.5.3 コイン交換問題II","text":"<p>Question</p> <p>\\(n\\) 種類のコインが与えられ、\\(i\\) 番目の種類のコインの額面は \\(coins[i - 1]\\) で、目標金額は \\(amt\\) です。各種類のコインは複数回選択でき、目標金額を構成できるコインの組み合わせは何通りありますか。以下の例を参照してください。</p> <p></p> <p> 図 14-26 コイン交換問題IIの例データ </p>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_2","level":3,"title":"1. 動的プログラミングアプローチ","text":"<p>前の問題と比較して、この問題の目標は組み合わせの数を決定することであるため、部分問題は次のようになります:最初の \\(i\\) 種類のコインを使用して金額 \\(a\\) を構成できる組み合わせの数。\\(dp\\) テーブルはサイズ \\((n+1) \\times (amt + 1)\\) の二次元行列のまま残ります。</p> <p>現在の状態の組み合わせ数は、現在のコインを選択しない組み合わせと現在のコインを選択する組み合わせの合計です。状態遷移方程式は:</p> \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\] <p>目標金額が \\(0\\) の場合、目標金額を構成するのにコインは必要ないため、最初の列のすべての \\(dp[i, 0]\\) は \\(1\\) に初期化されるべきです。コインがない場合、任意の金額 >0 を構成することは不可能であるため、最初の行のすべての \\(dp[0, a]\\) は \\(0\\) に設定されるべきです。</p>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_2","level":3,"title":"2. コード実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py<pre><code>def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n \"\"\"硬貨交換 II:動的プログラミング\"\"\"\n n = len(coins)\n # dp テーブルを初期化\n dp = [[0] * (amt + 1) for _ in range(n + 1)]\n # 最初の列を初期化\n for i in range(n + 1):\n dp[i][0] = 1\n # 状態遷移\n for i in range(1, n + 1):\n for a in range(1, amt + 1):\n if coins[i - 1] > a:\n # 目標金額を超える場合、硬貨 i を選択しない\n dp[i][a] = dp[i - 1][a]\n else:\n # 硬貨 i を選択しないのと選択するのとの両方の選択肢の和\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n return dp[n][amt]\n</code></pre> coin_change_ii.cpp<pre><code>/* 硬貨両替 II:動的プログラミング */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n int n = coins.size();\n // DPテーブルを初期化\n vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n // 最初の列を初期化\n for (int i = 0; i <= n; i++) {\n dp[i][0] = 1;\n }\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[i][a] = dp[i - 1][a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n return dp[n][amt];\n}\n</code></pre> coin_change_ii.java<pre><code>/* 硬貨両替 II:動的プログラミング */\nint coinChangeIIDP(int[] coins, int amt) {\n int n = coins.length;\n // DPテーブルを初期化\n int[][] dp = new int[n + 1][amt + 1];\n // 最初の列を初期化\n for (int i = 0; i <= n; i++) {\n dp[i][0] = 1;\n }\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[i][a] = dp[i - 1][a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n return dp[n][amt];\n}\n</code></pre> coin_change_ii.cs<pre><code>[class]{coin_change_ii}-[func]{CoinChangeIIDP}\n</code></pre> coin_change_ii.go<pre><code>[class]{}-[func]{coinChangeIIDP}\n</code></pre> coin_change_ii.swift<pre><code>[class]{}-[func]{coinChangeIIDP}\n</code></pre> coin_change_ii.js<pre><code>[class]{}-[func]{coinChangeIIDP}\n</code></pre> coin_change_ii.ts<pre><code>[class]{}-[func]{coinChangeIIDP}\n</code></pre> coin_change_ii.dart<pre><code>[class]{}-[func]{coinChangeIIDP}\n</code></pre> coin_change_ii.rs<pre><code>[class]{}-[func]{coin_change_ii_dp}\n</code></pre> coin_change_ii.c<pre><code>[class]{}-[func]{coinChangeIIDP}\n</code></pre> coin_change_ii.kt<pre><code>[class]{}-[func]{coinChangeIIDP}\n</code></pre> coin_change_ii.rb<pre><code>[class]{}-[func]{coin_change_ii_dp}\n</code></pre>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_2","level":3,"title":"3. 空間最適化","text":"<p>空間最適化アプローチは同じで、コインの次元を削除するだけです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py<pre><code>def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n \"\"\"硬貨交換 II:空間最適化動的プログラミング\"\"\"\n n = len(coins)\n # dp テーブルを初期化\n dp = [0] * (amt + 1)\n dp[0] = 1\n # 状態遷移\n for i in range(1, n + 1):\n # 順序で走査\n for a in range(1, amt + 1):\n if coins[i - 1] > a:\n # 目標金額を超える場合、硬貨 i を選択しない\n dp[a] = dp[a]\n else:\n # 硬貨 i を選択しないのと選択するのとの両方の選択肢の和\n dp[a] = dp[a] + dp[a - coins[i - 1]]\n return dp[amt]\n</code></pre> coin_change_ii.cpp<pre><code>/* 硬貨両替 II:空間最適化動的プログラミング */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n int n = coins.size();\n // DPテーブルを初期化\n vector<int> dp(amt + 1, 0);\n dp[0] = 1;\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[a] = dp[a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.java<pre><code>/* 硬貨両替 II:空間最適化動的プログラミング */\nint coinChangeIIDPComp(int[] coins, int amt) {\n int n = coins.length;\n // DPテーブルを初期化\n int[] dp = new int[amt + 1];\n dp[0] = 1;\n // 状態遷移\n for (int i = 1; i <= n; i++) {\n for (int a = 1; a <= amt; a++) {\n if (coins[i - 1] > a) {\n // 目標金額を超える場合、硬貨 i を選択しない\n dp[a] = dp[a];\n } else {\n // 選択しない場合と硬貨 i を選択する場合の2つの選択肢の合計\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.cs<pre><code>[class]{coin_change_ii}-[func]{CoinChangeIIDPComp}\n</code></pre> coin_change_ii.go<pre><code>[class]{}-[func]{coinChangeIIDPComp}\n</code></pre> coin_change_ii.swift<pre><code>[class]{}-[func]{coinChangeIIDPComp}\n</code></pre> coin_change_ii.js<pre><code>[class]{}-[func]{coinChangeIIDPComp}\n</code></pre> coin_change_ii.ts<pre><code>[class]{}-[func]{coinChangeIIDPComp}\n</code></pre> coin_change_ii.dart<pre><code>[class]{}-[func]{coinChangeIIDPComp}\n</code></pre> coin_change_ii.rs<pre><code>[class]{}-[func]{coin_change_ii_dp_comp}\n</code></pre> coin_change_ii.c<pre><code>[class]{}-[func]{coinChangeIIDPComp}\n</code></pre> coin_change_ii.kt<pre><code>[class]{}-[func]{coinChangeIIDPComp}\n</code></pre> coin_change_ii.rb<pre><code>[class]{}-[func]{coin_change_ii_dp_comp}\n</code></pre>","path":["第 14 章 動的計画法","14.5 無制限ナップサック問題"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"第 9 章 グラフ","text":"<p>Abstract</p> <p>人生の旅路において、私たちの一人一人はノードであり、無数の見えない辺で結ばれています。</p> <p>一つ一つの出会いと別れが、この広大な人生のグラフに独特の印を残していきます。</p>","path":["第 9 章 グラフ"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"章の内容","text":"<ul> <li>9.1 グラフ</li> <li>9.2 グラフの基本操作</li> <li>9.3 グラフの走査</li> <li>9.4 まとめ</li> </ul>","path":["第 9 章 グラフ"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1 グラフ","text":"<p>グラフは非線形データ構造の一種で、頂点と辺で構成されます。グラフ\\(G\\)は、頂点の集合\\(V\\)と辺の集合\\(E\\)の組み合わせとして抽象的に表現できます。以下の例は、5つの頂点と7つの辺を含むグラフを示しています。</p> \\[ \\begin{aligned} V & = \\{ 1, 2, 3, 4, 5 \\} \\newline E & = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G & = \\{ V, E \\} \\newline \\end{aligned} \\] <p>頂点をノード、辺をノードを接続する参照(ポインタ)と見なすと、グラフは連結リストから拡張されたデータ構造として見ることができます。下図に示すように、線形関係(連結リスト)や分割統治関係(木)と比較して、ネットワーク関係(グラフ)は自由度が高いため、より複雑です。</p> <p></p> <p> 図 9-1 連結リスト、木、グラフの関係 </p>","path":["第 9 章 グラフ","9.1 グラフ"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1 グラフの一般的な種類と用語","text":"<p>グラフは、辺に方向があるかどうかによって無向グラフと有向グラフに分けることができます(下図参照)。</p> <ul> <li>無向グラフでは、辺は2つの頂点間の「双方向」接続を表します。例えば、Facebookの「友達」関係です。</li> <li>有向グラフでは、辺に方向性があります。つまり、辺\\(A \\rightarrow B\\)と\\(A \\leftarrow B\\)は互いに独立しています。例えば、InstagramやTikTokの「フォロー」と「フォロワー」の関係です。</li> </ul> <p></p> <p> 図 9-2 有向グラフと無向グラフ </p> <p>すべての頂点が接続されているかどうかによって、グラフは連結グラフと非連結グラフに分けることができます(下図参照)。</p> <ul> <li>連結グラフでは、任意の頂点から開始して他の任意の頂点に到達することが可能です。</li> <li>非連結グラフでは、任意の開始頂点から到達できない頂点が少なくとも1つ存在します。</li> </ul> <p></p> <p> 図 9-3 連結グラフと非連結グラフ </p> <p>辺に重み変数を追加することもでき、その結果として重み付きグラフが生まれます(下図参照)。例えば、Instagramでは、システムがあなたと他のユーザーとの間の相互作用レベル(いいね、閲覧、コメントなど)によってフォロワーとフォロー中のリストをソートします。このような相互作用ネットワークは重み付きグラフで表現できます。</p> <p></p> <p> 図 9-4 重み付きグラフと重みなしグラフ </p> <p>グラフデータ構造には、以下のような一般的に使用される用語があります。</p> <ul> <li>隣接:2つの頂点を接続する辺がある場合、これら2つの頂点は「隣接」していると言われます。上図では、頂点1の隣接頂点は頂点2、3、5です。</li> <li>パス:頂点Aから頂点Bまでに通過する辺のシーケンスを、AからBへのパスと呼びます。上図では、辺のシーケンス1-5-2-4は頂点1から頂点4へのパスです。</li> <li>次数:頂点が持つ辺の数です。有向グラフの場合、入次数はその頂点を指す辺の数、出次数はその頂点から出る辺の数を指します。</li> </ul>","path":["第 9 章 グラフ","9.1 グラフ"],"tags":[]},{"location":"chapter_graph/graph/#912","level":2,"title":"9.1.2 グラフの表現","text":"<p>グラフの一般的な表現には「隣接行列」と「隣接リスト」があります。以下の例では無向グラフを使用します。</p>","path":["第 9 章 グラフ","9.1 グラフ"],"tags":[]},{"location":"chapter_graph/graph/#1","level":3,"title":"1. 隣接行列","text":"<p>グラフの頂点数を\\(n\\)とすると、隣接行列は\\(n \\times n\\)の行列を使用してグラフを表現します。各行(列)は頂点を表し、行列要素は辺を表し、2つの頂点間に辺があるかどうかを\\(1\\)または\\(0\\)で示します。</p> <p>下図に示すように、隣接行列を\\(M\\)、頂点のリストを\\(V\\)とすると、行列要素\\(M[i, j] = 1\\)は頂点\\(V[i]\\)と頂点\\(V[j]\\)の間に辺があることを示し、逆に\\(M[i, j] = 0\\)は2つの頂点間に辺がないことを示します。</p> <p></p> <p> 図 9-5 隣接行列によるグラフの表現 </p> <p>隣接行列には以下の特性があります。</p> <ul> <li>頂点は自分自身に接続することはできないため、隣接行列の主対角線上の要素は意味がありません。</li> <li>無向グラフの場合、両方向の辺は等価であるため、隣接行列は主対角線に関して対称です。</li> <li>隣接行列の要素を\\(1\\)と\\(0\\)から重みに置き換えることで、重み付きグラフを表現できます。</li> </ul> <p>隣接行列でグラフを表現する場合、行列要素に直接アクセスして辺を取得できるため、追加、削除、検索、変更の操作が効率的で、すべて時間計算量\\(O(1)\\)です。ただし、行列の空間計算量は\\(O(n^2)\\)で、より多くのメモリを消費します。</p>","path":["第 9 章 グラフ","9.1 グラフ"],"tags":[]},{"location":"chapter_graph/graph/#2","level":3,"title":"2. 隣接リスト","text":"<p>隣接リストは\\(n\\)個の連結リストを使用してグラフを表現し、各連結リストノードは頂点を表します。\\(i\\)番目の連結リストは頂点\\(i\\)に対応し、すべての隣接頂点(その頂点に接続された頂点)を含みます。下図は隣接リストを使用して格納されたグラフの例を示しています。</p> <p></p> <p> 図 9-6 隣接リストによるグラフの表現 </p> <p>隣接リストは実際の辺のみを格納し、辺の総数は\\(n^2\\)よりもはるかに少ないことが多く、より空間効率的です。ただし、隣接リストで辺を見つけるには連結リストを走査する必要があるため、その時間効率は隣接行列ほど良くありません。</p> <p>上図を観察すると、隣接リストの構造はハッシュテーブルの「チェイン法」と非常に似ているため、同様の方法を使用して効率を最適化できます。例えば、連結リストが長い場合、それをAVL木や赤黒木に変換して、時間効率を\\(O(n)\\)から\\(O(\\log n)\\)に最適化できます。連結リストをハッシュテーブルに変換することで、時間計算量を\\(O(1)\\)に削減することもできます。</p>","path":["第 9 章 グラフ","9.1 グラフ"],"tags":[]},{"location":"chapter_graph/graph/#913","level":2,"title":"9.1.3 グラフの一般的な応用","text":"<p>下表に示すように、多くの現実世界のシステムはグラフでモデル化でき、対応する問題はグラフ計算問題に削減できます。</p> <p> 表 9-1 現実生活の一般的なグラフ </p> 頂点 辺 グラフ計算問題 ソーシャルネットワーク ユーザー フォロー / フォロワー関係 潜在的フォロー推薦 地下鉄路線 駅 駅間の接続性 最短ルート推薦 太陽系 天体 天体間の重力 惑星軌道計算","path":["第 9 章 グラフ","9.1 グラフ"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2 グラフの基本操作","text":"<p>グラフの基本操作は「辺」に対する操作と「頂点」に対する操作に分けることができます。「隣接行列」と「隣接リスト」の2つの表現方法の下では、実装が異なります。</p>","path":["第 9 章 グラフ","9.2 グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1 隣接行列に基づく実装","text":"<p>\\(n\\)個の頂点を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装されます。</p> <ul> <li>辺の追加または削除:隣接行列内の指定された辺を直接変更し、\\(O(1)\\)時間を使用します。無向グラフであるため、両方向の辺を同時に更新する必要があります。</li> <li>頂点の追加:隣接行列の末尾に行と列を追加し、すべて\\(0\\)で埋めます。\\(O(n)\\)時間を使用します。</li> <li>頂点の削除:隣接行列内の行と列を削除します。最悪の場合は最初の行と列が削除されるときで、\\((n-1)^2\\)個の要素を「上と左に移動」する必要があり、\\(O(n^2)\\)時間を使用します。</li> <li>初期化:\\(n\\)個の頂点を渡し、長さ\\(n\\)の頂点リスト<code>vertices</code>を初期化し、\\(O(n)\\)時間を使用します。\\(n \\times n\\)サイズの隣接行列<code>adjMat</code>を初期化し、\\(O(n^2)\\)時間を使用します。</li> </ul> 隣接行列の初期化辺の追加辺の削除頂点の追加頂点の削除 <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 9-7 隣接行列での初期化、辺の追加と削除、頂点の追加と削除 </p> <p>以下は隣接行列を使用して表現されたグラフの実装コードです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py<pre><code>class GraphAdjMat:\n \"\"\"隣接行列に基づく無向グラフクラス\"\"\"\n\n def __init__(self, vertices: list[int], edges: list[list[int]]):\n \"\"\"コンストラクタ\"\"\"\n # 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す\n self.vertices: list[int] = []\n # 隣接行列、行と列のインデックスは「頂点インデックス」に対応\n self.adj_mat: list[list[int]] = []\n # 頂点を追加\n for val in vertices:\n self.add_vertex(val)\n # 辺を追加\n # edges要素は頂点インデックスを表す\n for e in edges:\n self.add_edge(e[0], e[1])\n\n def size(self) -> int:\n \"\"\"頂点数を取得\"\"\"\n return len(self.vertices)\n\n def add_vertex(self, val: int):\n \"\"\"頂点を追加\"\"\"\n n = self.size()\n # 頂点リストに新しい頂点値を追加\n self.vertices.append(val)\n # 隣接行列に行を追加\n new_row = [0] * n\n self.adj_mat.append(new_row)\n # 隣接行列に列を追加\n for row in self.adj_mat:\n row.append(0)\n\n def remove_vertex(self, index: int):\n \"\"\"頂点を削除\"\"\"\n if index >= self.size():\n raise IndexError()\n # 頂点リストから`index`の頂点を削除\n self.vertices.pop(index)\n # 隣接行列から`index`の行を削除\n self.adj_mat.pop(index)\n # 隣接行列から`index`の列を削除\n for row in self.adj_mat:\n row.pop(index)\n\n def add_edge(self, i: int, j: int):\n \"\"\"辺を追加\"\"\"\n # パラメータi、jは頂点要素のインデックスに対応\n # インデックスの範囲外と等価性を処理\n if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n raise IndexError()\n # 無向グラフでは、隣接行列は主対角線について対称、すなわち (i, j) == (j, i) を満たす\n self.adj_mat[i][j] = 1\n self.adj_mat[j][i] = 1\n\n def remove_edge(self, i: int, j: int):\n \"\"\"辺を削除\"\"\"\n # パラメータi、jは頂点要素のインデックスに対応\n # インデックスの範囲外と等価性を処理\n if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n raise IndexError()\n self.adj_mat[i][j] = 0\n self.adj_mat[j][i] = 0\n\n def print(self):\n \"\"\"隣接行列を出力\"\"\"\n print(\"頂点リスト =\", self.vertices)\n print(\"隣接行列 =\")\n print_matrix(self.adj_mat)\n</code></pre> graph_adjacency_matrix.cpp<pre><code>/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n vector<int> vertices; // 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す\n vector<vector<int>> adjMat; // 隣接行列、行と列のインデックスは「頂点インデックス」に対応\n\n public:\n /* コンストラクタ */\n GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n // 頂点を追加\n for (int val : vertices) {\n addVertex(val);\n }\n // 辺を追加\n // 辺の要素は頂点インデックスを表す\n for (const vector<int> &edge : edges) {\n addEdge(edge[0], edge[1]);\n }\n }\n\n /* 頂点数を取得 */\n int size() const {\n return vertices.size();\n }\n\n /* 頂点を追加 */\n void addVertex(int val) {\n int n = size();\n // 頂点リストに新しい頂点値を追加\n vertices.push_back(val);\n // 隣接行列に行を追加\n adjMat.emplace_back(vector<int>(n, 0));\n // 隣接行列に列を追加\n for (vector<int> &row : adjMat) {\n row.push_back(0);\n }\n }\n\n /* 頂点を削除 */\n void removeVertex(int index) {\n if (index >= size()) {\n throw out_of_range(\"Vertex does not exist\");\n }\n // 頂点リストから`index`の頂点を削除\n vertices.erase(vertices.begin() + index);\n // 隣接行列から`index`の行を削除\n adjMat.erase(adjMat.begin() + index);\n // 隣接行列から`index`の列を削除\n for (vector<int> &row : adjMat) {\n row.erase(row.begin() + index);\n }\n }\n\n /* 辺を追加 */\n // パラメータi、jは頂点要素のインデックスに対応\n void addEdge(int i, int j) {\n // インデックス範囲外と等価性を処理\n if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n throw out_of_range(\"Vertex does not exist\");\n }\n // 無向グラフでは、隣接行列は主対角線について対称、即ち(i, j) == (j, i)を満たす\n adjMat[i][j] = 1;\n adjMat[j][i] = 1;\n }\n\n /* 辺を削除 */\n // パラメータi、jは頂点要素のインデックスに対応\n void removeEdge(int i, int j) {\n // インデックス範囲外と等価性を処理\n if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n throw out_of_range(\"Vertex does not exist\");\n }\n adjMat[i][j] = 0;\n adjMat[j][i] = 0;\n }\n\n /* 隣接行列を印刷 */\n void print() {\n cout << \"頂点リスト = \";\n printVector(vertices);\n cout << \"隣接行列 =\" << endl;\n printVectorMatrix(adjMat);\n }\n};\n</code></pre> graph_adjacency_matrix.java<pre><code>/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n List<Integer> vertices; // 頂点リスト、要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す\n List<List<Integer>> adjMat; // 隣接行列、行と列のインデックスは「頂点インデックス」に対応\n\n /* コンストラクタ */\n public GraphAdjMat(int[] vertices, int[][] edges) {\n this.vertices = new ArrayList<>();\n this.adjMat = new ArrayList<>();\n // 頂点を追加\n for (int val : vertices) {\n addVertex(val);\n }\n // 辺を追加\n // 辺の要素は頂点インデックスを表す\n for (int[] e : edges) {\n addEdge(e[0], e[1]);\n }\n }\n\n /* 頂点数を取得 */\n public int size() {\n return vertices.size();\n }\n\n /* 頂点を追加 */\n public void addVertex(int val) {\n int n = size();\n // 頂点リストに新しい頂点値を追加\n vertices.add(val);\n // 隣接行列に行を追加\n List<Integer> newRow = new ArrayList<>(n);\n for (int j = 0; j < n; j++) {\n newRow.add(0);\n }\n adjMat.add(newRow);\n // 隣接行列に列を追加\n for (List<Integer> row : adjMat) {\n row.add(0);\n }\n }\n\n /* 頂点を削除 */\n public void removeVertex(int index) {\n if (index >= size())\n throw new IndexOutOfBoundsException();\n // 頂点リストから `index` の頂点を削除\n vertices.remove(index);\n // 隣接行列から `index` の行を削除\n adjMat.remove(index);\n // 隣接行列から `index` の列を削除\n for (List<Integer> row : adjMat) {\n row.remove(index);\n }\n }\n\n /* 辺を追加 */\n // パラメータ i、j は頂点要素のインデックスに対応\n public void addEdge(int i, int j) {\n // インデックスの範囲外と等価性を処理\n if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n throw new IndexOutOfBoundsException();\n // 無向グラフでは、隣接行列は主対角線について対称、すなわち (i, j) == (j, i) を満たす\n adjMat.get(i).set(j, 1);\n adjMat.get(j).set(i, 1);\n }\n\n /* 辺を削除 */\n // パラメータ i、j は頂点要素のインデックスに対応\n public void removeEdge(int i, int j) {\n // インデックスの範囲外と等価性を処理\n if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n throw new IndexOutOfBoundsException();\n adjMat.get(i).set(j, 0);\n adjMat.get(j).set(i, 0);\n }\n\n /* 隣接行列を出力 */\n public void print() {\n System.out.print(\"頂点リスト = \");\n System.out.println(vertices);\n System.out.println(\"隣接行列 =\");\n PrintUtil.printMatrix(adjMat);\n }\n}\n</code></pre> graph_adjacency_matrix.cs<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.go<pre><code>[class]{graphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.swift<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.js<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.ts<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.dart<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.rs<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.c<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.kt<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre> graph_adjacency_matrix.rb<pre><code>[class]{GraphAdjMat}-[func]{}\n</code></pre>","path":["第 9 章 グラフ","9.2 グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#922","level":2,"title":"9.2.2 隣接リストに基づく実装","text":"<p>総計\\(n\\)個の頂点と\\(m\\)個の辺を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装できます。</p> <ul> <li>辺の追加:対応する頂点の連結リストの末尾に辺を追加するだけで、\\(O(1)\\)時間を使用します。無向グラフであるため、両方向に同時に辺を追加する必要があります。</li> <li>辺の削除:対応する頂点の連結リスト内で指定された辺を見つけて削除し、\\(O(m)\\)時間を使用します。無向グラフでは、両方向の辺を同時に削除する必要があります。</li> <li>頂点の追加:隣接リストに連結リストを追加し、新しい頂点をリストのヘッドノードにし、\\(O(1)\\)時間を使用します。</li> <li>頂点の削除:隣接リスト全体を走査し、指定された頂点を含むすべての辺を削除する必要があり、\\(O(n + m)\\)時間を使用します。</li> <li>初期化:隣接リストに\\(n\\)個の頂点と\\(2m\\)個の辺を作成し、\\(O(n + m)\\)時間を使用します。</li> </ul> 隣接リストの初期化辺の追加辺の削除頂点の追加頂点の削除 <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 9-8 隣接リストでの初期化、辺の追加と削除、頂点の追加と削除 </p> <p>以下は隣接リストのコード実装です。上図と比較して、実際のコードには以下の違いがあります。</p> <ul> <li>頂点の追加と削除の便宜、およびコードの簡素化のため、連結リストの代わりにリスト(動的配列)を使用します。</li> <li>ハッシュテーブルを使用して隣接リストを格納し、<code>key</code>が頂点インスタンス、<code>value</code>がその頂点の隣接頂点のリスト(連結リスト)です。</li> </ul> <p>さらに、隣接リストで頂点を表現するために<code>Vertex</code>クラスを使用します。その理由は:隣接行列のようにリストインデックスを使用して異なる頂点を区別する場合、インデックス\\(i\\)の頂点を削除したい場合、隣接リスト全体を走査し、\\(i\\)より大きいすべてのインデックスを1つずつ減少させる必要があり、これは非常に非効率的です。しかし、各頂点が一意の<code>Vertex</code>インスタンスである場合、頂点を削除しても他の頂点に変更を加える必要がありません。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py<pre><code>class GraphAdjList:\n \"\"\"隣接リストに基づく無向グラフクラス\"\"\"\n\n def __init__(self, edges: list[list[Vertex]]):\n \"\"\"コンストラクタ\"\"\"\n # 隣接リスト、キー: 頂点、値: その頂点の隣接する全頂点\n self.adj_list = dict[Vertex, list[Vertex]]()\n # すべての頂点と辺を追加\n for edge in edges:\n self.add_vertex(edge[0])\n self.add_vertex(edge[1])\n self.add_edge(edge[0], edge[1])\n\n def size(self) -> int:\n \"\"\"頂点数を取得\"\"\"\n return len(self.adj_list)\n\n def add_edge(self, vet1: Vertex, vet2: Vertex):\n \"\"\"辺を追加\"\"\"\n if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n raise ValueError()\n # 辺 vet1 - vet2 を追加\n self.adj_list[vet1].append(vet2)\n self.adj_list[vet2].append(vet1)\n\n def remove_edge(self, vet1: Vertex, vet2: Vertex):\n \"\"\"辺を削除\"\"\"\n if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n raise ValueError()\n # 辺 vet1 - vet2 を削除\n self.adj_list[vet1].remove(vet2)\n self.adj_list[vet2].remove(vet1)\n\n def add_vertex(self, vet: Vertex):\n \"\"\"頂点を追加\"\"\"\n if vet in self.adj_list:\n return\n # 隣接リストに新しい連結リストを追加\n self.adj_list[vet] = []\n\n def remove_vertex(self, vet: Vertex):\n \"\"\"頂点を削除\"\"\"\n if vet not in self.adj_list:\n raise ValueError()\n # 隣接リストから頂点vetに対応する連結リストを削除\n self.adj_list.pop(vet)\n # 他の頂点の連結リストを走査し、vetを含むすべての辺を削除\n for vertex in self.adj_list:\n if vet in self.adj_list[vertex]:\n self.adj_list[vertex].remove(vet)\n\n def print(self):\n \"\"\"隣接リストを出力\"\"\"\n print(\"隣接リスト =\")\n for vertex in self.adj_list:\n tmp = [v.val for v in self.adj_list[vertex]]\n print(f\"{vertex.val}: {tmp},\")\n</code></pre> graph_adjacency_list.cpp<pre><code>/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n public:\n // 隣接リスト、キー:頂点、値:その頂点のすべての隣接頂点\n unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n /* ベクターから指定されたノードを削除 */\n void remove(vector<Vertex *> &vec, Vertex *vet) {\n for (int i = 0; i < vec.size(); i++) {\n if (vec[i] == vet) {\n vec.erase(vec.begin() + i);\n break;\n }\n }\n }\n\n /* コンストラクタ */\n GraphAdjList(const vector<vector<Vertex *>> &edges) {\n // すべての頂点と辺を追加\n for (const vector<Vertex *> &edge : edges) {\n addVertex(edge[0]);\n addVertex(edge[1]);\n addEdge(edge[0], edge[1]);\n }\n }\n\n /* 頂点数を取得 */\n int size() {\n return adjList.size();\n }\n\n /* 辺を追加 */\n void addEdge(Vertex *vet1, Vertex *vet2) {\n if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n throw invalid_argument(\"Vertex does not exist\");\n // 辺 vet1 - vet2 を追加\n adjList[vet1].push_back(vet2);\n adjList[vet2].push_back(vet1);\n }\n\n /* 辺を削除 */\n void removeEdge(Vertex *vet1, Vertex *vet2) {\n if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n throw invalid_argument(\"Vertex does not exist\");\n // 辺 vet1 - vet2 を削除\n remove(adjList[vet1], vet2);\n remove(adjList[vet2], vet1);\n }\n\n /* 頂点を追加 */\n void addVertex(Vertex *vet) {\n if (adjList.count(vet))\n return;\n // 隣接リストに新しい連結リストを追加\n adjList[vet] = vector<Vertex *>();\n }\n\n /* 頂点を削除 */\n void removeVertex(Vertex *vet) {\n if (!adjList.count(vet))\n throw invalid_argument(\"Vertex does not exist\");\n // 隣接リストから頂点vetに対応する連結リストを削除\n adjList.erase(vet);\n // 他の頂点の連結リストを走査し、vetを含むすべての辺を削除\n for (auto &adj : adjList) {\n remove(adj.second, vet);\n }\n }\n\n /* 隣接リストを印刷 */\n void print() {\n cout << \"隣接リスト =\" << endl;\n for (auto &adj : adjList) {\n const auto &key = adj.first;\n const auto &vec = adj.second;\n cout << key->val << \": \";\n printVector(vetsToVals(vec));\n }\n }\n};\n</code></pre> graph_adjacency_list.java<pre><code>/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n // 隣接リスト、キー: 頂点、値: その頂点のすべての隣接頂点\n Map<Vertex, List<Vertex>> adjList;\n\n /* コンストラクタ */\n public GraphAdjList(Vertex[][] edges) {\n this.adjList = new HashMap<>();\n // すべての頂点と辺を追加\n for (Vertex[] edge : edges) {\n addVertex(edge[0]);\n addVertex(edge[1]);\n addEdge(edge[0], edge[1]);\n }\n }\n\n /* 頂点数を取得 */\n public int size() {\n return adjList.size();\n }\n\n /* 辺を追加 */\n public void addEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n throw new IllegalArgumentException();\n // 辺 vet1 - vet2 を追加\n adjList.get(vet1).add(vet2);\n adjList.get(vet2).add(vet1);\n }\n\n /* 辺を削除 */\n public void removeEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n throw new IllegalArgumentException();\n // 辺 vet1 - vet2 を削除\n adjList.get(vet1).remove(vet2);\n adjList.get(vet2).remove(vet1);\n }\n\n /* 頂点を追加 */\n public void addVertex(Vertex vet) {\n if (adjList.containsKey(vet))\n return;\n // 隣接リストに新しい連結リストを追加\n adjList.put(vet, new ArrayList<>());\n }\n\n /* 頂点を削除 */\n public void removeVertex(Vertex vet) {\n if (!adjList.containsKey(vet))\n throw new IllegalArgumentException();\n // 隣接リストから頂点 vet に対応する連結リストを削除\n adjList.remove(vet);\n // 他の頂点の連結リストを走査し、vet を含むすべての辺を削除\n for (List<Vertex> list : adjList.values()) {\n list.remove(vet);\n }\n }\n\n /* 隣接リストを出力 */\n public void print() {\n System.out.println(\"隣接リスト =\");\n for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n List<Integer> tmp = new ArrayList<>();\n for (Vertex vertex : pair.getValue())\n tmp.add(vertex.val);\n System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n }\n }\n}\n</code></pre> graph_adjacency_list.cs<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.go<pre><code>[class]{graphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.swift<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.js<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.ts<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.dart<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.rs<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.c<pre><code>[class]{AdjListNode}-[func]{}\n\n[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.kt<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre> graph_adjacency_list.rb<pre><code>[class]{GraphAdjList}-[func]{}\n</code></pre>","path":["第 9 章 グラフ","9.2 グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#923","level":2,"title":"9.2.3 効率の比較","text":"<p>グラフに\\(n\\)個の頂点と\\(m\\)個の辺があると仮定すると、下表は隣接行列と隣接リストの時間効率と空間効率を比較しています。</p> <p> 表 9-2 隣接行列と隣接リストの比較 </p> 隣接行列 隣接リスト(連結リスト) 隣接リスト(ハッシュテーブル) 隣接性の判定 \\(O(1)\\) \\(O(m)\\) \\(O(1)\\) 辺の追加 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 辺の削除 \\(O(1)\\) \\(O(m)\\) \\(O(1)\\) 頂点の追加 \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) 頂点の削除 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) メモリ空間使用量 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\) <p>上表を観察すると、隣接リスト(ハッシュテーブル)が最高の時間効率と空間効率を持っているように見えます。しかし、実際には、隣接行列での辺に対する操作がより効率的で、単一の配列アクセスまたは代入操作のみが必要です。全体的に、隣接行列は「空間と時間のトレードオフ」の原則を例示し、隣接リストは「時間と空間のトレードオフ」を例示しています。</p>","path":["第 9 章 グラフ","9.2 グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3 グラフ走査","text":"<p>木は「一対多」の関係を表現し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木をグラフの特別なケースと見なすことができます。明らかに、木の走査操作もグラフ走査操作の特別なケースです。</p> <p>グラフと木の両方で、走査操作を実装するために探索アルゴリズムの応用が必要です。グラフ走査は2つのタイプに分けることができます:幅優先探索(BFS)と深さ優先探索(DFS)です。</p>","path":["第 9 章 グラフ","9.3 グラフ走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1 幅優先探索","text":"<p>幅優先探索は近くから遠くへの走査方法で、ある頂点から開始し、常に最も近い頂点を優先的に訪問し、層ごとに外側に展開していきます。下図に示すように、左上の頂点から開始し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、以下同様に、すべての頂点が訪問されるまで続けます。</p> <p></p> <p> 図 9-9 グラフの幅優先走査 </p>","path":["第 9 章 グラフ","9.3 グラフ走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1","level":3,"title":"1. アルゴリズムの実装","text":"<p>BFSは通常キューの助けを借りて実装されます(下記のコード参照)。キューは「先入先出」で、これは「近くから遠くへ」走査するBFSの考え方と一致します。</p> <ol> <li>開始頂点<code>startVet</code>をキューに追加し、ループを開始します。</li> <li>ループの各反復で、キューの先頭の頂点をポップし、それを訪問済みとして記録し、次にその頂点のすべての隣接頂点をキューの末尾に追加します。</li> <li>すべての頂点が訪問されるまで手順<code>2.</code>を繰り返します。</li> </ol> <p>頂点の再訪問を防ぐために、ハッシュセット<code>visited</code>を使用してどのノードが訪問されたかを記録します。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py<pre><code>def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n \"\"\"幅優先走査\"\"\"\n # 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得\n # 頂点走査シーケンス\n res = []\n # ハッシュセット、訪問済み頂点を記録するために使用\n visited = set[Vertex]([start_vet])\n # BFSを実装するために使用されるキュー\n que = deque[Vertex]([start_vet])\n # 頂点vetから開始し、すべての頂点が訪問されるまでループ\n while len(que) > 0:\n vet = que.popleft() # キューの先頭の頂点をデキュー\n res.append(vet) # 訪問済み頂点を記録\n # その頂点のすべての隣接頂点を走査\n for adj_vet in graph.adj_list[vet]:\n if adj_vet in visited:\n continue # 既に訪問済みの頂点をスキップ\n que.append(adj_vet) # 未訪問の頂点のみをエンキュー\n visited.add(adj_vet) # 頂点を訪問済みとしてマーク\n # 頂点走査シーケンスを返す\n return res\n</code></pre> graph_bfs.cpp<pre><code>/* 幅優先走査 */\n// 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n // 頂点走査順序\n vector<Vertex *> res;\n // ハッシュセット、訪問済み頂点を記録するために使用\n unordered_set<Vertex *> visited = {startVet};\n // BFSを実装するために使用されるキュー\n queue<Vertex *> que;\n que.push(startVet);\n // 頂点vetから開始し、すべての頂点が訪問されるまでループ\n while (!que.empty()) {\n Vertex *vet = que.front();\n que.pop(); // キューの先頭の頂点をデキュー\n res.push_back(vet); // 訪問済み頂点を記録\n // その頂点のすべての隣接頂点を走査\n for (auto adjVet : graph.adjList[vet]) {\n if (visited.count(adjVet))\n continue; // すでに訪問済みの頂点をスキップ\n que.push(adjVet); // 未訪問の頂点のみをエンキュー\n visited.emplace(adjVet); // 頂点を訪問済みとしてマーク\n }\n }\n // 頂点走査順序を返す\n return res;\n}\n</code></pre> graph_bfs.java<pre><code>/* 幅優先走査 */\n// 隣接リストを使用してグラフを表現し、指定した頂点のすべての隣接頂点を取得\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n // 頂点走査順序\n List<Vertex> res = new ArrayList<>();\n // ハッシュセット、訪問済みの頂点を記録するために使用\n Set<Vertex> visited = new HashSet<>();\n visited.add(startVet);\n // BFS を実装するために使用するキュー\n Queue<Vertex> que = new LinkedList<>();\n que.offer(startVet);\n // 頂点 vet から開始し、すべての頂点が訪問されるまでループ\n while (!que.isEmpty()) {\n Vertex vet = que.poll(); // キューの先頭の頂点をデキュー\n res.add(vet); // 訪問した頂点を記録\n // その頂点のすべての隣接頂点を走査\n for (Vertex adjVet : graph.adjList.get(vet)) {\n if (visited.contains(adjVet))\n continue; // すでに訪問済みの頂点をスキップ\n que.offer(adjVet); // 未訪問の頂点のみをエンキュー\n visited.add(adjVet); // 頂点を訪問済みとしてマーク\n }\n }\n // 頂点走査順序を返す\n return res;\n}\n</code></pre> graph_bfs.cs<pre><code>[class]{graph_bfs}-[func]{GraphBFS}\n</code></pre> graph_bfs.go<pre><code>[class]{}-[func]{graphBFS}\n</code></pre> graph_bfs.swift<pre><code>[class]{}-[func]{graphBFS}\n</code></pre> graph_bfs.js<pre><code>[class]{}-[func]{graphBFS}\n</code></pre> graph_bfs.ts<pre><code>[class]{}-[func]{graphBFS}\n</code></pre> graph_bfs.dart<pre><code>[class]{}-[func]{graphBFS}\n</code></pre> graph_bfs.rs<pre><code>[class]{}-[func]{graph_bfs}\n</code></pre> graph_bfs.c<pre><code>[class]{Queue}-[func]{}\n\n[class]{}-[func]{isVisited}\n\n[class]{}-[func]{graphBFS}\n</code></pre> graph_bfs.kt<pre><code>[class]{}-[func]{graphBFS}\n</code></pre> graph_bfs.rb<pre><code>[class]{}-[func]{graph_bfs}\n</code></pre> <p>コードは比較的抽象的ですが、下図と比較することでより良く理解できます。</p> <1><2><3><4><5><6><7><8><9><10><11> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 9-10 グラフの幅優先探索の手順 </p> <p>幅優先走査のシーケンスは一意ですか?</p> <p>一意ではありません。幅優先走査は「近くから遠く」の順序で走査することのみを要求し、同じ距離の頂点の走査順序は任意にできます。例えば、上図では、頂点\\(1\\)と\\(3\\)の訪問順序を交換できますし、頂点\\(2\\)、\\(4\\)、\\(6\\)の順序も同様です。</p>","path":["第 9 章 グラフ","9.3 グラフ走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2. 計算量分析","text":"<p>時間計算量:すべての頂点が一度ずつエンキューおよびデキューされ、\\(O(|V|)\\)時間を使用します。隣接頂点を走査する過程で、無向グラフであるため、すべての辺が\\(2\\)回訪問され、\\(O(2|E|)\\)時間を使用します。全体で\\(O(|V| + |E|)\\)時間を使用します。</p> <p>空間計算量:リスト<code>res</code>、ハッシュセット<code>visited</code>、キュー<code>que</code>の最大頂点数は\\(|V|\\)で、\\(O(|V|)\\)空間を使用します。</p>","path":["第 9 章 グラフ","9.3 グラフ走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932","level":2,"title":"9.3.2 深さ優先探索","text":"<p>深さ優先探索は可能な限り遠くまで行き、それ以上のパスがない場合にバックトラックする走査方法です。下図に示すように、左上の頂点から開始し、それ以上のパスがなくなるまで現在の頂点のいずれかの隣接頂点を訪問し、次に戻って続行し、すべての頂点が走査されるまで続けます。</p> <p></p> <p> 図 9-11 グラフの深さ優先走査 </p>","path":["第 9 章 グラフ","9.3 グラフ走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1_1","level":3,"title":"1. アルゴリズムの実装","text":"<p>この「可能な限り遠くまで行ってから戻る」アルゴリズムパラダイムは通常再帰に基づいて実装されます。幅優先探索と同様に、深さ優先探索でも、再訪問を避けるために訪問済み頂点を記録するハッシュセット<code>visited</code>の助けが必要です。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py<pre><code>def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n \"\"\"深さ優先走査のヘルパー関数\"\"\"\n res.append(vet) # 訪問済み頂点を記録\n visited.add(vet) # 頂点を訪問済みとしてマーク\n # その頂点のすべての隣接頂点を走査\n for adjVet in graph.adj_list[vet]:\n if adjVet in visited:\n continue # 既に訪問済みの頂点をスキップ\n # 隣接頂点を再帰的に訪問\n dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n \"\"\"深さ優先走査\"\"\"\n # 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得\n # 頂点走査シーケンス\n res = []\n # ハッシュセット、訪問済み頂点を記録するために使用\n visited = set[Vertex]()\n dfs(graph, visited, res, start_vet)\n return res\n</code></pre> graph_dfs.cpp<pre><code>/* 深さ優先走査ヘルパー関数 */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n res.push_back(vet); // 訪問済み頂点を記録\n visited.emplace(vet); // 頂点を訪問済みとしてマーク\n // その頂点のすべての隣接頂点を走査\n for (Vertex *adjVet : graph.adjList[vet]) {\n if (visited.count(adjVet))\n continue; // すでに訪問済みの頂点をスキップ\n // 隣接頂点を再帰的に訪問\n dfs(graph, visited, res, adjVet);\n }\n}\n\n/* 深さ優先走査 */\n// 隣接リストを使用してグラフを表現し、指定された頂点のすべての隣接頂点を取得\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n // 頂点走査順序\n vector<Vertex *> res;\n // ハッシュセット、訪問済み頂点を記録するために使用\n unordered_set<Vertex *> visited;\n dfs(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.java<pre><code>/* 深さ優先走査の補助関数 */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n res.add(vet); // 訪問した頂点を記録\n visited.add(vet); // 頂点を訪問済みとしてマーク\n // その頂点のすべての隣接頂点を走査\n for (Vertex adjVet : graph.adjList.get(vet)) {\n if (visited.contains(adjVet))\n continue; // すでに訪問済みの頂点をスキップ\n // 隣接頂点を再帰的に訪問\n dfs(graph, visited, res, adjVet);\n }\n}\n\n/* 深さ優先走査 */\n// 隣接リストを使用してグラフを表現し、指定した頂点のすべての隣接頂点を取得\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n // 頂点走査順序\n List<Vertex> res = new ArrayList<>();\n // ハッシュセット、訪問済みの頂点を記録するために使用\n Set<Vertex> visited = new HashSet<>();\n dfs(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.cs<pre><code>[class]{graph_dfs}-[func]{DFS}\n\n[class]{graph_dfs}-[func]{GraphDFS}\n</code></pre> graph_dfs.go<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graphDFS}\n</code></pre> graph_dfs.swift<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graphDFS}\n</code></pre> graph_dfs.js<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graphDFS}\n</code></pre> graph_dfs.ts<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graphDFS}\n</code></pre> graph_dfs.dart<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graphDFS}\n</code></pre> graph_dfs.rs<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graph_dfs}\n</code></pre> graph_dfs.c<pre><code>[class]{}-[func]{isVisited}\n\n[class]{}-[func]{dfs}\n\n[class]{}-[func]{graphDFS}\n</code></pre> graph_dfs.kt<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graphDFS}\n</code></pre> graph_dfs.rb<pre><code>[class]{}-[func]{dfs}\n\n[class]{}-[func]{graph_dfs}\n</code></pre> <p>深さ優先探索のアルゴリズムプロセスを下図に示します。</p> <ul> <li>破線は下向きの再帰を表し、新しい頂点を訪問するために新しい再帰メソッドが開始されたことを示します。</li> <li>曲線の破線は上向きのバックトラックを表し、この再帰メソッドがこのメソッドが開始された位置に戻ったことを示します。</li> </ul> <p>理解を深めるため、下図とコードを組み合わせて、DFSプロセス全体を頭の中でシミュレート(または描画)することをお勧めします。各再帰メソッドがいつ開始され、いつ戻るかを含めてです。</p> <1><2><3><4><5><6><7><8><9><10><11> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 9-12 グラフの深さ優先探索の手順 </p> <p>深さ優先走査のシーケンスは一意ですか?</p> <p>幅優先走査と同様に、深さ優先走査シーケンスの順序も一意ではありません。ある頂点が与えられた場合、どの方向を最初に探索することも可能です。つまり、隣接頂点の順序は任意にシャッフルできますが、すべて深さ優先走査の一部です。</p> <p>木の走査を例に取ると、「根 \\(\\rightarrow\\) 左 \\(\\rightarrow\\) 右」、「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」、「左 \\(\\rightarrow\\) 右 \\(\\rightarrow\\) 根」は、それぞれ前順、中順、後順走査に対応します。これらは3つの異なる走査優先度を示していますが、3つすべてが深さ優先走査と見なされます。</p>","path":["第 9 章 グラフ","9.3 グラフ走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2_1","level":3,"title":"2. 計算量分析","text":"<p>時間計算量:すべての頂点が一度訪問され、\\(O(|V|)\\)時間を使用します。すべての辺が2回訪問され、\\(O(2|E|)\\)時間を使用します。全体で\\(O(|V| + |E|)\\)時間を使用します。</p> <p>空間計算量:リスト<code>res</code>、ハッシュセット<code>visited</code>の最大頂点数は\\(|V|\\)で、最大再帰深度は\\(|V|\\)です。したがって、\\(O(|V|)\\)空間を使用します。</p>","path":["第 9 章 グラフ","9.3 グラフ走査"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4 まとめ","text":"","path":["第 9 章 グラフ","9.4 まとめ"],"tags":[]},{"location":"chapter_graph/summary/#1","level":3,"title":"1. 重要な復習","text":"<ul> <li>グラフは頂点と辺で構成されます。頂点の集合と辺の集合として記述できます。</li> <li>線形関係(連結リストなど)や階層関係(木など)と比較して、ネットワーク関係(グラフ)はより大きな柔軟性を提供し、より複雑になります。</li> <li>有向グラフでは、辺に方向があります。連結グラフでは、任意の頂点から他の任意の頂点に到達できます。重み付きグラフでは、各辺に関連する重み変数があります。</li> <li>隣接行列は、行列(2次元配列)を使用してグラフを表現する方法です。行と列は頂点を表します。行列要素の値は、2つの頂点間に辺があるかどうかを示し、辺がある場合は\\(1\\)、ない場合は\\(0\\)を使用します。隣接行列は辺の追加、削除、チェックなどの操作に非常に効率的ですが、より多くのスペースが必要です。</li> <li>隣接リストは、連結リストの集合を使用してグラフを表現するもう一つの一般的な方法です。グラフ内の各頂点には、その隣接するすべての頂点を含むリストがあります。\\(i\\)番目のリストは頂点\\(i\\)を表します。隣接リストは隣接行列と比較してより少ないスペースを使用します。ただし、辺を見つけるためにリストを走査する必要があるため、時間効率は低くなります。</li> <li>隣接リストの連結リストが十分に長くなったとき、ルックアップ効率を向上させるために赤黒木やハッシュテーブルに変換できます。</li> <li>アルゴリズム設計の観点から、隣接行列は「空間と時間のトレードオフ」の概念を反映し、隣接リストは「時間と空間のトレードオフ」を反映します。</li> <li>グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実世界のシステムをモデル化するために使用できます。</li> <li>木はグラフの特別なケースであり、木の走査もグラフ走査の特別なケースです。</li> <li>グラフの幅優先走査は、近くから遠くへと層ごとに拡張する探索方法で、通常キューを使用します。</li> <li>グラフの深さ優先走査は、それ以上のパスがない場合にバックトラックする前に、まず終端に到達することを優先する探索方法です。しばしば再帰を使用して実装されます。</li> </ul>","path":["第 9 章 グラフ","9.4 まとめ"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: パスは頂点のシーケンスとして定義されますか、それとも辺のシーケンスとして定義されますか?</p> <p>グラフ理論では、グラフ内のパスは頂点のシーケンスを結ぶ有限または無限の辺のシーケンスです。</p> <p>この文書では、パスは頂点のシーケンスではなく、辺のシーケンスと考えられます。これは、2つの頂点を結ぶ複数の辺がある可能性があり、その場合各辺がパスに対応するためです。</p> <p>Q: 非連結グラフでは、走査できない点がありますか?</p> <p>非連結グラフでは、特定の点から到達できない頂点が少なくとも1つあります。非連結グラフを走査するには、グラフのすべての連結成分を走査するために複数の開始点を設定する必要があります。</p> <p>Q: 隣接リストで、「その頂点に接続されたすべての頂点」の順序は重要ですか?</p> <p>任意の順序で構いません。ただし、実際のアプリケーションでは、頂点が追加された順序や頂点値の順序など、特定のルールに従ってそれらをソートする必要がある場合があります。これにより、特定の極値を持つ頂点を素早く見つけることができます。</p>","path":["第 9 章 グラフ","9.4 まとめ"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"第 15 章 貪欲法","text":"<p>Abstract</p> <p>ひまわりは太陽の方を向き、常に自分にとって最大の成長を求めます。</p> <p>貪欲な戦略は、一連の単純な選択を通じて、段階的に最良の答えへと導きます。</p>","path":["第 15 章 貪欲法"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"章の内容","text":"<ul> <li>15.1 貪欲アルゴリズム</li> <li>15.2 分数ナップサック問題</li> <li>15.3 最大容量問題</li> <li>15.4 最大積分割問題</li> <li>15.5 まとめ</li> </ul>","path":["第 15 章 貪欲法"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2 分数ナップサック問題","text":"<p>Question</p> <p>\\(n\\) 個のアイテムが与えられ、\\(i\\) 番目のアイテムの重量は \\(wgt[i-1]\\) で値は \\(val[i-1]\\) です。容量が \\(cap\\) のナップサックがあります。各アイテムは1回のみ選択できますが、アイテムの一部を選択することができ、その値は選択された重量の割合に基づいて計算されます。限られた容量の下でナップサック内のアイテムの最大値は何ですか?例を下の図に示します。</p> <p></p> <p> 図 15-3 分数ナップサック問題の例データ </p> <p>分数ナップサック問題は全体的に0-1ナップサック問題と非常に似ており、現在のアイテム \\(i\\) と容量 \\(c\\) を含み、ナップサックの限られた容量内で値を最大化することを目的としています。</p> <p>違いは、この問題ではアイテムの一部のみを選択できることです。下の図に示すように、アイテムを任意に分割し、重量の割合に基づいて対応する値を計算できます。</p> <ol> <li>アイテム \\(i\\) について、その単位重量あたりの値は \\(val[i-1] / wgt[i-1]\\) で、単位値と呼ばれます。</li> <li>重量 \\(w\\) のアイテム \\(i\\) の一部をナップサックに入れるとすると、ナップサックに追加される値は \\(w \\times val[i-1] / wgt[i-1]\\) です。</li> </ol> <p></p> <p> 図 15-4 アイテムの単位重量あたりの値 </p>","path":["第 15 章 貪欲法","15.2 分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1","level":3,"title":"1. 貪欲戦略の決定","text":"<p>ナップサック内のアイテムの総値を最大化することは、本質的に単位重量あたりの値を最大化することを意味します。これから、下の図に示す貪欲戦略を導出できます。</p> <ol> <li>アイテムを単位値の高い順から低い順にソートします。</li> <li>すべてのアイテムを反復し、**各ラウンドで最も高い単位値を持つアイテムを貪欲に選択**します。</li> <li>ナップサックの残り容量が不十分な場合、現在のアイテムの一部を使用してナップサックを満たします。</li> </ol> <p></p> <p> 図 15-5 分数ナップサック問題の貪欲戦略 </p>","path":["第 15 章 貪欲法","15.2 分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2","level":3,"title":"2. コード実装","text":"<p>アイテムを単位値でソートするために <code>Item</code> クラスを作成しました。ナップサックが満たされるまでループして貪欲な選択を行い、その後終了して解を返します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py<pre><code>class Item:\n \"\"\"アイテム\"\"\"\n\n def __init__(self, w: int, v: int):\n self.w = w # アイテムの重量\n self.v = v # アイテムの価値\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n \"\"\"分数ナップサック:貪欲法\"\"\"\n # アイテムリストを作成、2 つの属性を含む:重量、価値\n items = [Item(w, v) for w, v in zip(wgt, val)]\n # 単位価値 item.v / item.w で高い順にソート\n items.sort(key=lambda item: item.v / item.w, reverse=True)\n # 貪欲選択をループ\n res = 0\n for item in items:\n if item.w <= cap:\n # 残り容量が十分な場合、アイテム全体をナップサックに入れる\n res += item.v\n cap -= item.w\n else:\n # 残り容量が不十分な場合、アイテムの一部をナップサックに入れる\n res += (item.v / item.w) * cap\n # 残り容量がなくなったため、ループを中断\n break\n return res\n</code></pre> fractional_knapsack.cpp<pre><code>/* アイテム */\nclass Item {\n public:\n int w; // アイテムの重量\n int v; // アイテムの価値\n\n Item(int w, int v) : w(w), v(v) {\n }\n};\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n // アイテムリストを作成、2つの属性を含む:重量、価値\n vector<Item> items;\n for (int i = 0; i < wgt.size(); i++) {\n items.push_back(Item(wgt[i], val[i]));\n }\n // 単位価値 item.v / item.w で高い順にソート\n sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n // 貪欲選択をループ\n double res = 0;\n for (auto &item : items) {\n if (item.w <= cap) {\n // 残り容量が十分な場合、アイテム全体をナップサックに入れる\n res += item.v;\n cap -= item.w;\n } else {\n // 残り容量が不十分な場合、アイテムの一部をナップサックに入れる\n res += (double)item.v / item.w * cap;\n // 残り容量がなくなったため、ループを中断\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.java<pre><code>/* アイテム */\nclass Item {\n int w; // アイテムの重量\n int v; // アイテムの価値\n\n public Item(int w, int v) {\n this.w = w;\n this.v = v;\n }\n}\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n // アイテムリストを作成、2つの属性を含む:重量、価値\n Item[] items = new Item[wgt.length];\n for (int i = 0; i < wgt.length; i++) {\n items[i] = new Item(wgt[i], val[i]);\n }\n // 単位価値 item.v / item.w で高い順にソート\n Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n // 貪欲選択をループ\n double res = 0;\n for (Item item : items) {\n if (item.w <= cap) {\n // 残り容量が十分な場合、アイテム全体をナップサックに入れる\n res += item.v;\n cap -= item.w;\n } else {\n // 残り容量が不十分な場合、アイテムの一部をナップサックに入れる\n res += (double) item.v / item.w * cap;\n // 残り容量がなくなったため、ループを中断\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.cs<pre><code>[class]{Item}-[func]{}\n\n[class]{fractional_knapsack}-[func]{FractionalKnapsack}\n</code></pre> fractional_knapsack.go<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractionalKnapsack}\n</code></pre> fractional_knapsack.swift<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractionalKnapsack}\n</code></pre> fractional_knapsack.js<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractionalKnapsack}\n</code></pre> fractional_knapsack.ts<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractionalKnapsack}\n</code></pre> fractional_knapsack.dart<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractionalKnapsack}\n</code></pre> fractional_knapsack.rs<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractional_knapsack}\n</code></pre> fractional_knapsack.c<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractionalKnapsack}\n</code></pre> fractional_knapsack.kt<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractionalKnapsack}\n</code></pre> fractional_knapsack.rb<pre><code>[class]{Item}-[func]{}\n\n[class]{}-[func]{fractional_knapsack}\n</code></pre> <p>ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、時間計算量は \\(O(n)\\) です。ここで \\(n\\) はアイテムの数です。</p> <p><code>Item</code> オブジェクトリストが初期化されるため、空間計算量は \\(O(n)\\) です。</p>","path":["第 15 章 貪欲法","15.2 分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3","level":3,"title":"3. 正しさの証明","text":"<p>背理法を使用します。アイテム \\(x\\) が最高の単位値を持ち、あるアルゴリズムが最大値 <code>res</code> を生成するが、解にアイテム \\(x\\) が含まれていないと仮定します。</p> <p>今、ナップサックから任意のアイテムの単位重量を取り除き、アイテム \\(x\\) の単位重量で置き換えます。アイテム \\(x\\) の単位値が最高であるため、置き換え後の総値は確実に <code>res</code> より大きくなります。これは <code>res</code> が最適解であるという仮定と矛盾し、最適解には必ずアイテム \\(x\\) が含まれることを証明します。</p> <p>この解の他のアイテムについても、上記の矛盾を構築できます。全体的に、**単位値がより大きいアイテムは常により良い選択**であり、貪欲戦略が効果的であることを証明します。</p> <p>下の図に示すように、アイテムの重量と単位値をそれぞれ二次元チャートの横軸と縦軸と見なすと、分数ナップサック問題は「限られた横軸範囲内で囲まれる最大面積を求める」ことに変換できます。この類推は、幾何学的観点から貪欲戦略の効果を理解するのに役立ちます。</p> <p></p> <p> 図 15-6 分数ナップサック問題の幾何学的表現 </p>","path":["第 15 章 貪欲法","15.2 分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1 貪欲アルゴリズム","text":"<p>貪欲アルゴリズムは最適化問題を解決するための一般的なアルゴリズムで、基本的に問題の各意思決定段階で最も良い選択をすること、つまり局所的に最適な決定を貪欲に行い、グローバルに最適な解を見つけることを望みます。貪欲アルゴリズムは簡潔で効率的であり、多くの実用的な問題で広く使用されています。</p> <p>貪欲アルゴリズムと動的プログラミングは、どちらも最適化問題を解決するためによく使用されます。両者は最適部分構造の性質に依存するなど、いくつかの類似点を共有していますが、動作方法が異なります。</p> <ul> <li>動的プログラミングは現在の決定段階ですべての以前の決定を考慮し、過去の部分問題の解を使用して現在の部分問題の解を構築します。</li> <li>貪欲アルゴリズムは過去の決定を考慮せず、代わりに貪欲な選択を続け、問題が解決されるまで問題の範囲を継続的に狭めます。</li> </ul> <p>まず、「完全ナップサック問題」の章で紹介された「コイン交換」の例を通じて貪欲アルゴリズムの動作原理を理解しましょう。すでによく知っていると思います。</p> <p>Question</p> <p>\\(n\\) 種類のコインが与えられ、\\(i\\) 番目の種類のコインの額面は \\(coins[i - 1]\\) で、目標金額は \\(amt\\) です。各種類のコインは無制限に利用可能で、目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は \\(-1\\) を返してください。</p> <p>この問題で採用される貪欲戦略を下の図に示します。目標金額が与えられたとき、**それに最も近く、それを超えないコインを貪欲に選択**し、目標金額が満たされるまでこのステップを繰り返します。</p> <p></p> <p> 図 15-1 コイン交換の貪欲戦略 </p> <p>実装コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py<pre><code>def coin_change_greedy(coins: list[int], amt: int) -> int:\n \"\"\"硬貨交換:貪欲法\"\"\"\n # coins リストがソートされていると仮定\n i = len(coins) - 1\n count = 0\n # 残り金額がなくなるまで貪欲選択をループ\n while amt > 0:\n # 残り金額に最も近く、それより小さい硬貨を見つける\n while i > 0 and coins[i] > amt:\n i -= 1\n # coins[i] を選択\n amt -= coins[i]\n count += 1\n # 実行可能な解が見つからない場合、-1 を返す\n return count if amt == 0 else -1\n</code></pre> coin_change_greedy.cpp<pre><code>/* 硬貨両替:貪欲法 */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n // 硬貨リストが順序付けされていると仮定\n int i = coins.size() - 1;\n int count = 0;\n // 残り金額がなくなるまで貪欲選択をループ\n while (amt > 0) {\n // 残り金額に近く、それ以下の最小硬貨を見つける\n while (i > 0 && coins[i] > amt) {\n i--;\n }\n // coins[i] を選択\n amt -= coins[i];\n count++;\n }\n // 実行可能な解が見つからない場合、-1 を返す\n return amt == 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.java<pre><code>/* 硬貨両替:貪欲法 */\nint coinChangeGreedy(int[] coins, int amt) {\n // 硬貨リストが順序付けされていると仮定\n int i = coins.length - 1;\n int count = 0;\n // 残り金額がなくなるまで貪欲選択をループ\n while (amt > 0) {\n // 残り金額に近く、それ以下の最小硬貨を見つける\n while (i > 0 && coins[i] > amt) {\n i--;\n }\n // coins[i] を選択\n amt -= coins[i];\n count++;\n }\n // 実行可能な解が見つからない場合、-1 を返す\n return amt == 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.cs<pre><code>[class]{coin_change_greedy}-[func]{CoinChangeGreedy}\n</code></pre> coin_change_greedy.go<pre><code>[class]{}-[func]{coinChangeGreedy}\n</code></pre> coin_change_greedy.swift<pre><code>[class]{}-[func]{coinChangeGreedy}\n</code></pre> coin_change_greedy.js<pre><code>[class]{}-[func]{coinChangeGreedy}\n</code></pre> coin_change_greedy.ts<pre><code>[class]{}-[func]{coinChangeGreedy}\n</code></pre> coin_change_greedy.dart<pre><code>[class]{}-[func]{coinChangeGreedy}\n</code></pre> coin_change_greedy.rs<pre><code>[class]{}-[func]{coin_change_greedy}\n</code></pre> coin_change_greedy.c<pre><code>[class]{}-[func]{coinChangeGreedy}\n</code></pre> coin_change_greedy.kt<pre><code>[class]{}-[func]{coinChangeGreedy}\n</code></pre> coin_change_greedy.rb<pre><code>[class]{}-[func]{coin_change_greedy}\n</code></pre> <p>感嘆するかもしれません:なんて簡潔なんだ!貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。</p>","path":["第 15 章 貪欲法","15.1 貪欲アルゴリズム"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511","level":2,"title":"15.1.1 貪欲アルゴリズムの利点と制限","text":"<p>貪欲アルゴリズムは直接的で実装が簡単であるだけでなく、通常非常に効率的でもあります。上記のコードで、最小のコイン額面を \\(\\min(coins)\\) とすると、貪欲な選択ループは最大 \\(amt / \\min(coins)\\) 回実行され、時間計算量は \\(O(amt / \\min(coins))\\) になります。これは動的プログラミング解法の時間計算量 \\(O(n \\times amt)\\) よりも一桁小さいです。</p> <p>しかし、一部のコイン額面の組み合わせでは、貪欲アルゴリズムは最適解を見つけることができません。下の図は2つの例を示しています。</p> <ul> <li>正の例 \\(coins = [1, 5, 10, 20, 50, 100]\\):このコインの組み合わせでは、任意の \\(amt\\) に対して、貪欲アルゴリズムは最適解を見つけることができます。</li> <li>負の例 \\(coins = [1, 20, 50]\\):\\(amt = 60\\) とすると、貪欲アルゴリズムは組み合わせ \\(50 + 1 \\times 10\\) しか見つけられず、合計11枚のコインですが、動的プログラミングは最適解 \\(20 + 20 + 20\\) を見つけることができ、3枚のコインのみが必要です。</li> <li>負の例 \\(coins = [1, 49, 50]\\):\\(amt = 98\\) とすると、貪欲アルゴリズムは組み合わせ \\(50 + 1 \\times 48\\) しか見つけられず、合計49枚のコインですが、動的プログラミングは最適解 \\(49 + 49\\) を見つけることができ、2枚のコインのみが必要です。</li> </ul> <p></p> <p> 図 15-2 貪欲アルゴリズムが最適解を見つけられない例 </p> <p>これは、コイン交換問題において、貪欲アルゴリズムがグローバルに最適な解を見つけることを保証できず、非常に悪い解を見つける可能性があることを意味します。動的プログラミングの方が適しています。</p> <p>一般的に、貪欲アルゴリズムの適用性は2つのカテゴリに分類されます。</p> <ol> <li>最適解を見つけることが保証される:これらの場合、貪欲アルゴリズムはしばしば最良の選択で、バックトラッキングや動的プログラミングよりも効率的である傾向があります。</li> <li>準最適解を見つけることができる:貪欲アルゴリズムはここでも適用可能です。多くの複雑な問題では、グローバル最適解を見つけることは非常に困難であり、高効率の準最適解を見つけることも非常に価値があります。</li> </ol>","path":["第 15 章 貪欲法","15.1 貪欲アルゴリズム"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2 貪欲アルゴリズムの特徴","text":"<p>それでは、どのような問題が貪欲アルゴリズムで解決するのに適しているのでしょうか?言い換えれば、どのような条件下で貪欲アルゴリズムは最適解を見つけることを保証できるのでしょうか?</p> <p>動的プログラミングと比較して、貪欲アルゴリズムはより厳しい使用条件を持ち、主に問題の2つの性質に焦点を当てています。</p> <ul> <li>貪欲選択性:局所的に最適な選択が常にグローバルに最適な解に導くことができる場合のみ、貪欲アルゴリズムは最適解を得ることを保証できます。</li> <li>最適部分構造:元の問題の最適解はその部分問題の最適解を含みます。</li> </ul> <p>最適部分構造は「動的プログラミング」の章ですでに紹介されているため、ここではこれ以上議論しません。一部の問題には明らかな最適部分構造がありませんが、それでも貪欲アルゴリズムを使用して解決できることに注意することが重要です。</p> <p>主に貪欲選択性を決定する方法を探索します。その記述は単純に見えますが、実際には、多くの問題の貪欲選択性を証明することは容易ではありません。</p> <p>例えば、コイン交換問題では、貪欲選択性を反証するために反例を簡単に挙げることができますが、それを証明することははるかに困難です。**コインの組み合わせが貪欲アルゴリズムを使用して解決できるためには、どのような条件を満たす必要があるか**と尋ねられた場合、厳密な数学的証明を提供することが困難であるため、しばしば直感や例に頼って曖昧な答えを提供しなければなりません。</p> <p>Quote</p> <p>ある論文では、コインの組み合わせが任意の金額に対して貪欲アルゴリズムを使用して最適解を見つけることができるかどうかを判定するための時間計算量 \\(O(n^3)\\) のアルゴリズムが提示されています。</p> <p>Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.</p>","path":["第 15 章 貪欲法","15.1 貪欲アルゴリズム"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513","level":2,"title":"15.1.3 貪欲アルゴリズムによる問題解決のステップ","text":"<p>貪欲問題の問題解決プロセスは、一般的に以下の3つのステップに分けることができます。</p> <ol> <li>問題分析:問題の特徴を整理し理解する。状態定義、最適化目標、制約などを含みます。このステップはバックトラッキングや動的プログラミングでも関与します。</li> <li>貪欲戦略の決定:各ステップで貪欲な選択をする方法を決定する。この戦略は各ステップで問題の規模を縮小し、最終的に問題全体を解決できます。</li> <li>正確性の証明:通常、問題が貪欲選択性と最適部分構造の両方を持つことを証明する必要があります。このステップには、帰納法や背理法などの数学的証明が必要な場合があります。</li> </ol> <p>貪欲戦略の決定は問題解決の核心ステップですが、実装は容易ではない場合があります。主な理由は以下の通りです。</p> <ul> <li>異なる問題間で貪欲戦略は大きく異なる。多くの問題では、貪欲戦略はかなり直接的で、一般的な思考と試行を通じて思いつくことができます。しかし、一部の複雑な問題では、貪欲戦略は非常に見つけにくく、これは個人の問題解決経験とアルゴリズム能力の真のテストです。</li> <li>一部の貪欲戦略は非常に誤解を招く。自信を持って貪欲戦略を設計し、コードを書いてテストに提出したとき、一部のテストケースが通らない可能性が高いです。これは設計された貪欲戦略が「部分的に正しい」だけであるためで、上記のコイン交換の例で説明した通りです。</li> </ul> <p>正確性を確保するために、貪欲戦略に対して厳密な数学的証明を提供すべきで、通常は背理法や数学的帰納法を含みます。</p> <p>しかし、正確性を証明することは容易な作業ではない場合があります。途方に暮れた場合、通常はテストケースに基づいてコードをデバッグし、貪欲戦略を段階的に修正し検証することを選択します。</p>","path":["第 15 章 貪欲法","15.1 貪欲アルゴリズム"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4 貪欲アルゴリズムで解決される典型的な問題","text":"<p>貪欲アルゴリズムは、貪欲選択と最適部分構造の性質を満たす最適化問題によく適用されます。以下は典型的な貪欲アルゴリズム問題のいくつかです。</p> <ul> <li>コイン交換問題:一部のコインの組み合わせでは、貪欲アルゴリズムは常に最適解を提供します。</li> <li>区間スケジューリング問題:いくつかのタスクがあり、それぞれが一定期間にわたって行われるとします。目標はできるだけ多くのタスクを完了することです。常に最も早く終了するタスクを選択すると、貪欲アルゴリズムは最適解を達成できます。</li> <li>分数ナップサック問題:アイテムのセットと運搬容量が与えられ、目標は総重量が運搬容量を超えず、総価値が最大化されるようなアイテムのセットを選択することです。常に最高の価値対重量比(価値/重量)のアイテムを選択すると、貪欲アルゴリズムは一部のケースで最適解を達成できます。</li> <li>株式取引問題:株価の履歴のセットが与えられ、複数回の取引を行うことができますが、すでに株式を所有している場合は売却後でないと再度購入できません。目標は最大利益を達成することです。</li> <li>ハフマン符号化:ハフマン符号化は無損失データ圧縮に使用される貪欲アルゴリズムです。ハフマン木を構築することにより、常に最低頻度の2つのノードを統合し、最小重み付きパス長(符号化長)のハフマン木を生成します。</li> <li>ダイクストラのアルゴリズム:これは与えられたソース頂点から他のすべての頂点への最短経路問題を解決するための貪欲アルゴリズムです。</li> </ul>","path":["第 15 章 貪欲法","15.1 貪欲アルゴリズム"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3 最大容量問題","text":"<p>Question</p> <p>配列 \\(ht\\) を入力します。各要素は垂直仕切りの高さを表します。配列内の任意の2つの仕切りと、それらの間のスペースによってコンテナを形成できます。</p> <p>コンテナの容量は高さと幅の積(面積)で、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。</p> <p>コンテナの容量を最大化する2つの仕切りを配列から選択し、この最大容量を返してください。例を下の図に示します。</p> <p></p> <p> 図 15-7 最大容量問題の例データ </p> <p>コンテナは任意の2つの仕切りによって形成されるため、この問題の状態は2つの仕切りのインデックスで表現され、\\([i, j]\\) と表記されます。</p> <p>問題の記述によれば、容量は高さと幅の積に等しく、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。容量 \\(cap[i, j]\\) の式は:</p> \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\] <p>配列の長さを \\(n\\) と仮定すると、2つの仕切りの組み合わせ数(状態の総数)は \\(C_n^2 = \\frac{n(n - 1)}{2}\\) です。最も直接的なアプローチは**すべての可能な状態を列挙する**ことで、時間計算量は \\(O(n^2)\\) になります。</p>","path":["第 15 章 貪欲法","15.3 最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1","level":3,"title":"1. 貪欲戦略の決定","text":"<p>この問題にはより効率的な解法があります。下の図に示すように、インデックス \\(i < j\\) かつ高さ \\(ht[i] < ht[j]\\) の状態 \\([i, j]\\) を選択します。つまり、\\(i\\) は短い仕切り、\\(j\\) は高い仕切りです。</p> <p></p> <p> 図 15-8 初期状態 </p> <p>下の図に示すように、高い仕切り \\(j\\) を短い仕切り \\(i\\) に近づけて移動すると、容量は確実に減少します。</p> <p>これは、高い仕切り \\(j\\) を移動すると、幅 \\(j-i\\) が確実に減少するためです。高さは短い仕切りによって決定されるため、高さは同じまま(\\(i\\) が短い仕切りのまま)か減少(移動した \\(j\\) が短い仕切りになる)しかありません。</p> <p></p> <p> 図 15-9 高い仕切りを内側に移動した後の状態 </p> <p>逆に、短い仕切り \\(i\\) を内側に移動することによってのみ容量を増加させることが可能です。幅は確実に減少しますが、高さが増加する可能性があります(移動した短い仕切り \\(i\\) が高くなる場合)。例えば、下の図では、短い仕切りを移動した後に面積が増加しています。</p> <p></p> <p> 図 15-10 短い仕切りを内側に移動した後の状態 </p> <p>これにより、この問題の貪欲戦略が導かれます:コンテナの両端に2つのポインタを初期化し、各ラウンドで短い仕切りに対応するポインタを内側に移動し、2つのポインタが出会うまで続けます。</p> <p>下の図は貪欲戦略の実行を示しています。</p> <ol> <li>最初に、ポインタ \\(i\\) と \\(j\\) が配列の両端に配置されます。</li> <li>現在の状態の容量 \\(cap[i, j]\\) を計算し、最大容量を更新します。</li> <li>仕切り \\(i\\) と \\(j\\) の高さを比較し、短い仕切りを1ステップ内側に移動します。</li> <li>\\(i\\) と \\(j\\) が出会うまでステップ <code>2.</code> と <code>3.</code> を繰り返します。</li> </ol> <1><2><3><4><5><6><7><8><9> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 15-11 最大容量問題の貪欲プロセス </p>","path":["第 15 章 貪欲法","15.3 最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2","level":3,"title":"2. 実装","text":"<p>コードは最大 \\(n\\) 回ループするため、時間計算量は \\(O(n)\\) です。</p> <p>変数 \\(i\\)、\\(j\\)、\\(res\\) は一定量の追加スペースを使用するため、空間計算量は \\(O(1)\\) です。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py<pre><code>def max_capacity(ht: list[int]) -> int:\n \"\"\"最大容量:貪欲法\"\"\"\n # i、j を初期化、配列の両端で分割させる\n i, j = 0, len(ht) - 1\n # 初期最大容量は 0\n res = 0\n # 2 つの板が出会うまで貪欲選択をループ\n while i < j:\n # 最大容量を更新\n cap = min(ht[i], ht[j]) * (j - i)\n res = max(res, cap)\n # 短い板を内側に移動\n if ht[i] < ht[j]:\n i += 1\n else:\n j -= 1\n return res\n</code></pre> max_capacity.cpp<pre><code>/* 最大容量:貪欲法 */\nint maxCapacity(vector<int> &ht) {\n // i、j を初期化し、配列の両端で分割させる\n int i = 0, j = ht.size() - 1;\n // 初期最大容量は 0\n int res = 0;\n // 2つの板が出会うまで貪欲選択をループ\n while (i < j) {\n // 最大容量を更新\n int cap = min(ht[i], ht[j]) * (j - i);\n res = max(res, cap);\n // より短い板を内側に移動\n if (ht[i] < ht[j]) {\n i++;\n } else {\n j--;\n }\n }\n return res;\n}\n</code></pre> max_capacity.java<pre><code>/* 最大容量:貪欲法 */\nint maxCapacity(int[] ht) {\n // i、j を初期化し、配列の両端で分割させる\n int i = 0, j = ht.length - 1;\n // 初期最大容量は 0\n int res = 0;\n // 2つの板が出会うまで貪欲選択をループ\n while (i < j) {\n // 最大容量を更新\n int cap = Math.min(ht[i], ht[j]) * (j - i);\n res = Math.max(res, cap);\n // より短い板を内側に移動\n if (ht[i] < ht[j]) {\n i++;\n } else {\n j--;\n }\n }\n return res;\n}\n</code></pre> max_capacity.cs<pre><code>[class]{max_capacity}-[func]{MaxCapacity}\n</code></pre> max_capacity.go<pre><code>[class]{}-[func]{maxCapacity}\n</code></pre> max_capacity.swift<pre><code>[class]{}-[func]{maxCapacity}\n</code></pre> max_capacity.js<pre><code>[class]{}-[func]{maxCapacity}\n</code></pre> max_capacity.ts<pre><code>[class]{}-[func]{maxCapacity}\n</code></pre> max_capacity.dart<pre><code>[class]{}-[func]{maxCapacity}\n</code></pre> max_capacity.rs<pre><code>[class]{}-[func]{max_capacity}\n</code></pre> max_capacity.c<pre><code>[class]{}-[func]{maxCapacity}\n</code></pre> max_capacity.kt<pre><code>[class]{}-[func]{maxCapacity}\n</code></pre> max_capacity.rb<pre><code>[class]{}-[func]{max_capacity}\n</code></pre>","path":["第 15 章 貪欲法","15.3 最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3","level":3,"title":"3. 正しさの証明","text":"<p>貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。</p> <p>例えば、\\(i\\) が短い仕切りで \\(j\\) が高い仕切りである状態 \\(cap[i, j]\\) の下で、短い仕切り \\(i\\) を貪欲に1ステップ内側に移動すると、下の図に示す「スキップされた」状態につながります。これは、これらの状態の容量を後で検証できないことを意味します。</p> \\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\] <p></p> <p> 図 15-12 短い仕切りの移動によってスキップされる状態 </p> <p>観察すると、これらのスキップされた状態は実際には高い仕切り \\(j\\) が内側に移動したすべての状態**です。高い仕切りを内側に移動すると容量が確実に減少することをすでに証明しました。したがって、スキップされた状態は最適解である可能性がなく、**それらをスキップしても最適解を見逃すことはありません。</p> <p>分析により、短い仕切りを移動する操作は「安全」であり、貪欲戦略が効果的であることが示されます。</p>","path":["第 15 章 貪欲法","15.3 最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4 最大積切断問題","text":"<p>Question</p> <p>正の整数 \\(n\\) が与えられたとき、それを合計が \\(n\\) になる少なくとも2つの正の整数に分割し、これらの整数の最大積を求めてください。下の図に示すとおりです。</p> <p></p> <p> 図 15-13 最大積切断問題の定義 </p> <p>\\(n\\) を \\(m\\) 個の整数因子に分割すると仮定し、\\(i\\) 番目の因子を \\(n_i\\) と表記すると、</p> \\[ n = \\sum_{i=1}^{m}n_i \\] <p>この問題の目標は、すべての整数因子の最大積を見つけることです。すなわち、</p> \\[ \\max(\\prod_{i=1}^{m}n_i) \\] <p>考慮すべき点:分割数 \\(m\\) はどの程度大きくすべきか、各 \\(n_i\\) は何であるべきか?</p>","path":["第 15 章 貪欲法","15.4 最大積切断問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1","level":3,"title":"1. 貪欲戦略の決定","text":"<p>経験的に、2つの整数の積は多くの場合その和よりも大きくなります。\\(n\\) から因子 \\(2\\) を分割すると仮定すると、その積は \\(2(n-2)\\) です。この積を \\(n\\) と比較します:</p> \\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\] <p>下の図に示すように、\\(n \\geq 4\\) のとき、\\(2\\) を分割すると積が増加します。これは4以上の整数を分割すべきであることを示しています。</p> <p>貪欲戦略1:分割スキームが \\(\\geq 4\\) の因子を含む場合、それらはさらに分割されるべきです。最終的な分割は因子 \\(1\\)、\\(2\\)、\\(3\\) のみを含むべきです。</p> <p></p> <p> 図 15-14 分割による積の増加 </p> <p>次に、どの因子が最適かを考慮します。因子 \\(1\\)、\\(2\\)、\\(3\\) の中で、明らかに \\(1\\) が最悪です。\\(1 \\times (n-1) < n\\) が常に成り立つため、\\(1\\) を分割すると実際に積が減少します。</p> <p>下の図に示すように、\\(n = 6\\) のとき、\\(3 \\times 3 > 2 \\times 2 \\times 2\\) です。これは \\(3\\) を分割する方が \\(2\\) を分割するよりも良いことを意味します。</p> <p>貪欲戦略2:分割スキームには最大で2つの \\(2\\) があるべきです。3つの \\(2\\) は常に2つの \\(3\\) に置き換えてより高い積を得ることができるからです。</p> <p></p> <p> 図 15-15 最適な分割因子 </p> <p>上記から、以下の貪欲戦略を導出できます。</p> <ol> <li>入力整数 \\(n\\) について、余りが \\(0\\)、\\(1\\)、または \\(2\\) になるまで因子 \\(3\\) を継続的に分割します。</li> <li>余りが \\(0\\) の場合、\\(n\\) が \\(3\\) の倍数であることを意味するため、それ以上の行動は取りません。</li> <li>余りが \\(2\\) の場合、さらに分割を続けず、そのまま保持します。</li> <li>余りが \\(1\\) の場合、\\(2 \\times 2 > 1 \\times 3\\) であるため、最後の \\(3\\) を \\(2\\) に置き換えるべきです。</li> </ol>","path":["第 15 章 貪欲法","15.4 最大積切断問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2","level":3,"title":"2. コード実装","text":"<p>下の図に示すように、整数を分割するためにループを使用する必要はなく、床除算演算を使用して \\(3\\) の数 \\(a\\) を取得し、剰余演算を使用して余り \\(b\\) を取得できます。したがって:</p> \\[ n = 3a + b \\] <p>\\(n \\leq 3\\) の境界ケースでは、\\(1\\) を分割する必要があり、積は \\(1 \\times (n - 1)\\) であることに注意してください。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py<pre><code>def max_product_cutting(n: int) -> int:\n \"\"\"切断の最大積:貪欲法\"\"\"\n # n <= 3 の場合、1 を切り出す必要がある\n if n <= 3:\n return 1 * (n - 1)\n # 貪欲的に 3 を切り出す、a は 3 の個数、b は余り\n a, b = n // 3, n % 3\n if b == 1:\n # 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換\n return int(math.pow(3, a - 1)) * 2 * 2\n if b == 2:\n # 余りが 2 の場合、何もしない\n return int(math.pow(3, a)) * 2\n # 余りが 0 の場合、何もしない\n return int(math.pow(3, a))\n</code></pre> max_product_cutting.cpp<pre><code>/* 最大積切断:貪欲法 */\nint maxProductCutting(int n) {\n // n <= 3 の場合、1 を切り出す必要がある\n if (n <= 3) {\n return 1 * (n - 1);\n }\n // 貪欲に 3 を切り出す。a は 3 の個数、b は余り\n int a = n / 3;\n int b = n % 3;\n if (b == 1) {\n // 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換\n return (int)pow(3, a - 1) * 2 * 2;\n }\n if (b == 2) {\n // 余りが 2 の場合、何もしない\n return (int)pow(3, a) * 2;\n }\n // 余りが 0 の場合、何もしない\n return (int)pow(3, a);\n}\n</code></pre> max_product_cutting.java<pre><code>/* 最大積切断:貪欲法 */\nint maxProductCutting(int n) {\n // n <= 3 の場合、1 を切り出す必要がある\n if (n <= 3) {\n return 1 * (n - 1);\n }\n // 貪欲に 3 を切り出す。a は 3 の個数、b は余り\n int a = n / 3;\n int b = n % 3;\n if (b == 1) {\n // 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換\n return (int) Math.pow(3, a - 1) * 2 * 2;\n }\n if (b == 2) {\n // 余りが 2 の場合、何もしない\n return (int) Math.pow(3, a) * 2;\n }\n // 余りが 0 の場合、何もしない\n return (int) Math.pow(3, a);\n}\n</code></pre> max_product_cutting.cs<pre><code>[class]{max_product_cutting}-[func]{MaxProductCutting}\n</code></pre> max_product_cutting.go<pre><code>[class]{}-[func]{maxProductCutting}\n</code></pre> max_product_cutting.swift<pre><code>[class]{}-[func]{maxProductCutting}\n</code></pre> max_product_cutting.js<pre><code>[class]{}-[func]{maxProductCutting}\n</code></pre> max_product_cutting.ts<pre><code>[class]{}-[func]{maxProductCutting}\n</code></pre> max_product_cutting.dart<pre><code>[class]{}-[func]{maxProductCutting}\n</code></pre> max_product_cutting.rs<pre><code>[class]{}-[func]{max_product_cutting}\n</code></pre> max_product_cutting.c<pre><code>[class]{}-[func]{maxProductCutting}\n</code></pre> max_product_cutting.kt<pre><code>[class]{}-[func]{maxProductCutting}\n</code></pre> max_product_cutting.rb<pre><code>[class]{}-[func]{max_product_cutting}\n</code></pre> <p></p> <p> 図 15-16 切断後の最大積の計算方法 </p> <p>時間計算量はプログラミング言語のべき乗演算の実装に依存します。Pythonでは、よく使用されるべき乗計算関数は3種類あります:</p> <ul> <li>演算子 <code>**</code> と関数 <code>pow()</code> の両方の時間計算量は \\(O(\\log a)\\) です。</li> <li><code>math.pow()</code> 関数は内部でC言語ライブラリの <code>pow()</code> 関数を呼び出し、浮動小数点べき乗を実行し、時間計算量は \\(O(1)\\) です。</li> </ul> <p>変数 \\(a\\) と \\(b\\) は一定サイズの追加スペースを使用するため、空間計算量は \\(O(1)\\) です。</p>","path":["第 15 章 貪欲法","15.4 最大積切断問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3","level":3,"title":"3. 正しさの証明","text":"<p>背理法を使用し、\\(n \\geq 3\\) のケースのみを分析します。</p> <ol> <li>すべての因子 \\(\\leq 3\\):最適分割スキームが因子 \\(x \\geq 4\\) を含むと仮定すると、それを確実に \\(2(x-2)\\) にさらに分割でき、より大きな積を得られます。これは仮定と矛盾します。</li> <li>分割スキームに \\(1\\) が含まれない:最適分割スキームが因子 \\(1\\) を含むと仮定すると、それを確実に別の因子と結合してより大きな積を得られます。これは仮定と矛盾します。</li> <li>分割スキームには最大で2つの \\(2\\) が含まれる:最適分割スキームが3つの \\(2\\) を含むと仮定すると、それらを確実に2つの \\(3\\) に置き換えて、より高い積を達成できます。これは仮定と矛盾します。</li> </ol>","path":["第 15 章 貪欲法","15.4 最大積切断問題"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5 まとめ","text":"<ul> <li>貪欲アルゴリズムは最適化問題を解決するためによく使用され、原理は各決定段階で局所的に最適な決定を行い、グローバルに最適な解を達成することです。</li> <li>貪欲アルゴリズムは貪欲な選択を次々と反復的に行い、各ラウンドで問題をより小さな部分問題に変換し、問題が解決されるまで続けます。</li> <li>貪欲アルゴリズムは実装が簡単なだけでなく、問題解決効率も高いです。動的プログラミングと比較して、貪欲アルゴリズムは一般的により低い時間計算量を持ちます。</li> <li>コイン交換問題において、貪欲アルゴリズムは特定のコインの組み合わせに対して最適解を保証できますが、他の組み合わせでは貪欲アルゴリズムが非常に悪い解を見つける可能性があります。</li> <li>貪欲アルゴリズム解法に適した問題は2つの主要な性質を持ちます:貪欲選択性と最適部分構造。貪欲選択性は貪欲戦略の効果を表します。</li> <li>一部の複雑な問題では、貪欲選択性を証明することは簡単ではありません。逆に、無効性を証明することはしばしばより容易で、コイン交換問題などがその例です。</li> <li>貪欲問題の解決は主に3つのステップから構成されます:問題分析、貪欲戦略の決定、正しさの証明。このうち、貪欲戦略の決定が重要なステップであり、正しさの証明がしばしば挑戦となります。</li> <li>分数ナップサック問題は0-1ナップサック問題に基づいてアイテムの一部の選択を可能にし、したがって貪欲アルゴリズムを使用して解決できます。貪欲戦略の正しさは背理法によって証明できます。</li> <li>最大容量問題は全探索法で解決でき、時間計算量は \\(O(n^2)\\) です。貪欲戦略を設計することで、各ラウンドで短い板を内側に移動し、時間計算量を \\(O(n)\\) に最適化します。</li> <li>切断後の最大積問題において、2つの貪欲戦略を導出します:\\(\\geq 4\\) の整数は継続的に切断されるべきで、最適な切断因子は \\(3\\) です。コードにはべき乗演算が含まれ、時間計算量はべき乗演算の実装方法に依存し、一般的に \\(O(1)\\) または \\(O(\\log n)\\) です。</li> </ul>","path":["第 15 章 貪欲法","15.5 まとめ"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"第 6 章 ハッシュ表","text":"<p>Abstract</p> <p>コンピューティングの世界において、ハッシュ表は賢い司書のようなものです。</p> <p>インデックス番号の計算方法を理解し、目的の本を迅速に取得することを可能にします。</p>","path":["第 6 章 ハッシュ表"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"章の内容","text":"<ul> <li>6.1 ハッシュ表</li> <li>6.2 ハッシュ衝突</li> <li>6.3 ハッシュアルゴリズム</li> <li>6.4 まとめ</li> </ul>","path":["第 6 章 ハッシュ表"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3 ハッシュアルゴリズム","text":"<p>前の2つの節では、ハッシュ表の動作原理とハッシュ衝突を処理する方法を紹介しました。しかし、オープンアドレス法と連鎖法はどちらも**衝突が発生した際にハッシュ表が正常に機能することのみを保証でき、ハッシュ衝突の発生頻度を減らすことはできません**。</p> <p>ハッシュ衝突があまりにも頻繁に発生すると、ハッシュ表の性能は劇的に悪化します。下図に示すように、連鎖法ハッシュ表では、理想的なケースではキー値ペアがバケット間に均等に分散され、最適なクエリ効率を実現します。最悪のケースでは、すべてのキー値ペアが同じバケットに格納され、時間計算量が\\(O(n)\\)に悪化します。</p> <p></p> <p> 図 6-8 ハッシュ衝突の理想的および最悪のケース </p> <p>キー値ペアの分布はハッシュ関数によって決定されます。ハッシュ関数の計算ステップを思い出すと、まずハッシュ値を計算し、次に配列長で剰余を取ります:</p> <pre><code>index = hash(key) % capacity\n</code></pre> <p>上記の式を観察すると、ハッシュ表の容量<code>capacity</code>が固定されている場合、ハッシュアルゴリズム<code>hash()</code>が出力値を決定し、それによってハッシュ表におけるキー値ペアの分布を決定します。</p> <p>これは、ハッシュ衝突の確率を減らすために、ハッシュアルゴリズム<code>hash()</code>の設計に焦点を当てるべきであることを意味します。</p>","path":["第 6 章 ハッシュ表","6.3 ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631","level":2,"title":"6.3.1 ハッシュアルゴリズムの目標","text":"<p>「高速で安定した」ハッシュ表データ構造を実現するために、ハッシュアルゴリズムは以下の特性を持つべきです:</p> <ul> <li>決定性: 同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成するべきです。そうでなければハッシュ表は信頼できません。</li> <li>高効率: ハッシュ値を計算するプロセスは十分に高速である必要があります。計算オーバーヘッドが小さいほど、ハッシュ表はより実用的になります。</li> <li>均等分散: ハッシュアルゴリズムはキー値ペアがハッシュ表に均等に分散されることを保証するべきです。分散が均等であるほど、ハッシュ衝突の確率は低くなります。</li> </ul> <p>実際、ハッシュアルゴリズムはハッシュ表の実装だけでなく、他の分野でも広く応用されています。</p> <ul> <li>パスワード保存: ユーザーパスワードのセキュリティを保護するために、システムは通常平文パスワードを保存せず、パスワードのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力のハッシュ値を計算し、保存されているハッシュ値と比較します。一致すれば、パスワードは正しいと見なされます。</li> <li>データ整合性チェック: データ送信者はデータのハッシュ値を計算して一緒に送信できます。受信者は受信したデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。一致すれば、データは完全であると見なされます。</li> </ul> <p>暗号化アプリケーションでは、ハッシュ値から元のパスワードを推測するなどの逆行分析を防ぐために、ハッシュアルゴリズムはより高いレベルのセキュリティ機能が必要です。</p> <ul> <li>一方向性: ハッシュ値から入力データに関する情報を推測することは不可能であるべきです。</li> <li>衝突耐性: 同じハッシュ値を生成する2つの異なる入力を見つけることは極めて困難であるべきです。</li> <li>雪崩効果: 入力の小さな変更は、出力に大きく予測不可能な変化をもたらすべきです。</li> </ul> <p>**「均等分散」と「衝突耐性」は2つの別々の概念**であることに注意してください。均等分散を満たしても、必ずしも衝突耐性があるとは限りません。例えば、ランダムな入力<code>key</code>の下で、ハッシュ関数<code>key % 100</code>は均等に分散された出力を生成できます。しかし、このハッシュアルゴリズムは過度にシンプルで、下二桁が同じすべての<code>key</code>は同じ出力を持つため、ハッシュ値から使用可能な<code>key</code>を簡単に推測でき、パスワードを破ることができます。</p>","path":["第 6 章 ハッシュ表","6.3 ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632","level":2,"title":"6.3.2 ハッシュアルゴリズムの設計","text":"<p>ハッシュアルゴリズムの設計は多くの要因を考慮する必要がある複雑な問題です。しかし、要求が少ない一部のシナリオでは、いくつかの簡単なハッシュアルゴリズムを設計することもできます。</p> <ul> <li>加算ハッシュ: 入力の各文字のASCIIコードを合計し、合計をハッシュ値として使用します。</li> <li>乗算ハッシュ: 乗算の非相関性を利用し、各ラウンドで定数を乗算し、各文字のASCIIコードをハッシュ値に累積します。</li> <li>XORハッシュ: 入力データの各要素をXORすることでハッシュ値を累積します。</li> <li>回転ハッシュ: 各文字のASCIIコードをハッシュ値に累積し、各累積前にハッシュ値に回転操作を実行します。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py<pre><code>def add_hash(key: str) -> int:\n \"\"\"加法ハッシュ\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash += ord(c)\n return hash % modulus\n\ndef mul_hash(key: str) -> int:\n \"\"\"乗法ハッシュ\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash = 31 * hash + ord(c)\n return hash % modulus\n\ndef xor_hash(key: str) -> int:\n \"\"\"XORハッシュ\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash ^= ord(c)\n return hash % modulus\n\ndef rot_hash(key: str) -> int:\n \"\"\"回転ハッシュ\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n return hash % modulus\n</code></pre> simple_hash.cpp<pre><code>/* 加算ハッシュ */\nint addHash(string key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash = (hash + (int)c) % MODULUS;\n }\n return (int)hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(string key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash = (31 * hash + (int)c) % MODULUS;\n }\n return (int)hash;\n}\n\n/* XORハッシュ */\nint xorHash(string key) {\n int hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash ^= (int)c;\n }\n return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(string key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n }\n return (int)hash;\n}\n</code></pre> simple_hash.java<pre><code>/* 加算ハッシュ */\nint addHash(String key) {\n long hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash = (hash + (int) c) % MODULUS;\n }\n return (int) hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(String key) {\n long hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash = (31 * hash + (int) c) % MODULUS;\n }\n return (int) hash;\n}\n\n/* XORハッシュ */\nint xorHash(String key) {\n int hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash ^= (int) c;\n }\n return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(String key) {\n long hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n }\n return (int) hash;\n}\n</code></pre> simple_hash.cs<pre><code>[class]{simple_hash}-[func]{AddHash}\n\n[class]{simple_hash}-[func]{MulHash}\n\n[class]{simple_hash}-[func]{XorHash}\n\n[class]{simple_hash}-[func]{RotHash}\n</code></pre> simple_hash.go<pre><code>[class]{}-[func]{addHash}\n\n[class]{}-[func]{mulHash}\n\n[class]{}-[func]{xorHash}\n\n[class]{}-[func]{rotHash}\n</code></pre> simple_hash.swift<pre><code>[class]{}-[func]{addHash}\n\n[class]{}-[func]{mulHash}\n\n[class]{}-[func]{xorHash}\n\n[class]{}-[func]{rotHash}\n</code></pre> simple_hash.js<pre><code>[class]{}-[func]{addHash}\n\n[class]{}-[func]{mulHash}\n\n[class]{}-[func]{xorHash}\n\n[class]{}-[func]{rotHash}\n</code></pre> simple_hash.ts<pre><code>[class]{}-[func]{addHash}\n\n[class]{}-[func]{mulHash}\n\n[class]{}-[func]{xorHash}\n\n[class]{}-[func]{rotHash}\n</code></pre> simple_hash.dart<pre><code>[class]{}-[func]{addHash}\n\n[class]{}-[func]{mulHash}\n\n[class]{}-[func]{xorHash}\n\n[class]{}-[func]{rotHash}\n</code></pre> simple_hash.rs<pre><code>[class]{}-[func]{add_hash}\n\n[class]{}-[func]{mul_hash}\n\n[class]{}-[func]{xor_hash}\n\n[class]{}-[func]{rot_hash}\n</code></pre> simple_hash.c<pre><code>[class]{}-[func]{addHash}\n\n[class]{}-[func]{mulHash}\n\n[class]{}-[func]{xorHash}\n\n[class]{}-[func]{rotHash}\n</code></pre> simple_hash.kt<pre><code>[class]{}-[func]{addHash}\n\n[class]{}-[func]{mulHash}\n\n[class]{}-[func]{xorHash}\n\n[class]{}-[func]{rotHash}\n</code></pre> simple_hash.rb<pre><code>[class]{}-[func]{add_hash}\n\n[class]{}-[func]{mul_hash}\n\n[class]{}-[func]{xor_hash}\n\n[class]{}-[func]{rot_hash}\n</code></pre> <p>各ハッシュアルゴリズムの最後のステップが大きな素数\\(1000000007\\)の剰余を取ることで、ハッシュ値が適切な範囲内にあることを保証していることが観察されます。なぜ素数の剰余を取ることが強調されるのか、または合成数の剰余を取ることの欠点は何かを考える価値があります。これは興味深い質問です。</p> <p>結論として:大きな素数を剰余として使用することで、ハッシュ値の均等分散を最大化できます。素数は他の数と共通因子を持たないため、剰余演算によって引き起こされる周期的パターンを減らし、ハッシュ衝突を回避できます。</p> <p>例えば、合成数\\(9\\)を剰余として選択するとします。これは\\(3\\)で割り切れるため、\\(3\\)で割り切れるすべての<code>key</code>はハッシュ値\\(0\\)、\\(3\\)、\\(6\\)にマッピングされます。</p> \\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\] <p>入力<code>key</code>がたまたまこの種の等差数列分布を持つ場合、ハッシュ値がクラスターし、ハッシュ衝突を悪化させます。今度は<code>modulus</code>を素数\\(13\\)に置き換えるとします。<code>key</code>と<code>modulus</code>の間に共通因子がないため、出力ハッシュ値の均等性が大幅に改善されます。</p> \\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\] <p><code>key</code>がランダムで均等に分散されることが保証されている場合、剰余として素数または合成数を選択しても、両方とも均等に分散されたハッシュ値を生成できることは注目に値します。しかし、<code>key</code>の分布にある種の周期性がある場合、合成数の剰余はクラスタリングを引き起こしやすくなります。</p> <p>要約すると、通常は素数を剰余として選択し、この素数は周期的パターンを可能な限り排除し、ハッシュアルゴリズムの堅牢性を向上させるために十分大きくある必要があります。</p>","path":["第 6 章 ハッシュ表","6.3 ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633","level":2,"title":"6.3.3 一般的なハッシュアルゴリズム","text":"<p>上記で言及した簡単なハッシュアルゴリズムはかなり「脆弱」で、ハッシュアルゴリズムの設計目標から程遠いことは難しくありません。例えば、加算とXORは交換法則に従うため、加算ハッシュとXORハッシュは同じ内容だが順序が異なる文字列を区別できず、ハッシュ衝突を悪化させ、セキュリティ問題を引き起こす可能性があります。</p> <p>実際には、通常MD5、SHA-1、SHA-2、SHA-3などの標準ハッシュアルゴリズムを使用します。これらは任意の長さの入力データを固定長のハッシュ値にマッピングできます。</p> <p>過去1世紀にわたって、ハッシュアルゴリズムは継続的なアップグレードと最適化のプロセスにありました。一部の研究者はハッシュアルゴリズムの性能向上に努め、ハッカーを含む他の人々はハッシュアルゴリズムのセキュリティ問題を見つけることに専念しています。以下の表は、実用的なアプリケーションで一般的に使用されるハッシュアルゴリズムを示しています。</p> <ul> <li>MD5とSHA-1は複数回攻撃に成功しており、さまざまなセキュリティアプリケーションで放棄されています。</li> <li>SHA-2シリーズ、特にSHA-256は、現在最も安全なハッシュアルゴリズムの1つで、成功した攻撃は報告されておらず、さまざまなセキュリティアプリケーションとプロトコルで一般的に使用されています。</li> <li>SHA-3はSHA-2と比較して実装コストが低く、計算効率が高いですが、現在の使用範囲はSHA-2シリーズほど広範囲ではありません。</li> </ul> <p> 表 6-2 一般的なハッシュアルゴリズム </p> MD5 SHA-1 SHA-2 SHA-3 リリース年 1992 1995 2002 2008 出力長 128 bit 160 bit 256/512 bit 224/256/384/512 bit ハッシュ衝突 頻繁 頻繁 まれ まれ セキュリティレベル 低、攻撃に成功している 低、攻撃に成功している 高 高 アプリケーション 放棄、データ整合性チェックにまだ使用 放棄 暗号通貨取引検証、デジタル署名など SHA-2の代替として使用可能","path":["第 6 章 ハッシュ表","6.3 ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#_1","level":1,"title":"データ構造におけるハッシュ値","text":"<p>ハッシュ表のキーは整数、小数、文字列などのさまざまなデータ型にできることを知っています。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュ表のバケットインデックスを計算します。Pythonを例に取ると、<code>hash()</code>関数を使用してさまざまなデータ型のハッシュ値を計算できます。</p> <ul> <li>整数とブール値のハッシュ値は、それら自身の値です。</li> <li>浮動小数点数と文字列のハッシュ値の計算はより複雑で、興味のある読者は自分で研究することをお勧めします。</li> <li>タプルのハッシュ値は、その各要素のハッシュ値の組み合わせで、単一のハッシュ値になります。</li> <li>オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすることで、内容に基づいてハッシュ値を生成できます。</li> </ul> <p>Tip</p> <p>異なるプログラミング言語における組み込みハッシュ値計算関数の定義と方法は異なることに注意してください。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin built_in_hash.py<pre><code>num = 3\nhash_num = hash(num)\n# 整数3のハッシュ値は3\n\nbol = True\nhash_bol = hash(bol)\n# ブール値Trueのハッシュ値は1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# 小数3.14159のハッシュ値は326484311674566659\n\nstr = \"Hello 算法\"\nhash_str = hash(str)\n# 文字列\"Hello 算法\"のハッシュ値は4617003410720528961\n\ntup = (12836, \"小哈\")\nhash_tup = hash(tup)\n# タプル(12836, '小哈')のハッシュ値は1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# ListNodeオブジェクト0x1058fd810のハッシュ値は274267521\n</code></pre> built_in_hash.cpp<pre><code>int num = 3;\nsize_t hashNum = hash<int>()(num);\n// 整数3のハッシュ値は3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// ブール値1のハッシュ値は1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// 小数3.14159のハッシュ値は4614256650576692846\n\nstring str = \"Hello 算法\";\nsize_t hashStr = hash<string>()(str);\n// 文字列\"Hello 算法\"のハッシュ値は15466937326284535026\n\n// C++では、組み込みstd::hash()は基本データ型のハッシュ値のみを提供\n// 配列とオブジェクトのハッシュ値は別途実装が必要\n</code></pre> built_in_hash.java<pre><code>int num = 3;\nint hashNum = Integer.hashCode(num);\n// 整数3のハッシュ値は3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// ブール値trueのハッシュ値は1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// 小数3.14159のハッシュ値は-1340954729\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode();\n// 文字列\"Hello 算法\"のハッシュ値は-727081396\n\nObject[] arr = { 12836, \"小哈\" };\nint hashTup = Arrays.hashCode(arr);\n// 配列[12836, 小哈]のハッシュ値は1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// ListNodeオブジェクトutils.ListNode@7dc5e7b4のハッシュ値は2110121908\n</code></pre> built_in_hash.cs<pre><code>int num = 3;\nint hashNum = num.GetHashCode();\n// 整数3のハッシュ値は3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// ブール値trueのハッシュ値は1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// 小数3.14159のハッシュ値は-1340954729;\n\nstring str = \"Hello 算法\";\nint hashStr = str.GetHashCode();\n// 文字列\"Hello 算法\"のハッシュ値は-586107568;\n\nobject[] arr = [12836, \"小哈\"];\nint hashTup = arr.GetHashCode();\n// 配列[12836, 小哈]のハッシュ値は42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// ListNodeオブジェクト0のハッシュ値は39053774;\n</code></pre> built_in_hash.go<pre><code>// Goには組み込みのハッシュコード関数が提供されていません\n</code></pre> built_in_hash.swift<pre><code>let num = 3\nlet hashNum = num.hashValue\n// 整数3のハッシュ値は9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// ブール値trueのハッシュ値は-4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// 小数3.14159のハッシュ値は-2465384235396674631\n\nlet str = \"Hello 算法\"\nlet hashStr = str.hashValue\n// 文字列\"Hello 算法\"のハッシュ値は-7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"小哈\")]\nlet hashTup = arr.hashValue\n// 配列[AnyHashable(12836), AnyHashable(\"小哈\")]のハッシュ値は-2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// ListNodeオブジェクトutils.ListNodeのハッシュ値は-2434780518035996159\n</code></pre> built_in_hash.js<pre><code>// JavaScriptには組み込みのハッシュコード関数が提供されていません\n</code></pre> built_in_hash.ts<pre><code>// TypeScriptには組み込みのハッシュコード関数が提供されていません\n</code></pre> built_in_hash.dart<pre><code>int num = 3;\nint hashNum = num.hashCode;\n// 整数3のハッシュ値は34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// ブール値trueのハッシュ値は1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// 小数3.14159のハッシュ値は2570631074981783\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode;\n// 文字列\"Hello 算法\"のハッシュ値は468167534\n\nList arr = [12836, \"小哈\"];\nint hashArr = arr.hashCode;\n// 配列[12836, 小哈]のハッシュ値は976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// ListNodeオブジェクトInstance of 'ListNode'のハッシュ値は1033450432\n</code></pre> built_in_hash.rs<pre><code>use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// 整数3のハッシュ値は568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// ブール値trueのハッシュ値は4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// 小数3.14159のハッシュ値は2566941990314602357\n\nlet str = \"Hello 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 文字列\"Hello 算法\"のハッシュ値は16092673739211250988\n\nlet arr = (&12836, &\"小哈\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// タプル(12836, \"小哈\")のハッシュ値は1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// ListNodeオブジェクトRefCell { value: ListNode { val: 42, next: None } }のハッシュ値は15387811073369036852\n</code></pre> built_in_hash.c<pre><code>// Cには組み込みのハッシュコード関数が提供されていません\n</code></pre> built_in_hash.kt<pre><code>\n</code></pre> <p>多くのプログラミング言語では、不変オブジェクトのみがハッシュ表の<code>key</code>として機能できます。リスト(動的配列)を<code>key</code>として使用する場合、リストの内容が変更されると、そのハッシュ値も変更され、ハッシュ表で元の<code>value</code>を見つけることができなくなります。</p> <p>カスタムオブジェクト(連結リストノードなど)のメンバー変数は可変ですが、ハッシュ可能です。これは、オブジェクトのハッシュ値が通常そのメモリアドレスに基づいて生成されるためです。オブジェクトの内容が変更されても、メモリアドレスは同じままなので、ハッシュ値は変更されません。</p> <p>異なるコンソールで出力されるハッシュ値が異なることに気づいたかもしれません。これは、Pythonインタープリターが起動するたびに文字列ハッシュ関数にランダムソルトを追加するためです。このアプローチはHashDoS攻撃を効果的に防ぎ、ハッシュアルゴリズムのセキュリティを向上させます。</p>","path":["第 6 章 ハッシュ表","6.3 ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2 ハッシュ衝突","text":"<p>前節で述べたように、**ほとんどの場合、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論的にはハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列容量のサイズの場合、複数の整数が必然的に同じバケットインデックスにマッピングされます。</p> <p>ハッシュ衝突は誤ったクエリ結果につながり、ハッシュ表の使いやすさに深刻な影響を与える可能性があります。この問題に対処するために、ハッシュ衝突が発生するたびに、衝突が消えるまでハッシュ表のリサイズを実行します。このアプローチは非常にシンプルで直接的であり、うまく機能します。しかし、テーブルの拡張には大量のデータ移行とハッシュコードの再計算が含まれ、これらは高コストであるため、非常に非効率的に見えます。効率を向上させるために、以下の戦略を採用できます:</p> <ol> <li>**ハッシュ衝突が発生した場合でも、ターゲット要素の検索が適切に機能する**ようにハッシュ表のデータ構造を改善する。</li> <li>深刻な衝突が観察され、必要になる前に、拡張は最後の手段とする。</li> </ol> <p>ハッシュ表の構造を改善する主な方法は2つあります:「連鎖法」と「オープンアドレス法」です。</p>","path":["第 6 章 ハッシュ表","6.2 ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621","level":2,"title":"6.2.1 連鎖法","text":"<p>元のハッシュ表では、各バケットは1つのキー値ペアのみを格納できます。連鎖法は単一の要素を連結リストに変換し、キー値ペアをリストノードとして扱い、衝突するすべてのキー値ペアを同じ連結リストに格納します。下図は連鎖法を使用したハッシュ表の例を示しています。</p> <p></p> <p> 図 6-5 連鎖法ハッシュ表 </p> <p>連鎖法で実装されたハッシュ表の操作は以下のように変更されます:</p> <ul> <li>要素のクエリ: <code>key</code>を入力し、ハッシュ関数を通してバケットインデックスを取得し、連結リストのヘッドノードにアクセスします。連結リストを走査してキーを比較し、ターゲットキー値ペアを見つけます。</li> <li>要素の追加: ハッシュ関数を通して連結リストのヘッドノードにアクセスし、ノード(キー値ペア)をリストに追加します。</li> <li>要素の削除: ハッシュ関数の結果に基づいて連結リストのヘッドにアクセスし、連結リストを走査してターゲットノードを見つけて削除します。</li> </ul> <p>連鎖法には以下の制限があります:</p> <ul> <li>空間使用量の増加: 連結リストにはノードポインタが含まれており、配列よりも多くのメモリ空間を消費します。</li> <li>クエリ効率の低下: 対応する要素を見つけるために連結リストの線形走査が必要になるためです。</li> </ul> <p>以下のコードは連鎖法ハッシュ表の簡単な実装を提供し、注意すべき2つの点があります:</p> <ul> <li>簡単にするために、連結リストの代わりにリスト(動的配列)を使用します。この設定では、ハッシュ表(配列)は複数のバケットを含み、各バケットはリストです。</li> <li>この実装にはハッシュ表のリサイズメソッドが含まれています。負荷率が\\(\\frac{2}{3}\\)を超えると、ハッシュ表を元のサイズの2倍に拡張します。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py<pre><code>class HashMapChaining:\n \"\"\"チェーンアドレス法ハッシュテーブル\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n self.size = 0 # キー値ペアの数\n self.capacity = 4 # ハッシュテーブルの容量\n self.load_thres = 2.0 / 3.0 # 拡張をトリガーする負荷率の閾値\n self.extend_ratio = 2 # 拡張の倍数\n self.buckets = [[] for _ in range(self.capacity)] # バケット配列\n\n def hash_func(self, key: int) -> int:\n \"\"\"ハッシュ関数\"\"\"\n return key % self.capacity\n\n def load_factor(self) -> float:\n \"\"\"負荷率\"\"\"\n return self.size / self.capacity\n\n def get(self, key: int) -> str | None:\n \"\"\"照会操作\"\"\"\n index = self.hash_func(key)\n bucket = self.buckets[index]\n # バケットを走査し、キーが見つかれば対応する val を返す\n for pair in bucket:\n if pair.key == key:\n return pair.val\n # キーが見つからない場合、None を返す\n return None\n\n def put(self, key: int, val: str):\n \"\"\"追加操作\"\"\"\n # 負荷率が閾値を超えた場合、拡張を実行\n if self.load_factor() > self.load_thres:\n self.extend()\n index = self.hash_func(key)\n bucket = self.buckets[index]\n # バケットを走査し、指定されたキーに遭遇した場合、対応する val を更新して返す\n for pair in bucket:\n if pair.key == key:\n pair.val = val\n return\n # キーが見つからない場合、キー値ペアを末尾に追加\n pair = Pair(key, val)\n bucket.append(pair)\n self.size += 1\n\n def remove(self, key: int):\n \"\"\"削除操作\"\"\"\n index = self.hash_func(key)\n bucket = self.buckets[index]\n # バケットを走査し、その中からキー値ペアを削除\n for pair in bucket:\n if pair.key == key:\n bucket.remove(pair)\n self.size -= 1\n break\n\n def extend(self):\n \"\"\"ハッシュテーブルを拡張\"\"\"\n # 元のハッシュテーブルを一時的に保存\n buckets = self.buckets\n # 拡張された新しいハッシュテーブルを初期化\n self.capacity *= self.extend_ratio\n self.buckets = [[] for _ in range(self.capacity)]\n self.size = 0\n # 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動\n for bucket in buckets:\n for pair in bucket:\n self.put(pair.key, pair.val)\n\n def print(self):\n \"\"\"ハッシュテーブルを出力\"\"\"\n for bucket in self.buckets:\n res = []\n for pair in bucket:\n res.append(str(pair.key) + \" -> \" + pair.val)\n print(res)\n</code></pre> hash_map_chaining.cpp<pre><code>/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n private:\n int size; // キー値ペアの数\n int capacity; // ハッシュテーブルの容量\n double loadThres; // 拡張をトリガーする負荷率の閾値\n int extendRatio; // 拡張倍率\n vector<vector<Pair *>> buckets; // バケット配列\n\n public:\n /* コンストラクタ */\n HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n buckets.resize(capacity);\n }\n\n /* デストラクタ */\n ~HashMapChaining() {\n for (auto &bucket : buckets) {\n for (Pair *pair : bucket) {\n // メモリを解放\n delete pair;\n }\n }\n }\n\n /* ハッシュ関数 */\n int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 負荷率 */\n double loadFactor() {\n return (double)size / (double)capacity;\n }\n\n /* クエリ操作 */\n string get(int key) {\n int index = hashFunc(key);\n // バケットを走査、キーが見つかった場合、対応するvalを返却\n for (Pair *pair : buckets[index]) {\n if (pair->key == key) {\n return pair->val;\n }\n }\n // キーが見つからない場合、空文字列を返却\n return \"\";\n }\n\n /* 追加操作 */\n void put(int key, string val) {\n // 負荷率が閾値を超えた場合、拡張を実行\n if (loadFactor() > loadThres) {\n extend();\n }\n int index = hashFunc(key);\n // バケットを走査、指定キーに遭遇した場合、対応するvalを更新して返却\n for (Pair *pair : buckets[index]) {\n if (pair->key == key) {\n pair->val = val;\n return;\n }\n }\n // キーが見つからない場合、キー値ペアを末尾に追加\n buckets[index].push_back(new Pair(key, val));\n size++;\n }\n\n /* 削除操作 */\n void remove(int key) {\n int index = hashFunc(key);\n auto &bucket = buckets[index];\n // バケットを走査、キー値ペアを削除\n for (int i = 0; i < bucket.size(); i++) {\n if (bucket[i]->key == key) {\n Pair *tmp = bucket[i];\n bucket.erase(bucket.begin() + i); // キー値ペアを削除\n delete tmp; // メモリを解放\n size--;\n return;\n }\n }\n }\n\n /* ハッシュテーブルを拡張 */\n void extend() {\n // 元のハッシュテーブルを一時保存\n vector<vector<Pair *>> bucketsTmp = buckets;\n // 拡張された新しいハッシュテーブルを初期化\n capacity *= extendRatio;\n buckets.clear();\n buckets.resize(capacity);\n size = 0;\n // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動\n for (auto &bucket : bucketsTmp) {\n for (Pair *pair : bucket) {\n put(pair->key, pair->val);\n // メモリを解放\n delete pair;\n }\n }\n }\n\n /* ハッシュテーブルを印刷 */\n void print() {\n for (auto &bucket : buckets) {\n cout << \"[\";\n for (Pair *pair : bucket) {\n cout << pair->key << \" -> \" << pair->val << \", \";\n }\n cout << \"]\\n\";\n }\n }\n};\n</code></pre> hash_map_chaining.java<pre><code>/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n int size; // キー値ペアの数\n int capacity; // ハッシュテーブルの容量\n double loadThres; // 拡張をトリガーする負荷率の閾値\n int extendRatio; // 拡張倍率\n List<List<Pair>> buckets; // バケット配列\n\n /* コンストラクタ */\n public HashMapChaining() {\n size = 0;\n capacity = 4;\n loadThres = 2.0 / 3.0;\n extendRatio = 2;\n buckets = new ArrayList<>(capacity);\n for (int i = 0; i < capacity; i++) {\n buckets.add(new ArrayList<>());\n }\n }\n\n /* ハッシュ関数 */\n int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 負荷率 */\n double loadFactor() {\n return (double) size / capacity;\n }\n\n /* クエリ操作 */\n String get(int key) {\n int index = hashFunc(key);\n List<Pair> bucket = buckets.get(index);\n // バケットを走査、キーが見つかった場合対応するvalを返す\n for (Pair pair : bucket) {\n if (pair.key == key) {\n return pair.val;\n }\n }\n // キーが見つからない場合、nullを返す\n return null;\n }\n\n /* 追加操作 */\n void put(int key, String val) {\n // 負荷率が閾値を超えた場合、拡張を実行\n if (loadFactor() > loadThres) {\n extend();\n }\n int index = hashFunc(key);\n List<Pair> bucket = buckets.get(index);\n // バケットを走査、指定したキーに遭遇した場合、対応するvalを更新して戻る\n for (Pair pair : bucket) {\n if (pair.key == key) {\n pair.val = val;\n return;\n }\n }\n // キーが見つからない場合、キー値ペアを末尾に追加\n Pair pair = new Pair(key, val);\n bucket.add(pair);\n size++;\n }\n\n /* 削除操作 */\n void remove(int key) {\n int index = hashFunc(key);\n List<Pair> bucket = buckets.get(index);\n // バケットを走査、その中からキー値ペアを削除\n for (Pair pair : bucket) {\n if (pair.key == key) {\n bucket.remove(pair);\n size--;\n break;\n }\n }\n }\n\n /* ハッシュテーブルを拡張 */\n void extend() {\n // 元のハッシュテーブルを一時的に保存\n List<List<Pair>> bucketsTmp = buckets;\n // 拡張された新しいハッシュテーブルを初期化\n capacity *= extendRatio;\n buckets = new ArrayList<>(capacity);\n for (int i = 0; i < capacity; i++) {\n buckets.add(new ArrayList<>());\n }\n size = 0;\n // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動\n for (List<Pair> bucket : bucketsTmp) {\n for (Pair pair : bucket) {\n put(pair.key, pair.val);\n }\n }\n }\n\n /* ハッシュテーブルを印刷 */\n void print() {\n for (List<Pair> bucket : buckets) {\n List<String> res = new ArrayList<>();\n for (Pair pair : bucket) {\n res.add(pair.key + \" -> \" + pair.val);\n }\n System.out.println(res);\n }\n }\n}\n</code></pre> hash_map_chaining.cs<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.go<pre><code>[class]{hashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.swift<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.js<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.ts<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.dart<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.rs<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.c<pre><code>[class]{Node}-[func]{}\n\n[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.kt<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> hash_map_chaining.rb<pre><code>[class]{HashMapChaining}-[func]{}\n</code></pre> <p>連結リストが非常に長い場合、クエリ効率\\(O(n)\\)が悪いことは注目に値します。この場合、リストを「AVL木」または「赤黒木」に変換して、クエリ操作の時間計算量を\\(O(\\log n)\\)に最適化できます。</p>","path":["第 6 章 ハッシュ表","6.2 ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622","level":2,"title":"6.2.2 オープンアドレス法","text":"<p>オープンアドレス法は追加のデータ構造を導入せず、代わりに「複数回プローブ」を通してハッシュ衝突を処理します。プローブ方法には主に線形プローブ、二次プローブ、二重ハッシュがあります。</p> <p>線形プローブを例にして、オープンアドレス法ハッシュ表のメカニズムを紹介しましょう。</p>","path":["第 6 章 ハッシュ表","6.2 ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1","level":3,"title":"1. 線形プローブ","text":"<p>線形プローブは固定ステップの線形検索をプローブに使用し、通常のハッシュ表とは異なります。</p> <ul> <li>要素の挿入: ハッシュ関数を使用してバケットインデックスを計算します。バケットに既に要素が含まれている場合、衝突位置から線形に前方に走査し(通常ステップサイズは\\(1\\))、空のバケットが見つかるまで進み、要素を挿入します。</li> <li>要素の検索: ハッシュ衝突に遭遇した場合、同じステップサイズを使用して線形に前方に走査し、対応する要素が見つかったら<code>value</code>を返します。空のバケットに遭遇した場合、ターゲット要素がハッシュ表にないことを意味するため、<code>None</code>を返します。</li> </ul> <p>下図はオープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布を示しています。このハッシュ関数によると、下二桁が同じキーは同じバケットにマッピングされます。線形プローブを通して、それらはそのバケットとその下のバケットに順次格納されます。</p> <p></p> <p> 図 6-6 オープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布 </p> <p>しかし、線形プローブは「クラスタリング」を作りやすい傾向があります。具体的には、配列内の連続的に占有された位置が長いほど、これらの連続した位置でハッシュ衝突が発生する確率が高くなり、その位置でのクラスタリングの成長をさらに促進し、悪循環を形成し、最終的に挿入、削除、クエリ、更新操作の効率低下につながります。</p> <p>**オープンアドレス法ハッシュ表では要素を直接削除できない**ことに注意することが重要です。要素を削除すると、配列に空のバケット<code>None</code>が作成されます。要素を検索する際、線形プローブがこの空のバケットに遭遇すると戻ってしまい、このバケットの下の要素にアクセスできなくなります。プログラムはこれらの要素が存在しないと誤って仮定する可能性があります。下図に示すとおりです。</p> <p></p> <p> 図 6-7 オープンアドレス法での削除によるクエリ問題 </p> <p>この問題を解決するために、遅延削除メカニズムを採用できます:ハッシュ表から要素を直接削除する代わりに、定数<code>TOMBSTONE</code>を使用してバケットをマークします。このメカニズムでは、<code>None</code>と<code>TOMBSTONE</code>の両方が空のバケットを表し、キー値ペアを保持できます。ただし、線形プローブが<code>TOMBSTONE</code>に遭遇した場合、その下にまだキー値ペアがある可能性があるため、走査を続ける必要があります。</p> <p>しかし、遅延削除はハッシュ表の性能劣化を加速する可能性があります。削除操作のたびに削除マークが生成され、<code>TOMBSTONE</code>が増加すると、線形プローブがターゲット要素を見つけるために複数の<code>TOMBSTONE</code>をスキップする必要がある可能性があるため、検索時間も増加します。</p> <p>これに対処するために、線形プローブ中に最初に遭遇した<code>TOMBSTONE</code>のインデックスを記録し、検索されたターゲット要素とその<code>TOMBSTONE</code>の位置を交換することを検討してください。これを行う利点は、要素がクエリまたは追加されるたびに、要素がその理想的な位置(プローブの開始点)により近いバケットに移動され、クエリ効率が最適化されることです。</p> <p>以下のコードは、遅延削除を使用したオープンアドレス法(線形プローブ)ハッシュ表を実装しています。ハッシュ表の空間をより有効に活用するために、ハッシュ表を「循環配列」として扱います。配列の終わりを超えると、最初に戻って走査を続けます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py<pre><code>class HashMapOpenAddressing:\n \"\"\"オープンアドレス法ハッシュテーブル\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n self.size = 0 # キー値ペアの数\n self.capacity = 4 # ハッシュテーブルの容量\n self.load_thres = 2.0 / 3.0 # 拡張をトリガーする負荷率の閾値\n self.extend_ratio = 2 # 拡張の倍数\n self.buckets: list[Pair | None] = [None] * self.capacity # バケット配列\n self.TOMBSTONE = Pair(-1, \"-1\") # 削除マーク\n\n def hash_func(self, key: int) -> int:\n \"\"\"ハッシュ関数\"\"\"\n return key % self.capacity\n\n def load_factor(self) -> float:\n \"\"\"負荷率\"\"\"\n return self.size / self.capacity\n\n def find_bucket(self, key: int) -> int:\n \"\"\"key に対応するバケットインデックスを検索\"\"\"\n index = self.hash_func(key)\n first_tombstone = -1\n # 線形探査、空のバケットに遭遇したらブレーク\n while self.buckets[index] is not None:\n # キーに遭遇した場合、対応するバケットインデックスを返す\n if self.buckets[index].key == key:\n # 削除マークが以前に遭遇していた場合、キー値ペアをそのインデックスに移動\n if first_tombstone != -1:\n self.buckets[first_tombstone] = self.buckets[index]\n self.buckets[index] = self.TOMBSTONE\n return first_tombstone # 移動されたバケットインデックスを返す\n return index # バケットインデックスを返す\n # 最初に遭遇した削除マークを記録\n if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n first_tombstone = index\n # バケットインデックスを計算、末尾を超えた場合は先頭に戻る\n index = (index + 1) % self.capacity\n # キーが存在しない場合、挿入ポイントのインデックスを返す\n return index if first_tombstone == -1 else first_tombstone\n\n def get(self, key: int) -> str:\n \"\"\"照会操作\"\"\"\n # key に対応するバケットインデックスを検索\n index = self.find_bucket(key)\n # キー値ペアが見つかれば、対応する val を返す\n if self.buckets[index] not in [None, self.TOMBSTONE]:\n return self.buckets[index].val\n # キー値ペアが存在しない場合、None を返す\n return None\n\n def put(self, key: int, val: str):\n \"\"\"追加操作\"\"\"\n # 負荷率が閾値を超えた場合、拡張を実行\n if self.load_factor() > self.load_thres:\n self.extend()\n # key に対応するバケットインデックスを検索\n index = self.find_bucket(key)\n # キー値ペアが見つかれば、val を上書きして返す\n if self.buckets[index] not in [None, self.TOMBSTONE]:\n self.buckets[index].val = val\n return\n # キー値ペアが存在しない場合、キー値ペアを追加\n self.buckets[index] = Pair(key, val)\n self.size += 1\n\n def remove(self, key: int):\n \"\"\"削除操作\"\"\"\n # key に対応するバケットインデックスを検索\n index = self.find_bucket(key)\n # キー値ペアが見つかれば、削除マークで覆う\n if self.buckets[index] not in [None, self.TOMBSTONE]:\n self.buckets[index] = self.TOMBSTONE\n self.size -= 1\n\n def extend(self):\n \"\"\"ハッシュテーブルを拡張\"\"\"\n # 元のハッシュテーブルを一時的に保存\n buckets_tmp = self.buckets\n # 拡張された新しいハッシュテーブルを初期化\n self.capacity *= self.extend_ratio\n self.buckets = [None] * self.capacity\n self.size = 0\n # 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動\n for pair in buckets_tmp:\n if pair not in [None, self.TOMBSTONE]:\n self.put(pair.key, pair.val)\n\n def print(self):\n \"\"\"ハッシュテーブルを出力\"\"\"\n for pair in self.buckets:\n if pair is None:\n print(\"None\")\n elif pair is self.TOMBSTONE:\n print(\"TOMBSTONE\")\n else:\n print(pair.key, \"->\", pair.val)\n</code></pre> hash_map_open_addressing.cpp<pre><code>/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n private:\n int size; // キー値ペアの数\n int capacity = 4; // ハッシュテーブルの容量\n const double loadThres = 2.0 / 3.0; // 拡張をトリガーする負荷率の閾値\n const int extendRatio = 2; // 拡張倍率\n vector<Pair *> buckets; // バケット配列\n Pair *TOMBSTONE = new Pair(-1, \"-1\"); // 削除マーク\n\n public:\n /* コンストラクタ */\n HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n }\n\n /* デストラクタ */\n ~HashMapOpenAddressing() {\n for (Pair *pair : buckets) {\n if (pair != nullptr && pair != TOMBSTONE) {\n delete pair;\n }\n }\n delete TOMBSTONE;\n }\n\n /* ハッシュ関数 */\n int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 負荷率 */\n double loadFactor() {\n return (double)size / capacity;\n }\n\n /* keyに対応するバケットインデックスを検索 */\n int findBucket(int key) {\n int index = hashFunc(key);\n int firstTombstone = -1;\n // 線形探査、空のバケットに遭遇したら中断\n while (buckets[index] != nullptr) {\n // keyに遭遇した場合、対応するバケットインデックスを返却\n if (buckets[index]->key == key) {\n // 以前に削除マークに遭遇していた場合、キー値ペアをそのインデックスに移動\n if (firstTombstone != -1) {\n buckets[firstTombstone] = buckets[index];\n buckets[index] = TOMBSTONE;\n return firstTombstone; // 移動されたバケットインデックスを返却\n }\n return index; // バケットインデックスを返却\n }\n // 最初に遭遇した削除マークを記録\n if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n firstTombstone = index;\n }\n // バケットインデックスを計算、末尾を超えた場合は先頭に戻る\n index = (index + 1) % capacity;\n }\n // keyが存在しない場合、挿入ポイントのインデックスを返却\n return firstTombstone == -1 ? index : firstTombstone;\n }\n\n /* クエリ操作 */\n string get(int key) {\n // keyに対応するバケットインデックスを検索\n int index = findBucket(key);\n // キー値ペアが見つかった場合、対応するvalを返却\n if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n return buckets[index]->val;\n }\n // キー値ペアが存在しない場合、空文字列を返却\n return \"\";\n }\n\n /* 追加操作 */\n void put(int key, string val) {\n // 負荷率が閾値を超えた場合、拡張を実行\n if (loadFactor() > loadThres) {\n extend();\n }\n // keyに対応するバケットインデックスを検索\n int index = findBucket(key);\n // キー値ペアが見つかった場合、valを上書きして返却\n if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n buckets[index]->val = val;\n return;\n }\n // キー値ペアが存在しない場合、キー値ペアを追加\n buckets[index] = new Pair(key, val);\n size++;\n }\n\n /* 削除操作 */\n void remove(int key) {\n // keyに対応するバケットインデックスを検索\n int index = findBucket(key);\n // キー値ペアが見つかった場合、削除マークで覆う\n if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n delete buckets[index];\n buckets[index] = TOMBSTONE;\n size--;\n }\n }\n\n /* ハッシュテーブルを拡張 */\n void extend() {\n // 元のハッシュテーブルを一時保存\n vector<Pair *> bucketsTmp = buckets;\n // 拡張された新しいハッシュテーブルを初期化\n capacity *= extendRatio;\n buckets = vector<Pair *>(capacity, nullptr);\n size = 0;\n // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動\n for (Pair *pair : bucketsTmp) {\n if (pair != nullptr && pair != TOMBSTONE) {\n put(pair->key, pair->val);\n delete pair;\n }\n }\n }\n\n /* ハッシュテーブルを印刷 */\n void print() {\n for (Pair *pair : buckets) {\n if (pair == nullptr) {\n cout << \"nullptr\" << endl;\n } else if (pair == TOMBSTONE) {\n cout << \"TOMBSTONE\" << endl;\n } else {\n cout << pair->key << \" -> \" << pair->val << endl;\n }\n }\n }\n};\n</code></pre> hash_map_open_addressing.java<pre><code>/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n private int size; // キー値ペアの数\n private int capacity = 4; // ハッシュテーブルの容量\n private final double loadThres = 2.0 / 3.0; // 拡張をトリガーする負荷率の閾値\n private final int extendRatio = 2; // 拡張倍率\n private Pair[] buckets; // バケット配列\n private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // 削除マーク\n\n /* コンストラクタ */\n public HashMapOpenAddressing() {\n size = 0;\n buckets = new Pair[capacity];\n }\n\n /* ハッシュ関数 */\n private int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 負荷率 */\n private double loadFactor() {\n return (double) size / capacity;\n }\n\n /* keyに対応するバケットインデックスを検索 */\n private int findBucket(int key) {\n int index = hashFunc(key);\n int firstTombstone = -1;\n // 線形探査、空のバケットに遭遇したら終了\n while (buckets[index] != null) {\n // keyに遭遇した場合、対応するバケットインデックスを返す\n if (buckets[index].key == key) {\n // 以前に削除マークに遭遇していた場合、キー値ペアをそのインデックスに移動\n if (firstTombstone != -1) {\n buckets[firstTombstone] = buckets[index];\n buckets[index] = TOMBSTONE;\n return firstTombstone; // 移動後のバケットインデックスを返す\n }\n return index; // バケットインデックスを返す\n }\n // 最初に遭遇した削除マークを記録\n if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n firstTombstone = index;\n }\n // バケットインデックスを計算、末尾を超えた場合は先頭に戻る\n index = (index + 1) % capacity;\n }\n // keyが存在しない場合、挿入ポイントのインデックスを返す\n return firstTombstone == -1 ? index : firstTombstone;\n }\n\n /* クエリ操作 */\n public String get(int key) {\n // keyに対応するバケットインデックスを検索\n int index = findBucket(key);\n // キー値ペアが見つかった場合、対応するvalを返す\n if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n return buckets[index].val;\n }\n // キー値ペアが存在しない場合、nullを返す\n return null;\n }\n\n /* 追加操作 */\n public void put(int key, String val) {\n // 負荷率が閾値を超えた場合、拡張を実行\n if (loadFactor() > loadThres) {\n extend();\n }\n // keyに対応するバケットインデックスを検索\n int index = findBucket(key);\n // キー値ペアが見つかった場合、valを上書きして戻る\n if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n buckets[index].val = val;\n return;\n }\n // キー値ペアが存在しない場合、キー値ペアを追加\n buckets[index] = new Pair(key, val);\n size++;\n }\n\n /* 削除操作 */\n public void remove(int key) {\n // keyに対応するバケットインデックスを検索\n int index = findBucket(key);\n // キー値ペアが見つかった場合、削除マークで覆う\n if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n buckets[index] = TOMBSTONE;\n size--;\n }\n }\n\n /* ハッシュテーブルを拡張 */\n private void extend() {\n // 元のハッシュテーブルを一時的に保存\n Pair[] bucketsTmp = buckets;\n // 拡張された新しいハッシュテーブルを初期化\n capacity *= extendRatio;\n buckets = new Pair[capacity];\n size = 0;\n // 元のハッシュテーブルから新しいハッシュテーブルにキー値ペアを移動\n for (Pair pair : bucketsTmp) {\n if (pair != null && pair != TOMBSTONE) {\n put(pair.key, pair.val);\n }\n }\n }\n\n /* ハッシュテーブルを印刷 */\n public void print() {\n for (Pair pair : buckets) {\n if (pair == null) {\n System.out.println(\"null\");\n } else if (pair == TOMBSTONE) {\n System.out.println(\"TOMBSTONE\");\n } else {\n System.out.println(pair.key + \" -> \" + pair.val);\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.cs<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.go<pre><code>[class]{hashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.swift<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.js<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.ts<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.dart<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.rs<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.c<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.kt<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre> hash_map_open_addressing.rb<pre><code>[class]{HashMapOpenAddressing}-[func]{}\n</code></pre>","path":["第 6 章 ハッシュ表","6.2 ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2","level":3,"title":"2. 二次プローブ","text":"<p>二次プローブは線形プローブに似ており、オープンアドレス法の一般的な戦略の1つです。衝突が発生した場合、二次プローブは単純に固定ステップ数をスキップするのではなく、「プローブ回数の二乗」に等しいステップ数、つまり\\(1, 4, 9, \\dots\\)ステップをスキップします。</p> <p>二次プローブには以下の利点があります:</p> <ul> <li>二次プローブは、プローブ回数の二乗の距離をスキップすることで、線形プローブのクラスタリング効果を軽減しようとします。</li> <li>二次プローブはより大きな距離をスキップして空の位置を見つけ、データをより均等に分散するのに役立ちます。</li> </ul> <p>しかし、二次プローブは完璧ではありません:</p> <ul> <li>クラスタリングは依然として存在し、つまり一部の位置は他の位置よりも占有される可能性が高いです。</li> <li>二乗の成長により、二次プローブはハッシュ表全体をプローブできない可能性があり、ハッシュ表に空のバケットがあっても、二次プローブがアクセスできない可能性があります。</li> </ul>","path":["第 6 章 ハッシュ表","6.2 ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3","level":3,"title":"3. 二重ハッシュ","text":"<p>名前が示すように、二重ハッシュ法は複数のハッシュ関数\\(f_1(x)\\)、\\(f_2(x)\\)、\\(f_3(x)\\)、\\(\\dots\\)をプローブに使用します。</p> <ul> <li>要素の挿入: ハッシュ関数\\(f_1(x)\\)が衝突に遭遇した場合、\\(f_2(x)\\)を試し、以下同様に、空の位置が見つかって要素が挿入されるまで続けます。</li> <li>要素の検索: 同じハッシュ関数の順序で検索し、ターゲット要素が見つかって返されるまで、または空の位置に遭遇するかすべてのハッシュ関数が試されるまで続け、要素がハッシュ表にないことを示し、<code>None</code>を返します。</li> </ul> <p>線形プローブと比較して、二重ハッシュ法はクラスタリングが起こりにくいですが、複数のハッシュ関数は追加の計算オーバーヘッドを導入します。</p> <p>Tip</p> <p>オープンアドレス法(線形プローブ、二次プローブ、二重ハッシュ)ハッシュ表はすべて「要素を直接削除できない」という問題があることに注意してください。</p>","path":["第 6 章 ハッシュ表","6.2 ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623","level":2,"title":"6.2.3 プログラミング言語の選択","text":"<p>異なるプログラミング言語は異なるハッシュ表実装戦略を採用しています。以下にいくつかの例を示します:</p> <ul> <li>Pythonはオープンアドレス法を使用します。<code>dict</code>辞書はプローブに疑似乱数を使用します。</li> <li>Javaは連鎖法を使用します。JDK 1.8以降、<code>HashMap</code>の配列長が64に達し、連結リストの長さが8に達すると、連結リストは検索性能を向上させるために赤黒木に変換されます。</li> <li>Goは連鎖法を使用します。Goは各バケットが最大8つのキー値ペアを格納できることを規定し、容量を超えた場合はオーバーフローバケットが連結されます。オーバーフローバケットが多すぎる場合、性能を確保するために特別な等容量リサイズ操作が実行されます。</li> </ul>","path":["第 6 章 ハッシュ表","6.2 ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1 ハッシュ表","text":"<p>ハッシュ表はハッシュマップとも呼ばれ、キーと値の間のマッピングを確立し、効率的な要素の取得を可能にするデータ構造です。具体的には、ハッシュ表に<code>key</code>を入力すると、\\(O(1)\\)の時間計算量で対応する<code>value</code>を取得できます。</p> <p>下図に示すように、\\(n\\)人の学生がいて、各学生には「名前」と「学籍番号」の2つのデータフィールドがあるとします。学籍番号を入力として対応する名前を返すクエリ機能を実装したい場合、下図に示すハッシュ表を使用できます。</p> <p></p> <p> 図 6-1 ハッシュ表の抽象的な表現 </p> <p>ハッシュ表に加えて、配列や連結リストもクエリ機能の実装に使用できますが、時間計算量が異なります。効率は以下の表で比較されています:</p> <ul> <li>要素の挿入: 配列(または連結リスト)の末尾に要素を追加するだけです。この操作の時間計算量は\\(O(1)\\)です。</li> <li>要素の検索: 配列(または連結リスト)がソートされていないため、要素を検索するにはすべての要素を走査する必要があります。この操作の時間計算量は\\(O(n)\\)です。</li> <li>要素の削除: 要素を削除するには、まずその要素を見つけてから、配列(または連結リスト)から削除します。この操作の時間計算量は\\(O(n)\\)です。</li> </ul> <p> 表 6-1 一般的な操作の時間効率の比較 </p> 配列 連結リスト ハッシュ表 要素の検索 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) 要素の挿入 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 要素の削除 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) <p>観察されるように、**ハッシュ表における操作(挿入、削除、検索、変更)の時間計算量は\\(O(1)\\)**で、非常に効率的です。</p>","path":["第 6 章 ハッシュ表","6.1 ハッシュ表"],"tags":[]},{"location":"chapter_hashing/hash_map/#611","level":2,"title":"6.1.1 ハッシュ表の一般的な操作","text":"<p>ハッシュ表の一般的な操作には、初期化、クエリ、キー値ペアの追加、キー値ペアの削除があります。以下はコード例です:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin hash_map.py<pre><code># ハッシュ表を初期化\nhmap: dict = {}\n\n# 追加操作\n# ハッシュ表にキー値ペア (key, value) を追加\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n# クエリ操作\n# ハッシュ表にキーを入力し、値を取得\nname: str = hmap[15937]\n\n# 削除操作\n# ハッシュ表からキー値ペア (key, value) を削除\nhmap.pop(10583)\n</code></pre> hash_map.cpp<pre><code>/* ハッシュ表を初期化 */\nunordered_map<int, string> map;\n\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nmap[12836] = \"小哈\";\nmap[15937] = \"小啰\";\nmap[16750] = \"小算\";\nmap[13276] = \"小法\";\nmap[10583] = \"小鸭\";\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nstring name = map[15937];\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nmap.erase(10583);\n</code></pre> hash_map.java<pre><code>/* ハッシュ表を初期化 */\nMap<Integer, String> map = new HashMap<>();\n\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nmap.put(12836, \"小哈\");\nmap.put(15937, \"小啰\");\nmap.put(16750, \"小算\");\nmap.put(13276, \"小法\");\nmap.put(10583, \"小鸭\");\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nString name = map.get(15937);\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nmap.remove(10583);\n</code></pre> hash_map.cs<pre><code>/* ハッシュ表を初期化 */\nDictionary<int, string> map = new() {\n /* 追加操作 */\n // ハッシュ表にキー値ペア (key, value) を追加\n { 12836, \"小哈\" },\n { 15937, \"小啰\" },\n { 16750, \"小算\" },\n { 13276, \"小法\" },\n { 10583, \"小鸭\" }\n};\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nstring name = map[15937];\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nmap.Remove(10583);\n</code></pre> hash_map_test.go<pre><code>/* ハッシュ表を初期化 */\nhmap := make(map[int]string)\n\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nname := hmap[15937]\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\ndelete(hmap, 10583)\n</code></pre> hash_map.swift<pre><code>/* ハッシュ表を初期化 */\nvar map: [Int: String] = [:]\n\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nmap[12836] = \"小哈\"\nmap[15937] = \"小啰\"\nmap[16750] = \"小算\"\nmap[13276] = \"小法\"\nmap[10583] = \"小鸭\"\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nlet name = map[15937]!\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nmap.removeValue(forKey: 10583)\n</code></pre> hash_map.js<pre><code>/* ハッシュ表を初期化 */\nconst map = new Map();\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nmap.set(12836, '小哈');\nmap.set(15937, '小啰');\nmap.set(16750, '小算');\nmap.set(13276, '小法');\nmap.set(10583, '小鸭');\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nlet name = map.get(15937);\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nmap.delete(10583);\n</code></pre> hash_map.ts<pre><code>/* ハッシュ表を初期化 */\nconst map = new Map<number, string>();\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nmap.set(12836, '小哈');\nmap.set(15937, '小啰');\nmap.set(16750, '小算');\nmap.set(13276, '小法');\nmap.set(10583, '小鸭');\nconsole.info('\\n追加後、ハッシュ表は\\nKey -> Value');\nconsole.info(map);\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nlet name = map.get(15937);\nconsole.info('\\n学籍番号15937を入力、名前を問い合わせ ' + name);\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nmap.delete(10583);\nconsole.info('\\n10583を削除後、ハッシュ表は\\nKey -> Value');\nconsole.info(map);\n</code></pre> hash_map.dart<pre><code>/* ハッシュ表を初期化 */\nMap<int, String> map = {};\n\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nmap[12836] = \"小哈\";\nmap[15937] = \"小啰\";\nmap[16750] = \"小算\";\nmap[13276] = \"小法\";\nmap[10583] = \"小鸭\";\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nString name = map[15937];\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nmap.remove(10583);\n</code></pre> hash_map.rs<pre><code>use std::collections::HashMap;\n\n/* ハッシュ表を初期化 */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* 追加操作 */\n// ハッシュ表にキー値ペア (key, value) を追加\nmap.insert(12836, \"小哈\".to_string());\nmap.insert(15937, \"小啰\".to_string());\nmap.insert(16750, \"小算\".to_string());\nmap.insert(13279, \"小法\".to_string());\nmap.insert(10583, \"小鸭\".to_string());\n\n/* クエリ操作 */\n// ハッシュ表にキーを入力し、値を取得\nlet _name: Option<&String> = map.get(&15937);\n\n/* 削除操作 */\n// ハッシュ表からキー値ペア (key, value) を削除\nlet _removed_value: Option<String> = map.remove(&10583);\n</code></pre> hash_map.c<pre><code>// Cには組み込みのハッシュ表が提供されていません\n</code></pre> hash_map.kt<pre><code>\n</code></pre> <p>ハッシュ表を走査する一般的な方法は3つあります:キー値ペアの走査、キーの走査、値の走査。以下はコード例です:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin hash_map.py<pre><code># ハッシュ表を走査\n# キー値ペア key->value を走査\nfor key, value in hmap.items():\n print(key, \"->\", value)\n# キーのみを走査\nfor key in hmap.keys():\n print(key)\n# 値のみを走査\nfor value in hmap.values():\n print(value)\n</code></pre> hash_map.cpp<pre><code>/* ハッシュ表を走査 */\n// キー値ペア key->value を走査\nfor (auto kv: map) {\n cout << kv.first << \" -> \" << kv.second << endl;\n}\n// イテレータを使用してキー値ペア key->value を走査\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n cout << iter->first << \"->\" << iter->second << endl;\n}\n</code></pre> hash_map.java<pre><code>/* ハッシュ表を走査 */\n// キー値ペア key->value を走査\nfor (Map.Entry<Integer, String> kv: map.entrySet()) {\n System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// キーのみを走査\nfor (int key: map.keySet()) {\n System.out.println(key);\n}\n// 値のみを走査\nfor (String val: map.values()) {\n System.out.println(val);\n}\n</code></pre> hash_map.cs<pre><code>/* ハッシュ表を走査 */\n// キー値ペア Key->Value を走査\nforeach (var kv in map) {\n Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// キーのみを走査\nforeach (int key in map.Keys) {\n Console.WriteLine(key);\n}\n// 値のみを走査\nforeach (string val in map.Values) {\n Console.WriteLine(val);\n}\n</code></pre> hash_map_test.go<pre><code>/* ハッシュ表を走査 */\n// キー値ペア key->value を走査\nfor key, value := range hmap {\n fmt.Println(key, \"->\", value)\n}\n// キーのみを走査\nfor key := range hmap {\n fmt.Println(key)\n}\n// 値のみを走査\nfor _, value := range hmap {\n fmt.Println(value)\n}\n</code></pre> hash_map.swift<pre><code>/* ハッシュ表を走査 */\n// キー値ペア Key->Value を走査\nfor (key, value) in map {\n print(\"\\(key) -> \\(value)\")\n}\n// キーのみを走査\nfor key in map.keys {\n print(key)\n}\n// 値のみを走査\nfor value in map.values {\n print(value)\n}\n</code></pre> hash_map.js<pre><code>/* ハッシュ表を走査 */\nconsole.info('\\nキー値ペア Key->Value を走査');\nfor (const [k, v] of map.entries()) {\n console.info(k + ' -> ' + v);\n}\nconsole.info('\\nキーのみを走査 Key');\nfor (const k of map.keys()) {\n console.info(k);\n}\nconsole.info('\\n値のみを走査 Value');\nfor (const v of map.values()) {\n console.info(v);\n}\n</code></pre> hash_map.ts<pre><code>/* ハッシュ表を走査 */\nconsole.info('\\nキー値ペア Key->Value を走査');\nfor (const [k, v] of map.entries()) {\n console.info(k + ' -> ' + v);\n}\nconsole.info('\\nキーのみを走査 Key');\nfor (const k of map.keys()) {\n console.info(k);\n}\nconsole.info('\\n値のみを走査 Value');\nfor (const v of map.values()) {\n console.info(v);\n}\n</code></pre> hash_map.dart<pre><code>/* ハッシュ表を走査 */\n// キー値ペア Key->Value を走査\nmap.forEach((key, value) {\nprint('$key -> $value');\n});\n\n// キーのみを走査 Key\nmap.keys.forEach((key) {\nprint(key);\n});\n\n// 値のみを走査 Value\nmap.values.forEach((value) {\nprint(value);\n});\n</code></pre> hash_map.rs<pre><code>/* ハッシュ表を走査 */\n// キー値ペア Key->Value を走査\nfor (key, value) in &map {\n println!(\"{key} -> {value}\");\n}\n\n// キーのみを走査 Key\nfor key in map.keys() {\n println!(\"{key}\");\n}\n\n// 値のみを走査 Value\nfor value in map.values() {\n println!(\"{value}\");\n}\n</code></pre> hash_map.c<pre><code>// Cには組み込みのハッシュ表が提供されていません\n</code></pre> hash_map.kt<pre><code>\n</code></pre>","path":["第 6 章 ハッシュ表","6.1 ハッシュ表"],"tags":[]},{"location":"chapter_hashing/hash_map/#612","level":2,"title":"6.1.2 ハッシュ表の簡単な実装","text":"<p>まず、最も簡単なケースを考えてみましょう:配列のみを使ってハッシュ表を実装すること。ハッシュ表において、配列の各空きスロットはバケットと呼ばれ、各バケットはキー値ペアを格納できます。したがって、クエリ操作は<code>key</code>に対応するバケットを見つけ、そこから<code>value</code>を取得することになります。</p> <p>では、<code>key</code>に基づいて対応するバケットをどのように特定するのでしょうか?これはハッシュ関数によって実現されます。ハッシュ関数の役割は、より大きな入力空間をより小さな出力空間にマッピングすることです。ハッシュ表では、入力空間はすべてのキーで構成され、出力空間はすべてのバケット(配列インデックス)で構成されます。つまり、<code>key</code>が与えられた場合、ハッシュ関数を使用して対応するキー値ペアの配列内の格納位置を決定できます。</p> <p>与えられた<code>key</code>に対して、ハッシュ関数の計算は2つのステップで構成されます:</p> <ol> <li>特定のハッシュアルゴリズム<code>hash()</code>を使用してハッシュ値を計算します。</li> <li>ハッシュ値をバケット数(配列長)<code>capacity</code>で剰余を取り、キーに対応する配列<code>index</code>を取得します。</li> </ol> <pre><code>index = hash(key) % capacity\n</code></pre> <p>その後、<code>index</code>を使用してハッシュ表内の対応するバケットにアクセスし、<code>value</code>を取得できます。</p> <p>配列長が<code>capacity = 100</code>で、ハッシュアルゴリズムが<code>hash(key) = key</code>として定義されているとします。したがって、ハッシュ関数は<code>key % 100</code>として表現できます。以下の図は、<code>key</code>を学籍番号、<code>value</code>を名前として、ハッシュ関数の動作原理を示しています。</p> <p></p> <p> 図 6-2 ハッシュ関数の動作原理 </p> <p>以下のコードは簡単なハッシュ表を実装しています。ここでは、<code>key</code>と<code>value</code>を<code>Pair</code>クラスにカプセル化してキー値ペアを表現しています。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py<pre><code>class Pair:\n \"\"\"キー値ペア\"\"\"\n\n def __init__(self, key: int, val: str):\n self.key = key\n self.val = val\n\nclass ArrayHashMap:\n \"\"\"配列実装に基づくハッシュテーブル\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n # 100個のバケットを含む配列を初期化\n self.buckets: list[Pair | None] = [None] * 100\n\n def hash_func(self, key: int) -> int:\n \"\"\"ハッシュ関数\"\"\"\n index = key % 100\n return index\n\n def get(self, key: int) -> str:\n \"\"\"照会操作\"\"\"\n index: int = self.hash_func(key)\n pair: Pair = self.buckets[index]\n if pair is None:\n return None\n return pair.val\n\n def put(self, key: int, val: str):\n \"\"\"追加操作\"\"\"\n pair = Pair(key, val)\n index: int = self.hash_func(key)\n self.buckets[index] = pair\n\n def remove(self, key: int):\n \"\"\"削除操作\"\"\"\n index: int = self.hash_func(key)\n # None に設定し、削除を表現\n self.buckets[index] = None\n\n def entry_set(self) -> list[Pair]:\n \"\"\"すべてのキー値ペアを取得\"\"\"\n result: list[Pair] = []\n for pair in self.buckets:\n if pair is not None:\n result.append(pair)\n return result\n\n def key_set(self) -> list[int]:\n \"\"\"すべてのキーを取得\"\"\"\n result = []\n for pair in self.buckets:\n if pair is not None:\n result.append(pair.key)\n return result\n\n def value_set(self) -> list[str]:\n \"\"\"すべての値を取得\"\"\"\n result = []\n for pair in self.buckets:\n if pair is not None:\n result.append(pair.val)\n return result\n\n def print(self):\n \"\"\"ハッシュテーブルを出力\"\"\"\n for pair in self.buckets:\n if pair is not None:\n print(pair.key, \"->\", pair.val)\n</code></pre> array_hash_map.cpp<pre><code>/* キー値ペア */\nstruct Pair {\n public:\n int key;\n string val;\n Pair(int key, string val) {\n this->key = key;\n this->val = val;\n }\n};\n\n/* 配列実装に基づくハッシュテーブル */\nclass ArrayHashMap {\n private:\n vector<Pair *> buckets;\n\n public:\n ArrayHashMap() {\n // 配列を初期化、100個のバケットを含む\n buckets = vector<Pair *>(100);\n }\n\n ~ArrayHashMap() {\n // メモリを解放\n for (const auto &bucket : buckets) {\n delete bucket;\n }\n buckets.clear();\n }\n\n /* ハッシュ関数 */\n int hashFunc(int key) {\n int index = key % 100;\n return index;\n }\n\n /* クエリ操作 */\n string get(int key) {\n int index = hashFunc(key);\n Pair *pair = buckets[index];\n if (pair == nullptr)\n return \"\";\n return pair->val;\n }\n\n /* 追加操作 */\n void put(int key, string val) {\n Pair *pair = new Pair(key, val);\n int index = hashFunc(key);\n buckets[index] = pair;\n }\n\n /* 削除操作 */\n void remove(int key) {\n int index = hashFunc(key);\n // メモリを解放してnullptrに設定\n delete buckets[index];\n buckets[index] = nullptr;\n }\n\n /* すべてのキー値ペアを取得 */\n vector<Pair *> pairSet() {\n vector<Pair *> pairSet;\n for (Pair *pair : buckets) {\n if (pair != nullptr) {\n pairSet.push_back(pair);\n }\n }\n return pairSet;\n }\n\n /* すべてのキーを取得 */\n vector<int> keySet() {\n vector<int> keySet;\n for (Pair *pair : buckets) {\n if (pair != nullptr) {\n keySet.push_back(pair->key);\n }\n }\n return keySet;\n }\n\n /* すべての値を取得 */\n vector<string> valueSet() {\n vector<string> valueSet;\n for (Pair *pair : buckets) {\n if (pair != nullptr) {\n valueSet.push_back(pair->val);\n }\n }\n return valueSet;\n }\n\n /* ハッシュテーブルを印刷 */\n void print() {\n for (Pair *kv : pairSet()) {\n cout << kv->key << \" -> \" << kv->val << endl;\n }\n }\n};\n</code></pre> array_hash_map.java<pre><code>/* キー値ペア */\nclass Pair {\n public int key;\n public String val;\n\n public Pair(int key, String val) {\n this.key = key;\n this.val = val;\n }\n}\n\n/* 配列実装に基づくハッシュテーブル */\nclass ArrayHashMap {\n private List<Pair> buckets;\n\n public ArrayHashMap() {\n // 100個のバケットを含む配列を初期化\n buckets = new ArrayList<>();\n for (int i = 0; i < 100; i++) {\n buckets.add(null);\n }\n }\n\n /* ハッシュ関数 */\n private int hashFunc(int key) {\n int index = key % 100;\n return index;\n }\n\n /* クエリ操作 */\n public String get(int key) {\n int index = hashFunc(key);\n Pair pair = buckets.get(index);\n if (pair == null)\n return null;\n return pair.val;\n }\n\n /* 追加操作 */\n public void put(int key, String val) {\n Pair pair = new Pair(key, val);\n int index = hashFunc(key);\n buckets.set(index, pair);\n }\n\n /* 削除操作 */\n public void remove(int key) {\n int index = hashFunc(key);\n // nullに設定して削除を示す\n buckets.set(index, null);\n }\n\n /* すべてのキー値ペアを取得 */\n public List<Pair> pairSet() {\n List<Pair> pairSet = new ArrayList<>();\n for (Pair pair : buckets) {\n if (pair != null)\n pairSet.add(pair);\n }\n return pairSet;\n }\n\n /* すべてのキーを取得 */\n public List<Integer> keySet() {\n List<Integer> keySet = new ArrayList<>();\n for (Pair pair : buckets) {\n if (pair != null)\n keySet.add(pair.key);\n }\n return keySet;\n }\n\n /* すべての値を取得 */\n public List<String> valueSet() {\n List<String> valueSet = new ArrayList<>();\n for (Pair pair : buckets) {\n if (pair != null)\n valueSet.add(pair.val);\n }\n return valueSet;\n }\n\n /* ハッシュテーブルを印刷 */\n public void print() {\n for (Pair kv : pairSet()) {\n System.out.println(kv.key + \" -> \" + kv.val);\n }\n }\n}\n</code></pre> array_hash_map.cs<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.go<pre><code>[class]{pair}-[func]{}\n\n[class]{arrayHashMap}-[func]{}\n</code></pre> array_hash_map.swift<pre><code>[file]{utils/pair.swift}-[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.js<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.ts<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.dart<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.rs<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.c<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.kt<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre> array_hash_map.rb<pre><code>[class]{Pair}-[func]{}\n\n[class]{ArrayHashMap}-[func]{}\n</code></pre>","path":["第 6 章 ハッシュ表","6.1 ハッシュ表"],"tags":[]},{"location":"chapter_hashing/hash_map/#613","level":2,"title":"6.1.3 ハッシュ衝突とリサイズ","text":"<p>本質的に、ハッシュ関数の役割は、すべてのキーの入力空間全体を、すべての配列インデックスの出力空間にマッピングすることです。しかし、入力空間は出力空間よりもはるかに大きいことがよくあります。したがって、理論的には、「複数の入力が同じ出力に対応する」ケースが常に存在します。</p> <p>上記の例では、与えられたハッシュ関数で、入力<code>key</code>の下二桁が同じ場合、ハッシュ関数は同じ出力を生成します。例えば、学籍番号12836と20336の2人の学生をクエリすると、以下のことがわかります:</p> <pre><code>12836 % 100 = 36\n20336 % 100 = 36\n</code></pre> <p>下図に示すように、両方の学籍番号が同じ名前を指しており、これは明らかに間違っています。この複数の入力が同じ出力に対応する状況をハッシュ衝突と呼びます。</p> <p></p> <p> 図 6-3 ハッシュ衝突の例 </p> <p>ハッシュ表の容量\\(n\\)が増加するにつれて、複数のキーが同じバケットに割り当てられる確率が減少し、衝突が少なくなることは理解しやすいです。したがって、ハッシュ表をリサイズすることでハッシュ衝突を減らすことができます。</p> <p>下図に示すように、リサイズ前は、キー値ペア<code>(136, A)</code>と<code>(236, D)</code>が衝突していました。しかし、リサイズ後は衝突が解決されています。</p> <p></p> <p> 図 6-4 ハッシュ表のリサイズ </p> <p>配列の拡張と同様に、ハッシュ表のリサイズにはすべてのキー値ペアを元のハッシュ表から新しいものに移行する必要があり、時間がかかります。さらに、ハッシュ表の<code>capacity</code>が変更されるため、ハッシュ関数を使用してすべてのキー値ペアの格納位置を再計算する必要があり、リサイズプロセスの計算オーバーヘッドがさらに増加します。したがって、プログラミング言語は頻繁なリサイズを防ぐために、ハッシュ表に十分大きな容量を割り当てることがよくあります。</p> <p>負荷率はハッシュ表の重要な概念です。ハッシュ表内の要素数とバケット数の比率として定義されます。ハッシュ衝突の深刻度を測定するために使用され、しばしばハッシュ表のリサイズのトリガーとしても機能します。例えば、Javaでは、負荷率が\\(0.75\\)を超えると、システムはハッシュ表を元のサイズの2倍にリサイズします。</p>","path":["第 6 章 ハッシュ表","6.1 ハッシュ表"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4 まとめ","text":"","path":["第 6 章 ハッシュ表","6.4 まとめ"],"tags":[]},{"location":"chapter_hashing/summary/#1","level":3,"title":"1. 重要なポイント","text":"<ul> <li>入力<code>key</code>が与えられると、ハッシュ表は\\(O(1)\\)の時間で対応する<code>value</code>を取得でき、非常に効率的です。</li> <li>一般的なハッシュ表の操作には、クエリ、キー値ペアの追加、キー値ペアの削除、ハッシュ表の走査があります。</li> <li>ハッシュ関数は<code>key</code>を配列インデックスにマッピングし、対応するバケットにアクセスして<code>value</code>を取得できるようにします。</li> <li>2つの異なるキーがハッシュ化後に同じ配列インデックスになる場合があり、誤ったクエリ結果につながります。この現象はハッシュ衝突として知られています。</li> <li>ハッシュ表の容量が大きいほど、ハッシュ衝突の確率は低くなります。したがって、ハッシュ表のリサイズはハッシュ衝突を緩和できます。配列のリサイズと同様に、ハッシュ表のリサイズはコストが高いです。</li> <li>要素数をバケット数で割った負荷率は、ハッシュ衝突の深刻度を反映し、しばしばハッシュ表リサイズのトリガー条件として使用されます。</li> <li>連鎖法は各要素を連結リストに変換し、衝突するすべての要素を同じリストに格納することでハッシュ衝突に対処します。ただし、過度に長いリストはクエリ効率を低下させる可能性があり、リストを赤黒木に変換することで改善できます。</li> <li>オープンアドレス法は複数回のプローブを通してハッシュ衝突を処理します。線形プローブは固定ステップサイズを使用しますが、要素を削除できず、クラスタリングを起こしやすい傾向があります。多重ハッシュはプローブに複数のハッシュ関数を使用し、線形プローブと比較してクラスタリングを減らしますが、計算オーバーヘッドが増加します。</li> <li>異なるプログラミング言語はさまざまなハッシュ表実装を採用しています。例えば、Javaの<code>HashMap</code>は連鎖法を使用し、Pythonの<code>dict</code>はオープンアドレス法を採用しています。</li> <li>ハッシュ表では、決定性、高効率、均等分散を持つハッシュアルゴリズムが望まれます。暗号化では、ハッシュアルゴリズムは衝突耐性と雪崩効果も持つべきです。</li> <li>ハッシュアルゴリズムは通常、ハッシュ値の均等分散を保証し、ハッシュ衝突を減らすために、大きな素数を剰余として使用します。</li> <li>一般的なハッシュアルゴリズムには、MD5、SHA-1、SHA-2、SHA-3があります。MD5はファイル整合性チェックによく使用され、SHA-2は安全なアプリケーションとプロトコルで一般的に使用されます。</li> <li>プログラミング言語は通常、ハッシュ表のバケットインデックスを計算するために、データ型に対して組み込みのハッシュアルゴリズムを提供します。一般的に、不変オブジェクトのみがハッシュ可能です。</li> </ul>","path":["第 6 章 ハッシュ表","6.4 まとめ"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: ハッシュ表の時間計算量が\\(O(n)\\)に悪化するのはいつですか?</p> <p>ハッシュ表の時間計算量は、ハッシュ衝突が深刻な場合に\\(O(n)\\)に悪化する可能性があります。ハッシュ関数が適切に設計され、容量が適切に設定され、衝突が均等に分散されている場合、時間計算量は\\(O(1)\\)です。プログラミング言語の組み込みハッシュ表を使用する場合、通常は時間計算量を\\(O(1)\\)と考えます。</p> <p>Q: なぜハッシュ関数\\(f(x) = x\\)を使用しないのですか?これなら衝突を排除できます。</p> <p>ハッシュ関数\\(f(x) = x\\)では、各要素が一意のバケットインデックスに対応し、これは配列と同等です。しかし、入力空間は通常出力空間(配列長)よりもはるかに大きいため、ハッシュ関数の最後のステップは配列長の剰余を取ることがよくあります。言い換えると、ハッシュ表の目標は、\\(O(1)\\)のクエリ効率を提供しながら、より大きな状態空間をより小さなものにマッピングすることです。</p> <p>Q: ハッシュ表がこれらの構造を使って実装されているにもかかわらず、なぜ配列、連結リスト、二分木よりも効率的になれるのですか?</p> <p>まず、ハッシュ表は時間効率が高いですが、空間効率は低いです。ハッシュ表のメモリの大部分は未使用のままです。</p> <p>次に、ハッシュ表は特定のユースケースでのみ時間効率が高いです。配列や連結リストを使用して同じ時間計算量で機能を実装できる場合、通常はハッシュ表を使用するよりも高速です。これは、ハッシュ関数の計算がオーバーヘッドを発生させ、時間計算量の定数因子が大きくなるためです。</p> <p>最後に、ハッシュ表の時間計算量は悪化する可能性があります。例えば、連鎖法では、連結リストや赤黒木で検索操作を実行し、これは依然として\\(O(n)\\)時間に悪化するリスクがあります。</p> <p>Q: 多重ハッシュにも要素を直接削除できないという欠陥がありますか?削除としてマークされた空間は再利用できますか?</p> <p>多重ハッシュはオープンアドレス法の一形態であり、すべてのオープンアドレス法には要素を直接削除できないという欠点があります。要素を削除済みとしてマークする必要があります。マークされた空間は再利用できます。ハッシュ表に新しい要素を挿入する際、ハッシュ関数が削除済みとしてマークされた位置を指している場合、その位置は新しい要素によって使用できます。これにより、ハッシュ表のプローブシーケンスを維持しながら、空間の効率的な使用が保証されます。</p> <p>Q: なぜ線形プローブの検索プロセス中にハッシュ衝突が発生するのですか?</p> <p>検索プロセス中、ハッシュ関数は対応するバケットとキー値ペアを指します。<code>key</code>が一致しない場合、ハッシュ衝突を示します。したがって、線形プローブは正しいキー値ペアが見つかるか検索が失敗するまで、事前に決められたステップサイズで下方向に検索します。</p> <p>Q: なぜハッシュ表のリサイズがハッシュ衝突を緩和できるのですか?</p> <p>ハッシュ関数の最後のステップは、出力を配列インデックス範囲内に保つために、配列長\\(n\\)の剰余を取ることがよくあります。リサイズ時、配列長\\(n\\)が変化し、キーに対応するインデックスも変化する可能性があります。以前に同じバケットにマッピングされていたキーが、リサイズ後に複数のバケットに分散される可能性があり、それによってハッシュ衝突が緩和されます。</p>","path":["第 6 章 ハッシュ表","6.4 まとめ"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"第 8 章 ヒープ","text":"<p>Abstract</p> <p>ヒープは山とその険しい峰のように、層をなして起伏し、それぞれが独特の形を持っています。</p> <p>各山の頂は散らばった高さで上下しますが、最も高いものが常に最初に注目を集めます。</p>","path":["第 8 章 ヒープ"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"章の内容","text":"<ul> <li>8.1 ヒープ</li> <li>8.2 ヒープ構築操作</li> <li>8.3 Top-k 問題</li> <li>8.4 まとめ</li> </ul>","path":["第 8 章 ヒープ"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2 ヒープ構築操作","text":"<p>場合によっては、リストのすべての要素を使用してヒープを構築したいことがあり、このプロセスは「ヒープ構築操作」として知られています。</p>","path":["第 8 章 ヒープ","8.2 ヒープ構築操作"],"tags":[]},{"location":"chapter_heap/build_heap/#821","level":2,"title":"8.2.1 ヒープ挿入操作による実装","text":"<p>まず、空のヒープを作成し、次にリストを反復処理して、各要素に対して順番に「ヒープ挿入操作」を実行します。これは、要素をヒープの末尾に追加し、次に下から上に「ヒープ化」することを意味します。</p> <p>ヒープに要素が追加されるたびに、ヒープの長さは1つずつ増加します。ノードは二分木に上から下に追加されるため、ヒープは「上から下に」構築されます。</p> <p>要素数を\\(n\\)とすると、各要素の挿入操作は\\(O(\\log{n})\\)時間かかるため、このヒープ構築方法の時間計算量は\\(O(n \\log n)\\)です。</p>","path":["第 8 章 ヒープ","8.2 ヒープ構築操作"],"tags":[]},{"location":"chapter_heap/build_heap/#822","level":2,"title":"8.2.2 走査によるヒープ化の実装","text":"<p>実際には、2つのステップでより効率的なヒープ構築方法を実装できます。</p> <ol> <li>リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。</li> <li>ヒープを逆順(レベル順走査の逆)で走査し、各非葉ノードに対して「上から下のヒープ化」を実行します。</li> </ol> <p>ノードをヒープ化した後、そのノードを根とする部分木は有効な部分ヒープになります。走査が逆順であるため、ヒープは「下から上に」構築されます。</p> <p>逆走査を選択する理由は、現在のノードの下の部分木がすでに有効な部分ヒープであることを保証し、現在のノードのヒープ化を効果的にするためです。</p> <p>言及する価値があるのは、**葉ノードは子を持たないため、自然に有効な部分ヒープを形成し、ヒープ化する必要がない**ということです。以下のコードに示すように、最後の非葉ノードは最後のノードの親です。そこから開始して逆順に走査してヒープ化を実行します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def __init__(self, nums: list[int]):\n \"\"\"コンストラクタ、入力リストに基づいてヒープを構築\"\"\"\n # すべてのリスト要素をヒープに追加\n self.max_heap = nums\n # 葉以外のすべてのノードをヒープ化\n for i in range(self.parent(self.size() - 1), -1, -1):\n self.sift_down(i)\n</code></pre> my_heap.cpp<pre><code>/* コンストラクタ、入力リストに基づいてヒープを構築 */\nMaxHeap(vector<int> nums) {\n // すべてのリスト要素をヒープに追加\n maxHeap = nums;\n // 葉以外のすべてのノードをヒープ化\n for (int i = parent(size() - 1); i >= 0; i--) {\n siftDown(i);\n }\n}\n</code></pre> my_heap.java<pre><code>/* コンストラクタ、入力リストに基づいてヒープを構築 */\nMaxHeap(List<Integer> nums) {\n // すべてのリスト要素をヒープに追加\n maxHeap = new ArrayList<>(nums);\n // 葉を除くすべてのノードをヒープ化\n for (int i = parent(size() - 1); i >= 0; i--) {\n siftDown(i);\n }\n}\n</code></pre> my_heap.cs<pre><code>[class]{MaxHeap}-[func]{MaxHeap}\n</code></pre> my_heap.go<pre><code>[class]{maxHeap}-[func]{newMaxHeap}\n</code></pre> my_heap.swift<pre><code>[class]{MaxHeap}-[func]{init}\n</code></pre> my_heap.js<pre><code>[class]{MaxHeap}-[func]{constructor}\n</code></pre> my_heap.ts<pre><code>[class]{MaxHeap}-[func]{constructor}\n</code></pre> my_heap.dart<pre><code>[class]{MaxHeap}-[func]{MaxHeap}\n</code></pre> my_heap.rs<pre><code>[class]{MaxHeap}-[func]{new}\n</code></pre> my_heap.c<pre><code>[class]{MaxHeap}-[func]{newMaxHeap}\n</code></pre> my_heap.kt<pre><code>[class]{MaxHeap}-[func]{}\n</code></pre> my_heap.rb<pre><code>[class]{MaxHeap}-[func]{initialize}\n</code></pre>","path":["第 8 章 ヒープ","8.2 ヒープ構築操作"],"tags":[]},{"location":"chapter_heap/build_heap/#823","level":2,"title":"8.2.3 計算量分析","text":"<p>次に、この第2のヒープ構築方法の時間計算量を計算してみましょう。</p> <ul> <li>完備二分木のノード数を\\(n\\)と仮定すると、葉ノードの数は\\((n + 1) / 2\\)です。ここで\\(/\\) は整数除算です。したがって、ヒープ化が必要なノードの数は\\((n - 1) / 2\\)です。</li> <li>「上から下のヒープ化」のプロセスでは、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ\\(\\log n\\)です。</li> </ul> <p>この2つを掛け合わせると、ヒープ構築プロセスの時間計算量は\\(O(n \\log n)\\)となります。しかし、この推定は正確ではありません。二分木の下位レベルには上位よりもはるかに多くのノードがあるという性質を考慮していないからです。</p> <p>より正確な計算を行いましょう。計算を簡素化するため、\\(n\\)個のノードと高さ\\(h\\)を持つ「完全二分木」を仮定します。この仮定は結果の正確性に影響しません。</p> <p></p> <p> 図 8-5 完全二分木の各レベルのノード数 </p> <p>上図に示すように、ノードが「上から下にヒープ化される」最大反復回数は、そのノードから葉ノードまでの距離と等しく、これは正確に「ノードの高さ」です。したがって、各レベルで「ノード数×ノードの高さ」を合計して、**すべてのノードの総ヒープ化反復回数を得る**ことができます。</p> \\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\] <p>上記の方程式を簡素化するために、高校の数列の知識を使用する必要があります。まず\\(T(h)\\)に\\(2\\)を掛けて以下を得ます:</p> \\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^h\\times1 \\newline \\end{aligned} \\] <p>変位法を使用して\\(2T(h)\\)から\\(T(h)\\)を減算すると、以下を得ます:</p> \\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\] <p>方程式を観察すると、\\(T(h)\\)は等比数列であり、和の公式を使用して直接計算でき、時間計算量は以下になります:</p> \\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\] <p>さらに、高さ\\(h\\)の完全二分木は\\(n = 2^{h+1} - 1\\)個のノードを持つため、計算量は\\(O(2^h) = O(n)\\)です。この計算は、**リストを入力してヒープを構築する時間計算量が\\(O(n)\\)であり、非常に効率的である**ことを示しています。</p>","path":["第 8 章 ヒープ","8.2 ヒープ構築操作"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1 ヒープ","text":"<p>ヒープは特定の条件を満たす完備二分木で、主に次の2つのタイプに分類されます(下図参照)。</p> <ul> <li>最小ヒープ:任意のノードの値 \\(\\leq\\) その子ノードの値。</li> <li>最大ヒープ:任意のノードの値 \\(\\geq\\) その子ノードの値。</li> </ul> <p></p> <p> 図 8-1 最小ヒープと最大ヒープ </p> <p>完備二分木の特別なケースとして、ヒープには以下の特性があります:</p> <ul> <li>最下位層のノードは左から右に埋められ、他の層のノードは完全に埋められています。</li> <li>二分木の根ノードをヒープの「先頭」と呼び、最も右下のノードをヒープの「末尾」と呼びます。</li> <li>最大ヒープ(最小ヒープ)の場合、先頭要素(根)の値はすべての要素の中で最大(最小)です。</li> </ul>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#811","level":2,"title":"8.1.1 ヒープの一般的な操作","text":"<p>多くのプログラミング言語が優先度キューを提供していることに注意してください。これは優先度付きソートを持つキューとして定義される抽象データ構造です。</p> <p>実際には、ヒープは優先度キューを実装するためによく使用されます。最大ヒープは、要素が降順でデキューされる優先度キューに対応します。使用の観点から、「優先度キュー」と「ヒープ」を同等のデータ構造と考えることができます。したがって、この本では両者を特別に区別せず、統一して「ヒープ」と呼びます。</p> <p>ヒープの一般的な操作を下表に示します。メソッド名はプログラミング言語によって異なる場合があります。</p> <p> 表 8-1 ヒープ操作の効率 </p> メソッド名 説明 時間計算量 <code>push()</code> ヒープに要素を追加 \\(O(\\log n)\\) <code>pop()</code> ヒープから先頭要素を削除 \\(O(\\log n)\\) <code>peek()</code> 先頭要素にアクセス(最大/最小ヒープの場合、最大/最小値) \\(O(1)\\) <code>size()</code> ヒープ内の要素数を取得 \\(O(1)\\) <code>isEmpty()</code> ヒープが空かどうかをチェック \\(O(1)\\) <p>実際には、プログラミング言語によって提供されるヒープクラス(または優先度キュークラス)を直接使用できます。</p> <p>ソートアルゴリズムで「昇順」と「降順」があるように、<code>flag</code>を設定するか<code>Comparator</code>を変更することで「最小ヒープ」と「最大ヒープ」を切り替えることができます。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py<pre><code># 最小ヒープの初期化\nmin_heap, flag = [], 1\n# 最大ヒープの初期化\nmax_heap, flag = [], -1\n\n# Pythonのheapqモジュールはデフォルトで最小ヒープを実装\n# 要素をヒープにプッシュする前に負の値にすることで、順序を反転させ、最大ヒープを実装\n# この例では、flag = 1は最小ヒープに対応し、flag = -1は最大ヒープに対応\n\n# ヒープに要素をプッシュ\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# ヒープの先頭要素を取得\npeek: int = flag * max_heap[0] # 5\n\n# ヒープの先頭要素をポップ\n# ポップされた要素は降順のシーケンスを形成\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# ヒープのサイズを取得\nsize: int = len(max_heap)\n\n# ヒープが空かどうかをチェック\nis_empty: bool = not max_heap\n\n# リストからヒープを作成\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n</code></pre> heap.cpp<pre><code>/* ヒープの初期化 */\n// 最小ヒープの初期化\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// 最大ヒープの初期化\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* ヒープに要素をプッシュ */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* ヒープの先頭要素を取得 */\nint peek = maxHeap.top(); // 5\n\n/* ヒープの先頭要素をポップ */\n// ポップされた要素は降順のシーケンスを形成\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.size();\n\n/* ヒープが空かどうかをチェック */\nbool isEmpty = maxHeap.empty();\n\n/* リストからヒープを作成 */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n</code></pre> heap.java<pre><code>/* ヒープの初期化 */\n// 最小ヒープの初期化\nQueue<Integer> minHeap = new PriorityQueue<>();\n// 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* ヒープに要素をプッシュ */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* ヒープの先頭要素を取得 */\nint peek = maxHeap.peek(); // 5\n\n/* ヒープの先頭要素をポップ */\n// ポップされた要素は降順のシーケンスを形成\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.size();\n\n/* ヒープが空かどうかをチェック */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* リストからヒープを作成 */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n</code></pre> heap.cs<pre><code>/* ヒープの初期化 */\n// 最小ヒープの初期化\nPriorityQueue<int, int> minHeap = new();\n// 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y - x));\n\n/* ヒープに要素をプッシュ */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* ヒープの先頭要素を取得 */\nint peek = maxHeap.Peek();//5\n\n/* ヒープの先頭要素をポップ */\n// ポップされた要素は降順のシーケンスを形成\npeek = maxHeap.Dequeue(); // 5\npeek = maxHeap.Dequeue(); // 4\npeek = maxHeap.Dequeue(); // 3\npeek = maxHeap.Dequeue(); // 2\npeek = maxHeap.Dequeue(); // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.Count;\n\n/* ヒープが空かどうかをチェック */\nbool isEmpty = maxHeap.Count == 0;\n\n/* リストからヒープを作成 */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n</code></pre> heap.go<pre><code>// Goでは、heap.Interfaceを実装することで整数の最大ヒープを構築できます\n// heap.Interfaceを実装するには、sort.Interfaceも実装する必要があります\ntype intHeap []any\n\n// heap.InterfaceのPushメソッド、要素をヒープにプッシュ\nfunc (h *intHeap) Push(x any) {\n // PushとPopの両方でポインタレシーバーを使用\n // スライスの要素を調整するだけでなく、その長さも変更するため\n *h = append(*h, x.(int))\n}\n\n// heap.InterfaceのPopメソッド、ヒープの先頭要素を削除\nfunc (h *intHeap) Pop() any {\n // ヒープからポップする要素は末尾に格納\n last := (*h)[len(*h)-1]\n *h = (*h)[:len(*h)-1]\n return last\n}\n\n// sort.InterfaceのLenメソッド\nfunc (h *intHeap) Len() int {\n return len(*h)\n}\n\n// sort.InterfaceのLessメソッド\nfunc (h *intHeap) Less(i, j int) bool {\n // 最小ヒープを実装したい場合は、これを小なり比較に変更\n return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// sort.InterfaceのSwapメソッド\nfunc (h *intHeap) Swap(i, j int) {\n (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top ヒープの先頭要素を取得\nfunc (h *intHeap) Top() any {\n return (*h)[0]\n}\n\n/* ドライバーコード */\nfunc TestHeap(t *testing.T) {\n /* ヒープの初期化 */\n // 最大ヒープの初期化\n maxHeap := &intHeap{}\n heap.Init(maxHeap)\n /* ヒープに要素をプッシュ */\n // heap.Interfaceのメソッドを呼び出して要素を追加\n heap.Push(maxHeap, 1)\n heap.Push(maxHeap, 3)\n heap.Push(maxHeap, 2)\n heap.Push(maxHeap, 4)\n heap.Push(maxHeap, 5)\n\n /* ヒープの先頭要素を取得 */\n top := maxHeap.Top()\n fmt.Printf(\"ヒープの先頭要素は %d\\n\", top)\n\n /* ヒープの先頭要素をポップ */\n // heap.Interfaceのメソッドを呼び出して要素を削除\n heap.Pop(maxHeap) // 5\n heap.Pop(maxHeap) // 4\n heap.Pop(maxHeap) // 3\n heap.Pop(maxHeap) // 2\n heap.Pop(maxHeap) // 1\n\n /* ヒープのサイズを取得 */\n size := len(*maxHeap)\n fmt.Printf(\"ヒープ内の要素数は %d\\n\", size)\n\n /* ヒープが空かどうかをチェック */\n isEmpty := len(*maxHeap) == 0\n fmt.Printf(\"ヒープは空ですか? %t\\n\", isEmpty)\n}\n</code></pre> heap.swift<pre><code>/* ヒープの初期化 */\n// SwiftのHeap型は最大ヒープと最小ヒープの両方をサポートし、swift-collectionsライブラリが必要\nvar heap = Heap<Int>()\n\n/* ヒープに要素をプッシュ */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* ヒープの先頭要素を取得 */\nvar peek = heap.max()!\n\n/* ヒープの先頭要素をポップ */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* ヒープのサイズを取得 */\nlet size = heap.count\n\n/* ヒープが空かどうかをチェック */\nlet isEmpty = heap.isEmpty\n\n/* リストからヒープを作成 */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n</code></pre> heap.js<pre><code>// JavaScriptは組み込みのHeapクラスを提供していません\n</code></pre> heap.ts<pre><code>// TypeScriptは組み込みのHeapクラスを提供していません\n</code></pre> heap.dart<pre><code>// Dartは組み込みのHeapクラスを提供していません\n</code></pre> heap.rs<pre><code>use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* ヒープの初期化 */\n// 最小ヒープの初期化\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// 最大ヒープの初期化\nlet mut max_heap = BinaryHeap::new();\n\n/* ヒープに要素をプッシュ */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* ヒープの先頭要素を取得 */\nlet peek = max_heap.peek().unwrap(); // 5\n\n/* ヒープの先頭要素をポップ */\n// ポップされた要素は降順のシーケンスを形成\nlet peek = max_heap.pop().unwrap(); // 5\nlet peek = max_heap.pop().unwrap(); // 4\nlet peek = max_heap.pop().unwrap(); // 3\nlet peek = max_heap.pop().unwrap(); // 2\nlet peek = max_heap.pop().unwrap(); // 1\n\n/* ヒープのサイズを取得 */\nlet size = max_heap.len();\n\n/* ヒープが空かどうかをチェック */\nlet is_empty = max_heap.is_empty();\n\n/* リストからヒープを作成 */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n</code></pre> heap.c<pre><code>// Cは組み込みのHeapクラスを提供していません\n</code></pre> heap.kt<pre><code>/* ヒープの初期化 */\n// 最小ヒープの初期化\nvar minHeap = PriorityQueue<Int>()\n// 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* ヒープに要素をプッシュ */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* ヒープの先頭要素を取得 */\nvar peek = maxHeap.peek() // 5\n\n/* ヒープの先頭要素をポップ */\n// ポップされた要素は降順のシーケンスを形成\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* ヒープのサイズを取得 */\nval size = maxHeap.size\n\n/* ヒープが空かどうかをチェック */\nval isEmpty = maxHeap.isEmpty()\n\n/* リストからヒープを作成 */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n</code></pre> heap.rb<pre><code>\n</code></pre>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#812","level":2,"title":"8.1.2 ヒープの実装","text":"<p>以下の実装は最大ヒープです。最小ヒープに変換するには、すべてのサイズ論理比較を反転させるだけです(例えば、\\(\\geq\\)を\\(\\leq\\)に置き換える)。興味のある読者は自分で実装することをお勧めします。</p>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#1","level":3,"title":"1. ヒープの格納と表現","text":"<p>「二分木」の節で述べたように、完備二分木は配列表現に非常に適しています。ヒープは完備二分木の一種なので、配列を使用してヒープを格納します。</p> <p>配列を使用して二分木を表現する場合、要素はノード値を表し、インデックスは二分木内のノード位置を表します。ノードポインタはインデックスマッピング公式を通じて実装されます。</p> <p>下図に示すように、インデックス\\(i\\)が与えられた場合、その左の子のインデックスは\\(2i + 1\\)、右の子のインデックスは\\(2i + 2\\)、親のインデックスは\\((i - 1) / 2\\)(床除算)です。インデックスが範囲外の場合、nullノードまたはノードが存在しないことを意味します。</p> <p></p> <p> 図 8-2 ヒープの表現と格納 </p> <p>後で便利に使用するため、インデックスマッピング公式を関数にカプセル化できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def left(self, i: int) -> int:\n \"\"\"左の子ノードのインデックスを取得\"\"\"\n return 2 * i + 1\n\ndef right(self, i: int) -> int:\n \"\"\"右の子ノードのインデックスを取得\"\"\"\n return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n \"\"\"親ノードのインデックスを取得\"\"\"\n return (i - 1) // 2 # 整数除算で切り下げ\n</code></pre> my_heap.cpp<pre><code>/* 左の子ノードのインデックスを取得 */\nint left(int i) {\n return 2 * i + 1;\n}\n\n/* 右の子ノードのインデックスを取得 */\nint right(int i) {\n return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(int i) {\n return (i - 1) / 2; // 整数除算で切り下げ\n}\n</code></pre> my_heap.java<pre><code>/* 左の子ノードのインデックスを取得 */\nint left(int i) {\n return 2 * i + 1;\n}\n\n/* 右の子ノードのインデックスを取得 */\nint right(int i) {\n return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(int i) {\n return (i - 1) / 2; // 整数除算で切り下げ\n}\n</code></pre> my_heap.cs<pre><code>[class]{MaxHeap}-[func]{Left}\n\n[class]{MaxHeap}-[func]{Right}\n\n[class]{MaxHeap}-[func]{Parent}\n</code></pre> my_heap.go<pre><code>[class]{maxHeap}-[func]{left}\n\n[class]{maxHeap}-[func]{right}\n\n[class]{maxHeap}-[func]{parent}\n</code></pre> my_heap.swift<pre><code>[class]{MaxHeap}-[func]{left}\n\n[class]{MaxHeap}-[func]{right}\n\n[class]{MaxHeap}-[func]{parent}\n</code></pre> my_heap.js<pre><code>[class]{MaxHeap}-[func]{left}\n\n[class]{MaxHeap}-[func]{right}\n\n[class]{MaxHeap}-[func]{parent}\n</code></pre> my_heap.ts<pre><code>[class]{MaxHeap}-[func]{left}\n\n[class]{MaxHeap}-[func]{right}\n\n[class]{MaxHeap}-[func]{parent}\n</code></pre> my_heap.dart<pre><code>[class]{MaxHeap}-[func]{_left}\n\n[class]{MaxHeap}-[func]{_right}\n\n[class]{MaxHeap}-[func]{_parent}\n</code></pre> my_heap.rs<pre><code>[class]{MaxHeap}-[func]{left}\n\n[class]{MaxHeap}-[func]{right}\n\n[class]{MaxHeap}-[func]{parent}\n</code></pre> my_heap.c<pre><code>[class]{MaxHeap}-[func]{left}\n\n[class]{MaxHeap}-[func]{right}\n\n[class]{MaxHeap}-[func]{parent}\n</code></pre> my_heap.kt<pre><code>[class]{MaxHeap}-[func]{left}\n\n[class]{MaxHeap}-[func]{right}\n\n[class]{MaxHeap}-[func]{parent}\n</code></pre> my_heap.rb<pre><code>[class]{MaxHeap}-[func]{left}\n\n[class]{MaxHeap}-[func]{right}\n\n[class]{MaxHeap}-[func]{parent}\n</code></pre>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#2","level":3,"title":"2. ヒープの先頭要素へのアクセス","text":"<p>ヒープの先頭要素は二分木の根ノードで、リストの最初の要素でもあります:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def peek(self) -> int:\n \"\"\"ヒープの先頭要素にアクセス\"\"\"\n return self.max_heap[0]\n</code></pre> my_heap.cpp<pre><code>/* ヒープの先頭要素にアクセス */\nint peek() {\n return maxHeap[0];\n}\n</code></pre> my_heap.java<pre><code>/* ヒープの先頭要素にアクセス */\nint peek() {\n return maxHeap.get(0);\n}\n</code></pre> my_heap.cs<pre><code>[class]{MaxHeap}-[func]{Peek}\n</code></pre> my_heap.go<pre><code>[class]{maxHeap}-[func]{peek}\n</code></pre> my_heap.swift<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre> my_heap.js<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre> my_heap.ts<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre> my_heap.dart<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre> my_heap.rs<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre> my_heap.c<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre> my_heap.kt<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre> my_heap.rb<pre><code>[class]{MaxHeap}-[func]{peek}\n</code></pre>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#3","level":3,"title":"3. ヒープへの要素挿入","text":"<p>要素<code>val</code>が与えられた場合、まずそれをヒープの底に追加します。追加後、<code>val</code>がヒープ内の他の要素より大きい可能性があるため、ヒープの完全性が損なわれる可能性があります。したがって、挿入されたノードから根ノードまでのパスを修復する必要があります。この操作はヒープ化と呼ばれます。</p> <p>挿入されたノードから開始して、下から上にヒープ化を実行します。下図に示すように、挿入されたノードの値をその親ノードと比較し、挿入されたノードが大きい場合はそれらを交換します。次にこの操作を続行し、根に到達するか、交換が不要なノードに遭遇するまで、下から上にヒープ内の各ノードを修復します。</p> <1><2><3><4><5><6><7><8><9> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 8-3 ヒープへの要素挿入の手順 </p> <p>総ノード数を\\(n\\)とすると、木の高さは\\(O(\\log n)\\)です。したがって、ヒープ化操作のループ反復回数は最大\\(O(\\log n)\\)で、要素挿入操作の時間計算量は\\(O(\\log n)\\)になります。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def push(self, val: int):\n \"\"\"ヒープに要素をプッシュ\"\"\"\n # ノードを追加\n self.max_heap.append(val)\n # 下から上へヒープ化\n self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n \"\"\"ノードiから開始して、下から上へヒープ化\"\"\"\n while True:\n # ノードiの親ノードを取得\n p = self.parent(i)\n # 「ルートノードを越える」または「ノードが修復不要」の場合、ヒープ化を終了\n if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n break\n # 2つのノードを交換\n self.swap(i, p)\n # 上向きのループヒープ化\n i = p\n</code></pre> my_heap.cpp<pre><code>/* ヒープに要素をプッシュ */\nvoid push(int val) {\n // ノードを追加\n maxHeap.push_back(val);\n // 下から上へヒープ化\n siftUp(size() - 1);\n}\n\n/* ノードiから上向きにヒープ化を開始 */\nvoid siftUp(int i) {\n while (true) {\n // ノードiの親ノードを取得\n int p = parent(i);\n // 「ルートノードを超える」または「ノードが修復不要」の場合、ヒープ化を終了\n if (p < 0 || maxHeap[i] <= maxHeap[p])\n break;\n // 2つのノードを交換\n swap(maxHeap[i], maxHeap[p]);\n // 上向きにループしてヒープ化\n i = p;\n }\n}\n</code></pre> my_heap.java<pre><code>/* 要素をヒープにプッシュ */\nvoid push(int val) {\n // ノードを追加\n maxHeap.add(val);\n // 下から上へヒープ化\n siftUp(size() - 1);\n}\n\n/* ノード i から上向きにヒープ化を開始 */\nvoid siftUp(int i) {\n while (true) {\n // ノード i の親ノードを取得\n int p = parent(i);\n // 「根ノードを越える」または「ノードが修復不要」の場合、ヒープ化を終了\n if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n break;\n // 2つのノードを交換\n swap(i, p);\n // 上向きにヒープ化をループ\n i = p;\n }\n}\n</code></pre> my_heap.cs<pre><code>[class]{MaxHeap}-[func]{Push}\n\n[class]{MaxHeap}-[func]{SiftUp}\n</code></pre> my_heap.go<pre><code>[class]{maxHeap}-[func]{push}\n\n[class]{maxHeap}-[func]{siftUp}\n</code></pre> my_heap.swift<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{siftUp}\n</code></pre> my_heap.js<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{siftUp}\n</code></pre> my_heap.ts<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{siftUp}\n</code></pre> my_heap.dart<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{siftUp}\n</code></pre> my_heap.rs<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{sift_up}\n</code></pre> my_heap.c<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{siftUp}\n</code></pre> my_heap.kt<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{siftUp}\n</code></pre> my_heap.rb<pre><code>[class]{MaxHeap}-[func]{push}\n\n[class]{MaxHeap}-[func]{sift_up}\n</code></pre>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#4","level":3,"title":"4. ヒープからの先頭要素削除","text":"<p>ヒープの先頭要素は二分木の根ノード、つまりリストの最初の要素です。リストから最初の要素を直接削除すると、二分木内のすべてのノードインデックスが変更され、後続の修復にヒープ化を使用することが困難になります。要素インデックスの変更を最小限に抑えるため、次の手順を使用します。</p> <ol> <li>ヒープの先頭要素と底の要素を交換します(根ノードと最も右の葉ノードを交換)。</li> <li>交換後、リストからヒープの底を削除します(交換されているため、実際には元の先頭要素が削除される)。</li> <li>根ノードから開始して、上から下にヒープ化を実行します。</li> </ol> <p>下図に示すように、「上から下のヒープ化」の方向は「下から上のヒープ化」と反対です。根ノードの値をその2つの子と比較し、最大の子と交換します。次に、葉ノードに到達するか、交換が不要なノードに遭遇するまで、この操作を繰り返します。</p> <1><2><3><4><5><6><7><8><9><10> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 8-4 ヒープからの先頭要素削除の手順 </p> <p>要素挿入操作と同様に、先頭要素削除操作の時間計算量も\\(O(\\log n)\\)です。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def pop(self) -> int:\n \"\"\"要素をヒープから出す\"\"\"\n # 空の処理\n if self.is_empty():\n raise IndexError(\"Heap is empty\")\n # ルートノードと最右端の葉ノードを交換(最初の要素と最後の要素を交換)\n self.swap(0, self.size() - 1)\n # ノードを削除\n val = self.max_heap.pop()\n # 上から下へヒープ化\n self.sift_down(0)\n # ヒープの先頭要素を返す\n return val\n\ndef sift_down(self, i: int):\n \"\"\"ノードiから開始して、上から下へヒープ化\"\"\"\n while True:\n # i、l、rの中で最大のノードを決定し、maとする\n l, r, ma = self.left(i), self.right(i), i\n if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n ma = l\n if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n ma = r\n # ノードiが最大またはインデックスl、rが範囲外の場合、さらなるヒープ化は不要、ブレーク\n if ma == i:\n break\n # 2つのノードを交換\n self.swap(i, ma)\n # 下向きのループヒープ化\n i = ma\n</code></pre> my_heap.cpp<pre><code>/* 要素がヒープから退出 */\nvoid pop() {\n // 空の処理\n if (isEmpty()) {\n throw out_of_range(\"Heap is empty\");\n }\n // ルートノードを最も右の葉ノードと交換(最初の要素と最後の要素を交換)\n swap(maxHeap[0], maxHeap[size() - 1]);\n // ノードを削除\n maxHeap.pop_back();\n // 上から下へヒープ化\n siftDown(0);\n}\n\n/* ノードiから下向きにヒープ化を開始 */\nvoid siftDown(int i) {\n while (true) {\n // i、l、rの中で最大のノードを決定し、maとして記録\n int l = left(i), r = right(i), ma = i;\n if (l < size() && maxHeap[l] > maxHeap[ma])\n ma = l;\n if (r < size() && maxHeap[r] > maxHeap[ma])\n ma = r;\n // ノードiが最大、またはインデックスl、rが範囲外の場合、これ以上のヒープ化は不要、ブレーク\n if (ma == i)\n break;\n swap(maxHeap[i], maxHeap[ma]);\n // 下向きにループしてヒープ化\n i = ma;\n }\n}\n</code></pre> my_heap.java<pre><code>/* 要素がヒープから退出 */\nint pop() {\n // 空の処理\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n // 根ノードを最も右の葉ノードと交換(最初の要素を最後の要素と交換)\n swap(0, size() - 1);\n // ノードを削除\n int val = maxHeap.remove(size() - 1);\n // 上から下へヒープ化\n siftDown(0);\n // ヒープの先頭要素を返す\n return val;\n}\n\n/* ノード i から下向きにヒープ化を開始 */\nvoid siftDown(int i) {\n while (true) {\n // i、l、r の中で最大のノードを決定し、ma とする\n int l = left(i), r = right(i), ma = i;\n if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n ma = l;\n if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n ma = r;\n // ノード i が最大の場合、またはインデックス l、r が範囲外の場合、さらなるヒープ化は不要、終了\n if (ma == i)\n break;\n // 2つのノードを交換\n swap(i, ma);\n // 下向きにヒープ化をループ\n i = ma;\n }\n}\n</code></pre> my_heap.cs<pre><code>[class]{MaxHeap}-[func]{Pop}\n\n[class]{MaxHeap}-[func]{SiftDown}\n</code></pre> my_heap.go<pre><code>[class]{maxHeap}-[func]{pop}\n\n[class]{maxHeap}-[func]{siftDown}\n</code></pre> my_heap.swift<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{siftDown}\n</code></pre> my_heap.js<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{siftDown}\n</code></pre> my_heap.ts<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{siftDown}\n</code></pre> my_heap.dart<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{siftDown}\n</code></pre> my_heap.rs<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{sift_down}\n</code></pre> my_heap.c<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{siftDown}\n</code></pre> my_heap.kt<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{siftDown}\n</code></pre> my_heap.rb<pre><code>[class]{MaxHeap}-[func]{pop}\n\n[class]{MaxHeap}-[func]{sift_down}\n</code></pre>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#813","level":2,"title":"8.1.3 ヒープの一般的な応用","text":"<ul> <li>優先度キュー:ヒープは優先度キューを実装するための好ましいデータ構造で、エンキュー操作とデキュー操作の両方の時間計算量が\\(O(\\log n)\\)、キュー構築の時間計算量が\\(O(n)\\)で、すべて非常に効率的です。</li> <li>ヒープソート:データセットが与えられた場合、それらからヒープを作成し、次に要素削除操作を継続的に実行して順序付けされたデータを取得できます。ただし、ヒープソートを実装するより洗練された方法があり、「ヒープソート」の章で説明されています。</li> <li>最大\\(k\\)個の要素の発見:これは古典的なアルゴリズム問題であり、一般的な使用例でもあります。Weiboホット検索のトップ10ホットニュースの選択や、トップ10の売れ筋商品の選択などです。</li> </ul>","path":["第 8 章 ヒープ","8.1 ヒープ"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4 まとめ","text":"","path":["第 8 章 ヒープ","8.4 まとめ"],"tags":[]},{"location":"chapter_heap/summary/#1","level":3,"title":"1. 重要な復習","text":"<ul> <li>ヒープは完備二分木で、その構築性質に基づいて最大ヒープまたは最小ヒープに分類できます。最大ヒープの先頭要素は最大で、最小ヒープの先頭要素は最小です。</li> <li>優先度キューは、デキューの優先度を持つキューとして定義され、通常ヒープを使用して実装されます。</li> <li>ヒープの一般的な操作とそれに対応する時間計算量には以下があります:ヒープへの要素挿入\\(O(\\log n)\\)、ヒープからの先頭要素削除\\(O(\\log n)\\)、ヒープの先頭要素へのアクセス\\(O(1)\\)。</li> <li>完備二分木は配列で表現するのに適しているため、ヒープは一般的に配列を使用して格納されます。</li> <li>ヒープ化操作はヒープの性質を維持するために使用され、ヒープの挿入操作と削除操作の両方で使用されます。</li> <li>\\(n\\)個の要素が入力として与えられた場合のヒープ構築の時間計算量は\\(O(n)\\)に最適化でき、これは非常に効率的です。</li> <li>Top-kは古典的なアルゴリズム問題で、ヒープデータ構造を使用して効率的に解決でき、時間計算量は\\(O(n \\log k)\\)です。</li> </ul>","path":["第 8 章 ヒープ","8.4 まとめ"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか?</p> <p>この2つは、どちらも「ヒープ」と呼ばれますが、同じ概念ではありません。コンピュータシステムメモリのヒープは動的メモリ割り当ての一部で、プログラムが実行中にデータを格納するために使用できます。プログラムは、オブジェクトや配列などの複雑な構造を格納するために、一定量のヒープメモリを要求できます。割り当てられたデータが不要になったときは、メモリリークを防ぐためにプログラムがこのメモリを解放する必要があります。スタックメモリと比較して、ヒープメモリの管理と使用にはより多くの注意が必要で、不適切な使用はメモリリークやダングリングポインタにつながる可能性があります。</p>","path":["第 8 章 ヒープ","8.4 まとめ"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3 Top-k問題","text":"<p>Question</p> <p>長さ\\(n\\)の順序付けられていない配列<code>nums</code>が与えられたとき、配列内の最大\\(k\\)個の要素を返してください。</p> <p>この問題について、まず2つの直接的な解法を紹介し、次により効率的なヒープベースの方法を説明します。</p>","path":["第 8 章 ヒープ","8.3 Top-k問題"],"tags":[]},{"location":"chapter_heap/top_k/#831-1","level":2,"title":"8.3.1 方法1:反復選択","text":"<p>下図に示すように、\\(k\\)回の反復を実行し、各回で\\(1\\)番目、\\(2\\)番目、\\(\\dots\\)、\\(k\\)番目に大きい要素を抽出できます。時間計算量は\\(O(nk)\\)です。</p> <p>この方法は\\(k \\ll n\\)の場合にのみ適しています。\\(k\\)が\\(n\\)に近い場合、時間計算量は\\(O(n^2)\\)に近づき、非常に時間がかかります。</p> <p></p> <p> 図 8-6 最大k個の要素を反復的に見つける </p> <p>Tip</p> <p>\\(k = n\\)の場合、完全に順序付けられたシーケンスを得ることができ、これは「選択ソート」アルゴリズムと同等です。</p>","path":["第 8 章 ヒープ","8.3 Top-k問題"],"tags":[]},{"location":"chapter_heap/top_k/#832-2","level":2,"title":"8.3.2 方法2:ソート","text":"<p>下図に示すように、まず配列<code>nums</code>をソートし、次に最後の\\(k\\)個の要素を返すことができます。時間計算量は\\(O(n \\log n)\\)です。</p> <p>明らかに、この方法はタスクを「やりすぎ」ています。最大\\(k\\)個の要素を見つけるだけでよく、他の要素をソートする必要はありません。</p> <p></p> <p> 図 8-7 ソートによる最大k個の要素の発見 </p>","path":["第 8 章 ヒープ","8.3 Top-k問題"],"tags":[]},{"location":"chapter_heap/top_k/#833-3","level":2,"title":"8.3.3 方法3:ヒープ","text":"<p>以下のプロセスに示すように、ヒープに基づいてTop-k問題をより効率的に解決できます。</p> <ol> <li>最小ヒープを初期化します。先頭要素が最小になります。</li> <li>まず、配列の最初の\\(k\\)個の要素をヒープに挿入します。</li> <li>\\(k + 1\\)番目の要素から開始し、現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに挿入します。</li> <li>走査を完了した後、ヒープには最大\\(k\\)個の要素が含まれています。</li> </ol> <1><2><3><4><5><6><7><8><9> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 8-8 ヒープに基づく最大k個の要素の発見 </p> <p>サンプルコードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py<pre><code>def top_k_heap(nums: list[int], k: int) -> list[int]:\n \"\"\"ヒープを使用して配列内の最大k個の要素を見つける\"\"\"\n # 最小ヒープを初期化\n heap = []\n # 配列の最初のk個の要素をヒープに入力\n for i in range(k):\n heapq.heappush(heap, nums[i])\n # k+1番目の要素から、ヒープの長さをkに保つ\n for i in range(k, len(nums)):\n # 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力\n if nums[i] > heap[0]:\n heapq.heappop(heap)\n heapq.heappush(heap, nums[i])\n return heap\n</code></pre> top_k.cpp<pre><code>/* ヒープを使用して配列内の最大k個の要素を見つける */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n // 最小ヒープを初期化\n priority_queue<int, vector<int>, greater<int>> heap;\n // 配列の最初のk個の要素をヒープに入力\n for (int i = 0; i < k; i++) {\n heap.push(nums[i]);\n }\n // k+1番目の要素から、ヒープの長さをkに保つ\n for (int i = k; i < nums.size(); i++) {\n // 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力\n if (nums[i] > heap.top()) {\n heap.pop();\n heap.push(nums[i]);\n }\n }\n return heap;\n}\n</code></pre> top_k.java<pre><code>/* ヒープを使用して配列内の最大 k 個の要素を検索 */\nQueue<Integer> topKHeap(int[] nums, int k) {\n // 最小ヒープを初期化\n Queue<Integer> heap = new PriorityQueue<Integer>();\n // 配列の最初の k 個の要素をヒープに入力\n for (int i = 0; i < k; i++) {\n heap.offer(nums[i]);\n }\n // k+1 番目の要素から、ヒープの長さを k に保つ\n for (int i = k; i < nums.length; i++) {\n // 現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに入力\n if (nums[i] > heap.peek()) {\n heap.poll();\n heap.offer(nums[i]);\n }\n }\n return heap;\n}\n</code></pre> top_k.cs<pre><code>[class]{top_k}-[func]{TopKHeap}\n</code></pre> top_k.go<pre><code>[class]{}-[func]{topKHeap}\n</code></pre> top_k.swift<pre><code>[class]{}-[func]{topKHeap}\n</code></pre> top_k.js<pre><code>[class]{}-[func]{pushMinHeap}\n\n[class]{}-[func]{popMinHeap}\n\n[class]{}-[func]{peekMinHeap}\n\n[class]{}-[func]{getMinHeap}\n\n[class]{}-[func]{topKHeap}\n</code></pre> top_k.ts<pre><code>[class]{}-[func]{pushMinHeap}\n\n[class]{}-[func]{popMinHeap}\n\n[class]{}-[func]{peekMinHeap}\n\n[class]{}-[func]{getMinHeap}\n\n[class]{}-[func]{topKHeap}\n</code></pre> top_k.dart<pre><code>[class]{}-[func]{topKHeap}\n</code></pre> top_k.rs<pre><code>[class]{}-[func]{top_k_heap}\n</code></pre> top_k.c<pre><code>[class]{}-[func]{pushMinHeap}\n\n[class]{}-[func]{popMinHeap}\n\n[class]{}-[func]{peekMinHeap}\n\n[class]{}-[func]{getMinHeap}\n\n[class]{}-[func]{topKHeap}\n</code></pre> top_k.kt<pre><code>[class]{}-[func]{topKHeap}\n</code></pre> top_k.rb<pre><code>[class]{}-[func]{top_k_heap}\n</code></pre> <p>合計\\(n\\)回のヒープ挿入と削除が実行され、最大ヒープサイズが\\(k\\)であるため、時間計算量は\\(O(n \\log k)\\)です。この方法は非常に効率的で、\\(k\\)が小さい場合、時間計算量は\\(O(n)\\)に近づき、\\(k\\)が大きい場合でも、時間計算量は\\(O(n \\log n)\\)を超えません。</p> <p>さらに、この方法は動的データストリームのシナリオに適しています。データを継続的に追加することで、ヒープ内の要素を維持し、最大\\(k\\)個の要素の動的更新を実現できます。</p>","path":["第 8 章 ヒープ","8.3 Top-k問題"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"はじめに","text":"<p>数年前、私はLeetCodeで「剣指Offer」の問題解答を共有し、多くの読者から励ましとサポートを受けました。読者とのやり取りの中で、最もよく聞かれた質問は「アルゴリズムの勉強をどう始めたらよいか」でした。次第に、私はこの質問に強い関心を抱くようになりました。</p> <p>問題を直接解くことが最も人気のある方法のようです。これはシンプルで直接的で効果的です。しかし、問題解決はマインスイーパーをプレイするようなものです。自学自習の能力が高い人は、地雷を一つずつ回避していくことができますが、しっかりとした基礎がない人は、何度もつまずいて挫折しながら後退することになるかもしれません。教科書を読むことも一般的な方法ですが、就職活動中の人にとって、卒業論文の執筆、履歴書の提出、筆記試験や面接の準備が既にエネルギーの大部分を消費しており、分厚い本を読むことはしばしば困難な挑戦となります。</p> <p>もしあなたが同様の悩みを抱えているなら、この本があなたを見つけることができて幸運です。この本は、この質問に対する私の答えです。これが最良の解決策ではないかもしれませんが、少なくとも積極的な試みです。この本があなたに直接内定をもたらすことはできませんが、データ構造とアルゴリズムの「知識地図」を探索する手引きとなり、さまざまな「地雷」の形、大きさ、位置を理解し、さまざまな「地雷除去方法」をマスターできるようお手伝いします。これらのスキルがあれば、より快適に問題を解き、文献を読むことができ、徐々に知識体系を構築できると信じています。</p> <p>私は、ファインマン教授の言葉に深く同感します。「知識は無料ではありません。注意を払わなければならないのです。」この意味で、この本は完全に「無料」ではありません。この本に対するあなたの貴重な「注意」に応えるために、私は最善を尽くし、最大の「注意」を払ってこの本を書きます。</p> <p>自分の限界を認識しており、この本の内容が時間をかけて洗練されたにもかかわらず、間違いは確実に残っていることを理解しています。先生方や学生の皆様からの批評と訂正を心から歓迎いたします。</p> <p></p> Hello, Algo! <p>コンピュータの出現は世界に大きな変化をもたらしました。高速な計算能力と優れたプログラム可能性により、コンピュータはアルゴリズムを実行しデータを処理するための理想的な媒体となりました。ビデオゲームのリアルなグラフィックス、自動運転の知的な判断、AlphaGoの見事な囲碁ゲーム、ChatGPTの自然な対話など、これらのアプリケーションはすべて、コンピュータ上で動作するアルゴリズムの精巧な実演です。</p> <p>実際、コンピュータの出現以前から、アルゴリズムとデータ構造は世界の至る所に存在していました。初期のアルゴリズムは比較的シンプルで、古代の計数方法や道具作りの手順などがありました。文明が進歩するにつれて、アルゴリズムはより洗練され複雑になりました。職人の精巧な技術から、生産力を解放する工業製品、宇宙を支配する科学法則まで、ほぼすべての平凡または驚異的なことの背後には、アルゴリズムの巧妙な思考があります。</p> <p>同様に、データ構造は至る所にあります。ソーシャルネットワークから地下鉄路線まで、多くのシステムは「グラフ」としてモデル化できます。国から家族まで、社会組織の主要な形態は「木」の特徴を示します。冬服は「スタック」のようで、最初に着たものが最後に脱がれます。バドミントンのシャトル筒は「キュー」に似ており、一方の端で挿入し、もう一方の端で取り出します。辞書は「ハッシュテーブル」のようで、目標エントリを素早く検索できます。</p> <p>この本は、明確で理解しやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、プログラミングを通じてそれらを実装できるようになることを目指しています。この基盤の上で、この本は複雑な世界におけるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示すことに努めています。この本があなたのお役に立てることを願っています!</p>","path":["序","はじめに"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"第 1 章 アルゴリズムとの出会い","text":"<p>Abstract</p> <p>優雅な乙女が踊ります。データと絡み合い、アルゴリズムのメロディーに合わせてスカートをなびかせながら。</p> <p>彼女があなたをダンスに誘います。彼女のステップに従って、論理と美に満ちたアルゴリズムの世界に入りましょう。</p>","path":["第 1 章 アルゴリズムを知る","第 1 章 アルゴリズムとの出会い"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"章の内容","text":"<ul> <li>1.1 アルゴリズムはどこにでもある</li> <li>1.2 アルゴリズムとは何か</li> <li>1.3 まとめ</li> </ul>","path":["第 1 章 アルゴリズムを知る","第 1 章 アルゴリズムとの出会い"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1 アルゴリズムは至る所にある","text":"<p>「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし、多くのアルゴリズムは複雑な数学を含まず、基本的な論理により多く依存しており、これは私たちの日常生活の至る所で見ることができます。</p> <p>アルゴリズムについて正式に議論を始める前に、興味深い事実を共有する価値があります。あなたは無意識のうちに多くのアルゴリズムを学び、日常生活でそれらを応用することに慣れています。ここで、この点を証明するためにいくつかの具体的な例を挙げます。</p> <p>例1:辞書の引き方。英語の辞書では、単語がアルファベット順に並んでいます。\\(r\\)で始まる単語を探していると仮定すると、通常は以下の方法で行います:</p> <ol> <li>辞書を大体半分ぐらいのところで開き、そのページの最初の語彙を確認します。例えば\\(m\\)で始まる文字だとしましょう。</li> <li>\\(r\\)はアルファベットで\\(m\\)の後に来るので、前半を無視して、探索空間を後半に絞ります。</li> <li>\\(r\\)で始まる単語を見つけるまで、ステップ<code>1.</code>と<code>2.</code>を繰り返します。</li> </ol> <1><2><3><4><5> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 1-1 辞書を引く過程 </p> <p>辞書を引くことは、小学生にとって必須のスキルですが、実際には有名な「二分探索」アルゴリズムです。データ構造の観点から、辞書をソートされた「配列」と考えることができます。アルゴリズムの観点から、辞書で単語を探すために取られる一連の行動は、「二分探索」アルゴリズムと見なすことができます。</p> <p>例2:トランプの整理。トランプをプレイするとき、手札を昇順に並べる必要があります。以下の過程で示されます。</p> <ol> <li>トランプを「整列済み」と「未整列」のセクションに分けます。最初は一番左のカードが既に整列していると仮定します。</li> <li>未整列セクションからカードを1枚取り出し、整列済みセクションの正しい位置に挿入します。この後、左端の2枚のカードが整列します。</li> <li>すべてのカードが整列するまで、ステップ<code>2</code>を繰り返します。</li> </ol> <p></p> <p> 図 1-2 トランプの整理過程 </p> <p>上記のトランプを整理する方法は、実質的に「挿入ソート」アルゴリズムであり、小さなデータセットに対して非常に効率的です。多くのプログラミング言語のソート関数には挿入ソートが含まれています。</p> <p>例3:お釣りの計算。スーパーマーケットで\\(69\\)の買い物をしたと仮定します。レジ係に\\(100\\)を渡すと、\\(31\\)のお釣りを提供する必要があります。この過程は以下の図で明確に理解できます。</p> <ol> <li>選択肢は\\(31\\)以下の価値のある通貨で、\\(1\\)、\\(5\\)、\\(10\\)、\\(20\\)が含まれます。</li> <li>選択肢から最大の\\(20\\)を取り出し、\\(31 - 20 = 11\\)が残ります。</li> <li>残りの選択肢から最大の\\(10\\)を取り出し、\\(11 - 10 = 1\\)が残ります。</li> <li>残りの選択肢から最大の\\(1\\)を取り出し、\\(1 - 1 = 0\\)が残ります。</li> <li>お釣りの計算が完了し、解答は\\(20 + 10 + 1 = 31\\)です。</li> </ol> <p></p> <p> 図 1-3 お釣りの計算過程 </p> <p>記述されたステップでは、利用可能な最大の額面を使用して各段階で最良の選択肢を選ぶことで、効果的なお釣り計算戦略につながります。データ構造とアルゴリズムの観点から、このアプローチは「貪欲」アルゴリズムとして知られています。</p> <p>料理の準備から宇宙旅行まで、ほぼすべての問題解決にはアルゴリズムが関わっています。コンピュータの出現により、メモリにデータ構造を格納し、CPUとGPUを呼び出してアルゴリズムを実行するコードを書くことができるようになりました。このようにして、現実世界の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できます。</p> <p>Tip</p> <p>データ構造、アルゴリズム、配列、二分探索などの概念についてまだ混乱している場合は、読み続けることをお勧めします。この本は、データ構造とアルゴリズムの理解の領域へと優しく導いてくれるでしょう。</p>","path":["第 1 章 アルゴリズムを知る","1.1 アルゴリズムは至る所にある"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3 まとめ","text":"<ul> <li>アルゴリズムは日常生活にありふれており、思っているほどアクセスしにくく複雑なものではありません。実際、私たちは既に無意識のうちに多くのアルゴリズムを学び、生活の様々な問題を解決するために使用しています。</li> <li>辞書で単語を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム概念を体現しています。</li> <li>トランプを整理する過程は挿入ソートアルゴリズムと非常に似ています。挿入ソートアルゴリズムは小さなデータセットのソートに適しています。</li> <li>通貨でお釣りを計算するステップは本質的に貪欲アルゴリズムに従っており、各ステップでその時点での最良の選択をします。</li> <li>アルゴリズムは有限時間内で特定の問題を解決するための段階的な指示のセットですが、データ構造はコンピュータでのデータの組織化と保存方法を定義します。</li> <li>データ構造とアルゴリズムは密接に関連しています。データ構造はアルゴリズムの基礎であり、アルゴリズムはデータ構造の機能を活用するステージです。</li> <li>データ構造とアルゴリズムをブロックの組み立てに例えることができます。ブロックはデータを表し、ブロックの形状と接続方法はデータ構造を表し、ブロックを組み立てるステップはアルゴリズムに対応します。</li> </ul>","path":["第 1 章 アルゴリズムを知る","1.3 まとめ"],"tags":[]},{"location":"chapter_introduction/summary/#1-q-a","level":3,"title":"1. Q & A","text":"<p>Q:プログラマーとして、日常の仕事でアルゴリズムを手動で実装する必要があることはめったにありません。最も一般的に使用されるアルゴリズムは、既にプログラミング言語とライブラリに組み込まれており、すぐに使用できます。これは、私たちが仕事で遭遇する問題が、カスタムアルゴリズム設計を必要とする複雑さのレベルにまだ達していないことを示唆していますか?</p> <p>特定の仕事スキルが武術の「技」のようなものだとすれば、基礎科目は「内功」のようなものです。</p> <p>アルゴリズム(およびその他の基礎科目)を学ぶ意義は、必ずしも仕事でそれらを一から実装することではなく、概念の確固たる理解に基づいて、より専門的な意思決定と問題解決を可能にし、それによって仕事の全体的な質を向上させることだと私は信じています。例えば、すべてのプログラミング言語には組み込みのソート関数があります:</p> <ul> <li>データ構造とアルゴリズムを学んでいない場合、どんなデータが与えられても、このソート関数に渡すだけかもしれません。スムーズに動作し、良いパフォーマンスを示し、問題がないように見えます。</li> <li>しかし、アルゴリズムを学んだことがあれば、組み込みのソート関数の時間複雑度は通常\\(O(n \\log n)\\)であることを理解しています。さらに、データが固定桁数の整数(学生IDなど)で構成されている場合、基数ソートのようなより効率的なアプローチを適用でき、時間複雑度をO(nk)に削減できます。ここでkは桁数です。大量のデータを処理する際、節約された時間は重要な価値に変わります — コストの削減、ユーザーエクスペリエンスの向上、システムパフォーマンスの向上。</li> </ul> <p>エンジニアリングでは、多くの問題を最適に解決することは困難です。ほとんどは「準最適」解決策で対処されます。問題の難しさは、その固有の複雑さだけでなく、それに取り組む人の知識と経験にも依存します。専門知識と経験が深いほど、分析がより徹底的になり、問題をより優雅に解決できます。</p>","path":["第 1 章 アルゴリズムを知る","1.3 まとめ"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2 アルゴリズムとは何か","text":"","path":["第 1 章 アルゴリズムを知る","1.2 アルゴリズムとは何か"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121","level":2,"title":"1.2.1 アルゴリズムの定義","text":"<p>アルゴリズムは、有限時間内で特定の問題を解決するための一連の指示またはステップです。以下の特徴があります:</p> <ul> <li>問題が明確に定義されており、入力と出力の明確な定義が含まれています。</li> <li>アルゴリズムは実行可能で、有限の回数のステップ、時間、メモリ空間内で完了できることを意味します。</li> <li>各ステップには明確な意味があります。同じ入力と条件の下で出力は一貫して同じです。</li> </ul>","path":["第 1 章 アルゴリズムを知る","1.2 アルゴリズムとは何か"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122","level":2,"title":"1.2.2 データ構造の定義","text":"<p>データ構造は、コンピュータ内でデータを組織し保存する方法で、以下の設計目標があります:</p> <ul> <li>コンピュータのメモリを節約するために空間占有を最小化する。</li> <li>データ操作を可能な限り高速にし、データのアクセス、追加、削除、更新などをカバーする。</li> <li>効率的なアルゴリズム実行を可能にするために、簡潔なデータ表現と論理情報を提供する。</li> </ul> <p>データ構造の設計はバランスを取る行為であり、しばしばトレードオフが必要です。一つの側面を改善したい場合、しばしば別の側面で妥協する必要があります。以下は2つの例です:</p> <ul> <li>配列と比較して、連結リストはデータの追加と削除においてより便利ですが、データアクセス速度を犠牲にします。</li> <li>連結リストと比較して、グラフはより豊富な論理情報を提供しますが、より多くのメモリ空間が必要です。</li> </ul>","path":["第 1 章 アルゴリズムを知る","1.2 アルゴリズムとは何か"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3 データ構造とアルゴリズムの関係","text":"<p>以下の図に示すように、データ構造とアルゴリズムは高度に関連し、密接に統合されており、具体的には以下の3つの側面があります:</p> <ul> <li>データ構造はアルゴリズムの基礎です。構造化されたデータ保存とアルゴリズムのためのデータ操作方法を提供します。</li> <li>アルゴリズムはデータ構造に活力を注入します。データ構造だけではデータ情報を保存するだけです。アルゴリズムの応用によって、特定の問題を解決できます。</li> <li>アルゴリズムは異なるデータ構造に基づいて実装できることが多いですが、実行効率は大きく異なることがあります。適切なデータ構造を選択することが鍵です。</li> </ul> <p></p> <p> 図 1-4 データ構造とアルゴリズムの関係 </p> <p>データ構造とアルゴリズムは、以下の図に示すように、ブロックのセットに例えることができます。ブロックセットには多数のピースが含まれ、詳細な組み立て説明書が付いています。これらの説明書に段階的に従うことで、複雑なブロックモデルを構築できます。</p> <p></p> <p> 図 1-5 ブロックの組み立て </p> <p>両者の詳細な対応関係は以下の表に示されています。</p> <p> 表 1-1 データ構造とアルゴリズムをブロックと比較 </p> データ構造とアルゴリズム ブロック 入力データ 未組み立てのブロック データ構造 ブロックの組織、形状、サイズ、接続などを含む アルゴリズム ブロックを望ましい形状に組み立てる一連のステップ 出力データ 完成したブロックモデル <p>データ構造とアルゴリズムはプログラミング言語から独立していることは注目に値します。この理由により、この本は複数のプログラミング言語での実装を提供できます。</p> <p>慣習的な略語</p> <p>実生活の議論では、「データ構造とアルゴリズム」を単純に「アルゴリズム」と呼ぶことがよくあります。例えば、よく知られたLeetCodeアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識をテストしています。</p>","path":["第 1 章 アルゴリズムを知る","1.2 アルゴリズムとは何か"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"第 0 章 序文","text":"<p>Abstract</p> <p>アルゴリズムは美しい交響曲のようで、コードの一行一行がリズムのように流れています。</p> <p>この本があなたの心の中で静かに響き、独特で深い旋律を残すことを願っています。</p>","path":["第 0 章 前書き","第 0 章 序文"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"章の内容","text":"<ul> <li>0.1 本書について</li> <li>0.2 本書の使い方</li> <li>0.3 まとめ</li> </ul>","path":["第 0 章 前書き","第 0 章 序文"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1 この本について","text":"<p>このオープンソースプロジェクトは、データ構造とアルゴリズムに関する無料で初心者にやさしいクラッシュコースの作成を目指しています。</p> <ul> <li>アニメーション付きの図解、理解しやすい内容、滑らかな学習曲線により、初心者がデータ構造とアルゴリズムの「知識マップ」を探索するのに役立ちます。</li> <li>ワンクリックでコードを実行できるため、読者のプログラミングスキルの向上と、アルゴリズムの動作原理およびデータ構造の基礎実装の理解に役立ちます。</li> <li>教えることによる学習を促進し、質問や洞察の共有を自由に行ってください。議論を通じて一緒に成長しましょう。</li> </ul>","path":["第 0 章 前書き","0.1 この本について"],"tags":[]},{"location":"chapter_preface/about_the_book/#011","level":2,"title":"0.1.1 対象読者","text":"<p>もしあなたがアルゴリズムに触れたばかりで経験が限られている場合、またはアルゴリズムである程度の経験を積んでいても、データ構造とアルゴリズムについて曖昧な理解しかなく、常に「分かった」と「うーん」の間を行き来している場合、この本はあなたのためのものです!</p> <p>すでにある程度の問題解決経験を積んでおり、ほとんどのタイプの問題に精通している場合、この本はアルゴリズムの知識体系を復習し整理するのに役立ちます。リポジトリのソースコードは「問題解決ツールキット」や「アルゴリズムチートシート」として使用できます。</p> <p>もしあなたがアルゴリズムの専門家であれば、貴重な提案をいただくか、参加して協力していただければと思います。</p> <p>前提条件</p> <p>少なくとも一つのプログラミング言語で簡単なコードを書いて読むことができる必要があります。</p>","path":["第 0 章 前書き","0.1 この本について"],"tags":[]},{"location":"chapter_preface/about_the_book/#012","level":2,"title":"0.1.2 内容構成","text":"<p>本書の主な内容を下図に示します。</p> <ul> <li>計算量解析: データ構造とアルゴリズムを評価する側面と方法を探求します。時間計算量と空間計算量を導出する方法、および一般的なタイプと例を扱います。</li> <li>データ構造: 基本的なデータ型、分類方法、定義、長所と短所、一般的な操作、タイプ、応用、および配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の実装方法に焦点を当てます。</li> <li>アルゴリズム: アルゴリズムを定義し、その長所と短所、効率性、応用シナリオ、問題解決ステップについて議論し、検索、ソート、分割統治、バックトラッキング、動的プログラミング、貪欲アルゴリズムなど、さまざまなアルゴリズムのサンプル問題を含みます。</li> </ul> <p></p> <p> 図 0-1 本書の主な内容 </p>","path":["第 0 章 前書き","0.1 この本について"],"tags":[]},{"location":"chapter_preface/about_the_book/#013","level":2,"title":"0.1.3 謝辞","text":"<p>本書は、オープンソースコミュニティの多数の貢献者による共同の努力のもと、継続的に改善されています。時間と労力を注いで執筆に携わってくださったすべての方々に感謝します。貢献者は以下のとおりです(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、Keynman、KeiichiKasai、KawaiiAsh。</p> <p>この本のコードレビュー作業は、coderonion, Gonglja, gvenusleo, hpstory, justin‐tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi(アルファベット順)によって完了されました。彼らの時間と努力に感謝し、様々な言語でのコードの標準化と統一性を確保してくださいました。</p> <p>本書の繁体字中国語版は Shyam-Chen および Dr-XYZ によってレビューされ、英語版は yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0、magentaqin によってレビューされ、日本語版は eltociear によってレビューされました。彼らの継続的な貢献のおかげで、本書はより幅広い読者層に提供することができています。ここに深く感謝いたします。</p> <p>この本の制作過程において、多くの方々から貴重な支援をいただきました。これらに限定されませんが:</p> <ul> <li>会社でのメンター、李熙博士に感謝します。ある会話で「早く行動しろ」と励ましていただき、この本を書く決意を固めることができました。</li> <li>ガールフレンドのBubbleに感謝します。この本の最初の読者として、アルゴリズム初心者の視点から多くの貴重な提案をいただき、この本を初心者により適したものにしてくださいました。</li> <li>Tengbao、Qibao、Feibaoに感謝します。この本のクリエイティブな名前を考えてくださり、みんなが初めて「Hello World!」を書いた時の素晴らしい思い出を呼び起こしてくれました。</li> <li>Xiaoquanに感謝します。知的財産に関する専門的な支援を提供してくださり、このオープンソース本の開発において重要な役割を果たしてくださいました。</li> <li>Sutongに感謝します。この本の美しいカバーとロゴをデザインしてくださり、私の要求で何度も修正を辛抱強く行ってくださいました。</li> <li>@squidfunk に感謝します。執筆と組版の提案、および彼が開発したオープンソースドキュメントテーマ Material-for-MkDocs を提供してくださいました。</li> </ul> <p>執筆の過程で、データ構造とアルゴリズムに関する多数の教科書や記事を深く研究しました。これらの作品は模範的なモデルとして機能し、この本の内容の正確性と品質を確保してくださいました。先人の方々の貴重な貢献に感謝いたします!</p> <p>この本は、理論と実践を組み合わせた学習を提唱しており、この点で \"Dive into Deep Learning\" からインスピレーションを受けています。この優れた本をすべての読者に強くお勧めします。</p> <p>継続的な支援と励ましにより、この興味深い仕事をすることを可能にしてくださった両親に心から感謝いたします。</p>","path":["第 0 章 前書き","0.1 この本について"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2 読み方","text":"<p>Tip</p> <p>最良の読書体験のために、このセクションを通読することをお勧めします。</p>","path":["第 0 章 前書き","0.2 読み方"],"tags":[]},{"location":"chapter_preface/suggestions/#021","level":2,"title":"0.2.1 記述規則","text":"<ul> <li>タイトルの後に「*」が付いた章は任意であり、比較的難易度の高い内容が含まれています。時間に制約がある場合は、これらをスキップすることをお勧めします。</li> <li>技術用語は太字(印刷版およびPDF版)または下線(Web版)で表示されます。例えば、配列などです。技術文書をより良く理解するために、これらに慣れることをお勧めします。</li> <li>**太字のテキスト**は重要な内容や要約文を示し、特別な注意を払う価値があります。</li> <li>特定の意味を持つ単語や句は「引用符」で示され、曖昧さを避けます。</li> <li>プログラミング言語間で一致しない用語については、この本はPythonに従います。例えば、<code>null</code>を意味するために<code>None</code>を使用します。</li> <li>この本は、よりコンパクトなコンテンツレイアウトと引き換えに、プログラミング言語のコメント規約を部分的に無視しています。コメントは主に3つのタイプで構成されています:タイトルコメント、内容コメント、複数行コメント。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin <pre><code>\"\"\"関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント\"\"\"\n\n# 詳細を説明するためのコメント\n\n\"\"\"\n複数行\nコメント\n\"\"\"\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre> <pre><code>/* 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント */\n\n// 詳細を説明するためのコメント\n\n/**\n * 複数行\n * コメント\n */\n</code></pre>","path":["第 0 章 前書き","0.2 読み方"],"tags":[]},{"location":"chapter_preface/suggestions/#022","level":2,"title":"0.2.2 アニメーション図解による効率的学習","text":"<p>テキストと比較して、動画や画像は情報密度が高く、より構造化されており、理解しやすくなっています。この本では、重要で難しい概念は主にアニメーションと図解を通じて提示され、テキストは説明と補足として機能します。</p> <p>下図に示すようなアニメーションや図解のある内容に遭遇した場合、図の理解を優先し、テキストを補足として、両方を統合して包括的な理解を得てください。</p> <p></p> <p> 図 0-2 アニメーション図解の例 </p>","path":["第 0 章 前書き","0.2 読み方"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3 コーディング実践による理解の深化","text":"<p>この本のソースコードはGitHubリポジトリでホストされています。下図に示すように、ソースコードにはテスト例が付属しており、ワンクリックで実行できます。</p> <p>時間に余裕がある場合は、自分でコードをタイプすることをお勧めします。時間がない場合は、少なくともすべてのコードを読んで実行してください。</p> <p>コードを読むだけと比較して、コードを書くことは多くの場合、より多くの学習をもたらします。実践による学習こそが真の学習方法です。</p> <p></p> <p> 図 0-3 コード実行例 </p> <p>コードを実行するための設定には、主に3つのステップが含まれます。</p> <p>ステップ1:ローカルプログラミング環境をインストール。付録のチュートリアルに従ってインストールするか、すでにインストールされている場合はこのステップをスキップしてください。</p> <p>ステップ2:コードリポジトリをクローンまたはダウンロード。GitHubリポジトリを訪問してください。</p> <p>Gitがインストールされている場合は、次のコマンドを使用してリポジトリをクローンします:</p> <pre><code>git clone https://github.com/krahets/hello-algo.git\n</code></pre> <p>または、下図に示す場所にある「Download ZIP」ボタンをクリックして、コードを圧縮ZIPファイルとして直接ダウンロードすることもできます。その後、ローカルで展開するだけです。</p> <p></p> <p> 図 0-4 リポジトリのクローンとコードのダウンロード </p> <p>ステップ3:ソースコードを実行。下図に示すように、上部にファイル名が記載されたコードブロックについては、リポジトリの<code>codes</code>フォルダで対応するソースコードファイルを見つけることができます。これらのファイルはワンクリックで実行でき、不要なデバッグ時間を節約し、学習に集中できます。</p> <p></p> <p> 図 0-5 コードブロックと対応するソースコードファイル </p>","path":["第 0 章 前書き","0.2 読み方"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4 議論による共同学習","text":"<p>この本を読んでいる間、学べなかった点を飛ばさないでください。コメントセクションで気軽に質問してください。喜んでお答えし、通常2日以内に回答できます。</p> <p>下図に示すように、各章の下部にコメントセクションがあります。これらのコメントに注意を払うことをお勧めします。他の人が遭遇した問題を知ることで、知識のギャップを特定し、より深い思索を促すだけでなく、仲間の読者の質問に答えたり、洞察を共有したり、相互の向上を促進したりすることで寛大に貢献することも招待します。</p> <p></p> <p> 図 0-6 コメントセクションの例 </p>","path":["第 0 章 前書き","0.2 読み方"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5 アルゴリズム学習パス","text":"<p>全体的に、データ構造とアルゴリズムをマスターする旅は3つの段階に分けることができます:</p> <ol> <li>段階1:アルゴリズムの入門。さまざまなデータ構造の特性と使用法に慣れ、異なるアルゴリズムの原理、プロセス、用途、効率について学ぶ必要があります。</li> <li>段階2:アルゴリズム問題の練習。Sword for OfferやLeetCode Hot 100などの人気のある問題から始めることをお勧めし、少なくとも100問を蓄積して主流のアルゴリズム問題に慣れることです。練習を始めると忘却が課題になる可能性がありますが、これは正常なことですのでご安心ください。「エビングハウスの忘却曲線」に従って問題を復習することができ、通常3〜5回の反復の後、それらを覚えることができるでしょう。</li> <li>段階3:知識体系の構築。学習の面では、アルゴリズムコラム記事、解法フレームワーク、アルゴリズム教科書を読んで知識体系を継続的に豊かにすることができます。練習の面では、トピック別分類、一つの問題に対する複数の解法、複数の問題に対する一つの解法など、高度な戦略を試すことができます。これらの戦略に関する洞察は、さまざまなコミュニティで見つけることができます。</li> </ol> <p>下図に示すように、この本は主に「段階1」をカバーしており、段階2と3により効率的に取り組むのに役立つことを目的としています。</p> <p></p> <p> 図 0-7 アルゴリズム学習パス </p>","path":["第 0 章 前書き","0.2 読み方"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3 まとめ","text":"<ul> <li>この本の主な読者はアルゴリズムの初心者です。すでに基本的な知識をお持ちの場合、この本はアルゴリズムの知識を体系的に復習するのに役立ち、この本のソースコードは「コーディングツールキット」としても使用できます。</li> <li>この本は3つの主要なセクション、計算量解析、データ構造、アルゴリズムで構成されており、この分野のほとんどのトピックをカバーしています。</li> <li>アルゴリズムの初心者にとって、多くの回り道や一般的な落とし穴を避けるために、初期段階で入門書を読むことが重要です。</li> <li>本書内のアニメーションと図は通常、重要なポイントと難しい知識を紹介するために使用されます。本を読む際にはこれらにより多くの注意を払う必要があります。</li> <li>実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、自分でコードをタイプすることを強くお勧めします。</li> <li>この本のWeb版の各章には議論セクションがあり、いつでも質問や洞察を共有することを歓迎します。</li> </ul>","path":["第 0 章 前書き","0.3 まとめ"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"参考文献","text":"<p>[1] Thomas H. Cormen, et al. Introduction to Algorithms (3<sup>rd</sup> Edition).</p> <p>[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1<sup>st</sup> Edition).</p> <p>[3] Robert Sedgewick, et al. Algorithms (4<sup>th</sup> Edition).</p> <p>[4] Yan Weimin. Data Structures (C Language Version).</p> <p>[5] Deng Junhui. Data Structures (C++ Language Version, Third Edition).</p> <p>[6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).</p> <p>[7] Cheng Jie. Speaking of Data Structures.</p> <p>[8] Wang Zheng. The Beauty of Data Structures and Algorithms.</p> <p>[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6<sup>th</sup> Edition).</p> <p>[10] Aston Zhang, et al. Dive into Deep Learning.</p>","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章 探索","text":"<p>Abstract</p> <p>探索は未知への冒険です。神秘的な空間の隅々まで巡る必要があるかもしれませんし、あるいはすぐに目標を見つけることができるかもしれません。</p> <p>この発見の旅において、それぞれの探査は予期しない答えで終わるかもしれません。</p>","path":["第 10 章 探索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"章の内容","text":"<ul> <li>10.1 二分探索</li> <li>10.2 二分探索の挿入点</li> <li>10.3 二分探索の境界</li> <li>10.4 ハッシュ最適化戦略</li> <li>10.5 探索アルゴリズムの再認識</li> <li>10.6 まとめ</li> </ul>","path":["第 10 章 探索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1 二分探索","text":"<p>二分探索は分割統治戦略を用いる効率的な探索アルゴリズムです。配列内の要素の整列順序を利用し、各反復で探索区間を半分に減らしながら、目標要素が見つかるか探索区間が空になるまで続行します。</p> <p>Question</p> <p>長さ\\(n\\)の配列<code>nums</code>が与えられ、要素は重複なしで昇順に配列されています。この配列内の要素<code>target</code>のインデックスを見つけて返してください。配列に要素が含まれていない場合は\\(-1\\)を返してください。例を下図に示します。</p> <p></p> <p> 図 10-1 Binary search example data </p> <p>下図に示すように、まず\\(i = 0\\)と\\(j = n - 1\\)でポインタを初期化し、それぞれ配列の最初と最後の要素を指します。これらはまた全体の探索区間\\([0, n - 1]\\)を表します。角括弧は閉区間を示し、境界値自身も含むことに注意してください。</p> <p>そして、以下の2つのステップをループで実行する可能性があります。</p> <ol> <li>中点インデックス\\(m = \\lfloor {(i + j) / 2} \\rfloor\\)を計算します。ここで\\(\\lfloor \\: \\rfloor\\)は床関数を表します。</li> <li><code>nums[m]</code>と<code>target</code>の比較に基づいて、以下の3つのケースのうち1つを選択して実行します。<ol> <li><code>nums[m] < target</code>の場合、<code>target</code>は区間\\([m + 1, j]\\)にあることを示すため、\\(i = m + 1\\)とします。</li> <li><code>nums[m] > target</code>の場合、<code>target</code>は区間\\([i, m - 1]\\)にあることを示すため、\\(j = m - 1\\)とします。</li> <li><code>nums[m] = target</code>の場合、<code>target</code>が見つかったことを示すため、インデックス\\(m\\)を返します。</li> </ol> </li> </ol> <p>配列に目標要素が含まれていない場合、探索区間は最終的に空になり、\\(-1\\)を返して終了します。</p> <1><2><3><4><5><6><7> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 10-2 Binary search process </p> <p>\\(i\\)と\\(j\\)が両方とも<code>int</code>型であるため、**\\(i + j\\)は<code>int</code>型の範囲を超える可能性がある**ことは注目に値します。大きな数のオーバーフローを避けるため、通常は式\\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\)を使用して中点を計算します。</p> <p>コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py<pre><code>def binary_search(nums: list[int], target: int) -> int:\n \"\"\"二分探索(両端閉区間)\"\"\"\n # 両端閉区間 [0, n-1] を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素を指す\n i, j = 0, len(nums) - 1\n # 検索区間が空になるまでループ(i > j のとき空)\n while i <= j:\n # 理論的には、Pythonの数値は無限に大きくなることができる(メモリサイズに依存)ため、大きな数のオーバーフローを考慮する必要はない\n m = i + (j - i) // 2 # 中点インデックス m を計算\n if nums[m] < target:\n i = m + 1 # この場合、target は区間 [m+1, j] にあることを示す\n elif nums[m] > target:\n j = m - 1 # この場合、target は区間 [i, m-1] にあることを示す\n else:\n return m # ターゲット要素が見つかったため、そのインデックスを返す\n return -1 # ターゲット要素が見つからなかったため、-1 を返す\n</code></pre> binary_search.cpp<pre><code>/* 二分探索(両端閉区間) */\nint binarySearch(vector<int> &nums, int target) {\n // 両端閉区間[0, n-1]を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素を指す\n int i = 0, j = nums.size() - 1;\n // 探索区間が空になるまでループ(i > jの時空になる)\n while (i <= j) {\n int m = i + (j - i) / 2; // 中点インデックスmを計算\n if (nums[m] < target) // この状況はtargetが区間[m+1, j]にあることを示す\n i = m + 1;\n else if (nums[m] > target) // この状況はtargetが区間[i, m-1]にあることを示す\n j = m - 1;\n else // ターゲット要素が見つかったため、そのインデックスを返す\n return m;\n }\n // ターゲット要素が見つからなかったため、-1を返す\n return -1;\n}\n</code></pre> binary_search.java<pre><code>/* 二分探索(両端閉区間) */\nint binarySearch(int[] nums, int target) {\n // 両端閉区間 [0, n-1] を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素を指す\n int i = 0, j = nums.length - 1;\n // 探索区間が空になるまでループ(i > j のとき空)\n while (i <= j) {\n int m = i + (j - i) / 2; // 中点インデックス m を計算\n if (nums[m] < target) // この状況は target が区間 [m+1, j] にあることを示す\n i = m + 1;\n else if (nums[m] > target) // この状況は target が区間 [i, m-1] にあることを示す\n j = m - 1;\n else // 目標要素を見つけたので、そのインデックスを返す\n return m;\n }\n // 目標要素を見つけられなかったので、-1 を返す\n return -1;\n}\n</code></pre> binary_search.cs<pre><code>[class]{binary_search}-[func]{BinarySearch}\n</code></pre> binary_search.go<pre><code>[class]{}-[func]{binarySearch}\n</code></pre> binary_search.swift<pre><code>[class]{}-[func]{binarySearch}\n</code></pre> binary_search.js<pre><code>[class]{}-[func]{binarySearch}\n</code></pre> binary_search.ts<pre><code>[class]{}-[func]{binarySearch}\n</code></pre> binary_search.dart<pre><code>[class]{}-[func]{binarySearch}\n</code></pre> binary_search.rs<pre><code>[class]{}-[func]{binary_search}\n</code></pre> binary_search.c<pre><code>[class]{}-[func]{binarySearch}\n</code></pre> binary_search.kt<pre><code>[class]{}-[func]{binarySearch}\n</code></pre> binary_search.rb<pre><code>[class]{}-[func]{binary_search}\n</code></pre> <p>時間計算量は\\(O(\\log n)\\)です:二分ループにおいて、区間は各ラウンドで半分に減少するため、反復回数は\\(\\log_2 n\\)となります。</p> <p>空間計算量は\\(O(1)\\)です:ポインタ\\(i\\)と\\(j\\)は定数サイズの空間を占有します。</p>","path":["第 10 章 探索","10.1 二分探索"],"tags":[]},{"location":"chapter_searching/binary_search/#1011","level":2,"title":"10.1.1 区間表現方法","text":"<p>上記の閉区間の他に、もう一つの一般的な区間表現は「左閉右開」区間で、\\([0, n)\\)として定義され、左境界は自身を含み、右境界は含みません。この表現では、\\(i = j\\)のとき区間\\([i, j)\\)は空になります。</p> <p>この表現に基づいて同じ機能を持つ二分探索アルゴリズムを実装できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py<pre><code>def binary_search_lcro(nums: list[int], target: int) -> int:\n \"\"\"二分探索(左閉右開区間)\"\"\"\n # 左閉右開区間 [0, n) を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素+1を指す\n i, j = 0, len(nums)\n # 検索区間が空になるまでループ(i = j のとき空)\n while i < j:\n m = i + (j - i) // 2 # 中点インデックス m を計算\n if nums[m] < target:\n i = m + 1 # この場合、target は区間 [m+1, j) にあることを示す\n elif nums[m] > target:\n j = m # この場合、target は区間 [i, m) にあることを示す\n else:\n return m # ターゲット要素が見つかったため、そのインデックスを返す\n return -1 # ターゲット要素が見つからなかったため、-1 を返す\n</code></pre> binary_search.cpp<pre><code>/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n // 左閉右開区間[0, n)を初期化、すなわちi、jはそれぞれ配列の最初の要素と最後の要素+1を指す\n int i = 0, j = nums.size();\n // 探索区間が空になるまでループ(i = jの時空になる)\n while (i < j) {\n int m = i + (j - i) / 2; // 中点インデックスmを計算\n if (nums[m] < target) // この状況はtargetが区間[m+1, j)にあることを示す\n i = m + 1;\n else if (nums[m] > target) // この状況はtargetが区間[i, m)にあることを示す\n j = m;\n else // ターゲット要素が見つかったため、そのインデックスを返す\n return m;\n }\n // ターゲット要素が見つからなかったため、-1を返す\n return -1;\n}\n</code></pre> binary_search.java<pre><code>/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(int[] nums, int target) {\n // 左閉右開区間 [0, n) を初期化、すなわち i, j はそれぞれ配列の最初の要素と最後の要素+1を指す\n int i = 0, j = nums.length;\n // 探索区間が空になるまでループ(i = j のとき空)\n while (i < j) {\n int m = i + (j - i) / 2; // 中点インデックス m を計算\n if (nums[m] < target) // この状況は target が区間 [m+1, j) にあることを示す\n i = m + 1;\n else if (nums[m] > target) // この状況は target が区間 [i, m) にあることを示す\n j = m;\n else // 目標要素を見つけたので、そのインデックスを返す\n return m;\n }\n // 目標要素を見つけられなかったので、-1 を返す\n return -1;\n}\n</code></pre> binary_search.cs<pre><code>[class]{binary_search}-[func]{BinarySearchLCRO}\n</code></pre> binary_search.go<pre><code>[class]{}-[func]{binarySearchLCRO}\n</code></pre> binary_search.swift<pre><code>[class]{}-[func]{binarySearchLCRO}\n</code></pre> binary_search.js<pre><code>[class]{}-[func]{binarySearchLCRO}\n</code></pre> binary_search.ts<pre><code>[class]{}-[func]{binarySearchLCRO}\n</code></pre> binary_search.dart<pre><code>[class]{}-[func]{binarySearchLCRO}\n</code></pre> binary_search.rs<pre><code>[class]{}-[func]{binary_search_lcro}\n</code></pre> binary_search.c<pre><code>[class]{}-[func]{binarySearchLCRO}\n</code></pre> binary_search.kt<pre><code>[class]{}-[func]{binarySearchLCRO}\n</code></pre> binary_search.rb<pre><code>[class]{}-[func]{binary_search_lcro}\n</code></pre> <p>下図に示すように、2つの区間表現タイプにおいて、二分探索アルゴリズムの初期化、ループ条件、区間縮小操作が異なります。</p> <p>「閉区間」表現では両方の境界が包含的であるため、ポインタ\\(i\\)と\\(j\\)による区間縮小操作も対称的です。これによりエラーが発生しにくくなるため、一般的に「閉区間」アプローチの使用が推奨されます。</p> <p></p> <p> 図 10-3 Two types of interval definitions </p>","path":["第 10 章 探索","10.1 二分探索"],"tags":[]},{"location":"chapter_searching/binary_search/#1012","level":2,"title":"10.1.2 利点と制限","text":"<p>二分探索は時間と空間の両方の面で良好な性能を示します。</p> <ul> <li>二分探索は時間効率が良いです。大きなデータセットでは、対数時間計算量が大きな利点を提供します。例えば、サイズ\\(n = 2^{20}\\)のデータセットが与えられた場合、線形探索は\\(2^{20} = 1048576\\)回の反復が必要ですが、二分探索は\\(\\log_2 2^{20} = 20\\)回のループのみで済みます。</li> <li>二分探索には追加の空間が必要ありません。追加の空間に依存する探索アルゴリズム(ハッシュ探索など)と比較して、二分探索はより空間効率的です。</li> </ul> <p>しかし、二分探索は以下の懸念により、すべてのシナリオに適しているとは限りません。</p> <ul> <li>二分探索はソート済みデータにのみ適用できます。未ソートのデータは二分探索を適用する前にソートする必要があり、ソートアルゴリズムは通常\\(O(n \\log n)\\)の時間計算量を持つため、これは価値がないかもしれません。このコストは線形探索よりも高く、二分探索自体は言うまでもありません。頻繁な挿入があるシナリオでは、配列を順序に保つコストは非常に高く、特定の位置に新しい要素を挿入する時間計算量は\\(O(n)\\)です。</li> <li>二分探索は配列のみを使用できます。二分探索には非連続(ジャンプ)要素アクセスが必要で、これは連結リストでは非効率的です。そのため、連結リストや連結リストに基づくデータ構造はこのアルゴリズムに適していない可能性があります。</li> <li>線形探索は小さなデータセットでより良い性能を示します。線形探索では各反復で1つの判定操作のみが必要ですが、二分探索では1つの加算、1つの除算、1つから3つの判定操作、1つの加算(減算)を含み、合計4つから6つの操作が必要です。そのため、データサイズ\\(n\\)が小さい場合、線形探索は二分探索よりも高速です。</li> </ul>","path":["第 10 章 探索","10.1 二分探索"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3 二分探索の境界","text":"","path":["第 10 章 探索","10.3 二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031","level":2,"title":"10.3.1 左境界を見つける","text":"<p>Question</p> <p>重複要素を含む可能性がある長さ\\(n\\)のソート済み配列<code>nums</code>が与えられ、最も左の要素<code>target</code>のインデックスを返してください。要素が配列に存在しない場合は、\\(-1\\)を返してください。</p> <p>挿入位置の二分探索方法を思い出すと、探索完了後、インデックス\\(i\\)は<code>target</code>の最も左の出現を指します。したがって、挿入位置の探索は本質的に最も左の<code>target</code>のインデックスを見つけることと同じです。</p> <p>挿入位置を見つける関数を使用して<code>target</code>の左境界を見つけることができます。配列に<code>target</code>が含まれていない可能性があることに注意してください。これは以下の2つの結果につながる可能性があります:</p> <ul> <li>挿入位置のインデックス\\(i\\)が範囲外です。</li> <li>要素<code>nums[i]</code>が<code>target</code>と等しくありません。</li> </ul> <p>これらの場合、単に\\(-1\\)を返します。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py<pre><code>def binary_search_left_edge(nums: list[int], target: int) -> int:\n \"\"\"最左端のターゲットの二分探索\"\"\"\n # ターゲットの挿入位置を見つけることと同等\n i = binary_search_insertion(nums, target)\n # ターゲットが見つからなかった場合、-1 を返す\n if i == len(nums) or nums[i] != target:\n return -1\n # ターゲットが見つかった場合、インデックス i を返す\n return i\n</code></pre> binary_search_edge.cpp<pre><code>/* 最左のターゲットの二分探索 */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n // targetの挿入ポイントを見つけることと等価\n int i = binarySearchInsertion(nums, target);\n // targetが見つからなかったため、-1を返す\n if (i == nums.size() || nums[i] != target) {\n return -1;\n }\n // targetが見つかったため、インデックスiを返す\n return i;\n}\n</code></pre> binary_search_edge.java<pre><code>/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(int[] nums, int target) {\n // target の挿入点を見つけることと等価\n int i = binary_search_insertion.binarySearchInsertion(nums, target);\n // target を見つけられなかったので、-1 を返す\n if (i == nums.length || nums[i] != target) {\n return -1;\n }\n // target を見つけたので、インデックス i を返す\n return i;\n}\n</code></pre> binary_search_edge.cs<pre><code>[class]{binary_search_edge}-[func]{BinarySearchLeftEdge}\n</code></pre> binary_search_edge.go<pre><code>[class]{}-[func]{binarySearchLeftEdge}\n</code></pre> binary_search_edge.swift<pre><code>[class]{}-[func]{binarySearchLeftEdge}\n</code></pre> binary_search_edge.js<pre><code>[class]{}-[func]{binarySearchLeftEdge}\n</code></pre> binary_search_edge.ts<pre><code>[class]{}-[func]{binarySearchLeftEdge}\n</code></pre> binary_search_edge.dart<pre><code>[class]{}-[func]{binarySearchLeftEdge}\n</code></pre> binary_search_edge.rs<pre><code>[class]{}-[func]{binary_search_left_edge}\n</code></pre> binary_search_edge.c<pre><code>[class]{}-[func]{binarySearchLeftEdge}\n</code></pre> binary_search_edge.kt<pre><code>[class]{}-[func]{binarySearchLeftEdge}\n</code></pre> binary_search_edge.rb<pre><code>[class]{}-[func]{binary_search_left_edge}\n</code></pre>","path":["第 10 章 探索","10.3 二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032","level":2,"title":"10.3.2 右境界を見つける","text":"<p><code>target</code>の最も右の出現をどのように見つけるでしょうか?最も直接的な方法は、<code>nums[m] == target</code>の場合に探索境界を調整する方法を変更して、従来の二分探索ロジックを修正することです。コードはここでは省略されています。興味がある場合は、自分でコードを実装してみてください。</p> <p>以下では、さらに2つの巧妙な方法を紹介します。</p>","path":["第 10 章 探索","10.3 二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1. 左境界探索を再利用する","text":"<p><code>target</code>の最も右の出現を見つけるには、最も左の<code>target</code>を見つけるために使用された関数を再利用できます。具体的には、最も右のターゲットの探索を最も左のターゲット + 1の探索に変換します。</p> <p>下図に示すように、探索完了後、ポインタ\\(i\\)は最も左の<code>target + 1</code>(存在する場合)を指し、ポインタ\\(j\\)は<code>target</code>の最も右の出現を指します。したがって、\\(j\\)を返すことで右境界が得られます。</p> <p></p> <p> 図 10-7 Transforming the search for the right boundary into the search for the left boundary </p> <p>返される挿入位置は\\(i\\)であることに注意してください。したがって、\\(j\\)を得るためには1を引く必要があります:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py<pre><code>def binary_search_right_edge(nums: list[int], target: int) -> int:\n \"\"\"最右端のターゲットの二分探索\"\"\"\n # 最左端のターゲット + 1 を見つけることに変換\n i = binary_search_insertion(nums, target + 1)\n # j は最右端のターゲットを指し、i はターゲットより大きい最初の要素を指す\n j = i - 1\n # ターゲットが見つからなかった場合、-1 を返す\n if j == -1 or nums[j] != target:\n return -1\n # ターゲットが見つかった場合、インデックス j を返す\n return j\n</code></pre> binary_search_edge.cpp<pre><code>/* 最右のターゲットの二分探索 */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n // 最左のtarget + 1を見つけることに変換\n int i = binarySearchInsertion(nums, target + 1);\n // jは最右のターゲットを指し、iはtargetより大きい最初の要素を指す\n int j = i - 1;\n // targetが見つからなかったため、-1を返す\n if (j == -1 || nums[j] != target) {\n return -1;\n }\n // targetが見つかったため、インデックスjを返す\n return j;\n}\n</code></pre> binary_search_edge.java<pre><code>/* 最も右の target を二分探索 */\nint binarySearchRightEdge(int[] nums, int target) {\n // 最も左の target + 1 を見つけることに変換\n int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n // j は最も右の target を指し、i は target より大きい最初の要素を指す\n int j = i - 1;\n // target を見つけられなかったので、-1 を返す\n if (j == -1 || nums[j] != target) {\n return -1;\n }\n // target を見つけたので、インデックス j を返す\n return j;\n}\n</code></pre> binary_search_edge.cs<pre><code>[class]{binary_search_edge}-[func]{BinarySearchRightEdge}\n</code></pre> binary_search_edge.go<pre><code>[class]{}-[func]{binarySearchRightEdge}\n</code></pre> binary_search_edge.swift<pre><code>[class]{}-[func]{binarySearchRightEdge}\n</code></pre> binary_search_edge.js<pre><code>[class]{}-[func]{binarySearchRightEdge}\n</code></pre> binary_search_edge.ts<pre><code>[class]{}-[func]{binarySearchRightEdge}\n</code></pre> binary_search_edge.dart<pre><code>[class]{}-[func]{binarySearchRightEdge}\n</code></pre> binary_search_edge.rs<pre><code>[class]{}-[func]{binary_search_right_edge}\n</code></pre> binary_search_edge.c<pre><code>[class]{}-[func]{binarySearchRightEdge}\n</code></pre> binary_search_edge.kt<pre><code>[class]{}-[func]{binarySearchRightEdge}\n</code></pre> binary_search_edge.rb<pre><code>[class]{}-[func]{binary_search_right_edge}\n</code></pre>","path":["第 10 章 探索","10.3 二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2","level":3,"title":"2. 要素探索に変換する","text":"<p>配列に<code>target</code>が含まれていない場合、\\(i\\)と\\(j\\)は最終的に<code>target</code>より大きい最初の要素と小さい最初の要素をそれぞれ指します。</p> <p>したがって、下図に示すように、配列に存在しない要素を構築して、左と右の境界を探索できます。</p> <ul> <li>最も左の<code>target</code>を見つけるには:<code>target - 0.5</code>を探索することに変換でき、ポインタ\\(i\\)を返します。</li> <li>最も右の<code>target</code>を見つけるには:<code>target + 0.5</code>を探索することに変換でき、ポインタ\\(j\\)を返します。</li> </ul> <p></p> <p> 図 10-8 Transforming the search for boundaries into the search for an element </p> <p>コードはここでは省略されていますが、このアプローチについて注意すべき2つの重要な点があります。</p> <ul> <li>与えられた配列<code>nums</code>には小数が含まれていないため、等しい場合の処理は心配ありません。</li> <li>ただし、このアプローチで小数を導入するには、<code>target</code>変数を浮動小数点型に変更する必要があります(Pythonでは変更は不要です)。</li> </ul>","path":["第 10 章 探索","10.3 二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2 二分探索による挿入","text":"<p>二分探索は目標要素を探索するだけでなく、目標要素の挿入位置を探索するなど、多くの変種問題を解決するためにも使用されます。</p>","path":["第 10 章 探索","10.2 二分探索による挿入"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021","level":2,"title":"10.2.1 重複要素がない場合","text":"<p>Question</p> <p>一意の要素を持つ長さ\\(n\\)のソート済み配列<code>nums</code>と要素<code>target</code>が与えられ、ソート順を維持しながら<code>target</code>を<code>nums</code>に挿入します。<code>target</code>が配列にすでに存在する場合は、既存の要素の左側に挿入します。挿入後の配列における<code>target</code>のインデックスを返してください。下図に示す例を参照してください。</p> <p></p> <p> 図 10-4 Example data for binary search insertion point </p> <p>前のセクションの二分探索コードを再利用したい場合、以下の2つの質問に答える必要があります。</p> <p>質問1:配列にすでに<code>target</code>が含まれている場合、挿入位置は既存要素のインデックスになりますか?</p> <p><code>target</code>を等しい要素の左側に挿入するという要件は、新しく挿入される<code>target</code>が元の<code>target</code>の位置を置き換えることを意味します。つまり、配列に<code>target</code>が含まれている場合、挿入位置は確かにその<code>target</code>のインデックスです。</p> <p>質問2:配列に<code>target</code>が含まれていない場合、どのインデックスに挿入されますか?</p> <p>二分探索プロセスをさらに考えてみましょう:<code>nums[m] < target</code>のとき、ポインタ\\(i\\)が移動します。これは、ポインタ\\(i\\)が<code>target</code>以上の要素に近づいていることを意味します。同様に、ポインタ\\(j\\)は常に<code>target</code>以下の要素に近づいています。</p> <p>したがって、二分の終了時には確実に:\\(i\\)は<code>target</code>より大きい最初の要素を指し、\\(j\\)は<code>target</code>より小さい最初の要素を指します。配列に<code>target</code>が含まれていない場合、挿入位置は\\(i\\)であることは明らかです。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py<pre><code>def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n \"\"\"挿入位置の二分探索(重複要素なし)\"\"\"\n i, j = 0, len(nums) - 1 # 両端閉区間 [0, n-1] を初期化\n while i <= j:\n m = i + (j - i) // 2 # 中点インデックス m を計算\n if nums[m] < target:\n i = m + 1 # ターゲットは区間 [m+1, j] にある\n elif nums[m] > target:\n j = m - 1 # ターゲットは区間 [i, m-1] にある\n else:\n return m # ターゲットが見つかった場合、挿入位置 m を返す\n # ターゲットが見つからなかった場合、挿入位置 i を返す\n return i\n</code></pre> binary_search_insertion.cpp<pre><code>/* 挿入ポイントの二分探索(重複要素なし) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n int i = 0, j = nums.size() - 1; // 両端閉区間[0, n-1]を初期化\n while (i <= j) {\n int m = i + (j - i) / 2; // 中点インデックスmを計算\n if (nums[m] < target) {\n i = m + 1; // ターゲットは区間[m+1, j]にある\n } else if (nums[m] > target) {\n j = m - 1; // ターゲットは区間[i, m-1]にある\n } else {\n return m; // ターゲットが見つかったため、挿入ポイントmを返す\n }\n }\n // ターゲットが見つからなかったため、挿入ポイントiを返す\n return i;\n}\n</code></pre> binary_search_insertion.java<pre><code>/* 挿入点の二分探索(重複要素なし) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n int i = 0, j = nums.length - 1; // 両端閉区間 [0, n-1] を初期化\n while (i <= j) {\n int m = i + (j - i) / 2; // 中点インデックス m を計算\n if (nums[m] < target) {\n i = m + 1; // target は区間 [m+1, j] にある\n } else if (nums[m] > target) {\n j = m - 1; // target は区間 [i, m-1] にある\n } else {\n return m; // target を見つけたので、挿入点 m を返す\n }\n }\n // target を見つけられなかったので、挿入点 i を返す\n return i;\n}\n</code></pre> binary_search_insertion.cs<pre><code>[class]{binary_search_insertion}-[func]{BinarySearchInsertionSimple}\n</code></pre> binary_search_insertion.go<pre><code>[class]{}-[func]{binarySearchInsertionSimple}\n</code></pre> binary_search_insertion.swift<pre><code>[class]{}-[func]{binarySearchInsertionSimple}\n</code></pre> binary_search_insertion.js<pre><code>[class]{}-[func]{binarySearchInsertionSimple}\n</code></pre> binary_search_insertion.ts<pre><code>[class]{}-[func]{binarySearchInsertionSimple}\n</code></pre> binary_search_insertion.dart<pre><code>[class]{}-[func]{binarySearchInsertionSimple}\n</code></pre> binary_search_insertion.rs<pre><code>[class]{}-[func]{binary_search_insertion_simple}\n</code></pre> binary_search_insertion.c<pre><code>[class]{}-[func]{binarySearchInsertionSimple}\n</code></pre> binary_search_insertion.kt<pre><code>[class]{}-[func]{binarySearchInsertionSimple}\n</code></pre> binary_search_insertion.rb<pre><code>[class]{}-[func]{binary_search_insertion_simple}\n</code></pre>","path":["第 10 章 探索","10.2 二分探索による挿入"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022","level":2,"title":"10.2.2 重複要素がある場合","text":"<p>Question</p> <p>前の質問に基づいて、配列に重複要素が含まれている可能性があると仮定し、他はすべて同じとします。</p> <p>配列に<code>target</code>の複数の出現がある場合、通常の二分探索は<code>target</code>の1つの出現のインデックスのみを返すことができ、その位置の左右に<code>target</code>の出現がいくつあるかを特定することはできません。</p> <p>問題では目標要素を最も左の位置に挿入することが要求されているため、配列内の最も左の<code>target</code>のインデックスを見つける必要があります。最初に下図に示すステップを通してこれを実装することを考えてみましょう。</p> <ol> <li>二分探索を実行して<code>target</code>の任意のインデックス、例えば\\(k\\)を見つけます。</li> <li>インデックス\\(k\\)から開始して、最も左の<code>target</code>の出現が見つかるまで左に線形探索を行い、このインデックスを返します。</li> </ol> <p></p> <p> 図 10-5 Linear search for the insertion point of duplicate elements </p> <p>この方法は実現可能ですが、線形探索を含むため、時間計算量は\\(O(n)\\)です。この方法は、配列に多くの重複する<code>target</code>が含まれている場合に非効率です。</p> <p>今度は二分探索コードを拡張することを考えてみましょう。下図に示すように、全体的なプロセスは同じままです。各ラウンドで、まず中間インデックス\\(m\\)を計算し、次に<code>target</code>と<code>nums[m]</code>の値を比較して、以下のケースになります。</p> <ul> <li><code>nums[m] < target</code>または<code>nums[m] > target</code>のとき、これは<code>target</code>がまだ見つかっていないことを意味するため、通常の二分探索を使用して探索範囲を狭め、ポインタ\\(i\\)と\\(j\\)を<code>target</code>に近づけます。</li> <li><code>nums[m] == target</code>のとき、これは<code>target</code>より小さい要素が範囲\\([i, m - 1]\\)にあることを示すため、\\(j = m - 1\\)を使用して範囲を狭め、ポインタ\\(j\\)を<code>target</code>より小さい要素に近づけます。</li> </ul> <p>ループ後、\\(i\\)は最も左の<code>target</code>を指し、\\(j\\)は<code>target</code>より小さい最初の要素を指すため、インデックス\\(i\\)が挿入位置です。</p> <1><2><3><4><5><6><7><8> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 10-6 Steps for binary search insertion point of duplicate elements </p> <p>以下のコードを観察してください。分岐<code>nums[m] > target</code>と<code>nums[m] == target</code>の操作は同じであるため、これら2つの分岐をマージできます。</p> <p>それでも、ロジックがより明確になり、可読性が向上するため、条件を展開したままにしておくことができます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py<pre><code>def binary_search_insertion(nums: list[int], target: int) -> int:\n \"\"\"挿入位置の二分探索(重複要素あり)\"\"\"\n i, j = 0, len(nums) - 1 # 両端閉区間 [0, n-1] を初期化\n while i <= j:\n m = i + (j - i) // 2 # 中点インデックス m を計算\n if nums[m] < target:\n i = m + 1 # ターゲットは区間 [m+1, j] にある\n elif nums[m] > target:\n j = m - 1 # ターゲットは区間 [i, m-1] にある\n else:\n j = m - 1 # ターゲット未満の最初の要素は区間 [i, m-1] にある\n # 挿入位置 i を返す\n return i\n</code></pre> binary_search_insertion.cpp<pre><code>/* 挿入ポイントの二分探索(重複要素あり) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n int i = 0, j = nums.size() - 1; // 両端閉区間[0, n-1]を初期化\n while (i <= j) {\n int m = i + (j - i) / 2; // 中点インデックスmを計算\n if (nums[m] < target) {\n i = m + 1; // ターゲットは区間[m+1, j]にある\n } else if (nums[m] > target) {\n j = m - 1; // ターゲットは区間[i, m-1]にある\n } else {\n j = m - 1; // ターゲット未満の最初の要素は区間[i, m-1]にある\n }\n }\n // 挿入ポイントiを返す\n return i;\n}\n</code></pre> binary_search_insertion.java<pre><code>/* 挿入点の二分探索(重複要素あり) */\nint binarySearchInsertion(int[] nums, int target) {\n int i = 0, j = nums.length - 1; // 両端閉区間 [0, n-1] を初期化\n while (i <= j) {\n int m = i + (j - i) / 2; // 中点インデックス m を計算\n if (nums[m] < target) {\n i = m + 1; // target は区間 [m+1, j] にある\n } else if (nums[m] > target) {\n j = m - 1; // target は区間 [i, m-1] にある\n } else {\n j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n }\n }\n // 挿入点 i を返す\n return i;\n}\n</code></pre> binary_search_insertion.cs<pre><code>[class]{binary_search_insertion}-[func]{BinarySearchInsertion}\n</code></pre> binary_search_insertion.go<pre><code>[class]{}-[func]{binarySearchInsertion}\n</code></pre> binary_search_insertion.swift<pre><code>[class]{}-[func]{binarySearchInsertion}\n</code></pre> binary_search_insertion.js<pre><code>[class]{}-[func]{binarySearchInsertion}\n</code></pre> binary_search_insertion.ts<pre><code>[class]{}-[func]{binarySearchInsertion}\n</code></pre> binary_search_insertion.dart<pre><code>[class]{}-[func]{binarySearchInsertion}\n</code></pre> binary_search_insertion.rs<pre><code>[class]{}-[func]{binary_search_insertion}\n</code></pre> binary_search_insertion.c<pre><code>[class]{}-[func]{binarySearchInsertion}\n</code></pre> binary_search_insertion.kt<pre><code>[class]{}-[func]{binarySearchInsertion}\n</code></pre> binary_search_insertion.rb<pre><code>[class]{}-[func]{binary_search_insertion}\n</code></pre> <p>Tip</p> <p>このセクションのコードは「閉区間」を使用しています。「左閉右開」に興味がある場合は、自分でコードを実装してみてください。</p> <p>要約すると、二分探索は本質的にポインタ\\(i\\)と\\(j\\)の探索目標を設定することです。これらの目標は特定の要素(<code>target</code>など)または要素の範囲(<code>target</code>より小さいものなど)である可能性があります。</p> <p>二分探索の連続ループにおいて、ポインタ\\(i\\)と\\(j\\)は段階的に事前定義された目標に近づきます。最終的に、それらは答えを見つけるか、境界を越えた後に停止します。</p>","path":["第 10 章 探索","10.2 二分探索による挿入"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4 ハッシュ最適化戦略","text":"<p>アルゴリズム問題において、線形探索をハッシュベースの探索に置き換えることで、アルゴリズムの時間計算量を削減することがよくあります。アルゴリズム問題を使用して理解を深めましょう。</p> <p>Question</p> <p>整数配列<code>nums</code>と目標要素<code>target</code>が与えられ、配列内で「和」が<code>target</code>に等しい2つの要素を探索し、それらの配列インデックスを返してください。任意の解が受け入れられます。</p>","path":["第 10 章 探索","10.4 ハッシュ最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1 線形探索:時間を空間と交換","text":"<p>すべての可能な組み合わせを直接横断することを考えてみます。下図に示すように、ネストしたループを開始し、各反復で2つの整数の和が<code>target</code>に等しいかどうかを判断します。そうであれば、それらのインデックスを返します。</p> <p></p> <p> 図 10-9 Linear search solution for two-sum problem </p> <p>コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py<pre><code>def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n \"\"\"方法一:ブルートフォース列挙\"\"\"\n # 二重ループ、時間計算量は O(n^2)\n for i in range(len(nums) - 1):\n for j in range(i + 1, len(nums)):\n if nums[i] + nums[j] == target:\n return [i, j]\n return []\n</code></pre> two_sum.cpp<pre><code>/* 方法一:ブルートフォース列挙 */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n int size = nums.size();\n // 二重ループ、時間計算量はO(n^2)\n for (int i = 0; i < size - 1; i++) {\n for (int j = i + 1; j < size; j++) {\n if (nums[i] + nums[j] == target)\n return {i, j};\n }\n }\n return {};\n}\n</code></pre> two_sum.java<pre><code>/* 方法一: 暴力列挙 */\nint[] twoSumBruteForce(int[] nums, int target) {\n int size = nums.length;\n // 二重ループ、時間計算量は O(n^2)\n for (int i = 0; i < size - 1; i++) {\n for (int j = i + 1; j < size; j++) {\n if (nums[i] + nums[j] == target)\n return new int[] { i, j };\n }\n }\n return new int[0];\n}\n</code></pre> two_sum.cs<pre><code>[class]{two_sum}-[func]{TwoSumBruteForce}\n</code></pre> two_sum.go<pre><code>[class]{}-[func]{twoSumBruteForce}\n</code></pre> two_sum.swift<pre><code>[class]{}-[func]{twoSumBruteForce}\n</code></pre> two_sum.js<pre><code>[class]{}-[func]{twoSumBruteForce}\n</code></pre> two_sum.ts<pre><code>[class]{}-[func]{twoSumBruteForce}\n</code></pre> two_sum.dart<pre><code>[class]{}-[func]{twoSumBruteForce}\n</code></pre> two_sum.rs<pre><code>[class]{}-[func]{two_sum_brute_force}\n</code></pre> two_sum.c<pre><code>[class]{}-[func]{twoSumBruteForce}\n</code></pre> two_sum.kt<pre><code>[class]{}-[func]{twoSumBruteForce}\n</code></pre> two_sum.rb<pre><code>[class]{}-[func]{two_sum_brute_force}\n</code></pre> <p>この方法の時間計算量は\\(O(n^2)\\)、空間計算量は\\(O(1)\\)で、大容量データでは非常に時間がかかる可能性があります。</p>","path":["第 10 章 探索","10.4 ハッシュ最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042","level":2,"title":"10.4.2 ハッシュ探索:空間を時間と交換","text":"<p>ハッシュテーブルの使用を考えてみましょう。キーと値のペアはそれぞれ配列要素とそのインデックスです。配列をループし、各反復中に下図に示すステップを実行します。</p> <ol> <li>数値<code>target - nums[i]</code>がハッシュテーブルにあるかどうかを確認します。ある場合は、これら2つの要素のインデックスを直接返します。</li> <li>キーと値のペア<code>nums[i]</code>とインデックス<code>i</code>をハッシュテーブルに追加します。</li> </ol> <1><2><3> <p></p> <p></p> <p></p> <p> 図 10-10 Help hash table solve two-sum </p> <p>実装コードは以下に示され、単一のループのみが必要です:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py<pre><code>def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n \"\"\"方法二:補助ハッシュテーブル\"\"\"\n # 補助ハッシュテーブル、空間計算量は O(n)\n dic = {}\n # 単一ループ、時間計算量は O(n)\n for i in range(len(nums)):\n if target - nums[i] in dic:\n return [dic[target - nums[i]], i]\n dic[nums[i]] = i\n return []\n</code></pre> two_sum.cpp<pre><code>/* 方法二:補助ハッシュテーブル */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n int size = nums.size();\n // 補助ハッシュテーブル、空間計算量はO(n)\n unordered_map<int, int> dic;\n // 単層ループ、時間計算量はO(n)\n for (int i = 0; i < size; i++) {\n if (dic.find(target - nums[i]) != dic.end()) {\n return {dic[target - nums[i]], i};\n }\n dic.emplace(nums[i], i);\n }\n return {};\n}\n</code></pre> two_sum.java<pre><code>/* 方法二: 補助ハッシュテーブル */\nint[] twoSumHashTable(int[] nums, int target) {\n int size = nums.length;\n // 補助ハッシュテーブル、空間計算量は O(n)\n Map<Integer, Integer> dic = new HashMap<>();\n // 単一層ループ、時間計算量は O(n)\n for (int i = 0; i < size; i++) {\n if (dic.containsKey(target - nums[i])) {\n return new int[] { dic.get(target - nums[i]), i };\n }\n dic.put(nums[i], i);\n }\n return new int[0];\n}\n</code></pre> two_sum.cs<pre><code>[class]{two_sum}-[func]{TwoSumHashTable}\n</code></pre> two_sum.go<pre><code>[class]{}-[func]{twoSumHashTable}\n</code></pre> two_sum.swift<pre><code>[class]{}-[func]{twoSumHashTable}\n</code></pre> two_sum.js<pre><code>[class]{}-[func]{twoSumHashTable}\n</code></pre> two_sum.ts<pre><code>[class]{}-[func]{twoSumHashTable}\n</code></pre> two_sum.dart<pre><code>[class]{}-[func]{twoSumHashTable}\n</code></pre> two_sum.rs<pre><code>[class]{}-[func]{two_sum_hash_table}\n</code></pre> two_sum.c<pre><code>[class]{HashTable}-[func]{}\n\n[class]{}-[func]{twoSumHashTable}\n</code></pre> two_sum.kt<pre><code>[class]{}-[func]{twoSumHashTable}\n</code></pre> two_sum.rb<pre><code>[class]{}-[func]{two_sum_hash_table}\n</code></pre> <p>この方法は、ハッシュ探索を使用することで時間計算量を\\(O(n^2)\\)から\\(O(n)\\)に削減し、実行時効率を大幅に向上させます。</p> <p>追加のハッシュテーブルを維持する必要があるため、空間計算量は\\(O(n)\\)です。それにもかかわらず、この方法は全体的により均衡のとれた時空間効率を持ち、この問題の最適解となります。</p>","path":["第 10 章 探索","10.4 ハッシュ最適化戦略"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5 探索アルゴリズムの再検討","text":"<p>探索アルゴリズム(検索アルゴリズム)は、配列、連結リスト、木、グラフなどのデータ構造内で特定の基準を満たす1つ以上の要素を取得するために使用されます。</p> <p>探索アルゴリズムは、そのアプローチに基づいて以下の2つのカテゴリに分けることができます。</p> <ul> <li>データ構造を横断することで目標要素を特定する:配列、連結リスト、木、グラフの横断など。</li> <li>データの組織構造や既存のデータを使用して効率的な要素探索を実現する:二分探索、ハッシュ探索、二分探索木探索など。</li> </ul> <p>これらのトピックは前の章で紹介されたため、私たちには馴染みのないものではありません。このセクションでは、より体系的な観点から探索アルゴリズムを再検討します。</p>","path":["第 10 章 探索","10.5 探索アルゴリズムの再検討"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1 総当たり探索","text":"<p>総当たり探索は、データ構造のすべての要素を横断することで目標要素を特定します。</p> <ul> <li>「線形探索」は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から開始し、目標要素が見つかるか、目標要素を見つけることなく他端に到達するまで、各要素に一つずつアクセスします。</li> <li>「幅優先探索」と「深さ優先探索」は、グラフと木の2つの横断戦略です。幅優先探索は初期ノードから開始し、層ごと(左から右へ)に探索し、近くから遠くのノードにアクセスします。深さ優先探索は初期ノードから開始し、パスの終端(上から下へ)まで追跡し、その後バックトラックして他のパスを試し、データ構造全体が横断されるまで続行します。</li> </ul> <p>総当たり探索の利点は、その単純さと汎用性であり、**データの前処理や追加のデータ構造の助けが不要**です。</p> <p>ただし、**このタイプのアルゴリズムの時間計算量は\\(O(n)\\)**で、\\(n\\)は要素数であるため、大規模なデータセットでは性能が悪くなります。</p>","path":["第 10 章 探索","10.5 探索アルゴリズムの再検討"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052","level":2,"title":"10.5.2 適応的探索","text":"<p>適応的探索は、データの固有の性質(順序など)を使用して探索プロセスを最適化し、それにより目標要素をより効率的に特定します。</p> <ul> <li>「二分探索」はデータの整列性を使用して効率的な探索を実現し、配列にのみ適用可能です。</li> <li>「ハッシュ探索」はハッシュテーブルを使用して探索データと目標データの間にキーと値のマッピングを確立し、それによりクエリ操作を実装します。</li> <li>特定の木構造(二分探索木など)での「木探索」は、ノード値の比較に基づいてノードを迅速に除外し、それにより目標要素を特定します。</li> </ul> <p>これらのアルゴリズムの利点は高効率であり、時間計算量が\\(O(\\log n)\\)または\\(O(1)\\)にまで達します。</p> <p>ただし、これらのアルゴリズムを使用するには、多くの場合データの前処理が必要です。例えば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索の両方で追加のデータ構造の助けが必要です。これらの構造を維持することも、時間と空間の面でより多くのオーバーヘッドが必要です。</p> <p>Tip</p> <p>適応的探索アルゴリズムは、多くの場合探索アルゴリズムと呼ばれ、主に特定のデータ構造内で目標要素を迅速に取得するために使用されます。</p>","path":["第 10 章 探索","10.5 探索アルゴリズムの再検討"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053","level":2,"title":"10.5.3 探索方法の選択","text":"<p>サイズ\\(n\\)のデータセットが与えられた場合、線形探索、二分探索、木探索、ハッシュ探索、またはその他の方法を使用して目標要素を取得できます。これらの方法の動作原理を下図に示します。</p> <p></p> <p> 図 10-11 Various search strategies </p> <p>前述の方法の特性と操作効率を以下の表に示します。</p> <p> 表 10-1 探索アルゴリズム効率の比較 </p> 線形探索 二分探索 木探索 ハッシュ探索 要素探索 \\(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)\\) データ順序性 無順序 順序 順序 無順序 <p>探索アルゴリズムの選択は、データ量、探索性能要件、データクエリと更新の頻度などにも依存します。</p> <p>線形探索</p> <ul> <li>汎用性が良く、データ前処理操作が不要です。データを一度だけクエリする必要がある場合、他の3つの方法のデータ前処理時間は線形探索の時間よりも長くなります。</li> <li>小容量のデータに適しており、時間計算量が効率に与える影響は小さいです。</li> <li>データ更新が非常に頻繁なシナリオに適しています。この方法はデータの追加メンテナンスを必要としないためです。</li> </ul> <p>二分探索</p> <ul> <li>より大きなデータ量に適しており、安定した性能と最悪ケースの時間計算量\\(O(\\log n)\\)を持ちます。</li> <li>ただし、データ量が大きすぎることはできません。配列の保存には連続したメモリ空間が必要だからです。</li> <li>頻繁な追加と削除があるシナリオには適していません。順序付き配列の維持に多くのオーバーヘッドが発生するためです。</li> </ul> <p>ハッシュ探索</p> <ul> <li>高速クエリ性能が不可欠なシナリオに適しており、平均時間計算量は\\(O(1)\\)です。</li> <li>順序付きデータや範囲探索が必要なシナリオには適していません。ハッシュテーブルはデータの順序性を維持できないためです。</li> <li>ハッシュ関数とハッシュ衝突処理戦略への依存度が高く、性能劣化のリスクが大きいです。</li> <li>過度に大容量のデータには適していません。ハッシュテーブルは衝突を最小化し、良好なクエリ性能を提供するために追加の空間が必要だからです。</li> </ul> <p>木探索</p> <ul> <li>大容量データに適しています。木ノードはメモリ内に分散して保存されるためです。</li> <li>順序付きデータの維持や範囲探索に適しています。</li> <li>ノードの継続的な追加と削除により、二分探索木は偏る可能性があり、時間計算量が\\(O(n)\\)に劣化する可能性があります。</li> <li>AVL木や赤黒木を使用する場合、操作は\\(O(\\log n)\\)効率で安定して実行できますが、木のバランスを維持する操作により追加のオーバーヘッドが追加されます。</li> </ul>","path":["第 10 章 探索","10.5 探索アルゴリズムの再検討"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6 まとめ","text":"<ul> <li>二分探索はデータの順序に依存し、探索区間を反復的に半分にすることで探索を実行します。入力データがソート済みである必要があり、配列または配列ベースのデータ構造にのみ適用可能です。</li> <li>無順序データセット内のエントリを見つけるには、総当たり探索が必要な場合があります。データ構造に基づいて異なる探索アルゴリズムを適用できます:線形探索は配列と連結リストに適しており、幅優先探索(BFS)と深さ優先探索(DFS)はグラフと木に適しています。これらのアルゴリズムは非常に汎用性が高く、データの前処理が不要ですが、\\(O(n)\\)という高い時間計算量を持ちます。</li> <li>ハッシュ探索、木探索、二分探索は効率的な探索方法で、特定のデータ構造内で目標要素を迅速に特定できます。これらのアルゴリズムは非常に効率的で、時間計算量が\\(O(\\log n)\\)または\\(O(1)\\)にまで達しますが、通常は追加のデータ構造を収容するために追加の空間が必要です。</li> <li>実際には、データ量、探索性能要件、データクエリと更新頻度などの要因を分析して、適切な探索方法を選択する必要があります。</li> <li>線形探索は小さなデータや頻繁に更新される(変動性の高い)データに理想的です。二分探索は大きくてソート済みのデータに適しています。ハッシュ探索は高いクエリ効率が必要で範囲クエリが不要なデータに適しています。木探索は順序を維持し、範囲クエリをサポートする必要がある大きな動的データに最も適しています。</li> <li>線形探索をハッシュ探索に置き換えることは、実行時性能を最適化する一般的な戦略で、時間計算量を\\(O(n)\\)から\\(O(1)\\)に削減します。</li> </ul>","path":["第 10 章 探索","10.6 まとめ"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"第 11 章 ソート","text":"<p>Abstract</p> <p>ソートは混沌を秩序に変える魔法の鍵のようなもので、データをより効率的に理解し処理することを可能にします。</p> <p>単純な昇順であろうと複雑なカテゴリ配列であろうと、ソートはデータの調和美を明らかにします。</p>","path":["第 11 章 ソート"],"tags":[]},{"location":"chapter_sorting/#_1","level":2,"title":"章の内容","text":"<ul> <li>11.1 ソートアルゴリズム</li> <li>11.2 選択ソート</li> <li>11.3 バブルソート</li> <li>11.4 挿入ソート</li> <li>11.5 クイックソート</li> <li>11.6 マージソート</li> <li>11.7 ヒープソート</li> <li>11.8 バケットソート</li> <li>11.9 計数ソート</li> <li>11.10 基数ソート</li> <li>11.11 まとめ</li> </ul>","path":["第 11 章 ソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3 バブルソート","text":"<p>バブルソートは、隣接する要素を継続的に比較し交換することで動作します。このプロセスは泡が底から上に上昇するようなものなので、「バブルソート」と名付けられました。</p> <p>下図に示すように、バブリングプロセスは要素交換を使用してシミュレートできます:配列の左端から開始して右に移動し、隣接する要素の各ペアを比較します。左の要素が右の要素より大きい場合は、それらを交換します。横断後、最大要素は配列の右端にバブルアップします。</p> <1><2><3><4><5><6><7> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 11-4 Simulating bubble process using element swap </p>","path":["第 11 章 ソート","11.3 バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131","level":2,"title":"11.3.1 アルゴリズムプロセス","text":"<p>配列の長さを\\(n\\)とします。バブルソートのステップは下図に示されます:</p> <ol> <li>まず、\\(n\\)個の要素に対して1回の「バブル」パスを実行し、最大要素を正しい位置に交換します。</li> <li>次に、残りの\\(n - 1\\)個の要素に対して「バブル」パスを実行し、2番目に大きい要素を正しい位置に交換します。</li> <li>この方法で続行します;\\(n - 1\\)回のパスの後、最大\\(n - 1\\)個の要素が正しい位置に移動されます。</li> <li>残りの唯一の要素は**必ず**最小であるため、**さらなる**ソートは必要ありません。この時点で、配列はソートされます。</li> </ol> <p></p> <p> 図 11-5 Bubble sort process </p> <p>コード例は以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py<pre><code>def bubble_sort(nums: list[int]):\n \"\"\"バブルソート\"\"\"\n n = len(nums)\n # 外側のループ:未ソート範囲は [0, i]\n for i in range(n - 1, 0, -1):\n # 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動\n for j in range(i):\n if nums[j] > nums[j + 1]:\n # nums[j] と nums[j + 1] を交換\n nums[j], nums[j + 1] = nums[j + 1], nums[j]\n</code></pre> bubble_sort.cpp<pre><code>/* バブルソート */\nvoid bubbleSort(vector<int> &nums) {\n // 外側ループ:未ソート範囲は[0, i]\n for (int i = nums.size() - 1; i > 0; i--) {\n // 内側ループ:未ソート範囲[0, i]内の最大要素を範囲の右端に交換\n for (int j = 0; j < i; j++) {\n if (nums[j] > nums[j + 1]) {\n // nums[j]とnums[j + 1]を交換\n // ここではstdのswapを使用\n swap(nums[j], nums[j + 1]);\n }\n }\n }\n}\n</code></pre> bubble_sort.java<pre><code>/* バブルソート */\nvoid bubbleSort(int[] nums) {\n // 外側ループ: 未ソート範囲は [0, i]\n for (int i = nums.length - 1; i > 0; i--) {\n // 内側ループ: 未ソート範囲 [0, i] の最大要素を範囲の右端に交換\n for (int j = 0; j < i; j++) {\n if (nums[j] > nums[j + 1]) {\n // nums[j] と nums[j + 1] を交換\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n }\n }\n }\n}\n</code></pre> bubble_sort.cs<pre><code>[class]{bubble_sort}-[func]{BubbleSort}\n</code></pre> bubble_sort.go<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> bubble_sort.swift<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> bubble_sort.js<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> bubble_sort.ts<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> bubble_sort.dart<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> bubble_sort.rs<pre><code>[class]{}-[func]{bubble_sort}\n</code></pre> bubble_sort.c<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> bubble_sort.kt<pre><code>[class]{}-[func]{bubbleSort}\n</code></pre> bubble_sort.rb<pre><code>[class]{}-[func]{bubble_sort}\n</code></pre>","path":["第 11 章 ソート","11.3 バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132","level":2,"title":"11.3.2 効率の最適化","text":"<p>「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、<code>flag</code>変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。</p> <p>この最適化があっても、バブルソートの最悪時間計算量と平均時間計算量は\\(O(n^2)\\)のままです。ただし、入力配列がすでにソートされている場合、最良ケース時間計算量は\\(O(n)\\)まで低くなる可能性があります。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py<pre><code>def bubble_sort_with_flag(nums: list[int]):\n \"\"\"バブルソート(フラグによる最適化)\"\"\"\n n = len(nums)\n # 外側のループ:未ソート範囲は [0, i]\n for i in range(n - 1, 0, -1):\n flag = False # フラグを初期化\n # 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動\n for j in range(i):\n if nums[j] > nums[j + 1]:\n # nums[j] と nums[j + 1] を交換\n nums[j], nums[j + 1] = nums[j + 1], nums[j]\n flag = True # 要素を交換したことを記録\n if not flag:\n break # この回の「バブリング」で要素が交換されなかった場合、終了\n</code></pre> bubble_sort.cpp<pre><code>/* バブルソート(フラグ最適化版)*/\nvoid bubbleSortWithFlag(vector<int> &nums) {\n // 外側ループ:未ソート範囲は[0, i]\n for (int i = nums.size() - 1; i > 0; i--) {\n bool flag = false; // フラグを初期化\n // 内側ループ:未ソート範囲[0, i]内の最大要素を範囲の右端に交換\n for (int j = 0; j < i; j++) {\n if (nums[j] > nums[j + 1]) {\n // nums[j]とnums[j + 1]を交換\n // ここではstdのswapを使用\n swap(nums[j], nums[j + 1]);\n flag = true; // 交換された要素を記録\n }\n }\n if (!flag)\n break; // この回の「バブリング」で要素が交換されなかった場合、終了\n }\n}\n</code></pre> bubble_sort.java<pre><code>/* バブルソート(フラグによる最適化) */\nvoid bubbleSortWithFlag(int[] nums) {\n // 外側ループ: 未ソート範囲は [0, i]\n for (int i = nums.length - 1; i > 0; i--) {\n boolean flag = false; // フラグを初期化\n // 内側ループ: 未ソート範囲 [0, i] の最大要素を範囲の右端に交換\n for (int j = 0; j < i; j++) {\n if (nums[j] > nums[j + 1]) {\n // nums[j] と nums[j + 1] を交換\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n flag = true; // 交換された要素を記録\n }\n }\n if (!flag)\n break; // この「バブリング」ラウンドで要素が交換されなかった場合、終了\n }\n}\n</code></pre> bubble_sort.cs<pre><code>[class]{bubble_sort}-[func]{BubbleSortWithFlag}\n</code></pre> bubble_sort.go<pre><code>[class]{}-[func]{bubbleSortWithFlag}\n</code></pre> bubble_sort.swift<pre><code>[class]{}-[func]{bubbleSortWithFlag}\n</code></pre> bubble_sort.js<pre><code>[class]{}-[func]{bubbleSortWithFlag}\n</code></pre> bubble_sort.ts<pre><code>[class]{}-[func]{bubbleSortWithFlag}\n</code></pre> bubble_sort.dart<pre><code>[class]{}-[func]{bubbleSortWithFlag}\n</code></pre> bubble_sort.rs<pre><code>[class]{}-[func]{bubble_sort_with_flag}\n</code></pre> bubble_sort.c<pre><code>[class]{}-[func]{bubbleSortWithFlag}\n</code></pre> bubble_sort.kt<pre><code>[class]{}-[func]{bubbleSortWithFlag}\n</code></pre> bubble_sort.rb<pre><code>[class]{}-[func]{bubble_sort_with_flag}\n</code></pre>","path":["第 11 章 ソート","11.3 バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133","level":2,"title":"11.3.3 アルゴリズムの特性","text":"<ul> <li>\\(O(n^2)\\)の時間計算量、適応ソート。 各「バブリング」ラウンドは長さ\\(n - 1\\)、\\(n - 2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\)の配列セグメントを横断し、合計は\\((n - 1) n / 2\\)となります。<code>flag</code>最適化により、配列がすでにソートされている場合、最良ケース時間計算量は\\(O(n)\\)に達する可能性があります。</li> <li>\\(O(1)\\)の空間計算量、インプレースソート。 ポインタ\\(i\\)と\\(j\\)によって定数量の追加空間のみが使用されます。</li> <li>安定ソート。 等しい要素は「バブリング」中に交換されないため、元の順序が保持され、これは安定ソートになります。</li> </ul>","path":["第 11 章 ソート","11.3 バブルソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8 バケットソート","text":"<p>前述のソートアルゴリズムはすべて「比較ベースのソートアルゴリズム」で、値を比較することで要素をソートします。このようなソートアルゴリズムは \\(O(n \\log n)\\) より良い時間計算量を持つことはできません。次に、線形時間計算量を達成できるいくつかの「非比較ソートアルゴリズム」について議論します。</p> <p>バケットソートは分割統治戦略の典型的な応用です。一連の順序付けられたバケットを設定し、各バケットがデータの範囲を含み、入力データをこれらのバケットに均等に分散させることで動作します。そして、各バケット内のデータを個別にソートします。最後に、すべてのバケットからのソート済みデータを順次マージして最終結果を生成します。</p>","path":["第 11 章 ソート","11.8 バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181","level":2,"title":"11.8.1 アルゴリズムの過程","text":"<p>長さ \\(n\\) の配列で、\\([0, 1)\\) の範囲の浮動小数点数を考えてみます。バケットソートの過程は以下の図に示されています。</p> <ol> <li>\\(k\\) 個のバケットを初期化し、\\(n\\) 個の要素をこれらの \\(k\\) 個のバケットに分散させます。</li> <li>各バケットを個別にソートします(プログラミング言語の組み込みソート関数を使用)。</li> <li>最小から最大のバケットの順序で結果をマージします。</li> </ol> <p></p> <p> 図 11-13 バケットソートアルゴリズムの過程 </p> <p>コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py<pre><code>def bucket_sort(nums: list[float]):\n \"\"\"バケットソート\"\"\"\n # k = n/2 個のバケットを初期化、各バケットに平均2個の要素を配置することを期待\n k = len(nums) // 2\n buckets = [[] for _ in range(k)]\n # 1. 配列要素を各バケットに分散\n for num in nums:\n # 入力データ範囲は [0, 1)、num * k を使用してインデックス範囲 [0, k-1] にマッピング\n i = int(num * k)\n # num をバケット i に追加\n buckets[i].append(num)\n # 2. 各バケットをソート\n for bucket in buckets:\n # 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能\n bucket.sort()\n # 3. バケットを走査して結果をマージ\n i = 0\n for bucket in buckets:\n for num in bucket:\n nums[i] = num\n i += 1\n</code></pre> bucket_sort.cpp<pre><code>/* バケットソート */\nvoid bucketSort(vector<float> &nums) {\n // k = n/2個のバケットを初期化、各バケットに2つの要素を割り当てることを期待\n int k = nums.size() / 2;\n vector<vector<float>> buckets(k);\n // 1. 配列要素を各バケットに分配\n for (float num : nums) {\n // 入力データ範囲は[0, 1)、num * kを使用してインデックス範囲[0, k-1]にマップ\n int i = num * k;\n // bucket_idxバケットに数値を追加\n buckets[i].push_back(num);\n }\n // 2. 各バケットをソート\n for (vector<float> &bucket : buckets) {\n // 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能\n sort(bucket.begin(), bucket.end());\n }\n // 3. バケットを走査して結果をマージ\n int i = 0;\n for (vector<float> &bucket : buckets) {\n for (float num : bucket) {\n nums[i++] = num;\n }\n }\n}\n</code></pre> bucket_sort.java<pre><code>/* バケットソート */\nvoid bucketSort(float[] nums) {\n // k = n/2 個のバケットを初期化、各バケットに期待される要素数は 2 個\n int k = nums.length / 2;\n List<List<Float>> buckets = new ArrayList<>();\n for (int i = 0; i < k; i++) {\n buckets.add(new ArrayList<>());\n }\n // 1. 配列要素を各バケットに分散\n for (float num : nums) {\n // 入力データ範囲は [0, 1)、num * k を使ってインデックス範囲 [0, k-1] にマッピング\n int i = (int) (num * k);\n // num をバケット i に追加\n buckets.get(i).add(num);\n }\n // 2. 各バケットをソート\n for (List<Float> bucket : buckets) {\n // 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能\n Collections.sort(bucket);\n }\n // 3. バケットを走査して結果をマージ\n int i = 0;\n for (List<Float> bucket : buckets) {\n for (float num : bucket) {\n nums[i++] = num;\n }\n }\n}\n</code></pre> bucket_sort.cs<pre><code>[class]{bucket_sort}-[func]{BucketSort}\n</code></pre> bucket_sort.go<pre><code>[class]{}-[func]{bucketSort}\n</code></pre> bucket_sort.swift<pre><code>[class]{}-[func]{bucketSort}\n</code></pre> bucket_sort.js<pre><code>[class]{}-[func]{bucketSort}\n</code></pre> bucket_sort.ts<pre><code>[class]{}-[func]{bucketSort}\n</code></pre> bucket_sort.dart<pre><code>[class]{}-[func]{bucketSort}\n</code></pre> bucket_sort.rs<pre><code>[class]{}-[func]{bucket_sort}\n</code></pre> bucket_sort.c<pre><code>[class]{}-[func]{bucketSort}\n</code></pre> bucket_sort.kt<pre><code>[class]{}-[func]{bucketSort}\n</code></pre> bucket_sort.rb<pre><code>[class]{}-[func]{bucket_sort}\n</code></pre>","path":["第 11 章 ソート","11.8 バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182","level":2,"title":"11.8.2 アルゴリズムの特徴","text":"<p>バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに100万個の要素が含まれ、システムメモリの制限によりすべてのデータを同時にロードできない場合、データを1,000個のバケットに分割し、各バケットを個別にソートしてから結果をマージできます。</p> <ul> <li>時間計算量は \\(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)\\) 時間がかかります。</li> <li>空間計算量は \\(O(n + k)\\)、非インプレースソート:\\(k\\) 個のバケットと合計 \\(n\\) 個の要素のための追加スペースが必要です。</li> <li>バケットソートが安定かどうかは、各バケット内で使用されるソートアルゴリズムが安定かどうかに依存します。</li> </ul>","path":["第 11 章 ソート","11.8 バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183","level":2,"title":"11.8.3 均等分散を達成する方法","text":"<p>バケットソートの理論的時間計算量は \\(O(n)\\) に達することができます。重要なことは、すべてのバケットに要素を均等に分散させることです。実世界のデータはしばしば均一に分散されていないからです。例えば、eBayのすべての商品を価格範囲で10個のバケットに均等に分散させたいとします。しかし、商品価格の分散は均等でない可能性があり、100ドル未満の商品が多く、500ドル以上の商品が少ないかもしれません。価格範囲を均等に10分割すると、各バケットの商品数の差が大きくなります。</p> <p>均等分散を達成するために、最初におおよその境界を設定して、データを3つのバケットに大まかに分割できます。分散が完了した後、より多くのアイテムを持つバケットをさらに3つのバケットに分割し、すべてのバケットの要素数がほぼ等しくなるまで続けます。</p> <p>以下の図に示すように、この方法は本質的に再帰木を構築し、葉ノードの要素数ができるだけ均等になることを目指します。もちろん、各ラウンドでデータを3つのバケットに分割する必要はありません - 分割戦略はデータの独特な特性に適応的に調整できます。</p> <p></p> <p> 図 11-14 バケットの再帰的分割 </p> <p>商品価格の確率分布を事前に知っている場合、データの確率分布に基づいて各バケットの価格境界を設定できます。データ分布を具体的に計算する必要は必ずしもなく、代わりに確率モデルを使用してデータ特性に基づいて近似できることに注意してください。</p> <p>以下の図に示すように、商品価格が正規分布に従うと仮定すると、バケット間でアイテムの分散のバランスを取るために合理的な価格区間を定義できます。</p> <p></p> <p> 図 11-15 確率分布に基づくバケット分割 </p>","path":["第 11 章 ソート","11.8 バケットソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9 計数ソート","text":"<p>計数ソートは要素の数をカウントすることでソートを実現し、通常は整数配列に適用されます。</p>","path":["第 11 章 ソート","11.9 計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191","level":2,"title":"11.9.1 簡単な実装","text":"<p>簡単な例から始めましょう。長さ \\(n\\) の配列 <code>nums</code> が与えられ、すべての要素が「非負整数」である場合、計数ソートの全体的な過程は以下の図に示されています。</p> <ol> <li>配列を走査して最大数を見つけ、それを \\(m\\) とし、長さ \\(m + 1\\) の補助配列 <code>counter</code> を作成します。</li> <li><code>counter</code> を使用して <code>nums</code> 内の各数の出現回数をカウントします。ここで <code>counter[num]</code> は数 <code>num</code> の出現回数に対応します。カウント方法は簡単で、<code>nums</code> を走査し(現在の数を <code>num</code> とする)、各ラウンドで <code>counter[num]</code> を \\(1\\) 増やします。</li> <li><code>counter</code> のインデックスは自然に順序付けられているため、すべての数は本質的にすでにソートされています。次に、<code>counter</code> を走査し、出現順に <code>nums</code> を昇順で埋めます。</li> </ol> <p></p> <p> 図 11-16 計数ソートの過程 </p> <p>コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py<pre><code>def counting_sort_naive(nums: list[int]):\n \"\"\"計数ソート\"\"\"\n # シンプルな実装、オブジェクトのソートには使用できない\n # 1. 配列内の最大要素 m を統計\n m = 0\n for num in nums:\n m = max(m, num)\n # 2. 各数字の出現回数を統計\n # counter[num] は num の出現回数を表す\n counter = [0] * (m + 1)\n for num in nums:\n counter[num] += 1\n # 3. counter を走査し、各要素を元の配列 nums に埋め戻し\n i = 0\n for num in range(m + 1):\n for _ in range(counter[num]):\n nums[i] = num\n i += 1\n</code></pre> counting_sort.cpp<pre><code>/* カウントソート */\n// 簡単な実装、オブジェクトのソートには使用できない\nvoid countingSortNaive(vector<int> &nums) {\n // 1. 配列の最大要素mを統計\n int m = 0;\n for (int num : nums) {\n m = max(m, num);\n }\n // 2. 各数字の出現回数を統計\n // counter[num]はnumの出現回数を表す\n vector<int> counter(m + 1, 0);\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counterを走査し、各要素を元の配列numsに戻す\n int i = 0;\n for (int num = 0; num < m + 1; num++) {\n for (int j = 0; j < counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n}\n</code></pre> counting_sort.java<pre><code>/* 計数ソート */\n// 簡単な実装、オブジェクトのソートには使用できない\nvoid countingSortNaive(int[] nums) {\n // 1. 配列の最大要素 m を統計\n int m = 0;\n for (int num : nums) {\n m = Math.max(m, num);\n }\n // 2. 各数字の出現回数を統計\n // counter[num] は num の出現回数を表す\n int[] counter = new int[m + 1];\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counter を走査し、各要素を元の配列 nums に戻す\n int i = 0;\n for (int num = 0; num < m + 1; num++) {\n for (int j = 0; j < counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n}\n</code></pre> counting_sort.cs<pre><code>[class]{counting_sort}-[func]{CountingSortNaive}\n</code></pre> counting_sort.go<pre><code>[class]{}-[func]{countingSortNaive}\n</code></pre> counting_sort.swift<pre><code>[class]{}-[func]{countingSortNaive}\n</code></pre> counting_sort.js<pre><code>[class]{}-[func]{countingSortNaive}\n</code></pre> counting_sort.ts<pre><code>[class]{}-[func]{countingSortNaive}\n</code></pre> counting_sort.dart<pre><code>[class]{}-[func]{countingSortNaive}\n</code></pre> counting_sort.rs<pre><code>[class]{}-[func]{counting_sort_naive}\n</code></pre> counting_sort.c<pre><code>[class]{}-[func]{countingSortNaive}\n</code></pre> counting_sort.kt<pre><code>[class]{}-[func]{countingSortNaive}\n</code></pre> counting_sort.rb<pre><code>[class]{}-[func]{counting_sort_naive}\n</code></pre> <p>計数ソートとバケットソートの関係</p> <p>バケットソートの観点から、計数ソートにおける計数配列 <code>counter</code> の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。</p>","path":["第 11 章 ソート","11.9 計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192","level":2,"title":"11.9.2 完全な実装","text":"<p>注意深い読者は気付くかもしれませんが、入力データがオブジェクトの場合、上記の手順 <code>3.</code> は無効です。入力データが商品オブジェクトで、価格(クラスメンバ変数)で商品をソートしたいとします。しかし、上記のアルゴリズムは結果としてソート済みの価格のみを提供できます。</p> <p>では、元のデータのソート結果をどのように取得できるでしょうか?まず、<code>counter</code> の「前置和」を計算します。名前が示すように、インデックス <code>i</code> での前置和 <code>prefix[i]</code> は、配列の最初の <code>i</code> 個の要素の和に等しいです:</p> \\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\] <p>前置和には明確な意味があります。<code>prefix[num] - 1</code> は結果配列 <code>res</code> における要素 <code>num</code> の最後の出現のインデックスを表します。この情報は重要で、各要素が結果配列のどこに現れるべきかを教えてくれます。次に、元の配列 <code>nums</code> の各要素 <code>num</code> を逆順で走査し、各反復で以下の2つの手順を実行します。</p> <ol> <li>インデックス <code>prefix[num] - 1</code> で配列 <code>res</code> に <code>num</code> を埋めます。</li> <li>前置和 <code>prefix[num]</code> を \\(1\\) 減らして、<code>num</code> を配置する次のインデックスを取得します。</li> </ol> <p>走査後、配列 <code>res</code> にはソートされた結果が含まれ、最後に <code>res</code> が元の配列 <code>nums</code> を置き換えます。完全な計数ソートの過程は以下の図に示されています。</p> <1><2><3><4><5><6><7><8> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 11-17 計数ソートの過程 </p> <p>計数ソートの実装コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py<pre><code>def counting_sort(nums: list[int]):\n \"\"\"計数ソート\"\"\"\n # 完全な実装、オブジェクトのソートが可能で、安定ソート\n # 1. 配列内の最大要素 m を統計\n m = max(nums)\n # 2. 各数字の出現回数を統計\n # counter[num] は num の出現回数を表す\n counter = [0] * (m + 1)\n for num in nums:\n counter[num] += 1\n # 3. counter の前置和を計算し、「出現回数」を「末尾インデックス」に変換\n # counter[num]-1 は res において num が最後に出現するインデックス\n for i in range(m):\n counter[i + 1] += counter[i]\n # 4. nums を逆順に走査し、各要素を結果配列 res に配置\n # 結果を記録するための配列 res を初期化\n n = len(nums)\n res = [0] * n\n for i in range(n - 1, -1, -1):\n num = nums[i]\n res[counter[num] - 1] = num # num を対応するインデックスに配置\n counter[num] -= 1 # 前置和を1減らし、num を配置する次のインデックスを取得\n # 結果配列 res を使用して元の配列 nums を上書き\n for i in range(n):\n nums[i] = res[i]\n</code></pre> counting_sort.cpp<pre><code>/* カウントソート */\n// 完全な実装、オブジェクトのソートが可能で安定ソート\nvoid countingSort(vector<int> &nums) {\n // 1. 配列の最大要素mを統計\n int m = 0;\n for (int num : nums) {\n m = max(m, num);\n }\n // 2. 各数字の出現回数を統計\n // counter[num]はnumの出現回数を表す\n vector<int> counter(m + 1, 0);\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counterの前缀和を計算し、「出現回数」を「末尾インデックス」に変換\n // counter[num]-1はnumがresで現れる最後のインデックス\n for (int i = 0; i < m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. numsを逆順で走査し、各要素を結果配列resに配置\n // 結果を記録する配列resを初期化\n int n = nums.size();\n vector<int> res(n);\n for (int i = n - 1; i >= 0; i--) {\n int num = nums[i];\n res[counter[num] - 1] = num; // numを対応するインデックスに配置\n counter[num]--; // 前缀和を1減らし、numを配置する次のインデックスを取得\n }\n // 結果配列resで元の配列numsを上書き\n nums = res;\n}\n</code></pre> counting_sort.java<pre><code>/* 計数ソート */\n// 完全な実装、オブジェクトをソートでき、安定ソート\nvoid countingSort(int[] nums) {\n // 1. 配列の最大要素 m を統計\n int m = 0;\n for (int num : nums) {\n m = Math.max(m, num);\n }\n // 2. 各数字の出現回数を統計\n // counter[num] は num の出現回数を表す\n int[] counter = new int[m + 1];\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counter の累積和を計算し、「出現回数」を「尻尾インデックス」に変換\n // counter[num]-1 は res 内で num が出現する最後のインデックス\n for (int i = 0; i < m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums を逆順に走査し、各要素を結果配列 res に配置\n // 結果を記録する配列 res を初期化\n int n = nums.length;\n int[] res = new int[n];\n for (int i = n - 1; i >= 0; i--) {\n int num = nums[i];\n res[counter[num] - 1] = num; // num を対応するインデックスに配置\n counter[num]--; // 累積和を 1 減算し、num を配置する次のインデックスを取得\n }\n // 結果配列 res を使って元の配列 nums を上書き\n for (int i = 0; i < n; i++) {\n nums[i] = res[i];\n }\n}\n</code></pre> counting_sort.cs<pre><code>[class]{counting_sort}-[func]{CountingSort}\n</code></pre> counting_sort.go<pre><code>[class]{}-[func]{countingSort}\n</code></pre> counting_sort.swift<pre><code>[class]{}-[func]{countingSort}\n</code></pre> counting_sort.js<pre><code>[class]{}-[func]{countingSort}\n</code></pre> counting_sort.ts<pre><code>[class]{}-[func]{countingSort}\n</code></pre> counting_sort.dart<pre><code>[class]{}-[func]{countingSort}\n</code></pre> counting_sort.rs<pre><code>[class]{}-[func]{counting_sort}\n</code></pre> counting_sort.c<pre><code>[class]{}-[func]{countingSort}\n</code></pre> counting_sort.kt<pre><code>[class]{}-[func]{countingSort}\n</code></pre> counting_sort.rb<pre><code>[class]{}-[func]{counting_sort}\n</code></pre>","path":["第 11 章 ソート","11.9 計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193","level":2,"title":"11.9.3 アルゴリズムの特徴","text":"<ul> <li>時間計算量は \\(O(n + m)\\)、非適応ソート:<code>nums</code> と <code>counter</code> の走査が含まれ、どちらも線形時間を使用します。一般的に、\\(n \\gg m\\) であり、時間計算量は \\(O(n)\\) に近づきます。</li> <li>空間計算量は \\(O(n + m)\\)、非インプレースソート:長さ \\(n\\) の配列 <code>res</code> と長さ \\(m\\) の配列 <code>counter</code> をそれぞれ使用します。</li> <li>安定ソート:要素が「右から左」の順序で <code>res</code> に埋められるため、<code>nums</code> の走査を逆順にすることで、等しい要素間の相対位置の変化を防ぎ、安定したソートを実現できます。実際、<code>nums</code> を順番に走査しても正しいソート結果を生成できますが、結果は不安定です。</li> </ul>","path":["第 11 章 ソート","11.9 計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194","level":2,"title":"11.9.4 制限事項","text":"<p>今までに、計数ソートは非常に巧妙だと感じるかもしれません。単に量をカウントするだけで効率的なソートを実現できるからです。しかし、計数ソートを使用するための前提条件は比較的厳しいです。</p> <p>計数ソートは非負整数にのみ適用できます。他のタイプのデータに適用したい場合、これらのデータが要素の元の順序を変更することなく非負整数に変換できることを保証する必要があります。例えば、負の整数を含む配列の場合、最初にすべての数に定数を加えて、すべてを正の数に変換し、ソート完了後に元に戻すことができます。</p> <p>計数ソートは値の範囲が小さい大きなデータセットに適しています。例えば、上記の例では、\\(m\\) は大きすぎるべきではありません。そうでなければ、あまりにも多くのスペースを占有してしまいます。そして \\(n \\ll m\\) の場合、計数ソートは \\(O(m)\\) 時間を使用し、\\(O(n \\log n)\\) ソートアルゴリズムより遅い可能性があります。</p>","path":["第 11 章 ソート","11.9 計数ソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7 ヒープソート","text":"<p>Tip</p> <p>この節を読む前に、「ヒープ」の章を必ず完了させてください。</p> <p>ヒープソートは、ヒープデータ構造に基づく効率的なソートアルゴリズムです。すでに学習した「ヒープの構築」と「要素の抽出」操作を使用してヒープソートを実装できます。</p> <ol> <li>配列を入力し、最小ヒープを構築します。ここで、最小要素がヒープの頂上に位置します。</li> <li>継続的に抽出操作を実行し、抽出された要素を順次記録して、最小から最大までのソート済みリストを取得します。</li> </ol> <p>上記の方法は実現可能ですが、ポップされた要素を格納するための追加の配列が必要で、やや空間を消費します。実際には、通常、より優雅な実装を使用します。</p>","path":["第 11 章 ソート","11.7 ヒープソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171","level":2,"title":"11.7.1 アルゴリズムの流れ","text":"<p>配列の長さを \\(n\\) とすると、ヒープソートの過程は以下の通りです。</p> <ol> <li>配列を入力し、最大ヒープを構築します。この手順の後、最大要素がヒープの頂上に位置します。</li> <li>ヒープの頂上要素(最初の要素)とヒープの底部要素(最後の要素)を交換します。この交換の後、ヒープの長さを \\(1\\) 減らし、ソート済み要素の数を \\(1\\) 増やします。</li> <li>ヒープの頂上から開始して、上から下へのsift-down操作を実行します。sift-downの後、ヒープの性質が復元されます。</li> <li>手順 <code>2.</code> と <code>3.</code> を繰り返します。\\(n - 1\\) ラウンドループして、配列のソートを完了します。</li> </ol> <p>Tip</p> <p>実際、要素抽出操作も手順 <code>2.</code> と <code>3.</code> を含み、抽出された要素をヒープから削除する追加の手順があります。</p> <1><2><3><4><5><6><7><8><9><10><11><12> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 11-12 ヒープソートの過程 </p> <p>コードの実装では、「ヒープ」の章からのsift-down関数 <code>sift_down()</code> を使用しました。最大要素が抽出されるにつれてヒープの長さが減少するため、<code>sift_down()</code> 関数に長さパラメータ \\(n\\) を追加して、ヒープの現在の有効長を指定する必要があることに注意することが重要です。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py<pre><code>def sift_down(nums: list[int], n: int, i: int):\n \"\"\"ヒープの長さが n、ノード i から上から下へヒープ化を開始\"\"\"\n while True:\n # i、l、r の中で最大のノードを判定し、ma とする\n l = 2 * i + 1\n r = 2 * i + 2\n ma = i\n if l < n and nums[l] > nums[ma]:\n ma = l\n if r < n and nums[r] > nums[ma]:\n ma = r\n # ノード i が最大または l、r のインデックスが範囲外の場合、さらなるヒープ化は不要、ループを抜ける\n if ma == i:\n break\n # 2つのノードを交換\n nums[i], nums[ma] = nums[ma], nums[i]\n # 下向きにヒープ化をループ\n i = ma\n\ndef heap_sort(nums: list[int]):\n \"\"\"ヒープソート\"\"\"\n # ヒープ構築操作:葉ノード以外のすべてのノードをヒープ化\n for i in range(len(nums) // 2 - 1, -1, -1):\n sift_down(nums, len(nums), i)\n # ヒープから最大要素を抽出し、n-1 回繰り返す\n for i in range(len(nums) - 1, 0, -1):\n # ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換)\n nums[0], nums[i] = nums[i], nums[0]\n # ルートノードから上から下へヒープ化を開始\n sift_down(nums, i, 0)\n</code></pre> heap_sort.cpp<pre><code>/* ヒープの長さはn、ノードiから上から下へヒープ化を開始 */\nvoid siftDown(vector<int> &nums, int n, int i) {\n while (true) {\n // i、l、r の中で最大のノードを決定し、maとして記録\n int l = 2 * i + 1;\n int r = 2 * i + 2;\n int ma = i;\n if (l < n && nums[l] > nums[ma])\n ma = l;\n if (r < n && nums[r] > nums[ma])\n ma = r;\n // ノードiが最大か、インデックスl、rが境界外の場合、それ以上のヒープ化は不要で終了\n if (ma == i) {\n break;\n }\n // 二つのノードを交換\n swap(nums[i], nums[ma]);\n // 下向きにヒープ化をループ\n i = ma;\n }\n}\n\n/* ヒープソート */\nvoid heapSort(vector<int> &nums) {\n // ヒープ構築操作:葉以外のすべてのノードをヒープ化\n for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n siftDown(nums, nums.size(), i);\n }\n // ヒープから最大要素を抽出し、n-1回繰り返す\n for (int i = nums.size() - 1; i > 0; --i) {\n // ルートノードを最右葉ノードと交換(最初の要素を最後の要素と交換)\n swap(nums[0], nums[i]);\n // ルートノードから上から下へヒープ化を開始\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.java<pre><code>/* ヒープの長さは n、ノード i から上から下へヒープ化開始 */\nvoid siftDown(int[] nums, int n, int i) {\n while (true) {\n // i, l, r の中で最大のノードを判定し、ma とする\n int l = 2 * i + 1;\n int r = 2 * i + 2;\n int ma = i;\n if (l < n && nums[l] > nums[ma])\n ma = l;\n if (r < n && nums[r] > nums[ma])\n ma = r;\n // ノード i が最大、またはインデックス l, r が範囲外の場合、さらなるヒープ化は不要、ブレーク\n if (ma == i)\n break;\n // 2つのノードを交換\n int temp = nums[i];\n nums[i] = nums[ma];\n nums[ma] = temp;\n // 下向きにヒープ化をループ\n i = ma;\n }\n}\n\n/* ヒープソート */\nvoid heapSort(int[] nums) {\n // ヒープ構築操作: 葉ノード以外のすべてのノードをヒープ化\n for (int i = nums.length / 2 - 1; i >= 0; i--) {\n siftDown(nums, nums.length, i);\n }\n // ヒープから最大要素を抽出し、n-1 回繰り返し\n for (int i = nums.length - 1; i > 0; i--) {\n // ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換)\n int tmp = nums[0];\n nums[0] = nums[i];\n nums[i] = tmp;\n // ルートノードから上から下へヒープ化開始\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.cs<pre><code>[class]{heap_sort}-[func]{SiftDown}\n\n[class]{heap_sort}-[func]{HeapSort}\n</code></pre> heap_sort.go<pre><code>[class]{}-[func]{siftDown}\n\n[class]{}-[func]{heapSort}\n</code></pre> heap_sort.swift<pre><code>[class]{}-[func]{siftDown}\n\n[class]{}-[func]{heapSort}\n</code></pre> heap_sort.js<pre><code>[class]{}-[func]{siftDown}\n\n[class]{}-[func]{heapSort}\n</code></pre> heap_sort.ts<pre><code>[class]{}-[func]{siftDown}\n\n[class]{}-[func]{heapSort}\n</code></pre> heap_sort.dart<pre><code>[class]{}-[func]{siftDown}\n\n[class]{}-[func]{heapSort}\n</code></pre> heap_sort.rs<pre><code>[class]{}-[func]{sift_down}\n\n[class]{}-[func]{heap_sort}\n</code></pre> heap_sort.c<pre><code>[class]{}-[func]{siftDown}\n\n[class]{}-[func]{heapSort}\n</code></pre> heap_sort.kt<pre><code>[class]{}-[func]{siftDown}\n\n[class]{}-[func]{heapSort}\n</code></pre> heap_sort.rb<pre><code>[class]{}-[func]{sift_down}\n\n[class]{}-[func]{heap_sort}\n</code></pre>","path":["第 11 章 ソート","11.7 ヒープソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172","level":2,"title":"11.7.2 アルゴリズムの特徴","text":"<ul> <li>時間計算量は \\(O(n \\log n)\\)、非適応ソート:ヒープの構築は \\(O(n)\\) 時間を使用します。ヒープから最大要素を抽出するには \\(O(\\log n)\\) 時間がかかり、\\(n - 1\\) ラウンドループします。</li> <li>空間計算量は \\(O(1)\\)、インプレースソート:いくつかのポインタ変数が \\(O(1)\\) 空間を使用します。要素の交換とヒープ化操作は元の配列で実行されます。</li> <li>非安定ソート:ヒープの頂上と底部要素の交換中に、等しい要素の相対位置が変わる可能性があります。</li> </ul>","path":["第 11 章 ソート","11.7 ヒープソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4 挿入ソート","text":"<p>挿入ソートは、トランプのデッキを手動でソートするプロセスによく似た動作をするシンプルなソートアルゴリズムです。</p> <p>具体的には、未ソート区間からベース要素を選択し、その左側のソート済み区間の要素と比較して、要素を正しい位置に挿入します。</p> <p>下図は、要素が配列に挿入される方法を示しています。ベース要素を<code>base</code>とすると、ターゲットインデックスから<code>base</code>までのすべての要素を右に1つずつシフトし、その後<code>base</code>をターゲットインデックスに割り当てる必要があります。</p> <p></p> <p> 図 11-6 Single insertion operation </p>","path":["第 11 章 ソート","11.4 挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1 アルゴリズムプロセス","text":"<p>挿入ソートの全体的なプロセスは下図に示されます。</p> <ol> <li>配列の最初の要素をソート済みとみなします。</li> <li>2番目の要素を<code>base</code>として選択し、正しい位置に挿入して、最初の2つの要素をソート済みにします。</li> <li>3番目の要素を<code>base</code>として選択し、正しい位置に挿入して、最初の3つの要素をソート済みにします。</li> <li>この方法で続行し、最後の反復では、最後の要素を<code>base</code>として取り、正しい位置に挿入した後、すべての要素がソートされます。</li> </ol> <p></p> <p> 図 11-7 Insertion sort process </p> <p>コード例は以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py<pre><code>def insertion_sort(nums: list[int]):\n \"\"\"挿入ソート\"\"\"\n # 外側のループ:ソート済み範囲は [0, i-1]\n for i in range(1, len(nums)):\n base = nums[i]\n j = i - 1\n # 内側のループ:base をソート済み範囲 [0, i-1] の正しい位置に挿入\n while j >= 0 and nums[j] > base:\n nums[j + 1] = nums[j] # nums[j] を右に1つ移動\n j -= 1\n nums[j + 1] = base # base を正しい位置に代入\n</code></pre> insertion_sort.cpp<pre><code>/* 挿入ソート */\nvoid insertionSort(vector<int> &nums) {\n // 外側ループ:ソート済み範囲は[0, i-1]\n for (int i = 1; i < nums.size(); i++) {\n int base = nums[i], j = i - 1;\n // 内側ループ:baseをソート済み範囲[0, i-1]内の正しい位置に挿入\n while (j >= 0 && nums[j] > base) {\n nums[j + 1] = nums[j]; // nums[j]を一つ右に移動\n j--;\n }\n nums[j + 1] = base; // baseを正しい位置に代入\n }\n}\n</code></pre> insertion_sort.java<pre><code>/* 挿入ソート */\nvoid insertionSort(int[] nums) {\n // 外側ループ: ソート済み範囲は [0, i-1]\n for (int i = 1; i < nums.length; i++) {\n int base = nums[i], j = i - 1;\n // 内側ループ: base をソート済み範囲 [0, i-1] の正しい位置に挿入\n while (j >= 0 && nums[j] > base) {\n nums[j + 1] = nums[j]; // nums[j] を右に1つ移動\n j--;\n }\n nums[j + 1] = base; // base を正しい位置に代入\n }\n}\n</code></pre> insertion_sort.cs<pre><code>[class]{insertion_sort}-[func]{InsertionSort}\n</code></pre> insertion_sort.go<pre><code>[class]{}-[func]{insertionSort}\n</code></pre> insertion_sort.swift<pre><code>[class]{}-[func]{insertionSort}\n</code></pre> insertion_sort.js<pre><code>[class]{}-[func]{insertionSort}\n</code></pre> insertion_sort.ts<pre><code>[class]{}-[func]{insertionSort}\n</code></pre> insertion_sort.dart<pre><code>[class]{}-[func]{insertionSort}\n</code></pre> insertion_sort.rs<pre><code>[class]{}-[func]{insertion_sort}\n</code></pre> insertion_sort.c<pre><code>[class]{}-[func]{insertionSort}\n</code></pre> insertion_sort.kt<pre><code>[class]{}-[func]{insertionSort}\n</code></pre> insertion_sort.rb<pre><code>[class]{}-[func]{insertion_sort}\n</code></pre>","path":["第 11 章 ソート","11.4 挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142","level":2,"title":"11.4.2 アルゴリズムの特性","text":"<ul> <li>時間計算量は\\(O(n^2)\\)、適応ソート:最悪の場合、各挿入操作には\\(n - 1\\)、\\(n-2\\)、...、\\(2\\)、\\(1\\)のループが必要で、合計は\\((n - 1) n / 2\\)となり、時間計算量は\\(O(n^2)\\)です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量\\(O(n)\\)を実現します。</li> <li>空間計算量は\\(O(1)\\)、インプレースソート:ポインタ\\(i\\)と\\(j\\)は定数量の追加空間を使用します。</li> <li>安定ソート:挿入操作中、等しい要素の右側に要素を挿入し、順序を変更しません。</li> </ul>","path":["第 11 章 ソート","11.4 挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143","level":2,"title":"11.4.3 挿入ソートの利点","text":"<p>挿入ソートの時間計算量は\\(O(n^2)\\)で、次に学習するクイックソートの時間計算量は\\(O(n \\log n)\\)です。挿入ソートはより高い時間計算量を持ちますが、小さな入力サイズでは通常より高速です。</p> <p>この結論は線形探索と二分探索の結論と似ています。時間計算量が\\(O(n \\log n)\\)で分割統治戦略に基づくクイックソートなどのアルゴリズムは、多くの場合より多くの単位操作を含みます。小さな入力サイズでは、\\(n^2\\)と\\(n \\log n\\)の数値は近く、計算量が支配的でなく、ラウンドあたりの単位操作数が決定的な役割を果たします。</p> <p>実際、多くのプログラミング言語(Javaなど)は、組み込みソート関数内で挿入ソートを使用しています。一般的なアプローチは:長い配列に対しては、クイックソートなどの分割統治戦略に基づくソートアルゴリズムを使用し、短い配列に対しては挿入ソートを直接使用します。</p> <p>バブルソート、選択ソート、挿入ソートはすべて時間計算量\\(O(n^2)\\)を持ちますが、実際には、挿入ソートはバブルソートや選択ソートよりも一般的に使用されます。主な理由は以下の通りです。</p> <ul> <li>バブルソートは要素交換に基づき、一時変数の使用が必要で、3つの単位操作を含みます;挿入ソートは要素代入に基づき、1つの単位操作のみが必要です。したがって、バブルソートの計算オーバーヘッドは一般的に挿入ソートよりも高くなります。</li> <li>選択ソートの時間計算量は常に\\(O(n^2)\\)です。部分的に順序付けられたデータのセットが与えられた場合、挿入ソートは通常選択ソートよりも効率的です。</li> <li>選択ソートは不安定で、マルチレベルソートに適用できません。</li> </ul>","path":["第 11 章 ソート","11.4 挿入ソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6 マージソート","text":"<p>マージソートは分割統治戦略に基づくソートアルゴリズムで、下図に示す「分割」と「マージ」フェーズを含みます。</p> <ol> <li>分割フェーズ:中点から配列を再帰的に分割し、長い配列のソート問題をより短い配列に変換します。</li> <li>マージフェーズ:サブ配列の長さが1になったときに分割を停止し、その後マージを開始します。2つの短いソート済み配列を連続的により長いソート済み配列にマージし、プロセスが完了するまで続行します。</li> </ol> <p></p> <p> 図 11-10 The divide and merge phases of merge sort </p>","path":["第 11 章 ソート","11.6 マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1 アルゴリズムワークフロー","text":"<p>下図に示すように、「分割フェーズ」は中点から配列を上から下に2つのサブ配列に再帰的に分割します。</p> <ol> <li>中点<code>mid</code>を計算し、左サブ配列(区間<code>[left, mid]</code>)と右サブ配列(区間<code>[mid + 1, right]</code>)を再帰的に分割します。</li> <li>サブ配列の長さが1になるまでステップ<code>1.</code>を再帰的に続行し、その後停止します。</li> </ol> <p>「マージフェーズ」は左と右のサブ配列を下から上にソート済み配列に結合します。重要なのは、マージが長さ1のサブ配列から開始され、マージフェーズ中に各サブ配列がソートされることです。</p> <1><2><3><4><5><6><7><8><9><10> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 11-11 Merge sort process </p> <p>マージソートの再帰順序は二分木の後順横断と一致することが観察できます。</p> <ul> <li>後順横断:まず左のサブツリーを再帰的に横断し、次に右のサブツリーを横断し、最後にルートノードを処理します。</li> <li>マージソート:まず左のサブ配列を再帰的に処理し、次に右のサブ配列を処理し、最後にマージを実行します。</li> </ul> <p>マージソートの実装は以下のコードに示されます。<code>nums</code>でマージされる区間は<code>[left, right]</code>で、<code>tmp</code>の対応する区間は<code>[0, right - left]</code>であることに注意してください。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py<pre><code>def merge(nums: list[int], left: int, mid: int, right: int):\n \"\"\"左サブ配列と右サブ配列をマージ\"\"\"\n # 左サブ配列区間は [left, mid]、右サブ配列区間は [mid+1, right]\n # 一時配列 tmp を作成してマージ結果を格納\n tmp = [0] * (right - left + 1)\n # 左右サブ配列の開始インデックスを初期化\n i, j, k = left, mid + 1, 0\n # 両方のサブ配列に要素が残っている間、より小さい要素を一時配列にコピー\n while i <= mid and j <= right:\n if nums[i] <= nums[j]:\n tmp[k] = nums[i]\n i += 1\n else:\n tmp[k] = nums[j]\n j += 1\n k += 1\n # 残った左右サブ配列の要素を一時配列にコピー\n while i <= mid:\n tmp[k] = nums[i]\n i += 1\n k += 1\n while j <= right:\n tmp[k] = nums[j]\n j += 1\n k += 1\n # 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック\n for k in range(0, len(tmp)):\n nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n \"\"\"マージソート\"\"\"\n # 終了条件\n if left >= right:\n return # サブ配列の長さが1のときに再帰を終了\n # 分割段階\n mid = left + (right - left) // 2 # 中点を計算\n merge_sort(nums, left, mid) # 左サブ配列を再帰的に処理\n merge_sort(nums, mid + 1, right) # 右サブ配列を再帰的に処理\n # マージ段階\n merge(nums, left, mid, right)\n</code></pre> merge_sort.cpp<pre><code>/* 左サブ配列と右サブ配列をマージ */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n // 左サブ配列の区間は[left, mid]、右サブ配列の区間は[mid+1, right]\n // マージ結果を保存する一時配列tmpを作成\n vector<int> tmp(right - left + 1);\n // 左右サブ配列の開始インデックスを初期化\n int i = left, j = mid + 1, k = 0;\n // 両サブ配列に要素がある間、小さい方の要素を一時配列にコピー\n while (i <= mid && j <= right) {\n if (nums[i] <= nums[j])\n tmp[k++] = nums[i++];\n else\n tmp[k++] = nums[j++];\n }\n // 左右サブ配列の残りの要素を一時配列にコピー\n while (i <= mid) {\n tmp[k++] = nums[i++];\n }\n while (j <= right) {\n tmp[k++] = nums[j++];\n }\n // 一時配列tmpの要素を元の配列numsの対応する区間にコピー\n for (k = 0; k < tmp.size(); k++) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* マージソート */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n // 終了条件\n if (left >= right)\n return; // サブ配列の長さが1の時、再帰を終了\n // 分割段階\n int mid = left + (right - left) / 2; // 中点を計算\n mergeSort(nums, left, mid); // 左サブ配列を再帰的に処理\n mergeSort(nums, mid + 1, right); // 右サブ配列を再帰的に処理\n // マージ段階\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.java<pre><code>/* 左部分配列と右部分配列をマージ */\nvoid merge(int[] nums, int left, int mid, int right) {\n // 左部分配列区間は [left, mid]、右部分配列区間は [mid+1, right]\n // 一時配列 tmp を作成してマージ結果を格納\n int[] tmp = new int[right - left + 1];\n // 左右部分配列の開始インデックスを初期化\n int i = left, j = mid + 1, k = 0;\n // 両部分配列にまだ要素がある間、比較してより小さい要素を一時配列にコピー\n while (i <= mid && j <= right) {\n if (nums[i] <= nums[j])\n tmp[k++] = nums[i++];\n else\n tmp[k++] = nums[j++];\n }\n // 左右部分配列の残りの要素を一時配列にコピー\n while (i <= mid) {\n tmp[k++] = nums[i++];\n }\n while (j <= right) {\n tmp[k++] = nums[j++];\n }\n // 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック\n for (k = 0; k < tmp.length; k++) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* マージソート */\nvoid mergeSort(int[] nums, int left, int right) {\n // 終了条件\n if (left >= right)\n return; // 部分配列の長さが 1 のとき再帰を終了\n // 分割段階\n int mid = left + (right - left) / 2; // 中点を計算\n mergeSort(nums, left, mid); // 左部分配列を再帰的に処理\n mergeSort(nums, mid + 1, right); // 右部分配列を再帰的に処理\n // マージ段階\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.cs<pre><code>[class]{merge_sort}-[func]{Merge}\n\n[class]{merge_sort}-[func]{MergeSort}\n</code></pre> merge_sort.go<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{mergeSort}\n</code></pre> merge_sort.swift<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{mergeSort}\n</code></pre> merge_sort.js<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{mergeSort}\n</code></pre> merge_sort.ts<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{mergeSort}\n</code></pre> merge_sort.dart<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{mergeSort}\n</code></pre> merge_sort.rs<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{merge_sort}\n</code></pre> merge_sort.c<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{mergeSort}\n</code></pre> merge_sort.kt<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{mergeSort}\n</code></pre> merge_sort.rb<pre><code>[class]{}-[func]{merge}\n\n[class]{}-[func]{merge_sort}\n</code></pre>","path":["第 11 章 ソート","11.6 マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162","level":2,"title":"11.6.2 アルゴリズムの特性","text":"<ul> <li>\\(O(n \\log n)\\)の時間計算量、非適応ソート:分割により高さ\\(\\log n\\)の再帰ツリーが作成され、各層で合計\\(n\\)回の操作をマージし、全体的な時間計算量は\\(O(n \\log n)\\)となります。</li> <li>\\(O(n)\\)の空間計算量、非インプレースソート:再帰深度は\\(\\log n\\)で、\\(O(\\log n)\\)のスタックフレーム空間を使用します。マージ操作には補助配列が必要で、追加の\\(O(n)\\)空間を使用します。</li> <li>安定ソート:マージプロセス中、等しい要素の順序は変更されません。</li> </ul>","path":["第 11 章 ソート","11.6 マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163","level":2,"title":"11.6.3 連結リストのソート","text":"<p>連結リストの場合、マージソートは他のソートアルゴリズムよりも大きな利点があります。連結リストソートタスクの空間計算量を\\(O(1)\\)に最適化できます。</p> <ul> <li>分割フェーズ:「再帰」の代わりに「反復」を使用して連結リスト分割作業を実行できるため、再帰で使用されるスタックフレーム空間を節約できます。</li> <li>マージフェーズ:連結リストでは、ノードの挿入と削除操作は参照(ポインタ)を変更することで実現できるため、マージフェーズ(2つの短い順序付きリストを1つの長い順序付きリストに結合)中に追加のリストを作成する必要がありません。</li> </ul> <p>実装の詳細は比較的複雑で、興味のある読者は関連資料を参照して学習してください。</p>","path":["第 11 章 ソート","11.6 マージソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5 クイックソート","text":"<p>クイックソートは分割統治戦略に基づくソートアルゴリズムで、その効率性と幅広い応用で知られています。</p> <p>クイックソートのコア操作は「ピボット分割」で、配列から要素を「ピボット」として選択し、ピボットより小さいすべての要素をその左側に移動し、ピボットより大きいすべての要素をその右側に移動することを目的としています。具体的に、ピボット分割のプロセスは下図に示されます。</p> <ol> <li>配列の最も左の要素をピボットとして選択し、2つのポインタ<code>i</code>と<code>j</code>を初期化して配列の両端をそれぞれ指すようにします。</li> <li>各ラウンドで<code>i</code>(<code>j</code>)を使用してピボットより大きい(小さい)最初の要素を探索し、次にこれら2つの要素を交換するループを設定します。</li> <li><code>i</code>と<code>j</code>が出会うまでステップ<code>2.</code>を繰り返し、最後にピボットを2つのサブ配列の境界に交換します。</li> </ol> <1><2><3><4><5><6><7><8><9> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 11-8 Pivot division process </p> <p>ピボット分割後、元の配列は3つの部分に分割されます:左サブ配列、ピボット、右サブ配列で、「左サブ配列の任意の要素 \\(\\leq\\) ピボット \\(\\leq\\) 右サブ配列の任意の要素」を満たします。したがって、これら2つのサブ配列のみをソートすればよいのです。</p> <p>クイックソートの分割統治戦略</p> <p>ピボット分割の本質は、より長い配列のソート問題をより短い2つの配列に簡素化することです。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def partition(self, nums: list[int], left: int, right: int) -> int:\n \"\"\"分割\"\"\"\n # nums[left] をピボットとして使用\n i, j = left, right\n while i < j:\n while i < j and nums[j] >= nums[left]:\n j -= 1 # 右から左へピボットより小さい最初の要素を探す\n while i < j and nums[i] <= nums[left]:\n i += 1 # 左から右へピボットより大きい最初の要素を探す\n # 要素を交換\n nums[i], nums[j] = nums[j], nums[i]\n # ピボットを2つのサブ配列の境界に交換\n nums[i], nums[left] = nums[left], nums[i]\n return i # ピボットのインデックスを返す\n</code></pre> quick_sort.cpp<pre><code>/* 分割 */\nint partition(vector<int> &nums, int left, int right) {\n // nums[left]をピボットとして使用\n int i = left, j = right;\n while (i < j) {\n while (i < j && nums[j] >= nums[left])\n j--; // 右から左へピボットより小さい最初の要素を検索\n while (i < j && nums[i] <= nums[left])\n i++; // 左から右へピボットより大きい最初の要素を検索\n swap(nums, i, j); // これら二つの要素を交換\n }\n swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換\n return i; // ピボットのインデックスを返す\n}\n</code></pre> quick_sort.java<pre><code>/* 要素を交換 */\nvoid swap(int[] nums, int i, int j) {\n int tmp = nums[i];\n nums[i] = nums[j];\n nums[j] = tmp;\n}\n\n/* 分割 */\nint partition(int[] nums, int left, int right) {\n // nums[left] を基準値として使用\n int i = left, j = right;\n while (i < j) {\n while (i < j && nums[j] >= nums[left])\n j--; // 右から左へ、基準値より小さい最初の要素を検索\n while (i < j && nums[i] <= nums[left])\n i++; // 左から右へ、基準値より大きい最初の要素を検索\n swap(nums, i, j); // これら2つの要素を交換\n }\n swap(nums, i, left); // 基準値を2つの部分配列の境界に交換\n return i; // 基準値のインデックスを返す\n}\n</code></pre> quick_sort.cs<pre><code>[class]{quickSort}-[func]{Swap}\n\n[class]{quickSort}-[func]{Partition}\n</code></pre> quick_sort.go<pre><code>[class]{quickSort}-[func]{partition}\n</code></pre> quick_sort.swift<pre><code>[class]{}-[func]{partition}\n</code></pre> quick_sort.js<pre><code>[class]{QuickSort}-[func]{swap}\n\n[class]{QuickSort}-[func]{partition}\n</code></pre> quick_sort.ts<pre><code>[class]{QuickSort}-[func]{swap}\n\n[class]{QuickSort}-[func]{partition}\n</code></pre> quick_sort.dart<pre><code>[class]{QuickSort}-[func]{_swap}\n\n[class]{QuickSort}-[func]{_partition}\n</code></pre> quick_sort.rs<pre><code>[class]{QuickSort}-[func]{partition}\n</code></pre> quick_sort.c<pre><code>[class]{}-[func]{swap}\n\n[class]{}-[func]{partition}\n</code></pre> quick_sort.kt<pre><code>[class]{}-[func]{swap}\n\n[class]{}-[func]{partition}\n</code></pre> quick_sort.rb<pre><code>[class]{QuickSort}-[func]{partition}\n</code></pre>","path":["第 11 章 ソート","11.5 クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151","level":2,"title":"11.5.1 アルゴリズムプロセス","text":"<p>クイックソートの全体的なプロセスは下図に示されます。</p> <ol> <li>まず、元の配列に対して「ピボット分割」を実行し、未ソートの左と右のサブ配列を取得します。</li> <li>次に、左と右のサブ配列に対してそれぞれ再帰的に「ピボット分割」を実行します。</li> <li>サブ配列の長さが1になるまで再帰を続け、配列全体のソートを完了します。</li> </ol> <p></p> <p> 図 11-9 Quick sort process </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def quick_sort(self, nums: list[int], left: int, right: int):\n \"\"\"クイックソート\"\"\"\n # サブ配列の長さが1のときに再帰を終了\n if left >= right:\n return\n # 分割\n pivot = self.partition(nums, left, right)\n # 左サブ配列と右サブ配列を再帰的に処理\n self.quick_sort(nums, left, pivot - 1)\n self.quick_sort(nums, pivot + 1, right)\n</code></pre> quick_sort.cpp<pre><code>/* クイックソート */\nvoid quickSort(vector<int> &nums, int left, int right) {\n // サブ配列の長さが1の時、再帰を終了\n if (left >= right)\n return;\n // 分割\n int pivot = partition(nums, left, right);\n // 左サブ配列と右サブ配列を再帰的に処理\n quickSort(nums, left, pivot - 1);\n quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.java<pre><code>/* クイックソート */\nvoid quickSort(int[] nums, int left, int right) {\n // 部分配列の長さが 1 のとき再帰を終了\n if (left >= right)\n return;\n // 分割\n int pivot = partition(nums, left, right);\n // 左部分配列と右部分配列を再帰的に処理\n quickSort(nums, left, pivot - 1);\n quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.cs<pre><code>[class]{quickSort}-[func]{QuickSort}\n</code></pre> quick_sort.go<pre><code>[class]{quickSort}-[func]{quickSort}\n</code></pre> quick_sort.swift<pre><code>[class]{}-[func]{quickSort}\n</code></pre> quick_sort.js<pre><code>[class]{QuickSort}-[func]{quickSort}\n</code></pre> quick_sort.ts<pre><code>[class]{QuickSort}-[func]{quickSort}\n</code></pre> quick_sort.dart<pre><code>[class]{QuickSort}-[func]{quickSort}\n</code></pre> quick_sort.rs<pre><code>[class]{QuickSort}-[func]{quick_sort}\n</code></pre> quick_sort.c<pre><code>[class]{}-[func]{quickSort}\n</code></pre> quick_sort.kt<pre><code>[class]{}-[func]{quickSort}\n</code></pre> quick_sort.rb<pre><code>[class]{QuickSort}-[func]{quick_sort}\n</code></pre>","path":["第 11 章 ソート","11.5 クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152","level":2,"title":"11.5.2 アルゴリズムの特徴","text":"<ul> <li>\\(O(n \\log n)\\)の時間計算量、非適応ソート:平均的なケースでは、ピボット分割の再帰レベルは\\(\\log n\\)で、レベルあたりのループの総数は\\(n\\)であり、全体で\\(O(n \\log n)\\)の時間を使用します。最悪の場合、各ラウンドのピボット分割は長さ\\(n\\)の配列を長さ\\(0\\)と\\(n - 1\\)の2つのサブ配列に分割し、再帰レベル数が\\(n\\)に達すると、各レベルのループ数は\\(n\\)で、使用される総時間は\\(O(n^2)\\)です。</li> <li>\\(O(n)\\)の空間計算量、インプレースソート:入力配列が完全に逆順の場合、最悪の再帰深度は\\(n\\)に達し、\\(O(n)\\)のスタックフレーム空間を使用します。ソート操作は追加の配列の助けなしに元の配列で実行されます。</li> <li>非安定ソート:ピボット分割の最終ステップで、ピボットは等しい要素の右側に交換される可能性があります。</li> </ul>","path":["第 11 章 ソート","11.5 クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153","level":2,"title":"11.5.3 なぜクイックソートは高速なのか","text":"<p>名前が示すように、クイックソートは効率性の面で一定の利点を持つべきです。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、以下の理由で一般的により効率的です。</p> <ul> <li>最悪ケースシナリオの低い確率:クイックソートの最悪時間計算量は\\(O(n^2)\\)で、マージソートほど安定していませんが、ほとんどの場合、クイックソートは\\(O(n \\log n)\\)の時間計算量で動作できます。</li> <li>高いキャッシュ利用率:ピボット分割操作中、システムはサブ配列全体をキャッシュにロードできるため、要素により効率的にアクセスできます。対照的に、「ヒープソート」などのアルゴリズムは要素にジャンプ方式でアクセスする必要があり、この特徴を欠いています。</li> <li>計算量の小さな定数係数:上記3つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作の総数が最も少ないです。これは「挿入ソート」が「バブルソート」よりも高速な理由と似ています。</li> </ul>","path":["第 11 章 ソート","11.5 クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4 ピボット最適化","text":"<p>クイックソートの時間効率は特定の入力で劣化する可能性があります。例えば、入力配列が完全に逆順の場合、最も左の要素をピボットとして選択するため、ピボット分割後、ピボットは配列の右端に交換され、左サブ配列の長さが\\(n - 1\\)、右サブ配列の長さが\\(0\\)になります。この方法を続けると、各ラウンドのピボット分割でサブ配列の長さが\\(0\\)になり、分割統治戦略が失敗し、クイックソートは「バブルソート」に似た形に劣化します。</p> <p>この状況を避けるため、ピボット分割でピボット選択戦略を最適化できます。例えば、要素をランダムに選択してピボットとすることができます。ただし、運が悪く、一貫して最適でないピボットを選択した場合、効率はまだ満足できません。</p> <p>プログラミング言語は通常「疑似乱数」を生成することに注意することが重要です。疑似乱数シーケンスに対して特定のテストケースを構築すると、クイックソートの効率はまだ劣化する可能性があります。</p> <p>さらなる改善のため、3つの候補要素(通常は配列の最初、最後、中点の要素)を選択し、**これら3つの候補要素の中央値をピボットとして使用**できます。この方法で、ピボットが「小さすぎず大きすぎない」確率が大幅に増加します。もちろん、さらに多くの候補要素を選択してアルゴリズムの堅牢性をさらに向上させることもできます。この方法により、時間計算量が\\(O(n^2)\\)に劣化する確率が大幅に削減されます。</p> <p>サンプルコードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n \"\"\"3つの候補要素の中央値を選択\"\"\"\n l, m, r = nums[left], nums[mid], nums[right]\n if (l <= m <= r) or (r <= m <= l):\n return mid # m は l と r の間\n if (m <= l <= r) or (r <= l <= m):\n return left # l は m と r の間\n return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n \"\"\"分割(三点中央値)\"\"\"\n # nums[left] をピボットとして使用\n med = self.median_three(nums, left, (left + right) // 2, right)\n # 中央値を配列の最左端に交換\n nums[left], nums[med] = nums[med], nums[left]\n # nums[left] をピボットとして使用\n i, j = left, right\n while i < j:\n while i < j and nums[j] >= nums[left]:\n j -= 1 # 右から左へピボットより小さい最初の要素を探す\n while i < j and nums[i] <= nums[left]:\n i += 1 # 左から右へピボットより大きい最初の要素を探す\n # 要素を交換\n nums[i], nums[j] = nums[j], nums[i]\n # ピボットを2つのサブ配列の境界に交換\n nums[i], nums[left] = nums[left], nums[i]\n return i # ピボットのインデックスを返す\n</code></pre> quick_sort.cpp<pre><code>/* 三つの候補要素の中央値を選択 */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n int l = nums[left], m = nums[mid], r = nums[right];\n if ((l <= m && m <= r) || (r <= m && m <= l))\n return mid; // mはlとrの間\n if ((m <= l && l <= r) || (r <= l && l <= m))\n return left; // lはmとrの間\n return right;\n}\n\n/* 分割(三つの中央値) */\nint partition(vector<int> &nums, int left, int right) {\n // 三つの候補要素の中央値を選択\n int med = medianThree(nums, left, (left + right) / 2, right);\n // 中央値を配列の最左位置に交換\n swap(nums, left, med);\n // nums[left]をピボットとして使用\n int i = left, j = right;\n while (i < j) {\n while (i < j && nums[j] >= nums[left])\n j--; // 右から左へピボットより小さい最初の要素を検索\n while (i < j && nums[i] <= nums[left])\n i++; // 左から右へピボットより大きい最初の要素を検索\n swap(nums, i, j); // これら二つの要素を交換\n }\n swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換\n return i; // ピボットのインデックスを返す\n}\n</code></pre> quick_sort.java<pre><code>/* 3つの候補要素の中央値を選択 */\nint medianThree(int[] nums, int left, int mid, int right) {\n int l = nums[left], m = nums[mid], r = nums[right];\n if ((l <= m && m <= r) || (r <= m && m <= l))\n return mid; // m は l と r の間\n if ((m <= l && l <= r) || (r <= l && l <= m))\n return left; // l は m と r の間\n return right;\n}\n\n/* 分割(3つの中央値) */\nint partition(int[] nums, int left, int right) {\n // 3つの候補要素の中央値を選択\n int med = medianThree(nums, left, (left + right) / 2, right);\n // 中央値を配列の最左端の位置に交換\n swap(nums, left, med);\n // nums[left] を基準値として使用\n int i = left, j = right;\n while (i < j) {\n while (i < j && nums[j] >= nums[left])\n j--; // 右から左へ、基準値より小さい最初の要素を検索\n while (i < j && nums[i] <= nums[left])\n i++; // 左から右へ、基準値より大きい最初の要素を検索\n swap(nums, i, j); // これら2つの要素を交換\n }\n swap(nums, i, left); // 基準値を2つの部分配列の境界に交換\n return i; // 基準値のインデックスを返す\n}\n</code></pre> quick_sort.cs<pre><code>[class]{QuickSortMedian}-[func]{MedianThree}\n\n[class]{QuickSortMedian}-[func]{Partition}\n</code></pre> quick_sort.go<pre><code>[class]{quickSortMedian}-[func]{medianThree}\n\n[class]{quickSortMedian}-[func]{partition}\n</code></pre> quick_sort.swift<pre><code>[class]{}-[func]{medianThree}\n\n[class]{}-[func]{partitionMedian}\n</code></pre> quick_sort.js<pre><code>[class]{QuickSortMedian}-[func]{medianThree}\n\n[class]{QuickSortMedian}-[func]{partition}\n</code></pre> quick_sort.ts<pre><code>[class]{QuickSortMedian}-[func]{medianThree}\n\n[class]{QuickSortMedian}-[func]{partition}\n</code></pre> quick_sort.dart<pre><code>[class]{QuickSortMedian}-[func]{_medianThree}\n\n[class]{QuickSortMedian}-[func]{_partition}\n</code></pre> quick_sort.rs<pre><code>[class]{QuickSortMedian}-[func]{median_three}\n\n[class]{QuickSortMedian}-[func]{partition}\n</code></pre> quick_sort.c<pre><code>[class]{}-[func]{medianThree}\n\n[class]{}-[func]{partitionMedian}\n</code></pre> quick_sort.kt<pre><code>[class]{}-[func]{medianThree}\n\n[class]{}-[func]{partitionMedian}\n</code></pre> quick_sort.rb<pre><code>[class]{QuickSortMedian}-[func]{median_three}\n\n[class]{QuickSortMedian}-[func]{partition}\n</code></pre>","path":["第 11 章 ソート","11.5 クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155","level":2,"title":"11.5.5 末尾再帰最適化","text":"<p>特定の入力では、クイックソートはより多くの空間を占有する可能性があります。例えば、完全に順序付けられた入力配列を考えてみましょう。再帰でのサブ配列の長さを\\(m\\)とします。各ラウンドのピボット分割で、長さ\\(0\\)の左サブ配列と長さ\\(m - 1\\)の右サブ配列が生成されます。これは、再帰呼び出しごとに問題サイズが1つの要素のみ減少することを意味し、各レベルの再帰での削減が非常に小さくなります。 結果として、再帰ツリーの高さは\\(n − 1\\)に達する可能性があり、これには\\(O(n)\\)のスタックフレーム空間が必要です。</p> <p>スタックフレーム空間の蓄積を防ぐため、各ラウンドのピボットソート後に2つのサブ配列の長さを比較し、**より短いサブ配列のみを再帰的にソート**できます。より短いサブ配列の長さは\\(n / 2\\)を超えないため、この方法は再帰深度が\\(\\log n\\)を超えないことを保証し、最悪空間計算量を\\(O(\\log n)\\)に最適化します。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def quick_sort(self, nums: list[int], left: int, right: int):\n \"\"\"クイックソート(末尾再帰最適化)\"\"\"\n # サブ配列の長さが1のときに終了\n while left < right:\n # 分割操作\n pivot = self.partition(nums, left, right)\n # 2つのサブ配列のうち短い方に対してクイックソートを実行\n if pivot - left < right - pivot:\n self.quick_sort(nums, left, pivot - 1) # 左サブ配列を再帰的にソート\n left = pivot + 1 # 残りの未ソート区間は [pivot + 1, right]\n else:\n self.quick_sort(nums, pivot + 1, right) # 右サブ配列を再帰的にソート\n right = pivot - 1 # 残りの未ソート区間は [left, pivot - 1]\n</code></pre> quick_sort.cpp<pre><code>/* クイックソート(末尾再帰最適化) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n // サブ配列の長さが1の時終了\n while (left < right) {\n // 分割操作\n int pivot = partition(nums, left, right);\n // 二つのサブ配列のうち短い方でクイックソートを実行\n if (pivot - left < right - pivot) {\n quickSort(nums, left, pivot - 1); // 左サブ配列を再帰的にソート\n left = pivot + 1; // 残りの未ソート区間は[pivot + 1, right]\n } else {\n quickSort(nums, pivot + 1, right); // 右サブ配列を再帰的にソート\n right = pivot - 1; // 残りの未ソート区間は[left, pivot - 1]\n }\n }\n}\n</code></pre> quick_sort.java<pre><code>/* クイックソート(末尾再帰最適化) */\nvoid quickSort(int[] nums, int left, int right) {\n // 部分配列の長さが 1 のとき終了\n while (left < right) {\n // 分割操作\n int pivot = partition(nums, left, right);\n // 2つの部分配列のうち短い方にクイックソートを実行\n if (pivot - left < right - pivot) {\n quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n left = pivot + 1; // 残りの未ソート区間は [pivot + 1, right]\n } else {\n quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n right = pivot - 1; // 残りの未ソート区間は [left, pivot - 1]\n }\n }\n}\n</code></pre> quick_sort.cs<pre><code>[class]{QuickSortTailCall}-[func]{QuickSort}\n</code></pre> quick_sort.go<pre><code>[class]{quickSortTailCall}-[func]{quickSort}\n</code></pre> quick_sort.swift<pre><code>[class]{}-[func]{quickSortTailCall}\n</code></pre> quick_sort.js<pre><code>[class]{QuickSortTailCall}-[func]{quickSort}\n</code></pre> quick_sort.ts<pre><code>[class]{QuickSortTailCall}-[func]{quickSort}\n</code></pre> quick_sort.dart<pre><code>[class]{QuickSortTailCall}-[func]{quickSort}\n</code></pre> quick_sort.rs<pre><code>[class]{QuickSortTailCall}-[func]{quick_sort}\n</code></pre> quick_sort.c<pre><code>[class]{}-[func]{quickSortTailCall}\n</code></pre> quick_sort.kt<pre><code>[class]{}-[func]{quickSortTailCall}\n</code></pre> quick_sort.rb<pre><code>[class]{QuickSortTailCall}-[func]{quick_sort}\n</code></pre>","path":["第 11 章 ソート","11.5 クイックソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10 基数ソート","text":"<p>前の節では計数ソートを紹介しました。これは、データサイズ \\(n\\) が大きいがデータ範囲 \\(m\\) が小さいシナリオに適しています。\\(n = 10^6\\) の学生IDをソートする必要があり、各IDが \\(8\\) 桁の数字であるとします。これは、データ範囲 \\(m = 10^8\\) が非常に大きいことを意味します。この場合、計数ソートを使用すると、大量のメモリスペースが必要になります。基数ソートはこの状況を回避できます。</p> <p>基数ソートは計数ソートと同じ核心概念を共有し、要素の頻度をカウントすることでソートします。同時に、基数ソートは数字の桁間の漸進的関係を利用してこれを基盤としています。桁を一度に一つずつ処理してソートし、最終的なソート順序を達成します。</p>","path":["第 11 章 ソート","11.10 基数ソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101","level":2,"title":"11.10.1 アルゴリズムの過程","text":"<p>学生IDデータを例として、最下位桁を \\(1\\) 番目、最上位桁を \\(8\\) 番目とすると、基数ソートの過程は以下の図に示されています。</p> <ol> <li>桁 \\(k = 1\\) を初期化します。</li> <li>学生IDの \\(k\\) 番目の桁に対して「計数ソート」を実行します。完了後、データは \\(k\\) 番目の桁に基づいて最小から最大までソートされます。</li> <li>\\(k\\) を \\(1\\) 増やし、手順 <code>2.</code> に戻って反復を続け、すべての桁がソートされるまで続けます。この時点で過程が終了します。</li> </ol> <p></p> <p> 図 11-18 基数ソートアルゴリズムの過程 </p> <p>以下、コード実装を詳しく見てみます。基数 \\(d\\) での数 \\(x\\) に対して、その \\(k\\) 番目の桁 \\(x_k\\) を取得するには、以下の計算式を使用できます:</p> \\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\] <p>ここで \\(\\lfloor a \\rfloor\\) は浮動小数点数 \\(a\\) の切り捨てを表し、\\(\\bmod \\: d\\) は \\(d\\) による剰余を表します。学生IDデータの場合、\\(d = 10\\) で \\(k \\in [1, 8]\\) です。</p> <p>さらに、\\(k\\) 番目の桁に基づいてソートできるように、計数ソートのコードを少し修正する必要があります:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py<pre><code>def digit(num: int, exp: int) -> int:\n \"\"\"要素 num の k 番目の桁を取得、exp = 10^(k-1)\"\"\"\n # k の代わりに exp を渡すことで、ここでコストの高い累乗計算を避けることができる\n return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n \"\"\"計数ソート(nums の k 番目の桁に基づく)\"\"\"\n # 10進数の桁の範囲は 0~9、したがって長さ10のバケット配列が必要\n counter = [0] * 10\n n = len(nums)\n # 数字 0~9 の出現回数を統計\n for i in range(n):\n d = digit(nums[i], exp) # nums[i] の k 番目の桁を取得、d とする\n counter[d] += 1 # 数字 d の出現回数を統計\n # 前置和を計算し、「出現回数」を「配列インデックス」に変換\n for i in range(1, 10):\n counter[i] += counter[i - 1]\n # 逆順に走査し、バケット統計に基づいて各要素を res に配置\n res = [0] * n\n for i in range(n - 1, -1, -1):\n d = digit(nums[i], exp)\n j = counter[d] - 1 # 配列内の d のインデックス j を取得\n res[j] = nums[i] # 現在の要素をインデックス j に配置\n counter[d] -= 1 # d の数を1減らす\n # 結果を使用して元の配列 nums を上書き\n for i in range(n):\n nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n \"\"\"基数ソート\"\"\"\n # 配列の最大要素を取得し、最大桁数を判定するために使用\n m = max(nums)\n # 最下位桁から最上位桁まで走査\n exp = 1\n while exp <= m:\n # 配列要素の k 番目の桁に対して計数ソートを実行\n # k = 1 -> exp = 1\n # k = 2 -> exp = 10\n # つまり、exp = 10^(k-1)\n counting_sort_digit(nums, exp)\n exp *= 10\n</code></pre> radix_sort.cpp<pre><code>/* 要素numのk番目の桁を取得、exp = 10^(k-1) */\nint digit(int num, int exp) {\n // kの代わりにexpを渡すことで、ここで繰り返される高価な冪乗計算を避けることができる\n return (num / exp) % 10;\n}\n\n/* カウントソート(numsのk番目の桁に基づく) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n // 10進数の桁範囲は0~9なので、長さ10のバケット配列が必要\n vector<int> counter(10, 0);\n int n = nums.size();\n // 数字0~9の出現回数を統計\n for (int i = 0; i < n; i++) {\n int d = digit(nums[i], exp); // nums[i]のk番目の桁を取得、dとして記録\n counter[d]++; // 数字dの出現回数を統計\n }\n // 前缀和を計算し、「出現回数」を「配列インデックス」に変換\n for (int i = 1; i < 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 逆順で走査し、バケット統計に基づいて各要素をresに配置\n vector<int> res(n, 0);\n for (int i = n - 1; i >= 0; i--) {\n int d = digit(nums[i], exp);\n int j = counter[d] - 1; // dが配列内にあるインデックスjを取得\n res[j] = nums[i]; // 現在の要素をインデックスjに配置\n counter[d]--; // dのカウントを1減らす\n }\n // 結果で元の配列numsを上書き\n for (int i = 0; i < n; i++)\n nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(vector<int> &nums) {\n // 配列の最大要素を取得、最大桁数を判定するために使用\n int m = *max_element(nums.begin(), nums.end());\n // 最下位桁から最上位桁まで走査\n for (int exp = 1; exp <= m; exp *= 10)\n // 配列要素のk番目の桁でカウントソートを実行\n // k = 1 -> exp = 1\n // k = 2 -> exp = 10\n // つまり、exp = 10^(k-1)\n countingSortDigit(nums, exp);\n}\n</code></pre> radix_sort.java<pre><code>/* 要素 num の k 番目の桁を取得、exp = 10^(k-1) */\nint digit(int num, int exp) {\n // k の代わりに exp を渡すことで、ここでコストの高い累乗計算の繰り返しを避けることができる\n return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 番目の桁に基づく) */\nvoid countingSortDigit(int[] nums, int exp) {\n // 10進数の桁の範囲は 0~9、したがって長さ 10 のバケット配列が必要\n int[] counter = new int[10];\n int n = nums.length;\n // 桁 0~9 の出現回数を統計\n for (int i = 0; i < n; i++) {\n int d = digit(nums[i], exp); // nums[i] の k 番目の桁を取得、d とする\n counter[d]++; // 桁 d の出現回数を統計\n }\n // 累積和を計算し、「出現回数」を「配列インデックス」に変換\n for (int i = 1; i < 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 逆順に走査し、バケット統計に基づいて各要素を res に配置\n int[] res = new int[n];\n for (int i = n - 1; i >= 0; i--) {\n int d = digit(nums[i], exp);\n int j = counter[d] - 1; // 配列内での d のインデックス j を取得\n res[j] = nums[i]; // 現在の要素をインデックス j に配置\n counter[d]--; // d のカウントを 1 減らす\n }\n // 結果で元の配列 nums を上書き\n for (int i = 0; i < n; i++)\n nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(int[] nums) {\n // 配列の最大要素を取得し、最大桁数を判定するために使用\n int m = Integer.MIN_VALUE;\n for (int num : nums)\n if (num > m)\n m = num;\n // 最下位桁から最上位桁まで走査\n for (int exp = 1; exp <= m; exp *= 10) {\n // 配列要素の k 番目の桁に対して計数ソートを実行\n // k = 1 -> exp = 1\n // k = 2 -> exp = 10\n // すなわち exp = 10^(k-1)\n countingSortDigit(nums, exp);\n }\n}\n</code></pre> radix_sort.cs<pre><code>[class]{radix_sort}-[func]{Digit}\n\n[class]{radix_sort}-[func]{CountingSortDigit}\n\n[class]{radix_sort}-[func]{RadixSort}\n</code></pre> radix_sort.go<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{countingSortDigit}\n\n[class]{}-[func]{radixSort}\n</code></pre> radix_sort.swift<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{countingSortDigit}\n\n[class]{}-[func]{radixSort}\n</code></pre> radix_sort.js<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{countingSortDigit}\n\n[class]{}-[func]{radixSort}\n</code></pre> radix_sort.ts<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{countingSortDigit}\n\n[class]{}-[func]{radixSort}\n</code></pre> radix_sort.dart<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{countingSortDigit}\n\n[class]{}-[func]{radixSort}\n</code></pre> radix_sort.rs<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{counting_sort_digit}\n\n[class]{}-[func]{radix_sort}\n</code></pre> radix_sort.c<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{countingSortDigit}\n\n[class]{}-[func]{radixSort}\n</code></pre> radix_sort.kt<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{countingSortDigit}\n\n[class]{}-[func]{radixSort}\n</code></pre> radix_sort.rb<pre><code>[class]{}-[func]{digit}\n\n[class]{}-[func]{counting_sort_digit}\n\n[class]{}-[func]{radix_sort}\n</code></pre> <p>なぜ最下位桁から開始するのか?</p> <p>連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が \\(a < b\\) で、2番目のラウンドが \\(a > b\\) の場合、2番目のラウンドの結果が最初のラウンドの結果を置き換えます。上位桁は下位桁より優先されるため、上位桁の前に下位桁をソートすることが理にかなっています。</p>","path":["第 11 章 ソート","11.10 基数ソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102","level":2,"title":"11.10.2 アルゴリズムの特徴","text":"<p>計数ソートと比較して、基数ソートはより大きな数値範囲に適していますが、データが固定桁数で表現でき、桁数があまり大きくないことを前提としています。例えば、浮動小数点数は桁数 \\(k\\) が大きい可能性があり、時間計算量 \\(O(nk) \\gg O(n^2)\\) につながる可能性があるため、基数ソートには適していません。</p> <ul> <li>時間計算量は \\(O(nk)\\)、非適応ソート:データサイズを \\(n\\)、データが基数 \\(d\\)、最大桁数を \\(k\\) とすると、単一桁のソートには \\(O(n + d)\\) 時間がかかり、すべての \\(k\\) 桁のソートには \\(O((n + d)k)\\) 時間がかかります。一般的に、\\(d\\) と \\(k\\) はどちらも比較的小さく、時間計算量は \\(O(n)\\) に近づきます。</li> <li>空間計算量は \\(O(n + d)\\)、非インプレースソート:計数ソートと同様に、基数ソートは長さ \\(n\\) と \\(d\\) の配列 <code>res</code> と <code>counter</code> にそれぞれ依存します。</li> <li>安定ソート:計数ソートが安定な場合、基数ソートも安定です。計数ソートが不安定な場合、基数ソートは正しいソート順序を保証できません。</li> </ul>","path":["第 11 章 ソート","11.10 基数ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2 選択ソート","text":"<p>選択ソートは非常にシンプルな原理で動作します:各反復で未ソート区間から最小要素を選択し、ソート済みセクションの末尾に移動するループを使用します。</p> <p>配列の長さを\\(n\\)とすると、選択ソートのステップは下図に示されます。</p> <ol> <li>最初に、すべての要素は未ソートで、つまり未ソート(インデックス)区間は\\([0, n-1]\\)です。</li> <li>区間\\([0, n-1]\\)の最小要素を選択し、インデックス\\(0\\)の要素と交換します。この後、配列の最初の要素がソートされます。</li> <li>区間\\([1, n-1]\\)の最小要素を選択し、インデックス\\(1\\)の要素と交換します。この後、配列の最初の2つの要素がソートされます。</li> <li>この方法で続行します。\\(n - 1\\)ラウンドの選択と交換の後、最初の\\(n - 1\\)個の要素がソートされます。</li> <li>残りの唯一の要素は結果的に最大要素であり、ソートする必要がないため、配列はソートされます。</li> </ol> <1><2><3><4><5><6><7><8><9><10><11> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 11-2 Selection sort process </p> <p>コードでは、\\(k\\)を使用して未ソート区間内の最小要素を記録します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py<pre><code>def selection_sort(nums: list[int]):\n \"\"\"選択ソート\"\"\"\n n = len(nums)\n # 外側のループ:未ソート範囲は [i, n-1]\n for i in range(n - 1):\n # 内側のループ:未ソート範囲内で最小要素を見つける\n k = i\n for j in range(i + 1, n):\n if nums[j] < nums[k]:\n k = j # 最小要素のインデックスを記録\n # 最小要素を未ソート範囲の先頭要素と交換\n nums[i], nums[k] = nums[k], nums[i]\n</code></pre> selection_sort.cpp<pre><code>/* 選択ソート */\nvoid selectionSort(vector<int> &nums) {\n int n = nums.size();\n // 外側ループ:未ソート範囲は[i, n-1]\n for (int i = 0; i < n - 1; i++) {\n // 内側ループ:未ソート範囲内で最小要素を見つける\n int k = i;\n for (int j = i + 1; j < n; j++) {\n if (nums[j] < nums[k])\n k = j; // 最小要素のインデックスを記録\n }\n // 最小要素を未ソート範囲の最初の要素と交換\n swap(nums[i], nums[k]);\n }\n}\n</code></pre> selection_sort.java<pre><code>/* 選択ソート */\nvoid selectionSort(int[] nums) {\n int n = nums.length;\n // 外側ループ: 未ソート範囲は [i, n-1]\n for (int i = 0; i < n - 1; i++) {\n // 内側ループ: 未ソート範囲内で最小要素を見つける\n int k = i;\n for (int j = i + 1; j < n; j++) {\n if (nums[j] < nums[k])\n k = j; // 最小要素のインデックスを記録\n }\n // 最小要素と未ソート範囲の最初の要素を交換\n int temp = nums[i];\n nums[i] = nums[k];\n nums[k] = temp;\n }\n}\n</code></pre> selection_sort.cs<pre><code>[class]{selection_sort}-[func]{SelectionSort}\n</code></pre> selection_sort.go<pre><code>[class]{}-[func]{selectionSort}\n</code></pre> selection_sort.swift<pre><code>[class]{}-[func]{selectionSort}\n</code></pre> selection_sort.js<pre><code>[class]{}-[func]{selectionSort}\n</code></pre> selection_sort.ts<pre><code>[class]{}-[func]{selectionSort}\n</code></pre> selection_sort.dart<pre><code>[class]{}-[func]{selectionSort}\n</code></pre> selection_sort.rs<pre><code>[class]{}-[func]{selection_sort}\n</code></pre> selection_sort.c<pre><code>[class]{}-[func]{selectionSort}\n</code></pre> selection_sort.kt<pre><code>[class]{}-[func]{selectionSort}\n</code></pre> selection_sort.rb<pre><code>[class]{}-[func]{selection_sort}\n</code></pre>","path":["第 11 章 ソート","11.2 選択ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121","level":2,"title":"11.2.1 アルゴリズムの特性","text":"<ul> <li>\\(O(n^2)\\)の時間計算量、非適応ソート:外側ループに\\(n - 1\\)回の反復があり、未ソートセクションの長さは最初の反復で\\(n\\)から始まり、最後の反復で\\(2\\)まで減少します。つまり、各外側ループ反復にはそれぞれ\\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(3\\)、\\(2\\)回の内側ループ反復が含まれ、合計は\\(\\frac{(n - 1)(n + 2)}{2}\\)となります。</li> <li>\\(O(1)\\)の空間計算量、インプレースソート:ポインタ\\(i\\)と\\(j\\)で定数の追加空間を使用します。</li> <li>非安定ソート:下図に示すように、要素<code>nums[i]</code>は等しい要素の右側に交換される可能性があり、相対順序が変わる原因となります。</li> </ul> <p> 図 11-3 Selection sort instability example </p>","path":["第 11 章 ソート","11.2 選択ソート"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1 ソートアルゴリズム","text":"<p>ソートアルゴリズムは、データセットを特定の順序で配列するために使用されます。ソートアルゴリズムは、順序付けられたデータは通常、より効率的に探索、分析、処理できるため、幅広い応用があります。</p> <p>下図に示すように、ソートアルゴリズムのデータ型は整数、浮動小数点数、文字、文字列などです。ソート基準は、数値サイズ、文字ASCII順序、またはカスタム基準など、必要に応じて設定できます。</p> <p></p> <p> 図 11-1 Data types and comparator examples </p>","path":["第 11 章 ソート","11.1 ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1 評価次元","text":"<p>実行効率:ソートアルゴリズムの時間計算量ができるだけ低いことを期待し、全体的な操作数も少ないこと(時間計算量の定数項を下げる)を望みます。大容量データでは、実行効率が特に重要です。</p> <p>インプレース性:名前が示すとおり、インプレースソートは元の配列を直接操作することで実現され、追加のヘルパー配列が不要であるため、メモリを節約します。一般的に、インプレースソートはデータ移動操作が少なく、高速です。</p> <p>安定性:安定ソートは、ソート後に配列内の等しい要素の相対順序が変わらないことを保証します。</p> <p>安定ソートは、マルチキーソートシナリオにおいて必要条件です。学生情報を格納するテーブルがあり、第1列と第2列がそれぞれ名前と年齢であるとします。この場合、不安定ソートは入力データの順序を失う可能性があります:</p> <pre><code># 入力データは名前でソート済み\n# (名前, 年齢)\n ('A', 19)\n ('B', 18)\n ('C', 21)\n ('D', 19)\n ('E', 23)\n\n# 不安定ソートアルゴリズムを使用してリストを年齢でソートすると仮定すると、\n# 結果は('D', 19)と('A', 19)の相対位置を変更し、\n# 入力データが名前でソート済みであるという性質が失われる\n ('B', 18)\n ('D', 19)\n ('A', 19)\n ('C', 21)\n ('E', 23)\n</code></pre> <p>適応性:適応ソートは入力データ内の既存の順序情報を活用して計算負荷を削減し、より最適な時間効率を実現します。適応ソートアルゴリズムの最良ケース時間計算量は、通常平均ケース時間計算量よりも優れています。</p> <p>比較ベースまたは非比較ベース:比較ベースソートは比較演算子(\\(<\\)、\\(=\\)、\\(>\\))に依存して要素の相対順序を決定し、配列全体をソートします。理論的最適時間計算量は\\(O(n \\log n)\\)です。一方、非比較ソートは比較演算子を使用せず、\\(O(n)\\)の時間計算量を実現できますが、汎用性は比較的劣ります。</p>","path":["第 11 章 ソート","11.1 ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112","level":2,"title":"11.1.2 理想的なソートアルゴリズム","text":"<p>高速実行、インプレース、安定、適応、汎用。明らかに、これらのすべての特徴を組み合わせたソートアルゴリズムは今日まで見つかっていません。したがって、ソートアルゴリズムを選択する際は、データの特定の特徴と問題の要件に基づいて決定する必要があります。</p> <p>次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価次元に基づいてそれぞれの利点と欠点を分析します。</p>","path":["第 11 章 ソート","11.1 ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11 まとめ","text":"","path":["第 11 章 ソート","11.11 まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#1","level":3,"title":"1. 重要な復習","text":"<ul> <li>バブルソートは隣接する要素を交換することで動作します。フラグを追加して早期リターンを可能にすることで、バブルソートの最良ケースの時間計算量を \\(O(n)\\) に最適化できます。</li> <li>挿入ソートは、未ソート区間から要素を取り出してソート済み区間の正しい位置に挿入することで各ラウンドをソートします。挿入ソートの時間計算量は \\(O(n^2)\\) ですが、単位あたりの操作が比較的少ないため、少量のデータのソートでは非常に人気があります。</li> <li>クイックソートは歩哨分割操作に基づいています。歩哨分割では、常に最悪のピボットを選ぶ可能性があり、時間計算量が \\(O(n^2)\\) に劣化する可能性があります。中央値やランダムピボットを導入することで、そのような劣化の確率を減らすことができます。末尾再帰は再帰の深さを効果的に減らし、空間計算量を \\(O(\\log n)\\) に最適化します。</li> <li>マージソートには分割とマージの2つの段階があり、通常分割統治戦略を体現しています。マージソートでは、配列のソートには補助配列の作成が必要で、空間計算量は \\(O(n)\\) になります。しかし、リストのソートの空間計算量は \\(O(1)\\) に最適化できます。</li> <li>バケットソートは3つの手順から構成されます:データをバケットに分散、各バケット内でのソート、バケット順での結果のマージ。これも分割統治戦略を体現し、非常に大きなデータセットに適しています。バケットソートの鍵はデータの均等分散です。</li> <li>計数ソートはバケットソートの変形で、各データポイントの出現回数をカウントすることでソートします。計数ソートは限られた範囲のデータを持つ大きなデータセットに適しており、データを正の整数に変換する必要があります。</li> <li>基数ソートは桁ごとにソートすることでデータを処理し、データが固定長の数値として表現される必要があります。</li> <li>全体的に、私たちは高効率、安定性、インプレース操作、適応性を持つソートアルゴリズムを求めています。しかし、他のデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たすソートアルゴリズムは存在しません。実際の応用では、データの特性に基づいて適切なソートアルゴリズムを選択する必要があります。</li> <li>以下の図は、効率性、安定性、インプレース性、適応性の観点から主流のソートアルゴリズムを比較しています。</li> </ul> <p> 図 11-19 ソートアルゴリズムの比較 </p>","path":["第 11 章 ソート","11.11 まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: ソートアルゴリズムの安定性はいつ必要ですか?</p> <p>実際には、オブジェクトの一つの属性に基づいてソートする場合があります。例えば、学生は名前と身長の属性を持ち、多段階ソートを実装することを目指します:最初に名前で <code>(A, 180) (B, 185) (C, 170) (D, 170)</code> を取得し、次に身長で。ソートアルゴリズムが不安定なため、<code>(D, 170) (C, 170) (A, 180) (B, 185)</code> になってしまう可能性があります。</p> <p>学生DとCの位置が交換され、名前の順序性が破られているのが分かります。これは望ましくありません。</p> <p>Q: 歩哨分割での「右から左への検索」と「左から右への検索」の順序を交換できますか?</p> <p>いいえ、最左要素をピボットとして使用する場合、最初に「右から左への検索」を行い、次に「左から右への検索」を行う必要があります。この結論はやや直観に反するので、理由を分析してみましょう。</p> <p>歩哨分割 <code>partition()</code> の最後のステップは <code>nums[left]</code> と <code>nums[i]</code> を交換することです。交換後、ピボットの左側の要素はすべてピボット以下になります。これには最後の交換前に <code>nums[left] >= nums[i]</code> が成り立つ必要があります。「左から右への検索」を最初に行い、ピボットより大きい要素が見つからない場合、<code>i == j</code> でループを終了し、<code>nums[j] == nums[i] > nums[left]</code> となる可能性があります。つまり、最終交換操作はピボットより大きい要素を配列の左端に交換し、歩哨分割を失敗させます。</p> <p>例えば、配列 <code>[0, 0, 0, 0, 1]</code> が与えられた場合、最初に「左から右への検索」を行うと、歩哨分割後の配列は <code>[1, 0, 0, 0, 0]</code> となり、これは正しくありません。</p> <p>さらに考えると、<code>nums[right]</code> をピボットとして選択する場合、まったく逆で、最初に「左から右への検索」を行う必要があります。</p> <p>Q: 末尾再帰最適化について、短い配列を選択することで再帰の深さが \\(\\log n\\) を超えないことを保証するのはなぜですか?</p> <p>再帰の深さは現在リターンしていない再帰メソッドの数です。歩哨分割の各ラウンドは元の配列を2つの副配列に分割します。末尾再帰最適化により、再帰的に続行する副配列の長さは最大でも元の配列長の半分です。最悪の場合常に長さを半分にすると仮定すると、最終的な再帰の深さは \\(\\log n\\) になります。</p> <p>元のクイックソートを見直すと、より大きな配列を継続的に再帰処理する可能性があり、最悪の場合 \\(n\\)、\\(n - 1\\)、...、\\(2\\)、\\(1\\) で、再帰の深さは \\(n\\) になります。末尾再帰最適化はこのシナリオを回避できます。</p> <p>Q: 配列のすべての要素が等しい場合、クイックソートの時間計算量は \\(O(n^2)\\) ですか?この劣化ケースをどう処理すべきですか?</p> <p>はい。この状況については、歩哨分割を使用して配列をピボットより小さい、等しい、大きいの3つの部分に分割することを検討してください。小さい部分と大きい部分のみを再帰的に進めます。この方法では、すべての入力要素が等しい配列を1ラウンドの歩哨分割だけでソートできます。</p> <p>Q: なぜバケットソートの最悪ケース時間計算量は \\(O(n^2)\\) ですか?</p> <p>最悪の場合、すべての要素が同じバケットに配置されます。これらの要素をソートするために \\(O(n^2)\\) アルゴリズムを使用する場合、時間計算量は \\(O(n^2)\\) になります。</p>","path":["第 11 章 ソート","11.11 まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"第 5 章 スタックとキュー","text":"<p>Abstract</p> <p>スタックは積み重ねられた猫のようなもので、キューは一列に並んだ猫のようなものです。</p> <p>それらはそれぞれ、後入先出(LIFO)と先入先出(FIFO)の論理関係を表しています。</p>","path":["第 5 章 スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"章の内容","text":"<ul> <li>5.1 スタック</li> <li>5.2 キュー</li> <li>5.3 両端キュー</li> <li>5.4 まとめ</li> </ul>","path":["第 5 章 スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3 両端キュー","text":"<p>キューでは、先頭からの要素の削除や末尾への要素の追加のみが可能です。下図に示すように、両端キュー(deque)はより柔軟性を提供し、先頭と末尾の両方で要素の追加や削除を可能にします。</p> <p></p> <p> 図 5-7 両端キューの操作 </p>","path":["第 5 章 スタックとキュー","5.3 両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531","level":2,"title":"5.3.1 両端キューの一般的な操作","text":"<p>両端キューの一般的な操作は以下の通りです。具体的なメソッド名は使用するプログラミング言語によって異なります。</p> <p> 表 5-3 両端キューの操作効率 </p> メソッド名 説明 時間計算量 <code>pushFirst()</code> 先頭に要素を追加 \\(O(1)\\) <code>pushLast()</code> 末尾に要素を追加 \\(O(1)\\) <code>popFirst()</code> 先頭要素を削除 \\(O(1)\\) <code>popLast()</code> 末尾要素を削除 \\(O(1)\\) <code>peekFirst()</code> 先頭要素にアクセス \\(O(1)\\) <code>peekLast()</code> 末尾要素にアクセス \\(O(1)\\) <p>同様に、プログラミング言語で実装された両端キュークラスを直接使用することができます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin deque.py<pre><code>from collections import deque\n\n# 両端キューを初期化\ndeq: deque[int] = deque()\n\n# 要素をエンキュー\ndeq.append(2) # 末尾に追加\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3) # 先頭に追加\ndeq.appendleft(1)\n\n# 要素にアクセス\nfront: int = deq[0] # 先頭要素\nrear: int = deq[-1] # 末尾要素\n\n# 要素をデキュー\npop_front: int = deq.popleft() # 先頭要素をデキュー\npop_rear: int = deq.pop() # 末尾要素をデキュー\n\n# 両端キューの長さを取得\nsize: int = len(deq)\n\n# 両端キューが空かどうかを確認\nis_empty: bool = len(deq) == 0\n</code></pre> deque.cpp<pre><code>/* 両端キューを初期化 */\ndeque<int> deque;\n\n/* 要素をエンキュー */\ndeque.push_back(2); // 末尾に追加\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // 先頭に追加\ndeque.push_front(1);\n\n/* 要素にアクセス */\nint front = deque.front(); // 先頭要素\nint back = deque.back(); // 末尾要素\n\n/* 要素をデキュー */\ndeque.pop_front(); // 先頭要素をデキュー\ndeque.pop_back(); // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.size();\n\n/* 両端キューが空かどうかを確認 */\nbool empty = deque.empty();\n</code></pre> deque.java<pre><code>/* 両端キューを初期化 */\nDeque<Integer> deque = new LinkedList<>();\n\n/* 要素をエンキュー */\ndeque.offerLast(2); // 末尾に追加\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3); // 先頭に追加\ndeque.offerFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.peekFirst(); // 先頭要素\nint peekLast = deque.peekLast(); // 末尾要素\n\n/* 要素をデキュー */\nint popFirst = deque.pollFirst(); // 先頭要素をデキュー\nint popLast = deque.pollLast(); // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.size();\n\n/* 両端キューが空かどうかを確認 */\nboolean isEmpty = deque.isEmpty();\n</code></pre> deque.cs<pre><code>/* 両端キューを初期化 */\n// C#では、LinkedListを両端キューとして使用\nLinkedList<int> deque = new();\n\n/* 要素をエンキュー */\ndeque.AddLast(2); // 末尾に追加\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3); // 先頭に追加\ndeque.AddFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.First.Value; // 先頭要素\nint peekLast = deque.Last.Value; // 末尾要素\n\n/* 要素をデキュー */\ndeque.RemoveFirst(); // 先頭要素をデキュー\ndeque.RemoveLast(); // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.Count;\n\n/* 両端キューが空かどうかを確認 */\nbool isEmpty = deque.Count == 0;\n</code></pre> deque_test.go<pre><code>/* 両端キューを初期化 */\n// Goでは、listを両端キューとして使用\ndeque := list.New()\n\n/* 要素をエンキュー */\ndeque.PushBack(2) // 末尾に追加\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3) // 先頭に追加\ndeque.PushFront(1)\n\n/* 要素にアクセス */\nfront := deque.Front() // 先頭要素\nrear := deque.Back() // 末尾要素\n\n/* 要素をデキュー */\ndeque.Remove(front) // 先頭要素をデキュー\ndeque.Remove(rear) // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nsize := deque.Len()\n\n/* 両端キューが空かどうかを確認 */\nisEmpty := deque.Len() == 0\n</code></pre> deque.swift<pre><code>/* 両端キューを初期化 */\n// Swiftには組み込みの両端キュークラスがないため、Arrayを両端キューとして使用\nvar deque: [Int] = []\n\n/* 要素をエンキュー */\ndeque.append(2) // 末尾に追加\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // 先頭に追加\ndeque.insert(1, at: 0)\n\n/* 要素にアクセス */\nlet peekFirst = deque.first! // 先頭要素\nlet peekLast = deque.last! // 末尾要素\n\n/* 要素をデキュー */\n// Arrayを使用する場合、popFirstの計算量はO(n)\nlet popFirst = deque.removeFirst() // 先頭要素をデキュー\nlet popLast = deque.removeLast() // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nlet size = deque.count\n\n/* 両端キューが空かどうかを確認 */\nlet isEmpty = deque.isEmpty\n</code></pre> deque.js<pre><code>/* 両端キューを初期化 */\n// JavaScriptには組み込みの両端キューがないため、Arrayを両端キューとして使用\nconst deque = [];\n\n/* 要素をエンキュー */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 注意:unshift()は配列のため時間計算量がO(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 要素にアクセス */\nconst peekFirst = deque[0]; // 先頭要素\nconst peekLast = deque[deque.length - 1]; // 末尾要素\n\n/* 要素をデキュー */\n// 注意:shift()は配列のため時間計算量がO(n)\nconst popFront = deque.shift(); // 先頭要素をデキュー\nconst popBack = deque.pop(); // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nconst size = deque.length;\n\n/* 両端キューが空かどうかを確認 */\nconst isEmpty = size === 0;\n</code></pre> deque.ts<pre><code>/* 両端キューを初期化 */\n// TypeScriptには組み込みの両端キューがないため、Arrayを両端キューとして使用\nconst deque: number[] = [];\n\n/* 要素をエンキュー */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 注意:unshift()は配列のため時間計算量がO(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 要素にアクセス */\nconst peekFirst: number = deque[0]; // 先頭要素\nconst peekLast: number = deque[deque.length - 1]; // 末尾要素\n\n/* 要素をデキュー */\n// 注意:shift()は配列のため時間計算量がO(n)\nconst popFront: number = deque.shift() as number; // 先頭要素をデキュー\nconst popBack: number = deque.pop() as number; // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nconst size: number = deque.length;\n\n/* 両端キューが空かどうかを確認 */\nconst isEmpty: boolean = size === 0;\n</code></pre> deque.dart<pre><code>/* 両端キューを初期化 */\n// Dartでは、Queueが両端キューとして定義される\nQueue<int> deque = Queue<int>();\n\n/* 要素をエンキュー */\ndeque.addLast(2); // 末尾に追加\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // 先頭に追加\ndeque.addFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.first; // 先頭要素\nint peekLast = deque.last; // 末尾要素\n\n/* 要素をデキュー */\nint popFirst = deque.removeFirst(); // 先頭要素をデキュー\nint popLast = deque.removeLast(); // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.length;\n\n/* 両端キューが空かどうかを確認 */\nbool isEmpty = deque.isEmpty;\n</code></pre> deque.rs<pre><code>/* 両端キューを初期化 */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 要素をエンキュー */\ndeque.push_back(2); // 末尾に追加\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // 先頭に追加\ndeque.push_front(1);\n\n/* 要素にアクセス */\nif let Some(front) = deque.front() { // 先頭要素\n}\nif let Some(rear) = deque.back() { // 末尾要素\n}\n\n/* 要素をデキュー */\nif let Some(pop_front) = deque.pop_front() { // 先頭要素をデキュー\n}\nif let Some(pop_rear) = deque.pop_back() { // 末尾要素をデキュー\n}\n\n/* 両端キューの長さを取得 */\nlet size = deque.len();\n\n/* 両端キューが空かどうかを確認 */\nlet is_empty = deque.is_empty();\n</code></pre> deque.c<pre><code>// Cには組み込みの両端キューが提供されていません\n</code></pre> deque.kt<pre><code>\n</code></pre>","path":["第 5 章 スタックとキュー","5.3 両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532","level":2,"title":"5.3.2 両端キューの実装 *","text":"<p>両端キューの実装は通常のキューの実装と似ており、連結リストまたは配列を基盤となるデータ構造として使用できます。</p>","path":["第 5 章 スタックとキュー","5.3 両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1","level":3,"title":"1. 双方向連結リストに基づく実装","text":"<p>前節で、通常の単一連結リストを使ってキューを実装したことを思い出してください。これは先頭からの削除(デキュー操作に対応)と末尾への新しい要素の追加(エンキュー操作に対応)を便利に行えるためでした。</p> <p>両端キューでは、先頭と末尾の両方でエンキューとデキュー操作を実行できます。つまり、両端キューは逆方向の操作も実装する必要があります。このため、両端キューの基盤となるデータ構造として「双方向連結リスト」を使用します。</p> <p>下図に示すように、双方向連結リストの先頭ノードと末尾ノードをそれぞれ両端キューの前端と後端として扱い、両端でのノードの追加と削除機能を実装します。</p> LinkedListDequepushLast()pushFirst()popLast()popFirst() <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 5-8 双方向連結リストによる両端キューのエンキューとデキュー操作の実装 </p> <p>実装コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py<pre><code>class ListNode:\n \"\"\"双方向連結リストノード\"\"\"\n\n def __init__(self, val: int):\n \"\"\"コンストラクタ\"\"\"\n self.val: int = val\n self.next: ListNode | None = None # 後続ノードへの参照\n self.prev: ListNode | None = None # 前駆ノードへの参照\n\nclass LinkedListDeque:\n \"\"\"双方向連結リストベースの双端キュークラス\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n self._front: ListNode | None = None # ヘッドノード front\n self._rear: ListNode | None = None # テールノード rear\n self._size: int = 0 # 双端キューの長さ\n\n def size(self) -> int:\n \"\"\"双端キューの長さを取得\"\"\"\n return self._size\n\n def is_empty(self) -> bool:\n \"\"\"双端キューが空かどうかを判定\"\"\"\n return self._size == 0\n\n def push(self, num: int, is_front: bool):\n \"\"\"エンキュー操作\"\"\"\n node = ListNode(num)\n # リストが空の場合、front と rear の両方を node に向ける\n if self.is_empty():\n self._front = self._rear = node\n # 前端エンキュー操作\n elif is_front:\n # ノードをリストの先頭に追加\n self._front.prev = node\n node.next = self._front\n self._front = node # ヘッドノードを更新\n # 後端エンキュー操作\n else:\n # ノードをリストの末尾に追加\n self._rear.next = node\n node.prev = self._rear\n self._rear = node # テールノードを更新\n self._size += 1 # キューの長さを更新\n\n def push_first(self, num: int):\n \"\"\"前端エンキュー\"\"\"\n self.push(num, True)\n\n def push_last(self, num: int):\n \"\"\"後端エンキュー\"\"\"\n self.push(num, False)\n\n def pop(self, is_front: bool) -> int:\n \"\"\"デキュー操作\"\"\"\n if self.is_empty():\n raise IndexError(\"Double-ended queue is empty\")\n # 前端デキュー操作\n if is_front:\n val: int = self._front.val # ヘッドノードの値を一時的に保存\n # ヘッドノードを削除\n fnext: ListNode | None = self._front.next\n if fnext is not None:\n fnext.prev = None\n self._front.next = None\n self._front = fnext # ヘッドノードを更新\n # 後端デキュー操作\n else:\n val: int = self._rear.val # テールノードの値を一時的に保存\n # テールノードを削除\n rprev: ListNode | None = self._rear.prev\n if rprev is not None:\n rprev.next = None\n self._rear.prev = None\n self._rear = rprev # テールノードを更新\n self._size -= 1 # キューの長さを更新\n return val\n\n def pop_first(self) -> int:\n \"\"\"前端デキュー\"\"\"\n return self.pop(True)\n\n def pop_last(self) -> int:\n \"\"\"後端デキュー\"\"\"\n return self.pop(False)\n\n def peek_first(self) -> int:\n \"\"\"前端要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Double-ended queue is empty\")\n return self._front.val\n\n def peek_last(self) -> int:\n \"\"\"後端要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Double-ended queue is empty\")\n return self._rear.val\n\n def to_array(self) -> list[int]:\n \"\"\"出力用の配列を返す\"\"\"\n node = self._front\n res = [0] * self.size()\n for i in range(self.size()):\n res[i] = node.val\n node = node.next\n return res\n</code></pre> linkedlist_deque.cpp<pre><code>/* 双方向連結リストノード */\nstruct DoublyListNode {\n int val; // ノードの値\n DoublyListNode *next; // 後続ノードへのポインタ\n DoublyListNode *prev; // 前続ノードへのポインタ\n DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n }\n};\n\n/* 双方向連結リストに基づく両端キュークラス */\nclass LinkedListDeque {\n private:\n DoublyListNode *front, *rear; // 先頭ノードfront、末尾ノードrear\n int queSize = 0; // 両端キューの長さ\n\n public:\n /* コンストラクタ */\n LinkedListDeque() : front(nullptr), rear(nullptr) {\n }\n\n /* デストラクタ */\n ~LinkedListDeque() {\n // 連結リストを走査、ノードを削除、メモリを解放\n DoublyListNode *pre, *cur = front;\n while (cur != nullptr) {\n pre = cur;\n cur = cur->next;\n delete pre;\n }\n }\n\n /* 両端キューの長さを取得 */\n int size() {\n return queSize;\n }\n\n /* 両端キューが空かどうかを判定 */\n bool isEmpty() {\n return size() == 0;\n }\n\n /* エンキュー操作 */\n void push(int num, bool isFront) {\n DoublyListNode *node = new DoublyListNode(num);\n // リストが空の場合、frontとrearの両方をnodeに向ける\n if (isEmpty())\n front = rear = node;\n // 先頭エンキュー操作\n else if (isFront) {\n // ノードをリストの先頭に追加\n front->prev = node;\n node->next = front;\n front = node; // 先頭ノードを更新\n // 末尾エンキュー操作\n } else {\n // ノードをリストの末尾に追加\n rear->next = node;\n node->prev = rear;\n rear = node; // 末尾ノードを更新\n }\n queSize++; // キュー長を更新\n }\n\n /* 先頭エンキュー */\n void pushFirst(int num) {\n push(num, true);\n }\n\n /* 末尾エンキュー */\n void pushLast(int num) {\n push(num, false);\n }\n\n /* デキュー操作 */\n int pop(bool isFront) {\n if (isEmpty())\n throw out_of_range(\"Queue is empty\");\n int val;\n // 先頭デキュー操作\n if (isFront) {\n val = front->val; // 先頭ノードの値を一時保存\n // 先頭ノードを削除\n DoublyListNode *fNext = front->next;\n if (fNext != nullptr) {\n fNext->prev = nullptr;\n front->next = nullptr;\n }\n delete front;\n front = fNext; // 先頭ノードを更新\n // 末尾デキュー操作\n } else {\n val = rear->val; // 末尾ノードの値を一時保存\n // 末尾ノードを削除\n DoublyListNode *rPrev = rear->prev;\n if (rPrev != nullptr) {\n rPrev->next = nullptr;\n rear->prev = nullptr;\n }\n delete rear;\n rear = rPrev; // 末尾ノードを更新\n }\n queSize--; // キュー長を更新\n return val;\n }\n\n /* 先頭デキュー */\n int popFirst() {\n return pop(true);\n }\n\n /* 末尾デキュー */\n int popLast() {\n return pop(false);\n }\n\n /* 先頭要素にアクセス */\n int peekFirst() {\n if (isEmpty())\n throw out_of_range(\"Double-ended queue is empty\");\n return front->val;\n }\n\n /* 末尾要素にアクセス */\n int peekLast() {\n if (isEmpty())\n throw out_of_range(\"Double-ended queue is empty\");\n return rear->val;\n }\n\n /* 印刷用に配列を返却 */\n vector<int> toVector() {\n DoublyListNode *node = front;\n vector<int> res(size());\n for (int i = 0; i < res.size(); i++) {\n res[i] = node->val;\n node = node->next;\n }\n return res;\n }\n};\n</code></pre> linkedlist_deque.java<pre><code>/* 双方向連結リストノード */\nclass ListNode {\n int val; // ノード値\n ListNode next; // 後続ノードへの参照\n ListNode prev; // 前任ノードへの参照\n\n ListNode(int val) {\n this.val = val;\n prev = next = null;\n }\n}\n\n/* 双方向連結リストに基づく両端キュークラス */\nclass LinkedListDeque {\n private ListNode front, rear; // 先頭ノード front、末尾ノード rear\n private int queSize = 0; // 両端キューの長さ\n\n public LinkedListDeque() {\n front = rear = null;\n }\n\n /* 両端キューの長さを取得 */\n public int size() {\n return queSize;\n }\n\n /* 両端キューが空かどうかを判定 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* エンキュー操作 */\n private void push(int num, boolean isFront) {\n ListNode node = new ListNode(num);\n // リストが空の場合、front と rear の両方を node に指す\n if (isEmpty())\n front = rear = node;\n // 先頭エンキュー操作\n else if (isFront) {\n // node をリストの先頭に追加\n front.prev = node;\n node.next = front;\n front = node; // front を更新\n // 末尾エンキュー操作\n } else {\n // node をリストの末尾に追加\n rear.next = node;\n node.prev = rear;\n rear = node; // rear を更新\n }\n queSize++; // 長さを更新\n }\n\n /* 先頭エンキュー */\n public void pushFirst(int num) {\n push(num, true);\n }\n\n /* 末尾エンキュー */\n public void pushLast(int num) {\n push(num, false);\n }\n\n /* デキュー操作 */\n private int pop(boolean isFront) {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n int val;\n // 先頭デキュー操作\n if (isFront) {\n val = front.val; // 一時的に先頭ノード値を保存\n // 次のノードを削除\n ListNode fNext = front.next;\n if (fNext != null) {\n fNext.prev = null;\n front.next = null;\n }\n front = fNext; // front を更新\n // 末尾デキュー操作\n } else {\n val = rear.val; // 一時的に末尾ノード値を保存\n // 前のノードを削除\n ListNode rPrev = rear.prev;\n if (rPrev != null) {\n rPrev.next = null;\n rear.prev = null;\n }\n rear = rPrev; // rear を更新\n }\n queSize--; // 長さを更新\n return val;\n }\n\n /* 先頭デキュー */\n public int popFirst() {\n return pop(true);\n }\n\n /* 末尾デキュー */\n public int popLast() {\n return pop(false);\n }\n\n /* 先頭要素にアクセス */\n public int peekFirst() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return front.val;\n }\n\n /* 末尾要素にアクセス */\n public int peekLast() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return rear.val;\n }\n\n /* 配列を返す */\n public int[] toArray() {\n ListNode node = front;\n int[] res = new int[size()];\n for (int i = 0; i < res.length; i++) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_deque.cs<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.go<pre><code>[class]{linkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.swift<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.js<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.ts<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.dart<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.rs<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.c<pre><code>[class]{DoublyListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.kt<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre> linkedlist_deque.rb<pre><code>[class]{ListNode}-[func]{}\n\n[class]{LinkedListDeque}-[func]{}\n</code></pre>","path":["第 5 章 スタックとキュー","5.3 両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2","level":3,"title":"2. 配列に基づく実装","text":"<p>下図に示すように、配列でキューを実装するのと同様に、循環配列を使って両端キューを実装することもできます。</p> ArrayDequepushLast()pushFirst()popLast()popFirst() <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 5-9 配列による両端キューのエンキューとデキュー操作の実装 </p> <p>実装では「前端エンキュー」と「後端デキュー」のメソッドを追加するだけです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py<pre><code>class ArrayDeque:\n \"\"\"循環配列ベースの双端キュークラス\"\"\"\n\n def __init__(self, capacity: int):\n \"\"\"コンストラクタ\"\"\"\n self._nums: list[int] = [0] * capacity\n self._front: int = 0\n self._size: int = 0\n\n def capacity(self) -> int:\n \"\"\"双端キューの容量を取得\"\"\"\n return len(self._nums)\n\n def size(self) -> int:\n \"\"\"双端キューの長さを取得\"\"\"\n return self._size\n\n def is_empty(self) -> bool:\n \"\"\"双端キューが空かどうかを判定\"\"\"\n return self._size == 0\n\n def index(self, i: int) -> int:\n \"\"\"循環配列のインデックスを計算\"\"\"\n # モジュロ演算によって循環配列を実装\n # i が配列の末尾を超えた場合、先頭に戻る\n # i が配列の先頭を超えた場合、末尾に戻る\n return (i + self.capacity()) % self.capacity()\n\n def push_first(self, num: int):\n \"\"\"前端エンキュー\"\"\"\n if self._size == self.capacity():\n print(\"双端キューが満杯です\")\n return\n # フロントポインタを左に1つ移動\n # モジュロ演算によってフロントが配列の先頭を超えて末尾に戻ることを実装\n self._front = self.index(self._front - 1)\n # num を前端に追加\n self._nums[self._front] = num\n self._size += 1\n\n def push_last(self, num: int):\n \"\"\"後端エンキュー\"\"\"\n if self._size == self.capacity():\n print(\"双端キューが満杯です\")\n return\n # リアポインタを計算、リアインデックス + 1 を指す\n rear = self.index(self._front + self._size)\n # num を後端に追加\n self._nums[rear] = num\n self._size += 1\n\n def pop_first(self) -> int:\n \"\"\"前端デキュー\"\"\"\n num = self.peek_first()\n # フロントポインタを1つ後ろに移動\n self._front = self.index(self._front + 1)\n self._size -= 1\n return num\n\n def pop_last(self) -> int:\n \"\"\"後端デキュー\"\"\"\n num = self.peek_last()\n self._size -= 1\n return num\n\n def peek_first(self) -> int:\n \"\"\"前端要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Double-ended queue is empty\")\n return self._nums[self._front]\n\n def peek_last(self) -> int:\n \"\"\"後端要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Double-ended queue is empty\")\n # 後端要素のインデックスを計算\n last = self.index(self._front + self._size - 1)\n return self._nums[last]\n\n def to_array(self) -> list[int]:\n \"\"\"出力用の配列を返す\"\"\"\n # 有効な長さ範囲内の要素のみを変換\n res = []\n for i in range(self._size):\n res.append(self._nums[self.index(self._front + i)])\n return res\n</code></pre> array_deque.cpp<pre><code>/* 循環配列に基づく両端キュークラス */\nclass ArrayDeque {\n private:\n vector<int> nums; // 両端キューの要素を格納する配列\n int front; // 先頭ポインタ、先頭要素を指す\n int queSize; // 両端キューの長さ\n\n public:\n /* コンストラクタ */\n ArrayDeque(int capacity) {\n nums.resize(capacity);\n front = queSize = 0;\n }\n\n /* 両端キューの容量を取得 */\n int capacity() {\n return nums.size();\n }\n\n /* 両端キューの長さを取得 */\n int size() {\n return queSize;\n }\n\n /* 両端キューが空かどうかを判定 */\n bool isEmpty() {\n return queSize == 0;\n }\n\n /* 循環配列のインデックスを計算 */\n int index(int i) {\n // 剰余演算で循環配列を実現\n // iが配列の末尾を超えた場合、先頭に戻る\n // iが配列の先頭を超えた場合、末尾に戻る\n return (i + capacity()) % capacity();\n }\n\n /* 先頭エンキュー */\n void pushFirst(int num) {\n if (queSize == capacity()) {\n cout << \"Double-ended queue is full\" << endl;\n return;\n }\n // 先頭ポインタを1つ左に移動\n // 剰余演算でfrontが配列の先頭を越えて末尾に戻ることを実現\n front = index(front - 1);\n // numを先頭に追加\n nums[front] = num;\n queSize++;\n }\n\n /* 末尾エンキュー */\n void pushLast(int num) {\n if (queSize == capacity()) {\n cout << \"Double-ended queue is full\" << endl;\n return;\n }\n // 末尾ポインタを計算、末尾インデックス + 1を指す\n int rear = index(front + queSize);\n // numを末尾に追加\n nums[rear] = num;\n queSize++;\n }\n\n /* 先頭デキュー */\n int popFirst() {\n int num = peekFirst();\n // 先頭ポインタを1つ後ろに移動\n front = index(front + 1);\n queSize--;\n return num;\n }\n\n /* 末尾デキュー */\n int popLast() {\n int num = peekLast();\n queSize--;\n return num;\n }\n\n /* 先頭要素にアクセス */\n int peekFirst() {\n if (isEmpty())\n throw out_of_range(\"Double-ended queue is empty\");\n return nums[front];\n }\n\n /* 末尾要素にアクセス */\n int peekLast() {\n if (isEmpty())\n throw out_of_range(\"Double-ended queue is empty\");\n // 末尾要素のインデックスを計算\n int last = index(front + queSize - 1);\n return nums[last];\n }\n\n /* 印刷用に配列を返却 */\n vector<int> toVector() {\n // 有効な長さ範囲内の要素のみを変換\n vector<int> res(queSize);\n for (int i = 0, j = front; i < queSize; i++, j++) {\n res[i] = nums[index(j)];\n }\n return res;\n }\n};\n</code></pre> array_deque.java<pre><code>/* 循環配列に基づく両端キュークラス */\nclass ArrayDeque {\n private int[] nums; // 両端キューの要素を格納する配列\n private int front; // 先頭ポインタ、先頭要素を指す\n private int queSize; // 両端キューの長さ\n\n /* コンストラクタ */\n public ArrayDeque(int capacity) {\n this.nums = new int[capacity];\n front = queSize = 0;\n }\n\n /* 両端キューの容量を取得 */\n public int capacity() {\n return nums.length;\n }\n\n /* 両端キューの長さを取得 */\n public int size() {\n return queSize;\n }\n\n /* 両端キューが空かどうかを判定 */\n public boolean isEmpty() {\n return queSize == 0;\n }\n\n /* 循環配列インデックスを計算 */\n private int index(int i) {\n // モジュロ演算により循環配列を実装\n // i が配列の末尾を超える場合、先頭に戻る\n // i が配列の先頭を超える場合、末尾に戻る\n return (i + capacity()) % capacity();\n }\n\n /* 先頭エンキュー */\n public void pushFirst(int num) {\n if (queSize == capacity()) {\n System.out.println(\"両端キューが満杯です\");\n return;\n }\n // 先頭ポインタを左に移動し、境界を越える場合は配列の末尾に回る\n front = index(front - 1);\n // 先頭に num を追加\n nums[front] = num;\n queSize++;\n }\n\n /* 末尾エンキュー */\n public void pushLast(int num) {\n if (queSize == capacity()) {\n System.out.println(\"両端キューが満杯です\");\n return;\n }\n // 末尾ポインタを計算し、末尾に要素を追加\n int rear = index(front + queSize);\n nums[rear] = num;\n queSize++;\n }\n\n /* 先頭デキュー */\n public int popFirst() {\n int num = peekFirst();\n // 先頭ポインタを右に移動\n front = index(front + 1);\n queSize--;\n return num;\n }\n\n /* 末尾デキュー */\n public int popLast() {\n int num = peekLast();\n queSize--;\n return num;\n }\n\n /* 先頭要素にアクセス */\n public int peekFirst() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return nums[front];\n }\n\n /* 末尾要素にアクセス */\n public int peekLast() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n // 末尾要素のインデックスを計算\n int last = index(front + queSize - 1);\n return nums[last];\n }\n\n /* 配列を返す */\n public int[] toArray() {\n // front から開始して queSize 個の要素のみをコピー\n int[] res = new int[queSize];\n for (int i = 0, j = front; i < queSize; i++, j++) {\n res[i] = nums[index(j)];\n }\n return res;\n }\n}\n</code></pre> array_deque.cs<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.go<pre><code>[class]{arrayDeque}-[func]{}\n</code></pre> array_deque.swift<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.js<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.ts<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.dart<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.rs<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.c<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.kt<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre> array_deque.rb<pre><code>[class]{ArrayDeque}-[func]{}\n</code></pre>","path":["第 5 章 スタックとキュー","5.3 両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533","level":2,"title":"5.3.3 両端キューの応用","text":"<p>両端キューはスタックとキューの両方のロジックを組み合わせているため、それぞれのすべてのユースケースを実装でき、より大きな柔軟性を提供します。</p> <p>ソフトウェアの「元に戻す」機能は通常スタックを使って実装されることを知っています:システムは各変更操作をスタックに<code>push</code>し、次に<code>pop</code>して元に戻すことを実装します。しかし、システムリソースの制限を考慮して、ソフトウェアは元に戻すステップの数を制限することがよくあります(例えば、最後の50ステップのみを許可)。スタックの長さが50を超えた場合、ソフトウェアはスタックの底部(キューの前端)で削除操作を実行する必要があります。しかし、通常のスタックではこの機能を実行できないため、両端キューが必要になります。「元に戻す」のコアロジックは依然としてスタックの後入れ先出し原則に従いますが、両端キューはより柔軟にいくつかの追加ロジックを実装できることに注意してください。</p>","path":["第 5 章 スタックとキュー","5.3 両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2 キュー","text":"<p>キューは、先入先出(FIFO)ルールに従う線形データ構造です。名前が示すように、キューは行列の現象をシミュレートし、新参者は列の後ろに並び、前の人が最初に列を離れます。</p> <p>下図に示すように、キューの前面を「ヘッド」、後面を「テール」と呼びます。キューの後ろに要素を追加する操作を「エンキュー」、前から要素を削除する操作を「デキュー」と呼びます。</p> <p></p> <p> 図 5-4 キューの先入先出ルール </p>","path":["第 5 章 スタックとキュー","5.2 キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1 キューの一般的な操作","text":"<p>キューの一般的な操作を下表に示します。メソッド名はプログラミング言語によって異なる場合があることに注意してください。ここでは、スタックで使用したのと同じ命名規則を使用します。</p> <p> 表 5-2 キュー操作の効率 </p> メソッド名 説明 時間計算量 <code>push()</code> 要素をエンキュー、テールに追加 \\(O(1)\\) <code>pop()</code> ヘッド要素をデキュー \\(O(1)\\) <code>peek()</code> ヘッド要素にアクセス \\(O(1)\\) <p>プログラミング言語で用意されているキュークラスを直接使用できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin queue.py<pre><code>from collections import deque\n\n# キューを初期化\n# Pythonでは、一般的にdequeクラスをキューとして使用します\n# queue.Queue()は純粋なキュークラスですが、使いにくいため推奨されません\nque: deque[int] = deque()\n\n# 要素をエンキュー\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# 最初の要素にアクセス\nfront: int = que[0]\n\n# 要素をデキュー\npop: int = que.popleft()\n\n# キューの長さを取得\nsize: int = len(que)\n\n# キューが空かどうかチェック\nis_empty: bool = len(que) == 0\n</code></pre> queue.cpp<pre><code>/* キューを初期化 */\nqueue<int> queue;\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 最初の要素にアクセス */\nint front = queue.front();\n\n/* 要素をデキュー */\nqueue.pop();\n\n/* キューの長さを取得 */\nint size = queue.size();\n\n/* キューが空かどうかチェック */\nbool empty = queue.empty();\n</code></pre> queue.java<pre><code>/* キューを初期化 */\nQueue<Integer> queue = new LinkedList<>();\n\n/* 要素をエンキュー */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* 最初の要素にアクセス */\nint peek = queue.peek();\n\n/* 要素をデキュー */\nint pop = queue.poll();\n\n/* キューの長さを取得 */\nint size = queue.size();\n\n/* キューが空かどうかチェック */\nboolean isEmpty = queue.isEmpty();\n</code></pre> queue.cs<pre><code>/* キューを初期化 */\nQueue<int> queue = new();\n\n/* 要素をエンキュー */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* 最初の要素にアクセス */\nint peek = queue.Peek();\n\n/* 要素をデキュー */\nint pop = queue.Dequeue();\n\n/* キューの長さを取得 */\nint size = queue.Count;\n\n/* キューが空かどうかチェック */\nbool isEmpty = queue.Count == 0;\n</code></pre> queue_test.go<pre><code>/* キューを初期化 */\n// Goでは、listをキューとして使用\nqueue := list.New()\n\n/* 要素をエンキュー */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* 最初の要素にアクセス */\npeek := queue.Front()\n\n/* 要素をデキュー */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* キューの長さを取得 */\nsize := queue.Len()\n\n/* キューが空かどうかチェック */\nisEmpty := queue.Len() == 0\n</code></pre> queue.swift<pre><code>/* キューを初期化 */\n// Swiftには組み込みのキュークラスがないため、Arrayをキューとして使用\nvar queue: [Int] = []\n\n/* 要素をエンキュー */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* 最初の要素にアクセス */\nlet peek = queue.first!\n\n/* 要素をデキュー */\n// 配列なので、removeFirstの計算量はO(n)\nlet pool = queue.removeFirst()\n\n/* キューの長さを取得 */\nlet size = queue.count\n\n/* キューが空かどうかチェック */\nlet isEmpty = queue.isEmpty\n</code></pre> queue.js<pre><code>/* キューを初期化 */\n// JavaScriptには組み込みのキューがないため、Arrayをキューとして使用\nconst queue = [];\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 最初の要素にアクセス */\nconst peek = queue[0];\n\n/* 要素をデキュー */\n// 基礎構造が配列なので、shift()メソッドの時間計算量はO(n)\nconst pop = queue.shift();\n\n/* キューの長さを取得 */\nconst size = queue.length;\n\n/* キューが空かどうかチェック */\nconst empty = queue.length === 0;\n</code></pre> queue.ts<pre><code>/* キューを初期化 */\n// TypeScriptには組み込みのキューがないため、Arrayをキューとして使用\nconst queue: number[] = [];\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 最初の要素にアクセス */\nconst peek = queue[0];\n\n/* 要素をデキュー */\n// 基礎構造が配列なので、shift()メソッドの時間計算量はO(n)\nconst pop = queue.shift();\n\n/* キューの長さを取得 */\nconst size = queue.length;\n\n/* キューが空かどうかチェック */\nconst empty = queue.length === 0;\n</code></pre> queue.dart<pre><code>/* キューを初期化 */\n// DartのQueueクラスは双方向キューですが、キューとして使用できます\nQueue<int> queue = Queue();\n\n/* 要素をエンキュー */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* 最初の要素にアクセス */\nint peek = queue.first;\n\n/* 要素をデキュー */\nint pop = queue.removeFirst();\n\n/* キューの長さを取得 */\nint size = queue.length;\n\n/* キューが空かどうかチェック */\nbool isEmpty = queue.isEmpty;\n</code></pre> queue.rs<pre><code>/* 双方向キューを初期化 */\n// Rustでは、双方向キューを通常のキューとして使用\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 要素をエンキュー */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* 最初の要素にアクセス */\nif let Some(front) = deque.front() {\n}\n\n/* 要素をデキュー */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* キューの長さを取得 */\nlet size = deque.len();\n\n/* キューが空かどうかチェック */\nlet is_empty = deque.is_empty();\n</code></pre> queue.c<pre><code>// Cは組み込みのキューを提供していません\n</code></pre> queue.kt<pre><code>\n</code></pre>","path":["第 5 章 スタックとキュー","5.2 キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522","level":2,"title":"5.2.2 キューの実装","text":"<p>キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列の両方がこの要件を満たします。</p>","path":["第 5 章 スタックとキュー","5.2 キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1","level":3,"title":"1. 連結リストベースの実装","text":"<p>下図に示すように、連結リストの「ヘッドノード」と「テールノード」をそれぞれキューの「フロント」と「リア」と考えることができます。ノードは後ろでのみ追加でき、前でのみ削除できるように規定されています。</p> LinkedListQueuepush()pop() <p></p> <p></p> <p></p> <p> 図 5-5 連結リストによるキュー実装のエンキューとデキュー操作 </p> <p>以下は、連結リストを使用してキューを実装するコードです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py<pre><code>class LinkedListQueue:\n \"\"\"連結リストベースのキュークラス\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n self._front: ListNode | None = None # ヘッドノード front\n self._rear: ListNode | None = None # テールノード rear\n self._size: int = 0\n\n def size(self) -> int:\n \"\"\"キューの長さを取得\"\"\"\n return self._size\n\n def is_empty(self) -> bool:\n \"\"\"キューが空かどうかを判定\"\"\"\n return self._size == 0\n\n def push(self, num: int):\n \"\"\"エンキュー\"\"\"\n # テールノードの後ろに num を追加\n node = ListNode(num)\n # キューが空の場合、ヘッドとテールノードの両方をそのノードに向ける\n if self._front is None:\n self._front = node\n self._rear = node\n # キューが空でない場合、そのノードをテールノードの後ろに追加\n else:\n self._rear.next = node\n self._rear = node\n self._size += 1\n\n def pop(self) -> int:\n \"\"\"デキュー\"\"\"\n num = self.peek()\n # ヘッドノードを削除\n self._front = self._front.next\n self._size -= 1\n return num\n\n def peek(self) -> int:\n \"\"\"フロント要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Queue is empty\")\n return self._front.val\n\n def to_list(self) -> list[int]:\n \"\"\"出力用のリストに変換\"\"\"\n queue = []\n temp = self._front\n while temp:\n queue.append(temp.val)\n temp = temp.next\n return queue\n</code></pre> linkedlist_queue.cpp<pre><code>/* 連結リストに基づくキュークラス */\nclass LinkedListQueue {\n private:\n ListNode *front, *rear; // 先頭ノードfront、末尾ノードrear\n int queSize;\n\n public:\n LinkedListQueue() {\n front = nullptr;\n rear = nullptr;\n queSize = 0;\n }\n\n ~LinkedListQueue() {\n // 連結リストを走査、ノードを削除、メモリを解放\n freeMemoryLinkedList(front);\n }\n\n /* キューの長さを取得 */\n int size() {\n return queSize;\n }\n\n /* キューが空かどうかを判定 */\n bool isEmpty() {\n return queSize == 0;\n }\n\n /* エンキュー */\n void push(int num) {\n // 末尾ノードの後ろにnumを追加\n ListNode *node = new ListNode(num);\n // キューが空の場合、先頭と末尾ノードの両方をそのノードに向ける\n if (front == nullptr) {\n front = node;\n rear = node;\n }\n // キューが空でない場合、そのノードを末尾ノードの後ろに追加\n else {\n rear->next = node;\n rear = node;\n }\n queSize++;\n }\n\n /* デキュー */\n int pop() {\n int num = peek();\n // 先頭ノードを削除\n ListNode *tmp = front;\n front = front->next;\n // メモリを解放\n delete tmp;\n queSize--;\n return num;\n }\n\n /* 先頭要素にアクセス */\n int peek() {\n if (size() == 0)\n throw out_of_range(\"Queue is empty\");\n return front->val;\n }\n\n /* 連結リストをVectorに変換して返却 */\n vector<int> toVector() {\n ListNode *node = front;\n vector<int> res(size());\n for (int i = 0; i < res.size(); i++) {\n res[i] = node->val;\n node = node->next;\n }\n return res;\n }\n};\n</code></pre> linkedlist_queue.java<pre><code>/* 連結リストに基づくキュークラス */\nclass LinkedListQueue {\n private ListNode front, rear; // 先頭ノード front、末尾ノード rear\n private int queSize = 0;\n\n public LinkedListQueue() {\n front = null;\n rear = null;\n }\n\n /* キューの長さを取得 */\n public int size() {\n return queSize;\n }\n\n /* キューが空かどうかを判定 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* エンキュー */\n public void push(int num) {\n // 末尾ノードの後ろに num を追加\n ListNode node = new ListNode(num);\n // キューが空の場合、先頭と末尾ノードの両方をそのノードにポイント\n if (front == null) {\n front = node;\n rear = node;\n // キューが空でない場合、そのノードを末尾ノードの後ろに追加\n } else {\n rear.next = node;\n rear = node;\n }\n queSize++;\n }\n\n /* デキュー */\n public int pop() {\n int num = peek();\n // 先頭ノードを削除\n front = front.next;\n queSize--;\n return num;\n }\n\n /* 先頭要素にアクセス */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return front.val;\n }\n\n /* 連結リストを配列に変換して返す */\n public int[] toArray() {\n ListNode node = front;\n int[] res = new int[size()];\n for (int i = 0; i < res.length; i++) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_queue.cs<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.go<pre><code>[class]{linkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.swift<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.js<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.ts<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.dart<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.rs<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.c<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.kt<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre> linkedlist_queue.rb<pre><code>[class]{LinkedListQueue}-[func]{}\n</code></pre>","path":["第 5 章 スタックとキュー","5.2 キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2","level":3,"title":"2. 配列ベースの実装","text":"<p>配列の最初の要素を削除する時間計算量は\\(O(n)\\)で、デキュー操作が非効率になります。しかし、この問題は以下のように巧妙に回避できます。</p> <p>変数<code>front</code>を使用してフロント要素のインデックスを示し、変数<code>size</code>を維持してキューの長さを記録します。<code>rear = front + size</code>を定義し、これはテール要素の直後の位置を指します。</p> <p>この設計により、配列内の要素の有効な間隔は<code>[front, rear - 1]</code>です。各操作の実装方法を下図に示します。</p> <ul> <li>エンキュー操作:入力要素を<code>rear</code>インデックスに割り当て、<code>size</code>を1増加させます。</li> <li>デキュー操作:単に<code>front</code>を1増加させ、<code>size</code>を1減少させます。</li> </ul> <p>エンキューとデキュー操作は両方とも単一の操作のみを必要とし、それぞれの時間計算量は\\(O(1)\\)です。</p> ArrayQueuepush()pop() <p></p> <p></p> <p></p> <p> 図 5-6 配列によるキュー実装のエンキューとデキュー操作 </p> <p>問題に気づくかもしれません:エンキューとデキュー操作が継続的に実行されると、<code>front</code>と<code>rear</code>の両方が右に移動し、最終的に配列の末尾に到達してそれ以上移動できなくなります。これを解決するために、配列を「循環配列」として扱い、配列の末尾を先頭に接続します。</p> <p>循環配列では、<code>front</code>または<code>rear</code>が末尾に到達すると、配列の先頭にループバックする必要があります。この循環パターンは、以下のコードに示すように「剰余演算」で実現できます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py<pre><code>class ArrayQueue:\n \"\"\"循環配列ベースのキュークラス\"\"\"\n\n def __init__(self, size: int):\n \"\"\"コンストラクタ\"\"\"\n self._nums: list[int] = [0] * size # キュー要素を格納する配列\n self._front: int = 0 # フロントポインタ、フロント要素を指す\n self._size: int = 0 # キューの長さ\n\n def capacity(self) -> int:\n \"\"\"キューの容量を取得\"\"\"\n return len(self._nums)\n\n def size(self) -> int:\n \"\"\"キューの長さを取得\"\"\"\n return self._size\n\n def is_empty(self) -> bool:\n \"\"\"キューが空かどうかを判定\"\"\"\n return self._size == 0\n\n def push(self, num: int):\n \"\"\"エンキュー\"\"\"\n if self._size == self.capacity():\n raise IndexError(\"Queue is full\")\n # リアポインタを計算、リアインデックス + 1 を指す\n # モジュロ演算を使用してリアポインタを配列の末尾から先頭に戻す\n rear: int = (self._front + self._size) % self.capacity()\n # num をリアに追加\n self._nums[rear] = num\n self._size += 1\n\n def pop(self) -> int:\n \"\"\"デキュー\"\"\"\n num: int = self.peek()\n # フロントポインタを1つ後ろに移動、末尾を超えた場合は配列の先頭に戻る\n self._front = (self._front + 1) % self.capacity()\n self._size -= 1\n return num\n\n def peek(self) -> int:\n \"\"\"フロント要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Queue is empty\")\n return self._nums[self._front]\n\n def to_list(self) -> list[int]:\n \"\"\"出力用の配列を返す\"\"\"\n res = [0] * self.size()\n j: int = self._front\n for i in range(self.size()):\n res[i] = self._nums[(j % self.capacity())]\n j += 1\n return res\n</code></pre> array_queue.cpp<pre><code>/* 循環配列に基づくキュークラス */\nclass ArrayQueue {\n private:\n int *nums; // キュー要素を格納する配列\n int front; // 先頭ポインタ、先頭要素を指す\n int queSize; // キューの長さ\n int queCapacity; // キューの容量\n\n public:\n ArrayQueue(int capacity) {\n // 配列を初期化\n nums = new int[capacity];\n queCapacity = capacity;\n front = queSize = 0;\n }\n\n ~ArrayQueue() {\n delete[] nums;\n }\n\n /* キューの容量を取得 */\n int capacity() {\n return queCapacity;\n }\n\n /* キューの長さを取得 */\n int size() {\n return queSize;\n }\n\n /* キューが空かどうかを判定 */\n bool isEmpty() {\n return size() == 0;\n }\n\n /* エンキュー */\n void push(int num) {\n if (queSize == queCapacity) {\n cout << \"Queue is full\" << endl;\n return;\n }\n // 末尾ポインタを計算、末尾インデックス + 1を指す\n // 剰余演算を使用して末尾ポインタが配列の末尾から先頭に戻るようにラップ\n int rear = (front + queSize) % queCapacity;\n // numを末尾に追加\n nums[rear] = num;\n queSize++;\n }\n\n /* デキュー */\n int pop() {\n int num = peek();\n // 先頭ポインタを1つ後ろに移動、末尾を超えた場合は配列の先頭に戻る\n front = (front + 1) % queCapacity;\n queSize--;\n return num;\n }\n\n /* 先頭要素にアクセス */\n int peek() {\n if (isEmpty())\n throw out_of_range(\"Queue is empty\");\n return nums[front];\n }\n\n /* 配列をVectorに変換して返却 */\n vector<int> toVector() {\n // 有効な長さ範囲内の要素のみを変換\n vector<int> arr(queSize);\n for (int i = 0, j = front; i < queSize; i++, j++) {\n arr[i] = nums[j % queCapacity];\n }\n return arr;\n }\n};\n</code></pre> array_queue.java<pre><code>/* 配列に基づくキュークラス */\nclass ArrayQueue {\n private int[] nums; // 要素を格納する配列\n private int front; // キューヘッドポインタ、最初の要素を指す\n private int queSize; // キューの長さ\n\n public ArrayQueue(int capacity) {\n nums = new int[capacity];\n front = queSize = 0;\n }\n\n /* キューの容量を取得 */\n public int capacity() {\n return nums.length;\n }\n\n /* キューの長さを取得 */\n public int size() {\n return queSize;\n }\n\n /* キューが空かどうかを判定 */\n public boolean isEmpty() {\n return queSize == 0;\n }\n\n /* エンキュー */\n public void push(int num) {\n if (queSize == capacity()) {\n System.out.println(\"キューが満杯です\");\n return;\n }\n // リアポインタを計算:front + queSize\n // モジュロ操作により rear が配列の長さを超えることを回避\n int rear = (front + queSize) % capacity();\n // 要素をキューリアに追加\n nums[rear] = num;\n queSize++;\n }\n\n /* デキュー */\n public int pop() {\n int num = peek();\n // キューヘッドポインタを後ろに1つ移動、モジュロ操作により範囲を超えることを回避\n front = (front + 1) % capacity();\n queSize--;\n return num;\n }\n\n /* キューヘッド要素にアクセス */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return nums[front];\n }\n\n /* 配列を返す */\n public int[] toArray() {\n // front から開始して queSize 個の要素のみをコピー\n int[] res = new int[queSize];\n for (int i = 0, j = front; i < queSize; i++, j++) {\n res[i] = nums[j % capacity()];\n }\n return res;\n }\n}\n</code></pre> array_queue.cs<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.go<pre><code>[class]{arrayQueue}-[func]{}\n</code></pre> array_queue.swift<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.js<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.ts<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.dart<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.rs<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.c<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.kt<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> array_queue.rb<pre><code>[class]{ArrayQueue}-[func]{}\n</code></pre> <p>上記のキュー実装にはまだ制限があります:長さが固定されています。しかし、この問題は解決が困難ではありません。配列を必要に応じて自動拡張できる動的配列に置き換えることができます。興味のある読者は自分で実装してみてください。</p> <p>2つの実装の比較はスタックの場合と一貫しており、ここでは繰り返しません。</p>","path":["第 5 章 スタックとキュー","5.2 キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523","level":2,"title":"5.2.3 キューの典型的な応用","text":"<ul> <li>Amazonの注文:買い物客が注文を行った後、これらの注文はキューに参加し、システムは順番に処理します。独身の日などのイベント中は、短時間で大量の注文が生成され、高い同時実行性がエンジニアにとって重要な課題となります。</li> <li>様々なToDoリスト:「先着順」機能が必要なシナリオ、例えばプリンターのタスクキューやレストランの配達キューなど、キューで処理順序を効果的に維持できます。</li> </ul>","path":["第 5 章 スタックとキュー","5.2 キュー"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1 スタック","text":"<p>スタックは、後入先出(LIFO)の原則に従う線形データ構造です。</p> <p>スタックをテーブル上の皿の山に例えることができます。底の皿にアクセスするには、まず上の皿を取り除く必要があります。皿を様々な種類の要素(整数、文字、オブジェクトなど)に置き換えることで、スタックと呼ばれるデータ構造を得ることができます。</p> <p>下図に示すように、要素の山の上部を「スタックのトップ」、下部を「スタックのボトム」と呼びます。スタックのトップに要素を追加する操作を「プッシュ」、トップ要素を削除する操作を「ポップ」と呼びます。</p> <p></p> <p> 図 5-1 スタックの後入先出ルール </p>","path":["第 5 章 スタックとキュー","5.1 スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1 スタックの一般的な操作","text":"<p>スタックの一般的な操作を下表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、例として<code>push()</code>、<code>pop()</code>、<code>peek()</code>を使用します。</p> <p> 表 5-1 スタック操作の効率 </p> メソッド 説明 時間計算量 <code>push()</code> 要素をスタックにプッシュ(トップに追加) \\(O(1)\\) <code>pop()</code> スタックからトップ要素をポップ \\(O(1)\\) <code>peek()</code> スタックのトップ要素にアクセス \\(O(1)\\) <p>通常、プログラミング言語に組み込まれているスタッククラスを直接使用できます。ただし、一部の言語では具体的にスタッククラスを提供していない場合があります。これらの場合、言語の「配列」または「連結リスト」をスタックとして使用し、プログラムでスタックロジックに関連しない操作を無視できます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlin stack.py<pre><code># スタックを初期化\n# Pythonには組み込みのスタッククラスがないため、listをスタックとして使用\nstack: list[int] = []\n\n# 要素をスタックにプッシュ\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# スタックのトップ要素にアクセス\npeek: int = stack[-1]\n\n# スタックから要素をポップ\npop: int = stack.pop()\n\n# スタックの長さを取得\nsize: int = len(stack)\n\n# スタックが空かどうかチェック\nis_empty: bool = len(stack) == 0\n</code></pre> stack.cpp<pre><code>/* スタックを初期化 */\nstack<int> stack;\n\n/* 要素をスタックにプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックのトップ要素にアクセス */\nint top = stack.top();\n\n/* スタックから要素をポップ */\nstack.pop(); // 戻り値なし\n\n/* スタックの長さを取得 */\nint size = stack.size();\n\n/* スタックが空かどうかチェック */\nbool empty = stack.empty();\n</code></pre> stack.java<pre><code>/* スタックを初期化 */\nStack<Integer> stack = new Stack<>();\n\n/* 要素をスタックにプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックのトップ要素にアクセス */\nint peek = stack.peek();\n\n/* スタックから要素をポップ */\nint pop = stack.pop();\n\n/* スタックの長さを取得 */\nint size = stack.size();\n\n/* スタックが空かどうかチェック */\nboolean isEmpty = stack.isEmpty();\n</code></pre> stack.cs<pre><code>/* スタックを初期化 */\nStack<int> stack = new();\n\n/* 要素をスタックにプッシュ */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* スタックのトップ要素にアクセス */\nint peek = stack.Peek();\n\n/* スタックから要素をポップ */\nint pop = stack.Pop();\n\n/* スタックの長さを取得 */\nint size = stack.Count;\n\n/* スタックが空かどうかチェック */\nbool isEmpty = stack.Count == 0;\n</code></pre> stack_test.go<pre><code>/* スタックを初期化 */\n// Goでは、Sliceをスタックとして使用することが推奨されます\nvar stack []int\n\n/* 要素をスタックにプッシュ */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* スタックのトップ要素にアクセス */\npeek := stack[len(stack)-1]\n\n/* スタックから要素をポップ */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* スタックの長さを取得 */\nsize := len(stack)\n\n/* スタックが空かどうかチェック */\nisEmpty := len(stack) == 0\n</code></pre> stack.swift<pre><code>/* スタックを初期化 */\n// Swiftには組み込みのスタッククラスがないため、Arrayをスタックとして使用\nvar stack: [Int] = []\n\n/* 要素をスタックにプッシュ */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* スタックのトップ要素にアクセス */\nlet peek = stack.last!\n\n/* スタックから要素をポップ */\nlet pop = stack.removeLast()\n\n/* スタックの長さを取得 */\nlet size = stack.count\n\n/* スタックが空かどうかチェック */\nlet isEmpty = stack.isEmpty\n</code></pre> stack.js<pre><code>/* スタックを初期化 */\n// JavaScriptには組み込みのスタッククラスがないため、Arrayをスタックとして使用\nconst stack = [];\n\n/* 要素をスタックにプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックのトップ要素にアクセス */\nconst peek = stack[stack.length-1];\n\n/* スタックから要素をポップ */\nconst pop = stack.pop();\n\n/* スタックの長さを取得 */\nconst size = stack.length;\n\n/* スタックが空かどうかチェック */\nconst is_empty = stack.length === 0;\n</code></pre> stack.ts<pre><code>/* スタックを初期化 */\n// TypeScriptには組み込みのスタッククラスがないため、Arrayをスタックとして使用\nconst stack: number[] = [];\n\n/* 要素をスタックにプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックのトップ要素にアクセス */\nconst peek = stack[stack.length - 1];\n\n/* スタックから要素をポップ */\nconst pop = stack.pop();\n\n/* スタックの長さを取得 */\nconst size = stack.length;\n\n/* スタックが空かどうかチェック */\nconst is_empty = stack.length === 0;\n</code></pre> stack.dart<pre><code>/* スタックを初期化 */\n// Dartには組み込みのスタッククラスがないため、Listをスタックとして使用\nList<int> stack = [];\n\n/* 要素をスタックにプッシュ */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* スタックのトップ要素にアクセス */\nint peek = stack.last;\n\n/* スタックから要素をポップ */\nint pop = stack.removeLast();\n\n/* スタックの長さを取得 */\nint size = stack.length;\n\n/* スタックが空かどうかチェック */\nbool isEmpty = stack.isEmpty;\n</code></pre> stack.rs<pre><code>/* スタックを初期化 */\n// Vecをスタックとして使用\nlet mut stack: Vec<i32> = Vec::new();\n\n/* 要素をスタックにプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックのトップ要素にアクセス */\nlet top = stack.last().unwrap();\n\n/* スタックから要素をポップ */\nlet pop = stack.pop().unwrap();\n\n/* スタックの長さを取得 */\nlet size = stack.len();\n\n/* スタックが空かどうかチェック */\nlet is_empty = stack.is_empty();\n</code></pre> stack.c<pre><code>// Cは組み込みのスタックを提供していません\n</code></pre> stack.kt<pre><code>\n</code></pre>","path":["第 5 章 スタックとキュー","5.1 スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2 スタックの実装","text":"<p>スタックがどのように動作するかをより深く理解するために、自分でスタッククラスを実装してみましょう。</p> <p>スタックは後入先出の原則に従うため、スタックのトップでのみ要素を追加または削除できます。しかし、配列と連結リストの両方は任意の位置で要素を追加・削除できるため、スタックは制限された配列または連結リストと見なすことができます。言い換えれば、配列や連結リストの特定の無関係な操作を「遮蔽」して、外部の動作をスタックの特性に合わせることができます。</p>","path":["第 5 章 スタックとキュー","5.1 スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1. 連結リストベースの実装","text":"<p>連結リストを使用してスタックを実装する場合、リストのヘッドノードをスタックのトップ、テールノードをスタックのボトムと考えることができます。</p> <p>下図に示すように、プッシュ操作では、単に連結リストのヘッドに要素を挿入します。このノード挿入方法は「ヘッド挿入」として知られています。ポップ操作では、リストからヘッドノードを削除するだけです。</p> LinkedListStackpush()pop() <p></p> <p></p> <p></p> <p> 図 5-2 連結リストによるスタック実装のプッシュとポップ操作 </p> <p>以下は、連結リストに基づくスタック実装のサンプルコードです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py<pre><code>class LinkedListStack:\n \"\"\"連結リストベースのスタッククラス\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n self._peek: ListNode | None = None\n self._size: int = 0\n\n def size(self) -> int:\n \"\"\"スタックの長さを取得\"\"\"\n return self._size\n\n def is_empty(self) -> bool:\n \"\"\"スタックが空かどうかを判定\"\"\"\n return self._size == 0\n\n def push(self, val: int):\n \"\"\"プッシュ\"\"\"\n node = ListNode(val)\n node.next = self._peek\n self._peek = node\n self._size += 1\n\n def pop(self) -> int:\n \"\"\"ポップ\"\"\"\n num = self.peek()\n self._peek = self._peek.next\n self._size -= 1\n return num\n\n def peek(self) -> int:\n \"\"\"スタックトップ要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Stack is empty\")\n return self._peek.val\n\n def to_list(self) -> list[int]:\n \"\"\"出力用のリストに変換\"\"\"\n arr = []\n node = self._peek\n while node:\n arr.append(node.val)\n node = node.next\n arr.reverse()\n return arr\n</code></pre> linkedlist_stack.cpp<pre><code>/* 連結リストに基づくスタッククラス */\nclass LinkedListStack {\n private:\n ListNode *stackTop; // 先頭ノードをスタックトップとして使用\n int stkSize; // スタックの長さ\n\n public:\n LinkedListStack() {\n stackTop = nullptr;\n stkSize = 0;\n }\n\n ~LinkedListStack() {\n // 連結リストを走査、ノードを削除、メモリを解放\n freeMemoryLinkedList(stackTop);\n }\n\n /* スタックの長さを取得 */\n int size() {\n return stkSize;\n }\n\n /* スタックが空かどうかを判定 */\n bool isEmpty() {\n return size() == 0;\n }\n\n /* プッシュ */\n void push(int num) {\n ListNode *node = new ListNode(num);\n node->next = stackTop;\n stackTop = node;\n stkSize++;\n }\n\n /* ポップ */\n int pop() {\n int num = top();\n ListNode *tmp = stackTop;\n stackTop = stackTop->next;\n // メモリを解放\n delete tmp;\n stkSize--;\n return num;\n }\n\n /* スタックトップ要素にアクセス */\n int top() {\n if (isEmpty())\n throw out_of_range(\"Stack is empty\");\n return stackTop->val;\n }\n\n /* リストを配列に変換して返却 */\n vector<int> toVector() {\n ListNode *node = stackTop;\n vector<int> res(size());\n for (int i = res.size() - 1; i >= 0; i--) {\n res[i] = node->val;\n node = node->next;\n }\n return res;\n }\n};\n</code></pre> linkedlist_stack.java<pre><code>/* 連結リストに基づくスタッククラス */\nclass LinkedListStack {\n private ListNode stackPeek; // ヘッドノードをスタックトップとして使用\n private int stkSize = 0; // スタックの長さ\n\n public LinkedListStack() {\n stackPeek = null;\n }\n\n /* スタックの長さを取得 */\n public int size() {\n return stkSize;\n }\n\n /* スタックが空かどうかを判定 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* プッシュ */\n public void push(int num) {\n ListNode node = new ListNode(num);\n node.next = stackPeek;\n stackPeek = node;\n stkSize++;\n }\n\n /* ポップ */\n public int pop() {\n int num = peek();\n stackPeek = stackPeek.next;\n stkSize--;\n return num;\n }\n\n /* スタックトップ要素にアクセス */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return stackPeek.val;\n }\n\n /* List を Array に変換して返す */\n public int[] toArray() {\n ListNode node = stackPeek;\n int[] res = new int[size()];\n for (int i = res.length - 1; i >= 0; i--) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_stack.cs<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.go<pre><code>[class]{linkedListStack}-[func]{}\n</code></pre> linkedlist_stack.swift<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.js<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.ts<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.dart<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.rs<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.c<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.kt<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre> linkedlist_stack.rb<pre><code>[class]{LinkedListStack}-[func]{}\n</code></pre>","path":["第 5 章 スタックとキュー","5.1 スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2","level":3,"title":"2. 配列ベースの実装","text":"<p>配列を使用してスタックを実装する場合、配列の末尾をスタックのトップと考えることができます。下図に示すように、プッシュとポップ操作は、それぞれ配列の末尾での要素の追加と削除に対応し、どちらも時間計算量\\(O(1)\\)です。</p> ArrayStackpush()pop() <p></p> <p></p> <p></p> <p> 図 5-3 配列によるスタック実装のプッシュとポップ操作 </p> <p>スタックにプッシュされる要素が継続的に増加する可能性があるため、動的配列を使用でき、配列拡張を自分で処理する必要がありません。以下はサンプルコードです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py<pre><code>class ArrayStack:\n \"\"\"配列ベースのスタッククラス\"\"\"\n\n def __init__(self):\n \"\"\"コンストラクタ\"\"\"\n self._stack: list[int] = []\n\n def size(self) -> int:\n \"\"\"スタックの長さを取得\"\"\"\n return len(self._stack)\n\n def is_empty(self) -> bool:\n \"\"\"スタックが空かどうかを判定\"\"\"\n return self.size() == 0\n\n def push(self, item: int):\n \"\"\"プッシュ\"\"\"\n self._stack.append(item)\n\n def pop(self) -> int:\n \"\"\"ポップ\"\"\"\n if self.is_empty():\n raise IndexError(\"Stack is empty\")\n return self._stack.pop()\n\n def peek(self) -> int:\n \"\"\"スタックトップ要素にアクセス\"\"\"\n if self.is_empty():\n raise IndexError(\"Stack is empty\")\n return self._stack[-1]\n\n def to_list(self) -> list[int]:\n \"\"\"出力用の配列を返す\"\"\"\n return self._stack\n</code></pre> array_stack.cpp<pre><code>/* 配列に基づくスタッククラス */\nclass ArrayStack {\n private:\n vector<int> stack;\n\n public:\n /* スタックの長さを取得 */\n int size() {\n return stack.size();\n }\n\n /* スタックが空かどうかを判定 */\n bool isEmpty() {\n return stack.size() == 0;\n }\n\n /* プッシュ */\n void push(int num) {\n stack.push_back(num);\n }\n\n /* ポップ */\n int pop() {\n int num = top();\n stack.pop_back();\n return num;\n }\n\n /* スタックトップ要素にアクセス */\n int top() {\n if (isEmpty())\n throw out_of_range(\"Stack is empty\");\n return stack.back();\n }\n\n /* Vectorを返却 */\n vector<int> toVector() {\n return stack;\n }\n};\n</code></pre> array_stack.java<pre><code>/* 配列に基づくスタッククラス */\nclass ArrayStack {\n private ArrayList<Integer> stack;\n\n public ArrayStack() {\n // リスト(動的配列)を初期化\n stack = new ArrayList<>();\n }\n\n /* スタックの長さを取得 */\n public int size() {\n return stack.size();\n }\n\n /* スタックが空かどうかを判定 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* プッシュ */\n public void push(int num) {\n stack.add(num);\n }\n\n /* ポップ */\n public int pop() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return stack.remove(size() - 1);\n }\n\n /* スタックトップ要素にアクセス */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return stack.get(size() - 1);\n }\n\n /* List を Array に変換して返す */\n public Object[] toArray() {\n return stack.toArray();\n }\n}\n</code></pre> array_stack.cs<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.go<pre><code>[class]{arrayStack}-[func]{}\n</code></pre> array_stack.swift<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.js<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.ts<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.dart<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.rs<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.c<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.kt<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre> array_stack.rb<pre><code>[class]{ArrayStack}-[func]{}\n</code></pre>","path":["第 5 章 スタックとキュー","5.1 スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513-2","level":2,"title":"5.1.3 2つの実装の比較","text":"<p>サポートされる操作</p> <p>両方の実装は、スタックで定義されたすべての操作をサポートします。配列実装はさらにランダムアクセスをサポートしますが、これはスタック定義の範囲を超えており、一般的には使用されません。</p> <p>時間効率</p> <p>配列ベースの実装では、プッシュとポップ操作の両方が事前に割り当てられた連続メモリで発生し、良好なキャッシュ局所性があるため効率が高くなります。しかし、プッシュ操作が配列容量を超える場合、リサイズメカニズムがトリガーされ、そのプッシュ操作の時間計算量は\\(O(n)\\)になります。</p> <p>連結リスト実装では、リスト拡張は非常に柔軟で、配列拡張のような効率低下の問題はありません。しかし、プッシュ操作にはノードオブジェクトの初期化とポインタの変更が必要なため、効率は比較的低くなります。プッシュされる要素がすでにノードオブジェクトの場合、初期化ステップをスキップでき、効率が向上します。</p> <p>したがって、プッシュとポップ操作の要素が<code>int</code>や<code>double</code>などの基本データ型の場合、以下の結論を導くことができます:</p> <ul> <li>配列ベースのスタック実装は拡張時に効率が低下しますが、拡張は低頻度操作であるため、平均効率は高くなります。</li> <li>連結リストベースのスタック実装はより安定した効率パフォーマンスを提供します。</li> </ul> <p>空間効率</p> <p>リストを初期化する際、システムは「初期容量」を割り当てますが、これは実際の必要量を超える可能性があります。さらに、拡張メカニズムは通常、特定の係数(2倍など)で容量を増加させ、これも実際の必要量を超える可能性があります。したがって、配列ベースのスタックは一部の空間を無駄にする可能性があります。</p> <p>しかし、連結リストノードはポインタを格納するための追加空間が必要なため、連結リストノードが占有する空間は比較的大きくなります。</p> <p>まとめると、どちらの実装がよりメモリ効率的かを単純に判断することはできません。特定の状況に基づく分析が必要です。</p>","path":["第 5 章 スタックとキュー","5.1 スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4 スタックの典型的な応用","text":"<ul> <li>ブラウザの戻ると進む、ソフトウェアの元に戻すとやり直し。新しいWebページを開くたびに、ブラウザは前のページをスタックにプッシュし、戻る操作(本質的にはポップ操作)を通じて前のページに戻ることができます。戻ると進むの両方をサポートするには、2つのスタックが連携して動作する必要があります。</li> <li>プログラムのメモリ管理。関数が呼び出されるたびに、システムはスタックのトップにスタックフレームを追加して関数のコンテキスト情報を記録します。再帰関数では、下方向の再帰フェーズはスタックへのプッシュを続け、上方向のバックトラッキングフェーズはスタックからのポップを続けます。</li> </ul>","path":["第 5 章 スタックとキュー","5.1 スタック"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4 まとめ","text":"","path":["第 5 章 スタックとキュー","5.4 まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1","level":3,"title":"1. 重要なポイント","text":"<ul> <li>スタックは後入れ先出し(LIFO)の原則に従うデータ構造で、配列または連結リストを使って実装できます。</li> <li>時間効率の観点では、スタックの配列実装の方が平均的な効率が高いです。ただし、拡張時には単一のプッシュ操作の時間計算量が\\(O(n)\\)に悪化する可能性があります。対照的に、スタックの連結リスト実装はより安定した効率を提供します。</li> <li>空間効率に関しては、スタックの配列実装は一定程度の空間の無駄につながる可能性があります。ただし、連結リストのノードが占有するメモリ空間は一般的に配列の要素よりも大きいことに注意することが重要です。</li> <li>キューは先入れ先出し(FIFO)の原則に従うデータ構造で、同様に配列または連結リストを使って実装できます。キューの時間と空間効率に関する結論は、スタックと似ています。</li> <li>両端キュー(deque)はより柔軟なキューの種類で、両端での要素の追加と削除を可能にします。</li> </ul>","path":["第 5 章 スタックとキュー","5.4 まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: ブラウザの進む・戻る機能は双方向連結リストで実装されているのですか?</p> <p>ブラウザの進む・戻るナビゲーションは本質的に「スタック」概念の現れです。ユーザーが新しいページを訪問すると、そのページがスタックの先頭に追加されます。戻るボタンをクリックすると、ページがスタックの先頭からポップされます。両端キュー(deque)は、「両端キュー」の章で述べたように、いくつかの追加操作を便利に実装できます。</p> <p>Q: スタックからポップした後、ポップされたノードのメモリを解放する必要がありますか?</p> <p>ポップされたノードが後で使用される場合は、そのメモリを解放する必要はありません。自動ガベージコレクションを持つJavaやPythonなどの言語では、手動のメモリ解放は必要ありません。CやC++では、手動のメモリ解放が必要です。</p> <p>Q: 両端キューは2つのスタックを結合したもののように見えます。その用途は何ですか?</p> <p>両端キューは、スタックとキューの組み合わせまたは2つのスタックを結合したもので、スタックとキューの両方のロジックを示します。したがって、スタックとキューのすべてのアプリケーションを実装でき、より大きな柔軟性を提供します。</p> <p>Q: 元に戻すとやり直しは具体的にどのように実装されるのですか?</p> <p>元に戻すとやり直しの操作は2つのスタックを使って実装されます:元に戻す用のスタック<code>A</code>とやり直し用のスタック<code>B</code>です。</p> <ol> <li>ユーザーが操作を実行するたびに、それがスタック<code>A</code>にプッシュされ、スタック<code>B</code>がクリアされます。</li> <li>ユーザーが「元に戻す」を実行すると、最新の操作がスタック<code>A</code>からポップされ、スタック<code>B</code>にプッシュされます。</li> <li>ユーザーが「やり直し」を実行すると、最新の操作がスタック<code>B</code>からポップされ、スタック<code>A</code>に戻されます。</li> </ol>","path":["第 5 章 スタックとキュー","5.4 まとめ"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章 木","text":"<p>Abstract</p> <p>そびえ立つ木は活力に満ちた本質を放ち、深い根と豊かな葉を誇りながらも、その枝は疎らに散らばり、幽玄な雰囲気を醸し出しています。</p> <p>それはデータにおける分割統治の鮮やかな形を私たちに示しています。</p>","path":["第 7 章 木"],"tags":[]},{"location":"chapter_tree/#_1","level":2,"title":"章の内容","text":"<ul> <li>7.1 二分木</li> <li>7.2 二分木の走査</li> <li>7.3 二分木の配列表現</li> <li>7.4 二分探索木</li> <li>7.5 AVL木 *</li> <li>7.6 まとめ</li> </ul>","path":["第 7 章 木"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3 二分木の配列表現","text":"<p>連結リスト表現では、二分木の格納単位はノード<code>TreeNode</code>であり、ノードはポインタによって接続されます。連結リスト表現での二分木の基本操作については前の節で紹介しました。</p> <p>では、配列を使って二分木を表現することはできるでしょうか?答えはイエスです。</p>","path":["第 7 章 木","7.3 二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731","level":2,"title":"7.3.1 完全二分木の表現","text":"<p>まず簡単なケースから分析してみましょう。完全二分木が与えられたとき、レベル順探索の順序に従ってすべてのノードを配列に格納し、各ノードは一意の配列インデックスに対応します。</p> <p>レベル順探索の特性に基づいて、親ノードのインデックスとその子ノードの間の「マッピング公式」を導き出すことができます:ノードのインデックスが\\(i\\)の場合、その左の子のインデックスは\\(2i + 1\\)、右の子のインデックスは\\(2i + 2\\)です。下図は、さまざまなノードのインデックス間のマッピング関係を示しています。</p> <p></p> <p> 図 7-12 完全二分木の配列表現 </p> <p>マッピング公式は、連結リストのノード参照(ポインタ)と同様の役割を果たします。配列内の任意のノードが与えられたとき、マッピング公式を使用してその左(右)の子ノードにアクセスできます。</p>","path":["第 7 章 木","7.3 二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2 任意の二分木の表現","text":"<p>完全二分木は特別なケースです。二分木の中間レベルには多くの<code>None</code>値が存在することがよくあります。レベル順探索のシーケンスにはこれらの<code>None</code>値が含まれないため、このシーケンスだけに依存して<code>None</code>値の数と分布を推測することはできません。つまり、複数の二分木構造が同じレベル順探索シーケンスと一致する可能性があります。</p> <p>下図に示すように、完全でない二分木が与えられた場合、上記の配列表現方法は失敗します。</p> <p></p> <p> 図 7-13 レベル順探索シーケンスが複数の二分木の可能性に対応 </p> <p>この問題を解決するために、レベル順探索シーケンスですべての<code>None</code>値を明示的に書き出すことを検討できます。下図に示すように、この処理後、レベル順探索シーケンスは二分木を一意に表現できます。サンプルコードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code># 二分木の配列表現\n# Noneを使用して空のスロットを表現\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// 最大整数値INT_MAXを使用して空のスロットをマーク\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// Integerラッパークラスを使用してnullで空のスロットをマーク\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// nullable int (int?)を使用してnullで空のスロットをマーク\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// any型スライスを使用してnilで空のスロットをマーク\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// optional Int (Int?)を使用してnilで空のスロットをマーク\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// nullを使用して空のスロットを表現\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// nullを使用して空のスロットを表現\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// nullable int (int?)を使用してnullで空のスロットをマーク\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// Noneを使用して空のスロットをマーク\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// 最大int値を使用して空のスロットをマーク、したがってノード値はINT_MAXであってはならない\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n</code></pre> <pre><code>/* 二分木の配列表現 */\n// nullを使用して空のスロットを表現\nval tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n</code></pre> <pre><code>\n</code></pre> <p></p> <p> 図 7-14 任意の種類の二分木の配列表現 </p> <p>注目すべきは、完備二分木は配列表現に非常に適している**ということです。完備二分木の定義を思い出すと、<code>None</code>は最下位レベルでのみ、かつ右側に向かって現れます。**つまり、すべての<code>None</code>値は確実にレベル順探索シーケンスの最後に現れます。</p> <p>これは、配列を使用して完備二分木を表現する際、すべての<code>None</code>値の格納を省略できることを意味し、非常に便利です。下図に例を示します。</p> <p></p> <p> 図 7-15 完備二分木の配列表現 </p> <p>以下のコードは、配列表現に基づく二分木を実装し、次の操作を含みます:</p> <ul> <li>ノードが与えられたとき、その値、左(右)の子ノード、および親ノードを取得する。</li> <li>前順、中順、後順、およびレベル順探索シーケンスを取得する。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py<pre><code>class ArrayBinaryTree:\n \"\"\"配列ベースの二分木クラス\"\"\"\n\n def __init__(self, arr: list[int | None]):\n \"\"\"コンストラクタ\"\"\"\n self._tree = list(arr)\n\n def size(self):\n \"\"\"リストの容量\"\"\"\n return len(self._tree)\n\n def val(self, i: int) -> int | None:\n \"\"\"インデックスiのノードの値を取得\"\"\"\n # インデックスが範囲外の場合、Noneを返し、空席を表す\n if i < 0 or i >= self.size():\n return None\n return self._tree[i]\n\n def left(self, i: int) -> int | None:\n \"\"\"インデックスiのノードの左の子のインデックスを取得\"\"\"\n return 2 * i + 1\n\n def right(self, i: int) -> int | None:\n \"\"\"インデックスiのノードの右の子のインデックスを取得\"\"\"\n return 2 * i + 2\n\n def parent(self, i: int) -> int | None:\n \"\"\"インデックスiのノードの親のインデックスを取得\"\"\"\n return (i - 1) // 2\n\n def level_order(self) -> list[int]:\n \"\"\"レベル順走査\"\"\"\n self.res = []\n # 配列を走査\n for i in range(self.size()):\n if self.val(i) is not None:\n self.res.append(self.val(i))\n return self.res\n\n def dfs(self, i: int, order: str):\n \"\"\"深さ優先走査\"\"\"\n if self.val(i) is None:\n return\n # 前順走査\n if order == \"pre\":\n self.res.append(self.val(i))\n self.dfs(self.left(i), order)\n # 中順走査\n if order == \"in\":\n self.res.append(self.val(i))\n self.dfs(self.right(i), order)\n # 後順走査\n if order == \"post\":\n self.res.append(self.val(i))\n\n def pre_order(self) -> list[int]:\n \"\"\"前順走査\"\"\"\n self.res = []\n self.dfs(0, order=\"pre\")\n return self.res\n\n def in_order(self) -> list[int]:\n \"\"\"中順走査\"\"\"\n self.res = []\n self.dfs(0, order=\"in\")\n return self.res\n\n def post_order(self) -> list[int]:\n \"\"\"後順走査\"\"\"\n self.res = []\n self.dfs(0, order=\"post\")\n return self.res\n</code></pre> array_binary_tree.cpp<pre><code>/* 配列ベースの二分木クラス */\nclass ArrayBinaryTree {\n public:\n /* コンストラクタ */\n ArrayBinaryTree(vector<int> arr) {\n tree = arr;\n }\n\n /* リストの容量 */\n int size() {\n return tree.size();\n }\n\n /* インデックス i のノードの値を取得 */\n int val(int i) {\n // インデックスが範囲外の場合、INT_MAX を返す(null を表す)\n if (i < 0 || i >= size())\n return INT_MAX;\n return tree[i];\n }\n\n /* インデックス i のノードの左の子のインデックスを取得 */\n int left(int i) {\n return 2 * i + 1;\n }\n\n /* インデックス i のノードの右の子のインデックスを取得 */\n int right(int i) {\n return 2 * i + 2;\n }\n\n /* インデックス i のノードの親のインデックスを取得 */\n int parent(int i) {\n return (i - 1) / 2;\n }\n\n /* レベル順走査 */\n vector<int> levelOrder() {\n vector<int> res;\n // 配列を走査\n for (int i = 0; i < size(); i++) {\n if (val(i) != INT_MAX)\n res.push_back(val(i));\n }\n return res;\n }\n\n /* 前順走査 */\n vector<int> preOrder() {\n vector<int> res;\n dfs(0, \"pre\", res);\n return res;\n }\n\n /* 中順走査 */\n vector<int> inOrder() {\n vector<int> res;\n dfs(0, \"in\", res);\n return res;\n }\n\n /* 後順走査 */\n vector<int> postOrder() {\n vector<int> res;\n dfs(0, \"post\", res);\n return res;\n }\n\n private:\n vector<int> tree;\n\n /* 深さ優先走査 */\n void dfs(int i, string order, vector<int> &res) {\n // 空の位置の場合、戻る\n if (val(i) == INT_MAX)\n return;\n // 前順走査\n if (order == \"pre\")\n res.push_back(val(i));\n dfs(left(i), order, res);\n // 中順走査\n if (order == \"in\")\n res.push_back(val(i));\n dfs(right(i), order, res);\n // 後順走査\n if (order == \"post\")\n res.push_back(val(i));\n }\n};\n</code></pre> array_binary_tree.java<pre><code>/* 配列ベースの二分木クラス */\nclass ArrayBinaryTree {\n private List<Integer> tree;\n\n /* コンストラクタ */\n public ArrayBinaryTree(List<Integer> arr) {\n tree = new ArrayList<>(arr);\n }\n\n /* リストの容量 */\n public int size() {\n return tree.size();\n }\n\n /* インデックス i のノードの値を取得 */\n public Integer val(int i) {\n // インデックスが範囲外の場合、null を返す(空の位置を表す)\n if (i < 0 || i >= size())\n return null;\n return tree.get(i);\n }\n\n /* インデックス i のノードの左の子のインデックスを取得 */\n public Integer left(int i) {\n return 2 * i + 1;\n }\n\n /* インデックス i のノードの右の子のインデックスを取得 */\n public Integer right(int i) {\n return 2 * i + 2;\n }\n\n /* インデックス i のノードの親のインデックスを取得 */\n public Integer parent(int i) {\n return (i - 1) / 2;\n }\n\n /* レベル順走査 */\n public List<Integer> levelOrder() {\n List<Integer> res = new ArrayList<>();\n // 配列を走査\n for (int i = 0; i < size(); i++) {\n if (val(i) != null)\n res.add(val(i));\n }\n return res;\n }\n\n /* 深さ優先走査 */\n private void dfs(Integer i, String order, List<Integer> res) {\n // 空の位置の場合、戻る\n if (val(i) == null)\n return;\n // 前順走査\n if (\"pre\".equals(order))\n res.add(val(i));\n dfs(left(i), order, res);\n // 中順走査\n if (\"in\".equals(order))\n res.add(val(i));\n dfs(right(i), order, res);\n // 後順走査\n if (\"post\".equals(order))\n res.add(val(i));\n }\n\n /* 前順走査 */\n public List<Integer> preOrder() {\n List<Integer> res = new ArrayList<>();\n dfs(0, \"pre\", res);\n return res;\n }\n\n /* 中順走査 */\n public List<Integer> inOrder() {\n List<Integer> res = new ArrayList<>();\n dfs(0, \"in\", res);\n return res;\n }\n\n /* 後順走査 */\n public List<Integer> postOrder() {\n List<Integer> res = new ArrayList<>();\n dfs(0, \"post\", res);\n return res;\n }\n}\n</code></pre> array_binary_tree.cs<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.go<pre><code>[class]{arrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.swift<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.js<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.ts<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.dart<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.rs<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.c<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.kt<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre> array_binary_tree.rb<pre><code>[class]{ArrayBinaryTree}-[func]{}\n</code></pre>","path":["第 7 章 木","7.3 二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733","level":2,"title":"7.3.3 利点と制限","text":"<p>二分木の配列表現には以下の利点があります:</p> <ul> <li>配列は連続したメモリ空間に格納されるため、キャッシュフレンドリーで、より高速なアクセスと探索が可能です。</li> <li>ポインタを格納する必要がないため、スペースを節約できます。</li> <li>ノードへのランダムアクセスが可能です。</li> </ul> <p>しかし、配列表現にはいくつかの制限もあります:</p> <ul> <li>配列格納には連続したメモリ空間が必要なため、大量のデータを持つ木の格納には適していません。</li> <li>ノードの追加や削除には配列の挿入や削除操作が必要で、効率が低くなります。</li> <li>二分木に多くの<code>None</code>値がある場合、配列に含まれるノードデータの割合が低くなり、空間利用率が低下します。</li> </ul>","path":["第 7 章 木","7.3 二分木の配列表現"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5 AVL木 *","text":"<p>「二分探索木」の節では、複数の挿入と削除の後、二分探索木が連結リストに退化する可能性があることを述べました。このような場合、すべての操作の時間計算量が\\(O(\\log n)\\)から\\(O(n)\\)に悪化します。</p> <p>下図に示すように、2つのノード削除操作の後、この二分探索木は連結リストに退化します。</p> <p></p> <p> 図 7-24 ノード削除後のAVL木の退化 </p> <p>例えば、下図に示す完全二分木では、2つのノードを挿入した後、木が左に大きく傾き、検索操作の時間計算量も悪化します。</p> <p></p> <p> 図 7-25 ノード挿入後のAVL木の退化 </p> <p>1962年、G. M. Adelson-VelskyとE. M. Landisが論文「An algorithm for the organization of information」でAVL木を提案しました。この論文では、ノードの継続的な追加と削除の後もAVL木が退化しないことを保証する一連の操作について詳述し、さまざまな操作の時間計算量を\\(O(\\log n)\\)レベルに維持しました。つまり、頻繁な追加、削除、検索、変更が必要なシナリオで、AVL木は常に効率的なデータ操作性能を維持でき、大きな応用価値があります。</p>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-avl","level":2,"title":"7.5.1 AVL木の一般的な用語","text":"<p>AVL木は二分探索木でありかつ平衡二分木でもあり、これら2つの種類の二分木のすべての性質を満たしているため、平衡二分探索木です。</p>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1","level":3,"title":"1. ノードの高さ","text":"<p>AVL木に関連する操作ではノードの高さを取得する必要があるため、ノードクラスに<code>height</code>変数を追加する必要があります:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>class TreeNode:\n \"\"\"AVL木ノード\"\"\"\n def __init__(self, val: int):\n self.val: int = val # ノード値\n self.height: int = 0 # ノードの高さ\n self.left: TreeNode | None = None # 左の子への参照\n self.right: TreeNode | None = None # 右の子への参照\n</code></pre> <pre><code>/* AVL木ノード */\nstruct TreeNode {\n int val{}; // ノード値\n int height = 0; // ノードの高さ\n TreeNode *left{}; // 左の子\n TreeNode *right{}; // 右の子\n TreeNode() = default;\n explicit TreeNode(int x) : val(x){}\n};\n</code></pre> <pre><code>/* AVL木ノード */\nclass TreeNode {\n public int val; // ノード値\n public int height; // ノードの高さ\n public TreeNode left; // 左の子\n public TreeNode right; // 右の子\n public TreeNode(int x) { val = x; }\n}\n</code></pre> <pre><code>/* AVL木ノード */\nclass TreeNode(int? x) {\n public int? val = x; // ノード値\n public int height; // ノードの高さ\n public TreeNode? left; // 左の子への参照\n public TreeNode? right; // 右の子への参照\n}\n</code></pre> <pre><code>/* AVL木ノード */\ntype TreeNode struct {\n Val int // ノード値\n Height int // ノードの高さ\n Left *TreeNode // 左の子への参照\n Right *TreeNode // 右の子への参照\n}\n</code></pre> <pre><code>/* AVL木ノード */\nclass TreeNode {\n var val: Int // ノード値\n var height: Int // ノードの高さ\n var left: TreeNode? // 左の子\n var right: TreeNode? // 右の子\n\n init(x: Int) {\n val = x\n height = 0\n }\n}\n</code></pre> <pre><code>/* AVL木ノード */\nclass TreeNode {\n val; // ノード値\n height; // ノードの高さ\n left; // 左の子ポインタ\n right; // 右の子ポインタ\n constructor(val, left, right, height) {\n this.val = val === undefined ? 0 : val;\n this.height = height === undefined ? 0 : height;\n this.left = left === undefined ? null : left;\n this.right = right === undefined ? null : right;\n }\n}\n</code></pre> <pre><code>/* AVL木ノード */\nclass TreeNode {\n val: number; // ノード値\n height: number; // ノードの高さ\n left: TreeNode | null; // 左の子ポインタ\n right: TreeNode | null; // 右の子ポインタ\n constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n this.val = val === undefined ? 0 : val;\n this.height = height === undefined ? 0 : height;\n this.left = left === undefined ? null : left;\n this.right = right === undefined ? null : right;\n }\n}\n</code></pre> <pre><code>/* AVL木ノード */\nclass TreeNode {\n int val; // ノード値\n int height; // ノードの高さ\n TreeNode? left; // 左の子\n TreeNode? right; // 右の子\n TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL木ノード */\nstruct TreeNode {\n val: i32, // ノード値\n height: i32, // ノードの高さ\n left: Option<Rc<RefCell<TreeNode>>>, // 左の子\n right: Option<Rc<RefCell<TreeNode>>>, // 右の子\n}\n\nimpl TreeNode {\n /* コンストラクタ */\n fn new(val: i32) -> Rc<RefCell<Self>> {\n Rc::new(RefCell::new(Self {\n val,\n height: 0,\n left: None,\n right: None\n }))\n }\n}\n</code></pre> <pre><code>/* AVL木ノード */\nTreeNode struct TreeNode {\n int val;\n int height;\n struct TreeNode *left;\n struct TreeNode *right;\n} TreeNode;\n\n/* コンストラクタ */\nTreeNode *newTreeNode(int val) {\n TreeNode *node;\n\n node = (TreeNode *)malloc(sizeof(TreeNode));\n node->val = val;\n node->height = 0;\n node->left = NULL;\n node->right = NULL;\n return node;\n}\n</code></pre> <pre><code>/* AVL木ノード */\nclass TreeNode(val _val: Int) { // ノード値\n val height: Int = 0 // ノードの高さ\n val left: TreeNode? = null // 左の子\n val right: TreeNode? = null // 右の子\n}\n</code></pre> <pre><code>\n</code></pre> <p>「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、つまり通過する「辺」の数を指します。重要なのは、葉ノードの高さは\\(0\\)で、nullノードの高さは\\(-1\\)であることです。ノードの高さを取得し、更新するための2つのユーティリティ関数を作成します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def height(self, node: TreeNode | None) -> int:\n \"\"\"ノードの高さを取得\"\"\"\n # 空ノードの高さは-1、葉ノードの高さは0\n if node is not None:\n return node.height\n return -1\n\ndef update_height(self, node: TreeNode | None):\n \"\"\"ノードの高さを更新\"\"\"\n # ノードの高さ = 最も高い部分木の高さ + 1\n node.height = max([self.height(node.left), self.height(node.right)]) + 1\n</code></pre> avl_tree.cpp<pre><code>/* ノードの高さを取得 */\nint height(TreeNode *node) {\n // 空ノードの高さは-1、葉ノードの高さは0\n return node == nullptr ? -1 : node->height;\n}\n\n/* ノードの高さを更新 */\nvoid updateHeight(TreeNode *node) {\n // ノードの高さ = 最も高い部分木の高さ + 1\n node->height = max(height(node->left), height(node->right)) + 1;\n}\n</code></pre> avl_tree.java<pre><code>/* ノードの高さを取得 */\nint height(TreeNode node) {\n // 空ノードの高さは -1、葉ノードの高さは 0\n return node == null ? -1 : node.height;\n}\n\n/* ノードの高さを更新 */\nvoid updateHeight(TreeNode node) {\n // ノードの高さは最も高い部分木の高さ + 1\n node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n</code></pre> avl_tree.cs<pre><code>[class]{AVLTree}-[func]{Height}\n\n[class]{AVLTree}-[func]{UpdateHeight}\n</code></pre> avl_tree.go<pre><code>[class]{aVLTree}-[func]{height}\n\n[class]{aVLTree}-[func]{updateHeight}\n</code></pre> avl_tree.swift<pre><code>[class]{AVLTree}-[func]{height}\n\n[class]{AVLTree}-[func]{updateHeight}\n</code></pre> avl_tree.js<pre><code>[class]{AVLTree}-[func]{height}\n\n[class]{AVLTree}-[func]{updateHeight}\n</code></pre> avl_tree.ts<pre><code>[class]{AVLTree}-[func]{height}\n\n[class]{AVLTree}-[func]{updateHeight}\n</code></pre> avl_tree.dart<pre><code>[class]{AVLTree}-[func]{height}\n\n[class]{AVLTree}-[func]{updateHeight}\n</code></pre> avl_tree.rs<pre><code>[class]{AVLTree}-[func]{height}\n\n[class]{AVLTree}-[func]{update_height}\n</code></pre> avl_tree.c<pre><code>[class]{}-[func]{height}\n\n[class]{}-[func]{updateHeight}\n</code></pre> avl_tree.kt<pre><code>[class]{AVLTree}-[func]{height}\n\n[class]{AVLTree}-[func]{updateHeight}\n</code></pre> avl_tree.rb<pre><code>[class]{AVLTree}-[func]{height}\n\n[class]{AVLTree}-[func]{update_height}\n</code></pre>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2","level":3,"title":"2. ノードの平衡因子","text":"<p>ノードの平衡因子は、そのノードの左部分木の高さから右部分木の高さを引いた値として定義され、nullノードの平衡因子は\\(0\\)として定義されます。後で使いやすくするため、ノードの平衡因子を取得する機能も関数にカプセル化します:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def balance_factor(self, node: TreeNode | None) -> int:\n \"\"\"バランス因子を取得\"\"\"\n # 空ノードのバランス因子は0\n if node is None:\n return 0\n # ノードのバランス因子 = 左部分木の高さ - 右部分木の高さ\n return self.height(node.left) - self.height(node.right)\n</code></pre> avl_tree.cpp<pre><code>/* 平衡因子を取得 */\nint balanceFactor(TreeNode *node) {\n // 空ノードの平衡因子は0\n if (node == nullptr)\n return 0;\n // ノードの平衡因子 = 左部分木の高さ - 右部分木の高さ\n return height(node->left) - height(node->right);\n}\n</code></pre> avl_tree.java<pre><code>/* 平衡因子を取得 */\nint balanceFactor(TreeNode node) {\n // 空ノードの平衡因子は 0\n if (node == null)\n return 0;\n // ノードの平衡因子 = 左部分木の高さ - 右部分木の高さ\n return height(node.left) - height(node.right);\n}\n</code></pre> avl_tree.cs<pre><code>[class]{AVLTree}-[func]{BalanceFactor}\n</code></pre> avl_tree.go<pre><code>[class]{aVLTree}-[func]{balanceFactor}\n</code></pre> avl_tree.swift<pre><code>[class]{AVLTree}-[func]{balanceFactor}\n</code></pre> avl_tree.js<pre><code>[class]{AVLTree}-[func]{balanceFactor}\n</code></pre> avl_tree.ts<pre><code>[class]{AVLTree}-[func]{balanceFactor}\n</code></pre> avl_tree.dart<pre><code>[class]{AVLTree}-[func]{balanceFactor}\n</code></pre> avl_tree.rs<pre><code>[class]{AVLTree}-[func]{balance_factor}\n</code></pre> avl_tree.c<pre><code>[class]{}-[func]{balanceFactor}\n</code></pre> avl_tree.kt<pre><code>[class]{AVLTree}-[func]{balanceFactor}\n</code></pre> avl_tree.rb<pre><code>[class]{AVLTree}-[func]{balance_factor}\n</code></pre> <p>Tip</p> <p>平衡因子を\\(f\\)とすると、AVL木の任意のノードの平衡因子は\\(-1 \\le f \\le 1\\)を満たします。</p>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-avl","level":2,"title":"7.5.2 AVL木の回転","text":"<p>AVL木の特徴的な機能は「回転」操作で、これは二分木の中順探索シーケンスに影響を与えることなく、不平衡なノードのバランスを回復できます。つまり、回転操作は「二分探索木」の性質を維持しながら、木を「平衡二分木」に戻すことができます。</p> <p>絶対平衡因子が\\(> 1\\)のノードを「不平衡ノード」と呼びます。不平衡のタイプに応じて、4種類の回転があります:右回転、左回転、右左回転、左右回転です。以下、これらの回転操作について詳しく説明します。</p>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_1","level":3,"title":"1. 右回転","text":"<p>下図に示すように、二分木で下から上への最初の不平衡ノードは「ノード3」です。この不平衡ノードを根とする部分木に焦点を当て、これを<code>node</code>とし、その左の子を<code>child</code>として、「右回転」を実行します。右回転後、部分木は再びバランスが取れ、同時に二分探索木の性質も維持されます。</p> <1><2><3><4> <p></p> <p></p> <p></p> <p></p> <p> 図 7-26 右回転の手順 </p> <p>下図に示すように、<code>child</code>ノードに右の子(<code>grand_child</code>と表記)がある場合、右回転で手順を追加する必要があります:<code>grand_child</code>を<code>node</code>の左の子に設定します。</p> <p></p> <p> 図 7-27 grand_childがある右回転 </p> <p>「右回転」は比喩的な用語で、実際にはノードポインタを変更することで実現されます。以下のコードで示されます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n \"\"\"右回転操作\"\"\"\n child = node.left\n grand_child = child.right\n # childを中心にnodeを右に回転\n child.right = node\n node.left = grand_child\n # ノードの高さを更新\n self.update_height(node)\n self.update_height(child)\n # 回転後の部分木のルートを返す\n return child\n</code></pre> avl_tree.cpp<pre><code>/* 右回転操作 */\nTreeNode *rightRotate(TreeNode *node) {\n TreeNode *child = node->left;\n TreeNode *grandChild = child->right;\n // childを中心にnodeを右に回転\n child->right = node;\n node->left = grandChild;\n // ノードの高さを更新\n updateHeight(node);\n updateHeight(child);\n // 回転後の部分木のルートを返す\n return child;\n}\n</code></pre> avl_tree.java<pre><code>/* 右回転操作 */\nTreeNode rightRotate(TreeNode node) {\n TreeNode child = node.left;\n TreeNode grandChild = child.right;\n // child を軸として node を右に回転\n child.right = node;\n node.left = grandChild;\n // ノードの高さを更新\n updateHeight(node);\n updateHeight(child);\n // 回転後の部分木の根を返す\n return child;\n}\n</code></pre> avl_tree.cs<pre><code>[class]{AVLTree}-[func]{RightRotate}\n</code></pre> avl_tree.go<pre><code>[class]{aVLTree}-[func]{rightRotate}\n</code></pre> avl_tree.swift<pre><code>[class]{AVLTree}-[func]{rightRotate}\n</code></pre> avl_tree.js<pre><code>[class]{AVLTree}-[func]{rightRotate}\n</code></pre> avl_tree.ts<pre><code>[class]{AVLTree}-[func]{rightRotate}\n</code></pre> avl_tree.dart<pre><code>[class]{AVLTree}-[func]{rightRotate}\n</code></pre> avl_tree.rs<pre><code>[class]{AVLTree}-[func]{right_rotate}\n</code></pre> avl_tree.c<pre><code>[class]{}-[func]{rightRotate}\n</code></pre> avl_tree.kt<pre><code>[class]{AVLTree}-[func]{rightRotate}\n</code></pre> avl_tree.rb<pre><code>[class]{AVLTree}-[func]{right_rotate}\n</code></pre>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_1","level":3,"title":"2. 左回転","text":"<p>対応して、上記の不平衡二分木の「鏡像」を考慮すると、下図に示す「左回転」操作を実行する必要があります。</p> <p></p> <p> 図 7-28 左回転操作 </p> <p>同様に、下図に示すように、<code>child</code>ノードに左の子(<code>grand_child</code>と表記)がある場合、左回転で手順を追加する必要があります:<code>grand_child</code>を<code>node</code>の右の子に設定します。</p> <p></p> <p> 図 7-29 grand_childがある左回転 </p> <p>**右回転と左回転の操作は論理的に対称であり、2つの対称的な不平衡タイプを解決します**ことが観察できます。対称性に基づいて、右回転の実装コードですべての<code>left</code>を<code>right</code>に、すべての<code>right</code>を<code>left</code>に置き換えることで、左回転の実装コードを得ることができます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n \"\"\"左回転操作\"\"\"\n child = node.right\n grand_child = child.left\n # childを中心にnodeを左に回転\n child.left = node\n node.right = grand_child\n # ノードの高さを更新\n self.update_height(node)\n self.update_height(child)\n # 回転後の部分木のルートを返す\n return child\n</code></pre> avl_tree.cpp<pre><code>/* 左回転操作 */\nTreeNode *leftRotate(TreeNode *node) {\n TreeNode *child = node->right;\n TreeNode *grandChild = child->left;\n // childを中心にnodeを左に回転\n child->left = node;\n node->right = grandChild;\n // ノードの高さを更新\n updateHeight(node);\n updateHeight(child);\n // 回転後の部分木のルートを返す\n return child;\n}\n</code></pre> avl_tree.java<pre><code>/* 左回転操作 */\nTreeNode leftRotate(TreeNode node) {\n TreeNode child = node.right;\n TreeNode grandChild = child.left;\n // child を軸として node を左に回転\n child.left = node;\n node.right = grandChild;\n // ノードの高さを更新\n updateHeight(node);\n updateHeight(child);\n // 回転後の部分木の根を返す\n return child;\n}\n</code></pre> avl_tree.cs<pre><code>[class]{AVLTree}-[func]{LeftRotate}\n</code></pre> avl_tree.go<pre><code>[class]{aVLTree}-[func]{leftRotate}\n</code></pre> avl_tree.swift<pre><code>[class]{AVLTree}-[func]{leftRotate}\n</code></pre> avl_tree.js<pre><code>[class]{AVLTree}-[func]{leftRotate}\n</code></pre> avl_tree.ts<pre><code>[class]{AVLTree}-[func]{leftRotate}\n</code></pre> avl_tree.dart<pre><code>[class]{AVLTree}-[func]{leftRotate}\n</code></pre> avl_tree.rs<pre><code>[class]{AVLTree}-[func]{left_rotate}\n</code></pre> avl_tree.c<pre><code>[class]{}-[func]{leftRotate}\n</code></pre> avl_tree.kt<pre><code>[class]{AVLTree}-[func]{leftRotate}\n</code></pre> avl_tree.rb<pre><code>[class]{AVLTree}-[func]{left_rotate}\n</code></pre>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3","level":3,"title":"3. 左右回転","text":"<p>下図に示す不平衡ノード3の場合、左回転または右回転のいずれかだけでは部分木のバランスを回復できません。この場合、まず<code>child</code>に対して「左回転」を実行し、次に<code>node</code>に対して「右回転」を実行する必要があります。</p> <p></p> <p> 図 7-30 左右回転 </p>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4. 右左回転","text":"<p>下図に示すように、上記の不平衡二分木の鏡像ケースでは、まず<code>child</code>に対して「右回転」を実行し、次に<code>node</code>に対して「左回転」を実行する必要があります。</p> <p></p> <p> 図 7-31 右左回転 </p>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5. 回転の選択","text":"<p>下図に示す4種類の不平衡は、それぞれ上記で説明したケースに対応し、右回転、左右回転、右左回転、左回転が必要です。</p> <p></p> <p> 図 7-32 AVL木の4つの回転ケース </p> <p>下表に示すように、不平衡ノードの平衡因子とその高い側の子の平衡因子の符号を判断することで、不平衡ノードが上記のどのケースに属するかを決定します。</p> <p> 表 7-3 4つの回転ケースの選択条件 </p> 不平衡ノードの平衡因子 子ノードの平衡因子 使用する回転方法 \\(> 1\\)(左に傾いた木) \\(\\geq 0\\) 右回転 \\(> 1\\)(左に傾いた木) \\(<0\\) 左回転してから右回転 \\(< -1\\)(右に傾いた木) \\(\\leq 0\\) 左回転 \\(< -1\\)(右に傾いた木) \\(>0\\) 右回転してから左回転 <p>便宜上、回転操作を関数にカプセル化します。この関数により、さまざまな種類の不平衡に対して回転を実行し、不平衡ノードのバランスを回復できます。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def rotate(self, node: TreeNode | None) -> TreeNode | None:\n \"\"\"回転操作を実行して部分木のバランスを復元\"\"\"\n # nodeのバランス因子を取得\n balance_factor = self.balance_factor(node)\n # 左偏り木\n if balance_factor > 1:\n if self.balance_factor(node.left) >= 0:\n # 右回転\n return self.right_rotate(node)\n else:\n # 左回転してから右回転\n node.left = self.left_rotate(node.left)\n return self.right_rotate(node)\n # 右偏り木\n elif balance_factor < -1:\n if self.balance_factor(node.right) <= 0:\n # 左回転\n return self.left_rotate(node)\n else:\n # 右回転してから左回転\n node.right = self.right_rotate(node.right)\n return self.left_rotate(node)\n # バランスの取れた木、回転不要、戻る\n return node\n</code></pre> avl_tree.cpp<pre><code>/* 回転操作を実行して部分木の平衡を回復 */\nTreeNode *rotate(TreeNode *node) {\n // nodeの平衡因子を取得\n int _balanceFactor = balanceFactor(node);\n // 左に傾いた木\n if (_balanceFactor > 1) {\n if (balanceFactor(node->left) >= 0) {\n // 右回転\n return rightRotate(node);\n } else {\n // 先に左回転、その後右回転\n node->left = leftRotate(node->left);\n return rightRotate(node);\n }\n }\n // 右に傾いた木\n if (_balanceFactor < -1) {\n if (balanceFactor(node->right) <= 0) {\n // 左回転\n return leftRotate(node);\n } else {\n // 先に右回転、その後左回転\n node->right = rightRotate(node->right);\n return leftRotate(node);\n }\n }\n // 平衡な木、回転不要、そのまま戻る\n return node;\n}\n</code></pre> avl_tree.java<pre><code>/* 回転操作を実行して部分木の平衡を回復 */\nTreeNode rotate(TreeNode node) {\n // node の平衡因子を取得\n int balanceFactor = balanceFactor(node);\n // 左傾斜の木\n if (balanceFactor > 1) {\n if (balanceFactor(node.left) >= 0) {\n // 右回転\n return rightRotate(node);\n } else {\n // 先に左回転、その後右回転\n node.left = leftRotate(node.left);\n return rightRotate(node);\n }\n }\n // 右傾斜の木\n if (balanceFactor < -1) {\n if (balanceFactor(node.right) <= 0) {\n // 左回転\n return leftRotate(node);\n } else {\n // 先に右回転、その後左回転\n node.right = rightRotate(node.right);\n return leftRotate(node);\n }\n }\n // 平衡木、回転は不要、戻る\n return node;\n}\n</code></pre> avl_tree.cs<pre><code>[class]{AVLTree}-[func]{Rotate}\n</code></pre> avl_tree.go<pre><code>[class]{aVLTree}-[func]{rotate}\n</code></pre> avl_tree.swift<pre><code>[class]{AVLTree}-[func]{rotate}\n</code></pre> avl_tree.js<pre><code>[class]{AVLTree}-[func]{rotate}\n</code></pre> avl_tree.ts<pre><code>[class]{AVLTree}-[func]{rotate}\n</code></pre> avl_tree.dart<pre><code>[class]{AVLTree}-[func]{rotate}\n</code></pre> avl_tree.rs<pre><code>[class]{AVLTree}-[func]{rotate}\n</code></pre> avl_tree.c<pre><code>[class]{}-[func]{rotate}\n</code></pre> avl_tree.kt<pre><code>[class]{AVLTree}-[func]{rotate}\n</code></pre> avl_tree.rb<pre><code>[class]{AVLTree}-[func]{rotate}\n</code></pre>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-avl","level":2,"title":"7.5.3 AVL木の一般的な操作","text":"","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_2","level":3,"title":"1. ノードの挿入","text":"<p>AVL木のノード挿入操作は二分探索木のそれと似ています。唯一の違いは、AVL木でノードを挿入した後、そのノードから根ノードまでのパス上に一連の不平衡ノードが現れる可能性があることです。したがって、このノードから始めて上向きに回転操作を実行し、すべての不平衡ノードのバランスを回復する必要があります。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def insert(self, val):\n \"\"\"ノードを挿入\"\"\"\n self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n \"\"\"再帰的にノードを挿入(ヘルパーメソッド)\"\"\"\n if node is None:\n return TreeNode(val)\n # 1. 挿入位置を見つけてノードを挿入\n if val < node.val:\n node.left = self.insert_helper(node.left, val)\n elif val > node.val:\n node.right = self.insert_helper(node.right, val)\n else:\n # 重複ノードは挿入しない、戻る\n return node\n # ノードの高さを更新\n self.update_height(node)\n # 2. 回転操作を実行して部分木のバランスを復元\n return self.rotate(node)\n</code></pre> avl_tree.cpp<pre><code>/* ノードを挿入 */\nvoid insert(int val) {\n root = insertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入(ヘルパーメソッド) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n if (node == nullptr)\n return new TreeNode(val);\n /* 1. 挿入位置を見つけてノードを挿入 */\n if (val < node->val)\n node->left = insertHelper(node->left, val);\n else if (val > node->val)\n node->right = insertHelper(node->right, val);\n else\n return node; // 重複ノードは挿入しない、そのまま戻る\n updateHeight(node); // ノードの高さを更新\n /* 2. 回転操作を実行して部分木の平衡を回復 */\n node = rotate(node);\n // 部分木のルートノードを返す\n return node;\n}\n</code></pre> avl_tree.java<pre><code>/* ノードを挿入 */\nvoid insert(int val) {\n root = insertHelper(root, val);\n}\n\n/* 再帰的にノードを挿入(補助メソッド) */\nTreeNode insertHelper(TreeNode node, int val) {\n if (node == null)\n return new TreeNode(val);\n /* 1. 挿入位置を見つけてノードを挿入 */\n if (val < node.val)\n node.left = insertHelper(node.left, val);\n else if (val > node.val)\n node.right = insertHelper(node.right, val);\n else\n return node; // 重複ノードは挿入しない、戻る\n updateHeight(node); // ノードの高さを更新\n /* 2. 回転操作を実行して部分木の平衡を回復 */\n node = rotate(node);\n // 部分木の根ノードを返す\n return node;\n}\n</code></pre> avl_tree.cs<pre><code>[class]{AVLTree}-[func]{Insert}\n\n[class]{AVLTree}-[func]{InsertHelper}\n</code></pre> avl_tree.go<pre><code>[class]{aVLTree}-[func]{insert}\n\n[class]{aVLTree}-[func]{insertHelper}\n</code></pre> avl_tree.swift<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{AVLTree}-[func]{insertHelper}\n</code></pre> avl_tree.js<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{AVLTree}-[func]{insertHelper}\n</code></pre> avl_tree.ts<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{AVLTree}-[func]{insertHelper}\n</code></pre> avl_tree.dart<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{AVLTree}-[func]{insertHelper}\n</code></pre> avl_tree.rs<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{AVLTree}-[func]{insert_helper}\n</code></pre> avl_tree.c<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{}-[func]{insertHelper}\n</code></pre> avl_tree.kt<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{AVLTree}-[func]{insertHelper}\n</code></pre> avl_tree.rb<pre><code>[class]{AVLTree}-[func]{insert}\n\n[class]{AVLTree}-[func]{insert_helper}\n</code></pre>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_2","level":3,"title":"2. ノードの削除","text":"<p>同様に、二分探索木でのノード削除方法に基づいて、下から上へ回転操作を実行してすべての不平衡ノードのバランスを回復する必要があります。コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def remove(self, val: int):\n \"\"\"ノードを削除\"\"\"\n self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n \"\"\"再帰的にノードを削除(ヘルパーメソッド)\"\"\"\n if node is None:\n return None\n # 1. ノードを見つけて削除\n if val < node.val:\n node.left = self.remove_helper(node.left, val)\n elif val > node.val:\n node.right = self.remove_helper(node.right, val)\n else:\n if node.left is None or node.right is None:\n child = node.left or node.right\n # 子ノード数 = 0、ノードを削除して戻る\n if child is None:\n return None\n # 子ノード数 = 1、ノードを削除\n else:\n node = child\n else:\n # 子ノード数 = 2、中順走査の次のノードを削除し、それで現在のノードを置き換え\n temp = node.right\n while temp.left is not None:\n temp = temp.left\n node.right = self.remove_helper(node.right, temp.val)\n node.val = temp.val\n # ノードの高さを更新\n self.update_height(node)\n # 2. 回転操作を実行して部分木のバランスを復元\n return self.rotate(node)\n</code></pre> avl_tree.cpp<pre><code>/* ノードを削除 */\nvoid remove(int val) {\n root = removeHelper(root, val);\n}\n\n/* ノードを再帰的に削除(ヘルパーメソッド) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n if (node == nullptr)\n return nullptr;\n /* 1. ノードを見つけて削除 */\n if (val < node->val)\n node->left = removeHelper(node->left, val);\n else if (val > node->val)\n node->right = removeHelper(node->right, val);\n else {\n if (node->left == nullptr || node->right == nullptr) {\n TreeNode *child = node->left != nullptr ? node->left : node->right;\n // 子ノード数 = 0、ノードを削除して戻る\n if (child == nullptr) {\n delete node;\n return nullptr;\n }\n // 子ノード数 = 1、ノードを削除\n else {\n delete node;\n node = child;\n }\n } else {\n // 子ノード数 = 2、中順走査の次のノードを削除し、現在のノードと置き換える\n TreeNode *temp = node->right;\n while (temp->left != nullptr) {\n temp = temp->left;\n }\n int tempVal = temp->val;\n node->right = removeHelper(node->right, temp->val);\n node->val = tempVal;\n }\n }\n updateHeight(node); // ノードの高さを更新\n /* 2. 回転操作を実行して部分木の平衡を回復 */\n node = rotate(node);\n // 部分木のルートノードを返す\n return node;\n}\n</code></pre> avl_tree.java<pre><code>/* ノードを削除 */\nvoid remove(int val) {\n root = removeHelper(root, val);\n}\n\n/* 再帰的にノードを削除(補助メソッド) */\nTreeNode removeHelper(TreeNode node, int val) {\n if (node == null)\n return null;\n /* 1. ノードを見つけて削除 */\n if (val < node.val)\n node.left = removeHelper(node.left, val);\n else if (val > node.val)\n node.right = removeHelper(node.right, val);\n else {\n if (node.left == null || node.right == null) {\n TreeNode child = node.left != null ? node.left : node.right;\n // 子ノード数 = 0、ノードを削除して戻る\n if (child == null)\n return null;\n // 子ノード数 = 1、ノードを削除\n else\n node = child;\n } else {\n // 子ノード数 = 2、中順走査の次のノードを削除し、現在のノードをそれで置き換える\n TreeNode temp = node.right;\n while (temp.left != null) {\n temp = temp.left;\n }\n node.right = removeHelper(node.right, temp.val);\n node.val = temp.val;\n }\n }\n updateHeight(node); // ノードの高さを更新\n /* 2. 回転操作を実行して部分木の平衡を回復 */\n node = rotate(node);\n // 部分木の根ノードを返す\n return node;\n}\n</code></pre> avl_tree.cs<pre><code>[class]{AVLTree}-[func]{Remove}\n\n[class]{AVLTree}-[func]{RemoveHelper}\n</code></pre> avl_tree.go<pre><code>[class]{aVLTree}-[func]{remove}\n\n[class]{aVLTree}-[func]{removeHelper}\n</code></pre> avl_tree.swift<pre><code>[class]{AVLTree}-[func]{remove}\n\n[class]{AVLTree}-[func]{removeHelper}\n</code></pre> avl_tree.js<pre><code>[class]{AVLTree}-[func]{remove}\n\n[class]{AVLTree}-[func]{removeHelper}\n</code></pre> avl_tree.ts<pre><code>[class]{AVLTree}-[func]{remove}\n\n[class]{AVLTree}-[func]{removeHelper}\n</code></pre> avl_tree.dart<pre><code>[class]{AVLTree}-[func]{remove}\n\n[class]{AVLTree}-[func]{removeHelper}\n</code></pre> avl_tree.rs<pre><code>[class]{AVLTree}-[func]{remove}\n\n[class]{AVLTree}-[func]{remove_helper}\n</code></pre> avl_tree.c<pre><code>[class]{AVLTree}-[func]{removeItem}\n\n[class]{}-[func]{removeHelper}\n</code></pre> avl_tree.kt<pre><code>[class]{AVLTree}-[func]{remove}\n\n[class]{AVLTree}-[func]{removeHelper}\n</code></pre> avl_tree.rb<pre><code>[class]{AVLTree}-[func]{remove}\n\n[class]{AVLTree}-[func]{remove_helper}\n</code></pre>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3_1","level":3,"title":"3. ノードの検索","text":"<p>AVL木でのノード検索操作は二分探索木のそれと一致しており、ここでは詳述しません。</p>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-avl","level":2,"title":"7.5.4 AVL木の典型的な応用","text":"<ul> <li>大量のデータの整理と格納に使用され、検索頻度が高く、挿入と削除の頻度が低いシナリオに適しています。</li> <li>データベースのインデックスシステムの構築に使用されます。</li> <li>赤黒木も一般的な平衡二分探索木の一種です。AVL木と比較して、赤黒木はより緩い平衡条件を持ち、ノードの挿入と削除にかかる回転数が少なく、ノードの追加と削除操作の平均効率が高くなります。</li> </ul>","path":["第 7 章 木","7.5 AVL木 *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4 二分探索木","text":"<p>下図に示すように、二分探索木は以下の条件を満たします。</p> <ol> <li>根ノードについて、左部分木のすべてのノードの値 \\(<\\) 根ノードの値 \\(<\\) 右部分木のすべてのノードの値。</li> <li>任意のノードの左と右の部分木も二分探索木です。つまり、条件<code>1.</code>も満たします。</li> </ol> <p></p> <p> 図 7-16 二分探索木 </p>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741","level":2,"title":"7.4.1 二分探索木の操作","text":"<p>二分探索木をクラス<code>BinarySearchTree</code>としてカプセル化し、木の根ノードを指すメンバー変数<code>root</code>を宣言します。</p>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1","level":3,"title":"1. ノードの検索","text":"<p>ターゲットノード値<code>num</code>が与えられた場合、二分探索木の性質に従って検索できます。下図に示すように、ノード<code>cur</code>を宣言し、二分木の根ノード<code>root</code>から開始し、ノード値<code>cur.val</code>と<code>num</code>のサイズを比較するループを行います。</p> <ul> <li><code>cur.val < num</code>の場合、ターゲットノードは<code>cur</code>の右部分木にあることを意味するため、<code>cur = cur.right</code>を実行します。</li> <li><code>cur.val > num</code>の場合、ターゲットノードは<code>cur</code>の左部分木にあることを意味するため、<code>cur = cur.left</code>を実行します。</li> <li><code>cur.val = num</code>の場合、ターゲットノードが見つかったことを意味するため、ループを終了してノードを返します。</li> </ul> <1><2><3><4> <p></p> <p></p> <p></p> <p></p> <p> 図 7-17 二分探索木でのノード検索例 </p> <p>二分探索木での検索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドでケースの半分を排除します。ループ数は最大で二分木の高さです。二分木が平衡している場合、\\(O(\\log n)\\)の時間を使用します。コード例は以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py<pre><code>def search(self, num: int) -> TreeNode | None:\n \"\"\"ノードを探索\"\"\"\n cur = self._root\n # ループで探索、葉ノードを通過した後にブレーク\n while cur is not None:\n # ターゲットノードはcurの右部分木にある\n if cur.val < num:\n cur = cur.right\n # ターゲットノードはcurの左部分木にある\n elif cur.val > num:\n cur = cur.left\n # ターゲットノードを発見、ループをブレーク\n else:\n break\n return cur\n</code></pre> binary_search_tree.cpp<pre><code>/* ノードを検索 */\nTreeNode *search(int num) {\n TreeNode *cur = root;\n // ループで検索、葉ノードを通り過ぎたら終了\n while (cur != nullptr) {\n // 目標ノードはcurの右部分木にある\n if (cur->val < num)\n cur = cur->right;\n // 目標ノードはcurの左部分木にある\n else if (cur->val > num)\n cur = cur->left;\n // 目標ノードを見つけた、ループを抜ける\n else\n break;\n }\n // 目標ノードを返す\n return cur;\n}\n</code></pre> binary_search_tree.java<pre><code>/* ノードを検索 */\nTreeNode search(int num) {\n TreeNode cur = root;\n // ループで検索、葉ノードを通過後に終了\n while (cur != null) {\n // 対象ノードは cur の右部分木にある\n if (cur.val < num)\n cur = cur.right;\n // 対象ノードは cur の左部分木にある\n else if (cur.val > num)\n cur = cur.left;\n // 対象ノードを見つけた、ループを終了\n else\n break;\n }\n // 対象ノードを返す\n return cur;\n}\n</code></pre> binary_search_tree.cs<pre><code>[class]{BinarySearchTree}-[func]{Search}\n</code></pre> binary_search_tree.go<pre><code>[class]{binarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.swift<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.js<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.ts<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.dart<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.rs<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.c<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.kt<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre> binary_search_tree.rb<pre><code>[class]{BinarySearchTree}-[func]{search}\n</code></pre>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2","level":3,"title":"2. ノードの挿入","text":"<p>挿入する要素<code>num</code>が与えられた場合、二分探索木の性質「左部分木 < 根ノード < 右部分木」を維持するため、挿入操作は下図に示すように進行します。</p> <ol> <li>挿入位置を見つける: 検索操作と同様に、根ノードから開始し、現在のノード値と<code>num</code>のサイズ関係に従って下向きにループし、葉ノードを通過(<code>None</code>に走査)するまで、ループを終了します。</li> <li>この位置にノードを挿入: ノード<code>num</code>を初期化し、<code>None</code>があった場所に配置します。</li> </ol> <p></p> <p> 図 7-18 二分探索木へのノード挿入 </p> <p>コード実装では、以下の2点に注意してください。</p> <ul> <li>二分探索木は重複ノードの存在を許可しません。そうでなければ、その定義に違反します。したがって、挿入するノードが既に木に存在する場合、挿入は実行されず、ノードは直接戻ります。</li> <li>挿入操作を実行するには、前のループからのノードを保存するためにノード<code>pre</code>を使用する必要があります。このようにして、<code>None</code>に走査したときに、その親ノードを取得でき、ノード挿入操作を完了できます。</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py<pre><code>def insert(self, num: int):\n \"\"\"ノードを挿入\"\"\"\n # 木が空の場合、ルートノードを初期化\n if self._root is None:\n self._root = TreeNode(num)\n return\n # ループで探索、葉ノードを通過した後にブレーク\n cur, pre = self._root, None\n while cur is not None:\n # 重複ノードを発見したため、戻る\n if cur.val == num:\n return\n pre = cur\n # 挿入位置はcurの右部分木にある\n if cur.val < num:\n cur = cur.right\n # 挿入位置はcurの左部分木にある\n else:\n cur = cur.left\n # ノードを挿入\n node = TreeNode(num)\n if pre.val < num:\n pre.right = node\n else:\n pre.left = node\n</code></pre> binary_search_tree.cpp<pre><code>/* ノードを挿入 */\nvoid insert(int num) {\n // 木が空の場合、ルートノードを初期化\n if (root == nullptr) {\n root = new TreeNode(num);\n return;\n }\n TreeNode *cur = root, *pre = nullptr;\n // ループで検索、葉ノードを通り過ぎたら終了\n while (cur != nullptr) {\n // 重複ノードを見つけた場合、戻る\n if (cur->val == num)\n return;\n pre = cur;\n // 挿入位置はcurの右部分木にある\n if (cur->val < num)\n cur = cur->right;\n // 挿入位置はcurの左部分木にある\n else\n cur = cur->left;\n }\n // ノードを挿入\n TreeNode *node = new TreeNode(num);\n if (pre->val < num)\n pre->right = node;\n else\n pre->left = node;\n}\n</code></pre> binary_search_tree.java<pre><code>/* ノードを挿入 */\nvoid insert(int num) {\n // 木が空の場合、根ノードを初期化\n if (root == null) {\n root = new TreeNode(num);\n return;\n }\n TreeNode cur = root, pre = null;\n // ループで検索、葉ノードを通過後に終了\n while (cur != null) {\n // 重複ノードを見つけた場合、戻る\n if (cur.val == num)\n return;\n pre = cur;\n // 挿入位置は cur の右部分木にある\n if (cur.val < num)\n cur = cur.right;\n // 挿入位置は cur の左部分木にある\n else\n cur = cur.left;\n }\n // ノードを挿入\n TreeNode node = new TreeNode(num);\n if (pre.val < num)\n pre.right = node;\n else\n pre.left = node;\n}\n</code></pre> binary_search_tree.cs<pre><code>[class]{BinarySearchTree}-[func]{Insert}\n</code></pre> binary_search_tree.go<pre><code>[class]{binarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.swift<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.js<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.ts<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.dart<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.rs<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.c<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.kt<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> binary_search_tree.rb<pre><code>[class]{BinarySearchTree}-[func]{insert}\n</code></pre> <p>ノードの検索と同様に、ノードの挿入には\\(O(\\log n)\\)の時間を使用します。</p>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3","level":3,"title":"3. ノードの削除","text":"<p>まず、二分木でターゲットノードを見つけ、それを削除します。ノードの挿入と同様に、削除操作が完了した後も、二分探索木の性質「左部分木 < 根ノード < 右部分木」が満たされることを保証する必要があります。したがって、ターゲットノードの子ノード数に基づいて、0、1、2の3つのケースに分け、対応するノード削除操作を実行します。</p> <p>下図に示すように、削除するノードの次数が\\(0\\)の場合、そのノードは葉ノードであることを意味し、直接削除できます。</p> <p></p> <p> 図 7-19 二分探索木でのノード削除(次数0) </p> <p>下図に示すように、削除するノードの次数が\\(1\\)の場合、削除するノードをその子ノードで置き換えるだけで十分です。</p> <p></p> <p> 図 7-20 二分探索木でのノード削除(次数1) </p> <p>削除するノードの次数が\\(2\\)の場合、直接削除することはできませんが、ノードを使用して置き換える必要があります。二分探索木の性質「左部分木 \\(<\\) 根ノード \\(<\\) 右部分木」を維持するため、このノードは右部分木の最小ノードまたは左部分木の最大ノードのいずれかです。</p> <p>右部分木の最小ノード(中順走査での次のノード)を選択すると仮定すると、削除操作は下図に示すように進行します。</p> <ol> <li>削除するノードの「中順走査シーケンス」での次のノードを見つけ、<code>tmp</code>として示します。</li> <li>削除するノードの値を<code>tmp</code>の値で置き換え、木内でノード<code>tmp</code>を再帰的に削除します。</li> </ol> <1><2><3><4> <p></p> <p></p> <p></p> <p></p> <p> 図 7-21 二分探索木でのノード削除(次数2) </p> <p>ノードを削除する操作も\\(O(\\log n)\\)の時間を使用します。削除するノードを見つけるのに\\(O(\\log n)\\)の時間が必要で、中順走査の後継ノードを取得するのに\\(O(\\log n)\\)の時間が必要です。コード例は以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py<pre><code>def remove(self, num: int):\n \"\"\"ノードを削除\"\"\"\n # 木が空の場合、戻る\n if self._root is None:\n return\n # ループで探索、葉ノードを通過した後にブレーク\n cur, pre = self._root, None\n while cur is not None:\n # 削除するノードを発見、ループをブレーク\n if cur.val == num:\n break\n pre = cur\n # 削除するノードはcurの右部分木にある\n if cur.val < num:\n cur = cur.right\n # 削除するノードはcurの左部分木にある\n else:\n cur = cur.left\n # 削除するノードが存在しない場合、戻る\n if cur is None:\n return\n\n # 子ノード数 = 0 または 1\n if cur.left is None or cur.right is None:\n # 子ノード数 = 0/1の場合、child = null/その子ノード\n child = cur.left or cur.right\n # ノードcurを削除\n if cur != self._root:\n if pre.left == cur:\n pre.left = child\n else:\n pre.right = child\n else:\n # 削除されるノードがルートの場合、ルートを再割り当て\n self._root = child\n # 子ノード数 = 2\n else:\n # curの中順走査の次のノードを取得\n tmp: TreeNode = cur.right\n while tmp.left is not None:\n tmp = tmp.left\n # 再帰的にノードtmpを削除\n self.remove(tmp.val)\n # curをtmpで置き換え\n cur.val = tmp.val\n</code></pre> binary_search_tree.cpp<pre><code>/* ノードを削除 */\nvoid remove(int num) {\n // 木が空の場合、戻る\n if (root == nullptr)\n return;\n TreeNode *cur = root, *pre = nullptr;\n // ループで検索、葉ノードを通り過ぎたら終了\n while (cur != nullptr) {\n // 削除するノードを見つけた、ループを抜ける\n if (cur->val == num)\n break;\n pre = cur;\n // 削除するノードはcurの右部分木にある\n if (cur->val < num)\n cur = cur->right;\n // 削除するノードはcurの左部分木にある\n else\n cur = cur->left;\n }\n // 削除するノードがない場合、戻る\n if (cur == nullptr)\n return;\n // 子ノード数 = 0 または 1\n if (cur->left == nullptr || cur->right == nullptr) {\n // 子ノード数 = 0 / 1の場合、child = nullptr / その子ノード\n TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n // ノードcurを削除\n if (cur != root) {\n if (pre->left == cur)\n pre->left = child;\n else\n pre->right = child;\n } else {\n // 削除されるノードがルートの場合、ルートを再割り当て\n root = child;\n }\n // メモリを解放\n delete cur;\n }\n // 子ノード数 = 2\n else {\n // curの中順走査の次のノードを取得\n TreeNode *tmp = cur->right;\n while (tmp->left != nullptr) {\n tmp = tmp->left;\n }\n int tmpVal = tmp->val;\n // ノードtmpを再帰的に削除\n remove(tmp->val);\n // curをtmpで置き換え\n cur->val = tmpVal;\n }\n}\n</code></pre> binary_search_tree.java<pre><code>/* ノードを削除 */\nvoid remove(int num) {\n // 木が空の場合、戻る\n if (root == null)\n return;\n TreeNode cur = root, pre = null;\n // ループで検索、葉ノードを通過後に終了\n while (cur != null) {\n // 削除するノードを見つけた、ループを終了\n if (cur.val == num)\n break;\n pre = cur;\n // 削除するノードは cur の右部分木にある\n if (cur.val < num)\n cur = cur.right;\n // 削除するノードは cur の左部分木にある\n else\n cur = cur.left;\n }\n // 削除するノードがない場合、戻る\n if (cur == null)\n return;\n // 子ノード数 = 0 または 1\n if (cur.left == null || cur.right == null) {\n // 子ノード数 = 0/1 の場合、child = null/その子ノード\n TreeNode child = cur.left != null ? cur.left : cur.right;\n // ノード cur を削除\n if (cur != root) {\n if (pre.left == cur)\n pre.left = child;\n else\n pre.right = child;\n } else {\n // 削除されるノードが根の場合、根を再割り当て\n root = child;\n }\n }\n // 子ノード数 = 2\n else {\n // cur の中順走査の次のノードを取得\n TreeNode tmp = cur.right;\n while (tmp.left != null) {\n tmp = tmp.left;\n }\n // 再帰的にノード tmp を削除\n remove(tmp.val);\n // cur を tmp で置き換える\n cur.val = tmp.val;\n }\n}\n</code></pre> binary_search_tree.cs<pre><code>[class]{BinarySearchTree}-[func]{Remove}\n</code></pre> binary_search_tree.go<pre><code>[class]{binarySearchTree}-[func]{remove}\n</code></pre> binary_search_tree.swift<pre><code>[class]{BinarySearchTree}-[func]{remove}\n</code></pre> binary_search_tree.js<pre><code>[class]{BinarySearchTree}-[func]{remove}\n</code></pre> binary_search_tree.ts<pre><code>[class]{BinarySearchTree}-[func]{remove}\n</code></pre> binary_search_tree.dart<pre><code>[class]{BinarySearchTree}-[func]{remove}\n</code></pre> binary_search_tree.rs<pre><code>[class]{BinarySearchTree}-[func]{remove}\n</code></pre> binary_search_tree.c<pre><code>[class]{BinarySearchTree}-[func]{removeItem}\n</code></pre> binary_search_tree.kt<pre><code>[class]{BinarySearchTree}-[func]{remove}\n</code></pre> binary_search_tree.rb<pre><code>[class]{BinarySearchTree}-[func]{remove}\n</code></pre>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4","level":3,"title":"4. 中順走査は順序付けされている","text":"<p>下図に示すように、二分木の中順走査は「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」の走査順序に従い、二分探索木は「左子ノード \\(<\\) 根ノード \\(<\\) 右子ノード」のサイズ関係を満たします。</p> <p>これは、二分探索木で中順走査を実行するときに、常に次に小さいノードが最初に走査されることを意味し、重要な性質につながります:二分探索木の中順走査のシーケンスは昇順です。</p> <p>中順走査の昇順性質を使用して、二分探索木で順序付けされたデータを取得するには\\(O(n)\\)の時間のみが必要で、追加のソート操作は不要であり、非常に効率的です。</p> <p></p> <p> 図 7-22 二分探索木の中順走査シーケンス </p>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742","level":2,"title":"7.4.2 二分探索木の効率","text":"<p>データのセットが与えられた場合、配列または二分探索木を使用して格納することを検討します。下の表を観察すると、二分探索木のすべての操作は対数時間計算量を持ち、安定して効率的です。配列は、頻繁な追加と検索や削除の頻度が少ないシナリオでのみ、二分探索木よりも効率的です。</p> <p> 表 7-2 配列と探索木の効率比較 </p> 未ソート配列 二分探索木 要素の検索 \\(O(n)\\) \\(O(\\log n)\\) 要素の挿入 \\(O(1)\\) \\(O(\\log n)\\) 要素の削除 \\(O(n)\\) \\(O(\\log n)\\) <p>理想的には、二分探索木は「平衡」しており、任意のノードを\\(\\log n\\)ループ内で見つけることができます。</p> <p>しかし、二分探索木で継続的にノードを挿入および削除すると、下図に示すように連結リストに退化する可能性があり、さまざまな操作の時間計算量も\\(O(n)\\)に悪化します。</p> <p></p> <p> 図 7-23 二分探索木の退化 </p>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743","level":2,"title":"7.4.3 二分探索木の一般的な応用","text":"<ul> <li>システムでの多レベルインデックスとして使用され、効率的な検索、挿入、削除操作を実装します。</li> <li>特定の検索アルゴリズムの基盤となるデータ構造として機能します。</li> <li>データストリームを格納して、その順序付けされた状態を維持するために使用されます。</li> </ul>","path":["第 7 章 木","7.4 二分探索木"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1 二分木","text":"<p>二分木は、祖先と子孫の間の階層関係を表現し、「二つに分割する」分割統治法の論理を体現する非線形データ構造です。連結リストと同様に、二分木の基本単位はノードであり、各ノードは値、左の子ノードへの参照、右の子ノードへの参照を含みます。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>class TreeNode:\n \"\"\"二分木ノード\"\"\"\n def __init__(self, val: int):\n self.val: int = val # ノード値\n self.left: TreeNode | None = None # 左の子ノードへの参照\n self.right: TreeNode | None = None # 右の子ノードへの参照\n</code></pre> <pre><code>/* 二分木ノード */\nstruct TreeNode {\n int val; // ノード値\n TreeNode *left; // 左の子ノードへのポインタ\n TreeNode *right; // 右の子ノードへのポインタ\n TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n</code></pre> <pre><code>/* 二分木ノード */\nclass TreeNode {\n int val; // ノード値\n TreeNode left; // 左の子ノードへの参照\n TreeNode right; // 右の子ノードへの参照\n TreeNode(int x) { val = x; }\n}\n</code></pre> <pre><code>/* 二分木ノード */\nclass TreeNode(int? x) {\n public int? val = x; // ノード値\n public TreeNode? left; // 左の子ノードへの参照\n public TreeNode? right; // 右の子ノードへの参照\n}\n</code></pre> <pre><code>/* 二分木ノード */\ntype TreeNode struct {\n Val int\n Left *TreeNode\n Right *TreeNode\n}\n/* コンストラクタ */\nfunc NewTreeNode(v int) *TreeNode {\n return &TreeNode{\n Left: nil, // 左の子ノードへのポインタ\n Right: nil, // 右の子ノードへのポインタ\n Val: v, // ノード値\n }\n}\n</code></pre> <pre><code>/* 二分木ノード */\nclass TreeNode {\n var val: Int // ノード値\n var left: TreeNode? // 左の子ノードへの参照\n var right: TreeNode? // 右の子ノードへの参照\n\n init(x: Int) {\n val = x\n }\n}\n</code></pre> <pre><code>/* 二分木ノード */\nclass TreeNode {\n val; // ノード値\n left; // 左の子ノードへのポインタ\n right; // 右の子ノードへのポインタ\n constructor(val, left, right) {\n this.val = val === undefined ? 0 : val;\n this.left = left === undefined ? null : left;\n this.right = right === undefined ? null : right;\n }\n}\n</code></pre> <pre><code>/* 二分木ノード */\nclass TreeNode {\n val: number;\n left: TreeNode | null;\n right: TreeNode | null;\n\n constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n this.val = val === undefined ? 0 : val; // ノード値\n this.left = left === undefined ? null : left; // 左の子ノードへの参照\n this.right = right === undefined ? null : right; // 右の子ノードへの参照\n }\n}\n</code></pre> <pre><code>/* 二分木ノード */\nclass TreeNode {\n int val; // ノード値\n TreeNode? left; // 左の子ノードへの参照\n TreeNode? right; // 右の子ノードへの参照\n TreeNode(this.val, [this.left, this.right]);\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 二分木ノード */\nstruct TreeNode {\n val: i32, // ノード値\n left: Option<Rc<RefCell<TreeNode>>>, // 左の子ノードへの参照\n right: Option<Rc<RefCell<TreeNode>>>, // 右の子ノードへの参照\n}\n\nimpl TreeNode {\n /* コンストラクタ */\n fn new(val: i32) -> Rc<RefCell<Self>> {\n Rc::new(RefCell::new(Self {\n val,\n left: None,\n right: None\n }))\n }\n}\n</code></pre> <pre><code>/* 二分木ノード */\ntypedef struct TreeNode {\n int val; // ノード値\n int height; // ノードの高さ\n struct TreeNode *left; // 左の子ノードへのポインタ\n struct TreeNode *right; // 右の子ノードへのポインタ\n} TreeNode;\n\n/* コンストラクタ */\nTreeNode *newTreeNode(int val) {\n TreeNode *node;\n\n node = (TreeNode *)malloc(sizeof(TreeNode));\n node->val = val;\n node->height = 0;\n node->left = NULL;\n node->right = NULL;\n return node;\n}\n</code></pre> <pre><code>/* 二分木ノード */\nclass TreeNode(val _val: Int) { // ノード値\n val left: TreeNode? = null // 左の子ノードへの参照\n val right: TreeNode? = null // 右の子ノードへの参照\n}\n</code></pre> <pre><code>\n</code></pre> <p>各ノードは2つの参照(ポインタ)を持ち、それぞれ左の子ノードと右の子ノードを指しています。このノードは、これら2つの子ノードの親ノードと呼ばれます。二分木のノードが与えられたとき、このノードの左の子とその下にあるすべてのノードで形成される木を、このノードの左部分木と呼びます。同様に、右部分木も定義できます。</p> <p>二分木では、葉ノードを除いて、他のすべてのノードは子ノードと空でない部分木を含みます。 下図に示すように、「ノード2」を親ノードとして見ると、その左と右の子ノードはそれぞれ「ノード4」と「ノード5」です。左部分木は「ノード4」とその下にあるすべてのノードで形成され、右部分木は「ノード5」とその下にあるすべてのノードで形成されます。</p> <p></p> <p> 図 7-1 親ノード、子ノード、部分木 </p>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#711","level":2,"title":"7.1.1 二分木の一般的な用語","text":"<p>二分木でよく使用される用語を下図に示します。</p> <ul> <li>根ノード:二分木の最上位レベルにあるノードで、親ノードを持ちません。</li> <li>葉ノード:子ノードを持たないノードで、両方のポインタが<code>None</code>を指しています。</li> <li>辺:2つのノードを結ぶ線分で、ノード間の参照(ポインタ)を表現します。</li> <li>ノードのレベル:上から下に向かって増加し、根ノードがレベル1です。</li> <li>ノードの次数:ノードが持つ子ノードの数です。二分木では、次数は0、1、または2になります。</li> <li>二分木の高さ:根ノードから最も遠い葉ノードまでの辺の数です。</li> <li>ノードの深さ:根ノードからそのノードまでの辺の数です。</li> <li>ノードの高さ:最も遠い葉ノードからそのノードまでの辺の数です。</li> </ul> <p></p> <p> 図 7-2 二分木の一般的な用語 </p> <p>Tip</p> <p>「高さ」と「深さ」は通常「通過する辺の数」として定義しますが、一部の問題や教科書では「通過するノードの数」として定義されることがあります。この場合、高さと深さの両方を1だけ増やす必要があります。</p>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#712","level":2,"title":"7.1.2 二分木の基本操作","text":"","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#1","level":3,"title":"1. 二分木の初期化","text":"<p>連結リストと同様に、二分木の初期化では、まずノードを作成し、次にそれらの間の参照(ポインタ)を確立します。</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py<pre><code># 二分木の初期化\n# ノードの初期化\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# ノード間の参照(ポインタ)を結ぶ\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n</code></pre> binary_tree.cpp<pre><code>/* 二分木の初期化 */\n// ノードの初期化\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n</code></pre> binary_tree.java<pre><code>// ノードの初期化\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.cs<pre><code>/* 二分木の初期化 */\n// ノードの初期化\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.go<pre><code>/* 二分木の初期化 */\n// ノードの初期化\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// ノード間の参照(ポインタ)を結ぶ\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n</code></pre> binary_tree.swift<pre><code>// ノードの初期化\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// ノード間の参照(ポインタ)を結ぶ\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n</code></pre> binary_tree.js<pre><code>/* 二分木の初期化 */\n// ノードの初期化\nlet n1 = new TreeNode(1),\n n2 = new TreeNode(2),\n n3 = new TreeNode(3),\n n4 = new TreeNode(4),\n n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.ts<pre><code>/* 二分木の初期化 */\n// ノードの初期化\nlet n1 = new TreeNode(1),\n n2 = new TreeNode(2),\n n3 = new TreeNode(3),\n n4 = new TreeNode(4),\n n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.dart<pre><code>/* 二分木の初期化 */\n// ノードの初期化\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.rs<pre><code>// ノードの初期化\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n</code></pre> binary_tree.c<pre><code>/* 二分木の初期化 */\n// ノードの初期化\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// ノード間の参照(ポインタ)を結ぶ\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n</code></pre> binary_tree.kt<pre><code>// ノードの初期化\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// ノード間の参照(ポインタ)を結ぶ\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n</code></pre> binary_tree.rb<pre><code>\n</code></pre>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#2","level":3,"title":"2. ノードの挿入と削除","text":"<p>連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。下図に例を示します。</p> <p></p> <p> 図 7-3 二分木でのノードの挿入と削除 </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py<pre><code># ノードの挿入と削除\np = TreeNode(0)\n# n1 -> n2の間にノードPを挿入\nn1.left = p\np.left = n2\n# ノードPを削除\nn1.left = n2\n</code></pre> binary_tree.cpp<pre><code>/* ノードの挿入と削除 */\nTreeNode* P = new TreeNode(0);\n// n1とn2の間にノードPを挿入\nn1->left = P;\nP->left = n2;\n// ノードPを削除\nn1->left = n2;\n</code></pre> binary_tree.java<pre><code>TreeNode P = new TreeNode(0);\n// n1とn2の間にノードPを挿入\nn1.left = P;\nP.left = n2;\n// ノードPを削除\nn1.left = n2;\n</code></pre> binary_tree.cs<pre><code>/* ノードの挿入と削除 */\nTreeNode P = new(0);\n// n1とn2の間にノードPを挿入\nn1.left = P;\nP.left = n2;\n// ノードPを削除\nn1.left = n2;\n</code></pre> binary_tree.go<pre><code>/* ノードの挿入と削除 */\n// n1とn2の間にノードPを挿入\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// ノードPを削除\nn1.Left = n2\n</code></pre> binary_tree.swift<pre><code>let P = TreeNode(x: 0)\n// n1とn2の間にノードPを挿入\nn1.left = P\nP.left = n2\n// ノードPを削除\nn1.left = n2\n</code></pre> binary_tree.js<pre><code>/* ノードの挿入と削除 */\nlet P = new TreeNode(0);\n// n1とn2の間にノードPを挿入\nn1.left = P;\nP.left = n2;\n// ノードPを削除\nn1.left = n2;\n</code></pre> binary_tree.ts<pre><code>/* ノードの挿入と削除 */\nconst P = new TreeNode(0);\n// n1とn2の間にノードPを挿入\nn1.left = P;\nP.left = n2;\n// ノードPを削除\nn1.left = n2;\n</code></pre> binary_tree.dart<pre><code>/* ノードの挿入と削除 */\nTreeNode P = new TreeNode(0);\n// n1とn2の間にノードPを挿入\nn1.left = P;\nP.left = n2;\n// ノードPを削除\nn1.left = n2;\n</code></pre> binary_tree.rs<pre><code>let p = TreeNode::new(0);\n// n1とn2の間にノードPを挿入\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// ノードPを削除\nn1.borrow_mut().left = Some(n2);\n</code></pre> binary_tree.c<pre><code>/* ノードの挿入と削除 */\nTreeNode *P = newTreeNode(0);\n// n1とn2の間にノードPを挿入\nn1->left = P;\nP->left = n2;\n// ノードPを削除\nn1->left = n2;\n</code></pre> binary_tree.kt<pre><code>val P = TreeNode(0)\n// n1とn2の間にノードPを挿入\nn1.left = P\nP.left = n2\n// ノードPを削除\nn1.left = n2\n</code></pre> binary_tree.rb<pre><code>\n</code></pre> <p>Tip</p> <p>ノードの挿入は二分木の元の論理構造を変更する可能性があり、ノードの削除は通常そのノードとそのすべての部分木を削除することになることに注意してください。したがって、二分木では、挿入と削除は通常一連の操作を通じて実行され、意味のある結果を得ます。</p>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#713","level":2,"title":"7.1.3 二分木の一般的な種類","text":"","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#1_1","level":3,"title":"1. 完全二分木","text":"<p>下図に示すように、完全二分木では、すべてのレベルがノードで完全に埋められています。完全二分木では、葉ノードの次数は\\(0\\)で、他のすべてのノードの次数は\\(2\\)です。ノードの総数は\\(2^{h+1} - 1\\)として計算でき、ここで\\(h\\)は木の高さです。これは標準的な指数関係を示し、自然界の細胞分裂の一般的な現象を反映しています。</p> <p>Tip</p> <p>中国語圏では、完全二分木はしばしば満二分木と呼ばれることに注意してください。</p> <p></p> <p> 図 7-4 完全二分木 </p>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#2_1","level":3,"title":"2. 完備二分木","text":"<p>下図に示すように、完備二分木は、最下位レベルのみが完全に埋められていない可能性がある二分木で、最下位レベルのノードは左から右に連続して埋められる必要があります。完全二分木は完備二分木でもあることに注意してください。</p> <p></p> <p> 図 7-5 完備二分木 </p>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3. 満二分木","text":"<p>下図に示すように、満二分木では、葉ノードを除いて、他のすべてのノードが2つの子ノードを持ちます。</p> <p></p> <p> 図 7-6 満二分木 </p>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4. 平衡二分木","text":"<p>下図に示すように、平衡二分木では、任意のノードの左と右の部分木の高さの絶対差が1を超えません。</p> <p></p> <p> 図 7-7 平衡二分木 </p>","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4 二分木の退化","text":"<p>下図は、二分木の理想的な構造と退化した構造を示しています。二分木は、すべてのレベルが埋められているときに「完全二分木」になり、すべてのノードが一方に偏っているときに「連結リスト」に退化します。</p> <ul> <li>完全二分木は、二分木の「分割統治法」の利点を十分に活用できる理想的なシナリオです。</li> <li>一方、連結リストは別の極端を表し、すべての操作が線形になり、時間計算量が\\(O(n)\\)になります。</li> </ul> <p></p> <p> 図 7-8 二分木の最良と最悪の構造 </p> <p>下表に示すように、最良と最悪の構造では、二分木は葉ノード数、総ノード数、高さの最大値または最小値を達成します。</p> <p> 表 7-1 二分木の最良と最悪の構造 </p> 完全二分木 連結リスト レベル\\(i\\)のノード数 \\(2^{i-1}\\) \\(1\\) 高さ\\(h\\)の木の葉ノード数 \\(2^h\\) \\(1\\) 高さ\\(h\\)の木の総ノード数 \\(2^{h+1} - 1\\) \\(h + 1\\) 総ノード数\\(n\\)の木の高さ \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["第 7 章 木","7.1 二分木"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2 二分木の走査","text":"<p>物理的構造の観点から見ると、木は連結リストに基づくデータ構造です。したがって、その走査方法はポインタを通してノードに一つずつアクセスすることを含みます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑で、検索アルゴリズムの支援が必要です。</p> <p>二分木の一般的な走査方法には、レベル順走査、前順走査、中順走査、後順走査があります。</p>","path":["第 7 章 木","7.2 二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721","level":2,"title":"7.2.1 レベル順走査","text":"<p>下図に示すように、レベル順走査は二分木を上から下へ、層ごとに走査します。各レベル内では、左から右へノードを訪問します。</p> <p>レベル順走査は本質的に幅優先走査の一種で、幅優先探索(BFS)とも呼ばれ、「周囲に向かって外向きに拡張する」層ごとの走査方法を体現しています。</p> <p></p> <p> 図 7-9 二分木のレベル順走査 </p>","path":["第 7 章 木","7.2 二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1","level":3,"title":"1. コード実装","text":"<p>幅優先走査は通常「キュー」の助けを借りて実装されます。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとの進行」規則に従います。両者の基本的な考え方は一致しています。実装コードは以下の通りです:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py<pre><code>def level_order(root: TreeNode | None) -> list[int]:\n \"\"\"レベル順走査\"\"\"\n # キューを初期化し、ルートノードを追加\n queue: deque[TreeNode] = deque()\n queue.append(root)\n # 走査シーケンスを格納するリストを初期化\n res = []\n while queue:\n node: TreeNode = queue.popleft() # キューからデキュー\n res.append(node.val) # ノードの値を保存\n if node.left is not None:\n queue.append(node.left) # 左の子ノードをエンキュー\n if node.right is not None:\n queue.append(node.right) # 右の子ノードをエンキュー\n return res\n</code></pre> binary_tree_bfs.cpp<pre><code>/* レベル順走査 */\nvector<int> levelOrder(TreeNode *root) {\n // キューを初期化、ルートノードを追加\n queue<TreeNode *> queue;\n queue.push(root);\n // 走査順序を保存するリストを初期化\n vector<int> vec;\n while (!queue.empty()) {\n TreeNode *node = queue.front();\n queue.pop(); // キューからデキュー\n vec.push_back(node->val); // ノード値を保存\n if (node->left != nullptr)\n queue.push(node->left); // 左の子ノードをエンキュー\n if (node->right != nullptr)\n queue.push(node->right); // 右の子ノードをエンキュー\n }\n return vec;\n}\n</code></pre> binary_tree_bfs.java<pre><code>/* レベル順走査 */\nList<Integer> levelOrder(TreeNode root) {\n // キューを初期化し、根ノードを追加\n Queue<TreeNode> queue = new LinkedList<>();\n queue.add(root);\n // 走査順序を格納するリストを初期化\n List<Integer> list = new ArrayList<>();\n while (!queue.isEmpty()) {\n TreeNode node = queue.poll(); // キューのデキュー\n list.add(node.val); // ノードの値を保存\n if (node.left != null)\n queue.offer(node.left); // 左の子ノードをエンキュー\n if (node.right != null)\n queue.offer(node.right); // 右の子ノードをエンキュー\n }\n return list;\n}\n</code></pre> binary_tree_bfs.cs<pre><code>[class]{binary_tree_bfs}-[func]{LevelOrder}\n</code></pre> binary_tree_bfs.go<pre><code>[class]{}-[func]{levelOrder}\n</code></pre> binary_tree_bfs.swift<pre><code>[class]{}-[func]{levelOrder}\n</code></pre> binary_tree_bfs.js<pre><code>[class]{}-[func]{levelOrder}\n</code></pre> binary_tree_bfs.ts<pre><code>[class]{}-[func]{levelOrder}\n</code></pre> binary_tree_bfs.dart<pre><code>[class]{}-[func]{levelOrder}\n</code></pre> binary_tree_bfs.rs<pre><code>[class]{}-[func]{level_order}\n</code></pre> binary_tree_bfs.c<pre><code>[class]{}-[func]{levelOrder}\n</code></pre> binary_tree_bfs.kt<pre><code>[class]{}-[func]{levelOrder}\n</code></pre> binary_tree_bfs.rb<pre><code>[class]{}-[func]{level_order}\n</code></pre>","path":["第 7 章 木","7.2 二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2","level":3,"title":"2. 計算量分析","text":"<ul> <li>時間計算量は\\(O(n)\\): すべてのノードが一度ずつ訪問され、\\(O(n)\\)の時間がかかります。ここで\\(n\\)はノード数です。</li> <li>空間計算量は\\(O(n)\\): 最悪の場合、つまり完全二分木の場合、最下位レベルに走査する前に、キューは最大\\((n + 1) / 2\\)個のノードを同時に含むことができ、\\(O(n)\\)の空間を占有します。</li> </ul>","path":["第 7 章 木","7.2 二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722","level":2,"title":"7.2.2 前順、中順、後順走査","text":"<p>対応して、前順、中順、後順走査はすべて深度優先走査に属し、深度優先探索(DFS)とも呼ばれ、「まず最後まで進み、その後バックトラックして続行する」走査方法を体現しています。</p> <p>下図は二分木に対して深度優先走査を実行する動作原理を示しています。深度優先走査は二分木全体を「歩き回る」ようなもので、各ノードで3つの位置に遭遇し、それらは前順、中順、後順走査に対応しています。</p> <p></p> <p> 図 7-10 二分探索木の前順、中順、後順走査 </p>","path":["第 7 章 木","7.2 二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1_1","level":3,"title":"1. コード実装","text":"<p>深度優先探索は通常再帰に基づいて実装されます:</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py<pre><code>def pre_order(root: TreeNode | None):\n \"\"\"前順走査\"\"\"\n if root is None:\n return\n # 訪問順序: ルートノード -> 左部分木 -> 右部分木\n res.append(root.val)\n pre_order(root=root.left)\n pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n \"\"\"中順走査\"\"\"\n if root is None:\n return\n # 訪問順序: 左部分木 -> ルートノード -> 右部分木\n in_order(root=root.left)\n res.append(root.val)\n in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n \"\"\"後順走査\"\"\"\n if root is None:\n return\n # 訪問順序: 左部分木 -> 右部分木 -> ルートノード\n post_order(root=root.left)\n post_order(root=root.right)\n res.append(root.val)\n</code></pre> binary_tree_dfs.cpp<pre><code>/* 前順走査 */\nvoid preOrder(TreeNode *root) {\n if (root == nullptr)\n return;\n // 訪問優先度:ルートノード -> 左部分木 -> 右部分木\n vec.push_back(root->val);\n preOrder(root->left);\n preOrder(root->right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode *root) {\n if (root == nullptr)\n return;\n // 訪問優先度:左部分木 -> ルートノード -> 右部分木\n inOrder(root->left);\n vec.push_back(root->val);\n inOrder(root->right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode *root) {\n if (root == nullptr)\n return;\n // 訪問優先度:左部分木 -> 右部分木 -> ルートノード\n postOrder(root->left);\n postOrder(root->right);\n vec.push_back(root->val);\n}\n</code></pre> binary_tree_dfs.java<pre><code>/* 前順走査 */\nvoid preOrder(TreeNode root) {\n if (root == null)\n return;\n // 訪問優先度: 根ノード -> 左部分木 -> 右部分木\n list.add(root.val);\n preOrder(root.left);\n preOrder(root.right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode root) {\n if (root == null)\n return;\n // 訪問優先度: 左部分木 -> 根ノード -> 右部分木\n inOrder(root.left);\n list.add(root.val);\n inOrder(root.right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode root) {\n if (root == null)\n return;\n // 訪問優先度: 左部分木 -> 右部分木 -> 根ノード\n postOrder(root.left);\n postOrder(root.right);\n list.add(root.val);\n}\n</code></pre> binary_tree_dfs.cs<pre><code>[class]{binary_tree_dfs}-[func]{PreOrder}\n\n[class]{binary_tree_dfs}-[func]{InOrder}\n\n[class]{binary_tree_dfs}-[func]{PostOrder}\n</code></pre> binary_tree_dfs.go<pre><code>[class]{}-[func]{preOrder}\n\n[class]{}-[func]{inOrder}\n\n[class]{}-[func]{postOrder}\n</code></pre> binary_tree_dfs.swift<pre><code>[class]{}-[func]{preOrder}\n\n[class]{}-[func]{inOrder}\n\n[class]{}-[func]{postOrder}\n</code></pre> binary_tree_dfs.js<pre><code>[class]{}-[func]{preOrder}\n\n[class]{}-[func]{inOrder}\n\n[class]{}-[func]{postOrder}\n</code></pre> binary_tree_dfs.ts<pre><code>[class]{}-[func]{preOrder}\n\n[class]{}-[func]{inOrder}\n\n[class]{}-[func]{postOrder}\n</code></pre> binary_tree_dfs.dart<pre><code>[class]{}-[func]{preOrder}\n\n[class]{}-[func]{inOrder}\n\n[class]{}-[func]{postOrder}\n</code></pre> binary_tree_dfs.rs<pre><code>[class]{}-[func]{pre_order}\n\n[class]{}-[func]{in_order}\n\n[class]{}-[func]{post_order}\n</code></pre> binary_tree_dfs.c<pre><code>[class]{}-[func]{preOrder}\n\n[class]{}-[func]{inOrder}\n\n[class]{}-[func]{postOrder}\n</code></pre> binary_tree_dfs.kt<pre><code>[class]{}-[func]{preOrder}\n\n[class]{}-[func]{inOrder}\n\n[class]{}-[func]{postOrder}\n</code></pre> binary_tree_dfs.rb<pre><code>[class]{}-[func]{pre_order}\n\n[class]{}-[func]{in_order}\n\n[class]{}-[func]{post_order}\n</code></pre> <p>Tip</p> <p>深度優先探索は反復に基づいても実装できます。興味のある読者は自分で学習してください。</p> <p>下図は二分木の前順走査の再帰プロセスを示しており、これは「再帰」と「復帰」という2つの反対の部分に分けることができます。</p> <ol> <li>「再帰」は新しいメソッドを開始することを意味し、プログラムはこのプロセスで次のノードにアクセスします。</li> <li>「復帰」は関数が戻ることを意味し、現在のノードが完全にアクセスされたことを示します。</li> </ol> <1><2><3><4><5><6><7><8><9><10><11> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 図 7-11 前順走査の再帰プロセス </p>","path":["第 7 章 木","7.2 二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2_1","level":3,"title":"2. 計算量分析","text":"<ul> <li>時間計算量は\\(O(n)\\): すべてのノードが一度ずつ訪問され、\\(O(n)\\)の時間を使用します。</li> <li>空間計算量は\\(O(n)\\): 最悪の場合、つまり木が連結リストに退化した場合、再帰の深さは\\(n\\)に達し、システムは\\(O(n)\\)のスタックフレーム空間を占有します。</li> </ul>","path":["第 7 章 木","7.2 二分木の走査"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6 まとめ","text":"","path":["第 7 章 木","7.6 まとめ"],"tags":[]},{"location":"chapter_tree/summary/#1","level":3,"title":"1. 重要なポイント","text":"<ul> <li>二分木は非線形データ構造で、「一つを二つに分ける」分割統治のロジックを反映しています。各二分木ノードには値と2つのポインタが含まれ、それぞれ左と右の子ノードを指します。</li> <li>二分木のノードについて、その左(右)子ノードとその下に形成される木は、まとめてそのノードの左(右)部分木と呼ばれます。</li> <li>二分木に関連する用語には、根ノード、葉ノード、レベル、次数、エッジ、高さ、深さがあります。</li> <li>二分木の初期化、ノードの挿入、ノードの削除の操作は、連結リストの操作と似ています。</li> <li>一般的な二分木の種類には、完全二分木、完備二分木、満二分木、平衡二分木があります。完全二分木は理想的な状態を表し、連結リストは退化後の最悪の状態です。</li> <li>二分木は、ノード値と空きスロットをレベル順走査シーケンスで配置し、親ノードと子ノード間のインデックスマッピング関係に基づいてポインタを実装することで、配列を使用して表現できます。</li> <li>二分木のレベル順走査は幅優先探索手法で、「円を拡大しながら」の層ごとの走査方式を反映しています。通常はキューを使用して実装されます。</li> <li>前順、中順、後順走査はすべて深度優先探索手法で、「まず最後まで行き、その後バックトラックして続行する」走査方式を反映しています。通常は再帰を使用して実装されます。</li> <li>二分探索木は要素検索のための効率的なデータ構造で、検索、挿入、削除操作の時間計算量はすべて\\(O(\\log n)\\)です。二分探索木が連結リストに退化すると、これらの時間計算量は\\(O(n)\\)に悪化します。</li> <li>AVL木は平衡二分探索木とも呼ばれ、回転操作を通して継続的なノード挿入と削除後も木が平衡を保つことを保証します。</li> <li>AVL木の回転操作には、右回転、左回転、右左回転、左右回転があります。ノードの挿入または削除後、AVL木はボトムアップ方式でこれらの回転を実行して自己平衡を取ります。</li> </ul>","path":["第 7 章 木","7.6 まとめ"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2. Q & A","text":"<p>Q: 一つのノードのみを持つ二分木について、木の高さと根ノードの深さの両方が\\(0\\)ですか?</p> <p>はい、高さと深さは通常「通過したエッジの数」として定義されるためです。</p> <p>Q: 二分木における挿入と削除は一般的に一連の操作によって達成されます。ここでの「一連の操作」とは何を指しますか?子ノードのリソースを解放することを意味しますか?</p> <p>二分探索木を例に取ると、ノードを削除する操作は3つの異なるシナリオで処理する必要があり、それぞれ複数ステップのノード操作が必要です。</p> <p>Q: 二分木のDFS走査で前順、中順、後順の3つのシーケンスがあるのはなぜですか?その用途は何ですか?</p> <p>配列の順次および逆順走査と同様に、前順、中順、後順走査は二分木を走査する3つの方法であり、特定の順序で走査結果を取得できます。例えば、二分探索木では、ノードサイズが「左子ノード値 < 根ノード値 < 右子ノード値」を満たすため、「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」の優先順位で木を走査することで、順序付けられたノードシーケンスを取得できます。</p> <p>Q: 不平衡ノード<code>node</code>、<code>child</code>、<code>grand_child</code>間の関係を処理する右回転操作において、右回転後に<code>node</code>とその親ノード間の接続と<code>node</code>の元のリンクが失われるのではありませんか?</p> <p>この問題を再帰的な観点から見る必要があります。<code>right_rotate(root)</code>操作は部分木の根ノードを渡し、最終的に<code>return child</code>で回転された部分木の根ノードを返します。部分木の根ノードとその親ノード間の接続は、この関数が戻った後に確立され、これは右回転操作の保守範囲外です。</p> <p>Q: C++では、関数は<code>private</code>と<code>public</code>セクションに分かれています。これにはどのような考慮事項がありますか?なぜ<code>height()</code>関数と<code>updateHeight()</code>関数がそれぞれ<code>public</code>と<code>private</code>に配置されているのですか?</p> <p>これはメソッドの使用範囲によります。メソッドがクラス内でのみ使用される場合、<code>private</code>に設計されます。例えば、ユーザーが独自に<code>updateHeight()</code>を呼び出すことは意味がありません。これは挿入または削除操作の一ステップに過ぎないからです。しかし、<code>height()</code>はノードの高さにアクセスするためのもので、<code>vector.size()</code>と同様であるため、使用のために<code>public</code>に設定されています。</p> <p>Q: 入力データのセットから二分探索木をどのように構築しますか?根ノードの選択は非常に重要ですか?</p> <p>はい、木を構築する方法は二分探索木コードの<code>build_tree()</code>メソッドで提供されています。根ノードの選択については、通常入力データをソートし、中央の要素を根ノードとして選択し、再帰的に左と右の部分木を構築します。このアプローチは木の平衡を最大化します。</p> <p>Q: Javaでは、文字列比較に常に<code>equals()</code>メソッドを使用する必要がありますか?</p> <p>Javaでは、プリミティブデータ型の場合、<code>==</code>は2つの変数の値が等しいかどうかを比較するために使用されます。参照型の場合、2つのシンボルの動作原理は異なります。</p> <ul> <li><code>==</code>: 2つの変数が同じオブジェクトを指しているかどうか、つまりメモリ内の位置が同じかどうかを比較するために使用されます。</li> <li><code>equals()</code>: 2つのオブジェクトの値が等しいかどうかを比較するために使用されます。</li> </ul> <p>したがって、値を比較するには<code>equals()</code>を使用すべきです。ただし、<code>String a = \"hi\"; String b = \"hi\";</code>で初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、<code>a == b</code>も2つの文字列の内容を比較するために使用できます。</p> <p>Q: 最下位レベルに到達する前に、幅優先走査でキュー内のノード数は\\(2^h\\)ですか?</p> <p>はい、例えば高さ\\(h = 2\\)の満二分木は合計\\(n = 7\\)個のノードを持ち、最下位レベルには\\(4 = 2^h = (n + 1) / 2\\)個のノードがあります。</p>","path":["第 7 章 木","7.6 まとめ"],"tags":[]}]} |