This commit is contained in:
krahets
2024-04-06 03:02:20 +08:00
parent 0a9daa8b9f
commit 8d37c215c8
148 changed files with 70398 additions and 408 deletions
@@ -0,0 +1,441 @@
---
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$ 的分治過程。
![二分搜尋的分治過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" }
<p align="center"> 圖 12-4 &nbsp; 二分搜尋的分治過程 </p>
在實現程式碼中,我們宣告一個遞迴函式 `dfs()` 來求解問題 $f(i, j)$
=== "Python"
```python title="binary_search_recur.py"
def dfs(nums: list[int], target: int, i: int, j: int) -> int:
"""二分搜尋:問題 f(i, j)"""
# 若區間為空,代表無目標元素,則返回 -1
if i > j:
return -1
# 計算中點索引 m
m = (i + j) // 2
if nums[m] < target:
# 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j)
elif nums[m] > target:
# 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1)
else:
# 找到目標元素,返回其索引
return m
def binary_search(nums: list[int], target: int) -> int:
"""二分搜尋"""
n = len(nums)
# 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1)
```
=== "C++"
```cpp title="binary_search_recur.cpp"
/* 二分搜尋:問題 f(i, j) */
int dfs(vector<int> &nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(vector<int> &nums, int target) {
int n = nums.size();
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Java"
```java title="binary_search_recur.java"
/* 二分搜尋:問題 f(i, j) */
int dfs(int[] nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(int[] nums, int target) {
int n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "C#"
```csharp title="binary_search_recur.cs"
/* 二分搜尋:問題 f(i, j) */
int DFS(int[] nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return DFS(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return DFS(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int BinarySearch(int[] nums, int target) {
int n = nums.Length;
// 求解問題 f(0, n-1)
return DFS(nums, target, 0, n - 1);
}
```
=== "Go"
```go title="binary_search_recur.go"
/* 二分搜尋:問題 f(i, j) */
func dfs(nums []int, target, i, j int) int {
// 如果區間為空,代表沒有目標元素,則返回 -1
if i > j {
return -1
}
// 計算索引中點
m := i + ((j - i) >> 1)
//判斷中點與目標元素大小
if nums[m] < target {
// 小於則遞迴右半陣列
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m+1, j)
} else if nums[m] > target {
// 小於則遞迴左半陣列
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m-1)
} else {
// 找到目標元素,返回其索引
return m
}
}
/* 二分搜尋 */
func binarySearch(nums []int, target int) int {
n := len(nums)
return dfs(nums, target, 0, n-1)
}
```
=== "Swift"
```swift title="binary_search_recur.swift"
/* 二分搜尋:問題 f(i, j) */
func dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {
// 若區間為空,代表無目標元素,則返回 -1
if i > j {
return -1
}
// 計算中點索引 m
let m = (i + j) / 2
if nums[m] < target {
// 遞迴子問題 f(m+1, j)
return dfs(nums: nums, target: target, i: m + 1, j: j)
} else if nums[m] > target {
// 遞迴子問題 f(i, m-1)
return dfs(nums: nums, target: target, i: i, j: m - 1)
} else {
// 找到目標元素,返回其索引
return m
}
}
/* 二分搜尋 */
func binarySearch(nums: [Int], target: Int) -> Int {
// 求解問題 f(0, n-1)
dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)
}
```
=== "JS"
```javascript title="binary_search_recur.js"
/* 二分搜尋:問題 f(i, j) */
function dfs(nums, target, i, j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
const m = i + ((j - i) >> 1);
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
function binarySearch(nums, target) {
const n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "TS"
```typescript title="binary_search_recur.ts"
/* 二分搜尋:問題 f(i, j) */
function dfs(nums: number[], target: number, i: number, j: number): number {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
const m = i + ((j - i) >> 1);
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
function binarySearch(nums: number[], target: number): number {
const n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Dart"
```dart title="binary_search_recur.dart"
/* 二分搜尋:問題 f(i, j) */
int dfs(List<int> nums, int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) ~/ 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(List<int> nums, int target) {
int n = nums.length;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Rust"
```rust title="binary_search_recur.rs"
/* 二分搜尋:問題 f(i, j) */
fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {
// 若區間為空,代表無目標元素,則返回 -1
if i > j {
return -1;
}
let m: i32 = (i + j) / 2;
if nums[m as usize] < target {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if nums[m as usize] > target {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
fn binary_search(nums: &[i32], target: i32) -> i32 {
let n = nums.len() as i32;
// 求解問題 f(0, n-1)
dfs(nums, target, 0, n - 1)
}
```
=== "C"
```c title="binary_search_recur.c"
/* 二分搜尋:問題 f(i, j) */
int dfs(int nums[], int target, int i, int j) {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1;
}
// 計算中點索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目標元素,返回其索引
return m;
}
}
/* 二分搜尋 */
int binarySearch(int nums[], int target, int numsSize) {
int n = numsSize;
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Kotlin"
```kotlin title="binary_search_recur.kt"
/* 二分搜尋:問題 f(i, j) */
fun dfs(
nums: IntArray,
target: Int,
i: Int,
j: Int
): Int {
// 若區間為空,代表無目標元素,則返回 -1
if (i > j) {
return -1
}
// 計算中點索引 m
val m = (i + j) / 2
return if (nums[m] < target) {
// 遞迴子問題 f(m+1, j)
dfs(nums, target, m + 1, j)
} else if (nums[m] > target) {
// 遞迴子問題 f(i, m-1)
dfs(nums, target, i, m - 1)
} else {
// 找到目標元素,返回其索引
m
}
}
/* 二分搜尋 */
fun binarySearch(nums: IntArray, target: Int): Int {
val n = nums.size
// 求解問題 f(0, n-1)
return dfs(nums, target, 0, n - 1)
}
```
=== "Ruby"
```ruby title="binary_search_recur.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{binary_search}
```
=== "Zig"
```zig title="binary_search_recur.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{binarySearch}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D,%20target%3A%20int,%20i%3A%20int,%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%9A%E9%97%AE%E9%A2%98%20f%28i,%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E6%97%A0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28m%2B1,%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20m%20%2B%201,%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i,%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20i,%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E6%B1%82%E8%A7%A3%E9%97%AE%E9%A2%98%20f%280,%20n-1%29%0A%20%20%20%20return%20dfs%28nums,%20target,%200,%20n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D,%20target%3A%20int,%20i%3A%20int,%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%9A%E9%97%AE%E9%A2%98%20f%28i,%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E6%97%A0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28m%2B1,%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20m%20%2B%201,%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i,%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20i,%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E6%B1%82%E8%A7%A3%E9%97%AE%E9%A2%98%20f%280,%20n-1%29%0A%20%20%20%20return%20dfs%28nums,%20target,%200,%20n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
@@ -0,0 +1,539 @@
---
comments: true
---
# 12.3 &nbsp; 構建二元樹問題
!!! question
給定一棵二元樹的前序走訪 `preorder` 和中序走訪 `inorder` ,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如圖 12-5 所示)。
![構建二元樹的示例資料](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" }
<p align="center"> 圖 12-5 &nbsp; 構建二元樹的示例資料 </p>
### 1. &nbsp; 判斷是否為分治問題
原問題定義為從 `preorder``inorder` 構建二元樹,是一個典型的分治問題。
- **問題可以分解**:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
- **子問題是獨立的**:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
- **子問題的解可以合併**:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
### 2. &nbsp; 如何劃分子樹
根據以上分析,這道題可以使用分治來求解,**但如何透過前序走訪 `preorder` 和中序走訪 `inorder` 來劃分左子樹和右子樹呢**
根據定義,`preorder``inorder` 都可以劃分為三個部分。
- 前序走訪:`[ 根節點 | 左子樹 | 右子樹 ]` ,例如圖 12-5 的樹對應 `[ 3 | 9 | 2 1 7 ]`
- 中序走訪:`[ 左子樹 | 根節點 | 右子樹 ]` ,例如圖 12-5 的樹對應 `[ 9 | 3 | 1 2 7 ]`
以上圖資料為例,我們可以透過圖 12-6 所示的步驟得到劃分結果。
1. 前序走訪的首元素 3 是根節點的值。
2. 查詢根節點 3 在 `inorder` 中的索引,利用該索引可將 `inorder` 劃分為 `[ 9 | 3 1 2 7 ]`
3. 根據 `inorder` 的劃分結果,易得左子樹和右子樹的節點數量分別為 1 和 3 ,從而可將 `preorder` 劃分為 `[ 3 | 9 | 2 1 7 ]`
![在前序走訪和中序走訪中劃分子樹](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" }
<p align="center"> 圖 12-6 &nbsp; 在前序走訪和中序走訪中劃分子樹 </p>
### 3. &nbsp; 基於變數描述子樹區間
根據以上劃分方法,**我們已經得到根節點、左子樹、右子樹在 `preorder``inorder` 中的索引區間**。而為了描述這些索引區間,我們需要藉助幾個指標變數。
- 將當前樹的根節點在 `preorder` 中的索引記為 $i$ 。
- 將當前樹的根節點在 `inorder` 中的索引記為 $m$ 。
- 將當前樹在 `inorder` 中的索引區間記為 $[l, r]$ 。
如表 12-1 所示,透過以上變數即可表示根節點在 `preorder` 中的索引,以及子樹在 `inorder` 中的索引區間。
<p align="center"> 表 12-1 &nbsp; 根節點和子樹在前序走訪和中序走訪中的索引 </p>
<div class="center-table" markdown>
| | 根節點在 `preorder` 中的索引 | 子樹在 `inorder` 中的索引區間 |
| ------ | ---------------------------- | ----------------------------- |
| 當前樹 | $i$ | $[l, r]$ |
| 左子樹 | $i + 1$ | $[l, m-1]$ |
| 右子樹 | $i + 1 + (m - l)$ | $[m+1, r]$ |
</div>
請注意,右子樹根節點索引中的 $(m-l)$ 的含義是“左子樹的節點數量”,建議結合圖 12-7 理解。
![根節點和左右子樹的索引區間表示](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" }
<p align="center"> 圖 12-7 &nbsp; 根節點和左右子樹的索引區間表示 </p>
### 4. &nbsp; 程式碼實現
為了提升查詢 $m$ 的效率,我們藉助一個雜湊表 `hmap` 來儲存陣列 `inorder` 中元素到索引的對映:
=== "Python"
```python title="build_tree.py"
def dfs(
preorder: list[int],
inorder_map: dict[int, int],
i: int,
l: int,
r: int,
) -> TreeNode | None:
"""構建二元樹:分治"""
# 子樹區間為空時終止
if r - l < 0:
return None
# 初始化根節點
root = TreeNode(preorder[i])
# 查詢 m ,從而劃分左右子樹
m = inorder_map[preorder[i]]
# 子問題:構建左子樹
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
# 子問題:構建右子樹
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
# 返回根節點
return root
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
"""構建二元樹"""
# 初始化雜湊表,儲存 inorder 元素到索引的對映
inorder_map = {val: i for i, val in enumerate(inorder)}
root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)
return root
```
=== "C++"
```cpp title="build_tree.cpp"
/* 構建二元樹:分治 */
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return NULL;
// 初始化根節點
TreeNode *root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
return root;
}
```
=== "Java"
```java title="build_tree.java"
/* 構建二元樹:分治 */
TreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return null;
// 初始化根節點
TreeNode root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode buildTree(int[] preorder, int[] inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Map<Integer, Integer> inorderMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
}
TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "C#"
```csharp title="build_tree.cs"
/* 構建二元樹:分治 */
TreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return null;
// 初始化根節點
TreeNode root = new(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode? BuildTree(int[] preorder, int[] inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Dictionary<int, int> inorderMap = [];
for (int i = 0; i < inorder.Length; i++) {
inorderMap.TryAdd(inorder[i], i);
}
TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);
return root;
}
```
=== "Go"
```go title="build_tree.go"
/* 構建二元樹:分治 */
func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {
// 子樹區間為空時終止
if r-l < 0 {
return nil
}
// 初始化根節點
root := NewTreeNode(preorder[i])
// 查詢 m ,從而劃分左右子樹
m := inorderMap[preorder[i]]
// 子問題:構建左子樹
root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)
// 子問題:構建右子樹
root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)
// 返回根節點
return root
}
/* 構建二元樹 */
func buildTree(preorder, inorder []int) *TreeNode {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
inorderMap := make(map[int]int, len(inorder))
for i := 0; i < len(inorder); i++ {
inorderMap[inorder[i]] = i
}
root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)
return root
}
```
=== "Swift"
```swift title="build_tree.swift"
/* 構建二元樹:分治 */
func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {
// 子樹區間為空時終止
if r - l < 0 {
return nil
}
// 初始化根節點
let root = TreeNode(x: preorder[i])
// 查詢 m ,從而劃分左右子樹
let m = inorderMap[preorder[i]]!
// 子問題:構建左子樹
root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)
// 子問題:構建右子樹
root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)
// 返回根節點
return root
}
/* 構建二元樹 */
func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)
}
```
=== "JS"
```javascript title="build_tree.js"
/* 構建二元樹:分治 */
function dfs(preorder, inorderMap, i, l, r) {
// 子樹區間為空時終止
if (r - l < 0) return null;
// 初始化根節點
const root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
const m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
function buildTree(preorder, inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = new Map();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "TS"
```typescript title="build_tree.ts"
/* 構建二元樹:分治 */
function dfs(
preorder: number[],
inorderMap: Map<number, number>,
i: number,
l: number,
r: number
): TreeNode | null {
// 子樹區間為空時終止
if (r - l < 0) return null;
// 初始化根節點
const root: TreeNode = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
const m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = new Map<number, number>();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "Dart"
```dart title="build_tree.dart"
/* 構建二元樹:分治 */
TreeNode? dfs(
List<int> preorder,
Map<int, int> inorderMap,
int i,
int l,
int r,
) {
// 子樹區間為空時終止
if (r - l < 0) {
return null;
}
// 初始化根節點
TreeNode? root = TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]]!;
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode? buildTree(List<int> preorder, List<int> inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Map<int, int> inorderMap = {};
for (int i = 0; i < inorder.length; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
```
=== "Rust"
```rust title="build_tree.rs"
/* 構建二元樹:分治 */
fn dfs(
preorder: &[i32],
inorder_map: &HashMap<i32, i32>,
i: i32,
l: i32,
r: i32,
) -> Option<Rc<RefCell<TreeNode>>> {
// 子樹區間為空時終止
if r - l < 0 {
return None;
}
// 初始化根節點
let root = TreeNode::new(preorder[i as usize]);
// 查詢 m ,從而劃分左右子樹
let m = inorder_map.get(&preorder[i as usize]).unwrap();
// 子問題:構建左子樹
root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);
// 子問題:構建右子樹
root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);
// 返回根節點
Some(root)
}
/* 構建二元樹 */
fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let mut inorder_map: HashMap<i32, i32> = HashMap::new();
for i in 0..inorder.len() {
inorder_map.insert(inorder[i], i as i32);
}
let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);
root
}
```
=== "C"
```c title="build_tree.c"
/* 構建二元樹:分治 */
TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {
// 子樹區間為空時終止
if (r - l < 0)
return NULL;
// 初始化根節點
TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));
root->val = preorder[i];
root->left = NULL;
root->right = NULL;
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);
// 子問題:構建右子樹
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);
for (int i = 0; i < inorderSize; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);
free(inorderMap);
return root;
}
```
=== "Kotlin"
```kotlin title="build_tree.kt"
/* 構建二元樹:分治 */
fun dfs(preorder: IntArray, inorderMap: Map<Int?, Int?>, i: Int, l: Int, r: Int): TreeNode? {
// 子樹區間為空時終止
if (r - l < 0) return null
// 初始化根節點
val root = TreeNode(preorder[i])
// 查詢 m ,從而劃分左右子樹
val m = inorderMap[preorder[i]]!!
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)
// 返回根節點
return root
}
/* 構建二元樹 */
fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
val inorderMap: MutableMap<Int?, Int?> = HashMap()
for (i in inorder.indices) {
inorderMap[inorder[i]] = i
}
val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)
return root
}
```
=== "Ruby"
```ruby title="build_tree.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{build_tree}
```
=== "Zig"
```zig title="build_tree.zig"
[class]{}-[func]{dfs}
[class]{}-[func]{buildTree}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D,%0A%20%20%20%20inorder_map%3A%20dict%5Bint,%20int%5D,%0A%20%20%20%20i%3A%20int,%0A%20%20%20%20l%3A%20int,%0A%20%20%20%20r%3A%20int,%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A0%91%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E7%BB%88%E6%AD%A2%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%20m%20%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%88%92%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A0%91%0A%20%20%20%20root.left%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201,%20l,%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20root.right%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201%20%2B%20m%20-%20l,%20m%20%2B%201,%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D,%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E5%AD%98%E5%82%A8%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E6%98%A0%E5%B0%84%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i,%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder,%20inorder_map,%200,%200,%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3,%209,%202,%201,%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9,%203,%201,%202,%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder,%20inorder%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D,%0A%20%20%20%20inorder_map%3A%20dict%5Bint,%20int%5D,%0A%20%20%20%20i%3A%20int,%0A%20%20%20%20l%3A%20int,%0A%20%20%20%20r%3A%20int,%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A0%91%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E7%BB%88%E6%AD%A2%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%20m%20%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%88%92%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A0%91%0A%20%20%20%20root.left%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201,%20l,%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20root.right%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201%20%2B%20m%20-%20l,%20m%20%2B%201,%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D,%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E5%AD%98%E5%82%A8%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E6%98%A0%E5%B0%84%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i,%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder,%20inorder_map,%200,%200,%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3,%209,%202,%201,%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9,%203,%201,%202,%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder,%20inorder%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
圖 12-8 展示了構建二元樹的遞迴過程,各個節點是在向下“遞”的過程中建立的,而各條邊(引用)是在向上“迴”的過程中建立的。
=== "<1>"
![構建二元樹的遞迴過程](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" }
=== "<2>"
![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png){ class="animation-figure" }
=== "<3>"
![built_tree_step3](build_binary_tree_problem.assets/built_tree_step3.png){ class="animation-figure" }
=== "<4>"
![built_tree_step4](build_binary_tree_problem.assets/built_tree_step4.png){ class="animation-figure" }
=== "<5>"
![built_tree_step5](build_binary_tree_problem.assets/built_tree_step5.png){ class="animation-figure" }
=== "<6>"
![built_tree_step6](build_binary_tree_problem.assets/built_tree_step6.png){ class="animation-figure" }
=== "<7>"
![built_tree_step7](build_binary_tree_problem.assets/built_tree_step7.png){ class="animation-figure" }
=== "<8>"
![built_tree_step8](build_binary_tree_problem.assets/built_tree_step8.png){ class="animation-figure" }
=== "<9>"
![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png){ class="animation-figure" }
<p align="center"> 圖 12-8 &nbsp; 構建二元樹的遞迴過程 </p>
每個遞迴函式內的前序走訪 `preorder` 和中序走訪 `inorder` 的劃分結果如圖 12-9 所示。
![每個遞迴函式中的劃分結果](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" }
<p align="center"> 圖 12-9 &nbsp; 每個遞迴函式中的劃分結果 </p>
設樹的節點數量為 $n$ ,初始化每一個節點(執行一個遞迴函式 `dfs()` )使用 $O(1)$ 時間。**因此總體時間複雜度為 $O(n)$** 。
雜湊表儲存 `inorder` 元素到索引的對映,空間複雜度為 $O(n)$ 。在最差情況下,即二元樹退化為鏈結串列時,遞迴深度達到 $n$ ,使用 $O(n)$ 的堆疊幀空間。**因此總體空間複雜度為 $O(n)$** 。
@@ -0,0 +1,101 @@
---
comments: true
---
# 12.1 &nbsp; 分治演算法
<u>分治(divide and conquer</u>,全稱分而治之,是一種非常重要且常見的演算法策略。分治通常基於遞迴實現,包括“分”和“治”兩個步驟。
1. **分(劃分階段)**:遞迴地將原問題分解為兩個或多個子問題,直至到達最小子問題時終止。
2. **治(合併階段)**:從已知解的最小子問題開始,從底至頂地將子問題的解進行合併,從而構建出原問題的解。
如圖 12-1 所示,“合併排序”是分治策略的典型應用之一。
1. **分**:遞迴地將原陣列(原問題)劃分為兩個子陣列(子問題),直到子陣列只剩一個元素(最小子問題)。
2. **治**:從底至頂地將有序的子陣列(子問題的解)進行合併,從而得到有序的原陣列(原問題的解)。
![合併排序的分治策略](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" }
<p align="center"> 圖 12-1 &nbsp; 合併排序的分治策略 </p>
## 12.1.1 &nbsp; 如何判斷分治問題
一個問題是否適合使用分治解決,通常可以參考以下幾個判斷依據。
1. **問題可以分解**:原問題可以分解成規模更小、類似的子問題,以及能夠以相同方式遞迴地進行劃分。
2. **子問題是獨立的**:子問題之間沒有重疊,互不依賴,可以獨立解決。
3. **子問題的解可以合併**:原問題的解透過合併子問題的解得來。
顯然,合併排序滿足以上三個判斷依據。
1. **問題可以分解**:遞迴地將陣列(原問題)劃分為兩個子陣列(子問題)。
2. **子問題是獨立的**:每個子陣列都可以獨立地進行排序(子問題可以獨立進行求解)。
3. **子問題的解可以合併**:兩個有序子陣列(子問題的解)可以合併為一個有序陣列(原問題的解)。
## 12.1.2 &nbsp; 透過分治提升效率
**分治不僅可以有效地解決演算法問題,往往還可以提升演算法效率**。在排序演算法中,快速排序、合併排序、堆積排序相較於選擇、冒泡、插入排序更快,就是因為它們應用了分治策略。
那麼,我們不禁發問:**為什麼分治可以提升演算法效率,其底層邏輯是什麼**?換句話說,將大問題分解為多個子問題、解決子問題、將子問題的解合併為原問題的解,這幾步的效率為什麼比直接解決原問題的效率更高?這個問題可以從操作數量和平行計算兩方面來討論。
### 1. &nbsp; 操作數量最佳化
以“泡沫排序”為例,其處理一個長度為 $n$ 的陣列需要 $O(n^2)$ 時間。假設我們按照圖 12-2 所示的方式,將陣列從中點處分為兩個子陣列,則劃分需要 $O(n)$ 時間,排序每個子陣列需要 $O((n / 2)^2)$ 時間,合併兩個子陣列需要 $O(n)$ 時間,總體時間複雜度為:
$$
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
$$
![劃分陣列前後的泡沫排序](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png){ class="animation-figure" }
<p align="center"> 圖 12-2 &nbsp; 劃分陣列前後的泡沫排序 </p>
接下來,我們計算以下不等式,其左邊和右邊分別為劃分前和劃分後的操作總數:
$$
\begin{aligned}
n^2 & > \frac{n^2}{2} + 2n \newline
n^2 - \frac{n^2}{2} - 2n & > 0 \newline
n(n - 4) & > 0
\end{aligned}
$$
**這意味著當 $n > 4$ 時,劃分後的操作數量更少,排序效率應該更高**。請注意,劃分後的時間複雜度仍然是平方階 $O(n^2)$ ,只是複雜度中的常數項變小了。
進一步想,**如果我們把子陣列不斷地再從中點處劃分為兩個子陣列**,直至子陣列只剩一個元素時停止劃分呢?這種思路實際上就是“合併排序”,時間複雜度為 $O(n \log n)$ 。
再思考,**如果我們多設定幾個劃分點**,將原陣列平均劃分為 $k$ 個子陣列呢?這種情況與“桶排序”非常類似,它非常適合排序海量資料,理論上時間複雜度可以達到 $O(n + k)$ 。
### 2. &nbsp; 平行計算最佳化
我們知道,分治生成的子問題是相互獨立的,**因此通常可以並行解決**。也就是說,分治不僅可以降低演算法的時間複雜度,**還有利於作業系統的並行最佳化**。
並行最佳化在多核或多處理器的環境中尤其有效,因為系統可以同時處理多個子問題,更加充分地利用計算資源,從而顯著減少總體的執行時間。
比如在圖 12-3 所示的“桶排序”中,我們將海量的資料平均分配到各個桶中,則可所有桶的排序任務分散到各個計算單元,完成後再合併結果。
![桶排序的平行計算](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png){ class="animation-figure" }
<p align="center"> 圖 12-3 &nbsp; 桶排序的平行計算 </p>
## 12.1.3 &nbsp; 分治常見應用
一方面,分治可以用來解決許多經典演算法問題。
- **尋找最近點對**:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。
- **大整數乘法**:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。
- **矩陣乘法**:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。
- **河內塔問題**:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。
- **求解逆序對**:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。
另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。
- **二分搜尋**:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。
- **合併排序**:本節開頭已介紹,不再贅述。
- **快速排序**:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。
- **桶排序**:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。
- **樹**:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。
- **堆積**:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。
- **雜湊表**:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。
可以看出,**分治是一種“潤物細無聲”的演算法思想**,隱含在各種演算法與資料結構之中。
@@ -0,0 +1,545 @@
---
comments: true
---
# 12.4 &nbsp; 河內塔問題
在合併排序和構建二元樹中,我們都是將原問題分解為兩個規模為原問題一半的子問題。然而對於河內塔問題,我們採用不同的分解策略。
!!! question
給定三根柱子,記為 `A``B``C` 。起始狀態下,柱子 `A` 上套著 $n$ 個圓盤,它們從上到下按照從小到大的順序排列。我們的任務是要把這 $n$ 個圓盤移到柱子 `C` 上,並保持它們的原有順序不變(如圖 12-10 所示)。在移動圓盤的過程中,需要遵守以下規則。
1. 圓盤只能從一根柱子頂部拿出,從另一根柱子頂部放入。
2. 每次只能移動一個圓盤。
3. 小圓盤必須時刻位於大圓盤之上。
![河內塔問題示例](hanota_problem.assets/hanota_example.png){ class="animation-figure" }
<p align="center"> 圖 12-10 &nbsp; 河內塔問題示例 </p>
**我們將規模為 $i$ 的河內塔問題記作 $f(i)$** 。例如 $f(3)$ 代表將 $3$ 個圓盤從 `A` 移動至 `C` 的河內塔問題。
### 1. &nbsp; 考慮基本情況
如圖 12-11 所示,對於問題 $f(1)$ ,即當只有一個圓盤時,我們將它直接從 `A` 移動至 `C` 即可。
=== "<1>"
![規模為 1 的問題的解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png){ class="animation-figure" }
<p align="center"> 圖 12-11 &nbsp; 規模為 1 的問題的解 </p>
如圖 12-12 所示,對於問題 $f(2)$ ,即當有兩個圓盤時,**由於要時刻滿足小圓盤在大圓盤之上,因此需要藉助 `B` 來完成移動**。
1. 先將上面的小圓盤從 `A` 移至 `B`
2. 再將大圓盤從 `A` 移至 `C`
3. 最後將小圓盤從 `B` 移至 `C`
=== "<1>"
![規模為 2 的問題的解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png){ class="animation-figure" }
=== "<3>"
![hanota_f2_step3](hanota_problem.assets/hanota_f2_step3.png){ class="animation-figure" }
=== "<4>"
![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png){ class="animation-figure" }
<p align="center"> 圖 12-12 &nbsp; 規模為 2 的問題的解 </p>
解決問題 $f(2)$ 的過程可總結為:**將兩個圓盤藉助 `B``A` 移至 `C`** 。其中,`C` 稱為目標柱、`B` 稱為緩衝柱。
### 2. &nbsp; 子問題分解
對於問題 $f(3)$ ,即當有三個圓盤時,情況變得稍微複雜了一些。
因為已知 $f(1)$ 和 $f(2)$ 的解,所以我們可從分治角度思考,**將 `A` 頂部的兩個圓盤看作一個整體**,執行圖 12-13 所示的步驟。這樣三個圓盤就被順利地從 `A` 移至 `C` 了。
1.`B` 為目標柱、`C` 為緩衝柱,將兩個圓盤從 `A` 移至 `B`
2.`A` 中剩餘的一個圓盤從 `A` 直接移動至 `C`
3.`C` 為目標柱、`A` 為緩衝柱,將兩個圓盤從 `B` 移至 `C`
=== "<1>"
![規模為 3 的問題的解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" }
=== "<2>"
![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png){ class="animation-figure" }
=== "<3>"
![hanota_f3_step3](hanota_problem.assets/hanota_f3_step3.png){ class="animation-figure" }
=== "<4>"
![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png){ class="animation-figure" }
<p align="center"> 圖 12-13 &nbsp; 規模為 3 的問題的解 </p>
從本質上看,**我們將問題 $f(3)$ 劃分為兩個子問題 $f(2)$ 和一個子問題 $f(1)$** 。按順序解決這三個子問題之後,原問題隨之得到解決。這說明子問題是獨立的,而且解可以合併。
至此,我們可總結出圖 12-14 所示的解決河內塔問題的分治策略:將原問題 $f(n)$ 劃分為兩個子問題 $f(n-1)$ 和一個子問題 $f(1)$ ,並按照以下順序解決這三個子問題。
1. 將 $n-1$ 個圓盤藉助 `C``A` 移至 `B`
2. 將剩餘 $1$ 個圓盤從 `A` 直接移至 `C`
3. 將 $n-1$ 個圓盤藉助 `A``B` 移至 `C`
對於這兩個子問題 $f(n-1)$ ,**可以透過相同的方式進行遞迴劃分**,直至達到最小子問題 $f(1)$ 。而 $f(1)$ 的解是已知的,只需一次移動操作即可。
![解決河內塔問題的分治策略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" }
<p align="center"> 圖 12-14 &nbsp; 解決河內塔問題的分治策略 </p>
### 3. &nbsp; 程式碼實現
在程式碼中,我們宣告一個遞迴函式 `dfs(i, src, buf, tar)` ,它的作用是將柱 `src` 頂部的 $i$ 個圓盤藉助緩衝柱 `buf` 移動至目標柱 `tar`
=== "Python"
```python title="hanota.py"
def move(src: list[int], tar: list[int]):
"""移動一個圓盤"""
# 從 src 頂部拿出一個圓盤
pan = src.pop()
# 將圓盤放入 tar 頂部
tar.append(pan)
def dfs(i: int, src: list[int], buf: list[int], tar: list[int]):
"""求解河內塔問題 f(i)"""
# 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1:
move(src, tar)
return
# 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf)
# 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar)
# 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar)
def solve_hanota(A: list[int], B: list[int], C: list[int]):
"""求解河內塔問題"""
n = len(A)
# 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C)
```
=== "C++"
```cpp title="hanota.cpp"
/* 移動一個圓盤 */
void move(vector<int> &src, vector<int> &tar) {
// 從 src 頂部拿出一個圓盤
int pan = src.back();
src.pop_back();
// 將圓盤放入 tar 頂部
tar.push_back(pan);
}
/* 求解河內塔問題 f(i) */
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
int n = A.size();
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "Java"
```java title="hanota.java"
/* 移動一個圓盤 */
void move(List<Integer> src, List<Integer> tar) {
// 從 src 頂部拿出一個圓盤
Integer pan = src.remove(src.size() - 1);
// 將圓盤放入 tar 頂部
tar.add(pan);
}
/* 求解河內塔問題 f(i) */
void dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {
int n = A.size();
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "C#"
```csharp title="hanota.cs"
/* 移動一個圓盤 */
void Move(List<int> src, List<int> tar) {
// 從 src 頂部拿出一個圓盤
int pan = src[^1];
src.RemoveAt(src.Count - 1);
// 將圓盤放入 tar 頂部
tar.Add(pan);
}
/* 求解河內塔問題 f(i) */
void DFS(int i, List<int> src, List<int> buf, List<int> tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
Move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
DFS(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
Move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
DFS(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void SolveHanota(List<int> A, List<int> B, List<int> C) {
int n = A.Count;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
DFS(n, A, B, C);
}
```
=== "Go"
```go title="hanota.go"
/* 移動一個圓盤 */
func move(src, tar *list.List) {
// 從 src 頂部拿出一個圓盤
pan := src.Back()
// 將圓盤放入 tar 頂部
tar.PushBack(pan.Value)
// 移除 src 頂部圓盤
src.Remove(pan)
}
/* 求解河內塔問題 f(i) */
func dfsHanota(i int, src, buf, tar *list.List) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1 {
move(src, tar)
return
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfsHanota(i-1, src, tar, buf)
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar)
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfsHanota(i-1, buf, src, tar)
}
/* 求解河內塔問題 */
func solveHanota(A, B, C *list.List) {
n := A.Len()
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfsHanota(n, A, B, C)
}
```
=== "Swift"
```swift title="hanota.swift"
/* 移動一個圓盤 */
func move(src: inout [Int], tar: inout [Int]) {
// 從 src 頂部拿出一個圓盤
let pan = src.popLast()!
// 將圓盤放入 tar 頂部
tar.append(pan)
}
/* 求解河內塔問題 f(i) */
func dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1 {
move(src: &src, tar: &tar)
return
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src: &src, tar: &tar)
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)
}
/* 求解河內塔問題 */
func solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {
let n = A.count
// 串列尾部是柱子頂部
// 將 src 頂部 n 個圓盤藉助 B 移到 C
dfs(i: n, src: &A, buf: &B, tar: &C)
}
```
=== "JS"
```javascript title="hanota.js"
/* 移動一個圓盤 */
function move(src, tar) {
// 從 src 頂部拿出一個圓盤
const pan = src.pop();
// 將圓盤放入 tar 頂部
tar.push(pan);
}
/* 求解河內塔問題 f(i) */
function dfs(i, src, buf, tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i === 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
function solveHanota(A, B, C) {
const n = A.length;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "TS"
```typescript title="hanota.ts"
/* 移動一個圓盤 */
function move(src: number[], tar: number[]): void {
// 從 src 頂部拿出一個圓盤
const pan = src.pop();
// 將圓盤放入 tar 頂部
tar.push(pan);
}
/* 求解河內塔問題 f(i) */
function dfs(i: number, src: number[], buf: number[], tar: number[]): void {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i === 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
function solveHanota(A: number[], B: number[], C: number[]): void {
const n = A.length;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "Dart"
```dart title="hanota.dart"
/* 移動一個圓盤 */
void move(List<int> src, List<int> tar) {
// 從 src 頂部拿出一個圓盤
int pan = src.removeLast();
// 將圓盤放入 tar 頂部
tar.add(pan);
}
/* 求解河內塔問題 f(i) */
void dfs(int i, List<int> src, List<int> buf, List<int> tar) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
void solveHanota(List<int> A, List<int> B, List<int> C) {
int n = A.length;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "Rust"
```rust title="hanota.rs"
/* 移動一個圓盤 */
fn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {
// 從 src 頂部拿出一個圓盤
let pan = src.remove(src.len() - 1);
// 將圓盤放入 tar 頂部
tar.push(pan);
}
/* 求解河內塔問題 f(i) */
fn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if i == 1 {
move_pan(src, tar);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move_pan(src, tar);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar);
}
/* 求解河內塔問題 */
fn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {
let n = A.len() as i32;
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C);
}
```
=== "C"
```c title="hanota.c"
/* 移動一個圓盤 */
void move(int *src, int *srcSize, int *tar, int *tarSize) {
// 從 src 頂部拿出一個圓盤
int pan = src[*srcSize - 1];
src[*srcSize - 1] = 0;
(*srcSize)--;
// 將圓盤放入 tar 頂部
tar[*tarSize] = pan;
(*tarSize)++;
}
/* 求解河內塔問題 f(i) */
void dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, srcSize, tar, tarSize);
return;
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, srcSize, tar, tarSize);
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);
}
/* 求解河內塔問題 */
void solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(*ASize, A, ASize, B, BSize, C, CSize);
}
```
=== "Kotlin"
```kotlin title="hanota.kt"
/* 移動一個圓盤 */
fun move(src: MutableList<Int>, tar: MutableList<Int>) {
// 從 src 頂部拿出一個圓盤
val pan: Int = src.removeAt(src.size - 1)
// 將圓盤放入 tar 頂部
tar.add(pan)
}
/* 求解河內塔問題 f(i) */
fun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {
// 若 src 只剩下一個圓盤,則直接將其移到 tar
if (i == 1) {
move(src, tar)
return
}
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
dfs(i - 1, src, tar, buf)
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
move(src, tar)
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
dfs(i - 1, buf, src, tar)
}
/* 求解河內塔問題 */
fun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {
val n = A.size
// 將 A 頂部 n 個圓盤藉助 B 移到 C
dfs(n, A, B, C)
}
```
=== "Ruby"
```ruby title="hanota.rb"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solve_hanota}
```
=== "Zig"
```zig title="hanota.zig"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
??? pythontutor "視覺化執行"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%22%22%22%0A%20%20%20%20%23%20%E4%BB%8E%20src%20%E9%A1%B6%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%86%E5%9C%86%E7%9B%98%E6%94%BE%E5%85%A5%20tar%20%E9%A1%B6%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int,%20src%3A%20list%5Bint%5D,%20buf%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%20src%20%E5%8F%AA%E5%89%A9%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E5%B0%86%E5%85%B6%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20src%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201,%20src,%20tar,%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%281%29%20%EF%BC%9A%E5%B0%86%20src%20%E5%89%A9%E4%BD%99%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20buf%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201,%20buf,%20src,%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D,%20B%3A%20list%5Bint%5D,%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%86%20A%20%E9%A1%B6%E9%83%A8%20n%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n,%20A,%20B,%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%97%E8%A1%A8%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A1%B6%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5,%204,%203,%202,%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%E4%B8%8B%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A,%20B,%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%86%E7%9B%98%E7%A7%BB%E5%8A%A8%E5%AE%8C%E6%88%90%E5%90%8E%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%22%22%22%0A%20%20%20%20%23%20%E4%BB%8E%20src%20%E9%A1%B6%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%86%E5%9C%86%E7%9B%98%E6%94%BE%E5%85%A5%20tar%20%E9%A1%B6%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int,%20src%3A%20list%5Bint%5D,%20buf%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%20src%20%E5%8F%AA%E5%89%A9%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E5%B0%86%E5%85%B6%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20src%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201,%20src,%20tar,%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%281%29%20%EF%BC%9A%E5%B0%86%20src%20%E5%89%A9%E4%BD%99%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20buf%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201,%20buf,%20src,%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D,%20B%3A%20list%5Bint%5D,%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%86%20A%20%E9%A1%B6%E9%83%A8%20n%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n,%20A,%20B,%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%97%E8%A1%A8%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A1%B6%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5,%204,%203,%202,%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%E4%B8%8B%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A,%20B,%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%86%E7%9B%98%E7%A7%BB%E5%8A%A8%E5%AE%8C%E6%88%90%E5%90%8E%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
如圖 12-15 所示,河內塔問題形成一棵高度為 $n$ 的遞迴樹,每個節點代表一個子問題,對應一個開啟的 `dfs()` 函式,**因此時間複雜度為 $O(2^n)$ ,空間複雜度為 $O(n)$** 。
![河內塔問題的遞迴樹](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" }
<p align="center"> 圖 12-15 &nbsp; 河內塔問題的遞迴樹 </p>
!!! quote
河內塔問題源自一個古老的傳說。在古印度的一個寺廟裡,僧侶們有三根高大的鑽石柱子,以及 $64$ 個大小不一的金圓盤。僧侶們不斷地移動圓盤,他們相信在最後一個圓盤被正確放置的那一刻,這個世界就會結束。
然而,即使僧侶們每秒鐘移動一次,總共需要大約 $2^{64} \approx 1.84×10^{19}$ 秒,合約 $5850$ 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。
@@ -0,0 +1,22 @@
---
comments: true
icon: material/set-split
---
# 第 12 章 &nbsp; 分治
![分治](../assets/covers/chapter_divide_and_conquer.jpg){ class="cover-image" }
!!! abstract
難題被逐層拆解,每一次的拆解都使它變得更為簡單。
分而治之揭示了一個重要的事實:從簡單做起,一切都不再複雜。
## Chapter Contents
- [12.1 &nbsp; 分治演算法](https://www.hello-algo.com/en/chapter_divide_and_conquer/divide_and_conquer/)
- [12.2 &nbsp; 分治搜尋策略](https://www.hello-algo.com/en/chapter_divide_and_conquer/binary_search_recur/)
- [12.3 &nbsp; 構建樹問題](https://www.hello-algo.com/en/chapter_divide_and_conquer/build_binary_tree_problem/)
- [12.4 &nbsp; 河內塔問題](https://www.hello-algo.com/en/chapter_divide_and_conquer/hanota_problem/)
- [12.5 &nbsp; 小結](https://www.hello-algo.com/en/chapter_divide_and_conquer/summary/)
@@ -0,0 +1,15 @@
---
comments: true
---
# 12.5 &nbsp; 小結
- 分治是一種常見的演算法設計策略,包括分(劃分)和治(合併)兩個階段,通常基於遞迴實現。
- 判斷是否是分治演算法問題的依據包括:問題能否分解、子問題是否獨立、子問題能否合併。
- 合併排序是分治策略的典型應用,其遞迴地將陣列劃分為等長的兩個子陣列,直到只剩一個元素時開始逐層合併,從而完成排序。
- 引入分治策略往往可以提升演算法效率。一方面,分治策略減少了操作數量;另一方面,分治後有利於系統的並行最佳化。
- 分治既可以解決許多演算法問題,也廣泛應用於資料結構與演算法設計中,處處可見其身影。
- 相較於暴力搜尋,自適應搜尋效率更高。時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的。
- 二分搜尋是分治策略的另一個典型應用,它不包含將子問題的解進行合併的步驟。我們可以透過遞迴分治實現二分搜尋。
- 在構建二元樹的問題中,構建樹(原問題)可以劃分為構建左子樹和右子樹(子問題),這可以透過劃分前序走訪和中序走訪的索引區間來實現。
- 在河內塔問題中,一個規模為 $n$ 的問題可以劃分為兩個規模為 $n-1$ 的子問題和一個規模為 $1$ 的子問題。按順序解決這三個子問題後,原問題隨之得到解決。