当前位置: 首页 > news >正文

10_函数递归_从阶乘到递归调用栈

函数递归:从阶乘到递归调用栈

一、本篇文章要解决什么问题

上一篇学了函数——函数可以调用别的函数。那函数能不能调用自己?能,这就是递归。

递归是 C 语言中非常有特色的一种编程技巧,很多数据结构(树、图)和算法(分治、回溯)都依赖递归。但对初学者来说,递归最大的困难是"想不清楚执行过程"。

这篇文章帮你搞定三件事:

  1. 递归到底是什么——为什么函数可以调用自己而不乱套
  2. 递归的两个核心条件:递归出口(什么时候停)+ 递归关系(怎么把大问题变小)
  3. 递归调用栈是怎么回事——帮你从底层理解"递"和"归"的过程

二、先用一个简单例子理解

2.1 套娃的故事

俄罗斯套娃:打开一个娃娃,里面还有一个娃娃,再打开还有一个……直到最小的那个打不开了。

:一层一层打开——把大问题拆成小问题

:一层一层合上——小问题解决了,大问题自然有答案

递归就是"自己调用自己",但每次调用时参数在变小(或向终止条件靠近),最终到达一个"最小问题"可以直接解决,不需要再调用自己。

2.2 递归和循环的关系

递归能做的事,循环也能做(反之亦然)。区别在于表达方式:

  • 循环:告诉计算机"怎么做"(步骤)
  • 递归:告诉计算机"问题是什么"(定义)

有些场景用递归表达更自然(如树形结构遍历),有些场景用循环更高效。初学者先要理解递归的思想,再根据场景选择。


三、核心知识点讲解

3.1 递归的两个必要元素

任何一个递归函数必须包含两部分:

  1. 递归出口(终止条件):什么情况下不继续递归,直接返回答案
  2. 递归关系:怎么把规模为 n 的问题转化为规模为 n-1(或更小)的问题

没有出口 = 无限递归 = 栈溢出(程序崩溃)。

3.2 阶乘——最经典的递归入门

阶乘定义:n! = 1 × 2 × 3 × ... × n。但用递归思想看:

n! = n × (n-1)! 0! = 1 ← 递归出口(0 的阶乘定义为 1)
#include<stdio.h>intfactorial(intn){if(n<0)// 非法输入:负数没有阶乘{return-1;// 返回 -1 表示错误(调用前应先检查 n)}if(n==0||n==1)// 递归出口:0! = 1, 1! = 1{return1;}returnn*factorial(n-1);// 递归关系}intmain(void){printf("5! = %d\n",factorial(5));printf("0! = %d\n",factorial(0));// printf("-3! = %d\n", factorial(-3)); // 建议在调用前检查,不传入负数return0;}

运行结果:

5! = 120 0! = 1

图10-1 阶乘递归展开图:图解 factorial(5) 的完整递进和回溯过程。

图10-2 递归调用栈示意图:从内存角度解释递归的"栈"本质和栈溢出的风险。

3.3 递归调用栈——"递"和"归"的详细过程

factorial(5)为例:

递(一层层进入): factorial(5) → 5 * factorial(4) → 4 * factorial(3) → 3 * factorial(2) → 2 * factorial(1) → return 1 ← 到达出口 归(一层层返回): ← return 1 ← return 2 * 1 = 2 ← return 3 * 2 = 6 ← return 4 * 6 = 24 ← return 5 * 24 = 120

每次调用factorial都会在调用栈上创建一个新的栈帧,保存该层调用的局部变量(这里是n)和返回位置。当到达出口后,栈帧逐个销毁,返回值逐层传回。如果递归太深(比如factorial(100000)),栈空间不够用,就发生栈溢出(Stack Overflow)

3.4 斐波那契数列——递归的双分支

斐波那契:F(1) = 1, F(2) = 1, F(n) = F(n-1) + F(n-2)

#include<stdio.h>intfibonacci(intn){if(n==1||n==2)// 递归出口:两个终止条件{return1;}returnfibonacci(n-1)+fibonacci(n-2);// 两个递归调用}intmain(void){for(inti=1;i<=10;i++){printf("%d ",fibonacci(i));}printf("\n");return0;}

运行结果:

1 1 2 3 5 8 13 21 34 55

