androidstudio调用系统相机为什么resultcode一直返回0_函数递归调用?看这文就够了...

19efe7581c5f14a2b1e0aae9a73eb818.png

作者 | Cooper Song

责编 | Elle

出品 | 程序人生(ID:coder_life)

我猜,大多数程序员第一次接触函数的递归调用都是在算斐波那契数列某项值的时候,这是函数递归调用最常见的应用之一。规定第一项和第二项为1,后面的项,每一项都是其前面两项的和。

用公式表示就是f(n)=f(n-2)+f(n-1)。

而进一步转化,就是f(n)=[f(n-2-2)+f(n-2-1)]+[f(n-1-2)+f(n-1-1)]。

很明显,这是一个递归的过程。

递归的优点是算法简单、容易理解,代码行数少。

但递归也有缺点,咱们将上面的f(n)再化简一下就变成了f(n)=[f(n-4)+f(n-3)]+[f(n-3)+f(n-2)],可以看出,f(n-3)被计算了两次,而f(n-4)+f(n-3)就是再计算f(n-2),又与最后一项f(n-2)是一样的,f(n-2)也被重复计算了。因此,递归的一大缺点就是存在大量的重复计算,运行起来浪费时间也浪费空间。

递归的另一个缺点是递归的层数不能太多(不能递归太深)。那递归得太深了会怎样呢?答案是会爆栈。那么什么是爆栈呢?又是怎样引发爆栈的呢?下面就要从最底层的角度讲一讲函数调用及函数递归调用的原理,相信读完了就会找到答案。

这就要先从程序的链接和装入说起了。

010dc124a960e18bfc6d1b3bc05522d8.png

程序的链接(Link)

一个程序是由多个模块构成的,以C语言为例,有头文件,只有引用了这个头文件你才能使用scanf和printf;还有头文件,只有引用了这个头文件你才能直接调用strlen函数得到字符串的大小。所谓程序的链接,就是将整个程序的所有目标模块(比如程序员自己写的头文件和函数)以及其他所需要的库函数装配成一个完整的装入模块。

原来每个模块都有每个模块的逻辑地址,经过链接后,形成了统一的从0开始的逻辑地址,如下图所示。

4e69ec39595b686322cff494dca58a03.png

如何理解模块?看上图大概就有了概念,一个函数就是一个模块。

c94a0fcdad64afee103035e118e8711d.png

程序的装入(Load)

学过计算机组成原理的同学都知道,在计算机中有个部件叫程序计数器(Program Counter,简称PC),它存放的是程序要执行的下一条指令的地址,CPU要到内存当中去取指令,取到CPU中进行译码分析然后执行。

程序原本存储在磁盘上,因此只经过链接还不能运行,还需要装入主存(内存),CPU通过PC提供的线索到内存中去取指令,如此循环往复,程序才得以运行下去。虽然程序的第一条指令的逻辑地址是0,但它装入内存时在内存中的地址可不是0,因为内存中的低地址是留给系统使用的,也就是系统区,比系统区的地址高的空间才是留给用户使用的,也就是用户区。虽然装入内存后其地址不再是从0开始,但其相对地址是不变的,将上面链接好的装入模块装入内存,内存空间示意图如下。

8e5910e3e74509ef596cb92382695860.png1d815368b9002b6bab817110411f9255.png

函数的调用

所谓函数的调用,就是程序原本在主模块中顺序执行,遇到调用指令暂时到别的模块执行,在别的模块执行完后再返回主模块的下一条指令继续执行,如下图所示。

eadbcd800a4dca1b1c1819c821fbd78c.png

为什么可以执行着执行着就跳到别的模块执行了?又为什么在别的模块执行完了又回到原来的模块执行了呢?之所以能跳到别的模块执行,是因为函数调用指令就指明了目标模块的首地址,将目标模块的首地址传送给了程序计数器PC,就中断了程序的顺序执行,然后进入目标模块执行。之所以执行完子模块还能回到主模块中执行,是因为内存中有一个专门实现函数调用的栈区,在执行调用指令的时候,就将主模块调用指令之后的指令的地址入了栈,当子模块执行到返回指令的时候,再出栈,将栈顶元素(也就是主模块中要执行的下一条指令的地址)传给PC,程序的执行就又回到了主模块。

