数据结构-递归算法

一、递归的核心概念

递归(Recursion)是程序设计中的一种重要思想,指的是函数直接或间接调用自身 的编程技巧。其核心逻辑是"大事化小"------将一个复杂的大问题,拆解成与原问题结构相同但规模更小的子问题,直到子问题小到可以直接解决(即递归终止条件),再通过子问题的解反向推导得到原问题的解。

形象地说,递归就像"俄罗斯套娃":每个套娃的结构都相同,打开外层套娃会看到更小的套娃,直到打开最里面的小娃娃(终止条件),整个拆解过程就结束了。

能用递归解决问题要满足三个条件:

  1. 子问题与原问题结构一致:拆解后的子问题必须和原问题的解决逻辑、数据结构完全相同,仅问题规模更小。这样才能保证递归函数可复用自身逻辑处理子问题,形成"大事化小"的拆解链条。

  2. 递归的调用次数有限:每次递归调用时,必须使问题规模向终止条件的方向递减。如果问题规模不缩小甚至扩大,即使存在终止条件,也可能因无法触及临界值而陷入无限递归。

  3. 存在明确的递归终止条件:必须有一个清晰的"出口",当问题规模缩小到某个临界值时,不再调用自身,直接返回确定的结果。若缺少终止条件,会导致函数无限递归,最终引发栈溢出错误。

二、递归的工作原理

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) 开始,逐层分解为子问题,每一层调用都会创建新栈帧,栈的增长方向是从高地址向低地址延伸 (栈顶指针向下移动),具体入栈顺序如下:

  1. 第一步:主函数 main() 调用 factorial(5)

    • main() 函数先拥有一个栈帧(保存 main() 的局部变量 n=5、返回地址(程序入口的下一条指令)等信息)。
    • 当执行 printf 中的 factorial(5) 时,系统在栈顶创建 factorial(5) 的栈帧,然后跳转到 factorial 函数的执行逻辑。
  2. 第二步:factorial(5) 调用 factorial(4)

    • factorial(5) 的栈帧中保存:参数 n=5、返回地址(回到 5 * factorial(4) 的计算逻辑)、函数执行上下文。
    • 由于 n=5 不满足终止条件,执行 return 5 * factorial(4),触发对 factorial(4) 的调用,系统在栈顶(factorial(5) 栈帧下方)创建 factorial(4) 的栈帧。
  3. 第三步:factorial(4) 调用 factorial(3)

    • factorial(4) 栈帧保存:参数 n=4、返回地址(回到 4 * factorial(3) 的计算逻辑)。
    • 不满足终止条件,调用 factorial(3),创建 factorial(3) 栈帧(位于 factorial(4) 栈帧下方)。
  4. 第四步:factorial(3) 调用 factorial(2)

    • 栈帧保存 n=3 和返回地址(回到 3 * factorial(2)),调用 factorial(2),创建对应栈帧。
  5. 第五步:factorial(2) 调用 factorial(1)

    • 栈帧保存 n=2 和返回地址(回到 2 * factorial(1)),调用 factorial(1),创建对应栈帧。
  6. 终止: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())都包含以下关键部分(不同编译器略有差异,但核心一致):

  1. 返回地址(Return Address) :保存当前函数执行完毕后,需要回到的调用函数的指令地址(例如 factorial(1) 的返回地址是 factorial(2)2 * factorial(1) 的计算指令地址)。
  2. 函数参数(Parameters) :保存传入函数的参数值(例如 factorial(5) 的栈帧中保存参数 n=5factorial(1) 的栈帧中保存 n=1)。
  3. 局部变量(Local Variables) :保存函数内部定义的局部变量(本例中 factorial 函数无额外局部变量,main() 函数的栈帧中保存局部变量 n=5)。
  4. 栈基址指针(EBP,栈帧指针):用于固定当前栈帧的起始位置,方便访问栈帧内的参数、局部变量(相当于栈帧的 "锚点")。
  5. 栈顶指针(ESP):指向当前栈顶的位置,随着栈帧的创建和销毁动态移动(创建栈帧时 ESP 减小,释放栈帧时 ESP 增大)。
  6. 临时数据 / 执行上下文:保存函数执行过程中产生的临时计算结果、寄存器状态等。
