在程序设计中,递归(Recursion)是一个很常见的概念,合理使用递归,可以提升代码的可读性,但同时也可能会带来一些问题。

下面以阶乘(Factorial)为例来说明一下递归的用法,实现语言是PHP:

<?php

function factorial($n) {
if ($n == 0) {
return 1;
} return factorial($n - 1) * $n;
} var_dump(factorial(100)); ?>

如果安装了XDebug的话,可能会遇到如下错误:

Fatal error: Maximum function nesting level of ’100′ reached, aborting!

注:这是XDebug的一个保护机制,可以通过max_nesting_level选项来设置。

即便代码能正常运行,只要我们不断增大参数,程序迟早会报错:

Fatal error:  Allowed memory size of … bytes exhausted

为什么呢?简单点说就是递归造成了栈溢出。有几个方法可以用来规避这个问题,比如说利用尾调用(Tail Call)来消除递归对栈的影响。

下面以Lua作为描述语言来说明尾调用的含义,代码如下:

function factorial(n)
if (n == 0) then
return 1
end return factorial(n - 1) * n
end print(factorial(100))

这段代码同样会遇到栈溢出的问题。怎样利用尾调用来搞定呢?让我们先来看看尾调用的定义:如果一个函数在执行了一次函数调用后,不再做别的事就称为尾调用。形象点说就是直接返回一个函数调用。尾调用不会返回原来的函数,所以不需要额外的栈保留调用函数的数据。上面代码改成尾调用后类似下面代码的样子:

function factorial(n, accumulator)
accumulator = accumulator or 1 if (n == 0) then
return accumulator
end return factorial(n - 1, accumulator * n)
end print(factorial(100))

注:关于Lua中尾调用的介绍可以参考:Proper Tail Recursion

照猫画虎,我们用PHP来实现一个尾调用版本的阶乘:

<?php

function factorial($n, $accumulator = 1) {
if ($n == 0) {
return $accumulator;
} return factorial($n - 1, $accumulator * $n);
} var_dump(factorial(100)); ?>

可惜测试后才发现PHP根本不支持尾调用!好在天无绝人之路,仔细阅读维基百科中关于尾调用的介绍,你会发现里面提到了Trampoline的概念。简单点说就是利用高阶函数消除递归,依照这样的理论基础,我们可以把上面的尾调用代码改写成如下方式:

<?php

function factorial($n, $accumulator = 1) {
if ($n == 0) {
return $accumulator;
} return function() use($n, $accumulator) {
return factorial($n - 1, $accumulator * $n);
};
} function trampoline($callback, $params) {
$result = call_user_func_array($callback, $params); while (is_callable($result)) {
$result = $result();
} return $result;
} var_dump(trampoline('factorial', array(100))); ?>

看上去不错,不过我不得不向大家道个歉,本文用递归实现阶乘其实是个玩笑,实际上只要用一个循环就行了,《代码大全》里专门提到了这一点:

<?php

function factorial($n) {
$result = 1; for ($i = 1; $i <= $n; $i++) {
$result *= $i;
} return $result;
} var_dump(factorial(100)); ?>

还有很多别的方法可以用来规避递归引起的栈溢出问题,比如说Python中可以通过装饰器和异常来消灭尾调用,让人有一种别有洞天的感觉:

另外,Python之父关于为何不在Python中支持尾调用的博文也很有看头:

好了,就写到这吧。除非能提升代码可读性,否则没有必要使用递归;迫不得已之时,最好考虑使用Tail Call或Trampoline等技术来规避潜在的栈溢出问题。

