20260228 142501 Cpp 入门第十五课

20260228_142501_CPP_入门第十五课.md

第十五章:递归算法——函数调用自己

你好!欢迎来到第十五章!在前面的章节,我们学习了递推,从已知条件一步步推出结果。这一章我们学习另一种重要的思想:递归——函数自己调用自己。就像俄罗斯套娃,一个大娃娃里面套着一个一模一样的小娃娃。递归在解决某些问题时特别简洁,比如遍历文件夹、计算阶乘、汉诺塔等。让我们一起来探索递归的奥秘吧!


15.1 递归算法程序范例

先来看一个最简单的递归例子:计算阶乘 n!。阶乘的定义是:
- n! = 1 × 2 × 3 × … × n
- 也可以用递归定义:n! = n × (n-1)!,且 0! = 1。

#include <iostream>
using namespace std;

// 递归函数求阶乘
int factorial(int n) {
    if (n == 0) {          // 递归终止条件
        return 1;
    } else {
        return n * factorial(n - 1);   // 递归调用
    }
}

int main() {
    int n;
    cout << "请输入一个整数:";
    cin >> n;
    cout << n << "! = " << factorial(n) << endl;
    return 0;
}

运行示例(输入5):

5! = 120

这个程序里,factorial 函数在计算过程中又调用了自己,这就是递归。但递归不能无限进行下去,必须有一个终止条件(这里 n == 0 时返回1),否则就会陷入死循环(最终栈溢出)。


15.2 递归算法的用法

15.2.1 什么是递归?

递归就是一个函数直接或间接地调用自身。递归可以把一个复杂问题分解成与原问题相似但规模更小的子问题,直到子问题简单到可以直接求解(终止条件)。

生活中的递归例子:
- 俄罗斯套娃:打开一个娃娃,里面是一个更小的娃娃,直到最小的那个。
- 查字典:要查一个词,发现解释里用了另一个词,又去查那个词,直到某个词的解释简单易懂。

15.2.2 递归的要素

  1. 递归终止条件(基线条件):问题规模足够小,可以直接得到答案,不再调用自身。
  2. 递归关系(递归步骤):将原问题转化为规模更小的子问题,并调用自身解决子问题,然后组合结果。

15.2.3 递归的执行过程

factorial(3) 为例:
- 调用 factorial(3) → 3 != 0,执行 3 * factorial(2)
- 调用 factorial(2) → 2 != 0,执行 2 * factorial(1)
- 调用 factorial(1) → 1 != 0,执行 1 * factorial(0)
- 调用 factorial(0) → 0 == 0,返回 1
- 回到 factorial(1),得到 11 = 1,返回
- 回到 factorial(2),得到 2
1 = 2,返回
- 回到 factorial(3),得到 3*2 = 6,返回

这个过程像一层层深入,再一层层返回,所以递归需要系统栈来保存每一层的局部变量和返回地址。

15.2.4 递归与循环的比较

特点 递归 循环
代码简洁性 通常更简洁,符合数学定义 可能需要更多代码
可读性 对某些问题很直观 通用
效率 有函数调用开销,可能重复计算 通常更快
栈溢出风险 层次太深会栈溢出 无此问题
适用问题 树、图、分治等 线性重复

注意:能用循环解决的问题尽量用循环,但有些问题(如汉诺塔、树的遍历)用递归更自然。

15.2.5 递归的注意事项

  • 必须有终止条件,否则无限递归导致程序崩溃。
  • 递归层次不能太深,否则会栈溢出(系统栈空间有限,一般几千层)。
  • 避免重复计算:比如递归求斐波那契数会有大量重复,可以用记忆化(备忘录)优化。

15.3 编程实例讲解

实例1:递归求斐波那契数列(简单版,可能有重复计算)

#include <iostream>
using namespace std;

int fib(int n) {
    if (n == 1 || n == 2) {
        return 1;
    } else {
        return fib(n - 1) + fib(n - 2);   // 大量重复
    }
}

int main() {
    int n;
    cout << "请输入n:";
    cin >> n;
    cout << "第" << n << "项是:" << fib(n) << endl;
    return 0;
}

问题:n=40 时就会非常慢,因为重复计算太多。可以用记忆化优化。

实例2:记忆化递归(备忘录)

用一个数组保存已经算过的值,避免重复计算。

#include <iostream>
using namespace std;

const int N = 100;
long long memo[N] = {0};   // 记忆数组,初始0表示未计算

long long fib(int n) {
    if (n == 1 || n == 2) return 1;
    if (memo[n] != 0) return memo[n];   // 直接返回已计算结果
    memo[n] = fib(n - 1) + fib(n - 2);   // 计算并保存
    return memo[n];
}