二.递归返回与栈帧销毁过程(逐层出栈,计算结果)(了解)

递归的返回过程遵循「先进后出」原则,从栈顶的 factorial(1) 开始,逐层销毁栈帧并计算阶乘结果,具体流程如下:

  1. factorial(1) 返回,栈帧销毁

    • factorial(1) 执行 return 1,将返回值 1 存入指定寄存器(如 EAX)。
    • factorial(1) 的栈帧从栈顶弹出(ESP 上移,释放内存),执行流程通过返回地址回到 factorial(2) 的断点处(2 * factorial(1))。
  2. factorial(2) 计算并返回,栈帧销毁

    • factorial(2) 从寄存器中获取 factorial(1) 的返回值 1,执行计算 2 * 1 = 2
    • 执行 return 2,将结果 2 存入寄存器,随后 factorial(2) 栈帧销毁,流程回到 factorial(3) 的断点处(3 * factorial(2))。
  3. factorial(3) 计算并返回,栈帧销毁

    • 获取 factorial(2) 的返回值 2,计算 3 * 2 = 6,返回 6 并销毁栈帧,流程回到 factorial(4)
  4. factorial(4) 计算并返回,栈帧销毁

    • 获取返回值 6,计算 4 * 6 = 24,返回 24 并销毁栈帧,流程回到 factorial(5)
  5. factorial(5) 计算并返回,栈帧销毁

    • 获取返回值 24,计算 5 * 24 = 120,返回 120 并销毁栈帧,流程回到 main() 函数。
  6. main() 函数接收结果并输出

    • main() 从寄存器中获取 factorial(5) 的返回值 120,执行 printf("%d! = %d\n", 5, 120),输出结果 5! = 120
    • main() 函数执行完毕后,其栈帧也被销毁,程序正常退出。

至此,所有递归栈帧均已销毁,栈内存恢复到程序启动前的状态。

总结

  1. 递归调用的核心是调用栈的逐层入栈(创建栈帧)逐层出栈(销毁栈帧),遵循先进后出原则;
  2. 每个递归函数调用都会创建独立栈帧,保存参数、返回地址等关键信息,这是递归能记住 "上一层调用状态" 的原因;
  3. 阶乘递归的栈帧变化:从 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. 问题分析

斐波那契数列定义:第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);
}
相关推荐
醉风塘2 小时前
数据库索引深度解析:从数据结构到最佳实践
数据结构·数据库
wanghowie2 小时前
01.09 Java基础篇|算法与数据结构实战
java·数据结构·算法
Doro再努力2 小时前
【数据结构07】双向链表完结+栈
数据结构·链表
苏宸啊2 小时前
二叉树与堆:高效数据结构解析
数据结构
蒙奇D索大2 小时前
【数据结构】考研408 | 散列查找性能剖析:装填因子、冲突策略与优化全攻略
数据结构·考研·算法·哈希算法·改行学it
zore_c2 小时前
【数据结构】堆——超详解!!!(包含堆的实现)
c语言·开发语言·数据结构·经验分享·笔记·算法·链表
月明长歌3 小时前
【码道初阶】【LeetCode387】如何高效找到字符串中第一个不重复的字符?
java·开发语言·数据结构·算法·leetcode·哈希算法
罗湖老棍子3 小时前
C++ 自定义排序与优先队列运算符重载
数据结构·c++·算法··优先队列·运算符重载
良木生香3 小时前
【数据结构-初阶】二叉树(1)---树的相关概念
c语言·数据结构·算法·蓝桥杯