12.6 練習¶
12.6.1 知識鞏固¶
1. 哪些任務適合分治¶
一位同學想用“先分成兩半,分別解決,再合併結果”的方法完成以下任務。 請分別判斷它們是“適合分治”“可以分治,但不會減少總工作量”還是“兩半不能獨立解決”,並說明理由。
- 將一個無序陣列排序;
- 求一個陣列中的最大值;
- 按順序執行一串
push(x)、pop()堆疊操作,並輸出每次pop()得到的元素。
參考答案
- 適合:對半分解、兩半獨立排序、\(O(n)\) 合併——這正是合併排序。
- 可以分治,但不會減少總工作量:左右兩半仍需合計檢查全部 \(n\) 個元素, 和直接掃描一樣都是 \(O(n)\) 時間。
- 兩半不能獨立解決:後半段開始時的堆疊內容取決於前半段執行結果, 兩半不能在互不知道結果時獨立完成。
2. 快速冪是怎樣減少計算的¶
下面的遞迴函式用分治計算 \(x^n\):
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):
- 遞迴呼叫時,參數
n依次變成哪些值? - 從最深層開始返回時,各層依次返回什麼值?
- 為什麼要先儲存
half,而不是把fast_pow(x, n // 2)寫兩遍?
參考答案
-
參數依次為
5 → 2 → 1 → 0,每次都把指數減半,直到到達終止條件。 -
n = 0時返回 1;n = 1時返回 \(1×1×3=3\);n = 2時返回 \(3×3=9\);n = 5時返回 \(9×9×3=243\)。 -
如果把
fast_pow(x, n // 2)在乘法兩邊各寫一次,兩次遞迴會計算完全相同的子問題。 先把結果儲存為half,每層就只遞迴一次,遞迴深度約為 \(\log n\); 呼叫兩次會造成大量重複計算。
3. 從走訪序列拆分左右子樹¶
一棵沒有重複節點的二元樹,其前序走訪和中序走訪分別為:
- 前序走訪:
[A, B, D, E, C] - 中序走訪:
[D, B, E, A, C]
只完成根節點這一層的拆分,不必繼續遞迴,也不必畫出整棵樹:
- 根節點是什麼?
- 左、右子樹在中序走訪中分別對應哪一段?
- 左、右子樹在前序走訪中分別對應哪一段?根節點有哪些直接孩子?
參考答案
-
前序走訪的第一個節點是根節點,因此根節點為
A。 -
A把中序走訪分成兩部分:左子樹為[D, B, E],右子樹為[C]。 -
左子樹有 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}\)。
解題提示
- n 等於 0 時答案是 1
- 算出 x 的 n // 2 次方後儲存為 half,不要遞迴呼叫第二次
- 當 n < 0 時,先把 x 改為 1 / x,再把 n 改為 -n;使用 C++ 或 Java 時,可先把 n 轉為 64 位整數,避免最小的 32 位整數取相反數時溢位
說明: 連結中的題解使用迭代位運算,與本練習指定的遞迴分治方法不同;請根據本題的解題提示完成遞迴版本