假设模块A中的指令是:

add ax,bx ;本条指令的地址为10000

call B ;调用模块B本条指令的地址为10001

mov dx,ax ;本条指令的地址为10002

假设模块B中的指令是:

sub cx,dx ;本条指令的地址为15000

mov bx,cx ;本条指令的地址为15001

ret ;本条指令的地址为15002

模块A为主模块,模块B为目标模块,在执行call B指令的时候,函数调用栈区示意图如下(左边为调用前,右边为调用后),SP为栈顶指针。

3fee8351e23c6ddd92c65426848c8563.png

执行完call B,就开始在模块B中执行,一直执行到ret返回指令,此时函数调用栈区示意图如下(左边为返回后,右边为返回前)。

ed8fa9d8cbd9d5b87a660da1752b0cca.png

执行完ret返回指令,将栈顶元素出栈送给程序计数器PC以供CPU继续执行主模块A中的剩余指令。

实际上,函数调用时入栈保护的不仅仅有主模块中调用指令之后的指令的地址,还有一些变量或者说数据,每个函数都有每个函数的局部变量,在主函数中调用子函数,主函数中的局部变量必须入栈保护,否则就会丢失。比如下面这个例子:

int add(int x,int y)

{

int a=x+1;

int b=y+1;

int c=a+b;

return c;

}

int main

{

int a=1,b=2;

int c=add(a,b);

printf(“%d+%d=%d ”,a,b,c);

return 0;

}

主函数和add函数里都有变量a和b,执行完add函数再返回到主函数中a的值必须还为1,b的值必须还为2,因此可以在调用add函数前先将主函数的所有变量(a和b)入栈保护,待执行完返回主函数时再出栈送给变量a和变量b。

9d6804c065eaa97b88a8aa8060d18b87.png

递归函数的调用

递归函数的调用本质上也是函数的调用,只不过是自己在调用自己罢了。

以求斐波那契数列的项为例:

int fibonacci(int n)

{

if(n==1||n==2) //假设本条指令的地址为10000

return 1; //假设本条指令的地址为10001

int a=fibonacci(n-2); //假设本条指令的地址为10002

int b=fibonacci(n-1); //假设本条指令的地址为10003

int c=a+b; //假设本条指令的地址为10004

return c; //假设本条指令的地址为10005

}

如果进入函数的n是1或者是2,那么就直接返回1;

否则,就继续递归下去。

假设主函数调用斐波那契函数的指令的地址为15000,其下一条指令的地址为15001。

假设我们要求斐波那契数列的第5项,公式为

f(5)=f(3)+f(4)

=[f(1)+f(2)]+[f(2)+f(3)]

=[f(1)+f(2)]+[f(2)+[f(1)+f(2)]]

函数调用栈的示意图如下。

第一步,从主函数中进入斐波那契函数,传入的n为5。

第二步,斐波那契函数中执行到int a=fibonacci(n-2),将下一条指令的地址压入栈,也就是将10003入栈,此时的n=5,将n=5压入数据栈,传入的n=3。

cd0d7ab95ef7e1a559315cfd5932b596.png

第三步,斐波那契函数中执行到int a=fibonacci(n-2),将下一条指令的地址压入栈,也就是将10003入栈,此时的n=3,将n=3压入数据栈,传入的n=1。

263301fbd8ef1c05131e5bc806a5280c.png2dce91a4db310bebbf9d719a9c16045a.png

第四步,此时n=1,可以直接返回1给上层的斐波那契函数的a,返回的同时出栈10003给程序计数器PC,出栈n=3给上一层斐波那契函数的n,回到上层的斐波那契函数。

