跳轉至

12.6   練習

12.6.1   知識鞏固

1.   哪些任務適合分治

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

  1. 將一個無序陣列排序;
  2. 求一個陣列中的最大值;
  3. 按順序執行一串 push(x)pop() 堆疊操作,並輸出每次 pop() 得到的元素。
參考答案
  1. 適合:對半分解、兩半獨立排序、\(O(n)\) 合併——這正是合併排序。
  2. 可以分治,但不會減少總工作量:左右兩半仍需合計檢查全部 \(n\) 個元素, 和直接掃描一樣都是 \(O(n)\) 時間。
  3. 兩半不能獨立解決:後半段開始時的堆疊內容取決於前半段執行結果, 兩半不能在互不知道結果時獨立完成。

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

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

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 = 3n = 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 題目解析

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

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