今天在进行数据排序时候用到递归,但是耗费内存太大,于是想找一找有没有既提升效率又节省内存的算法,然后发现尾递归确实不错,只可惜php并没有对此作优化支持.

虽然如此,但还是学习了,下面总结一下:

尾递归 --概念

如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

实例

为了理解尾递归是如何工作的,让我们再次以递归的形式计算阶乘。首先,这可以很容易让我们理解为什么之前所定义的递归不是尾递归。回忆之前对计算n!的定义:在每个活跃期计算n倍的(n-1)!的值,让n=n-1并持续这个过程直到n=1为止。这种定义不是尾递归的,因为每个活跃期的返回值都依赖于用n乘以下一个活跃期的返回值,因此每次调用产生的栈帧将不得不保存在栈上直到下一个子调用的返回值确定。现在让我们考虑以尾递归的形式来定义计算n!的过程。
这种定义还需要接受第二个参数a,除此之外并没有太大区别。a(初始化为1)维护递归层次的深度。这就让我们避免了每次还需要将返回值再乘以n。然而,在每次递归调用中,令a=na并且n=n-1。继续递归调用,直到n=1,这满足结束条件,此时直接返回a即可。
代码实例3-2给出了一个C函数facttail,它接受一个整数n并以尾递归的形式计算n的阶乘。这个函数还接受一个参数a,a的初始值为1。facttail使用a来维护递归层次的深度,除此之外它和fact很相似。读者可以注意一下函数的具体实现和尾递归定义的相似之处。
示例3-2:以尾递归的形式计算阶乘的一个函数实现
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);
}

示例3-2中的函数是尾递归的,因为对facttail的单次递归调用是函数返回前最后执行的一条语句。在facttail中碰巧最后一条语句也是对facttail的调用,但这并不是必需的。换句话说,在递归调用之后还可以有其他的语句执行,只是它们只能在递归调用没有执行时才可以执行 。
尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。
也许在C语言中有很多的特例,但编程语言不只有 C语言,在函数式语言Erlang中(亦是栈语言),如果想要保持语言的高并发特性,就必须用尾递归来替代传统的递归。

尾递归与传统递归比较

以下是具体实例:
线性递归:
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);
}

当n = 5时
对于传统线性递归, 他的递归过程如下:
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的值,但是因为是递归的原理虽然仍然要返回给上层,依次到顶部才给出结果,但是不需要再做计算了,这点的好处就是每次分配的内存不会因为递归而扩大。

在效率上,两者的确差不多。

尾递归的优势

        与普通递归相比,由于尾递归的调用处于方法的最后,因此方法之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次方法中留在堆栈中的数据完全清除,把空间让给最后的递归调用。这样的优化便使得递归不会在调用堆栈上产生堆积,意味着即时是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。

编译器优化支持尾递归说明:

尾递归在某些语言的实现上,能避免上述所说的问题,注意是某些语言上,尾递归本身并不能消除函数调用栈过长的问题,那什么是尾递归呢?在上面写的一般递归函数 func() 中,我们可以看到,func(n)  是依赖于 func(n-1) 的,func(n) 只有在得到 func(n-1) 的结果之后,才能计算它自己的返回值,因此理论上,在 func(n-1) 返回之前,func(n),不能结束返回。因此func(n)就必须保留它在栈上的数据,直到func(n-1)先返回,而尾递归的实现则可以在编译器的帮助下,消除这个限制

让我们先回顾一下函数调用的大概过程:

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

