--- 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" } > **说明:** 链接中的题解使用迭代位运算,与本练习指定的递归分治方法不同;请根据本题的解题提示完成递归版本