int main() {
    int n;
    cout << "请输入n:";
    cin >> n;
    cout << "第" << n << "项是:" << fib(n) << endl;
    return 0;
}

实例3:汉诺塔问题

题目:有三根柱子A、B、C,A柱上有n个大小不同的圆盘,大盘在下小盘在上。要求把所有圆盘从A移到C,每次只能移动一个,且小盘不能压在大盘上。输出移动步骤。

递归思路
- 要把n个盘子从A移到C,可以先把上面n-1个盘子从A移到B(借助C),然后把最大的盘子从A移到C,最后把n-1个盘子从B移到C(借助A)。

#include <iostream>
using namespace std;

void hanoi(int n, char from, char to, char aux) {
    if (n == 1) {
        cout << "移动盘子 1 从 " << from << " 到 " << to << endl;
        return;
    }
    hanoi(n - 1, from, aux, to);          // 把上面n-1个从from移到aux
    cout << "移动盘子 " << n << " 从 " << from << " 到 " << to << endl;
    hanoi(n - 1, aux, to, from);          // 把n-1个从aux移到to
}

int main() {
    int n;
    cout << "请输入盘子数:";
    cin >> n;
    hanoi(n, 'A', 'C', 'B');
    return 0;
}

运行示例(n=3):

移动盘子 1 从 A 到 C
移动盘子 2 从 A 到 B
移动盘子 1 从 C 到 B
移动盘子 3 从 A 到 C
移动盘子 1 从 B 到 A
移动盘子 2 从 B 到 C
移动盘子 1 从 A 到 C

实例4:递归遍历文件夹(伪代码,需要用到系统函数)

虽然不能直接运行,但可以展示思想:遍历文件夹时,如果是文件就处理,如果是文件夹就递归进入。

#include <iostream>
#include <filesystem>  // C++17 需要
namespace fs = std::filesystem;

void listFiles(const fs::path &path) {
    for (const auto &entry : fs::directory_iterator(path)) {
        if (entry.is_directory()) {
            listFiles(entry.path());   // 递归子文件夹
        } else {
            cout << entry.path() << endl;
        }
    }
}

实例5:递归求最大公约数(辗转相除法)

辗转相除法的递归定义:gcd(a, b) = gcd(b, a % b),直到 b=0。

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

实例6:全排列生成

用递归生成一个数组的所有排列(经典回溯)。

#include <iostream>
using namespace std;

void permute(int arr[], int l, int r) {
    if (l == r) {
        for (int i = 0; i <= r; i++) cout << arr[i] << " ";
        cout << endl;
    } else {
        for (int i = l; i <= r; i++) {
            swap(arr[l], arr[i]);
            permute(arr, l + 1, r);
            swap(arr[l], arr[i]);   // 回溯
        }
    }
}

int main() {
    int arr[] = {1, 2, 3};
    int n = sizeof(arr) / sizeof(arr[0]);
    permute(arr, 0, n - 1);
    return 0;
}

15.4 阶段性编程练习

  1. 练习1:用递归求 1+2+…+n 的和。
  2. 练习2:用递归判断一个字符串是否是回文(如 “abcba”)。
  3. 练习3:用递归实现二分查找(在有序数组中找某个数)。
  4. 练习4:用递归输出杨辉三角的第n行(提示:杨辉三角可以用递归公式 C(n,k) = C(n-1,k-1) + C(n-1,k))。
  5. 练习5:用递归解决“数字反转”问题(输入一个整数,输出反转后的数,如 1234 → 4321,考虑负数?)

15.5 第15章编程作业

作业1:递归求幂

写一个递归函数 double power(double x, int n) 计算 x 的 n 次幂(n 为非负整数)。要求时间复杂度 O(n)。

作业2:猴子吃桃(递归版)

猴子第一天摘了若干桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天又将剩下的桃子吃了一半,又多吃了一个。以后每天都这样。到第10天早上想再吃时,只剩一个桃子了。用递归函数求第一天摘了多少个桃子。

作业3:整数划分

将一个正整数 n 划分成若干个正整数之和,求划分的种数。例如 n=4 有 5 种划分:4, 3+1, 2+2, 2+1+1, 1+1+1+1。用递归实现。

作业4:汉诺塔步数计算

在汉诺塔问题中,移动 n 个盘子需要多少步?推导出公式并用递归验证。

作业5:八皇后问题(选做)

在 8×8 的国际象棋棋盘上放置八个皇后,使得它们互不攻击(即任意两个不在同一行、同一列、同一对角线上)。用递归回溯法求解并输出所有解(或一个解)。


恭喜你完成了第十五章的学习!递归是一种强大的编程思想,它让我们能简洁地解决许多复杂问题。虽然递归有时效率不高,但它的思维方式非常重要,也是学习更高级算法(如动态规划、回溯、分治)的基础。加油!🚀