c语言递归为什么会自动返回,聊聊递归与尾递归——仍然以C语言为例
前言
大约半个月前,我在《深入理解栈内存与函数调用栈——以C语言为例》这篇文章的结尾给自己挖了个坑。鉴于我挖了没管的坑已经两只手都数不过来了,所以是时候填一填了。
看官可以先食用之前那篇文章,以获得关于函数调用栈的背景知识。
递归(recursion)
递归并不是一个程序设计领域专属的概念,有很多其他丰富的例子:
德罗斯特效应(Droste effect),即与原图相同的图重复嵌套出现,得名于1904年出产的Droste牌可可粉包装。
俄罗斯套娃(Matryoshka),下图是一个已经打开到最内层的套娃。
西尔平斯基三角形(Sierpinski triangle),等边三角形的不断细分。
GNU = "GNU's not Unix",GNU = "GNU's not Unix",……
从前有座山,山上有座庙,庙里有个老和尚正在讲故事,讲的什么呢?从前有座山,山上有座庙,庙里有个老和尚正在讲故事,讲的什么呢?从前有座山,山上有座庙………(泥垢
当然,递归也是我们入门程序设计时很早就会接触的概念,一句话:函数直接或间接地调用自身。在大学计算机课程初期,递归也贯穿在整个学习过程中,从比较简单的阶乘、斐波那契数列、辗转相除法,到回溯法、树的遍历、深度优先搜索等等。
以最简单的斐波那契数列为例,它的递归描述如下:
int fibonacci(int n) {
assert(n >= 0);
if (n == 1 || n == 2) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
当然,我们也有对应的迭代描述:
int fibonacci(int n) {
assert(n >= 0);
if (n == 1 || n == 2) {
return 1;
}
int f1 = 1, f2 = 1, result = 0;
for (int j = 3; j <= n; j++) {
result = f1 + f2;
f2 = f1;
f1 = result;
}
return result;
}
说白了,递归的思想就是“大事化小,小事化了”,将原问题逐层拆解为规模较小的子问题,直至遇到递归终止条件再逐层返回。设定正确的递归终止条件很重要,否则永远得不出结果,妥妥造成栈溢出。
既然问题的递归解法都有等价的迭代解法,那么递归还有什么存在的意义呢?别忘了,迭代法要求我们必须自己记录下每个子问题的解(递归的话就是函数调用栈的栈帧帮我们记录了),对于复杂的递归问题还要引入辅助数据结构来转化为非递归的,思路比较绕,并且代码会膨胀,递归解法则简明又方便。
斐波那契数列的例子太naive,只是多用了两个变量而已。下面换一道已经被问烂了的面试题,就能看出更明显的差别。
输入一棵二叉树,求该树的深度。深度指从根节点开始到叶子节点的最长路径的长度。
(二叉)树就是一种经典的自然递归的数据结构,所以该题的递归解法非常straightforward,代码如下。
int treeDepth(BiTreeNode *root) {
if (root == null) {
return 0;
}
int dLeft = treeDepth(root->left);
int dRight = treeDepth(root->right);
return (dLeft > dRight) ? (dLeft + 1) : (dRight + 1);
}
如果不用递归就麻烦一些,下面给出一种基于层序遍历的思路,需要引入队列。
int treeDepth(BiTreeNode *root) {
if (root == null) {
return 0;
}
int depth = 0;
queue q;
while (!q.empty()) {
depth++;
int size = q.size();
while (size--) {
BiTreeNode *node = q.front();
q.pop();
if (node->left != null) {
q.push(node->left);
}
if (node->right != null) {
q.push(node->right);
}
}
}
return depth;
}
虽然仍然不难理解,但代码确实多了不少哈。
例子举完了。由于递归涉及到非常密集的函数调用和返回,故维护栈帧也间接造成了大量的入栈和出栈操作,这就是一般情况下同一问题的递归解法比迭代解法执行效率低的原因。但凡事不是绝对的,如果我们能把普通递归优化成尾递归,效率就会大大提升,下面具体看看。
尾递归(tail recursion)
所谓尾递归,就是指函数在最末尾的一条语句调用自身,并且不能出现调用自身之外的其他表达式逻辑。以递归计算阶乘的函数为例:
int fact1(int n) {
assert(n >= 0);
if (n == 0 || n == 1) {
return 1;
}
return n * fact1(n - 1);
}
这种写法就不是尾递归,因为递归调用虽然出现在末尾,但本次调用的结果需要依赖n乘以下次调用的结果,不是单纯的调用。下面这种写法就是尾递归:
// m initialized as 1
int fact2(int n, int m) {
assert(n >= 0);
if (n == 0) {
return 1;
} else if (n == 1) {
return m;
}
return fact2(n - 1, n * m);
}
通过多引入一个参数m来记录当前调用的结果,就可以将函数调用外部的乘n操作消灭掉。
为什么尾递归比普通递归的效率要高呢?我们以计算5的阶乘为例,写出fact1()函数执行时栈空间的变化如下。
// 递归阶段,栈帧逐渐增多
f(5)
5 * f(4)
5 * [4 * f(3)]
5 * [4 * [3 * f(2)]]
// 递归终止,此时有f(5)~f(1)共5个栈帧
5 * [4 * [3 * [2 * f(1)]]]
// 递归返回
5 * [4 * [3 * [2 * 1]]]
5 * [4 * [3 * 2]]
5 * [4 * 6]
5 * 24
// 得出结果
120
在普通递归的情况下,这段程序创建并销毁了5个栈帧。而fact2()函数执行时栈空间的变化如下。
// 始终只有1个栈帧
f(5, 1)
f(4, 5)
f(3, 20)
f(2, 60)
f(1, 120)
// 得出结果
120
也就是说,因为尾递归不再依赖除了函数自身之外的其他逻辑,所以也就没有必要维护每一层的EBP指针地址和返回地址了。编译器会自动优化尾递归的情况:不再每递归调用一次产生一个栈帧,而是在原有栈帧的基础上直接修改,这比普通递归的效率无疑要高很多了。
The End
今晚初雪,部门出来聚餐,喝酒吃肉,好不舒适。
本文的最后几段是在回家的出租车上用手机写完的,希望没错吧。
民那晚安。
c语言递归为什么会自动返回,聊聊递归与尾递归——仍然以C语言为例相关推荐
- 【转】 asp.net从视频文件中抓取一桢并生成图像文件的方法 实现多语言本地化应用程序 自动返回上次请求页面...
asp.net从视频文件中抓取一桢并生成图像文件的方法 http://www.bianceng.cn/webkf/aspx/201012/21428.htm WebUIValidation.js ht ...
- c语言自定义函数多个返回值,C语言函数返回值
C语言函数返回值教程 如果,我们希望函数不返回任何值,那么我们需要显式的指明其返回类型为 C语言函数不返回值 语法 void funcName(paramType1 param1, paramType ...
- C语言实现扫雷(可自动显示无雷区)
今天我们用c语言写一个简单版本的扫雷,等以后,我写的游戏多了,我们再去写一个游戏大厅,可以供玩家选择自己想玩的游戏 分析: (1)我们用两个二维数组分别存储给玩家展示的面板和储存地雷的 (2)地雷用字 ...
- Swift2.0语言教程之函数的返回值与函数类型
Swift2.0语言教程之函数的返回值与函数类型 Swift2.0中函数的返回值 根据是否具有返回值,函数可以分为无返回值函数和有返回值函数.以下将会对这两种函数类型进行讲解. Swift2.0中具有 ...
- C语言---初识递归///看了这么久的递归,终于会用了~~
这是上机课老师出的一道题目,题较简单,刚开始使用循环加数组写出来,后来想到这种先得后排的方法可以用递归来做..... 输出整数各位数字 本题要求编写程序,对输入的一个整数,从高位开始逐位分割并输出它的 ...
- 易语言魔兽世界怀旧服自动钓鱼源码
疫情期间,学习了一下易语言,写个自动钓鱼前台辅助,自己调了一个晚上,还可以. 视角要调到水平行 .版本 2 .支持库 dm .程序集 窗口程序集_启动窗口 .子程序 _按钮1_被单击 .局部变量 句柄 ...
- 换零钱程序c语言,《SICP》换零钱的递归法与迭代法
咳咳..先说一段废话.. 最近开始看SICP这本书,正看到了换零钱的部分.看到里面那么多简明生动的例子,还有作者的细心讲解,真是唤起了对学习的无限激情.之前也看过王垠的一些文章,提到了诸如Lisp.s ...
- 魅族html查看程序退出,魅族MX2左下角屏幕失灵自动点击怎么解决_魅族2程序自动返回退出的原因...
类型:手机主题大小:5.0M语言:中文 评分:5.0 标签: 立即下载 先说说这个问题是怎么来的.可以肯定的是魅族2左下角屏幕失灵绝对不是机子硬件问题,换屏幕什么的你就去找坑吧!很简单就能解决的问题, ...
- 【C语言】二叉树中序遍历(递归和非递归)算法
二叉树中序遍历的实现思想是: 访问当前节点的左子树: 访问根节点: 访问当前节点的右子树: 图 1 二叉树 以图 1 为例,采用中序遍历的思想遍历该二叉树的过程为: 访问该二叉树的根节点,找到 1: ...
最新文章
- system pause in C#
- 转: 用css把图片转为灰色图
- TX Text Control文字处理教程
- 从shiro源码角度学习工厂方法设计模式
- cg word List2
- springboot 控制台程序读取配置文件(原创)
- 印度朋友手把手教你学Scala(10):Scala里的样本对象
- Ubuntu和window10 安装双系统
- 递增的整数序列链表的插入_LeetCode基础算法题第178篇:和为零的N个唯一整数
- 使用asp.net mvc开发应用程序,页面中的page.IsPostback还有用处吗?
- 区块链开发公司开拓新用途 区块链对网络安全的作用
- word绿豆沙颜色设置_Win7系统下将txt和word背景颜色设置为豆沙绿的方法
- 如何将经典算法与人工智能结合?NeurIPS 2021
- apicloud常用方法总结
- 无限火力跳跳机器人_英雄联盟无限火力小拳拳升降机蒸汽机器人
- 究竟什么是可重入锁?
- 【北邮国院大三上】电子商务法(e-commerce law)知识点整理——Banking Lawe-Payment
- npm list 报错 extraneous
- 裸辞接单第一个月的收入
- Linux各个发行版本代号整理
热门文章
- MyBatis_多表查询的结果封装
- 如何看待制造企业的数字化转型,有哪些成功案例可以分享?
- WebSocket 二、编写WebSocket服务器端
- 毕业生找工作时一定要知道的常识
- 录音转文字软件免费的软件有哪些?这几个录音转文字工具推荐给你
- 【心- 稻盛和夫】阅读笔记
- Bootstrap学习资料整理
- python连接plc_Python与PLC踩坑实录:成功解决西门子 PLC S7-200_SMART与PC连接时不能同时用Python的snap7包和step7软件同时连接...
- 十二大相似图片搜索网站(以图搜图)
- java华南理工大学出版_Java程序设计实验实训教程