什么是尾递归,尾递归的优势以及语言支持情况说明相关推荐

  1. Android平台语言支持状态

    1.上表中的红色表示MTK新添加的语言,标记"N"表示当前版本不支持:标记"Y"表示mtk.google均支持:标记"GD_MN"表示Goo ...

  2. c语言 尾递归,尾递归的笔记

    尾递归,在工作中从来没有用到,仅仅是听过. 早晨看文章在Java中谈尾递归–尾递归和垃圾回收的比较的时候,觉得蛮有意思的,尾递归居然可以和JVM的GC放在一起比较,所以搜索了一些文章,作为收藏. 如下 ...

  3. golang对比java的优势_golang语言和其它编程语言的对比

    在软件行业做过一段时间的人都知道,没有万能的编程语言,也没有万能开发框架,更没有万能的解决方案.任何新技术的产生都应该归功于一部分人对老旧技术的强烈不满.Go语言也不例外.比如,C语言的依赖管理.C+ ...

  4. java 脚本语言交互_Java学习笔记--脚本语言支持API

    Java语言的动态性之脚本语言支持API 随着Java平台的流行,很多的脚本语言(scripting language)都可以运行在Java虚拟机啊上,其中比较流行的有JavaScript.JRuby ...

  5. go语言 不支持动态加载_动态语言支持

    go语言 不支持动态加载 本文是我们名为" 高级Java "的学院课程的一部分. 本课程旨在帮助您最有效地使用Java. 它讨论了高级主题,包括对象创建,并发,序列化,反射等. 它 ...

  6. 微软在动态语言支持上超越了Java?

    当.NET在2000/2001年第一次发布的时候,Java社区认为它仅仅是从语言以及标准库上对Java的一个"克隆".我们把二者的简单实例代码进行比较以后就可以很轻易地得出这样一个 ...

  7. Java7 一些新特性及脚本语言支持API--笔记

    1.switch条件语句中可以加入字符串了,实现方法是利用了字符串的hashcode()值作业真正的值 2.增加了一种可以在字面量中使用的进制,二进制,通过在数字前面加"0b"或& ...

  8. “易写易库(EXEK)”项目启动,用易语言开发易语言支持库

    "易写易库"(EXEK,E Xie E Ku)项目已经启动,用易语言开发易语言支持库.我(liigo)准备用一个月左右的业余时间,完成本项目的一期工程. 用易语言开发易语言支持库, ...

  9. 【客服系统】在线客服系统源码外贸聊天通讯带翻译多语言支持网页安卓苹果打包封装APP

    随着全球化的加速推进,外贸行业对于在线客服系统的需求日益增长.一款功能强大.支持多语言交流.适用于网页和移动端的在线客服系统源码成为了众多企业的首选.本文将介绍一款名为"外贸聊天通讯带翻译多 ...

最新文章

  1. 三层架构中ajax,基于mvc三层架构和ajax技术实现最简单的文件上传
  2. [改善Java代码] 避免instanceof非预期结果
  3. (017)java后台开发之客户端通过HTTP获取接口Json数据
  4. HDU4473_Exam
  5. 软件架构设计最佳实践
  6. Linux常用指令2
  7. 协议形式化安全分析 Scyther 并非所有协议可以照抄就搬
  8. 无监督学习︱GAN 在 NLP 中遇到瓶颈+稀疏编码自学习+对偶学习
  9. cmd echo写入shell_为什么说Shell脚本就是最好的教程和笔记呢?
  10. WCF开发之消息契约(MessageContract)
  11. Laya的位图字体bitmapFont字体用法
  12. Linux 访问 Windows 代理服务器配置
  13. 抖音巨量百应怎么入驻?
  14. AVCaptureDevice中通过调用VideoZoomFactor方法调整焦距实现拉近拉远镜头进行拍照录制视频(动画缩放画面,不闪屏)
  15. 计算机接入因特网有几种方式有哪些,简述几种因特网的接入方式?
  16. 春秋战国开局名臣搭配推荐
  17. 战略支援部队信息工程大学的计算机类,战略支援部队信息工程大学2018年硕士研究生招生简章...
  18. dubbo下Dubbo协议注册中心理解SimpleRegistryService之register,getRegistered,notify方法理解注释
  19. 【Appium踩坑】Encountered internal error running command: Error executing adbExec.
  20. 基于Python实现的口罩佩戴检测

热门文章

  1. EMV规范(四)——读应用数据
  2. 【RF】【元素定位】 Other element would receive the click
  3. 7种常见的APPUI界面设计布局风格欣赏
  4. Matlab 2014a安装文件下载、安装教程及破解教程!!!
  5. 关于巨量千川出价方法和技巧深度分析
  6. samba服务器搭建详细配置
  7. Pr 入门教程如何个性化“时间轴”面板?
  8. python数值运算m op n_M OP N数值运算问题
  9. 3.5 二维随机变量函数的分布
  10. 2023年推荐几款开源或免费的web应用防火墙