8eea4b17b3dcaf5f16f40031d16c7f7f.png

第五步,执行程序计数器PC指向的指令(内存地址为10003的指令),也就是执行int b=fibonacci(n-1),是一个函数调用,将下一条指令的地址压入栈,也就是将10004入栈,此时n=3,将n=3压入数据栈,此时a=1,将a=1压入数据栈,传入的n=2。

7a5b318471b5d45eac7360b05e0da3ba.pngd3853ceaf310b5286a3d91d5fffd0377.png

第六步,此时n=2,可以直接返回1给上层斐波那契函数的b,返回的同时出栈10004给程序计数器PC,出栈n=3给上一层斐波那契函数的n,出栈a=1给上一层斐波那契函数的a,回到了上层的斐波那契函数。

527a1735102e6f94e00a1b085d3532b6.png

第七步,执行程序计数器PC指向的指令(内存地址为10004的指令),也就是执行int c=a+b,然后顺序执行一直到返回,返回2给上一层斐波那契函数的a,返回的同时出栈10003给程序计数器PC,出栈n=5给上一层的斐波那契函数的n,回到上层的斐波那契函数。

f(5)=f(3)+f(4)

=[f(1)+f(2)]+[f(2)+f(3)]

=[f(1)+f(2)]+[f(2)+[f(1)+f(2)]]

此时红色部分已通过递归计算完成。

第八步,执行程序计数器PC指向的指令(内存地址为10003的指令),也就是执行int b=fibonacci(n-1),是一个函数调用,将下一条指令的地址压入栈,也就是将10004入栈,此时n=5,将n=5压入数据栈,此时a=2,将a=2压入数据栈,传入的n=4。

第九步,斐波那契函数中执行到int a=fibonacci(n-2),将下一条指令的地址压入栈,也就是将10003入栈,此时的n=4,将n=4压入数据栈,传入的n=2。

e0f24172865d4812bb12f02fed67533c.png

第十步,此时n=2,可以直接返回1给上层的斐波那契函数的a,返回的同时出栈10003给程序计数器PC,出栈n=4给上一层斐波那契函数的n,回到上层的斐波那契函数。

第十一步,执行程序计数器PC指向的指令(内存地址为10003的指令),也就是执行int b=fibonacci(n-1),是一个函数调用,将下一条指令的地址压入栈,也就是将10004入栈,此时n=4,将n=4压入数据栈,此时a=1,将a=1压入数据栈,传入的n=3。

第十一步,斐波那契函数中执行到int a=fibonacci(n-2),将下一条指令的地址压入栈,也就是将10003入栈,此时的n=3,将n=3压入数据栈,传入的n=1。

0721f796af016d935755184e65fff7fd.png735eaa31267871623ee9cf07e2ef2e67.png

第十二步,此时n=1,可以直接返回1给上层的斐波那契函数的a,返回的同时出栈10003给程序计数器PC,出栈n=3给上一层斐波那契函数的n,回到上层的斐波那契函数。

第十三步,执行程序计数器PC指向的指令(内存地址为10003的指令),也就是执行int b=fibonacci(n-1),是一个函数调用,将下一条指令的地址压入栈,此时n=3,将n=3压入数据栈,此时a=1,将a=1压入数据栈,传入的n=2。

5ab01cd432006938dec6f6c26ec4a7bd.pnge462fc08b18e6afc5b219c6e5a4920b2.png

第十四步,此时n=2,可以直接返回1给上层的斐波那契函数的b,返回的同时出栈10004给程序计数器PC,出栈n=3给上一层斐波那契函数的n,出栈a=1给上一层斐波那契函数的a,回到上层的斐波那契函数。

第十五步,执行程序计数器PC指向的指令(内存地址为10004的指令),也就是执行int c=a+b,然后顺序执行一直到返回,返回2给上层斐波那契函数的b,返回的同时出栈10004给程序计数器PC,出栈n=4给上一层的斐波那契函数的n,回到上层的斐波那契函数。

