一、递归的核心概念
递归(Recursion)是程序设计中的一种重要思想,指的是函数直接或间接调用自身 的编程技巧。其核心逻辑是"大事化小"------将一个复杂的大问题,拆解成与原问题结构相同但规模更小的子问题,直到子问题小到可以直接解决(即递归终止条件),再通过子问题的解反向推导得到原问题的解。
形象地说,递归就像"俄罗斯套娃":每个套娃的结构都相同,打开外层套娃会看到更小的套娃,直到打开最里面的小娃娃(终止条件),整个拆解过程就结束了。
能用递归解决问题要满足三个条件:
-
子问题与原问题结构一致:拆解后的子问题必须和原问题的解决逻辑、数据结构完全相同,仅问题规模更小。这样才能保证递归函数可复用自身逻辑处理子问题,形成"大事化小"的拆解链条。
-
递归的调用次数有限:每次递归调用时,必须使问题规模向终止条件的方向递减。如果问题规模不缩小甚至扩大,即使存在终止条件,也可能因无法触及临界值而陷入无限递归。
-
存在明确的递归终止条件:必须有一个清晰的"出口",当问题规模缩小到某个临界值时,不再调用自身,直接返回确定的结果。若缺少终止条件,会导致函数无限递归,最终引发栈溢出错误。
二、递归的工作原理
C语言中,函数调用会在内存的"栈区"开辟一块独立的栈帧,用于存储函数的参数、局部变量等信息。递归调用的本质是多次重复这个"开辟栈帧"的过程,具体分为两个阶段:
1. 递推阶段(拆解问题)
函数每次调用自身时,都会将当前的参数、局部变量等信息压入栈中,然后处理规模更小的子问题。这个过程会一直持续,直到达到递归终止条件。
不熟悉栈的可以看我 数据结构与算法专辑---栈
2. 回溯阶段(合并结果)
当子问题得到解决后,函数会从栈中弹出之前压入的信息,恢复到上一层函数的执行环境,然后结合子问题的解计算当前层的结果。这个过程逐层回溯,最终得到原问题的解。
三、C语言递归示例详解
下面通过4个典型示例,从简单到复杂,帮助大家深入理解递归的使用场景和实现逻辑。
示例1:计算n的阶乘(最基础递归)
1. 问题分析
f(n)=1; n=1
f(n)=n*f(n-1) n>1
第一个式子给出了递归的终止条件(递归出口),第二个式子给出了f(n)与f(n-1)之间的关系(递归体)。
2. 代码实现
cpp
#include <stdio.h>
// 递归计算n的阶乘
int factorial(int n) {
// 递归终止条件:n=0或n=1时,阶乘为1
if (n == 0 || n == 1) {
return 1;
}
// 递推:n! = n * (n-1)!,调用自身处理子问题(n-1)!
return n * factorial(n - 1);
}
int main() {
int n = 5;
printf("%d! = %d\n", n, factorial(n));
return 0;
}
// 输出结果:5! = 120
3. 了解这段阶乘递归代码在栈内存中的实现过程,深入理解递归的实现机制
一、栈内存的核心工作机制
程序运行时的 栈(Call Stack,也叫执行栈)是一块先进后出(LIFO)的内存区域,专门用于管理函数调用过程。当发生函数调用时,系统会在栈顶为被调用函数创建一块独立的内存空间(称为「栈帧 / 活动记录」);当函数执行完毕(返回值或执行结束),对应的栈帧会从栈顶弹出,内存被释放,执行流程回到调用函数的断点处继续执行。
以 n=5 为例,递归调用会从 factorial(5) 开始,逐层分解为子问题,每一层调用都会创建新栈帧,栈的增长方向是从高地址向低地址延伸 (栈顶指针向下移动),具体入栈顺序如下:
-
第一步:主函数
main()调用factorial(5)main()函数先拥有一个栈帧(保存main()的局部变量n=5、返回地址(程序入口的下一条指令)等信息)。- 当执行
printf中的factorial(5)时,系统在栈顶创建factorial(5)的栈帧,然后跳转到factorial函数的执行逻辑。
-
第二步:
factorial(5)调用factorial(4)factorial(5)的栈帧中保存:参数n=5、返回地址(回到5 * factorial(4)的计算逻辑)、函数执行上下文。- 由于
n=5不满足终止条件,执行return 5 * factorial(4),触发对factorial(4)的调用,系统在栈顶(factorial(5)栈帧下方)创建factorial(4)的栈帧。
-
第三步:
factorial(4)调用factorial(3)factorial(4)栈帧保存:参数n=4、返回地址(回到4 * factorial(3)的计算逻辑)。- 不满足终止条件,调用
factorial(3),创建factorial(3)栈帧(位于factorial(4)栈帧下方)。
-
第四步:
factorial(3)调用factorial(2)- 栈帧保存
n=3和返回地址(回到3 * factorial(2)),调用factorial(2),创建对应栈帧。
- 栈帧保存
-
第五步:
factorial(2)调用factorial(1)- 栈帧保存
n=2和返回地址(回到2 * factorial(1)),调用factorial(1),创建对应栈帧。
- 栈帧保存
-
终止:
factorial(1)满足递归终止条件factorial(1)栈帧保存n=1,此时触发if (n==0 || n==1),直接返回 1,无需继续调用子函数。
此时,栈中从栈底到栈顶的栈帧顺序为:main() → factorial(5) → factorial(4) → factorial(3) → factorial(2) → factorial(1)(栈顶为 factorial(1))。
单个栈帧的核心组成结构(了解)
每个 factorial(n) 函数的栈帧(包括 main())都包含以下关键部分(不同编译器略有差异,但核心一致):
- 返回地址(Return Address) :保存当前函数执行完毕后,需要回到的调用函数的指令地址(例如
factorial(1)的返回地址是factorial(2)中2 * factorial(1)的计算指令地址)。 - 函数参数(Parameters) :保存传入函数的参数值(例如
factorial(5)的栈帧中保存参数n=5,factorial(1)的栈帧中保存n=1)。 - 局部变量(Local Variables) :保存函数内部定义的局部变量(本例中
factorial函数无额外局部变量,main()函数的栈帧中保存局部变量n=5)。 - 栈基址指针(EBP,栈帧指针):用于固定当前栈帧的起始位置,方便访问栈帧内的参数、局部变量(相当于栈帧的 "锚点")。
- 栈顶指针(ESP):指向当前栈顶的位置,随着栈帧的创建和销毁动态移动(创建栈帧时 ESP 减小,释放栈帧时 ESP 增大)。
- 临时数据 / 执行上下文:保存函数执行过程中产生的临时计算结果、寄存器状态等。
二.递归返回与栈帧销毁过程(逐层出栈,计算结果)(了解)
递归的返回过程遵循「先进后出」原则,从栈顶的 factorial(1) 开始,逐层销毁栈帧并计算阶乘结果,具体流程如下:
-
factorial(1)返回,栈帧销毁factorial(1)执行return 1,将返回值 1 存入指定寄存器(如 EAX)。factorial(1)的栈帧从栈顶弹出(ESP 上移,释放内存),执行流程通过返回地址回到factorial(2)的断点处(2 * factorial(1))。
-
factorial(2)计算并返回,栈帧销毁factorial(2)从寄存器中获取factorial(1)的返回值 1,执行计算2 * 1 = 2。- 执行
return 2,将结果 2 存入寄存器,随后factorial(2)栈帧销毁,流程回到factorial(3)的断点处(3 * factorial(2))。
-
factorial(3)计算并返回,栈帧销毁- 获取
factorial(2)的返回值 2,计算3 * 2 = 6,返回 6 并销毁栈帧,流程回到factorial(4)。
- 获取
-
factorial(4)计算并返回,栈帧销毁- 获取返回值 6,计算
4 * 6 = 24,返回 24 并销毁栈帧,流程回到factorial(5)。
- 获取返回值 6,计算
-
factorial(5)计算并返回,栈帧销毁- 获取返回值 24,计算
5 * 24 = 120,返回 120 并销毁栈帧,流程回到main()函数。
- 获取返回值 24,计算
-
main()函数接收结果并输出main()从寄存器中获取factorial(5)的返回值 120,执行printf("%d! = %d\n", 5, 120),输出结果5! = 120。main()函数执行完毕后,其栈帧也被销毁,程序正常退出。
至此,所有递归栈帧均已销毁,栈内存恢复到程序启动前的状态。
总结
- 递归调用的核心是调用栈的逐层入栈(创建栈帧)和逐层出栈(销毁栈帧),遵循先进后出原则;
- 每个递归函数调用都会创建独立栈帧,保存参数、返回地址等关键信息,这是递归能记住 "上一层调用状态" 的原因;
- 阶乘递归的栈帧变化:从
factorial(5)到factorial(1)入栈,再从factorial(1)到factorial(5)出栈并计算结果,最终得到 120。
四.递归算法的设计步骤
1.明确递归的结束条件和递归终止时的处理方法。
2.确定求解问题的递归模型。技巧:你在设计递归算法时切勿层层展开子问题使得问题复杂化,
不如只考虑递归中第一层于第二层之间的关系(不确定加上第三层与第二层的关系是否与之一样)加上 1.即可求出递归模型。如知道 5!=5*4! 4!=4*3!加上1!=1 求出
f(n)=1; n=1
f(n)=n*f(n-1) n>1
的递归模型求解阶乘就能解决问题了。
五.示例2:计算斐波那契数列(经典递归问题)
- 问题分析
斐波那契数列定义:第1项和第2项均为1,从第3项开始,每一项等于前两项之和,即F(n) = F(n-1) + F(n-2)(n≥3)。 终止条件:F(1)=1,F(2)=1。
F(1)=1 n=1
F(2)=1 n=1
F(n) = F(n-1) + F(n-2) n>=3
2.代码
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int fib(int n)
{
if (n == 1 || n == 2)
{
return 1;
}
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int a = 0;
scanf("%d", &a);
int b = fib(a);
printf("%d", b);
}