什么是递归?
递归是一种编程技巧,指的是一个函数在其定义中直接或间接地调用自身,就是“函数自己调用自己”。

递归通常用于解决可以被分解成相似子问题的问题,它将一个复杂的大问题,分解成一个或多个与原问题结构相同但规模更小的子问题,直到子问题简单到可以直接解决为止。
递归的两个核心要素
一个有效的递归函数必须包含以下两个关键部分,否则会导致无限递归,最终引发栈溢出错误。
a) 基线条件 / 递归出口
这是递归的“停止条件”,当满足这个条件时,递归调用停止,函数开始一层层地返回结果,不再进行新的递归调用,没有基线条件的递归是死循环。
b) 递归条件
这是函数调用自身的部分,它负责将问题规模缩小,向基线条件逼近,每次递归调用都应处理一个比原问题更小的、同类型的问题。

一个经典的比喻: 想象一下你站在一列镜子前,你会看到无数个反射的影像,这就是递归。
- 递归调用:你看到的每一面镜子里的影像,都是另一个“你”在看镜子。
- 基线条件:如果其中一面镜子是磨砂的,或者你闭上了眼睛,影像就停止了,这就是递归的出口。
一个简单的例子:计算阶乘
阶乘是解释递归最经典的例子,一个非负整数 n 的阶乘(写作 n!)定义为:
n! = n * (n-1) * (n-2) * ... * 10! = 1(这是基线条件)
我们可以把阶乘问题分解:
n! = n * (n-1)!
可以看到,n! 的计算依赖于 (n-1)!,而 (n-1)! 的计算又依赖于 (n-2)!,以此类推,直到 0!,这完美符合递归的思想。

C 语言代码实现
#include <stdio.h>
// 函数声明
long factorial(int n);
int main() {
int num = 5;
printf("%d 的阶乘是: %ld\n", num, factorial(num)); // 输出: 5 的阶乘是: 120
return 0;
}
// 递归函数定义
long factorial(int n) {
// 1. 基线条件:当 n 为 0 或 1 时,停止递归
if (n == 0 || n == 1) {
return 1;
}
// 2. 递归条件:函数调用自身,处理更小规模的问题
else {
return n * factorial(n - 1);
}
}
代码执行过程解析(以 factorial(3) 为例)
调用 factorial(3) 的过程如下:
-
factorial(3)被调用n是 3,不满足基线条件 (n != 0 && n != 1)。- 执行
return 3 * factorial(2);。 - 等待
factorial(2)的结果,当前调用被“挂起”,等待返回值。
-
factorial(2)被调用n是 2,不满足基线条件。- 执行
return 2 * factorial(1);。 - 等待
factorial(1)的结果。factorial(2)调用被挂起。
-
factorial(1)被调用n是 1,满足基线条件!- 执行
return 1;。 - 递归结束,开始返回。
factorial(1)返回值1。
-
返回到
factorial(2)factorial(2)等待到了factorial(1)的返回值1。- 它现在可以计算自己的结果:
2 * 1 = 2。 factorial(2)返回值2。
-
返回到
factorial(3)factorial(3)等待到了factorial(2)的返回值2。- 它现在可以计算自己的结果:
3 * 2 = 6。 factorial(3)返回值6。
-
返回到
main函数main函数得到了最终结果6,并打印出来。
这个调用和返回的过程,就像一个栈,后进先出,每次递归调用都像一个新压入栈的帧,当基线条件满足后,开始从栈顶弹出并计算。
递归的优缺点
优点
- 代码简洁、优雅:对于某些问题(如树的遍历、图的搜索),递归的实现方式比循环更直观,代码量更少,可读性更高。
- 问题分解自然:递归的思想与数学上许多问题的定义(如阶乘、斐波那契数列)和算法思想(如分治法、动态规划)非常吻合。
缺点
- 性能开销:每次函数调用都需要在内存的调用栈上创建一个新的栈帧,用于存储参数、返回地址和局部变量,这个过程有时间和空间开销。
- 栈溢出风险:如果递归的深度过深(计算一个非常大的数的阶乘),调用栈可能会被占满,导致栈溢出错误,程序崩溃,循环则没有这个问题。
- 调试困难:递归的调用链和状态管理比较复杂,当出现问题时,跟踪和调试可能比循环更困难。
另一个经典例子:斐波那契数列
斐波那契数列的定义是:F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2) (当 n > 1 时)。
#include <stdio.h>
// 函数声明
int fibonacci(int n);
int main() {
int num = 10;
printf("斐波那契数列的第 %d 项是: %d\n", num, fibonacci(num)); // 输出: 55
return 0;
}
// 递归函数定义
int fibonacci(int n) {
// 基线条件
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
}
// 递归条件
else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
注意:这个斐波那契的递归实现虽然直观,但效率极低,因为它会重复计算很多值(例如计算
fib(5)时,fib(3)会被计算两次),在实际应用中,通常使用带备忘录的递归或迭代(循环)来优化。
递归 vs. 迭代(循环)
对于同一个问题,通常既可以用递归解决,也可以用循环(迭代)解决。
| 特性 | 递归 | 迭代 (循环) |
|---|---|---|
| 实现方式 | 函数调用自身 | 使用 for, while, do-while 等循环结构 |
| 代码可读性 | 对于特定问题(如树)更清晰、简洁 | 通常更直接,逻辑步骤明确 |
| 性能 | 较低,有函数调用开销 | 较高,开销小 |
| 内存使用 | 较高,使用调用栈 | 较低,只使用少量循环变量 |
| 风险 | 可能导致栈溢出 | 一般不会溢出(除非逻辑错误导致死循环) |
用循环实现阶乘:
#include <stdio.h>
long factorial_iterative(int n) {
long result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
int main() {
printf("5 的阶乘是: %ld\n", factorial_iterative(5)); // 输出: 120
return 0;
}
递归是 C 语言中一个强大而优雅的工具,理解它的核心——基线条件和递归条件——是掌握递归的关键。
- 何时使用递归? 当问题的定义本身就是递归的,或者使用递归能让代码结构更清晰、更易于理解时(例如处理树、图等数据结构)。
- 何时避免递归? 当问题规模可能很大,或者对性能有严格要求时,应优先考虑使用迭代(循环)来实现,或者对递归进行优化(如尾递归优化,但 C 语言标准编译器不一定支持)。