第十六步,执行程序计数器PC指向的指令(内存地址为10004的指令),也就是执行int c=a+b,然后顺序执行一直到返回,返回3给上层斐波那契函数的b,返回的同时出栈10004给程序计数器PC,出栈n=5给上一层的斐波那契函数的n,出栈a=2给上一层的斐波那契函数的a,回到上层的斐波那契函数。

f(5)=f(3)+f(4)

=[f(1)+f(2)]+[f(2)+f(3)]

=[f(1)+f(2)]+[f(2)+[f(1)+f(2)]]

此时红色部分已通过递归计算完成。

第十七步,执行程序计数器PC指向的指令(内存地址为10004的指令),也就是执行int c=a+b,然后顺序执行一直到返回,返回5给上层斐波那契函数的接收者,返回的同时出栈15001给程序计数器PC,出栈主函数中的数据(未体现在图中),回到主函数。

此时斐波那契第五项计算完成。

225ccd9bf056a1d9d7122033bd0211a4.png

后记

到了揭晓为什么会爆栈的时刻了,内存中实现函数调用的栈区的大小是有限的,如果递归层数太深,入栈的内容越来越多,甚至出现只入栈不出栈的情况(还没有符合返回条件执行到返回指令栈就满了),如此进行下去,栈满、栈溢出、爆栈只是时间问题,因此在实际项目应用中,如果不能估算出递归的深度,函数递归就要慎用了。

本文虽以斐波那契数列为例介绍函数递归调用的底层原理,但在真正的面试中如果面试官问到了斐波那契数列相关的问题,还是不要给面试官回答一个递归的解法,原因之一就是当n非常大的时候容易爆栈,原因之二就是文章开头说的会产生大量的重复计算。在这里我给大家再提一种解法,就是动态规划(DP)解法。不要一看到动态规划就害怕,斐波那契数列的动态规划解法还是很好理解的。先开一个大一些的数组f。

int fibonacci(int n)

{

f[1]=1,f[2]=1;

for(int i=3;i<=n;i++)

{

f[i]=f[i-2]+f[i-1];

}

return f[n];

}

这样无非是把递归变成了循环,但优点是不会出现重复计算。

简单的递归实现求斐波那契数列项的算法底层之复杂是我没有想象到的,直到一张图一张图亲手画出来我才大吃一惊,在这里我要感谢底层硬件工程师的辛勤付出,没有他们为我们布线铺路,我们是无法使用高级语言轻松编程的。

本文的介绍本着一切从简、方便理解的原则,可能有些地方与实际情况有出入,但是基本思想是一样的。如有不当之处,还请大家批评指正。

b6193608df22b2e8e7831bf34e1201b3.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/526748.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

http请求 url 竖线_http.createServer创建http服务

语法&#xff1a;http.createServer([options][, requestListener])第二个参数requestListener&#xff0c;是一个自动添加到request事件的方法。返回一个新的 http.Server实例。之前代码是使用server.on(request,callback)来监听请求事件&#xff0c;由于http.createServer第二…

物理不突出能学计算机吗,物理成绩不突出,高中选科怎么办?3个理由让你解除后顾之忧!...

目前确定将要于2021年采用新高考“312”模式的有河北、辽宁、江苏、福建、湖南、湖北、广东和重庆8个省市。但是很多高一新生和家长们&#xff0c;面对选科问题时&#xff0c;却是犯了愁。“想选物理呢&#xff0c;物理成绩不突出&#xff0c;没有优势&#xff0c;听说选考物理…

@bean注解和@component注解的区别_通过源码查看 @Component,@Service 等注解是如何被解析的...

点击上方“匠心零度”&#xff0c;选择“设为星标”做积极的人&#xff0c;而不是积极废人来源&#xff1a;my.oschina.net/floor/blog/4325651前言1.Component解析流程找入口找核心方法概要分析2.查文档找思路3. 探寻Component派生性流程1. 确定metadataReader2.查看match方法…

