跳轉至
紙質書（簡體中文版）已發行，詳情請見這裡

# 12.6 練習

## 12.6.1 知識鞏固

### 1. 哪些任務適合分治

一位同學想用“先分成兩半，分別解決，再合併結果”的方法完成以下任務。 請分別判斷它們是“適合分治”“可以分治，但不會減少總工作量”還是“兩半不能獨立解決”，並說明理由。

1. 將一個無序陣列排序；
2. 求一個陣列中的最大值；
3. 按順序執行一串 `push(x)`、`pop()` 堆疊操作，並輸出每次 `pop()` 得到的元素。

參考答案

1. 適合：對半分解、兩半獨立排序、\(O(n)\) 合併——這正是合併排序。
2. 可以分治，但不會減少總工作量：左右兩半仍需合計檢查全部 \(n\) 個元素， 和直接掃描一樣都是 \(O(n)\) 時間。
3. 兩半不能獨立解決：後半段開始時的堆疊內容取決於前半段執行結果， 兩半不能在互不知道結果時獨立完成。

### 2. 快速冪是怎樣減少計算的

下面的遞迴函式用分治計算 \(x^n\)：

Python C++Java C#Go Swift JS TS Dart Rust C Kotlin Ruby
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

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;
    }

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;
    }

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;
    }

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
    }

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
    }

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;
    }

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;
    }

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;
    }

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
    }

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;
    }

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
    }

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`，而不是在乘法兩邊各呼叫一次相同的子問題？

參考答案

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. 左、右子樹在前序走訪中分別對應哪一段？根節點有哪些直接孩子？

參考答案

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}\)。

解題提示

1. n 等於 0 時答案是 1
2. 算出 x 的 n // 2 次方後儲存為 half，不要遞迴呼叫第二次
3. 當 n < 0 時，先把 x 改為 1 / x，再把 n 改為 -n；使用 C++ 或 Java 時，可先把 n 轉為 64 位整數，避免最小的 32 位整數取相反數時溢位

LeetCode 題目解析

> 說明： 連結中的題解使用迭代位運算，與本練習指定的遞迴分治方法不同；請根據本題的解題提示完成遞迴版本

##### 歡迎在評論區留下你的見解、問題或建議