--- comments: true --- # 12.6   練習 ## 12.6.1   知識鞏固 ### 1.   哪些任務適合分治 一位同學想用“先分成兩半,分別解決,再合併結果”的方法完成以下任務。 請分別判斷它們是“適合分治”“可以分治,但不會減少總工作量”還是“兩半不能獨立解決”,並說明理由。 1. 將一個無序陣列排序; 2. 求一個陣列中的最大值; 3. 按順序執行一串 `push(x)`、`pop()` 堆疊操作,並輸出每次 `pop()` 得到的元素。 ??? success "參考答案" 1. 適合:對半分解、兩半獨立排序、$O(n)$ 合併——這正是合併排序。 2. 可以分治,但不會減少總工作量:左右兩半仍需合計檢查全部 $n$ 個元素, 和直接掃描一樣都是 $O(n)$ 時間。 3. 兩半不能獨立解決:後半段開始時的堆疊內容取決於前半段執行結果, 兩半不能在互不知道結果時獨立完成。 ### 2.   快速冪是怎樣減少計算的 下面的遞迴函式用分治計算 $x^n$: === "Python" ```python title="fast_power.py" def fast_pow(x: int, n: int) -> int: """快速冪""" if n == 0: return 1 half = fast_pow(x, n // 2) if n % 2 == 0: return half * half return half * half * x ``` === "C++" ```cpp title="fast_power.cpp" /* 快速冪 */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Java" ```java title="fast_power.java" /* 快速冪 */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "C#" ```csharp title="fast_power.cs" /* 快速冪 */ int FastPow(int x, int n) { if (n == 0) { return 1; } int half = FastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Go" ```go title="fast_power.go" /* 快速冪 */ func fastPow(x, n int) int { if n == 0 { return 1 } half := fastPow(x, n/2) if n%2 == 0 { return half * half } return half * half * x } ``` === "Swift" ```swift title="fast_power.swift" /* 快速冪 */ func fastPow(x: Int, n: Int) -> Int { if n == 0 { return 1 } let half = fastPow(x: x, n: n / 2) if n % 2 == 0 { return half * half } return half * half * x } ``` === "JS" ```javascript title="fast_power.js" /* 快速冪 */ function fastPow(x, n) { if (n === 0) { return 1; } const half = fastPow(x, Math.floor(n / 2)); if (n % 2 === 0) { return half * half; } return half * half * x; } ``` === "TS" ```typescript title="fast_power.ts" /* 快速冪 */ function fastPow(x: number, n: number): number { if (n === 0) { return 1; } const half = fastPow(x, Math.floor(n / 2)); if (n % 2 === 0) { return half * half; } return half * half * x; } ``` === "Dart" ```dart title="fast_power.dart" /* 快速冪 */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n ~/ 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Rust" ```rust title="fast_power.rs" /* 快速冪 */ fn fast_pow(x: i32, n: i32) -> i32 { if n == 0 { return 1; } let half = fast_pow(x, n / 2); if n % 2 == 0 { return half * half; } half * half * x } ``` === "C" ```c title="fast_power.c" /* 快速冪 */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Kotlin" ```kotlin title="fast_power.kt" /* 快速冪 */ fun fastPow(x: Int, n: Int): Int { if (n == 0) { return 1 } val half = fastPow(x, n / 2) if (n % 2 == 0) { return half * half } return half * half * x } ``` === "Ruby" ```ruby title="fast_power.rb" ### 快速冪 ### def fast_pow(x, n) return 1 if n == 0 half = fast_pow(x, n / 2) return half * half if n.even? half * half * x end ``` 令 `x = 3`、`n = 5`,用這個函式計算: 1. 遞迴呼叫時,參數 `n` 依次變成哪些值? 2. 從最深層開始返回時,各層依次返回什麼值? 3. 為什麼要先把遞迴結果儲存為 `half`,而不是在乘法兩邊各呼叫一次相同的子問題? ??? 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. 如果在乘法兩邊各呼叫一次相同的子問題,兩次遞迴會進行完全相同的計算。 先把結果儲存為 `half`,每層就只遞迴一次,遞迴深度約為 $\log n$; 呼叫兩次會造成大量重複計算。 ### 3.   從走訪序列拆分左右子樹 一棵沒有重複節點的二元樹,其前序走訪和中序走訪分別為: - 前序走訪:`[A, B, D, E, C]` - 中序走訪:`[D, B, E, A, C]` 只完成根節點這一層的拆分,不必繼續遞迴,也不必畫出整棵樹: 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" } > **說明:** 連結中的題解使用迭代位運算,與本練習指定的遞迴分治方法不同;請根據本題的解題提示完成遞迴版本