尾递归,在工作中从来没有用到,仅仅是听过。
早晨看文章在Java中谈尾递归–尾递归和垃圾回收的比较的时候,觉得蛮有意思的,尾递归居然可以和JVM的GC放在一起比较,所以搜索了一些文章,作为收藏。
如下截取自在Java中谈尾递归–尾递归和垃圾回收的比较,部分文字做了删减,但不影响理解。
尾递归优化解决的是内存溢出的问题,而垃圾回收解决的是内存泄露的问题
内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。
自动垃圾回收机制的特点是:
解决了所有情况下的内存泄露的问题,但还可以由于其他原因内存溢出
针对内存中的堆空间
正在运行的方法中的堆中的对象是不会被管理的,因为还有引用(栈帧没有被清空)
与之相对,尾递归优化的特点是:
优化了递归调用时的内存溢出问题
针对内存中的堆空间和栈空间
只在递归调用的时候使用,而且只能对于写成尾递归形式的递归进行优化
正在运行的方法的堆和栈空间正是优化的目标
通过比较可以发现尾递归和GC是完全不一样的,JAVA不会是因为有GC所以不需要尾递归优化。
如下截取自Scala之尾递归,对递归和循环的区别有一个清晰的表述,蛮有意思的。
在把递归转换成尾递归中,有一个道理非常重要。递归是表达问题的一个方式,而循环是寻找解决问题的一个方式。尾递归的本质是循环而不是递归。因此在转换的过程中,不要把思路拘泥于递归上,而应该扩展到循环上。理论上来讲,任何递归都可以转换成循环,因为尾递归的本质是循环,因此任何递归也应该都可以转换为尾递归。
为如下这句话点赞。
递归是表达问题的一个方式,而循环是寻找解决问题的一个方式。
相比较而言,循环是比较反人类的,理解起来着实困难,比如树的遍历,
递归的解法为,先遍历左树,后遍历右树,再访问根节点,完了。
循环的解法就复杂了,除了循环,还需要用栈或者队列来做辅助,好难理解。
但如下这个推理过程,Jackie表示不能认同。
任何递归都可以转换成循环,因为尾递归的本质是循环,因此任何递归也应该都可以转换为尾递归。
关于Java中尾递归的优化,介绍了尾调用和尾调用的优化。
另外还有阿里的招聘广告,保存下来备用吧。
花名有孚,支付宝工程师
有希望加入支付宝的同学,可以把简历发到我的个人邮箱[email protected]
如下截取自线性递归和尾递归,给出了线性递归和尾递归的定义。
线性递归:即一般型的递归,一个函数直接或间接地调用自身,是为直接或间接递归,在调用过程中,需要压栈,可能会导致程序崩溃。
尾递归:尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去.
如下截取自尾递归。
分析了一大堆,那么尾递归到底有什么好处呢?呵呵,可读性好,代码强壮,易维护。
另外,不是所有的递归都可以变成尾递归的。
从老赵点滴 – 追求编程之美找到了如下文章,从样例代码看,使用的应当是C#,还好不影响理解。站点使用.net技术来构建,比较有特点。
奇怪的是没有找到《尾递归对时间与空间复杂度的影响(下)》的链接。
一般来说,递归需要有:边界条件、递归前进段和递归返回段。
当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
注意:
递归就是在过程或函数里调用自身;
在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
递归的定义
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
递归的原理
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。虽然编译器能够优化尾递归造成的栈溢出问题,但是在编程中,我们还是应该尽量避免尾递归的出现,因为所有的尾递归都是可以用简单的goto循环替代的。
若非注明,均为原创,欢迎转载,转载请注明来源:尾递归的笔记