前面我们聊了分治法、动态规划、贪心、回溯。今天聊递归与递推,这两兄弟长得像,但脾气不一样。递归是"自己调用自己,直到撞墙才停";递推是"从已知出发,一步一步推未知"。软考里经常把它们放在一起考,或者让你区分,或者让你填空补全递归出口。
一、递归和递推:先搞清楚区别
在软考官方教材里,递归和递推是分开列的两个算法设计方法。很多考生容易搞混,我们先一句话区分:
| 递归 | 递推 |
|---|---|
| 自顶向下,从大问题拆成小问题,自己调用自己 | 自底向上,从已知初始值出发,逐步计算后续值 |
| 需要递归出口(终止条件) | 需要递推公式和初始条件 |
| 代码里有函数自己调用自己 | 代码里通常是循环迭代 |
| 空间复杂度较高(调用栈) | 空间复杂度较低(通常 O(1) 或 O(n)) |
软考里,斐波那契数列是同时考察递归和递推的最佳载体。下面直接上代码。
二、案例一:斐波那契数列(递归 vs 递推)
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21...
规律:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)(n≥3)
递归版(自顶向下)
#include <stdio.h>
/* 递归求斐波那契第 n 项 */
int fib_recursive(int n) {
if (n == 1 || n == 2) {
return 1; /* 递归出口 */
}
return fib_recursive(n - 1) + fib_recursive(n - 2); /* 自己调用自己 */
}
int main() {
int n = 6;
printf("F(%d) = %d\n", n, fib_recursive(n));
return 0;
}
递推版(自底向上)
#include <stdio.h>
/* 递推求斐波那契第 n 项 */
int fib_iterative(int n) {
int i;
int f1 = 1, f2 = 1, fn;
if (n == 1 || n == 2) return 1;
for (i = 3; i <= n; i++) {
fn = f1 + f2; /* 递推公式:当前项 = 前两项之和 */
f1 = f2; /* 更新前两项 */
f2 = fn;
}
return fn;
}
int main() {
int n = 6;
printf("F(%d) = %d\n", n, fib_iterative(n));
return 0;
}
关键点解析
1. 递归出口 if (n == 1 || n == 2) return 1; 这行是递归的"刹车片"。没有它,函数会无限调用自己,直到栈溢出。软考填空里,递归出口是必考点。
2. 递归的时间复杂度 递归版斐波那契的时间复杂度是 O(2^n),因为存在大量重复计算(比如 F(5) 要算 F(4) 和 F(3),F(4) 又要算 F(3) 和 F(2),F(3) 被算了两次)。空间复杂度是 O(n),主要是递归调用栈的深度。
3. 递推的时间复杂度 递推版时间复杂度是 O(n),空间复杂度是 O(1)(只用了三个变量)。所以实际工程中,递推版远优于递归版。
4. 软考怎么考? 上午选择题可能问你"斐波那契数列用递归实现的时间复杂度是多少";下午第四题可能给你递归代码,让你补全递归出口或递归调用式。
三、案例二:汉诺塔(经典递归)
汉诺塔是软考里递归算法的经典案例,几乎每次提到递归都会拿它举例。
问题描述:三根柱子 A、B、C,A 上有 n 个盘子,大盘在下,小盘在上。要求把盘子从 A 移到 C,每次只能移一个,且大盘不能压小盘。
递归思路:
-
把上面 n-1 个盘子从 A 移到 B(借助 C);
-
把第 n 个(最大的)盘子从 A 移到 C;
-
把 n-1 个盘子从 B 移到 C(借助 A)。
你看,大问题(n 个盘子)拆成了三个小问题,其中两个是"自己调用自己"(n-1 个盘子)。
完整代码
#include <stdio.h>
/* 将 n 个盘子从 start 柱,借助 temp 柱,移到 target 柱 */
void hanoi(int n, char start, char temp, char target) {
if (n == 1) {
/* 递归出口:只有一个盘子,直接移 */
printf("Move disk 1 from %c to %c\n", start, target);
return;
}
/* 步骤1:把 n-1 个盘子从 start 移到 temp,target 做辅助 */
hanoi(n - 1, start, target, temp);
/* 步骤2:把第 n 个盘子从 start 移到 target */
printf("Move disk %d from %c to %c\n", n, start, target);
/* 步骤3:把 n-1 个盘子从 temp 移到 target,start 做辅助 */
hanoi(n - 1, temp, start, target);
}
int main() {
int n = 3;
printf("汉诺塔 %d 个盘子的移动步骤:\n", n);
hanoi(n, 'A', 'B', 'C');
return 0;
}
关键点解析
1. 递归出口 if (n == 1) 是出口。只有一个盘子时,不需要再拆分了,直接移动。
2. 参数顺序的变化 注意三次调用 hanoi 时,三个柱子的角色在变化:
-
第一次:
hanoi(n-1, start, target, temp)—— start 是起点,temp 是终点,target 是辅助; -
第二次:
printf直接移最大的盘子; -
第三次:
hanoi(n-1, temp, start, target)—— temp 是起点,target 是终点,start 是辅助。
这个参数顺序是软考填空的常见考点,经常让你填 hanoi(n-1, ___, ___, ___) 里的参数。
3. 时间复杂度 汉诺塔递归式是 T(n) = 2T(n-1) + 1,解出来时间复杂度是 O(2^n)。移动次数恰好是 2^n - 1。
四、案例三:阶乘(最简单的递归)
阶乘是理解递归最直观的例子,虽然简单,但软考里可能作为填空题的一部分出现。
问题描述:n! = n × (n-1) × (n-2) × ... × 1,且 0! = 1, 1! = 1。
完整代码
#include <stdio.h>
/* 递归求阶乘 */
int factorial(int n) {
if (n == 0 || n == 1) {
return 1; /* 递归出口 */
}
return n * factorial(n - 1); /* n! = n × (n-1)! */
}
/* 递推求阶乘 */
int factorial_iter(int n) {
int result = 1;
int i;
for (i = 2; i <= n; i++) {
result = result * i; /* 递推:result_i = result_{i-1} × i */
}
return result;
}
int main() {
int n = 5;
printf("%d! = %d\n", n, factorial(n));
printf("%d! = %d\n", n, factorial_iter(n));
return 0;
}
关键点
-
递归版:出口
n == 0 || n == 1,递归式n * factorial(n-1); -
递推版:初始
result = 1,递推式result *= i; -
时间复杂度都是 O(n),但递归版空间复杂度 O(n)(调用栈),递推版 O(1)。
五、模拟题一(斐波那契)
【说明】 斐波那契数列定义为:F(1) = 1,F(2) = 1,F(n) = F(n-1) + F(n-2)(n ≥ 3)。以下分别给出递归和递推的 C 语言实现。
【C代码】
#include <stdio.h>
/* 递归实现 */
int fib_recursive(int n) {
if ((1)) {
return 1;
}
return (2);
}
/* 递推实现 */
int fib_iterative(int n) {
int i, f1 = 1, f2 = 1, fn;
if (n == 1 || n == 2) return 1;
for (i = 3; i <= n; i++) {
fn = (3);
f1 = f2;
f2 = fn;
}
return fn;
}
int main() {
printf("%d\n", fib_recursive(6));
printf("%d\n", fib_iterative(6));
return 0;
}
【问题1】(9分) 根据说明和 C 代码,补齐 (1)~(3) 处的空缺。
【问题2】(3分) 递归实现采用了(4)算法设计策略,其时间复杂度为(5)(用 O 符号表示)。
【问题3】(3分) 递推实现的时间复杂度为(6)(用 O 符号表示),空间复杂度为(7)(用 O 符号表示)。
六、模拟题二(汉诺塔)
【说明】 汉诺塔问题:将 n 个盘子从 A 柱借助 B 柱移到 C 柱,每次只能移动一个盘子,且大盘不能压小盘。采用递归算法,将移动 n 个盘子的问题分解为三个子问题。
【C代码】
#include <stdio.h>
void hanoi(int n, char start, char temp, char target) {
if (n == 1) {
printf("Move disk 1 from %c to %c\n", start, target);
return;
}
/* 将 n-1 个盘子从 start 移到 temp */
(1);
/* 将第 n 个盘子从 start 移到 target */
printf("Move disk %d from %c to %c\n", n, start, target);
/* 将 n-1 个盘子从 temp 移到 target */
(2);
}
int main() {
int n = 3;
hanoi(n, 'A', 'B', 'C');
return 0;
}
【问题1】(6分) 根据说明和 C 代码,补齐 (1)~(2) 处的空缺。
【问题2】(3分) 该算法采用了(3)算法设计策略。
【问题3】(3分) 该算法移动 n 个盘子所需的最少移动次数为(4)(用含 n 的公式表示),其时间复杂度为(5)(用 O 符号表示)。
【问题4】(3分) 若将上述递归算法改为非递归实现,通常需要借助的数据结构是(6)。
七、参考答案
模拟题一
【问题1】
-
(1) n == 1 || n == 2(或
n <= 2) -
(2) fib_recursive(n - 1) + fib_recursive(n - 2)
-
(3) f1 + f2
【问题2】
-
(4) 递归(或递归法)
-
(5) O(2^n)
【问题3】
-
(6) O(n)
-
(7) O(1)
模拟题二
【问题1】
-
(1) hanoi(n - 1, start, target, temp)
-
(2) hanoi(n - 1, temp, start, target)
【问题2】
-
(3) 递归(或递归法 / 分治,但更准确填递归)
【问题3】
-
(4) 2^n - 1
-
(5) O(2^n)
【问题4】
-
(6) 栈(或堆栈 / Stack)
解析:递归转非递归,本质上是用显式栈模拟系统调用栈。汉诺塔的非递归实现确实需要借助栈来保存中间状态。
八、小结
递归和递推在软考第四题里,经常作为"送分识别题"出现。记住这几个判断技巧:
| 看到什么 | 填什么 |
|---|---|
| 函数自己调用自己 | 递归 |
| 有明确的递归出口(n==1, n==0 等) | 递归 |
| 循环 + 从已知推未知(f1, f2 推 fn) | 递推 |
| 汉诺塔、阶乘、斐波那契、二叉树遍历 | 递归 |
| 爬楼梯、最小数生成、阶乘循环版 | 递推 |
另外,递归出口和递归调用式是填空的两大必考点。出口通常是 n == 1、n == 0、list == NULL 这类边界条件;调用式通常就是函数名加参数变化(n-1、n-2、next 指针等)。
最后提醒一句:递归代码好看但效率低,递推代码啰嗦但效率高。软考如果让你选,除非题目明确要求递归,否则能递推就递推。
九、下一篇预告
【软考算法】软件设计师下午第四题之链表、树与图:指针操作、遍历顺序与邻接矩阵的"基本功"
如果说递归是"函数的自我修养",那链表、树、图就是"数据的自我修养"。下一篇我们聊聊链表的头插尾插、二叉树的前中后序遍历、图的 DFS 和 BFS,以及邻接矩阵和邻接表在软考里怎么考。

310

被折叠的 条评论
为什么被折叠?



