NOI C++ 课程体系 / 算法篇 / 第5讲
CSP-J 入门级 · 基础算法 · m4-05

递归法

函数调用自身,大事化小 —— 掌握递归的三要素、递归树分析、记忆化搜索,在竞赛中灵活运用递归思想解题。

6 大模块 3 大递归要素 4 个经典案例 5 大易错点 32 道分层练习
本讲 · 第 29/56 讲递归法
模块 01

情景导航

用生活中的"自我引用"现象,直觉地理解递归思想。

故事俄罗斯套娃的启示

小明收到了一个俄罗斯套娃作为生日礼物。他兴奋地打开最外面的大娃娃,发现里面有一个稍小一点的娃娃,形状完全一样。

再打开,里面又有一个更小的……一层一层打开,直到最里面是一个实心的小娃娃,再也打不开了。

"每个娃娃里面都藏着一个更小的自己" —— 这就是递归!

递归的核心思想:把一个大问题分解为结构相同但规模更小的子问题,直到子问题简单到可以直接解决(base case),然后逐层返回,组合出大问题的答案。

就像打开套娃一样:打开 → 里面还是套娃 → 继续打开 → ... → 最小的实心娃娃(终止条件)→ 合上 → 合上 → ... → 完整套娃

💡 递归的两个关键

① 自我调用:函数调用自身来解决规模更小的子问题

② 终止条件(Base Case):当问题足够小时直接返回答案,不再递归

没有终止条件的递归 = 死循环 → 栈溢出(Stack Overflow)💥

①生活类比:递归无处不在
🪆

俄罗斯套娃

每个娃娃里藏着更小的自己,直到最小的实心娃娃。

→ 递归调用 + 终止条件
🪞

镜中镜

两面镜子相对,看到无限嵌套的影像。

→ 递归的自相似性
📖

查字典

查一个词 → 解释中有不认识的词 → 再查字典 → 直到全认识。

→ 递归分解
🏢

公司层级汇报

CEO问VP → VP问总监 → 总监问经理 → 经理问员工 → 逐层汇报回来。

→ 递归调用 + 返回值
②编程定义

递归(Recursion):函数在其定义中调用自身来解决问题的一种编程技术。递归将大问题分解为结构相同但规模更小的子问题,直到子问题可以直接求解。

递归的数学表达:f(n) = G(f(n-1), f(n-2), ...),配合 f(base) = 已知值。

📌 本讲核心知识地图

板块核心内容对应大纲
递归三要素终止条件、递归关系、规模缩小递归法
阶乘递归n! = n × (n-1)!经典递归案例
斐波那契递归F(n) = F(n-1) + F(n-2) + 递归树分析递归效率分析
记忆化搜索用数组缓存已计算结果,避免重复计算递归优化
汉诺塔递归经典分治递归递归应用
辗转相除法gcd(a,b) = gcd(b, a%b)递归应用
③核心口诀
🔑 递归三要素

① 终止条件(Base Case):最小的问题直接答,不再调用
② 递归关系:大问题怎么拆成小问题?f(n) = ? × f(n-1)
③ 规模缩小:每次调用必须让问题更小,确保能终止!

🔑 递归模板

int f(int n) {
  if (n == base) return 已知值; // 终止条件
  return ... f(更小的n) ...; // 递归调用
}

🔑 递归与递推

递归:自顶向下,大问题拆小问题,代码简洁优雅
递推:自底向上,小问题推大问题,运行效率高
递归 + 记忆化 = 递推的效率 + 递归的简洁!

模块 02

知识探究

深入理解递归三要素,通过四个经典案例掌握递归的设计与分析方法。

1递归的三要素 核心

一个正确的递归函数必须包含以下三个要素:

要素① 终止条件
  • 也叫 Base Case / 递归基
  • 问题规模最小时的答案
  • 不再调用自身,直接返回
  • 没有终止条件 = 死递归!
要素② 递归关系
  • 大问题如何用小问题的答案表示
  • 如:n! = n × (n-1)!
  • 体现"自我相似"的结构
  • 这是递归的核心逻辑
要素③ 规模缩小
  • 每次递归调用参数必须变小
  • 保证最终能到达终止条件
  • 如:f(n) 调用 f(n-1)
  • 参数不变 = 无限递归!