效率问题fibonacci(5)会调用fibonacci(4)fibonacci(3),而fibonacci(4)又会调用fibonacci(3)——重复计算了大量子问题。这是斐波那契递归实现的最大缺点,也是为什么面试里常被问到优化方案。


图10-3 斐波那契递归重复计算图:直观展示为什么斐波那契递归效率低。

四、完整代码示例

一个递归思想的实用案例——递归打印一个整数按位拆分的结果:

#include<stdio.h>// 递归打印整数的每一位voidprintDigits(intn){if(n<10)// 递归出口:只剩一位{printf("%d ",n);return;}printDigits(n/10);// 先递归打印高位printf("%d ",n%10);// 再打印当前最低位}// 循环版本(用于对比)voidprintDigitsLoop(intn){// 先处理逆序问题:把每一位存到数组再倒序输出intdigits[10];intcount=0;while(n>0){digits[count++]=n%10;n/=10;}for(inti=count-1;i>=0;i--){printf("%d ",digits[i]);}}intmain(void){intnum=12345;printf("数字:%d\n",num);printf("递归打印:");printDigits(num);printf("\n");printf("循环打印:");printDigitsLoop(num);printf("\n");// 演示 10 以内数字(直接触发出口)printf("\n数字 7 递归打印:");printDigits(7);printf("\n");// 注意:当前 printDigits 主要面向正整数。输入 0 会输出 "0"(因 n<10 成立),// 输入负数则 n%10 的结果与具体实现有关,建议调用前检查参数。return0;}

五、运行结果

数字:12345 递归打印:1 2 3 4 5 循环打印:1 2 3 4 5 数字 7 递归打印:7

六、代码逐行解析

递归打印的核心逻辑:

