什么是尾递归,尾递归的优势以及语言支持情况说明
今天在进行数据排序时候用到递归,但是耗费内存太大,于是想找一找有没有既提升效率又节省内存的算法,然后发现尾递归确实不错,只可惜php并没有对此作优化支持.
虽然如此,但还是学习了,下面总结一下:
尾递归 --概念
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/*facttail.c*/
#include"facttail.h"
/*facttail*/
int facttail( int n, int a)
{
/*Compute a factorialina tail - recursive manner.*/
if (n < 0)
return 0;
else if (n == 0)
return 1;
else if (n == 1)
return a;
else
return facttail(n - 1, n * a);
}
|
尾递归与传统递归比较
1
2
3
4
5
|
long Rescuvie( long n) {
return (n == 1) ? 1 : n * Rescuvie(n - 1);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
long TailRescuvie( long n, long a) {
return (n == 1) ? a : TailRescuvie(n - 1, a * n);
}
long TailRescuvie( long n) { //封装用的
return (n == 0) ? 1 : TailRescuvie(n, 1);
}
|
Rescuvie(5){5 * Rescuvie(4)}{5 * {4 * Rescuvie(3)}}{5 * {4 * {3 * Rescuvie(2)}}}{5 * {4 * {3 * {2 * Rescuvie(1)}}}}{5 * {4 * {3 * {2 * 1}}}}{5 * {4 * {3 * 2}}}{5 * {4 * 6}}{5 * 24}120
对于尾递归, 他的递归过程如下:
TailRescuvie(5) // 所以在运算上和内存占用上节省了很多,直接传回结果TailRescuvie(5, 1) return 120↑
TailRescuvie(4, 5) return 120↑
TailRescuvie(3, 20) return 120↑
TailRescuvie(2, 60) return 120↑
TailRescuvie(1, 120) return 120↑
120 //当运行到最后时,return a => return 120 ,将120返回上一级
说明:
尾递归的效果就是去除了将下层的结果再次返回给上层,需要上层继续计算才得出结果的弊端,如果仔细观看例子就可以看出,其实每个递归的结果是存储在第二个参数a中的,到最后一次计算的时候,会只返回一个a的值,但是因为是递归的原理虽然仍然要返回给上层,依次到顶部才给出结果,但是不需要再做计算了,这点的好处就是每次分配的内存不会因为递归而扩大。
在效率上,两者的确差不多。
尾递归的优势
编译器优化支持尾递归说明:
让我们先回顾一下函数调用的大概过程:
1)调用开始前,调用方(或函数本身)会往栈上压相关的数据,参数,返回地址,局部变量等。
2)执行函数。
3)清理栈上相关的数据,返回。
因此,在函数 A 执行的时候,如果在第二步中,它又调用了另一个函数 B,B 又调用 C.... 栈就会不断地增长不断地装入数据,当这个调用链很深的时候,栈很容易就满 了,这就是一般递归函数所容易面临的大问题。
一直在强调,尾递归的实现依赖于编译器的帮助(或者说语言的规定),为什么这样说呢?先看下面的程序:
1 #include <stdio.h> 2 3 int tail_func(int n, int res) 4 { 5 if (n <= 1) return res; 6 7 return tail_func(n - 1, n * res); 8 } 9 10 11 int main() 12 { 13 int dummy[1024*1024]; // 尽可能占用栈。 14 15 tail_func(2048*2048, 1); 16 17 return 1; 18 }
上面这个程序在开了编译优化和没开编译优化的情况下编出来的结果是不一样的,如果不开启优化,直接 gcc -o tr func_tail.c 编译然后运行的话,程序会爆栈崩溃,但如果开优化的话:gcc -o tr -O2 func_tail.c,上面的程序最后就能正常运行。
这里面的原因就在于,尾递归的写法只是具备了使当前函数在调用下一个函数前把当前占有的栈销毁,但是会不会真的这样做,是要具体看编译器是否最终这样做,如果在语言层面上,没有规定要优化这种尾调用,那编译器就可以有自己的选择来做不同的实现,在这种情况下,尾递归就不一定能解决一般递归的问题。
我们可以先看看上面的例子在开优化与没开优化的情况下,编译出来的汇编代码有什么不同,首先是没开优化编译出来的汇编tail_func:
1 .LFB3: 2 pushq %rbp 3 .LCFI3: 4 movq %rsp, %rbp 5 .LCFI4: 6 subq $16, %rsp 7 .LCFI5: 8 movl %edi, -4(%rbp) 9 movl %esi, -8(%rbp) 10 cmpl $1, -4(%rbp) 11 jg .L4 12 movl -8(%rbp), %eax 13 movl %eax, -12(%rbp) 14 jmp .L3 15 .L4: 16 movl -8(%rbp), %eax 17 movl %eax, %esi 18 imull -4(%rbp), %esi 19 movl -4(%rbp), %edi 20 decl %edi 21 call tail_func 22 movl %eax, -12(%rbp) 23 .L3: 24 movl -12(%rbp), %eax 25 leave 26 ret
注意上面标红色的一条语句,call 指令就是直接进行了函数调用,它会先压栈,然后再 jmp 去 tail_func,而当前的栈还在用!就是说,尾递归的作用没有发挥。
再看看开了优化得到的汇编:
1 tail_func: 2 .LFB13: 3 cmpl $1, %edi 4 jle .L8 5 .p2align 4,,7 6 .L9: 7 imull %edi, %esi 8 decl %edi 9 cmpl $1, %edi 10 jg .L9 11 .L8: 12 movl %esi, %eax 13 ret
注意第7,第10行,尤其是第10行!tail_func() 里面没有函数调用!它只是把当前函数的第二个参数改了一下,直接就又跳到函数开始的地方。此处的实现本质其实就是:下一个函数调用继续延用了当前函数的栈!
这就是尾递归所能带来的效果: 控制栈的增长,且减少压栈,程序运行的效率也可能更高!
上面所写的是 c 的实现,正如前面所说的,这并不是所有语言都摆支持,有些语言,比如说 python, 尾递归的写法在 python 上就没有任何作用,该爆的时候还是会爆。
def func(n, res):if (n <= 1):return resreturn func(n-1, n*res)if __name__ =='__main__':print func(4096, 1)
不仅仅是 python,据说 C# 也不支持,我在网上搜到了这个链接:https://connect.microsoft.com/VisualStudio/feedback/details/166013/c-compiler-should-optimize-tail-calls,微软的人在上面回答说,实现这个优化有些问题需要处理,并不是想像中那么容易,因此暂时没有实现,但是这个回答是在2007年的时候了,到现在岁月变迁,不知支持了没?我看老赵写的尾递归博客是在2009年,用 c# 作的例子,估计现在 c# 是支持这个优化的了(待考).
尾调用
前面的讨论一直都集中在尾递归上,这其实有些狭隘,尾递归的优化属于尾调用优化这个大范畴,所谓尾调用,形式它与尾递归很像,都是一个函数内最后一个动作是调用下一个函数,不同的只是调用的是谁,显然尾递归只是尾调用的一个特例。
int func1(int a) {static int b = 3;return a + b; }int func2(int c) {static int b = 2;return func1(c+b); }
上面例子中,func2在调用func1之前显然也是可以完全丢掉自己占有的栈空间的,原因与尾递归一样,因此理论上也是可以进行优化的,而事实上这种优化也一直是程序编译优化里的一个常见选项,甚至很多的语言在标准里就直接要求要对尾调用进行优化,原因很明显,尾调用在程序里是经常出现的,优化它不仅能减少栈空间使用,通常也能给程序运行效率带来比较大的提升。
漫谈递归:尾递归与CPS
php可以采用函数自行优化,可以参考
http://www.nowamagic.net/librarys/veda/detail/2334
什么是尾递归,尾递归的优势以及语言支持情况说明相关推荐
- Android平台语言支持状态
1.上表中的红色表示MTK新添加的语言,标记"N"表示当前版本不支持:标记"Y"表示mtk.google均支持:标记"GD_MN"表示Goo ...
- c语言 尾递归,尾递归的笔记
尾递归,在工作中从来没有用到,仅仅是听过. 早晨看文章在Java中谈尾递归–尾递归和垃圾回收的比较的时候,觉得蛮有意思的,尾递归居然可以和JVM的GC放在一起比较,所以搜索了一些文章,作为收藏. 如下 ...
- golang对比java的优势_golang语言和其它编程语言的对比
在软件行业做过一段时间的人都知道,没有万能的编程语言,也没有万能开发框架,更没有万能的解决方案.任何新技术的产生都应该归功于一部分人对老旧技术的强烈不满.Go语言也不例外.比如,C语言的依赖管理.C+ ...
- java 脚本语言交互_Java学习笔记--脚本语言支持API
Java语言的动态性之脚本语言支持API 随着Java平台的流行,很多的脚本语言(scripting language)都可以运行在Java虚拟机啊上,其中比较流行的有JavaScript.JRuby ...
- go语言 不支持动态加载_动态语言支持
go语言 不支持动态加载 本文是我们名为" 高级Java "的学院课程的一部分. 本课程旨在帮助您最有效地使用Java. 它讨论了高级主题,包括对象创建,并发,序列化,反射等. 它 ...
- 微软在动态语言支持上超越了Java?
当.NET在2000/2001年第一次发布的时候,Java社区认为它仅仅是从语言以及标准库上对Java的一个"克隆".我们把二者的简单实例代码进行比较以后就可以很轻易地得出这样一个 ...
- Java7 一些新特性及脚本语言支持API--笔记
1.switch条件语句中可以加入字符串了,实现方法是利用了字符串的hashcode()值作业真正的值 2.增加了一种可以在字面量中使用的进制,二进制,通过在数字前面加"0b"或& ...
- “易写易库(EXEK)”项目启动,用易语言开发易语言支持库
"易写易库"(EXEK,E Xie E Ku)项目已经启动,用易语言开发易语言支持库.我(liigo)准备用一个月左右的业余时间,完成本项目的一期工程. 用易语言开发易语言支持库, ...
- 【客服系统】在线客服系统源码外贸聊天通讯带翻译多语言支持网页安卓苹果打包封装APP
随着全球化的加速推进,外贸行业对于在线客服系统的需求日益增长.一款功能强大.支持多语言交流.适用于网页和移动端的在线客服系统源码成为了众多企业的首选.本文将介绍一款名为"外贸聊天通讯带翻译多 ...
最新文章
- 三层架构中ajax,基于mvc三层架构和ajax技术实现最简单的文件上传
- [改善Java代码] 避免instanceof非预期结果
- (017)java后台开发之客户端通过HTTP获取接口Json数据
- HDU4473_Exam
- 软件架构设计最佳实践
- Linux常用指令2
- 协议形式化安全分析 Scyther 并非所有协议可以照抄就搬
- 无监督学习︱GAN 在 NLP 中遇到瓶颈+稀疏编码自学习+对偶学习
- cmd echo写入shell_为什么说Shell脚本就是最好的教程和笔记呢?
- WCF开发之消息契约(MessageContract)
- Laya的位图字体bitmapFont字体用法
- Linux 访问 Windows 代理服务器配置
- 抖音巨量百应怎么入驻?
- AVCaptureDevice中通过调用VideoZoomFactor方法调整焦距实现拉近拉远镜头进行拍照录制视频(动画缩放画面,不闪屏)
- 计算机接入因特网有几种方式有哪些,简述几种因特网的接入方式?
- 春秋战国开局名臣搭配推荐
- 战略支援部队信息工程大学的计算机类,战略支援部队信息工程大学2018年硕士研究生招生简章...
- dubbo下Dubbo协议注册中心理解SimpleRegistryService之register,getRegistered,notify方法理解注释
- 【Appium踩坑】Encountered internal error running command: Error executing adbExec.
- 基于Python实现的口罩佩戴检测