🔑 递归函数的通用模板

int solve(int n) {
  // ① 终止条件:问题足够小,直接返回
  if (n == 1) return 已知值;

  // ② 递归调用:拆分为更小的子问题
  int sub = solve(n - 1); // ③ 规模缩小

  // 组合子问题的结果
  return ... sub ...;
}

案例经典案例1:阶乘 入门

计算 n! = 1 × 2 × 3 × ... × n。这是最简单的递归入门案例。

🔍 三要素分析

① 终止条件:0! = 1(或 1! = 1),最小的阶乘直接返回 1

② 递归关系:n! = n × (n-1)! — 大阶乘 = 当前数 × 小一号的阶乘

③ 规模缩小:每次调用 n 减 1,从 n → n-1 → n-2 → ... → 1

C++factorial.cpp — 递归求阶乘
#include <iostream>
using namespace std;

long long fact(int n) {
    // ① 终止条件:0! = 1
    if (n == 0 || n == 1) return 1;

// ② 递归关系 + ③ 规模缩小
    return n * fact(n - 1);
}

int main() {
    int n; cin >> n;
    cout << fact(n) << endl;
    return 0;
}
运行结果
输入:5
输出:120

📊 执行过程追踪(n=5)

fact(5) = 5 × fact(4)

    fact(4) = 4 × fact(3)

        fact(3) = 3 × fact(2)

            fact(2) = 2 × fact(1)

                fact(1) = 1 ← 终止条件!

            fact(2) = 2 × 1 = 2

        fact(3) = 3 × 2 = 6

    fact(4) = 4 × 6 = 24

fact(5) = 5 × 24 = 120 ✅

递归分两阶段:"递推"(向下调用)→ 到达 base case → "回归"(逐层返回结果)

案例经典案例2:斐波那契递归 必会

F(n) = F(n-1) + F(n-2),F(1)=1, F(2)=1。用递归来实现。

C++fib_recursive.cpp — 朴素递归
long long fib(int n) {
    if (n <= 2) return 1;       // 终止条件
    return fib(n-1) + fib(n-2); // 递归调用两次!
}

⚠️ 朴素递归的致命问题:重复计算

计算 fib(5) 时,fib(3) 被计算了 2 次,fib(2) 被计算了 3 次!

计算 fib(40) 时,fib(1) 被调用了 上亿次!

时间复杂度:O(2ⁿ) — 指数级增长,n=40 就要等几十秒!

🌳递归树分析 核心工具

递归树是分析递归算法效率的核心工具。每次递归调用产生子节点,形成一棵树。

🌳 fib(5) 的递归树
fib(5) fib(4) fib(3) fib(3) fib(2) fib(2) fib(1) fib(2) fib(1) 1 1 叶子节点 = 终止条件 (返回1) | 总调用次数 = 15次 (fib(5))
n总调用次数叶子节点数时间复杂度
fib(5)15 次8—
fib(10)177 次89—
fib(20)21,891 次10,946—
fib(40)~3.3亿次~1.65亿O(2ⁿ)

💡 递归树揭示了什么?

• 树的深度 = 递归的最大层数 = O(n)

• 树的节点总数 = 总调用次数 = O(2ⁿ)

• 大量重复子问题:fib(3) 出现了 3 次,fib(2) 出现了 5 次

解决方案:把已计算的结果存起来(记忆化),避免重复计算!

⚡记忆化搜索 提速关键

记忆化搜索(Memoization):用一个数组缓存已计算过的结果。每次递归前先查缓存,如果算过就直接返回,不再重复计算。

❌ 朴素递归 O(2ⁿ)
C++慢!
long long fib(int n) {
  if(n<=2) return 1;
  return fib(n-1)
       + fib(n-2);
}
✅ 记忆化搜索 O(n)
C++快!
long long memo[100]={};
long long fib(int n) {
  if(n<=2) return 1;
  if(memo[n]) return memo[n];
  memo[n]=fib(n-1)+fib(n-2);
  return memo[n];
}
C++fib_memo.cpp — 完整记忆化搜索
#include <iostream>
using namespace std;

long long memo[100] = {0};  // 缓存数组,初始为0

