【软考算法】软件设计师下午第四题之递归与递推:斐波那契、汉诺塔与阶乘的“自我调用“

前面我们聊了分治法、动态规划、贪心、回溯。今天聊递归与递推,这两兄弟长得像,但脾气不一样。递归是"自己调用自己,直到撞墙才停";递推是"从已知出发,一步一步推未知"。软考里经常把它们放在一起考,或者让你区分,或者让你填空补全递归出口。

一、递归和递推:先搞清楚区别

在软考官方教材里,递归和递推是分开列的两个算法设计方法。很多考生容易搞混,我们先一句话区分:

递归递推
自顶向下,从大问题拆成小问题,自己调用自己自底向上,从已知初始值出发,逐步计算后续值
需要递归出口(终止条件)需要递推公式初始条件
代码里有函数自己调用自己代码里通常是循环迭代
空间复杂度较高(调用栈)空间复杂度较低(通常 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,每次只能移一个,且大盘不能压小盘。

递归思路

  1. 把上面 n-1 个盘子从 A 移到 B(借助 C);

  2. 把第 n 个(最大的)盘子从 A 移到 C;

  3. 把 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 == 1n == 0list == NULL 这类边界条件;调用式通常就是函数名加参数变化(n-1n-2next 指针等)。

最后提醒一句:递归代码好看但效率低,递推代码啰嗦但效率高。软考如果让你选,除非题目明确要求递归,否则能递推就递推。


九、下一篇预告

【软考算法】软件设计师下午第四题之链表、树与图:指针操作、遍历顺序与邻接矩阵的"基本功"

如果说递归是"函数的自我修养",那链表、树、图就是"数据的自我修养"。下一篇我们聊聊链表的头插尾插、二叉树的前中后序遍历、图的 DFS 和 BFS,以及邻接矩阵和邻接表在软考里怎么考。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值