mirror of
https://github.com/krahets/hello-algo.git
synced 2026-09-03 05:37:13 +00:00
build
This commit is contained in:
@@ -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”按鈕即可發起拉取請求。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 16-3 頁面編輯按鍵 </p>
|
||||
|
||||
圖片無法直接修改,需要透過新建 [Issue](https://github.com/krahets/hello-algo/issues) 或評論留言來描述問題,我們會盡快重新繪製並替換圖片。
|
||||
|
||||
### 2. 內容創作
|
||||
|
||||
如果您有興趣參與此開源專案,包括將程式碼翻譯成其他程式語言、擴展文章內容等,那麼需要實施以下 Pull Request 工作流程。
|
||||
|
||||
1. 登入 GitHub ,將本書的[程式碼倉庫](https://github.com/krahets/hello-algo) Fork 到個人帳號下。
|
||||
2. 進入您的 Fork 倉庫網頁,使用 `git clone` 命令將倉庫克隆至本地。
|
||||
3. 在本地進行內容創作,並進行完整測試,驗證程式碼的正確性。
|
||||
4. 將本地所做更改 Commit ,然後 Push 至遠端倉庫。
|
||||
5. 重新整理倉庫網頁,點選“Create pull request”按鈕即可發起拉取請求。
|
||||
|
||||
### 3. Docker 部署
|
||||
|
||||
在 `hello-algo` 根目錄下,執行以下 Docker 指令碼,即可在 `http://localhost:8000` 訪問本專案:
|
||||
|
||||
```shell
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
使用以下命令即可刪除部署:
|
||||
|
||||
```shell
|
||||
docker-compose down
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/help-circle-outline
|
||||
---
|
||||
|
||||
# 第 16 章 附錄
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [16.1 程式設計環境安裝](installation.md)
|
||||
- [16.2 一起參與創作](contribution.md)
|
||||
- [16.3 術語表](terminology.md)
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.1 程式設計環境安裝
|
||||
|
||||
## 16.1.1 安裝 IDE
|
||||
|
||||
推薦使用開源、輕量的 VS Code 作為本地整合開發環境(IDE)。訪問 [VS Code 官網](https://code.visualstudio.com/),根據作業系統選擇相應版本的 VS Code 進行下載和安裝。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 16-1 從官網下載 VS Code </p>
|
||||
|
||||
VS Code 擁有強大的擴展包生態系統,支持大多數程式語言的執行和除錯。以 Python 為例,安裝“Python Extension Pack”擴展包之後,即可進行 Python 程式碼除錯。安裝步驟如圖 16-2 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 16-2 安裝 VS Code 擴展包 </p>
|
||||
|
||||
## 16.1.2 安裝語言環境
|
||||
|
||||
### 1. 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. 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. Java 環境
|
||||
|
||||
1. 下載並安裝 [OpenJDK](https://jdk.java.net/18/)(版本需滿足 > JDK 9)。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `java` ,安裝 Extension Pack for Java 。
|
||||
|
||||
### 4. 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. Go 環境
|
||||
|
||||
1. 下載並安裝 [go](https://go.dev/dl/) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `go` ,安裝 Go 。
|
||||
3. 按快捷鍵 `Ctrl + Shift + P` 撥出命令欄,輸入 go ,選擇 `Go: Install/Update Tools` ,全部勾選並安裝即可。
|
||||
|
||||
### 6. 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. JavaScript 環境
|
||||
|
||||
1. 下載並安裝 [Node.js](https://nodejs.org/en/) 。
|
||||
2. (可選)在 VS Code 的擴充功能市場中搜索 `Prettier` ,安裝程式碼格式化工具。
|
||||
|
||||
### 8. TypeScript 環境
|
||||
|
||||
1. 同 JavaScript 環境安裝步驟。
|
||||
2. 安裝 [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation) 。
|
||||
3. 在 VS Code 的擴充功能市場中搜索 `typescript` ,安裝 [Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) 。
|
||||
|
||||
### 9. Dart 環境
|
||||
|
||||
1. 下載並安裝 [Dart](https://dart.dev/get-dart) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `dart` ,安裝 [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code) 。
|
||||
|
||||
### 10. 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,142 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.3 術語表
|
||||
|
||||
表 16-1 列出了書中出現的重要術語。建議記住各個名詞的英文叫法,以便閱讀英文文獻。
|
||||
|
||||
<p align="center"> 表 16-1 資料結構與演算法的重要名詞 </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$ 記號 |
|
||||
| asymptotic upper bound | 漸近上界 |
|
||||
| sign-magnitude | 原碼 |
|
||||
| 1’s complement | 一補數 |
|
||||
| 2’s 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 | 雜湊表 |
|
||||
| hash set | 雜湊集合 |
|
||||
| bucket | 桶 |
|
||||
| hash function | 雜湊函式 |
|
||||
| hash collision | 雜湊衝突 |
|
||||
| load factor | 負載因子 |
|
||||
| separate chaining | 鏈結位址 |
|
||||
| open addressing | 開放定址 |
|
||||
| linear probing | 線性探查 |
|
||||
| lazy deletion | 懶刪除 |
|
||||
| binary tree | 二元樹 |
|
||||
| tree node | 樹節點 |
|
||||
| left-child node | 左子節點 |
|
||||
| right-child node | 右子節點 |
|
||||
| parent node | 父節點 |
|
||||
| left subtree | 左子樹 |
|
||||
| right subtree | 右子樹 |
|
||||
| root node | 根節點 |
|
||||
| leaf node | 葉節點 |
|
||||
| edge | 邊 |
|
||||
| level | 層 |
|
||||
| degree | 度 |
|
||||
| height | 高度 |
|
||||
| depth | 深度 |
|
||||
| perfect binary tree | 完美二元樹 |
|
||||
| complete binary tree | 完全二元樹 |
|
||||
| full binary tree | 完滿二元樹 |
|
||||
| balanced binary tree | 平衡二元樹 |
|
||||
| binary search tree | 二元搜尋樹 |
|
||||
| AVL tree | AVL 樹 |
|
||||
| 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$ 問題 |
|
||||
| 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$ 皇后問題 |
|
||||
| dynamic programming | 動態規劃 |
|
||||
| initial state | 初始狀態 |
|
||||
| state-transition equation | 狀態轉移方程 |
|
||||
| knapsack problem | 背包問題 |
|
||||
| edit distance problem | 編輯距離問題 |
|
||||
| greedy algorithm | 貪婪演算法 |
|
||||
|
||||
</div>
|
||||
+1373
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 4.6 練習
|
||||
|
||||
## 4.6.1 知識鞏固
|
||||
|
||||
### 1. 陣列和鏈結串列怎樣找到元素
|
||||
|
||||
陣列和單向鏈結串列中都按順序儲存了 `[A, B, C, D, E]`。現在要讀取第 4 個元素 `D`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 陣列可以直接使用哪個索引?
|
||||
2. 單向鏈結串列從頭節點 `A` 開始,要沿著 `next` 依次經過哪些節點?
|
||||
3. 當要讀取的元素位置越來越靠後時,兩種結構所需的步驟會怎樣變化?哪一種更適合反覆按位置讀取?為什麼?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 若索引從 0 開始,第 4 個元素的索引是 3,陣列可直接讀取 `arr[3]`。
|
||||
|
||||
2. 單向鏈結串列必須從頭開始,訪問路徑為 `A → B → C → D`,需要沿 `next` 前進 3 次。
|
||||
|
||||
3. 陣列可以根據首位址和索引直接定位元素,按位置訪問的時間複雜度為 $O(1)$。
|
||||
單向鏈結串列訪問第 $k$ 個節點時,必須從頭節點開始,沿著 `next` 前進 $k-1$ 次,
|
||||
最壞需要 $O(n)$ 時間。
|
||||
|
||||
這裡只比較按位置讀取,不表示鏈結串列在所有操作上都更慢。
|
||||
|
||||
### 2. 陣列和鏈結串列怎樣插入元素
|
||||
|
||||
陣列和單向鏈結串列中都儲存了 `A、B、C、D`。現在要在 `B` 後面插入 `X`:
|
||||
|
||||
- 陣列容量為 5,當前狀態是 `[A, B, C, D, _]`;
|
||||
- 鏈結串列為 `A → B → C → D`,並且已經拿到了指向節點 `B` 的引用。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 陣列需要移動哪些元素?寫出插入後的陣列。
|
||||
2. 鏈結串列應按什麼順序修改 `X.next` 和 `B.next`?寫出插入後的鏈結串列。
|
||||
3. 為什麼比較插入效率時,要特別說明“已經拿到了節點 B 的引用”?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 陣列要先把 `D` 向右移動一格,再把 `C` 向右移動一格,最後把 `X` 放入索引 2,
|
||||
得到 `[A, B, X, C, D]`。
|
||||
|
||||
2. `B.next` 原本指向 `C`。應先令 `X.next = B.next`,讓 `X` 指向 `C`;
|
||||
再令 `B.next = X`。結果為 `A → B → X → C → D`。
|
||||
如果先覆蓋 `B.next` 又沒有儲存原來的連線,就可能找不到 `C`。
|
||||
|
||||
3. 已知 `B` 的位置後,鏈結串列插入只需修改兩個連線,可以在 $O(1)$ 時間完成。
|
||||
如果還要從頭查詢 `B`,查詢過程本身可能需要 $O(n)$ 時間。
|
||||
|
||||
### 3. 串列容量是怎樣增長的
|
||||
|
||||
一個基於陣列實現的串列當前內容為 `[A, B, C]`,長度 `size = 3`,容量 `capacity = 4`。
|
||||
規定容量不足時,新陣列的容量擴大為原來的 2 倍。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 追加 `D` 後,串列的長度和容量分別是多少?是否需要擴容?
|
||||
2. 接著追加 `E` 時,容量會變為多少?需要複製幾個原有元素?
|
||||
3. 底層陣列的長度不可變,為什麼串列的容量看起來卻可以增長?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. `D` 可以放入最後一個空位。此時內容為 `[A, B, C, D]`,
|
||||
`size = 4`、`capacity = 4`,不需要擴容。
|
||||
|
||||
2. 再追加 `E` 時已經沒有空位,需要建立容量為 8 的新陣列,
|
||||
將 `A、B、C、D` 共 4 個原有元素複製過去,再加入 `E`。
|
||||
此時 `size = 5`、`capacity = 8`。
|
||||
|
||||
3. 原來的陣列本身沒有變長。串列建立了一個更大的新陣列,複製原有元素,
|
||||
再改用新陣列作為底層儲存,因此對使用者來說容量增長了。
|
||||
|
||||
## 4.6.2 程式設計練習
|
||||
|
||||
### 1. 陣列表示的大整數加一
|
||||
|
||||
陣列 `digits` 從左到右儲存一個非負整數的各位數字,例如 `[3, 0, 8]` 表示 308。
|
||||
數字 0 用 `[0]` 表示;其他輸入的第一位均不為 0。
|
||||
|
||||
請模擬一次十進位制豎式加法,將這個整數增加 1,並把結果仍按相同的陣列形式返回。
|
||||
可以直接修改 `digits`;若最前面產生新的進位,可以返回一個更長的陣列。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 像做豎式加法一樣,從陣列最後一位開始
|
||||
2. 當前位小於 9 時加一後即可立即返回
|
||||
3. 當前位等於 9 時把它改成 0;若所有位都是 9,需要在最前面補 1
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 反轉單向鏈結串列
|
||||
|
||||
給定一個單向鏈結串列的頭節點 `head`。每個節點包含一個值和指向下一節點的 `next`。
|
||||
|
||||
請使用迭代方法反轉所有節點之間的連線,並返回反轉後的頭節點。
|
||||
要求不建立新的鏈結串列節點。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 先在紙上畫出三個相連的節點和 prev、cur 兩個指標
|
||||
2. 改寫 cur.next 前,必須先用 nxt 儲存原來的下一個節點
|
||||
3. 反轉 cur.next 後,令 prev = cur,再令 cur = nxt,繼續處理原鏈結串列中的下一個節點
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/reverse-linked-list/solutions/2361282/206-fan-zhuan-lian-biao-shuang-zhi-zhen-r1jel/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/view-list-outline
|
||||
---
|
||||
|
||||
# 第 4 章 陣列與鏈結串列
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
資料結構的世界如同一堵厚實的磚牆。
|
||||
|
||||
陣列的磚塊整齊排列,逐個緊貼。鏈結串列的磚塊分散各處,連線的藤蔓自由地穿梭於磚縫之間。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [4.1 陣列](array.md)
|
||||
- [4.2 鏈結串列](linked_list.md)
|
||||
- [4.3 串列](list.md)
|
||||
- [4.4 記憶體與快取 *](ram_and_cache.md)
|
||||
- [4.5 小結](summary.md)
|
||||
- [4.6 練習](exercises.md)
|
||||
+1467
File diff suppressed because it is too large
Load Diff
+2301
File diff suppressed because one or more lines are too long
@@ -0,0 +1,84 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# 4.4 記憶體與快取 *
|
||||
|
||||
在本章的前兩節中,我們探討了陣列和鏈結串列這兩種基礎且重要的資料結構,它們分別代表了“連續儲存”和“分散儲存”兩種物理結構。
|
||||
|
||||
實際上,**物理結構在很大程度上決定了程式對記憶體和快取的使用效率**,進而影響演算法程式的整體效能。
|
||||
|
||||
## 4.4.1 計算機儲存裝置
|
||||
|
||||
計算機中包括三種類型的儲存裝置:<u>硬碟(hard disk)</u>、<u>記憶體(random-access memory, RAM)</u>、<u>快取(cache memory)</u>。表 4-2 展示了它們在計算機系統中的不同角色和效能特點。
|
||||
|
||||
<p align="center"> 表 4-2 計算機的儲存裝置 </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 核心之間的物理距離會變遠,從而導致資料傳輸時間增加,元素訪問延遲變高。在當前技術下,多層級的快取結構是容量、速度和成本之間的最佳平衡點。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 4-9 計算機儲存系統 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
計算機的儲存層次結構體現了速度、容量和成本三者之間的精妙平衡。實際上,這種權衡普遍存在於所有工業領域,它要求我們在不同的優勢和限制之間找到最佳平衡點。
|
||||
|
||||
總的來說,**硬碟用於長期儲存大量資料,記憶體用於臨時儲存程式執行中正在處理的資料,而快取則用於儲存經常訪問的資料和指令**,以提高程式執行效率。三者共同協作,確保計算機系統高效執行。
|
||||
|
||||
如圖 4-10 所示,在程式執行時,資料會從硬碟中被讀取到記憶體中,供 CPU 計算使用。快取可以看作 CPU 的一部分,**它透過智慧地從記憶體載入資料**,給 CPU 提供高速的資料讀取,從而顯著提升程式的執行效率,減少對較慢的記憶體的依賴。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 4-10 硬碟、記憶體和快取之間的資料流通 </p>
|
||||
|
||||
## 4.4.2 資料結構的記憶體效率
|
||||
|
||||
在記憶體空間利用方面,陣列和鏈結串列各自具有優勢和侷限性。
|
||||
|
||||
一方面,**記憶體是有限的,且同一塊記憶體不能被多個程式共享**,因此我們希望資料結構能夠儘可能高效地利用空間。陣列的元素緊密排列,不需要額外的空間來儲存鏈結串列節點間的引用(指標),因此空間效率更高。然而,陣列需要一次性分配足夠的連續記憶體空間,這可能導致記憶體浪費,陣列擴容也需要額外的時間和空間成本。相比之下,鏈結串列以“節點”為單位進行動態記憶體分配和回收,提供了更大的靈活性。
|
||||
|
||||
另一方面,在程式執行時,**隨著反覆申請與釋放記憶體,空閒記憶體的碎片化程度會越來越高**,從而導致記憶體的利用效率降低。陣列由於其連續的儲存方式,相對不容易導致記憶體碎片化。相反,鏈結串列的元素是分散儲存的,在頻繁的插入與刪除操作中,更容易導致記憶體碎片化。
|
||||
|
||||
## 4.4.3 資料結構的快取效率
|
||||
|
||||
快取雖然在空間容量上遠小於記憶體,但它比記憶體快得多,在程式執行速度上起著至關重要的作用。由於快取的容量有限,只能儲存一小部分頻繁訪問的資料,因此當 CPU 嘗試訪問的資料不在快取中時,就會發生<u>快取未命中(cache miss)</u>,此時 CPU 不得不從速度較慢的記憶體中載入所需資料。
|
||||
|
||||
顯然,**“快取未命中”越少,CPU 讀寫資料的效率就越高**,程式效能也就越好。我們將 CPU 從快取中成功獲取資料的比例稱為<u>快取命中率(cache hit rate)</u>,這個指標通常用來衡量快取效率。
|
||||
|
||||
為了儘可能達到更高的效率,快取會採取以下資料載入機制。
|
||||
|
||||
- **快取行**:快取不是單個位元組地儲存與載入資料,而是以快取行為單位。相比於單個位元組的傳輸,快取行的傳輸形式更加高效。
|
||||
- **預取機制**:處理器會嘗試預測資料訪問模式(例如順序訪問、固定步長跳躍訪問等),並根據特定模式將資料載入至快取之中,從而提升命中率。
|
||||
- **空間區域性**:如果一個數據被訪問,那麼它附近的資料可能近期也會被訪問。因此,快取在載入某一資料時,也會載入其附近的資料,以提高命中率。
|
||||
- **時間區域性**:如果一個數據被訪問,那麼它在不久的將來很可能再次被訪問。快取利用這一原理,透過保留最近訪問過的資料來提高命中率。
|
||||
|
||||
實際上,**陣列和鏈結串列對快取的利用效率是不同的**,主要體現在以下幾個方面。
|
||||
|
||||
- **佔用空間**:鏈結串列元素比陣列元素佔用空間更多,導致快取中容納的有效資料量更少。
|
||||
- **快取行**:鏈結串列資料分散在記憶體各處,而快取是“按行載入”的,因此載入到無效資料的比例更高。
|
||||
- **預取機制**:陣列比鏈結串列的資料訪問模式更具“可預測性”,即系統更容易猜出即將被載入的資料。
|
||||
- **空間區域性**:陣列被儲存在集中的記憶體空間中,因此被載入資料附近的資料更有可能即將被訪問。
|
||||
|
||||
總體而言,**陣列具有更高的快取命中率,因此它在操作效率上通常優於鏈結串列**。這使得在解決演算法問題時,基於陣列實現的資料結構往往更受歡迎。
|
||||
|
||||
需要注意的是,**高快取效率並不意味著陣列在所有情況下都優於鏈結串列**。實際應用中選擇哪種資料結構,應根據具體需求來決定。例如,陣列和鏈結串列都可以實現“堆疊”資料結構(下一章會詳細介紹),但它們適用於不同場景。
|
||||
|
||||
- 在做演算法題時,我們會傾向於選擇基於陣列實現的堆疊,因為它提供了更高的操作效率和隨機訪問的能力,代價僅是需要預先為陣列分配一定的記憶體空間。
|
||||
- 如果資料量非常大、動態性很高、堆疊的預期大小難以估計,那麼基於鏈結串列實現的堆疊更加合適。鏈結串列能夠將大量資料分散儲存於記憶體的不同部分,並且避免了陣列擴容產生的額外開銷。
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 陣列和鏈結串列是兩種基本的資料結構,分別代表資料在計算機記憶體中的兩種儲存方式:連續空間儲存和分散空間儲存。兩者的特點呈現出互補的特性。
|
||||
- 陣列支持隨機訪問、佔用記憶體較少;但插入和刪除元素效率低,且初始化後長度不可變。
|
||||
- 鏈結串列透過更改引用(指標)實現高效的節點插入與刪除,且可以靈活調整長度;但節點訪問效率低、佔用記憶體較多。常見的鏈結串列型別包括單向鏈結串列、環形鏈結串列、雙向鏈結串列。
|
||||
- 串列是一種支持增刪查改的元素有序集合,通常基於動態陣列實現。它保留了陣列的優勢,同時可以靈活調整長度。
|
||||
- 串列的出現大幅提高了陣列的實用性,但可能導致部分記憶體空間浪費。
|
||||
- 程式執行時,資料主要儲存在記憶體中。陣列可提供更高的記憶體空間效率,而鏈結串列則在記憶體使用上更加靈活。
|
||||
- 快取透過快取行、預取機制以及空間區域性和時間區域性等資料載入機制,為 CPU 提供快速資料訪問,顯著提升程式的執行效率。
|
||||
- 由於陣列具有更高的快取命中率,因此它通常比鏈結串列更高效。在選擇資料結構時,應根據具體需求和場景做出恰當選擇。
|
||||
|
||||
### 2. 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]] * n` 生成了一個二維串列,其中每一個 `[0]` 都是獨立的嗎?
|
||||
|
||||
不是獨立的。此二維串列中,所有的 `[0]` 實際上是同一個物件的引用。如果我們修改其中一個元素,會發現所有的對應元素都會隨之改變。
|
||||
|
||||
如果希望二維串列中的每個 `[0]` 都是獨立的,可以使用 `res = [[0] for _ in range(n)]` 來實現。這種方式的原理是初始化了 $n$ 個獨立的 `[0]` 串列物件。
|
||||
|
||||
**Q**:操作 `res = [0] * n` 生成了一個串列,其中每一個整數 0 都是獨立的嗎?
|
||||
|
||||
在該串列中,所有整數 0 都是同一個物件的引用。這是因為 Python 對小整數(通常是 -5 到 256)採用了快取池機制,以便最大化物件複用,從而提升效能。
|
||||
|
||||
雖然它們指向同一個物件,但我們仍然可以獨立修改串列中的每個元素,這是因為 Python 的整數是“不可變物件”。當我們修改某個元素時,實際上是切換為另一個物件的引用,而不是改變原有物件本身。
|
||||
|
||||
然而,當串列元素是“可變物件”時(例如串列、字典或類別例項等),修改某個元素會直接改變該物件本身,所有引用該物件的元素都會產生相同變化。
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,96 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 13.6 練習
|
||||
|
||||
## 13.6.1 知識鞏固
|
||||
|
||||
### 1. 這段排列演算法會漏掉結果嗎
|
||||
|
||||
一個回溯演算法按 `1、2、3` 的順序嘗試生成全部排列。每次選擇數字 `x` 時,它會:
|
||||
|
||||
1. 把 `x` 加到當前路徑末尾;
|
||||
2. 把 `x` 標記為“已使用”;
|
||||
3. 遞迴填寫下一個位置。
|
||||
|
||||
遞迴返回後,同學只從路徑末尾刪除了 `x`,然後繼續嘗試下一個數字。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 演算法首先得到哪個排列?它還能得到全部 6 個排列嗎?
|
||||
2. 遞迴返回上一層前,只刪除路徑末尾的數字是否足夠?如果不夠,還需要做什麼?說明理由。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 它首先得到 `[1, 2, 3]`,但無法得到全部排列。雖然返回時路徑變短了,
|
||||
數字 1、2、3 的標記仍都是“已使用”,後續分支便沒有可選數字。
|
||||
|
||||
2. 不夠。刪除路徑末尾的 `x` 後,還必須把 `x` 重新標記為“未使用”。
|
||||
當前路徑和已使用標記共同描述搜尋狀態;選擇時修改了兩處,回退時也必須把兩處都恢復,
|
||||
其他分支才能再次選擇 `x`。
|
||||
|
||||
### 2. 數字的選擇順序重要嗎
|
||||
|
||||
給定排好序的陣列 `[2, 3, 5]` 和目標值 5,每個數可以重複選擇。
|
||||
演算法規定每條搜尋路徑中的數字只能按從小到大的順序出現。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 能得到哪些不同的組合?
|
||||
2. 為什麼同一組數字不需要按不同順序重複搜尋?“從小到大”的限制起到了什麼作用?
|
||||
3. 當前路徑為 `[3]`、還差 2 時,下一個候選數是 3。為什麼此時可以停止檢查這一層後面的所有候選數?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 不同的組合為 `[2, 3]` 和 `[5]`。
|
||||
|
||||
2. 本題把 `[2, 3]` 和 `[3, 2]` 看作同一種組合,數字的選擇順序不計入答案。
|
||||
規定路徑中的數字從小到大出現,就能在搜尋時直接跳過 `[3, 2]` 這類重複組合。
|
||||
|
||||
3. 當前還差 2,而候選數 3 已經大於 2。因為陣列已經排好序,
|
||||
3 後面的候選數只會更大,也都不可能加入當前組合,所以可以直接結束這一層的檢查。
|
||||
|
||||
### 3. 下一枚皇后可以放在哪些位置
|
||||
|
||||
在一個 `4 × 4` 棋盤上按行放置皇后,行、列下標都從 0 開始。
|
||||
目前已經在 `(0, 1)` 和 `(1, 3)` 放置了皇后,現在要在第 2 行放置下一個皇后。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 哪些列會因為“同列”而被排除?
|
||||
2. 在剩餘列中,哪些位置會因為“同一條對角線”而被排除?
|
||||
3. 第 2 行還剩哪些位置可以嘗試?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 第 1 列和第 3 列已經有皇后,因此位置 `(2, 1)` 和 `(2, 3)` 被排除。
|
||||
|
||||
2. 在剩餘位置中,`(2, 2)` 與 `(1, 3)` 位於同一條對角線上,因此也被排除。
|
||||
位置 `(2, 0)` 與已有兩個皇后都不在同列或同一條對角線上。
|
||||
|
||||
3. 第 2 行可以嘗試的位置只有 `(2, 0)`。
|
||||
|
||||
這一步只說明當前放置合法;之後若無法完成棋盤,仍需回退並嘗試更早的其他選擇。
|
||||
|
||||
## 13.6.2 程式設計練習
|
||||
|
||||
### 1. 無重複元素的全排列
|
||||
|
||||
整數陣列 `nums` 至少包含一個元素,並且其中各元素互不相同。
|
||||
請列出把這些元素各使用一次所能形成的全部順序,並將每一種順序作為一個陣列返回。
|
||||
結果中各排列的先後次序不作要求。
|
||||
請使用回溯,並用布林陣列記錄每個位置的元素是否已經選入當前排列。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 遞迴深度表示正在填寫排列中的第幾個位置
|
||||
2. 每一層只嘗試尚未使用的元素
|
||||
3. 路徑長度達到 `nums` 的長度時,把它的副本加入答案
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/permutations/solutions/2363882/46-quan-pai-lie-hui-su-qing-xi-tu-jie-by-6o7h/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **說明:** 連結題解透過交換陣列元素,把已選元素依次放到陣列前部;本練習使用布林陣列記錄每個元素是否已選。兩種方法都能避免同一元素被重複選擇,但程式碼結構不同
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/map-marker-path
|
||||
---
|
||||
|
||||
# 第 13 章 回溯
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
我們如同迷宮中的探索者,在前進的道路上可能會遇到困難。
|
||||
|
||||
回溯的力量讓我們能夠重新開始,不斷嘗試,最終找到通往光明的出口。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [13.1 回溯演算法](backtracking_algorithm.md)
|
||||
- [13.2 全排列問題](permutations_problem.md)
|
||||
- [13.3 子集和問題](subset_sum_problem.md)
|
||||
- [13.4 N 皇后問題](n_queens_problem.md)
|
||||
- [13.5 小結](summary.md)
|
||||
- [13.6 練習](exercises.md)
|
||||
@@ -0,0 +1,754 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 13.4 n 皇后問題
|
||||
|
||||
!!! question
|
||||
|
||||
根據國際象棋的規則,皇后可以攻擊與同處一行、一列或一條斜線上的棋子。給定 $n$ 個皇后和一個 $n \times n$ 大小的棋盤,尋找使得所有皇后之間無法相互攻擊的擺放方案。
|
||||
|
||||
如圖 13-15 所示,當 $n = 4$ 時,共可以找到兩個解。從回溯演算法的角度看,$n \times n$ 大小的棋盤共有 $n^2$ 個格子,給出了所有的選擇 `choices` 。在逐個放置皇后的過程中,棋盤狀態在不斷地變化,每個時刻的棋盤就是狀態 `state` 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-15 4 皇后問題的解 </p>
|
||||
|
||||
圖 13-16 展示了本題的三個約束條件:**多個皇后不能在同一行、同一列、同一條對角線上**。值得注意的是,對角線分為主對角線 `\` 和次對角線 `/` 兩種。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-16 n 皇后問題的約束條件 </p>
|
||||
|
||||
### 1. 逐行放置策略
|
||||
|
||||
皇后的數量和棋盤的行數都為 $n$ ,因此我們容易得到一個推論:**棋盤每行都允許且只允許放置一個皇后**。
|
||||
|
||||
也就是說,我們可以採取逐行放置策略:從第一行開始,在每行放置一個皇后,直至最後一行結束。
|
||||
|
||||
圖 13-17 所示為 4 皇后問題的逐行放置過程。受畫幅限制,圖 13-17 僅展開了第一行的其中一個搜尋分支,並且將不滿足列約束和對角線約束的方案都進行了剪枝。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-17 逐行放置策略 </p>
|
||||
|
||||
從本質上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出現多個皇后的所有搜尋分支。
|
||||
|
||||
### 2. 列與對角線剪枝
|
||||
|
||||
為了滿足列約束,我們可以利用一個長度為 $n$ 的布林型陣列 `cols` 記錄每一列是否有皇后。在每次決定放置前,我們透過 `cols` 將已有皇后的列進行剪枝,並在回溯中動態更新 `cols` 的狀態。
|
||||
|
||||
!!! tip
|
||||
|
||||
請注意,矩陣的起點位於左上角,其中行索引從上到下增加,列索引從左到右增加。
|
||||
|
||||
那麼,如何處理對角線約束呢?設棋盤中某個格子的行列索引為 $(row, col)$ ,選定矩陣中的某條主對角線,我們發現該對角線上所有格子的行索引減列索引都相等,**即主對角線上所有格子的 $row - col$ 為恆定值**。
|
||||
|
||||
也就是說,如果兩個格子滿足 $row_1 - col_1 = row_2 - col_2$ ,則它們一定處在同一條主對角線上。利用該規律,我們可以藉助圖 13-18 所示的陣列 `diags1` 記錄每條主對角線上是否有皇后。
|
||||
|
||||
同理,**次對角線上的所有格子的 $row + col$ 是恆定值**。我們同樣也可以藉助陣列 `diags2` 來處理次對角線約束。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-18 處理列約束和對角線約束 </p>
|
||||
|
||||
### 3. 程式碼實現
|
||||
|
||||
請注意,$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)
|
||||
return
|
||||
}
|
||||
// 走訪所有列
|
||||
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 {
|
||||
res.push(state.clone());
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for col in 0..n {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
let diag1 = row + n - 1 - col;
|
||||
let diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = "Q".into();
|
||||
(cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][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![vec!["#".to_string(); n]; n];
|
||||
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: MutableList<MutableList<String>>,
|
||||
res: MutableList<MutableList<MutableList<String>>?>,
|
||||
cols: BooleanArray,
|
||||
diags1: BooleanArray,
|
||||
diags2: BooleanArray
|
||||
) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row == n) {
|
||||
val copyState = mutableListOf<MutableList<String>>()
|
||||
for (sRow in state) {
|
||||
copyState.add(sRow.toMutableList())
|
||||
}
|
||||
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): MutableList<MutableList<MutableList<String>>?> {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
val state = mutableListOf<MutableList<String>>()
|
||||
for (i in 0..<n) {
|
||||
val row = mutableListOf<String>()
|
||||
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 = mutableListOf<MutableList<MutableList<String>>?>()
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2)
|
||||
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="n_queens.rb"
|
||||
### 回溯演算法:n 皇后 ###
|
||||
def backtrack(row, n, state, res, cols, diags1, diags2)
|
||||
# 當放置完所有行時,記錄解
|
||||
if row == n
|
||||
res << state.map { |row| row.dup }
|
||||
return
|
||||
end
|
||||
|
||||
# 走訪所有列
|
||||
for col in 0...n
|
||||
# 計算該格子對應的主對角線和次對角線
|
||||
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
|
||||
# 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2)
|
||||
# 回退:將該格子恢復為空位
|
||||
state[row][col] = "#"
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
### 求解 n 皇后 ###
|
||||
def n_queens(n)
|
||||
# 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
state = Array.new(n) { Array.new(n, "#") }
|
||||
cols = Array.new(n, false) # 記錄列是否有皇后
|
||||
diags1 = Array.new(2 * n - 1, false) # 記錄主對角線上是否有皇后
|
||||
diags2 = Array.new(2 * n - 1, false) # 記錄次對角線上是否有皇后
|
||||
res = []
|
||||
backtrack(0, n, state, res, cols, diags1, diags2)
|
||||
|
||||
res
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%0A%20%20%20%20n%3A%20int%2C%0A%20%20%20%20state%3A%20list%5Blist%5Bstr%5D%5D%2C%0A%20%20%20%20res%3A%20list%5Blist%5Blist%5Bstr%5D%5D%5D%2C%0A%20%20%20%20cols%3A%20list%5Bbool%5D%2C%0A%20%20%20%20diags1%3A%20list%5Bbool%5D%2C%0A%20%20%20%20diags2%3A%20list%5Bbool%5D%2C%0A%29%3A%0A%20%20%20%20%22%22%22%E5%9B%9E%E6%BA%AF%E6%BC%94%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%E7%95%B6%E6%94%BE%E7%BD%AE%E5%AE%8C%E6%89%80%E6%9C%89%E8%A1%8C%E6%99%82%EF%BC%8C%E8%A8%98%E9%8C%84%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%E8%B5%B0%E8%A8%AA%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%A8%88%E7%AE%97%E8%A9%B2%E6%A0%BC%E5%AD%90%E5%B0%8D%E6%87%89%E7%9A%84%E4%B8%BB%E5%B0%8D%E8%A7%92%E7%B7%9A%E5%92%8C%E6%AC%A1%E5%B0%8D%E8%A7%92%E7%B7%9A%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%A8%B1%E8%A9%B2%E6%A0%BC%E5%AD%90%E6%89%80%E5%9C%A8%E5%88%97%E3%80%81%E4%B8%BB%E5%B0%8D%E8%A7%92%E7%B7%9A%E3%80%81%E6%AC%A1%E5%B0%8D%E8%A7%92%E7%B7%9A%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%98%97%E8%A9%A6%EF%BC%9A%E5%B0%87%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E5%9C%A8%E8%A9%B2%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%2C%20n%2C%20state%2C%20res%2C%20cols%2C%20diags1%2C%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%87%E8%A9%B2%E6%A0%BC%E5%AD%90%E6%81%A2%E5%BE%A9%E7%82%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%2An%20%E5%A4%A7%E5%B0%8F%E7%9A%84%E6%A3%8B%E7%9B%A4%EF%BC%8C%E5%85%B6%E4%B8%AD%20%27Q%27%20%E4%BB%A3%E8%A1%A8%E7%9A%87%E5%90%8E%EF%BC%8C%27%23%27%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%2A%20n%20%20%23%20%E8%A8%98%E9%8C%84%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%2A%20%282%20%2A%20n%20-%201%29%20%20%23%20%E8%A8%98%E9%8C%84%E4%B8%BB%E5%B0%8D%E8%A7%92%E7%B7%9A%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%2A%20%282%20%2A%20n%20-%201%29%20%20%23%20%E8%A8%98%E9%8C%84%E6%AC%A1%E5%B0%8D%E8%A7%92%E7%B7%9A%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%2C%20n%2C%20state%2C%20res%2C%20cols%2C%20diags1%2C%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%BC%B8%E5%85%A5%E6%A3%8B%E7%9B%A4%E9%95%B7%E5%AF%AC%E7%82%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%A8%AE%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%2C%0A%20%20%20%20n%3A%20int%2C%0A%20%20%20%20state%3A%20list%5Blist%5Bstr%5D%5D%2C%0A%20%20%20%20res%3A%20list%5Blist%5Blist%5Bstr%5D%5D%5D%2C%0A%20%20%20%20cols%3A%20list%5Bbool%5D%2C%0A%20%20%20%20diags1%3A%20list%5Bbool%5D%2C%0A%20%20%20%20diags2%3A%20list%5Bbool%5D%2C%0A%29%3A%0A%20%20%20%20%22%22%22%E5%9B%9E%E6%BA%AF%E6%BC%94%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%E7%95%B6%E6%94%BE%E7%BD%AE%E5%AE%8C%E6%89%80%E6%9C%89%E8%A1%8C%E6%99%82%EF%BC%8C%E8%A8%98%E9%8C%84%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%E8%B5%B0%E8%A8%AA%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%A8%88%E7%AE%97%E8%A9%B2%E6%A0%BC%E5%AD%90%E5%B0%8D%E6%87%89%E7%9A%84%E4%B8%BB%E5%B0%8D%E8%A7%92%E7%B7%9A%E5%92%8C%E6%AC%A1%E5%B0%8D%E8%A7%92%E7%B7%9A%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%A8%B1%E8%A9%B2%E6%A0%BC%E5%AD%90%E6%89%80%E5%9C%A8%E5%88%97%E3%80%81%E4%B8%BB%E5%B0%8D%E8%A7%92%E7%B7%9A%E3%80%81%E6%AC%A1%E5%B0%8D%E8%A7%92%E7%B7%9A%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%98%97%E8%A9%A6%EF%BC%9A%E5%B0%87%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E5%9C%A8%E8%A9%B2%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%2C%20n%2C%20state%2C%20res%2C%20cols%2C%20diags1%2C%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%87%E8%A9%B2%E6%A0%BC%E5%AD%90%E6%81%A2%E5%BE%A9%E7%82%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%2An%20%E5%A4%A7%E5%B0%8F%E7%9A%84%E6%A3%8B%E7%9B%A4%EF%BC%8C%E5%85%B6%E4%B8%AD%20%27Q%27%20%E4%BB%A3%E8%A1%A8%E7%9A%87%E5%90%8E%EF%BC%8C%27%23%27%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%2A%20n%20%20%23%20%E8%A8%98%E9%8C%84%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%2A%20%282%20%2A%20n%20-%201%29%20%20%23%20%E8%A8%98%E9%8C%84%E4%B8%BB%E5%B0%8D%E8%A7%92%E7%B7%9A%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%2A%20%282%20%2A%20n%20-%201%29%20%20%23%20%E8%A8%98%E9%8C%84%E6%AC%A1%E5%B0%8D%E8%A7%92%E7%B7%9A%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%2C%20n%2C%20state%2C%20res%2C%20cols%2C%20diags1%2C%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%BC%B8%E5%85%A5%E6%A3%8B%E7%9B%A4%E9%95%B7%E5%AF%AC%E7%82%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%A8%AE%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 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 回溯演算法本質是窮舉法,透過對解空間進行深度優先走訪來尋找符合條件的解。在搜尋過程中,遇到滿足條件的解則記錄,直至找到所有解或走訪完成後結束。
|
||||
- 回溯演算法的搜尋過程包括嘗試與回退兩個部分。它透過深度優先搜尋來嘗試各種選擇,當遇到不滿足約束條件的情況時,則撤銷上一步的選擇,退回到之前的狀態,並繼續嘗試其他選擇。嘗試與回退是兩個方向相反的操作。
|
||||
- 回溯問題通常包含多個約束條件,它們可用於實現剪枝操作。剪枝可以提前結束不必要的搜尋分支,大幅提升搜尋效率。
|
||||
- 回溯演算法主要可用於解決搜尋問題和約束滿足問題。組合最佳化問題雖然可以用回溯演算法解決,但往往存在效率更高或效果更好的解法。
|
||||
- 全排列問題旨在搜尋給定集合元素的所有可能的排列。我們藉助一個陣列來記錄每個元素是否被選擇,剪掉重複選擇同一元素的搜尋分支,確保每個元素只被選擇一次。
|
||||
- 在全排列問題中,如果集合中存在重複元素,則最終結果會出現重複排列。我們需要約束相等元素在每輪中只能被選擇一次,這通常藉助一個雜湊集合來實現。
|
||||
- 子集和問題的目標是在給定集合中找到和為目標值的所有子集。集合不區分元素順序,而搜尋過程會輸出所有順序的結果,產生重複子集。我們在回溯前將資料進行排序,並設定一個變數來指示每一輪的走訪起始點,從而將生成重複子集的搜尋分支進行剪枝。
|
||||
- 對於子集和問題,陣列中的相等元素會產生重複集合。我們利用陣列已排序的前置條件,透過判斷相鄰元素是否相等實現剪枝,從而確保相等元素在每輪中只能被選中一次。
|
||||
- $n$ 皇后問題旨在尋找將 $n$ 個皇后放置到 $n \times n$ 尺寸棋盤上的方案,要求所有皇后兩兩之間無法攻擊對方。該問題的約束條件有行約束、列約束、主對角線和次對角線約束。為滿足行約束,我們採用按行放置的策略,保證每一行放置一個皇后。
|
||||
- 列約束和對角線約束的處理方式類似。對於列約束,我們利用一個陣列來記錄每一列是否有皇后,從而指示選中的格子是否合法。對於對角線約束,我們藉助兩個陣列來分別記錄該主、次對角線上是否存在皇后;難點在於找出處在同一主(副)對角線上的格子所滿足的行列索引規律。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:怎麼理解回溯和遞迴的關係?
|
||||
|
||||
總的來看,回溯是一種“演算法策略”,而遞迴更像是一個“工具”。
|
||||
|
||||
- 回溯演算法通常基於遞迴實現。然而,回溯是遞迴的應用場景之一,是遞迴在搜尋問題中的應用。
|
||||
- 遞迴的結構體現了“子問題分解”的解題範式,常用於解決分治、回溯、動態規劃(記憶化遞迴)等問題。
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 2.6 練習
|
||||
|
||||
## 2.6.1 知識鞏固
|
||||
|
||||
### 1. 迭代與遞迴的時間和空間
|
||||
|
||||
下面兩段程式碼都計算 $1 + 2 + \dots + n$(設 $n \ge 1$)。請把 `n` 設為 4,
|
||||
按照程式實際執行的順序回答問題,然後比較兩種寫法的效率。
|
||||
|
||||
```python
|
||||
def sum_iter(n):
|
||||
s = 0
|
||||
for i in range(1, n + 1):
|
||||
s += i
|
||||
return s
|
||||
|
||||
def sum_recur(n):
|
||||
if n == 1:
|
||||
return 1
|
||||
return n + sum_recur(n - 1)
|
||||
```
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 執行 `sum_iter(4)` 時,每輪迴圈結束後,變數 `s` 的值分別是多少?
|
||||
2. 執行 `sum_recur(4)` 時,會依次呼叫哪些函式?從最深的一層開始返回時,結果怎樣得到?
|
||||
3. 兩種寫法的時間複雜度和空間複雜度分別是多少?結合第 1、2 問的執行過程說明理由。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 迴圈變數 `i` 依次為 `1、2、3、4`,每輪結束後,`s` 依次變為
|
||||
`1、3、6、10`,所以 `sum_iter(4)` 返回 10。
|
||||
|
||||
2. 函式依次呼叫
|
||||
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`。
|
||||
`sum_recur(1)` 返回 1,隨後各層依次得到 `2 + 1 = 3`、`3 + 3 = 6`、`4 + 6 = 10`。
|
||||
在最深處,4 次函式呼叫都尚未結束。
|
||||
|
||||
3. 兩段程式碼都進行與 $n$ 成正比的迴圈或呼叫,因此時間複雜度均為 $O(n)$ 。
|
||||
空間複雜度不同:迭代版只使用常數個變數,為 $O(1)$ ;
|
||||
遞迴版在到達終止條件前,前面的函式呼叫都要等待返回結果,因此呼叫堆疊中最多同時儲存 $n$ 次呼叫,
|
||||
空間複雜度為 $O(n)$。
|
||||
|
||||
分析空間複雜度時,除程式碼中的變數外,還要考慮遞迴呼叫佔用的空間。
|
||||
|
||||
### 2. 三段程式碼的時間複雜度
|
||||
|
||||
以下三個程式碼片段的輸入均為正整數 $n$ 。請按時間複雜度從低到高排序,並寫出各自的複雜度。
|
||||
|
||||
```python
|
||||
# 片段一
|
||||
s = 0
|
||||
for i in range(n):
|
||||
s += i
|
||||
|
||||
# 片段二
|
||||
s = 0
|
||||
for i in range(n):
|
||||
for j in range(i, n):
|
||||
s += j
|
||||
|
||||
# 片段三
|
||||
while n > 1:
|
||||
n = n // 2
|
||||
```
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
從低到高為:片段三 $O(\log n)$、片段一 $O(n)$、片段二 $O(n^2)$。
|
||||
片段三每輪把 $n$ 縮小為原來的一半,約迴圈 $\log_2 n$ 次。
|
||||
片段一的迴圈恰好執行 $n$ 次。片段二的內層迴圈次數依次為
|
||||
$n,n-1,\dots,1$,總次數為 $n(n+1)/2$,因此屬於平方階。
|
||||
|
||||
### 3. 哪種反轉更節省空間
|
||||
|
||||
要將陣列 `nums` 中的元素全部反轉,有兩種做法:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 新建一個等長陣列 `res`,倒序複製後返回;
|
||||
2. 用兩個索引 `i` 和 `j` 分別從首、尾向中間移動,逐對交換 `nums[i]` 與 `nums[j]` 。
|
||||
|
||||
兩種做法的空間複雜度各是多少?哪種屬於“原地”操作?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 需要與輸入等長的輔助陣列,空間複雜度 $O(n)$。
|
||||
|
||||
2. 只使用兩個索引變數,
|
||||
空間複雜度 $O(1)$ ,屬於原地操作。
|
||||
|
||||
需要注意:原地反轉會修改輸入陣列,
|
||||
僅在允許修改輸入時才應優先選用;若需保留原陣列,第 1 種做法的複製開銷不可避免。
|
||||
|
||||
## 2.6.2 程式設計練習
|
||||
|
||||
### 1. 費波那契數
|
||||
|
||||
費波那契數列滿足:$F(0)=0$、$F(1)=1$,並且當 $n\ge2$ 時,
|
||||
$F(n)=F(n-1)+F(n-2)$。
|
||||
|
||||
給定非負整數 `n`,請使用迴圈計算並返回 $F(n)$,不使用遞迴。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 先單獨處理 n 為 0 和 1 的情況
|
||||
2. 計算下一項時只需要前兩項,無須儲存整個數列
|
||||
3. 更新兩個變數時,注意不要過早覆蓋仍會用到的舊值
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/fibonacci-number/solutions/2361746/509-fei-bo-na-qi-shu-dong-tai-gui-hua-qi-so8h/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/timer-sand
|
||||
---
|
||||
|
||||
# 第 2 章 複雜度分析
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
複雜度分析猶如浩瀚的演算法宇宙中的時空嚮導。
|
||||
|
||||
它帶領我們在時間與空間這兩個維度上深入探索,尋找更優雅的解決方案。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [2.1 演算法效率評估](performance_evaluation.md)
|
||||
- [2.2 迭代與遞迴](iteration_and_recursion.md)
|
||||
- [2.3 時間複雜度](time_complexity.md)
|
||||
- [2.4 空間複雜度](space_complexity.md)
|
||||
- [2.5 小結](summary.md)
|
||||
- [2.6 練習](exercises.md)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.1 演算法效率評估
|
||||
|
||||
在演算法設計中,我們先後追求以下兩個層面的目標。
|
||||
|
||||
1. **找到問題解法**:演算法需要在規定的輸入範圍內可靠地求得問題的正確解。
|
||||
2. **尋求最優解法**:同一個問題可能存在多種解法,我們希望找到儘可能高效的演算法。
|
||||
|
||||
也就是說,在能夠解決問題的前提下,演算法效率已成為衡量演算法優劣的主要評價指標,它包括以下兩個維度。
|
||||
|
||||
- **時間效率**:演算法執行時間的長短。
|
||||
- **空間效率**:演算法佔用記憶體空間的大小。
|
||||
|
||||
簡而言之,**我們的目標是設計“既快又省”的資料結構與演算法**。而有效地評估演算法效率至關重要,因為只有這樣,我們才能將各種演算法進行對比,進而指導演算法設計與最佳化過程。
|
||||
|
||||
效率評估方法主要分為兩種:實際測試、理論估算。
|
||||
|
||||
## 2.1.1 實際測試
|
||||
|
||||
假設我們現在有演算法 `A` 和演算法 `B` ,它們都能解決同一問題,現在需要對比這兩個演算法的效率。最直接的方法是找一臺計算機,執行這兩個演算法,並監控記錄它們的執行時間和記憶體佔用情況。這種評估方式能夠反映真實情況,但也存在較大的侷限性。
|
||||
|
||||
一方面,**難以排除測試環境的干擾因素**。硬體配置會影響演算法的效能表現。比如一個演算法的並行度較高,那麼它就更適合在多核 CPU 上執行,一個演算法的記憶體操作密集,那麼它在高效能記憶體上的表現就會更好。也就是說,演算法在不同的機器上的測試結果可能是不一致的。這意味著我們需要在各種機器上進行測試,統計平均效率,而這是不現實的。
|
||||
|
||||
另一方面,**展開完整測試非常耗費資源**。隨著輸入資料量的變化,演算法會表現出不同的效率。例如,在輸入資料量較小時,演算法 `A` 的執行時間比演算法 `B` 短;而在輸入資料量較大時,測試結果可能恰恰相反。因此,為了得到有說服力的結論,我們需要測試各種規模的輸入資料,而這需要耗費大量的計算資源。
|
||||
|
||||
## 2.1.2 理論估算
|
||||
|
||||
由於實際測試具有較大的侷限性,我們可以考慮僅透過一些計算來評估演算法的效率。這種估算方法被稱為<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,59 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
**演算法效率評估**
|
||||
|
||||
- 時間效率和空間效率是衡量演算法優劣的兩個主要評價指標。
|
||||
- 我們可以透過實際測試來評估演算法效率,但難以消除測試環境的影響,且會耗費大量計算資源。
|
||||
- 複雜度分析可以消除實際測試的弊端,分析結果適用於所有執行平臺,並且能夠揭示演算法在不同資料規模下的效率。
|
||||
|
||||
**時間複雜度**
|
||||
|
||||
- 時間複雜度用於衡量演算法執行時間隨資料量增長的趨勢,可以有效評估演算法效率,但在某些情況下可能失效,如在輸入的資料量較小或時間複雜度相同時,無法精確對比演算法效率的優劣。
|
||||
- 最差時間複雜度使用大 $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. 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$ 就很好選了,這時增長趨勢已經佔主導了。
|
||||
|
||||
**Q** 是否存在根據實際使用場景,選擇犧牲時間(或空間)來設計演算法的情況?
|
||||
|
||||
在實際應用中,大部分情況會選擇犧牲空間換時間。例如資料庫索引,我們通常選擇建立 B+ 樹或雜湊索引,佔用大量記憶體空間,以換取 $O(\log n)$ 甚至 $O(1)$ 的高效查詢。
|
||||
|
||||
在空間資源寶貴的場景,也會選擇犧牲時間換空間。例如在嵌入式開發中,裝置記憶體很寶貴,工程師可能會放棄使用雜湊表,選擇使用陣列順序查詢,以節省記憶體佔用,代價是查詢變慢。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.2 基本資料型別
|
||||
|
||||
當談及計算機中的資料時,我們會想到文字、圖片、影片、語音、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 基本資料型別的佔用空間和取值範圍 </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=""
|
||||
# Ruby 的串列可以自由儲存各種基本資料型別和物件引用
|
||||
data = [0, 0.0, 'a', false, ListNode(0)]
|
||||
```
|
||||
|
||||
??? 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%8F%88%E7%B5%90%E4%B8%B2%E5%88%97%E7%AF%80%E9%BB%9E%E9%A1%9E%E5%88%A5%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E7%AF%80%E9%BB%9E%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%BE%8C%E7%B9%BC%E7%AF%80%E9%BB%9E%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%A8%AE%E5%9F%BA%E6%9C%AC%E8%B3%87%E6%96%99%E5%9E%8B%E5%88%A5%E4%BE%86%E5%88%9D%E5%A7%8B%E5%8C%96%E9%99%A3%E5%88%97%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20%2A%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20%2A%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E5%85%83%E5%AF%A6%E9%9A%9B%E4%B8%8A%E6%98%AF%E9%95%B7%E5%BA%A6%E7%82%BA%201%20%E7%9A%84%E5%AD%97%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B%270%27%5D%20%2A%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20%2A%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E4%B8%B2%E5%88%97%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%84%B2%E5%AD%98%E5%90%84%E7%A8%AE%E5%9F%BA%E6%9C%AC%E8%B3%87%E6%96%99%E5%9E%8B%E5%88%A5%E5%92%8C%E7%89%A9%E4%BB%B6%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0%2C%200.0%2C%20%27a%27%2C%20False%2C%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%8F%88%E7%B5%90%E4%B8%B2%E5%88%97%E7%AF%80%E9%BB%9E%E9%A1%9E%E5%88%A5%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E7%AF%80%E9%BB%9E%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%BE%8C%E7%B9%BC%E7%AF%80%E9%BB%9E%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%A8%AE%E5%9F%BA%E6%9C%AC%E8%B3%87%E6%96%99%E5%9E%8B%E5%88%A5%E4%BE%86%E5%88%9D%E5%A7%8B%E5%8C%96%E9%99%A3%E5%88%97%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20%2A%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20%2A%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E5%85%83%E5%AF%A6%E9%9A%9B%E4%B8%8A%E6%98%AF%E9%95%B7%E5%BA%A6%E7%82%BA%201%20%E7%9A%84%E5%AD%97%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B%270%27%5D%20%2A%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20%2A%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E4%B8%B2%E5%88%97%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%84%B2%E5%AD%98%E5%90%84%E7%A8%AE%E5%9F%BA%E6%9C%AC%E8%B3%87%E6%96%99%E5%9E%8B%E5%88%A5%E5%92%8C%E7%89%A9%E4%BB%B6%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0%2C%200.0%2C%20%27a%27%2C%20False%2C%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,95 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.4 字元編碼 *
|
||||
|
||||
在計算機中,所有資料都是以二進位制數的形式儲存的,字元 `char` 也不例外。為了表示字元,我們需要建立一套“字元集”,規定每個字元和二進位制數之間的一一對應關係。有了字元集之後,計算機就可以透過查表完成二進位制數到字元的轉換。
|
||||
|
||||
## 3.4.1 ASCII 字元集
|
||||
|
||||
<u>ASCII 碼</u>是最早出現的字元集,其全稱為 American Standard Code for Information Interchange(美國標準資訊交換程式碼)。它使用 7 位二進位制數(一個位元組的低 7 位)表示一個字元,最多能夠表示 128 個不同的字元。如圖 3-6 所示,ASCII 碼包括英文字母的大小寫、數字 0 ~ 9、一些標點符號,以及一些控制字元(如換行符和製表符)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-6 ASCII 碼 </p>
|
||||
|
||||
然而,**ASCII 碼僅能夠表示英文**。隨著計算機的全球化,誕生了一種能夠表示更多語言的 <u>EASCII</u> 字元集。它在 ASCII 的 7 位基礎上擴展到 8 位,能夠表示 256 個不同的字元。
|
||||
|
||||
在世界範圍內,陸續出現了一批適用於不同地區的 EASCII 字元集。這些字元集的前 128 個字元統一為 ASCII 碼,後 128 個字元定義不同,以適應不同語言的需求。
|
||||
|
||||
## 3.4.2 GBK 字元集
|
||||
|
||||
後來人們發現,**EASCII 碼仍然無法滿足許多語言的字元數量要求**。比如漢字有近十萬個,光日常使用的就有幾千個。中國國家標準總局於 1980 年釋出了 <u>GB2312</u> 字元集,其收錄了 6763 個漢字,基本滿足了漢字的計算機處理需要。
|
||||
|
||||
然而,GB2312 無法處理部分罕見字和繁體字。<u>GBK</u> 字元集是在 GB2312 的基礎上擴展得到的,它共收錄了 21886 個漢字。在 GBK 的編碼方案中,ASCII 字元使用一個位元組表示,漢字使用兩個位元組表示。
|
||||
|
||||
## 3.4.3 Unicode 字元集
|
||||
|
||||
隨著計算機技術的蓬勃發展,字元集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字元集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字元集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。
|
||||
|
||||
那個時代的研究人員就在想:**如果推出一個足夠完整的字元集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎**?在這種想法的驅動下,一個大而全的字元集 Unicode 應運而生。
|
||||
|
||||
<u>Unicode</u> 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字元集之中,提供一種通用的字元集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。
|
||||
|
||||
Unicode 作為一種通用字元集,本質上是給每個字元分配唯一的“碼點”(字元編號),其取值範圍為 U+0000 至 U+10FFFF,構成了統一的字元編號空間。然而,**Unicode 並沒有規定在計算機中如何儲存這些字元碼點**。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?
|
||||
|
||||
對於以上問題,**一種直接的解決方案是將所有字元儲存為等長的編碼**。如圖 3-7 所示,“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“Hello 演算法”中的所有字元都編碼為 2 位元組長度。這樣系統就可以每隔 2 位元組解析一個字元,恢復這個短語的內容了。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-7 Unicode 編碼示例 </p>
|
||||
|
||||
然而 ASCII 碼已經向我們證明,編碼英文只需 1 位元組。若採用上述方案,英文文字佔用空間的大小將會是 ASCII 編碼下的兩倍,非常浪費記憶體空間。因此,我們需要一種更加高效的 Unicode 編碼方法。
|
||||
|
||||
## 3.4.4 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$ ,與假設矛盾。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-8 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 程式語言的字元編碼
|
||||
|
||||
對於以往的大多數程式語言,程式執行中的字串都採用 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 資料結構分類
|
||||
|
||||
常見的資料結構包括陣列、鏈結串列、堆疊、佇列、雜湊表、樹、堆積、圖,它們可以從“邏輯結構”和“物理結構”兩個維度進行分類。
|
||||
|
||||
## 3.1.1 邏輯結構:線性與非線性
|
||||
|
||||
**邏輯結構揭示了資料元素之間的邏輯關係**。在陣列和鏈結串列中,資料按照一定順序排列,體現了資料之間的線性關係;而在樹中,資料從頂部向下按層次排列,表現出“祖先”與“後代”之間的派生關係;圖則由節點和邊構成,反映了複雜的網路關係。
|
||||
|
||||
如圖 3-1 所示,邏輯結構可分為“線性”和“非線性”兩大類。線性結構比較直觀,指資料在邏輯關係上呈線性排列;非線性結構則相反,呈非線性排列。
|
||||
|
||||
- **線性資料結構**:陣列、鏈結串列、堆疊、佇列、雜湊表,元素之間是一對一的順序關係。
|
||||
- **非線性資料結構**:樹、堆積、圖、雜湊表。
|
||||
|
||||
非線性資料結構可以進一步劃分為樹形結構和網狀結構。
|
||||
|
||||
- **樹形結構**:樹、堆積、雜湊表,元素之間是一對多的關係。
|
||||
- **網狀結構**:圖,元素之間是多對多的關係。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-1 線性資料結構與非線性資料結構 </p>
|
||||
|
||||
## 3.1.2 物理結構:連續與分散
|
||||
|
||||
**當演算法程式執行時,正在處理的資料主要儲存在記憶體中**。圖 3-2 展示了一個計算機記憶體條,其中每個黑色方塊都包含一塊記憶體空間。我們可以將記憶體想象成一個巨大的 Excel 表格,其中每個單元格都可以儲存一定大小的資料。
|
||||
|
||||
**系統透過記憶體位址來訪問目標位置的資料**。如圖 3-2 所示,計算機根據特定規則為表格中的每個單元格分配編號,確保每個記憶體空間都有唯一的記憶體位址。有了這些位址,程式便可以訪問記憶體中的資料。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-2 記憶體條、記憶體空間、記憶體位址 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
值得說明的是,將記憶體比作 Excel 表格是一個簡化的類比,實際記憶體的工作機制比較複雜,涉及位址空間、記憶體管理、快取機制、虛擬記憶體和物理記憶體等概念。
|
||||
|
||||
記憶體是所有程式的共享資源,當某塊記憶體被某個程式佔用時,則通常無法被其他程式同時使用了。**因此在資料結構與演算法的設計中,記憶體資源是一個重要的考慮因素**。比如,演算法所佔用的記憶體峰值不應超過系統剩餘空閒記憶體;如果缺少連續大塊的記憶體空間,那麼所選用的資料結構必須能夠儲存在分散的記憶體空間內。
|
||||
|
||||
如圖 3-3 所示,**物理結構反映了資料在計算機記憶體中的儲存方式**,可分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。物理結構從底層決定了資料的訪問、更新、增刪等操作方法,兩種物理結構在時間效率和空間效率方面呈現出互補的特點。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-3 連續空間儲存與分散空間儲存 </p>
|
||||
|
||||
值得說明的是,**所有資料結構都是基於陣列、鏈結串列或二者的組合實現的**。例如,堆疊和佇列既可以使用陣列實現,也可以使用鏈結串列實現;而雜湊表的實現可能同時包含陣列和鏈結串列。
|
||||
|
||||
- **基於陣列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖、矩陣、張量(維度 $\geq 3$ 的陣列)等。
|
||||
- **基於鏈結串列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖等。
|
||||
|
||||
鏈結串列在初始化後,仍可以在程式執行過程中對其長度進行調整,因此也稱“動態資料結構”。陣列在初始化後長度不可變,因此也稱“靜態資料結構”。值得注意的是,陣列可透過重新分配記憶體實現長度變化,從而具備一定的“動態性”。
|
||||
|
||||
!!! tip
|
||||
|
||||
如果你感覺物理結構理解起來有困難,建議先閱讀下一章,然後再回顧本節內容。
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 3.6 練習
|
||||
|
||||
## 3.6.1 知識鞏固
|
||||
|
||||
### 1. 生活場景中的資料關係
|
||||
|
||||
請根據資料之間的關係,為下面三個場景選擇“線性結構、樹形結構、網狀結構”中的一種,並說明理由:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 同學們排成一列,每人只關心自己前面和後面的人;
|
||||
2. 學校按“學校 → 年級 → 班級”分層管理;
|
||||
3. 城市道路連線多個路口,一個路口可以通向多個其他路口,也可能形成環路。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 是線性結構。除排在最前和最後的人外,每個人都只與前面和後面各一人相鄰,關係沿一條線展開。
|
||||
|
||||
2. 是樹形結構。每個班級屬於一個年級,每個年級又屬於學校,關係從上到下分層展開。
|
||||
|
||||
3. 是網狀結構。一個路口可以連線多個路口,路線之間還能形成環,不能排成單一順序或嚴格層級。
|
||||
|
||||
判斷結構時應先觀察元素之間的關係,而不是先考慮它在記憶體中佔多少空間。
|
||||
|
||||
### 2. 邏輯順序怎樣存進記憶體
|
||||
|
||||
為了儲存邏輯順序 `A → B → C`,現有兩種簡化的記憶體安排:
|
||||
|
||||
- 方案甲:`A、B、C` 分別放在編號為 `20、21、22` 的記憶體格中;
|
||||
- 方案乙:`A、B、C` 分別放在編號為 `20、7、31` 的記憶體格中,並由 `A` 記錄 `B` 的位置、`B` 記錄 `C` 的位置。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 哪個方案屬於連續空間儲存?哪個屬於分散空間儲存?
|
||||
2. 兩個方案分別更接近陣列還是鏈結串列?
|
||||
3. 方案乙的記憶體格編號沒有按大小排列,為什麼仍能表示 `A → B → C` 的邏輯順序?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 方案甲使用連續的記憶體格,屬於連續空間儲存;方案乙的節點分散在不同位置,屬於分散空間儲存。
|
||||
|
||||
2. 方案甲更接近陣列,方案乙更接近鏈結串列。
|
||||
|
||||
3. 邏輯順序由節點之間記錄的連線關係決定,而不是由記憶體格編號的大小決定。
|
||||
從 `A` 記錄的位置可以找到 `B`,再從 `B` 記錄的位置找到 `C`,所以仍能依次訪問 `A、B、C`。
|
||||
|
||||
這也說明邏輯結構和物理結構是觀察同一組資料的兩個不同角度。
|
||||
|
||||
### 3. 作業記錄中的資料型別與結構
|
||||
|
||||
某學習小組按座位順序記錄 4 名同學是否交了作業,得到:
|
||||
|
||||
`[true, false, true, true]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 每個元素適合使用哪種基本資料型別?
|
||||
2. 這 4 個元素按座位順序排成一列,使用了什麼邏輯結構?
|
||||
3. 如果以後改為記錄每人的作業分數 `[90, 0, 85, 100]`,改變的是資料的“內容型別”還是“組織方式”?說明理由。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 每個元素只表示“是”或“否”,適合使用布林型別 `bool`。
|
||||
|
||||
2. 這些元素按座位順序排列,形成線性結構,可以用陣列儲存。
|
||||
|
||||
3. 改變的是內容型別:元素由布林值變成了整數。組織方式沒有改變,
|
||||
這些資料仍然按座位順序排成一列,仍可使用陣列這一線性結構。
|
||||
|
||||
基本資料型別描述“存的是什麼”,資料結構描述“資料怎樣組織”。
|
||||
|
||||
## 3.6.2 程式設計練習
|
||||
|
||||
### 1. 統計二進位制表示中的 1
|
||||
|
||||
給定非負整數 `n`,請統計它的二進位制表示中共有多少個 1。
|
||||
|
||||
要求使用位運算完成,不把二進位制表示轉換成字串,也不使用直接統計 1 的內建函式。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. n & 1 可以取出 n 的最右邊一位,用它判斷這一位是否為 1
|
||||
2. 右移一位表示丟掉當前最右邊的二進位制位;多數語言使用運算子 >>
|
||||
3. 完成逐位檢查並右移的方法後,再觀察:n & (n - 1) 會把 n 中最右邊的一個 1 變成 0
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/number-of-1-bits/solutions/2361978/191-wei-1-de-ge-shu-wei-yun-suan-qing-xi-40rw/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/shape-outline
|
||||
---
|
||||
|
||||
# 第 3 章 資料結構
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
資料結構如同一副穩固而多樣的框架。
|
||||
|
||||
它為資料的有序組織提供了藍圖,演算法得以在此基礎上生動起來。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [3.1 資料結構分類](classification_of_data_structure.md)
|
||||
- [3.2 基本資料型別](basic_data_types.md)
|
||||
- [3.3 數字編碼 *](number_encoding.md)
|
||||
- [3.4 字元編碼 *](character_encoding.md)
|
||||
- [3.5 小結](summary.md)
|
||||
- [3.6 練習](exercises.md)
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.3 數字編碼 *
|
||||
|
||||
!!! tip
|
||||
|
||||
在本書中,標題帶有 * 符號的是選讀章節。如果你時間有限或感到理解困難,可以先跳過,等學完必讀章節後再單獨攻克。
|
||||
|
||||
## 3.3.1 原碼、一補數和二補數
|
||||
|
||||
在上一節的表格中我們發現,所有整數型別能夠表示的負數都比正數多一個,例如 `byte` 的取值範圍是 $[-128, 127]$ 。這個現象比較反直覺,它的內在原因涉及原碼、一補數、二補數的相關知識。
|
||||
|
||||
首先需要指出,**數字是以“二補數”的形式儲存在計算機中的**。在分析這樣做的原因之前,首先給出三者的定義。
|
||||
|
||||
- **原碼**:我們將數字的二進位制表示的最高位視為符號位,其中 $0$ 表示正數,$1$ 表示負數,其餘位表示數字的值。
|
||||
- **一補數**:正數的一補數與其原碼相同,負數的一補數是對其原碼除符號位外的所有位取反。
|
||||
- **二補數**:正數的二補數與其原碼相同,負數的二補數是在其一補數的基礎上加 $1$ 。
|
||||
|
||||
圖 3-4 展示了原碼、一補數和二補數之間的轉換方法。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-4 原碼、一補數與二補數之間的相互轉換 </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 浮點數編碼
|
||||
|
||||
細心的你可能會發現:`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}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-5 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 所示,指數位 $\mathrm{E} = 0$ 和 $\mathrm{E} = 255$ 具有特殊含義,**用於表示零、無窮大、$\mathrm{NaN}$ 等**。
|
||||
|
||||
<p align="center"> 表 3-2 指數位含義 </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,70 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 資料結構可以從邏輯結構和物理結構兩個角度進行分類。邏輯結構描述了資料元素之間的邏輯關係,而物理結構描述了資料在計算機記憶體中的儲存方式。
|
||||
- 常見的邏輯結構包括線性、樹狀和網狀等。通常我們根據邏輯結構將資料結構分為線性(陣列、鏈結串列、堆疊、佇列)和非線性(樹、圖、堆積)兩種。雜湊表的實現可能同時包含線性資料結構和非線性資料結構。
|
||||
- 當程式執行時,資料被儲存在計算機記憶體中。每個記憶體空間都擁有對應的記憶體位址,程式透過這些記憶體位址訪問資料。
|
||||
- 物理結構主要分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。所有資料結構都是由陣列、鏈結串列或兩者的組合實現的。
|
||||
- 計算機中的基本資料型別包括整數 `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. Q & A
|
||||
|
||||
**Q**:為什麼雜湊表同時包含線性資料結構和非線性資料結構?
|
||||
|
||||
雜湊表底層是陣列,而為了解決雜湊衝突,我們可能會使用“鏈式位址”(後續“雜湊衝突”章節會講):陣列中每個桶指向一個鏈結串列,當鏈結串列長度超過一定閾值時,又可能被轉化為樹(通常為紅黑樹)。
|
||||
|
||||
從儲存的角度來看,雜湊表的底層是陣列,其中每一個桶槽位可能包含一個值,也可能包含一個鏈結串列或一棵樹。因此,雜湊表可能同時包含線性資料結構(陣列、鏈結串列)和非線性資料結構(樹)。
|
||||
|
||||
**Q**:`char` 型別的長度是 1 位元組嗎?
|
||||
|
||||
`char` 型別的長度由程式語言採用的編碼方法決定。例如,Java、JavaScript、TypeScript、C# 都採用 UTF-16 編碼(儲存 Unicode 碼點),因此 `char` 型別的長度為 2 位元組。
|
||||
|
||||
**Q**:基於陣列實現的資料結構也稱“靜態資料結構” 是否有歧義?堆疊也可以進行出堆疊和入堆疊等操作,這些操作都是“動態”的。
|
||||
|
||||
堆疊確實可以實現動態的資料操作,但資料結構仍然是“靜態”(長度不可變)的。儘管基於陣列的資料結構可以動態地新增或刪除元素,但它們的容量是固定的。如果資料量超出了預分配的大小,就需要建立一個新的更大的陣列,並將舊陣列的內容複製到新陣列中。
|
||||
|
||||
**Q**:在構建堆疊(佇列)的時候,未指定它的大小,為什麼它們是“靜態資料結構”呢?
|
||||
|
||||
在高階程式語言中,我們無須人工指定堆疊(佇列)的初始容量,這個工作由類別內部自動完成。例如,Java 的 `ArrayList` 的初始容量通常為 10。另外,擴容操作也是自動實現的。詳見後續的“串列”章節。
|
||||
|
||||
**Q**:原碼轉二補數的方法是“先取反後加 1”,那麼二補數轉原碼應該是逆運算“先減 1 後取反”,而二補數轉原碼也一樣可以透過“先取反後加 1”得到,這是為什麼呢?
|
||||
|
||||
這是因為原碼和二補數的相互轉換實際上是計算“補數”的過程。我們先給出補數的定義:假設 $a + b = c$ ,那麼我們稱 $a$ 是 $b$ 到 $c$ 的補數,反之也稱 $b$ 是 $a$ 到 $c$ 的補數。
|
||||
|
||||
給定一個 $n = 4$ 位長度的二進位制數 $0010$ ,如果將這個數字看作原碼(不考慮符號位),那麼它的二補數需透過“先取反後加 1”得到:
|
||||
|
||||
$$
|
||||
0010 \rightarrow 1101 \rightarrow 1110
|
||||
$$
|
||||
|
||||
我們會發現,原碼和二補數的和是 $0010 + 1110 = 10000$ ,也就是說,二補數 $1110$ 是原碼 $0010$ 到 $10000$ 的“補數”。**這意味著上述“先取反後加 1”實際上是計算到 $10000$ 的補數的過程**。
|
||||
|
||||
那麼,二補數 $1110$ 到 $10000$ 的“補數”是多少呢?我們依然可以用“先取反後加 1”得到它:
|
||||
|
||||
$$
|
||||
1110 \rightarrow 0001 \rightarrow 0010
|
||||
$$
|
||||
|
||||
換句話說,原碼和二補數互為對方到 $10000$ 的“補數”,因此“原碼轉二補數”和“二補數轉原碼”可以用相同的操作(先取反後加 1 )實現。
|
||||
|
||||
當然,我們也可以用逆運算來求二補數 $1110$ 的原碼,即“先減 1 後取反”:
|
||||
|
||||
$$
|
||||
1110 \rightarrow 1101 \rightarrow 0010
|
||||
$$
|
||||
|
||||
總結來看,“先取反後加 1”和“先減 1 後取反”這兩種運算都是在計算到 $10000$ 的補數,它們是等價的。
|
||||
|
||||
本質上看,“取反”操作實際上是求到 $1111$ 的補數(因為恆有 `原碼 + 一補數 = 1111`);而在一補數基礎上再加 1 得到的二補數,就是到 $10000$ 的補數。
|
||||
|
||||
上述以 $n = 4$ 為例,其可被推廣至任意位數的二進位制數。
|
||||
@@ -0,0 +1,456 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.2 分治搜尋策略
|
||||
|
||||
我們已經學過,搜尋演算法分為兩大類。
|
||||
|
||||
- **暴力搜尋**:它透過走訪資料結構實現,時間複雜度為 $O(n)$ 。
|
||||
- **自適應搜尋**:它利用特有的資料組織形式或先驗資訊,時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$ 。
|
||||
|
||||
實際上,**時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的**,例如二分搜尋和樹。
|
||||
|
||||
- 二分搜尋的每一步都將問題(在陣列中搜索目標元素)分解為一個小問題(在陣列的一半中搜索目標元素),這個過程一直持續到陣列為空或找到目標元素為止。
|
||||
- 樹是分治思想的代表,在二元搜尋樹、AVL 樹、堆積等資料結構中,各種操作的時間複雜度皆為 $O(\log n)$ 。
|
||||
|
||||
二分搜尋的分治策略如下所示。
|
||||
|
||||
- **問題可以分解**:二分搜尋遞迴地將原問題(在陣列中進行查詢)分解為子問題(在陣列的一半中進行查詢),這是透過比較中間元素和目標元素來實現的。
|
||||
- **子問題是獨立的**:在二分搜尋中,每輪只處理一個子問題,它不受其他子問題的影響。
|
||||
- **子問題的解無須合併**:二分搜尋旨在查詢一個特定元素,因此不需要將子問題的解進行合併。當子問題得到解決時,原問題也會同時得到解決。
|
||||
|
||||
分治能夠提升搜尋效率,本質上是因為暴力搜尋每輪只能排除一個選項,**而分治搜尋每輪可以排除一半選項**。
|
||||
|
||||
### 1. 基於分治實現二分搜尋
|
||||
|
||||
在之前的章節中,二分搜尋是基於遞推(迭代)實現的。現在我們基於分治(遞迴)來實現它。
|
||||
|
||||
!!! 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$ 的分治過程。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-4 二分搜尋的分治過程 </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 - i) / 2;
|
||||
if nums[m as usize] < target {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if nums[m as usize] > target {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
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"
|
||||
### 二分搜尋:問題 f(i, j) ###
|
||||
def dfs(nums, target, i, j)
|
||||
# 若區間為空,代表無目標元素,則返回 -1
|
||||
return -1 if i > j
|
||||
|
||||
# 計算中點索引 m
|
||||
m = (i + j) / 2
|
||||
|
||||
if nums[m] < target
|
||||
# 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j)
|
||||
elsif nums[m] > target
|
||||
# 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1)
|
||||
else
|
||||
# 找到目標元素,返回其索引
|
||||
return m
|
||||
end
|
||||
end
|
||||
|
||||
### 二分搜尋 ###
|
||||
def binary_search(nums, target)
|
||||
n = nums.length
|
||||
# 求解問題 f(0, n-1)
|
||||
dfs(nums, target, 0, n - 1)
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%9A%E5%95%8F%E9%A1%8C%20f%28i%2C%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E7%84%A1%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%EF%BC%8C%E5%89%87%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%81%9E%E8%BF%B4%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28m%2B1%2C%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20m%20%2B%201%2C%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%81%9E%E8%BF%B4%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28i%2C%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20i%2C%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A8%99%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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%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%E5%95%8F%E9%A1%8C%20f%280%2C%20n-1%29%0A%20%20%20%20return%20dfs%28nums%2C%20target%2C%200%2C%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%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22%2C%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%9A%E5%95%8F%E9%A1%8C%20f%28i%2C%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E7%84%A1%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%EF%BC%8C%E5%89%87%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%81%9E%E8%BF%B4%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28m%2B1%2C%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20m%20%2B%201%2C%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%81%9E%E8%BF%B4%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28i%2C%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums%2C%20target%2C%20i%2C%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A8%99%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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%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%E5%95%8F%E9%A1%8C%20f%280%2C%20n-1%29%0A%20%20%20%20return%20dfs%28nums%2C%20target%2C%200%2C%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%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
@@ -0,0 +1,559 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.3 構建二元樹問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定一棵二元樹的前序走訪 `preorder` 和中序走訪 `inorder` ,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如圖 12-5 所示)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-5 構建二元樹的示例資料 </p>
|
||||
|
||||
### 1. 判斷是否為分治問題
|
||||
|
||||
原問題定義為從 `preorder` 和 `inorder` 構建二元樹,是一個典型的分治問題。
|
||||
|
||||
- **問題可以分解**:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
|
||||
- **子問題是獨立的**:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
|
||||
- **子問題的解可以合併**:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
|
||||
|
||||
### 2. 如何劃分子樹
|
||||
|
||||
根據以上分析,這道題可以使用分治來求解,**但如何透過前序走訪 `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 ]` 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-6 在前序走訪和中序走訪中劃分子樹 </p>
|
||||
|
||||
### 3. 基於變數描述子樹區間
|
||||
|
||||
根據以上劃分方法,**我們已經得到根節點、左子樹、右子樹在 `preorder` 和 `inorder` 中的索引區間**。而為了描述這些索引區間,我們需要藉助幾個指標變數。
|
||||
|
||||
- 將當前樹的根節點在 `preorder` 中的索引記為 $i$ 。
|
||||
- 將當前樹的根節點在 `inorder` 中的索引記為 $m$ 。
|
||||
- 將當前樹在 `inorder` 中的索引區間記為 $[l, r]$ 。
|
||||
|
||||
如表 12-1 所示,透過以上變數即可表示根節點在 `preorder` 中的索引,以及子樹在 `inorder` 中的索引區間。
|
||||
|
||||
<p align="center"> 表 12-1 根節點和子樹在前序走訪和中序走訪中的索引 </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 理解。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-7 根節點和左右子樹的索引區間表示 </p>
|
||||
|
||||
### 4. 程式碼實現
|
||||
|
||||
為了提升查詢 $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 = HashMap<Int?, Int?>()
|
||||
for (i in inorder.indices) {
|
||||
inorderMap[inorder[i]] = i
|
||||
}
|
||||
val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="build_tree.rb"
|
||||
### 構建二元樹:分治 ###
|
||||
def dfs(preorder, inorder_map, i, l, r)
|
||||
# 子樹區間為空時終止
|
||||
return if r - l < 0
|
||||
|
||||
# 初始化根節點
|
||||
root = TreeNode.new(preorder[i])
|
||||
# 查詢 m ,從而劃分左右子樹
|
||||
m = inorder_map[preorder[i]]
|
||||
# 子問題:構建左子樹
|
||||
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
|
||||
# 子問題:構建右子樹
|
||||
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
|
||||
|
||||
# 返回根節點
|
||||
root
|
||||
end
|
||||
|
||||
### 構建二元樹 ###
|
||||
def build_tree(preorder, inorder)
|
||||
# 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
inorder_map = {}
|
||||
inorder.each_with_index { |val, i| inorder_map[val] = i }
|
||||
dfs(preorder, inorder_map, 0, 0, inorder.length - 1)
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%85%83%E6%A8%B9%E7%AF%80%E9%BB%9E%E9%A1%9E%E5%88%A5%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E7%AF%80%E9%BB%9E%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%E7%AF%80%E9%BB%9E%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%E7%AF%80%E9%BB%9E%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D%2C%0A%20%20%20%20inorder_map%3A%20dict%5Bint%2C%20int%5D%2C%0A%20%20%20%20i%3A%20int%2C%0A%20%20%20%20l%3A%20int%2C%0A%20%20%20%20r%3A%20int%2C%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%A7%8B%E5%BB%BA%E4%BA%8C%E5%85%83%E6%A8%B9%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A8%B9%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%E6%99%82%E7%B5%82%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%E7%AF%80%E9%BB%9E%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%A9%A2%20m%20%EF%BC%8C%E5%BE%9E%E8%80%8C%E5%8A%83%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A8%B9%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%EF%BC%9A%E6%A7%8B%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A8%B9%0A%20%20%20%20root.left%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%2C%20l%2C%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%EF%BC%9A%E6%A7%8B%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A8%B9%0A%20%20%20%20root.right%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%20%2B%20m%20-%20l%2C%20m%20%2B%201%2C%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E7%AF%80%E9%BB%9E%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D%2C%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%A7%8B%E5%BB%BA%E4%BA%8C%E5%85%83%E6%A8%B9%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%9C%E6%B9%8A%E8%A1%A8%EF%BC%8C%E5%84%B2%E5%AD%98%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E5%B0%8D%E6%98%A0%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i%2C%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder%2C%20inorder_map%2C%200%2C%200%2C%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%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%2C%209%2C%202%2C%201%2C%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9%2C%203%2C%201%2C%202%2C%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E8%B5%B0%E8%A8%AA%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E8%B5%B0%E8%A8%AA%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder%2C%20inorder%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%85%83%E6%A8%B9%E7%AF%80%E9%BB%9E%E9%A1%9E%E5%88%A5%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E7%AF%80%E9%BB%9E%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%E7%AF%80%E9%BB%9E%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%E7%AF%80%E9%BB%9E%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D%2C%0A%20%20%20%20inorder_map%3A%20dict%5Bint%2C%20int%5D%2C%0A%20%20%20%20i%3A%20int%2C%0A%20%20%20%20l%3A%20int%2C%0A%20%20%20%20r%3A%20int%2C%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%A7%8B%E5%BB%BA%E4%BA%8C%E5%85%83%E6%A8%B9%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A8%B9%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%E6%99%82%E7%B5%82%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%E7%AF%80%E9%BB%9E%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%A9%A2%20m%20%EF%BC%8C%E5%BE%9E%E8%80%8C%E5%8A%83%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A8%B9%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%EF%BC%9A%E6%A7%8B%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A8%B9%0A%20%20%20%20root.left%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%2C%20l%2C%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%EF%BC%9A%E6%A7%8B%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A8%B9%0A%20%20%20%20root.right%20%3D%20dfs%28preorder%2C%20inorder_map%2C%20i%20%2B%201%20%2B%20m%20-%20l%2C%20m%20%2B%201%2C%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E7%AF%80%E9%BB%9E%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D%2C%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%A7%8B%E5%BB%BA%E4%BA%8C%E5%85%83%E6%A8%B9%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%9C%E6%B9%8A%E8%A1%A8%EF%BC%8C%E5%84%B2%E5%AD%98%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E5%B0%8D%E6%98%A0%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i%2C%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder%2C%20inorder_map%2C%200%2C%200%2C%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%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%2C%209%2C%202%2C%201%2C%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9%2C%203%2C%201%2C%202%2C%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E8%B5%B0%E8%A8%AA%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E8%B5%B0%E8%A8%AA%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder%2C%20inorder%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
圖 12-8 展示了構建二元樹的遞迴過程,各個節點是在向下“遞”的過程中建立的,而各條邊(引用)是在向上“迴”的過程中建立的。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-8 構建二元樹的遞迴過程 </p>
|
||||
|
||||
每個遞迴函式內的前序走訪 `preorder` 和中序走訪 `inorder` 的劃分結果如圖 12-9 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-9 每個遞迴函式中的劃分結果 </p>
|
||||
|
||||
設樹的節點數量為 $n$ ,初始化每一個節點(執行一個遞迴函式 `dfs()` )使用 $O(1)$ 時間。**因此總體時間複雜度為 $O(n)$** 。
|
||||
|
||||
雜湊表儲存 `inorder` 元素到索引的對映,空間複雜度為 $O(n)$ 。在最差情況下,即二元樹退化為鏈結串列時,遞迴深度達到 $n$ ,使用 $O(n)$ 的堆疊幀空間。**因此總體空間複雜度為 $O(n)$** 。
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.1 分治演算法
|
||||
|
||||
<u>分治(divide and conquer)</u>,全稱分而治之,是一種非常重要且常見的演算法策略。分治通常基於遞迴實現,包括“分”和“治”兩個步驟。
|
||||
|
||||
1. **分(劃分階段)**:遞迴地將原問題分解為兩個或多個子問題,直至到達最小子問題時終止。
|
||||
2. **治(合併階段)**:從已知解的最小子問題開始,從底至頂地將子問題的解進行合併,從而構建出原問題的解。
|
||||
|
||||
如圖 12-1 所示,“合併排序”是分治策略的典型應用之一。
|
||||
|
||||
1. **分**:遞迴地將原陣列(原問題)劃分為兩個子陣列(子問題),直到子陣列只剩一個元素(最小子問題)。
|
||||
2. **治**:從底至頂地將有序的子陣列(子問題的解)進行合併,從而得到有序的原陣列(原問題的解)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-1 合併排序的分治策略 </p>
|
||||
|
||||
## 12.1.1 如何判斷分治問題
|
||||
|
||||
一個問題是否適合使用分治解決,通常可以參考以下幾個判斷依據。
|
||||
|
||||
1. **問題可以分解**:原問題可以分解成規模更小、類似的子問題,以及能夠以相同方式遞迴地進行劃分。
|
||||
2. **子問題是獨立的**:子問題之間沒有重疊,互不依賴,可以獨立解決。
|
||||
3. **子問題的解可以合併**:原問題的解透過合併子問題的解得來。
|
||||
|
||||
顯然,合併排序滿足以上三個判斷依據。
|
||||
|
||||
1. **問題可以分解**:遞迴地將陣列(原問題)劃分為兩個子陣列(子問題)。
|
||||
2. **子問題是獨立的**:每個子陣列都可以獨立地進行排序(子問題可以獨立進行求解)。
|
||||
3. **子問題的解可以合併**:兩個有序子陣列(子問題的解)可以合併為一個有序陣列(原問題的解)。
|
||||
|
||||
## 12.1.2 透過分治提升效率
|
||||
|
||||
**分治不僅可以有效地解決演算法問題,往往還可以提升演算法效率**。在排序演算法中,快速排序、合併排序、堆積排序相較於選擇、冒泡、插入排序更快,就是因為它們應用了分治策略。
|
||||
|
||||
那麼,我們不禁發問:**為什麼分治可以提升演算法效率,其底層邏輯是什麼**?換句話說,將大問題分解為多個子問題、解決子問題、將子問題的解合併為原問題的解,這幾步的效率為什麼比直接解決原問題的效率更高?這個問題可以從操作數量和平行計算兩方面來討論。
|
||||
|
||||
### 1. 操作數量最佳化
|
||||
|
||||
以“泡沫排序”為例,其處理一個長度為 $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)
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-2 劃分陣列前後的泡沫排序 </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. 平行計算最佳化
|
||||
|
||||
我們知道,分治生成的子問題是相互獨立的,**因此通常可以並行解決**。也就是說,分治不僅可以降低演算法的時間複雜度,**還有利於作業系統的並行最佳化**。
|
||||
|
||||
並行最佳化在多核或多處理器的環境中尤其有效,因為系統可以同時處理多個子問題,更加充分地利用計算資源,從而顯著減少總體的執行時間。
|
||||
|
||||
比如在圖 12-3 所示的“桶排序”中,我們將海量的資料平均分配到各個桶中,則可將所有桶的排序任務分散到各個計算單元,完成後再合併結果。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-3 桶排序的平行計算 </p>
|
||||
|
||||
## 12.1.3 分治常見應用
|
||||
|
||||
一方面,分治可以用來解決許多經典演算法問題。
|
||||
|
||||
- **尋找最近點對**:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。
|
||||
- **大整數乘法**:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。
|
||||
- **矩陣乘法**:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。
|
||||
- **河內塔問題**:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。
|
||||
- **求解逆序對**:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。
|
||||
|
||||
另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。
|
||||
|
||||
- **二分搜尋**:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。
|
||||
- **合併排序**:本節開頭已介紹,不再贅述。
|
||||
- **快速排序**:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。
|
||||
- **桶排序**:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。
|
||||
- **樹**:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。
|
||||
- **堆積**:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。
|
||||
- **雜湊表**:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。
|
||||
|
||||
可以看出,**分治是一種“潤物細無聲”的演算法思想**,隱含在各種演算法與資料結構之中。
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 12.6 練習
|
||||
|
||||
## 12.6.1 知識鞏固
|
||||
|
||||
### 1. 哪些任務適合分治
|
||||
|
||||
一位同學想用“先分成兩半,分別解決,再合併結果”的方法完成以下任務。
|
||||
請分別判斷它們是“適合分治”“可以分治,但不會減少總工作量”還是“兩半不能獨立解決”,並說明理由。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 將一個無序陣列排序;
|
||||
2. 求一個陣列中的最大值;
|
||||
3. 按順序執行一串 `push(x)`、`pop()` 堆疊操作,並輸出每次 `pop()` 得到的元素。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 適合:對半分解、兩半獨立排序、$O(n)$ 合併——這正是合併排序。
|
||||
2. 可以分治,但不會減少總工作量:左右兩半仍需合計檢查全部 $n$ 個元素,
|
||||
和直接掃描一樣都是 $O(n)$ 時間。
|
||||
3. 兩半不能獨立解決:後半段開始時的堆疊內容取決於前半段執行結果,
|
||||
兩半不能在互不知道結果時獨立完成。
|
||||
|
||||
### 2. 快速冪是怎樣減少計算的
|
||||
|
||||
下面的遞迴函式用分治計算 $x^n$:
|
||||
|
||||
```python
|
||||
def fast_pow(x, n):
|
||||
if n == 0:
|
||||
return 1
|
||||
half = fast_pow(x, n // 2)
|
||||
if n % 2 == 0:
|
||||
return half * half
|
||||
return half * half * x
|
||||
```
|
||||
|
||||
用它計算 `fast_pow(3, 5)`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 遞迴呼叫時,參數 `n` 依次變成哪些值?
|
||||
2. 從最深層開始返回時,各層依次返回什麼值?
|
||||
3. 為什麼要先儲存 `half`,而不是把 `fast_pow(x, n // 2)` 寫兩遍?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 參數依次為 `5 → 2 → 1 → 0`,每次都把指數減半,直到到達終止條件。
|
||||
|
||||
2. `n = 0` 時返回 1;`n = 1` 時返回 $1×1×3=3$;
|
||||
`n = 2` 時返回 $3×3=9$;`n = 5` 時返回 $9×9×3=243$。
|
||||
|
||||
3. 如果把 `fast_pow(x, n // 2)` 在乘法兩邊各寫一次,兩次遞迴會計算完全相同的子問題。
|
||||
先把結果儲存為 `half`,每層就只遞迴一次,遞迴深度約為 $\log n$;
|
||||
呼叫兩次會造成大量重複計算。
|
||||
|
||||
### 3. 從走訪序列拆分左右子樹
|
||||
|
||||
一棵沒有重複節點的二元樹,其前序走訪和中序走訪分別為:
|
||||
|
||||
- 前序走訪:`[A, B, D, E, C]`
|
||||
- 中序走訪:`[D, B, E, A, C]`
|
||||
|
||||
只完成根節點這一層的拆分,不必繼續遞迴,也不必畫出整棵樹:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 根節點是什麼?
|
||||
2. 左、右子樹在中序走訪中分別對應哪一段?
|
||||
3. 左、右子樹在前序走訪中分別對應哪一段?根節點有哪些直接孩子?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 前序走訪的第一個節點是根節點,因此根節點為 `A`。
|
||||
|
||||
2. `A` 把中序走訪分成兩部分:左子樹為 `[D, B, E]`,右子樹為 `[C]`。
|
||||
|
||||
3. 左子樹有 3 個節點,因此根節點 `A` 後面的 3 個前序元素屬於左子樹,
|
||||
即 `[B, D, E]`;剩餘的 `[C]` 屬於右子樹。
|
||||
所以根節點的左孩子是 `B`,右孩子是 `C`。
|
||||
|
||||
## 12.6.2 程式設計練習
|
||||
|
||||
### 1. 快速冪
|
||||
|
||||
給定實數 `x` 和整數 `n`,請計算 $x^n$,且不能呼叫語言內建的冪函式。
|
||||
要求使用遞迴分治:每次把指數縮小一半,並複用已經算出的子問題結果。
|
||||
本題規定 $x^0=1$,包括 `x = 0` 時;當 `n < 0` 時保證 `x != 0`,答案可轉化為 $(1/x)^{-n}$。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. n 等於 0 時答案是 1
|
||||
2. 算出 x 的 n // 2 次方後儲存為 half,不要遞迴呼叫第二次
|
||||
3. 當 n < 0 時,先把 x 改為 1 / x,再把 n 改為 -n;使用 C++ 或 Java 時,可先把 n 轉為 64 位整數,避免最小的 32 位整數取相反數時溢位
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/powx-n/solutions/241471/50-powx-n-kuai-su-mi-qing-xi-tu-jie-by-jyd/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **說明:** 連結中的題解使用迭代位運算,與本練習指定的遞迴分治方法不同;請根據本題的解題提示完成遞迴版本
|
||||
@@ -0,0 +1,560 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.4 河內塔問題
|
||||
|
||||
在合併排序和構建二元樹中,我們都是將原問題分解為兩個規模為原問題一半的子問題。然而對於河內塔問題,我們採用不同的分解策略。
|
||||
|
||||
!!! question
|
||||
|
||||
給定三根柱子,記為 `A`、`B` 和 `C` 。起始狀態下,柱子 `A` 上套著 $n$ 個圓盤,它們從上到下按照從小到大的順序排列。我們的任務是要把這 $n$ 個圓盤移到柱子 `C` 上,並保持它們的原有順序不變(如圖 12-10 所示)。在移動圓盤的過程中,需要遵守以下規則。
|
||||
|
||||
1. 圓盤只能從一根柱子頂部拿出,從另一根柱子頂部放入。
|
||||
2. 每次只能移動一個圓盤。
|
||||
3. 小圓盤必須時刻位於大圓盤之上。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-10 河內塔問題示例 </p>
|
||||
|
||||
**我們將規模為 $i$ 的河內塔問題記作 $f(i)$** 。例如 $f(3)$ 代表將 $3$ 個圓盤從 `A` 移動至 `C` 的河內塔問題。
|
||||
|
||||
### 1. 考慮基本情況
|
||||
|
||||
如圖 12-11 所示,對於問題 $f(1)$ ,即當只有一個圓盤時,我們將它直接從 `A` 移動至 `C` 即可。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-11 規模為 1 的問題的解 </p>
|
||||
|
||||
如圖 12-12 所示,對於問題 $f(2)$ ,即當有兩個圓盤時,**由於要時刻滿足小圓盤在大圓盤之上,因此需要藉助 `B` 來完成移動**。
|
||||
|
||||
1. 先將上面的小圓盤從 `A` 移至 `B` 。
|
||||
2. 再將大圓盤從 `A` 移至 `C` 。
|
||||
3. 最後將小圓盤從 `B` 移至 `C` 。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-12 規模為 2 的問題的解 </p>
|
||||
|
||||
解決問題 $f(2)$ 的過程可總結為:**將兩個圓盤藉助 `B` 從 `A` 移至 `C`** 。其中,`C` 稱為目標柱、`B` 稱為緩衝柱。
|
||||
|
||||
### 2. 子問題分解
|
||||
|
||||
對於問題 $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>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-13 規模為 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)$ 的解是已知的,只需一次移動操作即可。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-14 解決河內塔問題的分治策略 </p>
|
||||
|
||||
### 3. 程式碼實現
|
||||
|
||||
在程式碼中,我們宣告一個遞迴函式 `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.pop().unwrap();
|
||||
// 將圓盤放入 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 = 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"
|
||||
### 移動一個圓盤 ###
|
||||
def move(src, tar)
|
||||
# 從 src 頂部拿出一個圓盤
|
||||
pan = src.pop
|
||||
# 將圓盤放入 tar 頂部
|
||||
tar << pan
|
||||
end
|
||||
|
||||
### 求解河內塔問題 f(i) ###
|
||||
def dfs(i, src, buf, tar)
|
||||
# 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if i == 1
|
||||
move(src, tar)
|
||||
return
|
||||
end
|
||||
|
||||
# 子問題 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)
|
||||
end
|
||||
|
||||
### 求解河內塔問題 ###
|
||||
def solve_hanota(_A, _B, _C)
|
||||
n = _A.length
|
||||
# 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, _A, _B, _C)
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8B%95%E4%B8%80%E5%80%8B%E5%9C%93%E7%9B%A4%22%22%22%0A%20%20%20%20%23%20%E5%BE%9E%20src%20%E9%A0%82%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E5%80%8B%E5%9C%93%E7%9B%A4%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%87%E5%9C%93%E7%9B%A4%E6%94%BE%E5%85%A5%20tar%20%E9%A0%82%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int%2C%20src%3A%20list%5Bint%5D%2C%20buf%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B2%B3%E5%85%A7%E5%A1%94%E5%95%8F%E9%A1%8C%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%E5%80%8B%E5%9C%93%E7%9B%A4%EF%BC%8C%E5%89%87%E7%9B%B4%E6%8E%A5%E5%B0%87%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%2C%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28i-1%29%20%EF%BC%9A%E5%B0%87%20src%20%E9%A0%82%E9%83%A8%20i-1%20%E5%80%8B%E5%9C%93%E7%9B%A4%E8%97%89%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201%2C%20src%2C%20tar%2C%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%20f%281%29%20%EF%BC%9A%E5%B0%87%20src%20%E5%89%A9%E9%A4%98%E4%B8%80%E5%80%8B%E5%9C%93%E7%9B%A4%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28i-1%29%20%EF%BC%9A%E5%B0%87%20buf%20%E9%A0%82%E9%83%A8%20i-1%20%E5%80%8B%E5%9C%93%E7%9B%A4%E8%97%89%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201%2C%20buf%2C%20src%2C%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D%2C%20B%3A%20list%5Bint%5D%2C%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B2%B3%E5%85%A7%E5%A1%94%E5%95%8F%E9%A1%8C%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%87%20A%20%E9%A0%82%E9%83%A8%20n%20%E5%80%8B%E5%9C%93%E7%9B%A4%E8%97%89%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n%2C%20A%2C%20B%2C%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%E4%B8%B2%E5%88%97%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A0%82%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5%2C%204%2C%203%2C%202%2C%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8B%80%E6%85%8B%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%2C%20B%2C%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%93%E7%9B%A4%E7%A7%BB%E5%8B%95%E5%AE%8C%E6%88%90%E5%BE%8C%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%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8B%95%E4%B8%80%E5%80%8B%E5%9C%93%E7%9B%A4%22%22%22%0A%20%20%20%20%23%20%E5%BE%9E%20src%20%E9%A0%82%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E5%80%8B%E5%9C%93%E7%9B%A4%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%87%E5%9C%93%E7%9B%A4%E6%94%BE%E5%85%A5%20tar%20%E9%A0%82%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int%2C%20src%3A%20list%5Bint%5D%2C%20buf%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B2%B3%E5%85%A7%E5%A1%94%E5%95%8F%E9%A1%8C%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%E5%80%8B%E5%9C%93%E7%9B%A4%EF%BC%8C%E5%89%87%E7%9B%B4%E6%8E%A5%E5%B0%87%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%2C%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28i-1%29%20%EF%BC%9A%E5%B0%87%20src%20%E9%A0%82%E9%83%A8%20i-1%20%E5%80%8B%E5%9C%93%E7%9B%A4%E8%97%89%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201%2C%20src%2C%20tar%2C%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%20f%281%29%20%EF%BC%9A%E5%B0%87%20src%20%E5%89%A9%E9%A4%98%E4%B8%80%E5%80%8B%E5%9C%93%E7%9B%A4%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E5%95%8F%E9%A1%8C%20f%28i-1%29%20%EF%BC%9A%E5%B0%87%20buf%20%E9%A0%82%E9%83%A8%20i-1%20%E5%80%8B%E5%9C%93%E7%9B%A4%E8%97%89%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201%2C%20buf%2C%20src%2C%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D%2C%20B%3A%20list%5Bint%5D%2C%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B2%B3%E5%85%A7%E5%A1%94%E5%95%8F%E9%A1%8C%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%87%20A%20%E9%A0%82%E9%83%A8%20n%20%E5%80%8B%E5%9C%93%E7%9B%A4%E8%97%89%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n%2C%20A%2C%20B%2C%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%E4%B8%B2%E5%88%97%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A0%82%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5%2C%204%2C%203%2C%202%2C%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8B%80%E6%85%8B%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%2C%20B%2C%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%93%E7%9B%A4%E7%A7%BB%E5%8B%95%E5%AE%8C%E6%88%90%E5%BE%8C%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)$** 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-15 河內塔問題的遞迴樹 </p>
|
||||
|
||||
!!! quote
|
||||
|
||||
河內塔問題源自一個古老的傳說。在古印度的一個寺廟裡,僧侶們有三根高大的鑽石柱子,以及 $64$ 個大小不一的金圓盤。僧侶們不斷地移動圓盤,他們相信在最後一個圓盤被正確放置的那一刻,這個世界就會結束。
|
||||
|
||||
然而,即使僧侶們每秒鐘移動一次,總共需要大約 $2^{64} \approx 1.84×10^{19}$ 秒,合約 $5850$ 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/set-split
|
||||
---
|
||||
|
||||
# 第 12 章 分治
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
難題被逐層拆解,每一次的拆解都使它變得更為簡單。
|
||||
|
||||
分而治之揭示了一個重要的事實:從簡單做起,一切都不再複雜。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [12.1 分治演算法](divide_and_conquer.md)
|
||||
- [12.2 分治搜尋策略](binary_search_recur.md)
|
||||
- [12.3 構建樹問題](build_binary_tree_problem.md)
|
||||
- [12.4 河內塔問題](hanota_problem.md)
|
||||
- [12.5 小結](summary.md)
|
||||
- [12.6 練習](exercises.md)
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 分治是一種常見的演算法設計策略,包括分(劃分)和治(合併)兩個階段,通常基於遞迴實現。
|
||||
- 判斷是否是分治演算法問題的依據包括:問題能否分解、子問題是否獨立、子問題能否合併。
|
||||
- 合併排序是分治策略的典型應用,其遞迴地將陣列劃分為等長的兩個子陣列,直到只剩一個元素時開始逐層合併,從而完成排序。
|
||||
- 引入分治策略往往可以提升演算法效率。一方面,分治策略減少了操作數量;另一方面,分治後有利於系統的並行最佳化。
|
||||
- 分治既可以解決許多演算法問題,也廣泛應用於資料結構與演算法設計中,處處可見其身影。
|
||||
- 相較於暴力搜尋,自適應搜尋效率更高。時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的。
|
||||
- 二分搜尋是分治策略的另一個典型應用,它不包含將子問題的解進行合併的步驟。我們可以透過遞迴分治實現二分搜尋。
|
||||
- 在構建二元樹的問題中,構建樹(原問題)可以劃分為構建左子樹和右子樹(子問題),這可以透過劃分前序走訪和中序走訪的索引區間來實現。
|
||||
- 在河內塔問題中,一個規模為 $n$ 的問題可以劃分為兩個規模為 $n-1$ 的子問題和一個規模為 $1$ 的子問題。按順序解決這三個子問題後,原問題隨之得到解決。
|
||||
@@ -0,0 +1,958 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.2 動態規劃問題特性
|
||||
|
||||
在上一節中,我們學習了動態規劃是如何透過子問題分解來求解原問題的。實際上,子問題分解是一種通用的演算法思路,在分治、動態規劃、回溯中的側重點不同。
|
||||
|
||||
- 分治演算法遞迴地將原問題劃分為多個相互獨立的子問題,直至最小子問題,並在回溯中合併子問題的解,最終得到原問題的解。
|
||||
- 動態規劃也對問題進行遞迴分解,但與分治演算法的主要區別是,動態規劃中的子問題是相互依賴的,在分解過程中會出現許多重疊子問題。
|
||||
- 回溯演算法在嘗試和回退中窮舉所有可能的解,並透過剪枝避免不必要的搜尋分支。原問題的解由一系列決策步驟構成,我們可以將每個決策步驟之前的子序列看作一個子問題。
|
||||
|
||||
實際上,動態規劃常用來求解最最佳化問題,它們不僅包含重疊子問題,還具有另外兩大特性:最優子結構、無後效性。
|
||||
|
||||
## 14.2.1 最優子結構
|
||||
|
||||
我們對爬樓梯問題稍作改動,使之更加適合展示最優子結構概念。
|
||||
|
||||
!!! question "爬樓梯最小代價"
|
||||
|
||||
給定一個樓梯,你每步可以上 $1$ 階或者 $2$ 階,每一階樓梯上都貼有一個非負整數,表示你在該臺階所需要付出的代價。給定一個非負整數陣列 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 個臺階需要付出的代價,$cost[0]$ 為地面(起始點)。請計算最少需要付出多少代價才能到達頂部?
|
||||
|
||||
如圖 14-6 所示,若第 $1$、$2$、$3$ 階的代價分別為 $1$、$10$、$1$ ,則從地面爬到第 $3$ 階的最小代價為 $2$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-6 爬到第 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], dp[i - 2]) + cost[i]
|
||||
}
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="min_cost_climbing_stairs_dp.rb"
|
||||
### 爬樓梯最小代價:動態規劃 ###
|
||||
def min_cost_climbing_stairs_dp(cost)
|
||||
n = cost.length - 1
|
||||
return cost[n] if n == 1 || n == 2
|
||||
# 初始化 dp 表,用於儲存子問題的解
|
||||
dp = Array.new(n + 1, 0)
|
||||
# 初始狀態:預設最小子問題的解
|
||||
dp[1], dp[2] = cost[1], cost[2]
|
||||
# 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
(3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }
|
||||
dp[n]
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%A8%93%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E5%83%B9%EF%BC%9A%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%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%E6%96%BC%E5%84%B2%E5%AD%98%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B0%5D%20%2A%20%28n%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8B%80%E6%85%8B%EF%BC%9A%E9%A0%90%E8%A8%AD%E6%9C%80%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%2C%20dp%5B2%5D%20%3D%20cost%5B1%5D%2C%20cost%5B2%5D%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%BE%9E%E8%BC%83%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BC%83%E5%A4%A7%E5%AD%90%E5%95%8F%E9%A1%8C%0A%20%20%20%20for%20i%20in%20range%283%2C%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%2C%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%2C%201%2C%2010%2C%201%2C%201%2C%201%2C%2010%2C%201%2C%201%2C%2010%2C%201%5D%0A%20%20%20%20print%28f%22%E8%BC%B8%E5%85%A5%E6%A8%93%E6%A2%AF%E7%9A%84%E4%BB%A3%E5%83%B9%E4%B8%B2%E5%88%97%E7%82%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%A8%93%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E5%83%B9%E7%82%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%A8%93%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E5%83%B9%EF%BC%9A%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%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%E6%96%BC%E5%84%B2%E5%AD%98%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B0%5D%20%2A%20%28n%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8B%80%E6%85%8B%EF%BC%9A%E9%A0%90%E8%A8%AD%E6%9C%80%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%2C%20dp%5B2%5D%20%3D%20cost%5B1%5D%2C%20cost%5B2%5D%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%BE%9E%E8%BC%83%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BC%83%E5%A4%A7%E5%AD%90%E5%95%8F%E9%A1%8C%0A%20%20%20%20for%20i%20in%20range%283%2C%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%2C%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%2C%201%2C%2010%2C%201%2C%201%2C%201%2C%2010%2C%201%2C%201%2C%2010%2C%201%5D%0A%20%20%20%20print%28f%22%E8%BC%B8%E5%85%A5%E6%A8%93%E6%A2%AF%E7%9A%84%E4%BB%A3%E5%83%B9%E4%B8%B2%E5%88%97%E7%82%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%A8%93%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E5%83%B9%E7%82%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 展示了以上程式碼的動態規劃過程。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-7 爬樓梯最小代價的動態規劃過程 </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, tmp) + cost[i]
|
||||
a = tmp
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="min_cost_climbing_stairs_dp.rb"
|
||||
### 爬樓梯最小代價:動態規劃 ###
|
||||
def min_cost_climbing_stairs_dp(cost)
|
||||
n = cost.length - 1
|
||||
return cost[n] if n == 1 || n == 2
|
||||
# 初始化 dp 表,用於儲存子問題的解
|
||||
dp = Array.new(n + 1, 0)
|
||||
# 初始狀態:預設最小子問題的解
|
||||
dp[1], dp[2] = cost[1], cost[2]
|
||||
# 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
(3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }
|
||||
dp[n]
|
||||
end
|
||||
|
||||
# 爬樓梯最小代價:空間最佳化後的動態規劃
|
||||
def min_cost_climbing_stairs_dp_comp(cost)
|
||||
n = cost.length - 1
|
||||
return cost[n] if n == 1 || n == 2
|
||||
a, b = cost[1], cost[2]
|
||||
(3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }
|
||||
b
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%A8%93%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E5%83%B9%EF%BC%9A%E7%A9%BA%E9%96%93%E6%9C%80%E4%BD%B3%E5%8C%96%E5%BE%8C%E7%9A%84%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%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%2C%20b%20%3D%20cost%5B1%5D%2C%20cost%5B2%5D%0A%20%20%20%20for%20i%20in%20range%283%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20a%2C%20b%20%3D%20b%2C%20min%28a%2C%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%2C%201%2C%2010%2C%201%2C%201%2C%201%2C%2010%2C%201%2C%201%2C%2010%2C%201%5D%0A%20%20%20%20print%28f%22%E8%BC%B8%E5%85%A5%E6%A8%93%E6%A2%AF%E7%9A%84%E4%BB%A3%E5%83%B9%E4%B8%B2%E5%88%97%E7%82%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%A8%93%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E5%83%B9%E7%82%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%A8%93%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E5%83%B9%EF%BC%9A%E7%A9%BA%E9%96%93%E6%9C%80%E4%BD%B3%E5%8C%96%E5%BE%8C%E7%9A%84%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%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%2C%20b%20%3D%20cost%5B1%5D%2C%20cost%5B2%5D%0A%20%20%20%20for%20i%20in%20range%283%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20a%2C%20b%20%3D%20b%2C%20min%28a%2C%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%2C%201%2C%2010%2C%201%2C%201%2C%201%2C%2010%2C%201%2C%201%2C%2010%2C%201%5D%0A%20%20%20%20print%28f%22%E8%BC%B8%E5%85%A5%E6%A8%93%E6%A2%AF%E7%9A%84%E4%BB%A3%E5%83%B9%E4%B8%B2%E5%88%97%E7%82%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%A8%93%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E5%83%B9%E7%82%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 無後效性
|
||||
|
||||
無後效性是動態規劃能夠有效解決問題的重要特性之一,其定義為:**給定一個確定的狀態,它的未來發展只與當前狀態有關,而與過去經歷的所有狀態無關**。
|
||||
|
||||
以爬樓梯問題為例,給定狀態 $i$ ,它會發展出狀態 $i+1$ 和狀態 $i+2$ ,分別對應跳 $1$ 步和跳 $2$ 步。在做出這兩種選擇時,我們無須考慮狀態 $i$ 之前的狀態,它們對狀態 $i$ 的未來沒有影響。
|
||||
|
||||
然而,如果我們給爬樓梯問題新增一個約束,情況就不一樣了。
|
||||
|
||||
!!! question "帶約束爬樓梯"
|
||||
|
||||
給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階,**但不能連續兩輪跳 $1$ 階**,請問有多少種方案可以爬到樓頂?
|
||||
|
||||
如圖 14-8 所示,爬上第 $3$ 階僅剩 $2$ 種可行方案,其中連續三次跳 $1$ 階的方案不滿足約束條件,因此被捨棄。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-8 帶約束爬到第 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}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-9 考慮約束下的遞推關係 </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"
|
||||
### 帶約束爬樓梯:動態規劃 ###
|
||||
def climbing_stairs_constraint_dp(n)
|
||||
return 1 if n == 1 || n == 2
|
||||
|
||||
# 初始化 dp 表,用於儲存子問題的解
|
||||
dp = Array.new(n + 1) { Array.new(3, 0) }
|
||||
# 初始狀態:預設最小子問題的解
|
||||
dp[1][1], dp[1][2] = 1, 0
|
||||
dp[2][1], dp[2][2] = 0, 1
|
||||
# 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i in 3...(n + 1)
|
||||
dp[i][1] = dp[i - 1][2]
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
|
||||
end
|
||||
|
||||
dp[n][1] + dp[n][2]
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%B6%E7%B4%84%E6%9D%9F%E7%88%AC%E6%A8%93%E6%A2%AF%EF%BC%9A%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%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%E6%96%BC%E5%84%B2%E5%AD%98%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20%2A%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%8B%80%E6%85%8B%EF%BC%9A%E9%A0%90%E8%A8%AD%E6%9C%80%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%5B1%5D%2C%20dp%5B1%5D%5B2%5D%20%3D%201%2C%200%0A%20%20%20%20dp%5B2%5D%5B1%5D%2C%20dp%5B2%5D%5B2%5D%20%3D%200%2C%201%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%BE%9E%E8%BC%83%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BC%83%E5%A4%A7%E5%AD%90%E5%95%8F%E9%A1%8C%0A%20%20%20%20for%20i%20in%20range%283%2C%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%9A%8E%E6%A8%93%E6%A2%AF%E5%85%B1%E6%9C%89%20%7Bres%7D%20%E7%A8%AE%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%B6%E7%B4%84%E6%9D%9F%E7%88%AC%E6%A8%93%E6%A2%AF%EF%BC%9A%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%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%E6%96%BC%E5%84%B2%E5%AD%98%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20%2A%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%8B%80%E6%85%8B%EF%BC%9A%E9%A0%90%E8%A8%AD%E6%9C%80%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%5B1%5D%2C%20dp%5B1%5D%5B2%5D%20%3D%201%2C%200%0A%20%20%20%20dp%5B2%5D%5B1%5D%2C%20dp%5B2%5D%5B2%5D%20%3D%200%2C%201%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%BE%9E%E8%BC%83%E5%B0%8F%E5%AD%90%E5%95%8F%E9%A1%8C%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BC%83%E5%A4%A7%E5%AD%90%E5%95%8F%E9%A1%8C%0A%20%20%20%20for%20i%20in%20range%283%2C%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%9A%8E%E6%A8%93%E6%A2%AF%E5%85%B1%E6%9C%89%20%7Bres%7D%20%E7%A8%AE%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,972 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.6 編輯距離問題
|
||||
|
||||
編輯距離,也稱 Levenshtein 距離,指兩個字串之間互相轉換的最少修改次數,通常用於在資訊檢索和自然語言處理中度量兩個序列的相似度。
|
||||
|
||||
!!! question
|
||||
|
||||
輸入兩個字串 $s$ 和 $t$ ,返回將 $s$ 轉換為 $t$ 所需的最少編輯步數。
|
||||
|
||||
你可以在一個字串中進行三種編輯操作:插入一個字元、刪除一個字元、將字元替換為任意一個字元。
|
||||
|
||||
如圖 14-27 所示,將 `kitten` 轉換為 `sitting` 需要編輯 3 步,包括 2 次替換操作與 1 次新增操作;將 `hello` 轉換為 `algo` 需要 3 步,包括 2 次替換操作和 1 次刪除操作。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-27 編輯距離的示例資料 </p>
|
||||
|
||||
**編輯距離問題可以很自然地用決策樹模型來解釋**。字串對應樹節點,一輪決策(一次編輯操作)對應樹的一條邊。
|
||||
|
||||
如圖 14-28 所示,在不限制操作的情況下,每個節點都可以派生出許多條邊,每條邊對應一種操作,這意味著從 `hello` 轉換到 `algo` 有許多種可能的路徑。
|
||||
|
||||
從決策樹的角度看,本題的目標是求解節點 `hello` 和節點 `algo` 之間的最短路徑。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-28 基於決策樹模型表示編輯距離問題 </p>
|
||||
|
||||
### 1. 動態規劃思路
|
||||
|
||||
**第一步:思考每輪的決策,定義狀態,從而得到 $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]$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-29 編輯距離的狀態轉移 </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. 程式碼實現
|
||||
|
||||
=== "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], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="edit_distance.rb"
|
||||
### 編輯距離:動態規劃 ###
|
||||
def edit_distance_dp(s, t)
|
||||
n, m = s.length, t.length
|
||||
dp = Array.new(n + 1) { Array.new(m + 1, 0) }
|
||||
# 狀態轉移:首行首列
|
||||
(1...(n + 1)).each { |i| dp[i][0] = i }
|
||||
(1...(m + 1)).each { |j| dp[0][j] = j }
|
||||
# 狀態轉移:其餘行和列
|
||||
for i in 1...(n + 1)
|
||||
for j in 1...(m +1)
|
||||
if s[i - 1] == t[j - 1]
|
||||
# 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
else
|
||||
# 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
dp[n][m]
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%B7%A8%E8%BC%AF%E8%B7%9D%E9%9B%A2%EF%BC%9A%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%22%22%22%0A%20%20%20%20n%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20%2A%20%28m%20%2B%201%29%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%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%2C%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%2C%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%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%85%B6%E9%A4%98%E8%A1%8C%E5%92%8C%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281%2C%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%E5%85%A9%E5%AD%97%E5%85%83%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%89%87%E7%9B%B4%E6%8E%A5%E8%B7%B3%E9%81%8E%E6%AD%A4%E5%85%A9%E5%AD%97%E5%85%83%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%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%AA%E9%99%A4%E3%80%81%E6%9B%BF%E6%8F%9B%E9%80%99%E4%B8%89%E7%A8%AE%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%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%2C%20dp%5Bi%20-%201%5D%5Bj%5D%2C%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%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%0A%20%20%20%20%23%20%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%0A%20%20%20%20res%20%3D%20edit_distance_dp%28s%2C%20t%29%0A%20%20%20%20print%28f%22%E5%B0%87%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E7%82%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%B7%A8%E8%BC%AF%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%2C%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%B7%A8%E8%BC%AF%E8%B7%9D%E9%9B%A2%EF%BC%9A%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%22%22%22%0A%20%20%20%20n%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20%2A%20%28m%20%2B%201%29%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%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%2C%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%2C%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%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%85%B6%E9%A4%98%E8%A1%8C%E5%92%8C%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281%2C%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%E5%85%A9%E5%AD%97%E5%85%83%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%89%87%E7%9B%B4%E6%8E%A5%E8%B7%B3%E9%81%8E%E6%AD%A4%E5%85%A9%E5%AD%97%E5%85%83%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%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%AA%E9%99%A4%E3%80%81%E6%9B%BF%E6%8F%9B%E9%80%99%E4%B8%89%E7%A8%AE%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%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%2C%20dp%5Bi%20-%201%5D%5Bj%5D%2C%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%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%0A%20%20%20%20%23%20%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%0A%20%20%20%20res%20%3D%20edit_distance_dp%28s%2C%20t%29%0A%20%20%20%20print%28f%22%E5%B0%87%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E7%82%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%B7%A8%E8%BC%AF%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>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<10>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<11>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<12>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<13>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<14>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<15>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-30 編輯距離的動態規劃過程 </p>
|
||||
|
||||
### 3. 空間最佳化
|
||||
|
||||
由於 $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], dp[j]), leftup) + 1
|
||||
}
|
||||
leftup = temp // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="edit_distance.rb"
|
||||
### 編輯距離:空間最佳化後的動態規劃 ###
|
||||
def edit_distance_dp_comp(s, t)
|
||||
n, m = s.length, t.length
|
||||
dp = Array.new(m + 1, 0)
|
||||
# 狀態轉移:首行
|
||||
(1...(m + 1)).each { |j| dp[j] = j }
|
||||
# 狀態轉移:其餘行
|
||||
for i in 1...(n + 1)
|
||||
# 狀態轉移:首列
|
||||
leftup = dp.first # 暫存 dp[i-1, j-1]
|
||||
dp[0] += 1
|
||||
# 狀態轉移:其餘列
|
||||
for j in 1...(m + 1)
|
||||
temp = dp[j]
|
||||
if s[i - 1] == t[j - 1]
|
||||
# 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup
|
||||
else
|
||||
# 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = [dp[j - 1], dp[j], leftup].min + 1
|
||||
end
|
||||
leftup = temp # 更新為下一輪的 dp[i-1, j-1]
|
||||
end
|
||||
end
|
||||
dp[m]
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%B7%A8%E8%BC%AF%E8%B7%9D%E9%9B%A2%EF%BC%9A%E7%A9%BA%E9%96%93%E6%9C%80%E4%BD%B3%E5%8C%96%E5%BE%8C%E7%9A%84%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%22%22%22%0A%20%20%20%20n%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%0A%20%20%20%20for%20j%20in%20range%281%2C%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%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%85%B6%E9%A4%98%E8%A1%8C%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%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%AB%E5%AD%98%20dp%5Bi-1%2C%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%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%85%B6%E9%A4%98%E5%88%97%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281%2C%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%E5%85%A9%E5%AD%97%E5%85%83%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%89%87%E7%9B%B4%E6%8E%A5%E8%B7%B3%E9%81%8E%E6%AD%A4%E5%85%A9%E5%AD%97%E5%85%83%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%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%AA%E9%99%A4%E3%80%81%E6%9B%BF%E6%8F%9B%E9%80%99%E4%B8%89%E7%A8%AE%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%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%2C%20dp%5Bj%5D%2C%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%E7%82%BA%E4%B8%8B%E4%B8%80%E8%BC%AA%E7%9A%84%20dp%5Bi-1%2C%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%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%0A%20%20%20%20%23%20%E7%A9%BA%E9%96%93%E6%9C%80%E4%BD%B3%E5%8C%96%E5%BE%8C%E7%9A%84%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%0A%20%20%20%20res%20%3D%20edit_distance_dp_comp%28s%2C%20t%29%0A%20%20%20%20print%28f%22%E5%B0%87%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E7%82%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%B7%A8%E8%BC%AF%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%2C%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%B7%A8%E8%BC%AF%E8%B7%9D%E9%9B%A2%EF%BC%9A%E7%A9%BA%E9%96%93%E6%9C%80%E4%BD%B3%E5%8C%96%E5%BE%8C%E7%9A%84%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%22%22%22%0A%20%20%20%20n%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%0A%20%20%20%20for%20j%20in%20range%281%2C%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%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%85%B6%E9%A4%98%E8%A1%8C%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8B%80%E6%85%8B%E8%BD%89%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%AB%E5%AD%98%20dp%5Bi-1%2C%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%8B%80%E6%85%8B%E8%BD%89%E7%A7%BB%EF%BC%9A%E5%85%B6%E9%A4%98%E5%88%97%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281%2C%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%E5%85%A9%E5%AD%97%E5%85%83%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%89%87%E7%9B%B4%E6%8E%A5%E8%B7%B3%E9%81%8E%E6%AD%A4%E5%85%A9%E5%AD%97%E5%85%83%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%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%AA%E9%99%A4%E3%80%81%E6%9B%BF%E6%8F%9B%E9%80%99%E4%B8%89%E7%A8%AE%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%B7%A8%E8%BC%AF%E6%AD%A5%E6%95%B8%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%2C%20dp%5Bj%5D%2C%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%E7%82%BA%E4%B8%8B%E4%B8%80%E8%BC%AA%E7%9A%84%20dp%5Bi-1%2C%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%2C%20m%20%3D%20len%28s%29%2C%20len%28t%29%0A%0A%20%20%20%20%23%20%E7%A9%BA%E9%96%93%E6%9C%80%E4%BD%B3%E5%8C%96%E5%BE%8C%E7%9A%84%E5%8B%95%E6%85%8B%E8%A6%8F%E5%8A%83%0A%20%20%20%20res%20%3D%20edit_distance_dp_comp%28s%2C%20t%29%0A%20%20%20%20print%28f%22%E5%B0%87%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E7%82%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%B7%A8%E8%BC%AF%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,120 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 14.8 練習
|
||||
|
||||
## 14.8.1 知識鞏固
|
||||
|
||||
### 1. 什麼時候適合使用動態規劃
|
||||
|
||||
一位同學說:“只要能寫出遞推式,就應該使用動態規劃。”
|
||||
請判斷下面三個任務分別更適合使用動態規劃、回溯,還是使用迴圈或數學公式而不建立 `dp` 表,並寫出一個關鍵理由。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 使用面值 `[1, 3, 4]` 的硬幣湊出金額 6,求最少硬幣數,每種硬幣可重複使用;
|
||||
2. 輸出 `[1, 2, 3]` 的全部排列;
|
||||
3. 計算 $1 + 2 + \dots + n$。
|
||||
|
||||
對於你判斷為適合動態規劃的任務,再說明 `dp[i]` 表示什麼。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 適合動態規劃。令 `dp[i]` 表示湊出金額 `i` 所需的最少硬幣數。
|
||||
對於每枚不超過 `i` 的硬幣 `c`,都可以把 `dp[i-c] + 1` 作為候選答案,
|
||||
再從這些候選值中取最小值。不同選擇會反覆遇到同一金額,較大金額的最優解也能由較小金額的最優解構成。
|
||||
金額 6 的答案是 2,即 `3 + 3`。
|
||||
|
||||
2. 應使用回溯。題目要求逐個生成全部 6 個排列,回溯可以系統地嘗試一個選擇、繼續搜尋,
|
||||
再撤銷選擇並嘗試其他分支。無論採用什麼方法,實際輸出這些排列時都不能跳過列舉。
|
||||
|
||||
3. 使用迴圈或等差數列公式即可。雖然能寫出 `S(i) = S(i-1) + i`,但計算 `S(i)` 時只依賴一個更小的 `S(i-1)`,
|
||||
每個部分和只需計算一次,不存在重複子問題,因此無需建立 `dp` 表。“能寫遞推式”不等於“需要動態規劃”。
|
||||
|
||||
### 2. 背包表中的一個格子怎麼算
|
||||
|
||||
0-1 背包:物品重量 `wgt = [1, 2, 3]` ,價值 `val = [5, 11, 15]` ,背包容量 4 。
|
||||
`dp[i][c]` 表示只考慮前 $i$ 件物品、背包容量上限為 $c$ 時,能夠取得的最大價值;
|
||||
不要求恰好裝滿背包。
|
||||
|
||||
現在只計算狀態 `dp[3][4]`。已知 `dp[2][4] = 16`、`dp[2][1] = 5`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 不選第 3 件物品時,候選價值是多少?
|
||||
2. 選擇第 3 件物品時,還剩多少容量,候選價值是多少?
|
||||
3. `dp[3][4]` 應取多少?對應選擇了哪些物品?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 不選第 3 件物品時,沿用前兩件物品的結果,候選價值為 `dp[2][4] = 16`。
|
||||
|
||||
2. 第 3 件物品重量為 3,放入後還剩容量 $4-3=1$;候選價值為
|
||||
`dp[2][1] + 15 = 5 + 15 = 20`。
|
||||
|
||||
3. 比較 16 和 20,得到 `dp[3][4] = 20`。它對應選擇第 1、3 件物品,
|
||||
總重量為 $1+3=4$,總價值為 $5+15=20$。
|
||||
|
||||
這個狀態的計算體現了 0-1 背包的一次“選或不選”比較。
|
||||
|
||||
### 3. 背包容量應該按什麼順序更新
|
||||
|
||||
一個 0-1 背包只有一件物品:重量為 2,價值為 5;背包容量為 4。
|
||||
每件物品最多選擇一次,初始一維陣列為 `dp = [0, 0, 0, 0, 0]`。
|
||||
|
||||
一位同學在處理這件物品時,按容量從 2 到 4 更新:
|
||||
|
||||
- 更新 `dp[2]` 後得到 5;
|
||||
- 更新 `dp[3]` 後也得到 5;
|
||||
- 更新 `dp[4]` 時又使用剛得到的 `dp[2]`,於是得到 `dp[4] = 10`。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `dp[4] = 10` 這個結果是否正確?為什麼?
|
||||
2. 根據“每件物品最多選擇一次”的條件,`dp[4]` 應為多少?
|
||||
3. 處理每件物品時,容量應從大到小還是從小到大更新?這樣做避免了什麼問題?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 這個結果不正確。價值 10 相當於把這件價值為 5 的物品放入了兩次,
|
||||
違反了“每件物品最多選擇一次”的條件。
|
||||
|
||||
2. 背包中最多隻能放入這一件物品,所以正確的 `dp[4]` 為 5。
|
||||
|
||||
3. 容量應從大到小更新,即依次處理 4、3、2。
|
||||
這樣在計算 `dp[c]` 時,所讀取的 `dp[c-2]` 仍是處理當前物品之前的結果,
|
||||
不會在同一輪中重複使用當前物品。
|
||||
|
||||
## 14.8.2 程式設計練習
|
||||
|
||||
### 1. 爬樓梯的方案數
|
||||
|
||||
一段樓梯共有 `n` 階。你每次只能向上走 1 階或 2 階,且必須恰好到達第 `n` 階。
|
||||
請計算共有多少種不同的走法。規定 `n >= 1`,走法只按每一步走 1 階還是 2 階來區分。
|
||||
請使用一維動態規劃陣列完成,暫不使用只保留兩個狀態的空間最佳化寫法。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 到達第 i 階的最後一步,只可能跨 1 階或 2 階
|
||||
2. 因此有 dp[i] = dp[i-1] + dp[i-2]
|
||||
3. 先處理 n 為 1 和 2 的情況,再從第 3 階開始填表
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/climbing-stairs/solutions/2361764/70-pa-lou-ti-dong-tai-gui-hua-qing-xi-tu-ruwa/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **說明:** 題解講解了一維 dp 狀態和轉移,但給出的程式碼採用滾動變數壓縮空間;本練習要求先實現一維 dp 表,滾動變數僅作為選做最佳化
|
||||
|
||||
### 2. 0-1 背包
|
||||
|
||||
給定等長陣列 `wgt` 和 `val`,第 `i` 件物品的重量為正整數 `wgt[i]`、價值為非負整數 `val[i]`,
|
||||
背包容量 `cap` 為非負整數。每件物品最多選擇一次,請在總重量不超過 `cap` 的條件下,
|
||||
求能夠裝入背包的最大總價值。請使用一維動態規劃實現。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 初始化長度為 cap + 1 的陣列 dp,其中 dp[c] 表示容量上限為 c 時的最大價值
|
||||
2. 處理物品 i 時,比較“不選它”的 dp[c] 與“選它”的 dp[c-wgt[i]] + val[i]
|
||||
3. 容量必須從大到小更新,避免在同一輪中重複選擇當前物品
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/table-pivot
|
||||
---
|
||||
|
||||
# 第 14 章 動態規劃
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
小溪匯入河流,江河匯入大海。
|
||||
|
||||
動態規劃將小問題的解彙集成大問題的答案,一步步引領我們走向解決問題的彼岸。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [14.1 初探動態規劃](intro_to_dynamic_programming.md)
|
||||
- [14.2 DP 問題特性](dp_problem_features.md)
|
||||
- [14.3 DP 解題思路](dp_solution_pipeline.md)
|
||||
- [14.4 0-1 背包問題](knapsack_problem.md)
|
||||
- [14.5 完全背包問題](unbounded_knapsack_problem.md)
|
||||
- [14.6 編輯距離問題](edit_distance_problem.md)
|
||||
- [14.7 小結](summary.md)
|
||||
- [14.8 練習](exercises.md)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.7 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 動態規劃對問題進行分解,並透過儲存子問題的解來規避重複計算,提高計算效率。
|
||||
- 不考慮時間的前提下,所有動態規劃問題都可以用回溯(暴力搜尋)進行求解,但遞迴樹中存在大量的重疊子問題,效率極低。透過引入記憶化串列,可以儲存所有計算過的子問題的解,從而保證重疊子問題只被計算一次。
|
||||
- 記憶化搜尋是一種從頂至底的遞迴式解法,而與之對應的動態規劃是一種從底至頂的遞推式解法,其如同“填寫表格”一樣。由於當前狀態僅依賴某些區域性狀態,因此我們可以消除 $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
@@ -0,0 +1,110 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 9.5 練習
|
||||
|
||||
## 9.5.1 知識鞏固
|
||||
|
||||
### 1. 用兩種方式表示同一張圖
|
||||
|
||||
一個無向圖有 4 個頂點 `A、B、C、D`,邊為
|
||||
`A-B、A-C、B-C、C-D`。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 寫出它的鄰接表;
|
||||
2. 填寫只含 0 和 1 的鄰接矩陣;
|
||||
3. 如果要判斷 `A` 和 `D` 是否直接相連,哪種圖的表示方法只需檢視一個儲存條目?
|
||||
4. 如果圖的頂點很多但邊很少,哪種表示通常更節省空間?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 鄰接表為:
|
||||
|
||||
```text
|
||||
A: B, C
|
||||
B: A, C
|
||||
C: A, B, D
|
||||
D: C
|
||||
```
|
||||
|
||||
2. 鄰接矩陣為:
|
||||
|
||||
| | A | B | C | D |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A | 0 | 1 | 1 | 0 |
|
||||
| B | 1 | 0 | 1 | 0 |
|
||||
| C | 1 | 1 | 0 | 1 |
|
||||
| D | 0 | 0 | 1 | 0 |
|
||||
|
||||
3. 鄰接矩陣可直接檢視第 `A` 行、第 `D` 列,因此很適合判斷任意兩點是否直接相連。
|
||||
|
||||
4. 頂點很多但邊很少時,鄰接表只記錄實際存在的邊,通常比為每一對頂點都預留位置的鄰接矩陣更節省空間。
|
||||
|
||||
### 2. 廣度優先與深度優先訪問順序
|
||||
|
||||
一個無向圖的頂點為 `A、B、C、D、E`,邊為
|
||||
`A-B、A-C、B-D、C-D、D-E`。
|
||||
|
||||
從 A 出發,並規定遇到多個未訪問鄰接頂點時按字母順序選擇:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 寫出廣度優先走訪(BFS)的訪問順序;
|
||||
2. 寫出遞迴深度優先走訪(DFS)的訪問順序;
|
||||
3. 為什麼兩種走訪都需要記錄已經訪問過的頂點?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. BFS 的訪問順序為 `A, B, C, D, E`。它先訪問離 A 一條邊的 B、C,
|
||||
再訪問更遠的 D、E。
|
||||
|
||||
2. DFS 的訪問順序為 `A, B, D, C, E`。它依次進入當前頂點的未訪問鄰接頂點,
|
||||
因此先走過 `A → B → D → C`;C 沒有新的鄰接頂點時回到 D,再訪問 E。
|
||||
|
||||
3. 圖中存在環,例如 `A-B-D-C-A`。如果不記錄已訪問頂點,
|
||||
走訪可能反覆沿環訪問同一批頂點,無法正常結束。
|
||||
|
||||
### 3. 一次 BFS 能訪問整張圖嗎
|
||||
|
||||
一個無向圖有頂點 `A、B、C、D、E、F`,邊只有
|
||||
`A-B、B-C、D-E`。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 從 A 開始進行一次 BFS,可以訪問哪些頂點?
|
||||
2. 根據第 1 問,這一次 BFS 是否已經訪問圖中的所有頂點?為什麼?
|
||||
3. 若按字母順序掃描所有頂點,每遇到一個未訪問頂點就重新開始 BFS,
|
||||
各次 BFS 的起點是什麼?這個圖被分成幾個互不連通的部分(連通分量)?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 從 A 出發只能訪問 `A、B、C`。
|
||||
|
||||
2. 沒有訪問所有頂點。`D、E` 組成另一個互相連通的部分,F 是單獨的頂點;
|
||||
它們與 A 之間都沒有路徑,因此從 A 出發無法到達。
|
||||
|
||||
3. 三次 BFS 的起點依次為 `A、D、F`,分別訪問
|
||||
`{A, B, C}`、`{D, E}` 和 `{F}`。因此這個圖有 3 個連通分量。
|
||||
|
||||
## 9.5.2 程式設計練習
|
||||
|
||||
### 1. 判斷無向圖中是否存在路徑
|
||||
|
||||
給定一個含有 $n$ 個頂點的無向圖,頂點編號為 $0$ 到 $n-1$。陣列 `edges` 中的每一項 `[u, v]` 表示頂點 `u` 和 `v` 之間有一條無向邊。
|
||||
|
||||
再給定起點 `source` 和終點 `destination`。請先根據 `edges` 建立鄰接表,再使用 BFS 或 DFS
|
||||
判斷是否存在一條從 `source` 到 `destination` 的路徑:存在則返回 `true`,否則返回 `false`。
|
||||
圖中可能有環,也可能不連通。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 每條無向邊需要同時加入兩個方向
|
||||
2. 圖中可能有環,必須記錄已經訪問過的節點
|
||||
3. 從 source 出發,遇到 destination 返回 true,走訪結束仍未遇到則返回 false
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 9.1 圖
|
||||
|
||||
<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 所示,**相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)的自由度更高**,因而更為複雜。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-1 鏈結串列、樹、圖之間的關係 </p>
|
||||
|
||||
## 9.1.1 圖的常見型別與術語
|
||||
|
||||
根據邊是否具有方向,可分為<u>無向圖(undirected graph)</u>和<u>有向圖(directed graph)</u>,如圖 9-2 所示。
|
||||
|
||||
- 在無向圖中,邊表示兩頂點之間的“雙向”連線關係,例如微信或 QQ 中的“好友關係”。
|
||||
- 在有向圖中,邊具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 兩個方向的邊是相互獨立的,例如微博或抖音上的“關注”與“被關注”關係。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-2 有向圖與無向圖 </p>
|
||||
|
||||
根據所有頂點是否連通,可分為<u>連通圖(connected graph)</u>和<u>非連通圖(disconnected graph)</u>,如圖 9-3 所示。
|
||||
|
||||
- 對於連通圖,從某個頂點出發,可以到達其餘任意頂點。
|
||||
- 對於非連通圖,從某個頂點出發,至少有一個頂點無法到達。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-3 連通圖與非連通圖 </p>
|
||||
|
||||
我們還可以為邊新增“權重”變數,從而得到如圖 9-4 所示的<u>有權圖(weighted graph)</u>。例如在《王者榮耀》等手遊中,系統會根據共同遊戲時間來計算玩家之間的“親密度”,這種親密度網路就可以用有權圖來表示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-4 有權圖與無權圖 </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 圖的表示
|
||||
|
||||
圖的常用表示方式包括“鄰接矩陣”和“鄰接表”。以下使用無向圖進行舉例。
|
||||
|
||||
### 1. 鄰接矩陣
|
||||
|
||||
設圖的頂點數量為 $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$ 表示兩頂點之間無邊。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-5 圖的鄰接矩陣表示 </p>
|
||||
|
||||
鄰接矩陣具有以下特性。
|
||||
|
||||
- 在簡單圖中,頂點不能與自身相連,此時鄰接矩陣主對角線元素沒有意義。
|
||||
- 對於無向圖,兩個方向的邊等價,此時鄰接矩陣關於主對角線對稱。
|
||||
- 將鄰接矩陣的元素從 $1$ 和 $0$ 替換為權重,則可表示有權圖。
|
||||
|
||||
使用鄰接矩陣表示圖時,我們可以直接訪問矩陣元素以獲取邊,因此增刪查改操作的效率很高,時間複雜度均為 $O(1)$ 。然而,矩陣的空間複雜度為 $O(n^2)$ ,記憶體佔用較多。
|
||||
|
||||
### 2. 鄰接表
|
||||
|
||||
<u>鄰接表(adjacency list)</u>使用 $n$ 個鏈結串列來表示圖,鏈結串列節點表示頂點。第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點(與該頂點相連的頂點)。圖 9-6 展示了一個使用鄰接表儲存的圖的示例。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-6 圖的鄰接表表示 </p>
|
||||
|
||||
鄰接表僅儲存實際存在的邊,而邊的總數通常遠小於 $n^2$ ,因此它更加節省空間。然而,在鄰接表中需要透過走訪鏈結串列來查詢邊,因此其時間效率不如鄰接矩陣。
|
||||
|
||||
觀察圖 9-6 ,**鄰接表結構與雜湊表中的“鏈式位址”非常相似,因此我們也可以採用類似的方法來最佳化效率**。比如當鏈結串列較長時,可以將鏈結串列轉化為 AVL 樹或紅黑樹,從而將時間效率從 $O(n)$ 最佳化至 $O(\log n)$ ;還可以把鏈結串列轉換為雜湊表,從而將時間複雜度降至 $O(1)$ 。
|
||||
|
||||
## 9.1.3 圖的常見應用
|
||||
|
||||
如表 9-1 所示,許多現實系統可以用圖來建模,相應的問題也可以約化為圖計算問題。
|
||||
|
||||
<p align="center"> 表 9-1 現實生活中常見的圖 </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
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/graphql
|
||||
---
|
||||
|
||||
# 第 9 章 圖
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
在生命旅途中,我們就像是一個個節點,被無數看不見的邊相連。
|
||||
|
||||
每一次的相識與相離,都在這張巨大的網路圖中留下獨特的印記。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [9.1 圖](graph.md)
|
||||
- [9.2 圖基礎操作](graph_operations.md)
|
||||
- [9.3 圖的走訪](graph_traversal.md)
|
||||
- [9.4 小結](summary.md)
|
||||
- [9.5 練習](exercises.md)
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 9.4 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 圖由頂點和邊組成,可以表示為一組頂點和一組邊構成的集合。
|
||||
- 相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)具有更高的自由度,因而更為複雜。
|
||||
- 有向圖的邊具有方向性,連通圖中的任意頂點均可達,有權圖的每條邊都包含權重變數。
|
||||
- 鄰接矩陣利用矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間有邊或無邊。鄰接矩陣在增刪查改操作上效率很高,但空間佔用較多。
|
||||
- 鄰接表使用多個鏈結串列來表示圖,第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點。鄰接表相對於鄰接矩陣更加節省空間,但由於需要走訪鏈結串列來查詢邊,因此時間效率較低。
|
||||
- 當鄰接表中的鏈結串列過長時,可以將其轉換為紅黑樹或雜湊表,從而提升查詢效率。
|
||||
- 從演算法思想的角度分析,鄰接矩陣體現了“以空間換時間”,鄰接表體現了“以時間換空間”。
|
||||
- 圖可用於建模各類現實系統,如社交網路、地鐵線路等。
|
||||
- 樹是圖的一種特例,樹的走訪也是圖的走訪的一種特例。
|
||||
- 圖的廣度優先走訪是一種由近及遠、層層擴張的搜尋方式,通常藉助佇列實現。
|
||||
- 圖的深度優先走訪是一種優先走到底、無路可走時再回溯的搜尋方式,常基於遞迴來實現。
|
||||
|
||||
### 2. 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,90 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 15.6 練習
|
||||
|
||||
## 15.6.1 知識鞏固
|
||||
|
||||
### 1. 每次選最大硬幣一定最好嗎
|
||||
|
||||
硬幣面值為 `[1, 7, 10]`,目標金額為 14。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 按“每次取不超過剩餘金額的最大面值”執行,寫出所取硬幣;
|
||||
2. 是否存在使用更少硬幣的方案?如果存在,請寫出一種;如果不存在,請說明理由;
|
||||
3. 這能否說明該貪婪策略對任意硬幣面值都正確?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 貪婪依次選擇 `10 + 1 + 1 + 1 + 1`,共 5 枚硬幣。
|
||||
|
||||
2. 存在使用更少硬幣的方案:`7 + 7`,只需 2 枚硬幣。
|
||||
|
||||
3. 不能。這個反例說明,對任意硬幣面值,每次選擇當前能取的最大面值並不一定得到最少硬幣數;
|
||||
眼前最大的選擇可能破壞後續更好的組合。
|
||||
|
||||
### 2. 背包應該先裝哪件物品
|
||||
|
||||
一個容量為 4 千克的背包中可以裝入以下物品,物品允許只取一部分,
|
||||
所取價值與重量成正比:
|
||||
|
||||
- 物品 A:重量 4 千克,價值 20;
|
||||
- 物品 B:重量 3 千克,價值 18。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 兩件物品每千克的價值分別是多少?應先裝哪一件?
|
||||
2. 按分數背包的貪婪策略裝滿背包,最終價值是多少?
|
||||
3. 在物品可以切分、背包限制總重量的情況下,選擇物品時應該比較總價值還是每千克價值?為什麼?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. A 每千克價值為 `20 ÷ 4 = 5`,B 每千克價值為 `18 ÷ 3 = 6`,
|
||||
因此應先裝單位價值更高的 B。
|
||||
|
||||
2. 先裝入全部 B,佔用 3 千克、獲得價值 18;剩餘 1 千克容量,
|
||||
再裝入 1 千克 A,獲得價值 5。最終價值為 `18 + 5 = 23`。
|
||||
|
||||
3. 背包限制的是總重量,而且物品可以切分,所以應比較單位重量的價值。
|
||||
A 的總價值雖然更高,但每千克價值低於 B;若先裝滿 A,只能得到價值 20。
|
||||
|
||||
### 3. 兩個指標下一步怎樣移動
|
||||
|
||||
隔板高度為 `[1, 8, 6, 2, 5]`,用首尾雙指標尋找最大容量。
|
||||
容量等於“兩塊隔板中較短者的高度 × 兩塊隔板的下標距離”。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 初始時左指標在下標 0、右指標在下標 4,當前容量是多少?下一步應移動哪個指標?
|
||||
2. 按照第 1 問的選擇移動一次後,兩個指標分別位於哪個下標?此時容量是多少?下一步又應移動哪個指標?
|
||||
3. 在當前一對隔板中,可以移動較短隔板對應的指標,也可以移動較長隔板對應的指標。哪一種移動仍有可能得到更大的容量?為什麼?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 當前容量為 `min(1, 5) × (4 - 0) = 4`。左側隔板較短,因此移動左指標。
|
||||
|
||||
2. 移動左指標後,兩個指標分別位於下標 1 和 4。當前容量為
|
||||
`min(8, 5) × (4 - 1) = 15`。右側隔板較短,因此下一步移動右指標。
|
||||
|
||||
3. 移動較短隔板對應的指標,才仍有可能得到更大的容量。移動較長隔板後,兩塊隔板的距離一定縮短,而容器高度仍受未移動的短板限制,
|
||||
只可能不變或變小。因此容量不可能超過移動前;只有移動短板,才有機會遇到更高的隔板。
|
||||
|
||||
## 15.6.2 程式設計練習
|
||||
|
||||
### 1. 分數背包
|
||||
|
||||
給定等長陣列 `wgt` 和 `val`,其中 `wgt[i] > 0`、`val[i] >= 0`,背包容量 `cap >= 0`。
|
||||
每種物品只有一件,但允許只裝入其中一部分,
|
||||
獲得的價值按所裝重量佔該物品總重量的比例計算。請使用貪婪演算法,
|
||||
並以實數返回背包能夠獲得的最大總價值。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 先計算每件物品的單位重量價值 val[i] / wgt[i],除法結果要保留小數
|
||||
2. 單位價值越高的物品越先裝入背包
|
||||
3. 若剩餘容量小於當前物品重量,就裝入恰好填滿背包的部分並結束
|
||||
@@ -0,0 +1,557 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.2 分數背包問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定 $n$ 個物品,第 $i$ 個物品的重量為 $wgt[i-1]$、價值為 $val[i-1]$ ,和一個容量為 $cap$ 的背包。每個物品只能選擇一次,**但可以選擇物品的一部分,價值根據選擇的重量比例計算**,問在限定背包容量下背包中物品的最大價值。示例如圖 15-3 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-3 分數背包問題的示例資料 </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]$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-4 物品在單位重量下的價值 </p>
|
||||
|
||||
### 1. 貪婪策略確定
|
||||
|
||||
最大化背包內物品總價值,**本質上是最大化單位重量下的物品價值**。由此便可推理出圖 15-5 所示的貪婪策略。
|
||||
|
||||
1. 將物品按照單位價值從高到低進行排序。
|
||||
2. 走訪所有物品,**每輪貪婪地選擇單位價值最高的物品**。
|
||||
3. 若剩餘背包容量不足,則使用當前物品的一部分填滿背包。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-5 分數背包問題的貪婪策略 </p>
|
||||
|
||||
### 2. 程式碼實現
|
||||
|
||||
我們建立了一個物品類別 `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, _val: IntArray, c: Int): Double {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
var cap = c
|
||||
val items = arrayOfNulls<Item>(wgt.size)
|
||||
for (i in wgt.indices) {
|
||||
items[i] = Item(wgt[i], _val[i])
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }
|
||||
// 迴圈貪婪選擇
|
||||
var res = 0.0
|
||||
for (item in items) {
|
||||
if (item!!.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v
|
||||
cap -= item.w
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += item.v.toDouble() / item.w * cap
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="fractional_knapsack.rb"
|
||||
### 物品 ###
|
||||
class Item
|
||||
attr_accessor :w # 物品重量
|
||||
attr_accessor :v # 物品價值
|
||||
|
||||
def initialize(w, v)
|
||||
@w = w
|
||||
@v = v
|
||||
end
|
||||
end
|
||||
|
||||
### 分數背包:貪婪 ###
|
||||
def fractional_knapsack(wgt, val, cap)
|
||||
# 建立物品串列,包含兩個屬性:重量,價值
|
||||
items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }
|
||||
# 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }
|
||||
# 迴圈貪婪選擇
|
||||
res = 0
|
||||
for item in items
|
||||
if item.w <= cap
|
||||
# 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v
|
||||
cap -= item.w
|
||||
else
|
||||
# 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (item.v.to_f / item.w) * cap
|
||||
# 已無剩餘容量,因此跳出迴圈
|
||||
break
|
||||
end
|
||||
end
|
||||
res
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20w%3A%20int%2C%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%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%E5%83%B9%E5%80%BC%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B8%E8%83%8C%E5%8C%85%EF%BC%9A%E8%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E5%BB%BA%E7%AB%8B%E7%89%A9%E5%93%81%E4%B8%B2%E5%88%97%EF%BC%8C%E5%8C%85%E5%90%AB%E5%85%A9%E5%80%8B%E5%B1%AC%E6%80%A7%EF%BC%9A%E9%87%8D%E9%87%8F%E3%80%81%E5%83%B9%E5%80%BC%0A%20%20%20%20items%20%3D%20%5BItem%28w%2C%20v%29%20for%20w%2C%20v%20in%20zip%28wgt%2C%20val%29%5D%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E5%96%AE%E4%BD%8D%E5%83%B9%E5%80%BC%20item.v%20/%20item.w%20%E5%BE%9E%E9%AB%98%E5%88%B0%E4%BD%8E%E9%80%B2%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20/%20item.w%2C%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E8%B2%AA%E5%A9%AA%E9%81%B8%E6%93%87%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%E9%A4%98%E5%AE%B9%E9%87%8F%E5%85%85%E8%B6%B3%EF%BC%8C%E5%89%87%E5%B0%87%E7%95%B6%E5%89%8D%E7%89%A9%E5%93%81%E6%95%B4%E5%80%8B%E8%A3%9D%E9%80%B2%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%E9%A4%98%E5%AE%B9%E9%87%8F%E4%B8%8D%E8%B6%B3%EF%BC%8C%E5%89%87%E5%B0%87%E7%95%B6%E5%89%8D%E7%89%A9%E5%93%81%E7%9A%84%E4%B8%80%E9%83%A8%E5%88%86%E8%A3%9D%E9%80%B2%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%2A%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B7%B2%E7%84%A1%E5%89%A9%E9%A4%98%E5%AE%B9%E9%87%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E8%B7%B3%E5%87%BA%E8%BF%B4%E5%9C%88%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%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%E6%BC%94%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt%2C%20val%2C%20cap%29%0A%20%20%20%20print%28f%22%E4%B8%8D%E8%B6%85%E9%81%8E%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%E5%83%B9%E5%80%BC%E7%82%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%2C%20w%3A%20int%2C%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%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%E5%83%B9%E5%80%BC%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B8%E8%83%8C%E5%8C%85%EF%BC%9A%E8%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E5%BB%BA%E7%AB%8B%E7%89%A9%E5%93%81%E4%B8%B2%E5%88%97%EF%BC%8C%E5%8C%85%E5%90%AB%E5%85%A9%E5%80%8B%E5%B1%AC%E6%80%A7%EF%BC%9A%E9%87%8D%E9%87%8F%E3%80%81%E5%83%B9%E5%80%BC%0A%20%20%20%20items%20%3D%20%5BItem%28w%2C%20v%29%20for%20w%2C%20v%20in%20zip%28wgt%2C%20val%29%5D%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E5%96%AE%E4%BD%8D%E5%83%B9%E5%80%BC%20item.v%20/%20item.w%20%E5%BE%9E%E9%AB%98%E5%88%B0%E4%BD%8E%E9%80%B2%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20/%20item.w%2C%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E8%B2%AA%E5%A9%AA%E9%81%B8%E6%93%87%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%E9%A4%98%E5%AE%B9%E9%87%8F%E5%85%85%E8%B6%B3%EF%BC%8C%E5%89%87%E5%B0%87%E7%95%B6%E5%89%8D%E7%89%A9%E5%93%81%E6%95%B4%E5%80%8B%E8%A3%9D%E9%80%B2%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%E9%A4%98%E5%AE%B9%E9%87%8F%E4%B8%8D%E8%B6%B3%EF%BC%8C%E5%89%87%E5%B0%87%E7%95%B6%E5%89%8D%E7%89%A9%E5%93%81%E7%9A%84%E4%B8%80%E9%83%A8%E5%88%86%E8%A3%9D%E9%80%B2%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%2A%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B7%B2%E7%84%A1%E5%89%A9%E9%A4%98%E5%AE%B9%E9%87%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E8%B7%B3%E5%87%BA%E8%BF%B4%E5%9C%88%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%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%E6%BC%94%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt%2C%20val%2C%20cap%29%0A%20%20%20%20print%28f%22%E4%B8%8D%E8%B6%85%E9%81%8E%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%E5%83%B9%E5%80%BC%E7%82%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(\log n)$ ,空間複雜度通常為 $O(\log n)$ 或 $O(n)$ ,取決於程式語言的具體實現。
|
||||
|
||||
除排序之外,在最差情況下,需要走訪整個物品串列,**因此時間複雜度為 $O(n)$** ,其中 $n$ 為物品數量。
|
||||
|
||||
由於初始化了一個 `Item` 物件串列,**因此空間複雜度為 $O(n)$** 。
|
||||
|
||||
### 3. 正確性證明
|
||||
|
||||
採用反證法。假設物品 $x$ 是單位價值最高的物品,使用某演算法求得最大價值為 `res` ,但該解中不包含物品 $x$ 。
|
||||
|
||||
現在從背包中拿出單位重量的任意物品,並替換為單位重量的物品 $x$ 。由於物品 $x$ 的單位價值最高,因此替換後的總價值一定大於 `res` 。**這與 `res` 是最優解矛盾,說明最優解中必須包含物品 $x$** 。
|
||||
|
||||
對於該解中的其他物品,我們也可以構建出上述矛盾。總而言之,**單位價值更大的物品總是更優選擇**,這說明貪婪策略是有效的。
|
||||
|
||||
如圖 15-6 所示,如果將物品重量和物品單位價值分別看作一張二維圖表的橫軸和縱軸,則分數背包問題可轉化為“求在有限橫軸區間下圍成的最大面積”。這個類比可以幫助我們從幾何角度理解貪婪策略的有效性。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-6 分數背包問題的幾何表示 </p>
|
||||
@@ -0,0 +1,408 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.1 貪婪演算法
|
||||
|
||||
<u>貪婪演算法(greedy algorithm)</u>是一種常見的解決最佳化問題的演算法,其基本思想是在問題的每個決策階段,都選擇當前看起來最優的選擇,即貪婪地做出區域性最優的決策,以期獲得全域性最優解。貪婪演算法簡潔且高效,在許多實際問題中有著廣泛的應用。
|
||||
|
||||
貪婪演算法和動態規劃都常用於解決最佳化問題。它們之間存在一些相似之處,比如都依賴最優子結構性質,但工作原理不同。
|
||||
|
||||
- 動態規劃會根據之前階段的所有決策來考慮當前決策,並使用過去子問題的解來構建當前子問題的解。
|
||||
- 貪婪演算法不會考慮過去的決策,而是一路向前地進行貪婪選擇,不斷縮小問題範圍,直至問題被解決。
|
||||
|
||||
我們先透過例題“零錢兌換”瞭解貪婪演算法的工作原理。這道題已經在“完全背包問題”章節中介紹過,相信你對它並不陌生。
|
||||
|
||||
!!! question
|
||||
|
||||
給定 $n$ 種硬幣,第 $i$ 種硬幣的面值為 $coins[i - 1]$ ,目標金額為 $amt$ ,每種硬幣可以重複選取,問能夠湊出目標金額的最少硬幣數量。如果無法湊出目標金額,則返回 $-1$ 。
|
||||
|
||||
本題採取的貪婪策略如圖 15-1 所示。給定目標金額,**我們貪婪地選擇不大於且最接近它的硬幣**,不斷迴圈該步驟,直至湊出目標金額為止。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-1 零錢兌換的貪婪策略 </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"
|
||||
### 零錢兌換:貪婪 ###
|
||||
def coin_change_greedy(coins, amt)
|
||||
# 假設 coins 串列有序
|
||||
i = coins.length - 1
|
||||
count = 0
|
||||
# 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while amt > 0
|
||||
# 找到小於且最接近剩餘金額的硬幣
|
||||
while i > 0 && coins[i] > amt
|
||||
i -= 1
|
||||
end
|
||||
# 選擇 coins[i]
|
||||
amt -= coins[i]
|
||||
count += 1
|
||||
end
|
||||
# 若未找到可行方案, 則返回 -1
|
||||
amt == 0 ? count : -1
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9B%B6%E9%8C%A2%E5%85%8C%E6%8F%9B%EF%BC%9A%E8%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E5%81%87%E8%A8%AD%20coins%20%E4%B8%B2%E5%88%97%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%E8%BF%B4%E5%9C%88%E9%80%B2%E8%A1%8C%E8%B2%AA%E5%A9%AA%E9%81%B8%E6%93%87%EF%BC%8C%E7%9B%B4%E5%88%B0%E7%84%A1%E5%89%A9%E9%A4%98%E9%87%91%E9%A1%8D%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%E6%96%BC%E4%B8%94%E6%9C%80%E6%8E%A5%E8%BF%91%E5%89%A9%E9%A4%98%E9%87%91%E9%A1%8D%E7%9A%84%E7%A1%AC%E5%B9%A3%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%81%B8%E6%93%87%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%89%87%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%B2%AA%E5%A9%AA%EF%BC%9A%E8%83%BD%E5%A4%A0%E4%BF%9D%E8%AD%89%E6%89%BE%E5%88%B0%E5%85%A8%E5%9F%9F%E6%80%A7%E6%9C%80%E5%84%AA%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1%2C%205%2C%2010%2C%2020%2C%2050%2C%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E6%B9%8A%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%B9%A3%E6%95%B8%E9%87%8F%E7%82%BA%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%EF%BC%9A%E7%84%A1%E6%B3%95%E4%BF%9D%E8%AD%89%E6%89%BE%E5%88%B0%E5%85%A8%E5%9F%9F%E6%80%A7%E6%9C%80%E5%84%AA%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1%2C%2020%2C%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E6%B9%8A%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%B9%A3%E6%95%B8%E9%87%8F%E7%82%BA%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AF%A6%E9%9A%9B%E4%B8%8A%E9%9C%80%E8%A6%81%E7%9A%84%E6%9C%80%E5%B0%91%E6%95%B8%E9%87%8F%E7%82%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%2C%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9B%B6%E9%8C%A2%E5%85%8C%E6%8F%9B%EF%BC%9A%E8%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E5%81%87%E8%A8%AD%20coins%20%E4%B8%B2%E5%88%97%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%E8%BF%B4%E5%9C%88%E9%80%B2%E8%A1%8C%E8%B2%AA%E5%A9%AA%E9%81%B8%E6%93%87%EF%BC%8C%E7%9B%B4%E5%88%B0%E7%84%A1%E5%89%A9%E9%A4%98%E9%87%91%E9%A1%8D%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%E6%96%BC%E4%B8%94%E6%9C%80%E6%8E%A5%E8%BF%91%E5%89%A9%E9%A4%98%E9%87%91%E9%A1%8D%E7%9A%84%E7%A1%AC%E5%B9%A3%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%81%B8%E6%93%87%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%89%87%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%B2%AA%E5%A9%AA%EF%BC%9A%E8%83%BD%E5%A4%A0%E4%BF%9D%E8%AD%89%E6%89%BE%E5%88%B0%E5%85%A8%E5%9F%9F%E6%80%A7%E6%9C%80%E5%84%AA%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1%2C%205%2C%2010%2C%2020%2C%2050%2C%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E6%B9%8A%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%B9%A3%E6%95%B8%E9%87%8F%E7%82%BA%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%EF%BC%9A%E7%84%A1%E6%B3%95%E4%BF%9D%E8%AD%89%E6%89%BE%E5%88%B0%E5%85%A8%E5%9F%9F%E6%80%A7%E6%9C%80%E5%84%AA%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1%2C%2020%2C%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E6%B9%8A%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%B9%A3%E6%95%B8%E9%87%8F%E7%82%BA%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AF%A6%E9%9A%9B%E4%B8%8A%E9%9C%80%E8%A6%81%E7%9A%84%E6%9C%80%E5%B0%91%E6%95%B8%E9%87%8F%E7%82%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 貪婪演算法的優點與侷限性
|
||||
|
||||
**貪婪演算法不僅操作直接、實現簡單,而且通常效率也很高**。在以上程式碼中,記硬幣最小面值為 $\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$ 枚硬幣。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-2 貪婪演算法無法找出最優解的示例 </p>
|
||||
|
||||
也就是說,對於零錢兌換問題,貪婪演算法無法保證找到全域性最優解,並且有可能找到非常差的解。它更適合用動態規劃解決。
|
||||
|
||||
一般情況下,貪婪演算法的適用情況分以下兩種。
|
||||
|
||||
1. **可以保證找到最優解**:貪婪演算法在這種情況下往往是最優選擇,因為它往往比回溯、動態規劃更高效。
|
||||
2. **可以找到近似最優解**:貪婪演算法在這種情況下也是可用的。對於很多複雜問題來說,尋找全域性最優解非常困難,能以較高效率找到次優解也是非常不錯的。
|
||||
|
||||
## 15.1.2 貪婪演算法特性
|
||||
|
||||
那麼問題來了,什麼樣的問題適合用貪婪演算法求解呢?或者說,貪婪演算法在什麼情況下可以保證找到最優解?
|
||||
|
||||
相較於動態規劃,貪婪演算法的使用條件更加苛刻,其主要關注問題的兩個性質。
|
||||
|
||||
- **貪婪選擇性質**:只有當局部最優選擇始終可以導致全域性最優解時,貪婪演算法才能保證得到最優解。
|
||||
- **最優子結構**:原問題的最優解包含子問題的最優解。
|
||||
|
||||
最優子結構已經在“動態規劃”章節中介紹過,這裡不再贅述。值得注意的是,一些問題的最優子結構並不明顯,但仍然可使用貪婪演算法解決。
|
||||
|
||||
我們主要探究貪婪選擇性質的判斷方法。雖然它的描述看上去比較簡單,**但實際上對於許多問題,證明貪婪選擇性質並非易事**。
|
||||
|
||||
例如零錢兌換問題,我們雖然能夠容易地舉出反例,對貪婪選擇性質進行證偽,但證實的難度較大。如果問:**滿足什麼條件的硬幣組合可以使用貪婪演算法求解**?我們往往只能憑藉直覺或舉例子來給出一個模稜兩可的答案,而難以給出嚴謹的數學證明。
|
||||
|
||||
!!! quote
|
||||
|
||||
有一篇論文給出了一個 $O(n^3)$ 時間複雜度的演算法,用於判斷一個硬幣組合能否使用貪婪演算法找出任意金額的最優解。
|
||||
|
||||
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
|
||||
|
||||
## 15.1.3 貪婪演算法解題步驟
|
||||
|
||||
貪婪問題的解決流程大體可分為以下三步。
|
||||
|
||||
1. **問題分析**:梳理與理解問題特性,包括狀態定義、最佳化目標和約束條件等。這一步在回溯和動態規劃中都有涉及。
|
||||
2. **確定貪婪策略**:確定如何在每一步中做出貪婪選擇。這個策略能夠在每一步減小問題的規模,並最終解決整個問題。
|
||||
3. **正確性證明**:通常需要證明問題具有貪婪選擇性質和最優子結構。這個步驟可能需要用到數學證明,例如歸納法或反證法等。
|
||||
|
||||
確定貪婪策略是求解問題的核心步驟,但實施起來可能並不容易,主要有以下原因。
|
||||
|
||||
- **不同問題的貪婪策略的差異較大**。對於許多問題來說,貪婪策略比較淺顯,我們透過一些大概的思考與嘗試就能得出。而對於一些複雜問題,貪婪策略可能非常隱蔽,這種情況就非常考驗個人的解題經驗與演算法能力了。
|
||||
- **某些貪婪策略具有較強的迷惑性**。當我們滿懷信心設計好貪婪策略,寫出解題程式碼並提交執行,很可能發現部分測試樣例無法透過。這是因為設計的貪婪策略只是“部分正確”的,上文介紹的零錢兌換就是一個典型案例。
|
||||
|
||||
為了保證正確性,我們應該對貪婪策略進行嚴謹的數學證明,**通常需要用到反證法或數學歸納法**。
|
||||
|
||||
然而,正確性證明也很可能不是一件易事。如若沒有頭緒,我們通常會選擇面向測試用例進行程式碼除錯,一步步修改與驗證貪婪策略。
|
||||
|
||||
## 15.1.4 貪婪演算法典型例題
|
||||
|
||||
貪婪演算法常常應用在滿足貪婪選擇性質和最優子結構的最佳化問題中,以下列舉了一些典型的貪婪演算法問題。
|
||||
|
||||
- **硬幣找零問題**:在某些硬幣組合下,貪婪演算法總是可以得到最優解。
|
||||
- **區間排程問題**:假設你有一些任務,每個任務在一段時間內進行,你的目標是完成儘可能多的任務。如果每次都選擇結束時間最早的任務,那麼貪婪演算法就可以得到最優解。
|
||||
- **分數背包問題**:給定一組物品和一個載重量,你的目標是選擇一組物品,使得總重量不超過載重量,且總價值最大。如果每次都選擇價效比最高(價值 / 重量)的物品,那麼貪婪演算法在一些情況下可以得到最優解。
|
||||
- **股票買賣問題**:給定一組股票的歷史價格,你可以進行多次買賣,但如果你已經持有股票,那麼在賣出之前不能再買,目標是獲取最大利潤。
|
||||
- **霍夫曼編碼**:霍夫曼編碼是一種用於無損資料壓縮的貪婪演算法。透過構建霍夫曼樹,每次選擇出現頻率最低的兩個節點合併,最後得到的霍夫曼樹的帶權路徑長度(編碼長度)最小。
|
||||
- **Dijkstra 演算法**:它是一種解決給定源頂點到其餘各頂點的最短路徑問題的貪婪演算法。
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/head-heart-outline
|
||||
---
|
||||
|
||||
# 第 15 章 貪婪
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
向日葵朝著太陽轉動,時刻追求自身成長的最大可能。
|
||||
|
||||
貪婪策略在一輪輪的簡單選擇中,逐步導向最佳答案。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [15.1 貪婪演算法](greedy_algorithm.md)
|
||||
- [15.2 分數背包問題](fractional_knapsack_problem.md)
|
||||
- [15.3 最大容量問題](max_capacity_problem.md)
|
||||
- [15.4 最大切分乘積問題](max_product_cutting_problem.md)
|
||||
- [15.5 小結](summary.md)
|
||||
- [15.6 練習](exercises.md)
|
||||
@@ -0,0 +1,445 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.3 最大容量問題
|
||||
|
||||
!!! question
|
||||
|
||||
輸入一個陣列 $ht$ ,其中的每個元素代表一個垂直隔板的高度。陣列中的任意兩個隔板,以及它們之間的空間可以組成一個容器。
|
||||
|
||||
容器的容量等於高度和寬度的乘積(面積),其中高度由較短的隔板決定,寬度是兩個隔板的陣列索引之差。
|
||||
|
||||
請在陣列中選擇兩個隔板,使得組成的容器的容量最大,返回最大容量。示例如圖 15-7 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-7 最大容量問題的示例資料 </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. 貪婪策略確定
|
||||
|
||||
這道題還有更高效率的解法。如圖 15-8 所示,現選取一個狀態 $[i, j]$ ,其滿足索引 $i < j$ 且高度 $ht[i] < ht[j]$ ,即 $i$ 為短板、$j$ 為長板。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-8 初始狀態 </p>
|
||||
|
||||
如圖 15-9 所示,**若此時將長板 $j$ 向短板 $i$ 靠近,則容量一定變小**。
|
||||
|
||||
這是因為在移動長板 $j$ 後,寬度 $j-i$ 肯定變小;而高度由短板決定,因此高度只可能不變( $i$ 仍為短板)或變小(移動後的 $j$ 成為短板)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-9 向內移動長板後的狀態 </p>
|
||||
|
||||
反向思考,**我們只有向內收縮短板 $i$ ,才有可能使容量變大**。因為雖然寬度一定變小,**但高度可能會變大**(移動後的短板 $i$ 可能會變長)。例如在圖 15-10 中,移動短板後面積變大。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-10 向內移動短板後的狀態 </p>
|
||||
|
||||
由此便可推出本題的貪婪策略:初始化兩指標,使其分列容器兩端,每輪向內收縮短板對應的指標,直至兩指標相遇。
|
||||
|
||||
圖 15-11 展示了貪婪策略的執行過程。
|
||||
|
||||
1. 初始狀態下,指標 $i$ 和 $j$ 分列陣列兩端。
|
||||
2. 計算當前狀態的容量 $cap[i, j]$ ,並更新最大容量。
|
||||
3. 比較板 $i$ 和板 $j$ 的高度,並將短板向內移動一格。
|
||||
4. 迴圈執行第 `2.` 步和第 `3.` 步,直至 $i$ 和 $j$ 相遇時結束。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-11 最大容量問題的貪婪過程 </p>
|
||||
|
||||
### 2. 程式碼實現
|
||||
|
||||
程式碼迴圈最多 $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], ht[j]) * (j - i)
|
||||
res = max(res, cap)
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i++
|
||||
} else {
|
||||
j--
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_capacity.rb"
|
||||
### 最大容量:貪婪 ###
|
||||
def max_capacity(ht)
|
||||
# 初始化 i, j,使其分列陣列兩端
|
||||
i, j = 0, ht.length - 1
|
||||
# 初始最大容量為 0
|
||||
res = 0
|
||||
|
||||
# 迴圈貪婪選擇,直至兩板相遇
|
||||
while i < j
|
||||
# 更新最大容量
|
||||
cap = [ht[i], ht[j]].min * (j - i)
|
||||
res = [res, cap].max
|
||||
# 向內移動短板
|
||||
if ht[i] < ht[j]
|
||||
i += 1
|
||||
else
|
||||
j -= 1
|
||||
end
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20i%2C%20j%EF%BC%8C%E4%BD%BF%E5%85%B6%E5%88%86%E5%88%97%E9%99%A3%E5%88%97%E5%85%A9%E7%AB%AF%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E7%82%BA%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E8%B2%AA%E5%A9%AA%E9%81%B8%E6%93%87%EF%BC%8C%E7%9B%B4%E8%87%B3%E5%85%A9%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%2C%20ht%5Bj%5D%29%20%2A%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res%2C%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E5%90%91%E5%85%A7%E7%A7%BB%E5%8B%95%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%2C%208%2C%205%2C%202%2C%207%2C%207%2C%203%2C%204%5D%0A%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%E6%BC%94%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%E7%82%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%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20i%2C%20j%EF%BC%8C%E4%BD%BF%E5%85%B6%E5%88%86%E5%88%97%E9%99%A3%E5%88%97%E5%85%A9%E7%AB%AF%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E7%82%BA%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E8%B2%AA%E5%A9%AA%E9%81%B8%E6%93%87%EF%BC%8C%E7%9B%B4%E8%87%B3%E5%85%A9%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%2C%20ht%5Bj%5D%29%20%2A%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res%2C%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E5%90%91%E5%85%A7%E7%A7%BB%E5%8B%95%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%2C%208%2C%205%2C%202%2C%207%2C%207%2C%203%2C%204%5D%0A%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%E6%BC%94%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%E7%82%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. 正確性證明
|
||||
|
||||
之所以貪婪比窮舉更快,是因為每輪的貪婪選擇都會“跳過”一些狀態。
|
||||
|
||||
比如在狀態 $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]
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-12 移動短板導致被跳過的狀態 </p>
|
||||
|
||||
觀察發現,**這些被跳過的狀態實際上就是將長板 $j$ 向內移動的所有狀態**。前面我們已經證明內移長板一定會導致容量變小。也就是說,被跳過的狀態都不可能是最優解,**跳過它們不會導致錯過最優解**。
|
||||
|
||||
以上分析說明,移動短板的操作是“安全”的,貪婪策略是有效的。
|
||||
@@ -0,0 +1,411 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.4 最大切分乘積問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個正整數 $n$ ,將其切分為至少兩個正整數的和,求切分後所有整數的乘積最大是多少,如圖 15-13 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-13 最大切分乘積的問題定義 </p>
|
||||
|
||||
假設我們將 $n$ 切分為 $m$ 個整數因子,其中第 $i$ 個因子記為 $n_i$ ,即
|
||||
|
||||
$$
|
||||
n = \sum_{i=1}^{m}n_i
|
||||
$$
|
||||
|
||||
本題的目標是求得所有整數因子的最大乘積,即
|
||||
|
||||
$$
|
||||
\max(\prod_{i=1}^{m}n_i)
|
||||
$$
|
||||
|
||||
我們需要思考的是:切分數量 $m$ 應該多大,每個 $n_i$ 應該是多少?
|
||||
|
||||
### 1. 貪婪策略確定
|
||||
|
||||
根據經驗,兩個整數的乘積往往比它們的加和更大。假設從 $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$ 這三種因子。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-14 切分導致乘積變大 </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$ ,從而獲得更大的乘積。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-15 最優切分因子 </p>
|
||||
|
||||
綜上所述,可推理出以下貪婪策略。
|
||||
|
||||
1. 輸入整數 $n$ ,從其不斷地切分出因子 $3$ ,直至餘數為 $0$、$1$、$2$ 。
|
||||
2. 當餘數為 $0$ 時,代表 $n$ 是 $3$ 的倍數,因此不做任何處理。
|
||||
3. 當餘數為 $2$ 時,不繼續劃分,保留。
|
||||
4. 當餘數為 $1$ 時,由於 $2 \times 2 > 1 \times 3$ ,因此應將最後一個 $3$ 替換為 $2$ 。
|
||||
|
||||
### 2. 程式碼實現
|
||||
|
||||
如圖 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)).toInt() * 2 * 2
|
||||
}
|
||||
if (b == 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return 3.0.pow(a).toInt() * 2 * 2
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return 3.0.pow(a).toInt()
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_product_cutting.rb"
|
||||
### 最大切分乘積:貪婪 ###
|
||||
def max_product_cutting(n)
|
||||
# 當 n <= 3 時,必須切分出一個 1
|
||||
return 1 * (n - 1) if n <= 3
|
||||
# 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
a, b = n / 3, n % 3
|
||||
# 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return (3.pow(a - 1) * 2 * 2).to_i if b == 1
|
||||
# 當餘數為 2 時,不做處理
|
||||
return (3.pow(a) * 2).to_i if b == 2
|
||||
# 當餘數為 0 時,不做處理
|
||||
3.pow(a).to_i
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%A9%8D%EF%BC%9A%E8%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E7%95%B6%20n%20%3C%3D%203%20%E6%99%82%EF%BC%8C%E5%BF%85%E9%A0%88%E5%88%87%E5%88%86%E5%87%BA%E4%B8%80%E5%80%8B%201%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20%2A%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%E5%9C%B0%E5%88%87%E5%88%86%E5%87%BA%203%20%EF%BC%8Ca%20%E7%82%BA%203%20%E7%9A%84%E5%80%8B%E6%95%B8%EF%BC%8Cb%20%E7%82%BA%E9%A4%98%E6%95%B8%0A%20%20%20%20a%2C%20b%20%3D%20n%20//%203%2C%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%95%B6%E9%A4%98%E6%95%B8%E7%82%BA%201%20%E6%99%82%EF%BC%8C%E5%B0%87%E4%B8%80%E5%B0%8D%201%20%2A%203%20%E8%BD%89%E5%8C%96%E7%82%BA%202%20%2A%202%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%20-%201%29%29%20%2A%202%20%2A%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%95%B6%E9%A4%98%E6%95%B8%E7%82%BA%202%20%E6%99%82%EF%BC%8C%E4%B8%8D%E5%81%9A%E8%99%95%E7%90%86%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%20%2A%202%0A%20%20%20%20%23%20%E7%95%B6%E9%A4%98%E6%95%B8%E7%82%BA%200%20%E6%99%82%EF%BC%8C%E4%B8%8D%E5%81%9A%E8%99%95%E7%90%86%0A%20%20%20%20return%20int%28math.pow%283%2C%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%B2%AA%E5%A9%AA%E6%BC%94%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%A9%8D%E7%82%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%A9%8D%EF%BC%9A%E8%B2%AA%E5%A9%AA%22%22%22%0A%20%20%20%20%23%20%E7%95%B6%20n%20%3C%3D%203%20%E6%99%82%EF%BC%8C%E5%BF%85%E9%A0%88%E5%88%87%E5%88%86%E5%87%BA%E4%B8%80%E5%80%8B%201%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20%2A%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B2%AA%E5%A9%AA%E5%9C%B0%E5%88%87%E5%88%86%E5%87%BA%203%20%EF%BC%8Ca%20%E7%82%BA%203%20%E7%9A%84%E5%80%8B%E6%95%B8%EF%BC%8Cb%20%E7%82%BA%E9%A4%98%E6%95%B8%0A%20%20%20%20a%2C%20b%20%3D%20n%20//%203%2C%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%95%B6%E9%A4%98%E6%95%B8%E7%82%BA%201%20%E6%99%82%EF%BC%8C%E5%B0%87%E4%B8%80%E5%B0%8D%201%20%2A%203%20%E8%BD%89%E5%8C%96%E7%82%BA%202%20%2A%202%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%20-%201%29%29%20%2A%202%20%2A%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%95%B6%E9%A4%98%E6%95%B8%E7%82%BA%202%20%E6%99%82%EF%BC%8C%E4%B8%8D%E5%81%9A%E8%99%95%E7%90%86%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%20%2A%202%0A%20%20%20%20%23%20%E7%95%B6%E9%A4%98%E6%95%B8%E7%82%BA%200%20%E6%99%82%EF%BC%8C%E4%B8%8D%E5%81%9A%E8%99%95%E7%90%86%0A%20%20%20%20return%20int%28math.pow%283%2C%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%B2%AA%E5%A9%AA%E6%BC%94%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%A9%8D%E7%82%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>
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-16 最大切分乘積的計算方法 </p>
|
||||
|
||||
**時間複雜度取決於程式語言的冪運算的實現方法**。以 Python 為例,常用的冪計算函式有三種。
|
||||
|
||||
- 運算子 `**` 和函式 `pow()` 的時間複雜度均為 $O(\log a)$ 。
|
||||
- 函式 `math.pow()` 內部呼叫 C 語言庫的 `pow()` 函式,其執行浮點取冪,時間複雜度為 $O(1)$ 。
|
||||
|
||||
變數 $a$ 和 $b$ 使用常數大小的額外空間,**因此空間複雜度為 $O(1)$** 。
|
||||
|
||||
### 3. 正確性證明
|
||||
|
||||
使用反證法,只分析 $n \geq 4$ 的情況。
|
||||
|
||||
1. **所有因子 $\leq 3$** :假設最優切分方案中存在 $\geq 4$ 的因子 $x$ ,那麼一定可以將其繼續劃分為 $2(x-2)$ ,從而獲得更大(或相等)的乘積。這與假設矛盾。
|
||||
2. **切分方案不包含 $1$** :假設最優切分方案中存在一個因子 $1$ ,那麼它一定可以合併入另外一個因子中,以獲得更大的乘積。這與假設矛盾。
|
||||
3. **切分方案最多包含兩個 $2$** :假設最優切分方案中包含三個 $2$ ,那麼一定可以替換為兩個 $3$ ,乘積更大。這與假設矛盾。
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 貪婪演算法通常用於解決最最佳化問題,其原理是在每個決策階段都做出區域性最優的決策,以期獲得全域性最優解。
|
||||
- 貪婪演算法會迭代地做出一個又一個的貪婪選擇,每輪都將問題轉化成一個規模更小的子問題,直到問題被解決。
|
||||
- 貪婪演算法不僅實現簡單,還具有很高的解題效率。相比於動態規劃,貪婪演算法的時間複雜度通常更低。
|
||||
- 在零錢兌換問題中,對於某些硬幣組合,貪婪演算法可以保證找到最優解;對於另外一些硬幣組合則不然,貪婪演算法可能找到很差的解。
|
||||
- 適合用貪婪演算法求解的問題具有兩大性質:貪婪選擇性質和最優子結構。貪婪選擇性質代表貪婪策略的有效性。
|
||||
- 對於某些複雜問題,貪婪選擇性質的證明並不簡單。相對來說,證偽更加容易,例如零錢兌換問題。
|
||||
- 求解貪婪問題主要分為三步:問題分析、確定貪婪策略、正確性證明。其中,確定貪婪策略是核心步驟,正確性證明往往是難點。
|
||||
- 分數背包問題在 0-1 背包的基礎上,允許選擇物品的一部分,因此可使用貪婪演算法求解。貪婪策略的正確性可以使用反證法來證明。
|
||||
- 最大容量問題可使用窮舉法求解,時間複雜度為 $O(n^2)$ 。透過設計貪婪策略,每輪向內移動短板,可將時間複雜度最佳化至 $O(n)$ 。
|
||||
- 在最大切分乘積問題中,我們先後推理出兩個貪婪策略:$\geq 4$ 的整數都應該繼續切分,最優切分因子為 $3$ 。程式碼中包含冪運算,時間複雜度取決於冪運算實現方法,通常為 $O(1)$ 或 $O(\log n)$ 。
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 6.5 練習
|
||||
|
||||
## 6.5.1 知識鞏固
|
||||
|
||||
### 1. 發生雜湊衝突後怎樣查詢
|
||||
|
||||
一個雜湊表有 5 個桶,雜湊函式為 $h(x)=x \bmod 5$,衝突時把元素依次放進該桶的串列中。
|
||||
依次插入 `[1, 6, 11, 7]`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 寫出 0~4 號桶中的內容;
|
||||
2. 查詢 6 時會先進入哪個桶,並依次檢查哪些元素?
|
||||
3. 根據第 1 問寫出的桶內容,後插入的元素是否覆蓋了先插入的元素?結合這種衝突處理方式說明理由。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 因為 $1\bmod5=6\bmod5=11\bmod5=1$,而 $7\bmod5=2$,各桶為:
|
||||
|
||||
```text
|
||||
0: []
|
||||
1: [1, 6, 11]
|
||||
2: [7]
|
||||
3: []
|
||||
4: []
|
||||
```
|
||||
|
||||
2. 查詢 6 時先進入 1 號桶,再依次比較 1、6,第二次比較時找到目標。
|
||||
|
||||
3. 雜湊值相同只表示它們落入同一個桶,並不表示這些元素相等。鏈式位址把衝突元素都儲存在桶內,
|
||||
查詢時再逐個比較,因此 1、6、11 不會互相覆蓋。
|
||||
|
||||
### 2. 雜湊表擴容後元素去哪兒
|
||||
|
||||
一個使用鏈式位址的雜湊表原有 5 個桶,雜湊函式為 $h(x)=x\bmod5$。
|
||||
鍵 `[1, 6, 11]` 都在 1 號桶中。
|
||||
|
||||
現在把雜湊表擴容為 7 個桶,雜湊函式相應變為 $h(x)=x\bmod7$:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 分別計算 1、6、11 的新桶號。
|
||||
2. 擴容後哪些桶中有元素?
|
||||
3. 擴容時能否把原來 1 號桶中的串列原樣複製到新的 1 號桶?結合第 1、2 問的結果說明理由。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 新桶號分別為:
|
||||
|
||||
- $1\bmod7=1$;
|
||||
- $6\bmod7=6$;
|
||||
- $11\bmod7=4$。
|
||||
|
||||
2. 1 號桶儲存 1,4 號桶儲存 11,6 號桶儲存 6。三個鍵不再擠在同一個桶中。
|
||||
|
||||
3. 不能原樣複製。桶號由“鍵對桶數取餘”得到。桶數從 5 變成 7 後,同一個鍵的新桶號可能改變,
|
||||
因此必須重新計算每個鍵的位置。若把舊的 1 號桶原樣複製過去,之後按新公式查詢 6 和 11 時,
|
||||
會分別前往 6 號桶和 4 號桶,從而找不到它們。
|
||||
|
||||
### 3. 刪除 6 後還能找到 11 嗎
|
||||
|
||||
一個雜湊表有 5 個位置,索引為 `0~4`,雜湊函式為 $h(x)=x\bmod5$。
|
||||
發生衝突時,從雜湊函式算出的索引開始,向右尋找第一個空位。
|
||||
|
||||
依次插入 `[1, 6, 11]`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 三個數最終分別放在哪個索引?
|
||||
2. 查詢 11 時,會依次檢查哪些索引?
|
||||
3. 如果刪除 6 時直接把它的位置改成“從未使用的空位”,而查詢遇到空位就停止,
|
||||
再查詢 11 時會發生什麼?這個查詢結果是否正確?如果有問題,應怎樣避免?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 1 放在索引 1。6 也對映到索引 1,發生衝突後放在索引 2。
|
||||
11 同樣從索引 1 開始,依次跳過已佔用的索引 1、2,最終放在索引 3。
|
||||
|
||||
2. 查詢 11 時依次檢查索引 `1、2、3`,在索引 3 找到它。
|
||||
|
||||
3. 如果把索引 2 改成表示“從未使用”的空位,查詢 11 時檢查索引 1 後就會在索引 2 停止,
|
||||
從而錯誤地認為 11 不存在。刪除時應留下“已刪除”標記:
|
||||
查詢遇到該標記時繼續檢查下一個索引(越過索引 4 後回到索引 0),而以後的插入仍可重新使用這個位置。
|
||||
|
||||
## 6.5.2 程式設計練習
|
||||
|
||||
### 1. 比較兩個字串的字元組成
|
||||
|
||||
給定兩個只含小寫英文字母的字串 `s` 和 `t`。
|
||||
可以任意調整 `s` 中字元的位置,但不能新增、刪除或替換字元。
|
||||
|
||||
請判斷調整後能否得到 `t`;可以則返回 `true`,否則返回 `false`。
|
||||
請使用雜湊表記錄各字母的出現次數,不對字串中的字元排序。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 兩個字串長度不同,它們的字元組成一定不同
|
||||
2. 用雜湊表記錄每種字母的數量;掃描 s 時把對應計數加 1
|
||||
3. 掃描 t 時把對應計數減 1;所有計數最終都為 0,字元組成才相同
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/valid-anagram/solutions/2362065/242-you-xiao-de-zi-mu-yi-wei-ci-ha-xi-bi-cch7/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Executable
+1788
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/table-search
|
||||
---
|
||||
|
||||
# 第 6 章 雜湊表
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
在計算機世界中,雜湊表如同一位聰慧的圖書管理員。
|
||||
|
||||
他知道如何計算索書號,從而可以快速找到目標圖書。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [6.1 雜湊表](hash_map.md)
|
||||
- [6.2 雜湊衝突](hash_collision.md)
|
||||
- [6.3 雜湊演算法](hash_algorithm.md)
|
||||
- [6.4 小結](summary.md)
|
||||
- [6.5 練習](exercises.md)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 6.4 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 輸入 `key` ,雜湊表能夠在 $O(1)$ 時間內查詢到 `value` ,效率非常高。
|
||||
- 常見的雜湊表操作包括查詢、新增鍵值對、刪除鍵值對和走訪雜湊表等。
|
||||
- 雜湊函式將 `key` 對映為陣列索引,從而訪問對應桶並獲取 `value` 。
|
||||
- 兩個不同的 `key` 可能在經過雜湊函式後得到相同的陣列索引,導致查詢結果出錯,這種現象被稱為雜湊衝突。
|
||||
- 雜湊表容量越大,雜湊衝突的機率就越低。因此可以透過擴容雜湊表來緩解雜湊衝突。與陣列擴容類似,雜湊表擴容操作的開銷很大。
|
||||
- 負載因子定義為雜湊表中元素數量除以桶數量,反映了雜湊衝突的嚴重程度,常用作觸發雜湊表擴容的條件。
|
||||
- 鏈式位址透過將單個元素轉化為鏈結串列,將所有衝突元素儲存在同一個鏈結串列中。然而,鏈結串列過長會降低查詢效率,可以透過進一步將鏈結串列轉換為紅黑樹來提高效率。
|
||||
- 開放定址透過多次探測來處理雜湊衝突。線性探查使用固定步長,缺點是不能刪除元素,且容易產生聚集。多次雜湊使用多個雜湊函式進行探測,相較線性探查更不易產生聚集,但多個雜湊函式增加了計算量。
|
||||
- 不同程式語言採取了不同的雜湊表實現。例如,Java 的 `HashMap` 使用鏈式位址,而 Python 的 `Dict` 採用開放定址。
|
||||
- 在雜湊表中,我們希望雜湊演算法具有確定性、高效率和均勻分佈的特點。在密碼學中,雜湊演算法還應該具備抗碰撞性和雪崩效應。
|
||||
- 雜湊演算法通常採用大質數作為模數,以最大化地保證雜湊值均勻分佈,減少雜湊衝突。
|
||||
- 常見的雜湊演算法包括 MD5、SHA-1、SHA-2 和 SHA-3 等。MD5 常用於校驗檔案完整性,SHA-2 常用於安全應用與協議。
|
||||
- 程式語言通常會為資料型別提供內建雜湊演算法,用於計算雜湊表中的桶索引。通常情況下,只有不可變物件是可雜湊的。
|
||||
|
||||
### 2. 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` ,在擴容後可能會被分配到多個桶中,從而實現雜湊衝突的緩解。
|
||||
|
||||
**Q**:如果為了高效的存取,那麼直接使用陣列不就好了嗎?
|
||||
|
||||
當資料的 `key` 是連續的小範圍整數時,直接用陣列即可,簡單高效。但當 `key` 是其他型別(例如字串)時,就需要藉助雜湊函式將 `key` 對映為陣列索引,再透過桶陣列儲存元素,這樣的結構就是雜湊表。
|
||||
@@ -0,0 +1,376 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 8.2 建堆積操作
|
||||
|
||||
在某些情況下,我們希望使用一個串列的所有元素來構建一個堆積,這個過程被稱為“建堆積操作”。
|
||||
|
||||
## 8.2.1 藉助入堆積操作實現
|
||||
|
||||
我們首先建立一個空堆積,然後走訪串列,依次對每個元素執行“入堆積操作”,即先將元素新增至堆積的尾部,再對該元素執行“從底至頂”堆積化。
|
||||
|
||||
每當一個元素入堆積,堆積的長度就加一。由於節點是從頂到底依次被新增進二元樹的,因此堆積是“自上而下”構建的。
|
||||
|
||||
設元素數量為 $n$ ,每個元素的入堆積操作使用 $O(\log{n})$ 時間,因此該建堆積方法的時間複雜度為 $O(n \log n)$ 。
|
||||
|
||||
## 8.2.2 透過走訪堆積化實現
|
||||
|
||||
實際上,我們可以實現一種更為高效的建堆積方法,共分為兩步。
|
||||
|
||||
1. 將串列所有元素原封不動地新增到堆積中,此時堆積的性質尚未得到滿足。
|
||||
2. 倒序走訪堆積(層序走訪的倒序),依次對每個非葉節點執行“從頂至底堆積化”。
|
||||
|
||||
**每當堆積化一個節點後,以該節點為根節點的子樹就形成一個合法的子堆積**。而由於是倒序走訪,因此堆積是“自下而上”構建的。
|
||||
|
||||
之所以選擇倒序走訪,是因為這樣能夠保證當前節點之下的子樹已經是合法的子堆積,這樣堆積化當前節點才是有效的。
|
||||
|
||||
值得說明的是,**由於葉節點沒有子節點,因此它們天然就是合法的子堆積,無須堆積化**。如以下程式碼所示,最後一個非葉節點是最後一個節點的父節點,我們從它開始倒序走訪並執行堆積化:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="my_heap.py"
|
||||
def __init__(self, nums: list[int]):
|
||||
"""建構子,根據輸入串列建堆積"""
|
||||
# 將串列元素原封不動新增進堆積
|
||||
self.max_heap = nums
|
||||
# 堆積化除葉節點以外的其他所有節點
|
||||
for i in range(self.parent(self.size() - 1), -1, -1):
|
||||
self.sift_down(i)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="my_heap.cpp"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
MaxHeap(vector<int> nums) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
maxHeap = nums;
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (int i = parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="my_heap.java"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
MaxHeap(List<Integer> nums) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
maxHeap = new ArrayList<>(nums);
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (int i = parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="my_heap.cs"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
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: MutableList<Int>?) {
|
||||
// 使用串列而非陣列,這樣無須考慮擴容問題
|
||||
private val maxHeap = mutableListOf<Int>()
|
||||
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
init {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
maxHeap.addAll(nums!!)
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
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) {
|
||||
val temp = maxHeap[i]
|
||||
maxHeap[i] = maxHeap[j]
|
||||
maxHeap[j] = temp
|
||||
}
|
||||
|
||||
/* 獲取堆積大小 */
|
||||
fun size(): Int {
|
||||
return maxHeap.size
|
||||
}
|
||||
|
||||
/* 判斷堆積是否為空 */
|
||||
fun isEmpty(): Boolean {
|
||||
/* 判斷堆積是否為空 */
|
||||
return size() == 0
|
||||
}
|
||||
|
||||
/* 訪問堆積頂元素 */
|
||||
fun peek(): Int {
|
||||
return maxHeap[0]
|
||||
}
|
||||
|
||||
/* 元素入堆積 */
|
||||
fun push(_val: Int) {
|
||||
// 新增節點
|
||||
maxHeap.add(_val)
|
||||
// 從底至頂堆積化
|
||||
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 _val = maxHeap.removeAt(size() - 1)
|
||||
// 從頂至底堆積化
|
||||
siftDown(0)
|
||||
// 返回堆積頂元素
|
||||
return _val
|
||||
}
|
||||
|
||||
/* 從節點 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"
|
||||
### 建構子,根據輸入串列建堆積 ###
|
||||
def initialize(nums)
|
||||
# 將串列元素原封不動新增進堆積
|
||||
@max_heap = nums
|
||||
# 堆積化除葉節點以外的其他所有節點
|
||||
parent(size - 1).downto(0) do |i|
|
||||
sift_down(i)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%A0%82%E5%A0%86%E7%A9%8D%22%22%22%0A%0A%20%20%20%20def%20__init__%28self%2C%20nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%BB%BA%E6%A7%8B%E5%AD%90%EF%BC%8C%E6%A0%B9%E6%93%9A%E8%BC%B8%E5%85%A5%E4%B8%B2%E5%88%97%E5%BB%BA%E5%A0%86%E7%A9%8D%22%22%22%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%87%E4%B8%B2%E5%88%97%E5%85%83%E7%B4%A0%E5%8E%9F%E5%B0%81%E4%B8%8D%E5%8B%95%E6%96%B0%E5%A2%9E%E9%80%B2%E5%A0%86%E7%A9%8D%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%E7%A9%8D%E5%8C%96%E9%99%A4%E8%91%89%E7%AF%80%E9%BB%9E%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E7%AF%80%E9%BB%9E%0A%20%20%20%20%20%20%20%20for%20i%20in%20range%28self.parent%28self.size%28%29%20-%201%29%2C%20-1%2C%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%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E7%8D%B2%E5%8F%96%E5%B7%A6%E5%AD%90%E7%AF%80%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20%2A%20i%20%2B%201%0A%0A%20%20%20%20def%20right%28self%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E7%8D%B2%E5%8F%96%E5%8F%B3%E5%AD%90%E7%AF%80%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20%2A%20i%20%2B%202%0A%0A%20%20%20%20def%20parent%28self%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E7%8D%B2%E5%8F%96%E7%88%B6%E7%AF%80%E9%BB%9E%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%2C%20i%3A%20int%2C%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BA%A4%E6%8F%9B%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20self.max_heap%5Bi%5D%2C%20self.max_heap%5Bj%5D%20%3D%20self.max_heap%5Bj%5D%2C%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%E7%8D%B2%E5%8F%96%E5%A0%86%E7%A9%8D%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%2C%20i%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%BE%9E%E7%AF%80%E9%BB%9E%20i%20%E9%96%8B%E5%A7%8B%EF%BC%8C%E5%BE%9E%E9%A0%82%E8%87%B3%E5%BA%95%E5%A0%86%E7%A9%8D%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%B7%E7%AF%80%E9%BB%9E%20i%2C%20l%2C%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E7%AF%80%E9%BB%9E%EF%BC%8C%E8%A8%98%E7%82%BA%20ma%0A%20%20%20%20%20%20%20%20%20%20%20%20l%2C%20r%2C%20ma%20%3D%20self.left%28i%29%2C%20self.right%28i%29%2C%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%E7%AF%80%E9%BB%9E%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l%2C%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%89%87%E7%84%A1%E9%A0%88%E7%B9%BC%E7%BA%8C%E5%A0%86%E7%A9%8D%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%8F%9B%E5%85%A9%E7%AF%80%E9%BB%9E%0A%20%20%20%20%20%20%20%20%20%20%20%20self.swap%28i%2C%20ma%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E5%90%91%E4%B8%8B%E5%A0%86%E7%A9%8D%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%A0%82%E5%A0%86%E7%A9%8D%0A%20%20%20%20max_heap%20%3D%20MaxHeap%28%5B1%2C%202%2C%203%2C%204%2C%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%A0%82%E5%A0%86%E7%A9%8D%22%22%22%0A%0A%20%20%20%20def%20__init__%28self%2C%20nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%BB%BA%E6%A7%8B%E5%AD%90%EF%BC%8C%E6%A0%B9%E6%93%9A%E8%BC%B8%E5%85%A5%E4%B8%B2%E5%88%97%E5%BB%BA%E5%A0%86%E7%A9%8D%22%22%22%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%87%E4%B8%B2%E5%88%97%E5%85%83%E7%B4%A0%E5%8E%9F%E5%B0%81%E4%B8%8D%E5%8B%95%E6%96%B0%E5%A2%9E%E9%80%B2%E5%A0%86%E7%A9%8D%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%E7%A9%8D%E5%8C%96%E9%99%A4%E8%91%89%E7%AF%80%E9%BB%9E%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E7%AF%80%E9%BB%9E%0A%20%20%20%20%20%20%20%20for%20i%20in%20range%28self.parent%28self.size%28%29%20-%201%29%2C%20-1%2C%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%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E7%8D%B2%E5%8F%96%E5%B7%A6%E5%AD%90%E7%AF%80%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20%2A%20i%20%2B%201%0A%0A%20%20%20%20def%20right%28self%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E7%8D%B2%E5%8F%96%E5%8F%B3%E5%AD%90%E7%AF%80%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20%2A%20i%20%2B%202%0A%0A%20%20%20%20def%20parent%28self%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E7%8D%B2%E5%8F%96%E7%88%B6%E7%AF%80%E9%BB%9E%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%2C%20i%3A%20int%2C%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BA%A4%E6%8F%9B%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20self.max_heap%5Bi%5D%2C%20self.max_heap%5Bj%5D%20%3D%20self.max_heap%5Bj%5D%2C%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%E7%8D%B2%E5%8F%96%E5%A0%86%E7%A9%8D%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%2C%20i%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%BE%9E%E7%AF%80%E9%BB%9E%20i%20%E9%96%8B%E5%A7%8B%EF%BC%8C%E5%BE%9E%E9%A0%82%E8%87%B3%E5%BA%95%E5%A0%86%E7%A9%8D%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%B7%E7%AF%80%E9%BB%9E%20i%2C%20l%2C%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E7%AF%80%E9%BB%9E%EF%BC%8C%E8%A8%98%E7%82%BA%20ma%0A%20%20%20%20%20%20%20%20%20%20%20%20l%2C%20r%2C%20ma%20%3D%20self.left%28i%29%2C%20self.right%28i%29%2C%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%E7%AF%80%E9%BB%9E%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l%2C%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%89%87%E7%84%A1%E9%A0%88%E7%B9%BC%E7%BA%8C%E5%A0%86%E7%A9%8D%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%8F%9B%E5%85%A9%E7%AF%80%E9%BB%9E%0A%20%20%20%20%20%20%20%20%20%20%20%20self.swap%28i%2C%20ma%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E5%90%91%E4%B8%8B%E5%A0%86%E7%A9%8D%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%A0%82%E5%A0%86%E7%A9%8D%0A%20%20%20%20max_heap%20%3D%20MaxHeap%28%5B1%2C%202%2C%203%2C%204%2C%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 複雜度分析
|
||||
|
||||
下面,我們來嘗試推算第二種建堆積方法的時間複雜度。
|
||||
|
||||
- 假設完全二元樹的節點數量為 $n$ ,則葉節點數量為 $(n + 1) / 2$ ,其中 $/$ 為向下整除。因此需要堆積化的節點數量為 $(n - 1) / 2$ 。
|
||||
- 在從頂至底堆積化的過程中,每個節點最多堆積化到葉節點,因此最大迭代次數為二元樹高度 $\log n$ 。
|
||||
|
||||
將上述兩者相乘,可得到建堆積過程的時間複雜度為 $O(n \log n)$ 。**但這個估算結果並不準確,因為我們沒有考慮到二元樹底層節點數量遠多於頂層節點的性質**。
|
||||
|
||||
接下來我們來進行更為準確的計算。為了降低計算難度,假設給定一個節點數量為 $n$ 、高度為 $h$ 的“完美二元樹”,該假設不會影響計算結果的正確性。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 8-5 完美二元樹的各層節點數量 </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)$ ,非常高效**。
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 8.5 練習
|
||||
|
||||
## 8.5.1 知識鞏固
|
||||
|
||||
### 1. 數字 10 入堆積後怎樣調整
|
||||
|
||||
陣列 `[9, 7, 8, 3, 5]` 表示一個大頂堆積。現在將數字 10 入堆積。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 先把 10 新增到陣列末尾,它的父節點值是多少?
|
||||
2. 從新節點開始向上堆積化,寫出每次交換後的陣列。
|
||||
3. 最終的堆積頂元素是什麼?一共交換了幾次?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 10 新增後的索引為 5,其父節點索引為
|
||||
$\lfloor(5-1)/2\rfloor=2$,父節點值為 8。
|
||||
|
||||
2. 10 大於 8,第一次交換後為 `[9, 7, 10, 3, 5, 8]`;
|
||||
10 又大於父節點 9,第二次交換後為 `[10, 7, 9, 3, 5, 8]`。
|
||||
此時 10 已到達根節點,堆積化結束。
|
||||
|
||||
3. 最終堆積頂為 10,共交換 2 次。
|
||||
|
||||
### 2. 檢查小頂堆積的父子關係
|
||||
|
||||
陣列 `[1, 4, 3, 7, 6, 2]` 表示一棵完全二元樹。小頂堆積要求每個父節點都不大於它的孩子。
|
||||
對索引 $i$,左、右孩子索引分別為 $2i+1$ 和 $2i+2$。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 索引 2 的孩子索引和值分別是什麼?
|
||||
2. 索引 2 處的節點值為 3,它與孩子節點之間是否違反小頂堆積規則?如果違反,應交換哪兩個元素?
|
||||
3. 根據第 2 問的判斷:若違反規則,寫出交換後的陣列;若不違反,說明無須交換。最後檢查其餘父子關係。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 索引 2 的左孩子索引是 5、值為 2;右孩子索引是 6,但陣列長度為 6,因此右孩子不存在。
|
||||
|
||||
2. 父節點值 3 大於孩子值 2,違反小頂堆積規則,應交換索引 2 和索引 5 的元素。
|
||||
|
||||
3. 交換後得到 `[1, 4, 2, 7, 6, 3]`。逐一檢查:
|
||||
`1 ≤ 4`、`1 ≤ 2`;`4 ≤ 7`、`4 ≤ 6`;`2 ≤ 3`。
|
||||
所有父節點都不大於自己的孩子,因此現在滿足小頂堆積規則。
|
||||
|
||||
### 3. 用小頂堆積保留最大的三個數
|
||||
|
||||
要從資料流 `[4, 1, 7, 3, 8]` 中保留最大的 3 個數,可以維護一個大小不超過 3 的小頂堆積。
|
||||
|
||||
先把前 3 個數依次放入小頂堆積。堆積滿後,每讀入一個新數:
|
||||
如果它大於堆積頂,就刪除堆積頂並放入新數;否則堆積保持不變。
|
||||
|
||||
請寫出每次讀入數字後堆積中保留的數字和堆積頂。
|
||||
只需把保留的數字寫成集合,不要求寫出它們在堆積陣列中的排列。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
每次讀入數字後的結果如下:
|
||||
|
||||
| 讀入數字 | 保留的數字 | 堆積頂 |
|
||||
| --- | --- | --- |
|
||||
| 4 | `{4}` | 4 |
|
||||
| 1 | `{1, 4}` | 1 |
|
||||
| 7 | `{1, 4, 7}` | 1 |
|
||||
| 3 | `{3, 4, 7}` | 3 |
|
||||
| 8 | `{4, 7, 8}` | 4 |
|
||||
|
||||
堆積滿後,堆積頂是當前保留數字中最小的一個。只有新數字大於堆積頂時,
|
||||
才用新數字替換堆積頂。最終保留的 `{4, 7, 8}` 正是最大的 3 個數。
|
||||
|
||||
## 8.5.2 程式設計練習
|
||||
|
||||
### 1. 尋找陣列中的第 k 大元素
|
||||
|
||||
給定整數陣列 `nums` 和整數 $k$,其中 $1 \le k \le n$,$n$ 是陣列長度。把陣列按從大到小排列後,請返回第 $k$ 個位置上的元素。
|
||||
|
||||
重複元素需要分別計數。例如,`[5, 5, 2]` 的第 2 大元素仍是 5。請使用一個大小不超過 $k$ 的小頂堆積完成。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 第 k 大元素是最大的 k 個數中最小的那個
|
||||
2. 每個數先入小頂堆積,大小超過 k 時彈出最小值
|
||||
3. 走訪結束時堆積中保留最大的 k 個數,堆積頂就是答案
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/kth-largest-element-in-an-array/solutions/2361969/215-shu-zu-zhong-de-di-k-ge-zui-da-yuan-d786p/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **說明:** 連結中的題解講解了快速排序和快速選擇,沒有使用堆積;本練習請按照正文 8.3 節使用大小不超過 k 的小頂堆積完成
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/family-tree
|
||||
---
|
||||
|
||||
# 第 8 章 堆積
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
堆積就像是山嶽峰巒,層疊起伏、形態各異。
|
||||
|
||||
座座山峰高低錯落,而最高的山峰總是最先映入眼簾。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [8.1 堆積](heap.md)
|
||||
- [8.2 建堆積操作](build_heap.md)
|
||||
- [8.3 Top-k 問題](top_k.md)
|
||||
- [8.4 小結](summary.md)
|
||||
- [8.5 練習](exercises.md)
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 8.4 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 堆積是一棵完全二元樹,根據成立條件可分為大頂堆積和小頂堆積。大(小)頂堆積的堆積頂元素是最大(小)的。
|
||||
- 優先佇列的定義是具有出列優先順序的佇列,通常使用堆積來實現。
|
||||
- 堆積的常用操作及其對應的時間複雜度包括:元素入堆積 $O(\log n)$、堆積頂元素出堆積 $O(\log n)$ 和訪問堆積頂元素 $O(1)$ 等。
|
||||
- 完全二元樹非常適合用陣列表示,因此我們通常使用陣列來儲存堆積。
|
||||
- 堆積化操作用於維護堆積的性質,在入堆積和出堆積操作中都會用到。
|
||||
- 輸入 $n$ 個元素並建堆積的時間複雜度可以最佳化至 $O(n)$ ,非常高效。
|
||||
- Top-k 是一個經典演算法問題,可以使用堆積資料結構高效解決,時間複雜度為 $O(n \log k)$ 。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:資料結構的“堆積”與記憶體管理的“堆積”是同一個概念嗎?
|
||||
|
||||
兩者不是同一個概念,只是碰巧都叫“堆積”。計算機系統記憶體中的堆積是動態記憶體分配的一部分,程式在執行時可以使用它來儲存資料。程式可以請求一定量的堆積記憶體,用於儲存如物件和陣列等複雜結構。當這些資料不再需要時,程式需要釋放這些記憶體,以防止記憶體流失。相較於堆疊記憶體,堆積記憶體的管理和使用需要更謹慎,使用不當可能會導致記憶體流失和野指標等問題。
|
||||
@@ -0,0 +1,471 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 8.3 Top-k 問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個長度為 $n$ 的無序陣列 `nums` ,請返回陣列中最大的 $k$ 個元素。
|
||||
|
||||
對於該問題,我們先介紹兩種思路比較直接的解法,再介紹效率更高的堆積解法。
|
||||
|
||||
## 8.3.1 方法一:走訪選擇
|
||||
|
||||
我們可以進行圖 8-6 所示的 $k$ 輪走訪,分別在每輪中提取第 $1$、$2$、$\dots$、$k$ 大的元素,時間複雜度為 $O(nk)$ 。
|
||||
|
||||
此方法只適用於 $k \ll n$ 的情況,因為當 $k$ 與 $n$ 比較接近時,其時間複雜度趨向於 $O(n^2)$ ,非常耗時。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 8-6 走訪尋找最大的 k 個元素 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
當 $k = n$ 時,我們可以得到完整的有序序列,此時等價於“選擇排序”演算法。
|
||||
|
||||
## 8.3.2 方法二:排序
|
||||
|
||||
如圖 8-7 所示,我們可以先對陣列 `nums` 進行排序,再返回最右邊的 $k$ 個元素,時間複雜度為 $O(n \log n)$ 。
|
||||
|
||||
顯然,該方法“超額”完成任務了,因為我們只需找出最大的 $k$ 個元素即可,而不需要排序其他元素。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 8-7 排序尋找最大的 k 個元素 </p>
|
||||
|
||||
## 8.3.3 方法三:堆積
|
||||
|
||||
我們可以基於堆積更加高效地解決 Top-k 問題,流程如圖 8-8 所示。
|
||||
|
||||
1. 初始化一個小頂堆積,其堆積頂元素最小。
|
||||
2. 先將陣列的前 $k$ 個元素依次入堆積。
|
||||
3. 從第 $k + 1$ 個元素開始,若當前元素大於堆積頂元素,則將堆積頂元素出堆積,並將當前元素入堆積。
|
||||
4. 走訪完成後,堆積中儲存的就是最大的 $k$ 個元素。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 8-8 基於堆積尋找最大的 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"
|
||||
### 基於堆積查詢陣列中最大的 k 個元素 ###
|
||||
def top_k_heap(nums, k)
|
||||
# 初始化小頂堆積
|
||||
# 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積
|
||||
max_heap = MaxHeap.new([])
|
||||
|
||||
# 將陣列的前 k 個元素入堆積
|
||||
for i in 0...k
|
||||
push_min_heap(max_heap, nums[i])
|
||||
end
|
||||
|
||||
# 從第 k+1 個元素開始,保持堆積的長度為 k
|
||||
for i in k...nums.length
|
||||
# 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
|
||||
if nums[i] > peek_min_heap(max_heap)
|
||||
pop_min_heap(max_heap)
|
||||
push_min_heap(max_heap, nums[i])
|
||||
end
|
||||
end
|
||||
|
||||
get_min_heap(max_heap)
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E6%96%BC%E5%A0%86%E7%A9%8D%E6%9F%A5%E8%A9%A2%E9%99%A3%E5%88%97%E4%B8%AD%E6%9C%80%E5%A4%A7%E7%9A%84%20k%20%E5%80%8B%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%A0%82%E5%A0%86%E7%A9%8D%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E5%B0%87%E9%99%A3%E5%88%97%E7%9A%84%E5%89%8D%20k%20%E5%80%8B%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%E7%A9%8D%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20%23%20%E5%BE%9E%E7%AC%AC%20k%2B1%20%E5%80%8B%E5%85%83%E7%B4%A0%E9%96%8B%E5%A7%8B%EF%BC%8C%E4%BF%9D%E6%8C%81%E5%A0%86%E7%A9%8D%E7%9A%84%E9%95%B7%E5%BA%A6%E7%82%BA%20k%0A%20%20%20%20for%20i%20in%20range%28k%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E7%95%B6%E5%89%8D%E5%85%83%E7%B4%A0%E5%A4%A7%E6%96%BC%E5%A0%86%E7%A9%8D%E9%A0%82%E5%85%83%E7%B4%A0%EF%BC%8C%E5%89%87%E5%B0%87%E5%A0%86%E7%A9%8D%E9%A0%82%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%E7%A9%8D%E3%80%81%E7%95%B6%E5%89%8D%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%E7%A9%8D%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%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%2C%207%2C%206%2C%203%2C%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums%2C%20k%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20heapq%0A%0Adef%20top_k_heap%28nums%3A%20list%5Bint%5D%2C%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E6%96%BC%E5%A0%86%E7%A9%8D%E6%9F%A5%E8%A9%A2%E9%99%A3%E5%88%97%E4%B8%AD%E6%9C%80%E5%A4%A7%E7%9A%84%20k%20%E5%80%8B%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%A0%82%E5%A0%86%E7%A9%8D%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E5%B0%87%E9%99%A3%E5%88%97%E7%9A%84%E5%89%8D%20k%20%E5%80%8B%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%E7%A9%8D%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20%23%20%E5%BE%9E%E7%AC%AC%20k%2B1%20%E5%80%8B%E5%85%83%E7%B4%A0%E9%96%8B%E5%A7%8B%EF%BC%8C%E4%BF%9D%E6%8C%81%E5%A0%86%E7%A9%8D%E7%9A%84%E9%95%B7%E5%BA%A6%E7%82%BA%20k%0A%20%20%20%20for%20i%20in%20range%28k%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E7%95%B6%E5%89%8D%E5%85%83%E7%B4%A0%E5%A4%A7%E6%96%BC%E5%A0%86%E7%A9%8D%E9%A0%82%E5%85%83%E7%B4%A0%EF%BC%8C%E5%89%87%E5%B0%87%E5%A0%86%E7%A9%8D%E9%A0%82%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%E7%A9%8D%E3%80%81%E7%95%B6%E5%89%8D%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%E7%A9%8D%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%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%2C%207%2C%206%2C%203%2C%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums%2C%20k%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
總共執行了 $n$ 輪入堆積和出堆積,堆積的最大長度為 $k$ ,因此時間複雜度為 $O(n \log k)$ 。該方法的效率很高,當 $k$ 較小時,時間複雜度趨向 $O(n)$ ;當 $k$ 較大時,時間複雜度不會超過 $O(n \log n)$ 。
|
||||
|
||||
另外,該方法適用於動態資料流的使用場景。在不斷加入資料時,我們可以持續維護堆積內的元素,從而實現最大的 $k$ 個元素的動態更新。
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/rocket-launch-outline
|
||||
---
|
||||
|
||||
# 序
|
||||
|
||||
幾年前,我在力扣上分享了“劍指 Offer”系列題解,受到了許多讀者的鼓勵和支持。在與讀者交流期間,我最常被問的一個問題是“如何入門演算法”。逐漸地,我對這個問題產生了濃厚的興趣。
|
||||
|
||||
兩眼一抹黑地刷題似乎是最受歡迎的方法,簡單、直接且有效。然而刷題就如同玩“掃雷”遊戲,自學能力強的人能夠順利將地雷逐個排掉,而基礎不足的人很可能被炸得滿頭是包,並在挫折中步步退縮。通讀教材也是一種常見做法,但對於面向求職的人來說,畢業論文、投遞簡歷、準備筆試和面試已經消耗了大部分精力,啃厚重的書往往變成了一項艱鉅的挑戰。
|
||||
|
||||
如果你也面臨類似的困擾,那麼很幸運這本書“找”到了你。本書是我對這個問題給出的答案,即使不是最優解,也至少是一次積極的嘗試。本書雖然不足以讓你直接拿到 Offer,但會引導你探索資料結構與演算法的“知識地圖”,帶你瞭解不同“地雷”的形狀、大小和分佈位置,讓你掌握各種“排雷方法”。有了這些本領,相信你可以更加自如地刷題和閱讀文獻,逐步構建起完整的知識體系。
|
||||
|
||||
我深深贊同費曼教授所言:“Knowledge isn't free. You have to pay attention.”從這個意義上看,這本書並非完全“免費”。為了不辜負你為本書所付出的寶貴“注意力”,我會竭盡所能,投入最大的“注意力”來完成本書的創作。
|
||||
|
||||
本人自知學疏才淺,書中內容雖然已經過一段時間的打磨,但一定仍有許多錯誤,懇請各位老師和同學批評指正。
|
||||
|
||||
{ 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 演算法無處不在
|
||||
|
||||
當我們聽到“演算法”這個詞時,很自然地會想到數學。然而實際上,許多演算法並不涉及複雜數學,而是更多地依賴基本邏輯,這些邏輯在我們的日常生活中處處可見。
|
||||
|
||||
在正式探討演算法之前,有一個有趣的事實值得分享:**你已經在不知不覺中學會了許多演算法,並習慣將它們應用到日常生活中了**。下面我將舉幾個具體的例子來證實這一點。
|
||||
|
||||
**例一:查字典**。在字典裡,每個漢字都對應一個拼音,而字典是按照拼音字母順序排列的。假設我們需要查詢一個拼音首字母為 $r$ 的字,通常會按照圖 1-1 所示的方式實現。
|
||||
|
||||
1. 翻開字典約一半的頁數,檢視該頁的首字母是什麼,假設首字母為 $m$ 。
|
||||
2. 由於在拼音字母表中 $r$ 位於 $m$ 之後,所以排除字典前半部分,查詢範圍縮小到後半部分。
|
||||
3. 不斷重複步驟 `1.` 和步驟 `2.` ,直至找到拼音首字母為 $r$ 的頁碼為止。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 1-1 查字典步驟 </p>
|
||||
|
||||
查字典這個小學生必備技能,實際上就是著名的“二分搜尋”演算法。從資料結構的角度,我們可以把字典視為一個已排序的“陣列”;從演算法的角度,我們可以將上述查字典的一系列操作看作“二分搜尋”。
|
||||
|
||||
**例二:整理撲克**。我們在打牌時,每局都需要整理手中的撲克牌,使其從小到大排列,實現流程如圖 1-2 所示。
|
||||
|
||||
1. 將撲克牌劃分為“有序”和“無序”兩部分,並假設初始狀態下最左 1 張撲克牌已經有序。
|
||||
2. 在無序部分抽出一張撲克牌,插入至有序部分的正確位置;完成後最左 2 張撲克已經有序。
|
||||
3. 不斷迴圈步驟 `2.` ,每一輪將一張撲克牌從無序部分插入至有序部分,直至所有撲克牌都有序。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 1-2 撲克排序步驟 </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$ 元。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 1-3 貨幣找零過程 </p>
|
||||
|
||||
在以上步驟中,我們每一步都採取當前看來最好的選擇(儘可能用大面額的貨幣),最終得到了可行的找零方案。從資料結構與演算法的角度看,這種方法本質上是“貪婪”演算法。
|
||||
|
||||
小到烹飪一道菜,大到星際航行,幾乎所有問題的解決都離不開演算法。計算機的出現使得我們能夠透過程式設計將資料結構儲存在記憶體中,同時編寫程式碼呼叫 CPU 和 GPU 執行演算法。這樣一來,我們就能把生活中的問題轉移到計算機上,以更高效的方式解決各種複雜問題。
|
||||
|
||||
!!! tip
|
||||
|
||||
如果你對資料結構、演算法、陣列和二分搜尋等概念仍感到一知半解,請繼續往下閱讀,本書將引導你邁入資料結構與演算法的知識殿堂。
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/calculator-variant-outline
|
||||
---
|
||||
|
||||
# 第 1 章 初識演算法
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
一位少女翩翩起舞,與資料交織在一起,裙襬上飄揚著演算法的旋律。
|
||||
|
||||
她邀請你共舞,請緊跟她的步伐,踏入充滿邏輯與美感的演算法世界。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [1.1 演算法無處不在](algorithms_are_everywhere.md)
|
||||
- [1.2 演算法是什麼](what_is_dsa.md)
|
||||
- [1.3 小結](summary.md)
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 1.3 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 演算法在日常生活中無處不在,並不是遙不可及的高深知識。實際上,我們已經在不知不覺中學會了許多演算法,用以解決生活中的大小問題。
|
||||
- 查字典的原理與二分搜尋演算法相一致。二分搜尋演算法體現了分而治之的重要演算法思想。
|
||||
- 整理撲克的過程與插入排序演算法非常類似。插入排序演算法適合排序小型資料集。
|
||||
- 貨幣找零的步驟本質上是貪婪演算法,每一步都採取當前看來最好的選擇。
|
||||
- 演算法是在有限時間內解決特定問題的一組指令或操作步驟,而資料結構是計算機中組織和儲存資料的方式。
|
||||
- 資料結構與演算法緊密相連。資料結構是演算法的基石,而演算法為資料結構注入生命力。
|
||||
- 我們可以將資料結構與演算法類比為拼裝積木,積木代表資料,積木的形狀和連線方式等代表資料結構,拼裝積木的步驟則對應演算法。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:作為一名程式設計師,我在日常工作中從未用演算法解決過問題,常用演算法都被程式語言封裝好了,直接用就可以了;這是否意味著我們工作中的問題還沒有到達需要演算法的程度?
|
||||
|
||||
如果把具體的工作技能比作是武功的“招式”的話,那麼基礎科目應該更像是“內功”。
|
||||
|
||||
我認為學演算法(以及其他基礎科目)的意義不是在於在工作中從零實現它,而是基於學到的知識,在解決問題時能夠作出專業的反應和判斷,從而提升工作的整體質量。舉一個簡單例子,每種程式語言都內建了排序函式:
|
||||
|
||||
- 如果我們沒有學過資料結構與演算法,那麼給定任何資料,我們可能都塞給這個排序函式去做了。執行順暢、效能不錯,看上去並沒有什麼問題。
|
||||
- 但如果學過演算法,我們就會知道內建排序函式的時間複雜度是 $O(n \log n)$ ;而如果給定的資料是固定位數的整數(例如學號),那麼我們就可以用效率更高的“基數排序”來做,將時間複雜度降為 $O(nk)$ ,其中 $k$ 為位數。當資料體量很大時,節省出來的執行時間就能創造較大價值(成本降低、體驗變好等)。
|
||||
|
||||
在工程領域中,大量問題是難以達到最優解的,許多問題只是被“差不多”地解決了。問題的難易程度一方面取決於問題本身的性質,另一方面也取決於觀測問題的人的知識儲備。人的知識越完備、經驗越多,分析問題就會越深入,問題就能被解決得更優雅。
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 1.2 演算法是什麼
|
||||
|
||||
## 1.2.1 演算法定義
|
||||
|
||||
<u>演算法(algorithm)</u>是在有限時間內解決特定問題的一組指令或操作步驟,它具有以下特性。
|
||||
|
||||
- 問題是明確的,包含清晰的輸入和輸出定義。
|
||||
- 具有可行性,能夠在有限步驟、時間和記憶體空間下完成。
|
||||
- 各步驟都有確定的含義,在相同的輸入和執行條件下,輸出始終相同。
|
||||
|
||||
## 1.2.2 資料結構定義
|
||||
|
||||
<u>資料結構(data structure)</u>是組織和儲存資料的方式,涵蓋資料內容、資料之間關係和資料操作方法,它具有以下設計目標。
|
||||
|
||||
- 空間佔用儘量少,以節省計算機記憶體。
|
||||
- 資料操作儘可能快速,涵蓋資料訪問、新增、刪除、更新等。
|
||||
- 提供簡潔的資料表示和邏輯資訊,以便演算法高效執行。
|
||||
|
||||
**資料結構設計是一個充滿權衡的過程**。如果想在某方面取得提升,往往需要在另一方面作出妥協。下面舉兩個例子。
|
||||
|
||||
- 鏈結串列相較於陣列,在資料新增和刪除操作上更加便捷,但犧牲了資料訪問速度。
|
||||
- 圖相較於鏈結串列,提供了更豐富的邏輯資訊,但需要佔用更大的記憶體空間。
|
||||
|
||||
## 1.2.3 資料結構與演算法的關係
|
||||
|
||||
如圖 1-4 所示,資料結構與演算法高度相關、緊密結合,具體表現在以下三個方面。
|
||||
|
||||
- 資料結構是演算法的基石。資料結構為演算法提供了結構化儲存的資料,以及操作資料的方法。
|
||||
- 演算法為資料結構注入生命力。資料結構本身僅儲存資料資訊,結合演算法才能解決特定問題。
|
||||
- 演算法通常可以基於不同的資料結構實現,但執行效率可能相差很大,選擇合適的資料結構是關鍵。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 1-4 資料結構與演算法的關係 </p>
|
||||
|
||||
資料結構與演算法猶如圖 1-5 所示的拼裝積木。一套積木,除了包含許多零件之外,還附有詳細的組裝說明書。我們按照說明書一步步操作,就能組裝出精美的積木模型。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 1-5 拼裝積木 </p>
|
||||
|
||||
兩者的詳細對應關係如表 1-1 所示。
|
||||
|
||||
<p align="center"> 表 1-1 將資料結構與演算法類比為拼裝積木 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 資料結構與演算法 | 拼裝積木 |
|
||||
| -------------- | ---------------------------------------- |
|
||||
| 輸入資料 | 未拼裝的積木 |
|
||||
| 資料結構 | 積木組織形式,包括形狀、大小、連線方式等 |
|
||||
| 演算法 | 把積木拼成目標形態的一系列操作步驟 |
|
||||
| 輸出資料 | 積木模型 |
|
||||
|
||||
</div>
|
||||
|
||||
值得說明的是,資料結構與演算法是獨立於程式語言的。正因如此,本書得以提供基於多種程式語言的實現。
|
||||
|
||||
!!! tip "約定俗成的簡稱"
|
||||
|
||||
在實際討論時,我們通常會將“資料結構與演算法”簡稱為“演算法”。比如眾所周知的 LeetCode 演算法題目,實際上同時考查資料結構和演算法兩方面的知識。
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 0.1 關於本書
|
||||
|
||||
本專案旨在建立一本開源、免費、對新手友好的資料結構與演算法入門教程。
|
||||
|
||||
- 全書採用動畫圖解,內容清晰易懂、學習曲線平滑,引導初學者探索資料結構與演算法的知識地圖。
|
||||
- 源程式碼可一鍵執行,幫助讀者在練習中提升程式設計技能,瞭解演算法工作原理和資料結構底層實現。
|
||||
- 提倡讀者互助學習,歡迎大家在評論區提出問題與分享見解,在交流討論中共同進步。
|
||||
|
||||
## 0.1.1 目標讀者
|
||||
|
||||
若你是演算法初學者,從未接觸過演算法,或者已經有一些刷題經驗,對資料結構與演算法有模糊的認識,在會與不會之間反覆橫跳,那麼本書正是為你量身定製的!
|
||||
|
||||
如果你已經積累一定的刷題量,熟悉大部分題型,那麼本書可助你回顧與梳理演算法知識體系,倉庫源程式碼可以當作“刷題工具庫”或“演算法字典”來使用。
|
||||
|
||||
若你是演算法“大神”,我們期待收到你的寶貴建議,或者[一起參與創作](https://www.hello-algo.com/chapter_appendix/contribution/)。
|
||||
|
||||
!!! success "前置條件"
|
||||
|
||||
你需要至少具備任一語言的程式設計基礎,能夠閱讀和編寫簡單程式碼。
|
||||
|
||||
## 0.1.2 內容結構
|
||||
|
||||
本書的主要內容如圖 0-1 所示。
|
||||
|
||||
- **複雜度分析**:資料結構和演算法的評價維度與方法。時間複雜度和空間複雜度的推算方法、常見型別、示例等。
|
||||
- **資料結構**:基本資料型別和資料結構的分類方法。陣列、鏈結串列、堆疊、佇列、雜湊表、樹、堆積、圖等資料結構的定義、優缺點、常用操作、常見型別、典型應用、實現方法等。
|
||||
- **演算法**:搜尋、排序、分治、回溯、動態規劃、貪婪等演算法的定義、優缺點、效率、應用場景、解題步驟和示例問題等。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-1 本書主要內容 </p>
|
||||
|
||||
## 0.1.3 致謝
|
||||
|
||||
本書在開源社群眾多貢獻者的共同努力下不斷完善。感謝每一位投入時間與精力的撰稿人,他們是(按照 GitHub 自動生成的順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、pengchzn、QiLOL、Cathay-Chen、guowei-gong、xBLACKICEx、IsChristina、JoseHung、qualifier1024、hello-ikun、magentaqin、Guanngxu、thomasq0、sunshinesDL、L-Super、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、theNefelibatas、Shyam-Chen、sangxiaai、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、Nigh、Dr-XYZ、MolDuM、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、xjr7670、beatrix-chan、DullSword、qq909244296、iStig、boloboloda、hts0000、gledfish、fbigm、echo1937、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、JTCPOWI、KawaiiAsh、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、llql1211、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Senrian、Allen-Scai、19santosh99、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、codetypess、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Kunchen-Luo、Keynman 和 KeiichiKasai。
|
||||
|
||||
本書的程式碼審閱工作由 coderonion、curtishd、Gonglja、gvenusleo、hpstory、justin-tse、khoaxuantu、krahets、night-cruise、nuomi1、Reanon 和 rongyi 完成(按照首字母順序排列)。感謝他們付出的時間與精力,正是他們確保了各語言程式碼的規範與統一。
|
||||
|
||||
本書的英文版由 yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn 和 thomasq0 審閱;日文版由 eltociear 審閱;俄文版由 И. А. Шевкун 和 Yuyan Huang 審閱;繁體中文版由 Shyam-Chen 和 Dr-XYZ 審閱。正是因為他們的貢獻,這本書才能夠服務於更廣泛的讀者群體,感謝他們。
|
||||
|
||||
本書的 ePub 電子書生成工具由 zhongfq 開發。感謝他的貢獻,為讀者提供了更靈活的閱讀方式。
|
||||
|
||||
在本書的創作過程中,我得到了許多人的幫助。
|
||||
|
||||
- 感謝我在公司的導師李汐博士,在一次暢談中你鼓勵我“快行動起來”,堅定了我寫這本書的決心;
|
||||
- 感謝我的女朋友泡泡作為本書的首位讀者,從演算法小白的角度提出許多寶貴建議,使得本書更適合新手閱讀;
|
||||
- 感謝騰寶、琦寶、飛寶為本書起了一個富有創意的名字,喚起大家寫下第一行程式碼“Hello World!”的美好回憶;
|
||||
- 感謝校銓在智慧財產權方面提供的專業幫助,這對本開源書的完善起到了重要作用;
|
||||
- 感謝蘇潼為本書設計了精美的封面和 logo ,並在我的強迫症的驅使下多次耐心修改;
|
||||
- 感謝 @squidfunk 提供的排版建議,以及他開發的開源文件主題 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material) 。
|
||||
|
||||
在寫作過程中,我閱讀了許多關於資料結構與演算法的教材和文章。這些作品為本書提供了優秀的範本,確保了本書內容的準確性與品質。在此感謝所有老師和前輩的傑出貢獻!
|
||||
|
||||
本書倡導手腦並用的學習方式,在這一點上我深受[《動手學深度學習》](https://github.com/d2l-ai/d2l-zh)的啟發。在此向各位讀者強烈推薦這本優秀的著作。
|
||||
|
||||
**衷心感謝我的父母,正是你們一直以來的支持與鼓勵,讓我有機會做這件富有趣味的事**。
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/book-open-outline
|
||||
---
|
||||
|
||||
# 第 0 章 前言
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
演算法猶如美妙的交響樂,每一行程式碼都像韻律般流淌。
|
||||
|
||||
願這本書在你的腦海中輕輕響起,留下獨特而深刻的旋律。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [0.1 關於本書](about_the_book.md)
|
||||
- [0.2 如何使用本書](suggestions.md)
|
||||
- [0.3 小結](summary.md)
|
||||
@@ -0,0 +1,257 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 0.2 如何使用本書
|
||||
|
||||
!!! tip
|
||||
|
||||
為了獲得最佳的閱讀體驗,建議你通讀本節內容。
|
||||
|
||||
## 0.2.1 行文風格約定
|
||||
|
||||
- 標題後標註 `*` 的是選讀章節,內容相對困難。如果你的時間有限,可以先跳過。
|
||||
- 專業術語會使用黑體(紙質版和 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=""
|
||||
### 標題註釋,用於標註函式、類別、測試樣例等 ###
|
||||
|
||||
# 內容註釋,用於詳解程式碼
|
||||
|
||||
# 多行
|
||||
# 註釋
|
||||
```
|
||||
|
||||
## 0.2.2 在動畫圖解中高效學習
|
||||
|
||||
相較於文字,影片和圖片具有更高的資訊密度和結構化程度,更易於理解。在本書中,**重點和難點知識將主要透過動畫以圖解形式展示**,而文字則作為解釋與補充。
|
||||
|
||||
如果你在閱讀本書時,發現某段內容提供瞭如圖 0-2 所示的動畫圖解,**請以圖為主、以文字為輔**,綜合兩者來理解內容。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-2 動畫圖解示例 </p>
|
||||
|
||||
## 0.2.3 在程式碼實踐中加深理解
|
||||
|
||||
本書的配套程式碼託管在 [GitHub 倉庫](https://github.com/krahets/hello-algo)。如圖 0-3 所示,**源程式碼附有測試樣例,可一鍵執行**。
|
||||
|
||||
如果時間允許,**建議你參照程式碼自行敲一遍**。如果學習時間有限,請至少通讀並執行所有程式碼。
|
||||
|
||||
與閱讀程式碼相比,編寫程式碼的過程往往能帶來更多收穫。**動手學,才是真的學**。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-3 執行程式碼示例 </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”按鈕直接下載程式碼壓縮包,然後在本地解壓即可。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-4 克隆倉庫與下載程式碼 </p>
|
||||
|
||||
**第三步:執行源程式碼**。如圖 0-5 所示,對於頂部標有檔案名稱的程式碼塊,我們可以在倉庫的 `codes` 檔案夾內找到對應的源程式碼檔案。源程式碼檔案可一鍵執行,將幫助你節省不必要的除錯時間,讓你能夠專注於學習內容。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-5 程式碼塊與對應的源程式碼檔案 </p>
|
||||
|
||||
除了本地執行程式碼,**網頁版還支持 Python 程式碼的視覺化執行**(基於 [pythontutor](https://pythontutor.com/) 實現)。如圖 0-6 所示,你可以點選程式碼塊下方的“視覺化執行”來展開檢視,觀察演算法程式碼的執行過程;也可以點選“全屏觀看”,以獲得更好的閱覽體驗。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-6 Python 程式碼的視覺化執行 </p>
|
||||
|
||||
## 0.2.4 在提問討論中共同成長
|
||||
|
||||
在閱讀本書時,請不要輕易跳過那些沒學明白的知識點。**歡迎在評論區提出你的問題**,我和小夥伴們將竭誠為你解答,一般情況下可在兩天內回覆。
|
||||
|
||||
如圖 0-7 所示,網頁版每個章節的底部都配有評論區。希望你能多關注評論區的內容。一方面,你可以瞭解大家遇到的問題,從而查漏補缺,激發更深入的思考。另一方面,期待你能慷慨地回答其他小夥伴的問題,分享你的見解,幫助他人進步。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-7 評論區示例 </p>
|
||||
|
||||
## 0.2.5 演算法學習路線
|
||||
|
||||
從總體上看,我們可以將學習資料結構與演算法的過程劃分為三個階段。
|
||||
|
||||
1. **階段一:演算法入門**。我們需要熟悉各種資料結構的特點和用法,學習不同演算法的原理、流程、用途和效率等方面的內容。
|
||||
2. **階段二:刷演算法題**。建議從熱門題目開刷,先積累至少 100 道題目,熟悉主流的演算法問題。初次刷題時,“知識遺忘”可能是一個挑戰,但請放心,這是很正常的。我們可以按照“艾賓浩斯遺忘曲線”來複習題目,通常在進行 3~5 輪的重複後,就能將其牢記在心。推薦的題單和刷題計劃請見此 [GitHub 倉庫](https://github.com/krahets/LeetCode-Book)。
|
||||
3. **階段三:搭建知識體系**。在學習方面,我們可以閱讀演算法專欄文章、解題框架和演算法教材,以不斷豐富知識體系。在刷題方面,可以嘗試採用進階刷題策略,如按專題分類、一題多解、一解多題等,相關的刷題心得可以在各個社群找到。
|
||||
|
||||
如圖 0-8 所示,本書內容主要涵蓋“階段一”,旨在幫助你更高效地展開階段二和階段三的學習。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 0-8 演算法學習路線 </p>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 0.3 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 本書的主要受眾是演算法初學者。如果你已有一定基礎,本書能幫助你系統回顧演算法知識,書中源程式碼也可作為“刷題工具庫”使用。
|
||||
- 書中內容主要包括複雜度分析、資料結構和演算法三部分,涵蓋了該領域的大部分主題。
|
||||
- 對於演算法新手,在初學階段閱讀一本入門書至關重要,可以少走許多彎路。
|
||||
- 書中的動畫圖解通常用於介紹重點和難點知識。閱讀本書時,應給予這些內容更多關注。
|
||||
- 實踐乃學習程式設計之最佳途徑。強烈建議執行源程式碼並親自敲程式碼。
|
||||
- 本書網頁版的每個章節都設有評論區,歡迎隨時分享你的疑惑與見解。
|
||||
@@ -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.
|
||||
+713
@@ -0,0 +1,713 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.1 二分搜尋
|
||||
|
||||
<u>二分搜尋(binary search)</u>是一種基於分治策略的高效搜尋演算法。它利用資料的有序性,每輪縮小一半搜尋範圍,直至找到目標元素或搜尋區間為空為止。
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個長度為 $n$ 的陣列 `nums` ,元素按從小到大的順序排列且不重複。請查詢並返回元素 `target` 在該陣列中的索引。若陣列不包含該元素,則返回 $-1$ 。示例如圖 10-1 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-1 二分搜尋示例資料 </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>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-2 二分搜尋流程 </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"
|
||||
### 二分搜尋(雙閉區間) ###
|
||||
def binary_search(nums, target)
|
||||
# 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素
|
||||
i, j = 0, nums.length - 1
|
||||
|
||||
# 迴圈,當搜尋區間為空時跳出(當 i > j 時為空)
|
||||
while i <= j
|
||||
# 理論上 Ruby 的數字可以無限大(取決於記憶體大小),無須考慮大數越界問題
|
||||
m = (i + j) / 2 # 計算中點索引 m
|
||||
|
||||
if nums[m] < target
|
||||
i = m + 1 # 此情況說明 target 在區間 [m+1, j] 中
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # 此情況說明 target 在區間 [i, m-1] 中
|
||||
else
|
||||
return m # 找到目標元素,返回其索引
|
||||
end
|
||||
end
|
||||
|
||||
-1 # 未找到目標元素,返回 -1
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%20n-1%5D%20%EF%BC%8C%E5%8D%B3%20i%2C%20j%20%E5%88%86%E5%88%A5%E6%8C%87%E5%90%91%E9%99%A3%E5%88%97%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%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%EF%BC%8C%E7%95%B6%E6%90%9C%E5%B0%8B%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%E6%99%82%E8%B7%B3%E5%87%BA%EF%BC%88%E7%95%B6%20i%20%3E%20j%20%E6%99%82%E7%82%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%AB%96%E4%B8%8A%20Python%20%E7%9A%84%E6%95%B8%E5%AD%97%E5%8F%AF%E4%BB%A5%E7%84%A1%E9%99%90%E5%A4%A7%EF%BC%88%E5%8F%96%E6%B1%BA%E6%96%BC%E8%A8%98%E6%86%B6%E9%AB%94%E5%A4%A7%E5%B0%8F%EF%BC%89%EF%BC%8C%E7%84%A1%E9%A0%88%E8%80%83%E6%85%AE%E5%A4%A7%E6%95%B8%E8%B6%8A%E7%95%8C%E5%95%8F%E9%A1%8C%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bm%2B1%2C%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%A8%99%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%A8%99%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%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22%2C%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%20n-1%5D%20%EF%BC%8C%E5%8D%B3%20i%2C%20j%20%E5%88%86%E5%88%A5%E6%8C%87%E5%90%91%E9%99%A3%E5%88%97%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%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%EF%BC%8C%E7%95%B6%E6%90%9C%E5%B0%8B%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%E6%99%82%E8%B7%B3%E5%87%BA%EF%BC%88%E7%95%B6%20i%20%3E%20j%20%E6%99%82%E7%82%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%AB%96%E4%B8%8A%20Python%20%E7%9A%84%E6%95%B8%E5%AD%97%E5%8F%AF%E4%BB%A5%E7%84%A1%E9%99%90%E5%A4%A7%EF%BC%88%E5%8F%96%E6%B1%BA%E6%96%BC%E8%A8%98%E6%86%B6%E9%AB%94%E5%A4%A7%E5%B0%8F%EF%BC%89%EF%BC%8C%E7%84%A1%E9%A0%88%E8%80%83%E6%85%AE%E5%A4%A7%E6%95%B8%E8%B6%8A%E7%95%8C%E5%95%8F%E9%A1%8C%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%20%20%23%20%E8%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bm%2B1%2C%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%A8%99%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%A8%99%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%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
**時間複雜度為 $O(\log n)$** :在二分迴圈中,區間每輪縮小一半,因此迴圈次數為 $\log_2 n$ 。
|
||||
|
||||
**空間複雜度為 $O(1)$** :指標 $i$ 和 $j$ 使用常數大小空間。
|
||||
|
||||
## 10.1.1 區間表示方法
|
||||
|
||||
除了上述雙閉區間外,常見的區間表示還有“左閉右開”區間,定義為 $[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"
|
||||
### 二分搜尋(左閉右開區間) ###
|
||||
def binary_search_lcro(nums, target)
|
||||
# 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1
|
||||
i, j = 0, nums.length
|
||||
|
||||
# 迴圈,當搜尋區間為空時跳出(當 i = j 時為空)
|
||||
while i < j
|
||||
# 計算中點索引 m
|
||||
m = (i + j) / 2
|
||||
|
||||
if nums[m] < target
|
||||
i = m + 1 # 此情況說明 target 在區間 [m+1, j) 中
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # 此情況說明 target 在區間 [i, m) 中
|
||||
else
|
||||
return m # 找到目標元素,返回其索引
|
||||
end
|
||||
end
|
||||
|
||||
-1 # 未找到目標元素,返回 -1
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8D%80%E9%96%93%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%96%89%E5%8F%B3%E9%96%8B%E5%8D%80%E9%96%93%20%5B0%2C%20n%29%20%EF%BC%8C%E5%8D%B3%20i%2C%20j%20%E5%88%86%E5%88%A5%E6%8C%87%E5%90%91%E9%99%A3%E5%88%97%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%2C%20j%20%3D%200%2C%20len%28nums%29%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%EF%BC%8C%E7%95%B6%E6%90%9C%E5%B0%8B%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%E6%99%82%E8%B7%B3%E5%87%BA%EF%BC%88%E7%95%B6%20i%20%3D%20j%20%E6%99%82%E7%82%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bm%2B1%2C%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%A8%99%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%A8%99%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%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8D%80%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search_lcro%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22%2C%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_lcro%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8D%80%E9%96%93%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%96%89%E5%8F%B3%E9%96%8B%E5%8D%80%E9%96%93%20%5B0%2C%20n%29%20%EF%BC%8C%E5%8D%B3%20i%2C%20j%20%E5%88%86%E5%88%A5%E6%8C%87%E5%90%91%E9%99%A3%E5%88%97%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%2C%20j%20%3D%200%2C%20len%28nums%29%0A%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%EF%BC%8C%E7%95%B6%E6%90%9C%E5%B0%8B%E5%8D%80%E9%96%93%E7%82%BA%E7%A9%BA%E6%99%82%E8%B7%B3%E5%87%BA%EF%BC%88%E7%95%B6%20i%20%3D%20j%20%E6%99%82%E7%82%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bm%2B1%2C%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%E6%B3%81%E8%AA%AA%E6%98%8E%20target%20%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%A8%99%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%A8%99%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%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%EF%BC%88%E5%B7%A6%E9%96%89%E5%8F%B3%E9%96%8B%E5%8D%80%E9%96%93%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search_lcro%28nums%2C%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A8%99%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
如圖 10-3 所示,在兩種區間表示下,二分搜尋演算法的初始化、迴圈條件和縮小區間操作皆有所不同。
|
||||
|
||||
由於“雙閉區間”表示中的左右邊界都被定義為閉區間,因此透過指標 $i$ 和指標 $j$ 縮小區間的操作也是對稱的。這樣更不容易出錯,**因此一般建議採用“雙閉區間”的寫法**。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-3 兩種區間定義 </p>
|
||||
|
||||
## 10.1.2 優點與侷限性
|
||||
|
||||
二分搜尋在時間和空間方面都有較好的效能。
|
||||
|
||||
- 二分搜尋的時間效率高。在大資料量下,對數階的時間複雜度具有顯著優勢。例如,當資料大小 $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,503 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.3 二分搜尋邊界
|
||||
|
||||
## 10.3.1 查詢左邊界
|
||||
|
||||
!!! 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"
|
||||
### 二分搜尋最左一個 target ###
|
||||
def binary_search_left_edge(nums, target)
|
||||
# 等價於查詢 target 的插入點
|
||||
i = binary_search_insertion(nums, target)
|
||||
|
||||
# 未找到 target ,返回 -1
|
||||
return -1 if i == nums.length || nums[i] != target
|
||||
|
||||
i # 找到 target ,返回索引 i
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E5%80%8B%E5%B0%8F%E6%96%BC%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_left_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%9C%80%E5%B7%A6%E4%B8%80%E5%80%8B%20target%22%22%22%0A%20%20%20%20%23%20%E7%AD%89%E5%83%B9%E6%96%BC%E6%9F%A5%E8%A9%A2%20target%20%E7%9A%84%E6%8F%92%E5%85%A5%E9%BB%9E%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%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%21%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%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E5%B7%A6%E9%82%8A%E7%95%8C%E5%92%8C%E5%8F%B3%E9%82%8A%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_left_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%B7%A6%E4%B8%80%E5%80%8B%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E5%80%8B%E5%B0%8F%E6%96%BC%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_left_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%9C%80%E5%B7%A6%E4%B8%80%E5%80%8B%20target%22%22%22%0A%20%20%20%20%23%20%E7%AD%89%E5%83%B9%E6%96%BC%E6%9F%A5%E8%A9%A2%20target%20%E7%9A%84%E6%8F%92%E5%85%A5%E9%BB%9E%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%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%21%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%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E5%B7%A6%E9%82%8A%E7%95%8C%E5%92%8C%E5%8F%B3%E9%82%8A%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_left_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%B7%A6%E4%B8%80%E5%80%8B%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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 查詢右邊界
|
||||
|
||||
那麼如何查詢最右一個 `target` 呢?最直接的方式是修改程式碼,替換在 `nums[m] == target` 情況下的指標收縮操作。程式碼在此省略,有興趣的讀者可以自行實現。
|
||||
|
||||
下面我們介紹兩種更加取巧的方法。
|
||||
|
||||
### 1. 複用查詢左邊界
|
||||
|
||||
實際上,我們可以利用查詢最左元素的函式來查詢最右元素,具體方法為:**將查詢最右一個 `target` 轉化為查詢最左一個 `target + 1`**。
|
||||
|
||||
如圖 10-7 所示,查詢完成後,指標 $i$ 指向最左一個 `target + 1`(如果存在),而 $j$ 指向最右一個 `target` ,**因此返回 $j$ 即可**。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-7 將查詢右邊界轉化為查詢左邊界 </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"
|
||||
### 二分搜尋最右一個 target ###
|
||||
def binary_search_right_edge(nums, target)
|
||||
# 轉化為查詢最左一個 target + 1
|
||||
i = binary_search_insertion(nums, target + 1)
|
||||
|
||||
# j 指向最右一個 target ,i 指向首個大於 target 的元素
|
||||
j = i - 1
|
||||
|
||||
# 未找到 target ,返回 -1
|
||||
return -1 if j == -1 || nums[j] != target
|
||||
|
||||
j # 找到 target ,返回索引 j
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E5%80%8B%E5%B0%8F%E6%96%BC%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_right_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%9C%80%E5%8F%B3%E4%B8%80%E5%80%8B%20target%22%22%22%0A%20%20%20%20%23%20%E8%BD%89%E5%8C%96%E7%82%BA%E6%9F%A5%E8%A9%A2%E6%9C%80%E5%B7%A6%E4%B8%80%E5%80%8B%20target%20%2B%201%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%20target%20%2B%201%29%0A%20%20%20%20%23%20j%20%E6%8C%87%E5%90%91%E6%9C%80%E5%8F%B3%E4%B8%80%E5%80%8B%20target%20%EF%BC%8Ci%20%E6%8C%87%E5%90%91%E9%A6%96%E5%80%8B%E5%A4%A7%E6%96%BC%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%21%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%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E5%B7%A6%E9%82%8A%E7%95%8C%E5%92%8C%E5%8F%B3%E9%82%8A%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_right_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%8F%B3%E4%B8%80%E5%80%8B%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E5%80%8B%E5%B0%8F%E6%96%BC%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%20i%0A%20%20%20%20return%20i%0A%0Adef%20binary_search_right_edge%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%9C%80%E5%8F%B3%E4%B8%80%E5%80%8B%20target%22%22%22%0A%20%20%20%20%23%20%E8%BD%89%E5%8C%96%E7%82%BA%E6%9F%A5%E8%A9%A2%E6%9C%80%E5%B7%A6%E4%B8%80%E5%80%8B%20target%20%2B%201%0A%20%20%20%20i%20%3D%20binary_search_insertion%28nums%2C%20target%20%2B%201%29%0A%20%20%20%20%23%20j%20%E6%8C%87%E5%90%91%E6%9C%80%E5%8F%B3%E4%B8%80%E5%80%8B%20target%20%EF%BC%8Ci%20%E6%8C%87%E5%90%91%E9%A6%96%E5%80%8B%E5%A4%A7%E6%96%BC%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%21%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%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E5%B7%A6%E9%82%8A%E7%95%8C%E5%92%8C%E5%8F%B3%E9%82%8A%E7%95%8C%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_right_edge%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%8F%B3%E4%B8%80%E5%80%8B%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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. 轉化為查詢元素
|
||||
|
||||
我們知道,當陣列不包含 `target` 時,最終 $i$ 和 $j$ 會分別指向首個大於、小於 `target` 的元素。
|
||||
|
||||
因此,如圖 10-8 所示,我們可以構造一個陣列中不存在的元素,用於查詢左右邊界。
|
||||
|
||||
- 查詢最左一個 `target` :可以轉化為查詢 `target - 0.5` ,並返回指標 $i$ 。
|
||||
- 查詢最右一個 `target` :可以轉化為查詢 `target + 0.5` ,並返回指標 $j$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-8 將查詢邊界轉化為查詢元素 </p>
|
||||
|
||||
程式碼在此省略,以下兩點值得注意。
|
||||
|
||||
- 給定陣列不包含小數,這意味著我們無須關心如何處理相等的情況。
|
||||
- 因為該方法引入了小數,所以需要將函式中的變數 `target` 改為浮點數型別(Python 無須改動)。
|
||||
@@ -0,0 +1,674 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.2 二分搜尋插入點
|
||||
|
||||
二分搜尋不僅可用於搜尋目標元素,還可用於解決許多變種問題,比如搜尋目標元素的插入位置。
|
||||
|
||||
## 10.2.1 無重複元素的情況
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個長度為 $n$ 的有序陣列 `nums` 和一個元素 `target` ,陣列不存在重複元素。現將 `target` 插入陣列 `nums` 中,並保持其有序性。若陣列中已存在元素 `target` ,則插入到其左方。請返回插入後 `target` 在陣列中的索引。示例如圖 10-4 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-4 二分搜尋插入點示例資料 </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"
|
||||
### 二分搜尋插入點(無重複元素) ###
|
||||
def binary_search_insertion_simple(nums, target)
|
||||
# 初始化雙閉區間 [0, n-1]
|
||||
i, j = 0, nums.length - 1
|
||||
|
||||
while i <= j
|
||||
# 計算中點索引 m
|
||||
m = (i + j) / 2
|
||||
|
||||
if nums[m] < target
|
||||
i = m + 1 # target 在區間 [m+1, j] 中
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # target 在區間 [i, m-1] 中
|
||||
else
|
||||
return m # 找到 target ,返回插入點 m
|
||||
end
|
||||
end
|
||||
|
||||
i # 未找到 target ,返回插入點 i
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion_simple%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E7%84%A1%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%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%E9%BB%9E%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%E7%84%A1%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion_simple%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E7%84%A1%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%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%E9%BB%9E%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%E7%84%A1%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion_simple%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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 存在重複元素的情況
|
||||
|
||||
!!! question
|
||||
|
||||
在上一題的基礎上,規定陣列可能包含重複元素,其餘不變。
|
||||
|
||||
假設陣列中存在多個 `target` ,則普通二分搜尋只能返回其中一個 `target` 的索引,**而無法確定該元素的左邊和右邊還有多少 `target`**。
|
||||
|
||||
題目要求將目標元素插入到最左邊,**所以我們需要查詢陣列中最左一個 `target` 的索引**。初步考慮透過圖 10-5 所示的步驟實現。
|
||||
|
||||
1. 執行二分搜尋,得到任意一個 `target` 的索引,記為 $k$ 。
|
||||
2. 從索引 $k$ 開始,向左進行線性走訪,當找到最左邊的 `target` 時返回。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-5 線性查詢重複元素的插入點 </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>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-6 二分搜尋重複元素的插入點的步驟 </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"
|
||||
### 二分搜尋插入點(存在重複元素) ###
|
||||
def binary_search_insertion(nums, target)
|
||||
# 初始化雙閉區間 [0, n-1]
|
||||
i, j = 0, nums.length - 1
|
||||
|
||||
while i <= j
|
||||
# 計算中點索引 m
|
||||
m = (i + j) / 2
|
||||
|
||||
if nums[m] < target
|
||||
i = m + 1 # target 在區間 [m+1, j] 中
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # target 在區間 [i, m-1] 中
|
||||
else
|
||||
j = m - 1 # 首個小於 target 的元素在區間 [i, m-1] 中
|
||||
end
|
||||
end
|
||||
|
||||
i # 返回插入點 i
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_insertion%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E5%80%8B%E5%B0%8F%E6%96%BC%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%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%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%EF%BC%88%E5%AD%98%E5%9C%A8%E9%87%8D%E8%A4%87%E5%85%83%E7%B4%A0%EF%BC%89%22%22%22%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%20-%201%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%9B%99%E9%96%89%E5%8D%80%E9%96%93%20%5B0%2C%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%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%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%8D%80%E9%96%93%20%5Bm%2B1%2C%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%8D%80%E9%96%93%20%5Bi%2C%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%E5%80%8B%E5%B0%8F%E6%96%BC%20target%20%E7%9A%84%E5%85%83%E7%B4%A0%E5%9C%A8%E5%8D%80%E9%96%93%20%5Bi%2C%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%E9%BB%9E%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%E8%A4%87%E5%85%83%E7%B4%A0%E7%9A%84%E9%99%A3%E5%88%97%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%206%2C%206%2C%206%2C%206%2C%2010%2C%2012%2C%2015%5D%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%8F%92%E5%85%A5%E9%BB%9E%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20index%20%3D%20binary_search_insertion%28nums%2C%20target%29%0A%20%20%20%20print%28f%22%E5%85%83%E7%B4%A0%20%7Btarget%7D%20%E7%9A%84%E6%8F%92%E5%85%A5%E9%BB%9E%E7%9A%84%E7%B4%A2%E5%BC%95%E7%82%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$ 都逐漸逼近預先設定的目標。最終,它們或是成功找到答案,或是越過邊界後停止。
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 10.7 練習
|
||||
|
||||
## 10.7.1 知識鞏固
|
||||
|
||||
### 1. 二分搜尋怎樣縮小搜尋區間
|
||||
|
||||
在有序陣列 `[2, 5, 8, 12, 16, 23, 38]` 中查詢 16。
|
||||
使用雙閉區間 `[i, j]`,中點為
|
||||
$m=i+(j-i)/2$(向下取整)。
|
||||
|
||||
請寫出每輪的 `(i, j, m)`、中點元素,以及下一步如何縮小區間,
|
||||
直到找到目標。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
每輪查詢過程如下:
|
||||
|
||||
| 輪次 | `(i, j, m)` | 中點元素 | 下一步 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `(0, 6, 3)` | 12 | `12 < 16`,令 `i = 4` |
|
||||
| 2 | `(4, 6, 5)` | 23 | `23 > 16`,令 `j = 4` |
|
||||
| 3 | `(4, 4, 4)` | 16 | 找到目標,返回索引 4 |
|
||||
|
||||
陣列有序,所以中點值小於目標時可以排除中點及其左側;
|
||||
中點值大於目標時可以排除中點及其右側。
|
||||
|
||||
### 2. 重複元素的左右邊界
|
||||
|
||||
在陣列 `[1, 2, 2, 2, 4, 6]` 中查詢數字 2。
|
||||
一名同學用二分搜尋在索引 2 找到目標後立即返回,並說:
|
||||
“索引 2 就是數字 2 的左邊界。”
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 這名同學的說法是否正確?數字 2 的左、右邊界分別是多少?請說明理由。
|
||||
2. 查詢左邊界時,若中點元素等於目標,接下來應繼續搜尋哪一側?
|
||||
3. 查詢右邊界時又應繼續搜尋哪一側?只需說明方向,無須寫出完整的查詢過程。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 這名同學的說法不正確。找到一個 2 後立即返回,只能保證找到某一個 2,不能保證找到最左或最右的 2。
|
||||
本題的左邊界為索引 1,右邊界為索引 3。
|
||||
|
||||
2. 查詢左邊界時,即使中點元素等於 2,也要繼續搜尋左側,
|
||||
例如在雙閉區間寫法中令 `j = m - 1`。
|
||||
|
||||
3. 查詢右邊界時,中點元素等於 2 後應繼續搜尋右側,
|
||||
例如令 `i = m + 1`。
|
||||
|
||||
### 3. 不同資料該選哪種搜尋方法
|
||||
|
||||
請在“順序查詢、二分搜尋、雜湊表”中,為下面三個場景選擇合適的方法,並說明理由:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 在 $10^7$ 個已經有序且不再變動的整數中反覆查詢,不額外建立其他資料結構;
|
||||
2. 在頻繁插入、刪除的資料集中反覆判斷某個鍵是否存在,不要求保持有序,也不進行範圍查詢;
|
||||
3. 在無序陣列中只查詢一次某個值。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 二分搜尋:資料有序且靜態,$O(\log n)$ 且無須額外空間。
|
||||
2. 雜湊表:當雜湊函式能把鍵較均勻地分散到各桶時,插入、刪除和按鍵查詢的平均時間複雜度都是 $O(1)$。
|
||||
3. 直接從頭到尾走訪:只查詢一次時,排序或建立雜湊表都要先處理整個陣列,
|
||||
並不會減少這一次任務的總工作量。
|
||||
|
||||
選擇取決於資料是否有序、是否允許建立額外結構、查詢次數以及需要執行哪些操作。
|
||||
|
||||
## 10.7.2 程式設計練習
|
||||
|
||||
### 1. 有序陣列的二分搜尋
|
||||
|
||||
給定一個按嚴格遞增順序排列的整數陣列 `nums` 和目標值 `target`。請使用二分搜尋找到 `target`:若存在,返回它的陣列索引;若不存在,返回 -1。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 初始區間是 left = 0、right = n - 1,非空條件是 left <= right
|
||||
2. 用 mid = left + (right - left) // 2 計算中點
|
||||
3. 若 `nums[mid]` 小於 `target`,就把左邊界移到 `mid + 1`;若 `nums[mid]` 大於 `target`,就把右邊界移到 `mid - 1`;相等時立即返回
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/binary-search/solutions/1692151/by-jyd-i7xr/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 有序陣列的插入位置
|
||||
|
||||
給定一個按嚴格遞增順序排列的整數陣列 `nums` 和目標值 `target`。
|
||||
|
||||
若 `target` 已在陣列中,返回它的索引;否則,返回把 `target` 插入陣列後仍能保持嚴格遞增順序的位置。答案可能是 0,也可能等於陣列長度。請使用二分搜尋。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 答案可能是 0,也可能是陣列長度 n
|
||||
2. 使用雙閉區間時,若 `nums[mid]` 大於等於 `target`,就令 `right = mid - 1`,繼續檢查更靠左的位置;否則令 `left = mid + 1`
|
||||
3. 迴圈結束時 left 就是插入位置
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/text-search
|
||||
---
|
||||
|
||||
# 第 10 章 搜尋
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
搜尋是一場未知的冒險,我們或許需要走遍神秘空間的每個角落,又或許可以快速鎖定目標。
|
||||
|
||||
在這場尋覓之旅中,每一次探索都可能得到一個未曾料想的答案。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [10.1 二分搜尋](binary_search.md)
|
||||
- [10.2 二分搜尋插入點](binary_search_insertion.md)
|
||||
- [10.3 二分搜尋邊界](binary_search_edge.md)
|
||||
- [10.4 雜湊最佳化策略](replace_linear_by_hashing.md)
|
||||
- [10.5 重識搜尋演算法](searching_algorithm_revisited.md)
|
||||
- [10.6 小結](summary.md)
|
||||
- [10.7 練習](exercises.md)
|
||||
+546
@@ -0,0 +1,546 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.4 雜湊最佳化策略
|
||||
|
||||
在演算法題中,**我們常透過將線性查詢替換為雜湊查詢來降低演算法的時間複雜度**。我們藉助一個演算法題來加深理解。
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個整數陣列 `nums` 和一個目標元素 `target` ,請在陣列中搜索“和”為 `target` 的兩個元素,並返回它們的陣列索引。返回任意一個解即可。
|
||||
|
||||
## 10.4.1 線性查詢:以時間換空間
|
||||
|
||||
考慮直接走訪所有可能的組合。如圖 10-9 所示,我們開啟一個兩層迴圈,在每輪中判斷兩個整數的和是否為 `target` ,若是,則返回它們的索引。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-9 線性查詢求解兩數之和 </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; j < 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"
|
||||
### 方法一:暴力列舉 ###
|
||||
def two_sum_brute_force(nums, target)
|
||||
# 兩層迴圈,時間複雜度為 O(n^2)
|
||||
for i in 0...(nums.length - 1)
|
||||
for j in (i + 1)...nums.length
|
||||
return [i, j] if nums[i] + nums[j] == target
|
||||
end
|
||||
end
|
||||
|
||||
[]
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%B8%80%EF%BC%9A%E6%9A%B4%E5%8A%9B%E5%88%97%E8%88%89%22%22%22%0A%20%20%20%20%23%20%E5%85%A9%E5%B1%A4%E8%BF%B4%E5%9C%88%EF%BC%8C%E6%99%82%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E7%82%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%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%2B%20nums%5Bj%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bi%2C%20j%5D%0A%20%20%20%20return%20%5B%5D%0A%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%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_brute_force%28nums%2C%20target%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_brute_force%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%B8%80%EF%BC%9A%E6%9A%B4%E5%8A%9B%E5%88%97%E8%88%89%22%22%22%0A%20%20%20%20%23%20%E5%85%A9%E5%B1%A4%E8%BF%B4%E5%9C%88%EF%BC%8C%E6%99%82%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E7%82%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%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%2B%20nums%5Bj%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Bi%2C%20j%5D%0A%20%20%20%20return%20%5B%5D%0A%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%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_brute_force%28nums%2C%20target%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
此方法的時間複雜度為 $O(n^2)$ ,空間複雜度為 $O(1)$ ,在大資料量下非常耗時。
|
||||
|
||||
## 10.4.2 雜湊查詢:以空間換時間
|
||||
|
||||
考慮藉助一個雜湊表,鍵值對分別為陣列元素和元素索引。迴圈走訪陣列,每輪執行圖 10-10 所示的步驟。
|
||||
|
||||
1. 判斷數字 `target - nums[i]` 是否在雜湊表中,若是,則直接返回這兩個元素的索引。
|
||||
2. 將鍵值對 `nums[i]` 和索引 `i` 新增進雜湊表。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-10 輔助雜湊表求解兩數之和 </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"
|
||||
### 方法二:輔助雜湊表 ###
|
||||
def two_sum_hash_table(nums, target)
|
||||
# 輔助雜湊表,空間複雜度為 O(n)
|
||||
dic = {}
|
||||
# 單層迴圈,時間複雜度為 O(n)
|
||||
for i in 0...nums.length
|
||||
return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])
|
||||
|
||||
dic[nums[i]] = i
|
||||
end
|
||||
|
||||
[]
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%BA%8C%EF%BC%9A%E8%BC%94%E5%8A%A9%E9%9B%9C%E6%B9%8A%E8%A1%A8%22%22%22%0A%20%20%20%20%23%20%E8%BC%94%E5%8A%A9%E9%9B%9C%E6%B9%8A%E8%A1%A8%EF%BC%8C%E7%A9%BA%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E7%82%BA%20O%28n%29%0A%20%20%20%20dic%20%3D%20%7B%7D%0A%20%20%20%20%23%20%E5%96%AE%E5%B1%A4%E8%BF%B4%E5%9C%88%EF%BC%8C%E6%99%82%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E7%82%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%2C%20i%5D%0A%20%20%20%20%20%20%20%20dic%5Bnums%5Bi%5D%5D%20%3D%20i%0A%20%20%20%20return%20%5B%5D%0A%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%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_hash_table%28nums%2C%20target%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20two_sum_hash_table%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%96%B9%E6%B3%95%E4%BA%8C%EF%BC%9A%E8%BC%94%E5%8A%A9%E9%9B%9C%E6%B9%8A%E8%A1%A8%22%22%22%0A%20%20%20%20%23%20%E8%BC%94%E5%8A%A9%E9%9B%9C%E6%B9%8A%E8%A1%A8%EF%BC%8C%E7%A9%BA%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E7%82%BA%20O%28n%29%0A%20%20%20%20dic%20%3D%20%7B%7D%0A%20%20%20%20%23%20%E5%96%AE%E5%B1%A4%E8%BF%B4%E5%9C%88%EF%BC%8C%E6%99%82%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E7%82%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%2C%20i%5D%0A%20%20%20%20%20%20%20%20dic%5Bnums%5Bi%5D%5D%20%3D%20i%0A%20%20%20%20return%20%5B%5D%0A%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%2C%207%2C%2011%2C%2015%5D%0A%20%20%20%20target%20%3D%2013%0A%20%20%20%20res%20%3D%20two_sum_hash_table%28nums%2C%20target%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
此方法透過雜湊查詢將時間複雜度從 $O(n^2)$ 降至 $O(n)$ ,大幅提升執行效率。
|
||||
|
||||
由於需要維護一個額外的雜湊表,因此空間複雜度為 $O(n)$ 。**儘管如此,該方法的整體時空效率更為均衡,因此它是本題的最優解法**。
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.5 重識搜尋演算法
|
||||
|
||||
<u>搜尋演算法(searching algorithm)</u>用於在資料結構(例如陣列、鏈結串列、樹或圖)中搜索一個或一組滿足特定條件的元素。
|
||||
|
||||
搜尋演算法可根據實現思路分為以下兩類。
|
||||
|
||||
- **透過走訪資料結構來定位目標元素**,例如陣列、鏈結串列、樹和圖的走訪等。
|
||||
- **利用資料組織結構或資料包含的先驗資訊,實現高效元素查詢**,例如二分搜尋、雜湊查詢和二元搜尋樹查詢等。
|
||||
|
||||
不難發現,這些知識點都已在前面的章節中介紹過,因此搜尋演算法對於我們來說並不陌生。在本節中,我們將從更加系統的視角切入,重新審視搜尋演算法。
|
||||
|
||||
## 10.5.1 暴力搜尋
|
||||
|
||||
暴力搜尋透過走訪資料結構的每個元素來定位目標元素。
|
||||
|
||||
- “線性搜尋”適用於陣列和鏈結串列等線性資料結構。它從資料結構的一端開始,逐個訪問元素,直到找到目標元素或到達另一端仍沒有找到目標元素為止。
|
||||
- “廣度優先搜尋”和“深度優先搜尋”是圖和樹的兩種走訪策略。廣度優先搜尋從初始節點開始逐層搜尋,由近及遠地訪問各個節點。深度優先搜尋從初始節點開始,沿著一條路徑走到頭,再回溯並嘗試其他路徑,直到走訪完整個資料結構。
|
||||
|
||||
暴力搜尋的優點是簡單且通用性好,**無須對資料做預處理和藉助額外的資料結構**。
|
||||
|
||||
然而,**此類演算法的時間複雜度為 $O(n)$** ,其中 $n$ 為元素數量,因此在資料量較大的情況下效能較差。
|
||||
|
||||
## 10.5.2 自適應搜尋
|
||||
|
||||
自適應搜尋利用資料的特有屬性(例如有序性)來最佳化搜尋過程,從而更高效地定位目標元素。
|
||||
|
||||
- “二分搜尋”利用資料的有序性實現高效查詢,僅適用於陣列。
|
||||
- “雜湊查詢”利用雜湊表將搜尋資料和目標資料建立為鍵值對對映,從而實現查詢操作。
|
||||
- “樹查詢”在特定的樹結構(例如二元搜尋樹)中,基於比較節點值來快速排除節點,從而定位目標元素。
|
||||
|
||||
此類演算法的優點是效率高,**時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$** 。
|
||||
|
||||
然而,**使用這些演算法往往需要對資料進行預處理**。例如,二分搜尋需要預先對陣列進行排序,雜湊查詢和樹查詢都需要藉助額外的資料結構,維護這些資料結構也需要額外的時間和空間開銷。
|
||||
|
||||
!!! tip
|
||||
|
||||
自適應搜尋演算法常被稱為查詢演算法,**主要用於在特定資料結構中快速檢索目標元素**。
|
||||
|
||||
## 10.5.3 搜尋方法選取
|
||||
|
||||
給定大小為 $n$ 的一組資料,我們可以使用線性搜尋、二分搜尋、樹查詢、雜湊查詢等多種方法從中搜索目標元素。各個方法的工作原理如圖 10-11 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 10-11 多種搜尋策略 </p>
|
||||
|
||||
上述幾種方法的操作效率與特性如表 10-1 所示。
|
||||
|
||||
<p align="center"> 表 10-1 查詢演算法效率對比 </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)$ 效率下穩定執行,但維護樹平衡的操作會增加額外的開銷。
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.6 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 二分搜尋依賴資料的有序性,透過迴圈逐步縮減一半搜尋區間來進行查詢。它要求輸入資料有序,且僅適用於陣列或基於陣列實現的資料結構。
|
||||
- 暴力搜尋透過走訪資料結構來定位資料。線性搜尋適用於陣列和鏈結串列,廣度優先搜尋和深度優先搜尋適用於圖和樹。此類演算法通用性好,無須對資料進行預處理,但時間複雜度 $O(n)$ 較高。
|
||||
- 雜湊查詢、樹查詢和二分搜尋屬於高效搜尋方法,可在特定資料結構中快速定位目標元素。此類演算法效率高,時間複雜度可達 $O(\log n)$ 甚至 $O(1)$ ,但通常需要藉助額外資料結構。
|
||||
- 實際中,我們需要對資料規模、搜尋效能要求、資料查詢和更新頻率等因素進行具體分析,從而選擇合適的搜尋方法。
|
||||
- 線性搜尋適用於小型或頻繁更新的資料;二分搜尋適用於大型、排序的資料;雜湊查詢適用於對查詢效率要求較高且無須範圍查詢的資料;樹查詢適用於需要維護順序和支持範圍查詢的大型動態資料。
|
||||
- 用雜湊查詢替換線性查詢是一種常用的最佳化執行時間的策略,可將時間複雜度從 $O(n)$ 降至 $O(1)$ 。
|
||||
Executable
+607
@@ -0,0 +1,607 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.3 泡沫排序
|
||||
|
||||
<u>泡沫排序(bubble sort)</u>透過連續地比較與交換相鄰元素實現排序。這個過程就像氣泡從底部升到頂部一樣,因此得名泡沫排序。
|
||||
|
||||
如圖 11-4 所示,冒泡過程可以利用元素交換操作來模擬:從陣列最左端開始向右走訪,依次比較相鄰元素大小,如果“左元素 > 右元素”就交換二者。走訪完成後,最大的元素會被移動到陣列的最右端。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-4 利用元素交換操作模擬冒泡 </p>
|
||||
|
||||
## 11.3.1 演算法流程
|
||||
|
||||
設陣列的長度為 $n$ ,泡沫排序的步驟如圖 11-5 所示。
|
||||
|
||||
1. 首先,對 $n$ 個元素執行“冒泡”,**將陣列的最大元素交換至正確位置**。
|
||||
2. 接下來,對剩餘 $n - 1$ 個元素執行“冒泡”,**將第二大元素交換至正確位置**。
|
||||
3. 以此類推,經過 $n - 1$ 輪“冒泡”後,**前 $n - 1$ 大的元素都被交換至正確位置**。
|
||||
4. 僅剩的一個元素必定是最小元素,無須排序,因此陣列排序完成。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-5 泡沫排序流程 </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]
|
||||
nums.swap(j, j + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "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]
|
||||
val temp = nums[j]
|
||||
nums[j] = nums[j + 1]
|
||||
nums[j + 1] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="bubble_sort.rb"
|
||||
### 泡沫排序 ###
|
||||
def bubble_sort(nums)
|
||||
n = nums.length
|
||||
# 外迴圈:未排序區間為 [0, i]
|
||||
for i in (n - 1).downto(1)
|
||||
# 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
|
||||
for j in 0...i
|
||||
if nums[j] > nums[j + 1]
|
||||
# 交換 nums[j] 與 nums[j + 1]
|
||||
nums[j], nums[j + 1] = nums[j + 1], nums[j]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 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%E6%B3%A1%E6%B2%AB%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%E8%BF%B4%E5%9C%88%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%E7%82%BA%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A7%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B0%87%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%20%5B0%2C%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%8F%9B%E8%87%B3%E8%A9%B2%E5%8D%80%E9%96%93%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%8F%9B%20nums%5Bj%5D%20%E8%88%87%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%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E6%B3%A1%E6%B2%AB%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B3%A1%E6%B2%AB%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%E8%BF%B4%E5%9C%88%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%E7%82%BA%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A7%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B0%87%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%20%5B0%2C%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%8F%9B%E8%87%B3%E8%A9%B2%E5%8D%80%E9%96%93%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%8F%9B%20nums%5Bj%5D%20%E8%88%87%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%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E6%B3%A1%E6%B2%AB%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 11.3.2 效率最佳化
|
||||
|
||||
我們發現,如果某輪“冒泡”中沒有執行任何交換操作,說明陣列已經完成排序,可直接返回結果。因此,可以增加一個標誌位 `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]
|
||||
nums.swap(j, j + 1);
|
||||
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]
|
||||
val temp = nums[j]
|
||||
nums[j] = nums[j + 1]
|
||||
nums[j + 1] = temp
|
||||
flag = true // 記錄交換元素
|
||||
}
|
||||
}
|
||||
if (!flag) break // 此輪“冒泡”未交換任何元素,直接跳出
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="bubble_sort.rb"
|
||||
### 泡沫排序(標誌最佳化)###
|
||||
def bubble_sort_with_flag(nums)
|
||||
n = nums.length
|
||||
# 外迴圈:未排序區間為 [0, i]
|
||||
for i in (n - 1).downto(1)
|
||||
flag = false # 初始化標誌位
|
||||
|
||||
# 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端
|
||||
for j in 0...i
|
||||
if nums[j] > nums[j + 1]
|
||||
# 交換 nums[j] 與 nums[j + 1]
|
||||
nums[j], nums[j + 1] = nums[j + 1], nums[j]
|
||||
flag = true # 記錄交換元素
|
||||
end
|
||||
end
|
||||
|
||||
break unless flag # 此輪“冒泡”未交換任何元素,直接跳出
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%E6%B3%A1%E6%B2%AB%E6%8E%92%E5%BA%8F%EF%BC%88%E6%A8%99%E8%AA%8C%E6%9C%80%E4%BD%B3%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%E8%BF%B4%E5%9C%88%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%E7%82%BA%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20flag%20%3D%20False%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A8%99%E8%AA%8C%E4%BD%8D%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A7%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B0%87%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%20%5B0%2C%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%8F%9B%E8%87%B3%E8%A9%B2%E5%8D%80%E9%96%93%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%8F%9B%20nums%5Bj%5D%20%E8%88%87%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%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20flag%20%3D%20True%20%20%23%20%E8%A8%98%E9%8C%84%E4%BA%A4%E6%8F%9B%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%BC%AA%E2%80%9C%E5%86%92%E6%B3%A1%E2%80%9D%E6%9C%AA%E4%BA%A4%E6%8F%9B%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort_with_flag%28nums%29%0A%20%20%20%20print%28%22%E6%B3%A1%E6%B2%AB%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort_with_flag%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B3%A1%E6%B2%AB%E6%8E%92%E5%BA%8F%EF%BC%88%E6%A8%99%E8%AA%8C%E6%9C%80%E4%BD%B3%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%E8%BF%B4%E5%9C%88%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%E7%82%BA%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20flag%20%3D%20False%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A8%99%E8%AA%8C%E4%BD%8D%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A7%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B0%87%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%20%5B0%2C%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%8F%9B%E8%87%B3%E8%A9%B2%E5%8D%80%E9%96%93%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%8F%9B%20nums%5Bj%5D%20%E8%88%87%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%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20flag%20%3D%20True%20%20%23%20%E8%A8%98%E9%8C%84%E4%BA%A4%E6%8F%9B%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%BC%AA%E2%80%9C%E5%86%92%E6%B3%A1%E2%80%9D%E6%9C%AA%E4%BA%A4%E6%8F%9B%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort_with_flag%28nums%29%0A%20%20%20%20print%28%22%E6%B3%A1%E6%B2%AB%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 11.3.3 演算法特性
|
||||
|
||||
- **時間複雜度為 $O(n^2)$、自適應排序**:各輪“冒泡”走訪的陣列長度依次為 $n - 1$、$n - 2$、$\dots$、$2$、$1$ ,總和為 $(n - 1) n / 2$ 。在引入 `flag` 最佳化後,最佳時間複雜度可達到 $O(n)$ 。
|
||||
- **空間複雜度為 $O(1)$、原地排序**:指標 $i$ 和 $j$ 使用常數大小的額外空間。
|
||||
- **穩定排序**:由於在“冒泡”中遇到相等元素不交換。
|
||||
@@ -0,0 +1,471 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.8 桶排序
|
||||
|
||||
前述幾種排序演算法都屬於“基於比較的排序演算法”,它們透過比較元素間的大小來實現排序。此類排序演算法的時間複雜度無法超越 $O(n \log n)$ 。接下來,我們將探討幾種“非比較排序演算法”,它們的時間複雜度可以達到線性階。
|
||||
|
||||
<u>桶排序(bucket sort)</u>是分治策略的一個典型應用。它透過設定一些具有大小順序的桶,每個桶對應一個數據範圍,將資料平均分配到各個桶中;然後,在每個桶內部分別執行排序;最終按照桶的順序將所有資料合併。
|
||||
|
||||
## 11.8.1 演算法流程
|
||||
|
||||
考慮一個長度為 $n$ 的陣列,其元素是範圍 $[0, 1)$ 內的浮點數。桶排序的流程如圖 11-13 所示。
|
||||
|
||||
1. 初始化 $k$ 個桶,將 $n$ 個元素分配到 $k$ 個桶中。
|
||||
2. 對每個桶分別執行排序(這裡採用程式語言的內建排序函式)。
|
||||
3. 按照桶從小到大的順序合併結果。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-13 桶排序演算法流程 </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 &num in nums.iter() {
|
||||
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
|
||||
let i = (num * k as f64) as usize;
|
||||
// 將 num 新增進桶 i
|
||||
buckets[i].push(num);
|
||||
}
|
||||
// 2. 對各個桶執行排序
|
||||
for bucket in &mut buckets {
|
||||
// 使用內建排序函式,也可以替換成其他排序演算法
|
||||
bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
}
|
||||
// 3. 走訪桶合併結果
|
||||
let mut i = 0;
|
||||
for bucket in buckets.iter() {
|
||||
for &num in bucket.iter() {
|
||||
nums[i] = num;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="bucket_sort.c"
|
||||
/* 桶排序 */
|
||||
void bucketSort(float nums[], int n) {
|
||||
int k = n / 2; // 初始化 k = n/2 個桶
|
||||
int *sizes = malloc(k * sizeof(int)); // 記錄每個桶的大小
|
||||
float **buckets = malloc(k * sizeof(float *)); // 動態陣列的陣列(桶)
|
||||
// 為每個桶預分配足夠的空間
|
||||
for (int i = 0; i < k; ++i) {
|
||||
buckets[i] = (float *)malloc(n * sizeof(float));
|
||||
sizes[i] = 0;
|
||||
}
|
||||
// 1. 將陣列元素分配到各個桶中
|
||||
for (int i = 0; i < n; ++i) {
|
||||
int idx = (int)(nums[i] * k);
|
||||
buckets[idx][sizes[idx]++] = nums[i];
|
||||
}
|
||||
// 2. 對各個桶執行排序
|
||||
for (int i = 0; i < k; ++i) {
|
||||
qsort(buckets[i], sizes[i], sizeof(float), compare);
|
||||
}
|
||||
// 3. 合併排序後的桶
|
||||
int idx = 0;
|
||||
for (int i = 0; i < k; ++i) {
|
||||
for (int j = 0; j < sizes[i]; ++j) {
|
||||
nums[idx++] = buckets[i][j];
|
||||
}
|
||||
// 釋放記憶體
|
||||
free(buckets[i]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="bucket_sort.kt"
|
||||
/* 桶排序 */
|
||||
fun bucketSort(nums: FloatArray) {
|
||||
// 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
|
||||
val k = nums.size / 2
|
||||
val buckets = mutableListOf<MutableList<Float>>()
|
||||
for (i in 0..<k) {
|
||||
buckets.add(mutableListOf())
|
||||
}
|
||||
// 1. 將陣列元素分配到各個桶中
|
||||
for (num in nums) {
|
||||
// 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
|
||||
val i = (num * k).toInt()
|
||||
// 將 num 新增進桶 i
|
||||
buckets[i].add(num)
|
||||
}
|
||||
// 2. 對各個桶執行排序
|
||||
for (bucket in buckets) {
|
||||
// 使用內建排序函式,也可以替換成其他排序演算法
|
||||
bucket.sort()
|
||||
}
|
||||
// 3. 走訪桶合併結果
|
||||
var i = 0
|
||||
for (bucket in buckets) {
|
||||
for (num in bucket) {
|
||||
nums[i++] = num
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="bucket_sort.rb"
|
||||
### 桶排序 ###
|
||||
def bucket_sort(nums)
|
||||
# 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素
|
||||
k = nums.length / 2
|
||||
buckets = Array.new(k) { [] }
|
||||
|
||||
# 1. 將陣列元素分配到各個桶中
|
||||
nums.each do |num|
|
||||
# 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1]
|
||||
i = (num * k).to_i
|
||||
# 將 num 新增進桶 i
|
||||
buckets[i] << num
|
||||
end
|
||||
|
||||
# 2. 對各個桶執行排序
|
||||
buckets.each do |bucket|
|
||||
# 使用內建排序函式,也可以替換成其他排序演算法
|
||||
bucket.sort!
|
||||
end
|
||||
|
||||
# 3. 走訪桶合併結果
|
||||
i = 0
|
||||
buckets.each do |bucket|
|
||||
bucket.each do |num|
|
||||
nums[i] = num
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%E5%80%8B%E6%A1%B6%EF%BC%8C%E9%A0%90%E6%9C%9F%E5%90%91%E6%AF%8F%E5%80%8B%E6%A1%B6%E5%88%86%E9%85%8D%202%20%E5%80%8B%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%87%E9%99%A3%E5%88%97%E5%85%83%E7%B4%A0%E5%88%86%E9%85%8D%E5%88%B0%E5%90%84%E5%80%8B%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%BC%B8%E5%85%A5%E8%B3%87%E6%96%99%E7%AF%84%E5%9C%8D%E7%82%BA%20%5B0%2C%201%29%EF%BC%8C%E4%BD%BF%E7%94%A8%20num%20%2A%20k%20%E5%B0%8D%E6%98%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%AF%84%E5%9C%8D%20%5B0%2C%20k-1%5D%0A%20%20%20%20%20%20%20%20i%20%3D%20int%28num%20%2A%20k%29%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%87%20num%20%E6%96%B0%E5%A2%9E%E9%80%B2%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%B0%8D%E5%90%84%E5%80%8B%E6%A1%B6%E5%9F%B7%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%85%A7%E5%BB%BA%E6%8E%92%E5%BA%8F%E5%87%BD%E5%BC%8F%EF%BC%8C%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%9B%BF%E6%8F%9B%E6%88%90%E5%85%B6%E4%BB%96%E6%8E%92%E5%BA%8F%E6%BC%94%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%E8%B5%B0%E8%A8%AA%E6%A1%B6%E5%90%88%E4%BD%B5%E7%B5%90%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%A8%AD%E8%BC%B8%E5%85%A5%E8%B3%87%E6%96%99%E7%82%BA%E6%B5%AE%E9%BB%9E%E6%95%B8%EF%BC%8C%E7%AF%84%E5%9C%8D%E7%82%BA%20%5B0%2C%201%29%0A%20%20%20%20nums%20%3D%20%5B0.49%2C%200.96%2C%200.82%2C%200.09%2C%200.57%2C%200.43%2C%200.91%2C%200.75%2C%200.15%2C%200.37%5D%0A%20%20%20%20bucket_sort%28nums%29%0A%20%20%20%20print%28%22%E6%A1%B6%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bucket_sort%28nums%3A%20list%5Bfloat%5D%29%3A%0A%20%20%20%20%22%22%22%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%E5%80%8B%E6%A1%B6%EF%BC%8C%E9%A0%90%E6%9C%9F%E5%90%91%E6%AF%8F%E5%80%8B%E6%A1%B6%E5%88%86%E9%85%8D%202%20%E5%80%8B%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%87%E9%99%A3%E5%88%97%E5%85%83%E7%B4%A0%E5%88%86%E9%85%8D%E5%88%B0%E5%90%84%E5%80%8B%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%BC%B8%E5%85%A5%E8%B3%87%E6%96%99%E7%AF%84%E5%9C%8D%E7%82%BA%20%5B0%2C%201%29%EF%BC%8C%E4%BD%BF%E7%94%A8%20num%20%2A%20k%20%E5%B0%8D%E6%98%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%AF%84%E5%9C%8D%20%5B0%2C%20k-1%5D%0A%20%20%20%20%20%20%20%20i%20%3D%20int%28num%20%2A%20k%29%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%87%20num%20%E6%96%B0%E5%A2%9E%E9%80%B2%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%B0%8D%E5%90%84%E5%80%8B%E6%A1%B6%E5%9F%B7%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%85%A7%E5%BB%BA%E6%8E%92%E5%BA%8F%E5%87%BD%E5%BC%8F%EF%BC%8C%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%9B%BF%E6%8F%9B%E6%88%90%E5%85%B6%E4%BB%96%E6%8E%92%E5%BA%8F%E6%BC%94%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%E8%B5%B0%E8%A8%AA%E6%A1%B6%E5%90%88%E4%BD%B5%E7%B5%90%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%A8%AD%E8%BC%B8%E5%85%A5%E8%B3%87%E6%96%99%E7%82%BA%E6%B5%AE%E9%BB%9E%E6%95%B8%EF%BC%8C%E7%AF%84%E5%9C%8D%E7%82%BA%20%5B0%2C%201%29%0A%20%20%20%20nums%20%3D%20%5B0.49%2C%200.96%2C%200.82%2C%200.09%2C%200.57%2C%200.43%2C%200.91%2C%200.75%2C%200.15%2C%200.37%5D%0A%20%20%20%20bucket_sort%28nums%29%0A%20%20%20%20print%28%22%E6%A1%B6%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 11.8.2 演算法特性
|
||||
|
||||
桶排序適用於處理體量很大的資料。例如,輸入資料包含 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 如何實現平均分配
|
||||
|
||||
桶排序的時間複雜度理論上可以達到 $O(n)$ ,**關鍵在於將元素均勻分配到各個桶中**,因為實際資料往往不是均勻分佈的。例如,我們想要將淘寶上的所有商品按價格範圍平均分配到 10 個桶中,但商品價格分佈不均,低於 100 元的非常多,高於 1000 元的非常少。若將價格區間平均劃分為 10 個,各個桶中的商品數量差距會非常大。
|
||||
|
||||
為實現平均分配,我們可以先設定一條大致的分界線,將資料粗略地分到 3 個桶中。**分配完畢後,再將商品較多的桶繼續劃分為 3 個桶,直至所有桶中的元素數量大致相等**。
|
||||
|
||||
如圖 11-14 所示,這種方法本質上是建立一棵遞迴樹,目標是讓葉節點的值儘可能平均。當然,不一定要每輪將資料劃分為 3 個桶,具體劃分方式可根據資料特點靈活選擇。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-14 遞迴劃分桶 </p>
|
||||
|
||||
如果我們提前知道商品價格的機率分佈,**則可以根據資料機率分佈設定每個桶的價格分界線**。值得注意的是,資料分佈並不一定需要特意統計,也可以根據資料特點採用某種機率模型進行近似。
|
||||
|
||||
如圖 11-15 所示,我們假設商品價格服從正態分佈,這樣就可以合理地設定價格區間,從而將商品平均分配到各個桶中。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-15 根據機率分佈劃分桶 </p>
|
||||
@@ -0,0 +1,897 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.9 計數排序
|
||||
|
||||
<u>計數排序(counting sort)</u>透過統計元素數量來實現排序,通常應用於整數陣列。
|
||||
|
||||
## 11.9.1 簡單實現
|
||||
|
||||
先來看一個簡單的例子。給定一個長度為 $n$ 的陣列 `nums` ,其中的元素都是“非負整數”,計數排序的整體流程如圖 11-16 所示。
|
||||
|
||||
1. 走訪陣列,找出其中的最大數字,記為 $m$ ,然後建立一個長度為 $m + 1$ 的輔助陣列 `counter` 。
|
||||
2. **藉助 `counter` 統計 `nums` 中各數字的出現次數**,其中 `counter[num]` 對應數字 `num` 的出現次數。統計方法很簡單,只需走訪 `nums`(設當前數字為 `num`),每輪將 `counter[num]` 增加 $1$ 即可。
|
||||
3. **由於 `counter` 的各個索引天然有序,因此相當於所有數字已經排序好了**。接下來,我們走訪 `counter` ,根據各數字出現次數從小到大的順序填入 `nums` 即可。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-16 計數排序流程 </p>
|
||||
|
||||
程式碼如下所示:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="counting_sort.py"
|
||||
def counting_sort_naive(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 ,將各元素填入原陣列 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 = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. 走訪 counter ,將各元素填入原陣列 nums
|
||||
let i = 0;
|
||||
for (let num = 0; num < m + 1; num++) {
|
||||
for (let j = 0; j < counter[num]; j++, i++) {
|
||||
nums[i] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="counting_sort.ts"
|
||||
/* 計數排序 */
|
||||
// 簡單實現,無法用於排序物件
|
||||
function countingSortNaive(nums: number[]): void {
|
||||
// 1. 統計陣列最大元素 m
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. 走訪 counter ,將各元素填入原陣列 nums
|
||||
let i = 0;
|
||||
for (let num = 0; num < m + 1; num++) {
|
||||
for (let j = 0; j < counter[num]; j++, i++) {
|
||||
nums[i] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="counting_sort.dart"
|
||||
/* 計數排序 */
|
||||
// 簡單實現,無法用於排序物件
|
||||
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.iter().max().unwrap();
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
let mut counter = vec![0; m as usize + 1];
|
||||
for &num in nums.iter() {
|
||||
counter[num as usize] += 1;
|
||||
}
|
||||
// 3. 走訪 counter ,將各元素填入原陣列 nums
|
||||
let mut i = 0;
|
||||
for num in 0..m + 1 {
|
||||
for _ in 0..counter[num as usize] {
|
||||
nums[i] = num;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="counting_sort.c"
|
||||
/* 計數排序 */
|
||||
// 簡單實現,無法用於排序物件
|
||||
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, num)
|
||||
}
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
val counter = IntArray(m + 1)
|
||||
for (num in nums) {
|
||||
counter[num]++
|
||||
}
|
||||
// 3. 走訪 counter ,將各元素填入原陣列 nums
|
||||
var i = 0
|
||||
for (num in 0..<m + 1) {
|
||||
var j = 0
|
||||
while (j < counter[num]) {
|
||||
nums[i] = num
|
||||
j++
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="counting_sort.rb"
|
||||
### 計數排序 ###
|
||||
def counting_sort_naive(nums)
|
||||
# 簡單實現,無法用於排序物件
|
||||
# 1. 統計陣列最大元素 m
|
||||
m = 0
|
||||
nums.each { |num| m = [m, num].max }
|
||||
# 2. 統計各數字的出現次數
|
||||
# counter[num] 代表 num 的出現次數
|
||||
counter = Array.new(m + 1, 0)
|
||||
nums.each { |num| counter[num] += 1 }
|
||||
# 3. 走訪 counter ,將各元素填入原陣列 nums
|
||||
i = 0
|
||||
for num in 0...(m + 1)
|
||||
(0...counter[num]).each do
|
||||
nums[i] = num
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort_naive%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%B0%A1%E5%96%AE%E5%AF%A6%E7%8F%BE%EF%BC%8C%E7%84%A1%E6%B3%95%E7%94%A8%E6%96%BC%E6%8E%92%E5%BA%8F%E7%89%A9%E4%BB%B6%0A%20%20%20%20%23%201.%20%E7%B5%B1%E8%A8%88%E9%99%A3%E5%88%97%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%2C%20num%29%0A%20%20%20%20%23%202.%20%E7%B5%B1%E8%A8%88%E5%90%84%E6%95%B8%E5%AD%97%E7%9A%84%E5%87%BA%E7%8F%BE%E6%AC%A1%E6%95%B8%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%8F%BE%E6%AC%A1%E6%95%B8%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E8%B5%B0%E8%A8%AA%20counter%20%EF%BC%8C%E5%B0%87%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E5%8E%9F%E9%99%A3%E5%88%97%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%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort_naive%28nums%29%0A%20%20%20%20print%28f%22%E8%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%EF%BC%88%E7%84%A1%E6%B3%95%E6%8E%92%E5%BA%8F%E7%89%A9%E4%BB%B6%EF%BC%89%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort_naive%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%B0%A1%E5%96%AE%E5%AF%A6%E7%8F%BE%EF%BC%8C%E7%84%A1%E6%B3%95%E7%94%A8%E6%96%BC%E6%8E%92%E5%BA%8F%E7%89%A9%E4%BB%B6%0A%20%20%20%20%23%201.%20%E7%B5%B1%E8%A8%88%E9%99%A3%E5%88%97%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%2C%20num%29%0A%20%20%20%20%23%202.%20%E7%B5%B1%E8%A8%88%E5%90%84%E6%95%B8%E5%AD%97%E7%9A%84%E5%87%BA%E7%8F%BE%E6%AC%A1%E6%95%B8%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%8F%BE%E6%AC%A1%E6%95%B8%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E8%B5%B0%E8%A8%AA%20counter%20%EF%BC%8C%E5%B0%87%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E5%8E%9F%E9%99%A3%E5%88%97%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%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort_naive%28nums%29%0A%20%20%20%20print%28f%22%E8%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%EF%BC%88%E7%84%A1%E6%B3%95%E6%8E%92%E5%BA%8F%E7%89%A9%E4%BB%B6%EF%BC%89%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
!!! note "計數排序與桶排序的關聯"
|
||||
|
||||
從桶排序的角度看,我們可以將計數排序中的計數陣列 `counter` 的每個索引視為一個桶,將統計數量的過程看作將各個元素分配到對應的桶中。本質上,計數排序是桶排序在整型資料下的一個特例。
|
||||
|
||||
## 11.9.2 完整實現
|
||||
|
||||
細心的讀者可能發現了,**如果輸入資料是物件,上述步驟 `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>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-17 計數排序步驟 </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 = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
|
||||
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
|
||||
for (let i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
|
||||
// 初始化陣列 res 用於記錄結果
|
||||
const n = nums.length;
|
||||
const res = new Array(n);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const num = nums[i];
|
||||
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
|
||||
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
|
||||
}
|
||||
// 使用結果陣列 res 覆蓋原陣列 nums
|
||||
for (let i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="counting_sort.ts"
|
||||
/* 計數排序 */
|
||||
// 完整實現,可排序物件,並且是穩定排序
|
||||
function countingSort(nums: number[]): void {
|
||||
// 1. 統計陣列最大元素 m
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
|
||||
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
|
||||
for (let i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
|
||||
// 初始化陣列 res 用於記錄結果
|
||||
const n = nums.length;
|
||||
const res: number[] = new Array<number>(n);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const num = nums[i];
|
||||
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
|
||||
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
|
||||
}
|
||||
// 使用結果陣列 res 覆蓋原陣列 nums
|
||||
for (let i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="counting_sort.dart"
|
||||
/* 計數排序 */
|
||||
// 完整實現,可排序物件,並且是穩定排序
|
||||
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.iter().max().unwrap() as usize;
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
let mut counter = vec![0; m + 1];
|
||||
for &num in nums.iter() {
|
||||
counter[num as usize] += 1;
|
||||
}
|
||||
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
|
||||
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
|
||||
for i in 0..m {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
|
||||
// 初始化陣列 res 用於記錄結果
|
||||
let n = nums.len();
|
||||
let mut res = vec![0; n];
|
||||
for i in (0..n).rev() {
|
||||
let num = nums[i];
|
||||
res[counter[num as usize] - 1] = num; // 將 num 放置到對應索引處
|
||||
counter[num as usize] -= 1; // 令前綴和自減 1 ,得到下次放置 num 的索引
|
||||
}
|
||||
// 使用結果陣列 res 覆蓋原陣列 nums
|
||||
nums.copy_from_slice(&res)
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="counting_sort.c"
|
||||
/* 計數排序 */
|
||||
// 完整實現,可排序物件,並且是穩定排序
|
||||
void countingSort(int nums[], int size) {
|
||||
// 1. 統計陣列最大元素 m
|
||||
int m = 0;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (nums[i] > m) {
|
||||
m = nums[i];
|
||||
}
|
||||
}
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
int *counter = calloc(m, sizeof(int));
|
||||
for (int i = 0; i < size; i++) {
|
||||
counter[nums[i]]++;
|
||||
}
|
||||
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
|
||||
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
|
||||
for (int i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
|
||||
// 初始化陣列 res 用於記錄結果
|
||||
int *res = malloc(sizeof(int) * size);
|
||||
for (int i = size - 1; i >= 0; i--) {
|
||||
int num = nums[i];
|
||||
res[counter[num] - 1] = num; // 將 num 放置到對應索引處
|
||||
counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引
|
||||
}
|
||||
// 使用結果陣列 res 覆蓋原陣列 nums
|
||||
memcpy(nums, res, size * sizeof(int));
|
||||
// 5. 釋放記憶體
|
||||
free(res);
|
||||
free(counter);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="counting_sort.kt"
|
||||
/* 計數排序 */
|
||||
// 完整實現,可排序物件,並且是穩定排序
|
||||
fun countingSort(nums: IntArray) {
|
||||
// 1. 統計陣列最大元素 m
|
||||
var m = 0
|
||||
for (num in nums) {
|
||||
m = max(m, num)
|
||||
}
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
val counter = IntArray(m + 1)
|
||||
for (num in nums) {
|
||||
counter[num]++
|
||||
}
|
||||
// 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
|
||||
// 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
|
||||
for (i in 0..<m) {
|
||||
counter[i + 1] += counter[i]
|
||||
}
|
||||
// 4. 倒序走訪 nums ,將各元素填入結果陣列 res
|
||||
// 初始化陣列 res 用於記錄結果
|
||||
val n = nums.size
|
||||
val res = IntArray(n)
|
||||
for (i in n - 1 downTo 0) {
|
||||
val num = nums[i]
|
||||
res[counter[num] - 1] = num // 將 num 放置到對應索引處
|
||||
counter[num]-- // 令前綴和自減 1 ,得到下次放置 num 的索引
|
||||
}
|
||||
// 使用結果陣列 res 覆蓋原陣列 nums
|
||||
for (i in 0..<n) {
|
||||
nums[i] = res[i]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="counting_sort.rb"
|
||||
### 計數排序 ###
|
||||
def counting_sort(nums)
|
||||
# 完整實現,可排序物件,並且是穩定排序
|
||||
# 1. 統計陣列最大元素 m
|
||||
m = nums.max
|
||||
# 2. 統計各數字的出現次數
|
||||
# counter[num] 代表 num 的出現次數
|
||||
counter = Array.new(m + 1, 0)
|
||||
nums.each { |num| counter[num] += 1 }
|
||||
# 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引”
|
||||
# 即 counter[num]-1 是 num 在 res 中最後一次出現的索引
|
||||
(0...m).each { |i| counter[i + 1] += counter[i] }
|
||||
# 4. 倒序走訪 nums, 將各元素填入結果陣列 res
|
||||
# 初始化陣列 res 用於記錄結果
|
||||
n = nums.length
|
||||
res = Array.new(n, 0)
|
||||
(n - 1).downto(0).each do |i|
|
||||
num = nums[i]
|
||||
res[counter[num] - 1] = num # 將 num 放置到對應索引處
|
||||
counter[num] -= 1 # 令前綴和自減 1 ,得到下次放置 num 的索引
|
||||
end
|
||||
# 使用結果陣列 res 覆蓋原陣列 nums
|
||||
(0...n).each { |i| nums[i] = res[i] }
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%AE%8C%E6%95%B4%E5%AF%A6%E7%8F%BE%EF%BC%8C%E5%8F%AF%E6%8E%92%E5%BA%8F%E7%89%A9%E4%BB%B6%EF%BC%8C%E4%B8%A6%E4%B8%94%E6%98%AF%E7%A9%A9%E5%AE%9A%E6%8E%92%E5%BA%8F%0A%20%20%20%20%23%201.%20%E7%B5%B1%E8%A8%88%E9%99%A3%E5%88%97%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%B5%B1%E8%A8%88%E5%90%84%E6%95%B8%E5%AD%97%E7%9A%84%E5%87%BA%E7%8F%BE%E6%AC%A1%E6%95%B8%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%8F%BE%E6%AC%A1%E6%95%B8%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E6%B1%82%20counter%20%E7%9A%84%E5%89%8D%E7%B6%B4%E5%92%8C%EF%BC%8C%E5%B0%87%E2%80%9C%E5%87%BA%E7%8F%BE%E6%AC%A1%E6%95%B8%E2%80%9D%E8%BD%89%E6%8F%9B%E7%82%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%BE%8C%E4%B8%80%E6%AC%A1%E5%87%BA%E7%8F%BE%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%E8%B5%B0%E8%A8%AA%20nums%20%EF%BC%8C%E5%B0%87%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E7%B5%90%E6%9E%9C%E9%99%A3%E5%88%97%20res%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%99%A3%E5%88%97%20res%20%E7%94%A8%E6%96%BC%E8%A8%98%E9%8C%84%E7%B5%90%E6%9E%9C%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20res%20%3D%20%5B0%5D%20%2A%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20num%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20res%5Bcounter%5Bnum%5D%20-%201%5D%20%3D%20num%20%20%23%20%E5%B0%87%20num%20%E6%94%BE%E7%BD%AE%E5%88%B0%E5%B0%8D%E6%87%89%E7%B4%A2%E5%BC%95%E8%99%95%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%B6%B4%E5%92%8C%E8%87%AA%E6%B8%9B%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%B5%90%E6%9E%9C%E9%99%A3%E5%88%97%20res%20%E8%A6%86%E8%93%8B%E5%8E%9F%E9%99%A3%E5%88%97%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%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort%28nums%29%0A%20%20%20%20print%28f%22%E8%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20counting_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E8%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%AE%8C%E6%95%B4%E5%AF%A6%E7%8F%BE%EF%BC%8C%E5%8F%AF%E6%8E%92%E5%BA%8F%E7%89%A9%E4%BB%B6%EF%BC%8C%E4%B8%A6%E4%B8%94%E6%98%AF%E7%A9%A9%E5%AE%9A%E6%8E%92%E5%BA%8F%0A%20%20%20%20%23%201.%20%E7%B5%B1%E8%A8%88%E9%99%A3%E5%88%97%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%B5%B1%E8%A8%88%E5%90%84%E6%95%B8%E5%AD%97%E7%9A%84%E5%87%BA%E7%8F%BE%E6%AC%A1%E6%95%B8%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%8F%BE%E6%AC%A1%E6%95%B8%0A%20%20%20%20counter%20%3D%20%5B0%5D%20%2A%20%28m%20%2B%201%29%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20counter%5Bnum%5D%20%2B%3D%201%0A%20%20%20%20%23%203.%20%E6%B1%82%20counter%20%E7%9A%84%E5%89%8D%E7%B6%B4%E5%92%8C%EF%BC%8C%E5%B0%87%E2%80%9C%E5%87%BA%E7%8F%BE%E6%AC%A1%E6%95%B8%E2%80%9D%E8%BD%89%E6%8F%9B%E7%82%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%BE%8C%E4%B8%80%E6%AC%A1%E5%87%BA%E7%8F%BE%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%E8%B5%B0%E8%A8%AA%20nums%20%EF%BC%8C%E5%B0%87%E5%90%84%E5%85%83%E7%B4%A0%E5%A1%AB%E5%85%A5%E7%B5%90%E6%9E%9C%E9%99%A3%E5%88%97%20res%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%99%A3%E5%88%97%20res%20%E7%94%A8%E6%96%BC%E8%A8%98%E9%8C%84%E7%B5%90%E6%9E%9C%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20res%20%3D%20%5B0%5D%20%2A%20n%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20num%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20res%5Bcounter%5Bnum%5D%20-%201%5D%20%3D%20num%20%20%23%20%E5%B0%87%20num%20%E6%94%BE%E7%BD%AE%E5%88%B0%E5%B0%8D%E6%87%89%E7%B4%A2%E5%BC%95%E8%99%95%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%B6%B4%E5%92%8C%E8%87%AA%E6%B8%9B%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%B5%90%E6%9E%9C%E9%99%A3%E5%88%97%20res%20%E8%A6%86%E8%93%8B%E5%8E%9F%E9%99%A3%E5%88%97%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%2C%200%2C%201%2C%202%2C%200%2C%204%2C%200%2C%202%2C%202%2C%204%5D%0A%20%20%20%20counting_sort%28nums%29%0A%20%20%20%20print%28f%22%E8%A8%88%E6%95%B8%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%20%7Bnums%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 11.9.3 演算法特性
|
||||
|
||||
- **時間複雜度為 $O(n + m)$、非自適應排序** :涉及走訪 `nums` 和走訪 `counter` ,都使用線性時間。一般情況下 $n \gg m$ ,時間複雜度趨於 $O(n)$ 。
|
||||
- **空間複雜度為 $O(n + m)$、非原地排序**:藉助了長度分別為 $n$ 和 $m$ 的陣列 `res` 和 `counter` 。
|
||||
- **穩定排序**:由於向 `res` 中填充元素的順序是“從右向左”的,因此倒序走訪 `nums` 可以避免改變相等元素之間的相對位置,從而實現穩定排序。實際上,正序走訪 `nums` 也可以得到正確的排序結果,但結果是非穩定的。
|
||||
|
||||
## 11.9.4 侷限性
|
||||
|
||||
看到這裡,你也許會覺得計數排序非常巧妙,僅透過統計數量就可以實現高效的排序。然而,使用計數排序的前置條件相對較為嚴格。
|
||||
|
||||
**計數排序只適用於非負整數**。若想將其用於其他型別的資料,需要確保這些資料可以轉換為非負整數,並且在轉換過程中不能改變各個元素之間的相對大小關係。例如,對於包含負數的整數陣列,可以先給所有數字加上一個常數,將全部數字轉化為正數,排序完成後再轉換回去。
|
||||
|
||||
**計數排序適用於資料量大但資料範圍較小的情況**。比如,在上述示例中 $m$ 不能太大,否則會佔用過多空間。而當 $n \ll m$ 時,計數排序使用 $O(m)$ 時間,可能比 $O(n \log n)$ 的排序演算法還要慢。
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
comments: true
|
||||
hide: [edit]
|
||||
---
|
||||
|
||||
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
|
||||
|
||||
# 11.12 練習
|
||||
|
||||
## 11.12.1 知識鞏固
|
||||
|
||||
### 1. 選擇排序和泡沫排序的前幾輪
|
||||
|
||||
給定陣列 `[4, 2, 5, 1, 3]`,以下都按從小到大排序。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 按選擇排序模擬前兩輪,寫出每輪結束後的陣列,並指出此時哪些位置已經確定。
|
||||
2. 按泡沫排序模擬第一輪,寫出陣列狀態和交換次數,並指出哪個位置已經確定。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 前兩輪為:
|
||||
|
||||
| 輪次 | 陣列狀態 | 說明 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `[1, 2, 5, 4, 3]` | 最小元素 1 與首位交換 |
|
||||
| 2 | `[1, 2, 5, 4, 3]` | 數值 2 已位於索引 1,無須交換 |
|
||||
|
||||
此時前兩個位置已經確定,後續只需在 `[5, 4, 3]` 中繼續選擇最小元素。
|
||||
|
||||
2. 依次比較相鄰元素:4 與 2 交換、4 與 5 不換、5 與 1 交換、5 與 3 交換,
|
||||
得 `[2, 4, 1, 3, 5]` ,共 3 次交換;最大元素 5 已移動到陣列末尾,因此最後一個位置已經確定。
|
||||
|
||||
### 2. 相同元素的先後次序會變嗎
|
||||
|
||||
陣列 $[2_a, 2_b, 1]$ 中,$2_a$ 和 $2_b$ 的數值相同,但用下標標出了原有次序。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 寫出選擇排序第一輪結束後的陣列。$2_a$ 和 $2_b$ 的相對次序是否改變?
|
||||
2. 寫出泡沫排序第一輪結束後的陣列。$2_a$ 和 $2_b$ 的相對次序是否改變?
|
||||
3. 根據前兩問,說明兩種排序在保持相等元素原有次序方面有什麼不同。
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 選擇排序第一輪選出最小元素 1,並與首位 $2_a$ 交換,得
|
||||
$[1, 2_b, 2_a]$ 。$2_a$ 被換到了 $2_b$ 之後,兩者的相對次序已經改變。
|
||||
|
||||
2. 泡沫排序先比較 $2_a$ 和 $2_b$,因為兩者相等,不交換;再比較 $2_b$ 和 1 並交換,
|
||||
第一輪結束後為 $[2_a, 1, 2_b]$ 。$2_a$ 仍在 $2_b$ 之前,相對次序沒有改變。
|
||||
|
||||
3. 這個例子中,選擇排序會改變相等元素的原有次序;泡沫排序只在左邊元素大於右邊元素時
|
||||
交換相鄰元素。相等元素之間不交換,因此能保持它們原有的先後次序。
|
||||
|
||||
### 3. 比較計數排序和基數排序
|
||||
|
||||
學校要排序許多固定為 8 位的學號。請回答:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 從最低位開始的基數排序需要處理幾輪?
|
||||
2. 如果把學號直接當作整數使用計數排序,為什麼會準備大量沒有用到的計數位置?
|
||||
3. 根據前兩問,如果在計數排序和基數排序之間選擇一種處理大量固定為 8 位的學號,你會選擇哪一種?為什麼?
|
||||
|
||||
??? success "參考答案"
|
||||
|
||||
1. 學號有 8 個數位,因此從最低位到最高位共處理 8 輪;每輪只按 0~9 分組。
|
||||
|
||||
2. 直接計數需要為所有可能的 8 位取值預留位置,但實際學生只使用其中很少一部分,
|
||||
大多數計數位置會一直為 0。
|
||||
|
||||
3. 應選擇基數排序。它利用了“位數固定、每位只有 10 種取值”的特點,只需重複 8 輪穩定分組。
|
||||
計數排序若把整個 8 位學號當作整數下標,則需要為大量實際不會出現的數值預留計數位置。
|
||||
|
||||
## 11.12.2 程式設計練習
|
||||
|
||||
### 1. 用合併排序排列陣列
|
||||
|
||||
給定一個整數陣列 `nums`,請自行實現合併排序,把其中的元素按非遞減順序排列並返回。不要呼叫語言自帶的排序函式。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 區間長度不超過 1 時,它已經有序
|
||||
2. 從中點把區間分成兩半,並分別遞迴排序
|
||||
3. 用兩個指標合併兩個有序半區,再把結果寫回原陣列
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 用計數排序排列整數陣列
|
||||
|
||||
給定整數陣列 `nums` 和非負整數 $K$,陣列中的每個元素都在 $0$ 到 $K$ 之間。
|
||||
|
||||
請實現計數排序,把結果按非遞減順序寫回 `nums` 並返回 `nums`。
|
||||
不要使用元素之間的大小比較來決定順序,也不要呼叫語言自帶的排序函式。
|
||||
|
||||
??? tip "解題提示"
|
||||
|
||||
1. 因為每個元素都在 0 到 K 之間,可以直接用元素值作為計數陣列的索引
|
||||
2. 第一遍掃描 nums,令對應位置的計數加 1
|
||||
3. 再從 0 到 K 掃描計數陣列;數值 x 出現多少次,就向 nums 中連續寫入多少個 x
|
||||
@@ -0,0 +1,624 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.7 堆積排序
|
||||
|
||||
!!! tip
|
||||
|
||||
閱讀本節前,請確保已學完“堆積”章節。
|
||||
|
||||
<u>堆積排序(heap sort)</u>是一種基於堆積資料結構實現的高效排序演算法。我們可以利用已經學過的“建堆積操作”和“元素出堆積操作”實現堆積排序。
|
||||
|
||||
1. 輸入陣列並建立小頂堆積,此時最小元素位於堆積頂。
|
||||
2. 不斷執行出堆積操作,依次記錄出堆積元素,即可得到從小到大排序的序列。
|
||||
|
||||
以上方法雖然可行,但需要藉助一個額外陣列來儲存彈出的元素,比較浪費空間。在實際中,我們通常使用一種更加優雅的實現方式。
|
||||
|
||||
## 11.7.1 演算法流程
|
||||
|
||||
設陣列的長度為 $n$ ,堆積排序的流程如圖 11-12 所示。
|
||||
|
||||
1. 輸入陣列並建立大頂堆積。完成後,最大元素位於堆積頂。
|
||||
2. 將堆積頂元素(第一個元素)與堆積底元素(最後一個元素)交換。完成交換後,堆積的長度減 $1$ ,已排序元素數量加 $1$ 。
|
||||
3. 從堆積頂元素開始,從頂到底執行堆積化操作(sift down)。完成堆積化後,堆積的性質得到修復。
|
||||
4. 迴圈執行第 `2.` 步和第 `3.` 步。迴圈 $n - 1$ 輪後,即可完成陣列排序。
|
||||
|
||||
!!! tip
|
||||
|
||||
實際上,元素出堆積操作中也包含第 `2.` 步和第 `3.` 步,只是多了一個彈出元素的步驟。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<10>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<11>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<12>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-12 堆積排序步驟 </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;
|
||||
}
|
||||
// 交換兩節點
|
||||
nums.swap(i, ma);
|
||||
// 迴圈向下堆積化
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
/* 堆積排序 */
|
||||
fn heap_sort(nums: &mut [i32]) {
|
||||
// 建堆積操作:堆積化除葉節點以外的其他所有節點
|
||||
for i in (0..nums.len() / 2).rev() {
|
||||
sift_down(nums, nums.len(), i);
|
||||
}
|
||||
// 從堆積中提取最大元素,迴圈 n-1 輪
|
||||
for i in (1..nums.len()).rev() {
|
||||
// 交換根節點與最右葉節點(交換首元素與尾元素)
|
||||
nums.swap(0, i);
|
||||
// 以根節點為起點,從頂至底進行堆積化
|
||||
sift_down(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="heap_sort.c"
|
||||
/* 堆積的長度為 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
|
||||
// 交換兩節點
|
||||
val temp = nums[i]
|
||||
nums[i] = nums[ma]
|
||||
nums[ma] = temp
|
||||
// 迴圈向下堆積化
|
||||
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) {
|
||||
// 交換根節點與最右葉節點(交換首元素與尾元素)
|
||||
val temp = nums[0]
|
||||
nums[0] = nums[i]
|
||||
nums[i] = temp
|
||||
// 以根節點為起點,從頂至底進行堆積化
|
||||
siftDown(nums, i, 0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="heap_sort.rb"
|
||||
### 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 ###
|
||||
def sift_down(nums, n, i)
|
||||
while true
|
||||
# 判斷節點 i, l, r 中值最大的節點,記為 ma
|
||||
l = 2 * i + 1
|
||||
r = 2 * i + 2
|
||||
ma = i
|
||||
ma = l if l < n && nums[l] > nums[ma]
|
||||
ma = r if r < n && nums[r] > nums[ma]
|
||||
# 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
|
||||
break if ma == i
|
||||
# 交換兩節點
|
||||
nums[i], nums[ma] = nums[ma], nums[i]
|
||||
# 迴圈向下堆積化
|
||||
i = ma
|
||||
end
|
||||
end
|
||||
|
||||
### 堆積排序 ###
|
||||
def heap_sort(nums)
|
||||
# 建堆積操作:堆積化除葉節點以外的其他所有節點
|
||||
(nums.length / 2 - 1).downto(0) do |i|
|
||||
sift_down(nums, nums.length, i)
|
||||
end
|
||||
# 從堆積中提取最大元素,迴圈 n-1 輪
|
||||
(nums.length - 1).downto(1) do |i|
|
||||
# 交換根節點與最右葉節點(交換首元素與尾元素)
|
||||
nums[0], nums[i] = nums[i], nums[0]
|
||||
# 以根節點為起點,從頂至底進行堆積化
|
||||
sift_down(nums, i, 0)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%2C%20n%3A%20int%2C%20i%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%A0%86%E7%A9%8D%E7%9A%84%E9%95%B7%E5%BA%A6%E7%82%BA%20n%20%EF%BC%8C%E5%BE%9E%E7%AF%80%E9%BB%9E%20i%20%E9%96%8B%E5%A7%8B%EF%BC%8C%E5%BE%9E%E9%A0%82%E8%87%B3%E5%BA%95%E5%A0%86%E7%A9%8D%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%B7%E7%AF%80%E9%BB%9E%20i%2C%20l%2C%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E7%AF%80%E9%BB%9E%EF%BC%8C%E8%A8%98%E7%82%BA%20ma%0A%20%20%20%20%20%20%20%20l%20%3D%202%20%2A%20i%20%2B%201%0A%20%20%20%20%20%20%20%20r%20%3D%202%20%2A%20i%20%2B%202%0A%20%20%20%20%20%20%20%20ma%20%3D%20i%0A%20%20%20%20%20%20%20%20if%20l%20%3C%20n%20and%20nums%5Bl%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20if%20r%20%3C%20n%20and%20nums%5Br%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E7%AF%80%E9%BB%9E%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l%2C%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%89%87%E7%84%A1%E9%A0%88%E7%B9%BC%E7%BA%8C%E5%A0%86%E7%A9%8D%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%8F%9B%E5%85%A9%E7%AF%80%E9%BB%9E%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bma%5D%20%3D%20nums%5Bma%5D%2C%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E5%90%91%E4%B8%8B%E5%A0%86%E7%A9%8D%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%E7%A9%8D%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%BB%BA%E5%A0%86%E7%A9%8D%E6%93%8D%E4%BD%9C%EF%BC%9A%E5%A0%86%E7%A9%8D%E5%8C%96%E9%99%A4%E8%91%89%E7%AF%80%E9%BB%9E%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E7%AF%80%E9%BB%9E%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20//%202%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20len%28nums%29%2C%20i%29%0A%20%20%20%20%23%20%E5%BE%9E%E5%A0%86%E7%A9%8D%E4%B8%AD%E6%8F%90%E5%8F%96%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%B4%E5%9C%88%20n-1%20%E8%BC%AA%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8F%9B%E6%A0%B9%E7%AF%80%E9%BB%9E%E8%88%87%E6%9C%80%E5%8F%B3%E8%91%89%E7%AF%80%E9%BB%9E%EF%BC%88%E4%BA%A4%E6%8F%9B%E9%A6%96%E5%85%83%E7%B4%A0%E8%88%87%E5%B0%BE%E5%85%83%E7%B4%A0%EF%BC%89%0A%20%20%20%20%20%20%20%20nums%5B0%5D%2C%20nums%5Bi%5D%20%3D%20nums%5Bi%5D%2C%20nums%5B0%5D%0A%20%20%20%20%20%20%20%20%23%20%E4%BB%A5%E6%A0%B9%E7%AF%80%E9%BB%9E%E7%82%BA%E8%B5%B7%E9%BB%9E%EF%BC%8C%E5%BE%9E%E9%A0%82%E8%87%B3%E5%BA%95%E9%80%B2%E8%A1%8C%E5%A0%86%E7%A9%8D%E5%8C%96%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20i%2C%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20heap_sort%28nums%29%0A%20%20%20%20print%28%22%E5%A0%86%E7%A9%8D%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20sift_down%28nums%3A%20list%5Bint%5D%2C%20n%3A%20int%2C%20i%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%A0%86%E7%A9%8D%E7%9A%84%E9%95%B7%E5%BA%A6%E7%82%BA%20n%20%EF%BC%8C%E5%BE%9E%E7%AF%80%E9%BB%9E%20i%20%E9%96%8B%E5%A7%8B%EF%BC%8C%E5%BE%9E%E9%A0%82%E8%87%B3%E5%BA%95%E5%A0%86%E7%A9%8D%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%B7%E7%AF%80%E9%BB%9E%20i%2C%20l%2C%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E7%AF%80%E9%BB%9E%EF%BC%8C%E8%A8%98%E7%82%BA%20ma%0A%20%20%20%20%20%20%20%20l%20%3D%202%20%2A%20i%20%2B%201%0A%20%20%20%20%20%20%20%20r%20%3D%202%20%2A%20i%20%2B%202%0A%20%20%20%20%20%20%20%20ma%20%3D%20i%0A%20%20%20%20%20%20%20%20if%20l%20%3C%20n%20and%20nums%5Bl%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20if%20r%20%3C%20n%20and%20nums%5Br%5D%20%3E%20nums%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E7%AF%80%E9%BB%9E%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l%2C%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%89%87%E7%84%A1%E9%A0%88%E7%B9%BC%E7%BA%8C%E5%A0%86%E7%A9%8D%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%8F%9B%E5%85%A9%E7%AF%80%E9%BB%9E%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bma%5D%20%3D%20nums%5Bma%5D%2C%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20%23%20%E8%BF%B4%E5%9C%88%E5%90%91%E4%B8%8B%E5%A0%86%E7%A9%8D%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%E7%A9%8D%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%BB%BA%E5%A0%86%E7%A9%8D%E6%93%8D%E4%BD%9C%EF%BC%9A%E5%A0%86%E7%A9%8D%E5%8C%96%E9%99%A4%E8%91%89%E7%AF%80%E9%BB%9E%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E7%AF%80%E9%BB%9E%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20//%202%20-%201%2C%20-1%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20len%28nums%29%2C%20i%29%0A%20%20%20%20%23%20%E5%BE%9E%E5%A0%86%E7%A9%8D%E4%B8%AD%E6%8F%90%E5%8F%96%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%B4%E5%9C%88%20n-1%20%E8%BC%AA%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8F%9B%E6%A0%B9%E7%AF%80%E9%BB%9E%E8%88%87%E6%9C%80%E5%8F%B3%E8%91%89%E7%AF%80%E9%BB%9E%EF%BC%88%E4%BA%A4%E6%8F%9B%E9%A6%96%E5%85%83%E7%B4%A0%E8%88%87%E5%B0%BE%E5%85%83%E7%B4%A0%EF%BC%89%0A%20%20%20%20%20%20%20%20nums%5B0%5D%2C%20nums%5Bi%5D%20%3D%20nums%5Bi%5D%2C%20nums%5B0%5D%0A%20%20%20%20%20%20%20%20%23%20%E4%BB%A5%E6%A0%B9%E7%AF%80%E9%BB%9E%E7%82%BA%E8%B5%B7%E9%BB%9E%EF%BC%8C%E5%BE%9E%E9%A0%82%E8%87%B3%E5%BA%95%E9%80%B2%E8%A1%8C%E5%A0%86%E7%A9%8D%E5%8C%96%0A%20%20%20%20%20%20%20%20sift_down%28nums%2C%20i%2C%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20heap_sort%28nums%29%0A%20%20%20%20print%28%22%E5%A0%86%E7%A9%8D%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 11.7.2 演算法特性
|
||||
|
||||
- **時間複雜度為 $O(n \log n)$、非自適應排序**:建堆積操作使用 $O(n)$ 時間。從堆積中提取最大元素的時間複雜度為 $O(\log n)$ ,共迴圈 $n - 1$ 輪。
|
||||
- **空間複雜度為 $O(1)$、原地排序**:幾個指標變數使用 $O(1)$ 空間。元素交換和堆積化操作都是在原陣列上進行的。
|
||||
- **非穩定排序**:在交換堆積頂元素和堆積底元素時,相等元素的相對位置可能發生變化。
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/sort-ascending
|
||||
---
|
||||
|
||||
# 第 11 章 排序
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
排序猶如一把將混亂變為秩序的魔法鑰匙,使我們能以更高效的方式理解與處理資料。
|
||||
|
||||
無論是簡單的升序,還是複雜的分類排列,排序都向我們展示了資料的和諧美感。
|
||||
|
||||
## 本章內容
|
||||
|
||||
- [11.1 排序演算法](sorting_algorithm.md)
|
||||
- [11.2 選擇排序](selection_sort.md)
|
||||
- [11.3 泡沫排序](bubble_sort.md)
|
||||
- [11.4 插入排序](insertion_sort.md)
|
||||
- [11.5 快速排序](quick_sort.md)
|
||||
- [11.6 合併排序](merge_sort.md)
|
||||
- [11.7 堆積排序](heap_sort.md)
|
||||
- [11.8 桶排序](bucket_sort.md)
|
||||
- [11.9 計數排序](counting_sort.md)
|
||||
- [11.10 基數排序](radix_sort.md)
|
||||
- [11.11 小結](summary.md)
|
||||
- [11.12 練習](exercises.md)
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.4 插入排序
|
||||
|
||||
<u>插入排序(insertion sort)</u>是一種簡單的排序演算法,它的工作原理與手動整理一副牌的過程非常相似。
|
||||
|
||||
具體來說,我們在未排序區間選擇一個基準元素,將該元素與其左側已排序區間的元素逐一比較大小,並將該元素插入到正確的位置。
|
||||
|
||||
圖 11-6 展示了陣列插入元素的操作流程。設基準元素為 `base` ,我們需要將從目標索引到 `base` 之間的所有元素向右移動一位,然後將 `base` 賦值給目標索引。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-6 單次插入操作 </p>
|
||||
|
||||
## 11.4.1 演算法流程
|
||||
|
||||
插入排序的整體流程如圖 11-7 所示。
|
||||
|
||||
1. 初始狀態下,陣列的第 1 個元素已完成排序。
|
||||
2. 選取陣列的第 2 個元素作為 `base` ,將其插入到正確位置後,**陣列的前 2 個元素已排序**。
|
||||
3. 選取第 3 個元素作為 `base` ,將其插入到正確位置後,**陣列的前 3 個元素已排序**。
|
||||
4. 以此類推,在最後一輪中,選取最後一個元素作為 `base` ,將其插入到正確位置後,**所有元素均已排序**。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-7 插入排序流程 </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 插入到已排序區間 [0, i-1] 中的正確位置
|
||||
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"
|
||||
### 插入排序 ###
|
||||
def insertion_sort(nums)
|
||||
n = nums.length
|
||||
# 外迴圈:已排序區間為 [0, i-1]
|
||||
for i in 1...n
|
||||
base = nums[i]
|
||||
j = i - 1
|
||||
# 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置
|
||||
while j >= 0 && nums[j] > base
|
||||
nums[j + 1] = nums[j] # 將 nums[j] 向右移動一位
|
||||
j -= 1
|
||||
end
|
||||
nums[j + 1] = base # 將 base 賦值到正確位置
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
??? 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%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%E7%82%BA%20%5B0%2C%20i-1%5D%0A%20%20%20%20for%20i%20in%20range%281%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20base%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A7%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B0%87%20base%20%E6%8F%92%E5%85%A5%E5%88%B0%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%20%5B0%2C%20i-1%5D%20%E4%B8%AD%E7%9A%84%E6%AD%A3%E7%A2%BA%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%87%20nums%5Bj%5D%20%E5%90%91%E5%8F%B3%E7%A7%BB%E5%8B%95%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%87%20base%20%E8%B3%A6%E5%80%BC%E5%88%B0%E6%AD%A3%E7%A2%BA%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20insertion_sort%28nums%29%0A%20%20%20%20print%28%22%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20insertion_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E5%A4%96%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%E7%82%BA%20%5B0%2C%20i-1%5D%0A%20%20%20%20for%20i%20in%20range%281%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20base%20%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20j%20%3D%20i%20-%201%0A%20%20%20%20%20%20%20%20%23%20%E5%85%A7%E8%BF%B4%E5%9C%88%EF%BC%9A%E5%B0%87%20base%20%E6%8F%92%E5%85%A5%E5%88%B0%E5%B7%B2%E6%8E%92%E5%BA%8F%E5%8D%80%E9%96%93%20%5B0%2C%20i-1%5D%20%E4%B8%AD%E7%9A%84%E6%AD%A3%E7%A2%BA%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%87%20nums%5Bj%5D%20%E5%90%91%E5%8F%B3%E7%A7%BB%E5%8B%95%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%87%20base%20%E8%B3%A6%E5%80%BC%E5%88%B0%E6%AD%A3%E7%A2%BA%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%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20insertion_sort%28nums%29%0A%20%20%20%20print%28%22%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 11.4.2 演算法特性
|
||||
|
||||
- **時間複雜度為 $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 插入排序的優勢
|
||||
|
||||
插入排序的時間複雜度為 $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)$ 。**如果給定一組部分有序的資料,插入排序通常比選擇排序效率更高**。
|
||||
- 選擇排序不穩定,無法應用於多級排序。
|
||||
Executable
+700
@@ -0,0 +1,700 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.6 合併排序
|
||||
|
||||
<u>合併排序(merge sort)</u>是一種基於分治策略的排序演算法,包含圖 11-10 所示的“劃分”和“合併”階段。
|
||||
|
||||
1. **劃分階段**:透過遞迴不斷地將陣列從中點處分開,將長陣列的排序問題轉換為短陣列的排序問題。
|
||||
2. **合併階段**:當子陣列長度為 1 時終止劃分,開始合併,持續地將左右兩個較短的有序陣列合併為一個較長的有序陣列,直至結束。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-10 合併排序的劃分與合併階段 </p>
|
||||
|
||||
## 11.6.1 演算法流程
|
||||
|
||||
如圖 11-11 所示,“劃分階段”從頂至底遞迴地將陣列從中點切分為兩個子陣列。
|
||||
|
||||
1. 計算陣列中點 `mid` ,遞迴劃分左子陣列(區間 `[left, mid]` )和右子陣列(區間 `[mid + 1, right]` )。
|
||||
2. 遞迴執行步驟 `1.` ,直至子陣列區間長度為 1 時終止。
|
||||
|
||||
“合併階段”從底至頂地將左子陣列和右子陣列合併為一個有序陣列。需要注意的是,從長度為 1 的子陣列開始合併,合併階段中的每個子陣列都是有序的。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<10>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 11-11 合併排序步驟 </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 - left) / 2; // 計算中點
|
||||
mergeSort(nums, left, mid); // 遞迴左子陣列
|
||||
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
|
||||
// 合併階段
|
||||
merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="merge_sort.java"
|
||||
/* 合併左子陣列和右子陣列 */
|
||||
void merge(int[] nums, int left, int mid, int right) {
|
||||
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
|
||||
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
|
||||
int[] tmp = new int[right - left + 1];
|
||||
// 初始化左子陣列和右子陣列的起始索引
|
||||
int i = left, j = mid + 1, k = 0;
|
||||
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j])
|
||||
tmp[k++] = nums[i++];
|
||||
else
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++];
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
|
||||
for (k = 0; k < tmp.length; k++) {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
}
|
||||
|
||||
/* 合併排序 */
|
||||
void mergeSort(int[] nums, int left, int right) {
|
||||
// 終止條件
|
||||
if (left >= right)
|
||||
return; // 當子陣列長度為 1 時終止遞迴
|
||||
// 劃分階段
|
||||
int mid = left + (right - left) / 2; // 計算中點
|
||||
mergeSort(nums, left, mid); // 遞迴左子陣列
|
||||
mergeSort(nums, mid + 1, right); // 遞迴右子陣列
|
||||
// 合併階段
|
||||
merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="merge_sort.cs"
|
||||
/* 合併左子陣列和右子陣列 */
|
||||
void Merge(int[] nums, int left, int mid, int right) {
|
||||
// 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
|
||||
// 建立一個臨時陣列 tmp ,用於存放合併後的結果
|
||||
int[] tmp = new int[right - left + 1];
|
||||
// 初始化左子陣列和右子陣列的起始索引
|
||||
int i = left, j = mid + 1, k = 0;
|
||||
// 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j])
|
||||
tmp[k++] = nums[i++];
|
||||
else
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++];
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
|
||||
for (k = 0; k < tmp.Length; ++k) {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
}
|
||||
|
||||
/* 合併排序 */
|
||||
void MergeSort(int[] nums, int left, int right) {
|
||||
// 終止條件
|
||||
if (left >= right) return; // 當子陣列長度為 1 時終止遞迴
|
||||
// 劃分階段
|
||||
int mid = left + (right - left) / 2; // 計算中點
|
||||
MergeSort(nums, left, mid); // 遞迴左子陣列
|
||||
MergeSort(nums, mid + 1, right); // 遞迴右子陣列
|
||||
// 合併階段
|
||||
Merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "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 - left) / 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 - left) / 2 // 計算中點
|
||||
mergeSort(nums: &nums, left: left, right: mid) // 遞迴左子陣列
|
||||
mergeSort(nums: &nums, left: mid + 1, right: right) // 遞迴右子陣列
|
||||
// 合併階段
|
||||
merge(nums: &nums, left: left, mid: mid, right: right)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="merge_sort.js"
|
||||
/* 合併左子陣列和右子陣列 */
|
||||
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 - left) / 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 - left) / 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 - left) ~/ 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 - left) / 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 - left) / 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 - left) / 2 // 計算中點
|
||||
mergeSort(nums, left, mid) // 遞迴左子陣列
|
||||
mergeSort(nums, mid + 1, right) // 遞迴右子陣列
|
||||
// 合併階段
|
||||
merge(nums, left, mid, right)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="merge_sort.rb"
|
||||
### 合併左子陣列和右子陣列 ###
|
||||
def merge(nums, left, mid, right)
|
||||
# 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right]
|
||||
# 建立一個臨時陣列 tmp,用於存放合併後的結果
|
||||
tmp = Array.new(right - left + 1, 0)
|
||||
# 初始化左子陣列和右子陣列的起始索引
|
||||
i, j, k = left, mid + 1, 0
|
||||
# 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中
|
||||
while i <= mid && j <= right
|
||||
if nums[i] <= nums[j]
|
||||
tmp[k] = nums[i]
|
||||
i += 1
|
||||
else
|
||||
tmp[k] = nums[j]
|
||||
j += 1
|
||||
end
|
||||
k += 1
|
||||
end
|
||||
# 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中
|
||||
while i <= mid
|
||||
tmp[k] = nums[i]
|
||||
i += 1
|
||||
k += 1
|
||||
end
|
||||
while j <= right
|
||||
tmp[k] = nums[j]
|
||||
j += 1
|
||||
k += 1
|
||||
end
|
||||
# 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間
|
||||
(0...tmp.length).each do |k|
|
||||
nums[left + k] = tmp[k]
|
||||
end
|
||||
end
|
||||
|
||||
### 合併排序 ###
|
||||
def merge_sort(nums, left, right)
|
||||
# 終止條件
|
||||
# 當子陣列長度為 1 時終止遞迴
|
||||
return if left >= right
|
||||
# 劃分階段
|
||||
mid = left + (right - left) / 2 # 計算中點
|
||||
merge_sort(nums, left, mid) # 遞迴左子陣列
|
||||
merge_sort(nums, mid + 1, right) # 遞迴右子陣列
|
||||
# 合併階段
|
||||
merge(nums, left, mid, right)
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20merge%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20mid%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%90%88%E4%BD%B5%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%92%8C%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%8D%80%E9%96%93%E7%82%BA%20%5Bleft%2C%20mid%5D%2C%20%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E5%8D%80%E9%96%93%E7%82%BA%20%5Bmid%2B1%2C%20right%5D%0A%20%20%20%20%23%20%E5%BB%BA%E7%AB%8B%E4%B8%80%E5%80%8B%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%20tmp%20%EF%BC%8C%E7%94%A8%E6%96%BC%E5%AD%98%E6%94%BE%E5%90%88%E4%BD%B5%E5%BE%8C%E7%9A%84%E7%B5%90%E6%9E%9C%0A%20%20%20%20tmp%20%3D%20%5B0%5D%20%2A%20%28right%20-%20left%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%92%8C%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E7%9A%84%E8%B5%B7%E5%A7%8B%E7%B4%A2%E5%BC%95%0A%20%20%20%20i%2C%20j%2C%20k%20%3D%20left%2C%20mid%20%2B%201%2C%200%0A%20%20%20%20%23%20%E7%95%B6%E5%B7%A6%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E9%83%BD%E9%82%84%E6%9C%89%E5%85%83%E7%B4%A0%E6%99%82%EF%BC%8C%E9%80%B2%E8%A1%8C%E6%AF%94%E8%BC%83%E4%B8%A6%E5%B0%87%E8%BC%83%E5%B0%8F%E7%9A%84%E5%85%83%E7%B4%A0%E8%A4%87%E8%A3%BD%E5%88%B0%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%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%87%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%92%8C%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E7%9A%84%E5%89%A9%E9%A4%98%E5%85%83%E7%B4%A0%E8%A4%87%E8%A3%BD%E5%88%B0%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%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%87%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%20tmp%20%E4%B8%AD%E7%9A%84%E5%85%83%E7%B4%A0%E8%A4%87%E8%A3%BD%E5%9B%9E%E5%8E%9F%E9%99%A3%E5%88%97%20nums%20%E7%9A%84%E5%B0%8D%E6%87%89%E5%8D%80%E9%96%93%0A%20%20%20%20for%20k%20in%20range%280%2C%20len%28tmp%29%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bleft%20%2B%20k%5D%20%3D%20tmp%5Bk%5D%0A%0A%0Adef%20merge_sort%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%90%88%E4%BD%B5%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%B5%82%E6%AD%A2%E6%A2%9D%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%E7%95%B6%E5%AD%90%E9%99%A3%E5%88%97%E9%95%B7%E5%BA%A6%E7%82%BA%201%20%E6%99%82%E7%B5%82%E6%AD%A2%E9%81%9E%E8%BF%B4%0A%20%20%20%20%23%20%E5%8A%83%E5%88%86%E9%9A%8E%E6%AE%B5%0A%20%20%20%20mid%20%3D%20%28left%20%2B%20right%29%20//%202%20%20%23%20%E8%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%0A%20%20%20%20merge_sort%28nums%2C%20left%2C%20mid%29%20%20%23%20%E9%81%9E%E8%BF%B4%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%0A%20%20%20%20merge_sort%28nums%2C%20mid%20%2B%201%2C%20right%29%20%20%23%20%E9%81%9E%E8%BF%B4%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%0A%20%20%20%20%23%20%E5%90%88%E4%BD%B5%E9%9A%8E%E6%AE%B5%0A%20%20%20%20merge%28nums%2C%20left%2C%20mid%2C%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%2C%203%2C%202%2C%206%2C%200%2C%201%2C%205%2C%204%5D%0A%20%20%20%20merge_sort%28nums%2C%200%2C%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%90%88%E4%BD%B5%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20merge%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20mid%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%90%88%E4%BD%B5%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%92%8C%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%22%22%22%0A%20%20%20%20%23%20%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%8D%80%E9%96%93%E7%82%BA%20%5Bleft%2C%20mid%5D%2C%20%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E5%8D%80%E9%96%93%E7%82%BA%20%5Bmid%2B1%2C%20right%5D%0A%20%20%20%20%23%20%E5%BB%BA%E7%AB%8B%E4%B8%80%E5%80%8B%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%20tmp%20%EF%BC%8C%E7%94%A8%E6%96%BC%E5%AD%98%E6%94%BE%E5%90%88%E4%BD%B5%E5%BE%8C%E7%9A%84%E7%B5%90%E6%9E%9C%0A%20%20%20%20tmp%20%3D%20%5B0%5D%20%2A%20%28right%20-%20left%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%92%8C%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E7%9A%84%E8%B5%B7%E5%A7%8B%E7%B4%A2%E5%BC%95%0A%20%20%20%20i%2C%20j%2C%20k%20%3D%20left%2C%20mid%20%2B%201%2C%200%0A%20%20%20%20%23%20%E7%95%B6%E5%B7%A6%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E9%83%BD%E9%82%84%E6%9C%89%E5%85%83%E7%B4%A0%E6%99%82%EF%BC%8C%E9%80%B2%E8%A1%8C%E6%AF%94%E8%BC%83%E4%B8%A6%E5%B0%87%E8%BC%83%E5%B0%8F%E7%9A%84%E5%85%83%E7%B4%A0%E8%A4%87%E8%A3%BD%E5%88%B0%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%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%87%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%E5%92%8C%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%E7%9A%84%E5%89%A9%E9%A4%98%E5%85%83%E7%B4%A0%E8%A4%87%E8%A3%BD%E5%88%B0%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%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%87%E8%87%A8%E6%99%82%E9%99%A3%E5%88%97%20tmp%20%E4%B8%AD%E7%9A%84%E5%85%83%E7%B4%A0%E8%A4%87%E8%A3%BD%E5%9B%9E%E5%8E%9F%E9%99%A3%E5%88%97%20nums%20%E7%9A%84%E5%B0%8D%E6%87%89%E5%8D%80%E9%96%93%0A%20%20%20%20for%20k%20in%20range%280%2C%20len%28tmp%29%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bleft%20%2B%20k%5D%20%3D%20tmp%5Bk%5D%0A%0A%0Adef%20merge_sort%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20right%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%90%88%E4%BD%B5%E6%8E%92%E5%BA%8F%22%22%22%0A%20%20%20%20%23%20%E7%B5%82%E6%AD%A2%E6%A2%9D%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%E7%95%B6%E5%AD%90%E9%99%A3%E5%88%97%E9%95%B7%E5%BA%A6%E7%82%BA%201%20%E6%99%82%E7%B5%82%E6%AD%A2%E9%81%9E%E8%BF%B4%0A%20%20%20%20%23%20%E5%8A%83%E5%88%86%E9%9A%8E%E6%AE%B5%0A%20%20%20%20mid%20%3D%20%28left%20%2B%20right%29%20//%202%20%20%23%20%E8%A8%88%E7%AE%97%E4%B8%AD%E9%BB%9E%0A%20%20%20%20merge_sort%28nums%2C%20left%2C%20mid%29%20%20%23%20%E9%81%9E%E8%BF%B4%E5%B7%A6%E5%AD%90%E9%99%A3%E5%88%97%0A%20%20%20%20merge_sort%28nums%2C%20mid%20%2B%201%2C%20right%29%20%20%23%20%E9%81%9E%E8%BF%B4%E5%8F%B3%E5%AD%90%E9%99%A3%E5%88%97%0A%20%20%20%20%23%20%E5%90%88%E4%BD%B5%E9%9A%8E%E6%AE%B5%0A%20%20%20%20merge%28nums%2C%20left%2C%20mid%2C%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%2C%203%2C%202%2C%206%2C%200%2C%201%2C%205%2C%204%5D%0A%20%20%20%20merge_sort%28nums%2C%200%2C%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%90%88%E4%BD%B5%E6%8E%92%E5%BA%8F%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 11.6.2 演算法特性
|
||||
|
||||
- **時間複雜度為 $O(n \log n)$、非自適應排序**:劃分產生高度為 $\log n$ 的遞迴樹,每層合併的總操作數量為 $n$ ,因此總體時間複雜度為 $O(n \log n)$ 。
|
||||
- **空間複雜度為 $O(n)$、非原地排序**:遞迴深度為 $\log n$ ,使用 $O(\log n)$ 大小的堆疊幀空間。合併操作需要藉助輔助陣列實現,使用 $O(n)$ 大小的額外空間。
|
||||
- **穩定排序**:在合併過程中,相等元素的次序保持不變。
|
||||
|
||||
## 11.6.3 鏈結串列排序
|
||||
|
||||
對於鏈結串列,合併排序相較於其他排序演算法具有顯著優勢,**可以將鏈結串列排序任務的空間複雜度最佳化至 $O(1)$** 。
|
||||
|
||||
- **劃分階段**:可以使用“迭代”替代“遞迴”來實現鏈結串列劃分工作,從而省去遞迴使用的堆疊幀空間。
|
||||
- **合併階段**:在鏈結串列中,節點增刪操作僅需改變引用(指標)即可實現,因此合併階段(將兩個短有序鏈結串列合併為一個長有序鏈結串列)無須建立額外鏈結串列。
|
||||
|
||||
具體實現細節比較複雜,有興趣的讀者可以查閱相關資料進行學習。
|
||||
Executable
+1377
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user