centos7已有数据硬盘挂载_干货!如何给虚拟机增加虚拟硬盘,Linux再分区挂载

很多朋友一开始给虚拟机的硬盘都很小&#xff0c;用着用着虚拟硬盘就不够了&#xff0c;今天小编就给大伙说说如何给虚拟机添加虚拟硬盘在到Linux系统下分区格式化再到挂载。点击创建新的虚拟盘我分配一个8g的硬盘吧&#xff01;现在启动centos 7查看一下你刚刚添加的8G的硬盘&…

diy机器人图片 手绘纸箱_废物利用,她用几个纸箱让家里变成动物园,孩子回家乐坏了!收藏...

纸箱是我们日常生活中最常见的一种废品&#xff0c;现在随着网购的崛起&#xff0c;家里的纸箱越来越多&#xff0c;这时候你是直接丢弃&#xff0c;还是会想想是否可以再利用的价值&#xff1f;这些看似没用的纸箱&#xff0c;在创意妈的眼中可是个宝贝呢~它可以变废为宝成为孩…

计算机研究生考426分单科多少,考研初试426分,依然没被录取,他犯的错误,值得大家参考!...

原标题&#xff1a;考研初试426分&#xff0c;依然没被录取&#xff0c;他犯的错误&#xff0c;值得大家参考&#xff01;最近几天&#xff0c;考研初试成绩陆续发布。考试分数比较高的考生&#xff0c;有机会参加复试&#xff0c;就要尽早准备复试复习了。复试成绩也很重要&am…

70进货卖100利润是多少_一只周黑鸭随便就卖100多,那成本有多少?说出来你可能不信...

一只周黑鸭动不动就卖100多&#xff0c;那成本有多少&#xff1f;说出来你可能不信。如果你吃过周黑鸭&#xff0c;不知道每次想起的时候&#xff0c;尤其是午夜时分会不会有流口水的感觉&#xff0c;那种麻辣的口感&#xff0c;再搭配上冰凉爽口的啤酒&#xff0c;发自心底的那…

powerquery加载pdf_pdf转换为excel,你不会,同事点点鼠标2分钟就搞定了

Hello.大家好&#xff0c;最近office365进行了一次小的更新&#xff0c;这次更新在excel中添加了获取pdf文件中的表格的功能&#xff0c;操作起来十分的简单&#xff0c;下面就跟大家分享下它是如何操作的首先我们新建一个excel文件&#xff0c;然后点击数据功能组&#xff0c;…

防火墙设置导致服务器站点打开,服务器、网站、环境配置全正常网站打不开原来是系统防火墙造成的...

大家都知道网站是架在服务器上的&#xff0c;通过域名解析指向网站并在服务器上绑定域名&#xff0c;上传网站程序到指定的目录&#xff0c;并只要有适合网站运行的环境&#xff0c;网站目录权限正常网站就肯定可以正常运行了&#xff0c;但今天我们技术遇到了一个问题以上所说…

web前端开发技术期末考试_智慧树来我校开展WEB前端开发微专业导学

11月17日&#xff0c;在6号教学楼6102阶梯教室&#xff0c;智慧树工作人员带来了WEB前端开发微专业导学。根据前期长青联盟微专业的报名情况及学生学习情况&#xff0c;为进一步提高学生学习微专业的质量&#xff0c;学院联系了微专业教学平台的专家对教学平台的使用进行技术指…

计算机辅助设计还需要手绘吗,西安电脑如此发达为什么还要学习手绘

在处理器以八核成为主流、硬盘以T而论的今天&#xff0c;计算机辅助设计软件是设计师的得力工具。用铅笔、尺子要时间才能画好的一个室内空间平面布置图&#xff0c;如果用CAD可能一个小时都不用就完成了&#xff0c;而且很方便修改&#xff0c;可见在效率方面&#xff0c;电脑…

一键生成通讯录的软件_橙瓜码字自动写作软件,外貌描写对话描写一键生成

