在程序设计中,递归(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. 3. Oracle数据库逻辑备份与恢复

    一. Oracle逻辑备份介绍 Oracle逻辑备份的核心就是复制数据:Oracle提供的逻辑备份与恢复的命令有exp/imp,expdp/impdp.当然像表级复制(create table tab ...

  2. C#调用DLL报“试图加载格式不正确的程序”

    项目右键属性->项目设计器->生成->平台->把'默认设置(任何 CPU)'改为x86或者x64

  3. VS2017编译Poco1.9.0的64版本

    需要先准备好OpenSSL1.0.2 下载poco-poco-1.9.0-release.zip,解压,修改buildwin.cmd中的OPENSSL_DIR路径,特别注意OPENSSL_LIB的路径 ...

  4. [XPath] XPath 与 lxml (四)XPath 运算符

    XPath 中支持的运算符 # | 或: 返回所有 price 和 title 节点集合 >>> root.xpath('//price|//title') [<Element ...

  5. NUC972裸机调试步骤

    .将开发板设置成USB启动.开发板拨码开关与启动方式设置对应如下: 其中PA1 ,PA0对应S1和S0 上电或复位,使开发板从USB启动.开启NuWriter,选择正确的芯片型号,按下“Continu ...

  6. (原)android修改文件所属的用户组

    首先得安装了busybox: 命令如下: busybox fileName 其中的0表示root,改成1000则表示system,改成2000则表示shell.

  7. DLL注入之Appinit_Dlls

    AppInit_DLLs is a mechanism that allows an arbitrary list of DLLs to be loaded into each user mode p ...

  8. GitHub----初学习(一)

    刚开始学习GitHub,在这借鉴一下别人的总结,http://youngxhui.github.io/2016/05/03/GitHub-for-Windows%E4%BD%BF%E7%94%A8%E ...

  9. sqlserver添加查询 表、字段注释(转)

    环境:xp sp3,sql server2008 .sqlserver用语句给表注释 EXECUTE sp_addextendedproperty N'MS_Description', N'表注释', ...

  10. es6 - class的学习

    http://es6.ruanyifeng.com/#docs/class:class Person { constructor{ //构造函数,里边放不被继承的私有属性和方法 this.proper ...