数据结构:递归(Recursion)

article/2025/7/28 11:00:04

目录

示例1:先打印,再递归

 示例2:先递归,再打印

递归的两个阶段 

 递归是如何使用栈内存

复杂度分析

递归中的静态变量 

内存结构图解


递归:函数调用自己 + 必须有判断条件来使递归继续或停止

我们现在通过这两个示例代码,用“递归调用树”的方式,一步步直观分析,并进行对比。

示例1:先打印,再递归

void fun(int x)
{if(x > 0){printf("%d", x);fun(x - 1);}
}int main()
{int x = 3;fun(x);
}

执行流程(调用栈顺序) 

main -> fun(3)|--> print 3--> fun(2)|--> print 2--> fun(1)|--> print 1--> fun(0) -> 终止

调用树结构

fun(3)└── print 3└── fun(2)└── print 2└── fun(1)└── print 1└── fun(0) [结束]

输出结果

321

 

 示例2:先递归,再打印

void fun(int x)
{if(x > 0){fun(x - 1);printf("%d", x);}
}int main()
{int x = 3;fun(x);
}

执行流程(调用栈顺序)

main -> fun(3)|--> fun(2)|--> fun(1)|--> fun(0) [终止]<-- print 1<-- print 2<-- print 3

调用树结构

fun(3)└── fun(2)└── fun(1)└── fun(0) [结束]└── print 1└── print 2└── print 3

输出结果

123

 


 

递归的两个阶段 

递归包含两个阶段:calling(上升阶段 / Ascending)和returning(下降阶段 / Descending)

