This commit is contained in:
krahets
2024-04-06 03:02:20 +08:00
parent 0a9daa8b9f
commit 8d37c215c8
148 changed files with 70398 additions and 408 deletions
@@ -0,0 +1,53 @@
---
comments: true
---
# 16.2   一起參與創作
由於筆者能力有限,書中難免存在一些遺漏和錯誤,請您諒解。如果您發現了筆誤、連結失效、內容缺失、文字歧義、解釋不清晰或行文結構不合理等問題,請協助我們進行修正,以給讀者提供更優質的學習資源。
所有[撰稿人](https://github.com/krahets/hello-algo/graphs/contributors)的 GitHub ID 將在本書倉庫、網頁版和 PDF 版的主頁上進行展示,以感謝他們對開源社群的無私奉獻。
!!! success "開源的魅力"
紙質圖書的兩次印刷的間隔時間往往較久,內容更新非常不方便。
而在本開源書中,內容更迭的時間被縮短至數日甚至幾個小時。
### 1.   內容微調
如圖 16-3 所示,每個頁面的右上角都有“編輯圖示”。您可以按照以下步驟修改文字或程式碼。
1. 點選“編輯圖示”,如果遇到“需要 Fork 此倉庫”的提示,請同意該操作。
2. 修改 Markdown 源檔案內容,檢查內容的正確性,並儘量保持排版格式的統一。
3. 在頁面底部填寫修改說明,然後點選“Propose file change”按鈕。頁面跳轉後,點選“Create pull request”按鈕即可發起拉取請求。
![頁面編輯按鍵](contribution.assets/edit_markdown.png){ class="animation-figure" }
<p align="center"> 圖 16-3 &nbsp; 頁面編輯按鍵 </p>
圖片無法直接修改,需要透過新建 [Issue](https://github.com/krahets/hello-algo/issues) 或評論留言來描述問題,我們會盡快重新繪製並替換圖片。
### 2. &nbsp; 內容創作
如果您有興趣參與此開源專案,包括將程式碼翻譯成其他程式語言、擴展文章內容等,那麼需要實施以下 Pull Request 工作流程。
1. 登入 GitHub ,將本書的[程式碼倉庫](https://github.com/krahets/hello-algo) Fork 到個人帳號下。
2. 進入您的 Fork 倉庫網頁,使用 `git clone` 命令將倉庫克隆至本地。
3. 在本地進行內容創作,並進行完整測試,驗證程式碼的正確性。
4. 將本地所做更改 Commit ,然後 Push 至遠端倉庫。
5. 重新整理倉庫網頁,點選“Create pull request”按鈕即可發起拉取請求。
### 3. &nbsp; Docker 部署
`hello-algo` 根目錄下,執行以下 Docker 指令碼,即可在 `http://localhost:8000` 訪問本專案:
```shell
docker-compose up -d
```
使用以下命令即可刪除部署:
```shell
docker-compose down
```
+14
View File
@@ -0,0 +1,14 @@
---
comments: true
icon: material/help-circle-outline
---
# 第 16 章 &nbsp; 附錄
![附錄](../assets/covers/chapter_appendix.jpg){ class="cover-image" }
## Chapter Contents
- [16.1 &nbsp; 程式設計環境安裝](https://www.hello-algo.com/en/chapter_appendix/installation/)
- [16.2 &nbsp; 一起參與創作](https://www.hello-algo.com/en/chapter_appendix/contribution/)
- [16.3 &nbsp; 術語表](https://www.hello-algo.com/en/chapter_appendix/terminology/)
@@ -0,0 +1,71 @@
---
comments: true
---
# 16.1 &nbsp; 程式設計環境安裝
## 16.1.1 &nbsp; 安裝 IDE
推薦使用開源、輕量的 VS Code 作為本地整合開發環境(IDE)。訪問 [VS Code 官網](https://code.visualstudio.com/),根據作業系統選擇相應版本的 VS Code 進行下載和安裝。
![從官網下載 VS Code](installation.assets/vscode_installation.png){ class="animation-figure" }
<p align="center"> 圖 16-1 &nbsp; 從官網下載 VS Code </p>
VS Code 擁有強大的擴展包生態系統,支持大多數程式語言的執行和除錯。以 Python 為例,安裝“Python Extension Pack”擴展包之後,即可進行 Python 程式碼除錯。安裝步驟如圖 16-2 所示。
![安裝 VS Code 擴展包](installation.assets/vscode_extension_installation.png){ class="animation-figure" }
<p align="center"> 圖 16-2 &nbsp; 安裝 VS Code 擴展包 </p>
## 16.1.2 &nbsp; 安裝語言環境
### 1. &nbsp; Python 環境
1. 下載並安裝 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) ,需要 Python 3.10 或更新版本。
2. 在 VS Code 的擴充功能市場中搜索 `python` ,安裝 Python Extension Pack 。
3. (可選)在命令列輸入 `pip install black` ,安裝程式碼格式化工具。
### 2. &nbsp; C/C++ 環境
1. Windows 系統需要安裝 [MinGW](https://sourceforge.net/projects/mingw-w64/files/)[配置教程](https://blog.csdn.net/qq_33698226/article/details/129031241));MacOS 自帶 Clang ,無須安裝。
2. 在 VS Code 的擴充功能市場中搜索 `c++` ,安裝 C/C++ Extension Pack 。
3. (可選)開啟 Settings 頁面,搜尋 `Clang_format_fallback Style` 程式碼格式化選項,設定為 `{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }`
### 3. &nbsp; Java 環境
1. 下載並安裝 [OpenJDK](https://jdk.java.net/18/)(版本需滿足 > JDK 9)。
2. 在 VS Code 的擴充功能市場中搜索 `java` ,安裝 Extension Pack for Java 。
### 4. &nbsp; C# 環境
1. 下載並安裝 [.Net 8.0](https://dotnet.microsoft.com/en-us/download) 。
2. 在 VS Code 的擴充功能市場中搜索 `C# Dev Kit` ,安裝 C# Dev Kit [配置教程](https://code.visualstudio.com/docs/csharp/get-started))。
3. 也可使用 Visual Studio[安裝教程](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022))。
### 5. &nbsp; Go 環境
1. 下載並安裝 [go](https://go.dev/dl/) 。
2. 在 VS Code 的擴充功能市場中搜索 `go` ,安裝 Go 。
3. 按快捷鍵 `Ctrl + Shift + P` 撥出命令欄,輸入 go ,選擇 `Go: Install/Update Tools` ,全部勾選並安裝即可。
### 6. &nbsp; Swift 環境
1. 下載並安裝 [Swift](https://www.swift.org/download/) 。
2. 在 VS Code 的擴充功能市場中搜索 `swift` ,安裝 [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang) 。
### 7. &nbsp; JavaScript 環境
1. 下載並安裝 [node.js](https://nodejs.org/en/) 。
2. 在 VS Code 的擴充功能市場中搜索 `javascript` ,安裝 JavaScript (ES6) code snippets 。
3. (可選)在 VS Code 的擴充功能市場中搜索 `Prettier` ,安裝程式碼格式化工具。
### 8. &nbsp; Dart 環境
1. 下載並安裝 [Dart](https://dart.dev/get-dart) 。
2. 在 VS Code 的擴充功能市場中搜索 `dart` ,安裝 [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code) 。
### 9. &nbsp; Rust 環境
1. 下載並安裝 [Rust](https://www.rust-lang.org/tools/install) 。
2. 在 VS Code 的擴充功能市場中搜索 `rust` ,安裝 [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 。
@@ -0,0 +1,144 @@
---
comments: true
---
# 16.3 &nbsp; 術語表
表 16-1 列出了書中出現的重要術語,值得注意以下幾點。
- 建議記住名詞的英文叫法,以便閱讀英文文獻。
- 部分名詞在簡體中文和繁體中文下的叫法不同。
<p align="center"> 表 16-1 &nbsp; 資料結構與演算法的重要名詞 </p>
<div class="center-table" markdown>
| English | 简体中文 | 繁体中文 |
| ------------------------------ | -------------- | -------------- |
| algorithm | 算法 | 演算法 |
| data structure | 数据结构 | 資料結構 |
| code | 代码 | 程式碼 |
| file | 文件 | 檔案 |
| function | 函数 | 函式 |
| method | 方法 | 方法 |
| variable | 变量 | 變數 |
| asymptotic complexity analysis | 渐近复杂度分析 | 漸近複雜度分析 |
| time complexity | 时间复杂度 | 時間複雜度 |
| space complexity | 空间复杂度 | 空間複雜度 |
| loop | 循环 | 迴圈 |
| iteration | 迭代 | 迭代 |
| recursion | 递归 | 遞迴 |
| tail recursion | 尾递归 | 尾遞迴 |
| recursion tree | 递归树 | 遞迴樹 |
| big-$O$ notation | 大 $O$ 记号 | 大 $O$ 記號 |
| asymptotic upper bound | 渐近上界 | 漸近上界 |
| sign-magnitude | 原码 | 原碼 |
| 1s complement | 反码 | 一補數 |
| 2s complement | 补码 | 二補數 |
| 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 | 哈希表 | 雜湊表 |
| bucket | 桶 | 桶 |
| hash function | 哈希函数 | 雜湊函式 |
| hash collision | 哈希冲突 | 雜湊衝突 |
| load factor | 负载因子 | 負載因子 |
| separate chaining | 链式地址 | 鏈結位址 |
| open addressing | 开放寻址 | 開放定址 |
| linear probing | 线性探测 | 線性探查 |
| lazy deletion | 懒删除 | 懶刪除 |
| binary tree | 二叉树 | 二元樹 |
| tree node | 树节点 | 樹節點 |
| left-child node | 左子节点 | 左子節點 |
| right-child node | 右子节点 | 右子節點 |
| parent node | 父节点 | 父節點 |
| left subtree | 左子树 | 左子樹 |
| right subtree | 右子树 | 右子樹 |
| root node | 根节点 | 根節點 |
| leaf node | 叶节点 | 葉節點 |
| edge | 边 | 邊 |
| level | 层 | 層 |
| degree | 度 | 度 |
| height | 高度 | 高度 |
| depth | 深度 | 深度 |
| perfect binary tree | 完美二叉树 | 完美二元樹 |
| complete binary tree | 完全二叉树 | 完全二元樹 |
| full binary tree | 完满二叉树 | 完滿二元樹 |
| balanced binary tree | 平衡二叉树 | 平衡二元樹 |
| binary search tree | 二叉搜索树 | 二元搜尋樹 |
| AVL tree | AVL 树 | AVL 樹 |
| red-black tree | 红黑树 | 紅黑樹 |
| level-order traversal | 层序遍历 | 層序走訪 |
| breadth-first traversal | 广度优先遍历 | 廣度優先走訪 |
| depth-first traversal | 深度优先遍历 | 深度優先走訪 |
| binary search tree | 二叉搜索树 | 二元搜尋樹 |
| balanced binary search tree | 平衡二叉搜索树 | 平衡二元搜尋樹 |
| balance factor | 平衡因子 | 平衡因子 |
| heap | 堆 | 堆積 |
| max heap | 大顶堆 | 大頂堆積 |
| min heap | 小顶堆 | 小頂堆積 |
| priority queue | 优先队列 | 優先佇列 |
| heapify | 堆化 | 堆積化 |
| top-$k$ problem | Top-$k$ 问题 | Top-$k$ 問題 |
| graph | 图 | 圖 |
| vertex | 顶点 | 頂點 |
| undirected graph | 无向图 | 無向圖 |
| directed graph | 有向图 | 有向圖 |
| connected graph | 连通图 | 連通圖 |
| disconnected graph | 非连通图 | 非連通圖 |
| weighted graph | 有权图 | 有權圖 |
| adjacency | 邻接 | 鄰接 |
| path | 路径 | 路徑 |
| in-degree | 入度 | 入度 |
| out-degree | 出度 | 出度 |
| adjacency matrix | 邻接矩阵 | 鄰接矩陣 |
| adjacency list | 邻接表 | 鄰接表 |
| breadth-first search | 广度优先搜索 | 廣度優先搜尋 |
| depth-first search | 深度优先搜索 | 深度優先搜尋 |
| binary search | 二分查找 | 二分搜尋 |
| searching algorithm | 搜索算法 | 搜尋演算法 |
| sorting algorithm | 排序算法 | 排序演算法 |
| selection sort | 选择排序 | 選擇排序 |
| bubble sort | 冒泡排序 | 泡沫排序 |
| insertion sort | 插入排序 | 插入排序 |
| quick sort | 快速排序 | 快速排序 |
| merge sort | 归并排序 | 合併排序 |
| heap sort | 堆排序 | 堆積排序 |
| bucket sort | 桶排序 | 桶排序 |
| counting sort | 计数排序 | 計數排序 |
| radix sort | 基数排序 | 基數排序 |
| divide and conquer | 分治 | 分治 |
| hanota problem | 汉诺塔问题 | 河內塔問題 |
| backtracking algorithm | 回溯算法 | 回溯演算法 |
| constraint | 约束 | 約束 |
| solution | 解 | 解 |
| state | 状态 | 狀態 |
| pruning | 剪枝 | 剪枝 |
| permutations problem | 全排列问题 | 全排列問題 |
| subset-sum problem | 子集和问题 | 子集合問題 |
| $n$-queens problem | $n$ 皇后问题 | $n$ 皇后問題 |
| dynamic programming | 动态规划 | 動態規劃 |
| initial state | 初始状态 | 初始狀態 |
| state-transition equation | 状态转移方程 | 狀態轉移方程 |
| knapsack problem | 背包问题 | 背包問題 |
| edit distance problem | 编辑距离问题 | 編輯距離問題 |
| greedy algorithm | 贪心算法 | 貪婪演算法 |
</div>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
---
comments: true
icon: material/view-list-outline
---
# 第 4 章 &nbsp; 陣列與鏈結串列
![陣列與鏈結串列](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" }
!!! abstract
資料結構的世界如同一堵厚實的磚牆。
陣列的磚塊整齊排列,逐個緊貼。鏈結串列的磚塊分散各處,連線的藤蔓自由地穿梭於磚縫之間。
## Chapter Contents
- [4.1 &nbsp; 陣列](https://www.hello-algo.com/en/chapter_array_and_linkedlist/array/)
- [4.2 &nbsp; 鏈結串列](https://www.hello-algo.com/en/chapter_array_and_linkedlist/linked_list/)
- [4.3 &nbsp; 串列](https://www.hello-algo.com/en/chapter_array_and_linkedlist/list/)
- [4.4 &nbsp; 記憶體與快取 *](https://www.hello-algo.com/en/chapter_array_and_linkedlist/ram_and_cache/)
- [4.5 &nbsp; 小結](https://www.hello-algo.com/en/chapter_array_and_linkedlist/summary/)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,84 @@
---
comments: true
status: new
---
# 4.4 &nbsp; 記憶體與快取 *
在本章的前兩節中,我們探討了陣列和鏈結串列這兩種基礎且重要的資料結構,它們分別代表了“連續儲存”和“分散儲存”兩種物理結構。
實際上,**物理結構在很大程度上決定了程式對記憶體和快取的使用效率**,進而影響演算法程式的整體效能。
## 4.4.1 &nbsp; 計算機儲存裝置
計算機中包括三種類型的儲存裝置:<u>硬碟(hard disk</u>、<u>記憶體(random-access memory, RAM</u>、<u>快取(cache memory</u>。表 4-2 展示了它們在計算機系統中的不同角色和效能特點。
<p align="center"> 表 4-2 &nbsp; 計算機的儲存裝置 </p>
<div class="center-table" markdown>
| | 硬碟 | 記憶體 | 快取 |
| ------ | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
| 用途 | 長期儲存資料,包括作業系統、程式、檔案等 | 臨時儲存當前執行的程式和正在處理的資料 | 儲存經常訪問的資料和指令,減少 CPU 訪問記憶體的次數 |
| 易失性 | 斷電後資料不會丟失 | 斷電後資料會丟失 | 斷電後資料會丟失 |
| 容量 | 較大,TB 級別 | 較小,GB 級別 | 非常小,MB 級別 |
| 速度 | 較慢,幾百到幾千 MB/s | 較快,幾十 GB/s | 非常快,幾十到幾百 GB/s |
| 價格 | 較便宜,幾毛到幾元 / GB | 較貴,幾十到幾百元 / GB | 非常貴,隨 CPU 打包計價 |
</div>
我們可以將計算機儲存系統想象為圖 4-9 所示的金字塔結構。越靠近金字塔頂端的儲存裝置的速度越快、容量越小、成本越高。這種多層級的設計並非偶然,而是計算機科學家和工程師們經過深思熟慮的結果。
- **硬碟難以被記憶體取代**。首先,記憶體中的資料在斷電後會丟失,因此它不適合長期儲存資料;其次,記憶體的成本是硬碟的幾十倍,這使得它難以在消費者市場普及。
- **快取的大容量和高速度難以兼得**。隨著 L1、L2、L3 快取的容量逐步增大,其物理尺寸會變大,與 CPU 核心之間的物理距離會變遠,從而導致資料傳輸時間增加,元素訪問延遲變高。在當前技術下,多層級的快取結構是容量、速度和成本之間的最佳平衡點。
![計算機儲存系統](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" }
<p align="center"> 圖 4-9 &nbsp; 計算機儲存系統 </p>
!!! note
計算機的儲存層次結構體現了速度、容量和成本三者之間的精妙平衡。實際上,這種權衡普遍存在於所有工業領域,它要求我們在不同的優勢和限制之間找到最佳平衡點。
總的來說,**硬碟用於長期儲存大量資料,記憶體用於臨時儲存程式執行中正在處理的資料,而快取則用於儲存經常訪問的資料和指令**,以提高程式執行效率。三者共同協作,確保計算機系統高效執行。
如圖 4-10 所示,在程式執行時,資料會從硬碟中被讀取到記憶體中,供 CPU 計算使用。快取可以看作 CPU 的一部分,**它透過智慧地從記憶體載入資料**,給 CPU 提供高速的資料讀取,從而顯著提升程式的執行效率,減少對較慢的記憶體的依賴。
![硬碟、記憶體和快取之間的資料流通](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" }
<p align="center"> 圖 4-10 &nbsp; 硬碟、記憶體和快取之間的資料流通 </p>
## 4.4.2 &nbsp; 資料結構的記憶體效率
在記憶體空間利用方面,陣列和鏈結串列各自具有優勢和侷限性。
一方面,**記憶體是有限的,且同一塊記憶體不能被多個程式共享**,因此我們希望資料結構能夠儘可能高效地利用空間。陣列的元素緊密排列,不需要額外的空間來儲存鏈結串列節點間的引用(指標),因此空間效率更高。然而,陣列需要一次性分配足夠的連續記憶體空間,這可能導致記憶體浪費,陣列擴容也需要額外的時間和空間成本。相比之下,鏈結串列以“節點”為單位進行動態記憶體分配和回收,提供了更大的靈活性。
另一方面,在程式執行時,**隨著反覆申請與釋放記憶體,空閒記憶體的碎片化程度會越來越高**,從而導致記憶體的利用效率降低。陣列由於其連續的儲存方式,相對不容易導致記憶體碎片化。相反,鏈結串列的元素是分散儲存的,在頻繁的插入與刪除操作中,更容易導致記憶體碎片化。
## 4.4.3 &nbsp; 資料結構的快取效率
快取雖然在空間容量上遠小於記憶體,但它比記憶體快得多,在程式執行速度上起著至關重要的作用。由於快取的容量有限,只能儲存一小部分頻繁訪問的資料,因此當 CPU 嘗試訪問的資料不在快取中時,就會發生<u>快取未命中(cache miss</u>,此時 CPU 不得不從速度較慢的記憶體中載入所需資料。
顯然,**“快取未命中”越少,CPU 讀寫資料的效率就越高**,程式效能也就越好。我們將 CPU 從快取中成功獲取資料的比例稱為<u>快取命中率(cache hit rate</u>,這個指標通常用來衡量快取效率。
為了儘可能達到更高的效率,快取會採取以下資料載入機制。
- **快取行**:快取不是單個位元組地儲存與載入資料,而是以快取行為單位。相比於單個位元組的傳輸,快取行的傳輸形式更加高效。
- **預取機制**:處理器會嘗試預測資料訪問模式(例如順序訪問、固定步長跳躍訪問等),並根據特定模式將資料載入至快取之中,從而提升命中率。
- **空間區域性**:如果一個數據被訪問,那麼它附近的資料可能近期也會被訪問。因此,快取在載入某一資料時,也會載入其附近的資料,以提高命中率。
- **時間區域性**:如果一個數據被訪問,那麼它在不久的將來很可能再次被訪問。快取利用這一原理,透過保留最近訪問過的資料來提高命中率。
實際上,**陣列和鏈結串列對快取的利用效率是不同的**,主要體現在以下幾個方面。
- **佔用空間**:鏈結串列元素比陣列元素佔用空間更多,導致快取中容納的有效資料量更少。
- **快取行**:鏈結串列資料分散在記憶體各處,而快取是“按行載入”的,因此載入到無效資料的比例更高。
- **預取機制**:陣列比鏈結串列的資料訪問模式更具“可預測性”,即系統更容易猜出即將被載入的資料。
- **空間區域性**:陣列被儲存在集中的記憶體空間中,因此被載入資料附近的資料更有可能即將被訪問。
總體而言,**陣列具有更高的快取命中率,因此它在操作效率上通常優於鏈結串列**。這使得在解決演算法問題時,基於陣列實現的資料結構往往更受歡迎。
需要注意的是,**高快取效率並不意味著陣列在所有情況下都優於鏈結串列**。實際應用中選擇哪種資料結構,應根據具體需求來決定。例如,陣列和鏈結串列都可以實現“堆疊”資料結構(下一章會詳細介紹),但它們適用於不同場景。
- 在做演算法題時,我們會傾向於選擇基於陣列實現的堆疊,因為它提供了更高的操作效率和隨機訪問的能力,代價僅是需要預先為陣列分配一定的記憶體空間。
- 如果資料量非常大、動態性很高、堆疊的預期大小難以估計,那麼基於鏈結串列實現的堆疊更加合適。鏈結串列能夠將大量資料分散儲存於記憶體的不同部分,並且避免了陣列擴容產生的額外開銷。
@@ -0,0 +1,80 @@
---
comments: true
---
# 4.5 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 陣列和鏈結串列是兩種基本的資料結構,分別代表資料在計算機記憶體中的兩種儲存方式:連續空間儲存和分散空間儲存。兩者的特點呈現出互補的特性。
- 陣列支持隨機訪問、佔用記憶體較少;但插入和刪除元素效率低,且初始化後長度不可變。
- 鏈結串列透過更改引用(指標)實現高效的節點插入與刪除,且可以靈活調整長度;但節點訪問效率低、佔用記憶體較多。常見的鏈結串列型別包括單向鏈結串列、環形鏈結串列、雙向鏈結串列。
- 串列是一種支持增刪查改的元素有序集合,通常基於動態陣列實現。它保留了陣列的優勢,同時可以靈活調整長度。
- 串列的出現大幅提高了陣列的實用性,但可能導致部分記憶體空間浪費。
- 程式執行時,資料主要儲存在記憶體中。陣列可提供更高的記憶體空間效率,而鏈結串列則在記憶體使用上更加靈活。
- 快取透過快取行、預取機制以及空間區域性和時間區域性等資料載入機制,為 CPU 提供快速資料訪問,顯著提升程式的執行效率。
- 由於陣列具有更高的快取命中率,因此它通常比鏈結串列更高效。在選擇資料結構時,應根據具體需求和場景做出恰當選擇。
### 2. &nbsp; Q & A
**Q**:陣列儲存在堆疊上和儲存在堆積上,對時間效率和空間效率是否有影響?
儲存在堆疊上和堆積上的陣列都被儲存在連續記憶體空間內,資料操作效率基本一致。然而,堆疊和堆積具有各自的特點,從而導致以下不同點。
1. 分配和釋放效率:堆疊是一塊較小的記憶體,分配由編譯器自動完成;而堆積記憶體相對更大,可以在程式碼中動態分配,更容易碎片化。因此,堆積上的分配和釋放操作通常比堆疊上的慢。
2. 大小限制:堆疊記憶體相對較小,堆積的大小一般受限於可用記憶體。因此堆積更加適合儲存大型陣列。
3. 靈活性:堆疊上的陣列的大小需要在編譯時確定,而堆積上的陣列的大小可以在執行時動態確定。
**Q**:為什麼陣列要求相同型別的元素,而在鏈結串列中卻沒有強調相同型別呢?
鏈結串列由節點組成,節點之間透過引用(指標)連線,各個節點可以儲存不同型別的資料,例如 `int``double``string``object` 等。
相對地,陣列元素則必須是相同型別的,這樣才能透過計算偏移量來獲取對應元素位置。例如,陣列同時包含 `int``long` 兩種型別,單個元素分別佔用 4 位元組 和 8 位元組 ,此時就不能用以下公式計算偏移量了,因為陣列中包含了兩種“元素長度”。
```shell
# 元素記憶體位址 = 陣列記憶體位址(首元素記憶體位址) + 元素長度 * 元素索引
```
**Q**:刪除節點 `P` 後,是否需要把 `P.next` 設為 `None` 呢?
不修改 `P.next` 也可以。從該鏈結串列的角度看,從頭節點走訪到尾節點已經不會遇到 `P` 了。這意味著節點 `P` 已經從鏈結串列中刪除了,此時節點 `P` 指向哪裡都不會對該鏈結串列產生影響。
從資料結構與演算法(做題)的角度看,不斷開沒有關係,只要保證程式的邏輯是正確的就行。從標準庫的角度看,斷開更加安全、邏輯更加清晰。如果不斷開,假設被刪除節點未被正常回收,那麼它會影響後繼節點的記憶體回收。
**Q**:在鏈結串列中插入和刪除操作的時間複雜度是 $O(1)$ 。但是增刪之前都需要 $O(n)$ 的時間查詢元素,那為什麼時間複雜度不是 $O(n)$ 呢?
如果是先查詢元素、再刪除元素,時間複雜度確實是 $O(n)$ 。然而,鏈結串列的 $O(1)$ 增刪的優勢可以在其他應用上得到體現。例如,雙向佇列適合使用鏈結串列實現,我們維護一個指標變數始終指向頭節點、尾節點,每次插入與刪除操作都是 $O(1)$ 。
**Q**:圖“鏈結串列定義與儲存方式”中,淺藍色的儲存節點指標是佔用一塊記憶體位址嗎?還是和節點值各佔一半呢?
該示意圖只是定性表示,定量表示需要根據具體情況進行分析。
- 不同型別的節點值佔用的空間是不同的,比如 `int``long``double` 和例項物件等。
- 指標變數佔用的記憶體空間大小根據所使用的作業系統及編譯環境而定,大多為 8 位元組或 4 位元組。
**Q**:在串列末尾新增元素是否時時刻刻都為 $O(1)$ ?
如果新增元素時超出串列長度,則需要先擴容串列再新增。系統會申請一塊新的記憶體,並將原串列的所有元素搬運過去,這時候時間複雜度就會是 $O(n)$ 。
**Q**:“串列的出現極大地提高了陣列的實用性,但可能導致部分記憶體空間浪費”,這裡的空間浪費是指額外增加的變數如容量、長度、擴容倍數所佔的記憶體嗎?
這裡的空間浪費主要有兩方面含義:一方面,串列都會設定一個初始長度,我們不一定需要用這麼多;另一方面,為了防止頻繁擴容,擴容一般會乘以一個係數,比如 $\times 1.5$ 。這樣一來,也會出現很多空位,我們通常不能完全填滿它們。
**Q**:在 Python 中初始化 `n = [1, 2, 3]` 後,這 3 個元素的位址是相連的,但是初始化 `m = [2, 1, 3]` 會發現它們每個元素的 id 並不是連續的,而是分別跟 `n` 中的相同。這些元素的位址不連續,那麼 `m` 還是陣列嗎?
假如把串列元素換成鏈結串列節點 `n = [n1, n2, n3, n4, n5]` ,通常情況下這 5 個節點物件也分散儲存在記憶體各處。然而,給定一個串列索引,我們仍然可以在 $O(1)$ 時間內獲取節點記憶體位址,從而訪問到對應的節點。這是因為陣列中儲存的是節點的引用,而非節點本身。
與許多語言不同,Python 中的數字也被包裝為物件,串列中儲存的不是數字本身,而是對數字的引用。因此,我們會發現兩個陣列中的相同數字擁有同一個 id ,並且這些數字的記憶體位址無須連續。
**Q**C++ STL 裡面的 `std::list` 已經實現了雙向鏈結串列,但好像一些演算法書上不怎麼直接使用它,是不是因為有什麼侷限性呢?
一方面,我們往往更青睞使用陣列實現演算法,而只在必要時才使用鏈結串列,主要有兩個原因。
- 空間開銷:由於每個元素需要兩個額外的指標(一個用於前一個元素,一個用於後一個元素),所以 `std::list` 通常比 `std::vector` 更佔用空間。
- 快取不友好:由於資料不是連續存放的,因此 `std::list` 對快取的利用率較低。一般情況下,`std::vector` 的效能會更好。
另一方面,必要使用鏈結串列的情況主要是二元樹和圖。堆疊和佇列往往會使用程式語言提供的 `stack``queue` ,而非鏈結串列。
**Q**:初始化串列 `res = [0] * self.size()` 操作,會導致 `res` 的每個元素引用相同的位址嗎?
不會。但二維陣列會有這個問題,例如初始化二維串列 `res = [[0] * self.size()]` ,則多次引用了同一個串列 `[0]`
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
---
comments: true
icon: material/map-marker-path
---
# 第 13 章 &nbsp; 回溯
![回溯](../assets/covers/chapter_backtracking.jpg){ class="cover-image" }
!!! abstract
我們如同迷宮中的探索者,在前進的道路上可能會遇到困難。
回溯的力量讓我們能夠重新開始,不斷嘗試,最終找到通往光明的出口。
## Chapter Contents
- [13.1 &nbsp; 回溯演算法](https://www.hello-algo.com/en/chapter_backtracking/backtracking_algorithm/)
- [13.2 &nbsp; 全排列問題](https://www.hello-algo.com/en/chapter_backtracking/permutations_problem/)
- [13.3 &nbsp; 子集和問題](https://www.hello-algo.com/en/chapter_backtracking/subset_sum_problem/)
- [13.4 &nbsp; N 皇后問題](https://www.hello-algo.com/en/chapter_backtracking/n_queens_problem/)
- [13.5 &nbsp; 小結](https://www.hello-algo.com/en/chapter_backtracking/summary/)
@@ -0,0 +1,732 @@
---
comments: true
---
# 13.4 &nbsp; n 皇后問題
!!! question
根據國際象棋的規則,皇后可以攻擊與同處一行、一列或一條斜線上的棋子。給定 $n$ 個皇后和一個 $n \times n$ 大小的棋盤,尋找使得所有皇后之間無法相互攻擊的擺放方案。
如圖 13-15 所示,當 $n = 4$ 時,共可以找到兩個解。從回溯演算法的角度看,$n \times n$ 大小的棋盤共有 $n^2$ 個格子,給出了所有的選擇 `choices` 。在逐個放置皇后的過程中,棋盤狀態在不斷地變化,每個時刻的棋盤就是狀態 `state`
![4 皇后問題的解](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" }
<p align="center"> 圖 13-15 &nbsp; 4 皇后問題的解 </p>
圖 13-16 展示了本題的三個約束條件:**多個皇后不能在同一行、同一列、同一條對角線上**。值得注意的是,對角線分為主對角線 `\` 和次對角線 `/` 兩種。
![n 皇后問題的約束條件](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" }
<p align="center"> 圖 13-16 &nbsp; n 皇后問題的約束條件 </p>
### 1. &nbsp; 逐行放置策略
皇后的數量和棋盤的行數都為 $n$ ,因此我們容易得到一個推論:**棋盤每行都允許且只允許放置一個皇后**。
也就是說,我們可以採取逐行放置策略:從第一行開始,在每行放置一個皇后,直至最後一行結束。
圖 13-17 所示為 $4$ 皇后問題的逐行放置過程。受畫幅限制,圖 13-17 僅展開了第一行的其中一個搜尋分支,並且將不滿足列約束和對角線約束的方案都進行了剪枝。
![逐行放置策略](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" }
<p align="center"> 圖 13-17 &nbsp; 逐行放置策略 </p>
從本質上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出現多個皇后的所有搜尋分支。
### 2. &nbsp; 列與對角線剪枝
為了滿足列約束,我們可以利用一個長度為 $n$ 的布林型陣列 `cols` 記錄每一列是否有皇后。在每次決定放置前,我們透過 `cols` 將已有皇后的列進行剪枝,並在回溯中動態更新 `cols` 的狀態。
那麼,如何處理對角線約束呢?設棋盤中某個格子的行列索引為 $(row, col)$ ,選定矩陣中的某條主對角線,我們發現該對角線上所有格子的行索引減列索引都相等,**即對角線上所有格子的 $row - col$ 為恆定值**。
也就是說,如果兩個格子滿足 $row_1 - col_1 = row_2 - col_2$ ,則它們一定處在同一條主對角線上。利用該規律,我們可以藉助圖 13-18 所示的陣列 `diags1` 記錄每條主對角線上是否有皇后。
同理,**次對角線上的所有格子的 $row + col$ 是恆定值**。我們同樣也可以藉助陣列 `diags2` 來處理次對角線約束。
![處理列約束和對角線約束](n_queens_problem.assets/n_queens_cols_diagonals.png){ class="animation-figure" }
<p align="center"> 圖 13-18 &nbsp; 處理列約束和對角線約束 </p>
### 3. &nbsp; 程式碼實現
請注意,$n$ 維方陣中 $row - col$ 的範圍是 $[-n + 1, n - 1]$ $row + col$ 的範圍是 $[0, 2n - 2]$ ,所以主對角線和次對角線的數量都為 $2n - 1$ ,即陣列 `diags1``diags2` 的長度都為 $2n - 1$ 。
=== "Python"
```python title="n_queens.py"
def backtrack(
row: int,
n: int,
state: list[list[str]],
res: list[list[list[str]]],
cols: list[bool],
diags1: list[bool],
diags2: list[bool],
):
"""回溯演算法:n 皇后"""
# 當放置完所有行時,記錄解
if row == n:
res.append([list(row) for row in state])
return
# 走訪所有列
for col in range(n):
# 計算該格子對應的主對角線和次對角線
diag1 = row - col + n - 1
diag2 = row + col
# 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if not cols[col] and not diags1[diag1] and not diags2[diag2]:
# 嘗試:將皇后放置在該格子
state[row][col] = "Q"
cols[col] = diags1[diag1] = diags2[diag2] = True
# 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2)
# 回退:將該格子恢復為空位
state[row][col] = "#"
cols[col] = diags1[diag1] = diags2[diag2] = False
def n_queens(n: int) -> list[list[list[str]]]:
"""求解 n 皇后"""
# 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
state = [["#" for _ in range(n)] for _ in range(n)]
cols = [False] * n # 記錄列是否有皇后
diags1 = [False] * (2 * n - 1) # 記錄主對角線上是否有皇后
diags2 = [False] * (2 * n - 1) # 記錄次對角線上是否有皇后
res = []
backtrack(0, n, state, res, cols, diags1, diags2)
return res
```
=== "C++"
```cpp title="n_queens.cpp"
/* 回溯演算法:n 皇后 */
void backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,
vector<bool> &diags1, vector<bool> &diags2) {
// 當放置完所有行時,記錄解
if (row == n) {
res.push_back(state);
return;
}
// 走訪所有列
for (int col = 0; col < n; col++) {
// 計算該格子對應的主對角線和次對角線
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state[row][col] = "Q";
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state[row][col] = "#";
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 n 皇后 */
vector<vector<vector<string>>> nQueens(int n) {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
vector<vector<string>> state(n, vector<string>(n, "#"));
vector<bool> cols(n, false); // 記錄列是否有皇后
vector<bool> diags1(2 * n - 1, false); // 記錄主對角線上是否有皇后
vector<bool> diags2(2 * n - 1, false); // 記錄次對角線上是否有皇后
vector<vector<vector<string>>> res;
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Java"
```java title="n_queens.java"
/* 回溯演算法:n 皇后 */
void backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,
boolean[] cols, boolean[] diags1, boolean[] diags2) {
// 當放置完所有行時,記錄解
if (row == n) {
List<List<String>> copyState = new ArrayList<>();
for (List<String> sRow : state) {
copyState.add(new ArrayList<>(sRow));
}
res.add(copyState);
return;
}
// 走訪所有列
for (int col = 0; col < n; col++) {
// 計算該格子對應的主對角線和次對角線
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state.get(row).set(col, "Q");
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state.get(row).set(col, "#");
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 n 皇后 */
List<List<List<String>>> nQueens(int n) {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
List<List<String>> state = new ArrayList<>();
for (int i = 0; i < n; i++) {
List<String> row = new ArrayList<>();
for (int j = 0; j < n; j++) {
row.add("#");
}
state.add(row);
}
boolean[] cols = new boolean[n]; // 記錄列是否有皇后
boolean[] diags1 = new boolean[2 * n - 1]; // 記錄主對角線上是否有皇后
boolean[] diags2 = new boolean[2 * n - 1]; // 記錄次對角線上是否有皇后
List<List<List<String>>> res = new ArrayList<>();
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "C#"
```csharp title="n_queens.cs"
/* 回溯演算法:n 皇后 */
void Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,
bool[] cols, bool[] diags1, bool[] diags2) {
// 當放置完所有行時,記錄解
if (row == n) {
List<List<string>> copyState = [];
foreach (List<string> sRow in state) {
copyState.Add(new List<string>(sRow));
}
res.Add(copyState);
return;
}
// 走訪所有列
for (int col = 0; col < n; col++) {
// 計算該格子對應的主對角線和次對角線
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state[row][col] = "Q";
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
Backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state[row][col] = "#";
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 n 皇后 */
List<List<List<string>>> NQueens(int n) {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
List<List<string>> state = [];
for (int i = 0; i < n; i++) {
List<string> row = [];
for (int j = 0; j < n; j++) {
row.Add("#");
}
state.Add(row);
}
bool[] cols = new bool[n]; // 記錄列是否有皇后
bool[] diags1 = new bool[2 * n - 1]; // 記錄主對角線上是否有皇后
bool[] diags2 = new bool[2 * n - 1]; // 記錄次對角線上是否有皇后
List<List<List<string>>> res = [];
Backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Go"
```go title="n_queens.go"
/* 回溯演算法:n 皇后 */
func backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {
// 當放置完所有行時,記錄解
if row == n {
newState := make([][]string, len(*state))
for i, _ := range newState {
newState[i] = make([]string, len((*state)[0]))
copy(newState[i], (*state)[i])
}
*res = append(*res, newState)
}
// 走訪所有列
for col := 0; col < n; col++ {
// 計算該格子對應的主對角線和次對角線
diag1 := row - col + n - 1
diag2 := row + col
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {
// 嘗試:將皇后放置在該格子
(*state)[row][col] = "Q"
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true
// 放置下一行
backtrack(row+1, n, state, res, cols, diags1, diags2)
// 回退:將該格子恢復為空位
(*state)[row][col] = "#"
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false
}
}
}
/* 求解 n 皇后 */
func nQueens(n int) [][][]string {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
state := make([][]string, n)
for i := 0; i < n; i++ {
row := make([]string, n)
for i := 0; i < n; i++ {
row[i] = "#"
}
state[i] = row
}
// 記錄列是否有皇后
cols := make([]bool, n)
diags1 := make([]bool, 2*n-1)
diags2 := make([]bool, 2*n-1)
res := make([][][]string, 0)
backtrack(0, n, &state, &res, &cols, &diags1, &diags2)
return res
}
```
=== "Swift"
```swift title="n_queens.swift"
/* 回溯演算法:n 皇后 */
func backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {
// 當放置完所有行時,記錄解
if row == n {
res.append(state)
return
}
// 走訪所有列
for col in 0 ..< n {
// 計算該格子對應的主對角線和次對角線
let diag1 = row - col + n - 1
let diag2 = row + col
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
// 嘗試:將皇后放置在該格子
state[row][col] = "Q"
cols[col] = true
diags1[diag1] = true
diags2[diag2] = true
// 放置下一行
backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
// 回退:將該格子恢復為空位
state[row][col] = "#"
cols[col] = false
diags1[diag1] = false
diags2[diag2] = false
}
}
}
/* 求解 n 皇后 */
func nQueens(n: Int) -> [[[String]]] {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
var state = Array(repeating: Array(repeating: "#", count: n), count: n)
var cols = Array(repeating: false, count: n) // 記錄列是否有皇后
var diags1 = Array(repeating: false, count: 2 * n - 1) // 記錄主對角線上是否有皇后
var diags2 = Array(repeating: false, count: 2 * n - 1) // 記錄次對角線上是否有皇后
var res: [[[String]]] = []
backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
return res
}
```
=== "JS"
```javascript title="n_queens.js"
/* 回溯演算法:n 皇后 */
function backtrack(row, n, state, res, cols, diags1, diags2) {
// 當放置完所有行時,記錄解
if (row === n) {
res.push(state.map((row) => row.slice()));
return;
}
// 走訪所有列
for (let col = 0; col < n; col++) {
// 計算該格子對應的主對角線和次對角線
const diag1 = row - col + n - 1;
const diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state[row][col] = 'Q';
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state[row][col] = '#';
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 n 皇后 */
function nQueens(n) {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
const state = Array.from({ length: n }, () => Array(n).fill('#'));
const cols = Array(n).fill(false); // 記錄列是否有皇后
const diags1 = Array(2 * n - 1).fill(false); // 記錄主對角線上是否有皇后
const diags2 = Array(2 * n - 1).fill(false); // 記錄次對角線上是否有皇后
const res = [];
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "TS"
```typescript title="n_queens.ts"
/* 回溯演算法:n 皇后 */
function backtrack(
row: number,
n: number,
state: string[][],
res: string[][][],
cols: boolean[],
diags1: boolean[],
diags2: boolean[]
): void {
// 當放置完所有行時,記錄解
if (row === n) {
res.push(state.map((row) => row.slice()));
return;
}
// 走訪所有列
for (let col = 0; col < n; col++) {
// 計算該格子對應的主對角線和次對角線
const diag1 = row - col + n - 1;
const diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state[row][col] = 'Q';
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state[row][col] = '#';
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 n 皇后 */
function nQueens(n: number): string[][][] {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
const state = Array.from({ length: n }, () => Array(n).fill('#'));
const cols = Array(n).fill(false); // 記錄列是否有皇后
const diags1 = Array(2 * n - 1).fill(false); // 記錄主對角線上是否有皇后
const diags2 = Array(2 * n - 1).fill(false); // 記錄次對角線上是否有皇后
const res: string[][][] = [];
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Dart"
```dart title="n_queens.dart"
/* 回溯演算法:n 皇后 */
void backtrack(
int row,
int n,
List<List<String>> state,
List<List<List<String>>> res,
List<bool> cols,
List<bool> diags1,
List<bool> diags2,
) {
// 當放置完所有行時,記錄解
if (row == n) {
List<List<String>> copyState = [];
for (List<String> sRow in state) {
copyState.add(List.from(sRow));
}
res.add(copyState);
return;
}
// 走訪所有列
for (int col = 0; col < n; col++) {
// 計算該格子對應的主對角線和次對角線
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state[row][col] = "Q";
cols[col] = true;
diags1[diag1] = true;
diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state[row][col] = "#";
cols[col] = false;
diags1[diag1] = false;
diags2[diag2] = false;
}
}
}
/* 求解 n 皇后 */
List<List<List<String>>> nQueens(int n) {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
List<List<String>> state = List.generate(n, (index) => List.filled(n, "#"));
List<bool> cols = List.filled(n, false); // 記錄列是否有皇后
List<bool> diags1 = List.filled(2 * n - 1, false); // 記錄主對角線上是否有皇后
List<bool> diags2 = List.filled(2 * n - 1, false); // 記錄次對角線上是否有皇后
List<List<List<String>>> res = [];
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Rust"
```rust title="n_queens.rs"
/* 回溯演算法:n 皇后 */
fn backtrack(
row: usize,
n: usize,
state: &mut Vec<Vec<String>>,
res: &mut Vec<Vec<Vec<String>>>,
cols: &mut [bool],
diags1: &mut [bool],
diags2: &mut [bool],
) {
// 當放置完所有行時,記錄解
if row == n {
let mut copy_state: Vec<Vec<String>> = Vec::new();
for s_row in state.clone() {
copy_state.push(s_row);
}
res.push(copy_state);
return;
}
// 走訪所有列
for col in 0..n {
// 計算該格子對應的主對角線和次對角線
let diag1 = row + n - 1 - col;
let diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
// 嘗試:將皇后放置在該格子
state.get_mut(row).unwrap()[col] = "Q".into();
(cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state.get_mut(row).unwrap()[col] = "#".into();
(cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);
}
}
}
/* 求解 n 皇后 */
fn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
let mut state: Vec<Vec<String>> = Vec::new();
for _ in 0..n {
let mut row: Vec<String> = Vec::new();
for _ in 0..n {
row.push("#".into());
}
state.push(row);
}
let mut cols = vec![false; n]; // 記錄列是否有皇后
let mut diags1 = vec![false; 2 * n - 1]; // 記錄主對角線上是否有皇后
let mut diags2 = vec![false; 2 * n - 1]; // 記錄次對角線上是否有皇后
let mut res: Vec<Vec<Vec<String>>> = Vec::new();
backtrack(
0,
n,
&mut state,
&mut res,
&mut cols,
&mut diags1,
&mut diags2,
);
res
}
```
=== "C"
```c title="n_queens.c"
/* 回溯演算法:n 皇后 */
void backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],
bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {
// 當放置完所有行時,記錄解
if (row == n) {
res[*resSize] = (char **)malloc(sizeof(char *) * n);
for (int i = 0; i < n; ++i) {
res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));
strcpy(res[*resSize][i], state[i]);
}
(*resSize)++;
return;
}
// 走訪所有列
for (int col = 0; col < n; col++) {
// 計算該格子對應的主對角線和次對角線
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state[row][col] = 'Q';
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);
// 回退:將該格子恢復為空位
state[row][col] = '#';
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 n 皇后 */
char ***nQueens(int n, int *returnSize) {
char state[MAX_SIZE][MAX_SIZE];
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
state[i][j] = '#';
}
state[i][n] = '\0';
}
bool cols[MAX_SIZE] = {false}; // 記錄列是否有皇后
bool diags1[2 * MAX_SIZE - 1] = {false}; // 記錄主對角線上是否有皇后
bool diags2[2 * MAX_SIZE - 1] = {false}; // 記錄次對角線上是否有皇后
char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);
*returnSize = 0;
backtrack(0, n, state, res, returnSize, cols, diags1, diags2);
return res;
}
```
=== "Kotlin"
```kotlin title="n_queens.kt"
/* 回溯演算法:n 皇后 */
fun backtrack(
row: Int,
n: Int,
state: List<MutableList<String>>,
res: MutableList<List<List<String>>?>,
cols: BooleanArray,
diags1: BooleanArray,
diags2: BooleanArray
) {
// 當放置完所有行時,記錄解
if (row == n) {
val copyState: MutableList<List<String>> = ArrayList()
for (sRow in state) {
copyState.add(ArrayList(sRow))
}
res.add(copyState)
return
}
// 走訪所有列
for (col in 0..<n) {
// 計算該格子對應的主對角線和次對角線
val diag1 = row - col + n - 1
val diag2 = row + col
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 嘗試:將皇后放置在該格子
state[row][col] = "Q"
diags2[diag2] = true
diags1[diag1] = diags2[diag2]
cols[col] = diags1[diag1]
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2)
// 回退:將該格子恢復為空位
state[row][col] = "#"
diags2[diag2] = false
diags1[diag1] = diags2[diag2]
cols[col] = diags1[diag1]
}
}
}
/* 求解 n 皇后 */
fun nQueens(n: Int): List<List<List<String>>?> {
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
val state: MutableList<MutableList<String>> = ArrayList()
for (i in 0..<n) {
val row: MutableList<String> = ArrayList()
for (j in 0..<n) {
row.add("#")
}
state.add(row)
}
val cols = BooleanArray(n) // 記錄列是否有皇后
val diags1 = BooleanArray(2 * n - 1) // 記錄主對角線上是否有皇后
val diags2 = BooleanArray(2 * n - 1) // 記錄次對角線上是否有皇后
val res: MutableList<List<List<String>>?> = ArrayList()
backtrack(0, n, state, res, cols, diags1, diags2)
return res
}
```
=== "Ruby"
```ruby title="n_queens.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{n_queens}
```
=== "Zig"
```zig title="n_queens.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20backtrack%28%0A%20%20%20%20row%3A%20int,%0A%20%20%20%20n%3A%20int,%0A%20%20%20%20state%3A%20list%5Blist%5Bstr%5D%5D,%0A%20%20%20%20res%3A%20list%5Blist%5Blist%5Bstr%5D%5D%5D,%0A%20%20%20%20cols%3A%20list%5Bbool%5D,%0A%20%20%20%20diags1%3A%20list%5Bbool%5D,%0A%20%20%20%20diags2%3A%20list%5Bbool%5D,%0A%29%3A%0A%20%20%20%20%22%22%22%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%EF%BC%9AN%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%E6%94%BE%E7%BD%AE%E5%AE%8C%E6%89%80%E6%9C%89%E8%A1%8C%E6%97%B6%EF%BC%8C%E8%AE%B0%E5%BD%95%E8%A7%A3%0A%20%20%20%20if%20row%20%3D%3D%20n%3A%0A%20%20%20%20%20%20%20%20res.append%28%5Blist%28row%29%20for%20row%20in%20state%5D%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E6%89%80%E6%9C%89%E5%88%97%0A%20%20%20%20for%20col%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E8%AF%A5%E6%A0%BC%E5%AD%90%E5%AF%B9%E5%BA%94%E7%9A%84%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E5%92%8C%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%0A%20%20%20%20%20%20%20%20diag1%20%3D%20row%20-%20col%20%2B%20n%20-%201%0A%20%20%20%20%20%20%20%20diag2%20%3D%20row%20%2B%20col%0A%20%20%20%20%20%20%20%20%23%20%E5%89%AA%E6%9E%9D%EF%BC%9A%E4%B8%8D%E5%85%81%E8%AE%B8%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%89%80%E5%9C%A8%E5%88%97%E3%80%81%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E3%80%81%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E5%AD%98%E5%9C%A8%E7%9A%87%E5%90%8E%0A%20%20%20%20%20%20%20%20if%20not%20cols%5Bcol%5D%20and%20not%20diags1%5Bdiag1%5D%20and%20not%20diags2%5Bdiag2%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B0%9D%E8%AF%95%EF%BC%9A%E5%B0%86%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E5%9C%A8%E8%AF%A5%E6%A0%BC%E5%AD%90%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22Q%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20True%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%94%BE%E7%BD%AE%E4%B8%8B%E4%B8%80%E8%A1%8C%0A%20%20%20%20%20%20%20%20%20%20%20%20backtrack%28row%20%2B%201,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%9B%9E%E9%80%80%EF%BC%9A%E5%B0%86%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%81%A2%E5%A4%8D%E4%B8%BA%E7%A9%BA%E4%BD%8D%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22%23%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20False%0A%0A%0Adef%20n_queens%28n%3A%20int%29%20-%3E%20list%5Blist%5Blist%5Bstr%5D%5D%5D%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%20N%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20n*n%20%E5%A4%A7%E5%B0%8F%E7%9A%84%E6%A3%8B%E7%9B%98%EF%BC%8C%E5%85%B6%E4%B8%AD%20'Q'%20%E4%BB%A3%E8%A1%A8%E7%9A%87%E5%90%8E%EF%BC%8C'%23'%20%E4%BB%A3%E8%A1%A8%E7%A9%BA%E4%BD%8D%0A%20%20%20%20state%20%3D%20%5B%5B%22%23%22%20for%20_%20in%20range%28n%29%5D%20for%20_%20in%20range%28n%29%5D%0A%20%20%20%20cols%20%3D%20%5BFalse%5D%20*%20n%20%20%23%20%E8%AE%B0%E5%BD%95%E5%88%97%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags1%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags2%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20backtrack%280,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%204%0A%20%20%20%20res%20%3D%20n_queens%28n%29%0A%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A3%8B%E7%9B%98%E9%95%BF%E5%AE%BD%E4%B8%BA%20%7Bn%7D%22%29%0A%20%20%20%20print%28f%22%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E6%96%B9%E6%A1%88%E5%85%B1%E6%9C%89%20%7Blen%28res%29%7D%20%E7%A7%8D%22%29%0A%20%20%20%20for%20state%20in%20res%3A%0A%20%20%20%20%20%20%20%20print%28%22--------------------%22%29%0A%20%20%20%20%20%20%20%20for%20row%20in%20state%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print%28row%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=61&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20backtrack%28%0A%20%20%20%20row%3A%20int,%0A%20%20%20%20n%3A%20int,%0A%20%20%20%20state%3A%20list%5Blist%5Bstr%5D%5D,%0A%20%20%20%20res%3A%20list%5Blist%5Blist%5Bstr%5D%5D%5D,%0A%20%20%20%20cols%3A%20list%5Bbool%5D,%0A%20%20%20%20diags1%3A%20list%5Bbool%5D,%0A%20%20%20%20diags2%3A%20list%5Bbool%5D,%0A%29%3A%0A%20%20%20%20%22%22%22%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%EF%BC%9AN%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%E6%94%BE%E7%BD%AE%E5%AE%8C%E6%89%80%E6%9C%89%E8%A1%8C%E6%97%B6%EF%BC%8C%E8%AE%B0%E5%BD%95%E8%A7%A3%0A%20%20%20%20if%20row%20%3D%3D%20n%3A%0A%20%20%20%20%20%20%20%20res.append%28%5Blist%28row%29%20for%20row%20in%20state%5D%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E6%89%80%E6%9C%89%E5%88%97%0A%20%20%20%20for%20col%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E8%AF%A5%E6%A0%BC%E5%AD%90%E5%AF%B9%E5%BA%94%E7%9A%84%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E5%92%8C%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%0A%20%20%20%20%20%20%20%20diag1%20%3D%20row%20-%20col%20%2B%20n%20-%201%0A%20%20%20%20%20%20%20%20diag2%20%3D%20row%20%2B%20col%0A%20%20%20%20%20%20%20%20%23%20%E5%89%AA%E6%9E%9D%EF%BC%9A%E4%B8%8D%E5%85%81%E8%AE%B8%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%89%80%E5%9C%A8%E5%88%97%E3%80%81%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E3%80%81%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E5%AD%98%E5%9C%A8%E7%9A%87%E5%90%8E%0A%20%20%20%20%20%20%20%20if%20not%20cols%5Bcol%5D%20and%20not%20diags1%5Bdiag1%5D%20and%20not%20diags2%5Bdiag2%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B0%9D%E8%AF%95%EF%BC%9A%E5%B0%86%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E5%9C%A8%E8%AF%A5%E6%A0%BC%E5%AD%90%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22Q%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20True%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%94%BE%E7%BD%AE%E4%B8%8B%E4%B8%80%E8%A1%8C%0A%20%20%20%20%20%20%20%20%20%20%20%20backtrack%28row%20%2B%201,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%9B%9E%E9%80%80%EF%BC%9A%E5%B0%86%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%81%A2%E5%A4%8D%E4%B8%BA%E7%A9%BA%E4%BD%8D%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22%23%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20False%0A%0A%0Adef%20n_queens%28n%3A%20int%29%20-%3E%20list%5Blist%5Blist%5Bstr%5D%5D%5D%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%20N%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20n*n%20%E5%A4%A7%E5%B0%8F%E7%9A%84%E6%A3%8B%E7%9B%98%EF%BC%8C%E5%85%B6%E4%B8%AD%20'Q'%20%E4%BB%A3%E8%A1%A8%E7%9A%87%E5%90%8E%EF%BC%8C'%23'%20%E4%BB%A3%E8%A1%A8%E7%A9%BA%E4%BD%8D%0A%20%20%20%20state%20%3D%20%5B%5B%22%23%22%20for%20_%20in%20range%28n%29%5D%20for%20_%20in%20range%28n%29%5D%0A%20%20%20%20cols%20%3D%20%5BFalse%5D%20*%20n%20%20%23%20%E8%AE%B0%E5%BD%95%E5%88%97%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags1%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags2%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20backtrack%280,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%204%0A%20%20%20%20res%20%3D%20n_queens%28n%29%0A%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A3%8B%E7%9B%98%E9%95%BF%E5%AE%BD%E4%B8%BA%20%7Bn%7D%22%29%0A%20%20%20%20print%28f%22%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E6%96%B9%E6%A1%88%E5%85%B1%E6%9C%89%20%7Blen%28res%29%7D%20%E7%A7%8D%22%29%0A%20%20%20%20for%20state%20in%20res%3A%0A%20%20%20%20%20%20%20%20print%28%22--------------------%22%29%0A%20%20%20%20%20%20%20%20for%20row%20in%20state%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print%28row%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=61&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
逐行放置 $n$ 次,考慮列約束,則從第一行到最後一行分別有 $n$、$n-1$、$\dots$、$2$、$1$ 個選擇,使用 $O(n!)$ 時間。當記錄解時,需要複製矩陣 `state` 並新增進 `res` ,複製操作使用 $O(n^2)$ 時間。因此,**總體時間複雜度為 $O(n! \cdot n^2)$** 。實際上,根據對角線約束的剪枝也能夠大幅縮小搜尋空間,因而搜尋效率往往優於以上時間複雜度。
陣列 `state` 使用 $O(n^2)$ 空間,陣列 `cols`、`diags1` 和 `diags2` 皆使用 $O(n)$ 空間。最大遞迴深度為 $n$ ,使用 $O(n)$ 堆疊幀空間。因此,**空間複雜度為 $O(n^2)$** 。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
---
comments: true
---
# 13.5 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 回溯演算法本質是窮舉法,透過對解空間進行深度優先走訪來尋找符合條件的解。在搜尋過程中,遇到滿足條件的解則記錄,直至找到所有解或走訪完成後結束。
- 回溯演算法的搜尋過程包括嘗試與回退兩個部分。它透過深度優先搜尋來嘗試各種選擇,當遇到不滿足約束條件的情況時,則撤銷上一步的選擇,退回到之前的狀態,並繼續嘗試其他選擇。嘗試與回退是兩個方向相反的操作。
- 回溯問題通常包含多個約束條件,它們可用於實現剪枝操作。剪枝可以提前結束不必要的搜尋分支,大幅提升搜尋效率。
- 回溯演算法主要可用於解決搜尋問題和約束滿足問題。組合最佳化問題雖然可以用回溯演算法解決,但往往存在效率更高或效果更好的解法。
- 全排列問題旨在搜尋給定集合元素的所有可能的排列。我們藉助一個陣列來記錄每個元素是否被選擇,剪掉重複選擇同一元素的搜尋分支,確保每個元素只被選擇一次。
- 在全排列問題中,如果集合中存在重複元素,則最終結果會出現重複排列。我們需要約束相等元素在每輪中只能被選擇一次,這通常藉助一個雜湊表來實現。
- 子集和問題的目標是在給定集合中找到和為目標值的所有子集。集合不區分元素順序,而搜尋過程會輸出所有順序的結果,產生重複子集。我們在回溯前將資料進行排序,並設定一個變數來指示每一輪的走訪起始點,從而將生成重複子集的搜尋分支進行剪枝。
- 對於子集和問題,陣列中的相等元素會產生重複集合。我們利用陣列已排序的前置條件,透過判斷相鄰元素是否相等實現剪枝,從而確保相等元素在每輪中只能被選中一次。
- $n$ 皇后問題旨在尋找將 $n$ 個皇后放置到 $n \times n$ 尺寸棋盤上的方案,要求所有皇后兩兩之間無法攻擊對方。該問題的約束條件有行約束、列約束、主對角線和次對角線約束。為滿足行約束,我們採用按行放置的策略,保證每一行放置一個皇后。
- 列約束和對角線約束的處理方式類似。對於列約束,我們利用一個陣列來記錄每一列是否有皇后,從而指示選中的格子是否合法。對於對角線約束,我們藉助兩個陣列來分別記錄該主、次對角線上是否存在皇后;難點在於找處在到同一主(副)對角線上格子滿足的行列索引規律。
### 2. &nbsp; Q & A
**Q**:怎麼理解回溯和遞迴的關係?
總的來看,回溯是一種“演算法策略”,而遞迴更像是一個“工具”。
- 回溯演算法通常基於遞迴實現。然而,回溯是遞迴的應用場景之一,是遞迴在搜尋問題中的應用。
- 遞迴的結構體現了“子問題分解”的解題範式,常用於解決分治、回溯、動態規劃(記憶化遞迴)等問題。
@@ -0,0 +1,22 @@
---
comments: true
icon: material/timer-sand
---
# 第 2 章 &nbsp; 複雜度分析
![複雜度分析](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" }
!!! abstract
複雜度分析猶如浩瀚的演算法宇宙中的時空嚮導。
它帶領我們在時間與空間這兩個維度上深入探索,尋找更優雅的解決方案。
## Chapter Contents
- [2.1 &nbsp; 演算法效率評估](https://www.hello-algo.com/en/chapter_computational_complexity/performance_evaluation/)
- [2.2 &nbsp; 迭代與遞迴](https://www.hello-algo.com/en/chapter_computational_complexity/iteration_and_recursion/)
- [2.3 &nbsp; 時間複雜度](https://www.hello-algo.com/en/chapter_computational_complexity/time_complexity/)
- [2.4 &nbsp; 空間複雜度](https://www.hello-algo.com/en/chapter_computational_complexity/space_complexity/)
- [2.5 &nbsp; 小結](https://www.hello-algo.com/en/chapter_computational_complexity/summary/)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
---
comments: true
---
# 2.1 &nbsp; 演算法效率評估
在演算法設計中,我們先後追求以下兩個層面的目標。
1. **找到問題解法**:演算法需要在規定的輸入範圍內可靠地求得問題的正確解。
2. **尋求最優解法**:同一個問題可能存在多種解法,我們希望找到儘可能高效的演算法。
也就是說,在能夠解決問題的前提下,演算法效率已成為衡量演算法優劣的主要評價指標,它包括以下兩個維度。
- **時間效率**:演算法執行速度的快慢。
- **空間效率**:演算法佔用記憶體空間的大小。
簡而言之,**我們的目標是設計“既快又省”的資料結構與演算法**。而有效地評估演算法效率至關重要,因為只有這樣,我們才能將各種演算法進行對比,進而指導演算法設計與最佳化過程。
效率評估方法主要分為兩種:實際測試、理論估算。
## 2.1.1 &nbsp; 實際測試
假設我們現在有演算法 `A` 和演算法 `B` ,它們都能解決同一問題,現在需要對比這兩個演算法的效率。最直接的方法是找一臺計算機,執行這兩個演算法,並監控記錄它們的執行時間和記憶體佔用情況。這種評估方式能夠反映真實情況,但也存在較大的侷限性。
一方面,**難以排除測試環境的干擾因素**。硬體配置會影響演算法的效能。比如在某臺計算機中,演算法 `A` 的執行時間比演算法 `B` 短;但在另一臺配置不同的計算機中,可能得到相反的測試結果。這意味著我們需要在各種機器上進行測試,統計平均效率,而這是不現實的。
另一方面,**展開完整測試非常耗費資源**。隨著輸入資料量的變化,演算法會表現出不同的效率。例如,在輸入資料量較小時,演算法 `A` 的執行時間比演算法 `B` 短;而在輸入資料量較大時,測試結果可能恰恰相反。因此,為了得到有說服力的結論,我們需要測試各種規模的輸入資料,而這需要耗費大量的計算資源。
## 2.1.2 &nbsp; 理論估算
由於實際測試具有較大的侷限性,因此我們可以考慮僅透過一些計算來評估演算法的效率。這種估算方法被稱為<u>漸近複雜度分析(asymptotic complexity analysis</u>,簡稱<u>複雜度分析</u>。
複雜度分析能夠體現演算法執行所需的時間和空間資源與輸入資料大小之間的關係。**它描述了隨著輸入資料大小的增加,演算法執行所需時間和空間的增長趨勢**。這個定義有些拗口,我們可以將其分為三個重點來理解。
- “時間和空間資源”分別對應<u>時間複雜度(time complexity</u>和<u>空間複雜度(space complexity</u>。
- “隨著輸入資料大小的增加”意味著複雜度反映了演算法執行效率與輸入資料體量之間的關係。
- “時間和空間的增長趨勢”表示複雜度分析關注的不是執行時間或佔用空間的具體值,而是時間或空間增長的“快慢”。
**複雜度分析克服了實際測試方法的弊端**,體現在以下兩個方面。
- 它獨立於測試環境,分析結果適用於所有執行平臺。
- 它可以體現不同資料量下的演算法效率,尤其是在大資料量下的演算法效能。
!!! tip
如果你仍對複雜度的概念感到困惑,無須擔心,我們會在後續章節中詳細介紹。
複雜度分析為我們提供了一把評估演算法效率的“標尺”,使我們可以衡量執行某個演算法所需的時間和空間資源,對比不同演算法之間的效率。
複雜度是個數學概念,對於初學者可能比較抽象,學習難度相對較高。從這個角度看,複雜度分析可能不太適合作為最先介紹的內容。然而,當我們討論某個資料結構或演算法的特點時,難以避免要分析其執行速度和空間使用情況。
綜上所述,建議你在深入學習資料結構與演算法之前,**先對複雜度分析建立初步的瞭解,以便能夠完成簡單演算法的複雜度分析**。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,53 @@
---
comments: true
---
# 2.5 &nbsp; 小結
### 1. &nbsp; 重點回顧
**演算法效率評估**
- 時間效率和空間效率是衡量演算法優劣的兩個主要評價指標。
- 我們可以透過實際測試來評估演算法效率,但難以消除測試環境的影響,且會耗費大量計算資源。
- 複雜度分析可以消除實際測試的弊端,分析結果適用於所有執行平臺,並且能夠揭示演算法在不同資料規模下的效率。
**時間複雜度**
- 時間複雜度用於衡量演算法執行時間隨資料量增長的趨勢,可以有效評估演算法效率,但在某些情況下可能失效,如在輸入的資料量較小或時間複雜度相同時,無法精確對比演算法效率的優劣。
- 最差時間複雜度使用大 $O$ 符號表示,對應函式漸近上界,反映當 $n$ 趨向正無窮時,操作數量 $T(n)$ 的增長級別。
- 推算時間複雜度分為兩步,首先統計操作數量,然後判斷漸近上界。
- 常見時間複雜度從低到高排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$ 和 $O(n!)$ 等。
- 某些演算法的時間複雜度非固定,而是與輸入資料的分佈有關。時間複雜度分為最差、最佳、平均時間複雜度,最佳時間複雜度幾乎不用,因為輸入資料一般需要滿足嚴格條件才能達到最佳情況。
- 平均時間複雜度反映演算法在隨機資料輸入下的執行效率,最接近實際應用中的演算法效能。計算平均時間複雜度需要統計輸入資料分佈以及綜合後的數學期望。
**空間複雜度**
- 空間複雜度的作用類似於時間複雜度,用於衡量演算法佔用記憶體空間隨資料量增長的趨勢。
- 演算法執行過程中的相關記憶體空間可分為輸入空間、暫存空間、輸出空間。通常情況下,輸入空間不納入空間複雜度計算。暫存空間可分為暫存資料、堆疊幀空間和指令空間,其中堆疊幀空間通常僅在遞迴函式中影響空間複雜度。
- 我們通常只關注最差空間複雜度,即統計演算法在最差輸入資料和最差執行時刻下的空間複雜度。
- 常見空間複雜度從低到高排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$ 和 $O(2^n)$ 等。
### 2. &nbsp; Q & A
**Q**:尾遞迴的空間複雜度是 $O(1)$ 嗎?
理論上,尾遞迴函式的空間複雜度可以最佳化至 $O(1)$ 。不過絕大多數程式語言(例如 Java、Python、C++、Go、C# 等)不支持自動最佳化尾遞迴,因此通常認為空間複雜度是 $O(n)$ 。
**Q**:函式和方法這兩個術語的區別是什麼?
<u>函式(function</u>可以被獨立執行,所有參數都以顯式傳遞。<u>方法(method</u>與一個物件關聯,被隱式傳遞給呼叫它的物件,能夠對類別的例項中包含的資料進行操作。
下面以幾種常見的程式語言為例來說明。
- C 語言是程序式程式設計語言,沒有物件導向的概念,所以只有函式。但我們可以透過建立結構體(struct)來模擬物件導向程式設計,與結構體相關聯的函式就相當於其他程式語言中的方法。
- Java 和 C# 是物件導向的程式語言,程式碼塊(方法)通常作為某個類別的一部分。靜態方法的行為類似於函式,因為它被繫結在類別上,不能訪問特定的例項變數。
- C++ 和 Python 既支持程序式程式設計(函式),也支持物件導向程式設計(方法)。
**Q**:圖解“常見的空間複雜度型別”反映的是否是佔用空間的絕對大小?
不是,該圖展示的是空間複雜度,其反映的是增長趨勢,而不是佔用空間的絕對大小。
假設取 $n = 8$ ,你可能會發現每條曲線的值與函式對應不上。這是因為每條曲線都包含一個常數項,用於將取值範圍壓縮到一個視覺舒適的範圍內。
在實際中,因為我們通常不知道每個方法的“常數項”複雜度是多少,所以一般無法僅憑複雜度來選擇 $n = 8$ 之下的最優解法。但對於 $n = 8^5$ 就很好選了,這時增長趨勢已經佔主導了。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,189 @@
---
comments: true
---
# 3.2 &nbsp; 基本資料型別
當談及計算機中的資料時,我們會想到文字、圖片、影片、語音、3D 模型等各種形式。儘管這些資料的組織形式各異,但它們都由各種基本資料型別構成。
**基本資料型別是 CPU 可以直接進行運算的型別**,在演算法中直接被使用,主要包括以下幾種。
- 整數型別 `byte``short``int``long`
- 浮點數型別 `float``double` ,用於表示小數。
- 字元型別 `char` ,用於表示各種語言的字母、標點符號甚至表情符號等。
- 布林型別 `bool` ,用於表示“是”與“否”判斷。
**基本資料型別以二進位制的形式儲存在計算機中**。一個二進位制位即為 $1$ 位元。在絕大多數現代作業系統中,$1$ 位元組(byte)由 $8$ 位元(bit)組成。
基本資料型別的取值範圍取決於其佔用的空間大小。下面以 Java 為例。
- 整數型別 `byte` 佔用 $1$ 位元組 = $8$ 位元 ,可以表示 $2^{8}$ 個數字。
- 整數型別 `int` 佔用 $4$ 位元組 = $32$ 位元 ,可以表示 $2^{32}$ 個數字。
表 3-1 列舉了 Java 中各種基本資料型別的佔用空間、取值範圍和預設值。此表格無須死記硬背,大致理解即可,需要時可以透過查表來回憶。
<p align="center"> 表 3-1 &nbsp; 基本資料型別的佔用空間和取值範圍 </p>
<div class="center-table" markdown>
| 型別 | 符號 | 佔用空間 | 最小值 | 最大值 | 預設值 |
| ------ | -------- | -------- | ------------------------ | ----------------------- | -------------- |
| 整數 | `byte` | 1 位元組 | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | $0$ |
| | `short` | 2 位元組 | $-2^{15}$ | $2^{15} - 1$ | $0$ |
| | `int` | 4 位元組 | $-2^{31}$ | $2^{31} - 1$ | $0$ |
| | `long` | 8 位元組 | $-2^{63}$ | $2^{63} - 1$ | $0$ |
| 浮點數 | `float` | 4 位元組 | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ |
| | `double` | 8 位元組 | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | $0.0$ |
| 字元 | `char` | 2 位元組 | $0$ | $2^{16} - 1$ | $0$ |
| 布林 | `bool` | 1 位元組 | $\text{false}$ | $\text{true}$ | $\text{false}$ |
</div>
請注意,表 3-1 針對的是 Java 的基本資料型別的情況。每種程式語言都有各自的資料型別定義,它們的佔用空間、取值範圍和預設值可能會有所不同。
- 在 Python 中,整數型別 `int` 可以是任意大小,只受限於可用記憶體;浮點數 `float` 是雙精度 64 位;沒有 `char` 型別,單個字元實際上是長度為 1 的字串 `str`
- C 和 C++ 未明確規定基本資料型別的大小,而因實現和平臺各異。表 3-1 遵循 LP64 [資料模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用於包括 Linux 和 macOS 在內的 Unix 64 位作業系統。
- 字元 `char` 的大小在 C 和 C++ 中為 1 位元組,在大多數程式語言中取決於特定的字元編碼方法,詳見“字元編碼”章節。
- 即使表示布林量僅需 1 位($0$ 或 $1$),它在記憶體中通常也儲存為 1 位元組。這是因為現代計算機 CPU 通常將 1 位元組作為最小定址記憶體單元。
那麼,基本資料型別與資料結構之間有什麼關聯呢?我們知道,資料結構是在計算機中組織與儲存資料的方式。這句話的主語是“結構”而非“資料”。
如果想表示“一排數字”,我們自然會想到使用陣列。這是因為陣列的線性結構可以表示數字的相鄰關係和順序關係,但至於儲存的內容是整數 `int`、小數 `float` 還是字元 `char` ,則與“資料結構”無關。
換句話說,**基本資料型別提供了資料的“內容型別”,而資料結構提供了資料的“組織方式”**。例如以下程式碼,我們用相同的資料結構(陣列)來儲存與表示不同的基本資料型別,包括 `int``float``char``bool` 等。
=== "Python"
```python title=""
# 使用多種基本資料型別來初始化陣列
numbers: list[int] = [0] * 5
decimals: list[float] = [0.0] * 5
# Python 的字元實際上是長度為 1 的字串
characters: list[str] = ['0'] * 5
bools: list[bool] = [False] * 5
# Python 的串列可以自由儲存各種基本資料型別和物件引用
data = [0, 0.0, 'a', False, ListNode(0)]
```
=== "C++"
```cpp title=""
// 使用多種基本資料型別來初始化陣列
int numbers[5];
float decimals[5];
char characters[5];
bool bools[5];
```
=== "Java"
```java title=""
// 使用多種基本資料型別來初始化陣列
int[] numbers = new int[5];
float[] decimals = new float[5];
char[] characters = new char[5];
boolean[] bools = new boolean[5];
```
=== "C#"
```csharp title=""
// 使用多種基本資料型別來初始化陣列
int[] numbers = new int[5];
float[] decimals = new float[5];
char[] characters = new char[5];
bool[] bools = new bool[5];
```
=== "Go"
```go title=""
// 使用多種基本資料型別來初始化陣列
var numbers = [5]int{}
var decimals = [5]float64{}
var characters = [5]byte{}
var bools = [5]bool{}
```
=== "Swift"
```swift title=""
// 使用多種基本資料型別來初始化陣列
let numbers = Array(repeating: 0, count: 5)
let decimals = Array(repeating: 0.0, count: 5)
let characters: [Character] = Array(repeating: "a", count: 5)
let bools = Array(repeating: false, count: 5)
```
=== "JS"
```javascript title=""
// JavaScript 的陣列可以自由儲存各種基本資料型別和物件
const array = [0, 0.0, 'a', false];
```
=== "TS"
```typescript title=""
// 使用多種基本資料型別來初始化陣列
const numbers: number[] = [];
const characters: string[] = [];
const bools: boolean[] = [];
```
=== "Dart"
```dart title=""
// 使用多種基本資料型別來初始化陣列
List<int> numbers = List.filled(5, 0);
List<double> decimals = List.filled(5, 0.0);
List<String> characters = List.filled(5, 'a');
List<bool> bools = List.filled(5, false);
```
=== "Rust"
```rust title=""
// 使用多種基本資料型別來初始化陣列
let numbers: Vec<i32> = vec![0; 5];
let decimals: Vec<f32> = vec![0.0; 5];
let characters: Vec<char> = vec!['0'; 5];
let bools: Vec<bool> = vec![false; 5];
```
=== "C"
```c title=""
// 使用多種基本資料型別來初始化陣列
int numbers[10];
float decimals[10];
char characters[10];
bool bools[10];
```
=== "Kotlin"
```kotlin title=""
// 使用多種基本資料型別來初始化陣列
val numbers = IntArray(5)
val decinals = FloatArray(5)
val characters = CharArray(5)
val bools = BooleanArray(5)
```
=== "Ruby"
```ruby title=""
```
=== "Zig"
```zig title=""
```
??? pythontutor "視覺化執行"
<div style="height: 477px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
@@ -0,0 +1,97 @@
---
comments: true
---
# 3.4 &nbsp; 字元編碼 *
在計算機中,所有資料都是以二進位制數的形式儲存的,字元 `char` 也不例外。為了表示字元,我們需要建立一套“字符集”,規定每個字元和二進位制數之間的一一對應關係。有了字符集之後,計算機就可以透過查表完成二進位制數到字元的轉換。
## 3.4.1 &nbsp; ASCII 字符集
<u>ASCII 碼</u>是最早出現的字符集,其全稱為 American Standard Code for Information Interchange(美國標準資訊交換程式碼)。它使用 7 位二進位制數(一個位元組的低 7 位)表示一個字元,最多能夠表示 128 個不同的字元。如圖 3-6 所示,ASCII 碼包括英文字母的大小寫、數字 0 ~ 9、一些標點符號,以及一些控制字元(如換行符和製表符)。
![ASCII 碼](character_encoding.assets/ascii_table.png){ class="animation-figure" }
<p align="center"> 圖 3-6 &nbsp; ASCII 碼 </p>
然而,**ASCII 碼僅能夠表示英文**。隨著計算機的全球化,誕生了一種能夠表示更多語言的 <u>EASCII</u> 字符集。它在 ASCII 的 7 位基礎上擴展到 8 位,能夠表示 256 個不同的字元。
在世界範圍內,陸續出現了一批適用於不同地區的 EASCII 字符集。這些字符集的前 128 個字元統一為 ASCII 碼,後 128 個字元定義不同,以適應不同語言的需求。
## 3.4.2 &nbsp; GBK 字符集
後來人們發現,**EASCII 碼仍然無法滿足許多語言的字元數量要求**。比如漢字有近十萬個,光日常使用的就有幾千個。中國國家標準總局於 1980 年釋出了 <u>GB2312</u> 字符集,其收錄了 6763 個漢字,基本滿足了漢字的計算機處理需要。
然而,GB2312 無法處理部分罕見字和繁體字。<u>GBK</u> 字符集是在 GB2312 的基礎上擴展得到的,它共收錄了 21886 個漢字。在 GBK 的編碼方案中,ASCII 字元使用一個位元組表示,漢字使用兩個位元組表示。
## 3.4.3 &nbsp; Unicode 字符集
隨著計算機技術的蓬勃發展,字符集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字符集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字符集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。
那個時代的研究人員就在想:**如果推出一個足夠完整的字符集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎**?在這種想法的驅動下,一個大而全的字符集 Unicode 應運而生。
<u>Unicode</u> 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字符集之中,提供一種通用的字符集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。
自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。在龐大的 Unicode 字符集中,常用的字元佔用 2 位元組,有些生僻的字元佔用 3 位元組甚至 4 位元組。
Unicode 是一種通用字符集,本質上是給每個字元分配一個編號(稱為“碼點”),**但它並沒有規定在計算機中如何儲存這些字元碼點**。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?
對於以上問題,**一種直接的解決方案是將所有字元儲存為等長的編碼**。如圖 3-7 所示,“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“Hello 演算法”中的所有字元都編碼為 2 位元組長度。這樣系統就可以每隔 2 位元組解析一個字元,恢復這個短語的內容了。
![Unicode 編碼示例](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" }
<p align="center"> 圖 3-7 &nbsp; Unicode 編碼示例 </p>
然而 ASCII 碼已經向我們證明,編碼英文只需 1 位元組。若採用上述方案,英文文字佔用空間的大小將會是 ASCII 編碼下的兩倍,非常浪費記憶體空間。因此,我們需要一種更加高效的 Unicode 編碼方法。
## 3.4.4 &nbsp; UTF-8 編碼
目前,UTF-8 已成為國際上使用最廣泛的 Unicode 編碼方法。**它是一種可變長度的編碼**,使用 1 到 4 位元組來表示一個字元,根據字元的複雜性而變。ASCII 字元只需 1 位元組,拉丁字母和希臘字母需要 2 位元組,常用的中文字元需要 3 位元組,其他的一些生僻字元需要 4 位元組。
UTF-8 的編碼規則並不複雜,分為以下兩種情況。
- 對於長度為 1 位元組的字元,將最高位設定為 $0$ ,其餘 7 位設定為 Unicode 碼點。值得注意的是,ASCII 字元在 Unicode 字符集中佔據了前 128 個碼點。也就是說,**UTF-8 編碼可以向下相容 ASCII 碼**。這意味著我們可以使用 UTF-8 來解析年代久遠的 ASCII 碼文字。
- 對於長度為 $n$ 位元組的字元(其中 $n > 1$),將首個位元組的高 $n$ 位都設定為 $1$ ,第 $n + 1$ 位設定為 $0$ ;從第二個位元組開始,將每個位元組的高 2 位都設定為 $10$ ;其餘所有位用於填充字元的 Unicode 碼點。
圖 3-8 展示了“Hello演算法”對應的 UTF-8 編碼。觀察發現,由於最高 $n$ 位都設定為 $1$ ,因此系統可以透過讀取最高位 $1$ 的個數來解析出字元的長度為 $n$ 。
但為什麼要將其餘所有位元組的高 2 位都設定為 $10$ 呢?實際上,這個 $10$ 能夠起到校驗符的作用。假設系統從一個錯誤的位元組開始解析文字,位元組頭部的 $10$ 能夠幫助系統快速判斷出異常。
之所以將 $10$ 當作校驗符,是因為在 UTF-8 編碼規則下,不可能有字元的最高兩位是 $10$ 。這個結論可以用反證法來證明:假設一個字元的最高兩位是 $10$ ,說明該字元的長度為 $1$ ,對應 ASCII 碼。而 ASCII 碼的最高位應該是 $0$ ,與假設矛盾。
![UTF-8 編碼示例](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" }
<p align="center"> 圖 3-8 &nbsp; UTF-8 編碼示例 </p>
除了 UTF-8 之外,常見的編碼方式還包括以下兩種。
- **UTF-16 編碼**:使用 2 或 4 位元組來表示一個字元。所有的 ASCII 字元和常用的非英文字元,都用 2 位元組表示;少數字符需要用到 4 位元組表示。對於 2 位元組的字元,UTF-16 編碼與 Unicode 碼點相等。
- **UTF-32 編碼**:每個字元都使用 4 位元組。這意味著 UTF-32 比 UTF-8 和 UTF-16 更佔用空間,特別是對於 ASCII 字元佔比較高的文字。
從儲存空間佔用的角度看,使用 UTF-8 表示英文字元非常高效,因為它僅需 1 位元組;使用 UTF-16 編碼某些非英文字元(例如中文)會更加高效,因為它僅需 2 位元組,而 UTF-8 可能需要 3 位元組。
從相容性的角度看,UTF-8 的通用性最佳,許多工具和庫優先支持 UTF-8 。
## 3.4.5 &nbsp; 程式語言的字元編碼
對於以往的大多數程式語言,程式執行中的字串都採用 UTF-16 或 UTF-32 這類等長編碼。在等長編碼下,我們可以將字串看作陣列來處理,這種做法具有以下優點。
- **隨機訪問**:UTF-16 編碼的字串可以很容易地進行隨機訪問。UTF-8 是一種變長編碼,要想找到第 $i$ 個字元,我們需要從字串的開始處走訪到第 $i$ 個字元,這需要 $O(n)$ 的時間。
- **字元計數**:與隨機訪問類似,計算 UTF-16 編碼的字串的長度也是 $O(1)$ 的操作。但是,計算 UTF-8 編碼的字串的長度需要走訪整個字串。
- **字串操作**:在 UTF-16 編碼的字串上,很多字串操作(如分割、連線、插入、刪除等)更容易進行。在 UTF-8 編碼的字串上,進行這些操作通常需要額外的計算,以確保不會產生無效的 UTF-8 編碼。
實際上,程式語言的字元編碼方案設計是一個很有趣的話題,涉及許多因素。
- Java 的 `String` 型別使用 UTF-16 編碼,每個字元佔用 2 位元組。這是因為 Java 語言設計之初,人們認為 16 位足以表示所有可能的字元。然而,這是一個不正確的判斷。後來 Unicode 規範擴展到了超過 16 位,所以 Java 中的字元現在可能由一對 16 位的值(稱為“代理對”)表示。
- JavaScript 和 TypeScript 的字串使用 UTF-16 編碼的原因與 Java 類似。當 1995 年 Netscape 公司首次推出 JavaScript 語言時,Unicode 還處於發展早期,那時候使用 16 位的編碼就足以表示所有的 Unicode 字元了。
- C# 使用 UTF-16 編碼,主要是因為 .NET 平臺是由 Microsoft 設計的,而 Microsoft 的很多技術(包括 Windows 作業系統)都廣泛使用 UTF-16 編碼。
由於以上程式語言對字元數量的低估,它們不得不採取“代理對”的方式來表示超過 16 位長度的 Unicode 字元。這是一個不得已為之的無奈之舉。一方面,包含代理對的字串中,一個字元可能佔用 2 位元組或 4 位元組,從而喪失了等長編碼的優勢。另一方面,處理代理對需要額外增加程式碼,這提高了程式設計的複雜性和除錯難度。
出於以上原因,部分程式語言提出了一些不同的編碼方案。
- Python 中的 `str` 使用 Unicode 編碼,並採用一種靈活的字串表示,儲存的字元長度取決於字串中最大的 Unicode 碼點。若字串中全部是 ASCII 字元,則每個字元佔用 1 位元組;如果有字元超出了 ASCII 範圍,但全部在基本多語言平面(BMP)內,則每個字元佔用 2 位元組;如果有超出 BMP 的字元,則每個字元佔用 4 位元組。
- Go 語言的 `string` 型別在內部使用 UTF-8 編碼。Go 語言還提供了 `rune` 型別,它用於表示單個 Unicode 碼點。
- Rust 語言的 `str``String` 型別在內部使用 UTF-8 編碼。Rust 也提供了 `char` 型別,用於表示單個 Unicode 碼點。
需要注意的是,以上討論的都是字串在程式語言中的儲存方式,**這和字串如何在檔案中儲存或在網路中傳輸是不同的問題**。在檔案儲存或網路傳輸中,我們通常會將字串編碼為 UTF-8 格式,以達到最優的相容性和空間效率。
@@ -0,0 +1,58 @@
---
comments: true
---
# 3.1 &nbsp; 資料結構分類
常見的資料結構包括陣列、鏈結串列、堆疊、佇列、雜湊表、樹、堆積、圖,它們可以從“邏輯結構”和“物理結構”兩個維度進行分類。
## 3.1.1 &nbsp; 邏輯結構:線性與非線性
**邏輯結構揭示了資料元素之間的邏輯關係**。在陣列和鏈結串列中,資料按照一定順序排列,體現了資料之間的線性關係;而在樹中,資料從頂部向下按層次排列,表現出“祖先”與“後代”之間的派生關係;圖則由節點和邊構成,反映了複雜的網路關係。
如圖 3-1 所示,邏輯結構可分為“線性”和“非線性”兩大類。線性結構比較直觀,指資料在邏輯關係上呈線性排列;非線性結構則相反,呈非線性排列。
- **線性資料結構**:陣列、鏈結串列、堆疊、佇列、雜湊表,元素之間是一對一的順序關係。
- **非線性資料結構**:樹、堆積、圖、雜湊表。
非線性資料結構可以進一步劃分為樹形結構和網狀結構。
- **樹形結構**:樹、堆積、雜湊表,元素之間是一對多的關係。
- **網狀結構**:圖,元素之間是多對多的關係。
![線性資料結構與非線性資料結構](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" }
<p align="center"> 圖 3-1 &nbsp; 線性資料結構與非線性資料結構 </p>
## 3.1.2 &nbsp; 物理結構:連續與分散
**當演算法程式執行時,正在處理的資料主要儲存在記憶體中**。圖 3-2 展示了一個計算機記憶體條,其中每個黑色方塊都包含一塊記憶體空間。我們可以將記憶體想象成一個巨大的 Excel 表格,其中每個單元格都可以儲存一定大小的資料。
**系統透過記憶體位址來訪問目標位置的資料**。如圖 3-2 所示,計算機根據特定規則為表格中的每個單元格分配編號,確保每個記憶體空間都有唯一的記憶體位址。有了這些位址,程式便可以訪問記憶體中的資料。
![記憶體條、記憶體空間、記憶體位址](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" }
<p align="center"> 圖 3-2 &nbsp; 記憶體條、記憶體空間、記憶體位址 </p>
!!! tip
值得說明的是,將記憶體比作 Excel 表格是一個簡化的類比,實際記憶體的工作機制比較複雜,涉及位址空間、記憶體管理、快取機制、虛擬記憶體和物理記憶體等概念。
記憶體是所有程式的共享資源,當某塊記憶體被某個程式佔用時,則無法被其他程式同時使用了。**因此在資料結構與演算法的設計中,記憶體資源是一個重要的考慮因素**。比如,演算法所佔用的記憶體峰值不應超過系統剩餘空閒記憶體;如果缺少連續大塊的記憶體空間,那麼所選用的資料結構必須能夠儲存在分散的記憶體空間內。
如圖 3-3 所示,**物理結構反映了資料在計算機記憶體中的儲存方式**,可分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。物理結構從底層決定了資料的訪問、更新、增刪等操作方法,兩種物理結構在時間效率和空間效率方面呈現出互補的特點。
![連續空間儲存與分散空間儲存](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" }
<p align="center"> 圖 3-3 &nbsp; 連續空間儲存與分散空間儲存 </p>
值得說明的是,**所有資料結構都是基於陣列、鏈結串列或二者的組合實現的**。例如,堆疊和佇列既可以使用陣列實現,也可以使用鏈結串列實現;而雜湊表的實現可能同時包含陣列和鏈結串列。
- **基於陣列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖、矩陣、張量(維度 $\geq 3$ 的陣列)等。
- **基於鏈結串列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖等。
鏈結串列在初始化後,仍可以在程式執行過程中對其長度進行調整,因此也稱“動態資料結構”。陣列在初始化後長度不可變,因此也稱“靜態資料結構”。值得注意的是,陣列可透過重新分配記憶體實現長度變化,從而具備一定的“動態性”。
!!! tip
如果你感覺物理結構理解起來有困難,建議先閱讀下一章,然後再回顧本節內容。
@@ -0,0 +1,22 @@
---
comments: true
icon: material/shape-outline
---
# 第 3 章 &nbsp; 資料結構
![資料結構](../assets/covers/chapter_data_structure.jpg){ class="cover-image" }
!!! abstract
資料結構如同一副穩固而多樣的框架。
它為資料的有序組織提供了藍圖,演算法得以在此基礎上生動起來。
## Chapter Contents
- [3.1 &nbsp; 資料結構分類](https://www.hello-algo.com/en/chapter_data_structure/classification_of_data_structure/)
- [3.2 &nbsp; 基本資料型別](https://www.hello-algo.com/en/chapter_data_structure/basic_data_types/)
- [3.3 &nbsp; 數字編碼 *](https://www.hello-algo.com/en/chapter_data_structure/number_encoding/)
- [3.4 &nbsp; 字元編碼 *](https://www.hello-algo.com/en/chapter_data_structure/character_encoding/)
- [3.5 &nbsp; 小結](https://www.hello-algo.com/en/chapter_data_structure/summary/)
@@ -0,0 +1,162 @@
---
comments: true
---
# 3.3 &nbsp; 數字編碼 *
!!! note
在本書中,標題帶有 * 符號的是選讀章節。如果你時間有限或感到理解困難,可以先跳過,等學完必讀章節後再單獨攻克。
## 3.3.1 &nbsp; 原碼、一補數和二補數
在上一節的表格中我們發現,所有整數型別能夠表示的負數都比正數多一個,例如 `byte` 的取值範圍是 $[-128, 127]$ 。這個現象比較反直覺,它的內在原因涉及原碼、一補數、二補數的相關知識。
首先需要指出,**數字是以“二補數”的形式儲存在計算機中的**。在分析這樣做的原因之前,首先給出三者的定義。
- **原碼**:我們將數字的二進位制表示的最高位視為符號位,其中 $0$ 表示正數,$1$ 表示負數,其餘位表示數字的值。
- **一補數**:正數的一補數與其原碼相同,負數的一補數是對其原碼除符號位外的所有位取反。
- **二補數**:正數的二補數與其原碼相同,負數的二補數是在其一補數的基礎上加 $1$ 。
圖 3-4 展示了原碼、一補數和二補數之間的轉換方法。
![原碼、一補數與二補數之間的相互轉換](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" }
<p align="center"> 圖 3-4 &nbsp; 原碼、一補數與二補數之間的相互轉換 </p>
<u>原碼(sign-magnitude</u>雖然最直觀,但存在一些侷限性。一方面,**負數的原碼不能直接用於運算**。例如在原碼下計算 $1 + (-2)$ ,得到的結果是 $-3$ ,這顯然是不對的。
$$
\begin{aligned}
& 1 + (-2) \newline
& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline
& = 1000 \; 0011 \newline
& \rightarrow -3
\end{aligned}
$$
為了解決此問題,計算機引入了<u>一補數(1's complement</u>。如果我們先將原碼轉換為一補數,並在一補數下計算 $1 + (-2)$ ,最後將結果從一補數轉換回原碼,則可得到正確結果 $-1$ 。
$$
\begin{aligned}
& 1 + (-2) \newline
& \rightarrow 0000 \; 0001 \; \text{(原碼)} + 1000 \; 0010 \; \text{(原碼)} \newline
& = 0000 \; 0001 \; \text{(一補數)} + 1111 \; 1101 \; \text{(一補數)} \newline
& = 1111 \; 1110 \; \text{(一補數)} \newline
& = 1000 \; 0001 \; \text{(原碼)} \newline
& \rightarrow -1
\end{aligned}
$$
另一方面,**數字零的原碼有 $+0$ 和 $-0$ 兩種表示方式**。這意味著數字零對應兩個不同的二進位制編碼,這可能會帶來歧義。比如在條件判斷中,如果沒有區分正零和負零,則可能會導致判斷結果出錯。而如果我們想處理正零和負零歧義,則需要引入額外的判斷操作,這可能會降低計算機的運算效率。
$$
\begin{aligned}
+0 & \rightarrow 0000 \; 0000 \newline
-0 & \rightarrow 1000 \; 0000
\end{aligned}
$$
與原碼一樣,一補數也存在正負零歧義問題,因此計算機進一步引入了<u>二補數(2's complement</u>。我們先來觀察一下負零的原碼、一補數、二補數的轉換過程:
$$
\begin{aligned}
-0 \rightarrow \; & 1000 \; 0000 \; \text{(原碼)} \newline
= \; & 1111 \; 1111 \; \text{(一補數)} \newline
= 1 \; & 0000 \; 0000 \; \text{(二補數)} \newline
\end{aligned}
$$
在負零的一補數基礎上加 $1$ 會產生進位,但 `byte` 型別的長度只有 8 位,因此溢位到第 9 位的 $1$ 會被捨棄。也就是說,**負零的二補數為 $0000 \; 0000$ ,與正零的二補數相同**。這意味著在二補數表示中只存在一個零,正負零歧義從而得到解決。
還剩最後一個疑惑:`byte` 型別的取值範圍是 $[-128, 127]$ ,多出來的一個負數 $-128$ 是如何得到的呢?我們注意到,區間 $[-127, +127]$ 內的所有整數都有對應的原碼、一補數和二補數,並且原碼和二補數之間可以互相轉換。
然而,**二補數 $1000 \; 0000$ 是一個例外,它並沒有對應的原碼**。根據轉換方法,我們得到該二補數的原碼為 $0000 \; 0000$ 。這顯然是矛盾的,因為該原碼表示數字 $0$ ,它的二補數應該是自身。計算機規定這個特殊的二補數 $1000 \; 0000$ 代表 $-128$ 。實際上,$(-1) + (-127)$ 在二補數下的計算結果就是 $-128$ 。
$$
\begin{aligned}
& (-127) + (-1) \newline
& \rightarrow 1111 \; 1111 \; \text{(原碼)} + 1000 \; 0001 \; \text{(原碼)} \newline
& = 1000 \; 0000 \; \text{(一補數)} + 1111 \; 1110 \; \text{(一補數)} \newline
& = 1000 \; 0001 \; \text{(二補數)} + 1111 \; 1111 \; \text{(二補數)} \newline
& = 1000 \; 0000 \; \text{(二補數)} \newline
& \rightarrow -128
\end{aligned}
$$
你可能已經發現了,上述所有計算都是加法運算。這暗示著一個重要事實:**計算機內部的硬體電路主要是基於加法運算設計的**。這是因為加法運算相對於其他運算(比如乘法、除法和減法)來說,硬體實現起來更簡單,更容易進行並行化處理,運算速度更快。
請注意,這並不意味著計算機只能做加法。**透過將加法與一些基本邏輯運算結合,計算機能夠實現各種其他的數學運算**。例如,計算減法 $a - b$ 可以轉換為計算加法 $a + (-b)$ ;計算乘法和除法可以轉換為計算多次加法或減法。
現在我們可以總結出計算機使用二補數的原因:基於二補數表示,計算機可以用同樣的電路和操作來處理正數和負數的加法,不需要設計特殊的硬體電路來處理減法,並且無須特別處理正負零的歧義問題。這大大簡化了硬體設計,提高了運算效率。
二補數的設計非常精妙,因篇幅關係我們就先介紹到這裡,建議有興趣的讀者進一步深入瞭解。
## 3.3.2 &nbsp; 浮點數編碼
細心的你可能會發現:`int``float` 長度相同,都是 4 位元組 ,但為什麼 `float` 的取值範圍遠大於 `int` ?這非常反直覺,因為按理說 `float` 需要表示小數,取值範圍應該變小才對。
實際上,**這是因為浮點數 `float` 採用了不同的表示方式**。記一個 32 位元長度的二進位制數為:
$$
b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
$$
根據 IEEE 754 標準,32-bit 長度的 `float` 由以下三個部分構成。
- 符號位 $\mathrm{S}$ :佔 1 位 ,對應 $b_{31}$ 。
- 指數位 $\mathrm{E}$ :佔 8 位 ,對應 $b_{30} b_{29} \ldots b_{23}$ 。
- 分數位 $\mathrm{N}$ :佔 23 位 ,對應 $b_{22} b_{21} \ldots b_0$ 。
二進位制數 `float` 對應值的計算方法為:
$$
\text {val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2-127} \times\left(1 . b_{22} b_{21} \ldots b_0\right)_2
$$
轉化到十進位制下的計算公式為:
$$
\text {val}=(-1)^{\mathrm{S}} \times 2^{\mathrm{E} -127} \times (1 + \mathrm{N})
$$
其中各項的取值範圍為:
$$
\begin{aligned}
\mathrm{S} \in & \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline
(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} 2^{-i}) \subset [1, 2 - 2^{-23}]
\end{aligned}
$$
![IEEE 754 標準下的 float 的計算示例](number_encoding.assets/ieee_754_float.png){ class="animation-figure" }
<p align="center"> 圖 3-5 &nbsp; IEEE 754 標準下的 float 的計算示例 </p>
觀察圖 3-5 ,給定一個示例資料 $\mathrm{S} = 0$ $\mathrm{E} = 124$ $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ ,則有:
$$
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
$$
現在我們可以回答最初的問題:**`float` 的表示方式包含指數位,導致其取值範圍遠大於 `int`** 。根據以上計算,`float` 可表示的最大正數為 $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$ ,切換符號位便可得到最小負數。
**儘管浮點數 `float` 擴展了取值範圍,但其副作用是犧牲了精度**。整數型別 `int` 將全部 32 位元用於表示數字,數字是均勻分佈的;而由於指數位的存在,浮點數 `float` 的數值越大,相鄰兩個數字之間的差值就會趨向越大。
如表 3-2 所示,指數位 $E = 0$ 和 $E = 255$ 具有特殊含義,**用於表示零、無窮大、$\mathrm{NaN}$ 等**。
<p align="center"> 表 3-2 &nbsp; 指數位含義 </p>
<div class="center-table" markdown>
| 指數位 E | 分數位 $\mathrm{N} = 0$ | 分數位 $\mathrm{N} \ne 0$ | 計算公式 |
| ------------------ | ----------------------- | ------------------------- | ---------------------------------------------------------------------- |
| $0$ | $\pm 0$ | 次正規數 | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ |
| $1, 2, \dots, 254$ | 正規數 | 正規數 | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ |
| $255$ | $\pm \infty$ | $\mathrm{NaN}$ | |
</div>
值得說明的是,次正規數顯著提升了浮點數的精度。最小正正規數為 $2^{-126}$ ,最小正次正規數為 $2^{-126} \times 2^{-23}$ 。
雙精度 `double` 也採用類似於 `float` 的表示方法,在此不做贅述。
@@ -0,0 +1,38 @@
---
comments: true
---
# 3.5 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 資料結構可以從邏輯結構和物理結構兩個角度進行分類。邏輯結構描述了資料元素之間的邏輯關係,而物理結構描述了資料在計算機記憶體中的儲存方式。
- 常見的邏輯結構包括線性、樹狀和網狀等。通常我們根據邏輯結構將資料結構分為線性(陣列、鏈結串列、堆疊、佇列)和非線性(樹、圖、堆積)兩種。雜湊表的實現可能同時包含線性資料結構和非線性資料結構。
- 當程式執行時,資料被儲存在計算機記憶體中。每個記憶體空間都擁有對應的記憶體位址,程式透過這些記憶體位址訪問資料。
- 物理結構主要分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。所有資料結構都是由陣列、鏈結串列或兩者的組合實現的。
- 計算機中的基本資料型別包括整數 `byte``short``int``long` ,浮點數 `float``double` ,字元 `char` 和布林 `bool` 。它們的取值範圍取決於佔用空間大小和表示方式。
- 原碼、一補數和二補數是在計算機中編碼數字的三種方法,它們之間可以相互轉換。整數的原碼的最高位是符號位,其餘位是數字的值。
- 整數在計算機中是以二補數的形式儲存的。在二補數表示下,計算機可以對正數和負數的加法一視同仁,不需要為減法操作單獨設計特殊的硬體電路,並且不存在正負零歧義的問題。
- 浮點數的編碼由 1 位符號位、8 位指數位和 23 位分數位構成。由於存在指數位,因此浮點數的取值範圍遠大於整數,代價是犧牲了精度。
- ASCII 碼是最早出現的英文字符集,長度為 1 位元組,共收錄 127 個字元。GBK 字符集是常用的中文字符集,共收錄兩萬多個漢字。Unicode 致力於提供一個完整的字符集標準,收錄世界上各種語言的字元,從而解決由於字元編碼方法不一致而導致的亂碼問題。
- UTF-8 是最受歡迎的 Unicode 編碼方法,通用性非常好。它是一種變長的編碼方法,具有很好的擴展性,有效提升了儲存空間的使用效率。UTF-16 和 UTF-32 是等長的編碼方法。在編碼中文時,UTF-16 佔用的空間比 UTF-8 更小。Java 和 C# 等程式語言預設使用 UTF-16 編碼。
### 2. &nbsp; Q & A
**Q**:為什麼雜湊表同時包含線性資料結構和非線性資料結構?
雜湊表底層是陣列,而為了解決雜湊衝突,我們可能會使用“鏈式位址”(後續“雜湊衝突”章節會講):陣列中每個桶指向一個鏈結串列,當鏈結串列長度超過一定閾值時,又可能被轉化為樹(通常為紅黑樹)。
從儲存的角度來看,雜湊表的底層是陣列,其中每一個桶槽位可能包含一個值,也可能包含一個鏈結串列或一棵樹。因此,雜湊表可能同時包含線性資料結構(陣列、鏈結串列)和非線性資料結構(樹)。
**Q**`char` 型別的長度是 1 位元組嗎?
`char` 型別的長度由程式語言採用的編碼方法決定。例如,Java、JavaScript、TypeScript、C# 都採用 UTF-16 編碼(儲存 Unicode 碼點),因此 `char` 型別的長度為 2 位元組。
**Q**:基於陣列實現的資料結構也稱“靜態資料結構” 是否有歧義?堆疊也可以進行出堆疊和入堆疊等操作,這些操作都是“動態”的。
堆疊確實可以實現動態的資料操作,但資料結構仍然是“靜態”(長度不可變)的。儘管基於陣列的資料結構可以動態地新增或刪除元素,但它們的容量是固定的。如果資料量超出了預分配的大小,就需要建立一個新的更大的陣列,並將舊陣列的內容複製到新陣列中。
**Q**:在構建堆疊(佇列)的時候,未指定它的大小,為什麼它們是“靜態資料結構”呢?
在高階程式語言中,我們無須人工指定堆疊(佇列)的初始容量,這個工作由類別內部自動完成。例如,Java 的 `ArrayList` 的初始容量通常為 10。另外,擴容操作也是自動實現的。詳見後續的“串列”章節。
@@ -0,0 +1,441 @@
---
comments: true
---
# 12.2 &nbsp; 分治搜尋策略
我們已經學過,搜尋演算法分為兩大類。
- **暴力搜尋**:它透過走訪資料結構實現,時間複雜度為 $O(n)$ 。
- **自適應搜尋**:它利用特有的資料組織形式或先驗資訊,時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$ 。
實際上,**時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的**,例如二分搜尋和樹。
- 二分搜尋的每一步都將問題(在陣列中搜索目標元素)分解為一個小問題(在陣列的一半中搜索目標元素),這個過程一直持續到陣列為空或找到目標元素為止。
- 樹是分治思想的代表,在二元搜尋樹、AVL 樹、堆積等資料結構中,各種操作的時間複雜度皆為 $O(\log n)$ 。
二分搜尋的分治策略如下所示。
- **問題可以分解**:二分搜尋遞迴地將原問題(在陣列中進行查詢)分解為子問題(在陣列的一半中進行查詢),這是透過比較中間元素和目標元素來實現的。
- **子問題是獨立的**:在二分搜尋中,每輪只處理一個子問題,它不受其他子問題的影響。
- **子問題的解無須合併**:二分搜尋旨在查詢一個特定元素,因此不需要將子問題的解進行合併。當子問題得到解決時,原問題也會同時得到解決。
分治能夠提升搜尋效率,本質上是因為暴力搜尋每輪只能排除一個選項,**而分治搜尋每輪可以排除一半選項**。
### 1. &nbsp; 基於分治實現二分搜尋
在之前的章節中,二分搜尋是基於遞推(迭代)實現的。現在我們基於分治(遞迴)來實現它。
!!! question
給定一個長度為 $n$ 的有序陣列 `nums` ,其中所有元素都是唯一的,請查詢元素 `target`
從分治角度,我們將搜尋區間 $[i, j]$ 對應的子問題記為 $f(i, j)$ 。
以原問題 $f(0, n-1)$ 為起始點,透過以下步驟進行二分搜尋。
1. 計算搜尋區間 $[i, j]$ 的中點 $m$ ,根據它排除一半搜尋區間。
2. 遞迴求解規模減小一半的子問題,可能為 $f(i, m-1)$ 或 $f(m+1, j)$ 。
3. 迴圈第 `1.` 步和第 `2.` 步,直至找到 `target` 或區間為空時返回。
圖 12-4 展示了在陣列中二分搜尋元素 $6$ 的分治過程。
![二分搜尋的分治過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" }
<p align="center"> 圖 12-4 &nbsp; 二分搜尋的分治過程 </p>
在實現程式碼中,我們宣告一個遞迴函式 `dfs()` 來求解問題 $f(i, j)$
=== "Python"
```python title="binary_search_recur.py"
def dfs(nums: list[int], target: int, i: int, j: int) -> int:
"""二分搜尋:問題 f(i, j)"""
# 若區間為空,代表無目標元素,則返回 -1
if i > j:
return -1
# 計算中點索引 m
m = (i + j) // 2
if nums[m] < target:
# 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j)
elif nums[m] > target:
# 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1)
else:
# 找到目標元素,返回其索引
return m
def binary_search(nums: list[int], target: int) -> int:
"""二分搜尋"""
n = len(nums)
# 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1)
```
=== "C++"
```cpp title="binary_search_recur.cpp"
/* 二分搜尋:問題 f(i, j) */
int dfs(vector<int> &nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(vector<int> &nums, int target) {
int n = nums.size();
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Java"
```java title="binary_search_recur.java"
/* 二分搜尋:問題 f(i, j) */
int dfs(int[] nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(int[] nums, int target) {
int n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "C#"
```csharp title="binary_search_recur.cs"
/* 二分搜尋:問題 f(i, j) */
int DFS(int[] nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return DFS(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return DFS(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int BinarySearch(int[] nums, int target) {
int n = nums.Length;
// 求解問題 f(0, n-1)
return DFS(nums, target, 0, n - 1);
}
```
=== "Go"
```go title="binary_search_recur.go"
/* 二分搜尋:問題 f(i, j) */
func dfs(nums []int, target, i, j int) int {
// 如果區間為空,代表沒有目標元素,則返回 -1
if i > j {
return -1
}
// 計算索引中點
m := i + ((j - i) >> 1)
//判斷中點與目標元素大小
if nums[m] < target {
// 小於則遞迴右半陣列
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m+1, j)
} else if nums[m] > target {
// 小於則遞迴左半陣列
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m-1)
} else {
// 找到目標元素,返回其索引
return m
}
}
/* 二分搜尋 */
func binarySearch(nums []int, target int) int {
n := len(nums)
return dfs(nums, target, 0, n-1)
}
```
=== "Swift"
```swift title="binary_search_recur.swift"
/* 二分搜尋:問題 f(i, j) */
func dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {
// 若區間為空,代表無目標元素,則返回 -1
if i > j {
return -1
}
// 計算中點索引 m
let m = (i + j) / 2
if nums[m] < target {
// 遞迴子問題 f(m+1, j)
return dfs(nums: nums, target: target, i: m + 1, j: j)
} else if nums[m] > target {
// 遞迴子問題 f(i, m-1)
return dfs(nums: nums, target: target, i: i, j: m - 1)
} else {
// 找到目標元素,返回其索引
return m
}
}
/* 二分搜尋 */
func binarySearch(nums: [Int], target: Int) -> Int {
// 求解問題 f(0, n-1)
dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)
}
```
=== "JS"
```javascript title="binary_search_recur.js"
/* 二分搜尋:問題 f(i, j) */
function dfs(nums, target, i, j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
const m = i + ((j - i) >> 1);
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
function binarySearch(nums, target) {
const n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "TS"
```typescript title="binary_search_recur.ts"
/* 二分搜尋:問題 f(i, j) */
function dfs(nums: number[], target: number, i: number, j: number): number {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
const m = i + ((j - i) >> 1);
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
function binarySearch(nums: number[], target: number): number {
const n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Dart"
```dart title="binary_search_recur.dart"
/* 二分搜尋:問題 f(i, j) */
int dfs(List<int> nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) ~/ 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(List<int> nums, int target) {
int n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Rust"
```rust title="binary_search_recur.rs"
/* 二分搜尋:問題 f(i, j) */
fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {
// 若區間為空,代表無目標元素,則返回 -1
if i > j {
return -1;
}
let m: i32 = (i + j) / 2;
if nums[m as usize] < target {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if nums[m as usize] > target {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
fn binary_search(nums: &[i32], target: i32) -> i32 {
let n = nums.len() as i32;
// 求解問題 f(0, n-1)
dfs(nums, target, 0, n - 1)
}
```
=== "C"
```c title="binary_search_recur.c"
/* 二分搜尋:問題 f(i, j) */
int dfs(int nums[], int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(int nums[], int target, int numsSize) {
int n = numsSize;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Kotlin"
```kotlin title="binary_search_recur.kt"
/* 二分搜尋:問題 f(i, j) */
fun dfs(
nums: IntArray,
target: Int,
i: Int,
j: Int
): Int {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1
}
// 計算中點索引 m
val m = (i + j) / 2
return if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
dfs(nums, target, m + 1, j)
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
dfs(nums, target, i, m - 1)
} else {
// 找到目標元素,返回其索引
m
}
}
/* 二分搜尋 */
fun binarySearch(nums: IntArray, target: Int): Int {
val n = nums.size
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1)
}
```
=== "Ruby"
```ruby title="binary_search_recur.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{binary_search}
```
=== "Zig"
```zig title="binary_search_recur.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D,%20target%3A%20int,%20i%3A%20int,%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%9A%E9%97%AE%E9%A2%98%20f%28i,%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E6%97%A0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28m%2B1,%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20m%20%2B%201,%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i,%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20i,%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E6%B1%82%E8%A7%A3%E9%97%AE%E9%A2%98%20f%280,%20n-1%29%0A%20%20%20%20return%20dfs%28nums,%20target,%200,%20n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D,%20target%3A%20int,%20i%3A%20int,%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%9A%E9%97%AE%E9%A2%98%20f%28i,%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E6%97%A0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28m%2B1,%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20m%20%2B%201,%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i,%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20i,%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E6%B1%82%E8%A7%A3%E9%97%AE%E9%A2%98%20f%280,%20n-1%29%0A%20%20%20%20return%20dfs%28nums,%20target,%200,%20n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
@@ -0,0 +1,539 @@
---
comments: true
---
# 12.3 &nbsp; 構建二元樹問題
!!! question
給定一棵二元樹的前序走訪 `preorder` 和中序走訪 `inorder` ,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如圖 12-5 所示)。
![構建二元樹的示例資料](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" }
<p align="center"> 圖 12-5 &nbsp; 構建二元樹的示例資料 </p>
### 1. &nbsp; 判斷是否為分治問題
原問題定義為從 `preorder``inorder` 構建二元樹,是一個典型的分治問題。
- **問題可以分解**:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
- **子問題是獨立的**:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
- **子問題的解可以合併**:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
### 2. &nbsp; 如何劃分子樹
根據以上分析,這道題可以使用分治來求解,**但如何透過前序走訪 `preorder` 和中序走訪 `inorder` 來劃分左子樹和右子樹呢**
根據定義,`preorder``inorder` 都可以劃分為三個部分。
- 前序走訪:`[ 根節點 | 左子樹 | 右子樹 ]` ,例如圖 12-5 的樹對應 `[ 3 | 9 | 2 1 7 ]`
- 中序走訪:`[ 左子樹 | 根節點 | 右子樹 ]` ,例如圖 12-5 的樹對應 `[ 9 | 3 | 1 2 7 ]`
以上圖資料為例,我們可以透過圖 12-6 所示的步驟得到劃分結果。
1. 前序走訪的首元素 3 是根節點的值。
2. 查詢根節點 3 在 `inorder` 中的索引,利用該索引可將 `inorder` 劃分為 `[ 9 | 3 1 2 7 ]`
3. 根據 `inorder` 的劃分結果,易得左子樹和右子樹的節點數量分別為 1 和 3 ,從而可將 `preorder` 劃分為 `[ 3 | 9 | 2 1 7 ]`
![在前序走訪和中序走訪中劃分子樹](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" }
<p align="center"> 圖 12-6 &nbsp; 在前序走訪和中序走訪中劃分子樹 </p>
### 3. &nbsp; 基於變數描述子樹區間
根據以上劃分方法,**我們已經得到根節點、左子樹、右子樹在 `preorder``inorder` 中的索引區間**。而為了描述這些索引區間,我們需要藉助幾個指標變數。
- 將當前樹的根節點在 `preorder` 中的索引記為 $i$ 。
- 將當前樹的根節點在 `inorder` 中的索引記為 $m$ 。
- 將當前樹在 `inorder` 中的索引區間記為 $[l, r]$ 。
如表 12-1 所示,透過以上變數即可表示根節點在 `preorder` 中的索引,以及子樹在 `inorder` 中的索引區間。
<p align="center"> 表 12-1 &nbsp; 根節點和子樹在前序走訪和中序走訪中的索引 </p>
<div class="center-table" markdown>
| | 根節點在 `preorder` 中的索引 | 子樹在 `inorder` 中的索引區間 |
| ------ | ---------------------------- | ----------------------------- |
| 當前樹 | $i$ | $[l, r]$ |
| 左子樹 | $i + 1$ | $[l, m-1]$ |
| 右子樹 | $i + 1 + (m - l)$ | $[m+1, r]$ |
</div>
請注意,右子樹根節點索引中的 $(m-l)$ 的含義是“左子樹的節點數量”,建議結合圖 12-7 理解。
![根節點和左右子樹的索引區間表示](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" }
<p align="center"> 圖 12-7 &nbsp; 根節點和左右子樹的索引區間表示 </p>
### 4. &nbsp; 程式碼實現
為了提升查詢 $m$ 的效率,我們藉助一個雜湊表 `hmap` 來儲存陣列 `inorder` 中元素到索引的對映:
=== "Python"
```python title="build_tree.py"
def dfs(
preorder: list[int],
inorder_map: dict[int, int],
i: int,
l: int,
r: int,
) -> TreeNode | None:
"""構建二元樹:分治"""
# 子樹區間為空時終止
if r - l < 0:
return None
# 初始化根節點
root = TreeNode(preorder[i])
# 查詢 m ,從而劃分左右子樹
m = inorder_map[preorder[i]]
# 子問題:構建左子樹
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
# 子問題:構建右子樹
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
# 返回根節點
return root
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
"""構建二元樹"""
# 初始化雜湊表,儲存 inorder 元素到索引的對映
inorder_map = {val: i for i, val in enumerate(inorder)}
root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)
return root
```
=== "C++"
```cpp title="build_tree.cpp"
/* 構建二元樹:分治 */
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return NULL;
// 初始化根節點
TreeNode *root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
return root;
}
```
=== "Java"
```java title="build_tree.java"
/* 構建二元樹:分治 */
TreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return null;
// 初始化根節點
TreeNode root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode buildTree(int[] preorder, int[] inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Map<Integer, Integer> inorderMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
}
TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "C#"
```csharp title="build_tree.cs"
/* 構建二元樹:分治 */
TreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return null;
// 初始化根節點
TreeNode root = new(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode? BuildTree(int[] preorder, int[] inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Dictionary<int, int> inorderMap = [];
for (int i = 0; i < inorder.Length; i++) {
inorderMap.TryAdd(inorder[i], i);
}
TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);
return root;
}
```
=== "Go"
```go title="build_tree.go"
/* 構建二元樹:分治 */
func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {
// 子樹區間為空時終止
if r-l < 0 {
return nil
}
// 初始化根節點
root := NewTreeNode(preorder[i])
// 查詢 m ,從而劃分左右子樹
m := inorderMap[preorder[i]]
// 子問題:構建左子樹
root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)
// 子問題:構建右子樹
root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)
// 返回根節點
return root
}
/* 構建二元樹 */
func buildTree(preorder, inorder []int) *TreeNode {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
inorderMap := make(map[int]int, len(inorder))
for i := 0; i < len(inorder); i++ {
inorderMap[inorder[i]] = i
}
root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)
return root
}
```
=== "Swift"
```swift title="build_tree.swift"
/* 構建二元樹:分治 */
func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {
// 子樹區間為空時終止
if r - l < 0 {
return nil
}
// 初始化根節點
let root = TreeNode(x: preorder[i])
// 查詢 m ,從而劃分左右子樹
let m = inorderMap[preorder[i]]!
// 子問題:構建左子樹
root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)
// 子問題:構建右子樹
root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)
// 返回根節點
return root
}
/* 構建二元樹 */
func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)
}
```
=== "JS"
```javascript title="build_tree.js"
/* 構建二元樹:分治 */
function dfs(preorder, inorderMap, i, l, r) {
// 子樹區間為空時終止
if (r - l < 0) return null;
// 初始化根節點
const root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
const m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
function buildTree(preorder, inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = new Map();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "TS"
```typescript title="build_tree.ts"
/* 構建二元樹:分治 */
function dfs(
preorder: number[],
inorderMap: Map<number, number>,
i: number,
l: number,
r: number
): TreeNode | null {
// 子樹區間為空時終止
if (r - l < 0) return null;
// 初始化根節點
const root: TreeNode = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
const m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = new Map<number, number>();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "Dart"
```dart title="build_tree.dart"
/* 構建二元樹:分治 */
TreeNode? dfs(
List<int> preorder,
Map<int, int> inorderMap,
int i,
int l,
int r,
) {
// 子樹區間為空時終止
if (r - l < 0) {
return null;
}
// 初始化根節點
TreeNode? root = TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]]!;
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode? buildTree(List<int> preorder, List<int> inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Map<int, int> inorderMap = {};
for (int i = 0; i < inorder.length; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "Rust"
```rust title="build_tree.rs"
/* 構建二元樹:分治 */
fn dfs(
preorder: &[i32],
inorder_map: &HashMap<i32, i32>,
i: i32,
l: i32,
r: i32,
) -> Option<Rc<RefCell<TreeNode>>> {
// 子樹區間為空時終止
if r - l < 0 {
return None;
}
// 初始化根節點
let root = TreeNode::new(preorder[i as usize]);
// 查詢 m ,從而劃分左右子樹
let m = inorder_map.get(&preorder[i as usize]).unwrap();
// 子問題:構建左子樹
root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);
// 子問題:構建右子樹
root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);
// 返回根節點
Some(root)
}
/* 構建二元樹 */
fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let mut inorder_map: HashMap<i32, i32> = HashMap::new();
for i in 0..inorder.len() {
inorder_map.insert(inorder[i], i as i32);
}
let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);
root
}
```
=== "C"
```c title="build_tree.c"
/* 構建二元樹:分治 */
TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {
// 子樹區間為空時終止
if (r - l < 0)
return NULL;
// 初始化根節點
TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));
root->val = preorder[i];
root->left = NULL;
root->right = NULL;
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);
// 子問題:構建右子樹
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);
for (int i = 0; i < inorderSize; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);
free(inorderMap);
return root;
}
```
=== "Kotlin"
```kotlin title="build_tree.kt"
/* 構建二元樹:分治 */
fun dfs(preorder: IntArray, inorderMap: Map<Int?, Int?>, i: Int, l: Int, r: Int): TreeNode? {
// 子樹區間為空時終止
if (r - l < 0) return null
// 初始化根節點
val root = TreeNode(preorder[i])
// 查詢 m ,從而劃分左右子樹
val m = inorderMap[preorder[i]]!!
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)
// 返回根節點
return root
}
/* 構建二元樹 */
fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
val inorderMap: MutableMap<Int?, Int?> = HashMap()
for (i in inorder.indices) {
inorderMap[inorder[i]] = i
}
val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)
return root
}
```
=== "Ruby"
```ruby title="build_tree.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{build_tree}
```
=== "Zig"
```zig title="build_tree.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D,%0A%20%20%20%20inorder_map%3A%20dict%5Bint,%20int%5D,%0A%20%20%20%20i%3A%20int,%0A%20%20%20%20l%3A%20int,%0A%20%20%20%20r%3A%20int,%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A0%91%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E7%BB%88%E6%AD%A2%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%20m%20%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%88%92%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A0%91%0A%20%20%20%20root.left%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201,%20l,%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20root.right%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201%20%2B%20m%20-%20l,%20m%20%2B%201,%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D,%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E5%AD%98%E5%82%A8%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E6%98%A0%E5%B0%84%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i,%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder,%20inorder_map,%200,%200,%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3,%209,%202,%201,%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9,%203,%201,%202,%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder,%20inorder%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D,%0A%20%20%20%20inorder_map%3A%20dict%5Bint,%20int%5D,%0A%20%20%20%20i%3A%20int,%0A%20%20%20%20l%3A%20int,%0A%20%20%20%20r%3A%20int,%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A0%91%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E7%BB%88%E6%AD%A2%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%20m%20%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%88%92%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A0%91%0A%20%20%20%20root.left%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201,%20l,%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20root.right%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201%20%2B%20m%20-%20l,%20m%20%2B%201,%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D,%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E5%AD%98%E5%82%A8%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E6%98%A0%E5%B0%84%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i,%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder,%20inorder_map,%200,%200,%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3,%209,%202,%201,%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9,%203,%201,%202,%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder,%20inorder%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
圖 12-8 展示了構建二元樹的遞迴過程,各個節點是在向下“遞”的過程中建立的,而各條邊(引用)是在向上“迴”的過程中建立的。
=== "<1>"
![構建二元樹的遞迴過程](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" }
=== "<2>"
![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png){ class="animation-figure" }
=== "<3>"
![built_tree_step3](build_binary_tree_problem.assets/built_tree_step3.png){ class="animation-figure" }
=== "<4>"
![built_tree_step4](build_binary_tree_problem.assets/built_tree_step4.png){ class="animation-figure" }
=== "<5>"
![built_tree_step5](build_binary_tree_problem.assets/built_tree_step5.png){ class="animation-figure" }
=== "<6>"
![built_tree_step6](build_binary_tree_problem.assets/built_tree_step6.png){ class="animation-figure" }
=== "<7>"
![built_tree_step7](build_binary_tree_problem.assets/built_tree_step7.png){ class="animation-figure" }
=== "<8>"
![built_tree_step8](build_binary_tree_problem.assets/built_tree_step8.png){ class="animation-figure" }
=== "<9>"
![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png){ class="animation-figure" }
<p align="center"> 圖 12-8 &nbsp; 構建二元樹的遞迴過程 </p>
每個遞迴函式內的前序走訪 `preorder` 和中序走訪 `inorder` 的劃分結果如圖 12-9 所示。
![每個遞迴函式中的劃分結果](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" }
<p align="center"> 圖 12-9 &nbsp; 每個遞迴函式中的劃分結果 </p>
設樹的節點數量為 $n$ ,初始化每一個節點(執行一個遞迴函式 `dfs()` )使用 $O(1)$ 時間。**因此總體時間複雜度為 $O(n)$** 。
雜湊表儲存 `inorder` 元素到索引的對映,空間複雜度為 $O(n)$ 。在最差情況下,即二元樹退化為鏈結串列時,遞迴深度達到 $n$ ,使用 $O(n)$ 的堆疊幀空間。**因此總體空間複雜度為 $O(n)$** 。
@@ -0,0 +1,101 @@
---
comments: true
---
# 12.1 &nbsp; 分治演算法
<u>分治(divide and conquer</u>,全稱分而治之,是一種非常重要且常見的演算法策略。分治通常基於遞迴實現,包括“分”和“治”兩個步驟。
1. **分(劃分階段)**:遞迴地將原問題分解為兩個或多個子問題,直至到達最小子問題時終止。
2. **治(合併階段)**:從已知解的最小子問題開始,從底至頂地將子問題的解進行合併,從而構建出原問題的解。
如圖 12-1 所示,“合併排序”是分治策略的典型應用之一。
1. **分**:遞迴地將原陣列(原問題)劃分為兩個子陣列(子問題),直到子陣列只剩一個元素(最小子問題)。
2. **治**:從底至頂地將有序的子陣列(子問題的解)進行合併,從而得到有序的原陣列(原問題的解)。
![合併排序的分治策略](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" }
<p align="center"> 圖 12-1 &nbsp; 合併排序的分治策略 </p>
## 12.1.1 &nbsp; 如何判斷分治問題
一個問題是否適合使用分治解決,通常可以參考以下幾個判斷依據。
1. **問題可以分解**:原問題可以分解成規模更小、類似的子問題,以及能夠以相同方式遞迴地進行劃分。
2. **子問題是獨立的**:子問題之間沒有重疊,互不依賴,可以獨立解決。
3. **子問題的解可以合併**:原問題的解透過合併子問題的解得來。
顯然,合併排序滿足以上三個判斷依據。
1. **問題可以分解**:遞迴地將陣列(原問題)劃分為兩個子陣列(子問題)。
2. **子問題是獨立的**:每個子陣列都可以獨立地進行排序(子問題可以獨立進行求解)。
3. **子問題的解可以合併**:兩個有序子陣列(子問題的解)可以合併為一個有序陣列(原問題的解)。
## 12.1.2 &nbsp; 透過分治提升效率
**分治不僅可以有效地解決演算法問題,往往還可以提升演算法效率**。在排序演算法中,快速排序、合併排序、堆積排序相較於選擇、冒泡、插入排序更快,就是因為它們應用了分治策略。
那麼,我們不禁發問:**為什麼分治可以提升演算法效率,其底層邏輯是什麼**?換句話說,將大問題分解為多個子問題、解決子問題、將子問題的解合併為原問題的解,這幾步的效率為什麼比直接解決原問題的效率更高?這個問題可以從操作數量和平行計算兩方面來討論。
### 1. &nbsp; 操作數量最佳化
以“泡沫排序”為例,其處理一個長度為 $n$ 的陣列需要 $O(n^2)$ 時間。假設我們按照圖 12-2 所示的方式,將陣列從中點處分為兩個子陣列,則劃分需要 $O(n)$ 時間,排序每個子陣列需要 $O((n / 2)^2)$ 時間,合併兩個子陣列需要 $O(n)$ 時間,總體時間複雜度為:
$$
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
$$
![劃分陣列前後的泡沫排序](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png){ class="animation-figure" }
<p align="center"> 圖 12-2 &nbsp; 劃分陣列前後的泡沫排序 </p>
接下來,我們計算以下不等式,其左邊和右邊分別為劃分前和劃分後的操作總數:
$$
\begin{aligned}
n^2 & > \frac{n^2}{2} + 2n \newline
n^2 - \frac{n^2}{2} - 2n & > 0 \newline
n(n - 4) & > 0
\end{aligned}
$$
**這意味著當 $n > 4$ 時,劃分後的操作數量更少,排序效率應該更高**。請注意,劃分後的時間複雜度仍然是平方階 $O(n^2)$ ,只是複雜度中的常數項變小了。
進一步想,**如果我們把子陣列不斷地再從中點處劃分為兩個子陣列**,直至子陣列只剩一個元素時停止劃分呢?這種思路實際上就是“合併排序”,時間複雜度為 $O(n \log n)$ 。
再思考,**如果我們多設定幾個劃分點**,將原陣列平均劃分為 $k$ 個子陣列呢?這種情況與“桶排序”非常類似,它非常適合排序海量資料,理論上時間複雜度可以達到 $O(n + k)$ 。
### 2. &nbsp; 平行計算最佳化
我們知道,分治生成的子問題是相互獨立的,**因此通常可以並行解決**。也就是說,分治不僅可以降低演算法的時間複雜度,**還有利於作業系統的並行最佳化**。
並行最佳化在多核或多處理器的環境中尤其有效,因為系統可以同時處理多個子問題,更加充分地利用計算資源,從而顯著減少總體的執行時間。
比如在圖 12-3 所示的“桶排序”中,我們將海量的資料平均分配到各個桶中,則可所有桶的排序任務分散到各個計算單元,完成後再合併結果。
![桶排序的平行計算](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png){ class="animation-figure" }
<p align="center"> 圖 12-3 &nbsp; 桶排序的平行計算 </p>
## 12.1.3 &nbsp; 分治常見應用
一方面,分治可以用來解決許多經典演算法問題。
- **尋找最近點對**:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。
- **大整數乘法**:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。
- **矩陣乘法**:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。
- **河內塔問題**:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。
- **求解逆序對**:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。
另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。
- **二分搜尋**:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。
- **合併排序**:本節開頭已介紹,不再贅述。
- **快速排序**:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。
- **桶排序**:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。
- **樹**:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。
- **堆積**:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。
- **雜湊表**:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。
可以看出,**分治是一種“潤物細無聲”的演算法思想**,隱含在各種演算法與資料結構之中。
@@ -0,0 +1,545 @@
---
comments: true
---
# 12.4 &nbsp; 河內塔問題
在合併排序和構建二元樹中,我們都是將原問題分解為兩個規模為原問題一半的子問題。然而對於河內塔問題,我們採用不同的分解策略。
!!! question
給定三根柱子,記為 `A``B``C` 。起始狀態下,柱子 `A` 上套著 $n$ 個圓盤,它們從上到下按照從小到大的順序排列。我們的任務是要把這 $n$ 個圓盤移到柱子 `C` 上,並保持它們的原有順序不變(如圖 12-10 所示)。在移動圓盤的過程中,需要遵守以下規則。
1. 圓盤只能從一根柱子頂部拿出,從另一根柱子頂部放入。
2. 每次只能移動一個圓盤。
3. 小圓盤必須時刻位於大圓盤之上。
![河內塔問題示例](hanota_problem.assets/hanota_example.png){ class="animation-figure" }
<p align="center"> 圖 12-10 &nbsp; 河內塔問題示例 </p>
**我們將規模為 $i$ 的河內塔問題記作 $f(i)$** 。例如 $f(3)$ 代表將 $3$ 個圓盤從 `A` 移動至 `C` 的河內塔問題。
### 1. &nbsp; 考慮基本情況
如圖 12-11 所示,對於問題 $f(1)$ ,即當只有一個圓盤時,我們將它直接從 `A` 移動至 `C` 即可。
=== "<1>"
![規模為 1 的問題的解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png){ class="animation-figure" }
<p align="center"> 圖 12-11 &nbsp; 規模為 1 的問題的解 </p>
如圖 12-12 所示,對於問題 $f(2)$ ,即當有兩個圓盤時,**由於要時刻滿足小圓盤在大圓盤之上,因此需要藉助 `B` 來完成移動**。
1. 先將上面的小圓盤從 `A` 移至 `B`
2. 再將大圓盤從 `A` 移至 `C`
3. 最後將小圓盤從 `B` 移至 `C`
=== "<1>"
![規模為 2 的問題的解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png){ class="animation-figure" }
=== "<3>"
![hanota_f2_step3](hanota_problem.assets/hanota_f2_step3.png){ class="animation-figure" }
=== "<4>"
![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png){ class="animation-figure" }
<p align="center"> 圖 12-12 &nbsp; 規模為 2 的問題的解 </p>
解決問題 $f(2)$ 的過程可總結為:**將兩個圓盤藉助 `B``A` 移至 `C`** 。其中,`C` 稱為目標柱、`B` 稱為緩衝柱。
### 2. &nbsp; 子問題分解
對於問題 $f(3)$ ,即當有三個圓盤時,情況變得稍微複雜了一些。
因為已知 $f(1)$ 和 $f(2)$ 的解,所以我們可從分治角度思考,**將 `A` 頂部的兩個圓盤看作一個整體**,執行圖 12-13 所示的步驟。這樣三個圓盤就被順利地從 `A` 移至 `C` 了。
1.`B` 為目標柱、`C` 為緩衝柱,將兩個圓盤從 `A` 移至 `B`
2.`A` 中剩餘的一個圓盤從 `A` 直接移動至 `C`
3.`C` 為目標柱、`A` 為緩衝柱,將兩個圓盤從 `B` 移至 `C`
=== "<1>"
![規模為 3 的問題的解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png){ class="animation-figure" }
=== "<3>"
![hanota_f3_step3](hanota_problem.assets/hanota_f3_step3.png){ class="animation-figure" }
=== "<4>"
![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png){ class="animation-figure" }
<p align="center"> 圖 12-13 &nbsp; 規模為 3 的問題的解 </p>
從本質上看,**我們將問題 $f(3)$ 劃分為兩個子問題 $f(2)$ 和一個子問題 $f(1)$** 。按順序解決這三個子問題之後,原問題隨之得到解決。這說明子問題是獨立的,而且解可以合併。
至此,我們可總結出圖 12-14 所示的解決河內塔問題的分治策略:將原問題 $f(n)$ 劃分為兩個子問題 $f(n-1)$ 和一個子問題 $f(1)$ ,並按照以下順序解決這三個子問題。
1. 將 $n-1$ 個圓盤藉助 `C``A` 移至 `B`
2. 將剩餘 $1$ 個圓盤從 `A` 直接移至 `C`
3. 將 $n-1$ 個圓盤藉助 `A``B` 移至 `C`
對於這兩個子問題 $f(n-1)$ ,**可以透過相同的方式進行遞迴劃分**,直至達到最小子問題 $f(1)$ 。而 $f(1)$ 的解是已知的,只需一次移動操作即可。
![解決河內塔問題的分治策略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" }
<p align="center"> 圖 12-14 &nbsp; 解決河內塔問題的分治策略 </p>
### 3. &nbsp; 程式碼實現
在程式碼中,我們宣告一個遞迴函式 `dfs(i, src, buf, tar)` ,它的作用是將柱 `src` 頂部的 $i$ 個圓盤藉助緩衝柱 `buf` 移動至目標柱 `tar`
=== "Python"
```python title="hanota.py"
def move(src: list[int], tar: list[int]):
"""移動一個圓盤"""
# 從 src 頂部拿出一個圓盤
pan = src.pop()
# 將圓盤放入 tar 頂部
tar.append(pan)
def dfs(i: int, src: list[int], buf: list[int], tar: list[int]):
"""求解河內塔問題 f(i)"""
# 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1:
move(src, tar)
return
# 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf)
# 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar)
# 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar)
def solve_hanota(A: list[int], B: list[int], C: list[int]):
"""求解河內塔問題"""
n = len(A)
# 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C)
```
=== "C++"
```cpp title="hanota.cpp"
/* 移動一個圓盤 */
void move(vector<int> &src, vector<int> &tar) {
// 從 src 頂部拿出一個圓盤
int pan = src.back();
src.pop_back();
// 將圓盤放入 tar 頂部
tar.push_back(pan);
}
/* 求解河內塔問題 f(i) */
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
int n = A.size();
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "Java"
```java title="hanota.java"
/* 移動一個圓盤 */
void move(List<Integer> src, List<Integer> tar) {
// 從 src 頂部拿出一個圓盤
Integer pan = src.remove(src.size() - 1);
// 將圓盤放入 tar 頂部
tar.add(pan);
}
/* 求解河內塔問題 f(i) */
void dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {
int n = A.size();
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "C#"
```csharp title="hanota.cs"
/* 移動一個圓盤 */
void Move(List<int> src, List<int> tar) {
// 從 src 頂部拿出一個圓盤
int pan = src[^1];
src.RemoveAt(src.Count - 1);
// 將圓盤放入 tar 頂部
tar.Add(pan);
}
/* 求解河內塔問題 f(i) */
void DFS(int i, List<int> src, List<int> buf, List<int> tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
Move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
DFS(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
Move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
DFS(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void SolveHanota(List<int> A, List<int> B, List<int> C) {
int n = A.Count;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
DFS(n, A, B, C);
}
```
=== "Go"
```go title="hanota.go"
/* 移動一個圓盤 */
func move(src, tar *list.List) {
// 從 src 頂部拿出一個圓盤
pan := src.Back()
// 將圓盤放入 tar 頂部
tar.PushBack(pan.Value)
// 移除 src 頂部圓盤
src.Remove(pan)
}
/* 求解河內塔問題 f(i) */
func dfsHanota(i int, src, buf, tar *list.List) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1 {
move(src, tar)
return
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfsHanota(i-1, src, tar, buf)
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar)
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfsHanota(i-1, buf, src, tar)
}
/* 求解河內塔問題 */
func solveHanota(A, B, C *list.List) {
n := A.Len()
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfsHanota(n, A, B, C)
}
```
=== "Swift"
```swift title="hanota.swift"
/* 移動一個圓盤 */
func move(src: inout [Int], tar: inout [Int]) {
// 從 src 頂部拿出一個圓盤
let pan = src.popLast()!
// 將圓盤放入 tar 頂部
tar.append(pan)
}
/* 求解河內塔問題 f(i) */
func dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1 {
move(src: &src, tar: &tar)
return
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src: &src, tar: &tar)
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)
}
/* 求解河內塔問題 */
func solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {
let n = A.count
// 串列尾部是柱子頂部
// 將 src 頂部 n 個圓盤藉助 B 移到 C
dfs(i: n, src: &A, buf: &B, tar: &C)
}
```
=== "JS"
```javascript title="hanota.js"
/* 移動一個圓盤 */
function move(src, tar) {
// 從 src 頂部拿出一個圓盤
const pan = src.pop();
// 將圓盤放入 tar 頂部
tar.push(pan);
}
/* 求解河內塔問題 f(i) */
function dfs(i, src, buf, tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i === 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
function solveHanota(A, B, C) {
const n = A.length;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "TS"
```typescript title="hanota.ts"
/* 移動一個圓盤 */
function move(src: number[], tar: number[]): void {
// 從 src 頂部拿出一個圓盤
const pan = src.pop();
// 將圓盤放入 tar 頂部
tar.push(pan);
}
/* 求解河內塔問題 f(i) */
function dfs(i: number, src: number[], buf: number[], tar: number[]): void {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i === 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
function solveHanota(A: number[], B: number[], C: number[]): void {
const n = A.length;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "Dart"
```dart title="hanota.dart"
/* 移動一個圓盤 */
void move(List<int> src, List<int> tar) {
// 從 src 頂部拿出一個圓盤
int pan = src.removeLast();
// 將圓盤放入 tar 頂部
tar.add(pan);
}
/* 求解河內塔問題 f(i) */
void dfs(int i, List<int> src, List<int> buf, List<int> tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void solveHanota(List<int> A, List<int> B, List<int> C) {
int n = A.length;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "Rust"
```rust title="hanota.rs"
/* 移動一個圓盤 */
fn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {
// 從 src 頂部拿出一個圓盤
let pan = src.remove(src.len() - 1);
// 將圓盤放入 tar 頂部
tar.push(pan);
}
/* 求解河內塔問題 f(i) */
fn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1 {
move_pan(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move_pan(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
fn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {
let n = A.len() as i32;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "C"
```c title="hanota.c"
/* 移動一個圓盤 */
void move(int *src, int *srcSize, int *tar, int *tarSize) {
// 從 src 頂部拿出一個圓盤
int pan = src[*srcSize - 1];
src[*srcSize - 1] = 0;
(*srcSize)--;
// 將圓盤放入 tar 頂部
tar[*tarSize] = pan;
(*tarSize)++;
}
/* 求解河內塔問題 f(i) */
void dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, srcSize, tar, tarSize);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, srcSize, tar, tarSize);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);
}
/* 求解河內塔問題 */
void solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(*ASize, A, ASize, B, BSize, C, CSize);
}
```
=== "Kotlin"
```kotlin title="hanota.kt"
/* 移動一個圓盤 */
fun move(src: MutableList<Int>, tar: MutableList<Int>) {
// 從 src 頂部拿出一個圓盤
val pan: Int = src.removeAt(src.size - 1)
// 將圓盤放入 tar 頂部
tar.add(pan)
}
/* 求解河內塔問題 f(i) */
fun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar)
return
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf)
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar)
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar)
}
/* 求解河內塔問題 */
fun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {
val n = A.size
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C)
}
```
=== "Ruby"
```ruby title="hanota.rb"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solve_hanota}
```
=== "Zig"
```zig title="hanota.zig"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%22%22%22%0A%20%20%20%20%23%20%E4%BB%8E%20src%20%E9%A1%B6%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%86%E5%9C%86%E7%9B%98%E6%94%BE%E5%85%A5%20tar%20%E9%A1%B6%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int,%20src%3A%20list%5Bint%5D,%20buf%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%20src%20%E5%8F%AA%E5%89%A9%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E5%B0%86%E5%85%B6%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20src%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201,%20src,%20tar,%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%281%29%20%EF%BC%9A%E5%B0%86%20src%20%E5%89%A9%E4%BD%99%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20buf%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201,%20buf,%20src,%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D,%20B%3A%20list%5Bint%5D,%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%86%20A%20%E9%A1%B6%E9%83%A8%20n%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n,%20A,%20B,%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%97%E8%A1%A8%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A1%B6%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5,%204,%203,%202,%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%E4%B8%8B%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A,%20B,%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%86%E7%9B%98%E7%A7%BB%E5%8A%A8%E5%AE%8C%E6%88%90%E5%90%8E%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%22%22%22%0A%20%20%20%20%23%20%E4%BB%8E%20src%20%E9%A1%B6%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%86%E5%9C%86%E7%9B%98%E6%94%BE%E5%85%A5%20tar%20%E9%A1%B6%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int,%20src%3A%20list%5Bint%5D,%20buf%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%20src%20%E5%8F%AA%E5%89%A9%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E5%B0%86%E5%85%B6%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20src%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201,%20src,%20tar,%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%281%29%20%EF%BC%9A%E5%B0%86%20src%20%E5%89%A9%E4%BD%99%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20buf%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201,%20buf,%20src,%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D,%20B%3A%20list%5Bint%5D,%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%86%20A%20%E9%A1%B6%E9%83%A8%20n%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n,%20A,%20B,%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%97%E8%A1%A8%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A1%B6%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5,%204,%203,%202,%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%E4%B8%8B%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A,%20B,%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%86%E7%9B%98%E7%A7%BB%E5%8A%A8%E5%AE%8C%E6%88%90%E5%90%8E%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
如圖 12-15 所示,河內塔問題形成一棵高度為 $n$ 的遞迴樹,每個節點代表一個子問題,對應一個開啟的 `dfs()` 函式,**因此時間複雜度為 $O(2^n)$ ,空間複雜度為 $O(n)$** 。
![河內塔問題的遞迴樹](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" }
<p align="center"> 圖 12-15 &nbsp; 河內塔問題的遞迴樹 </p>
!!! quote
河內塔問題源自一個古老的傳說。在古印度的一個寺廟裡,僧侶們有三根高大的鑽石柱子,以及 $64$ 個大小不一的金圓盤。僧侶們不斷地移動圓盤,他們相信在最後一個圓盤被正確放置的那一刻,這個世界就會結束。
然而,即使僧侶們每秒鐘移動一次,總共需要大約 $2^{64} \approx 1.84×10^{19}$ 秒,合約 $5850$ 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。
@@ -0,0 +1,22 @@
---
comments: true
icon: material/set-split
---
# 第 12 章 &nbsp; 分治
![分治](../assets/covers/chapter_divide_and_conquer.jpg){ class="cover-image" }
!!! abstract
難題被逐層拆解,每一次的拆解都使它變得更為簡單。
分而治之揭示了一個重要的事實:從簡單做起,一切都不再複雜。
## Chapter Contents
- [12.1 &nbsp; 分治演算法](https://www.hello-algo.com/en/chapter_divide_and_conquer/divide_and_conquer/)
- [12.2 &nbsp; 分治搜尋策略](https://www.hello-algo.com/en/chapter_divide_and_conquer/binary_search_recur/)
- [12.3 &nbsp; 構建樹問題](https://www.hello-algo.com/en/chapter_divide_and_conquer/build_binary_tree_problem/)
- [12.4 &nbsp; 河內塔問題](https://www.hello-algo.com/en/chapter_divide_and_conquer/hanota_problem/)
- [12.5 &nbsp; 小結](https://www.hello-algo.com/en/chapter_divide_and_conquer/summary/)
@@ -0,0 +1,15 @@
---
comments: true
---
# 12.5 &nbsp; 小結
- 分治是一種常見的演算法設計策略,包括分(劃分)和治(合併)兩個階段,通常基於遞迴實現。
- 判斷是否是分治演算法問題的依據包括:問題能否分解、子問題是否獨立、子問題能否合併。
- 合併排序是分治策略的典型應用,其遞迴地將陣列劃分為等長的兩個子陣列,直到只剩一個元素時開始逐層合併,從而完成排序。
- 引入分治策略往往可以提升演算法效率。一方面,分治策略減少了操作數量;另一方面,分治後有利於系統的並行最佳化。
- 分治既可以解決許多演算法問題,也廣泛應用於資料結構與演算法設計中,處處可見其身影。
- 相較於暴力搜尋,自適應搜尋效率更高。時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的。
- 二分搜尋是分治策略的另一個典型應用,它不包含將子問題的解進行合併的步驟。我們可以透過遞迴分治實現二分搜尋。
- 在構建二元樹的問題中,構建樹(原問題)可以劃分為構建左子樹和右子樹(子問題),這可以透過劃分前序走訪和中序走訪的索引區間來實現。
- 在河內塔問題中,一個規模為 $n$ 的問題可以劃分為兩個規模為 $n-1$ 的子問題和一個規模為 $1$ 的子問題。按順序解決這三個子問題後,原問題隨之得到解決。
@@ -0,0 +1,978 @@
---
comments: true
---
# 14.2 &nbsp; 動態規劃問題特性
在上一節中,我們學習了動態規劃是如何透過子問題分解來求解原問題的。實際上,子問題分解是一種通用的演算法思路,在分治、動態規劃、回溯中的側重點不同。
- 分治演算法遞迴地將原問題劃分為多個相互獨立的子問題,直至最小子問題,並在回溯中合併子問題的解,最終得到原問題的解。
- 動態規劃也對問題進行遞迴分解,但與分治演算法的主要區別是,動態規劃中的子問題是相互依賴的,在分解過程中會出現許多重疊子問題。
- 回溯演算法在嘗試和回退中窮舉所有可能的解,並透過剪枝避免不必要的搜尋分支。原問題的解由一系列決策步驟構成,我們可以將每個決策步驟之前的子序列看作一個子問題。
實際上,動態規劃常用來求解最最佳化問題,它們不僅包含重疊子問題,還具有另外兩大特性:最優子結構、無後效性。
## 14.2.1 &nbsp; 最優子結構
我們對爬樓梯問題稍作改動,使之更加適合展示最優子結構概念。
!!! question "爬樓梯最小代價"
給定一個樓梯,你每步可以上 $1$ 階或者 $2$ 階,每一階樓梯上都貼有一個非負整數,表示你在該臺階所需要付出的代價。給定一個非負整數陣列 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 個臺階需要付出的代價,$cost[0]$ 為地面(起始點)。請計算最少需要付出多少代價才能到達頂部?
如圖 14-6 所示,若第 $1$、$2$、$3$ 階的代價分別為 $1$、$10$、$1$ ,則從地面爬到第 $3$ 階的最小代價為 $2$ 。
![爬到第 3 階的最小代價](dp_problem_features.assets/min_cost_cs_example.png){ class="animation-figure" }
<p align="center"> 圖 14-6 &nbsp; 爬到第 3 階的最小代價 </p>
設 $dp[i]$ 為爬到第 $i$ 階累計付出的代價,由於第 $i$ 階只可能從 $i - 1$ 階或 $i - 2$ 階走來,因此 $dp[i]$ 只可能等於 $dp[i - 1] + cost[i]$ 或 $dp[i - 2] + cost[i]$ 。為了儘可能減少代價,我們應該選擇兩者中較小的那一個:
$$
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
$$
這便可以引出最優子結構的含義:**原問題的最優解是從子問題的最優解構建得來的**。
本題顯然具有最優子結構:我們從兩個子問題最優解 $dp[i-1]$ 和 $dp[i-2]$ 中挑選出較優的那一個,並用它構建出原問題 $dp[i]$ 的最優解。
那麼,上一節的爬樓梯題目有沒有最優子結構呢?它的目標是求解方案數量,看似是一個計數問題,但如果換一種問法:“求解最大方案數量”。我們意外地發現,**雖然題目修改前後是等價的,但最優子結構浮現出來了**:第 $n$ 階最大方案數量等於第 $n-1$ 階和第 $n-2$ 階最大方案數量之和。所以說,最優子結構的解釋方式比較靈活,在不同問題中會有不同的含義。
根據狀態轉移方程,以及初始狀態 $dp[1] = cost[1]$ 和 $dp[2] = cost[2]$ ,我們就可以得到動態規劃程式碼:
=== "Python"
```python title="min_cost_climbing_stairs_dp.py"
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:
"""爬樓梯最小代價:動態規劃"""
n = len(cost) - 1
if n == 1 or n == 2:
return cost[n]
# 初始化 dp 表,用於儲存子問題的解
dp = [0] * (n + 1)
# 初始狀態:預設最小子問題的解
dp[1], dp[2] = cost[1], cost[2]
# 狀態轉移:從較小子問題逐步求解較大子問題
for i in range(3, n + 1):
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
return dp[n]
```
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
/* 爬樓梯最小代價:動態規劃 */
int minCostClimbingStairsDP(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
return cost[n];
// 初始化 dp 表,用於儲存子問題的解
vector<int> dp(n + 1);
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Java"
```java title="min_cost_climbing_stairs_dp.java"
/* 爬樓梯最小代價:動態規劃 */
int minCostClimbingStairsDP(int[] cost) {
int n = cost.length - 1;
if (n == 1 || n == 2)
return cost[n];
// 初始化 dp 表,用於儲存子問題的解
int[] dp = new int[n + 1];
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "C#"
```csharp title="min_cost_climbing_stairs_dp.cs"
/* 爬樓梯最小代價:動態規劃 */
int MinCostClimbingStairsDP(int[] cost) {
int n = cost.Length - 1;
if (n == 1 || n == 2)
return cost[n];
// 初始化 dp 表,用於儲存子問題的解
int[] dp = new int[n + 1];
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Go"
```go title="min_cost_climbing_stairs_dp.go"
/* 爬樓梯最小代價:動態規劃 */
func minCostClimbingStairsDP(cost []int) int {
n := len(cost) - 1
if n == 1 || n == 2 {
return cost[n]
}
min := func(a, b int) int {
if a < b {
return a
}
return b
}
// 初始化 dp 表,用於儲存子問題的解
dp := make([]int, n+1)
// 初始狀態:預設最小子問題的解
dp[1] = cost[1]
dp[2] = cost[2]
// 狀態轉移:從較小子問題逐步求解較大子問題
for i := 3; i <= n; i++ {
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
}
return dp[n]
}
```
=== "Swift"
```swift title="min_cost_climbing_stairs_dp.swift"
/* 爬樓梯最小代價:動態規劃 */
func minCostClimbingStairsDP(cost: [Int]) -> Int {
let n = cost.count - 1
if n == 1 || n == 2 {
return cost[n]
}
// 初始化 dp 表,用於儲存子問題的解
var dp = Array(repeating: 0, count: n + 1)
// 初始狀態:預設最小子問題的解
dp[1] = cost[1]
dp[2] = cost[2]
// 狀態轉移:從較小子問題逐步求解較大子問題
for i in 3 ... n {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
}
return dp[n]
}
```
=== "JS"
```javascript title="min_cost_climbing_stairs_dp.js"
/* 爬樓梯最小代價:動態規劃 */
function minCostClimbingStairsDP(cost) {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
// 初始化 dp 表,用於儲存子問題的解
const dp = new Array(n + 1);
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (let i = 3; i <= n; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "TS"
```typescript title="min_cost_climbing_stairs_dp.ts"
/* 爬樓梯最小代價:動態規劃 */
function minCostClimbingStairsDP(cost: Array<number>): number {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
// 初始化 dp 表,用於儲存子問題的解
const dp = new Array(n + 1);
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (let i = 3; i <= n; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Dart"
```dart title="min_cost_climbing_stairs_dp.dart"
/* 爬樓梯最小代價:動態規劃 */
int minCostClimbingStairsDP(List<int> cost) {
int n = cost.length - 1;
if (n == 1 || n == 2) return cost[n];
// 初始化 dp 表,用於儲存子問題的解
List<int> dp = List.filled(n + 1, 0);
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
/* 爬樓梯最小代價:動態規劃 */
fn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {
let n = cost.len() - 1;
if n == 1 || n == 2 {
return cost[n];
}
// 初始化 dp 表,用於儲存子問題的解
let mut dp = vec![-1; n + 1];
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for i in 3..=n {
dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];
}
dp[n]
}
```
=== "C"
```c title="min_cost_climbing_stairs_dp.c"
/* 爬樓梯最小代價:動態規劃 */
int minCostClimbingStairsDP(int cost[], int costSize) {
int n = costSize - 1;
if (n == 1 || n == 2)
return cost[n];
// 初始化 dp 表,用於儲存子問題的解
int *dp = calloc(n + 1, sizeof(int));
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];
}
int res = dp[n];
// 釋放記憶體
free(dp);
return res;
}
```
=== "Kotlin"
```kotlin title="min_cost_climbing_stairs_dp.kt"
/* 爬樓梯最小代價:動態規劃 */
fun minCostClimbingStairsDP(cost: IntArray): Int {
val n = cost.size - 1
if (n == 1 || n == 2) return cost[n]
// 初始化 dp 表,用於儲存子問題的解
val dp = IntArray(n + 1)
// 初始狀態:預設最小子問題的解
dp[1] = cost[1]
dp[2] = cost[2]
// 狀態轉移:從較小子問題逐步求解較大子問題
for (i in 3..n) {
dp[i] = (min(dp[i - 1].toDouble(), dp[i - 2].toDouble()) + cost[i]).toInt()
}
return dp[n]
}
```
=== "Ruby"
```ruby title="min_cost_climbing_stairs_dp.rb"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```
=== "Zig"
```zig title="min_cost_climbing_stairs_dp.zig"
// 爬樓梯最小代價:動態規劃
fn minCostClimbingStairsDP(comptime cost: []i32) i32 {
comptime var n = cost.len - 1;
if (n == 1 or n == 2) {
return cost[n];
}
// 初始化 dp 表,用於儲存子問題的解
var dp = [_]i32{-1} ** (n + 1);
// 初始狀態:預設最小子問題的解
dp[1] = cost[1];
dp[2] = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (3..n + 1) |i| {
dp[i] = @min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28n%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D,%20dp%5B2%5D%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%20%3D%20min%28dp%5Bi%20-%201%5D,%20dp%5Bi%20-%202%5D%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20dp%5Bn%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28n%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D,%20dp%5B2%5D%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%20%3D%20min%28dp%5Bi%20-%201%5D,%20dp%5Bi%20-%202%5D%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20dp%5Bn%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
圖 14-7 展示了以上程式碼的動態規劃過程。
![爬樓梯最小代價的動態規劃過程](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" }
<p align="center"> 圖 14-7 &nbsp; 爬樓梯最小代價的動態規劃過程 </p>
本題也可以進行空間最佳化,將一維壓縮至零維,使得空間複雜度從 $O(n)$ 降至 $O(1)$
=== "Python"
```python title="min_cost_climbing_stairs_dp.py"
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:
"""爬樓梯最小代價:空間最佳化後的動態規劃"""
n = len(cost) - 1
if n == 1 or n == 2:
return cost[n]
a, b = cost[1], cost[2]
for i in range(3, n + 1):
a, b = b, min(a, b) + cost[i]
return b
```
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
int minCostClimbingStairsDPComp(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Java"
```java title="min_cost_climbing_stairs_dp.java"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
int minCostClimbingStairsDPComp(int[] cost) {
int n = cost.length - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = Math.min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "C#"
```csharp title="min_cost_climbing_stairs_dp.cs"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
int MinCostClimbingStairsDPComp(int[] cost) {
int n = cost.Length - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = Math.Min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Go"
```go title="min_cost_climbing_stairs_dp.go"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
func minCostClimbingStairsDPComp(cost []int) int {
n := len(cost) - 1
if n == 1 || n == 2 {
return cost[n]
}
min := func(a, b int) int {
if a < b {
return a
}
return b
}
// 初始狀態:預設最小子問題的解
a, b := cost[1], cost[2]
// 狀態轉移:從較小子問題逐步求解較大子問題
for i := 3; i <= n; i++ {
tmp := b
b = min(a, tmp) + cost[i]
a = tmp
}
return b
}
```
=== "Swift"
```swift title="min_cost_climbing_stairs_dp.swift"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
func minCostClimbingStairsDPComp(cost: [Int]) -> Int {
let n = cost.count - 1
if n == 1 || n == 2 {
return cost[n]
}
var (a, b) = (cost[1], cost[2])
for i in 3 ... n {
(a, b) = (b, min(a, b) + cost[i])
}
return b
}
```
=== "JS"
```javascript title="min_cost_climbing_stairs_dp.js"
/* 爬樓梯最小代價:狀態壓縮後的動態規劃 */
function minCostClimbingStairsDPComp(cost) {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
let a = cost[1],
b = cost[2];
for (let i = 3; i <= n; i++) {
const tmp = b;
b = Math.min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "TS"
```typescript title="min_cost_climbing_stairs_dp.ts"
/* 爬樓梯最小代價:狀態壓縮後的動態規劃 */
function minCostClimbingStairsDPComp(cost: Array<number>): number {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
let a = cost[1],
b = cost[2];
for (let i = 3; i <= n; i++) {
const tmp = b;
b = Math.min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Dart"
```dart title="min_cost_climbing_stairs_dp.dart"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
int minCostClimbingStairsDPComp(List<int> cost) {
int n = cost.length - 1;
if (n == 1 || n == 2) return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
fn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {
let n = cost.len() - 1;
if n == 1 || n == 2 {
return cost[n];
};
let (mut a, mut b) = (cost[1], cost[2]);
for i in 3..=n {
let tmp = b;
b = cmp::min(a, tmp) + cost[i];
a = tmp;
}
b
}
```
=== "C"
```c title="min_cost_climbing_stairs_dp.c"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
int minCostClimbingStairsDPComp(int cost[], int costSize) {
int n = costSize - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = myMin(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Kotlin"
```kotlin title="min_cost_climbing_stairs_dp.kt"
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
fun minCostClimbingStairsDPComp(cost: IntArray): Int {
val n = cost.size - 1
if (n == 1 || n == 2) return cost[n]
var a = cost[1]
var b = cost[2]
for (i in 3..n) {
val tmp = b
b = (min(a.toDouble(), tmp.toDouble()) + cost[i]).toInt()
a = tmp
}
return b
}
```
=== "Ruby"
```ruby title="min_cost_climbing_stairs_dp.rb"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```
=== "Zig"
```zig title="min_cost_climbing_stairs_dp.zig"
// 爬樓梯最小代價:空間最佳化後的動態規劃
fn minCostClimbingStairsDPComp(cost: []i32) i32 {
var n = cost.len - 1;
if (n == 1 or n == 2) {
return cost[n];
}
var a = cost[1];
var b = cost[2];
// 狀態轉移:從較小子問題逐步求解較大子問題
for (3..n + 1) |i| {
var tmp = b;
b = @min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
??? pythontutor "視覺化執行"
<div style="height: 513px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp_comp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20a,%20b%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20a,%20b%20%3D%20b,%20min%28a,%20b%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20b%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp_comp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp_comp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20a,%20b%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20a,%20b%20%3D%20b,%20min%28a,%20b%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20b%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp_comp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 14.2.2 &nbsp; 無後效性
無後效性是動態規劃能夠有效解決問題的重要特性之一,其定義為:**給定一個確定的狀態,它的未來發展只與當前狀態有關,而與過去經歷的所有狀態無關**。
以爬樓梯問題為例,給定狀態 $i$ ,它會發展出狀態 $i+1$ 和狀態 $i+2$ ,分別對應跳 $1$ 步和跳 $2$ 步。在做出這兩種選擇時,我們無須考慮狀態 $i$ 之前的狀態,它們對狀態 $i$ 的未來沒有影響。
然而,如果我們給爬樓梯問題新增一個約束,情況就不一樣了。
!!! question "帶約束爬樓梯"
給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階,**但不能連續兩輪跳 $1$ 階**,請問有多少種方案可以爬到樓頂?
如圖 14-8 所示,爬上第 $3$ 階僅剩 $2$ 種可行方案,其中連續三次跳 $1$ 階的方案不滿足約束條件,因此被捨棄。
![帶約束爬到第 3 階的方案數量](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" }
<p align="center"> 圖 14-8 &nbsp; 帶約束爬到第 3 階的方案數量 </p>
在該問題中,如果上一輪是跳 $1$ 階上來的,那麼下一輪就必須跳 $2$ 階。這意味著,**下一步選擇不能由當前狀態(當前所在樓梯階數)獨立決定,還和前一個狀態(上一輪所在樓梯階數)有關**。
不難發現,此問題已不滿足無後效性,狀態轉移方程 $dp[i] = dp[i-1] + dp[i-2]$ 也失效了,因為 $dp[i-1]$ 代表本輪跳 $1$ 階,但其中包含了許多“上一輪是跳 $1$ 階上來的”方案,而為了滿足約束,我們就不能將 $dp[i-1]$ 直接計入 $dp[i]$ 中。
為此,我們需要擴展狀態定義:**狀態 $[i, j]$ 表示處在第 $i$ 階並且上一輪跳了 $j$ 階**,其中 $j \in \{1, 2\}$ 。此狀態定義有效地區分了上一輪跳了 $1$ 階還是 $2$ 階,我們可以據此判斷當前狀態是從何而來的。
- 當上一輪跳了 $1$ 階時,上上一輪只能選擇跳 $2$ 階,即 $dp[i, 1]$ 只能從 $dp[i-1, 2]$ 轉移過來。
- 當上一輪跳了 $2$ 階時,上上一輪可選擇跳 $1$ 階或跳 $2$ 階,即 $dp[i, 2]$ 可以從 $dp[i-2, 1]$ 或 $dp[i-2, 2]$ 轉移過來。
如圖 14-9 所示,在該定義下,$dp[i, j]$ 表示狀態 $[i, j]$ 對應的方案數。此時狀態轉移方程為:
$$
\begin{cases}
dp[i, 1] = dp[i-1, 2] \\
dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
\end{cases}
$$
![考慮約束下的遞推關係](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" }
<p align="center"> 圖 14-9 &nbsp; 考慮約束下的遞推關係 </p>
最終,返回 $dp[n, 1] + dp[n, 2]$ 即可,兩者之和代表爬到第 $n$ 階的方案總數:
=== "Python"
```python title="climbing_stairs_constraint_dp.py"
def climbing_stairs_constraint_dp(n: int) -> int:
"""帶約束爬樓梯:動態規劃"""
if n == 1 or n == 2:
return 1
# 初始化 dp 表,用於儲存子問題的解
dp = [[0] * 3 for _ in range(n + 1)]
# 初始狀態:預設最小子問題的解
dp[1][1], dp[1][2] = 1, 0
dp[2][1], dp[2][2] = 0, 1
# 狀態轉移:從較小子問題逐步求解較大子問題
for i in range(3, n + 1):
dp[i][1] = dp[i - 1][2]
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
return dp[n][1] + dp[n][2]
```
=== "C++"
```cpp title="climbing_stairs_constraint_dp.cpp"
/* 帶約束爬樓梯:動態規劃 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "Java"
```java title="climbing_stairs_constraint_dp.java"
/* 帶約束爬樓梯:動態規劃 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
int[][] dp = new int[n + 1][3];
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "C#"
```csharp title="climbing_stairs_constraint_dp.cs"
/* 帶約束爬樓梯:動態規劃 */
int ClimbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
int[,] dp = new int[n + 1, 3];
// 初始狀態:預設最小子問題的解
dp[1, 1] = 1;
dp[1, 2] = 0;
dp[2, 1] = 0;
dp[2, 2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i, 1] = dp[i - 1, 2];
dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];
}
return dp[n, 1] + dp[n, 2];
}
```
=== "Go"
```go title="climbing_stairs_constraint_dp.go"
/* 帶約束爬樓梯:動態規劃 */
func climbingStairsConstraintDP(n int) int {
if n == 1 || n == 2 {
return 1
}
// 初始化 dp 表,用於儲存子問題的解
dp := make([][3]int, n+1)
// 初始狀態:預設最小子問題的解
dp[1][1] = 1
dp[1][2] = 0
dp[2][1] = 0
dp[2][2] = 1
// 狀態轉移:從較小子問題逐步求解較大子問題
for i := 3; i <= n; i++ {
dp[i][1] = dp[i-1][2]
dp[i][2] = dp[i-2][1] + dp[i-2][2]
}
return dp[n][1] + dp[n][2]
}
```
=== "Swift"
```swift title="climbing_stairs_constraint_dp.swift"
/* 帶約束爬樓梯:動態規劃 */
func climbingStairsConstraintDP(n: Int) -> Int {
if n == 1 || n == 2 {
return 1
}
// 初始化 dp 表,用於儲存子問題的解
var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)
// 初始狀態:預設最小子問題的解
dp[1][1] = 1
dp[1][2] = 0
dp[2][1] = 0
dp[2][2] = 1
// 狀態轉移:從較小子問題逐步求解較大子問題
for i in 3 ... n {
dp[i][1] = dp[i - 1][2]
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
}
return dp[n][1] + dp[n][2]
}
```
=== "JS"
```javascript title="climbing_stairs_constraint_dp.js"
/* 帶約束爬樓梯:動態規劃 */
function climbingStairsConstraintDP(n) {
if (n === 1 || n === 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
const dp = Array.from(new Array(n + 1), () => new Array(3));
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (let i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "TS"
```typescript title="climbing_stairs_constraint_dp.ts"
/* 帶約束爬樓梯:動態規劃 */
function climbingStairsConstraintDP(n: number): number {
if (n === 1 || n === 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
const dp = Array.from({ length: n + 1 }, () => new Array(3));
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (let i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "Dart"
```dart title="climbing_stairs_constraint_dp.dart"
/* 帶約束爬樓梯:動態規劃 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "Rust"
```rust title="climbing_stairs_constraint_dp.rs"
/* 帶約束爬樓梯:動態規劃 */
fn climbing_stairs_constraint_dp(n: usize) -> i32 {
if n == 1 || n == 2 {
return 1;
};
// 初始化 dp 表,用於儲存子問題的解
let mut dp = vec![vec![-1; 3]; n + 1];
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for i in 3..=n {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
dp[n][1] + dp[n][2]
}
```
=== "C"
```c title="climbing_stairs_constraint_dp.c"
/* 帶約束爬樓梯:動態規劃 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
int **dp = malloc((n + 1) * sizeof(int *));
for (int i = 0; i <= n; i++) {
dp[i] = calloc(3, sizeof(int));
}
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
int res = dp[n][1] + dp[n][2];
// 釋放記憶體
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
free(dp);
return res;
}
```
=== "Kotlin"
```kotlin title="climbing_stairs_constraint_dp.kt"
/* 帶約束爬樓梯:動態規劃 */
fun climbingStairsConstraintDP(n: Int): Int {
if (n == 1 || n == 2) {
return 1
}
// 初始化 dp 表,用於儲存子問題的解
val dp = Array(n + 1) { IntArray(3) }
// 初始狀態:預設最小子問題的解
dp[1][1] = 1
dp[1][2] = 0
dp[2][1] = 0
dp[2][2] = 1
// 狀態轉移:從較小子問題逐步求解較大子問題
for (i in 3..n) {
dp[i][1] = dp[i - 1][2]
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
}
return dp[n][1] + dp[n][2]
}
```
=== "Ruby"
```ruby title="climbing_stairs_constraint_dp.rb"
[class]{}-[func]{climbing_stairs_constraint_dp}
```
=== "Zig"
```zig title="climbing_stairs_constraint_dp.zig"
// 帶約束爬樓梯:動態規劃
fn climbingStairsConstraintDP(comptime n: usize) i32 {
if (n == 1 or n == 2) {
return 1;
}
// 初始化 dp 表,用於儲存子問題的解
var dp = [_][3]i32{ [_]i32{ -1, -1, -1 } } ** (n + 1);
// 初始狀態:預設最小子問題的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 狀態轉移:從較小子問題逐步求解較大子問題
for (3..n + 1) |i| {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20climbing_stairs_constraint_dp%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B8%A6%E7%BA%A6%E6%9D%9F%E7%88%AC%E6%A5%BC%E6%A2%AF%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%203%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%5B1%5D,%20dp%5B1%5D%5B2%5D%20%3D%201,%200%0A%20%20%20%20dp%5B2%5D%5B1%5D,%20dp%5B2%5D%5B2%5D%20%3D%200,%201%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B1%5D%20%3D%20dp%5Bi%20-%201%5D%5B2%5D%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B2%5D%20%3D%20dp%5Bi%20-%202%5D%5B1%5D%20%2B%20dp%5Bi%20-%202%5D%5B2%5D%0A%20%20%20%20return%20dp%5Bn%5D%5B1%5D%20%2B%20dp%5Bn%5D%5B2%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%209%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_constraint_dp%28n%29%0A%20%20%20%20print%28f%22%E7%88%AC%20%7Bn%7D%20%E9%98%B6%E6%A5%BC%E6%A2%AF%E5%85%B1%E6%9C%89%20%7Bres%7D%20%E7%A7%8D%E6%96%B9%E6%A1%88%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20climbing_stairs_constraint_dp%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B8%A6%E7%BA%A6%E6%9D%9F%E7%88%AC%E6%A5%BC%E6%A2%AF%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%203%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%5B1%5D,%20dp%5B1%5D%5B2%5D%20%3D%201,%200%0A%20%20%20%20dp%5B2%5D%5B1%5D,%20dp%5B2%5D%5B2%5D%20%3D%200,%201%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B1%5D%20%3D%20dp%5Bi%20-%201%5D%5B2%5D%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B2%5D%20%3D%20dp%5Bi%20-%202%5D%5B1%5D%20%2B%20dp%5Bi%20-%202%5D%5B2%5D%0A%20%20%20%20return%20dp%5Bn%5D%5B1%5D%20%2B%20dp%5Bn%5D%5B2%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%209%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_constraint_dp%28n%29%0A%20%20%20%20print%28f%22%E7%88%AC%20%7Bn%7D%20%E9%98%B6%E6%A5%BC%E6%A2%AF%E5%85%B1%E6%9C%89%20%7Bres%7D%20%E7%A7%8D%E6%96%B9%E6%A1%88%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
在上面的案例中,由於僅需多考慮前面一個狀態,因此我們仍然可以透過擴展狀態定義,使得問題重新滿足無後效性。然而,某些問題具有非常嚴重的“有後效性”。
!!! question "爬樓梯與障礙生成"
給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階。**規定當爬到第 $i$ 階時,系統自動會在第 $2i$ 階上放上障礙物,之後所有輪都不允許跳到第 $2i$ 階上**。例如,前兩輪分別跳到了第 $2$、$3$ 階上,則之後就不能跳到第 $4$、$6$ 階上。請問有多少種方案可以爬到樓頂?
在這個問題中,下次跳躍依賴過去所有的狀態,因為每一次跳躍都會在更高的階梯上設定障礙,並影響未來的跳躍。對於這類問題,動態規劃往往難以解決。
實際上,許多複雜的組合最佳化問題(例如旅行商問題)不滿足無後效性。對於這類問題,我們通常會選擇使用其他方法,例如啟發式搜尋、遺傳演算法、強化學習等,從而在有限時間內得到可用的區域性最優解。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,996 @@
---
comments: true
---
# 14.6 &nbsp; 編輯距離問題
編輯距離,也稱 Levenshtein 距離,指兩個字串之間互相轉換的最少修改次數,通常用於在資訊檢索和自然語言處理中度量兩個序列的相似度。
!!! question
輸入兩個字串 $s$ 和 $t$ ,返回將 $s$ 轉換為 $t$ 所需的最少編輯步數。
你可以在一個字串中進行三種編輯操作:插入一個字元、刪除一個字元、將字元替換為任意一個字元。
如圖 14-27 所示,將 `kitten` 轉換為 `sitting` 需要編輯 3 步,包括 2 次替換操作與 1 次新增操作;將 `hello` 轉換為 `algo` 需要 3 步,包括 2 次替換操作和 1 次刪除操作。
![編輯距離的示例資料](edit_distance_problem.assets/edit_distance_example.png){ class="animation-figure" }
<p align="center"> 圖 14-27 &nbsp; 編輯距離的示例資料 </p>
**編輯距離問題可以很自然地用決策樹模型來解釋**。字串對應樹節點,一輪決策(一次編輯操作)對應樹的一條邊。
如圖 14-28 所示,在不限制操作的情況下,每個節點都可以派生出許多條邊,每條邊對應一種操作,這意味著從 `hello` 轉換到 `algo` 有許多種可能的路徑。
從決策樹的角度看,本題的目標是求解節點 `hello` 和節點 `algo` 之間的最短路徑。
![基於決策樹模型表示編輯距離問題](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" }
<p align="center"> 圖 14-28 &nbsp; 基於決策樹模型表示編輯距離問題 </p>
### 1. &nbsp; 動態規劃思路
**第一步:思考每輪的決策,定義狀態,從而得到 $dp$ 表**
每一輪的決策是對字串 $s$ 進行一次編輯操作。
我們希望在編輯操作的過程中,問題的規模逐漸縮小,這樣才能構建子問題。設字串 $s$ 和 $t$ 的長度分別為 $n$ 和 $m$ ,我們先考慮兩字串尾部的字元 $s[n-1]$ 和 $t[m-1]$ 。
- 若 $s[n-1]$ 和 $t[m-1]$ 相同,我們可以跳過它們,直接考慮 $s[n-2]$ 和 $t[m-2]$ 。
- 若 $s[n-1]$ 和 $t[m-1]$ 不同,我們需要對 $s$ 進行一次編輯(插入、刪除、替換),使得兩字串尾部的字元相同,從而可以跳過它們,考慮規模更小的問題。
也就是說,我們在字串 $s$ 中進行的每一輪決策(編輯操作),都會使得 $s$ 和 $t$ 中剩餘的待匹配字元發生變化。因此,狀態為當前在 $s$ 和 $t$ 中考慮的第 $i$ 和第 $j$ 個字元,記為 $[i, j]$ 。
狀態 $[i, j]$ 對應的子問題:**將 $s$ 的前 $i$ 個字元更改為 $t$ 的前 $j$ 個字元所需的最少編輯步數**。
至此,得到一個尺寸為 $(i+1) \times (j+1)$ 的二維 $dp$ 表。
**第二步:找出最優子結構,進而推導出狀態轉移方程**
考慮子問題 $dp[i, j]$ ,其對應的兩個字串的尾部字元為 $s[i-1]$ 和 $t[j-1]$ ,可根據不同編輯操作分為圖 14-29 所示的三種情況。
1. 在 $s[i-1]$ 之後新增 $t[j-1]$ ,則剩餘子問題 $dp[i, j-1]$ 。
2. 刪除 $s[i-1]$ ,則剩餘子問題 $dp[i-1, j]$ 。
3. 將 $s[i-1]$ 替換為 $t[j-1]$ ,則剩餘子問題 $dp[i-1, j-1]$ 。
![編輯距離的狀態轉移](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" }
<p align="center"> 圖 14-29 &nbsp; 編輯距離的狀態轉移 </p>
根據以上分析,可得最優子結構:$dp[i, j]$ 的最少編輯步數等於 $dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ 三者中的最少編輯步數,再加上本次的編輯步數 $1$ 。對應的狀態轉移方程為:
$$
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
$$
請注意,**當 $s[i-1]$ 和 $t[j-1]$ 相同時,無須編輯當前字元**,這種情況下的狀態轉移方程為:
$$
dp[i, j] = dp[i-1, j-1]
$$
**第三步:確定邊界條件和狀態轉移順序**
當兩字串都為空時,編輯步數為 $0$ ,即 $dp[0, 0] = 0$ 。當 $s$ 為空但 $t$ 不為空時,最少編輯步數等於 $t$ 的長度,即首行 $dp[0, j] = j$ 。當 $s$ 不為空但 $t$ 為空時,最少編輯步數等於 $s$ 的長度,即首列 $dp[i, 0] = i$ 。
觀察狀態轉移方程,解 $dp[i, j]$ 依賴左方、上方、左上方的解,因此透過兩層迴圈正序走訪整個 $dp$ 表即可。
### 2. &nbsp; 程式碼實現
=== "Python"
```python title="edit_distance.py"
def edit_distance_dp(s: str, t: str) -> int:
"""編輯距離:動態規劃"""
n, m = len(s), len(t)
dp = [[0] * (m + 1) for _ in range(n + 1)]
# 狀態轉移:首行首列
for i in range(1, n + 1):
dp[i][0] = i
for j in range(1, m + 1):
dp[0][j] = j
# 狀態轉移:其餘行和列
for i in range(1, n + 1):
for j in range(1, m + 1):
if s[i - 1] == t[j - 1]:
# 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1]
else:
# 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
return dp[n][m]
```
=== "C++"
```cpp title="edit_distance.cpp"
/* 編輯距離:動態規劃 */
int editDistanceDP(string s, string t) {
int n = s.length(), m = t.length();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 狀態轉移:首行首列
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 狀態轉移:其餘行和列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Java"
```java title="edit_distance.java"
/* 編輯距離:動態規劃 */
int editDistanceDP(String s, String t) {
int n = s.length(), m = t.length();
int[][] dp = new int[n + 1][m + 1];
// 狀態轉移:首行首列
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 狀態轉移:其餘行和列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "C#"
```csharp title="edit_distance.cs"
/* 編輯距離:動態規劃 */
int EditDistanceDP(string s, string t) {
int n = s.Length, m = t.Length;
int[,] dp = new int[n + 1, m + 1];
// 狀態轉移:首行首列
for (int i = 1; i <= n; i++) {
dp[i, 0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0, j] = j;
}
// 狀態轉移:其餘行和列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[i, j] = dp[i - 1, j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;
}
}
}
return dp[n, m];
}
```
=== "Go"
```go title="edit_distance.go"
/* 編輯距離:動態規劃 */
func editDistanceDP(s string, t string) int {
n := len(s)
m := len(t)
dp := make([][]int, n+1)
for i := 0; i <= n; i++ {
dp[i] = make([]int, m+1)
}
// 狀態轉移:首行首列
for i := 1; i <= n; i++ {
dp[i][0] = i
}
for j := 1; j <= m; j++ {
dp[0][j] = j
}
// 狀態轉移:其餘行和列
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
if s[i-1] == t[j-1] {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i-1][j-1]
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1
}
}
}
return dp[n][m]
}
```
=== "Swift"
```swift title="edit_distance.swift"
/* 編輯距離:動態規劃 */
func editDistanceDP(s: String, t: String) -> Int {
let n = s.utf8CString.count
let m = t.utf8CString.count
var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)
// 狀態轉移:首行首列
for i in 1 ... n {
dp[i][0] = i
}
for j in 1 ... m {
dp[0][j] = j
}
// 狀態轉移:其餘行和列
for i in 1 ... n {
for j in 1 ... m {
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1]
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
}
}
}
return dp[n][m]
}
```
=== "JS"
```javascript title="edit_distance.js"
/* 編輯距離:動態規劃 */
function editDistanceDP(s, t) {
const n = s.length,
m = t.length;
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
// 狀態轉移:首行首列
for (let i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (let j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 狀態轉移:其餘行和列
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] =
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "TS"
```typescript title="edit_distance.ts"
/* 編輯距離:動態規劃 */
function editDistanceDP(s: string, t: string): number {
const n = s.length,
m = t.length;
const dp = Array.from({ length: n + 1 }, () =>
Array.from({ length: m + 1 }, () => 0)
);
// 狀態轉移:首行首列
for (let i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (let j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 狀態轉移:其餘行和列
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] =
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Dart"
```dart title="edit_distance.dart"
/* 編輯距離:動態規劃 */
int editDistanceDP(String s, String t) {
int n = s.length, m = t.length;
List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));
// 狀態轉移:首行首列
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 狀態轉移:其餘行和列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Rust"
```rust title="edit_distance.rs"
/* 編輯距離:動態規劃 */
fn edit_distance_dp(s: &str, t: &str) -> i32 {
let (n, m) = (s.len(), t.len());
let mut dp = vec![vec![0; m + 1]; n + 1];
// 狀態轉移:首行首列
for i in 1..=n {
dp[i][0] = i as i32;
}
for j in 1..m {
dp[0][j] = j as i32;
}
// 狀態轉移:其餘行和列
for i in 1..=n {
for j in 1..=m {
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] =
std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
dp[n][m]
}
```
=== "C"
```c title="edit_distance.c"
/* 編輯距離:動態規劃 */
int editDistanceDP(char *s, char *t, int n, int m) {
int **dp = malloc((n + 1) * sizeof(int *));
for (int i = 0; i <= n; i++) {
dp[i] = calloc(m + 1, sizeof(int));
}
// 狀態轉移:首行首列
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 狀態轉移:其餘行和列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
int res = dp[n][m];
// 釋放記憶體
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
return res;
}
```
=== "Kotlin"
```kotlin title="edit_distance.kt"
/* 編輯距離:動態規劃 */
fun editDistanceDP(s: String, t: String): Int {
val n = s.length
val m = t.length
val dp = Array(n + 1) { IntArray(m + 1) }
// 狀態轉移:首行首列
for (i in 1..n) {
dp[i][0] = i
}
for (j in 1..m) {
dp[0][j] = j
}
// 狀態轉移:其餘行和列
for (i in 1..n) {
for (j in 1..m) {
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1]
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] =
(min(
min(dp[i][j - 1].toDouble(), dp[i - 1][j].toDouble()),
dp[i - 1][j - 1].toDouble()
) + 1).toInt()
}
}
}
return dp[n][m]
}
```
=== "Ruby"
```ruby title="edit_distance.rb"
[class]{}-[func]{edit_distance_dp}
```
=== "Zig"
```zig title="edit_distance.zig"
// 編輯距離:動態規劃
fn editDistanceDP(comptime s: []const u8, comptime t: []const u8) i32 {
comptime var n = s.len;
comptime var m = t.len;
var dp = [_][m + 1]i32{[_]i32{0} ** (m + 1)} ** (n + 1);
// 狀態轉移:首行首列
for (1..n + 1) |i| {
dp[i][0] = @intCast(i);
}
for (1..m + 1) |j| {
dp[0][j] = @intCast(j);
}
// 狀態轉移:其餘行和列
for (1..n + 1) |i| {
for (1..m + 1) |j| {
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[i][j] = @min(@min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%20%28m%20%2B%201%29%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%E9%A6%96%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B0%5D%20%3D%20i%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5B0%5D%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%E5%92%8C%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20min%28dp%5Bi%5D%5Bj%20-%201%5D,%20dp%5Bi%20-%201%5D%5Bj%5D,%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%29%20%2B%201%0A%20%20%20%20return%20dp%5Bn%5D%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%20%28m%20%2B%201%29%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%E9%A6%96%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B0%5D%20%3D%20i%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5B0%5D%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%E5%92%8C%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20min%28dp%5Bi%5D%5Bj%20-%201%5D,%20dp%5Bi%20-%201%5D%5Bj%5D,%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%29%20%2B%201%0A%20%20%20%20return%20dp%5Bn%5D%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
如圖 14-30 所示,編輯距離問題的狀態轉移過程與背包問題非常類似,都可以看作填寫一個二維網格的過程。
=== "<1>"
![編輯距離的動態規劃過程](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" }
=== "<2>"
![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png){ class="animation-figure" }
=== "<3>"
![edit_distance_dp_step3](edit_distance_problem.assets/edit_distance_dp_step3.png){ class="animation-figure" }
=== "<4>"
![edit_distance_dp_step4](edit_distance_problem.assets/edit_distance_dp_step4.png){ class="animation-figure" }
=== "<5>"
![edit_distance_dp_step5](edit_distance_problem.assets/edit_distance_dp_step5.png){ class="animation-figure" }
=== "<6>"
![edit_distance_dp_step6](edit_distance_problem.assets/edit_distance_dp_step6.png){ class="animation-figure" }
=== "<7>"
![edit_distance_dp_step7](edit_distance_problem.assets/edit_distance_dp_step7.png){ class="animation-figure" }
=== "<8>"
![edit_distance_dp_step8](edit_distance_problem.assets/edit_distance_dp_step8.png){ class="animation-figure" }
=== "<9>"
![edit_distance_dp_step9](edit_distance_problem.assets/edit_distance_dp_step9.png){ class="animation-figure" }
=== "<10>"
![edit_distance_dp_step10](edit_distance_problem.assets/edit_distance_dp_step10.png){ class="animation-figure" }
=== "<11>"
![edit_distance_dp_step11](edit_distance_problem.assets/edit_distance_dp_step11.png){ class="animation-figure" }
=== "<12>"
![edit_distance_dp_step12](edit_distance_problem.assets/edit_distance_dp_step12.png){ class="animation-figure" }
=== "<13>"
![edit_distance_dp_step13](edit_distance_problem.assets/edit_distance_dp_step13.png){ class="animation-figure" }
=== "<14>"
![edit_distance_dp_step14](edit_distance_problem.assets/edit_distance_dp_step14.png){ class="animation-figure" }
=== "<15>"
![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png){ class="animation-figure" }
<p align="center"> 圖 14-30 &nbsp; 編輯距離的動態規劃過程 </p>
### 3. &nbsp; 空間最佳化
由於 $dp[i,j]$ 是由上方 $dp[i-1, j]$、左方 $dp[i, j-1]$、左上方 $dp[i-1, j-1]$ 轉移而來的,而正序走訪會丟失左上方 $dp[i-1, j-1]$ ,倒序走訪無法提前構建 $dp[i, j-1]$ ,因此兩種走訪順序都不可取。
為此,我們可以使用一個變數 `leftup` 來暫存左上方的解 $dp[i-1, j-1]$ ,從而只需考慮左方和上方的解。此時的情況與完全背包問題相同,可使用正序走訪。程式碼如下所示:
=== "Python"
```python title="edit_distance.py"
def edit_distance_dp_comp(s: str, t: str) -> int:
"""編輯距離:空間最佳化後的動態規劃"""
n, m = len(s), len(t)
dp = [0] * (m + 1)
# 狀態轉移:首行
for j in range(1, m + 1):
dp[j] = j
# 狀態轉移:其餘行
for i in range(1, n + 1):
# 狀態轉移:首列
leftup = dp[0] # 暫存 dp[i-1, j-1]
dp[0] += 1
# 狀態轉移:其餘列
for j in range(1, m + 1):
temp = dp[j]
if s[i - 1] == t[j - 1]:
# 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup
else:
# 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = min(dp[j - 1], dp[j], leftup) + 1
leftup = temp # 更新為下一輪的 dp[i-1, j-1]
return dp[m]
```
=== "C++"
```cpp title="edit_distance.cpp"
/* 編輯距離:空間最佳化後的動態規劃 */
int editDistanceDPComp(string s, string t) {
int n = s.length(), m = t.length();
vector<int> dp(m + 1, 0);
// 狀態轉移:首行
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// 狀態轉移:其餘行
for (int i = 1; i <= n; i++) {
// 狀態轉移:首列
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i;
// 狀態轉移:其餘列
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Java"
```java title="edit_distance.java"
/* 編輯距離:空間最佳化後的動態規劃 */
int editDistanceDPComp(String s, String t) {
int n = s.length(), m = t.length();
int[] dp = new int[m + 1];
// 狀態轉移:首行
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// 狀態轉移:其餘行
for (int i = 1; i <= n; i++) {
// 狀態轉移:首列
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i;
// 狀態轉移:其餘列
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "C#"
```csharp title="edit_distance.cs"
/* 編輯距離:空間最佳化後的動態規劃 */
int EditDistanceDPComp(string s, string t) {
int n = s.Length, m = t.Length;
int[] dp = new int[m + 1];
// 狀態轉移:首行
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// 狀態轉移:其餘行
for (int i = 1; i <= n; i++) {
// 狀態轉移:首列
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i;
// 狀態轉移:其餘列
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Go"
```go title="edit_distance.go"
/* 編輯距離:空間最佳化後的動態規劃 */
func editDistanceDPComp(s string, t string) int {
n := len(s)
m := len(t)
dp := make([]int, m+1)
// 狀態轉移:首行
for j := 1; j <= m; j++ {
dp[j] = j
}
// 狀態轉移:其餘行
for i := 1; i <= n; i++ {
// 狀態轉移:首列
leftUp := dp[0] // 暫存 dp[i-1, j-1]
dp[0] = i
// 狀態轉移:其餘列
for j := 1; j <= m; j++ {
temp := dp[j]
if s[i-1] == t[j-1] {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftUp
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1
}
leftUp = temp // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m]
}
```
=== "Swift"
```swift title="edit_distance.swift"
/* 編輯距離:空間最佳化後的動態規劃 */
func editDistanceDPComp(s: String, t: String) -> Int {
let n = s.utf8CString.count
let m = t.utf8CString.count
var dp = Array(repeating: 0, count: m + 1)
// 狀態轉移:首行
for j in 1 ... m {
dp[j] = j
}
// 狀態轉移:其餘行
for i in 1 ... n {
// 狀態轉移:首列
var leftup = dp[0] // 暫存 dp[i-1, j-1]
dp[0] = i
// 狀態轉移:其餘列
for j in 1 ... m {
let temp = dp[j]
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
}
leftup = temp // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m]
}
```
=== "JS"
```javascript title="edit_distance.js"
/* 編輯距離:狀態壓縮後的動態規劃 */
function editDistanceDPComp(s, t) {
const n = s.length,
m = t.length;
const dp = new Array(m + 1).fill(0);
// 狀態轉移:首行
for (let j = 1; j <= m; j++) {
dp[j] = j;
}
// 狀態轉移:其餘行
for (let i = 1; i <= n; i++) {
// 狀態轉移:首列
let leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i;
// 狀態轉移:其餘列
for (let j = 1; j <= m; j++) {
const temp = dp[j];
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "TS"
```typescript title="edit_distance.ts"
/* 編輯距離:狀態壓縮後的動態規劃 */
function editDistanceDPComp(s: string, t: string): number {
const n = s.length,
m = t.length;
const dp = new Array(m + 1).fill(0);
// 狀態轉移:首行
for (let j = 1; j <= m; j++) {
dp[j] = j;
}
// 狀態轉移:其餘行
for (let i = 1; i <= n; i++) {
// 狀態轉移:首列
let leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i;
// 狀態轉移:其餘列
for (let j = 1; j <= m; j++) {
const temp = dp[j];
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Dart"
```dart title="edit_distance.dart"
/* 編輯距離:空間最佳化後的動態規劃 */
int editDistanceDPComp(String s, String t) {
int n = s.length, m = t.length;
List<int> dp = List.filled(m + 1, 0);
// 狀態轉移:首行
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// 狀態轉移:其餘行
for (int i = 1; i <= n; i++) {
// 狀態轉移:首列
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i;
// 狀態轉移:其餘列
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Rust"
```rust title="edit_distance.rs"
/* 編輯距離:空間最佳化後的動態規劃 */
fn edit_distance_dp_comp(s: &str, t: &str) -> i32 {
let (n, m) = (s.len(), t.len());
let mut dp = vec![0; m + 1];
// 狀態轉移:首行
for j in 1..m {
dp[j] = j as i32;
}
// 狀態轉移:其餘行
for i in 1..=n {
// 狀態轉移:首列
let mut leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i as i32;
// 狀態轉移:其餘列
for j in 1..=m {
let temp = dp[j];
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
dp[m]
}
```
=== "C"
```c title="edit_distance.c"
/* 編輯距離:空間最佳化後的動態規劃 */
int editDistanceDPComp(char *s, char *t, int n, int m) {
int *dp = calloc(m + 1, sizeof(int));
// 狀態轉移:首行
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// 狀態轉移:其餘行
for (int i = 1; i <= n; i++) {
// 狀態轉移:首列
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = i;
// 狀態轉移:其餘列
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
int res = dp[m];
// 釋放記憶體
free(dp);
return res;
}
```
=== "Kotlin"
```kotlin title="edit_distance.kt"
/* 編輯距離:空間最佳化後的動態規劃 */
fun editDistanceDPComp(s: String, t: String): Int {
val n = s.length
val m = t.length
val dp = IntArray(m + 1)
// 狀態轉移:首行
for (j in 1..m) {
dp[j] = j
}
// 狀態轉移:其餘行
for (i in 1..n) {
// 狀態轉移:首列
var leftup = dp[0] // 暫存 dp[i-1, j-1]
dp[0] = i
// 狀態轉移:其餘列
for (j in 1..m) {
val temp = dp[j]
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = (min(min(dp[j - 1].toDouble(), dp[j].toDouble()), leftup.toDouble()) + 1).toInt()
}
leftup = temp // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m]
}
```
=== "Ruby"
```ruby title="edit_distance.rb"
[class]{}-[func]{edit_distance_dp_comp}
```
=== "Zig"
```zig title="edit_distance.zig"
// 編輯距離:空間最佳化後的動態規劃
fn editDistanceDPComp(comptime s: []const u8, comptime t: []const u8) i32 {
comptime var n = s.len;
comptime var m = t.len;
var dp = [_]i32{0} ** (m + 1);
// 狀態轉移:首行
for (1..m + 1) |j| {
dp[j] = @intCast(j);
}
// 狀態轉移:其餘行
for (1..n + 1) |i| {
// 狀態轉移:首列
var leftup = dp[0]; // 暫存 dp[i-1, j-1]
dp[0] = @intCast(i);
// 狀態轉移:其餘列
for (1..m + 1) |j| {
var temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// 若兩字元相等,則直接跳過此兩字元
dp[j] = leftup;
} else {
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
dp[j] = @min(@min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
}
}
return dp[m];
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp_comp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E5%88%97%0A%20%20%20%20%20%20%20%20leftup%20%3D%20dp%5B0%5D%20%20%23%20%E6%9A%82%E5%AD%98%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20%20%20%20%20dp%5B0%5D%20%2B%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E5%88%97%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20temp%20%3D%20dp%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20leftup%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20min%28dp%5Bj%20-%201%5D,%20dp%5Bj%5D,%20leftup%29%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20leftup%20%3D%20temp%20%20%23%20%E6%9B%B4%E6%96%B0%E4%B8%BA%E4%B8%8B%E4%B8%80%E8%BD%AE%E7%9A%84%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20return%20dp%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp_comp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp_comp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E5%88%97%0A%20%20%20%20%20%20%20%20leftup%20%3D%20dp%5B0%5D%20%20%23%20%E6%9A%82%E5%AD%98%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20%20%20%20%20dp%5B0%5D%20%2B%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E5%88%97%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20temp%20%3D%20dp%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20leftup%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20min%28dp%5Bj%20-%201%5D,%20dp%5Bj%5D,%20leftup%29%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20leftup%20%3D%20temp%20%20%23%20%E6%9B%B4%E6%96%B0%E4%B8%BA%E4%B8%8B%E4%B8%80%E8%BD%AE%E7%9A%84%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20return%20dp%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp_comp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
@@ -0,0 +1,24 @@
---
comments: true
icon: material/table-pivot
---
# 第 14 章 &nbsp; 動態規劃
![動態規劃](../assets/covers/chapter_dynamic_programming.jpg){ class="cover-image" }
!!! abstract
小溪匯入河流,江河匯入大海。
動態規劃將小問題的解彙集成大問題的答案,一步步引領我們走向解決問題的彼岸。
## Chapter Contents
- [14.1 &nbsp; 初探動態規劃](https://www.hello-algo.com/en/chapter_dynamic_programming/intro_to_dynamic_programming/)
- [14.2 &nbsp; DP 問題特性](https://www.hello-algo.com/en/chapter_dynamic_programming/dp_problem_features/)
- [14.3 &nbsp; DP 解題思路](https://www.hello-algo.com/en/chapter_dynamic_programming/dp_solution_pipeline/)
- [14.4 &nbsp; 0-1 背包問題](https://www.hello-algo.com/en/chapter_dynamic_programming/knapsack_problem/)
- [14.5 &nbsp; 完全背包問題](https://www.hello-algo.com/en/chapter_dynamic_programming/unbounded_knapsack_problem/)
- [14.6 &nbsp; 編輯距離問題](https://www.hello-algo.com/en/chapter_dynamic_programming/edit_distance_problem/)
- [14.7 &nbsp; 小結](https://www.hello-algo.com/en/chapter_dynamic_programming/summary/)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
---
comments: true
---
# 14.7 &nbsp; 小結
- 動態規劃對問題進行分解,並透過儲存子問題的解來規避重複計算,提高計算效率。
- 不考慮時間的前提下,所有動態規劃問題都可以用回溯(暴力搜尋)進行求解,但遞迴樹中存在大量的重疊子問題,效率極低。透過引入記憶化串列,可以儲存所有計算過的子問題的解,從而保證重疊子問題只被計算一次。
- 記憶化搜尋是一種從頂至底的遞迴式解法,而與之對應的動態規劃是一種從底至頂的遞推式解法,其如同“填寫表格”一樣。由於當前狀態僅依賴某些區域性狀態,因此我們可以消除 $dp$ 表的一個維度,從而降低空間複雜度。
- 子問題分解是一種通用的演算法思路,在分治、動態規劃、回溯中具有不同的性質。
- 動態規劃問題有三大特性:重疊子問題、最優子結構、無後效性。
- 如果原問題的最優解可以從子問題的最優解構建得來,則它就具有最優子結構。
- 無後效性指對於一個狀態,其未來發展只與該狀態有關,而與過去經歷的所有狀態無關。許多組合最佳化問題不具有無後效性,無法使用動態規劃快速求解。
**背包問題**
- 背包問題是最典型的動態規劃問題之一,具有 0-1 背包、完全背包、多重背包等變種。
- 0-1 背包的狀態定義為前 $i$ 個物品在剩餘容量為 $c$ 的背包中的最大價值。根據不放入背包和放入背包兩種決策,可得到最優子結構,並構建出狀態轉移方程。在空間最佳化中,由於每個狀態依賴正上方和左上方的狀態,因此需要倒序走訪串列,避免左上方狀態被覆蓋。
- 完全背包問題的每種物品的選取數量無限制,因此選擇放入物品的狀態轉移與 0-1 背包問題不同。由於狀態依賴正上方和正左方的狀態,因此在空間最佳化中應當正序走訪。
- 零錢兌換問題是完全背包問題的一個變種。它從求“最大”價值變為求“最小”硬幣數量,因此狀態轉移方程中的 $\max()$ 應改為 $\min()$ 。從追求“不超過”背包容量到追求“恰好”湊出目標金額,因此使用 $amt + 1$ 來表示“無法湊出目標金額”的無效解。
- 零錢兌換問題 II 從求“最少硬幣數量”改為求“硬幣組合數量”,狀態轉移方程相應地從 $\min()$ 改為求和運算子。
**編輯距離問題**
- 編輯距離(Levenshtein 距離)用於衡量兩個字串之間的相似度,其定義為從一個字串到另一個字串的最少編輯步數,編輯操作包括新增、刪除、替換。
- 編輯距離問題的狀態定義為將 $s$ 的前 $i$ 個字元更改為 $t$ 的前 $j$ 個字元所需的最少編輯步數。當 $s[i] \ne t[j]$ 時,具有三種決策:新增、刪除、替換,它們都有相應的剩餘子問題。據此便可以找出最優子結構與構建狀態轉移方程。而當 $s[i] = t[j]$ 時,無須編輯當前字元。
- 在編輯距離中,狀態依賴其正上方、正左方、左上方的狀態,因此空間最佳化後正序或倒序走訪都無法正確地進行狀態轉移。為此,我們利用一個變數暫存左上方狀態,從而轉化到與完全背包問題等價的情況,可以在空間最佳化後進行正序走訪。
File diff suppressed because it is too large Load Diff
+103
View File
@@ -0,0 +1,103 @@
---
comments: true
---
# 9.1 &nbsp; 圖
<u>圖(graph</u>是一種非線性資料結構,由<u>頂點(vertex</u>和<u>邊(edge</u>組成。我們可以將圖 $G$ 抽象地表示為一組頂點 $V$ 和一組邊 $E$ 的集合。以下示例展示了一個包含 5 個頂點和 7 條邊的圖。
$$
\begin{aligned}
V & = \{ 1, 2, 3, 4, 5 \} \newline
E & = \{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \} \newline
G & = \{ V, E \} \newline
\end{aligned}
$$
如果將頂點看作節點,將邊看作連線各個節點的引用(指標),我們就可以將圖看作一種從鏈結串列拓展而來的資料結構。如圖 9-1 所示,**相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)的自由度更高**,因而更為複雜。
![鏈結串列、樹、圖之間的關係](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-1 &nbsp; 鏈結串列、樹、圖之間的關係 </p>
## 9.1.1 &nbsp; 圖的常見型別與術語
根據邊是否具有方向,可分為<u>無向圖(undirected graph</u>和<u>有向圖(directed graph</u>,如圖 9-2 所示。
- 在無向圖中,邊表示兩頂點之間的“雙向”連線關係,例如微信或 QQ 中的“好友關係”。
- 在有向圖中,邊具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 兩個方向的邊是相互獨立的,例如微博或抖音上的“關注”與“被關注”關係。
![有向圖與無向圖](graph.assets/directed_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-2 &nbsp; 有向圖與無向圖 </p>
根據所有頂點是否連通,可分為<u>連通圖(connected graph</u>和<u>非連通圖(disconnected graph</u>,如圖 9-3 所示。
- 對於連通圖,從某個頂點出發,可以到達其餘任意頂點。
- 對於非連通圖,從某個頂點出發,至少有一個頂點無法到達。
![連通圖與非連通圖](graph.assets/connected_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-3 &nbsp; 連通圖與非連通圖 </p>
我們還可以為邊新增“權重”變數,從而得到如圖 9-4 所示的<u>有權圖(weighted graph</u>。例如在《王者榮耀》等手遊中,系統會根據共同遊戲時間來計算玩家之間的“親密度”,這種親密度網路就可以用有權圖來表示。
![有權圖與無權圖](graph.assets/weighted_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-4 &nbsp; 有權圖與無權圖 </p>
圖資料結構包含以下常用術語。
- <u>鄰接(adjacency</u>:當兩頂點之間存在邊相連時,稱這兩頂點“鄰接”。在圖 9-4 中,頂點 1 的鄰接頂點為頂點 2、3、5。
- <u>路徑(path</u>:從頂點 A 到頂點 B 經過的邊構成的序列被稱為從 A 到 B 的“路徑”。在圖 9-4 中,邊序列 1-5-2-4 是頂點 1 到頂點 4 的一條路徑。
- <u>度(degree</u>:一個頂點擁有的邊數。對於有向圖,<u>入度(in-degree</u>表示有多少條邊指向該頂點,<u>出度(out-degree</u>表示有多少條邊從該頂點指出。
## 9.1.2 &nbsp; 圖的表示
圖的常用表示方式包括“鄰接矩陣”和“鄰接表”。以下使用無向圖進行舉例。
### 1. &nbsp; 鄰接矩陣
設圖的頂點數量為 $n$ <u>鄰接矩陣(adjacency matrix</u>使用一個 $n \times n$ 大小的矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間是否存在邊。
如圖 9-5 所示,設鄰接矩陣為 $M$、頂點串列為 $V$ ,那麼矩陣元素 $M[i, j] = 1$ 表示頂點 $V[i]$ 到頂點 $V[j]$ 之間存在邊,反之 $M[i, j] = 0$ 表示兩頂點之間無邊。
![圖的鄰接矩陣表示](graph.assets/adjacency_matrix.png){ class="animation-figure" }
<p align="center"> 圖 9-5 &nbsp; 圖的鄰接矩陣表示 </p>
鄰接矩陣具有以下特性。
- 頂點不能與自身相連,因此鄰接矩陣主對角線元素沒有意義。
- 對於無向圖,兩個方向的邊等價,此時鄰接矩陣關於主對角線對稱。
- 將鄰接矩陣的元素從 $1$ 和 $0$ 替換為權重,則可表示有權圖。
使用鄰接矩陣表示圖時,我們可以直接訪問矩陣元素以獲取邊,因此增刪查改操作的效率很高,時間複雜度均為 $O(1)$ 。然而,矩陣的空間複雜度為 $O(n^2)$ ,記憶體佔用較多。
### 2. &nbsp; 鄰接表
<u>鄰接表(adjacency list</u>使用 $n$ 個鏈結串列來表示圖,鏈結串列節點表示頂點。第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點(與該頂點相連的頂點)。圖 9-6 展示了一個使用鄰接表儲存的圖的示例。
![圖的鄰接表表示](graph.assets/adjacency_list.png){ class="animation-figure" }
<p align="center"> 圖 9-6 &nbsp; 圖的鄰接表表示 </p>
鄰接表僅儲存實際存在的邊,而邊的總數通常遠小於 $n^2$ ,因此它更加節省空間。然而,在鄰接表中需要透過走訪鏈結串列來查詢邊,因此其時間效率不如鄰接矩陣。
觀察圖 9-6 ,**鄰接表結構與雜湊表中的“鏈式位址”非常相似,因此我們也可以採用類似的方法來最佳化效率**。比如當鏈結串列較長時,可以將鏈結串列轉化為 AVL 樹或紅黑樹,從而將時間效率從 $O(n)$ 最佳化至 $O(\log n)$ ;還可以把鏈結串列轉換為雜湊表,從而將時間複雜度降至 $O(1)$ 。
## 9.1.3 &nbsp; 圖的常見應用
如表 9-1 所示,許多現實系統可以用圖來建模,相應的問題也可以約化為圖計算問題。
<p align="center"> 表 9-1 &nbsp; 現實生活中常見的圖 </p>
<div class="center-table" markdown>
| | 頂點 | 邊 | 圖計算問題 |
| -------- | ---- | -------------------- | ------------ |
| 社交網路 | 使用者 | 好友關係 | 潛在好友推薦 |
| 地鐵線路 | 站點 | 站點間的連通性 | 最短路線推薦 |
| 太陽系 | 星體 | 星體間的萬有引力作用 | 行星軌道計算 |
</div>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
---
comments: true
icon: material/graphql
---
# 第 9 章 &nbsp; 圖
![](../assets/covers/chapter_graph.jpg){ class="cover-image" }
!!! abstract
在生命旅途中,我們就像是一個個節點,被無數看不見的邊相連。
每一次的相識與相離,都在這張巨大的網路圖中留下獨特的印記。
## Chapter Contents
- [9.1 &nbsp; 圖](https://www.hello-algo.com/en/chapter_graph/graph/)
- [9.2 &nbsp; 圖基礎操作](https://www.hello-algo.com/en/chapter_graph/graph_operations/)
- [9.3 &nbsp; 圖的走訪](https://www.hello-algo.com/en/chapter_graph/graph_traversal/)
- [9.4 &nbsp; 小結](https://www.hello-algo.com/en/chapter_graph/summary/)
+35
View File
@@ -0,0 +1,35 @@
---
comments: true
---
# 9.4 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 圖由頂點和邊組成,可以表示為一組頂點和一組邊構成的集合。
- 相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)具有更高的自由度,因而更為複雜。
- 有向圖的邊具有方向性,連通圖中的任意頂點均可達,有權圖的每條邊都包含權重變數。
- 鄰接矩陣利用矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間有邊或無邊。鄰接矩陣在增刪查改操作上效率很高,但空間佔用較多。
- 鄰接表使用多個鏈結串列來表示圖,第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點。鄰接表相對於鄰接矩陣更加節省空間,但由於需要走訪鏈結串列來查詢邊,因此時間效率較低。
- 當鄰接表中的鏈結串列過長時,可以將其轉換為紅黑樹或雜湊表,從而提升查詢效率。
- 從演算法思想的角度分析,鄰接矩陣體現了“以空間換時間”,鄰接表體現了“以時間換空間”。
- 圖可用於建模各類現實系統,如社交網路、地鐵線路等。
- 樹是圖的一種特例,樹的走訪也是圖的走訪的一種特例。
- 圖的廣度優先走訪是一種由近及遠、層層擴張的搜尋方式,通常藉助佇列實現。
- 圖的深度優先走訪是一種優先走到底、無路可走時再回溯的搜尋方式,常基於遞迴來實現。
### 2. &nbsp; Q & A
**Q**:路徑的定義是頂點序列還是邊序列?
維基百科上不同語言版本的定義不一致:英文版是“路徑是一個邊序列”,而中文版是“路徑是一個頂點序列”。以下是英文版原文:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.
在本文中,路徑被視為一個邊序列,而不是一個頂點序列。這是因為兩個頂點之間可能存在多條邊連線,此時每條邊都對應一條路徑。
**Q**:非連通圖中是否會有無法走訪到的點?
在非連通圖中,從某個頂點出發,至少有一個頂點無法到達。走訪非連通圖需要設定多個起點,以走訪到圖的所有連通分量。
**Q**:在鄰接表中,“與該頂點相連的所有頂點”的頂點順序是否有要求?
可以是任意順序。但在實際應用中,可能需要按照指定規則來排序,比如按照頂點新增的次序,或者按照頂點值大小的順序等,這樣有助於快速查詢“帶有某種極值”的頂點。
@@ -0,0 +1,568 @@
---
comments: true
---
# 15.2 &nbsp; 分數背包問題
!!! question
給定 $n$ 個物品,第 $i$ 個物品的重量為 $wgt[i-1]$、價值為 $val[i-1]$ ,和一個容量為 $cap$ 的背包。每個物品只能選擇一次,**但可以選擇物品的一部分,價值根據選擇的重量比例計算**,問在限定背包容量下背包中物品的最大價值。示例如圖 15-3 所示。
![分數背包問題的示例資料](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" }
<p align="center"> 圖 15-3 &nbsp; 分數背包問題的示例資料 </p>
分數背包問題和 0-1 背包問題整體上非常相似,狀態包含當前物品 $i$ 和容量 $c$ ,目標是求限定背包容量下的最大價值。
不同點在於,本題允許只選擇物品的一部分。如圖 15-4 所示,**我們可以對物品任意地進行切分,並按照重量比例來計算相應價值**。
1. 對於物品 $i$ ,它在單位重量下的價值為 $val[i-1] / wgt[i-1]$ ,簡稱單位價值。
2. 假設放入一部分物品 $i$ ,重量為 $w$ ,則背包增加的價值為 $w \times val[i-1] / wgt[i-1]$ 。
![物品在單位重量下的價值](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" }
<p align="center"> 圖 15-4 &nbsp; 物品在單位重量下的價值 </p>
### 1. &nbsp; 貪婪策略確定
最大化背包內物品總價值,**本質上是最大化單位重量下的物品價值**。由此便可推理出圖 15-5 所示的貪婪策略。
1. 將物品按照單位價值從高到低進行排序。
2. 走訪所有物品,**每輪貪婪地選擇單位價值最高的物品**。
3. 若剩餘背包容量不足,則使用當前物品的一部分填滿背包。
![分數背包問題的貪婪策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" }
<p align="center"> 圖 15-5 &nbsp; 分數背包問題的貪婪策略 </p>
### 2. &nbsp; 程式碼實現
我們建立了一個物品類別 `Item` ,以便將物品按照單位價值進行排序。迴圈進行貪婪選擇,當背包已滿時跳出並返回解:
=== "Python"
```python title="fractional_knapsack.py"
class Item:
"""物品"""
def __init__(self, w: int, v: int):
self.w = w # 物品重量
self.v = v # 物品價值
def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
"""分數背包:貪婪"""
# 建立物品串列,包含兩個屬性:重量、價值
items = [Item(w, v) for w, v in zip(wgt, val)]
# 按照單位價值 item.v / item.w 從高到低進行排序
items.sort(key=lambda item: item.v / item.w, reverse=True)
# 迴圈貪婪選擇
res = 0
for item in items:
if item.w <= cap:
# 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v
cap -= item.w
else:
# 若剩餘容量不足,則將當前物品的一部分裝進背包
res += (item.v / item.w) * cap
# 已無剩餘容量,因此跳出迴圈
break
return res
```
=== "C++"
```cpp title="fractional_knapsack.cpp"
/* 物品 */
class Item {
public:
int w; // 物品重量
int v; // 物品價值
Item(int w, int v) : w(w), v(v) {
}
};
/* 分數背包:貪婪 */
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
// 建立物品串列,包含兩個屬性:重量、價值
vector<Item> items;
for (int i = 0; i < wgt.size(); i++) {
items.push_back(Item(wgt[i], val[i]));
}
// 按照單位價值 item.v / item.w 從高到低進行排序
sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });
// 迴圈貪婪選擇
double res = 0;
for (auto &item : items) {
if (item.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v;
cap -= item.w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += (double)item.v / item.w * cap;
// 已無剩餘容量,因此跳出迴圈
break;
}
}
return res;
}
```
=== "Java"
```java title="fractional_knapsack.java"
/* 物品 */
class Item {
int w; // 物品重量
int v; // 物品價值
public Item(int w, int v) {
this.w = w;
this.v = v;
}
}
/* 分數背包:貪婪 */
double fractionalKnapsack(int[] wgt, int[] val, int cap) {
// 建立物品串列,包含兩個屬性:重量、價值
Item[] items = new Item[wgt.length];
for (int i = 0; i < wgt.length; i++) {
items[i] = new Item(wgt[i], val[i]);
}
// 按照單位價值 item.v / item.w 從高到低進行排序
Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));
// 迴圈貪婪選擇
double res = 0;
for (Item item : items) {
if (item.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v;
cap -= item.w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += (double) item.v / item.w * cap;
// 已無剩餘容量,因此跳出迴圈
break;
}
}
return res;
}
```
=== "C#"
```csharp title="fractional_knapsack.cs"
/* 物品 */
class Item(int w, int v) {
public int w = w; // 物品重量
public int v = v; // 物品價值
}
/* 分數背包:貪婪 */
double FractionalKnapsack(int[] wgt, int[] val, int cap) {
// 建立物品串列,包含兩個屬性:重量、價值
Item[] items = new Item[wgt.Length];
for (int i = 0; i < wgt.Length; i++) {
items[i] = new Item(wgt[i], val[i]);
}
// 按照單位價值 item.v / item.w 從高到低進行排序
Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));
// 迴圈貪婪選擇
double res = 0;
foreach (Item item in items) {
if (item.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v;
cap -= item.w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += (double)item.v / item.w * cap;
// 已無剩餘容量,因此跳出迴圈
break;
}
}
return res;
}
```
=== "Go"
```go title="fractional_knapsack.go"
/* 物品 */
type Item struct {
w int // 物品重量
v int // 物品價值
}
/* 分數背包:貪婪 */
func fractionalKnapsack(wgt []int, val []int, cap int) float64 {
// 建立物品串列,包含兩個屬性:重量、價值
items := make([]Item, len(wgt))
for i := 0; i < len(wgt); i++ {
items[i] = Item{wgt[i], val[i]}
}
// 按照單位價值 item.v / item.w 從高到低進行排序
sort.Slice(items, func(i, j int) bool {
return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)
})
// 迴圈貪婪選擇
res := 0.0
for _, item := range items {
if item.w <= cap {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += float64(item.v)
cap -= item.w
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += float64(item.v) / float64(item.w) * float64(cap)
// 已無剩餘容量,因此跳出迴圈
break
}
}
return res
}
```
=== "Swift"
```swift title="fractional_knapsack.swift"
/* 物品 */
class Item {
var w: Int // 物品重量
var v: Int // 物品價值
init(w: Int, v: Int) {
self.w = w
self.v = v
}
}
/* 分數背包:貪婪 */
func fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {
// 建立物品串列,包含兩個屬性:重量、價值
var items = zip(wgt, val).map { Item(w: $0, v: $1) }
// 按照單位價值 item.v / item.w 從高到低進行排序
items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }
// 迴圈貪婪選擇
var res = 0.0
var cap = cap
for item in items {
if item.w <= cap {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += Double(item.v)
cap -= item.w
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += Double(item.v) / Double(item.w) * Double(cap)
// 已無剩餘容量,因此跳出迴圈
break
}
}
return res
}
```
=== "JS"
```javascript title="fractional_knapsack.js"
/* 物品 */
class Item {
constructor(w, v) {
this.w = w; // 物品重量
this.v = v; // 物品價值
}
}
/* 分數背包:貪婪 */
function fractionalKnapsack(wgt, val, cap) {
// 建立物品串列,包含兩個屬性:重量、價值
const items = wgt.map((w, i) => new Item(w, val[i]));
// 按照單位價值 item.v / item.w 從高到低進行排序
items.sort((a, b) => b.v / b.w - a.v / a.w);
// 迴圈貪婪選擇
let res = 0;
for (const item of items) {
if (item.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v;
cap -= item.w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += (item.v / item.w) * cap;
// 已無剩餘容量,因此跳出迴圈
break;
}
}
return res;
}
```
=== "TS"
```typescript title="fractional_knapsack.ts"
/* 物品 */
class Item {
w: number; // 物品重量
v: number; // 物品價值
constructor(w: number, v: number) {
this.w = w;
this.v = v;
}
}
/* 分數背包:貪婪 */
function fractionalKnapsack(wgt: number[], val: number[], cap: number): number {
// 建立物品串列,包含兩個屬性:重量、價值
const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));
// 按照單位價值 item.v / item.w 從高到低進行排序
items.sort((a, b) => b.v / b.w - a.v / a.w);
// 迴圈貪婪選擇
let res = 0;
for (const item of items) {
if (item.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v;
cap -= item.w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += (item.v / item.w) * cap;
// 已無剩餘容量,因此跳出迴圈
break;
}
}
return res;
}
```
=== "Dart"
```dart title="fractional_knapsack.dart"
/* 物品 */
class Item {
int w; // 物品重量
int v; // 物品價值
Item(this.w, this.v);
}
/* 分數背包:貪婪 */
double fractionalKnapsack(List<int> wgt, List<int> val, int cap) {
// 建立物品串列,包含兩個屬性:重量、價值
List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));
// 按照單位價值 item.v / item.w 從高到低進行排序
items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));
// 迴圈貪婪選擇
double res = 0;
for (Item item in items) {
if (item.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v;
cap -= item.w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += item.v / item.w * cap;
// 已無剩餘容量,因此跳出迴圈
break;
}
}
return res;
}
```
=== "Rust"
```rust title="fractional_knapsack.rs"
/* 物品 */
struct Item {
w: i32, // 物品重量
v: i32, // 物品價值
}
impl Item {
fn new(w: i32, v: i32) -> Self {
Self { w, v }
}
}
/* 分數背包:貪婪 */
fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {
// 建立物品串列,包含兩個屬性:重量、價值
let mut items = wgt
.iter()
.zip(val.iter())
.map(|(&w, &v)| Item::new(w, v))
.collect::<Vec<Item>>();
// 按照單位價值 item.v / item.w 從高到低進行排序
items.sort_by(|a, b| {
(b.v as f64 / b.w as f64)
.partial_cmp(&(a.v as f64 / a.w as f64))
.unwrap()
});
// 迴圈貪婪選擇
let mut res = 0.0;
for item in &items {
if item.w <= cap {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v as f64;
cap -= item.w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += item.v as f64 / item.w as f64 * cap as f64;
// 已無剩餘容量,因此跳出迴圈
break;
}
}
res
}
```
=== "C"
```c title="fractional_knapsack.c"
/* 物品 */
typedef struct {
int w; // 物品重量
int v; // 物品價值
} Item;
/* 分數背包:貪婪 */
float fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {
// 建立物品串列,包含兩個屬性:重量、價值
Item *items = malloc(sizeof(Item) * itemCount);
for (int i = 0; i < itemCount; i++) {
items[i] = (Item){.w = wgt[i], .v = val[i]};
}
// 按照單位價值 item.v / item.w 從高到低進行排序
qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);
// 迴圈貪婪選擇
float res = 0.0;
for (int i = 0; i < itemCount; i++) {
if (items[i].w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += items[i].v;
cap -= items[i].w;
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += (float)cap / items[i].w * items[i].v;
cap = 0;
break;
}
}
free(items);
return res;
}
```
=== "Kotlin"
```kotlin title="fractional_knapsack.kt"
/* 物品 */
class Item(
val w: Int, // 物品
val v: Int // 物品價值
)
/* 分數背包:貪婪 */
fun fractionalKnapsack(
wgt: IntArray,
value: IntArray,
c: Int
): Double {
// 建立物品串列,包含兩個屬性:重量、價值
var cap = c
val items = arrayOfNulls<Item>(wgt.size)
for (i in wgt.indices) {
items[i] = Item(wgt[i], value[i])
}
// 按照單位價值 item.v / item.w 從高到低進行排序
Arrays.sort(items, Comparator.comparingDouble { item: Item -> -(item.v.toDouble() / item.w) })
// 迴圈貪婪選擇
var res = 0.0
for (item in items) {
if (item!!.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v.toDouble()
cap -= item.w
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += item.v.toDouble() / item.w * cap
// 已無剩餘容量,因此跳出迴圈
break
}
}
return res
}
/* 分數背包:貪婪 */
fun fractionalKnapsack(
wgt: IntArray,
value: IntArray,
c: Int
): Double {
// 建立物品串列,包含兩個屬性:重量、價值
var cap = c
val items = arrayOfNulls<Item>(wgt.size)
for (i in wgt.indices) {
items[i] = Item(wgt[i], value[i])
}
// 按照單位價值 item.v / item.w 從高到低進行排序
Arrays.sort(items, Comparator.comparingDouble { item: Item -> -(item.v.toDouble() / item.w) })
// 迴圈貪婪選擇
var res = 0.0
for (item in items) {
if (item!!.w <= cap) {
// 若剩餘容量充足,則將當前物品整個裝進背包
res += item.v.toDouble()
cap -= item.w
} else {
// 若剩餘容量不足,則將當前物品的一部分裝進背包
res += item.v.toDouble() / item.w * cap
// 已無剩餘容量,因此跳出迴圈
break
}
}
return res
}
```
=== "Ruby"
```ruby title="fractional_knapsack.rb"
[class]{Item}-[func]{}
[class]{}-[func]{fractional_knapsack}
```
=== "Zig"
```zig title="fractional_knapsack.zig"
[class]{Item}-[func]{}
[class]{}-[func]{fractionalKnapsack}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E7%89%A9%E5%93%81%22%22%22%0A%20%20%20%20def%20__init__%28self,%20w%3A%20int,%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E7%89%A9%E5%93%81%E9%87%8D%E9%87%8F%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D,%20val%3A%20list%5Bint%5D,%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E8%83%8C%E5%8C%85%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9B%E5%BB%BA%E7%89%A9%E5%93%81%E5%88%97%E8%A1%A8%EF%BC%8C%E5%8C%85%E5%90%AB%E4%B8%A4%E4%B8%AA%E5%B1%9E%E6%80%A7%EF%BC%9A%E9%87%8D%E9%87%8F%E3%80%81%E4%BB%B7%E5%80%BC%0A%20%20%20%20items%20%3D%20%5BItem%28w,%20v%29%20for%20w,%20v%20in%20zip%28wgt,%20val%29%5D%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E5%8D%95%E4%BD%8D%E4%BB%B7%E5%80%BC%20item.v%20/%20item.w%20%E4%BB%8E%E9%AB%98%E5%88%B0%E4%BD%8E%E8%BF%9B%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20/%20item.w,%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E5%85%85%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E6%95%B4%E4%B8%AA%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E4%B8%8D%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E7%9A%84%E4%B8%80%E9%83%A8%E5%88%86%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20/%20item.w%29%20*%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B7%B2%E6%97%A0%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E8%B7%B3%E5%87%BA%E5%BE%AA%E7%8E%AF%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10,%2020,%2030,%2040,%2050%5D%0A%20%20%20%20val%20%3D%20%5B50,%20120,%20150,%20210,%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt,%20val,%20cap%29%0A%20%20%20%20print%28f%22%E4%B8%8D%E8%B6%85%E8%BF%87%E8%83%8C%E5%8C%85%E5%AE%B9%E9%87%8F%E7%9A%84%E6%9C%80%E5%A4%A7%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E7%89%A9%E5%93%81%22%22%22%0A%20%20%20%20def%20__init__%28self,%20w%3A%20int,%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E7%89%A9%E5%93%81%E9%87%8D%E9%87%8F%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D,%20val%3A%20list%5Bint%5D,%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E8%83%8C%E5%8C%85%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9B%E5%BB%BA%E7%89%A9%E5%93%81%E5%88%97%E8%A1%A8%EF%BC%8C%E5%8C%85%E5%90%AB%E4%B8%A4%E4%B8%AA%E5%B1%9E%E6%80%A7%EF%BC%9A%E9%87%8D%E9%87%8F%E3%80%81%E4%BB%B7%E5%80%BC%0A%20%20%20%20items%20%3D%20%5BItem%28w,%20v%29%20for%20w,%20v%20in%20zip%28wgt,%20val%29%5D%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E5%8D%95%E4%BD%8D%E4%BB%B7%E5%80%BC%20item.v%20/%20item.w%20%E4%BB%8E%E9%AB%98%E5%88%B0%E4%BD%8E%E8%BF%9B%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20/%20item.w,%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E5%85%85%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E6%95%B4%E4%B8%AA%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E4%B8%8D%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E7%9A%84%E4%B8%80%E9%83%A8%E5%88%86%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20/%20item.w%29%20*%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B7%B2%E6%97%A0%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E8%B7%B3%E5%87%BA%E5%BE%AA%E7%8E%AF%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10,%2020,%2030,%2040,%2050%5D%0A%20%20%20%20val%20%3D%20%5B50,%20120,%20150,%20210,%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt,%20val,%20cap%29%0A%20%20%20%20print%28f%22%E4%B8%8D%E8%B6%85%E8%BF%87%E8%83%8C%E5%8C%85%E5%AE%B9%E9%87%8F%E7%9A%84%E6%9C%80%E5%A4%A7%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
除排序之外,在最差情況下,需要走訪整個物品串列,**因此時間複雜度為 $O(n)$** ,其中 $n$ 為物品數量。
由於初始化了一個 `Item` 物件串列,**因此空間複雜度為 $O(n)$** 。
### 3. &nbsp; 正確性證明
採用反證法。假設物品 $x$ 是單位價值最高的物品,使用某演算法求得最大價值為 `res` ,但該解中不包含物品 $x$ 。
現在從背包中拿出單位重量的任意物品,並替換為單位重量的物品 $x$ 。由於物品 $x$ 的單位價值最高,因此替換後的總價值一定大於 `res` 。**這與 `res` 是最優解矛盾,說明最優解中必須包含物品 $x$** 。
對於該解中的其他物品,我們也可以構建出上述矛盾。總而言之,**單位價值更大的物品總是更優選擇**,這說明貪婪策略是有效的。
如圖 15-6 所示,如果將物品重量和物品單位價值分別看作一張二維圖表的橫軸和縱軸,則分數背包問題可轉化為“求在有限橫軸區間下圍成的最大面積”。這個類比可以幫助我們從幾何角度理解貪婪策略的有效性。
![分數背包問題的幾何表示](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" }
<p align="center"> 圖 15-6 &nbsp; 分數背包問題的幾何表示 </p>
@@ -0,0 +1,397 @@
---
comments: true
---
# 15.1 &nbsp; 貪婪演算法
<u>貪婪演算法(greedy algorithm</u>是一種常見的解決最佳化問題的演算法,其基本思想是在問題的每個決策階段,都選擇當前看起來最優的選擇,即貪婪地做出區域性最優的決策,以期獲得全域性最優解。貪婪演算法簡潔且高效,在許多實際問題中有著廣泛的應用。
貪婪演算法和動態規劃都常用於解決最佳化問題。它們之間存在一些相似之處,比如都依賴最優子結構性質,但工作原理不同。
- 動態規劃會根據之前階段的所有決策來考慮當前決策,並使用過去子問題的解來構建當前子問題的解。
- 貪婪演算法不會考慮過去的決策,而是一路向前地進行貪婪選擇,不斷縮小問題範圍,直至問題被解決。
我們先透過例題“零錢兌換”瞭解貪婪演算法的工作原理。這道題已經在“完全背包問題”章節中介紹過,相信你對它並不陌生。
!!! question
給定 $n$ 種硬幣,第 $i$ 種硬幣的面值為 $coins[i - 1]$ ,目標金額為 $amt$ ,每種硬幣可以重複選取,問能夠湊出目標金額的最少硬幣數量。如果無法湊出目標金額,則返回 $-1$ 。
本題採取的貪婪策略如圖 15-1 所示。給定目標金額,**我們貪婪地選擇不大於且最接近它的硬幣**,不斷迴圈該步驟,直至湊出目標金額為止。
![零錢兌換的貪婪策略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" }
<p align="center"> 圖 15-1 &nbsp; 零錢兌換的貪婪策略 </p>
實現程式碼如下所示:
=== "Python"
```python title="coin_change_greedy.py"
def coin_change_greedy(coins: list[int], amt: int) -> int:
"""零錢兌換:貪婪"""
# 假設 coins 串列有序
i = len(coins) - 1
count = 0
# 迴圈進行貪婪選擇,直到無剩餘金額
while amt > 0:
# 找到小於且最接近剩餘金額的硬幣
while i > 0 and coins[i] > amt:
i -= 1
# 選擇 coins[i]
amt -= coins[i]
count += 1
# 若未找到可行方案,則返回 -1
return count if amt == 0 else -1
```
=== "C++"
```cpp title="coin_change_greedy.cpp"
/* 零錢兌換:貪婪 */
int coinChangeGreedy(vector<int> &coins, int amt) {
// 假設 coins 串列有序
int i = coins.size() - 1;
int count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while (amt > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > amt) {
i--;
}
// 選擇 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,則返回 -1
return amt == 0 ? count : -1;
}
```
=== "Java"
```java title="coin_change_greedy.java"
/* 零錢兌換:貪婪 */
int coinChangeGreedy(int[] coins, int amt) {
// 假設 coins 串列有序
int i = coins.length - 1;
int count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while (amt > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > amt) {
i--;
}
// 選擇 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,則返回 -1
return amt == 0 ? count : -1;
}
```
=== "C#"
```csharp title="coin_change_greedy.cs"
/* 零錢兌換:貪婪 */
int CoinChangeGreedy(int[] coins, int amt) {
// 假設 coins 串列有序
int i = coins.Length - 1;
int count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while (amt > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > amt) {
i--;
}
// 選擇 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,則返回 -1
return amt == 0 ? count : -1;
}
```
=== "Go"
```go title="coin_change_greedy.go"
/* 零錢兌換:貪婪 */
func coinChangeGreedy(coins []int, amt int) int {
// 假設 coins 串列有序
i := len(coins) - 1
count := 0
// 迴圈進行貪婪選擇,直到無剩餘金額
for amt > 0 {
// 找到小於且最接近剩餘金額的硬幣
for i > 0 && coins[i] > amt {
i--
}
// 選擇 coins[i]
amt -= coins[i]
count++
}
// 若未找到可行方案,則返回 -1
if amt != 0 {
return -1
}
return count
}
```
=== "Swift"
```swift title="coin_change_greedy.swift"
/* 零錢兌換:貪婪 */
func coinChangeGreedy(coins: [Int], amt: Int) -> Int {
// 假設 coins 串列有序
var i = coins.count - 1
var count = 0
var amt = amt
// 迴圈進行貪婪選擇,直到無剩餘金額
while amt > 0 {
// 找到小於且最接近剩餘金額的硬幣
while i > 0 && coins[i] > amt {
i -= 1
}
// 選擇 coins[i]
amt -= coins[i]
count += 1
}
// 若未找到可行方案,則返回 -1
return amt == 0 ? count : -1
}
```
=== "JS"
```javascript title="coin_change_greedy.js"
/* 零錢兌換:貪婪 */
function coinChangeGreedy(coins, amt) {
// 假設 coins 陣列有序
let i = coins.length - 1;
let count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while (amt > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > amt) {
i--;
}
// 選擇 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,則返回 -1
return amt === 0 ? count : -1;
}
```
=== "TS"
```typescript title="coin_change_greedy.ts"
/* 零錢兌換:貪婪 */
function coinChangeGreedy(coins: number[], amt: number): number {
// 假設 coins 陣列有序
let i = coins.length - 1;
let count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while (amt > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > amt) {
i--;
}
// 選擇 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,則返回 -1
return amt === 0 ? count : -1;
}
```
=== "Dart"
```dart title="coin_change_greedy.dart"
/* 零錢兌換:貪婪 */
int coinChangeGreedy(List<int> coins, int amt) {
// 假設 coins 串列有序
int i = coins.length - 1;
int count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while (amt > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > amt) {
i--;
}
// 選擇 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,則返回 -1
return amt == 0 ? count : -1;
}
```
=== "Rust"
```rust title="coin_change_greedy.rs"
/* 零錢兌換:貪婪 */
fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {
// 假設 coins 串列有序
let mut i = coins.len() - 1;
let mut count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while amt > 0 {
// 找到小於且最接近剩餘金額的硬幣
while i > 0 && coins[i] > amt {
i -= 1;
}
// 選擇 coins[i]
amt -= coins[i];
count += 1;
}
// 若未找到可行方案,則返回 -1
if amt == 0 {
count
} else {
-1
}
}
```
=== "C"
```c title="coin_change_greedy.c"
/* 零錢兌換:貪婪 */
int coinChangeGreedy(int *coins, int size, int amt) {
// 假設 coins 串列有序
int i = size - 1;
int count = 0;
// 迴圈進行貪婪選擇,直到無剩餘金額
while (amt > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > amt) {
i--;
}
// 選擇 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,則返回 -1
return amt == 0 ? count : -1;
}
```
=== "Kotlin"
```kotlin title="coin_change_greedy.kt"
/* 零錢兌換:貪婪 */
fun coinChangeGreedy(coins: IntArray, amt: Int): Int {
// 假設 coins 串列有序
var am = amt
var i = coins.size - 1
var count = 0
// 迴圈進行貪婪選擇,直到無剩餘金額
while (am > 0) {
// 找到小於且最接近剩餘金額的硬幣
while (i > 0 && coins[i] > am) {
i--
}
// 選擇 coins[i]
am -= coins[i]
count++
}
// 若未找到可行方案,則返回 -1
return if (am == 0) count else -1
}
```
=== "Ruby"
```ruby title="coin_change_greedy.rb"
[class]{}-[func]{coin_change_greedy}
```
=== "Zig"
```zig title="coin_change_greedy.zig"
[class]{}-[func]{coinChangeGreedy}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D,%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%81%87%E8%AE%BE%20coins%20%E5%88%97%E8%A1%A8%E6%9C%89%E5%BA%8F%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%BF%9B%E8%A1%8C%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E5%88%B0%E6%97%A0%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E5%B0%8F%E4%BA%8E%E4%B8%94%E6%9C%80%E6%8E%A5%E8%BF%91%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%E7%9A%84%E7%A1%AC%E5%B8%81%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E9%80%89%E6%8B%A9%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E8%8B%A5%E6%9C%AA%E6%89%BE%E5%88%B0%E5%8F%AF%E8%A1%8C%E6%96%B9%E6%A1%88%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E8%83%BD%E5%A4%9F%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%205,%2010,%2020,%2050,%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E6%97%A0%E6%B3%95%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%2020,%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9E%E9%99%85%E4%B8%8A%E9%9C%80%E8%A6%81%E7%9A%84%E6%9C%80%E5%B0%91%E6%95%B0%E9%87%8F%E4%B8%BA%203%20%EF%BC%8C%E5%8D%B3%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D,%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%81%87%E8%AE%BE%20coins%20%E5%88%97%E8%A1%A8%E6%9C%89%E5%BA%8F%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%BF%9B%E8%A1%8C%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E5%88%B0%E6%97%A0%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E5%B0%8F%E4%BA%8E%E4%B8%94%E6%9C%80%E6%8E%A5%E8%BF%91%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%E7%9A%84%E7%A1%AC%E5%B8%81%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E9%80%89%E6%8B%A9%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E8%8B%A5%E6%9C%AA%E6%89%BE%E5%88%B0%E5%8F%AF%E8%A1%8C%E6%96%B9%E6%A1%88%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E8%83%BD%E5%A4%9F%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%205,%2010,%2020,%2050,%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E6%97%A0%E6%B3%95%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%2020,%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9E%E9%99%85%E4%B8%8A%E9%9C%80%E8%A6%81%E7%9A%84%E6%9C%80%E5%B0%91%E6%95%B0%E9%87%8F%E4%B8%BA%203%20%EF%BC%8C%E5%8D%B3%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
你可能會不由地發出感嘆:So clean !貪婪演算法僅用約十行程式碼就解決了零錢兌換問題。
## 15.1.1 &nbsp; 貪婪演算法的優點與侷限性
**貪婪演算法不僅操作直接、實現簡單,而且通常效率也很高**。在以上程式碼中,記硬幣最小面值為 $\min(coins)$ ,則貪婪選擇最多迴圈 $amt / \min(coins)$ 次,時間複雜度為 $O(amt / \min(coins))$ 。這比動態規劃解法的時間複雜度 $O(n \times amt)$ 小了一個數量級。
然而,**對於某些硬幣面值組合,貪婪演算法並不能找到最優解**。圖 15-2 給出了兩個示例。
- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:在該硬幣組合下,給定任意 $amt$ ,貪婪演算法都可以找到最優解。
- **反例 $coins = [1, 20, 50]$**:假設 $amt = 60$ ,貪婪演算法只能找到 $50 + 1 \times 10$ 的兌換組合,共計 $11$ 枚硬幣,但動態規劃可以找到最優解 $20 + 20 + 20$ ,僅需 $3$ 枚硬幣。
- **反例 $coins = [1, 49, 50]$**:假設 $amt = 98$ ,貪婪演算法只能找到 $50 + 1 \times 48$ 的兌換組合,共計 $49$ 枚硬幣,但動態規劃可以找到最優解 $49 + 49$ ,僅需 $2$ 枚硬幣。
![貪婪演算法無法找出最優解的示例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" }
<p align="center"> 圖 15-2 &nbsp; 貪婪演算法無法找出最優解的示例 </p>
也就是說,對於零錢兌換問題,貪婪演算法無法保證找到全域性最優解,並且有可能找到非常差的解。它更適合用動態規劃解決。
一般情況下,貪婪演算法的適用情況分以下兩種。
1. **可以保證找到最優解**:貪婪演算法在這種情況下往往是最優選擇,因為它往往比回溯、動態規劃更高效。
2. **可以找到近似最優解**:貪婪演算法在這種情況下也是可用的。對於很多複雜問題來說,尋找全域性最優解非常困難,能以較高效率找到次優解也是非常不錯的。
## 15.1.2 &nbsp; 貪婪演算法特性
那麼問題來了,什麼樣的問題適合用貪婪演算法求解呢?或者說,貪婪演算法在什麼情況下可以保證找到最優解?
相較於動態規劃,貪婪演算法的使用條件更加苛刻,其主要關注問題的兩個性質。
- **貪婪選擇性質**:只有當局部最優選擇始終可以導致全域性最優解時,貪婪演算法才能保證得到最優解。
- **最優子結構**:原問題的最優解包含子問題的最優解。
最優子結構已經在“動態規劃”章節中介紹過,這裡不再贅述。值得注意的是,一些問題的最優子結構並不明顯,但仍然可使用貪婪演算法解決。
我們主要探究貪婪選擇性質的判斷方法。雖然它的描述看上去比較簡單,**但實際上對於許多問題,證明貪婪選擇性質並非易事**。
例如零錢兌換問題,我們雖然能夠容易地舉出反例,對貪婪選擇性質進行證偽,但證實的難度較大。如果問:**滿足什麼條件的硬幣組合可以使用貪婪演算法求解**?我們往往只能憑藉直覺或舉例子來給出一個模稜兩可的答案,而難以給出嚴謹的數學證明。
!!! quote
有一篇論文給出了一個 $O(n^3)$ 時間複雜度的演算法,用於判斷一個硬幣組合能否使用貪婪演算法找出任意金額的最優解。
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
## 15.1.3 &nbsp; 貪婪演算法解題步驟
貪婪問題的解決流程大體可分為以下三步。
1. **問題分析**:梳理與理解問題特性,包括狀態定義、最佳化目標和約束條件等。這一步在回溯和動態規劃中都有涉及。
2. **確定貪婪策略**:確定如何在每一步中做出貪婪選擇。這個策略能夠在每一步減小問題的規模,並最終解決整個問題。
3. **正確性證明**:通常需要證明問題具有貪婪選擇性質和最優子結構。這個步驟可能需要用到數學證明,例如歸納法或反證法等。
確定貪婪策略是求解問題的核心步驟,但實施起來可能並不容易,主要有以下原因。
- **不同問題的貪婪策略的差異較大**。對於許多問題來說,貪婪策略比較淺顯,我們透過一些大概的思考與嘗試就能得出。而對於一些複雜問題,貪婪策略可能非常隱蔽,這種情況就非常考驗個人的解題經驗與演算法能力了。
- **某些貪婪策略具有較強的迷惑性**。當我們滿懷信心設計好貪婪策略,寫出解題程式碼並提交執行,很可能發現部分測試樣例無法透過。這是因為設計的貪婪策略只是“部分正確”的,上文介紹的零錢兌換就是一個典型案例。
為了保證正確性,我們應該對貪婪策略進行嚴謹的數學證明,**通常需要用到反證法或數學歸納法**。
然而,正確性證明也很可能不是一件易事。如若沒有頭緒,我們通常會選擇面向測試用例進行程式碼除錯,一步步修改與驗證貪婪策略。
## 15.1.4 &nbsp; 貪婪演算法典型例題
貪婪演算法常常應用在滿足貪婪選擇性質和最優子結構的最佳化問題中,以下列舉了一些典型的貪婪演算法問題。
- **硬幣找零問題**:在某些硬幣組合下,貪婪演算法總是可以得到最優解。
- **區間排程問題**:假設你有一些任務,每個任務在一段時間內進行,你的目標是完成儘可能多的任務。如果每次都選擇結束時間最早的任務,那麼貪婪演算法就可以得到最優解。
- **分數背包問題**:給定一組物品和一個載重量,你的目標是選擇一組物品,使得總重量不超過載重量,且總價值最大。如果每次都選擇價效比最高(價值 / 重量)的物品,那麼貪婪演算法在一些情況下可以得到最優解。
- **股票買賣問題**:給定一組股票的歷史價格,你可以進行多次買賣,但如果你已經持有股票,那麼在賣出之前不能再買,目標是獲取最大利潤。
- **霍夫曼編碼**:霍夫曼編碼是一種用於無損資料壓縮的貪婪演算法。透過構建霍夫曼樹,每次選擇出現頻率最低的兩個節點合併,最後得到的霍夫曼樹的帶權路徑長度(編碼長度)最小。
- **Dijkstra 演算法**:它是一種解決給定源頂點到其餘各頂點的最短路徑問題的貪婪演算法。
+22
View File
@@ -0,0 +1,22 @@
---
comments: true
icon: material/head-heart-outline
---
# 第 15 章 &nbsp; 貪婪
![貪婪](../assets/covers/chapter_greedy.jpg){ class="cover-image" }
!!! abstract
向日葵朝著太陽轉動,時刻追求自身成長的最大可能。
貪婪策略在一輪輪的簡單選擇中,逐步導向最佳答案。
## Chapter Contents
- [15.1 &nbsp; 貪婪演算法](https://www.hello-algo.com/en/chapter_greedy/greedy_algorithm/)
- [15.2 &nbsp; 分數背包問題](https://www.hello-algo.com/en/chapter_greedy/fractional_knapsack_problem/)
- [15.3 &nbsp; 最大容量問題](https://www.hello-algo.com/en/chapter_greedy/max_capacity_problem/)
- [15.4 &nbsp; 最大切分乘積問題](https://www.hello-algo.com/en/chapter_greedy/max_product_cutting_problem/)
- [15.5 &nbsp; 小結](https://www.hello-algo.com/en/chapter_greedy/summary/)
@@ -0,0 +1,430 @@
---
comments: true
---
# 15.3 &nbsp; 最大容量問題
!!! question
輸入一個陣列 $ht$ ,其中的每個元素代表一個垂直隔板的高度。陣列中的任意兩個隔板,以及它們之間的空間可以組成一個容器。
容器的容量等於高度和寬度的乘積(面積),其中高度由較短的隔板決定,寬度是兩個隔板的陣列索引之差。
請在陣列中選擇兩個隔板,使得組成的容器的容量最大,返回最大容量。示例如圖 15-7 所示。
![最大容量問題的示例資料](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" }
<p align="center"> 圖 15-7 &nbsp; 最大容量問題的示例資料 </p>
容器由任意兩個隔板圍成,**因此本題的狀態為兩個隔板的索引,記為 $[i, j]$** 。
根據題意,容量等於高度乘以寬度,其中高度由短板決定,寬度是兩隔板的陣列索引之差。設容量為 $cap[i, j]$ ,則可得計算公式:
$$
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
$$
設陣列長度為 $n$ ,兩個隔板的組合數量(狀態總數)為 $C_n^2 = \frac{n(n - 1)}{2}$ 個。最直接地,**我們可以窮舉所有狀態**,從而求得最大容量,時間複雜度為 $O(n^2)$ 。
### 1. &nbsp; 貪婪策略確定
這道題還有更高效率的解法。如圖 15-8 所示,現選取一個狀態 $[i, j]$ ,其滿足索引 $i < j$ 且高度 $ht[i] < ht[j]$ ,即 $i$ 為短板、$j$ 為長板。
![初始狀態](max_capacity_problem.assets/max_capacity_initial_state.png){ class="animation-figure" }
<p align="center"> 圖 15-8 &nbsp; 初始狀態 </p>
如圖 15-9 所示,**若此時將長板 $j$ 向短板 $i$ 靠近,則容量一定變小**。
這是因為在移動長板 $j$ 後,寬度 $j-i$ 肯定變小;而高度由短板決定,因此高度只可能不變( $i$ 仍為短板)或變小(移動後的 $j$ 成為短板)。
![向內移動長板後的狀態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" }
<p align="center"> 圖 15-9 &nbsp; 向內移動長板後的狀態 </p>
反向思考,**我們只有向內收縮短板 $i$ ,才有可能使容量變大**。因為雖然寬度一定變小,**但高度可能會變大**(移動後的短板 $i$ 可能會變長)。例如在圖 15-10 中,移動短板後面積變大。
![向內移動短板後的狀態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" }
<p align="center"> 圖 15-10 &nbsp; 向內移動短板後的狀態 </p>
由此便可推出本題的貪婪策略:初始化兩指標,使其分列容器兩端,每輪向內收縮短板對應的指標,直至兩指標相遇。
圖 15-11 展示了貪婪策略的執行過程。
1. 初始狀態下,指標 $i$ 和 $j$ 分列陣列兩端。
2. 計算當前狀態的容量 $cap[i, j]$ ,並更新最大容量。
3. 比較板 $i$ 和 板 $j$ 的高度,並將短板向內移動一格。
4. 迴圈執行第 `2.` 步和第 `3.` 步,直至 $i$ 和 $j$ 相遇時結束。
=== "<1>"
![最大容量問題的貪婪過程](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" }
=== "<2>"
![max_capacity_greedy_step2](max_capacity_problem.assets/max_capacity_greedy_step2.png){ class="animation-figure" }
=== "<3>"
![max_capacity_greedy_step3](max_capacity_problem.assets/max_capacity_greedy_step3.png){ class="animation-figure" }
=== "<4>"
![max_capacity_greedy_step4](max_capacity_problem.assets/max_capacity_greedy_step4.png){ class="animation-figure" }
=== "<5>"
![max_capacity_greedy_step5](max_capacity_problem.assets/max_capacity_greedy_step5.png){ class="animation-figure" }
=== "<6>"
![max_capacity_greedy_step6](max_capacity_problem.assets/max_capacity_greedy_step6.png){ class="animation-figure" }
=== "<7>"
![max_capacity_greedy_step7](max_capacity_problem.assets/max_capacity_greedy_step7.png){ class="animation-figure" }
=== "<8>"
![max_capacity_greedy_step8](max_capacity_problem.assets/max_capacity_greedy_step8.png){ class="animation-figure" }
=== "<9>"
![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png){ class="animation-figure" }
<p align="center"> 圖 15-11 &nbsp; 最大容量問題的貪婪過程 </p>
### 2. &nbsp; 程式碼實現
程式碼迴圈最多 $n$ 輪,**因此時間複雜度為 $O(n)$** 。
變數 $i$、$j$、$res$ 使用常數大小的額外空間,**因此空間複雜度為 $O(1)$** 。
=== "Python"
```python title="max_capacity.py"
def max_capacity(ht: list[int]) -> int:
"""最大容量:貪婪"""
# 初始化 i, j,使其分列陣列兩端
i, j = 0, len(ht) - 1
# 初始最大容量為 0
res = 0
# 迴圈貪婪選擇,直至兩板相遇
while i < j:
# 更新最大容量
cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
# 向內移動短板
if ht[i] < ht[j]:
i += 1
else:
j -= 1
return res
```
=== "C++"
```cpp title="max_capacity.cpp"
/* 最大容量:貪婪 */
int maxCapacity(vector<int> &ht) {
// 初始化 i, j,使其分列陣列兩端
int i = 0, j = ht.size() - 1;
// 初始最大容量為 0
int res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
int cap = min(ht[i], ht[j]) * (j - i);
res = max(res, cap);
// 向內移動短板
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Java"
```java title="max_capacity.java"
/* 最大容量:貪婪 */
int maxCapacity(int[] ht) {
// 初始化 i, j,使其分列陣列兩端
int i = 0, j = ht.length - 1;
// 初始最大容量為 0
int res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
int cap = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// 向內移動短板
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "C#"
```csharp title="max_capacity.cs"
/* 最大容量:貪婪 */
int MaxCapacity(int[] ht) {
// 初始化 i, j,使其分列陣列兩端
int i = 0, j = ht.Length - 1;
// 初始最大容量為 0
int res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
int cap = Math.Min(ht[i], ht[j]) * (j - i);
res = Math.Max(res, cap);
// 向內移動短板
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Go"
```go title="max_capacity.go"
/* 最大容量:貪婪 */
func maxCapacity(ht []int) int {
// 初始化 i, j,使其分列陣列兩端
i, j := 0, len(ht)-1
// 初始最大容量為 0
res := 0
// 迴圈貪婪選擇,直至兩板相遇
for i < j {
// 更新最大容量
capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)
res = int(math.Max(float64(res), float64(capacity)))
// 向內移動短板
if ht[i] < ht[j] {
i++
} else {
j--
}
}
return res
}
```
=== "Swift"
```swift title="max_capacity.swift"
/* 最大容量:貪婪 */
func maxCapacity(ht: [Int]) -> Int {
// 初始化 i, j,使其分列陣列兩端
var i = ht.startIndex, j = ht.endIndex - 1
// 初始最大容量為 0
var res = 0
// 迴圈貪婪選擇,直至兩板相遇
while i < j {
// 更新最大容量
let cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
// 向內移動短板
if ht[i] < ht[j] {
i += 1
} else {
j -= 1
}
}
return res
}
```
=== "JS"
```javascript title="max_capacity.js"
/* 最大容量:貪婪 */
function maxCapacity(ht) {
// 初始化 i, j,使其分列陣列兩端
let i = 0,
j = ht.length - 1;
// 初始最大容量為 0
let res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
const cap = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// 向內移動短板
if (ht[i] < ht[j]) {
i += 1;
} else {
j -= 1;
}
}
return res;
}
```
=== "TS"
```typescript title="max_capacity.ts"
/* 最大容量:貪婪 */
function maxCapacity(ht: number[]): number {
// 初始化 i, j,使其分列陣列兩端
let i = 0,
j = ht.length - 1;
// 初始最大容量為 0
let res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
const cap: number = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// 向內移動短板
if (ht[i] < ht[j]) {
i += 1;
} else {
j -= 1;
}
}
return res;
}
```
=== "Dart"
```dart title="max_capacity.dart"
/* 最大容量:貪婪 */
int maxCapacity(List<int> ht) {
// 初始化 i, j,使其分列陣列兩端
int i = 0, j = ht.length - 1;
// 初始最大容量為 0
int res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
int cap = min(ht[i], ht[j]) * (j - i);
res = max(res, cap);
// 向內移動短板
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Rust"
```rust title="max_capacity.rs"
/* 最大容量:貪婪 */
fn max_capacity(ht: &[i32]) -> i32 {
// 初始化 i, j,使其分列陣列兩端
let mut i = 0;
let mut j = ht.len() - 1;
// 初始最大容量為 0
let mut res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while i < j {
// 更新最大容量
let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;
res = std::cmp::max(res, cap);
// 向內移動短板
if ht[i] < ht[j] {
i += 1;
} else {
j -= 1;
}
}
res
}
```
=== "C"
```c title="max_capacity.c"
/* 最大容量:貪婪 */
int maxCapacity(int ht[], int htLength) {
// 初始化 i, j,使其分列陣列兩端
int i = 0;
int j = htLength - 1;
// 初始最大容量為 0
int res = 0;
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
int capacity = myMin(ht[i], ht[j]) * (j - i);
res = myMax(res, capacity);
// 向內移動短板
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Kotlin"
```kotlin title="max_capacity.kt"
/* 最大容量:貪婪 */
fun maxCapacity(ht: IntArray): Int {
// 初始化 i, j,使其分列陣列兩端
var i = 0
var j = ht.size - 1
// 初始最大容量為 0
var res = 0
// 迴圈貪婪選擇,直至兩板相遇
while (i < j) {
// 更新最大容量
val cap = (min(ht[i].toDouble(), ht[j].toDouble()) * (j - i)).toInt()
res = max(res.toDouble(), cap.toDouble()).toInt()
// 向內移動短板
if (ht[i] < ht[j]) {
i++
} else {
j--
}
}
return res
}
```
=== "Ruby"
```ruby title="max_capacity.rb"
[class]{}-[func]{max_capacity}
```
=== "Zig"
```zig title="max_capacity.zig"
[class]{}-[func]{maxCapacity}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20i,%20j%EF%BC%8C%E4%BD%BF%E5%85%B6%E5%88%86%E5%88%97%E6%95%B0%E7%BB%84%E4%B8%A4%E7%AB%AF%0A%20%20%20%20i,%20j%20%3D%200,%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E8%87%B3%E4%B8%A4%E6%9D%BF%E7%9B%B8%E9%81%87%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D,%20ht%5Bj%5D%29%20*%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res,%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E5%90%91%E5%86%85%E7%A7%BB%E5%8A%A8%E7%9F%AD%E6%9D%BF%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3,%208,%205,%202,%207,%207,%203,%204%5D%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20i,%20j%EF%BC%8C%E4%BD%BF%E5%85%B6%E5%88%86%E5%88%97%E6%95%B0%E7%BB%84%E4%B8%A4%E7%AB%AF%0A%20%20%20%20i,%20j%20%3D%200,%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E8%87%B3%E4%B8%A4%E6%9D%BF%E7%9B%B8%E9%81%87%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D,%20ht%5Bj%5D%29%20*%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res,%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E5%90%91%E5%86%85%E7%A7%BB%E5%8A%A8%E7%9F%AD%E6%9D%BF%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3,%208,%205,%202,%207,%207,%203,%204%5D%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
### 3. &nbsp; 正確性證明
之所以貪婪比窮舉更快,是因為每輪的貪婪選擇都會“跳過”一些狀態。
比如在狀態 $cap[i, j]$ 下,$i$ 為短板、$j$ 為長板。若貪婪地將短板 $i$ 向內移動一格,會導致圖 15-12 所示的狀態被“跳過”。**這意味著之後無法驗證這些狀態的容量大小**。
$$
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
$$
![移動短板導致被跳過的狀態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" }
<p align="center"> 圖 15-12 &nbsp; 移動短板導致被跳過的狀態 </p>
觀察發現,**這些被跳過的狀態實際上就是將長板 $j$ 向內移動的所有狀態**。前面我們已經證明內移長板一定會導致容量變小。也就是說,被跳過的狀態都不可能是最優解,**跳過它們不會導致錯過最優解**。
以上分析說明,移動短板的操作是“安全”的,貪婪策略是有效的。
@@ -0,0 +1,405 @@
---
comments: true
---
# 15.4 &nbsp; 最大切分乘積問題
!!! question
給定一個正整數 $n$ ,將其切分為至少兩個正整數的和,求切分後所有整數的乘積最大是多少,如圖 15-13 所示。
![最大切分乘積的問題定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" }
<p align="center"> 圖 15-13 &nbsp; 最大切分乘積的問題定義 </p>
假設我們將 $n$ 切分為 $m$ 個整數因子,其中第 $i$ 個因子記為 $n_i$ ,即
$$
n = \sum_{i=1}^{m}n_i
$$
本題的目標是求得所有整數因子的最大乘積,即
$$
\max(\prod_{i=1}^{m}n_i)
$$
我們需要思考的是:切分數量 $m$ 應該多大,每個 $n_i$ 應該是多少?
### 1. &nbsp; 貪婪策略確定
根據經驗,兩個整數的乘積往往比它們的加和更大。假設從 $n$ 中分出一個因子 $2$ ,則它們的乘積為 $2(n-2)$ 。我們將該乘積與 $n$ 作比較:
$$
\begin{aligned}
2(n-2) & \geq n \newline
2n - n - 4 & \geq 0 \newline
n & \geq 4
\end{aligned}
$$
如圖 15-14 所示,當 $n \geq 4$ 時,切分出一個 $2$ 後乘積會變大,**這說明大於等於 $4$ 的整數都應該被切分**。
**貪婪策略一**:如果切分方案中包含 $\geq 4$ 的因子,那麼它就應該被繼續切分。最終的切分方案只應出現 $1$、$2$、$3$ 這三種因子。
![切分導致乘積變大](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" }
<p align="center"> 圖 15-14 &nbsp; 切分導致乘積變大 </p>
接下來思考哪個因子是最優的。在 $1$、$2$、$3$ 這三個因子中,顯然 $1$ 是最差的,因為 $1 \times (n-1) < n$ 恆成立,即切分出 $1$ 反而會導致乘積減小。
如圖 15-15 所示,當 $n = 6$ 時,有 $3 \times 3 > 2 \times 2 \times 2$ 。**這意味著切分出 $3$ 比切分出 $2$ 更優**。
**貪婪策略二**:在切分方案中,最多隻應存在兩個 $2$ 。因為三個 $2$ 總是可以替換為兩個 $3$ ,從而獲得更大的乘積。
![最優切分因子](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" }
<p align="center"> 圖 15-15 &nbsp; 最優切分因子 </p>
綜上所述,可推理出以下貪婪策略。
1. 輸入整數 $n$ ,從其不斷地切分出因子 $3$ ,直至餘數為 $0$、$1$、$2$ 。
2. 當餘數為 $0$ 時,代表 $n$ 是 $3$ 的倍數,因此不做任何處理。
3. 當餘數為 $2$ 時,不繼續劃分,保留。
4. 當餘數為 $1$ 時,由於 $2 \times 2 > 1 \times 3$ ,因此應將最後一個 $3$ 替換為 $2$ 。
### 2. &nbsp; 程式碼實現
如圖 15-16 所示,我們無須透過迴圈來切分整數,而可以利用向下整除運算得到 $3$ 的個數 $a$ ,用取模運算得到餘數 $b$ ,此時有:
$$
n = 3 a + b
$$
請注意,對於 $n \leq 3$ 的邊界情況,必須拆分出一個 $1$ ,乘積為 $1 \times (n - 1)$ 。
=== "Python"
```python title="max_product_cutting.py"
def max_product_cutting(n: int) -> int:
"""最大切分乘積:貪婪"""
# 當 n <= 3 時,必須切分出一個 1
if n <= 3:
return 1 * (n - 1)
# 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
a, b = n // 3, n % 3
if b == 1:
# 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return int(math.pow(3, a - 1)) * 2 * 2
if b == 2:
# 當餘數為 2 時,不做處理
return int(math.pow(3, a)) * 2
# 當餘數為 0 時,不做處理
return int(math.pow(3, a))
```
=== "C++"
```cpp title="max_product_cutting.cpp"
/* 最大切分乘積:貪婪 */
int maxProductCutting(int n) {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return (int)pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 當餘數為 2 時,不做處理
return (int)pow(3, a) * 2;
}
// 當餘數為 0 時,不做處理
return (int)pow(3, a);
}
```
=== "Java"
```java title="max_product_cutting.java"
/* 最大切分乘積:貪婪 */
int maxProductCutting(int n) {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return (int) Math.pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 當餘數為 2 時,不做處理
return (int) Math.pow(3, a) * 2;
}
// 當餘數為 0 時,不做處理
return (int) Math.pow(3, a);
}
```
=== "C#"
```csharp title="max_product_cutting.cs"
/* 最大切分乘積:貪婪 */
int MaxProductCutting(int n) {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return (int)Math.Pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 當餘數為 2 時,不做處理
return (int)Math.Pow(3, a) * 2;
}
// 當餘數為 0 時,不做處理
return (int)Math.Pow(3, a);
}
```
=== "Go"
```go title="max_product_cutting.go"
/* 最大切分乘積:貪婪 */
func maxProductCutting(n int) int {
// 當 n <= 3 時,必須切分出一個 1
if n <= 3 {
return 1 * (n - 1)
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
a := n / 3
b := n % 3
if b == 1 {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return int(math.Pow(3, float64(a-1))) * 2 * 2
}
if b == 2 {
// 當餘數為 2 時,不做處理
return int(math.Pow(3, float64(a))) * 2
}
// 當餘數為 0 時,不做處理
return int(math.Pow(3, float64(a)))
}
```
=== "Swift"
```swift title="max_product_cutting.swift"
/* 最大切分乘積:貪婪 */
func maxProductCutting(n: Int) -> Int {
// 當 n <= 3 時,必須切分出一個 1
if n <= 3 {
return 1 * (n - 1)
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
let a = n / 3
let b = n % 3
if b == 1 {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return pow(3, a - 1) * 2 * 2
}
if b == 2 {
// 當餘數為 2 時,不做處理
return pow(3, a) * 2
}
// 當餘數為 0 時,不做處理
return pow(3, a)
}
```
=== "JS"
```javascript title="max_product_cutting.js"
/* 最大切分乘積:貪婪 */
function maxProductCutting(n) {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
let a = Math.floor(n / 3);
let b = n % 3;
if (b === 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return Math.pow(3, a - 1) * 2 * 2;
}
if (b === 2) {
// 當餘數為 2 時,不做處理
return Math.pow(3, a) * 2;
}
// 當餘數為 0 時,不做處理
return Math.pow(3, a);
}
```
=== "TS"
```typescript title="max_product_cutting.ts"
/* 最大切分乘積:貪婪 */
function maxProductCutting(n: number): number {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
let a: number = Math.floor(n / 3);
let b: number = n % 3;
if (b === 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return Math.pow(3, a - 1) * 2 * 2;
}
if (b === 2) {
// 當餘數為 2 時,不做處理
return Math.pow(3, a) * 2;
}
// 當餘數為 0 時,不做處理
return Math.pow(3, a);
}
```
=== "Dart"
```dart title="max_product_cutting.dart"
/* 最大切分乘積:貪婪 */
int maxProductCutting(int n) {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
int a = n ~/ 3;
int b = n % 3;
if (b == 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return (pow(3, a - 1) * 2 * 2).toInt();
}
if (b == 2) {
// 當餘數為 2 時,不做處理
return (pow(3, a) * 2).toInt();
}
// 當餘數為 0 時,不做處理
return pow(3, a).toInt();
}
```
=== "Rust"
```rust title="max_product_cutting.rs"
/* 最大切分乘積:貪婪 */
fn max_product_cutting(n: i32) -> i32 {
// 當 n <= 3 時,必須切分出一個 1
if n <= 3 {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
let a = n / 3;
let b = n % 3;
if b == 1 {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
3_i32.pow(a as u32 - 1) * 2 * 2
} else if b == 2 {
// 當餘數為 2 時,不做處理
3_i32.pow(a as u32) * 2
} else {
// 當餘數為 0 時,不做處理
3_i32.pow(a as u32)
}
}
```
=== "C"
```c title="max_product_cutting.c"
/* 最大切分乘積:貪婪 */
int maxProductCutting(int n) {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1);
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 當餘數為 2 時,不做處理
return pow(3, a) * 2;
}
// 當餘數為 0 時,不做處理
return pow(3, a);
}
```
=== "Kotlin"
```kotlin title="max_product_cutting.kt"
/* 最大切分乘積:貪婪 */
fun maxProductCutting(n: Int): Int {
// 當 n <= 3 時,必須切分出一個 1
if (n <= 3) {
return 1 * (n - 1)
}
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
val a = n / 3
val b = n % 3
if (b == 1) {
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
return 3.0.pow((a - 1).toDouble()).toInt() * 2 * 2
}
if (b == 2) {
// 當餘數為 2 時,不做處理
return 3.0.pow(a.toDouble()).toInt() * 2 * 2
}
// 當餘數為 0 時,不做處理
return 3.0.pow(a.toDouble()).toInt()
}
```
=== "Ruby"
```ruby title="max_product_cutting.rb"
[class]{}-[func]{max_product_cutting}
```
=== "Zig"
```zig title="max_product_cutting.zig"
[class]{}-[func]{maxProductCutting}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%20n%20%3C%3D%203%20%E6%97%B6%EF%BC%8C%E5%BF%85%E9%A1%BB%E5%88%87%E5%88%86%E5%87%BA%E4%B8%80%E4%B8%AA%201%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20*%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E5%9C%B0%E5%88%87%E5%88%86%E5%87%BA%203%20%EF%BC%8Ca%20%E4%B8%BA%203%20%E7%9A%84%E4%B8%AA%E6%95%B0%EF%BC%8Cb%20%E4%B8%BA%E4%BD%99%E6%95%B0%0A%20%20%20%20a,%20b%20%3D%20n%20//%203,%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%201%20%E6%97%B6%EF%BC%8C%E5%B0%86%E4%B8%80%E5%AF%B9%201%20*%203%20%E8%BD%AC%E5%8C%96%E4%B8%BA%202%20*%202%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%20-%201%29%29%20*%202%20*%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%202%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%29%29%20*%202%0A%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%200%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20return%20int%28math.pow%283,%20a%29%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%20n%20%3C%3D%203%20%E6%97%B6%EF%BC%8C%E5%BF%85%E9%A1%BB%E5%88%87%E5%88%86%E5%87%BA%E4%B8%80%E4%B8%AA%201%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20*%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E5%9C%B0%E5%88%87%E5%88%86%E5%87%BA%203%20%EF%BC%8Ca%20%E4%B8%BA%203%20%E7%9A%84%E4%B8%AA%E6%95%B0%EF%BC%8Cb%20%E4%B8%BA%E4%BD%99%E6%95%B0%0A%20%20%20%20a,%20b%20%3D%20n%20//%203,%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%201%20%E6%97%B6%EF%BC%8C%E5%B0%86%E4%B8%80%E5%AF%B9%201%20*%203%20%E8%BD%AC%E5%8C%96%E4%B8%BA%202%20*%202%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%20-%201%29%29%20*%202%20*%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%202%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%29%29%20*%202%0A%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%200%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20return%20int%28math.pow%283,%20a%29%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
![最大切分乘積的計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" }
<p align="center"> 圖 15-16 &nbsp; 最大切分乘積的計算方法 </p>
**時間複雜度取決於程式語言的冪運算的實現方法**。以 Python 為例,常用的冪計算函式有三種。
- 運算子 `**` 和函式 `pow()` 的時間複雜度均為 $O(\log a)$ 。
- 函式 `math.pow()` 內部呼叫 C 語言庫的 `pow()` 函式,其執行浮點取冪,時間複雜度為 $O(1)$ 。
變數 $a$ 和 $b$ 使用常數大小的額外空間,**因此空間複雜度為 $O(1)$** 。
### 3. &nbsp; 正確性證明
使用反證法,只分析 $n \geq 3$ 的情況。
1. **所有因子 $\leq 3$** :假設最優切分方案中存在 $\geq 4$ 的因子 $x$ ,那麼一定可以將其繼續劃分為 $2(x-2)$ ,從而獲得更大的乘積。這與假設矛盾。
2. **切分方案不包含 $1$** :假設最優切分方案中存在一個因子 $1$ ,那麼它一定可以合併入另外一個因子中,以獲得更大的乘積。這與假設矛盾。
3. **切分方案最多包含兩個 $2$** :假設最優切分方案中包含三個 $2$ ,那麼一定可以替換為兩個 $3$ ,乘積更大。這與假設矛盾。
+16
View File
@@ -0,0 +1,16 @@
---
comments: true
---
# 15.5 &nbsp; 小結
- 貪婪演算法通常用於解決最最佳化問題,其原理是在每個決策階段都做出區域性最優的決策,以期獲得全域性最優解。
- 貪婪演算法會迭代地做出一個又一個的貪婪選擇,每輪都將問題轉化成一個規模更小的子問題,直到問題被解決。
- 貪婪演算法不僅實現簡單,還具有很高的解題效率。相比於動態規劃,貪婪演算法的時間複雜度通常更低。
- 在零錢兌換問題中,對於某些硬幣組合,貪婪演算法可以保證找到最優解;對於另外一些硬幣組合則不然,貪婪演算法可能找到很差的解。
- 適合用貪婪演算法求解的問題具有兩大性質:貪婪選擇性質和最優子結構。貪婪選擇性質代表貪婪策略的有效性。
- 對於某些複雜問題,貪婪選擇性質的證明並不簡單。相對來說,證偽更加容易,例如零錢兌換問題。
- 求解貪婪問題主要分為三步:問題分析、確定貪婪策略、正確性證明。其中,確定貪婪策略是核心步驟,正確性證明往往是難點。
- 分數背包問題在 0-1 背包的基礎上,允許選擇物品的一部分,因此可使用貪婪演算法求解。貪婪策略的正確性可以使用反證法來證明。
- 最大容量問題可使用窮舉法求解,時間複雜度為 $O(n^2)$ 。透過設計貪婪策略,每輪向內移動短板,可將時間複雜度最佳化至 $O(n)$ 。
- 在最大切分乘積問題中,我們先後推理出兩個貪婪策略:$\geq 4$ 的整數都應該繼續切分,最優切分因子為 $3$ 。程式碼中包含冪運算,時間複雜度取決於冪運算實現方法,通常為 $O(1)$ 或 $O(\log n)$ 。
@@ -0,0 +1,972 @@
---
comments: true
---
# 6.3 &nbsp; 雜湊演算法
前兩節介紹了雜湊表的工作原理和雜湊衝突的處理方法。然而無論是開放定址還是鏈式位址,**它們只能保證雜湊表可以在發生衝突時正常工作,而無法減少雜湊衝突的發生**。
如果雜湊衝突過於頻繁,雜湊表的效能則會急劇劣化。如圖 6-8 所示,對於鏈式位址雜湊表,理想情況下鍵值對均勻分佈在各個桶中,達到最佳查詢效率;最差情況下所有鍵值對都儲存到同一個桶中,時間複雜度退化至 $O(n)$ 。
![雜湊衝突的最佳情況與最差情況](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" }
<p align="center"> 圖 6-8 &nbsp; 雜湊衝突的最佳情況與最差情況 </p>
**鍵值對的分佈情況由雜湊函式決定**。回憶雜湊函式的計算步驟,先計算雜湊值,再對陣列長度取模:
```shell
index = hash(key) % capacity
```
觀察以上公式,當雜湊表容量 `capacity` 固定時,**雜湊演算法 `hash()` 決定了輸出值**,進而決定了鍵值對在雜湊表中的分佈情況。
這意味著,為了降低雜湊衝突的發生機率,我們應當將注意力集中在雜湊演算法 `hash()` 的設計上。
## 6.3.1 &nbsp; 雜湊演算法的目標
為了實現“既快又穩”的雜湊表資料結構,雜湊演算法應具備以下特點。
- **確定性**:對於相同的輸入,雜湊演算法應始終產生相同的輸出。這樣才能確保雜湊表是可靠的。
- **效率高**:計算雜湊值的過程應該足夠快。計算開銷越小,雜湊表的實用性越高。
- **均勻分佈**:雜湊演算法應使得鍵值對均勻分佈在雜湊表中。分佈越均勻,雜湊衝突的機率就越低。
實際上,雜湊演算法除了可以用於實現雜湊表,還廣泛應用於其他領域中。
- **密碼儲存**:為了保護使用者密碼的安全,系統通常不會直接儲存使用者的明文密碼,而是儲存密碼的雜湊值。當用戶輸入密碼時,系統會對輸入的密碼計算雜湊值,然後與儲存的雜湊值進行比較。如果兩者匹配,那麼密碼就被視為正確。
- **資料完整性檢查**:資料傳送方可以計算資料的雜湊值並將其一同傳送;接收方可以重新計算接收到的資料的雜湊值,並與接收到的雜湊值進行比較。如果兩者匹配,那麼資料就被視為完整。
對於密碼學的相關應用,為了防止從雜湊值推導出原始密碼等逆向工程,雜湊演算法需要具備更高等級的安全特性。
- **單向性**:無法透過雜湊值反推出關於輸入資料的任何資訊。
- **抗碰撞性**:應當極難找到兩個不同的輸入,使得它們的雜湊值相同。
- **雪崩效應**:輸入的微小變化應當導致輸出的顯著且不可預測的變化。
請注意,**“均勻分佈”與“抗碰撞性”是兩個獨立的概念**,滿足均勻分佈不一定滿足抗碰撞性。例如,在隨機輸入 `key` 下,雜湊函式 `key % 100` 可以產生均勻分佈的輸出。然而該雜湊演算法過於簡單,所有後兩位相等的 `key` 的輸出都相同,因此我們可以很容易地從雜湊值反推出可用的 `key` ,從而破解密碼。
## 6.3.2 &nbsp; 雜湊演算法的設計
雜湊演算法的設計是一個需要考慮許多因素的複雜問題。然而對於某些要求不高的場景,我們也能設計一些簡單的雜湊演算法。
- **加法雜湊**:對輸入的每個字元的 ASCII 碼進行相加,將得到的總和作為雜湊值。
- **乘法雜湊**:利用乘法的不相關性,每輪乘以一個常數,將各個字元的 ASCII 碼累積到雜湊值中。
- **互斥或雜湊**:將輸入資料的每個元素透過互斥或操作累積到一個雜湊值中。
- **旋轉雜湊**:將每個字元的 ASCII 碼累積到一個雜湊值中,每次累積之前都會對雜湊值進行旋轉操作。
=== "Python"
```python title="simple_hash.py"
def add_hash(key: str) -> int:
"""加法雜湊"""
hash = 0
modulus = 1000000007
for c in key:
hash += ord(c)
return hash % modulus
def mul_hash(key: str) -> int:
"""乘法雜湊"""
hash = 0
modulus = 1000000007
for c in key:
hash = 31 * hash + ord(c)
return hash % modulus
def xor_hash(key: str) -> int:
"""互斥或雜湊"""
hash = 0
modulus = 1000000007
for c in key:
hash ^= ord(c)
return hash % modulus
def rot_hash(key: str) -> int:
"""旋轉雜湊"""
hash = 0
modulus = 1000000007
for c in key:
hash = (hash << 4) ^ (hash >> 28) ^ ord(c)
return hash % modulus
```
=== "C++"
```cpp title="simple_hash.cpp"
/* 加法雜湊 */
int addHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* 乘法雜湊 */
int mulHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (31 * hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* 互斥或雜湊 */
int xorHash(string key) {
int hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash ^= (int)c;
}
return hash & MODULUS;
}
/* 旋轉雜湊 */
int rotHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;
}
return (int)hash;
}
```
=== "Java"
```java title="simple_hash.java"
/* 加法雜湊 */
int addHash(String key) {
long hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash = (hash + (int) c) % MODULUS;
}
return (int) hash;
}
/* 乘法雜湊 */
int mulHash(String key) {
long hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash = (31 * hash + (int) c) % MODULUS;
}
return (int) hash;
}
/* 互斥或雜湊 */
int xorHash(String key) {
int hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash ^= (int) c;
}
return hash & MODULUS;
}
/* 旋轉雜湊 */
int rotHash(String key) {
long hash = 0;
final int MODULUS = 1000000007;
for (char c : key.toCharArray()) {
hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;
}
return (int) hash;
}
```
=== "C#"
```csharp title="simple_hash.cs"
/* 加法雜湊 */
int AddHash(string key) {
long hash = 0;
const int MODULUS = 1000000007;
foreach (char c in key) {
hash = (hash + c) % MODULUS;
}
return (int)hash;
}
/* 乘法雜湊 */
int MulHash(string key) {
long hash = 0;
const int MODULUS = 1000000007;
foreach (char c in key) {
hash = (31 * hash + c) % MODULUS;
}
return (int)hash;
}
/* 互斥或雜湊 */
int XorHash(string key) {
int hash = 0;
const int MODULUS = 1000000007;
foreach (char c in key) {
hash ^= c;
}
return hash & MODULUS;
}
/* 旋轉雜湊 */
int RotHash(string key) {
long hash = 0;
const int MODULUS = 1000000007;
foreach (char c in key) {
hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;
}
return (int)hash;
}
```
=== "Go"
```go title="simple_hash.go"
/* 加法雜湊 */
func addHash(key string) int {
var hash int64
var modulus int64
modulus = 1000000007
for _, b := range []byte(key) {
hash = (hash + int64(b)) % modulus
}
return int(hash)
}
/* 乘法雜湊 */
func mulHash(key string) int {
var hash int64
var modulus int64
modulus = 1000000007
for _, b := range []byte(key) {
hash = (31*hash + int64(b)) % modulus
}
return int(hash)
}
/* 互斥或雜湊 */
func xorHash(key string) int {
hash := 0
modulus := 1000000007
for _, b := range []byte(key) {
fmt.Println(int(b))
hash ^= int(b)
hash = (31*hash + int(b)) % modulus
}
return hash & modulus
}
/* 旋轉雜湊 */
func rotHash(key string) int {
var hash int64
var modulus int64
modulus = 1000000007
for _, b := range []byte(key) {
hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus
}
return int(hash)
}
```
=== "Swift"
```swift title="simple_hash.swift"
/* 加法雜湊 */
func addHash(key: String) -> Int {
var hash = 0
let MODULUS = 1_000_000_007
for c in key {
for scalar in c.unicodeScalars {
hash = (hash + Int(scalar.value)) % MODULUS
}
}
return hash
}
/* 乘法雜湊 */
func mulHash(key: String) -> Int {
var hash = 0
let MODULUS = 1_000_000_007
for c in key {
for scalar in c.unicodeScalars {
hash = (31 * hash + Int(scalar.value)) % MODULUS
}
}
return hash
}
/* 互斥或雜湊 */
func xorHash(key: String) -> Int {
var hash = 0
let MODULUS = 1_000_000_007
for c in key {
for scalar in c.unicodeScalars {
hash ^= Int(scalar.value)
}
}
return hash & MODULUS
}
/* 旋轉雜湊 */
func rotHash(key: String) -> Int {
var hash = 0
let MODULUS = 1_000_000_007
for c in key {
for scalar in c.unicodeScalars {
hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS
}
}
return hash
}
```
=== "JS"
```javascript title="simple_hash.js"
/* 加法雜湊 */
function addHash(key) {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash = (hash + c.charCodeAt(0)) % MODULUS;
}
return hash;
}
/* 乘法雜湊 */
function mulHash(key) {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash = (31 * hash + c.charCodeAt(0)) % MODULUS;
}
return hash;
}
/* 互斥或雜湊 */
function xorHash(key) {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash ^= c.charCodeAt(0);
}
return hash & MODULUS;
}
/* 旋轉雜湊 */
function rotHash(key) {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;
}
return hash;
}
```
=== "TS"
```typescript title="simple_hash.ts"
/* 加法雜湊 */
function addHash(key: string): number {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash = (hash + c.charCodeAt(0)) % MODULUS;
}
return hash;
}
/* 乘法雜湊 */
function mulHash(key: string): number {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash = (31 * hash + c.charCodeAt(0)) % MODULUS;
}
return hash;
}
/* 互斥或雜湊 */
function xorHash(key: string): number {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash ^= c.charCodeAt(0);
}
return hash & MODULUS;
}
/* 旋轉雜湊 */
function rotHash(key: string): number {
let hash = 0;
const MODULUS = 1000000007;
for (const c of key) {
hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;
}
return hash;
}
```
=== "Dart"
```dart title="simple_hash.dart"
/* 加法雜湊 */
int addHash(String key) {
int hash = 0;
final int MODULUS = 1000000007;
for (int i = 0; i < key.length; i++) {
hash = (hash + key.codeUnitAt(i)) % MODULUS;
}
return hash;
}
/* 乘法雜湊 */
int mulHash(String key) {
int hash = 0;
final int MODULUS = 1000000007;
for (int i = 0; i < key.length; i++) {
hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;
}
return hash;
}
/* 互斥或雜湊 */
int xorHash(String key) {
int hash = 0;
final int MODULUS = 1000000007;
for (int i = 0; i < key.length; i++) {
hash ^= key.codeUnitAt(i);
}
return hash & MODULUS;
}
/* 旋轉雜湊 */
int rotHash(String key) {
int hash = 0;
final int MODULUS = 1000000007;
for (int i = 0; i < key.length; i++) {
hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;
}
return hash;
}
```
=== "Rust"
```rust title="simple_hash.rs"
/* 加法雜湊 */
fn add_hash(key: &str) -> i32 {
let mut hash = 0_i64;
const MODULUS: i64 = 1000000007;
for c in key.chars() {
hash = (hash + c as i64) % MODULUS;
}
hash as i32
}
/* 乘法雜湊 */
fn mul_hash(key: &str) -> i32 {
let mut hash = 0_i64;
const MODULUS: i64 = 1000000007;
for c in key.chars() {
hash = (31 * hash + c as i64) % MODULUS;
}
hash as i32
}
/* 互斥或雜湊 */
fn xor_hash(key: &str) -> i32 {
let mut hash = 0_i64;
const MODULUS: i64 = 1000000007;
for c in key.chars() {
hash ^= c as i64;
}
(hash & MODULUS) as i32
}
/* 旋轉雜湊 */
fn rot_hash(key: &str) -> i32 {
let mut hash = 0_i64;
const MODULUS: i64 = 1000000007;
for c in key.chars() {
hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;
}
hash as i32
}
```
=== "C"
```c title="simple_hash.c"
/* 加法雜湊 */
int addHash(char *key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash = (hash + (unsigned char)key[i]) % MODULUS;
}
return (int)hash;
}
/* 乘法雜湊 */
int mulHash(char *key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash = (31 * hash + (unsigned char)key[i]) % MODULUS;
}
return (int)hash;
}
/* 互斥或雜湊 */
int xorHash(char *key) {
int hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash ^= (unsigned char)key[i];
}
return hash & MODULUS;
}
/* 旋轉雜湊 */
int rotHash(char *key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;
}
return (int)hash;
}
```
=== "Kotlin"
```kotlin title="simple_hash.kt"
/* 加法雜湊 */
fun addHash(key: String): Int {
var hash = 0L
for (c in key.toCharArray()) {
hash = (hash + c.code) % MODULUS
}
return hash.toInt()
}
/* 乘法雜湊 */
fun mulHash(key: String): Int {
var hash = 0L
for (c in key.toCharArray()) {
hash = (31 * hash + c.code) % MODULUS
}
return hash.toInt()
}
/* 互斥或雜湊 */
fun xorHash(key: String): Int {
var hash = 0
for (c in key.toCharArray()) {
hash = hash xor c.code
}
return hash and MODULUS
}
/* 旋轉雜湊 */
fun rotHash(key: String): Int {
var hash = 0L
for (c in key.toCharArray()) {
hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS
}
return hash.toInt()
}
```
=== "Ruby"
```ruby title="simple_hash.rb"
[class]{}-[func]{add_hash}
[class]{}-[func]{mul_hash}
[class]{}-[func]{xor_hash}
[class]{}-[func]{rot_hash}
```
=== "Zig"
```zig title="simple_hash.zig"
[class]{}-[func]{addHash}
[class]{}-[func]{mulHash}
[class]{}-[func]{xorHash}
[class]{}-[func]{rotHash}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20add_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%2B%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20mul_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%2031%20*%20hash%20%2B%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20xor_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%5E%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20rot_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%20%28hash%20%3C%3C%204%29%20%5E%20%28hash%20%3E%3E%2028%29%20%5E%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20key%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%0A%20%20%20%20hash%20%3D%20add_hash%28key%29%0A%20%20%20%20print%28f%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20mul_hash%28key%29%0A%20%20%20%20print%28f%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20xor_hash%28key%29%0A%20%20%20%20print%28f%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20rot_hash%28key%29%0A%20%20%20%20print%28f%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20add_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%2B%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20mul_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%2031%20*%20hash%20%2B%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20xor_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%5E%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20rot_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%20%28hash%20%3C%3C%204%29%20%5E%20%28hash%20%3E%3E%2028%29%20%5E%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20key%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%0A%20%20%20%20hash%20%3D%20add_hash%28key%29%0A%20%20%20%20print%28f%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20mul_hash%28key%29%0A%20%20%20%20print%28f%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20xor_hash%28key%29%0A%20%20%20%20print%28f%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20rot_hash%28key%29%0A%20%20%20%20print%28f%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
觀察發現,每種雜湊演算法的最後一步都是對大質數 $1000000007$ 取模,以確保雜湊值在合適的範圍內。值得思考的是,為什麼要強調對質數取模,或者說對合數取模的弊端是什麼?這是一個有趣的問題。
先丟擲結論:**使用大質數作為模數,可以最大化地保證雜湊值的均勻分佈**。因為質數不與其他數字存在公約數,可以減少因取模操作而產生的週期性模式,從而避免雜湊衝突。
舉個例子,假設我們選擇合數 $9$ 作為模數,它可以被 $3$ 整除,那麼所有可以被 $3$ 整除的 `key` 都會被對映到 $0$、$3$、$6$ 這三個雜湊值。
$$
\begin{aligned}
\text{modulus} & = 9 \newline
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
\text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \}
\end{aligned}
$$
如果輸入 `key` 恰好滿足這種等差數列的資料分佈,那麼雜湊值就會出現聚堆積,從而加重雜湊衝突。現在,假設將 `modulus` 替換為質數 $13$ ,由於 `key` 和 `modulus` 之間不存在公約數,因此輸出的雜湊值的均勻性會明顯提升。
$$
\begin{aligned}
\text{modulus} & = 13 \newline
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
\text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \}
\end{aligned}
$$
值得說明的是,如果能夠保證 `key` 是隨機均勻分佈的,那麼選擇質數或者合數作為模數都可以,它們都能輸出均勻分佈的雜湊值。而當 `key` 的分佈存在某種週期性時,對合數取模更容易出現聚集現象。
總而言之,我們通常選取質數作為模數,並且這個質數最好足夠大,以儘可能消除週期性模式,提升雜湊演算法的穩健性。
## 6.3.3 &nbsp; 常見雜湊演算法
不難發現,以上介紹的簡單雜湊演算法都比較“脆弱”,遠遠沒有達到雜湊演算法的設計目標。例如,由於加法和互斥或滿足交換律,因此加法雜湊和互斥或雜湊無法區分內容相同但順序不同的字串,這可能會加劇雜湊衝突,並引起一些安全問題。
在實際中,我們通常會用一些標準雜湊演算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它們可以將任意長度的輸入資料對映到恆定長度的雜湊值。
近一個世紀以來,雜湊演算法處在不斷升級與最佳化的過程中。一部分研究人員努力提升雜湊演算法的效能,另一部分研究人員和駭客則致力於尋找雜湊演算法的安全性問題。表 6-2 展示了在實際應用中常見的雜湊演算法。
- MD5 和 SHA-1 已多次被成功攻擊,因此它們被各類安全應用棄用。
- SHA-2 系列中的 SHA-256 是最安全的雜湊演算法之一,仍未出現成功的攻擊案例,因此常用在各類安全應用與協議中。
- SHA-3 相較 SHA-2 的實現開銷更低、計算效率更高,但目前使用覆蓋度不如 SHA-2 系列。
<p align="center"> 表 6-2 &nbsp; 常見的雜湊演算法 </p>
<div class="center-table" markdown>
| | MD5 | SHA-1 | SHA-2 | SHA-3 |
| -------- | ------------------------------ | ---------------- | ---------------------------- | ------------------- |
| 推出時間 | 1992 | 1995 | 2002 | 2008 |
| 輸出長度 | 128 bit | 160 bit | 256/512 bit | 224/256/384/512 bit |
| 雜湊衝突 | 較多 | 較多 | 很少 | 很少 |
| 安全等級 | 低,已被成功攻擊 | 低,已被成功攻擊 | 高 | 高 |
| 應用 | 已被棄用,仍用於資料完整性檢查 | 已被棄用 | 加密貨幣交易驗證、數字簽名等 | 可用於替代 SHA-2 |
</div>
## 6.3.4 &nbsp; 資料結構的雜湊值
我們知道,雜湊表的 `key` 可以是整數、小數或字串等資料型別。程式語言通常會為這些資料型別提供內建的雜湊演算法,用於計算雜湊表中的桶索引。以 Python 為例,我們可以呼叫 `hash()` 函式來計算各種資料型別的雜湊值。
- 整數和布林量的雜湊值就是其本身。
- 浮點數和字串的雜湊值計算較為複雜,有興趣的讀者請自行學習。
- 元組的雜湊值是對其中每一個元素進行雜湊,然後將這些雜湊值組合起來,得到單一的雜湊值。
- 物件的雜湊值基於其記憶體位址生成。透過重寫物件的雜湊方法,可實現基於內容生成雜湊值。
!!! tip
請注意,不同程式語言的內建雜湊值計算函式的定義和方法不同。
=== "Python"
```python title="built_in_hash.py"
num = 3
hash_num = hash(num)
# 整數 3 的雜湊值為 3
bol = True
hash_bol = hash(bol)
# 布林量 True 的雜湊值為 1
dec = 3.14159
hash_dec = hash(dec)
# 小數 3.14159 的雜湊值為 326484311674566659
str = "Hello 演算法"
hash_str = hash(str)
# 字串“Hello 演算法”的雜湊值為 4617003410720528961
tup = (12836, "小哈")
hash_tup = hash(tup)
# 元組 (12836, '小哈') 的雜湊值為 1029005403108185979
obj = ListNode(0)
hash_obj = hash(obj)
# 節點物件 <ListNode object at 0x1058fd810> 的雜湊值為 274267521
```
=== "C++"
```cpp title="built_in_hash.cpp"
int num = 3;
size_t hashNum = hash<int>()(num);
// 整數 3 的雜湊值為 3
bool bol = true;
size_t hashBol = hash<bool>()(bol);
// 布林量 1 的雜湊值為 1
double dec = 3.14159;
size_t hashDec = hash<double>()(dec);
// 小數 3.14159 的雜湊值為 4614256650576692846
string str = "Hello 演算法";
size_t hashStr = hash<string>()(str);
// 字串“Hello 演算法”的雜湊值為 15466937326284535026
// 在 C++ 中,內建 std:hash() 僅提供基本資料型別的雜湊值計算
// 陣列、物件的雜湊值計算需要自行實現
```
=== "Java"
```java title="built_in_hash.java"
int num = 3;
int hashNum = Integer.hashCode(num);
// 整數 3 的雜湊值為 3
boolean bol = true;
int hashBol = Boolean.hashCode(bol);
// 布林量 true 的雜湊值為 1231
double dec = 3.14159;
int hashDec = Double.hashCode(dec);
// 小數 3.14159 的雜湊值為 -1340954729
String str = "Hello 演算法";
int hashStr = str.hashCode();
// 字串“Hello 演算法”的雜湊值為 -727081396
Object[] arr = { 12836, "小哈" };
int hashTup = Arrays.hashCode(arr);
// 陣列 [12836, 小哈] 的雜湊值為 1151158
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode();
// 節點物件 utils.ListNode@7dc5e7b4 的雜湊值為 2110121908
```
=== "C#"
```csharp title="built_in_hash.cs"
int num = 3;
int hashNum = num.GetHashCode();
// 整數 3 的雜湊值為 3;
bool bol = true;
int hashBol = bol.GetHashCode();
// 布林量 true 的雜湊值為 1;
double dec = 3.14159;
int hashDec = dec.GetHashCode();
// 小數 3.14159 的雜湊值為 -1340954729;
string str = "Hello 演算法";
int hashStr = str.GetHashCode();
// 字串“Hello 演算法”的雜湊值為 -586107568;
object[] arr = [12836, "小哈"];
int hashTup = arr.GetHashCode();
// 陣列 [12836, 小哈] 的雜湊值為 42931033;
ListNode obj = new(0);
int hashObj = obj.GetHashCode();
// 節點物件 0 的雜湊值為 39053774;
```
=== "Go"
```go title="built_in_hash.go"
// Go 未提供內建 hash code 函式
```
=== "Swift"
```swift title="built_in_hash.swift"
let num = 3
let hashNum = num.hashValue
// 整數 3 的雜湊值為 9047044699613009734
let bol = true
let hashBol = bol.hashValue
// 布林量 true 的雜湊值為 -4431640247352757451
let dec = 3.14159
let hashDec = dec.hashValue
// 小數 3.14159 的雜湊值為 -2465384235396674631
let str = "Hello 演算法"
let hashStr = str.hashValue
// 字串“Hello 演算法”的雜湊值為 -7850626797806988787
let arr = [AnyHashable(12836), AnyHashable("小哈")]
let hashTup = arr.hashValue
// 陣列 [AnyHashable(12836), AnyHashable("小哈")] 的雜湊值為 -2308633508154532996
let obj = ListNode(x: 0)
let hashObj = obj.hashValue
// 節點物件 utils.ListNode 的雜湊值為 -2434780518035996159
```
=== "JS"
```javascript title="built_in_hash.js"
// JavaScript 未提供內建 hash code 函式
```
=== "TS"
```typescript title="built_in_hash.ts"
// TypeScript 未提供內建 hash code 函式
```
=== "Dart"
```dart title="built_in_hash.dart"
int num = 3;
int hashNum = num.hashCode;
// 整數 3 的雜湊值為 34803
bool bol = true;
int hashBol = bol.hashCode;
// 布林值 true 的雜湊值為 1231
double dec = 3.14159;
int hashDec = dec.hashCode;
// 小數 3.14159 的雜湊值為 2570631074981783
String str = "Hello 演算法";
int hashStr = str.hashCode;
// 字串“Hello 演算法”的雜湊值為 468167534
List arr = [12836, "小哈"];
int hashArr = arr.hashCode;
// 陣列 [12836, 小哈] 的雜湊值為 976512528
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode;
// 節點物件 Instance of 'ListNode' 的雜湊值為 1033450432
```
=== "Rust"
```rust title="built_in_hash.rs"
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let num = 3;
let mut num_hasher = DefaultHasher::new();
num.hash(&mut num_hasher);
let hash_num = num_hasher.finish();
// 整數 3 的雜湊值為 568126464209439262
let bol = true;
let mut bol_hasher = DefaultHasher::new();
bol.hash(&mut bol_hasher);
let hash_bol = bol_hasher.finish();
// 布林量 true 的雜湊值為 4952851536318644461
let dec: f32 = 3.14159;
let mut dec_hasher = DefaultHasher::new();
dec.to_bits().hash(&mut dec_hasher);
let hash_dec = dec_hasher.finish();
// 小數 3.14159 的雜湊值為 2566941990314602357
let str = "Hello 演算法";
let mut str_hasher = DefaultHasher::new();
str.hash(&mut str_hasher);
let hash_str = str_hasher.finish();
// 字串“Hello 演算法”的雜湊值為 16092673739211250988
let arr = (&12836, &"小哈");
let mut tup_hasher = DefaultHasher::new();
arr.hash(&mut tup_hasher);
let hash_tup = tup_hasher.finish();
// 元組 (12836, "小哈") 的雜湊值為 1885128010422702749
let node = ListNode::new(42);
let mut hasher = DefaultHasher::new();
node.borrow().val.hash(&mut hasher);
let hash = hasher.finish();
// 節點物件 RefCell { value: ListNode { val: 42, next: None } } 的雜湊值為15387811073369036852
```
=== "C"
```c title="built_in_hash.c"
// C 未提供內建 hash code 函式
```
=== "Kotlin"
```kotlin title="built_in_hash.kt"
val num = 3
val hashNum = num.hashCode()
// 整數 3 的雜湊值為 3
val bol = true
val hashBol = bol.hashCode()
// 布林量 true 的雜湊值為 1231
val dec = 3.14159
val hashDec = dec.hashCode()
// 小數 3.14159 的雜湊值為 -1340954729
val str = "Hello 演算法"
val hashStr = str.hashCode()
// 字串“Hello 演算法”的雜湊值為 -727081396
val arr = arrayOf<Any>(12836, "小哈")
val hashTup = arr.hashCode()
// 陣列 [12836, 小哈] 的雜湊值為 189568618
val obj = ListNode(0)
val hashObj = obj.hashCode()
// 節點物件 utils.ListNode@1d81eb93 的雜湊值為 495053715
```
=== "Ruby"
```ruby title="built_in_hash.rb"
```
=== "Zig"
```zig title="built_in_hash.zig"
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=19&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=19&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
在許多程式語言中,**只有不可變物件才可作為雜湊表的 `key`** 。假如我們將串列(動態陣列)作為 `key` ,當串列的內容發生變化時,它的雜湊值也隨之改變,我們就無法在雜湊表中查詢到原先的 `value` 了。
雖然自定義物件(比如鏈結串列節點)的成員變數是可變的,但它是可雜湊的。**這是因為物件的雜湊值通常是基於記憶體位址生成的**,即使物件的內容發生了變化,但它的記憶體位址不變,雜湊值仍然是不變的。
細心的你可能發現在不同控制檯中執行程式時,輸出的雜湊值是不同的。**這是因為 Python 直譯器在每次啟動時,都會為字串雜湊函式加入一個隨機的鹽(salt)值**。這種做法可以有效防止 HashDoS 攻擊,提升雜湊演算法的安全性。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
---
comments: true
icon: material/table-search
---
# 第 6 章 &nbsp; 雜湊表
![雜湊表](../assets/covers/chapter_hashing.jpg){ class="cover-image" }
!!! abstract
在計算機世界中,雜湊表如同一位聰慧的圖書管理員。
他知道如何計算索書號,從而可以快速找到目標圖書。
## Chapter Contents
- [6.1 &nbsp; 雜湊表](https://www.hello-algo.com/en/chapter_hashing/hash_map/)
- [6.2 &nbsp; 雜湊衝突](https://www.hello-algo.com/en/chapter_hashing/hash_collision/)
- [6.3 &nbsp; 雜湊演算法](https://www.hello-algo.com/en/chapter_hashing/hash_algorithm/)
- [6.4 &nbsp; 小結](https://www.hello-algo.com/en/chapter_hashing/summary/)
+51
View File
@@ -0,0 +1,51 @@
---
comments: true
---
# 6.4 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 輸入 `key` ,雜湊表能夠在 $O(1)$ 時間內查詢到 `value` ,效率非常高。
- 常見的雜湊表操作包括查詢、新增鍵值對、刪除鍵值對和走訪雜湊表等。
- 雜湊函式將 `key` 對映為陣列索引,從而訪問對應桶並獲取 `value`
- 兩個不同的 `key` 可能在經過雜湊函式後得到相同的陣列索引,導致查詢結果出錯,這種現象被稱為雜湊衝突。
- 雜湊表容量越大,雜湊衝突的機率就越低。因此可以透過擴容雜湊表來緩解雜湊衝突。與陣列擴容類似,雜湊表擴容操作的開銷很大。
- 負載因子定義為雜湊表中元素數量除以桶數量,反映了雜湊衝突的嚴重程度,常用作觸發雜湊表擴容的條件。
- 鏈式位址透過將單個元素轉化為鏈結串列,將所有衝突元素儲存在同一個鏈結串列中。然而,鏈結串列過長會降低查詢效率,可以透過進一步將鏈結串列轉換為紅黑樹來提高效率。
- 開放定址透過多次探測來處理雜湊衝突。線性探查使用固定步長,缺點是不能刪除元素,且容易產生聚集。多次雜湊使用多個雜湊函式進行探測,相較線性探查更不易產生聚集,但多個雜湊函式增加了計算量。
- 不同程式語言採取了不同的雜湊表實現。例如,Java 的 `HashMap` 使用鏈式位址,而 Python 的 `Dict` 採用開放定址。
- 在雜湊表中,我們希望雜湊演算法具有確定性、高效率和均勻分佈的特點。在密碼學中,雜湊演算法還應該具備抗碰撞性和雪崩效應。
- 雜湊演算法通常採用大質數作為模數,以最大化地保證雜湊值均勻分佈,減少雜湊衝突。
- 常見的雜湊演算法包括 MD5、SHA-1、SHA-2 和 SHA-3 等。MD5 常用於校驗檔案完整性,SHA-2 常用於安全應用與協議。
- 程式語言通常會為資料型別提供內建雜湊演算法,用於計算雜湊表中的桶索引。通常情況下,只有不可變物件是可雜湊的。
### 2. &nbsp; Q & A
**Q**:雜湊表的時間複雜度在什麼情況下是 $O(n)$ ?
當雜湊衝突比較嚴重時,雜湊表的時間複雜度會退化至 $O(n)$ 。當雜湊函式設計得比較好、容量設定比較合理、衝突比較平均時,時間複雜度是 $O(1)$ 。我們使用程式語言內建的雜湊表時,通常認為時間複雜度是 $O(1)$ 。
**Q**:為什麼不使用雜湊函式 $f(x) = x$ 呢?這樣就不會有衝突了。
在 $f(x) = x$ 雜湊函式下,每個元素對應唯一的桶索引,這與陣列等價。然而,輸入空間通常遠大於輸出空間(陣列長度),因此雜湊函式的最後一步往往是對陣列長度取模。換句話說,雜湊表的目標是將一個較大的狀態空間對映到一個較小的空間,並提供 $O(1)$ 的查詢效率。
**Q**:雜湊表底層實現是陣列、鏈結串列、二元樹,但為什麼效率可以比它們更高呢?
首先,雜湊表的時間效率變高,但空間效率變低了。雜湊表有相當一部分記憶體未使用。
其次,只是在特定使用場景下時間效率變高了。如果一個功能能夠在相同的時間複雜度下使用陣列或鏈結串列實現,那麼通常比雜湊表更快。這是因為雜湊函式計算需要開銷,時間複雜度的常數項更大。
最後,雜湊表的時間複雜度可能發生劣化。例如在鏈式位址中,我們採取在鏈結串列或紅黑樹中執行查詢操作,仍然有退化至 $O(n)$ 時間的風險。
**Q**:多次雜湊有不能直接刪除元素的缺陷嗎?標記為已刪除的空間還能再次使用嗎?
多次雜湊是開放定址的一種,開放定址法都有不能直接刪除元素的缺陷,需要透過標記刪除。標記為已刪除的空間可以再次使用。當將新元素插入雜湊表,並且透過雜湊函式找到標記為已刪除的位置時,該位置可以被新元素使用。這樣做既能保持雜湊表的探測序列不變,又能保證雜湊表的空間使用率。
**Q**:為什麼線上性探查中,查詢元素的時候會出現雜湊衝突呢?
查詢的時候透過雜湊函式找到對應的桶和鍵值對,發現 `key` 不匹配,這就代表有雜湊衝突。因此,線性探查法會根據預先設定的步長依次向下查詢,直至找到正確的鍵值對或無法找到跳出為止。
**Q**:為什麼雜湊表擴容能夠緩解雜湊衝突?
雜湊函式的最後一步往往是對陣列長度 $n$ 取模(取餘),讓輸出值落在陣列索引範圍內;在擴容後,陣列長度 $n$ 發生變化,而 `key` 對應的索引也可能發生變化。原先落在同一個桶的多個 `key` ,在擴容後可能會被分配到多個桶中,從而實現雜湊衝突的緩解。
+382
View File
@@ -0,0 +1,382 @@
---
comments: true
---
# 8.2 &nbsp; 建堆積操作
在某些情況下,我們希望使用一個串列的所有元素來構建一個堆積,這個過程被稱為“建堆積操作”。
## 8.2.1 &nbsp; 藉助入堆積操作實現
我們首先建立一個空堆積,然後走訪串列,依次對每個元素執行“入堆積操作”,即先將元素新增至堆積的尾部,再對該元素執行“從底至頂”堆積化。
每當一個元素入堆積,堆積的長度就加一。由於節點是從頂到底依次被新增進二元樹的,因此堆積是“自上而下”構建的。
設元素數量為 $n$ ,每個元素的入堆積操作使用 $O(\log{n})$ 時間,因此該建堆積方法的時間複雜度為 $O(n \log n)$ 。
## 8.2.2 &nbsp; 透過走訪堆積化實現
實際上,我們可以實現一種更為高效的建堆積方法,共分為兩步。
1. 將串列所有元素原封不動地新增到堆積中,此時堆積的性質尚未得到滿足。
2. 倒序走訪堆積(層序走訪的倒序),依次對每個非葉節點執行“從頂至底堆積化”。
**每當堆積化一個節點後,以該節點為根節點的子樹就形成一個合法的子堆積**。而由於是倒序走訪,因此堆積是“自下而上”構建的。
之所以選擇倒序走訪,是因為這樣能夠保證當前節點之下的子樹已經是合法的子堆積,這樣堆積化當前節點才是有效的。
值得說明的是,**由於葉節點沒有子節點,因此它們天然就是合法的子堆積,無須堆積化**。如以下程式碼所示,最後一個非葉節點是最後一個節點的父節點,我們從它開始倒序走訪並執行堆積化:
=== "Python"
```python title="my_heap.py"
def __init__(self, nums: list[int]):
"""建構子,根據輸入串列建堆積"""
# 將串列元素原封不動新增進堆積
self.max_heap = nums
# 堆積化除葉節點以外的其他所有節點
for i in range(self.parent(self.size() - 1), -1, -1):
self.sift_down(i)
```
=== "C++"
```cpp title="my_heap.cpp"
/* 建構子,根據輸入串列建堆積 */
MaxHeap(vector<int> nums) {
// 將串列元素原封不動新增進堆積
maxHeap = nums;
// 堆積化除葉節點以外的其他所有節點
for (int i = parent(size() - 1); i >= 0; i--) {
siftDown(i);
}
}
```
=== "Java"
```java title="my_heap.java"
/* 建構子,根據輸入串列建堆積 */
MaxHeap(List<Integer> nums) {
// 將串列元素原封不動新增進堆積
maxHeap = new ArrayList<>(nums);
// 堆積化除葉節點以外的其他所有節點
for (int i = parent(size() - 1); i >= 0; i--) {
siftDown(i);
}
}
```
=== "C#"
```csharp title="my_heap.cs"
/* 建構子,根據輸入串列建堆積 */
MaxHeap(IEnumerable<int> nums) {
// 將串列元素原封不動新增進堆積
maxHeap = new List<int>(nums);
// 堆積化除葉節點以外的其他所有節點
var size = Parent(this.Size() - 1);
for (int i = size; i >= 0; i--) {
SiftDown(i);
}
}
```
=== "Go"
```go title="my_heap.go"
/* 建構子,根據切片建堆積 */
func newMaxHeap(nums []any) *maxHeap {
// 將串列元素原封不動新增進堆積
h := &maxHeap{data: nums}
for i := h.parent(len(h.data) - 1); i >= 0; i-- {
// 堆積化除葉節點以外的其他所有節點
h.siftDown(i)
}
return h
}
```
=== "Swift"
```swift title="my_heap.swift"
/* 建構子,根據輸入串列建堆積 */
init(nums: [Int]) {
// 將串列元素原封不動新增進堆積
maxHeap = nums
// 堆積化除葉節點以外的其他所有節點
for i in (0 ... parent(i: size() - 1)).reversed() {
siftDown(i: i)
}
}
```
=== "JS"
```javascript title="my_heap.js"
/* 建構子,建立空堆積或根據輸入串列建堆積 */
constructor(nums) {
// 將串列元素原封不動新增進堆積
this.#maxHeap = nums === undefined ? [] : [...nums];
// 堆積化除葉節點以外的其他所有節點
for (let i = this.#parent(this.size() - 1); i >= 0; i--) {
this.#siftDown(i);
}
}
```
=== "TS"
```typescript title="my_heap.ts"
/* 建構子,建立空堆積或根據輸入串列建堆積 */
constructor(nums?: number[]) {
// 將串列元素原封不動新增進堆積
this.maxHeap = nums === undefined ? [] : [...nums];
// 堆積化除葉節點以外的其他所有節點
for (let i = this.parent(this.size() - 1); i >= 0; i--) {
this.siftDown(i);
}
}
```
=== "Dart"
```dart title="my_heap.dart"
/* 建構子,根據輸入串列建堆積 */
MaxHeap(List<int> nums) {
// 將串列元素原封不動新增進堆積
_maxHeap = nums;
// 堆積化除葉節點以外的其他所有節點
for (int i = _parent(size() - 1); i >= 0; i--) {
siftDown(i);
}
}
```
=== "Rust"
```rust title="my_heap.rs"
/* 建構子,根據輸入串列建堆積 */
fn new(nums: Vec<i32>) -> Self {
// 將串列元素原封不動新增進堆積
let mut heap = MaxHeap { max_heap: nums };
// 堆積化除葉節點以外的其他所有節點
for i in (0..=Self::parent(heap.size() - 1)).rev() {
heap.sift_down(i);
}
heap
}
```
=== "C"
```c title="my_heap.c"
/* 建構子,根據切片建堆積 */
MaxHeap *newMaxHeap(int nums[], int size) {
// 所有元素入堆積
MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));
maxHeap->size = size;
memcpy(maxHeap->data, nums, size * sizeof(int));
for (int i = parent(maxHeap, size - 1); i >= 0; i--) {
// 堆積化除葉節點以外的其他所有節點
siftDown(maxHeap, i);
}
return maxHeap;
}
```
=== "Kotlin"
```kotlin title="my_heap.kt"
/* 大頂堆積 */
class MaxHeap(nums: List<Int>?) {
// 使用串列而非陣列,這樣無須考慮擴容問題
// 將串列元素原封不動新增進堆積
private val maxHeap = ArrayList(nums!!)
/* 建構子,根據輸入串列建堆積 */
init {
// 堆積化除葉節點以外的其他所有節點
for (i in parent(size() - 1) downTo 0) {
siftDown(i)
}
}
/* 獲取左子節點的索引 */
private fun left(i: Int): Int {
return 2 * i + 1
}
/* 獲取右子節點的索引 */
private fun right(i: Int): Int {
return 2 * i + 2
}
/* 獲取父節點的索引 */
private fun parent(i: Int): Int {
return (i - 1) / 2 // 向下整除
}
/* 交換元素 */
private fun swap(i: Int, j: Int) {
maxHeap[i] = maxHeap[j].also { maxHeap[j] = maxHeap[i] }
}
/* 獲取堆積大小 */
fun size(): Int {
return maxHeap.size
}
/* 判斷堆積是否為空 */
fun isEmpty(): Boolean {
/* 判斷堆積是否為空 */
return size() == 0
}
/* 訪問堆積頂元素 */
fun peek(): Int {
return maxHeap[0]
}
/* 元素入堆積 */
fun push(value: Int) {
// 新增節點
maxHeap.add(value)
// 從底至頂堆積化
siftUp(size() - 1)
}
/* 從節點 i 開始,從底至頂堆積化 */
private fun siftUp(it: Int) {
// Kotlin的函式參數不可變,因此建立臨時變數
var i = it
while (true) {
// 獲取節點 i 的父節點
val p = parent(i)
// 當“越過根節點”或“節點無須修復”時,結束堆積化
if (p < 0 || maxHeap[i] <= maxHeap[p]) break
// 交換兩節點
swap(i, p)
// 迴圈向上堆積化
i = p
}
}
/* 元素出堆積 */
fun pop(): Int {
// 判空處理
if (isEmpty()) throw IndexOutOfBoundsException()
// 交換根節點與最右葉節點(交換首元素與尾元素)
swap(0, size() - 1)
// 刪除節點
val value = maxHeap.removeAt(size() - 1)
// 從頂至底堆積化
siftDown(0)
// 返回堆積頂元素
return value
}
/* 從節點 i 開始,從頂至底堆積化 */
private fun siftDown(it: Int) {
// Kotlin的函式參數不可變,因此建立臨時變數
var i = it
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
val l = left(i)
val r = right(i)
var ma = i
if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l
if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma == i) break
// 交換兩節點
swap(i, ma)
// 迴圈向下堆積化
i = ma
}
}
/* 列印堆積(二元樹) */
fun print() {
val queue = PriorityQueue { a: Int, b: Int -> b - a }
queue.addAll(maxHeap)
printHeap(queue)
}
}
```
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{__init__}
```
=== "Zig"
```zig title="my_heap.zig"
// 建構子,根據輸入串列建堆積
fn init(self: *Self, allocator: std.mem.Allocator, nums: []const T) !void {
if (self.max_heap != null) return;
self.max_heap = std.ArrayList(T).init(allocator);
// 將串列元素原封不動新增進堆積
try self.max_heap.?.appendSlice(nums);
// 堆積化除葉節點以外的其他所有節點
var i: usize = parent(self.size() - 1) + 1;
while (i > 0) : (i -= 1) {
try self.siftDown(i - 1);
}
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20MaxHeap%3A%0A%20%20%20%20%22%22%22%E5%A4%A7%E9%A1%B6%E5%A0%86%22%22%22%0A%0A%20%20%20%20def%20__init__%28self,%20nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%EF%BC%8C%E6%A0%B9%E6%8D%AE%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%BB%BA%E5%A0%86%22%22%22%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E5%8E%9F%E5%B0%81%E4%B8%8D%E5%8A%A8%E6%B7%BB%E5%8A%A0%E8%BF%9B%E5%A0%86%0A%20%20%20%20%20%20%20%20self.max_heap%20%3D%20nums%0A%20%20%20%20%20%20%20%20%23%20%E5%A0%86%E5%8C%96%E9%99%A4%E5%8F%B6%E8%8A%82%E7%82%B9%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20for%20i%20in%20range%28self.parent%28self.size%28%29%20-%201%29,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.sift_down%28i%29%0A%0A%20%20%20%20def%20left%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%201%0A%0A%20%20%20%20def%20right%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%202%0A%0A%20%20%20%20def%20parent%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E7%88%B6%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%20%28i%20-%201%29%20//%202%20%20%23%20%E5%90%91%E4%B8%8B%E6%95%B4%E9%99%A4%0A%0A%20%20%20%20def%20swap%28self,%20i%3A%20int,%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BA%A4%E6%8D%A2%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20self.max_heap%5Bi%5D,%20self.max_heap%5Bj%5D%20%3D%20self.max_heap%5Bj%5D,%20self.max_heap%5Bi%5D%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%22%22%22%0A%20%20%20%20%20%20%20%20return%20len%28self.max_heap%29%0A%0A%20%20%20%20def%20sift_down%28self,%20i%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BB%8E%E8%8A%82%E7%82%B9%20i%20%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E5%A0%86%E5%8C%96%22%22%22%0A%20%20%20%20%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E8%8A%82%E7%82%B9%20i,%20l,%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E8%8A%82%E7%82%B9%EF%BC%8C%E8%AE%B0%E4%B8%BA%20ma%0A%20%20%20%20%20%20%20%20%20%20%20%20l,%20r,%20ma%20%3D%20self.left%28i%29,%20self.right%28i%29,%20i%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20l%20%3C%20self.size%28%29%20and%20self.max_heap%5Bl%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20r%20%3C%20self.size%28%29%20and%20self.max_heap%5Br%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E8%8A%82%E7%82%B9%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l,%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%88%99%E6%97%A0%E9%A1%BB%E7%BB%A7%E7%BB%AD%E5%A0%86%E5%8C%96%EF%BC%8C%E8%B7%B3%E5%87%BA%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E4%B8%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20%20%20%20%20self.swap%28i,%20ma%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E5%90%91%E4%B8%8B%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap%20%3D%20MaxHeap%28%5B1,%202,%203,%204,%205%5D%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20MaxHeap%3A%0A%20%20%20%20%22%22%22%E5%A4%A7%E9%A1%B6%E5%A0%86%22%22%22%0A%0A%20%20%20%20def%20__init__%28self,%20nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%EF%BC%8C%E6%A0%B9%E6%8D%AE%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%BB%BA%E5%A0%86%22%22%22%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E5%8E%9F%E5%B0%81%E4%B8%8D%E5%8A%A8%E6%B7%BB%E5%8A%A0%E8%BF%9B%E5%A0%86%0A%20%20%20%20%20%20%20%20self.max_heap%20%3D%20nums%0A%20%20%20%20%20%20%20%20%23%20%E5%A0%86%E5%8C%96%E9%99%A4%E5%8F%B6%E8%8A%82%E7%82%B9%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20for%20i%20in%20range%28self.parent%28self.size%28%29%20-%201%29,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.sift_down%28i%29%0A%0A%20%20%20%20def%20left%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%201%0A%0A%20%20%20%20def%20right%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%202%0A%0A%20%20%20%20def%20parent%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E7%88%B6%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%20%28i%20-%201%29%20//%202%20%20%23%20%E5%90%91%E4%B8%8B%E6%95%B4%E9%99%A4%0A%0A%20%20%20%20def%20swap%28self,%20i%3A%20int,%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BA%A4%E6%8D%A2%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20self.max_heap%5Bi%5D,%20self.max_heap%5Bj%5D%20%3D%20self.max_heap%5Bj%5D,%20self.max_heap%5Bi%5D%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%22%22%22%0A%20%20%20%20%20%20%20%20return%20len%28self.max_heap%29%0A%0A%20%20%20%20def%20sift_down%28self,%20i%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BB%8E%E8%8A%82%E7%82%B9%20i%20%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E5%A0%86%E5%8C%96%22%22%22%0A%20%20%20%20%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E8%8A%82%E7%82%B9%20i,%20l,%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E8%8A%82%E7%82%B9%EF%BC%8C%E8%AE%B0%E4%B8%BA%20ma%0A%20%20%20%20%20%20%20%20%20%20%20%20l,%20r,%20ma%20%3D%20self.left%28i%29,%20self.right%28i%29,%20i%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20l%20%3C%20self.size%28%29%20and%20self.max_heap%5Bl%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20r%20%3C%20self.size%28%29%20and%20self.max_heap%5Br%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E8%8A%82%E7%82%B9%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l,%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%88%99%E6%97%A0%E9%A1%BB%E7%BB%A7%E7%BB%AD%E5%A0%86%E5%8C%96%EF%BC%8C%E8%B7%B3%E5%87%BA%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E4%B8%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20%20%20%20%20self.swap%28i,%20ma%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E5%90%91%E4%B8%8B%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap%20%3D%20MaxHeap%28%5B1,%202,%203,%204,%205%5D%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 8.2.3 &nbsp; 複雜度分析
下面,我們來嘗試推算第二種建堆積方法的時間複雜度。
- 假設完全二元樹的節點數量為 $n$ ,則葉節點數量為 $(n + 1) / 2$ ,其中 $/$ 為向下整除。因此需要堆積化的節點數量為 $(n - 1) / 2$ 。
- 在從頂至底堆積化的過程中,每個節點最多堆積化到葉節點,因此最大迭代次數為二元樹高度 $\log n$ 。
將上述兩者相乘,可得到建堆積過程的時間複雜度為 $O(n \log n)$ 。**但這個估算結果並不準確,因為我們沒有考慮到二元樹底層節點數量遠多於頂層節點的性質**。
接下來我們來進行更為準確的計算。為了降低計算難度,假設給定一個節點數量為 $n$ 、高度為 $h$ 的“完美二元樹”,該假設不會影響計算結果的正確性。
![完美二元樹的各層節點數量](build_heap.assets/heapify_operations_count.png){ class="animation-figure" }
<p align="center"> 圖 8-5 &nbsp; 完美二元樹的各層節點數量 </p>
如圖 8-5 所示,節點“從頂至底堆積化”的最大迭代次數等於該節點到葉節點的距離,而該距離正是“節點高度”。因此,我們可以對各層的“節點數量 $\times$ 節點高度”求和,**得到所有節點的堆積化迭代次數的總和**。
$$
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
$$
化簡上式需要藉助中學的數列知識,先將 $T(h)$ 乘以 $2$ ,得到:
$$
\begin{aligned}
T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline
\end{aligned}
$$
使用錯位相減法,用下式 $2 T(h)$ 減去上式 $T(h)$ ,可得:
$$
2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h
$$
觀察上式,發現 $T(h)$ 是一個等比數列,可直接使用求和公式,得到時間複雜度為:
$$
\begin{aligned}
T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
& = 2^{h+1} - h - 2 \newline
& = O(2^h)
\end{aligned}
$$
進一步,高度為 $h$ 的完美二元樹的節點數量為 $n = 2^{h+1} - 1$ ,易得複雜度為 $O(2^h) = O(n)$ 。以上推算表明,**輸入串列並建堆積的時間複雜度為 $O(n)$ ,非常高效**。
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
---
comments: true
icon: material/family-tree
---
# 第 8 章 &nbsp; 堆積
![堆積](../assets/covers/chapter_heap.jpg){ class="cover-image" }
!!! abstract
堆積就像是山嶽峰巒,層疊起伏、形態各異。
座座山峰高低錯落,而最高的山峰總是最先映入眼簾。
## Chapter Contents
- [8.1 &nbsp; 堆積](https://www.hello-algo.com/en/chapter_heap/heap/)
- [8.2 &nbsp; 建堆積操作](https://www.hello-algo.com/en/chapter_heap/build_heap/)
- [8.3 &nbsp; Top-k 問題](https://www.hello-algo.com/en/chapter_heap/top_k/)
- [8.4 &nbsp; 小結](https://www.hello-algo.com/en/chapter_heap/summary/)
+21
View File
@@ -0,0 +1,21 @@
---
comments: true
---
# 8.4 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 堆積是一棵完全二元樹,根據成立條件可分為大頂堆積和小頂堆積。大(小)頂堆積的堆積頂元素是最大(小)的。
- 優先佇列的定義是具有出列優先順序的佇列,通常使用堆積來實現。
- 堆積的常用操作及其對應的時間複雜度包括:元素入堆積 $O(\log n)$、堆積頂元素出堆積 $O(\log n)$ 和訪問堆積頂元素 $O(1)$ 等。
- 完全二元樹非常適合用陣列表示,因此我們通常使用陣列來儲存堆積。
- 堆積化操作用於維護堆積的性質,在入堆積和出堆積操作中都會用到。
- 輸入 $n$ 個元素並建堆積的時間複雜度可以最佳化至 $O(n)$ ,非常高效。
- Top-k 是一個經典演算法問題,可以使用堆積資料結構高效解決,時間複雜度為 $O(n \log k)$ 。
### 2. &nbsp; Q & A
**Q**:資料結構的“堆積”與記憶體管理的“堆積”是同一個概念嗎?
兩者不是同一個概念,只是碰巧都叫“堆積”。計算機系統記憶體中的堆積是動態記憶體分配的一部分,程式在執行時可以使用它來儲存資料。程式可以請求一定量的堆積記憶體,用於儲存如物件和陣列等複雜結構。當這些資料不再需要時,程式需要釋放這些記憶體,以防止記憶體流失。相較於堆疊記憶體,堆積記憶體的管理和使用需要更謹慎,使用不當可能會導致記憶體流失和野指標等問題。
+456
View File
@@ -0,0 +1,456 @@
---
comments: true
---
# 8.3 &nbsp; Top-k 問題
!!! question
給定一個長度為 $n$ 的無序陣列 `nums` ,請返回陣列中最大的 $k$ 個元素。
對於該問題,我們先介紹兩種思路比較直接的解法,再介紹效率更高的堆積解法。
## 8.3.1 &nbsp; 方法一:走訪選擇
我們可以進行圖 8-6 所示的 $k$ 輪走訪,分別在每輪中提取第 $1$、$2$、$\dots$、$k$ 大的元素,時間複雜度為 $O(nk)$ 。
此方法只適用於 $k \ll n$ 的情況,因為當 $k$ 與 $n$ 比較接近時,其時間複雜度趨向於 $O(n^2)$ ,非常耗時。
![走訪尋找最大的 k 個元素](top_k.assets/top_k_traversal.png){ class="animation-figure" }
<p align="center"> 圖 8-6 &nbsp; 走訪尋找最大的 k 個元素 </p>
!!! tip
當 $k = n$ 時,我們可以得到完整的有序序列,此時等價於“選擇排序”演算法。
## 8.3.2 &nbsp; 方法二:排序
如圖 8-7 所示,我們可以先對陣列 `nums` 進行排序,再返回最右邊的 $k$ 個元素,時間複雜度為 $O(n \log n)$ 。
顯然,該方法“超額”完成任務了,因為我們只需找出最大的 $k$ 個元素即可,而不需要排序其他元素。
![排序尋找最大的 k 個元素](top_k.assets/top_k_sorting.png){ class="animation-figure" }
<p align="center"> 圖 8-7 &nbsp; 排序尋找最大的 k 個元素 </p>
## 8.3.3 &nbsp; 方法三:堆積
我們可以基於堆積更加高效地解決 Top-k 問題,流程如圖 8-8 所示。
1. 初始化一個小頂堆積,其堆積頂元素最小。
2. 先將陣列的前 $k$ 個元素依次入堆積。
3. 從第 $k + 1$ 個元素開始,若當前元素大於堆積頂元素,則將堆積頂元素出堆積,並將當前元素入堆積。
4. 走訪完成後,堆積中儲存的就是最大的 $k$ 個元素。
=== "<1>"
![基於堆積尋找最大的 k 個元素](top_k.assets/top_k_heap_step1.png){ class="animation-figure" }
=== "<2>"
![top_k_heap_step2](top_k.assets/top_k_heap_step2.png){ class="animation-figure" }
=== "<3>"
![top_k_heap_step3](top_k.assets/top_k_heap_step3.png){ class="animation-figure" }
=== "<4>"
![top_k_heap_step4](top_k.assets/top_k_heap_step4.png){ class="animation-figure" }
=== "<5>"
![top_k_heap_step5](top_k.assets/top_k_heap_step5.png){ class="animation-figure" }
=== "<6>"
![top_k_heap_step6](top_k.assets/top_k_heap_step6.png){ class="animation-figure" }
=== "<7>"
![top_k_heap_step7](top_k.assets/top_k_heap_step7.png){ class="animation-figure" }
=== "<8>"
![top_k_heap_step8](top_k.assets/top_k_heap_step8.png){ class="animation-figure" }
=== "<9>"
![top_k_heap_step9](top_k.assets/top_k_heap_step9.png){ class="animation-figure" }
<p align="center"> 圖 8-8 &nbsp; 基於堆積尋找最大的 k 個元素 </p>
示例程式碼如下:
=== "Python"
```python title="top_k.py"
def top_k_heap(nums: list[int], k: int) -> list[int]:
"""基於堆積查詢陣列中最大的 k 個元素"""
# 初始化小頂堆積
heap = []
# 將陣列的前 k 個元素入堆積
for i in range(k):
heapq.heappush(heap, nums[i])
# 從第 k+1 個元素開始,保持堆積的長度為 k
for i in range(k, len(nums)):
# 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if nums[i] > heap[0]:
heapq.heappop(heap)
heapq.heappush(heap, nums[i])
return heap
```
=== "C++"
```cpp title="top_k.cpp"
/* 基於堆積查詢陣列中最大的 k 個元素 */
priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {
// 初始化小頂堆積
priority_queue<int, vector<int>, greater<int>> heap;
// 將陣列的前 k 個元素入堆積
for (int i = 0; i < k; i++) {
heap.push(nums[i]);
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (int i = k; i < nums.size(); i++) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > heap.top()) {
heap.pop();
heap.push(nums[i]);
}
}
return heap;
}
```
=== "Java"
```java title="top_k.java"
/* 基於堆積查詢陣列中最大的 k 個元素 */
Queue<Integer> topKHeap(int[] nums, int k) {
// 初始化小頂堆積
Queue<Integer> heap = new PriorityQueue<Integer>();
// 將陣列的前 k 個元素入堆積
for (int i = 0; i < k; i++) {
heap.offer(nums[i]);
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (int i = k; i < nums.length; i++) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > heap.peek()) {
heap.poll();
heap.offer(nums[i]);
}
}
return heap;
}
```
=== "C#"
```csharp title="top_k.cs"
/* 基於堆積查詢陣列中最大的 k 個元素 */
PriorityQueue<int, int> TopKHeap(int[] nums, int k) {
// 初始化小頂堆積
PriorityQueue<int, int> heap = new();
// 將陣列的前 k 個元素入堆積
for (int i = 0; i < k; i++) {
heap.Enqueue(nums[i], nums[i]);
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (int i = k; i < nums.Length; i++) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > heap.Peek()) {
heap.Dequeue();
heap.Enqueue(nums[i], nums[i]);
}
}
return heap;
}
```
=== "Go"
```go title="top_k.go"
/* 基於堆積查詢陣列中最大的 k 個元素 */
func topKHeap(nums []int, k int) *minHeap {
// 初始化小頂堆積
h := &minHeap{}
heap.Init(h)
// 將陣列的前 k 個元素入堆積
for i := 0; i < k; i++ {
heap.Push(h, nums[i])
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for i := k; i < len(nums); i++ {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if nums[i] > h.Top().(int) {
heap.Pop(h)
heap.Push(h, nums[i])
}
}
return h
}
```
=== "Swift"
```swift title="top_k.swift"
/* 基於堆積查詢陣列中最大的 k 個元素 */
func topKHeap(nums: [Int], k: Int) -> [Int] {
// 初始化一個小頂堆積,並將前 k 個元素建堆積
var heap = Heap(nums.prefix(k))
// 從第 k+1 個元素開始,保持堆積的長度為 k
for i in nums.indices.dropFirst(k) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if nums[i] > heap.min()! {
_ = heap.removeMin()
heap.insert(nums[i])
}
}
return heap.unordered
}
```
=== "JS"
```javascript title="top_k.js"
/* 元素入堆積 */
function pushMinHeap(maxHeap, val) {
// 元素取反
maxHeap.push(-val);
}
/* 元素出堆積 */
function popMinHeap(maxHeap) {
// 元素取反
return -maxHeap.pop();
}
/* 訪問堆積頂元素 */
function peekMinHeap(maxHeap) {
// 元素取反
return -maxHeap.peek();
}
/* 取出堆積中元素 */
function getMinHeap(maxHeap) {
// 元素取反
return maxHeap.getMaxHeap().map((num) => -num);
}
/* 基於堆積查詢陣列中最大的 k 個元素 */
function topKHeap(nums, k) {
// 初始化小頂堆積
// 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積
const maxHeap = new MaxHeap([]);
// 將陣列的前 k 個元素入堆積
for (let i = 0; i < k; i++) {
pushMinHeap(maxHeap, nums[i]);
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (let i = k; i < nums.length; i++) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > peekMinHeap(maxHeap)) {
popMinHeap(maxHeap);
pushMinHeap(maxHeap, nums[i]);
}
}
// 返回堆積中元素
return getMinHeap(maxHeap);
}
```
=== "TS"
```typescript title="top_k.ts"
/* 元素入堆積 */
function pushMinHeap(maxHeap: MaxHeap, val: number): void {
// 元素取反
maxHeap.push(-val);
}
/* 元素出堆積 */
function popMinHeap(maxHeap: MaxHeap): number {
// 元素取反
return -maxHeap.pop();
}
/* 訪問堆積頂元素 */
function peekMinHeap(maxHeap: MaxHeap): number {
// 元素取反
return -maxHeap.peek();
}
/* 取出堆積中元素 */
function getMinHeap(maxHeap: MaxHeap): number[] {
// 元素取反
return maxHeap.getMaxHeap().map((num: number) => -num);
}
/* 基於堆積查詢陣列中最大的 k 個元素 */
function topKHeap(nums: number[], k: number): number[] {
// 初始化小頂堆積
// 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積
const maxHeap = new MaxHeap([]);
// 將陣列的前 k 個元素入堆積
for (let i = 0; i < k; i++) {
pushMinHeap(maxHeap, nums[i]);
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (let i = k; i < nums.length; i++) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > peekMinHeap(maxHeap)) {
popMinHeap(maxHeap);
pushMinHeap(maxHeap, nums[i]);
}
}
// 返回堆積中元素
return getMinHeap(maxHeap);
}
```
=== "Dart"
```dart title="top_k.dart"
/* 基於堆積查詢陣列中最大的 k 個元素 */
MinHeap topKHeap(List<int> nums, int k) {
// 初始化小頂堆積,將陣列的前 k 個元素入堆積
MinHeap heap = MinHeap(nums.sublist(0, k));
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (int i = k; i < nums.length; i++) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > heap.peek()) {
heap.pop();
heap.push(nums[i]);
}
}
return heap;
}
```
=== "Rust"
```rust title="top_k.rs"
/* 基於堆積查詢陣列中最大的 k 個元素 */
fn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {
// BinaryHeap 是大頂堆積,使用 Reverse 將元素取反,從而實現小頂堆積
let mut heap = BinaryHeap::<Reverse<i32>>::new();
// 將陣列的前 k 個元素入堆積
for &num in nums.iter().take(k) {
heap.push(Reverse(num));
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for &num in nums.iter().skip(k) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if num > heap.peek().unwrap().0 {
heap.pop();
heap.push(Reverse(num));
}
}
heap
}
```
=== "C"
```c title="top_k.c"
/* 元素入堆積 */
void pushMinHeap(MaxHeap *maxHeap, int val) {
// 元素取反
push(maxHeap, -val);
}
/* 元素出堆積 */
int popMinHeap(MaxHeap *maxHeap) {
// 元素取反
return -pop(maxHeap);
}
/* 訪問堆積頂元素 */
int peekMinHeap(MaxHeap *maxHeap) {
// 元素取反
return -peek(maxHeap);
}
/* 取出堆積中元素 */
int *getMinHeap(MaxHeap *maxHeap) {
// 將堆積中所有元素取反並存入 res 陣列
int *res = (int *)malloc(maxHeap->size * sizeof(int));
for (int i = 0; i < maxHeap->size; i++) {
res[i] = -maxHeap->data[i];
}
return res;
}
/* 取出堆積中元素 */
int *getMinHeap(MaxHeap *maxHeap) {
// 將堆積中所有元素取反並存入 res 陣列
int *res = (int *)malloc(maxHeap->size * sizeof(int));
for (int i = 0; i < maxHeap->size; i++) {
res[i] = -maxHeap->data[i];
}
return res;
}
// 基於堆積查詢陣列中最大的 k 個元素的函式
int *topKHeap(int *nums, int sizeNums, int k) {
// 初始化小頂堆積
// 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積
int *empty = (int *)malloc(0);
MaxHeap *maxHeap = newMaxHeap(empty, 0);
// 將陣列的前 k 個元素入堆積
for (int i = 0; i < k; i++) {
pushMinHeap(maxHeap, nums[i]);
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (int i = k; i < sizeNums; i++) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > peekMinHeap(maxHeap)) {
popMinHeap(maxHeap);
pushMinHeap(maxHeap, nums[i]);
}
}
int *res = getMinHeap(maxHeap);
// 釋放記憶體
delMaxHeap(maxHeap);
return res;
}
```
=== "Kotlin"
```kotlin title="top_k.kt"
/* 基於堆積查詢陣列中最大的 k 個元素 */
fun topKHeap(nums: IntArray, k: Int): Queue<Int> {
// 初始化小頂堆積
val heap = PriorityQueue<Int>()
// 將陣列的前 k 個元素入堆積
for (i in 0..<k) {
heap.offer(nums[i])
}
// 從第 k+1 個元素開始,保持堆積的長度為 k
for (i in k..<nums.size) {
// 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if (nums[i] > heap.peek()) {
heap.poll()
heap.offer(nums[i])
}
}
return heap
}
```
=== "Ruby"
```ruby title="top_k.rb"
[class]{}-[func]{top_k_heap}
```
=== "Zig"
```zig title="top_k.zig"
[class]{}-[func]{topKHeap}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20heapq%0A%0Adef%20top_k_heap%28nums%3A%20list%5Bint%5D,%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E4%BA%8E%E5%A0%86%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9C%80%E5%A4%A7%E7%9A%84%20k%20%E4%B8%AA%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E5%B0%86%E6%95%B0%E7%BB%84%E7%9A%84%E5%89%8D%20k%20%E4%B8%AA%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap,%20nums%5Bi%5D%29%0A%20%20%20%20%23%20%E4%BB%8E%E7%AC%AC%20k%2B1%20%E4%B8%AA%E5%85%83%E7%B4%A0%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BF%9D%E6%8C%81%E5%A0%86%E7%9A%84%E9%95%BF%E5%BA%A6%E4%B8%BA%20k%0A%20%20%20%20for%20i%20in%20range%28k,%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%BD%93%E5%89%8D%E5%85%83%E7%B4%A0%E5%A4%A7%E4%BA%8E%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E5%B0%86%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%E3%80%81%E5%BD%93%E5%89%8D%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap,%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1,%207,%206,%203,%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums,%20k%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20heapq%0A%0Adef%20top_k_heap%28nums%3A%20list%5Bint%5D,%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E4%BA%8E%E5%A0%86%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9C%80%E5%A4%A7%E7%9A%84%20k%20%E4%B8%AA%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E5%B0%86%E6%95%B0%E7%BB%84%E7%9A%84%E5%89%8D%20k%20%E4%B8%AA%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap,%20nums%5Bi%5D%29%0A%20%20%20%20%23%20%E4%BB%8E%E7%AC%AC%20k%2B1%20%E4%B8%AA%E5%85%83%E7%B4%A0%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BF%9D%E6%8C%81%E5%A0%86%E7%9A%84%E9%95%BF%E5%BA%A6%E4%B8%BA%20k%0A%20%20%20%20for%20i%20in%20range%28k,%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%BD%93%E5%89%8D%E5%85%83%E7%B4%A0%E5%A4%A7%E4%BA%8E%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E5%B0%86%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%E3%80%81%E5%BD%93%E5%89%8D%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap,%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1,%207,%206,%203,%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums,%20k%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
總共執行了 $n$ 輪入堆積和出堆積,堆積的最大長度為 $k$ ,因此時間複雜度為 $O(n \log k)$ 。該方法的效率很高,當 $k$ 較小時,時間複雜度趨向 $O(n)$ ;當 $k$ 較大時,時間複雜度不會超過 $O(n \log n)$ 。
另外,該方法適用於動態資料流的使用場景。在不斷加入資料時,我們可以持續維護堆積內的元素,從而實現最大的 $k$ 個元素的動態更新。
+30
View File
@@ -0,0 +1,30 @@
---
comments: true
icon: material/rocket-launch-outline
---
# 序
幾年前,我在力扣上分享了“劍指 Offer”系列題解,受到了許多讀者的鼓勵和支持。在與讀者交流期間,我最常被問的一個問題是“如何入門演算法”。逐漸地,我對這個問題產生了濃厚的興趣。
兩眼一抹黑地刷題似乎是最受歡迎的方法,簡單、直接且有效。然而刷題就如同玩“掃雷”遊戲,自學能力強的人能夠順利將地雷逐個排掉,而基礎不足的人很可能被炸得滿頭是包,並在挫折中步步退縮。通讀教材也是一種常見做法,但對於面向求職的人來說,畢業論文、投遞簡歷、準備筆試和面試已經消耗了大部分精力,啃厚重的書往往變成了一項艱鉅的挑戰。
如果你也面臨類似的困擾,那麼很幸運這本書“找”到了你。本書是我對這個問題給出的答案,即使不是最優解,也至少是一次積極的嘗試。本書雖然不足以讓你直接拿到 Offer,但會引導你探索資料結構與演算法的“知識地圖”,帶你瞭解不同“地雷”的形狀、大小和分佈位置,讓你掌握各種“排雷方法”。有了這些本領,相信你可以更加自如地刷題和閱讀文獻,逐步構建起完整的知識體系。
我深深贊同費曼教授所言:“Knowledge isn't free. You have to pay attention.”從這個意義上看,這本書並非完全“免費”。為了不辜負你為本書所付出的寶貴“注意力”,我會竭盡所能,投入最大的“注意力”來完成本書的創作。
本人自知學疏才淺,書中內容雖然已經過一段時間的打磨,但一定仍有許多錯誤,懇請各位老師和同學批評指正。
![Hello 演算法](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" }
<div style="text-align: center;">
<h2 style="margin-top: 0.8em; margin-bottom: 0.8em;">Hello,演算法!</h2>
</div>
計算機的出現給世界帶來了巨大變革,它憑藉高速的計算能力和出色的可程式設計性,成為了執行演算法與處理資料的理想媒介。無論是電子遊戲的逼真畫面、自動駕駛的智慧決策,還是 AlphaGo 的精彩棋局、ChatGPT 的自然互動,這些應用都是演算法在計算機上的精妙演繹。
事實上,在計算機問世之前,演算法和資料結構就已經存在於世界的各個角落。早期的演算法相對簡單,例如古代的計數方法和工具製作步驟等。隨著文明的進步,演算法逐漸變得更加精細和複雜。從巧奪天工的匠人技藝、到解放生產力的工業產品、再到宇宙執行的科學規律,幾乎每一件平凡或令人驚歎的事物背後,都隱藏著精妙的演算法思想。
同樣,資料結構無處不在:大到社會網路,小到地鐵線路,許多系統都可以建模為“圖”;大到一個國家,小到一個家庭,社會的主要組織形式呈現出“樹”的特徵;冬天的衣服就像“堆疊”,最先穿上的最後才能脫下;羽毛球筒則如同“佇列”,一端放入、另一端取出;字典就像一個“雜湊表”,能夠快速查詢目標詞條。
本書旨在透過清晰易懂的動畫圖解和可執行的程式碼示例,使讀者理解演算法和資料結構的核心概念,並能夠透過程式設計來實現它們。在此基礎上,本書致力於揭示演算法在複雜世界中的生動體現,展現演算法之美。希望本書能夠幫助到你!
@@ -0,0 +1,66 @@
---
comments: true
---
# 1.1 &nbsp; 演算法無處不在
當我們聽到“演算法”這個詞時,很自然地會想到數學。然而實際上,許多演算法並不涉及複雜數學,而是更多地依賴基本邏輯,這些邏輯在我們的日常生活中處處可見。
在正式探討演算法之前,有一個有趣的事實值得分享:**你已經在不知不覺中學會了許多演算法,並習慣將它們應用到日常生活中了**。下面我將舉幾個具體的例子來證實這一點。
**例一:查字典**。在字典裡,每個漢字都對應一個拼音,而字典是按照拼音字母順序排列的。假設我們需要查詢一個拼音首字母為 $r$ 的字,通常會按照圖 1-1 所示的方式實現。
1. 翻開字典約一半的頁數,檢視該頁的首字母是什麼,假設首字母為 $m$ 。
2. 由於在拼音字母表中 $r$ 位於 $m$ 之後,所以排除字典前半部分,查詢範圍縮小到後半部分。
3. 不斷重複步驟 `1.` 和 步驟 `2.` ,直至找到拼音首字母為 $r$ 的頁碼為止。
=== "<1>"
![查字典步驟](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" }
=== "<2>"
![binary_search_dictionary_step2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" }
=== "<3>"
![binary_search_dictionary_step3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" }
=== "<4>"
![binary_search_dictionary_step4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" }
=== "<5>"
![binary_search_dictionary_step5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" }
<p align="center"> 圖 1-1 &nbsp; 查字典步驟 </p>
查字典這個小學生必備技能,實際上就是著名的“二分搜尋”演算法。從資料結構的角度,我們可以把字典視為一個已排序的“陣列”;從演算法的角度,我們可以將上述查字典的一系列操作看作“二分搜尋”。
**例二:整理撲克**。我們在打牌時,每局都需要整理手中的撲克牌,使其從小到大排列,實現流程如圖 1-2 所示。
1. 將撲克牌劃分為“有序”和“無序”兩部分,並假設初始狀態下最左 1 張撲克牌已經有序。
2. 在無序部分抽出一張撲克牌,插入至有序部分的正確位置;完成後最左 2 張撲克已經有序。
3. 不斷迴圈步驟 `2.` ,每一輪將一張撲克牌從無序部分插入至有序部分,直至所有撲克牌都有序。
![撲克排序步驟](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" }
<p align="center"> 圖 1-2 &nbsp; 撲克排序步驟 </p>
上述整理撲克牌的方法本質上是“插入排序”演算法,它在處理小型資料集時非常高效。許多程式語言的排序庫函式中都有插入排序的身影。
**例三:貨幣找零**。假設我們在超市購買了 $69$ 元的商品,給了收銀員 $100$ 元,則收銀員需要找我們 $31$ 元。他會很自然地完成如圖 1-3 所示的思考。
1. 可選項是比 $31$ 元面值更小的貨幣,包括 $1$ 元、$5$ 元、$10$ 元、$20$ 元。
2. 從可選項中拿出最大的 $20$ 元,剩餘 $31 - 20 = 11$ 元。
3. 從剩餘可選項中拿出最大的 $10$ 元,剩餘 $11 - 10 = 1$ 元。
4. 從剩餘可選項中拿出最大的 $1$ 元,剩餘 $1 - 1 = 0$ 元。
5. 完成找零,方案為 $20 + 10 + 1 = 31$ 元。
![貨幣找零過程](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" }
<p align="center"> 圖 1-3 &nbsp; 貨幣找零過程 </p>
在以上步驟中,我們每一步都採取當前看來最好的選擇(儘可能用大面額的貨幣),最終得到了可行的找零方案。從資料結構與演算法的角度看,這種方法本質上是“貪婪”演算法。
小到烹飪一道菜,大到星際航行,幾乎所有問題的解決都離不開演算法。計算機的出現使得我們能夠透過程式設計將資料結構儲存在記憶體中,同時編寫程式碼呼叫 CPU 和 GPU 執行演算法。這樣一來,我們就能把生活中的問題轉移到計算機上,以更高效的方式解決各種複雜問題。
!!! tip
如果你對資料結構、演算法、陣列和二分搜尋等概念仍感到一知半解,請繼續往下閱讀,本書將引導你邁入資料結構與演算法的知識殿堂。
@@ -0,0 +1,20 @@
---
comments: true
icon: material/calculator-variant-outline
---
# 第 1 章 &nbsp; 初識演算法
![初識演算法](../assets/covers/chapter_introduction.jpg){ class="cover-image" }
!!! abstract
一位少女翩翩起舞,與資料交織在一起,裙襬上飄揚著演算法的旋律。
她邀請你共舞,請緊跟她的步伐,踏入充滿邏輯與美感的演算法世界。
## Chapter Contents
- [1.1 &nbsp; 演算法無處不在](https://www.hello-algo.com/en/chapter_introduction/algorithms_are_everywhere/)
- [1.2 &nbsp; 演算法是什麼](https://www.hello-algo.com/en/chapter_introduction/what_is_dsa/)
- [1.3 &nbsp; 小結](https://www.hello-algo.com/en/chapter_introduction/summary/)
@@ -0,0 +1,13 @@
---
comments: true
---
# 1.3 &nbsp; 小結
- 演算法在日常生活中無處不在,並不是遙不可及的高深知識。實際上,我們已經在不知不覺中學會了許多演算法,用以解決生活中的大小問題。
- 查字典的原理與二分搜尋演算法相一致。二分搜尋演算法體現了分而治之的重要演算法思想。
- 整理撲克的過程與插入排序演算法非常類似。插入排序演算法適合排序小型資料集。
- 貨幣找零的步驟本質上是貪婪演算法,每一步都採取當前看來最好的選擇。
- 演算法是在有限時間內解決特定問題的一組指令或操作步驟,而資料結構是計算機中組織和儲存資料的方式。
- 資料結構與演算法緊密相連。資料結構是演算法的基石,而演算法是資料結構發揮作用的舞臺。
- 我們可以將資料結構與演算法類比為拼裝積木,積木代表資料,積木的形狀和連線方式等代表資料結構,拼裝積木的步驟則對應演算法。
@@ -0,0 +1,65 @@
---
comments: true
---
# 1.2 &nbsp; 演算法是什麼
## 1.2.1 &nbsp; 演算法定義
<u>演算法(algorithm</u>是在有限時間內解決特定問題的一組指令或操作步驟,它具有以下特性。
- 問題是明確的,包含清晰的輸入和輸出定義。
- 具有可行性,能夠在有限步驟、時間和記憶體空間下完成。
- 各步驟都有確定的含義,在相同的輸入和執行條件下,輸出始終相同。
## 1.2.2 &nbsp; 資料結構定義
<u>資料結構(data structure</u>是計算機中組織和儲存資料的方式,具有以下設計目標。
- 空間佔用儘量少,以節省計算機記憶體。
- 資料操作儘可能快速,涵蓋資料訪問、新增、刪除、更新等。
- 提供簡潔的資料表示和邏輯資訊,以便演算法高效執行。
**資料結構設計是一個充滿權衡的過程**。如果想在某方面取得提升,往往需要在另一方面作出妥協。下面舉兩個例子。
- 鏈結串列相較於陣列,在資料新增和刪除操作上更加便捷,但犧牲了資料訪問速度。
- 圖相較於鏈結串列,提供了更豐富的邏輯資訊,但需要佔用更大的記憶體空間。
## 1.2.3 &nbsp; 資料結構與演算法的關係
如圖 1-4 所示,資料結構與演算法高度相關、緊密結合,具體表現在以下三個方面。
- 資料結構是演算法的基石。資料結構為演算法提供了結構化儲存的資料,以及操作資料的方法。
- 演算法是資料結構發揮作用的舞臺。資料結構本身僅儲存資料資訊,結合演算法才能解決特定問題。
- 演算法通常可以基於不同的資料結構實現,但執行效率可能相差很大,選擇合適的資料結構是關鍵。
![資料結構與演算法的關係](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" }
<p align="center"> 圖 1-4 &nbsp; 資料結構與演算法的關係 </p>
資料結構與演算法猶如圖 1-5 所示的拼裝積木。一套積木,除了包含許多零件之外,還附有詳細的組裝說明書。我們按照說明書一步步操作,就能組裝出精美的積木模型。
![拼裝積木](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" }
<p align="center"> 圖 1-5 &nbsp; 拼裝積木 </p>
兩者的詳細對應關係如表 1-1 所示。
<p align="center"> 表 1-1 &nbsp; 將資料結構與演算法類比為拼裝積木 </p>
<div class="center-table" markdown>
| 資料結構與演算法 | 拼裝積木 |
| -------------- | ---------------------------------------- |
| 輸入資料 | 未拼裝的積木 |
| 資料結構 | 積木組織形式,包括形狀、大小、連線方式等 |
| 演算法 | 把積木拼成目標形態的一系列操作步驟 |
| 輸出資料 | 積木模型 |
</div>
值得說明的是,資料結構與演算法是獨立於程式語言的。正因如此,本書得以提供基於多種程式語言的實現。
!!! tip "約定俗成的簡稱"
在實際討論時,我們通常會將“資料結構與演算法”簡稱為“演算法”。比如眾所周知的 LeetCode 演算法題目,實際上同時考查資料結構和演算法兩方面的知識。
@@ -0,0 +1,56 @@
---
comments: true
---
# 0.1 &nbsp; 關於本書
本專案旨在建立一本開源、免費、對新手友好的資料結構與演算法入門教程。
- 全書採用動畫圖解,結構化地講解資料結構與演算法知識,內容清晰易懂,學習曲線平滑。
- 演算法源程式碼皆可一鍵執行,支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、Rust、C 和 Zig 等語言。
- 鼓勵讀者在線上章節評論區互幫互助、共同進步,提問與評論通常可在兩日內得到回覆。
## 0.1.1 &nbsp; 讀者物件
若你是演算法初學者,從未接觸過演算法,或者已經有一些刷題經驗,對資料結構與演算法有模糊的認識,在會與不會之間反覆橫跳,那麼本書正是為你量身定製的!
如果你已經積累一定的刷題量,熟悉大部分題型,那麼本書可助你回顧與梳理演算法知識體系,倉庫源程式碼可以當作“刷題工具庫”或“演算法字典”來使用。
若你是演算法“大神”,我們期待收到你的寶貴建議,或者[一起參與創作](https://www.hello-algo.com/chapter_appendix/contribution/)。
!!! success "前置條件"
你需要至少具備任一語言的程式設計基礎,能夠閱讀和編寫簡單程式碼。
## 0.1.2 &nbsp; 內容結構
本書的主要內容如圖 0-1 所示。
- **複雜度分析**:資料結構和演算法的評價維度與方法。時間複雜度和空間複雜度的推算方法、常見型別、示例等。
- **資料結構**:基本資料型別和資料結構的分類方法。陣列、鏈結串列、堆疊、佇列、雜湊表、樹、堆積、圖等資料結構的定義、優缺點、常用操作、常見型別、典型應用、實現方法等。
- **演算法**:搜尋、排序、分治、回溯、動態規劃、貪婪等演算法的定義、優缺點、效率、應用場景、解題步驟和示例問題等。
![本書主要內容](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" }
<p align="center"> 圖 0-1 &nbsp; 本書主要內容 </p>
## 0.1.3 &nbsp; 致謝
本書在開源社群眾多貢獻者的共同努力下不斷完善。感謝每一位投入時間與精力的撰稿人,他們是(按照 GitHub 自動生成的順序):krahets、codingonion、nuomi1、Gonglja、Reanon、justin-tse、danielsss、hpstory、S-N-O-R-L-A-X、night-cruise、msk397、gvenusleo、RiverTwilight、gyt95、zhuoqinyue、Zuoxun、Xia-Sang、mingXta、FangYuan33、GN-Yu、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、mgisr、JoseHung、qualifier1024、pengchzn、Guanngxu、longsizhuo、L-Super、what-is-me、yuan0221、lhxsm、Slone123c、WSL0809、longranger2、theNefelibatas、xiongsp、JeffersonHuang、hongyun-robot、K3v123、yuelinxin、a16su、gaofer、malone6、Wonderdch、xjr7670、DullSword、Horbin-Magician、NI-SW、reeswell、XC-Zero、XiaChuerwu、yd-j、iron-irax、huawuque404、MolDuM、Nigh、KorsChen、foursevenlove、52coder、bubble9um、youshaoXG、curly210102、gltianwen、fanchenggang、Transmigration-zhou、FloranceYeh、FreddieLi、ShiMaRing、lipusheng、Javesun99、JackYang-hellobobo、shanghai-Jerry、0130w、Keynman、psychelzh、logan-qiu、ZnYang2018、MwumLi、1ch0、Phoenix0415、qingpeng9802、Richard-Zhang1019、QiLOL、Suremotoo、Turing-1024-Lee、Evilrabbit520、GaochaoZhu、ZJKung、linzeyan、hezhizhen、ZongYangL、beintentional、czruby、coderlef、dshlstarr、szu17dmy、fbigm、gledfish、hts0000、boloboloda、iStig、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、liuxjerry、lucaswangdev、lyl625760、chadyi、noobcodemaker、selear、siqyka、syd168、4yDX3906、tao363、wangwang105、weibk、yabo083、yi427、yishangzhang、zhouLion、baagod、ElaBosak233、xb534、luluxia、yanedie、thomasq0、YangXuanyi 和 th1nk3r-ing 。
本書的程式碼審閱工作由 codingonion、Gonglja、gvenusleo、hpstory、justin-tse、krahets、night-cruise、nuomi1 和 Reanon 完成(按照首字母順序排列)。感謝他們付出的時間與精力,正是他們確保了各語言程式碼的規範與統一。
在本書的創作過程中,我得到了許多人的幫助。
- 感謝我在公司的導師李汐博士,在一次暢談中你鼓勵我“快行動起來”,堅定了我寫這本書的決心;
- 感謝我的女朋友泡泡作為本書的首位讀者,從演算法小白的角度提出許多寶貴建議,使得本書更適合新手閱讀;
- 感謝騰寶、琦寶、飛寶為本書起了一個富有創意的名字,喚起大家寫下第一行程式碼“Hello World!”的美好回憶;
- 感謝校銓在智慧財產權方面提供的專業幫助,這對本開源書的完善起到了重要作用;
- 感謝蘇潼為本書設計了精美的封面和 logo ,並在我的強迫症的驅使下多次耐心修改;
- 感謝 @squidfunk 提供的排版建議,以及他開發的開源文件主題 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。
在寫作過程中,我閱讀了許多關於資料結構與演算法的教材和文章。這些作品為本書提供了優秀的範本,確保了本書內容的準確性與品質。在此感謝所有老師和前輩的傑出貢獻!
本書倡導手腦並用的學習方式,在這一點上我深受[《動手學深度學習》](https://github.com/d2l-ai/d2l-zh)的啟發。在此向各位讀者強烈推薦這本優秀的著作。
**衷心感謝我的父母,正是你們一直以來的支持與鼓勵,讓我有機會做這件富有趣味的事**。
+20
View File
@@ -0,0 +1,20 @@
---
comments: true
icon: material/book-open-outline
---
# 第 0 章 &nbsp; 前言
![前言](../assets/covers/chapter_preface.jpg){ class="cover-image" }
!!! abstract
演算法猶如美妙的交響樂,每一行程式碼都像韻律般流淌。
願這本書在你的腦海中輕輕響起,留下獨特而深刻的旋律。
## Chapter Contents
- [0.1 &nbsp; 關於本書](https://www.hello-algo.com/en/chapter_preface/about_the_book/)
- [0.2 &nbsp; 如何使用本書](https://www.hello-algo.com/en/chapter_preface/suggestions/)
- [0.3 &nbsp; 小結](https://www.hello-algo.com/en/chapter_preface/summary/)
+265
View File
@@ -0,0 +1,265 @@
---
comments: true
---
# 0.2 &nbsp; 如何使用本書
!!! tip
為了獲得最佳的閱讀體驗,建議你通讀本節內容。
## 0.2.1 &nbsp; 行文風格約定
- 標題後標註 `*` 的是選讀章節,內容相對困難。如果你的時間有限,可以先跳過。
- 專業術語會使用黑體(紙質版和 PDF 版)或新增下劃線(網頁版),例如<u>陣列(array</u>。建議記住它們,以便閱讀文獻。
- 重點內容和總結性語句會 **加粗**,這類文字值得特別關注。
- 有特指含義的詞句會使用“引號”標註,以避免歧義。
- 當涉及程式語言之間不一致的名詞時,本書均以 Python 為準,例如使用 `None` 來表示“空”。
- 本書部分放棄了程式語言的註釋規範,以換取更加緊湊的內容排版。註釋主要分為三種類型:標題註釋、內容註釋、多行註釋。
=== "Python"
```python title=""
"""標題註釋,用於標註函式、類別、測試樣例等"""
# 內容註釋,用於詳解程式碼
"""
多行
註釋
"""
```
=== "C++"
```cpp title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "Java"
```java title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "C#"
```csharp title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "Go"
```go title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "Swift"
```swift title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "JS"
```javascript title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "TS"
```typescript title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "Dart"
```dart title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "Rust"
```rust title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "C"
```c title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "Kotlin"
```kotlin title=""
/* 標題註釋,用於標註函式、類別、測試樣例等 */
// 內容註釋,用於詳解程式碼
/**
* 多行
* 註釋
*/
```
=== "Ruby"
```ruby title=""
```
=== "Zig"
```zig title=""
// 標題註釋,用於標註函式、類別、測試樣例等
// 內容註釋,用於詳解程式碼
// 多行
// 註釋
```
## 0.2.2 &nbsp; 在動畫圖解中高效學習
相較於文字,影片和圖片具有更高的資訊密度和結構化程度,更易於理解。在本書中,**重點和難點知識將主要透過動畫以圖解形式展示**,而文字則作為解釋與補充。
如果你在閱讀本書時,發現某段內容提供瞭如圖 0-2 所示的動畫圖解,**請以圖為主、以文字為輔**,綜合兩者來理解內容。
![動畫圖解示例](../index.assets/animation.gif){ class="animation-figure" }
<p align="center"> 圖 0-2 &nbsp; 動畫圖解示例 </p>
## 0.2.3 &nbsp; 在程式碼實踐中加深理解
本書的配套程式碼託管在 [GitHub 倉庫](https://github.com/krahets/hello-algo)。如圖 0-3 所示,**源程式碼附有測試樣例,可一鍵執行**。
如果時間允許,**建議你參照程式碼自行敲一遍**。如果學習時間有限,請至少通讀並執行所有程式碼。
與閱讀程式碼相比,編寫程式碼的過程往往能帶來更多收穫。**動手學,才是真的學**。
![執行程式碼示例](../index.assets/running_code.gif){ class="animation-figure" }
<p align="center"> 圖 0-3 &nbsp; 執行程式碼示例 </p>
執行程式碼的前置工作主要分為三步。
**第一步:安裝本地程式設計環境**。請參照附錄所示的[教程](https://www.hello-algo.com/chapter_appendix/installation/)進行安裝,如果已安裝,則可跳過此步驟。
**第二步:克隆或下載程式碼倉庫**。前往 [GitHub 倉庫](https://github.com/krahets/hello-algo)。如果已經安裝 [Git](https://git-scm.com/downloads) ,可以透過以下命令克隆本倉庫:
```shell
git clone https://github.com/krahets/hello-algo.git
```
當然,你也可以在圖 0-4 所示的位置,點選“Download ZIP”按鈕直接下載程式碼壓縮包,然後在本地解壓即可。
![克隆倉庫與下載程式碼](suggestions.assets/download_code.png){ class="animation-figure" }
<p align="center"> 圖 0-4 &nbsp; 克隆倉庫與下載程式碼 </p>
**第三步:執行源程式碼**。如圖 0-5 所示,對於頂部標有檔案名稱的程式碼塊,我們可以在倉庫的 `codes` 檔案夾內找到對應的源程式碼檔案。源程式碼檔案可一鍵執行,將幫助你節省不必要的除錯時間,讓你能夠專注於學習內容。
![程式碼塊與對應的源程式碼檔案](suggestions.assets/code_md_to_repo.png){ class="animation-figure" }
<p align="center"> 圖 0-5 &nbsp; 程式碼塊與對應的源程式碼檔案 </p>
除了本地執行程式碼,**網頁版還支持 Python 程式碼的視覺化執行**(基於 [pythontutor](https://pythontutor.com/) 實現)。如圖 0-6 所示,你可以點選程式碼塊下方的“視覺化執行”來展開檢視,觀察演算法程式碼的執行過程;也可以點選“全屏觀看”,以獲得更好的閱覽體驗。
![Python 程式碼的視覺化執行](suggestions.assets/pythontutor_example.png){ class="animation-figure" }
<p align="center"> 圖 0-6 &nbsp; Python 程式碼的視覺化執行 </p>
## 0.2.4 &nbsp; 在提問討論中共同成長
在閱讀本書時,請不要輕易跳過那些沒學明白的知識點。**歡迎在評論區提出你的問題**,我和小夥伴們將竭誠為你解答,一般情況下可在兩天內回覆。
如圖 0-7 所示,網頁版每個章節的底部都配有評論區。希望你能多關注評論區的內容。一方面,你可以瞭解大家遇到的問題,從而查漏補缺,激發更深入的思考。另一方面,期待你能慷慨地回答其他小夥伴的問題,分享你的見解,幫助他人進步。
![評論區示例](../index.assets/comment.gif){ class="animation-figure" }
<p align="center"> 圖 0-7 &nbsp; 評論區示例 </p>
## 0.2.5 &nbsp; 演算法學習路線
從總體上看,我們可以將學習資料結構與演算法的過程劃分為三個階段。
1. **階段一:演算法入門**。我們需要熟悉各種資料結構的特點和用法,學習不同演算法的原理、流程、用途和效率等方面的內容。
2. **階段二:刷演算法題**。建議從熱門題目開刷,先積累至少 100 道題目,熟悉主流的演算法問題。初次刷題時,“知識遺忘”可能是一個挑戰,但請放心,這是很正常的。我們可以按照“艾賓浩斯遺忘曲線”來複習題目,通常在進行 3~5 輪的重複後,就能將其牢記在心。推薦的題單和刷題計劃請見此 [GitHub 倉庫](https://github.com/krahets/LeetCode-Book)。
3. **階段三:搭建知識體系**。在學習方面,我們可以閱讀演算法專欄文章、解題框架和演算法教材,以不斷豐富知識體系。在刷題方面,可以嘗試採用進階刷題策略,如按專題分類、一題多解、一解多題等,相關的刷題心得可以在各個社群找到。
如圖 0-8 所示,本書內容主要涵蓋“階段一”,旨在幫助你更高效地展開階段二和階段三的學習。
![演算法學習路線](suggestions.assets/learning_route.png){ class="animation-figure" }
<p align="center"> 圖 0-8 &nbsp; 演算法學習路線 </p>
+12
View File
@@ -0,0 +1,12 @@
---
comments: true
---
# 0.3 &nbsp; 小結
- 本書的主要受眾是演算法初學者。如果你已有一定基礎,本書能幫助你系統回顧演算法知識,書中源程式碼也可作為“刷題工具庫”使用。
- 書中內容主要包括複雜度分析、資料結構和演算法三部分,涵蓋了該領域的大部分主題。
- 對於演算法新手,在初學階段閱讀一本入門書至關重要,可以少走許多彎路。
- 書中的動畫圖解通常用於介紹重點和難點知識。閱讀本書時,應給予這些內容更多關注。
- 實踐乃學習程式設計之最佳途徑。強烈建議執行源程式碼並親自敲程式碼。
- 本書網頁版的每個章節都設有評論區,歡迎隨時分享你的疑惑與見解。
+25
View File
@@ -0,0 +1,25 @@
---
icon: material/bookshelf
---
# 參考文獻
[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).
[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).
[3] Robert Sedgewick, et al. Algorithms (4th Edition).
[4] 嚴蔚敏. 資料結構(C 語言版).
[5] 鄧俊輝. 資料結構(C++ 語言版,第三版).
[6] 馬克 艾倫 維斯著,陳越譯. 資料結構與演算法分析:Java語言描述(第三版).
[7] 程傑. 大話資料結構.
[8] 王爭. 資料結構與演算法之美.
[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).
[10] Aston Zhang, et al. Dive into Deep Learning.
+721
View File
@@ -0,0 +1,721 @@
---
comments: true
---
# 10.1 &nbsp; 二分搜尋
<u>二分搜尋(binary search</u>是一種基於分治策略的高效搜尋演算法。它利用資料的有序性,每輪縮小一半搜尋範圍,直至找到目標元素或搜尋區間為空為止。
!!! question
給定一個長度為 $n$ 的陣列 `nums` ,元素按從小到大的順序排列且不重複。請查詢並返回元素 `target` 在該陣列中的索引。若陣列不包含該元素,則返回 $-1$ 。示例如圖 10-1 所示。
![二分搜尋示例資料](binary_search.assets/binary_search_example.png){ class="animation-figure" }
<p align="center"> 圖 10-1 &nbsp; 二分搜尋示例資料 </p>
如圖 10-2 所示,我們先初始化指標 $i = 0$ 和 $j = n - 1$ ,分別指向陣列首元素和尾元素,代表搜尋區間 $[0, n - 1]$ 。請注意,中括號表示閉區間,其包含邊界值本身。
接下來,迴圈執行以下兩步。
1. 計算中點索引 $m = \lfloor {(i + j) / 2} \rfloor$ ,其中 $\lfloor \: \rfloor$ 表示向下取整操作。
2. 判斷 `nums[m]``target` 的大小關係,分為以下三種情況。
1. 當 `nums[m] < target` 時,說明 `target` 在區間 $[m + 1, j]$ 中,因此執行 $i = m + 1$ 。
2. 當 `nums[m] > target` 時,說明 `target` 在區間 $[i, m - 1]$ 中,因此執行 $j = m - 1$ 。
3. 當 `nums[m] = target` 時,說明找到 `target` ,因此返回索引 $m$ 。
若陣列不包含目標元素,搜尋區間最終會縮小為空。此時返回 $-1$ 。
=== "<1>"
![二分搜尋流程](binary_search.assets/binary_search_step1.png){ class="animation-figure" }
=== "<2>"
![binary_search_step2](binary_search.assets/binary_search_step2.png){ class="animation-figure" }
=== "<3>"
![binary_search_step3](binary_search.assets/binary_search_step3.png){ class="animation-figure" }
=== "<4>"
![binary_search_step4](binary_search.assets/binary_search_step4.png){ class="animation-figure" }
=== "<5>"
![binary_search_step5](binary_search.assets/binary_search_step5.png){ class="animation-figure" }
=== "<6>"
![binary_search_step6](binary_search.assets/binary_search_step6.png){ class="animation-figure" }
=== "<7>"
![binary_search_step7](binary_search.assets/binary_search_step7.png){ class="animation-figure" }
<p align="center"> 圖 10-2 &nbsp; 二分搜尋流程 </p>
值得注意的是,由於 $i$ 和 $j$ 都是 `int` 型別,**因此 $i + j$ 可能會超出 `int` 型別的取值範圍**。為了避免大數越界,我們通常採用公式 $m = \lfloor {i + (j - i) / 2} \rfloor$ 來計算中點。
程式碼如下所示:
=== "Python"
```python title="binary_search.py"
def binary_search(nums: list[int], target: int) -> int:
"""二分搜尋(雙閉區間)"""
# 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
i, j = 0, len(nums) - 1
# 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while i <= j:
# 理論上 Python 的數字可以無限大(取決於記憶體大小),無須考慮大數越界問題
m = (i + j) // 2 # 計算中點索引 m
if nums[m] < target:
i = m + 1 # 此情況說明 target 在區間 [m+1, j] 中
elif nums[m] > target:
j = m - 1 # 此情況說明 target 在區間 [i, m-1] 中
else:
return m # 找到目標元素,返回其索引
return -1 # 未找到目標元素,返回 -1
```
=== "C++"
```cpp title="binary_search.cpp"
/* 二分搜尋(雙閉區間) */
int binarySearch(vector<int> &nums, int target) {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
int i = 0, j = nums.size() - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Java"
```java title="binary_search.java"
/* 二分搜尋(雙閉區間) */
int binarySearch(int[] nums, int target) {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
int i = 0, j = nums.length - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "C#"
```csharp title="binary_search.cs"
/* 二分搜尋(雙閉區間) */
int BinarySearch(int[] nums, int target) {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
int i = 0, j = nums.Length - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Go"
```go title="binary_search.go"
/* 二分搜尋(雙閉區間) */
func binarySearch(nums []int, target int) int {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
i, j := 0, len(nums)-1
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
for i <= j {
m := i + (j-i)/2 // 計算中點索引 m
if nums[m] < target { // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1
} else if nums[m] > target { // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1
} else { // 找到目標元素,返回其索引
return m
}
}
// 未找到目標元素,返回 -1
return -1
}
```
=== "Swift"
```swift title="binary_search.swift"
/* 二分搜尋(雙閉區間) */
func binarySearch(nums: [Int], target: Int) -> Int {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
var i = nums.startIndex
var j = nums.endIndex - 1
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while i <= j {
let m = i + (j - i) / 2 // 計算中點索引 m
if nums[m] < target { // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1
} else if nums[m] > target { // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1
} else { // 找到目標元素,返回其索引
return m
}
}
// 未找到目標元素,返回 -1
return -1
}
```
=== "JS"
```javascript title="binary_search.js"
/* 二分搜尋(雙閉區間) */
function binarySearch(nums, target) {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
let i = 0,
j = nums.length - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
// 計算中點索引 m ,使用 parseInt() 向下取整
const m = parseInt(i + (j - i) / 2);
if (nums[m] < target)
// 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
else if (nums[m] > target)
// 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
else return m; // 找到目標元素,返回其索引
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "TS"
```typescript title="binary_search.ts"
/* 二分搜尋(雙閉區間) */
function binarySearch(nums: number[], target: number): number {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
let i = 0,
j = nums.length - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
// 計算中點索引 m
const m = Math.floor(i + (j - i) / 2);
if (nums[m] < target) {
// 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
} else if (nums[m] > target) {
// 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
} else {
// 找到目標元素,返回其索引
return m;
}
}
return -1; // 未找到目標元素,返回 -1
}
```
=== "Dart"
```dart title="binary_search.dart"
/* 二分搜尋(雙閉區間) */
int binarySearch(List<int> nums, int target) {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
int i = 0, j = nums.length - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
int m = i + (j - i) ~/ 2; // 計算中點索引 m
if (nums[m] < target) {
// 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
} else if (nums[m] > target) {
// 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
} else {
// 找到目標元素,返回其索引
return m;
}
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Rust"
```rust title="binary_search.rs"
/* 二分搜尋(雙閉區間) */
fn binary_search(nums: &[i32], target: i32) -> i32 {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
let mut i = 0;
let mut j = nums.len() as i32 - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while i <= j {
let m = i + (j - i) / 2; // 計算中點索引 m
if nums[m as usize] < target {
// 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
} else if nums[m as usize] > target {
// 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
} else {
// 找到目標元素,返回其索引
return m;
}
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "C"
```c title="binary_search.c"
/* 二分搜尋(雙閉區間) */
int binarySearch(int *nums, int len, int target) {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
int i = 0, j = len - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Kotlin"
```kotlin title="binary_search.kt"
/* 二分搜尋(雙閉區間) */
fun binarySearch(nums: IntArray, target: Int): Int {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
var i = 0
var j = nums.size - 1
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
val m = i + (j - i) / 2 // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1
else // 找到目標元素,返回其索引
return m
}
// 未找到目標元素,返回 -1
return -1
}
```
=== "Ruby"
```ruby title="binary_search.rb"
[class]{}-[func]{binary_search}
```
=== "Zig"
```zig title="binary_search.zig"
// 二分搜尋(雙閉區間)
fn binarySearch(comptime T: type, nums: std.ArrayList(T), target: T) T {
// 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
var i: usize = 0;
var j: usize = nums.items.len - 1;
// 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
while (i <= j) {
var m = i + (j - i) / 2; // 計算中點索引 m
if (nums.items[m] < target) { // 此情況說明 target 在區間 [m+1, j] 中
i = m + 1;
} else if (nums.items[m] > target) { // 此情況說明 target 在區間 [i, m-1] 中
j = m - 1;
} else { // 找到目標元素,返回其索引
return @intCast(m);
}
}
// 未找到目標元素,返回 -1
return -1;
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%20%EF%BC%8C%E5%8D%B3%20i,%20j%20%E5%88%86%E5%88%AB%E6%8C%87%E5%90%91%E6%95%B0%E7%BB%84%E9%A6%96%E5%85%83%E7%B4%A0%E3%80%81%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%EF%BC%8C%E5%BD%93%E6%90%9C%E7%B4%A2%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E8%B7%B3%E5%87%BA%EF%BC%88%E5%BD%93%20i%20%3E%20j%20%E6%97%B6%E4%B8%BA%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%90%86%E8%AE%BA%E4%B8%8A%20Python%20%E7%9A%84%E6%95%B0%E5%AD%97%E5%8F%AF%E4%BB%A5%E6%97%A0%E9%99%90%E5%A4%A7%EF%BC%88%E5%8F%96%E5%86%B3%E4%BA%8E%E5%86%85%E5%AD%98%E5%A4%A7%E5%B0%8F%EF%BC%89%EF%BC%8C%E6%97%A0%E9%A1%BB%E8%80%83%E8%99%91%E5%A4%A7%E6%95%B0%E8%B6%8A%E7%95%8C%E9%97%AE%E9%A2%98%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20return%20-1%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%20%EF%BC%8C%E5%8D%B3%20i,%20j%20%E5%88%86%E5%88%AB%E6%8C%87%E5%90%91%E6%95%B0%E7%BB%84%E9%A6%96%E5%85%83%E7%B4%A0%E3%80%81%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%EF%BC%8C%E5%BD%93%E6%90%9C%E7%B4%A2%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E8%B7%B3%E5%87%BA%EF%BC%88%E5%BD%93%20i%20%3E%20j%20%E6%97%B6%E4%B8%BA%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%90%86%E8%AE%BA%E4%B8%8A%20Python%20%E7%9A%84%E6%95%B0%E5%AD%97%E5%8F%AF%E4%BB%A5%E6%97%A0%E9%99%90%E5%A4%A7%EF%BC%88%E5%8F%96%E5%86%B3%E4%BA%8E%E5%86%85%E5%AD%98%E5%A4%A7%E5%B0%8F%EF%BC%89%EF%BC%8C%E6%97%A0%E9%A1%BB%E8%80%83%E8%99%91%E5%A4%A7%E6%95%B0%E8%B6%8A%E7%95%8C%E9%97%AE%E9%A2%98%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20return%20-1%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
**時間複雜度為 $O(\log n)$** :在二分迴圈中,區間每輪縮小一半,因此迴圈次數為 $\log_2 n$ 。
**空間複雜度為 $O(1)$** :指標 $i$ 和 $j$ 使用常數大小空間。
## 10.1.1 &nbsp; 區間表示方法
除了上述雙閉區間外,常見的區間表示還有“左閉右開”區間,定義為 $[0, n)$ ,即左邊界包含自身,右邊界不包含自身。在該表示下,區間 $[i, j)$ 在 $i = j$ 時為空。
我們可以基於該表示實現具有相同功能的二分搜尋演算法:
=== "Python"
```python title="binary_search.py"
def binary_search_lcro(nums: list[int], target: int) -> int:
"""二分搜尋(左閉右開區間)"""
# 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
i, j = 0, len(nums)
# 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while i < j:
m = (i + j) // 2 # 計算中點索引 m
if nums[m] < target:
i = m + 1 # 此情況說明 target 在區間 [m+1, j) 中
elif nums[m] > target:
j = m # 此情況說明 target 在區間 [i, m) 中
else:
return m # 找到目標元素,返回其索引
return -1 # 未找到目標元素,返回 -1
```
=== "C++"
```cpp title="binary_search.cpp"
/* 二分搜尋(左閉右開區間) */
int binarySearchLCRO(vector<int> &nums, int target) {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
int i = 0, j = nums.size();
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中
j = m;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Java"
```java title="binary_search.java"
/* 二分搜尋(左閉右開區間) */
int binarySearchLCRO(int[] nums, int target) {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
int i = 0, j = nums.length;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中
j = m;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "C#"
```csharp title="binary_search.cs"
/* 二分搜尋(左閉右開區間) */
int BinarySearchLCRO(int[] nums, int target) {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
int i = 0, j = nums.Length;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中
j = m;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Go"
```go title="binary_search.go"
/* 二分搜尋(左閉右開區間) */
func binarySearchLCRO(nums []int, target int) int {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
i, j := 0, len(nums)
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
for i < j {
m := i + (j-i)/2 // 計算中點索引 m
if nums[m] < target { // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1
} else if nums[m] > target { // 此情況說明 target 在區間 [i, m) 中
j = m
} else { // 找到目標元素,返回其索引
return m
}
}
// 未找到目標元素,返回 -1
return -1
}
```
=== "Swift"
```swift title="binary_search.swift"
/* 二分搜尋(左閉右開區間) */
func binarySearchLCRO(nums: [Int], target: Int) -> Int {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
var i = nums.startIndex
var j = nums.endIndex
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while i < j {
let m = i + (j - i) / 2 // 計算中點索引 m
if nums[m] < target { // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1
} else if nums[m] > target { // 此情況說明 target 在區間 [i, m) 中
j = m
} else { // 找到目標元素,返回其索引
return m
}
}
// 未找到目標元素,返回 -1
return -1
}
```
=== "JS"
```javascript title="binary_search.js"
/* 二分搜尋(左閉右開區間) */
function binarySearchLCRO(nums, target) {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
let i = 0,
j = nums.length;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
// 計算中點索引 m ,使用 parseInt() 向下取整
const m = parseInt(i + (j - i) / 2);
if (nums[m] < target)
// 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
else if (nums[m] > target)
// 此情況說明 target 在區間 [i, m) 中
j = m;
// 找到目標元素,返回其索引
else return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "TS"
```typescript title="binary_search.ts"
/* 二分搜尋(左閉右開區間) */
function binarySearchLCRO(nums: number[], target: number): number {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
let i = 0,
j = nums.length;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
// 計算中點索引 m
const m = Math.floor(i + (j - i) / 2);
if (nums[m] < target) {
// 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
} else if (nums[m] > target) {
// 此情況說明 target 在區間 [i, m) 中
j = m;
} else {
// 找到目標元素,返回其索引
return m;
}
}
return -1; // 未找到目標元素,返回 -1
}
```
=== "Dart"
```dart title="binary_search.dart"
/* 二分搜尋(左閉右開區間) */
int binarySearchLCRO(List<int> nums, int target) {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
int i = 0, j = nums.length;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
int m = i + (j - i) ~/ 2; // 計算中點索引 m
if (nums[m] < target) {
// 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
} else if (nums[m] > target) {
// 此情況說明 target 在區間 [i, m) 中
j = m;
} else {
// 找到目標元素,返回其索引
return m;
}
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Rust"
```rust title="binary_search.rs"
/* 二分搜尋(左閉右開區間) */
fn binary_search_lcro(nums: &[i32], target: i32) -> i32 {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
let mut i = 0;
let mut j = nums.len() as i32;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while i < j {
let m = i + (j - i) / 2; // 計算中點索引 m
if nums[m as usize] < target {
// 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
} else if nums[m as usize] > target {
// 此情況說明 target 在區間 [i, m) 中
j = m;
} else {
// 找到目標元素,返回其索引
return m;
}
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "C"
```c title="binary_search.c"
/* 二分搜尋(左閉右開區間) */
int binarySearchLCRO(int *nums, int len, int target) {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
int i = 0, j = len;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中
j = m;
else // 找到目標元素,返回其索引
return m;
}
// 未找到目標元素,返回 -1
return -1;
}
```
=== "Kotlin"
```kotlin title="binary_search.kt"
/* 二分搜尋(左閉右開區間) */
fun binarySearchLCRO(nums: IntArray, target: Int): Int {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
var i = 0
var j = nums.size
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i < j) {
val m = i + (j - i) / 2 // 計算中點索引 m
if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1
else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中
j = m
else // 找到目標元素,返回其索引
return m
}
// 未找到目標元素,返回 -1
return -1
}
```
=== "Ruby"
```ruby title="binary_search.rb"
[class]{}-[func]{binary_search_lcro}
```
=== "Zig"
```zig title="binary_search.zig"
// 二分搜尋(左閉右開區間)
fn binarySearchLCRO(comptime T: type, nums: std.ArrayList(T), target: T) T {
// 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
var i: usize = 0;
var j: usize = nums.items.len;
// 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
while (i <= j) {
var m = i + (j - i) / 2; // 計算中點索引 m
if (nums.items[m] < target) { // 此情況說明 target 在區間 [m+1, j) 中
i = m + 1;
} else if (nums.items[m] > target) { // 此情況說明 target 在區間 [i, m) 中
j = m;
} else { // 找到目標元素,返回其索引
return @intCast(m);
}
}
// 未找到目標元素,返回 -1
return -1;
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_lcro%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%B7%A6%E9%97%AD%E5%8F%B3%E5%BC%80%E5%8C%BA%E9%97%B4%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A6%E9%97%AD%E5%8F%B3%E5%BC%80%E5%8C%BA%E9%97%B4%20%5B0,%20n%29%20%EF%BC%8C%E5%8D%B3%20i,%20j%20%E5%88%86%E5%88%AB%E6%8C%87%E5%90%91%E6%95%B0%E7%BB%84%E9%A6%96%E5%85%83%E7%B4%A0%E3%80%81%E5%B0%BE%E5%85%83%E7%B4%A0%2B1%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%EF%BC%8C%E5%BD%93%E6%90%9C%E7%B4%A2%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E8%B7%B3%E5%87%BA%EF%BC%88%E5%BD%93%20i%20%3D%20j%20%E6%97%B6%E4%B8%BA%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%29%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m%29%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20return%20-1%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%B7%A6%E9%97%AD%E5%8F%B3%E5%BC%80%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search_lcro%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_lcro%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%B7%A6%E9%97%AD%E5%8F%B3%E5%BC%80%E5%8C%BA%E9%97%B4%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A6%E9%97%AD%E5%8F%B3%E5%BC%80%E5%8C%BA%E9%97%B4%20%5B0,%20n%29%20%EF%BC%8C%E5%8D%B3%20i,%20j%20%E5%88%86%E5%88%AB%E6%8C%87%E5%90%91%E6%95%B0%E7%BB%84%E9%A6%96%E5%85%83%E7%B4%A0%E3%80%81%E5%B0%BE%E5%85%83%E7%B4%A0%2B1%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%EF%BC%8C%E5%BD%93%E6%90%9C%E7%B4%A2%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E8%B7%B3%E5%87%BA%EF%BC%88%E5%BD%93%20i%20%3D%20j%20%E6%97%B6%E4%B8%BA%E7%A9%BA%EF%BC%89%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%29%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20%20%23%20%E6%AD%A4%E6%83%85%E5%86%B5%E8%AF%B4%E6%98%8E%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m%29%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20return%20-1%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%B7%A6%E9%97%AD%E5%8F%B3%E5%BC%80%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search_lcro%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
如圖 10-3 所示,在兩種區間表示下,二分搜尋演算法的初始化、迴圈條件和縮小區間操作皆有所不同。
由於“雙閉區間”表示中的左右邊界都被定義為閉區間,因此透過指標 $i$ 和指標 $j$ 縮小區間的操作也是對稱的。這樣更不容易出錯,**因此一般建議採用“雙閉區間”的寫法**。
![兩種區間定義](binary_search.assets/binary_search_ranges.png){ class="animation-figure" }
<p align="center"> 圖 10-3 &nbsp; 兩種區間定義 </p>
## 10.1.2 &nbsp; 優點與侷限性
二分搜尋在時間和空間方面都有較好的效能。
- 二分搜尋的時間效率高。在大資料量下,對數階的時間複雜度具有顯著優勢。例如,當資料大小 $n = 2^{20}$ 時,線性查詢需要 $2^{20} = 1048576$ 輪迴圈,而二分搜尋僅需 $\log_2 2^{20} = 20$ 輪迴圈。
- 二分搜尋無須額外空間。相較於需要藉助額外空間的搜尋演算法(例如雜湊查詢),二分搜尋更加節省空間。
然而,二分搜尋並非適用於所有情況,主要有以下原因。
- 二分搜尋僅適用於有序資料。若輸入資料無序,為了使用二分搜尋而專門進行排序,得不償失。因為排序演算法的時間複雜度通常為 $O(n \log n)$ ,比線性查詢和二分搜尋都更高。對於頻繁插入元素的場景,為保持陣列有序性,需要將元素插入到特定位置,時間複雜度為 $O(n)$ ,也是非常昂貴的。
- 二分搜尋僅適用於陣列。二分搜尋需要跳躍式(非連續地)訪問元素,而在鏈結串列中執行跳躍式訪問的效率較低,因此不適合應用在鏈結串列或基於鏈結串列實現的資料結構。
- 小資料量下,線性查詢效能更佳。線上性查詢中,每輪只需 1 次判斷操作;而在二分搜尋中,需要 1 次加法、1 次除法、1 ~ 3 次判斷操作、1 次加法(減法),共 4 ~ 6 個單元操作;因此,當資料量 $n$ 較小時,線性查詢反而比二分搜尋更快。
@@ -0,0 +1,494 @@
---
comments: true
---
# 10.3 &nbsp; 二分搜尋邊界
## 10.3.1 &nbsp; 查詢左邊界
!!! question
給定一個長度為 $n$ 的有序陣列 `nums` ,其中可能包含重複元素。請返回陣列中最左一個元素 `target` 的索引。若陣列中不包含該元素,則返回 $-1$ 。
回憶二分搜尋插入點的方法,搜尋完成後 $i$ 指向最左一個 `target` ,**因此查詢插入點本質上是在查詢最左一個 `target` 的索引**。
考慮透過查詢插入點的函式實現查詢左邊界。請注意,陣列中可能不包含 `target` ,這種情況可能導致以下兩種結果。
- 插入點的索引 $i$ 越界。
- 元素 `nums[i]``target` 不相等。
當遇到以上兩種情況時,直接返回 $-1$ 即可。程式碼如下所示:
=== "Python"
```python title="binary_search_edge.py"
def binary_search_left_edge(nums: list[int], target: int) -> int:
"""二分搜尋最左一個 target"""
# 等價於查詢 target 的插入點
i = binary_search_insertion(nums, target)
# 未找到 target ,返回 -1
if i == len(nums) or nums[i] != target:
return -1
# 找到 target ,返回索引 i
return i
```
=== "C++"
```cpp title="binary_search_edge.cpp"
/* 二分搜尋最左一個 target */
int binarySearchLeftEdge(vector<int> &nums, int target) {
// 等價於查詢 target 的插入點
int i = binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i == nums.size() || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
```
=== "Java"
```java title="binary_search_edge.java"
/* 二分搜尋最左一個 target */
int binarySearchLeftEdge(int[] nums, int target) {
// 等價於查詢 target 的插入點
int i = binary_search_insertion.binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i == nums.length || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
```
=== "C#"
```csharp title="binary_search_edge.cs"
/* 二分搜尋最左一個 target */
int BinarySearchLeftEdge(int[] nums, int target) {
// 等價於查詢 target 的插入點
int i = binary_search_insertion.BinarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i == nums.Length || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
```
=== "Go"
```go title="binary_search_edge.go"
/* 二分搜尋最左一個 target */
func binarySearchLeftEdge(nums []int, target int) int {
// 等價於查詢 target 的插入點
i := binarySearchInsertion(nums, target)
// 未找到 target ,返回 -1
if i == len(nums) || nums[i] != target {
return -1
}
// 找到 target ,返回索引 i
return i
}
```
=== "Swift"
```swift title="binary_search_edge.swift"
/* 二分搜尋最左一個 target */
func binarySearchLeftEdge(nums: [Int], target: Int) -> Int {
// 等價於查詢 target 的插入點
let i = binarySearchInsertion(nums: nums, target: target)
// 未找到 target ,返回 -1
if i == nums.endIndex || nums[i] != target {
return -1
}
// 找到 target ,返回索引 i
return i
}
```
=== "JS"
```javascript title="binary_search_edge.js"
/* 二分搜尋最左一個 target */
function binarySearchLeftEdge(nums, target) {
// 等價於查詢 target 的插入點
const i = binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i === nums.length || nums[i] !== target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
```
=== "TS"
```typescript title="binary_search_edge.ts"
/* 二分搜尋最左一個 target */
function binarySearchLeftEdge(nums: Array<number>, target: number): number {
// 等價於查詢 target 的插入點
const i = binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i === nums.length || nums[i] !== target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
```
=== "Dart"
```dart title="binary_search_edge.dart"
/* 二分搜尋最左一個 target */
int binarySearchLeftEdge(List<int> nums, int target) {
// 等價於查詢 target 的插入點
int i = binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i == nums.length || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
```
=== "Rust"
```rust title="binary_search_edge.rs"
/* 二分搜尋最左一個 target */
fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {
// 等價於查詢 target 的插入點
let i = binary_search_insertion(nums, target);
// 未找到 target ,返回 -1
if i == nums.len() as i32 || nums[i as usize] != target {
return -1;
}
// 找到 target ,返回索引 i
i
}
```
=== "C"
```c title="binary_search_edge.c"
/* 二分搜尋最左一個 target */
int binarySearchLeftEdge(int *nums, int numSize, int target) {
// 等價於查詢 target 的插入點
int i = binarySearchInsertion(nums, numSize, target);
// 未找到 target ,返回 -1
if (i == numSize || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
```
=== "Kotlin"
```kotlin title="binary_search_edge.kt"
/* 二分搜尋最左一個 target */
fun binarySearchLeftEdge(nums: IntArray, target: Int): Int {
// 等價於查詢 target 的插入點
val i = binarySearchInsertion(nums, target)
// 未找到 target ,返回 -1
if (i == nums.size || nums[i] != target) {
return -1
}
// 找到 target ,返回索引 i
return i
}
```
=== "Ruby"
```ruby title="binary_search_edge.rb"
[class]{}-[func]{binary_search_left_edge}
```
=== "Zig"
```zig title="binary_search_edge.zig"
[class]{}-[func]{binarySearchLeftEdge}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_left_edge%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%9C%80%E5%B7%A6%E4%B8%80%E4%B8%AA%20target%22%22%22%0A%20%20%20%20%23%20%E7%AD%89%E4%BB%B7%E4%BA%8E%E6%9F%A5%E6%89%BE%20target%20%E7%9A%84%E6%8F%92%E5%85%A5%E7%82%B9%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums,%20target%29%0A%20%20%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3D%3D%20len%28nums%29%20or%20nums%5Bi%5D%20!%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E7%B4%A2%E5%BC%95%20i%0A%20%20%20%20return%20i%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%8C%85%E5%90%AB%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%206,%206,%206,%206,%2010,%2012,%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E5%B7%A6%E8%BE%B9%E7%95%8C%E5%92%8C%E5%8F%B3%E8%BE%B9%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_left_edge%28nums,%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%B7%A6%E4%B8%80%E4%B8%AA%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_left_edge%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%9C%80%E5%B7%A6%E4%B8%80%E4%B8%AA%20target%22%22%22%0A%20%20%20%20%23%20%E7%AD%89%E4%BB%B7%E4%BA%8E%E6%9F%A5%E6%89%BE%20target%20%E7%9A%84%E6%8F%92%E5%85%A5%E7%82%B9%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums,%20target%29%0A%20%20%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3D%3D%20len%28nums%29%20or%20nums%5Bi%5D%20!%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E7%B4%A2%E5%BC%95%20i%0A%20%20%20%20return%20i%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%8C%85%E5%90%AB%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%206,%206,%206,%206,%2010,%2012,%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E5%B7%A6%E8%BE%B9%E7%95%8C%E5%92%8C%E5%8F%B3%E8%BE%B9%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_left_edge%28nums,%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%B7%A6%E4%B8%80%E4%B8%AA%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 10.3.2 &nbsp; 查詢右邊界
那麼如何查詢最右一個 `target` 呢?最直接的方式是修改程式碼,替換在 `nums[m] == target` 情況下的指標收縮操作。程式碼在此省略,有興趣的讀者可以自行實現。
下面我們介紹兩種更加取巧的方法。
### 1. &nbsp; 複用查詢左邊界
實際上,我們可以利用查詢最左元素的函式來查詢最右元素,具體方法為:**將查詢最右一個 `target` 轉化為查詢最左一個 `target + 1`**。
如圖 10-7 所示,查詢完成後,指標 $i$ 指向最左一個 `target + 1`(如果存在),而 $j$ 指向最右一個 `target` **因此返回 $j$ 即可**。
![將查詢右邊界轉化為查詢左邊界](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" }
<p align="center"> 圖 10-7 &nbsp; 將查詢右邊界轉化為查詢左邊界 </p>
請注意,返回的插入點是 $i$ ,因此需要將其減 $1$ ,從而獲得 $j$ :
=== "Python"
```python title="binary_search_edge.py"
def binary_search_right_edge(nums: list[int], target: int) -> int:
"""二分搜尋最右一個 target"""
# 轉化為查詢最左一個 target + 1
i = binary_search_insertion(nums, target + 1)
# j 指向最右一個 target ,i 指向首個大於 target 的元素
j = i - 1
# 未找到 target ,返回 -1
if j == -1 or nums[j] != target:
return -1
# 找到 target ,返回索引 j
return j
```
=== "C++"
```cpp title="binary_search_edge.cpp"
/* 二分搜尋最右一個 target */
int binarySearchRightEdge(vector<int> &nums, int target) {
// 轉化為查詢最左一個 target + 1
int i = binarySearchInsertion(nums, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
```
=== "Java"
```java title="binary_search_edge.java"
/* 二分搜尋最右一個 target */
int binarySearchRightEdge(int[] nums, int target) {
// 轉化為查詢最左一個 target + 1
int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
```
=== "C#"
```csharp title="binary_search_edge.cs"
/* 二分搜尋最右一個 target */
int BinarySearchRightEdge(int[] nums, int target) {
// 轉化為查詢最左一個 target + 1
int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
```
=== "Go"
```go title="binary_search_edge.go"
/* 二分搜尋最右一個 target */
func binarySearchRightEdge(nums []int, target int) int {
// 轉化為查詢最左一個 target + 1
i := binarySearchInsertion(nums, target+1)
// j 指向最右一個 target ,i 指向首個大於 target 的元素
j := i - 1
// 未找到 target ,返回 -1
if j == -1 || nums[j] != target {
return -1
}
// 找到 target ,返回索引 j
return j
}
```
=== "Swift"
```swift title="binary_search_edge.swift"
/* 二分搜尋最右一個 target */
func binarySearchRightEdge(nums: [Int], target: Int) -> Int {
// 轉化為查詢最左一個 target + 1
let i = binarySearchInsertion(nums: nums, target: target + 1)
// j 指向最右一個 target ,i 指向首個大於 target 的元素
let j = i - 1
// 未找到 target ,返回 -1
if j == -1 || nums[j] != target {
return -1
}
// 找到 target ,返回索引 j
return j
}
```
=== "JS"
```javascript title="binary_search_edge.js"
/* 二分搜尋最右一個 target */
function binarySearchRightEdge(nums, target) {
// 轉化為查詢最左一個 target + 1
const i = binarySearchInsertion(nums, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
const j = i - 1;
// 未找到 target ,返回 -1
if (j === -1 || nums[j] !== target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
```
=== "TS"
```typescript title="binary_search_edge.ts"
/* 二分搜尋最右一個 target */
function binarySearchRightEdge(nums: Array<number>, target: number): number {
// 轉化為查詢最左一個 target + 1
const i = binarySearchInsertion(nums, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
const j = i - 1;
// 未找到 target ,返回 -1
if (j === -1 || nums[j] !== target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
```
=== "Dart"
```dart title="binary_search_edge.dart"
/* 二分搜尋最右一個 target */
int binarySearchRightEdge(List<int> nums, int target) {
// 轉化為查詢最左一個 target + 1
int i = binarySearchInsertion(nums, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
```
=== "Rust"
```rust title="binary_search_edge.rs"
/* 二分搜尋最右一個 target */
fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {
// 轉化為查詢最左一個 target + 1
let i = binary_search_insertion(nums, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
let j = i - 1;
// 未找到 target ,返回 -1
if j == -1 || nums[j as usize] != target {
return -1;
}
// 找到 target ,返回索引 j
j
}
```
=== "C"
```c title="binary_search_edge.c"
/* 二分搜尋最右一個 target */
int binarySearchRightEdge(int *nums, int numSize, int target) {
// 轉化為查詢最左一個 target + 1
int i = binarySearchInsertion(nums, numSize, target + 1);
// j 指向最右一個 target ,i 指向首個大於 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
```
=== "Kotlin"
```kotlin title="binary_search_edge.kt"
/* 二分搜尋最右一個 target */
fun binarySearchRightEdge(nums: IntArray, target: Int): Int {
// 轉化為查詢最左一個 target + 1
val i = binarySearchInsertion(nums, target + 1)
// j 指向最右一個 target ,i 指向首個大於 target 的元素
val j = i - 1
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1
}
// 找到 target ,返回索引 j
return j
}
```
=== "Ruby"
```ruby title="binary_search_edge.rb"
[class]{}-[func]{binary_search_right_edge}
```
=== "Zig"
```zig title="binary_search_edge.zig"
[class]{}-[func]{binarySearchRightEdge}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_right_edge%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%9C%80%E5%8F%B3%E4%B8%80%E4%B8%AA%20target%22%22%22%0A%20%20%20%20%23%20%E8%BD%AC%E5%8C%96%E4%B8%BA%E6%9F%A5%E6%89%BE%E6%9C%80%E5%B7%A6%E4%B8%80%E4%B8%AA%20target%20%2B%201%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums,%20target%20%2B%201%29%0A%20%20%20%20%23%20j%20%E6%8C%87%E5%90%91%E6%9C%80%E5%8F%B3%E4%B8%80%E4%B8%AA%20target%20%EF%BC%8Ci%20%E6%8C%87%E5%90%91%E9%A6%96%E4%B8%AA%E5%A4%A7%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20j%20%3D%3D%20-1%20or%20nums%5Bj%5D%20!%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E7%B4%A2%E5%BC%95%20j%0A%20%20%20%20return%20j%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%8C%85%E5%90%AB%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%206,%206,%206,%206,%2010,%2012,%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E5%B7%A6%E8%BE%B9%E7%95%8C%E5%92%8C%E5%8F%B3%E8%BE%B9%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_right_edge%28nums,%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%8F%B3%E4%B8%80%E4%B8%AA%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_right_edge%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%9C%80%E5%8F%B3%E4%B8%80%E4%B8%AA%20target%22%22%22%0A%20%20%20%20%23%20%E8%BD%AC%E5%8C%96%E4%B8%BA%E6%9F%A5%E6%89%BE%E6%9C%80%E5%B7%A6%E4%B8%80%E4%B8%AA%20target%20%2B%201%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums,%20target%20%2B%201%29%0A%20%20%20%20%23%20j%20%E6%8C%87%E5%90%91%E6%9C%80%E5%8F%B3%E4%B8%80%E4%B8%AA%20target%20%EF%BC%8Ci%20%E6%8C%87%E5%90%91%E9%A6%96%E4%B8%AA%E5%A4%A7%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20j%20%3D%3D%20-1%20or%20nums%5Bj%5D%20!%3D%20target%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E7%B4%A2%E5%BC%95%20j%0A%20%20%20%20return%20j%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%8C%85%E5%90%AB%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%206,%206,%206,%206,%2010,%2012,%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E5%B7%A6%E8%BE%B9%E7%95%8C%E5%92%8C%E5%8F%B3%E8%BE%B9%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_right_edge%28nums,%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%8F%B3%E4%B8%80%E4%B8%AA%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
### 2. &nbsp; 轉化為查詢元素
我們知道,當陣列不包含 `target` 時,最終 $i$ 和 $j$ 會分別指向首個大於、小於 `target` 的元素。
因此,如圖 10-8 所示,我們可以構造一個陣列中不存在的元素,用於查詢左右邊界。
- 查詢最左一個 `target` :可以轉化為查詢 `target - 0.5` ,並返回指標 $i$ 。
- 查詢最右一個 `target` :可以轉化為查詢 `target + 0.5` ,並返回指標 $j$ 。
![將查詢邊界轉化為查詢元素](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" }
<p align="center"> 圖 10-8 &nbsp; 將查詢邊界轉化為查詢元素 </p>
程式碼在此省略,以下兩點值得注意。
- 給定陣列不包含小數,這意味著我們無須關心如何處理相等的情況。
- 因為該方法引入了小數,所以需要將函式中的變數 `target` 改為浮點數型別(Python 無須改動)。
@@ -0,0 +1,648 @@
---
comments: true
---
# 10.2 &nbsp; 二分搜尋插入點
二分搜尋不僅可用於搜尋目標元素,還可用於解決許多變種問題,比如搜尋目標元素的插入位置。
## 10.2.1 &nbsp; 無重複元素的情況
!!! question
給定一個長度為 $n$ 的有序陣列 `nums` 和一個元素 `target` ,陣列不存在重複元素。現將 `target` 插入陣列 `nums` 中,並保持其有序性。若陣列中已存在元素 `target` ,則插入到其左方。請返回插入後 `target` 在陣列中的索引。示例如圖 10-4 所示。
![二分搜尋插入點示例資料](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" }
<p align="center"> 圖 10-4 &nbsp; 二分搜尋插入點示例資料 </p>
如果想複用上一節的二分搜尋程式碼,則需要回答以下兩個問題。
**問題一**:當陣列中包含 `target` 時,插入點的索引是否是該元素的索引?
題目要求將 `target` 插入到相等元素的左邊,這意味著新插入的 `target` 替換了原來 `target` 的位置。也就是說,**當陣列包含 `target` 時,插入點的索引就是該 `target` 的索引**。
**問題二**:當陣列中不存在 `target` 時,插入點是哪個元素的索引?
進一步思考二分搜尋過程:當 `nums[m] < target` 時 $i$ 移動,這意味著指標 $i$ 在向大於等於 `target` 的元素靠近。同理,指標 $j$ 始終在向小於等於 `target` 的元素靠近。
因此二分結束時一定有:$i$ 指向首個大於 `target` 的元素,$j$ 指向首個小於 `target` 的元素。**易得當陣列不包含 `target` 時,插入索引為 $i$** 。程式碼如下所示:
=== "Python"
```python title="binary_search_insertion.py"
def binary_search_insertion_simple(nums: list[int], target: int) -> int:
"""二分搜尋插入點(無重複元素)"""
i, j = 0, len(nums) - 1 # 初始化雙閉區間 [0, n-1]
while i <= j:
m = (i + j) // 2 # 計算中點索引 m
if nums[m] < target:
i = m + 1 # target 在區間 [m+1, j] 中
elif nums[m] > target:
j = m - 1 # target 在區間 [i, m-1] 中
else:
return m # 找到 target ,返回插入點 m
# 未找到 target ,返回插入點 i
return i
```
=== "C++"
```cpp title="binary_search_insertion.cpp"
/* 二分搜尋插入點(無重複元素) */
int binarySearchInsertionSimple(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i;
}
```
=== "Java"
```java title="binary_search_insertion.java"
/* 二分搜尋插入點(無重複元素) */
int binarySearchInsertionSimple(int[] nums, int target) {
int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i;
}
```
=== "C#"
```csharp title="binary_search_insertion.cs"
/* 二分搜尋插入點(無重複元素) */
int BinarySearchInsertionSimple(int[] nums, int target) {
int i = 0, j = nums.Length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i;
}
```
=== "Go"
```go title="binary_search_insertion.go"
/* 二分搜尋插入點(無重複元素) */
func binarySearchInsertionSimple(nums []int, target int) int {
// 初始化雙閉區間 [0, n-1]
i, j := 0, len(nums)-1
for i <= j {
// 計算中點索引 m
m := i + (j-i)/2
if nums[m] < target {
// target 在區間 [m+1, j] 中
i = m + 1
} else if nums[m] > target {
// target 在區間 [i, m-1] 中
j = m - 1
} else {
// 找到 target ,返回插入點 m
return m
}
}
// 未找到 target ,返回插入點 i
return i
}
```
=== "Swift"
```swift title="binary_search_insertion.swift"
/* 二分搜尋插入點(無重複元素) */
func binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {
// 初始化雙閉區間 [0, n-1]
var i = nums.startIndex
var j = nums.endIndex - 1
while i <= j {
let m = i + (j - i) / 2 // 計算中點索引 m
if nums[m] < target {
i = m + 1 // target 在區間 [m+1, j] 中
} else if nums[m] > target {
j = m - 1 // target 在區間 [i, m-1] 中
} else {
return m // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i
}
```
=== "JS"
```javascript title="binary_search_insertion.js"
/* 二分搜尋插入點(無重複元素) */
function binarySearchInsertionSimple(nums, target) {
let i = 0,
j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i;
}
```
=== "TS"
```typescript title="binary_search_insertion.ts"
/* 二分搜尋插入點(無重複元素) */
function binarySearchInsertionSimple(
nums: Array<number>,
target: number
): number {
let i = 0,
j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i;
}
```
=== "Dart"
```dart title="binary_search_insertion.dart"
/* 二分搜尋插入點(無重複元素) */
int binarySearchInsertionSimple(List<int> nums, int target) {
int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) ~/ 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i;
}
```
=== "Rust"
```rust title="binary_search_insertion.rs"
/* 二分搜尋插入點(無重複元素) */
fn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {
let (mut i, mut j) = (0, nums.len() as i32 - 1); // 初始化雙閉區間 [0, n-1]
while i <= j {
let m = i + (j - i) / 2; // 計算中點索引 m
if nums[m as usize] < target {
i = m + 1; // target 在區間 [m+1, j] 中
} else if nums[m as usize] > target {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m;
}
}
// 未找到 target ,返回插入點 i
i
}
```
=== "C"
```c title="binary_search_insertion.c"
/* 二分搜尋插入點(無重複元素) */
int binarySearchInsertionSimple(int *nums, int numSize, int target) {
int i = 0, j = numSize - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i;
}
```
=== "Kotlin"
```kotlin title="binary_search_insertion.kt"
/* 二分搜尋插入點(無重複元素) */
fun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {
var i = 0
var j = nums.size - 1 // 初始化雙閉區間 [0, n-1]
while (i <= j) {
val m = i + (j - i) / 2 // 計算中點索引 m
if (nums[m] < target) {
i = m + 1 // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1 // target 在區間 [i, m-1] 中
} else {
return m // 找到 target ,返回插入點 m
}
}
// 未找到 target ,返回插入點 i
return i
}
```
=== "Ruby"
```ruby title="binary_search_insertion.rb"
[class]{}-[func]{binary_search_insertion_simple}
```
=== "Zig"
```zig title="binary_search_insertion.zig"
[class]{}-[func]{binarySearchInsertionSimple}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion_simple%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E6%97%A0%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20m%0A%20%20%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E6%97%A0%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion_simple%28nums,%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion_simple%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E6%97%A0%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20m%0A%20%20%20%20%23%20%E6%9C%AA%E6%89%BE%E5%88%B0%20target%20%EF%BC%8C%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E6%97%A0%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion_simple%28nums,%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 10.2.2 &nbsp; 存在重複元素的情況
!!! question
在上一題的基礎上,規定陣列可能包含重複元素,其餘不變。
假設陣列中存在多個 `target` ,則普通二分搜尋只能返回其中一個 `target` 的索引,**而無法確定該元素的左邊和右邊還有多少 `target`**。
題目要求將目標元素插入到最左邊,**所以我們需要查詢陣列中最左一個 `target` 的索引**。初步考慮透過圖 10-5 所示的步驟實現。
1. 執行二分搜尋,得到任意一個 `target` 的索引,記為 $k$ 。
2. 從索引 $k$ 開始,向左進行線性走訪,當找到最左邊的 `target` 時返回。
![線性查詢重複元素的插入點](binary_search_insertion.assets/binary_search_insertion_naive.png){ class="animation-figure" }
<p align="center"> 圖 10-5 &nbsp; 線性查詢重複元素的插入點 </p>
此方法雖然可用,但其包含線性查詢,因此時間複雜度為 $O(n)$ 。當陣列中存在很多重複的 `target` 時,該方法效率很低。
現考慮拓展二分搜尋程式碼。如圖 10-6 所示,整體流程保持不變,每輪先計算中點索引 $m$ ,再判斷 `target``nums[m]` 的大小關係,分為以下幾種情況。
- 當 `nums[m] < target``nums[m] > target` 時,說明還沒有找到 `target` ,因此採用普通二分搜尋的縮小區間操作,**從而使指標 $i$ 和 $j$ 向 `target` 靠近**。
- 當 `nums[m] == target` 時,說明小於 `target` 的元素在區間 $[i, m - 1]$ 中,因此採用 $j = m - 1$ 來縮小區間,**從而使指標 $j$ 向小於 `target` 的元素靠近**。
迴圈完成後,$i$ 指向最左邊的 `target` $j$ 指向首個小於 `target` 的元素,**因此索引 $i$ 就是插入點**。
=== "<1>"
![二分搜尋重複元素的插入點的步驟](binary_search_insertion.assets/binary_search_insertion_step1.png){ class="animation-figure" }
=== "<2>"
![binary_search_insertion_step2](binary_search_insertion.assets/binary_search_insertion_step2.png){ class="animation-figure" }
=== "<3>"
![binary_search_insertion_step3](binary_search_insertion.assets/binary_search_insertion_step3.png){ class="animation-figure" }
=== "<4>"
![binary_search_insertion_step4](binary_search_insertion.assets/binary_search_insertion_step4.png){ class="animation-figure" }
=== "<5>"
![binary_search_insertion_step5](binary_search_insertion.assets/binary_search_insertion_step5.png){ class="animation-figure" }
=== "<6>"
![binary_search_insertion_step6](binary_search_insertion.assets/binary_search_insertion_step6.png){ class="animation-figure" }
=== "<7>"
![binary_search_insertion_step7](binary_search_insertion.assets/binary_search_insertion_step7.png){ class="animation-figure" }
=== "<8>"
![binary_search_insertion_step8](binary_search_insertion.assets/binary_search_insertion_step8.png){ class="animation-figure" }
<p align="center"> 圖 10-6 &nbsp; 二分搜尋重複元素的插入點的步驟 </p>
觀察以下程式碼,判斷分支 `nums[m] > target``nums[m] == target` 的操作相同,因此兩者可以合併。
即便如此,我們仍然可以將判斷條件保持展開,因為其邏輯更加清晰、可讀性更好。
=== "Python"
```python title="binary_search_insertion.py"
def binary_search_insertion(nums: list[int], target: int) -> int:
"""二分搜尋插入點(存在重複元素)"""
i, j = 0, len(nums) - 1 # 初始化雙閉區間 [0, n-1]
while i <= j:
m = (i + j) // 2 # 計算中點索引 m
if nums[m] < target:
i = m + 1 # target 在區間 [m+1, j] 中
elif nums[m] > target:
j = m - 1 # target 在區間 [i, m-1] 中
else:
j = m - 1 # 首個小於 target 的元素在區間 [i, m-1] 中
# 返回插入點 i
return i
```
=== "C++"
```cpp title="binary_search_insertion.cpp"
/* 二分搜尋插入點(存在重複元素) */
int binarySearchInsertion(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i;
}
```
=== "Java"
```java title="binary_search_insertion.java"
/* 二分搜尋插入點(存在重複元素) */
int binarySearchInsertion(int[] nums, int target) {
int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i;
}
```
=== "C#"
```csharp title="binary_search_insertion.cs"
/* 二分搜尋插入點(存在重複元素) */
int BinarySearchInsertion(int[] nums, int target) {
int i = 0, j = nums.Length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i;
}
```
=== "Go"
```go title="binary_search_insertion.go"
/* 二分搜尋插入點(存在重複元素) */
func binarySearchInsertion(nums []int, target int) int {
// 初始化雙閉區間 [0, n-1]
i, j := 0, len(nums)-1
for i <= j {
// 計算中點索引 m
m := i + (j-i)/2
if nums[m] < target {
// target 在區間 [m+1, j] 中
i = m + 1
} else if nums[m] > target {
// target 在區間 [i, m-1] 中
j = m - 1
} else {
// 首個小於 target 的元素在區間 [i, m-1] 中
j = m - 1
}
}
// 返回插入點 i
return i
}
```
=== "Swift"
```swift title="binary_search_insertion.swift"
/* 二分搜尋插入點(存在重複元素) */
func binarySearchInsertion(nums: [Int], target: Int) -> Int {
// 初始化雙閉區間 [0, n-1]
var i = nums.startIndex
var j = nums.endIndex - 1
while i <= j {
let m = i + (j - i) / 2 // 計算中點索引 m
if nums[m] < target {
i = m + 1 // target 在區間 [m+1, j] 中
} else if nums[m] > target {
j = m - 1 // target 在區間 [i, m-1] 中
} else {
j = m - 1 // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i
}
```
=== "JS"
```javascript title="binary_search_insertion.js"
/* 二分搜尋插入點(存在重複元素) */
function binarySearchInsertion(nums, target) {
let i = 0,
j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i;
}
```
=== "TS"
```typescript title="binary_search_insertion.ts"
/* 二分搜尋插入點(存在重複元素) */
function binarySearchInsertion(nums: Array<number>, target: number): number {
let i = 0,
j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i;
}
```
=== "Dart"
```dart title="binary_search_insertion.dart"
/* 二分搜尋插入點(存在重複元素) */
int binarySearchInsertion(List<int> nums, int target) {
int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) ~/ 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i;
}
```
=== "Rust"
```rust title="binary_search_insertion.rs"
/* 二分搜尋插入點(存在重複元素) */
pub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {
let (mut i, mut j) = (0, nums.len() as i32 - 1); // 初始化雙閉區間 [0, n-1]
while i <= j {
let m = i + (j - i) / 2; // 計算中點索引 m
if nums[m as usize] < target {
i = m + 1; // target 在區間 [m+1, j] 中
} else if nums[m as usize] > target {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
i
}
```
=== "C"
```c title="binary_search_insertion.c"
/* 二分搜尋插入點(存在重複元素) */
int binarySearchInsertion(int *nums, int numSize, int target) {
int i = 0, j = numSize - 1; // 初始化雙閉區間 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 計算中點索引 m
if (nums[m] < target) {
i = m + 1; // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在區間 [i, m-1] 中
} else {
j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i;
}
```
=== "Kotlin"
```kotlin title="binary_search_insertion.kt"
/* 二分搜尋插入點(存在重複元素) */
fun binarySearchInsertion(nums: IntArray, target: Int): Int {
var i = 0
var j = nums.size - 1 // 初始化雙閉區間 [0, n-1]
while (i <= j) {
val m = i + (j - i) / 2 // 計算中點索引 m
if (nums[m] < target) {
i = m + 1 // target 在區間 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1 // target 在區間 [i, m-1] 中
} else {
j = m - 1 // 首個小於 target 的元素在區間 [i, m-1] 中
}
}
// 返回插入點 i
return i
}
```
=== "Ruby"
```ruby title="binary_search_insertion.rb"
[class]{}-[func]{binary_search_insertion}
```
=== "Zig"
```zig title="binary_search_insertion.zig"
[class]{}-[func]{binarySearchInsertion}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%8C%85%E5%90%AB%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%206,%206,%206,%206,%2010,%2012,%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion%28nums,%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i,%20j%20%3D%200,%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%20%5B0,%20n-1%5D%0A%20%20%20%20while%20i%20%3C%3D%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bm%2B1,%20j%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20target%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20-%201%20%20%23%20%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5Bi,%20m-1%5D%20%E4%B8%AD%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%8F%92%E5%85%A5%E7%82%B9%20i%0A%20%20%20%20return%20i%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%8C%85%E5%90%AB%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%206,%206,%206,%206,%2010,%2012,%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%8F%92%E5%85%A5%E7%82%B9%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion%28nums,%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%E4%B8%BA%20%7Bindex%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
!!! tip
本節的程式碼都是“雙閉區間”寫法。有興趣的讀者可以自行實現“左閉右開”寫法。
總的來看,二分搜尋無非就是給指標 $i$ 和 $j$ 分別設定搜尋目標,目標可能是一個具體的元素(例如 `target` ),也可能是一個元素範圍(例如小於 `target` 的元素)。
在不斷的迴圈二分中,指標 $i$ 和 $j$ 都逐漸逼近預先設定的目標。最終,它們或是成功找到答案,或是越過邊界後停止。
+23
View File
@@ -0,0 +1,23 @@
---
comments: true
icon: material/text-search
---
# 第 10 章 &nbsp; 搜尋
![搜尋](../assets/covers/chapter_searching.jpg){ class="cover-image" }
!!! abstract
搜尋是一場未知的冒險,我們或許需要走遍神秘空間的每個角落,又或許可以快速鎖定目標。
在這場尋覓之旅中,每一次探索都可能得到一個未曾料想的答案。
## Chapter Contents
- [10.1 &nbsp; 二分搜尋](https://www.hello-algo.com/en/chapter_searching/binary_search/)
- [10.2 &nbsp; 二分搜尋插入點](https://www.hello-algo.com/en/chapter_searching/binary_search_insertion/)
- [10.3 &nbsp; 二分搜尋邊界](https://www.hello-algo.com/en/chapter_searching/binary_search_edge/)
- [10.4 &nbsp; 雜湊最佳化策略](https://www.hello-algo.com/en/chapter_searching/replace_linear_by_hashing/)
- [10.5 &nbsp; 重識搜尋演算法](https://www.hello-algo.com/en/chapter_searching/searching_algorithm_revisited/)
- [10.6 &nbsp; 小結](https://www.hello-algo.com/en/chapter_searching/summary/)
+565
View File
@@ -0,0 +1,565 @@
---
comments: true
---
# 10.4 &nbsp; 雜湊最佳化策略
在演算法題中,**我們常透過將線性查詢替換為雜湊查詢來降低演算法的時間複雜度**。我們藉助一個演算法題來加深理解。
!!! question
給定一個整數陣列 `nums` 和一個目標元素 `target` ,請在陣列中搜索“和”為 `target` 的兩個元素,並返回它們的陣列索引。返回任意一個解即可。
## 10.4.1 &nbsp; 線性查詢:以時間換空間
考慮直接走訪所有可能的組合。如圖 10-9 所示,我們開啟一個兩層迴圈,在每輪中判斷兩個整數的和是否為 `target` ,若是,則返回它們的索引。
![線性查詢求解兩數之和](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" }
<p align="center"> 圖 10-9 &nbsp; 線性查詢求解兩數之和 </p>
程式碼如下所示:
=== "Python"
```python title="two_sum.py"
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:
"""方法一:暴力列舉"""
# 兩層迴圈,時間複雜度為 O(n^2)
for i in range(len(nums) - 1):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
```
=== "C++"
```cpp title="two_sum.cpp"
/* 方法一:暴力列舉 */
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
int size = nums.size();
// 兩層迴圈,時間複雜度為 O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return {i, j};
}
}
return {};
}
```
=== "Java"
```java title="two_sum.java"
/* 方法一:暴力列舉 */
int[] twoSumBruteForce(int[] nums, int target) {
int size = nums.length;
// 兩層迴圈,時間複雜度為 O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return new int[] { i, j };
}
}
return new int[0];
}
```
=== "C#"
```csharp title="two_sum.cs"
/* 方法一:暴力列舉 */
int[] TwoSumBruteForce(int[] nums, int target) {
int size = nums.Length;
// 兩層迴圈,時間複雜度為 O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return [i, j];
}
}
return [];
}
```
=== "Go"
```go title="two_sum.go"
/* 方法一:暴力列舉 */
func twoSumBruteForce(nums []int, target int) []int {
size := len(nums)
// 兩層迴圈,時間複雜度為 O(n^2)
for i := 0; i < size-1; i++ {
for j := i + 1; i < size; j++ {
if nums[i]+nums[j] == target {
return []int{i, j}
}
}
}
return nil
}
```
=== "Swift"
```swift title="two_sum.swift"
/* 方法一:暴力列舉 */
func twoSumBruteForce(nums: [Int], target: Int) -> [Int] {
// 兩層迴圈,時間複雜度為 O(n^2)
for i in nums.indices.dropLast() {
for j in nums.indices.dropFirst(i + 1) {
if nums[i] + nums[j] == target {
return [i, j]
}
}
}
return [0]
}
```
=== "JS"
```javascript title="two_sum.js"
/* 方法一:暴力列舉 */
function twoSumBruteForce(nums, target) {
const n = nums.length;
// 兩層迴圈,時間複雜度為 O(n^2)
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (nums[i] + nums[j] === target) {
return [i, j];
}
}
}
return [];
}
```
=== "TS"
```typescript title="two_sum.ts"
/* 方法一:暴力列舉 */
function twoSumBruteForce(nums: number[], target: number): number[] {
const n = nums.length;
// 兩層迴圈,時間複雜度為 O(n^2)
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (nums[i] + nums[j] === target) {
return [i, j];
}
}
}
return [];
}
```
=== "Dart"
```dart title="two_sum.dart"
/* 方法一: 暴力列舉 */
List<int> twoSumBruteForce(List<int> nums, int target) {
int size = nums.length;
// 兩層迴圈,時間複雜度為 O(n^2)
for (var i = 0; i < size - 1; i++) {
for (var j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target) return [i, j];
}
}
return [0];
}
```
=== "Rust"
```rust title="two_sum.rs"
/* 方法一:暴力列舉 */
pub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
let size = nums.len();
// 兩層迴圈,時間複雜度為 O(n^2)
for i in 0..size - 1 {
for j in i + 1..size {
if nums[i] + nums[j] == target {
return Some(vec![i as i32, j as i32]);
}
}
}
None
}
```
=== "C"
```c title="two_sum.c"
/* 方法一:暴力列舉 */
int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {
for (int i = 0; i < numsSize; ++i) {
for (int j = i + 1; j < numsSize; ++j) {
if (nums[i] + nums[j] == target) {
int *res = malloc(sizeof(int) * 2);
res[0] = i, res[1] = j;
*returnSize = 2;
return res;
}
}
}
*returnSize = 0;
return NULL;
}
```
=== "Kotlin"
```kotlin title="two_sum.kt"
/* 方法一:暴力列舉 */
fun twoSumBruteForce(nums: IntArray, target: Int): IntArray {
val size = nums.size
// 兩層迴圈,時間複雜度為 O(n^2)
for (i in 0..<size - 1) {
for (j in i + 1..<size) {
if (nums[i] + nums[j] == target) return intArrayOf(i, j)
}
}
return IntArray(0)
}
```
=== "Ruby"
```ruby title="two_sum.rb"
[class]{}-[func]{two_sum_brute_force}
```
=== "Zig"
```zig title="two_sum.zig"
// 方法一:暴力列舉
fn twoSumBruteForce(nums: []i32, target: i32) ?[2]i32 {
var size: usize = nums.len;
var i: usize = 0;
// 兩層迴圈,時間複雜度為 O(n^2)
while (i < size - 1) : (i += 1) {
var j = i + 1;
while (j < size) : (j += 1) {
if (nums[i] + nums[j] == target) {
return [_]i32{@intCast(i), @intCast(j)};
}
}
}
return null;
}
```
??? pythontutor "視覺化執行"
<div style="height: 441px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_brute_force%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%B8%80%EF%BC%9A%E6%9A%B4%E5%8A%9B%E6%9E%9A%E4%B8%BE%22%22%22%0A%20%20%20%20%23%20%E4%B8%A4%E5%B1%82%E5%BE%AA%E7%8E%AF%EF%BC%8C%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E4%B8%BA%20O%28n%5E2%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201,%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%2B%20nums%5Bj%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bi,%20j%5D%0A%20%20%20%20return%20%5B%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2,%207,%2011,%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_brute_force%28nums,%20target%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_brute_force%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%B8%80%EF%BC%9A%E6%9A%B4%E5%8A%9B%E6%9E%9A%E4%B8%BE%22%22%22%0A%20%20%20%20%23%20%E4%B8%A4%E5%B1%82%E5%BE%AA%E7%8E%AF%EF%BC%8C%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E4%B8%BA%20O%28n%5E2%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201,%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%2B%20nums%5Bj%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bi,%20j%5D%0A%20%20%20%20return%20%5B%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2,%207,%2011,%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_brute_force%28nums,%20target%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
此方法的時間複雜度為 $O(n^2)$ ,空間複雜度為 $O(1)$ ,在大資料量下非常耗時。
## 10.4.2 &nbsp; 雜湊查詢:以空間換時間
考慮藉助一個雜湊表,鍵值對分別為陣列元素和元素索引。迴圈走訪陣列,每輪執行圖 10-10 所示的步驟。
1. 判斷數字 `target - nums[i]` 是否在雜湊表中,若是,則直接返回這兩個元素的索引。
2. 將鍵值對 `nums[i]` 和索引 `i` 新增進雜湊表。
=== "<1>"
![輔助雜湊表求解兩數之和](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png){ class="animation-figure" }
=== "<2>"
![two_sum_hashtable_step2](replace_linear_by_hashing.assets/two_sum_hashtable_step2.png){ class="animation-figure" }
=== "<3>"
![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png){ class="animation-figure" }
<p align="center"> 圖 10-10 &nbsp; 輔助雜湊表求解兩數之和 </p>
實現程式碼如下所示,僅需單層迴圈即可:
=== "Python"
```python title="two_sum.py"
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:
"""方法二:輔助雜湊表"""
# 輔助雜湊表,空間複雜度為 O(n)
dic = {}
# 單層迴圈,時間複雜度為 O(n)
for i in range(len(nums)):
if target - nums[i] in dic:
return [dic[target - nums[i]], i]
dic[nums[i]] = i
return []
```
=== "C++"
```cpp title="two_sum.cpp"
/* 方法二:輔助雜湊表 */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
int size = nums.size();
// 輔助雜湊表,空間複雜度為 O(n)
unordered_map<int, int> dic;
// 單層迴圈,時間複雜度為 O(n)
for (int i = 0; i < size; i++) {
if (dic.find(target - nums[i]) != dic.end()) {
return {dic[target - nums[i]], i};
}
dic.emplace(nums[i], i);
}
return {};
}
```
=== "Java"
```java title="two_sum.java"
/* 方法二:輔助雜湊表 */
int[] twoSumHashTable(int[] nums, int target) {
int size = nums.length;
// 輔助雜湊表,空間複雜度為 O(n)
Map<Integer, Integer> dic = new HashMap<>();
// 單層迴圈,時間複雜度為 O(n)
for (int i = 0; i < size; i++) {
if (dic.containsKey(target - nums[i])) {
return new int[] { dic.get(target - nums[i]), i };
}
dic.put(nums[i], i);
}
return new int[0];
}
```
=== "C#"
```csharp title="two_sum.cs"
/* 方法二:輔助雜湊表 */
int[] TwoSumHashTable(int[] nums, int target) {
int size = nums.Length;
// 輔助雜湊表,空間複雜度為 O(n)
Dictionary<int, int> dic = [];
// 單層迴圈,時間複雜度為 O(n)
for (int i = 0; i < size; i++) {
if (dic.ContainsKey(target - nums[i])) {
return [dic[target - nums[i]], i];
}
dic.Add(nums[i], i);
}
return [];
}
```
=== "Go"
```go title="two_sum.go"
/* 方法二:輔助雜湊表 */
func twoSumHashTable(nums []int, target int) []int {
// 輔助雜湊表,空間複雜度為 O(n)
hashTable := map[int]int{}
// 單層迴圈,時間複雜度為 O(n)
for idx, val := range nums {
if preIdx, ok := hashTable[target-val]; ok {
return []int{preIdx, idx}
}
hashTable[val] = idx
}
return nil
}
```
=== "Swift"
```swift title="two_sum.swift"
/* 方法二:輔助雜湊表 */
func twoSumHashTable(nums: [Int], target: Int) -> [Int] {
// 輔助雜湊表,空間複雜度為 O(n)
var dic: [Int: Int] = [:]
// 單層迴圈,時間複雜度為 O(n)
for i in nums.indices {
if let j = dic[target - nums[i]] {
return [j, i]
}
dic[nums[i]] = i
}
return [0]
}
```
=== "JS"
```javascript title="two_sum.js"
/* 方法二:輔助雜湊表 */
function twoSumHashTable(nums, target) {
// 輔助雜湊表,空間複雜度為 O(n)
let m = {};
// 單層迴圈,時間複雜度為 O(n)
for (let i = 0; i < nums.length; i++) {
if (m[target - nums[i]] !== undefined) {
return [m[target - nums[i]], i];
} else {
m[nums[i]] = i;
}
}
return [];
}
```
=== "TS"
```typescript title="two_sum.ts"
/* 方法二:輔助雜湊表 */
function twoSumHashTable(nums: number[], target: number): number[] {
// 輔助雜湊表,空間複雜度為 O(n)
let m: Map<number, number> = new Map();
// 單層迴圈,時間複雜度為 O(n)
for (let i = 0; i < nums.length; i++) {
let index = m.get(target - nums[i]);
if (index !== undefined) {
return [index, i];
} else {
m.set(nums[i], i);
}
}
return [];
}
```
=== "Dart"
```dart title="two_sum.dart"
/* 方法二: 輔助雜湊表 */
List<int> twoSumHashTable(List<int> nums, int target) {
int size = nums.length;
// 輔助雜湊表,空間複雜度為 O(n)
Map<int, int> dic = HashMap();
// 單層迴圈,時間複雜度為 O(n)
for (var i = 0; i < size; i++) {
if (dic.containsKey(target - nums[i])) {
return [dic[target - nums[i]]!, i];
}
dic.putIfAbsent(nums[i], () => i);
}
return [0];
}
```
=== "Rust"
```rust title="two_sum.rs"
/* 方法二:輔助雜湊表 */
pub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
// 輔助雜湊表,空間複雜度為 O(n)
let mut dic = HashMap::new();
// 單層迴圈,時間複雜度為 O(n)
for (i, num) in nums.iter().enumerate() {
match dic.get(&(target - num)) {
Some(v) => return Some(vec![*v as i32, i as i32]),
None => dic.insert(num, i as i32),
};
}
None
}
```
=== "C"
```c title="two_sum.c"
/* 雜湊表 */
typedef struct {
int key;
int val;
UT_hash_handle hh; // 基於 uthash.h 實現
} HashTable;
/* 雜湊表查詢 */
HashTable *find(HashTable *h, int key) {
HashTable *tmp;
HASH_FIND_INT(h, &key, tmp);
return tmp;
}
/* 雜湊表元素插入 */
void insert(HashTable *h, int key, int val) {
HashTable *t = find(h, key);
if (t == NULL) {
HashTable *tmp = malloc(sizeof(HashTable));
tmp->key = key, tmp->val = val;
HASH_ADD_INT(h, key, tmp);
} else {
t->val = val;
}
}
/* 方法二:輔助雜湊表 */
int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {
HashTable *hashtable = NULL;
for (int i = 0; i < numsSize; i++) {
HashTable *t = find(hashtable, target - nums[i]);
if (t != NULL) {
int *res = malloc(sizeof(int) * 2);
res[0] = t->val, res[1] = i;
*returnSize = 2;
return res;
}
insert(hashtable, nums[i], i);
}
*returnSize = 0;
return NULL;
}
```
=== "Kotlin"
```kotlin title="two_sum.kt"
/* 方法二:輔助雜湊表 */
fun twoSumHashTable(nums: IntArray, target: Int): IntArray {
val size = nums.size
// 輔助雜湊表,空間複雜度為 O(n)
val dic = HashMap<Int, Int>()
// 單層迴圈,時間複雜度為 O(n)
for (i in 0..<size) {
if (dic.containsKey(target - nums[i])) {
return intArrayOf(dic[target - nums[i]]!!, i)
}
dic[nums[i]] = i
}
return IntArray(0)
}
```
=== "Ruby"
```ruby title="two_sum.rb"
[class]{}-[func]{two_sum_hash_table}
```
=== "Zig"
```zig title="two_sum.zig"
// 方法二:輔助雜湊表
fn twoSumHashTable(nums: []i32, target: i32) !?[2]i32 {
var size: usize = nums.len;
// 輔助雜湊表,空間複雜度為 O(n)
var dic = std.AutoHashMap(i32, i32).init(std.heap.page_allocator);
defer dic.deinit();
var i: usize = 0;
// 單層迴圈,時間複雜度為 O(n)
while (i < size) : (i += 1) {
if (dic.contains(target - nums[i])) {
return [_]i32{dic.get(target - nums[i]).?, @intCast(i)};
}
try dic.put(nums[i], @intCast(i));
}
return null;
}
```
??? pythontutor "視覺化執行"
<div style="height: 477px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_hash_table%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%BA%8C%EF%BC%9A%E8%BE%85%E5%8A%A9%E5%93%88%E5%B8%8C%E8%A1%A8%22%22%22%0A%20%20%20%20%23%20%E8%BE%85%E5%8A%A9%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E7%A9%BA%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E4%B8%BA%20O%28n%29%0A%20%20%20%20dic%20%3D%20%7B%7D%0A%20%20%20%20%23%20%E5%8D%95%E5%B1%82%E5%BE%AA%E7%8E%AF%EF%BC%8C%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E4%B8%BA%20O%28n%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20if%20target%20-%20nums%5Bi%5D%20in%20dic%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bdic%5Btarget%20-%20nums%5Bi%5D%5D,%20i%5D%0A%20%20%20%20%20%20%20%20dic%5Bnums%5Bi%5D%5D%20%3D%20i%0A%20%20%20%20return%20%5B%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2,%207,%2011,%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_hash_table%28nums,%20target%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_hash_table%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%BA%8C%EF%BC%9A%E8%BE%85%E5%8A%A9%E5%93%88%E5%B8%8C%E8%A1%A8%22%22%22%0A%20%20%20%20%23%20%E8%BE%85%E5%8A%A9%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E7%A9%BA%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E4%B8%BA%20O%28n%29%0A%20%20%20%20dic%20%3D%20%7B%7D%0A%20%20%20%20%23%20%E5%8D%95%E5%B1%82%E5%BE%AA%E7%8E%AF%EF%BC%8C%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E4%B8%BA%20O%28n%29%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20if%20target%20-%20nums%5Bi%5D%20in%20dic%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bdic%5Btarget%20-%20nums%5Bi%5D%5D,%20i%5D%0A%20%20%20%20%20%20%20%20dic%5Bnums%5Bi%5D%5D%20%3D%20i%0A%20%20%20%20return%20%5B%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B2,%207,%2011,%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_hash_table%28nums,%20target%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
此方法透過雜湊查詢將時間複雜度從 $O(n^2)$ 降至 $O(n)$ ,大幅提升執行效率。
由於需要維護一個額外的雜湊表,因此空間複雜度為 $O(n)$ 。**儘管如此,該方法的整體時空效率更為均衡,因此它是本題的最優解法**。
@@ -0,0 +1,94 @@
---
comments: true
---
# 10.5 &nbsp; 重識搜尋演算法
<u>搜尋演算法(searching algorithm</u>用於在資料結構(例如陣列、鏈結串列、樹或圖)中搜索一個或一組滿足特定條件的元素。
搜尋演算法可根據實現思路分為以下兩類。
- **透過走訪資料結構來定位目標元素**,例如陣列、鏈結串列、樹和圖的走訪等。
- **利用資料組織結構或資料包含的先驗資訊,實現高效元素查詢**,例如二分搜尋、雜湊查詢和二元搜尋樹查詢等。
不難發現,這些知識點都已在前面的章節中介紹過,因此搜尋演算法對於我們來說並不陌生。在本節中,我們將從更加系統的視角切入,重新審視搜尋演算法。
## 10.5.1 &nbsp; 暴力搜尋
暴力搜尋透過走訪資料結構的每個元素來定位目標元素。
- “線性搜尋”適用於陣列和鏈結串列等線性資料結構。它從資料結構的一端開始,逐個訪問元素,直到找到目標元素或到達另一端仍沒有找到目標元素為止。
- “廣度優先搜尋”和“深度優先搜尋”是圖和樹的兩種走訪策略。廣度優先搜尋從初始節點開始逐層搜尋,由近及遠地訪問各個節點。深度優先搜尋從初始節點開始,沿著一條路徑走到頭,再回溯並嘗試其他路徑,直到走訪完整個資料結構。
暴力搜尋的優點是簡單且通用性好,**無須對資料做預處理和藉助額外的資料結構**。
然而,**此類演算法的時間複雜度為 $O(n)$** ,其中 $n$ 為元素數量,因此在資料量較大的情況下效能較差。
## 10.5.2 &nbsp; 自適應搜尋
自適應搜尋利用資料的特有屬性(例如有序性)來最佳化搜尋過程,從而更高效地定位目標元素。
- “二分搜尋”利用資料的有序性實現高效查詢,僅適用於陣列。
- “雜湊查詢”利用雜湊表將搜尋資料和目標資料建立為鍵值對對映,從而實現查詢操作。
- “樹查詢”在特定的樹結構(例如二元搜尋樹)中,基於比較節點值來快速排除節點,從而定位目標元素。
此類演算法的優點是效率高,**時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$** 。
然而,**使用這些演算法往往需要對資料進行預處理**。例如,二分搜尋需要預先對陣列進行排序,雜湊查詢和樹查詢都需要藉助額外的資料結構,維護這些資料結構也需要額外的時間和空間開銷。
!!! tip
自適應搜尋演算法常被稱為查詢演算法,**主要用於在特定資料結構中快速檢索目標元素**。
## 10.5.3 &nbsp; 搜尋方法選取
給定大小為 $n$ 的一組資料,我們可以使用線性搜尋、二分搜尋、樹查詢、雜湊查詢等多種方法從中搜索目標元素。各個方法的工作原理如圖 10-11 所示。
![多種搜尋策略](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" }
<p align="center"> 圖 10-11 &nbsp; 多種搜尋策略 </p>
上述幾種方法的操作效率與特性如表 10-1 所示。
<p align="center"> 表 10-1 &nbsp; 查詢演算法效率對比 </p>
<div class="center-table" markdown>
| | 線性搜尋 | 二分搜尋 | 樹查詢 | 雜湊查詢 |
| ------------ | -------- | ------------------ | ------------------ | --------------- |
| 查詢元素 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ |
| 插入元素 | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
| 刪除元素 | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
| 額外空間 | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ |
| 資料預處理 | / | 排序 $O(n \log n)$ | 建樹 $O(n \log n)$ | 建雜湊表 $O(n)$ |
| 資料是否有序 | 無序 | 有序 | 有序 | 無序 |
</div>
搜尋演算法的選擇還取決於資料體量、搜尋效能要求、資料查詢與更新頻率等。
**線性搜尋**
- 通用性較好,無須任何資料預處理操作。假如我們僅需查詢一次資料,那麼其他三種方法的資料預處理的時間比線性搜尋的時間還要更長。
- 適用於體量較小的資料,此情況下時間複雜度對效率影響較小。
- 適用於資料更新頻率較高的場景,因為該方法不需要對資料進行任何額外維護。
**二分搜尋**
- 適用於大資料量的情況,效率表現穩定,最差時間複雜度為 $O(\log n)$ 。
- 資料量不能過大,因為儲存陣列需要連續的記憶體空間。
- 不適用於高頻增刪資料的場景,因為維護有序陣列的開銷較大。
**雜湊查詢**
- 適合對查詢效能要求很高的場景,平均時間複雜度為 $O(1)$ 。
- 不適合需要有序資料或範圍查詢的場景,因為雜湊表無法維護資料的有序性。
- 對雜湊函式和雜湊衝突處理策略的依賴性較高,具有較大的效能劣化風險。
- 不適合資料量過大的情況,因為雜湊表需要額外空間來最大程度地減少衝突,從而提供良好的查詢效能。
**樹查詢**
- 適用於海量資料,因為樹節點在記憶體中是分散儲存的。
- 適合需要維護有序資料或範圍查詢的場景。
- 在持續增刪節點的過程中,二元搜尋樹可能產生傾斜,時間複雜度劣化至 $O(n)$ 。
- 若使用 AVL 樹或紅黑樹,則各項操作可在 $O(\log n)$ 效率下穩定執行,但維護樹平衡的操作會增加額外的開銷。
+12
View File
@@ -0,0 +1,12 @@
---
comments: true
---
# 10.6 &nbsp; 小結
- 二分搜尋依賴資料的有序性,透過迴圈逐步縮減一半搜尋區間來進行查詢。它要求輸入資料有序,且僅適用於陣列或基於陣列實現的資料結構。
- 暴力搜尋透過走訪資料結構來定位資料。線性搜尋適用於陣列和鏈結串列,廣度優先搜尋和深度優先搜尋適用於圖和樹。此類演算法通用性好,無須對資料進行預處理,但時間複雜度 $O(n)$ 較高。
- 雜湊查詢、樹查詢和二分搜尋屬於高效搜尋方法,可在特定資料結構中快速定位目標元素。此類演算法效率高,時間複雜度可達 $O(\log n)$ 甚至 $O(1)$ ,但通常需要藉助額外資料結構。
- 實際中,我們需要對資料體量、搜尋效能要求、資料查詢和更新頻率等因素進行具體分析,從而選擇合適的搜尋方法。
- 線性搜尋適用於小型或頻繁更新的資料;二分搜尋適用於大型、排序的資料;雜湊查詢適用於對查詢效率要求較高且無須範圍查詢的資料;樹查詢適用於需要維護順序和支持範圍查詢的大型動態資料。
- 用雜湊查詢替換線性查詢是一種常用的最佳化執行時間的策略,可將時間複雜度從 $O(n)$ 降至 $O(1)$ 。
+623
View File
@@ -0,0 +1,623 @@
---
comments: true
---
# 11.3 &nbsp; 泡沫排序
<u>泡沫排序(bubble sort</u>透過連續地比較與交換相鄰元素實現排序。這個過程就像氣泡從底部升到頂部一樣,因此得名泡沫排序。
如圖 11-4 所示,冒泡過程可以利用元素交換操作來模擬:從陣列最左端開始向右走訪,依次比較相鄰元素大小,如果“左元素 > 右元素”就交換二者。走訪完成後,最大的元素會被移動到陣列的最右端。
=== "<1>"
![利用元素交換操作模擬冒泡](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
=== "<2>"
![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" }
=== "<3>"
![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png){ class="animation-figure" }
=== "<4>"
![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png){ class="animation-figure" }
=== "<5>"
![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png){ class="animation-figure" }
=== "<6>"
![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png){ class="animation-figure" }
=== "<7>"
![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" }
<p align="center"> 圖 11-4 &nbsp; 利用元素交換操作模擬冒泡 </p>
## 11.3.1 &nbsp; 演算法流程
設陣列的長度為 $n$ ,泡沫排序的步驟如圖 11-5 所示。
1. 首先,對 $n$ 個元素執行“冒泡”,**將陣列的最大元素交換至正確位置**。
2. 接下來,對剩餘 $n - 1$ 個元素執行“冒泡”,**將第二大元素交換至正確位置**。
3. 以此類推,經過 $n - 1$ 輪“冒泡”後,**前 $n - 1$ 大的元素都被交換至正確位置**。
4. 僅剩的一個元素必定是最小元素,無須排序,因此陣列排序完成。
![泡沫排序流程](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
<p align="center"> 圖 11-5 &nbsp; 泡沫排序流程 </p>
示例程式碼如下:
=== "Python"
```python title="bubble_sort.py"
def bubble_sort(nums: list[int]):
"""泡沫排序"""
n = len(nums)
# 外迴圈:未排序區間為 [0, i]
for i in range(n - 1, 0, -1):
# 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for j in range(i):
if nums[j] > nums[j + 1]:
# 交換 nums[j] 與 nums[j + 1]
nums[j], nums[j + 1] = nums[j + 1], nums[j]
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* 泡沫排序 */
void bubbleSort(vector<int> &nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
// 這裡使用了 std::swap() 函式
swap(nums[j], nums[j + 1]);
}
}
}
}
```
=== "Java"
```java title="bubble_sort.java"
/* 泡沫排序 */
void bubbleSort(int[] nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.length - 1; i > 0; i--) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "C#"
```csharp title="bubble_sort.cs"
/* 泡沫排序 */
void BubbleSort(int[] nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.Length - 1; i > 0; i--) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
}
}
}
}
```
=== "Go"
```go title="bubble_sort.go"
/* 泡沫排序 */
func bubbleSort(nums []int) {
// 外迴圈:未排序區間為 [0, i]
for i := len(nums) - 1; i > 0; i-- {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for j := 0; j < i; j++ {
if nums[j] > nums[j+1] {
// 交換 nums[j] 與 nums[j + 1]
nums[j], nums[j+1] = nums[j+1], nums[j]
}
}
}
}
```
=== "Swift"
```swift title="bubble_sort.swift"
/* 泡沫排序 */
func bubbleSort(nums: inout [Int]) {
// 外迴圈:未排序區間為 [0, i]
for i in nums.indices.dropFirst().reversed() {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for j in 0 ..< i {
if nums[j] > nums[j + 1] {
// 交換 nums[j] 與 nums[j + 1]
nums.swapAt(j, j + 1)
}
}
}
}
```
=== "JS"
```javascript title="bubble_sort.js"
/* 泡沫排序 */
function bubbleSort(nums) {
// 外迴圈:未排序區間為 [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "TS"
```typescript title="bubble_sort.ts"
/* 泡沫排序 */
function bubbleSort(nums: number[]): void {
// 外迴圈:未排序區間為 [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "Dart"
```dart title="bubble_sort.dart"
/* 泡沫排序 */
void bubbleSort(List<int> nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.length - 1; i > 0; i--) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "Rust"
```rust title="bubble_sort.rs"
/* 泡沫排序 */
fn bubble_sort(nums: &mut [i32]) {
// 外迴圈:未排序區間為 [0, i]
for i in (1..nums.len()).rev() {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for j in 0..i {
if nums[j] > nums[j + 1] {
// 交換 nums[j] 與 nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
=== "C"
```c title="bubble_sort.c"
/* 泡沫排序 */
void bubbleSort(int nums[], int size) {
// 外迴圈:未排序區間為 [0, i]
for (int i = size - 1; i > 0; i--) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
/* 泡沫排序 */
fun bubbleSort(nums: IntArray) {
// 外迴圈:未排序區間為 [0, i]
for (i in nums.size - 1 downTo 1) {
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (j in 0..<i) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
nums[j] = nums[j+1].also { nums[j+1] = nums[j] }
}
}
}
}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort}
```
=== "Zig"
```zig title="bubble_sort.zig"
// 泡沫排序
fn bubbleSort(nums: []i32) void {
// 外迴圈:未排序區間為 [0, i]
var i: usize = nums.len - 1;
while (i > 0) : (i -= 1) {
var j: usize = 0;
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
while (j < i) : (j += 1) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
var tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
```
??? pythontutor "視覺化執行"
<div style="height: 477px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i%5D%20%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E8%87%B3%E8%AF%A5%E5%8C%BA%E9%97%B4%E7%9A%84%E6%9C%80%E5%8F%B3%E7%AB%AF%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%20nums%5Bj%5D%20%E4%B8%8E%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D,%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D,%20nums%5Bj%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i%5D%20%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E8%87%B3%E8%AF%A5%E5%8C%BA%E9%97%B4%E7%9A%84%E6%9C%80%E5%8F%B3%E7%AB%AF%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%20nums%5Bj%5D%20%E4%B8%8E%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D,%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D,%20nums%5Bj%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.3.2 &nbsp; 效率最佳化
我們發現,如果某輪“冒泡”中沒有執行任何交換操作,說明陣列已經完成排序,可直接返回結果。因此,可以增加一個標誌位 `flag` 來監測這種情況,一旦出現就立即返回。
經過最佳化,泡沫排序的最差時間複雜度和平均時間複雜度仍為 $O(n^2)$ ;但當輸入陣列完全有序時,可達到最佳時間複雜度 $O(n)$ 。
=== "Python"
```python title="bubble_sort.py"
def bubble_sort_with_flag(nums: list[int]):
"""泡沫排序(標誌最佳化)"""
n = len(nums)
# 外迴圈:未排序區間為 [0, i]
for i in range(n - 1, 0, -1):
flag = False # 初始化標誌位
# 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for j in range(i):
if nums[j] > nums[j + 1]:
# 交換 nums[j] 與 nums[j + 1]
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True # 記錄交換元素
if not flag:
break # 此輪“冒泡”未交換任何元素,直接跳出
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* 泡沫排序(標誌最佳化)*/
void bubbleSortWithFlag(vector<int> &nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
bool flag = false; // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
// 這裡使用了 std::swap() 函式
swap(nums[j], nums[j + 1]);
flag = true; // 記錄交換元素
}
}
if (!flag)
break; // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
=== "Java"
```java title="bubble_sort.java"
/* 泡沫排序(標誌最佳化) */
void bubbleSortWithFlag(int[] nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.length - 1; i > 0; i--) {
boolean flag = false; // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 記錄交換元素
}
}
if (!flag)
break; // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
=== "C#"
```csharp title="bubble_sort.cs"
/* 泡沫排序(標誌最佳化)*/
void BubbleSortWithFlag(int[] nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.Length - 1; i > 0; i--) {
bool flag = false; // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
flag = true; // 記錄交換元素
}
}
if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
=== "Go"
```go title="bubble_sort.go"
/* 泡沫排序(標誌最佳化)*/
func bubbleSortWithFlag(nums []int) {
// 外迴圈:未排序區間為 [0, i]
for i := len(nums) - 1; i > 0; i-- {
flag := false // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for j := 0; j < i; j++ {
if nums[j] > nums[j+1] {
// 交換 nums[j] 與 nums[j + 1]
nums[j], nums[j+1] = nums[j+1], nums[j]
flag = true // 記錄交換元素
}
}
if flag == false { // 此輪“冒泡”未交換任何元素,直接跳出
break
}
}
}
```
=== "Swift"
```swift title="bubble_sort.swift"
/* 泡沫排序(標誌最佳化)*/
func bubbleSortWithFlag(nums: inout [Int]) {
// 外迴圈:未排序區間為 [0, i]
for i in nums.indices.dropFirst().reversed() {
var flag = false // 初始化標誌位
for j in 0 ..< i {
if nums[j] > nums[j + 1] {
// 交換 nums[j] 與 nums[j + 1]
nums.swapAt(j, j + 1)
flag = true // 記錄交換元素
}
}
if !flag { // 此輪“冒泡”未交換任何元素,直接跳出
break
}
}
}
```
=== "JS"
```javascript title="bubble_sort.js"
/* 泡沫排序(標誌最佳化)*/
function bubbleSortWithFlag(nums) {
// 外迴圈:未排序區間為 [0, i]
for (let i = nums.length - 1; i > 0; i--) {
let flag = false; // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 記錄交換元素
}
}
if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
=== "TS"
```typescript title="bubble_sort.ts"
/* 泡沫排序(標誌最佳化)*/
function bubbleSortWithFlag(nums: number[]): void {
// 外迴圈:未排序區間為 [0, i]
for (let i = nums.length - 1; i > 0; i--) {
let flag = false; // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 記錄交換元素
}
}
if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
=== "Dart"
```dart title="bubble_sort.dart"
/* 泡沫排序(標誌最佳化)*/
void bubbleSortWithFlag(List<int> nums) {
// 外迴圈:未排序區間為 [0, i]
for (int i = nums.length - 1; i > 0; i--) {
bool flag = false; // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 記錄交換元素
}
}
if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
=== "Rust"
```rust title="bubble_sort.rs"
/* 泡沫排序(標誌最佳化) */
fn bubble_sort_with_flag(nums: &mut [i32]) {
// 外迴圈:未排序區間為 [0, i]
for i in (1..nums.len()).rev() {
let mut flag = false; // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for j in 0..i {
if nums[j] > nums[j + 1] {
// 交換 nums[j] 與 nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 記錄交換元素
}
}
if !flag {
break; // 此輪“冒泡”未交換任何元素,直接跳出
};
}
}
```
=== "C"
```c title="bubble_sort.c"
/* 泡沫排序(標誌最佳化)*/
void bubbleSortWithFlag(int nums[], int size) {
// 外迴圈:未排序區間為 [0, i]
for (int i = size - 1; i > 0; i--) {
bool flag = false;
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
flag = true;
}
}
if (!flag)
break;
}
}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
/* 泡沫排序(標誌最佳化) */
fun bubbleSortWithFlag(nums: IntArray) {
// 外迴圈:未排序區間為 [0, i]
for (i in nums.size - 1 downTo 1) {
var flag = false // 初始化標誌位
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
for (j in 0..<i) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
nums[j] = nums[j + 1].also { nums[j] = nums[j + 1] }
flag = true // 記錄交換元素
}
}
if (!flag) break // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort_with_flag}
```
=== "Zig"
```zig title="bubble_sort.zig"
// 泡沫排序(標誌最佳化)
fn bubbleSortWithFlag(nums: []i32) void {
// 外迴圈:未排序區間為 [0, i]
var i: usize = nums.len - 1;
while (i > 0) : (i -= 1) {
var flag = false; // 初始化標誌位
var j: usize = 0;
// 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
while (j < i) : (j += 1) {
if (nums[j] > nums[j + 1]) {
// 交換 nums[j] 與 nums[j + 1]
var tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true;
}
}
if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出
}
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort_with_flag%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%EF%BC%88%E6%A0%87%E5%BF%97%E4%BC%98%E5%8C%96%EF%BC%89%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20flag%20%3D%20False%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%87%E5%BF%97%E4%BD%8D%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i%5D%20%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E8%87%B3%E8%AF%A5%E5%8C%BA%E9%97%B4%E7%9A%84%E6%9C%80%E5%8F%B3%E7%AB%AF%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%20nums%5Bj%5D%20%E4%B8%8E%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D,%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D,%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20flag%20%3D%20True%20%20%23%20%E8%AE%B0%E5%BD%95%E4%BA%A4%E6%8D%A2%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20if%20not%20flag%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%20%20%23%20%E6%AD%A4%E8%BD%AE%E2%80%9C%E5%86%92%E6%B3%A1%E2%80%9D%E6%9C%AA%E4%BA%A4%E6%8D%A2%E4%BB%BB%E4%BD%95%E5%85%83%E7%B4%A0%EF%BC%8C%E7%9B%B4%E6%8E%A5%E8%B7%B3%E5%87%BA%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20bubble_sort_with_flag%28nums%29%0A%20%20%20%20print%28%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort_with_flag%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%EF%BC%88%E6%A0%87%E5%BF%97%E4%BC%98%E5%8C%96%EF%BC%89%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20flag%20%3D%20False%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%87%E5%BF%97%E4%BD%8D%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i%5D%20%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E8%87%B3%E8%AF%A5%E5%8C%BA%E9%97%B4%E7%9A%84%E6%9C%80%E5%8F%B3%E7%AB%AF%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%20nums%5Bj%5D%20%E4%B8%8E%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D,%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D,%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20flag%20%3D%20True%20%20%23%20%E8%AE%B0%E5%BD%95%E4%BA%A4%E6%8D%A2%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20if%20not%20flag%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%20%20%23%20%E6%AD%A4%E8%BD%AE%E2%80%9C%E5%86%92%E6%B3%A1%E2%80%9D%E6%9C%AA%E4%BA%A4%E6%8D%A2%E4%BB%BB%E4%BD%95%E5%85%83%E7%B4%A0%EF%BC%8C%E7%9B%B4%E6%8E%A5%E8%B7%B3%E5%87%BA%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20bubble_sort_with_flag%28nums%29%0A%20%20%20%20print%28%22%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.3.3 &nbsp; 演算法特性
- **時間複雜度為 $O(n^2)$、自適應排序**:各輪“冒泡”走訪的陣列長度依次為 $n - 1$、$n - 2$、$\dots$、$2$、$1$ ,總和為 $(n - 1) n / 2$ 。在引入 `flag` 最佳化後,最佳時間複雜度可達到 $O(n)$ 。
- **空間複雜度為 $O(1)$、原地排序**:指標 $i$ 和 $j$ 使用常數大小的額外空間。
- **穩定排序**:由於在“冒泡”中遇到相等元素不交換。
+468
View File
@@ -0,0 +1,468 @@
---
comments: true
---
# 11.8 &nbsp; 桶排序
前述幾種排序演算法都屬於“基於比較的排序演算法”,它們透過比較元素間的大小來實現排序。此類排序演算法的時間複雜度無法超越 $O(n \log n)$ 。接下來,我們將探討幾種“非比較排序演算法”,它們的時間複雜度可以達到線性階。
<u>桶排序(bucket sort</u>是分治策略的一個典型應用。它透過設定一些具有大小順序的桶,每個桶對應一個數據範圍,將資料平均分配到各個桶中;然後,在每個桶內部分別執行排序;最終按照桶的順序將所有資料合併。
## 11.8.1 &nbsp; 演算法流程
考慮一個長度為 $n$ 的陣列,其元素是範圍 $[0, 1)$ 內的浮點數。桶排序的流程如圖 11-13 所示。
1. 初始化 $k$ 個桶,將 $n$ 個元素分配到 $k$ 個桶中。
2. 對每個桶分別執行排序(這裡採用程式語言的內建排序函式)。
3. 按照桶從小到大的順序合併結果。
![桶排序演算法流程](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" }
<p align="center"> 圖 11-13 &nbsp; 桶排序演算法流程 </p>
程式碼如下所示:
=== "Python"
```python title="bucket_sort.py"
def bucket_sort(nums: list[float]):
"""桶排序"""
# 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
k = len(nums) // 2
buckets = [[] for _ in range(k)]
# 1. 將陣列元素分配到各個桶中
for num in nums:
# 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
i = int(num * k)
# 將 num 新增進桶 i
buckets[i].append(num)
# 2. 對各個桶執行排序
for bucket in buckets:
# 使用內建排序函式,也可以替換成其他排序演算法
bucket.sort()
# 3. 走訪桶合併結果
i = 0
for bucket in buckets:
for num in bucket:
nums[i] = num
i += 1
```
=== "C++"
```cpp title="bucket_sort.cpp"
/* 桶排序 */
void bucketSort(vector<float> &nums) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
int k = nums.size() / 2;
vector<vector<float>> buckets(k);
// 1. 將陣列元素分配到各個桶中
for (float num : nums) {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
int i = num * k;
// 將 num 新增進桶 bucket_idx
buckets[i].push_back(num);
}
// 2. 對各個桶執行排序
for (vector<float> &bucket : buckets) {
// 使用內建排序函式,也可以替換成其他排序演算法
sort(bucket.begin(), bucket.end());
}
// 3. 走訪桶合併結果
int i = 0;
for (vector<float> &bucket : buckets) {
for (float num : bucket) {
nums[i++] = num;
}
}
}
```
=== "Java"
```java title="bucket_sort.java"
/* 桶排序 */
void bucketSort(float[] nums) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
int k = nums.length / 2;
List<List<Float>> buckets = new ArrayList<>();
for (int i = 0; i < k; i++) {
buckets.add(new ArrayList<>());
}
// 1. 將陣列元素分配到各個桶中
for (float num : nums) {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
int i = (int) (num * k);
// 將 num 新增進桶 i
buckets.get(i).add(num);
}
// 2. 對各個桶執行排序
for (List<Float> bucket : buckets) {
// 使用內建排序函式,也可以替換成其他排序演算法
Collections.sort(bucket);
}
// 3. 走訪桶合併結果
int i = 0;
for (List<Float> bucket : buckets) {
for (float num : bucket) {
nums[i++] = num;
}
}
}
```
=== "C#"
```csharp title="bucket_sort.cs"
/* 桶排序 */
void BucketSort(float[] nums) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
int k = nums.Length / 2;
List<List<float>> buckets = [];
for (int i = 0; i < k; i++) {
buckets.Add([]);
}
// 1. 將陣列元素分配到各個桶中
foreach (float num in nums) {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
int i = (int)(num * k);
// 將 num 新增進桶 i
buckets[i].Add(num);
}
// 2. 對各個桶執行排序
foreach (List<float> bucket in buckets) {
// 使用內建排序函式,也可以替換成其他排序演算法
bucket.Sort();
}
// 3. 走訪桶合併結果
int j = 0;
foreach (List<float> bucket in buckets) {
foreach (float num in bucket) {
nums[j++] = num;
}
}
}
```
=== "Go"
```go title="bucket_sort.go"
/* 桶排序 */
func bucketSort(nums []float64) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
k := len(nums) / 2
buckets := make([][]float64, k)
for i := 0; i < k; i++ {
buckets[i] = make([]float64, 0)
}
// 1. 將陣列元素分配到各個桶中
for _, num := range nums {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
i := int(num * float64(k))
// 將 num 新增進桶 i
buckets[i] = append(buckets[i], num)
}
// 2. 對各個桶執行排序
for i := 0; i < k; i++ {
// 使用內建切片排序函式,也可以替換成其他排序演算法
sort.Float64s(buckets[i])
}
// 3. 走訪桶合併結果
i := 0
for _, bucket := range buckets {
for _, num := range bucket {
nums[i] = num
i++
}
}
}
```
=== "Swift"
```swift title="bucket_sort.swift"
/* 桶排序 */
func bucketSort(nums: inout [Double]) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
let k = nums.count / 2
var buckets = (0 ..< k).map { _ in [Double]() }
// 1. 將陣列元素分配到各個桶中
for num in nums {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
let i = Int(num * Double(k))
// 將 num 新增進桶 i
buckets[i].append(num)
}
// 2. 對各個桶執行排序
for i in buckets.indices {
// 使用內建排序函式,也可以替換成其他排序演算法
buckets[i].sort()
}
// 3. 走訪桶合併結果
var i = nums.startIndex
for bucket in buckets {
for num in bucket {
nums[i] = num
i += 1
}
}
}
```
=== "JS"
```javascript title="bucket_sort.js"
/* 桶排序 */
function bucketSort(nums) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
const k = nums.length / 2;
const buckets = [];
for (let i = 0; i < k; i++) {
buckets.push([]);
}
// 1. 將陣列元素分配到各個桶中
for (const num of nums) {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
const i = Math.floor(num * k);
// 將 num 新增進桶 i
buckets[i].push(num);
}
// 2. 對各個桶執行排序
for (const bucket of buckets) {
// 使用內建排序函式,也可以替換成其他排序演算法
bucket.sort((a, b) => a - b);
}
// 3. 走訪桶合併結果
let i = 0;
for (const bucket of buckets) {
for (const num of bucket) {
nums[i++] = num;
}
}
}
```
=== "TS"
```typescript title="bucket_sort.ts"
/* 桶排序 */
function bucketSort(nums: number[]): void {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
const k = nums.length / 2;
const buckets: number[][] = [];
for (let i = 0; i < k; i++) {
buckets.push([]);
}
// 1. 將陣列元素分配到各個桶中
for (const num of nums) {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
const i = Math.floor(num * k);
// 將 num 新增進桶 i
buckets[i].push(num);
}
// 2. 對各個桶執行排序
for (const bucket of buckets) {
// 使用內建排序函式,也可以替換成其他排序演算法
bucket.sort((a, b) => a - b);
}
// 3. 走訪桶合併結果
let i = 0;
for (const bucket of buckets) {
for (const num of bucket) {
nums[i++] = num;
}
}
}
```
=== "Dart"
```dart title="bucket_sort.dart"
/* 桶排序 */
void bucketSort(List<double> nums) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
int k = nums.length ~/ 2;
List<List<double>> buckets = List.generate(k, (index) => []);
// 1. 將陣列元素分配到各個桶中
for (double _num in nums) {
// 輸入資料範圍為 [0, 1),使用 _num * k 對映到索引範圍 [0, k-1]
int i = (_num * k).toInt();
// 將 _num 新增進桶 bucket_idx
buckets[i].add(_num);
}
// 2. 對各個桶執行排序
for (List<double> bucket in buckets) {
bucket.sort();
}
// 3. 走訪桶合併結果
int i = 0;
for (List<double> bucket in buckets) {
for (double _num in bucket) {
nums[i++] = _num;
}
}
}
```
=== "Rust"
```rust title="bucket_sort.rs"
/* 桶排序 */
fn bucket_sort(nums: &mut [f64]) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
let k = nums.len() / 2;
let mut buckets = vec![vec![]; k];
// 1. 將陣列元素分配到各個桶中
for &mut num in &mut *nums {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
let i = (num * k as f64) as usize;
// 將 num 新增進桶 i
buckets[i].push(num);
}
// 2. 對各個桶執行排序
for bucket in &mut buckets {
// 使用內建排序函式,也可以替換成其他排序演算法
bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());
}
// 3. 走訪桶合併結果
let mut i = 0;
for bucket in &mut buckets {
for &mut num in bucket {
nums[i] = num;
i += 1;
}
}
}
```
=== "C"
```c title="bucket_sort.c"
/* 桶排序 */
void bucketSort(float nums[], int size) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
int k = size / 2;
float **buckets = calloc(k, sizeof(float *));
for (int i = 0; i < k; i++) {
// 每個桶最多可以分配 size 個元素
buckets[i] = calloc(size, sizeof(float));
}
// 1. 將陣列元素分配到各個桶中
for (int i = 0; i < size; i++) {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
int bucket_idx = nums[i] * k;
int j = 0;
// 如果桶中有資料且資料小於當前值 nums[i], 要將其放到當前桶的後面,相當於 cpp 中的 push_back
while (buckets[bucket_idx][j] > 0 && buckets[bucket_idx][j] < nums[i]) {
j++;
}
float temp = nums[i];
while (j < size && buckets[bucket_idx][j] > 0) {
swap(&temp, &buckets[bucket_idx][j]);
j++;
}
buckets[bucket_idx][j] = temp;
}
// 2. 對各個桶執行排序
for (int i = 0; i < k; i++) {
qsort(buckets[i], size, sizeof(float), compare_float);
}
// 3. 走訪桶合併結果
for (int i = 0, j = 0; j < k; j++) {
for (int l = 0; l < size; l++) {
if (buckets[j][l] > 0) {
nums[i++] = buckets[j][l];
}
}
}
// 釋放上述分配的記憶體
for (int i = 0; i < k; i++) {
free(buckets[i]);
}
free(buckets);
}
```
=== "Kotlin"
```kotlin title="bucket_sort.kt"
/* 桶排序 */
fun bucketSort(nums: FloatArray) {
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
val k = nums.size / 2
val buckets = ArrayList<ArrayList<Float>>()
for (i in 0..<k) {
buckets.add(ArrayList())
}
// 1. 將陣列元素分配到各個桶中
for (num in nums) {
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
val i = (num * k).toInt()
// 將 num 新增進桶 i
buckets[i].add(num)
}
// 2. 對各個桶執行排序
for (bucket in buckets) {
// 使用內建排序函式,也可以替換成其他排序演算法
bucket.sort()
}
// 3. 走訪桶合併結果
var i = 0
for (bucket in buckets) {
for (num in bucket) {
nums[i++] = num
}
}
}
```
=== "Ruby"
```ruby title="bucket_sort.rb"
[class]{}-[func]{bucket_sort}
```
=== "Zig"
```zig title="bucket_sort.zig"
[class]{}-[func]{bucketSort}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20bucket_sort%28nums%3A%20list%5Bfloat%5D%29%3A%0A%20%20%20%20%22%22%22%E6%A1%B6%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20k%20%3D%20n/2%20%E4%B8%AA%E6%A1%B6%EF%BC%8C%E9%A2%84%E6%9C%9F%E5%90%91%E6%AF%8F%E4%B8%AA%E6%A1%B6%E5%88%86%E9%85%8D%202%20%E4%B8%AA%E5%85%83%E7%B4%A0%0A%20%20%20%20k%20%3D%20len%28nums%29%20//%202%0A%20%20%20%20buckets%20%3D%20%5B%5B%5D%20for%20_%20in%20range%28k%29%5D%0A%20%20%20%20%23%201.%20%E5%B0%86%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%E5%88%86%E9%85%8D%E5%88%B0%E5%90%84%E4%B8%AA%E6%A1%B6%E4%B8%AD%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E8%8C%83%E5%9B%B4%E4%B8%BA%20%5B0,%201%29%EF%BC%8C%E4%BD%BF%E7%94%A8%20num%20*%20k%20%E6%98%A0%E5%B0%84%E5%88%B0%E7%B4%A2%E5%BC%95%E8%8C%83%E5%9B%B4%20%5B0,%20k-1%5D%0A%20%20%20%20%20%20%20%20i%20%3D%20int%28num%20*%20k%29%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%20num%20%E6%B7%BB%E5%8A%A0%E8%BF%9B%E6%A1%B6%20i%0A%20%20%20%20%20%20%20%20buckets%5Bi%5D.append%28num%29%0A%20%20%20%20%23%202.%20%E5%AF%B9%E5%90%84%E4%B8%AA%E6%A1%B6%E6%89%A7%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%86%85%E7%BD%AE%E6%8E%92%E5%BA%8F%E5%87%BD%E6%95%B0%EF%BC%8C%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%9B%BF%E6%8D%A2%E6%88%90%E5%85%B6%E4%BB%96%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%0A%20%20%20%20%20%20%20%20bucket.sort%28%29%0A%20%20%20%20%23%203.%20%E9%81%8D%E5%8E%86%E6%A1%B6%E5%90%88%E5%B9%B6%E7%BB%93%E6%9E%9C%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20for%20num%20in%20bucket%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%AE%BE%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E4%B8%BA%E6%B5%AE%E7%82%B9%E6%95%B0%EF%BC%8C%E8%8C%83%E5%9B%B4%E4%B8%BA%20%5B0,%201%29%0A%20%20%20%20nums%20%3D%20%5B0.49,%200.96,%200.82,%200.09,%200.57,%200.43,%200.91,%200.75,%200.15,%200.37%5D%0A%20%20%20%20bucket_sort%28nums%29%0A%20%20%20%20print%28%22%E6%A1%B6%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bucket_sort%28nums%3A%20list%5Bfloat%5D%29%3A%0A%20%20%20%20%22%22%22%E6%A1%B6%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20k%20%3D%20n/2%20%E4%B8%AA%E6%A1%B6%EF%BC%8C%E9%A2%84%E6%9C%9F%E5%90%91%E6%AF%8F%E4%B8%AA%E6%A1%B6%E5%88%86%E9%85%8D%202%20%E4%B8%AA%E5%85%83%E7%B4%A0%0A%20%20%20%20k%20%3D%20len%28nums%29%20//%202%0A%20%20%20%20buckets%20%3D%20%5B%5B%5D%20for%20_%20in%20range%28k%29%5D%0A%20%20%20%20%23%201.%20%E5%B0%86%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%E5%88%86%E9%85%8D%E5%88%B0%E5%90%84%E4%B8%AA%E6%A1%B6%E4%B8%AD%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E8%8C%83%E5%9B%B4%E4%B8%BA%20%5B0,%201%29%EF%BC%8C%E4%BD%BF%E7%94%A8%20num%20*%20k%20%E6%98%A0%E5%B0%84%E5%88%B0%E7%B4%A2%E5%BC%95%E8%8C%83%E5%9B%B4%20%5B0,%20k-1%5D%0A%20%20%20%20%20%20%20%20i%20%3D%20int%28num%20*%20k%29%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%20num%20%E6%B7%BB%E5%8A%A0%E8%BF%9B%E6%A1%B6%20i%0A%20%20%20%20%20%20%20%20buckets%5Bi%5D.append%28num%29%0A%20%20%20%20%23%202.%20%E5%AF%B9%E5%90%84%E4%B8%AA%E6%A1%B6%E6%89%A7%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%86%85%E7%BD%AE%E6%8E%92%E5%BA%8F%E5%87%BD%E6%95%B0%EF%BC%8C%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%9B%BF%E6%8D%A2%E6%88%90%E5%85%B6%E4%BB%96%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%0A%20%20%20%20%20%20%20%20bucket.sort%28%29%0A%20%20%20%20%23%203.%20%E9%81%8D%E5%8E%86%E6%A1%B6%E5%90%88%E5%B9%B6%E7%BB%93%E6%9E%9C%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20bucket%20in%20buckets%3A%0A%20%20%20%20%20%20%20%20for%20num%20in%20bucket%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%AE%BE%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E4%B8%BA%E6%B5%AE%E7%82%B9%E6%95%B0%EF%BC%8C%E8%8C%83%E5%9B%B4%E4%B8%BA%20%5B0,%201%29%0A%20%20%20%20nums%20%3D%20%5B0.49,%200.96,%200.82,%200.09,%200.57,%200.43,%200.91,%200.75,%200.15,%200.37%5D%0A%20%20%20%20bucket_sort%28nums%29%0A%20%20%20%20print%28%22%E6%A1%B6%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.8.2 &nbsp; 演算法特性
桶排序適用於處理體量很大的資料。例如,輸入資料包含 100 萬個元素,由於空間限制,系統記憶體無法一次性載入所有資料。此時,可以將資料分成 1000 個桶,然後分別對每個桶進行排序,最後將結果合併。
- **時間複雜度為 $O(n + k)$** :假設元素在各個桶內平均分佈,那麼每個桶內的元素數量為 $\frac{n}{k}$ 。假設排序單個桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 時間,則排序所有桶使用 $O(n \log\frac{n}{k})$ 時間。**當桶數量 $k$ 比較大時,時間複雜度則趨向於 $O(n)$** 。合併結果時需要走訪所有桶和元素,花費 $O(n + k)$ 時間。
- **自適應排序**:在最差情況下,所有資料被分配到一個桶中,且排序該桶使用 $O(n^2)$ 時間。
- **空間複雜度為 $O(n + k)$、非原地排序**:需要藉助 $k$ 個桶和總共 $n$ 個元素的額外空間。
- 桶排序是否穩定取決於排序桶內元素的演算法是否穩定。
## 11.8.3 &nbsp; 如何實現平均分配
桶排序的時間複雜度理論上可以達到 $O(n)$ ,**關鍵在於將元素均勻分配到各個桶中**,因為實際資料往往不是均勻分佈的。例如,我們想要將淘寶上的所有商品按價格範圍平均分配到 10 個桶中,但商品價格分佈不均,低於 100 元的非常多,高於 1000 元的非常少。若將價格區間平均劃分為 10 個,各個桶中的商品數量差距會非常大。
為實現平均分配,我們可以先設定一條大致的分界線,將資料粗略地分到 3 個桶中。**分配完畢後,再將商品較多的桶繼續劃分為 3 個桶,直至所有桶中的元素數量大致相等**。
如圖 11-14 所示,這種方法本質上是建立一棵遞迴樹,目標是讓葉節點的值儘可能平均。當然,不一定要每輪將資料劃分為 3 個桶,具體劃分方式可根據資料特點靈活選擇。
![遞迴劃分桶](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }
<p align="center"> 圖 11-14 &nbsp; 遞迴劃分桶 </p>
如果我們提前知道商品價格的機率分佈,**則可以根據資料機率分佈設定每個桶的價格分界線**。值得注意的是,資料分佈並不一定需要特意統計,也可以根據資料特點採用某種機率模型進行近似。
如圖 11-15 所示,我們假設商品價格服從正態分佈,這樣就可以合理地設定價格區間,從而將商品平均分配到各個桶中。
![根據機率分佈劃分桶](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" }
<p align="center"> 圖 11-15 &nbsp; 根據機率分佈劃分桶 </p>
@@ -0,0 +1,883 @@
---
comments: true
---
# 11.9 &nbsp; 計數排序
<u>計數排序(counting sort</u>透過統計元素數量來實現排序,通常應用於整數陣列。
## 11.9.1 &nbsp; 簡單實現
先來看一個簡單的例子。給定一個長度為 $n$ 的陣列 `nums` ,其中的元素都是“非負整數”,計數排序的整體流程如圖 11-16 所示。
1. 走訪陣列,找出其中的最大數字,記為 $m$ ,然後建立一個長度為 $m + 1$ 的輔助陣列 `counter`
2. **藉助 `counter` 統計 `nums` 中各數字的出現次數**,其中 `counter[num]` 對應數字 `num` 的出現次數。統計方法很簡單,只需走訪 `nums`(設當前數字為 `num`),每輪將 `counter[num]` 增加 $1$ 即可。
3. **由於 `counter` 的各個索引天然有序,因此相當於所有數字已經排序好了**。接下來,我們走訪 `counter` ,根據各數字出現次數從小到大的順序填入 `nums` 即可。
![計數排序流程](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" }
<p align="center"> 圖 11-16 &nbsp; 計數排序流程 </p>
程式碼如下所示:
=== "Python"
```python title="counting_sort.py"
def counting_sort_naive(nums: list[int]):
"""計數排序"""
# 簡單實現,無法用於排序物件
# 1. 統計陣列最大元素 m
m = 0
for num in nums:
m = max(m, num)
# 2. 統計各數字的出現次數
# counter[num] 代表 num 的出現次數
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. 走訪 counter ,將各元素填入原陣列 nums
i = 0
for num in range(m + 1):
for _ in range(counter[num]):
nums[i] = num
i += 1
```
=== "C++"
```cpp title="counting_sort.cpp"
/* 計數排序 */
// 簡單實現,無法用於排序物件
void countingSortNaive(vector<int> &nums) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Java"
```java title="counting_sort.java"
/* 計數排序 */
// 簡單實現,無法用於排序物件
void countingSortNaive(int[] nums) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "C#"
```csharp title="counting_sort.cs"
/* 計數排序 */
// 簡單實現,無法用於排序物件
void CountingSortNaive(int[] nums) {
// 1. 統計陣列最大元素 m
int m = 0;
foreach (int num in nums) {
m = Math.Max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
int[] counter = new int[m + 1];
foreach (int num in nums) {
counter[num]++;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Go"
```go title="counting_sort.go"
/* 計數排序 */
// 簡單實現,無法用於排序物件
func countingSortNaive(nums []int) {
// 1. 統計陣列最大元素 m
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
for i, num := 0, 0; num < m+1; num++ {
for j := 0; j < counter[num]; j++ {
nums[i] = num
i++
}
}
}
```
=== "Swift"
```swift title="counting_sort.swift"
/* 計數排序 */
// 簡單實現,無法用於排序物件
func countingSortNaive(nums: inout [Int]) {
// 1. 統計陣列最大元素 m
let m = nums.max()!
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
var counter = Array(repeating: 0, count: m + 1)
for num in nums {
counter[num] += 1
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
var i = 0
for num in 0 ..< m + 1 {
for _ in 0 ..< counter[num] {
nums[i] = num
i += 1
}
}
}
```
=== "JS"
```javascript title="counting_sort.js"
/* 計數排序 */
// 簡單實現,無法用於排序物件
function countingSortNaive(nums) {
// 1. 統計陣列最大元素 m
let m = 0;
for (const num of nums) {
m = Math.max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
const counter = new Array(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
let i = 0;
for (let num = 0; num < m + 1; num++) {
for (let j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "TS"
```typescript title="counting_sort.ts"
/* 計數排序 */
// 簡單實現,無法用於排序物件
function countingSortNaive(nums: number[]): void {
// 1. 統計陣列最大元素 m
let m = 0;
for (const num of nums) {
m = Math.max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
let i = 0;
for (let num = 0; num < m + 1; num++) {
for (let j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Dart"
```dart title="counting_sort.dart"
/* 計數排序 */
// 簡單實現,無法用於排序物件
void countingSortNaive(List<int> nums) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int _num in nums) {
m = max(m, _num);
}
// 2. 統計各數字的出現次數
// counter[_num] 代表 _num 的出現次數
List<int> counter = List.filled(m + 1, 0);
for (int _num in nums) {
counter[_num]++;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
int i = 0;
for (int _num = 0; _num < m + 1; _num++) {
for (int j = 0; j < counter[_num]; j++, i++) {
nums[i] = _num;
}
}
}
```
=== "Rust"
```rust title="counting_sort.rs"
/* 計數排序 */
// 簡單實現,無法用於排序物件
fn counting_sort_naive(nums: &mut [i32]) {
// 1. 統計陣列最大元素 m
let m = *nums.into_iter().max().unwrap();
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
let mut counter = vec![0; m as usize + 1];
for &num in &*nums {
counter[num as usize] += 1;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
let mut i = 0;
for num in 0..m + 1 {
for _ in 0..counter[num as usize] {
nums[i] = num;
i += 1;
}
}
}
```
=== "C"
```c title="counting_sort.c"
/* 計數排序 */
// 簡單實現,無法用於排序物件
void countingSortNaive(int nums[], int size) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int i = 0; i < size; i++) {
if (nums[i] > m) {
m = nums[i];
}
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
int *counter = calloc(m + 1, sizeof(int));
for (int i = 0; i < size; i++) {
counter[nums[i]]++;
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
// 4. 釋放記憶體
free(counter);
}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
/* 計數排序 */
// 簡單實現,無法用於排序物件
fun countingSortNaive(nums: IntArray) {
// 1. 統計陣列最大元素 m
var m = 0
for (num in nums) {
m = max(m.toDouble(), num.toDouble()).toInt()
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
val counter = IntArray(m + 1)
for (num in nums) {
counter[num]++
}
// 3. 走訪 counter ,將各元素填入原陣列 nums
var i = 0
for (num in 0..<m + 1) {
var j = 0
while (j < counter[num]) {
nums[i] = num
j++
i++
}
}
}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort_naive}
```
=== "Zig"
```zig title="counting_sort.zig"
[class]{}-[func]{countingSortNaive}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort_naive%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0%EF%BC%8C%E6%97%A0%E6%B3%95%E7%94%A8%E4%BA%8E%E6%8E%92%E5%BA%8F%E5%AF%B9%E8%B1%A1%0A%20%20%20%20%23%201.%20%E7%BB%9F%E8%AE%A1%E6%95%B0%E7%BB%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%20m%0A%20%20%20%20m%20%3D%200%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20max%28m,%20num%29%0A%20%20%20%20%23%202.%20%E7%BB%9F%E8%AE%A1%E5%90%84%E6%95%B0%E5%AD%97%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E4%BB%A3%E8%A1%A8%20num%20%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20counter%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E9%81%8D%E5%8E%86%20counter%20%EF%BC%8C%E5%B0%86%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20num%20in%20range%28m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20_%20in%20range%28counter%5Bnum%5D%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1,%200,%201,%202,%200,%204,%200,%202,%202,%204%5D%0A%20%20%20%20counting_sort_naive%28nums%29%0A%20%20%20%20print%28f%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%EF%BC%88%E6%97%A0%E6%B3%95%E6%8E%92%E5%BA%8F%E5%AF%B9%E8%B1%A1%EF%BC%89%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort_naive%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0%EF%BC%8C%E6%97%A0%E6%B3%95%E7%94%A8%E4%BA%8E%E6%8E%92%E5%BA%8F%E5%AF%B9%E8%B1%A1%0A%20%20%20%20%23%201.%20%E7%BB%9F%E8%AE%A1%E6%95%B0%E7%BB%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%20m%0A%20%20%20%20m%20%3D%200%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20max%28m,%20num%29%0A%20%20%20%20%23%202.%20%E7%BB%9F%E8%AE%A1%E5%90%84%E6%95%B0%E5%AD%97%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E4%BB%A3%E8%A1%A8%20num%20%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20counter%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E9%81%8D%E5%8E%86%20counter%20%EF%BC%8C%E5%B0%86%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20for%20num%20in%20range%28m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20_%20in%20range%28counter%5Bnum%5D%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20num%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1,%200,%201,%202,%200,%204,%200,%202,%202,%204%5D%0A%20%20%20%20counting_sort_naive%28nums%29%0A%20%20%20%20print%28f%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%EF%BC%88%E6%97%A0%E6%B3%95%E6%8E%92%E5%BA%8F%E5%AF%B9%E8%B1%A1%EF%BC%89%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
!!! note "計數排序與桶排序的關聯"
從桶排序的角度看,我們可以將計數排序中的計數陣列 `counter` 的每個索引視為一個桶,將統計數量的過程看作將各個元素分配到對應的桶中。本質上,計數排序是桶排序在整型資料下的一個特例。
## 11.9.2 &nbsp; 完整實現
細心的讀者可能發現了,**如果輸入資料是物件,上述步驟 `3.` 就失效了**。假設輸入資料是商品物件,我們想按照商品價格(類別的成員變數)對商品進行排序,而上述演算法只能給出價格的排序結果。
那麼如何才能得到原資料的排序結果呢?我們首先計算 `counter` 的“前綴和”。顧名思義,索引 `i` 處的前綴和 `prefix[i]` 等於陣列前 `i` 個元素之和:
$$
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
$$
**前綴和具有明確的意義,`prefix[num] - 1` 代表元素 `num` 在結果陣列 `res` 中最後一次出現的索引**。這個資訊非常關鍵,因為它告訴我們各個元素應該出現在結果陣列的哪個位置。接下來,我們倒序走訪原陣列 `nums` 的每個元素 `num` ,在每輪迭代中執行以下兩步。
1. 將 `num` 填入陣列 `res` 的索引 `prefix[num] - 1` 處。
2. 令前綴和 `prefix[num]` 減小 $1$ ,從而得到下次放置 `num` 的索引。
走訪完成後,陣列 `res` 中就是排序好的結果,最後使用 `res` 覆蓋原陣列 `nums` 即可。圖 11-17 展示了完整的計數排序流程。
=== "<1>"
![計數排序步驟](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
=== "<2>"
![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" }
=== "<3>"
![counting_sort_step3](counting_sort.assets/counting_sort_step3.png){ class="animation-figure" }
=== "<4>"
![counting_sort_step4](counting_sort.assets/counting_sort_step4.png){ class="animation-figure" }
=== "<5>"
![counting_sort_step5](counting_sort.assets/counting_sort_step5.png){ class="animation-figure" }
=== "<6>"
![counting_sort_step6](counting_sort.assets/counting_sort_step6.png){ class="animation-figure" }
=== "<7>"
![counting_sort_step7](counting_sort.assets/counting_sort_step7.png){ class="animation-figure" }
=== "<8>"
![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" }
<p align="center"> 圖 11-17 &nbsp; 計數排序步驟 </p>
計數排序的實現程式碼如下所示:
=== "Python"
```python title="counting_sort.py"
def counting_sort(nums: list[int]):
"""計數排序"""
# 完整實現,可排序物件,並且是穩定排序
# 1. 統計陣列最大元素 m
m = max(nums)
# 2. 統計各數字的出現次數
# counter[num] 代表 num 的出現次數
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
# 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for i in range(m):
counter[i + 1] += counter[i]
# 4. 倒序走訪 nums ,將各元素填入結果陣列 res
# 初始化陣列 res 用於記錄結果
n = len(nums)
res = [0] * n
for i in range(n - 1, -1, -1):
num = nums[i]
res[counter[num] - 1] = num # 將 num 放置到對應索引處
counter[num] -= 1 # 令前綴和自減 1 ,得到下次放置 num 的索引
# 使用結果陣列 res 覆蓋原陣列 nums
for i in range(n):
nums[i] = res[i]
```
=== "C++"
```cpp title="counting_sort.cpp"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
void countingSort(vector<int> &nums) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
int n = nums.size();
vector<int> res(n);
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
nums = res;
}
```
=== "Java"
```java title="counting_sort.java"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
void countingSort(int[] nums) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
int n = nums.length;
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "C#"
```csharp title="counting_sort.cs"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
void CountingSort(int[] nums) {
// 1. 統計陣列最大元素 m
int m = 0;
foreach (int num in nums) {
m = Math.Max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
int[] counter = new int[m + 1];
foreach (int num in nums) {
counter[num]++;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
int n = nums.Length;
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "Go"
```go title="counting_sort.go"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
func countingSort(nums []int) {
// 1. 統計陣列最大元素 m
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for i := 0; i < m; i++ {
counter[i+1] += counter[i]
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
n := len(nums)
res := make([]int, n)
for i := n - 1; i >= 0; i-- {
num := nums[i]
// 將 num 放置到對應索引處
res[counter[num]-1] = num
// 令前綴和自減 1 ,得到下次放置 num 的索引
counter[num]--
}
// 使用結果陣列 res 覆蓋原陣列 nums
copy(nums, res)
}
```
=== "Swift"
```swift title="counting_sort.swift"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
func countingSort(nums: inout [Int]) {
// 1. 統計陣列最大元素 m
let m = nums.max()!
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
var counter = Array(repeating: 0, count: m + 1)
for num in nums {
counter[num] += 1
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for i in 0 ..< m {
counter[i + 1] += counter[i]
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
var res = Array(repeating: 0, count: nums.count)
for i in nums.indices.reversed() {
let num = nums[i]
res[counter[num] - 1] = num // 將 num 放置到對應索引處
counter[num] -= 1 // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
for i in nums.indices {
nums[i] = res[i]
}
}
```
=== "JS"
```javascript title="counting_sort.js"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
function countingSort(nums) {
// 1. 統計陣列最大元素 m
let m = 0;
for (const num of nums) {
m = Math.max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
const counter = new Array(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for (let i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
const n = nums.length;
const res = new Array(n);
for (let i = n - 1; i >= 0; i--) {
const num = nums[i];
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "TS"
```typescript title="counting_sort.ts"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
function countingSort(nums: number[]): void {
// 1. 統計陣列最大元素 m
let m = 0;
for (const num of nums) {
m = Math.max(m, num);
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for (let i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
const n = nums.length;
const res: number[] = new Array<number>(n);
for (let i = n - 1; i >= 0; i--) {
const num = nums[i];
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "Dart"
```dart title="counting_sort.dart"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
void countingSort(List<int> nums) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int _num in nums) {
m = max(m, _num);
}
// 2. 統計各數字的出現次數
// counter[_num] 代表 _num 的出現次數
List<int> counter = List.filled(m + 1, 0);
for (int _num in nums) {
counter[_num]++;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[_num]-1 是 _num 在 res 中最後一次出現的索引
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
int n = nums.length;
List<int> res = List.filled(n, 0);
for (int i = n - 1; i >= 0; i--) {
int _num = nums[i];
res[counter[_num] - 1] = _num; // 將 _num 放置到對應索引處
counter[_num]--; // 令前綴和自減 1 ,得到下次放置 _num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
nums.setAll(0, res);
}
```
=== "Rust"
```rust title="counting_sort.rs"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
fn counting_sort(nums: &mut [i32]) {
// 1. 統計陣列最大元素 m
let m = *nums.into_iter().max().unwrap();
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
let mut counter = vec![0; m as usize + 1];
for &num in &*nums {
counter[num as usize] += 1;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for i in 0..m as usize {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
let n = nums.len();
let mut res = vec![0; n];
for i in (0..n).rev() {
let num = nums[i];
res[counter[num as usize] - 1] = num; // 將 num 放置到對應索引處
counter[num as usize] -= 1; // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
for i in 0..n {
nums[i] = res[i];
}
}
```
=== "C"
```c title="counting_sort.c"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
void countingSort(int nums[], int size) {
// 1. 統計陣列最大元素 m
int m = 0;
for (int i = 0; i < size; i++) {
if (nums[i] > m) {
m = nums[i];
}
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
int *counter = calloc(m, sizeof(int));
for (int i = 0; i < size; i++) {
counter[nums[i]]++;
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
int *res = malloc(sizeof(int) * size);
for (int i = size - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
memcpy(nums, res, size * sizeof(int));
// 5. 釋放記憶體
free(counter);
}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
/* 計數排序 */
// 完整實現,可排序物件,並且是穩定排序
fun countingSort(nums: IntArray) {
// 1. 統計陣列最大元素 m
var m = 0
for (num in nums) {
m = max(m.toDouble(), num.toDouble()).toInt()
}
// 2. 統計各數字的出現次數
// counter[num] 代表 num 的出現次數
val counter = IntArray(m + 1)
for (num in nums) {
counter[num]++
}
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
for (i in 0..<m) {
counter[i + 1] += counter[i]
}
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
// 初始化陣列 res 用於記錄結果
val n = nums.size
val res = IntArray(n)
for (i in n - 1 downTo 0) {
val num = nums[i]
res[counter[num] - 1] = num // 將 num 放置到對應索引處
counter[num]-- // 令前綴和自減 1 ,得到下次放置 num 的索引
}
// 使用結果陣列 res 覆蓋原陣列 nums
for (i in 0..<n) {
nums[i] = res[i]
}
}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort}
```
=== "Zig"
```zig title="counting_sort.zig"
[class]{}-[func]{countingSort}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%8E%B0%EF%BC%8C%E5%8F%AF%E6%8E%92%E5%BA%8F%E5%AF%B9%E8%B1%A1%EF%BC%8C%E5%B9%B6%E4%B8%94%E6%98%AF%E7%A8%B3%E5%AE%9A%E6%8E%92%E5%BA%8F%0A%20%20%20%20%23%201.%20%E7%BB%9F%E8%AE%A1%E6%95%B0%E7%BB%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%20m%0A%20%20%20%20m%20%3D%20max%28nums%29%0A%20%20%20%20%23%202.%20%E7%BB%9F%E8%AE%A1%E5%90%84%E6%95%B0%E5%AD%97%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E4%BB%A3%E8%A1%A8%20num%20%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20counter%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E6%B1%82%20counter%20%E7%9A%84%E5%89%8D%E7%BC%80%E5%92%8C%EF%BC%8C%E5%B0%86%E2%80%9C%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%E2%80%9D%E8%BD%AC%E6%8D%A2%E4%B8%BA%E2%80%9C%E5%B0%BE%E7%B4%A2%E5%BC%95%E2%80%9D%0A%20%20%20%20%23%20%E5%8D%B3%20counter%5Bnum%5D-1%20%E6%98%AF%20num%20%E5%9C%A8%20res%20%E4%B8%AD%E6%9C%80%E5%90%8E%E4%B8%80%E6%AC%A1%E5%87%BA%E7%8E%B0%E7%9A%84%E7%B4%A2%E5%BC%95%0A%20%20%20%20for%20i%20in%20range%28m%29%3A%0A%20%20%20%20%20%20%20%20counter%5Bi%20%2B%201%5D%20%2B%3D%20counter%5Bi%5D%0A%20%20%20%20%23%204.%20%E5%80%92%E5%BA%8F%E9%81%8D%E5%8E%86%20nums%20%EF%BC%8C%E5%B0%86%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E7%BB%93%E6%9E%9C%E6%95%B0%E7%BB%84%20res%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%20res%20%E7%94%A8%E4%BA%8E%E8%AE%B0%E5%BD%95%E7%BB%93%E6%9E%9C%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20res%20%3D%20%5B0%5D%20*%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20num%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20res%5Bcounter%5Bnum%5D%20-%201%5D%20%3D%20num%20%20%23%20%E5%B0%86%20num%20%E6%94%BE%E7%BD%AE%E5%88%B0%E5%AF%B9%E5%BA%94%E7%B4%A2%E5%BC%95%E5%A4%84%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20-%3D%201%20%20%23%20%E4%BB%A4%E5%89%8D%E7%BC%80%E5%92%8C%E8%87%AA%E5%87%8F%201%20%EF%BC%8C%E5%BE%97%E5%88%B0%E4%B8%8B%E6%AC%A1%E6%94%BE%E7%BD%AE%20num%20%E7%9A%84%E7%B4%A2%E5%BC%95%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E7%BB%93%E6%9E%9C%E6%95%B0%E7%BB%84%20res%20%E8%A6%86%E7%9B%96%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20res%5Bi%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1,%200,%201,%202,%200,%204,%200,%202,%202,%204%5D%0A%20%20%20%20counting_sort%28nums%29%0A%20%20%20%20print%28f%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%8E%B0%EF%BC%8C%E5%8F%AF%E6%8E%92%E5%BA%8F%E5%AF%B9%E8%B1%A1%EF%BC%8C%E5%B9%B6%E4%B8%94%E6%98%AF%E7%A8%B3%E5%AE%9A%E6%8E%92%E5%BA%8F%0A%20%20%20%20%23%201.%20%E7%BB%9F%E8%AE%A1%E6%95%B0%E7%BB%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%20m%0A%20%20%20%20m%20%3D%20max%28nums%29%0A%20%20%20%20%23%202.%20%E7%BB%9F%E8%AE%A1%E5%90%84%E6%95%B0%E5%AD%97%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20%23%20counter%5Bnum%5D%20%E4%BB%A3%E8%A1%A8%20num%20%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20counter%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E6%B1%82%20counter%20%E7%9A%84%E5%89%8D%E7%BC%80%E5%92%8C%EF%BC%8C%E5%B0%86%E2%80%9C%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%E2%80%9D%E8%BD%AC%E6%8D%A2%E4%B8%BA%E2%80%9C%E5%B0%BE%E7%B4%A2%E5%BC%95%E2%80%9D%0A%20%20%20%20%23%20%E5%8D%B3%20counter%5Bnum%5D-1%20%E6%98%AF%20num%20%E5%9C%A8%20res%20%E4%B8%AD%E6%9C%80%E5%90%8E%E4%B8%80%E6%AC%A1%E5%87%BA%E7%8E%B0%E7%9A%84%E7%B4%A2%E5%BC%95%0A%20%20%20%20for%20i%20in%20range%28m%29%3A%0A%20%20%20%20%20%20%20%20counter%5Bi%20%2B%201%5D%20%2B%3D%20counter%5Bi%5D%0A%20%20%20%20%23%204.%20%E5%80%92%E5%BA%8F%E9%81%8D%E5%8E%86%20nums%20%EF%BC%8C%E5%B0%86%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E7%BB%93%E6%9E%9C%E6%95%B0%E7%BB%84%20res%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%20res%20%E7%94%A8%E4%BA%8E%E8%AE%B0%E5%BD%95%E7%BB%93%E6%9E%9C%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20res%20%3D%20%5B0%5D%20*%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20num%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20res%5Bcounter%5Bnum%5D%20-%201%5D%20%3D%20num%20%20%23%20%E5%B0%86%20num%20%E6%94%BE%E7%BD%AE%E5%88%B0%E5%AF%B9%E5%BA%94%E7%B4%A2%E5%BC%95%E5%A4%84%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20-%3D%201%20%20%23%20%E4%BB%A4%E5%89%8D%E7%BC%80%E5%92%8C%E8%87%AA%E5%87%8F%201%20%EF%BC%8C%E5%BE%97%E5%88%B0%E4%B8%8B%E6%AC%A1%E6%94%BE%E7%BD%AE%20num%20%E7%9A%84%E7%B4%A2%E5%BC%95%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E7%BB%93%E6%9E%9C%E6%95%B0%E7%BB%84%20res%20%E8%A6%86%E7%9B%96%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20res%5Bi%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1,%200,%201,%202,%200,%204,%200,%202,%202,%204%5D%0A%20%20%20%20counting_sort%28nums%29%0A%20%20%20%20print%28f%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.9.3 &nbsp; 演算法特性
- **時間複雜度為 $O(n + m)$** :涉及走訪 `nums` 和走訪 `counter` ,都使用線性時間。一般情況下 $n \gg m$ ,時間複雜度趨於 $O(n)$ 。
- **空間複雜度為 $O(n + m)$、非原地排序**:藉助了長度分別為 $n$ 和 $m$ 的陣列 `res``counter`
- **穩定排序**:由於向 `res` 中填充元素的順序是“從右向左”的,因此倒序走訪 `nums` 可以避免改變相等元素之間的相對位置,從而實現穩定排序。實際上,正序走訪 `nums` 也可以得到正確的排序結果,但結果是非穩定的。
## 11.9.4 &nbsp; 侷限性
看到這裡,你也許會覺得計數排序非常巧妙,僅透過統計數量就可以實現高效的排序。然而,使用計數排序的前置條件相對較為嚴格。
**計數排序只適用於非負整數**。若想將其用於其他型別的資料,需要確保這些資料可以轉換為非負整數,並且在轉換過程中不能改變各個元素之間的相對大小關係。例如,對於包含負數的整數陣列,可以先給所有數字加上一個常數,將全部數字轉化為正數,排序完成後再轉換回去。
**計數排序適用於資料量大但資料範圍較小的情況**。比如,在上述示例中 $m$ 不能太大,否則會佔用過多空間。而當 $n \ll m$ 時,計數排序使用 $O(m)$ 時間,可能比 $O(n \log n)$ 的排序演算法還要慢。
+600
View File
@@ -0,0 +1,600 @@
---
comments: true
---
# 11.7 &nbsp; 堆積排序
!!! tip
閱讀本節前,請確保已學完“堆積“章節。
<u>堆積排序(heap sort</u>是一種基於堆積資料結構實現的高效排序演算法。我們可以利用已經學過的“建堆積操作”和“元素出堆積操作”實現堆積排序。
1. 輸入陣列並建立小頂堆積,此時最小元素位於堆積頂。
2. 不斷執行出堆積操作,依次記錄出堆積元素,即可得到從小到大排序的序列。
以上方法雖然可行,但需要藉助一個額外陣列來儲存彈出的元素,比較浪費空間。在實際中,我們通常使用一種更加優雅的實現方式。
## 11.7.1 &nbsp; 演算法流程
設陣列的長度為 $n$ ,堆積排序的流程如圖 11-12 所示。
1. 輸入陣列並建立大頂堆積。完成後,最大元素位於堆積頂。
2. 將堆積頂元素(第一個元素)與堆積底元素(最後一個元素)交換。完成交換後,堆積的長度減 $1$ ,已排序元素數量加 $1$ 。
3. 從堆積頂元素開始,從頂到底執行堆積化操作(sift down)。完成堆積化後,堆積的性質得到修復。
4. 迴圈執行第 `2.` 步和第 `3.` 步。迴圈 $n - 1$ 輪後,即可完成陣列排序。
!!! tip
實際上,元素出堆積操作中也包含第 `2.` 步和第 `3.` 步,只是多了一個彈出元素的步驟。
=== "<1>"
![堆積排序步驟](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" }
=== "<2>"
![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" }
=== "<3>"
![heap_sort_step3](heap_sort.assets/heap_sort_step3.png){ class="animation-figure" }
=== "<4>"
![heap_sort_step4](heap_sort.assets/heap_sort_step4.png){ class="animation-figure" }
=== "<5>"
![heap_sort_step5](heap_sort.assets/heap_sort_step5.png){ class="animation-figure" }
=== "<6>"
![heap_sort_step6](heap_sort.assets/heap_sort_step6.png){ class="animation-figure" }
=== "<7>"
![heap_sort_step7](heap_sort.assets/heap_sort_step7.png){ class="animation-figure" }
=== "<8>"
![heap_sort_step8](heap_sort.assets/heap_sort_step8.png){ class="animation-figure" }
=== "<9>"
![heap_sort_step9](heap_sort.assets/heap_sort_step9.png){ class="animation-figure" }
=== "<10>"
![heap_sort_step10](heap_sort.assets/heap_sort_step10.png){ class="animation-figure" }
=== "<11>"
![heap_sort_step11](heap_sort.assets/heap_sort_step11.png){ class="animation-figure" }
=== "<12>"
![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" }
<p align="center"> 圖 11-12 &nbsp; 堆積排序步驟 </p>
在程式碼實現中,我們使用了與“堆積”章節相同的從頂至底堆積化 `sift_down()` 函式。值得注意的是,由於堆積的長度會隨著提取最大元素而減小,因此我們需要給 `sift_down()` 函式新增一個長度參數 $n$ ,用於指定堆積的當前有效長度。程式碼如下所示:
=== "Python"
```python title="heap_sort.py"
def sift_down(nums: list[int], n: int, i: int):
"""堆積的長度為 n ,從節點 i 開始,從頂至底堆積化"""
while True:
# 判斷節點 i, l, r 中值最大的節點,記為 ma
l = 2 * i + 1
r = 2 * i + 2
ma = i
if l < n and nums[l] > nums[ma]:
ma = l
if r < n and nums[r] > nums[ma]:
ma = r
# 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if ma == i:
break
# 交換兩節點
nums[i], nums[ma] = nums[ma], nums[i]
# 迴圈向下堆積化
i = ma
def heap_sort(nums: list[int]):
"""堆積排序"""
# 建堆積操作:堆積化除葉節點以外的其他所有節點
for i in range(len(nums) // 2 - 1, -1, -1):
sift_down(nums, len(nums), i)
# 從堆積中提取最大元素,迴圈 n-1 輪
for i in range(len(nums) - 1, 0, -1):
# 交換根節點與最右葉節點(交換首元素與尾元素)
nums[0], nums[i] = nums[i], nums[0]
# 以根節點為起點,從頂至底進行堆積化
sift_down(nums, i, 0)
```
=== "C++"
```cpp title="heap_sort.cpp"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
void siftDown(vector<int> &nums, int n, int i) {
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma == i) {
break;
}
// 交換兩節點
swap(nums[i], nums[ma]);
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
void heapSort(vector<int> &nums) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (int i = nums.size() / 2 - 1; i >= 0; --i) {
siftDown(nums, nums.size(), i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (int i = nums.size() - 1; i > 0; --i) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
swap(nums[0], nums[i]);
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0);
}
}
```
=== "Java"
```java title="heap_sort.java"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
void siftDown(int[] nums, int n, int i) {
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma == i)
break;
// 交換兩節點
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
void heapSort(int[] nums) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (int i = nums.length / 2 - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (int i = nums.length - 1; i > 0; i--) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0);
}
}
```
=== "C#"
```csharp title="heap_sort.cs"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
void SiftDown(int[] nums, int n, int i) {
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma == i)
break;
// 交換兩節點
(nums[ma], nums[i]) = (nums[i], nums[ma]);
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
void HeapSort(int[] nums) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (int i = nums.Length / 2 - 1; i >= 0; i--) {
SiftDown(nums, nums.Length, i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (int i = nums.Length - 1; i > 0; i--) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
(nums[i], nums[0]) = (nums[0], nums[i]);
// 以根節點為起點,從頂至底進行堆積化
SiftDown(nums, i, 0);
}
}
```
=== "Go"
```go title="heap_sort.go"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
func siftDown(nums *[]int, n, i int) {
for true {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
l := 2*i + 1
r := 2*i + 2
ma := i
if l < n && (*nums)[l] > (*nums)[ma] {
ma = l
}
if r < n && (*nums)[r] > (*nums)[ma] {
ma = r
}
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if ma == i {
break
}
// 交換兩節點
(*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]
// 迴圈向下堆積化
i = ma
}
}
/* 堆積排序 */
func heapSort(nums *[]int) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for i := len(*nums)/2 - 1; i >= 0; i-- {
siftDown(nums, len(*nums), i)
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for i := len(*nums) - 1; i > 0; i-- {
// 交換根節點與最右葉節點(交換首元素與尾元素)
(*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0)
}
}
```
=== "Swift"
```swift title="heap_sort.swift"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
func siftDown(nums: inout [Int], n: Int, i: Int) {
var i = i
while true {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
let l = 2 * i + 1
let r = 2 * i + 2
var ma = i
if l < n, nums[l] > nums[ma] {
ma = l
}
if r < n, nums[r] > nums[ma] {
ma = r
}
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if ma == i {
break
}
// 交換兩節點
nums.swapAt(i, ma)
// 迴圈向下堆積化
i = ma
}
}
/* 堆積排序 */
func heapSort(nums: inout [Int]) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {
siftDown(nums: &nums, n: nums.count, i: i)
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for i in nums.indices.dropFirst().reversed() {
// 交換根節點與最右葉節點(交換首元素與尾元素)
nums.swapAt(0, i)
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums: &nums, n: i, i: 0)
}
}
```
=== "JS"
```javascript title="heap_sort.js"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
function siftDown(nums, n, i) {
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
let l = 2 * i + 1;
let r = 2 * i + 2;
let ma = i;
if (l < n && nums[l] > nums[ma]) {
ma = l;
}
if (r < n && nums[r] > nums[ma]) {
ma = r;
}
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma === i) {
break;
}
// 交換兩節點
[nums[i], nums[ma]] = [nums[ma], nums[i]];
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
function heapSort(nums) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (let i = nums.length - 1; i > 0; i--) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
[nums[0], nums[i]] = [nums[i], nums[0]];
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0);
}
}
```
=== "TS"
```typescript title="heap_sort.ts"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
function siftDown(nums: number[], n: number, i: number): void {
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
let l = 2 * i + 1;
let r = 2 * i + 2;
let ma = i;
if (l < n && nums[l] > nums[ma]) {
ma = l;
}
if (r < n && nums[r] > nums[ma]) {
ma = r;
}
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma === i) {
break;
}
// 交換兩節點
[nums[i], nums[ma]] = [nums[ma], nums[i]];
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
function heapSort(nums: number[]): void {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (let i = nums.length - 1; i > 0; i--) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
[nums[0], nums[i]] = [nums[i], nums[0]];
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0);
}
}
```
=== "Dart"
```dart title="heap_sort.dart"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
void siftDown(List<int> nums, int n, int i) {
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma]) ma = l;
if (r < n && nums[r] > nums[ma]) ma = r;
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma == i) break;
// 交換兩節點
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
void heapSort(List<int> nums) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (int i = nums.length - 1; i > 0; i--) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0);
}
}
```
=== "Rust"
```rust title="heap_sort.rs"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
fn sift_down(nums: &mut [i32], n: usize, mut i: usize) {
loop {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
let l = 2 * i + 1;
let r = 2 * i + 2;
let mut ma = i;
if l < n && nums[l] > nums[ma] {
ma = l;
}
if r < n && nums[r] > nums[ma] {
ma = r;
}
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if ma == i {
break;
}
// 交換兩節點
let temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
fn heap_sort(nums: &mut [i32]) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for i in (0..=nums.len() / 2 - 1).rev() {
sift_down(nums, nums.len(), i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for i in (1..=nums.len() - 1).rev() {
// 交換根節點與最右葉節點(交換首元素與尾元素)
let tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// 以根節點為起點,從頂至底進行堆積化
sift_down(nums, i, 0);
}
}
```
=== "C"
```c title="heap_sort.c"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
void siftDown(int nums[], int n, int i) {
while (1) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma == i) {
break;
}
// 交換兩節點
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// 迴圈向下堆積化
i = ma;
}
}
/* 堆積排序 */
void heapSort(int nums[], int n) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (int i = n / 2 - 1; i >= 0; --i) {
siftDown(nums, n, i);
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (int i = n - 1; i > 0; --i) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0);
}
}
```
=== "Kotlin"
```kotlin title="heap_sort.kt"
/* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */
fun siftDown(nums: IntArray, n: Int, li: Int) {
var i = li
while (true) {
// 判斷節點 i, l, r 中值最大的節點,記為 ma
val l = 2 * i + 1
val r = 2 * i + 2
var ma = i
if (l < n && nums[l] > nums[ma]) ma = l
if (r < n && nums[r] > nums[ma]) ma = r
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
if (ma == i) break
// 交換兩節點
nums[i] = nums[ma].also { nums[ma] = nums[i] }
// 迴圈向下堆積化
i = ma
}
}
/* 堆積排序 */
fun heapSort(nums: IntArray) {
// 建堆積操作:堆積化除葉節點以外的其他所有節點
for (i in nums.size / 2 - 1 downTo 0) {
siftDown(nums, nums.size, i)
}
// 從堆積中提取最大元素,迴圈 n-1 輪
for (i in nums.size - 1 downTo 1) {
// 交換根節點與最右葉節點(交換首元素與尾元素)
nums[0] = nums[i].also { nums[i] = nums[0] }
// 以根節點為起點,從頂至底進行堆積化
siftDown(nums, i, 0)
}
}
```
=== "Ruby"
```ruby title="heap_sort.rb"
[class]{}-[func]{sift_down}
[class]{}-[func]{heap_sort}
```
=== "Zig"
```zig title="heap_sort.zig"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20sift_down%28nums%3A%20list%5Bint%5D,%20n%3A%20int,%20i%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%A0%86%E7%9A%84%E9%95%BF%E5%BA%A6%E4%B8%BA%20n%20%EF%BC%8C%E4%BB%8E%E8%8A%82%E7%82%B9%20i%20%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E5%A0%86%E5%8C%96%22%22%22%0A%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E8%8A%82%E7%82%B9%20i,%20l,%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E8%8A%82%E7%82%B9%EF%BC%8C%E8%AE%B0%E4%B8%BA%20ma%0A%20%20%20%20%20%20%20%20l%20%3D%202%20*%20i%20%2B%201%0A%20%20%20%20%20%20%20%20r%20%3D%202%20*%20i%20%2B%202%0A%20%20%20%20%20%20%20%20ma%20%3D%20i%0A%20%20%20%20%20%20%20%20if%20l%20%3C%20n%20and%20nums%5Bl%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20if%20r%20%3C%20n%20and%20nums%5Br%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E8%8A%82%E7%82%B9%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l,%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%88%99%E6%97%A0%E9%A1%BB%E7%BB%A7%E7%BB%AD%E5%A0%86%E5%8C%96%EF%BC%8C%E8%B7%B3%E5%87%BA%0A%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E4%B8%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20nums%5Bi%5D,%20nums%5Bma%5D%20%3D%20nums%5Bma%5D,%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E5%90%91%E4%B8%8B%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0Adef%20heap_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%A0%86%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%BB%BA%E5%A0%86%E6%93%8D%E4%BD%9C%EF%BC%9A%E5%A0%86%E5%8C%96%E9%99%A4%E5%8F%B6%E8%8A%82%E7%82%B9%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E8%8A%82%E7%82%B9%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20//%202%20-%201,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20sift_down%28nums,%20len%28nums%29,%20i%29%0A%20%20%20%20%23%20%E4%BB%8E%E5%A0%86%E4%B8%AD%E6%8F%90%E5%8F%96%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E5%BE%AA%E7%8E%AF%20n-1%20%E8%BD%AE%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E6%A0%B9%E8%8A%82%E7%82%B9%E4%B8%8E%E6%9C%80%E5%8F%B3%E5%8F%B6%E8%8A%82%E7%82%B9%EF%BC%88%E4%BA%A4%E6%8D%A2%E9%A6%96%E5%85%83%E7%B4%A0%E4%B8%8E%E5%B0%BE%E5%85%83%E7%B4%A0%EF%BC%89%0A%20%20%20%20%20%20%20%20nums%5B0%5D,%20nums%5Bi%5D%20%3D%20nums%5Bi%5D,%20nums%5B0%5D%0A%20%20%20%20%20%20%20%20%23%20%E4%BB%A5%E6%A0%B9%E8%8A%82%E7%82%B9%E4%B8%BA%E8%B5%B7%E7%82%B9%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E8%BF%9B%E8%A1%8C%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20sift_down%28nums,%20i,%200%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20heap_sort%28nums%29%0A%20%20%20%20print%28%22%E5%A0%86%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20sift_down%28nums%3A%20list%5Bint%5D,%20n%3A%20int,%20i%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%A0%86%E7%9A%84%E9%95%BF%E5%BA%A6%E4%B8%BA%20n%20%EF%BC%8C%E4%BB%8E%E8%8A%82%E7%82%B9%20i%20%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E5%A0%86%E5%8C%96%22%22%22%0A%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E8%8A%82%E7%82%B9%20i,%20l,%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E8%8A%82%E7%82%B9%EF%BC%8C%E8%AE%B0%E4%B8%BA%20ma%0A%20%20%20%20%20%20%20%20l%20%3D%202%20*%20i%20%2B%201%0A%20%20%20%20%20%20%20%20r%20%3D%202%20*%20i%20%2B%202%0A%20%20%20%20%20%20%20%20ma%20%3D%20i%0A%20%20%20%20%20%20%20%20if%20l%20%3C%20n%20and%20nums%5Bl%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20if%20r%20%3C%20n%20and%20nums%5Br%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E8%8A%82%E7%82%B9%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l,%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%88%99%E6%97%A0%E9%A1%BB%E7%BB%A7%E7%BB%AD%E5%A0%86%E5%8C%96%EF%BC%8C%E8%B7%B3%E5%87%BA%0A%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E4%B8%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20nums%5Bi%5D,%20nums%5Bma%5D%20%3D%20nums%5Bma%5D,%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E5%90%91%E4%B8%8B%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0Adef%20heap_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%A0%86%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%BB%BA%E5%A0%86%E6%93%8D%E4%BD%9C%EF%BC%9A%E5%A0%86%E5%8C%96%E9%99%A4%E5%8F%B6%E8%8A%82%E7%82%B9%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E8%8A%82%E7%82%B9%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20//%202%20-%201,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20sift_down%28nums,%20len%28nums%29,%20i%29%0A%20%20%20%20%23%20%E4%BB%8E%E5%A0%86%E4%B8%AD%E6%8F%90%E5%8F%96%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E5%BE%AA%E7%8E%AF%20n-1%20%E8%BD%AE%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E6%A0%B9%E8%8A%82%E7%82%B9%E4%B8%8E%E6%9C%80%E5%8F%B3%E5%8F%B6%E8%8A%82%E7%82%B9%EF%BC%88%E4%BA%A4%E6%8D%A2%E9%A6%96%E5%85%83%E7%B4%A0%E4%B8%8E%E5%B0%BE%E5%85%83%E7%B4%A0%EF%BC%89%0A%20%20%20%20%20%20%20%20nums%5B0%5D,%20nums%5Bi%5D%20%3D%20nums%5Bi%5D,%20nums%5B0%5D%0A%20%20%20%20%20%20%20%20%23%20%E4%BB%A5%E6%A0%B9%E8%8A%82%E7%82%B9%E4%B8%BA%E8%B5%B7%E7%82%B9%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E8%BF%9B%E8%A1%8C%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20sift_down%28nums,%20i,%200%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20heap_sort%28nums%29%0A%20%20%20%20print%28%22%E5%A0%86%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.7.2 &nbsp; 演算法特性
- **時間複雜度為 $O(n \log n)$、非自適應排序**:建堆積操作使用 $O(n)$ 時間。從堆積中提取最大元素的時間複雜度為 $O(\log n)$ ,共迴圈 $n - 1$ 輪。
- **空間複雜度為 $O(1)$、原地排序**:幾個指標變數使用 $O(1)$ 空間。元素交換和堆積化操作都是在原陣列上進行的。
- **非穩定排序**:在交換堆積頂元素和堆積底元素時,相等元素的相對位置可能發生變化。
+28
View File
@@ -0,0 +1,28 @@
---
comments: true
icon: material/sort-ascending
---
# 第 11 章 &nbsp; 排序
![排序](../assets/covers/chapter_sorting.jpg){ class="cover-image" }
!!! abstract
排序猶如一把將混亂變為秩序的魔法鑰匙,使我們能以更高效的方式理解與處理資料。
無論是簡單的升序,還是複雜的分類排列,排序都向我們展示了資料的和諧美感。
## Chapter Contents
- [11.1 &nbsp; 排序演算法](https://www.hello-algo.com/en/chapter_sorting/sorting_algorithm/)
- [11.2 &nbsp; 選擇排序](https://www.hello-algo.com/en/chapter_sorting/selection_sort/)
- [11.3 &nbsp; 泡沫排序](https://www.hello-algo.com/en/chapter_sorting/bubble_sort/)
- [11.4 &nbsp; 插入排序](https://www.hello-algo.com/en/chapter_sorting/insertion_sort/)
- [11.5 &nbsp; 快速排序](https://www.hello-algo.com/en/chapter_sorting/quick_sort/)
- [11.6 &nbsp; 合併排序](https://www.hello-algo.com/en/chapter_sorting/merge_sort/)
- [11.7 &nbsp; 堆積排序](https://www.hello-algo.com/en/chapter_sorting/heap_sort/)
- [11.8 &nbsp; 桶排序](https://www.hello-algo.com/en/chapter_sorting/bucket_sort/)
- [11.9 &nbsp; 計數排序](https://www.hello-algo.com/en/chapter_sorting/counting_sort/)
- [11.10 &nbsp; 基數排序](https://www.hello-algo.com/en/chapter_sorting/radix_sort/)
- [11.11 &nbsp; 小結](https://www.hello-algo.com/en/chapter_sorting/summary/)
+301
View File
@@ -0,0 +1,301 @@
---
comments: true
---
# 11.4 &nbsp; 插入排序
<u>插入排序(insertion sort</u>是一種簡單的排序演算法,它的工作原理與手動整理一副牌的過程非常相似。
具體來說,我們在未排序區間選擇一個基準元素,將該元素與其左側已排序區間的元素逐一比較大小,並將該元素插入到正確的位置。
圖 11-6 展示了陣列插入元素的操作流程。設基準元素為 `base` ,我們需要將從目標索引到 `base` 之間的所有元素向右移動一位,然後將 `base` 賦值給目標索引。
![單次插入操作](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }
<p align="center"> 圖 11-6 &nbsp; 單次插入操作 </p>
## 11.4.1 &nbsp; 演算法流程
插入排序的整體流程如圖 11-7 所示。
1. 初始狀態下,陣列的第 1 個元素已完成排序。
2. 選取陣列的第 2 個元素作為 `base` ,將其插入到正確位置後,**陣列的前 2 個元素已排序**。
3. 選取第 3 個元素作為 `base` ,將其插入到正確位置後,**陣列的前 3 個元素已排序**。
4. 以此類推,在最後一輪中,選取最後一個元素作為 `base` ,將其插入到正確位置後,**所有元素均已排序**。
![插入排序流程](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
<p align="center"> 圖 11-7 &nbsp; 插入排序流程 </p>
示例程式碼如下:
=== "Python"
```python title="insertion_sort.py"
def insertion_sort(nums: list[int]):
"""插入排序"""
# 外迴圈:已排序區間為 [0, i-1]
for i in range(1, len(nums)):
base = nums[i]
j = i - 1
# 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while j >= 0 and nums[j] > base:
nums[j + 1] = nums[j] # 將 nums[j] 向右移動一位
j -= 1
nums[j + 1] = base # 將 base 賦值到正確位置
```
=== "C++"
```cpp title="insertion_sort.cpp"
/* 插入排序 */
void insertionSort(vector<int> &nums) {
// 外迴圈:已排序區間為 [0, i-1]
for (int i = 1; i < nums.size(); i++) {
int base = nums[i], j = i - 1;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位
j--;
}
nums[j + 1] = base; // 將 base 賦值到正確位置
}
}
```
=== "Java"
```java title="insertion_sort.java"
/* 插入排序 */
void insertionSort(int[] nums) {
// 外迴圈:已排序區間為 [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位
j--;
}
nums[j + 1] = base; // 將 base 賦值到正確位置
}
}
```
=== "C#"
```csharp title="insertion_sort.cs"
/* 插入排序 */
void InsertionSort(int[] nums) {
// 外迴圈:已排序區間為 [0, i-1]
for (int i = 1; i < nums.Length; i++) {
int bas = nums[i], j = i - 1;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 0 && nums[j] > bas) {
nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位
j--;
}
nums[j + 1] = bas; // 將 base 賦值到正確位置
}
}
```
=== "Go"
```go title="insertion_sort.go"
/* 插入排序 */
func insertionSort(nums []int) {
// 外迴圈:已排序區間為 [0, i-1]
for i := 1; i < len(nums); i++ {
base := nums[i]
j := i - 1
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
for j >= 0 && nums[j] > base {
nums[j+1] = nums[j] // 將 nums[j] 向右移動一位
j--
}
nums[j+1] = base // 將 base 賦值到正確位置
}
}
```
=== "Swift"
```swift title="insertion_sort.swift"
/* 插入排序 */
func insertionSort(nums: inout [Int]) {
// 外迴圈:已排序區間為 [0, i-1]
for i in nums.indices.dropFirst() {
let base = nums[i]
var j = i - 1
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while j >= 0, nums[j] > base {
nums[j + 1] = nums[j] // 將 nums[j] 向右移動一位
j -= 1
}
nums[j + 1] = base // 將 base 賦值到正確位置
}
}
```
=== "JS"
```javascript title="insertion_sort.js"
/* 插入排序 */
function insertionSort(nums) {
// 外迴圈:已排序區間為 [0, i-1]
for (let i = 1; i < nums.length; i++) {
let base = nums[i],
j = i - 1;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位
j--;
}
nums[j + 1] = base; // 將 base 賦值到正確位置
}
}
```
=== "TS"
```typescript title="insertion_sort.ts"
/* 插入排序 */
function insertionSort(nums: number[]): void {
// 外迴圈:已排序區間為 [0, i-1]
for (let i = 1; i < nums.length; i++) {
const base = nums[i];
let j = i - 1;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位
j--;
}
nums[j + 1] = base; // 將 base 賦值到正確位置
}
}
```
=== "Dart"
```dart title="insertion_sort.dart"
/* 插入排序 */
void insertionSort(List<int> nums) {
// 外迴圈:已排序區間為 [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位
j--;
}
nums[j + 1] = base; // 將 base 賦值到正確位置
}
}
```
=== "Rust"
```rust title="insertion_sort.rs"
/* 插入排序 */
fn insertion_sort(nums: &mut [i32]) {
// 外迴圈:已排序區間為 [0, i-1]
for i in 1..nums.len() {
let (base, mut j) = (nums[i], (i - 1) as i32);
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while j >= 0 && nums[j as usize] > base {
nums[(j + 1) as usize] = nums[j as usize]; // 將 nums[j] 向右移動一位
j -= 1;
}
nums[(j + 1) as usize] = base; // 將 base 賦值到正確位置
}
}
```
=== "C"
```c title="insertion_sort.c"
/* 插入排序 */
void insertionSort(int nums[], int size) {
// 外迴圈:已排序區間為 [0, i-1]
for (int i = 1; i < size; i++) {
int base = nums[i], j = i - 1;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 0 && nums[j] > base) {
// 將 nums[j] 向右移動一位
nums[j + 1] = nums[j];
j--;
}
// 將 base 賦值到正確位置
nums[j + 1] = base;
}
}
```
=== "Kotlin"
```kotlin title="insertion_sort.kt"
/* 插入排序 */
fun insertionSort(nums: IntArray) {
//外迴圈: 已排序元素為 1, 2, ..., n
for (i in nums.indices) {
val base = nums[i]
var j = i - 1
// 內迴圈: 將 base 插入到已排序部分的正確位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j] // 將 nums[j] 向右移動一位
j--
}
nums[j + 1] = base // 將 base 賦值到正確位置
}
}
```
=== "Ruby"
```ruby title="insertion_sort.rb"
[class]{}-[func]{insertion_sort}
```
=== "Zig"
```zig title="insertion_sort.zig"
// 插入排序
fn insertionSort(nums: []i32) void {
// 外迴圈:已排序區間為 [0, i-1]
var i: usize = 1;
while (i < nums.len) : (i += 1) {
var base = nums[i];
var j: usize = i;
// 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
while (j >= 1 and nums[j - 1] > base) : (j -= 1) {
nums[j] = nums[j - 1]; // 將 nums[j] 向右移動一位
}
nums[j] = base; // 將 base 賦值到正確位置
}
}
```
??? pythontutor "視覺化執行"
<div style="height: 513px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20insertion_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i-1%5D%0A%20%20%20%20for%20i%20in%20range%281,%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20base%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%20base%20%E6%8F%92%E5%85%A5%E5%88%B0%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i-1%5D%20%E4%B8%AD%E7%9A%84%E6%AD%A3%E7%A1%AE%E4%BD%8D%E7%BD%AE%0A%20%20%20%20%20%20%20%20while%20j%20%3E%3D%200%20and%20nums%5Bj%5D%20%3E%20base%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%5D%20%20%23%20%E5%B0%86%20nums%5Bj%5D%20%E5%90%91%E5%8F%B3%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%BD%8D%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20base%20%20%23%20%E5%B0%86%20base%20%E8%B5%8B%E5%80%BC%E5%88%B0%E6%AD%A3%E7%A1%AE%E4%BD%8D%E7%BD%AE%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20insertion_sort%28nums%29%0A%20%20%20%20print%28%22%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20insertion_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i-1%5D%0A%20%20%20%20for%20i%20in%20range%281,%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20base%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%20base%20%E6%8F%92%E5%85%A5%E5%88%B0%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i-1%5D%20%E4%B8%AD%E7%9A%84%E6%AD%A3%E7%A1%AE%E4%BD%8D%E7%BD%AE%0A%20%20%20%20%20%20%20%20while%20j%20%3E%3D%200%20and%20nums%5Bj%5D%20%3E%20base%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%5D%20%20%23%20%E5%B0%86%20nums%5Bj%5D%20%E5%90%91%E5%8F%B3%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%BD%8D%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20base%20%20%23%20%E5%B0%86%20base%20%E8%B5%8B%E5%80%BC%E5%88%B0%E6%AD%A3%E7%A1%AE%E4%BD%8D%E7%BD%AE%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20insertion_sort%28nums%29%0A%20%20%20%20print%28%22%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.4.2 &nbsp; 演算法特性
- **時間複雜度為 $O(n^2)$、自適應排序**:在最差情況下,每次插入操作分別需要迴圈 $n - 1$、$n-2$、$\dots$、$2$、$1$ 次,求和得到 $(n - 1) n / 2$ ,因此時間複雜度為 $O(n^2)$ 。在遇到有序資料時,插入操作會提前終止。當輸入陣列完全有序時,插入排序達到最佳時間複雜度 $O(n)$ 。
- **空間複雜度為 $O(1)$、原地排序**:指標 $i$ 和 $j$ 使用常數大小的額外空間。
- **穩定排序**:在插入操作過程中,我們會將元素插入到相等元素的右側,不會改變它們的順序。
## 11.4.3 &nbsp; 插入排序的優勢
插入排序的時間複雜度為 $O(n^2)$ ,而我們即將學習的快速排序的時間複雜度為 $O(n \log n)$ 。儘管插入排序的時間複雜度更高,**但在資料量較小的情況下,插入排序通常更快**。
這個結論與線性查詢和二分搜尋的適用情況的結論類似。快速排序這類 $O(n \log n)$ 的演算法屬於基於分治策略的排序演算法,往往包含更多單元計算操作。而在資料量較小時,$n^2$ 和 $n \log n$ 的數值比較接近,複雜度不佔主導地位,每輪中的單元操作數量起到決定性作用。
實際上,許多程式語言(例如 Java)的內建排序函式採用了插入排序,大致思路為:對於長陣列,採用基於分治策略的排序演算法,例如快速排序;對於短陣列,直接使用插入排序。
雖然泡沫排序、選擇排序和插入排序的時間複雜度都為 $O(n^2)$ ,但在實際情況中,**插入排序的使用頻率顯著高於泡沫排序和選擇排序**,主要有以下原因。
- 泡沫排序基於元素交換實現,需要藉助一個臨時變數,共涉及 3 個單元操作;插入排序基於元素賦值實現,僅需 1 個單元操作。因此,**泡沫排序的計算開銷通常比插入排序更高**。
- 選擇排序在任何情況下的時間複雜度都為 $O(n^2)$ 。**如果給定一組部分有序的資料,插入排序通常比選擇排序效率更高**。
- 選擇排序不穩定,無法應用於多級排序。
+708
View File
@@ -0,0 +1,708 @@
---
comments: true
---
# 11.6 &nbsp; 合併排序
<u>合併排序(merge sort</u>是一種基於分治策略的排序演算法,包含圖 11-10 所示的“劃分”和“合併”階段。
1. **劃分階段**:透過遞迴不斷地將陣列從中點處分開,將長陣列的排序問題轉換為短陣列的排序問題。
2. **合併階段**:當子陣列長度為 1 時終止劃分,開始合併,持續地將左右兩個較短的有序陣列合併為一個較長的有序陣列,直至結束。
![合併排序的劃分與合併階段](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" }
<p align="center"> 圖 11-10 &nbsp; 合併排序的劃分與合併階段 </p>
## 11.6.1 &nbsp; 演算法流程
如圖 11-11 所示,“劃分階段”從頂至底遞迴地將陣列從中點切分為兩個子陣列。
1. 計算陣列中點 `mid` ,遞迴劃分左子陣列(區間 `[left, mid]` )和右子陣列(區間 `[mid + 1, right]` )。
2. 遞迴執行步驟 `1.` ,直至子陣列區間長度為 1 時終止。
“合併階段”從底至頂地將左子陣列和右子陣列合併為一個有序陣列。需要注意的是,從長度為 1 的子陣列開始合併,合併階段中的每個子陣列都是有序的。
=== "<1>"
![合併排序步驟](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
=== "<2>"
![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" }
=== "<3>"
![merge_sort_step3](merge_sort.assets/merge_sort_step3.png){ class="animation-figure" }
=== "<4>"
![merge_sort_step4](merge_sort.assets/merge_sort_step4.png){ class="animation-figure" }
=== "<5>"
![merge_sort_step5](merge_sort.assets/merge_sort_step5.png){ class="animation-figure" }
=== "<6>"
![merge_sort_step6](merge_sort.assets/merge_sort_step6.png){ class="animation-figure" }
=== "<7>"
![merge_sort_step7](merge_sort.assets/merge_sort_step7.png){ class="animation-figure" }
=== "<8>"
![merge_sort_step8](merge_sort.assets/merge_sort_step8.png){ class="animation-figure" }
=== "<9>"
![merge_sort_step9](merge_sort.assets/merge_sort_step9.png){ class="animation-figure" }
=== "<10>"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" }
<p align="center"> 圖 11-11 &nbsp; 合併排序步驟 </p>
觀察發現,合併排序與二元樹後序走訪的遞迴順序是一致的。
- **後序走訪**:先遞迴左子樹,再遞迴右子樹,最後處理根節點。
- **合併排序**:先遞迴左子陣列,再遞迴右子陣列,最後處理合併。
合併排序的實現如以下程式碼所示。請注意,`nums` 的待合併區間為 `[left, right]` ,而 `tmp` 的對應區間為 `[0, right - left]`
=== "Python"
```python title="merge_sort.py"
def merge(nums: list[int], left: int, mid: int, right: int):
"""合併左子陣列和右子陣列"""
# 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
# 建立一個臨時陣列 tmp ,用於存放合併後的結果
tmp = [0] * (right - left + 1)
# 初始化左子陣列和右子陣列的起始索引
i, j, k = left, mid + 1, 0
# 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while i <= mid and j <= right:
if nums[i] <= nums[j]:
tmp[k] = nums[i]
i += 1
else:
tmp[k] = nums[j]
j += 1
k += 1
# 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while i <= mid:
tmp[k] = nums[i]
i += 1
k += 1
while j <= right:
tmp[k] = nums[j]
j += 1
k += 1
# 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for k in range(0, len(tmp)):
nums[left + k] = tmp[k]
def merge_sort(nums: list[int], left: int, right: int):
"""合併排序"""
# 終止條件
if left >= right:
return # 當子陣列長度為 1 時終止遞迴
# 劃分階段
mid = (left + right) // 2 # 計算中點
merge_sort(nums, left, mid) # 遞迴左子陣列
merge_sort(nums, mid + 1, right) # 遞迴右子陣列
# 合併階段
merge(nums, left, mid, right)
```
=== "C++"
```cpp title="merge_sort.cpp"
/* 合併左子陣列和右子陣列 */
void merge(vector<int> &nums, int left, int mid, int right) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
vector<int> tmp(right - left + 1);
// 初始化左子陣列和右子陣列的起始索引
int i = left, j = mid + 1, k = 0;
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (k = 0; k < tmp.size(); k++) {
nums[left + k] = tmp[k];
}
}
/* 合併排序 */
void mergeSort(vector<int> &nums, int left, int right) {
// 終止條件
if (left >= right)
return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
int mid = (left + right) / 2; // 計算中點
mergeSort(nums, left, mid); // 遞迴左子陣列
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right);
}
```
=== "Java"
```java title="merge_sort.java"
/* 合併左子陣列和右子陣列 */
void merge(int[] nums, int left, int mid, int right) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
int[] tmp = new int[right - left + 1];
// 初始化左子陣列和右子陣列的起始索引
int i = left, j = mid + 1, k = 0;
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
/* 合併排序 */
void mergeSort(int[] nums, int left, int right) {
// 終止條件
if (left >= right)
return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
int mid = (left + right) / 2; // 計算中點
mergeSort(nums, left, mid); // 遞迴左子陣列
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right);
}
```
=== "C#"
```csharp title="merge_sort.cs"
/* 合併左子陣列和右子陣列 */
void Merge(int[] nums, int left, int mid, int right) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
int[] tmp = new int[right - left + 1];
// 初始化左子陣列和右子陣列的起始索引
int i = left, j = mid + 1, k = 0;
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (k = 0; k < tmp.Length; ++k) {
nums[left + k] = tmp[k];
}
}
/* 合併排序 */
void MergeSort(int[] nums, int left, int right) {
// 終止條件
if (left >= right) return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
int mid = (left + right) / 2; // 計算中點
MergeSort(nums, left, mid); // 遞迴左子陣列
MergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
Merge(nums, left, mid, right);
}
```
=== "Go"
```go title="merge_sort.go"
/* 合併左子陣列和右子陣列 */
func merge(nums []int, left, mid, right int) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
tmp := make([]int, right-left+1)
// 初始化左子陣列和右子陣列的起始索引
i, j, k := left, mid+1, 0
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
for i <= mid && j <= right {
if nums[i] <= nums[j] {
tmp[k] = nums[i]
i++
} else {
tmp[k] = nums[j]
j++
}
k++
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
for i <= mid {
tmp[k] = nums[i]
i++
k++
}
for j <= right {
tmp[k] = nums[j]
j++
k++
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for k := 0; k < len(tmp); k++ {
nums[left+k] = tmp[k]
}
}
/* 合併排序 */
func mergeSort(nums []int, left, right int) {
// 終止條件
if left >= right {
return
}
// 劃分階段
mid := (left + right) / 2
mergeSort(nums, left, mid)
mergeSort(nums, mid+1, right)
// 合併階段
merge(nums, left, mid, right)
}
```
=== "Swift"
```swift title="merge_sort.swift"
/* 合併左子陣列和右子陣列 */
func merge(nums: inout [Int], left: Int, mid: Int, right: Int) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
var tmp = Array(repeating: 0, count: right - left + 1)
// 初始化左子陣列和右子陣列的起始索引
var i = left, j = mid + 1, k = 0
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while i <= mid, j <= right {
if nums[i] <= nums[j] {
tmp[k] = nums[i]
i += 1
} else {
tmp[k] = nums[j]
j += 1
}
k += 1
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while i <= mid {
tmp[k] = nums[i]
i += 1
k += 1
}
while j <= right {
tmp[k] = nums[j]
j += 1
k += 1
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for k in tmp.indices {
nums[left + k] = tmp[k]
}
}
/* 合併排序 */
func mergeSort(nums: inout [Int], left: Int, right: Int) {
// 終止條件
if left >= right { // 當子陣列長度為 1 時終止遞迴
return
}
// 劃分階段
let mid = (left + right) / 2 // 計算中點
mergeSort(nums: &nums, left: left, right: mid) // 遞迴左子陣列
mergeSort(nums: &nums, left: mid + 1, right: right) // 遞迴右子陣列
// 合併階段
merge(nums: &nums, left: left, mid: mid, right: right)
}
```
=== "JS"
```javascript title="merge_sort.js"
/* 合併左子陣列和右子陣列 */
function merge(nums, left, mid, right) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
const tmp = new Array(right - left + 1);
// 初始化左子陣列和右子陣列的起始索引
let i = left,
j = mid + 1,
k = 0;
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
/* 合併排序 */
function mergeSort(nums, left, right) {
// 終止條件
if (left >= right) return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
let mid = Math.floor((left + right) / 2); // 計算中點
mergeSort(nums, left, mid); // 遞迴左子陣列
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right);
}
```
=== "TS"
```typescript title="merge_sort.ts"
/* 合併左子陣列和右子陣列 */
function merge(nums: number[], left: number, mid: number, right: number): void {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
const tmp = new Array(right - left + 1);
// 初始化左子陣列和右子陣列的起始索引
let i = left,
j = mid + 1,
k = 0;
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
/* 合併排序 */
function mergeSort(nums: number[], left: number, right: number): void {
// 終止條件
if (left >= right) return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
let mid = Math.floor((left + right) / 2); // 計算中點
mergeSort(nums, left, mid); // 遞迴左子陣列
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right);
}
```
=== "Dart"
```dart title="merge_sort.dart"
/* 合併左子陣列和右子陣列 */
void merge(List<int> nums, int left, int mid, int right) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
List<int> tmp = List.filled(right - left + 1, 0);
// 初始化左子陣列和右子陣列的起始索引
int i = left, j = mid + 1, k = 0;
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
/* 合併排序 */
void mergeSort(List<int> nums, int left, int right) {
// 終止條件
if (left >= right) return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
int mid = (left + right) ~/ 2; // 計算中點
mergeSort(nums, left, mid); // 遞迴左子陣列
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right);
}
```
=== "Rust"
```rust title="merge_sort.rs"
/* 合併左子陣列和右子陣列 */
fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
let tmp_size = right - left + 1;
let mut tmp = vec![0; tmp_size];
// 初始化左子陣列和右子陣列的起始索引
let (mut i, mut j, mut k) = (left, mid + 1, 0);
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while i <= mid && j <= right {
if nums[i] <= nums[j] {
tmp[k] = nums[i];
i += 1;
} else {
tmp[k] = nums[j];
j += 1;
}
k += 1;
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while i <= mid {
tmp[k] = nums[i];
k += 1;
i += 1;
}
while j <= right {
tmp[k] = nums[j];
k += 1;
j += 1;
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for k in 0..tmp_size {
nums[left + k] = tmp[k];
}
}
/* 合併排序 */
fn merge_sort(nums: &mut [i32], left: usize, right: usize) {
// 終止條件
if left >= right {
return; // 當子陣列長度為 1 時終止遞迴
}
// 劃分階段
let mid = (left + right) / 2; // 計算中點
merge_sort(nums, left, mid); // 遞迴左子陣列
merge_sort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right);
}
```
=== "C"
```c title="merge_sort.c"
/* 合併左子陣列和右子陣列 */
void merge(int *nums, int left, int mid, int right) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
int tmpSize = right - left + 1;
int *tmp = (int *)malloc(tmpSize * sizeof(int));
// 初始化左子陣列和右子陣列的起始索引
int i = left, j = mid + 1, k = 0;
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (k = 0; k < tmpSize; ++k) {
nums[left + k] = tmp[k];
}
// 釋放記憶體
free(tmp);
}
/* 合併排序 */
void mergeSort(int *nums, int left, int right) {
// 終止條件
if (left >= right)
return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
int mid = (left + right) / 2; // 計算中點
mergeSort(nums, left, mid); // 遞迴左子陣列
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right);
}
```
=== "Kotlin"
```kotlin title="merge_sort.kt"
/* 合併左子陣列和右子陣列 */
fun merge(nums: IntArray, left: Int, mid: Int, right: Int) {
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
val tmp = IntArray(right - left + 1)
// 初始化左子陣列和右子陣列的起始索引
var i = left
var j = mid + 1
var k = 0
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) tmp[k++] = nums[i++]
else tmp[k++] = nums[j++]
}
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
while (i <= mid) {
tmp[k++] = nums[i++]
}
while (j <= right) {
tmp[k++] = nums[j++]
}
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
for (l in tmp.indices) {
nums[left + l] = tmp[l]
}
}
/* 合併排序 */
fun mergeSort(nums: IntArray, left: Int, right: Int) {
// 終止條件
if (left >= right) return // 當子陣列長度為 1 時終止遞迴
// 劃分階段
val mid = (left + right) / 2 // 計算中點
mergeSort(nums, left, mid) // 遞迴左子陣列
mergeSort(nums, mid + 1, right) // 遞迴右子陣列
// 合併階段
merge(nums, left, mid, right)
}
```
=== "Ruby"
```ruby title="merge_sort.rb"
[class]{}-[func]{merge}
[class]{}-[func]{merge_sort}
```
=== "Zig"
```zig title="merge_sort.zig"
// 合併左子陣列和右子陣列
// 左子陣列區間 [left, mid]
// 右子陣列區間 [mid + 1, right]
fn merge(nums: []i32, left: usize, mid: usize, right: usize) !void {
// 初始化輔助陣列
var mem_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer mem_arena.deinit();
const mem_allocator = mem_arena.allocator();
var tmp = try mem_allocator.alloc(i32, right + 1 - left);
std.mem.copy(i32, tmp, nums[left..right+1]);
// 左子陣列的起始索引和結束索引
var leftStart = left - left;
var leftEnd = mid - left;
// 右子陣列的起始索引和結束索引
var rightStart = mid + 1 - left;
var rightEnd = right - left;
// i, j 分別指向左子陣列、右子陣列的首元素
var i = leftStart;
var j = rightStart;
// 透過覆蓋原陣列 nums 來合併左子陣列和右子陣列
var k = left;
while (k <= right) : (k += 1) {
// 若“左子陣列已全部合併完”,則選取右子陣列元素,並且 j++
if (i > leftEnd) {
nums[k] = tmp[j];
j += 1;
// 否則,若“右子陣列已全部合併完”或“左子陣列元素 <= 右子陣列元素”,則選取左子陣列元素,並且 i++
} else if (j > rightEnd or tmp[i] <= tmp[j]) {
nums[k] = tmp[i];
i += 1;
// 否則,若“左右子陣列都未全部合併完”且“左子陣列元素 > 右子陣列元素”,則選取右子陣列元素,並且 j++
} else {
nums[k] = tmp[j];
j += 1;
}
}
}
// 合併排序
fn mergeSort(nums: []i32, left: usize, right: usize) !void {
// 終止條件
if (left >= right) return; // 當子陣列長度為 1 時終止遞迴
// 劃分階段
var mid = (left + right) / 2; // 計算中點
try mergeSort(nums, left, mid); // 遞迴左子陣列
try mergeSort(nums, mid + 1, right); // 遞迴右子陣列
// 合併階段
try merge(nums, left, mid, right);
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20merge%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20mid%3A%20int,%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%90%88%E5%B9%B6%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5Bleft,%20mid%5D,%20%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5Bmid%2B1,%20right%5D%0A%20%20%20%20%23%20%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%20tmp%20%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E6%94%BE%E5%90%88%E5%B9%B6%E5%90%8E%E7%9A%84%E7%BB%93%E6%9E%9C%0A%20%20%20%20tmp%20%3D%20%5B0%5D%20*%20%28right%20-%20left%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E8%B5%B7%E5%A7%8B%E7%B4%A2%E5%BC%95%0A%20%20%20%20i,%20j,%20k%20%3D%20left,%20mid%20%2B%201,%200%0A%20%20%20%20%23%20%E5%BD%93%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E9%83%BD%E8%BF%98%E6%9C%89%E5%85%83%E7%B4%A0%E6%97%B6%EF%BC%8C%E8%BF%9B%E8%A1%8C%E6%AF%94%E8%BE%83%E5%B9%B6%E5%B0%86%E8%BE%83%E5%B0%8F%E7%9A%84%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%88%B0%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%E4%B8%AD%0A%20%20%20%20while%20i%20%3C%3D%20mid%20and%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3C%3D%20nums%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E5%B0%86%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E5%89%A9%E4%BD%99%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%88%B0%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%E4%B8%AD%0A%20%20%20%20while%20i%20%3C%3D%20mid%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20while%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E5%B0%86%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%20tmp%20%E4%B8%AD%E7%9A%84%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%9B%9E%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%20%E7%9A%84%E5%AF%B9%E5%BA%94%E5%8C%BA%E9%97%B4%0A%20%20%20%20for%20k%20in%20range%280,%20len%28tmp%29%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bleft%20%2B%20k%5D%20%3D%20tmp%5Bk%5D%0A%0A%0Adef%20merge_sort%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%BB%88%E6%AD%A2%E6%9D%A1%E4%BB%B6%0A%20%20%20%20if%20left%20%3E%3D%20right%3A%0A%20%20%20%20%20%20%20%20return%20%20%23%20%E5%BD%93%E5%AD%90%E6%95%B0%E7%BB%84%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E6%97%B6%E7%BB%88%E6%AD%A2%E9%80%92%E5%BD%92%0A%20%20%20%20%23%20%E5%88%92%E5%88%86%E9%98%B6%E6%AE%B5%0A%20%20%20%20mid%20%3D%20%28left%20%2B%20right%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%0A%20%20%20%20merge_sort%28nums,%20left,%20mid%29%20%20%23%20%E9%80%92%E5%BD%92%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%0A%20%20%20%20merge_sort%28nums,%20mid%20%2B%201,%20right%29%20%20%23%20%E9%80%92%E5%BD%92%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%0A%20%20%20%20%23%20%E5%90%88%E5%B9%B6%E9%98%B6%E6%AE%B5%0A%20%20%20%20merge%28nums,%20left,%20mid,%20right%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B7,%203,%202,%206,%200,%201,%205,%204%5D%0A%20%20%20%20merge_sort%28nums,%200,%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20merge%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20mid%3A%20int,%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%90%88%E5%B9%B6%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5Bleft,%20mid%5D,%20%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5Bmid%2B1,%20right%5D%0A%20%20%20%20%23%20%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%20tmp%20%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E6%94%BE%E5%90%88%E5%B9%B6%E5%90%8E%E7%9A%84%E7%BB%93%E6%9E%9C%0A%20%20%20%20tmp%20%3D%20%5B0%5D%20*%20%28right%20-%20left%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E8%B5%B7%E5%A7%8B%E7%B4%A2%E5%BC%95%0A%20%20%20%20i,%20j,%20k%20%3D%20left,%20mid%20%2B%201,%200%0A%20%20%20%20%23%20%E5%BD%93%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E9%83%BD%E8%BF%98%E6%9C%89%E5%85%83%E7%B4%A0%E6%97%B6%EF%BC%8C%E8%BF%9B%E8%A1%8C%E6%AF%94%E8%BE%83%E5%B9%B6%E5%B0%86%E8%BE%83%E5%B0%8F%E7%9A%84%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%88%B0%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%E4%B8%AD%0A%20%20%20%20while%20i%20%3C%3D%20mid%20and%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3C%3D%20nums%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E5%B0%86%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E5%89%A9%E4%BD%99%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%88%B0%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%E4%B8%AD%0A%20%20%20%20while%20i%20%3C%3D%20mid%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20while%20j%20%3C%3D%20right%3A%0A%20%20%20%20%20%20%20%20tmp%5Bk%5D%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20j%20%2B%3D%201%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20%23%20%E5%B0%86%E4%B8%B4%E6%97%B6%E6%95%B0%E7%BB%84%20tmp%20%E4%B8%AD%E7%9A%84%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%9B%9E%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%20%E7%9A%84%E5%AF%B9%E5%BA%94%E5%8C%BA%E9%97%B4%0A%20%20%20%20for%20k%20in%20range%280,%20len%28tmp%29%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bleft%20%2B%20k%5D%20%3D%20tmp%5Bk%5D%0A%0A%0Adef%20merge_sort%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%BB%88%E6%AD%A2%E6%9D%A1%E4%BB%B6%0A%20%20%20%20if%20left%20%3E%3D%20right%3A%0A%20%20%20%20%20%20%20%20return%20%20%23%20%E5%BD%93%E5%AD%90%E6%95%B0%E7%BB%84%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E6%97%B6%E7%BB%88%E6%AD%A2%E9%80%92%E5%BD%92%0A%20%20%20%20%23%20%E5%88%92%E5%88%86%E9%98%B6%E6%AE%B5%0A%20%20%20%20mid%20%3D%20%28left%20%2B%20right%29%20//%202%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%0A%20%20%20%20merge_sort%28nums,%20left,%20mid%29%20%20%23%20%E9%80%92%E5%BD%92%E5%B7%A6%E5%AD%90%E6%95%B0%E7%BB%84%0A%20%20%20%20merge_sort%28nums,%20mid%20%2B%201,%20right%29%20%20%23%20%E9%80%92%E5%BD%92%E5%8F%B3%E5%AD%90%E6%95%B0%E7%BB%84%0A%20%20%20%20%23%20%E5%90%88%E5%B9%B6%E9%98%B6%E6%AE%B5%0A%20%20%20%20merge%28nums,%20left,%20mid,%20right%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B7,%203,%202,%206,%200,%201,%205,%204%5D%0A%20%20%20%20merge_sort%28nums,%200,%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.6.2 &nbsp; 演算法特性
- **時間複雜度為 $O(n \log n)$、非自適應排序**:劃分產生高度為 $\log n$ 的遞迴樹,每層合併的總操作數量為 $n$ ,因此總體時間複雜度為 $O(n \log n)$ 。
- **空間複雜度為 $O(n)$、非原地排序**:遞迴深度為 $\log n$ ,使用 $O(\log n)$ 大小的堆疊幀空間。合併操作需要藉助輔助陣列實現,使用 $O(n)$ 大小的額外空間。
- **穩定排序**:在合併過程中,相等元素的次序保持不變。
## 11.6.3 &nbsp; 鏈結串列排序
對於鏈結串列,合併排序相較於其他排序演算法具有顯著優勢,**可以將鏈結串列排序任務的空間複雜度最佳化至 $O(1)$** 。
- **劃分階段**:可以使用“迭代”替代“遞迴”來實現鏈結串列劃分工作,從而省去遞迴使用的堆疊幀空間。
- **合併階段**:在鏈結串列中,節點增刪操作僅需改變引用(指標)即可實現,因此合併階段(將兩個短有序鏈結串列合併為一個長有序鏈結串列)無須建立額外鏈結串列。
具體實現細節比較複雜,有興趣的讀者可以查閱相關資料進行學習。
File diff suppressed because it is too large Load Diff
+764
View File
@@ -0,0 +1,764 @@
---
comments: true
---
# 11.10 &nbsp; 基數排序
上一節介紹了計數排序,它適用於資料量 $n$ 較大但資料範圍 $m$ 較小的情況。假設我們需要對 $n = 10^6$ 個學號進行排序,而學號是一個 $8$ 位數字,這意味著資料範圍 $m = 10^8$ 非常大,使用計數排序需要分配大量記憶體空間,而基數排序可以避免這種情況。
<u>基數排序(radix sort</u>的核心思想與計數排序一致,也透過統計個數來實現排序。在此基礎上,基數排序利用數字各位之間的遞進關係,依次對每一位進行排序,從而得到最終的排序結果。
## 11.10.1 &nbsp; 演算法流程
以學號資料為例,假設數字的最低位是第 $1$ 位,最高位是第 $8$ 位,基數排序的流程如圖 11-18 所示。
1. 初始化位數 $k = 1$ 。
2. 對學號的第 $k$ 位執行“計數排序”。完成後,資料會根據第 $k$ 位從小到大排序。
3. 將 $k$ 增加 $1$ ,然後返回步驟 `2.` 繼續迭代,直到所有位都排序完成後結束。
![基數排序演算法流程](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" }
<p align="center"> 圖 11-18 &nbsp; 基數排序演算法流程 </p>
下面剖析程式碼實現。對於一個 $d$ 進位制的數字 $x$ ,要獲取其第 $k$ 位 $x_k$ ,可以使用以下計算公式:
$$
x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
$$
其中 $\lfloor a \rfloor$ 表示對浮點數 $a$ 向下取整,而 $\bmod \: d$ 表示對 $d$ 取模(取餘)。對於學號資料,$d = 10$ 且 $k \in [1, 8]$ 。
此外,我們需要小幅改動計數排序程式碼,使之可以根據數字的第 $k$ 位進行排序:
=== "Python"
```python title="radix_sort.py"
def digit(num: int, exp: int) -> int:
"""獲取元素 num 的第 k 位,其中 exp = 10^(k-1)"""
# 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (num // exp) % 10
def counting_sort_digit(nums: list[int], exp: int):
"""計數排序(根據 nums 第 k 位排序)"""
# 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
counter = [0] * 10
n = len(nums)
# 統計 0~9 各數字的出現次數
for i in range(n):
d = digit(nums[i], exp) # 獲取 nums[i] 第 k 位,記為 d
counter[d] += 1 # 統計數字 d 的出現次數
# 求前綴和,將“出現個數”轉換為“陣列索引”
for i in range(1, 10):
counter[i] += counter[i - 1]
# 倒序走訪,根據桶內統計結果,將各元素填入 res
res = [0] * n
for i in range(n - 1, -1, -1):
d = digit(nums[i], exp)
j = counter[d] - 1 # 獲取 d 在陣列中的索引 j
res[j] = nums[i] # 將當前元素填入索引 j
counter[d] -= 1 # 將 d 的數量減 1
# 使用結果覆蓋原陣列 nums
for i in range(n):
nums[i] = res[i]
def radix_sort(nums: list[int]):
"""基數排序"""
# 獲取陣列的最大元素,用於判斷最大位數
m = max(nums)
# 按照從低位到高位的順序走訪
exp = 1
while exp <= m:
# 對陣列元素的第 k 位執行計數排序
# k = 1 -> exp = 1
# k = 2 -> exp = 10
# 即 exp = 10^(k-1)
counting_sort_digit(nums, exp)
exp *= 10
```
=== "C++"
```cpp title="radix_sort.cpp"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
int digit(int num, int exp) {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (num / exp) % 10;
}
/* 計數排序(根據 nums 第 k 位排序) */
void countingSortDigit(vector<int> &nums, int exp) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
vector<int> counter(10, 0);
int n = nums.size();
// 統計 0~9 各數字的出現次數
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d
counter[d]++; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
vector<int> res(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d]--; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基數排序 */
void radixSort(vector<int> &nums) {
// 獲取陣列的最大元素,用於判斷最大位數
int m = *max_element(nums.begin(), nums.end());
// 按照從低位到高位的順序走訪
for (int exp = 1; exp <= m; exp *= 10)
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp);
}
```
=== "Java"
```java title="radix_sort.java"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
int digit(int num, int exp) {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (num / exp) % 10;
}
/* 計數排序(根據 nums 第 k 位排序) */
void countingSortDigit(int[] nums, int exp) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
int[] counter = new int[10];
int n = nums.length;
// 統計 0~9 各數字的出現次數
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d
counter[d]++; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d]--; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基數排序 */
void radixSort(int[] nums) {
// 獲取陣列的最大元素,用於判斷最大位數
int m = Integer.MIN_VALUE;
for (int num : nums)
if (num > m)
m = num;
// 按照從低位到高位的順序走訪
for (int exp = 1; exp <= m; exp *= 10)
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp);
}
```
=== "C#"
```csharp title="radix_sort.cs"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
int Digit(int num, int exp) {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (num / exp) % 10;
}
/* 計數排序(根據 nums 第 k 位排序) */
void CountingSortDigit(int[] nums, int exp) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
int[] counter = new int[10];
int n = nums.Length;
// 統計 0~9 各數字的出現次數
for (int i = 0; i < n; i++) {
int d = Digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d
counter[d]++; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int d = Digit(nums[i], exp);
int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d]--; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
/* 基數排序 */
void RadixSort(int[] nums) {
// 獲取陣列的最大元素,用於判斷最大位數
int m = int.MinValue;
foreach (int num in nums) {
if (num > m) m = num;
}
// 按照從低位到高位的順序走訪
for (int exp = 1; exp <= m; exp *= 10) {
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
CountingSortDigit(nums, exp);
}
}
```
=== "Go"
```go title="radix_sort.go"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
func digit(num, exp int) int {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (num / exp) % 10
}
/* 計數排序(根據 nums 第 k 位排序) */
func countingSortDigit(nums []int, exp int) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
counter := make([]int, 10)
n := len(nums)
// 統計 0~9 各數字的出現次數
for i := 0; i < n; i++ {
d := digit(nums[i], exp) // 獲取 nums[i] 第 k 位,記為 d
counter[d]++ // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for i := 1; i < 10; i++ {
counter[i] += counter[i-1]
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
res := make([]int, n)
for i := n - 1; i >= 0; i-- {
d := digit(nums[i], exp)
j := counter[d] - 1 // 獲取 d 在陣列中的索引 j
res[j] = nums[i] // 將當前元素填入索引 j
counter[d]-- // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for i := 0; i < n; i++ {
nums[i] = res[i]
}
}
/* 基數排序 */
func radixSort(nums []int) {
// 獲取陣列的最大元素,用於判斷最大位數
max := math.MinInt
for _, num := range nums {
if num > max {
max = num
}
}
// 按照從低位到高位的順序走訪
for exp := 1; max >= exp; exp *= 10 {
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp)
}
}
```
=== "Swift"
```swift title="radix_sort.swift"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
func digit(num: Int, exp: Int) -> Int {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
(num / exp) % 10
}
/* 計數排序(根據 nums 第 k 位排序) */
func countingSortDigit(nums: inout [Int], exp: Int) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
var counter = Array(repeating: 0, count: 10)
// 統計 0~9 各數字的出現次數
for i in nums.indices {
let d = digit(num: nums[i], exp: exp) // 獲取 nums[i] 第 k 位,記為 d
counter[d] += 1 // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for i in 1 ..< 10 {
counter[i] += counter[i - 1]
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
var res = Array(repeating: 0, count: nums.count)
for i in nums.indices.reversed() {
let d = digit(num: nums[i], exp: exp)
let j = counter[d] - 1 // 獲取 d 在陣列中的索引 j
res[j] = nums[i] // 將當前元素填入索引 j
counter[d] -= 1 // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for i in nums.indices {
nums[i] = res[i]
}
}
/* 基數排序 */
func radixSort(nums: inout [Int]) {
// 獲取陣列的最大元素,用於判斷最大位數
var m = Int.min
for num in nums {
if num > m {
m = num
}
}
// 按照從低位到高位的順序走訪
for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums: &nums, exp: exp)
}
}
```
=== "JS"
```javascript title="radix_sort.js"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
function digit(num, exp) {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return Math.floor(num / exp) % 10;
}
/* 計數排序(根據 nums 第 k 位排序) */
function countingSortDigit(nums, exp) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
const counter = new Array(10).fill(0);
const n = nums.length;
// 統計 0~9 各數字的出現次數
for (let i = 0; i < n; i++) {
const d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d
counter[d]++; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (let i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
const res = new Array(n).fill(0);
for (let i = n - 1; i >= 0; i--) {
const d = digit(nums[i], exp);
const j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d]--; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
/* 基數排序 */
function radixSort(nums) {
// 獲取陣列的最大元素,用於判斷最大位數
let m = Number.MIN_VALUE;
for (const num of nums) {
if (num > m) {
m = num;
}
}
// 按照從低位到高位的順序走訪
for (let exp = 1; exp <= m; exp *= 10) {
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
```
=== "TS"
```typescript title="radix_sort.ts"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
function digit(num: number, exp: number): number {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return Math.floor(num / exp) % 10;
}
/* 計數排序(根據 nums 第 k 位排序) */
function countingSortDigit(nums: number[], exp: number): void {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
const counter = new Array(10).fill(0);
const n = nums.length;
// 統計 0~9 各數字的出現次數
for (let i = 0; i < n; i++) {
const d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d
counter[d]++; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (let i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
const res = new Array(n).fill(0);
for (let i = n - 1; i >= 0; i--) {
const d = digit(nums[i], exp);
const j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d]--; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
/* 基數排序 */
function radixSort(nums: number[]): void {
// 獲取陣列的最大元素,用於判斷最大位數
let m = Number.MIN_VALUE;
for (const num of nums) {
if (num > m) {
m = num;
}
}
// 按照從低位到高位的順序走訪
for (let exp = 1; exp <= m; exp *= 10) {
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
```
=== "Dart"
```dart title="radix_sort.dart"
/* 獲取元素 _num 的第 k 位,其中 exp = 10^(k-1) */
int digit(int _num, int exp) {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (_num ~/ exp) % 10;
}
/* 計數排序(根據 nums 第 k 位排序) */
void countingSortDigit(List<int> nums, int exp) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
List<int> counter = List<int>.filled(10, 0);
int n = nums.length;
// 統計 0~9 各數字的出現次數
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d
counter[d]++; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
List<int> res = List<int>.filled(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d]--; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (int i = 0; i < n; i++) nums[i] = res[i];
}
/* 基數排序 */
void radixSort(List<int> nums) {
// 獲取陣列的最大元素,用於判斷最大位數
// dart 中 int 的長度是 64 位的
int m = -1 << 63;
for (int _num in nums) if (_num > m) m = _num;
// 按照從低位到高位的順序走訪
for (int exp = 1; exp <= m; exp *= 10)
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp);
}
```
=== "Rust"
```rust title="radix_sort.rs"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
fn digit(num: i32, exp: i32) -> usize {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return ((num / exp) % 10) as usize;
}
/* 計數排序(根據 nums 第 k 位排序) */
fn counting_sort_digit(nums: &mut [i32], exp: i32) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
let mut counter = [0; 10];
let n = nums.len();
// 統計 0~9 各數字的出現次數
for i in 0..n {
let d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d
counter[d] += 1; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for i in 1..10 {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
let mut res = vec![0; n];
for i in (0..n).rev() {
let d = digit(nums[i], exp);
let j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d] -= 1; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for i in 0..n {
nums[i] = res[i];
}
}
/* 基數排序 */
fn radix_sort(nums: &mut [i32]) {
// 獲取陣列的最大元素,用於判斷最大位數
let m = *nums.into_iter().max().unwrap();
// 按照從低位到高位的順序走訪
let mut exp = 1;
while exp <= m {
counting_sort_digit(nums, exp);
exp *= 10;
}
}
```
=== "C"
```c title="radix_sort.c"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
int digit(int num, int exp) {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (num / exp) % 10;
}
/* 計數排序(根據 nums 第 k 位排序) */
void countingSortDigit(int nums[], int size, int exp) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
int *counter = (int *)malloc((sizeof(int) * 10));
// 統計 0~9 各數字的出現次數
for (int i = 0; i < size; i++) {
// 獲取 nums[i] 第 k 位,記為 d
int d = digit(nums[i], exp);
// 統計數字 d 的出現次數
counter[d]++;
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
int *res = (int *)malloc(sizeof(int) * size);
for (int i = size - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d]--; // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (int i = 0; i < size; i++) {
nums[i] = res[i];
}
}
/* 基數排序 */
void radixSort(int nums[], int size) {
// 獲取陣列的最大元素,用於判斷最大位數
int max = INT32_MIN;
for (size_t i = 0; i < size - 1; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
// 按照從低位到高位的順序走訪
for (int exp = 1; max >= exp; exp *= 10)
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, size, exp);
}
```
=== "Kotlin"
```kotlin title="radix_sort.kt"
/* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */
fun digit(num: Int, exp: Int): Int {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return (num / exp) % 10
}
/* 計數排序(根據 nums 第 k 位排序) */
fun countingSortDigit(nums: IntArray, exp: Int) {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
val counter = IntArray(10)
val n = nums.size
// 統計 0~9 各數字的出現次數
for (i in 0..<n) {
val d = digit(nums[i], exp) // 獲取 nums[i] 第 k 位,記為 d
counter[d]++ // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
for (i in 1..9) {
counter[i] += counter[i - 1]
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
val res = IntArray(n)
for (i in n - 1 downTo 0) {
val d = digit(nums[i], exp)
val j = counter[d] - 1 // 獲取 d 在陣列中的索引 j
res[j] = nums[i] // 將當前元素填入索引 j
counter[d]-- // 將 d 的數量減 1
}
// 使用結果覆蓋原陣列 nums
for (i in 0..<n) nums[i] = res[i]
}
/* 基數排序 */
fun radixSort(nums: IntArray) {
// 獲取陣列的最大元素,用於判斷最大位數
var m = Int.MIN_VALUE
for (num in nums) if (num > m) m = num
var exp = 1
// 按照從低位到高位的順序走訪
while (exp <= m) {
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
countingSortDigit(nums, exp)
exp *= 10
}
}
```
=== "Ruby"
```ruby title="radix_sort.rb"
[class]{}-[func]{digit}
[class]{}-[func]{counting_sort_digit}
[class]{}-[func]{radix_sort}
```
=== "Zig"
```zig title="radix_sort.zig"
// 獲取元素 num 的第 k 位,其中 exp = 10^(k-1)
fn digit(num: i32, exp: i32) i32 {
// 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算
return @mod(@divFloor(num, exp), 10);
}
// 計數排序(根據 nums 第 k 位排序)
fn countingSortDigit(nums: []i32, exp: i32) !void {
// 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列
var mem_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
// defer mem_arena.deinit();
const mem_allocator = mem_arena.allocator();
var counter = try mem_allocator.alloc(usize, 10);
@memset(counter, 0);
var n = nums.len;
// 統計 0~9 各數字的出現次數
for (nums) |num| {
var d: u32 = @bitCast(digit(num, exp)); // 獲取 nums[i] 第 k 位,記為 d
counter[d] += 1; // 統計數字 d 的出現次數
}
// 求前綴和,將“出現個數”轉換為“陣列索引”
var i: usize = 1;
while (i < 10) : (i += 1) {
counter[i] += counter[i - 1];
}
// 倒序走訪,根據桶內統計結果,將各元素填入 res
var res = try mem_allocator.alloc(i32, n);
i = n - 1;
while (i >= 0) : (i -= 1) {
var d: u32 = @bitCast(digit(nums[i], exp));
var j = counter[d] - 1; // 獲取 d 在陣列中的索引 j
res[j] = nums[i]; // 將當前元素填入索引 j
counter[d] -= 1; // 將 d 的數量減 1
if (i == 0) break;
}
// 使用結果覆蓋原陣列 nums
i = 0;
while (i < n) : (i += 1) {
nums[i] = res[i];
}
}
// 基數排序
fn radixSort(nums: []i32) !void {
// 獲取陣列的最大元素,用於判斷最大位數
var m: i32 = std.math.minInt(i32);
for (nums) |num| {
if (num > m) m = num;
}
// 按照從低位到高位的順序走訪
var exp: i32 = 1;
while (exp <= m) : (exp *= 10) {
// 對陣列元素的第 k 位執行計數排序
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// 即 exp = 10^(k-1)
try countingSortDigit(nums, exp);
}
}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20digit%28num%3A%20int,%20exp%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%85%83%E7%B4%A0%20num%20%E7%9A%84%E7%AC%AC%20k%20%E4%BD%8D%EF%BC%8C%E5%85%B6%E4%B8%AD%20exp%20%3D%2010%5E%28k-1%29%22%22%22%0A%20%20%20%20%23%20%E4%BC%A0%E5%85%A5%20exp%20%E8%80%8C%E9%9D%9E%20k%20%E5%8F%AF%E4%BB%A5%E9%81%BF%E5%85%8D%E5%9C%A8%E6%AD%A4%E9%87%8D%E5%A4%8D%E6%89%A7%E8%A1%8C%E6%98%82%E8%B4%B5%E7%9A%84%E6%AC%A1%E6%96%B9%E8%AE%A1%E7%AE%97%0A%20%20%20%20return%20%28num%20//%20exp%29%20%25%2010%0A%0Adef%20counting_sort_digit%28nums%3A%20list%5Bint%5D,%20exp%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%EF%BC%88%E6%A0%B9%E6%8D%AE%20nums%20%E7%AC%AC%20k%20%E4%BD%8D%E6%8E%92%E5%BA%8F%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%8D%81%E8%BF%9B%E5%88%B6%E7%9A%84%E4%BD%8D%E8%8C%83%E5%9B%B4%E4%B8%BA%200~9%20%EF%BC%8C%E5%9B%A0%E6%AD%A4%E9%9C%80%E8%A6%81%E9%95%BF%E5%BA%A6%E4%B8%BA%2010%20%E7%9A%84%E6%A1%B6%E6%95%B0%E7%BB%84%0A%20%20%20%20counter%20%3D%20%5B0%5D%20*%2010%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E7%BB%9F%E8%AE%A1%200~9%20%E5%90%84%E6%95%B0%E5%AD%97%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20d%20%3D%20digit%28nums%5Bi%5D,%20exp%29%20%20%23%20%E8%8E%B7%E5%8F%96%20nums%5Bi%5D%20%E7%AC%AC%20k%20%E4%BD%8D%EF%BC%8C%E8%AE%B0%E4%B8%BA%20d%0A%20%20%20%20%20%20%20%20counter%5Bd%5D%20%2B%3D%201%20%20%23%20%E7%BB%9F%E8%AE%A1%E6%95%B0%E5%AD%97%20d%20%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20%23%20%E6%B1%82%E5%89%8D%E7%BC%80%E5%92%8C%EF%BC%8C%E5%B0%86%E2%80%9C%E5%87%BA%E7%8E%B0%E4%B8%AA%E6%95%B0%E2%80%9D%E8%BD%AC%E6%8D%A2%E4%B8%BA%E2%80%9C%E6%95%B0%E7%BB%84%E7%B4%A2%E5%BC%95%E2%80%9D%0A%20%20%20%20for%20i%20in%20range%281,%2010%29%3A%0A%20%20%20%20%20%20%20%20counter%5Bi%5D%20%2B%3D%20counter%5Bi%20-%201%5D%0A%20%20%20%20%23%20%E5%80%92%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%8C%E6%A0%B9%E6%8D%AE%E6%A1%B6%E5%86%85%E7%BB%9F%E8%AE%A1%E7%BB%93%E6%9E%9C%EF%BC%8C%E5%B0%86%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%20res%0A%20%20%20%20res%20%3D%20%5B0%5D%20*%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20d%20%3D%20digit%28nums%5Bi%5D,%20exp%29%0A%20%20%20%20%20%20%20%20j%20%3D%20counter%5Bd%5D%20-%201%20%20%23%20%E8%8E%B7%E5%8F%96%20d%20%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E7%B4%A2%E5%BC%95%20j%0A%20%20%20%20%20%20%20%20res%5Bj%5D%20%3D%20nums%5Bi%5D%20%20%23%20%E5%B0%86%E5%BD%93%E5%89%8D%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E7%B4%A2%E5%BC%95%20j%0A%20%20%20%20%20%20%20%20counter%5Bd%5D%20-%3D%201%20%20%23%20%E5%B0%86%20d%20%E7%9A%84%E6%95%B0%E9%87%8F%E5%87%8F%201%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E7%BB%93%E6%9E%9C%E8%A6%86%E7%9B%96%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20res%5Bi%5D%0A%0Adef%20radix_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%88%A4%E6%96%AD%E6%9C%80%E5%A4%A7%E4%BD%8D%E6%95%B0%0A%20%20%20%20m%20%3D%20max%28nums%29%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E4%BB%8E%E4%BD%8E%E4%BD%8D%E5%88%B0%E9%AB%98%E4%BD%8D%E7%9A%84%E9%A1%BA%E5%BA%8F%E9%81%8D%E5%8E%86%0A%20%20%20%20exp%20%3D%201%0A%20%20%20%20while%20exp%20%3C%3D%20m%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%AF%B9%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%20k%20%E4%BD%8D%E6%89%A7%E8%A1%8C%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%0A%20%20%20%20%20%20%20%20%23%20k%20%3D%201%20-%3E%20exp%20%3D%201%0A%20%20%20%20%20%20%20%20%23%20k%20%3D%202%20-%3E%20exp%20%3D%2010%0A%20%20%20%20%20%20%20%20%23%20%E5%8D%B3%20exp%20%3D%2010%5E%28k-1%29%0A%20%20%20%20%20%20%20%20counting_sort_digit%28nums,%20exp%29%0A%20%20%20%20%20%20%20%20exp%20*%3D%2010%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F%0A%20%20%20%20nums%20%3D%20%5B%0A%20%20%20%20%20%20%20%20105,%0A%20%20%20%20%20%20%20%20356,%0A%20%20%20%20%20%20%20%20428,%0A%20%20%20%20%20%20%20%20348,%0A%20%20%20%20%20%20%20%20818,%0A%20%20%20%20%5D%0A%20%20%20%20radix_sort%28nums%29%0A%20%20%20%20print%28%22%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20digit%28num%3A%20int,%20exp%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%85%83%E7%B4%A0%20num%20%E7%9A%84%E7%AC%AC%20k%20%E4%BD%8D%EF%BC%8C%E5%85%B6%E4%B8%AD%20exp%20%3D%2010%5E%28k-1%29%22%22%22%0A%20%20%20%20%23%20%E4%BC%A0%E5%85%A5%20exp%20%E8%80%8C%E9%9D%9E%20k%20%E5%8F%AF%E4%BB%A5%E9%81%BF%E5%85%8D%E5%9C%A8%E6%AD%A4%E9%87%8D%E5%A4%8D%E6%89%A7%E8%A1%8C%E6%98%82%E8%B4%B5%E7%9A%84%E6%AC%A1%E6%96%B9%E8%AE%A1%E7%AE%97%0A%20%20%20%20return%20%28num%20//%20exp%29%20%25%2010%0A%0Adef%20counting_sort_digit%28nums%3A%20list%5Bint%5D,%20exp%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%EF%BC%88%E6%A0%B9%E6%8D%AE%20nums%20%E7%AC%AC%20k%20%E4%BD%8D%E6%8E%92%E5%BA%8F%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%8D%81%E8%BF%9B%E5%88%B6%E7%9A%84%E4%BD%8D%E8%8C%83%E5%9B%B4%E4%B8%BA%200~9%20%EF%BC%8C%E5%9B%A0%E6%AD%A4%E9%9C%80%E8%A6%81%E9%95%BF%E5%BA%A6%E4%B8%BA%2010%20%E7%9A%84%E6%A1%B6%E6%95%B0%E7%BB%84%0A%20%20%20%20counter%20%3D%20%5B0%5D%20*%2010%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E7%BB%9F%E8%AE%A1%200~9%20%E5%90%84%E6%95%B0%E5%AD%97%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20d%20%3D%20digit%28nums%5Bi%5D,%20exp%29%20%20%23%20%E8%8E%B7%E5%8F%96%20nums%5Bi%5D%20%E7%AC%AC%20k%20%E4%BD%8D%EF%BC%8C%E8%AE%B0%E4%B8%BA%20d%0A%20%20%20%20%20%20%20%20counter%5Bd%5D%20%2B%3D%201%20%20%23%20%E7%BB%9F%E8%AE%A1%E6%95%B0%E5%AD%97%20d%20%E7%9A%84%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%0A%20%20%20%20%23%20%E6%B1%82%E5%89%8D%E7%BC%80%E5%92%8C%EF%BC%8C%E5%B0%86%E2%80%9C%E5%87%BA%E7%8E%B0%E4%B8%AA%E6%95%B0%E2%80%9D%E8%BD%AC%E6%8D%A2%E4%B8%BA%E2%80%9C%E6%95%B0%E7%BB%84%E7%B4%A2%E5%BC%95%E2%80%9D%0A%20%20%20%20for%20i%20in%20range%281,%2010%29%3A%0A%20%20%20%20%20%20%20%20counter%5Bi%5D%20%2B%3D%20counter%5Bi%20-%201%5D%0A%20%20%20%20%23%20%E5%80%92%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%8C%E6%A0%B9%E6%8D%AE%E6%A1%B6%E5%86%85%E7%BB%9F%E8%AE%A1%E7%BB%93%E6%9E%9C%EF%BC%8C%E5%B0%86%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%20res%0A%20%20%20%20res%20%3D%20%5B0%5D%20*%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20d%20%3D%20digit%28nums%5Bi%5D,%20exp%29%0A%20%20%20%20%20%20%20%20j%20%3D%20counter%5Bd%5D%20-%201%20%20%23%20%E8%8E%B7%E5%8F%96%20d%20%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E7%B4%A2%E5%BC%95%20j%0A%20%20%20%20%20%20%20%20res%5Bj%5D%20%3D%20nums%5Bi%5D%20%20%23%20%E5%B0%86%E5%BD%93%E5%89%8D%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E7%B4%A2%E5%BC%95%20j%0A%20%20%20%20%20%20%20%20counter%5Bd%5D%20-%3D%201%20%20%23%20%E5%B0%86%20d%20%E7%9A%84%E6%95%B0%E9%87%8F%E5%87%8F%201%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E7%BB%93%E6%9E%9C%E8%A6%86%E7%9B%96%E5%8E%9F%E6%95%B0%E7%BB%84%20nums%0A%20%20%20%20for%20i%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20res%5Bi%5D%0A%0Adef%20radix_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%88%A4%E6%96%AD%E6%9C%80%E5%A4%A7%E4%BD%8D%E6%95%B0%0A%20%20%20%20m%20%3D%20max%28nums%29%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E4%BB%8E%E4%BD%8E%E4%BD%8D%E5%88%B0%E9%AB%98%E4%BD%8D%E7%9A%84%E9%A1%BA%E5%BA%8F%E9%81%8D%E5%8E%86%0A%20%20%20%20exp%20%3D%201%0A%20%20%20%20while%20exp%20%3C%3D%20m%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%AF%B9%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%20k%20%E4%BD%8D%E6%89%A7%E8%A1%8C%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%0A%20%20%20%20%20%20%20%20%23%20k%20%3D%201%20-%3E%20exp%20%3D%201%0A%20%20%20%20%20%20%20%20%23%20k%20%3D%202%20-%3E%20exp%20%3D%2010%0A%20%20%20%20%20%20%20%20%23%20%E5%8D%B3%20exp%20%3D%2010%5E%28k-1%29%0A%20%20%20%20%20%20%20%20counting_sort_digit%28nums,%20exp%29%0A%20%20%20%20%20%20%20%20exp%20*%3D%2010%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F%0A%20%20%20%20nums%20%3D%20%5B%0A%20%20%20%20%20%20%20%20105,%0A%20%20%20%20%20%20%20%20356,%0A%20%20%20%20%20%20%20%20428,%0A%20%20%20%20%20%20%20%20348,%0A%20%20%20%20%20%20%20%20818,%0A%20%20%20%20%5D%0A%20%20%20%20radix_sort%28nums%29%0A%20%20%20%20print%28%22%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
!!! question "為什麼從最低位開始排序?"
在連續的排序輪次中,後一輪排序會覆蓋前一輪排序的結果。舉例來說,如果第一輪排序結果 $a < b$ ,而第二輪排序結果 $a > b$ ,那麼第二輪的結果將取代第一輪的結果。由於數字的高位優先順序高於低位,因此應該先排序低位再排序高位。
## 11.10.2 &nbsp; 演算法特性
相較於計數排序,基數排序適用於數值範圍較大的情況,**但前提是資料必須可以表示為固定位數的格式,且位數不能過大**。例如,浮點數不適合使用基數排序,因為其位數 $k$ 過大,可能導致時間複雜度 $O(nk) \gg O(n^2)$ 。
- **時間複雜度為 $O(nk)$**:設資料量為 $n$、資料為 $d$ 進位制、最大位數為 $k$ ,則對某一位執行計數排序使用 $O(n + d)$ 時間,排序所有 $k$ 位使用 $O((n + d)k)$ 時間。通常情況下,$d$ 和 $k$ 都相對較小,時間複雜度趨向 $O(n)$ 。
- **空間複雜度為 $O(n + d)$、非原地排序**:與計數排序相同,基數排序需要藉助長度為 $n$ 和 $d$ 的陣列 `res``counter`
- **穩定排序**:當計數排序穩定時,基數排序也穩定;當計數排序不穩定時,基數排序無法保證得到正確的排序結果。
@@ -0,0 +1,328 @@
---
comments: true
---
# 11.2 &nbsp; 選擇排序
<u>選擇排序(selection sort</u>的工作原理非常簡單:開啟一個迴圈,每輪從未排序區間選擇最小的元素,將其放到已排序區間的末尾。
設陣列的長度為 $n$ ,選擇排序的演算法流程如圖 11-2 所示。
1. 初始狀態下,所有元素未排序,即未排序(索引)區間為 $[0, n-1]$ 。
2. 選取區間 $[0, n-1]$ 中的最小元素,將其與索引 $0$ 處的元素交換。完成後,陣列前 1 個元素已排序。
3. 選取區間 $[1, n-1]$ 中的最小元素,將其與索引 $1$ 處的元素交換。完成後,陣列前 2 個元素已排序。
4. 以此類推。經過 $n - 1$ 輪選擇與交換後,陣列前 $n - 1$ 個元素已排序。
5. 僅剩的一個元素必定是最大元素,無須排序,因此陣列排序完成。
=== "<1>"
![選擇排序步驟](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" }
=== "<2>"
![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" }
=== "<3>"
![selection_sort_step3](selection_sort.assets/selection_sort_step3.png){ class="animation-figure" }
=== "<4>"
![selection_sort_step4](selection_sort.assets/selection_sort_step4.png){ class="animation-figure" }
=== "<5>"
![selection_sort_step5](selection_sort.assets/selection_sort_step5.png){ class="animation-figure" }
=== "<6>"
![selection_sort_step6](selection_sort.assets/selection_sort_step6.png){ class="animation-figure" }
=== "<7>"
![selection_sort_step7](selection_sort.assets/selection_sort_step7.png){ class="animation-figure" }
=== "<8>"
![selection_sort_step8](selection_sort.assets/selection_sort_step8.png){ class="animation-figure" }
=== "<9>"
![selection_sort_step9](selection_sort.assets/selection_sort_step9.png){ class="animation-figure" }
=== "<10>"
![selection_sort_step10](selection_sort.assets/selection_sort_step10.png){ class="animation-figure" }
=== "<11>"
![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" }
<p align="center"> 圖 11-2 &nbsp; 選擇排序步驟 </p>
在程式碼中,我們用 $k$ 來記錄未排序區間內的最小元素:
=== "Python"
```python title="selection_sort.py"
def selection_sort(nums: list[int]):
"""選擇排序"""
n = len(nums)
# 外迴圈:未排序區間為 [i, n-1]
for i in range(n - 1):
# 內迴圈:找到未排序區間內的最小元素
k = i
for j in range(i + 1, n):
if nums[j] < nums[k]:
k = j # 記錄最小元素的索引
# 將該最小元素與未排序區間的首個元素交換
nums[i], nums[k] = nums[k], nums[i]
```
=== "C++"
```cpp title="selection_sort.cpp"
/* 選擇排序 */
void selectionSort(vector<int> &nums) {
int n = nums.size();
// 外迴圈:未排序區間為 [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 內迴圈:找到未排序區間內的最小元素
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 記錄最小元素的索引
}
// 將該最小元素與未排序區間的首個元素交換
swap(nums[i], nums[k]);
}
}
```
=== "Java"
```java title="selection_sort.java"
/* 選擇排序 */
void selectionSort(int[] nums) {
int n = nums.length;
// 外迴圈:未排序區間為 [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 內迴圈:找到未排序區間內的最小元素
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 記錄最小元素的索引
}
// 將該最小元素與未排序區間的首個元素交換
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
```
=== "C#"
```csharp title="selection_sort.cs"
/* 選擇排序 */
void SelectionSort(int[] nums) {
int n = nums.Length;
// 外迴圈:未排序區間為 [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 內迴圈:找到未排序區間內的最小元素
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 記錄最小元素的索引
}
// 將該最小元素與未排序區間的首個元素交換
(nums[k], nums[i]) = (nums[i], nums[k]);
}
}
```
=== "Go"
```go title="selection_sort.go"
/* 選擇排序 */
func selectionSort(nums []int) {
n := len(nums)
// 外迴圈:未排序區間為 [i, n-1]
for i := 0; i < n-1; i++ {
// 內迴圈:找到未排序區間內的最小元素
k := i
for j := i + 1; j < n; j++ {
if nums[j] < nums[k] {
// 記錄最小元素的索引
k = j
}
}
// 將該最小元素與未排序區間的首個元素交換
nums[i], nums[k] = nums[k], nums[i]
}
}
```
=== "Swift"
```swift title="selection_sort.swift"
/* 選擇排序 */
func selectionSort(nums: inout [Int]) {
// 外迴圈:未排序區間為 [i, n-1]
for i in nums.indices.dropLast() {
// 內迴圈:找到未排序區間內的最小元素
var k = i
for j in nums.indices.dropFirst(i + 1) {
if nums[j] < nums[k] {
k = j // 記錄最小元素的索引
}
}
// 將該最小元素與未排序區間的首個元素交換
nums.swapAt(i, k)
}
}
```
=== "JS"
```javascript title="selection_sort.js"
/* 選擇排序 */
function selectionSort(nums) {
let n = nums.length;
// 外迴圈:未排序區間為 [i, n-1]
for (let i = 0; i < n - 1; i++) {
// 內迴圈:找到未排序區間內的最小元素
let k = i;
for (let j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) {
k = j; // 記錄最小元素的索引
}
}
// 將該最小元素與未排序區間的首個元素交換
[nums[i], nums[k]] = [nums[k], nums[i]];
}
}
```
=== "TS"
```typescript title="selection_sort.ts"
/* 選擇排序 */
function selectionSort(nums: number[]): void {
let n = nums.length;
// 外迴圈:未排序區間為 [i, n-1]
for (let i = 0; i < n - 1; i++) {
// 內迴圈:找到未排序區間內的最小元素
let k = i;
for (let j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) {
k = j; // 記錄最小元素的索引
}
}
// 將該最小元素與未排序區間的首個元素交換
[nums[i], nums[k]] = [nums[k], nums[i]];
}
}
```
=== "Dart"
```dart title="selection_sort.dart"
/* 選擇排序 */
void selectionSort(List<int> nums) {
int n = nums.length;
// 外迴圈:未排序區間為 [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 內迴圈:找到未排序區間內的最小元素
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k]) k = j; // 記錄最小元素的索引
}
// 將該最小元素與未排序區間的首個元素交換
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
```
=== "Rust"
```rust title="selection_sort.rs"
/* 選擇排序 */
fn selection_sort(nums: &mut [i32]) {
if nums.is_empty() {
return;
}
let n = nums.len();
// 外迴圈:未排序區間為 [i, n-1]
for i in 0..n - 1 {
// 內迴圈:找到未排序區間內的最小元素
let mut k = i;
for j in i + 1..n {
if nums[j] < nums[k] {
k = j; // 記錄最小元素的索引
}
}
// 將該最小元素與未排序區間的首個元素交換
nums.swap(i, k);
}
}
```
=== "C"
```c title="selection_sort.c"
/* 選擇排序 */
void selectionSort(int nums[], int n) {
// 外迴圈:未排序區間為 [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 內迴圈:找到未排序區間內的最小元素
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 記錄最小元素的索引
}
// 將該最小元素與未排序區間的首個元素交換
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
```
=== "Kotlin"
```kotlin title="selection_sort.kt"
/* 選擇排序 */
fun selectionSort(nums: IntArray) {
val n = nums.size
// 外迴圈:未排序區間為 [i, n-1]
for (i in 0..<n - 1) {
var k = i
// 內迴圈:找到未排序區間內的最小元素
for (j in i + 1..<n) {
if (nums[j] < nums[k]) k = j // 記錄最小元素的索引
}
// 將該最小元素與未排序區間的首個元素交換
nums[i] = nums[k].also { nums[k] = nums[i] }
}
}
```
=== "Ruby"
```ruby title="selection_sort.rb"
[class]{}-[func]{selection_sort}
```
=== "Zig"
```zig title="selection_sort.zig"
[class]{}-[func]{selectionSort}
```
??? pythontutor "視覺化執行"
<div style="height: 531px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20selection_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5Bi,%20n-1%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%89%BE%E5%88%B0%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E5%86%85%E7%9A%84%E6%9C%80%E5%B0%8F%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20k%20%3D%20i%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201,%20n%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3C%20nums%5Bk%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20k%20%3D%20j%20%20%23%20%E8%AE%B0%E5%BD%95%E6%9C%80%E5%B0%8F%E5%85%83%E7%B4%A0%E7%9A%84%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%E8%AF%A5%E6%9C%80%E5%B0%8F%E5%85%83%E7%B4%A0%E4%B8%8E%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E7%9A%84%E9%A6%96%E4%B8%AA%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%0A%20%20%20%20%20%20%20%20nums%5Bi%5D,%20nums%5Bk%5D%20%3D%20nums%5Bk%5D,%20nums%5Bi%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20selection_sort%28nums%29%0A%20%20%20%20print%28%22%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20selection_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5Bi,%20n-1%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%89%BE%E5%88%B0%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E5%86%85%E7%9A%84%E6%9C%80%E5%B0%8F%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20k%20%3D%20i%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%20%2B%201,%20n%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3C%20nums%5Bk%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20k%20%3D%20j%20%20%23%20%E8%AE%B0%E5%BD%95%E6%9C%80%E5%B0%8F%E5%85%83%E7%B4%A0%E7%9A%84%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%E8%AF%A5%E6%9C%80%E5%B0%8F%E5%85%83%E7%B4%A0%E4%B8%8E%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E7%9A%84%E9%A6%96%E4%B8%AA%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%0A%20%20%20%20%20%20%20%20nums%5Bi%5D,%20nums%5Bk%5D%20%3D%20nums%5Bk%5D,%20nums%5Bi%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4,%201,%203,%201,%205,%202%5D%0A%20%20%20%20selection_sort%28nums%29%0A%20%20%20%20print%28%22%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.2.1 &nbsp; 演算法特性
- **時間複雜度為 $O(n^2)$、非自適應排序**:外迴圈共 $n - 1$ 輪,第一輪的未排序區間長度為 $n$ ,最後一輪的未排序區間長度為 $2$ ,即各輪外迴圈分別包含 $n$、$n - 1$、$\dots$、$3$、$2$ 輪內迴圈,求和為 $\frac{(n - 1)(n + 2)}{2}$ 。
- **空間複雜度為 $O(1)$、原地排序**:指標 $i$ 和 $j$ 使用常數大小的額外空間。
- **非穩定排序**:如圖 11-3 所示,元素 `nums[i]` 有可能被交換至與其相等的元素的右邊,導致兩者的相對順序發生改變。
![選擇排序非穩定示例](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" }
<p align="center"> 圖 11-3 &nbsp; 選擇排序非穩定示例 </p>
@@ -0,0 +1,54 @@
---
comments: true
---
# 11.1 &nbsp; 排序演算法
<u>排序演算法(sorting algorithm</u>用於對一組資料按照特定順序進行排列。排序演算法有著廣泛的應用,因為有序資料通常能夠被更高效地查詢、分析和處理。
如圖 11-1 所示,排序演算法中的資料型別可以是整數、浮點數、字元或字串等。排序的判斷規則可根據需求設定,如數字大小、字元 ASCII 碼順序或自定義規則。
![資料型別和判斷規則示例](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" }
<p align="center"> 圖 11-1 &nbsp; 資料型別和判斷規則示例 </p>
## 11.1.1 &nbsp; 評價維度
**執行效率**:我們期望排序演算法的時間複雜度儘量低,且總體操作數量較少(時間複雜度中的常數項變小)。對於大資料量的情況,執行效率顯得尤為重要。
**就地性**:顧名思義,<u>原地排序</u>透過在原陣列上直接操作實現排序,無須藉助額外的輔助陣列,從而節省記憶體。通常情況下,原地排序的資料搬運操作較少,執行速度也更快。
**穩定性**<u>穩定排序</u>在完成排序後,相等元素在陣列中的相對順序不發生改變。
穩定排序是多級排序場景的必要條件。假設我們有一個儲存學生資訊的表格,第 1 列和第 2 列分別是姓名和年齡。在這種情況下,<u>非穩定排序</u>可能導致輸入資料的有序性喪失:
```shell
# 輸入資料是按照姓名排序好的
# (name, age)
('A', 19)
('B', 18)
('C', 21)
('D', 19)
('E', 23)
# 假設使用非穩定排序演算法按年齡排序串列,
# 結果中 ('D', 19) 和 ('A', 19) 的相對位置改變,
# 輸入資料按姓名排序的性質丟失
('B', 18)
('D', 19)
('A', 19)
('C', 21)
('E', 23)
```
**自適應性**<u>自適應排序</u>的時間複雜度會受輸入資料的影響,即最佳時間複雜度、最差時間複雜度、平均時間複雜度並不完全相等。
自適應性需要根據具體情況來評估。如果最差時間複雜度差於平均時間複雜度,說明排序演算法在某些資料下效能可能劣化,因此被視為負面屬性;而如果最佳時間複雜度優於平均時間複雜度,則被視為正面屬性。
**是否基於比較**<u>基於比較的排序</u>依賴比較運算子($<$、$=$、$>$)來判斷元素的相對順序,從而排序整個陣列,理論最優時間複雜度為 $O(n \log n)$ 。而<u>非比較排序</u>不使用比較運算子,時間複雜度可達 $O(n)$ ,但其通用性相對較差。
## 11.1.2 &nbsp; 理想排序演算法
**執行快、原地、穩定、正向自適應、通用性好**。顯然,迄今為止尚未發現兼具以上所有特性的排序演算法。因此,在選擇排序演算法時,需要根據具體的資料特點和問題需求來決定。
接下來,我們將共同學習各種排序演算法,並基於上述評價維度對各個排序演算法的優缺點進行分析。
+53
View File
@@ -0,0 +1,53 @@
---
comments: true
---
# 11.11 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 泡沫排序透過交換相鄰元素來實現排序。透過新增一個標誌位來實現提前返回,我們可以將泡沫排序的最佳時間複雜度最佳化到 $O(n)$ 。
- 插入排序每輪將未排序區間內的元素插入到已排序區間的正確位置,從而完成排序。雖然插入排序的時間複雜度為 $O(n^2)$ ,但由於單元操作相對較少,因此在小資料量的排序任務中非常受歡迎。
- 快速排序基於哨兵劃分操作實現排序。在哨兵劃分中,有可能每次都選取到最差的基準數,導致時間複雜度劣化至 $O(n^2)$ 。引入中位數基準數或隨機基準數可以降低這種劣化的機率。尾遞迴方法可以有效地減少遞迴深度,將空間複雜度最佳化到 $O(\log n)$ 。
- 合併排序包括劃分和合並兩個階段,典型地體現了分治策略。在合併排序中,排序陣列需要建立輔助陣列,空間複雜度為 $O(n)$ ;然而排序鏈結串列的空間複雜度可以最佳化至 $O(1)$ 。
- 桶排序包含三個步驟:資料分桶、桶內排序和合並結果。它同樣體現了分治策略,適用於資料體量很大的情況。桶排序的關鍵在於對資料進行平均分配。
- 計數排序是桶排序的一個特例,它透過統計資料出現的次數來實現排序。計數排序適用於資料量大但資料範圍有限的情況,並且要求資料能夠轉換為正整數。
- 基數排序透過逐位排序來實現資料排序,要求資料能夠表示為固定位數的數字。
- 總的來說,我們希望找到一種排序演算法,具有高效率、穩定、原地以及正向自適應性等優點。然而,正如其他資料結構和演算法一樣,沒有一種排序演算法能夠同時滿足所有這些條件。在實際應用中,我們需要根據資料的特性來選擇合適的排序演算法。
- 圖 11-19 對比了主流排序演算法的效率、穩定性、就地性和自適應性等。
![排序演算法對比](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" }
<p align="center"> 圖 11-19 &nbsp; 排序演算法對比 </p>
### 2. &nbsp; Q & A
**Q**:排序演算法穩定性在什麼情況下是必需的?
在現實中,我們有可能基於物件的某個屬性進行排序。例如,學生有姓名和身高兩個屬性,我們希望實現一個多級排序:先按照姓名進行排序,得到 `(A, 180) (B, 185) (C, 170) (D, 170)` ;再對身高進行排序。由於排序演算法不穩定,因此可能得到 `(D, 170) (C, 170) (A, 180) (B, 185)`
可以發現,學生 D 和 C 的位置發生了交換,姓名的有序性被破壞了,而這是我們不希望看到的。
**Q**:哨兵劃分中“從右往左查詢”與“從左往右查詢”的順序可以交換嗎?
不行,當我們以最左端元素為基準數時,必須先“從右往左查詢”再“從左往右查詢”。這個結論有些反直覺,我們來剖析一下原因。
哨兵劃分 `partition()` 的最後一步是交換 `nums[left]``nums[i]` 。完成交換後,基準數左邊的元素都 `<=` 基準數,**這就要求最後一步交換前 `nums[left] >= nums[i]` 必須成立**。假設我們先“從左往右查詢”,那麼如果找不到比基準數更大的元素,**則會在 `i == j` 時跳出迴圈,此時可能 `nums[j] == nums[i] > nums[left]`**。也就是說,此時最後一步交換操作會把一個比基準數更大的元素交換至陣列最左端,導致哨兵劃分失敗。
舉個例子,給定陣列 `[0, 0, 0, 0, 1]` ,如果先“從左向右查詢”,哨兵劃分後陣列為 `[1, 0, 0, 0, 0]` ,這個結果是不正確的。
再深入思考一下,如果我們選擇 `nums[right]` 為基準數,那麼正好反過來,必須先“從左往右查詢”。
**Q**:關於尾遞迴最佳化,為什麼選短的陣列能保證遞迴深度不超過 $\log n$ ?
遞迴深度就是當前未返回的遞迴方法的數量。每輪哨兵劃分我們將原陣列劃分為兩個子陣列。在尾遞迴最佳化後,向下遞迴的子陣列長度最大為原陣列長度的一半。假設最差情況,一直為一半長度,那麼最終的遞迴深度就是 $\log n$ 。
回顧原始的快速排序,我們有可能會連續地遞迴長度較大的陣列,最差情況下為 $n$、$n - 1$、$\dots$、$2$、$1$ ,遞迴深度為 $n$ 。尾遞迴最佳化可以避免這種情況出現。
**Q**:當陣列中所有元素都相等時,快速排序的時間複雜度是 $O(n^2)$ 嗎?該如何處理這種退化情況?
是的。對於這種情況,可以考慮透過哨兵劃分將陣列劃分為三個部分:小於、等於、大於基準數。僅向下遞迴小於和大於的兩部分。在該方法下,輸入元素全部相等的陣列,僅一輪哨兵劃分即可完成排序。
**Q**:桶排序的最差時間複雜度為什麼是 $O(n^2)$ ?
最差情況下,所有元素被分至同一個桶中。如果我們採用一個 $O(n^2)$ 演算法來排序這些元素,則時間複雜度為 $O(n^2)$ 。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
---
comments: true
icon: material/stack-overflow
---
# 第 5 章 &nbsp; 堆疊與佇列
![堆疊與佇列](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" }
!!! abstract
堆疊如同疊貓貓,而佇列就像貓貓排隊。
兩者分別代表先入後出和先入先出的邏輯關係。
## Chapter Contents
- [5.1 &nbsp; 堆疊](https://www.hello-algo.com/en/chapter_stack_and_queue/stack/)
- [5.2 &nbsp; 佇列](https://www.hello-algo.com/en/chapter_stack_and_queue/queue/)
- [5.3 &nbsp; 雙向佇列](https://www.hello-algo.com/en/chapter_stack_and_queue/deque/)
- [5.4 &nbsp; 小結](https://www.hello-algo.com/en/chapter_stack_and_queue/summary/)
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
---
comments: true
---
# 5.4 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 堆疊是一種遵循先入後出原則的資料結構,可透過陣列或鏈結串列來實現。
- 在時間效率方面,堆疊的陣列實現具有較高的平均效率,但在擴容過程中,單次入堆疊操作的時間複雜度會劣化至 $O(n)$ 。相比之下,堆疊的鏈結串列實現具有更為穩定的效率表現。
- 在空間效率方面,堆疊的陣列實現可能導致一定程度的空間浪費。但需要注意的是,鏈結串列節點所佔用的記憶體空間比陣列元素更大。
- 佇列是一種遵循先入先出原則的資料結構,同樣可以透過陣列或鏈結串列來實現。在時間效率和空間效率的對比上,佇列的結論與前述堆疊的結論相似。
- 雙向佇列是一種具有更高自由度的佇列,它允許在兩端進行元素的新增和刪除操作。
### 2. &nbsp; Q & A
**Q**:瀏覽器的前進後退是否是雙向鏈結串列實現?
瀏覽器的前進後退功能本質上是“堆疊”的體現。當用戶訪問一個新頁面時,該頁面會被新增到堆疊頂;當用戶點選後退按鈕時,該頁面會從堆疊頂彈出。使用雙向佇列可以方便地實現一些額外操作,這個在“雙向佇列”章節有提到。
**Q**:在出堆疊後,是否需要釋放出堆疊節點的記憶體?
如果後續仍需要使用彈出節點,則不需要釋放記憶體。若之後不需要用到,`Java``Python` 等語言擁有自動垃圾回收機制,因此不需要手動釋放記憶體;在 `C``C++` 中需要手動釋放記憶體。
**Q**:雙向佇列像是兩個堆疊拼接在了一起,它的用途是什麼?
雙向佇列就像是堆疊和佇列的組合或兩個堆疊拼在了一起。它表現的是堆疊 + 佇列的邏輯,因此可以實現堆疊與佇列的所有應用,並且更加靈活。
**Q**:撤銷(undo)和反撤銷(redo)具體是如何實現的?
使用兩個堆疊,堆疊 `A` 用於撤銷,堆疊 `B` 用於反撤銷。
1. 每當使用者執行一個操作,將這個操作壓入堆疊 `A` ,並清空堆疊 `B`
2. 當用戶執行“撤銷”時,從堆疊 `A` 中彈出最近的操作,並將其壓入堆疊 `B`
3. 當用戶執行“反撤銷”時,從堆疊 `B` 中彈出最近的操作,並將其壓入堆疊 `A`
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

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