PHP、Lua中的 尾调用的更多相关文章

  1. JavaScript 中的尾调用

    尾调用(Tail Call) 尾调用是函数式编程里比较重要的一个概念,它的意思是在函数的执行过程中,如果最后一个动作是一个函数的调用,即这个调用的返回值被当前函数直接返回,则称为尾调用,如下所示: f ...

  2. JavaScript中的尾调用优化

    文章来源自:http://www.zhufengpeixun.com/qianduanjishuziliao/javaScriptzhuanti/2017-08-08/768.html JavaScr ...

  3. Lua 正确的尾调用(proper tail call)

    Lua支持“尾调用消除(tail-call elimination)”.尾调用(tail call):当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”.例如,下面的代码就是一条“ ...

  4. [Lua] 尾调用消除(tail-call elimination)

    <Lua程序设计(第2版)> 6.3 正确的尾调用(proper tail call) Lua是支持尾调用消除(tail-call elimination)的,如下面对函数g的调用就是尾调 ...

  5. Lua中的metatable详解

    转自:http://www.jb51.net/article/56690.htm Lua 中 metatable 是一个普通的 table,但其主要有以下几个功能: 1.定义算术操作符和关系操作符的行 ...

  6. quick-cocos2dx lua中读取 加密 csv表

    我非常想把一些非必需的信息以CSV表的格式保存到客户端,以减少和服务器的通讯,降低压力.于是写了这么一个. 但因为大家觉得这样的话,需要每次登陆时来检测同步这些数据,会减慢登陆速度,于是没有用到. 我 ...

  7. 在JAVA中使用LUA脚本记,javaj调用lua脚本的函数(转)

    最近在做一些奇怪的东西,需要Java应用能够接受用户提交的脚本并执行,网络部分我选择了NanoHTTPD提供基本的HTTP服务器支持,并在Java能承载的许多脚本语言中选择了很久,比如Rhino,Jy ...

  8. 简述C/C++调用lua中实现的自定义函数

    1.首先说下目的,为什么要这么做 ? 正式项目中,希望主程序尽量不做修改,于是使用C/C++完成功能的主干(即不需要经常变动的部分)用lua这类轻量级的解释性语言实现一些存在不确定性的功能逻辑:所以, ...

  9. ulua c#调用lua中模拟的类成员函数

    项目使用ulua,我神烦这个东西.lua单纯在lua环境使用还好,一旦要跟外界交互,各种月经不调就来了.要记住贼多的细节,你才能稍微处理好.一个破栈,pop来push去,位置一会在-1,一会在-3,2 ...

随机推荐

  1. WAF Bypass数据库特性(Access探索篇)

    0x01 背景 无聊,测试了一下access特性 0x02 测试 常见有5个位置即:select * from admin where id=1[位置一]union[位置二]select[位置三]1, ...

  2. [转]linux下释放文件内存

    在Linux系统下,我们一般不需要去释放内存,因为系统已经将内存管理的很好.但是凡事也有例外,有的时候内存会被缓存占用掉,导致系统使用SWAP空间影响性能,此时就需要执行释放内存(清理缓存)的操作了. ...

  3. Dictionary的应用

    在C#中,Dictionary提供快速的基于兼职的元素查找.他的结构是这样的:Dictionary<[key], [value]> ,当你有很多元素的时候可以使用它.它包含在System. ...

  4. linux下getsockopt和setsockopt详解及测试

    linux下getsockopt和setsockopt详解及测试 NAME 名字 getsockopt, setsockopt - get and set options on sockets 获取或 ...

  5. RN(八)——react-native-image-viewer & react-native-swiper

    以项目(业务GO)为例: react-native-swiper 轮播(用在首页的图集轮播) https://github.com/leecade/react-native-swiper react- ...

  6. PyQt4简单的窗口程序

    下面的程序显示了一个简单的小窗口. #!/usr/bin/python # -*- coding:utf-8 -*- import sys from PyQt4 import QtGui app = ...

  7. 转 Mysql性能优化教程

    Mysql性能优化教程 背景及目标 厦门游家公司(4399.com)用于员工培训和分享. 针对用户群为已经使用过mysql环境,并有一定开发经验的工程师 针对高并发,海量数据的互联网环境. 本文语言为 ...

  8. 解析pdb文件得到未导出变量地址(转)

    程序要用到dbghelp.dll中的一些函数 http://msdn.microsoft.com/en-us/library/ms679291%28VS.85%29.aspx 要自己下载系统对应的符号 ...

  9. Django restframwork教程之类视图(class-based views)

    我们也可以使用类的views写我们的API,我们将看到这是一个强大的模式,允许我们重用公共功能,让我们的代码整洁 使用Class-based Views重新改写我们的API 打开views.py文件, ...

  10. 【EF框架】使用params参数传值防止SQL注入报错处理

    通过SqlParameter传时间参数,代码如下: var param = new List<SqlParameter>(); param.Add(new SqlParameter(&qu ...