第9讲:函数递归——用“套娃”思维解决复杂问题
🔁 第9讲:函数递归——用“套娃”思维解决复杂问题 🧩
适合刚掌握函数基础的你,学会递归,解锁编程新维度!
📚 目录
- 什么是递归?📦
- 递归的两大限制条件 🔒
- 递归实战案例 💡
- 3.1 求 n 的阶乘
- 3.2 顺序打印整数每一位
- 递归 vs 迭代:效率之争 ⚔️
- 斐波那契数列的“陷阱” ⚠️
- 递归的适用场景 🎯
- 拓展挑战:经典递归问题 🚀
什么是递归?📦——函数调用自身的“套娃”艺术
“递归”不是魔法,而是一种将大问题拆解为小问题的思维方式。
🧠 核心定义
递归 = 函数自己调用自己
#include <stdio.h>
int main() {printf("hehe\n");main(); // ❌ 错误示范:无限递归return 0;
}
📌 这段代码会无限打印 hehe
,最终导致 栈溢出(Stack Overflow)!
🔁 递归的哲学:大事化小
递归的精髓在于:
把一个复杂问题,转化为一个与原问题相似但规模更小的子问题来解决。
- “递”:层层推进,拆解问题(递推)
- “归”:子问题解决后,逐层返回结果(回归)
🎯 就像剥洋葱,一层一层剥到核心,再一层一层合回去。
递归的两大限制条件 🔒——防止“无限套娃”
写递归函数,必须满足两个铁律,否则就是“死递归”!
条件 | 说明 | 示例(阶乘) |
---|---|---|
✅ 1. 存在限制条件 | 当满足某个条件时,递归必须停止 | n == 0 时返回 1 |
✅ 2. 趋近于限制条件 | 每次递归调用,都要更接近终止条件 | Fact(n-1) → 规模减小 |
📌 没有终止条件 = 无限递归 → 栈溢出崩溃!
递归实战案例 💡
案例1:求 n 的阶乘 🎯
🧮 数学定义
n! = n × (n-1) × (n-2) × ... × 1
0! = 1 (特殊规定)
🔁 递归关系
Fact(n) = n × Fact(n-1)
✅ 代码实现
int Fact(int n) {if (n == 0) { // ✅ 终止条件return 1;} else {return n * Fact(n-1); // ✅ 规模减小}
}int main() {int n;scanf("%d", &n);printf("%d! = %d\n", n, Fact(n));return 0;
}
🖼️ 递归过程图解(以 Fact(5)
为例)
Fact(5)
├── 5 * Fact(4)├── 4 * Fact(3)├── 3 * Fact(2)├── 2 * Fact(1)├── 1 * Fact(0) → 1← 1← 2← 6
← 24
← 120 ✅
案例2:顺序打印整数每一位 🔢
🎯 题目
输入 1234
,输出 1 2 3 4
🧠 思路分析
直接取最高位很难?那就先打印前面的位,再打印最后一位!
Print(1234)
├── Print(123) → 先打印前三位
└── printf("4") → 再打印最后一位
✅ 代码实现
void Print(int n) {if (n > 9) { // ✅ 终止条件:不是一位数Print(n / 10); // ✅ 规模减小:去掉最后一位}printf("%d ", n % 10); // 打印最后一位(递归返回时执行)
}int main() {int m;scanf("%d", &m);Print(m);return 0;
}
🖼️ 执行流程(Print(1234)
)
Print(1234)
├── Print(123)├── Print(12)├── Print(1) → n<=9,不递归← printf("1 ")← printf("2 ")
← printf("3 ")
← printf("4 ")
输出:1 2 3 4 ✅
📌 关键点:printf
在递归调用之后,所以是“归”的过程中打印,自然就是从左到右。
递归 vs 迭代:效率之争 ⚔️
特性 | 递归(Recursion) | 迭代(Iteration) |
---|---|---|
代码风格 | 简洁,接近数学定义 | 稍长,逻辑清晰 |
空间效率 | 低(每次调用占用栈帧) | 高(通常 O(1) 空间) |
时间效率 | 可能低(重复计算) | 通常更高 |
适用场景 | 结构天然递归(树、图) | 简单循环任务 |
🔄 阶乘的迭代实现(更高效)
int Fact(int n) {int ret = 1;for (int i = 1; i <= n; i++) {ret *= i;}return ret;
}
✅ 优点:
-
无栈溢出风险
-
时间复杂度 O(n),空间复杂度 O(1)
🔥 递归的性能代价:栈帧开销与栈溢出风险 ⚠️
Fact函数虽然能正确计算结果,但其递归调用过程伴随着显著的运行时开销。
🧱 栈帧(Stack Frame)机制
在C语言中,每次函数调用都会在内存的栈区(stack)申请一块空间,称为函数栈帧或运行时堆栈。
- 作用:保存该次调用的局部变量、参数、返回地址等信息。
- 生命周期:函数不返回,栈帧就一直占用内存。
📦 递归的栈空间消耗
当递归发生时:
- 每一次递归调用都会创建独立的栈帧。
- 这些栈帧会层层叠加,直到递归终止。
- 然后才从最内层开始,逐层释放栈帧(“回归”过程)。
栈顶 ┌────────────┐│ Fact(0) │ ← 返回,释放├────────────┤│ Fact(1) │ ← 返回,释放├────────────┤│ Fact(2) │ ← 返回,释放├────────────┤│ ... │├────────────┤│ Fact(n-1) │ ← 等待 Fact(n-2) 返回├────────────┤│ Fact(n) │ ← 最外层调用 栈底 └────────────┘
🚨 栈溢出(Stack Overflow)风险
- 如果递归层次过深,会创建大量栈帧。
- 栈区空间有限(通常几MB),当栈帧总大小超过栈空间时,程序就会崩溃,报错
Stack overflow
。 - 因此,递归虽然简洁,但不适合深度过大的问题。
📌 结论:递归的简洁性是以时间和空间开销为代价的,需谨慎使用。
斐波那契数列的“陷阱” ⚠️——递归的反面教材
🧮 斐波那契定义
F(1) = 1
F(2) = 1
F(n) = F(n-1) + F(n-2) (n > 2)
❌ 低效的递归实现
int Fib(int n) {if (n <= 2) return 1;return Fib(n-1) + Fib(n-2);
}
📉 问题:指数级重复计算!
以 Fib(5)
为例:
Fib(5)/ \Fib(4) Fib(3)/ \ / \
Fib(3) Fib(2) Fib(2) Fib(1)
/ \
Fib(2) Fib(1)
📌 Fib(3)
被计算了 2次,Fib(2)
被计算了 3次!
当 n=50
时,计算时间长得无法接受!
✅ 高效的迭代实现
int Fib(int n) {if (n <= 2) return 1;int a = 1, b = 1, c;for (int i = 3; i <= n; i++) {c = a + b;a = b;b = c;}return c;
}
✅ 优点:
-
时间复杂度:O(n)
-
空间复杂度:O(1)
-
无重复计算
🔥 递归的性能代价:栈帧开销与栈溢出风险 ⚠️
Fact函数虽然能正确计算结果,但其递归调用过程伴随着显著的运行时开销。
🧱 栈帧(Stack Frame)机制
在C语言中,每次函数调用都会在内存的栈区(stack)申请一块空间,称为函数栈帧或运行时堆栈。
- 作用:保存该次调用的局部变量、参数、返回地址等信息。
- 生命周期:函数不返回,栈帧就一直占用内存。
📦 递归的栈空间消耗
当递归发生时:
- 每一次递归调用都会创建独立的栈帧。
- 这些栈帧会层层叠加,直到递归终止。
- 然后才从最内层开始,逐层释放栈帧(“回归”过程)。
栈顶 ┌────────────┐│ Fact(0) │ ← 返回,释放├────────────┤│ Fact(1) │ ← 返回,释放├────────────┤│ Fact(2) │ ← 返回,释放├────────────┤│ ... │├────────────┤│ Fact(n-1) │ ← 等待 Fact(n-2) 返回├────────────┤│ Fact(n) │ ← 最外层调用 栈底 └────────────┘
🚨 栈溢出(Stack Overflow)风险
- 如果递归层次过深,会创建大量栈帧。
- 栈区空间有限(通常几MB),当栈帧总大小超过栈空间时,程序就会崩溃,报错
Stack overflow
。 - 因此,递归虽然简洁,但不适合深度过大的问题。
📌 结论:递归的简洁性是以时间和空间开销为代价的,需谨慎使用。
递归的适用场景 🎯——何时使用递归?
✅ 推荐使用递归的场景:
问题类型 | 说明 |
---|---|
🌲 树形结构遍历 | 如文件夹遍历、二叉树遍历 |
🧩 分治算法 | 快速排序、归并排序 |
🧱 天然递归结构 | 汉诺塔、斐波那契(教学) |
🔍 回溯算法 | 八皇后、迷宫求解 |
❌ 避免使用递归的场景:
- 简单循环可解决的问题(如阶乘、斐波那契)
- 深度过大的递归(栈溢出风险)
拓展挑战:经典递归问题 🚀
🐸 问题1:青蛙跳台阶
一只青蛙一次可以跳1级或2级台阶,问跳上n级台阶有多少种跳法?
📌 递归关系:f(n) = f(n-1) + f(n-2)
(斐波那契!)
🏰 问题2:汉诺塔(Hanoi Tower)
三根柱子,A柱上有n个盘子(上小下大),要求借助B柱,将所有盘子移动到C柱,且大盘不能压小盘。
📌 递归思路:
- 把上面
n-1
个盘子从 A → B(借助 C) - 把第
n
个盘子从 A → C - 把
n-1
个盘子从 B → C(借助 A)
void hanoi(int n, char A, char B, char C) {if (n == 1) {printf("%c -> %c\n", A, C);} else {hanoi(n-1, A, C, B); // A→B 借助 Cprintf("%c -> %c\n", A, C);hanoi(n-1, B, A, C); // B→C 借助 A}
}
✅ 学习收获总结
技能 | 掌握情况 |
---|---|
✅ 理解递归本质:函数自调用 | ✔️ |
✅ 掌握两大限制条件 | ✔️ |
✅ 实现阶乘、打印数字等递归 | ✔️ |
✅ 理解递归 vs 迭代的优劣 | ✔️ |
✅ 识别递归的适用场景 | ✔️ |
✅ 避免低效递归(如Fib) | ✔️ |
🎯 递归不是万能钥匙,但它是打开复杂世界的一扇门
你已经掌握了“大事化小”的编程思维,继续加油,下一个难题等你来攻克!💪🔥
💬 需要本讲的 递归动画演示 / 汉诺塔可视化 / 源码工程?欢迎继续提问,我可以一键打包给你!