long long fib(int n) {
    // ① 终止条件
    if (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; cin >> n;
    cout << fib(n) << endl;
    return 0;
}
运行结果
输入:50
输出:12586269025
(朴素递归需要数小时,记忆化搜索 < 0.001秒!)
🔑 记忆化搜索 = 递归 + 缓存

① 开一个数组 memo[] 缓存结果(初始化为 0 或 -1)
② 递归函数开头:if (memo[n] != 0) return memo[n];
③ 计算完存入:memo[n] = 结果;
效果:O(2ⁿ) → O(n),提速亿倍!

案例经典案例3:汉诺塔递归 经典

将 n 个盘子从 A 柱移到 C 柱(借助 B 柱),每次只能移一个盘子,大盘不能放在小盘上。

🔍 递归分析

思路:把问题拆成三步:

① 把上面 n-1 个盘子从 A 移到 B(借助 C)→ hanoi(n-1, A, C, B)

② 把最大的盘子从 A 移到 C → move A → C

③ 把 B 上的 n-1 个盘子移到 C(借助 A)→ hanoi(n-1, B, A, C)

终止条件:n=1 时直接移动

C++hanoi.cpp — 递归输出移动步骤
#include <iostream>
using namespace std;

int step = 0;

void hanoi(int n, char from, char aux, char to) {
    // 终止条件:只有1个盘子,直接移动
    if (n == 1) {
        cout << ++step << ": " << from << " → " << to << endl;
        return;
    }

// ① 把 n-1 个从 from 移到 aux(借助 to)
    hanoi(n - 1, from, to, aux);

// ② 把最大的从 from 移到 to
    cout << ++step << ": " << from << " → " << to << endl;

// ③ 把 n-1 个从 aux 移到 to(借助 from)
    hanoi(n - 1, aux, from, to);
}

int main() {
    int n; cin >> n;
    hanoi(n, 'A', 'B', 'C');
    cout << "共 " << step << " 步" << endl;
    return 0;
}
运行结果 (n=3)
1: A → C
2: A → B
3: C → B
4: A → C
5: B → A
6: B → C
7: A → C
共 7 步 (= 2³-1)
案例经典案例4:辗转相除法(GCD) 实用

求两个正整数 a 和 b 的最大公约数(Greatest Common Divisor)。

🔍 递归分析

欧几里得算法:gcd(a, b) = gcd(b, a mod b)

终止条件:当 b = 0 时,gcd(a, 0) = a

规模缩小:每次 b 变成 a mod b,而 a mod b < b,所以参数严格递减

C++gcd.cpp — 辗转相除法
#include <iostream>
using namespace std;

int gcd(int a, int b) {
    if (b == 0) return a;  // 终止条件
    return gcd(b, a % b);    // 递归调用
}

int main() {
    int a, b; cin >> a >> b;
    cout << gcd(a, b) << endl;
    return 0;
}
运行结果
输入:48 36
输出:12

执行过程:
gcd(48, 36) → gcd(36, 12) → gcd(12, 0) → 返回 12

💡 GCD 递归的效率

辗转相除法的递归深度最多 O(log(min(a,b))) 层。

即使 a, b 是 10⁹ 级别的大数,递归也只需 ~30 层,非常快!

这是递归效率极高的典型案例 —— 每次调用规模缩小一半以上。

模块 03

实践应用

通过分析和编程题,巩固递归的设计、追踪与优化能力。

题1递归追踪

题目:以下递归函数 f(6) 的返回值是多少?请逐步追踪执行过程。

C++追踪题
int f(int n) {
    if (n == 1) return 1;
    if (n % 2 == 0) return f(n / 2) + 1;
    return f(n - 1) + n;
}
提示:注意两条递归路径:偶数走 n/2,奇数走 n-1。
查看参考答案

逐步追踪:

f(6) → 6 是偶数 → f(3) + 1

    f(3) → 3 是奇数 → f(2) + 3

        f(2) → 2 是偶数 → f(1) + 1

            f(1) → 1(终止条件)

        f(2) = 1 + 1 = 2

    f(3) = 2 + 3 = 5

f(6) = 5 + 1 = 6

答案:f(6) = 6

递归路径分析:6 → 3(偶数/2)→ 2(奇数-1)→ 1(终止)。共 3 层递归。

这个函数计算的是什么?对于偶数 n,f(n) = f(n/2)+1;对于奇数 n,f(n) = f(n-1)+n。这是一个混合递推关系。

题2递归树分析

题目:以下递归函数的时间复杂度是多少?画出 T(4) 的递归树。

int T(int n) {
    if (n <= 1) return 1;
    return T(n-1) + T(n-1) + 1;  // 两次调用 T(n-1)
}
查看参考答案

递归树分析(T(4)):

第0层:T(4) → 产生 2 个 T(3)

第1层:2 个 T(3) → 产生 4 个 T(2)

第2层:4 个 T(2) → 产生 8 个 T(1)

第3层:8 个 T(1) → 终止(返回1)

树的总节点数:1 + 2 + 4 + 8 = 15 = 2⁴ - 1

时间复杂度:O(2ⁿ)

详细分析:每层节点数翻倍(因为每次调用 2 个子问题),树深为 n,总节点 = 2ⁿ-1。

递推关系:T(n) = 2T(n-1) + 1,T(1)=1,解得 T(n) = 2ⁿ - 1

优化方案:两个 T(n-1) 是相同的子问题!用记忆化只需计算一次,复杂度降为 O(n)。

题3记忆化编程

题目:用记忆化搜索求解以下递推:f(1)=1, f(2)=2, f(n) = f(n-1) + 2×f(n-2) + n。求 f(20)。

查看参考答案
C++memo_example.cpp
#include <iostream>
using namespace std;

long long memo[25] = {0};

long long f(int n) {
    // 终止条件
    if (n == 1) return 1;
    if (n == 2) return 2;

// 查缓存
    if (memo[n] != 0) return memo[n];

// 递归计算 + 存缓存
    memo[n] = f(n-1) + 2*f(n-2) + n;
    return memo[n];
}

int main() {
    cout << f(20) << endl;
    return 0;
}

手动验证前几项:

f(1) = 1

f(2) = 2

f(3) = f(2) + 2×f(1) + 3 = 2 + 2 + 3 = 7

f(4) = f(3) + 2×f(2) + 4 = 7 + 4 + 4 = 15

f(5) = f(4) + 2×f(3) + 5 = 15 + 14 + 5 = 34

f(20) = 1223327

分析:这个递推每次调用两个子问题 f(n-1) 和 f(n-2),朴素递归的复杂度类似斐波那契 O(1.618ⁿ)。加记忆化后降为 O(n)。

模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
递归三要素终止条件 + 递归关系 + 规模缩小
递归执行过程"递推"向下调用 → 到达 base case → "回归"逐层返回
递归树分析递归效率的核心工具:节点数=调用次数,深度=递归层数
朴素递归问题重复子问题导致 O(2ⁿ) 指数级复杂度
记忆化搜索缓存已算结果,O(2ⁿ) → O(n),提速亿倍
阶乘递归n! = n × (n-1)!,递归深度 O(n)
汉诺塔递归三步分治,移动次数 = 2ⁿ-1
GCD 递归gcd(a,b) = gcd(b, a%b),深度 O(log n)
2拓展:尾递归优化

💡 什么是尾递归?

尾递归:递归调用是函数体中的最后一个操作,返回后不再做任何计算。

普通递归:return n × fact(n-1); → 调用后还要做乘法

尾递归:return fact_tail(n-1, n×acc); → 调用后直接返回

编译器可以将尾递归优化为循环,不消耗栈空间!

// 尾递归版阶乘
long long fact_tail(int n, long long acc = 1) {
    if (n <= 1) return acc;
    return fact_tail(n - 1, n * acc);  // 最后一步就是递归调用
}

注意:gcc 在 -O2 优化下会自动做尾递归优化。但大多数递归(如 fib)不是尾递归,无法优化。

3思考与启发

💡 学完本讲,你应该掌握的7项能力

1. 能写出递归函数的三要素:终止条件、递归关系、规模缩小。

2. 能手动追踪递归的执行过程(递推 + 回归两阶段)。

3. 能画递归树分析时间复杂度和调用次数。

4. 能识别重复子问题并用记忆化搜索优化。

5. 能用递归解决汉诺塔、GCD、阶乘等经典问题。

6. 能区分递归 vs 递推的优劣,选择合适的方法。

7. 知道栈溢出的风险,递归深度不宜超过 10⁵。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

忘记写终止条件

❌ 错误
没有 base case,递归无限调用,最终 Stack Overflow 程序崩溃。
✅ 正确
第一行就写终止条件:if (n == base) return 已知值; 这是递归的"安全阀"。
口诀:递归第一行,先写终止条件!
易错2

递归参数没有缩小

❌ 错误
f(n) 调用 f(n) 或 f(n+1),参数不变或增大,永远到不了终止条件。
✅ 正确
每次递归调用,参数必须严格变小(或趋向终止条件),如 f(n-1)、f(n/2)。
口诀:参数必须缩小,否则无限循环!
易错3

朴素递归算斐波那契

❌ 错误
return fib(n-1)+fib(n-2); n=40 就超时,n=50 根本跑不完。
✅ 正确
加记忆化或改用递推。竞赛中 fib 绝对不能用朴素递归!
口诀:递归有重复?记忆化来救!
易错4

递归深度过大导致栈溢出

❌ 错误
n=100000 时递归深度 10 万层,超出栈空间限制(通常 8MB),程序崩溃。
✅ 正确
递归深度超过 ~10000 时,改用递推(循环)实现。或手动扩大栈空间。
口诀:深度超万层,递推替递归!
易错5

记忆化数组初始化和查询搞混

❌ 错误
初始值设 0,但 f(n) 的合法结果也可能是 0,导致重复计算。
✅ 正确
如果结果可能为 0,初始值设为 -1,查询条件改为 if(memo[n] != -1)。
口诀:缓存初始值用 -1,避免和 0 搞混!
速查递归法速查表
经典问题递归关系终止条件复杂度
阶乘f(n) = n × f(n-1)f(0)=1O(n)
斐波那契F(n) = F(n-1) + F(n-2)F(1)=F(2)=1O(n) 记忆化
汉诺塔hanoi(n) = 2×hanoi(n-1)+1n=1直接移O(2ⁿ)
GCDgcd(a,b) = gcd(b, a%b)b=0返回aO(log n)
二分查找search(mid) → 左半/右半找到或空O(log n)
全排列选一个 → 递归剩余空集返回O(n!)
📌知识点笔记卡

⭐ 递归三要素

终止条件(base)

递归关系(拆分子问题)

规模缩小(趋近终止)

🌳 递归树

节点 = 一次调用

深度 = 递归层数

总节点 = 时间复杂度

⚡ 记忆化搜索

数组缓存已算结果

查询 → 命中直接返回

O(2ⁿ) → O(n)

🗼 汉诺塔

三步分治递归

移动次数 2ⁿ-1

经典递归思维

🔢 GCD

gcd(a,b)=gcd(b,a%b)

深度 O(log n)

高效递归典范

⚠️ 避坑指南

必写终止条件

参数必须缩小

深度过大用递推

课后练习

分层练习作业

A基础 / B提高 / C挑战 + 洛谷原题20道,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

递归函数必须包含的是()

A. 循环语句   B. 终止条件   C. 全局变量   D. 指针操作

查看参考答案

答案:B. 终止条件

详细分析:没有终止条件的递归会无限调用自身,导致栈溢出(Stack Overflow)。终止条件(Base Case)是递归的"安全阀",让递归在问题规模最小时直接返回答案,不再继续调用。

易错:只写了递归调用而忘记写终止条件,程序会无限调用自身,导致栈溢出(Stack Overflow)。

A2选择题

以下递归函数 f(4) 的值是()

int f(int n) {
    if (n == 1) return 1;
    return 2 * f(n - 1) + 1;
}

A. 7   B. 15   C. 31   D. 8

查看参考答案

答案:B. 15

逐步追踪:

f(1) = 1(终止条件)

f(2) = 2×f(1)+1 = 2×1+1 = 3

f(3) = 2×f(2)+1 = 2×3+1 = 7

f(4) = 2×f(3)+1 = 2×7+1 = 15

观察规律:f(n) = 2ⁿ-1(与汉诺塔移动次数相同!)

考点:递归要逐层展开:先算终止条件 f(1),再按 f(n)=2f(n−1)+1 一层层往回代。

易错:算到 f(3)=7 就停笔,漏掉最后一层 f(4)=15。

A3选择题

记忆化搜索的主要作用是()

A. 让代码更短   B. 避免重复计算,提高效率   C. 减少内存使用   D. 让递归不再需要终止条件

查看参考答案

答案:B. 避免重复计算,提高效率

详细分析:朴素递归(如斐波那契)会大量重复计算相同的子问题。记忆化搜索用数组缓存已算过的结果,下次遇到相同子问题时直接返回缓存值,将时间复杂度从 O(2ⁿ) 降为 O(n)。代价是需要 O(n) 的额外空间存储缓存。

易错:以为记忆化能“消除递归”或降低空间复杂度——它只是用额外的 O(n) 空间换取时间,递归结构本身不变。

A4编程题

用递归实现一个函数,计算正整数 n 的各位数字之和。例如 sum_digits(1234) = 1+2+3+4 = 10。

查看参考答案
#include <iostream>
using namespace std;

int sum_digits(int n) {
    // 终止条件:个位数直接返回
    if (n < 10) return n;

// 递归:个位 + 去掉个位后的数字之和
    return n % 10 + sum_digits(n / 10);
}

int main() {
    int n; cin >> n;
    cout << sum_digits(n) << endl;
    return 0;
}
运行结果
输入:1234
输出:10

执行过程:
sum_digits(1234) = 4 + sum_digits(123)
  sum_digits(123) = 3 + sum_digits(12)
    sum_digits(12) = 2 + sum_digits(1)
      sum_digits(1) = 1(终止)
    = 2 + 1 = 3
  = 3 + 3 = 6
= 4 + 6 = 10

三要素分析:① 终止:n<10 时返回 n ② 关系:n%10 + f(n/10) ③ 缩小:n/10 去掉末位

易错:终止条件写成 n<10 却对 n=0 也直接返回;或递归分支忘记 return,导致返回值丢失、结果不对。

🟡 B组 · 提高(4题)

B1编程题

用递归实现二分查找:在有序数组 a[] 中查找目标值 target,返回下标(找不到返回 -1)。

查看参考答案
#include <iostream>
using namespace std;

int a[] = {2,5,8,12,16,23,38,56,72,91};

int bsearch(int l, int r, int target) {
    // 终止条件1:范围无效,没找到
    if (l > r) return -1;

int mid = l + (r - l) / 2;

// 终止条件2:找到了
    if (a[mid] == target) return mid;

// 递归:在左半或右半继续找
    if (target < a[mid])
        return bsearch(l, mid - 1, target);
    else
        return bsearch(mid + 1, r, target);
}

int main() {
    cout << bsearch(0, 9, 23) << endl;  // 输出 5
    cout << bsearch(0, 9, 10) << endl;  // 输出 -1
    return 0;
}

三要素分析:① 终止:找到或范围为空 ② 关系:比较 mid 与 target,选左或右半 ③ 缩小:搜索范围每次减半

复杂度:O(log n),每次递归范围减半,最多 log₂n 层。

易错:递归时把区间传成 (l,mid) 或 (mid,r) 而没有 mid±1,区间不再缩小,会陷入无限递归。

B2编程题

用记忆化搜索求解:f(1)=1, f(n) = f(n-1) + f(⌊n/2⌋) + n。求 f(100)。(⌊x⌋ 表示向下取整)

查看参考答案
#include <iostream>
using namespace std;

long long memo[105];
bool computed[105] = {};  // 记录是否已计算

long long f(int n) {
    if (n == 1) return 1;
    if (computed[n]) return memo[n];

computed[n] = true;
    memo[n] = f(n-1) + f(n/2) + n;
    return memo[n];
}

int main() {
    cout << f(100) << endl;
    return 0;
}

注意:这里用 bool 数组 computed[] 而不是检查 memo[n]!=0,因为 f(n) 的值可能恰好为 0(虽然本题不会)。

分析:此递推调用 f(n-1) 和 f(n/2),如果不记忆化,子问题会大量重复。加记忆化后每个 n 只算一次,O(n)。

f(100) = 10930

B3编程题

用递归实现字符串反转:输入 "hello",输出 "olleh"。不允许使用循环。

查看参考答案
#include <iostream>
#include <string>
using namespace std;

string reverse_str(string s) {
    // 终止条件:空串或单字符
    if (s.length() <= 1) return s;

// 递归:最后一个字符 + 反转剩余部分
    return s[s.length()-1] + reverse_str(s.substr(0, s.length()-1));
}

int main() {
    string s; cin >> s;
    cout << reverse_str(s) << endl;
    return 0;
}
运行结果
输入:hello
输出:olleh

另一种写法(交换法):用两个指针从两端向中间递归交换字符,空间更优。

三要素:① 终止:长度≤1 ② 关系:末字符 + 反转(去掉末字符) ③ 缩小:每次去掉一个字符

思路:把“反转整个串”化为“最后一个字符 + 反转剩余前缀”,每递归一次规模减 1。

易错:终止条件写错(如 s.length()==0 时仍取 s[s.length()-1])导致越界;或返回时把字符顺序写反。

B4分析题

以下递归函数的时间复杂度是多少?请画递归树分析。

int f(int n) {
    if (n <= 1) return 1;
    return f(n/2) + f(n/2) + n;
}
查看参考答案

递归树分析:

第0层:f(n),工作量 n

第1层:2 个 f(n/2),总工作量 2×(n/2) = n

第2层:4 个 f(n/4),总工作量 4×(n/4) = n

...

第k层:2ᵏ 个 f(n/2ᵏ),总工作量 n

树的深度:n/2ᵏ = 1 → k = log₂n

每层工作量:都是 n

总工作量:n × log₂n

时间复杂度:O(n log n)

这就是归并排序的递归结构!每层做 O(n) 的工作,共 log n 层。

易错:把递归树各层的“节点个数”当成总工作量,忽略每个子问题的规模也在不断变小,误答 O(2ⁿ)。

🔴 C组 · 挑战(4题)

C1初赛真题

以下程序输出什么?

#include <iostream>
using namespace std;
int f(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    return f(n-1) + f(n-2);
}
int main() { cout << f(7) << endl; }

A. 8   B. 13   C. 21   D. 34

查看参考答案

答案:B. 13

分析:这就是斐波那契数列,f(0)=0, f(1)=1:

f(0)=0, f(1)=1, f(2)=1, f(3)=2, f(4)=3, f(5)=5, f(6)=8, f(7)=13

注意这里的初始条件与课上讲的 f(1)=f(2)=1 不同(f(0)=0),所以数值偏移了一位。

C2编程题

用递归实现全排列:输出 1~n 的所有排列(n≤8)。例如 n=3 时输出 123, 132, 213, 231, 312, 321。

查看参考答案
#include <iostream>
using namespace std;

int n;
int perm[10];
bool used[10] = {};

void dfs(int pos) {
    // 终止条件:已经填完 n 个位置
    if (pos == n) {
        for(int i=0;i<n;i++) cout << perm[i];
        cout << " ";
        return;
    }

// 尝试在每个位置填入 1~n 中未使用的数字
    for(int i=1;i<=n;i++) {
        if(!used[i]) {
            used[i] = true;
            perm[pos] = i;
            dfs(pos + 1);      // 递归填下一个位置
            used[i] = false;  // 回溯!
        }
    }
}

int main() {
    cin >> n;
    dfs(0);
    cout << endl;
    return 0;
}
运行结果 (n=3)
123 132 213 231 312 321

关键点:这是回溯法(DFS),是递归的重要应用。used[i]=false 是"回溯"操作,撤销选择以便尝试其他分支。

复杂度:O(n!),n 个数字的全排列有 n! 种。

思路:把“填位置”作为递归层次,第 pos 位从 1~n 中挑一个尚未使用的数字,填满后输出并回溯取消标记。

易错:回溯时忘记撤销 used[i]=false,只能输出第一个排列;或输出格式漏了分隔符被判格式错误。

C3编程题

用递归实现快速幂:计算 a^n mod p,要求时间复杂度 O(log n)。

查看参考答案
#include <iostream>
using namespace std;

long long fast_pow(long long a, long long n, long long p) {
    // 终止条件
    if (n == 0) return 1;

// 递归计算 a^(n/2)
    long long half = fast_pow(a, n / 2, p);
    half = half * half % p;

// 如果 n 是奇数,多乘一个 a
    if (n % 2 == 1)
        half = half * (a % p) % p;

return half;
}

int main() {
    long long a, n, p;
    cin >> a >> n >> p;
    cout << fast_pow(a, n, p) << endl;
    return 0;
}

递归分析:① 终止:n=0 返回 1 ② 关系:aⁿ = (a^(n/2))² × (n为奇数则再乘a) ③ 缩小:n/2 每次减半

复杂度:O(log n),递归深度 log₂n 层。即使 n=10¹⁸ 也只需 ~60 层。

竞赛必会:快速幂是竞赛中出现频率最高的递归算法之一!

易错:n==0 的终止条件漏写;或 n 为奇数时忘记在平方之后再乘一次 a,结果偏小。

C4综合编程

用递归 + 记忆化搜索求解"数字三角形"问题:给定 n 行数字三角形,从顶部到底部,每次只能走到下一行相邻的两个数之一,求路径上的最大和。

查看参考答案
#include <iostream>
#include <algorithm>
using namespace std;

int a[105][105];
int memo[105][105];
bool comp[105][105] = {};
int n;

int dfs(int r, int c) {
    // 终止条件:到达最后一行
    if (r == n - 1) return a[r][c];

// 查缓存
    if (comp[r][c]) return memo[r][c];

// 递归:取左下和右下的最大值 + 当前位置
    comp[r][c] = true;
    memo[r][c] = a[r][c] + max(dfs(r+1, c), dfs(r+1, c+1));
    return memo[r][c];
}

int main() {
    cin >> n;
    for(int i=0;i<n;i++)
        for(int j=0;j<=i;j++)
            cin >> a[i][j];
    cout << dfs(0, 0) << endl;
    return 0;
}

分析:二维递归 + 记忆化。状态是 (行, 列),每个状态只算一次。

复杂度:状态数 n²/2,每个状态 O(1),总 O(n²)。朴素递归 O(2ⁿ)。

这就是动态规划的递归写法(自顶向下的记忆化搜索 = 自底向上的 DP 递推)。

易错:只用 memo 数组而没有 comp 标记数组,无法区分“值为 0”和“尚未计算过”,会重复计算甚至死循环。

🏆 洛谷原题精选(20题)

以下为洛谷平台上与递归/记忆化搜索/回溯相关的经典题目,按难度排列。可在 www.luogu.com.cn 搜索题号提交。

1
P1028
数的计算
递推 / 递归入门
入门
2
P1044
栈(Catalan数)
递推 / Catalan
入门
3
P1060
01背包问题
记忆化搜索 / DP
入门
4
P1002
过河卒
递推 / 二维DP
入门
5
P1192
台阶问题
递推 / 斐波那契变体
入门
6
P1255
数楼梯
递推 / 高精度
入门
7
P1048
采药
记忆化搜索 / 01背包
普及-
8
P1057
传球游戏
递推 / 二维状态
普及-
9
P1130
红牌
递推 / 动态规划
普及-
10
P1010
幂次方
递归 / 分治
普及-
11
P1014
Cantor表
递推 / 找规律
普及-
12
P1064
金明的预算方案
记忆化搜索 / 分组背包
普及/提高-
13
P1464
Function
记忆化搜索 / 经典
普及/提高-
14
P1040
加分二叉树
记忆化搜索 / 区间DP
提高+
15
P1049
装箱问题
记忆化搜索 / 背包
普及-
16
P1088
火星人
递归 / 全排列
普及/提高-
17
P1091
合唱队形
递推 / LIS
提高+
18
P1115
最大子段和
递推 / DP入门
普及-
19
P1439
最长公共子序列
递推 / 记忆化
提高+
20
P1706
全排列问题
递归 / 回溯 / DFS
普及-

💡 洛谷刷题建议

① 从入门题开始:先做 P1028、P1192、P1255 打基础

② 重点突破:P1464(Function)是记忆化搜索的经典模板题

③ 进阶挑战:P1040(加分二叉树)和 P1439(LCS)是区间 DP + 记忆化的高频真题

④ 回溯专项:P1706(全排列)和 P1088(火星人)练习递归回溯

每道题建议:先自己想 15 分钟 → 看题解理解思路 → 自己写代码 → AC后看其他解法优化