当前位置: 首页 > 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.cnnetsun.cn/news/2535043.html

相关文章:

  • C++ 学习笔记---容器---vector(后续会更新)
  • CANN-ops-nn-昇腾NPU神经网络算子的积木盒子
  • 从翻车到封神:1个被低估的--no参数+2个隐藏材质关键词,让水面倒影清晰度突破人眼分辨极限
  • 如何用开源工具实现自动化硬件适配?OpCore-Simplify让跨平台部署变得简单
  • gcc下载地址
  • Keil C166嵌入式开发中的宽字符实现与优化
  • 飞行人形机器人空气动力学建模与CFD仿真实践
  • 抖音内容批量下载实战指南:从单视频到用户主页的高效方案
  • 企业内如何通过Taotoken实现API访问控制与审计
  • PostgreSQL 性能优化:从 3 秒到 30 毫秒,我做了这 5 件事
  • 文件上传漏洞深度解析:从getshell到六维纵深防御
  • IDA与Frida协同逆向:静态定位+动态Hook实战指南
  • Unity风格化山脉管线:轮廓生成+分层材质+程序植被
  • ThingsVis v1.1.15 版本更新:补齐嵌入与运维体验短板,多场景集成更可靠
  • 鸿蒙签名验证报错UNABLE_TO_VERIFY_LEAF_SIGNATURE根因解析
  • PE-bear:专注PE文件结构解析的静态分析利器
  • DeepSeek垂直搜索性能崩塌预警信号:当QPS>127且P99延迟突增>413ms时,必须立即执行的5项熔断操作(含Prometheus监控告警Rule模板)
  • KNN算法如何赋能GIS空间邻近性分析
  • 西班牙法院驳回西甲对 NordVPN 罚款请求,屏蔽令案件仍在审理
  • GPT-4混合专家架构真相:稀疏激活与动态路由原理
  • 学术演示文稿制作困境与LaTeX模板解决方案
  • JMeter分布式压测的Kerberos与OAuth双认证实战指南
  • 前端各类问题
  • 132、运动控制中的通信协议:EtherCAT详解
  • ReACT智能体:推理与行动解耦的AI工作流范式
  • 咨询项目交付周期缩短40%的关键不在算法,而在Agent工作流设计:3个被90%团队忽略的协同断点
  • 多智能体自学习系统:在部分可观测对抗环境中的端到端进化
  • 鸿蒙物流追踪页面构建:运单追踪与快捷入口模块详解
  • Deep Agent工程框架:解耦计划-执行-记忆-协作的智能体架构
  • Lovable不是UI美化!揭秘神经科学验证的4层用户依恋模型与落地SDK架构