void fun(int x)
{if(x > 0){// calling,又叫 Ascendingfun(x - 1);  // 如果这里有其他操作(例如 fun(x-1)*2),则属于 returning// returning,又叫 Descending}
}

我们假设执行 fun(3)

🌿 调用过程(Calling / Ascending):

fun(3)
└── fun(2)└── fun(1)└── fun(0)  // 到这里终止(因为 x > 0 不成立)

这部分就是不断往下递归调用自己的过程,称为“上升阶段”,因为递归在“深入栈底”,一层一层地压入调用栈中。

在这个阶段,“程序在挖坑”,但并没有开始“填坑”。

🌀 返回过程(Returning / Descending)

fun(0) 终止后,每一层函数从栈中返回: 

fun(0) 返回到 fun(1)
→ fun(1) 返回到 fun(2)
→ fun(2) 返回到 fun(3)
→ fun(3) 返回到 main

这个阶段称为“下降阶段”,也可以叫returning,因为每一次递归调用返回时都会执行“fun(x - 1)”后面的语句(如果有的话)。 

 


 递归是如何使用栈内存

在 C 语言中,每一次函数调用都会在内存中开辟一个栈帧(Stack Frame),用来保存该函数的局部变量、参数、返回地址等。

当你调用递归函数 fun(x) 时,每次调用都会压入(push)一个新的栈帧,等函数返回后再弹出(pop)这个栈帧。

我们以下面的代码为例:

void fun(int x)
{if(x > 0){printf("%d", x);fun(x - 1);}
}

✅ 第一步:main() 调用 fun(3)

+------------------------+ ← 栈顶(最新压栈)
| fun(x=3) 的栈帧         | ← 保存参数 x=3,返回地址
+------------------------+
| main() 的栈帧           |
+------------------------+

✅ 第二步:进入 fun(2)(x=2)

+------------------------+
| fun(x=2) 的栈帧         |
+------------------------+
| fun(x=3) 的栈帧         |
+------------------------+
| main() 的栈帧           |
+------------------------+

✅ 第三步:进入 fun(1)(x=1)

+------------------------+
| fun(x=1) 的栈帧         |
+------------------------+
| fun(x=2) 的栈帧         |
+------------------------+
| fun(x=3) 的栈帧         |
+------------------------+
| main() 的栈帧           |
+------------------------+

✅ 第四步:进入 fun(0)(不执行递归)

+------------------------+
| fun(x=0) 的栈帧         | ← 条件不成立,直接返回
+------------------------+
| fun(x=1) 的栈帧         |
| fun(x=2) 的栈帧         |
| fun(x=3) 的栈帧         |
| main() 的栈帧           |
+------------------------+

⬅️ 之后依次弹栈回到 main(),释放这些栈帧。


 

复杂度分析

✅ 时间复杂度:O(n)

我们要分析的是:fun(n) 一共要花费多少单位时间?

  • 执行一次 if (x > 0) :1 单位时间

  • 执行一次 printf("%d", x) :1 单位时间

  • 执行一次 fun(x - 1) :记作 T(n-1) 单位时间(递归调用自身的耗时)

所以,每次调用 fun(x) 的总时间 T(n) 是:

T(n) = 1(if 判断)+ 1(打印)+ T(n - 1)
T(n) = 1 + 1 + T(n-1)= 2 + T(n-1)T(n-1) = 2 + T(n-2)
T(n-2) = 2 + T(n-3)
...
T(1) = 2 + T(0)
T(0) = 1(只执行一次 if,条件不成立直接返回)

 所以:

T(n) = 2n + 1
名称表达式说明
总时间函数T(n) = 2n + 1包括每次调用的 if 和 printf,共 2n 次操作 + 1 次 base case
时间复杂度O(n)取最高阶忽略常数,线性复杂度

✅ 空间复杂度(栈空间):O(n)

  • 每次递归调用会创建一个新的栈帧。

  • 所以最多有 n+1 层递归(从 x = n 到 x = 0)

  • 所以空间复杂度也是:O(n)


 

递归中的静态变量 

int fun(int n)
{static int x = 0;  // 静态变量,只初始化一次,保存在**静态区(数据段)**if(n > 0){x++;return fun(n - 1) + x;}return 0;
}

普通局部变量:

  • 每次函数调用都会新建一个副本,存在栈区

  • 调用结束后销毁,不会“记住”上一次的值

 静态局部变量 static

  • 生命周期为整个程序运行期间

  • 只初始化一次

  • 存储在静态存储区(Data Segment),不在栈区

  • 所以每次递归调用都共享同一个 x

递归过程树 

                      fun(5)|vfun(4) + x=1 → return ?|vfun(3) + x=2 → return ?|vfun(2) + x=3 → return ?|vfun(1) + x=4 → return ?|vfun(0)       → return 0开始回溯:
fun(1): return 0 + x=5 = 5
fun(2): return 5 + x=5 = 10
fun(3): return 10 + x=5 = 15
fun(4): return 15 + x=5 = 20
fun(5): return 20 + x=5 = 25

 

 

❗ 注意:x 在每次递归调用时都自增,最终值是 x=5

而因为每次回溯时都加的是 当前 x=5,所以:

最终返回值 = 5 + 5 + 5 + 5 + 5 = 25

内存结构图解

+---------------------------+ ← 低地址
| 代码区(Text)             | ← 编译后的 fun() / main() 指令
+---------------------------+
| 数据段(Static / Global)  |
| static int x = 0;         | ← 所有递归共享这一个变量
+---------------------------+
| 栈区(Stack)              |
| fun(n=1) 的栈帧            |
| fun(n=2) 的栈帧            |
| fun(n=3) 的栈帧            |
| fun(n=4) 的栈帧            |
| fun(n=5) 的栈帧            |
| main() 的栈帧              |
+---------------------------+
|                           |
| 堆区(Heap)               | ← malloc / new 用
+---------------------------+ ← 高地址

http://www.hkcw.cn/article/yxrUXwebjg.shtml

相关文章

Python入门手册:类和对象

在Python中&#xff0c;面向对象编程&#xff08;OOP&#xff09;是一种核心的编程范式。通过类和对象&#xff0c;我们可以模拟现实世界中的事物和行为&#xff0c;使代码更加模块化、可复用和易于维护。今天&#xff0c;就让我们深入探讨Python中的类和对象&#xff0c;包括它…

从冷上电到main()函数,Bootloader都做了什么?

目录 1、硬件初始化 2、引导模式与应用模式的抉择 3、启动代码 在嵌入式系统中&#xff0c;从设备上电到执行应用程序的main()函数&#xff0c;Bootloader扮演着至关重要的角色。作为系统启动的首个程序&#xff0c;Bootloader负责初始化硬件、设置运行环境&#xff0c;并最…

电路图识图基础知识-保护环节、自锁环节及互锁环节(十)

1 电路中的自锁环节 自锁环节是指继电器得电后能通过自身的常开触点闭合&#xff0c;给其线圈供电的环节。如图所示的电路图中&#xff0c;辅助电路中并联于启动按钮开关SB2 旁边的KM 常开触点就是自锁环节(此触 电称为自锁触电)。 图中所示的自锁过程是&#xff1a;当QK 闭合后…

Linux Windows之wsl安装使用简介

参考资料 如何使用 WSL 在 Windows 上安装 Linuxwindows11 安装WSL2全流程旧版 WSL 的手动安装步骤 目录 一. 前期准备1.1 确认windows的版本1.2 开启Linux子系统的支持1.2.1 图形化方式1.2.2 命令行方式 1.3 安装wsl软件1.4 安装Linux分发版 二. 基本配置2.1 Windows Termina…

网红家装企业上海总部人去楼空 欠款风波引关注

端午节放假前,每天有上百人来找住范儿,因为公司欠了不少钱。6月1日下午,记者来到住范儿上海公司所在地,发现公司大门被木板封得严严实实。守在门口的保安指着木板上的通知对记者说:“也省得你报警了,直接打派出所电话吧。”据官网介绍,住范儿是家居建材新零售服务商,成…

正则表达式笔记

正则表达式笔记 前言一、基本字符匹配二、字符类三、量词四、定位符五、贪婪匹配和非贪婪匹配六、旗标七、分组和引用八、前瞻九、后顾 前言 参考GeekHour视频和资料&#xff0c;讲的挺好的&#xff0c;B站有[GeekHour正则表达式] 正则表达式在线工具网站&#xff1a;https://…

齐达内拒利雅得新月一亿欧年薪合同 静候法国国家队帅位

齐达内拒绝了利雅得新月开出的1亿欧元年薪合同。沙特球队利雅得新月正在寻找新主帅,并希望邀请赋闲在家的齐达内。利雅得新月愿意为齐达内支付一亿欧元年薪,签约一年,让他率队参加今夏世俱杯。然而,齐达内已经拒绝了这份高薪邀请。随后,利雅得新月开始联系国米主帅小因扎吉…

【论文解读】DETR | End-to-End Object Detection with Transformers

论文地址&#xff1a;https://arxiv.org/pdf/2005.12872 代码地址&#xff1a;https://github.com/facebookresearch/detr 摘要 本研究提出了一种新的方法&#xff0c;该方法将目标检测视为一个直接的集合预测问题。本研究的方法简化了检测流程&#xff0c;有效地消除了对许多…

(C++)STL:string类(三)非成员重载函数和类型转化函数解析使用

string类&#xff08;三&#xff09; 非成员重载函数relational operaters 关系运算符operatoroperator<< operator>>getline <string>头文件内的函数string转化为数字类型其他数值类型转化为string练习&#xff1a;字符串最后一个单词的长度 非成员重载函数…

[Python] Python运维:系统性能信息模块psutil和系统批量运维管理器paramiko

初次学习&#xff0c;如有错误还请指正 目录 系统性能信息模块psutil 获取系统性能信息 CPU信息 内存信息 磁盘信息 网络信息 其他信息 进程信息 实用的IP地址处理模块IPy IP地址、网段的基本处理 多网络计算方法 系统批量运维管理器paramiko paramiko 的安装 Li…

声光控灯电路Multisim仿真

5V交流源充当声音信号源&#xff0c;可调电阻充当光敏电阻。 白天&#xff0c;不管是否有声音&#xff0c;灯都不会亮。 夜晚&#xff0c;当有声音时&#xff0c;灯亮一段时间&#xff0c;然后熄灭。 仿真时遇到的问题&#xff1a; 问题1、必须按照一定的流程才能正常运行。…

Blueprints - List View Widget

一些学习笔记归档&#xff1b; 需要读取动态数据把多个条目显示在UI上的时候&#xff0c;可能用到List View组件&#xff1b;假如有Widget要使用在List View中&#xff0c;此Widget需要继承相关接口&#xff1a; 这样就能在List View控件中选择已经继承接口的Widget组件了&…

七.MySQL内置函数

1.日期函数 MySQL 日期与时间函数对照表 函数名称描述current_date()当前日期&#xff08;格式&#xff1a;YYYY-MM-DD&#xff09;current_time()当前时间&#xff08;格式&#xff1a;HH:MM:SS&#xff09;current_timestamp()当前日期和时间&#xff08;等同于 now()&#x…

神经网络与Transformer详解

1. 一个模型的典型场景 对用户咨询的法律问题做自动归类: 婚姻纠纷、劳动纠纷、合同纠纷、债权债务、房产纠纷、交通事故、医疗纠纷、版权纠纷 2. 模型就是一个数学公式 我们一般将这样的问题描述为:给定一组输入数据,经过一系列数学公式计算后,输出n个概率,分别代表该…

《Python基础》第2期:环境搭建

在开始编写 Python 代码前&#xff0c;还需要搭建 Python 的开发环境。 电脑是没办法直接读懂 Python 代码的&#xff0c;而是需要一个解释器&#xff0c;实时把代码翻译成字节码&#xff0c;字节码再转换成 0 和 1&#xff0c;电脑就能读懂了。 Python 的运行过程就是翻译一行…

多线程——定时任务ScheduledThreadPoolExecutor用法

创建 同样是用Executors工具类&#xff1a; 创建定时任务线程池 还有一个重载方法 接收一个额外的参数&#xff1a;线程工厂 创建单个定时任务执行器&#xff1a; 重载&#xff1a;也接收一个线程工厂 区别说明 singleThread&#xff1a; 这里可以new, 说明他是一个类 传…

AI 代理框架:使用正确的工具构建更智能的系统

AI 代理框架&#xff1a;使用正确的工具构建更智能的系统 探索 AI 代理框架如何支持从单代理设置到复杂的多代理编排的自主工作流。了解它们有何不同、何时使用它们以及如何开始使用实际工具。 AI 代理框架 从本质上讲&#xff0c;AI 代理是可以感知、计划和行动的程序。它们旨…

计算机网络 TCP篇常见面试题总结

目录 TCP 的三次握手与四次挥手详解 1. 三次握手&#xff08;Three-Way Handshake&#xff09; 2. 四次挥手&#xff08;Four-Way Handshake&#xff09; TCP 为什么可靠&#xff1f; 1. 序列号与确认应答&#xff08;ACK&#xff09; 2. 超时重传&#xff08;Retransmis…

系统架构设计师(一):计算机系统基础知识

系统架构设计师&#xff08;一&#xff09;&#xff1a;计算机系统基础知识 引言计算机系统概述计算机硬件处理器处理器指令集常见处理器 存储器总线总线性能指标总线分类按照总线在计算机中所处的位置划分按照连接方式分类按照功能分类 接口接口分类 计算机软件文件系统文件类…

软件无线电关键技术之正交调制技术

与传统调制方式相比&#xff0c;IQ 调制不直接对相位或频率调制&#xff0c;而是简单的对载波及其正交量进行线性调制&#xff0c;进而达到相位或频率调制的效果。IQ 调制具有较高的数据传输速率&#xff0c;高频带利用率&#xff0c;可有效抑制镜频边带以及易于实现等优势&…