voidprintDigits(intn){if(n<10)// 递归出口{printf("%d ",n);return;}printDigits(n/10);// 先递归处理高位printf("%d ",n%10);// 再打印当前最低位}

执行过程(以 n=123 为例):

printDigits(123) → if (123 < 10) 不成立 → printDigits(12) ← 递:先处理高位 → if (12 < 10) 不成立 → printDigits(1) ← 再递:只剩最高位 → if (1 < 10) 成立 → printf("1") → return → printf("2") ← 归:打印当前位 → printf("3") ← 归:打印当前位

关键点:printDigits(n/10)调用之后的代码在"归"阶段才执行。这就是递归的顺序控制能力——先处理深层问题,再回溯处理本层问题。用循环实现这个效果需要额外用数组倒序,代码远不如递归简洁。


七、初学者常见错误

错误1:缺少递归出口——无限递归

// 错误写法——没有出口!intfactorial(intn){returnn*factorial(n-1);// n 越来越小,但永远不会停}// 结果:栈溢出(Stack Overflow),程序崩溃

错误2:递归出口的条件覆盖面不够

// 错误写法——n=1 有出口,n=0 会无限递归intfactorial(intn){if(n==1)return1;returnn*factorial(n-1);}// factorial(0) → factorial(-1) → ... 无限递归// 正确:if (n <= 1) return 1;

错误3:递归参数没有向出口靠近

// 错误写法——参数不变,永远到不了出口voidbadRecursion(intn){if(n==0)return;badRecursion(n);// 应该传 n-1!}

错误4:大规模斐波那契递归卡死

printf("%d\n",fibonacci(50));// 极慢!计算量指数级增长// 要么用循环实现,要么用"记忆化"缓存中间结果

错误5:在递归函数里用 static 变量累积结果

// 这种写法虽然可能通过,但破坏了递归的纯粹性,第二次调用就有残留值intfactorial(intn){staticintresult=1;// 不推荐}

八、练习题

练习题1:递归求和

用递归实现一个函数int sum(int n),返回 1+2+…+n 的值。在 main 中测试sum(100)

提示:递归关系:sum(n) = n + sum(n-1),出口:sum(1) = 1。

练习题2:递归反转字符串

用递归实现一个函数,将用户输入的字符串反转输出(只输出,不修改原数组)。

提示:先输出最后一个字符,再递归处理前面的子串。出口:字符串长度为 0 或 1。

练习题3:汉诺塔问题(选做)

阅读汉诺塔问题的描述,用递归输出将 n 个盘子从 A 柱移到 C 柱的步骤(每次只能移一个盘子,且大盘不能放在小盘上面)。这是递归思想的经典问题,n=3 时应有 7 步。


九、本篇总结

  1. 递归 = 函数调用自己,必须包含递归出口和递归关系两个要素
  2. 缺少出口 = 无限递归 = 栈溢出,这是递归最严重的错误
  3. "递"是压栈过程(问题缩小),"归"是出栈过程(答案合并)
  4. 斐波那契递归实现效率低——重复计算太多,用循环或记忆化优化
  5. 递归和循环可以互相转换,选择让代码更清晰的写法

图10-4 递归打印数字的递/归过程图:帮助理解"递归调用之后的代码在归阶段执行"这一核心概念。

http://www.jsqmd.com/news/874009/

相关文章:

  • 聚焦2026年当前市场,宁波华维机械有限公司的PVC专用机解决方案 - 2026年企业推荐榜
  • CTF解题记录5(web)
  • 通过curl命令调试Taotoken大模型API,快速排查接入问题
  • 为什么你的AI Agent总在跨境清关环节“失语”?揭秘NLP+规则引擎混合推理的5个关键断点
  • 蜀冀昌生护栏网技术分享:护栏网定制、蜀冀昌生护栏网、蜀冀昌生钢筋网片、钢筋网片价格、钢筋网片公司、钢筋网片厂家哪家好选择指南 - 优质品牌商家
  • 2026年佛山墙面刷新口碑指数报告:基于消保委580条数据与行业协会权威认证 - 优家闲谈
  • 11_指针入门_地址指针变量解引用与指针运算
  • 施工现场安全事故预警准确率达94.6%?——解密某央企AI Agent边缘计算部署架构与3个月落地实录
  • 2025-2026年久韵红家具电话查询:选购前需核实资质并明确定制细节 - 品牌推荐
  • 甘肃太阳能草坪灯技术解析与靠谱厂家服务指南:甘肃草坪灯/甘肃路灯/甘肃道路灯/兰州中高杆灯/兰州交通信号灯/兰州地埋灯/选择指南 - 优质品牌商家
  • 从手工报表到实时BI:一个零售数据平台的踩坑与重构实战
  • 2026年5月国内十大游戏鼠标品牌推荐:专业评测排名电竞抓握防滑脱价格 - 品牌推荐
  • MySQL 进阶教程 第一章第二章
  • 在Taotoken模型广场中根据任务需求选择合适的ChatGPT版本
  • 2025-2026年时余家具电话查询:选购前需核实产品材质与风格适配 - 品牌推荐
  • AI 辅助用户画像与场景构建:从访谈文本到可验证的研究假设
  • 广州厂房搬迁避坑指南2026新规下如何选对靠谱搬家公司? - 生活服务
  • Go语言并发模式:Worker Pool
  • 为什么头部科技公司已停用公有版Midjourney?企业版专属水印、审计日志与API策略深度解密
  • AI 快速生成标准化问卷分析报告:从 SUS 到 UMUX-LITE,如何把“分数”写成“结论”
  • 【紧急预警】Apple Podcasts与Spotify已启动AI语音内容水印识别系统——3步完成合规声纹嵌入(含Python脚本+FFmpeg参数集)
  • 2026年景区智能监控系统实测评测:远程监控器、远程监控系统、远程监控设备、安防监控系统设备、数字高清监控、无线监控系统选择指南 - 优质品牌商家
  • Go语言上下文管理:Context模式
  • 【2026最新全网最细】MySQL卸载、下载、安装、配置、使用全流程图文解析、和细节讲解(保姆级教学)
  • 2026年Q2:高效节能电机厂家推荐、Y系列三相异步电机生产厂家、Y系列电机生产厂家价格、Y系列电机生产厂家推荐选择指南 - 优质品牌商家
  • 【Claude法律文档分析实战指南】:3大合规风险识别技巧+5类合同审查模板,法务人手一份的AI提效秘籍
  • 普宁新手妈妈月子中心哪家教带娃|出月子后能独立带娃吗 - 品牌观察
  • 2026.5.20,2026.5.21笔记
  • 行业观察:2026现阶段云南钢模板单价,中陆达钢模板如何以高性价比突围? - 2026年企业推荐榜
  • 【Java 抽象类(零基础完整版超详细教程)看完彻底弄懂 】