很多写手对于以古代为背景的小说&#xff0c;偶尔都会对某些地方感到棘手&#xff0c;有时候是历史的背景&#xff0c;有时候是当时的官制、称呼&#xff0c;或是一些特有的服饰&#xff0c;外貌的描写&#xff0c;而面对难题&#xff0c;很多时候我们也是选择上网查询&#xf…

gin 静态文件服务器拒绝,nginx实现简单的图片服务器(windows)+静态文件服务器-Go语言中文社区...

需求&#xff1a;能够使用 http://localhost/目录/图片名 访问本地(服务器)已经存在的图片首先需要在本地将nginx跑起来&#xff0c;这里使用默认端口80&#xff1b;在浏览器使用http://localhost看到下面的界面证明nginx启动正常。下面需要修改nginx.conf来实现直接使用http:/…

wegame每次登陆都要滑动验证_Vue项目中实现用户登录及token验证

在前后端完全分离的情况下&#xff0c;Vue项目中实现token验证大致思路如下&#xff1a;1、第一次登录的时候&#xff0c;前端调后端的登陆接口&#xff0c;发送用户名和密码2、后端收到请求&#xff0c;验证用户名和密码&#xff0c;验证成功&#xff0c;就给前端返回一个toke…

云服务器 性能监控软件,云监控 - 云应用监控 - ManageEngine Applications Manager

云监控什么是云监控?尽管许多组织仍然依赖于在自托管的数据中心中存储数据的本地方法&#xff0c;但在业务组织中采用云服务的情况已经逐渐增多。自然&#xff0c;这导致了一些云监控工具的出现。无论您使用的是公共、私有还是混合环境&#xff0c;对应用程序性能具有端到端可…

mysql导入dat文件_MySql导入和抽取大数量级文件数据

一、情况介绍需要处理的文件是一个3.41G的csv格式文件&#xff0c;现在需要把它导入数据库&#xff0c;进行后续处理和分析。二、导入数据该文件数据量庞大&#xff0c;无法用excel或者editplus之类普通软件打开&#xff0c;于是借用了pandas的方法查看了表的结构&#xff1a;方…

服务器操作系统与安装步骤,服务器操作系统与安装步骤

服务器操作系统与安装步骤 内容精选换一换如果在创建弹性云服务器时未设置密码&#xff0c;或密码丢失、过期&#xff0c;可以参见本节操作重置密码。密码丢失或过期前&#xff0c;已安装密码重置插件。公共镜像创建的弹性云服务器默认已安装一键重置密码插件。私有镜像创建的云…

文件服务器mfs,分布式文件系统MFS(moosefs)实现存储共享

这种架构除了性能问题而外&#xff0c;还存在单点故障&#xff0c;一旦这个NFS服务器发生故障&#xff0c;所有靠共享提供数据的应用就不再可用&#xff0c;尽管用rsync方式同步数据到另外一个服务器上做nfs服务的备份&#xff0c;但这对提高整个系统的性能毫无帮助。基于这样一…

mysql 1215_mysql执行带外键的sql文件时出现mysql ERROR 1215 (HY000): Cannot add foreign key constraint的解决...

ERROR 1215 (HY000): Cannot add foreign key constraint最近在建表时遇到了这个错误&#xff0c;然后找了下找到了解决办法&#xff0c;记录下&#xff1a;本来是要建两张表&#xff1a;出现下面的提示&#xff1a;然后开始上网查&#xff0c;有的说是用的引擎不同的原因&…

mysql里面可以用正则吗_Mysql中使用正则表达式

最近在《MYSQL必知必会》这本书中读到在SQL中使用正则表达式&#xff0c;以前是学过数据库也学过正则表达式&#xff0c;但是不知道原来他们可以一起使用&#xff0c;这里做下记录。首先是Mysql使用正则表达式的语法&#xff1a;SELECT ... FROM ... WHERE ... REGEXP ...(这…