数据结构-递归算法

一、递归的核心概念

递归(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. 代码实现

#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.代码

#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); }

Read more

算法笔记:洛谷 P1108 低价购买

洛谷:低价购买 1. 题目核心与难点 * 目标: 1 . 求最长下降子序列(LIS)的长度。 2 . 求达到该长度的不重复方案总数。 * 难点:方案去重。若两条路径数值序列完全一致(例如 10 8 6 和另外一组 10 8 6),即便位置不同,也只能算一种方案。 2.状态转移的定义 f [ i ] f[i] f[i]:以第 i i i 天价格结尾的最长下降子序列长度。 t [ i ] t[i] t[i]:以第 i i i 天价格结尾且长度为 f [ i

By Ne0inhk

轨迹数据压缩的Douglas-Peucker算法(附代码及原始数据)

机场出租车调度问题:数学建模实战解析 大家好!今天咱们来聊聊一个特别接地气的数学建模题目——机场的出租车调度问题。这是2019年全国大学生数学建模竞赛的C题,题目看着简单,实际上藏着不少玄机。咱们一起拆解这个题目,看看怎么用数学模型来解决现实生活中的难题。 问题背景:机场出租车的那些事儿 想象一下你刚从飞机下来,拖着行李箱走到出租车候客区,发现有两条队:一条是"短途专用通道",另一条是普通队。为什么会有这样的设计?背后其实是一套复杂的调度系统在运作。 题目给我们几个核心信息点: 1.大多数机场出租车司机会在"蓄车池"排队等待 2.机场管理人员会采集乘客目的地信息 3.对于短途乘客(比如目的地小于某个阈值d),会给司机"补偿"或安排他们优先接客 4.司机可以自主选择是否去"短途专用通道"排队 核心问题就是要我们设计一套合理的调度方案,在乘客等候时间、司机收益和机场管理效率之间找到平衡。 技术原理:排队论与博弈论的双剑合璧

By Ne0inhk
Flutter 三方库 diff_match_patch 鸿蒙文本比对拼接算法双向核心适配研判:毫秒解构海量字符差异区块建立丝滑无感知的协同编辑冲突强容错合并-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 diff_match_patch 鸿蒙文本比对拼接算法双向核心适配研判:毫秒解构海量字符差异区块建立丝滑无感知的协同编辑冲突强容错合并-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 diff_match_patch 鸿蒙文本比对拼接算法双向核心适配研判:毫秒解构海量字符差异区块建立丝滑无感知的协同编辑冲突强容错合并机制 在文本编辑器、版本控制系统或协同办公应用中,快速、精准地找出两段文字之间的差异并生成补丁(Patch)是核心能力。diff_match_patch 库基于 Google 开发的高效算法,提供了业界领先的文本处理解决方案。本文将详解该库在 OpenHarmony 环境下的适配与实战。 前言 随着鸿蒙分布式能力的不断增强,多终端设备(手机、平板、电脑)之间的文档同步与协作编辑变得愈发频繁。直接传输整段文本不仅浪费带宽,且难以处理冲突。diff_match_patch 通过计算文本的最小增量,能够大幅提升鸿蒙分布式数据通信的效率。 一、原理解析 1.1 基础概念 diff_match_patch

By Ne0inhk
Leetcode 129 移除元素 | 轮转数组

Leetcode 129 移除元素 | 轮转数组

1 题目 27. 移除元素 提示 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。 假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作: * 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。 * 返回 k。 用户评测: 评测机将使用以下代码测试您的解决方案: int[] nums = [...]; // 输入数组 int

By Ne0inhk