递归的定义

递归(http:/en.wikipedia.org/wiki/Recursive)是一种函数调用自身(直接或间接)的一种机制,这种强大的思想可以把某些复杂的概念变得极为简单。在计算机科学之外,尤其是在数学中,递归的概念屡见不鲜。例如:最常用于递归讲解的斐波那契数列便是一个极为典型的例子,而其他的例如阶层(n!)也可以转化为递归的定义(n! = n*(n-1)!).即使是在现实生活中,递归的思想也是随处可见:例如,由于学业问题你需要校长盖章,然而校长却说“只有教导主任盖章了我才会盖章”,当你找到教导主任,教导主任又说:“只有系主任盖章了我才会盖章”...直到你最终找到班主任,在得到班主任豪爽的盖章之后,你要依次返回到系主任、教导主任、最后得到校长的盖章,过程如下:

盖章的故事虽然索然无味(谁的大学生活没有点悲催的事情呢?不悲催,怎么证明我们年轻过),但却很好的体现了递归的基本思想,也就是递归的两个基本条件:

  1. 递归的退出条件,这是递归能够正常执行的必要条件,也是保证递归能够正确返回的必要条件。如果缺乏这个条件,递归就会无限进行下去,直到系统给予的资源耗尽
(在大多数语言中,都是堆栈空间耗尽),因此,如果你在编程中碰到类似“stack overflow”(C语言中,即栈溢出)和“max nest level of 100 reached”
(php中,超出递归限制)等错误,多半是没有正确的退出条件,导致了递归深度过大或者无限递归。2. 递推过程。由一层函数调用进入下一层函数调用的递推。以n!为例。在n>1的情况下。N! = N*(N-1)! 便是该递归函数的递推过程,我们也可以简单的称为“递归公式”。

有了这两个基本条件,我们便得到了递归的一般模式, 用代码可以描述为:

function Recur(  param ){if(  reach the baseCondition ){Calu();//计算return ;}//else just do it recursivelyparam = modify(param)/修改参数,准备进入下层调用Recur(param);
}

有了递归的一般模式,我们便可以轻松实现大多的递归函数。例如:经常提起的斐波那契数列的递归实现,再如,目录的递归访问:

function ScanDir($path){if(is_dir($path)){$handler = opendir($path);while($dir = readdir($handler)){if($dir == '.' || $dir == '..'){continue;}if(is_dir($path."/".$dir)){ScanDir($path."/".$dir."/");}else{echo "file: ".$path."/".$dir.PHP_EOL;}}}
}
ScanDir("./");

细心的同学可能发现,我们在表述的过程中,多次使用“层”这个术语。主要有两大原因:

1. 人们在分析递归的过程中,经常使用递归树的形式来分析递归函数的走向。以斐波那契数列为例,首先斐波那契数列的定义为:

因此,为了得到Fab(n)的值,我们常常需要展开为“递归树”的形式,如下图所示:

而递归的计算过程则是从上而下,从左而右,一旦到达递归树的叶子节点(也就是递归的退出条件),便又层层向上返回。如下图所示(引用网址:http:/www.csharpwin.com/csharpspace/12292r4006.shtml):

2. 堆栈的结构。

跟递归有关的另一个重要的概念是栈,借用百度百科中关于栈的解释:“在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。” 在linux系统中,也可以通过ulimit –s命令查看系统的最大栈大小。栈的特点是“后进先出”,也就是最后压入的元素有最高的优先权,每次压入数据时,栈层层向上叠放,而取数据时,则是从栈顶取出需要的数据。正是由于栈的这一特性,使得栈特别适合用于递归。具体来说,在递归程序运行时,系统会分配额定大小的栈空间,每次函数调用的参数、局部变量、函数返回地址(称为一个栈帧)都会被压入到栈空间中(称为“保护现场”,以便在合适的时候“返回现场”),每次该层的递归调用结束后,便无条件(由于无条件,使栈溢出攻击称为可能,可参考(http:/wenku.baidu.com/view/7fb00bc2d5bbfd0a7956737d.html )返回到之前保存的返回地址处继续执行代码。这样层层下来,栈的结构恰似一叠有规律的盘子:

作为递归的基本实例,以下可用于练习:

1. 目录的递归遍历。

2. 无限分类。

3. 二分查找和合并排序。

4. PHP内置的与递归行为有关的函数(如array_merge_recursive,array_walk_recursive,array_replace_recursive等,考虑它们的实现)

理解递归-函数调用的堆栈跟踪

在c语言中,可以通过GDB等调试工具跟踪函数调用的堆栈,从而细致追踪函数的运行过程(关于GDB的使用,推荐@左耳朵耗子之前的博客:http:/blog.csdn.net/haoel/article/details/2879 )。

而在php中,可以使用的调试方法有:

1.原生的print ,echo ,var_dump,print_r等,通常对于较为简单的程序,只需要在函数的 关键点输出即可。

2.Php内置的堆栈跟踪函数:debug_backtrace 和debug_print_backtrace.

3.xdebug 和xhprof等调试工具。

为了方便理解,还是以斐波那契数列为例(这里,我们假设n一定是非负数):

function fab($n){debug_print_backtrace();if($n == 1 || $n == 0){return $n;}             return fab($n - 1) + fab($n - 2);
}
fab(4); 

打印出的斐波那契的调用堆栈是

#0  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(1) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#3  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(0) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#3  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(1) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(1) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(0) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

初看这一堆乱七八糟的输出,似乎毫无头绪。其实对于上述的每一行输出,都包含如下几项内容:

A. 所在的栈层次,如#0表示是栈顶,#1表示第一层栈帧,#2表示第二层栈帧,依次类推,数字越大,表示所在的栈帧深度越大。

B. 调用的函数和参数。如fab(4)表示实际的执行函数是fab函数,4表示函数的实参。

C. 调用的位置:包括文件名和执行的行数。

实际上,我们加上一些额外的输出信息,便可以更加清晰的看到函数的调用堆栈和计算过程,例如:我们加上函数层次的基本信息:

function fab($n){echo “-- n = $n ----------------------------”.PHP_EOL;debug_print_backtrace();if($n == 1 || $n == 0){return $n;}             return fab($n - 1) + fab($n - 2);
}
fab(4);

则执行fab(4)之后的调用堆栈为:

---- n = 4 ---------------------------------------------
#0  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 3 ---------------------------------------------
#0  fab(3) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 2 ---------------------------------------------
#0  fab(2) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(3) called at [/search/nginx/html/test/Fab.php:9]
#2  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 1 ---------------------------------------------
#0  fab(1) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
#2  fab(3) called at [/search/nginx/html/test/Fab.php:9]
#3  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 0 ---------------------------------------------
#0  fab(0) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
#2  fab(3) called at [/search/nginx/html/test/Fab.php:9]
#3  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 1 ---------------------------------------------
#0  fab(1) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(3) called at [/search/nginx/html/test/Fab.php:9]
#2  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 2 ---------------------------------------------
#0  fab(2) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 1 ---------------------------------------------
#0  fab(1) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
#2  fab(4) called at [/search/nginx/html/test/Fab.php:11]
---- n = 0 ---------------------------------------------
#0  fab(0) called at [/search/nginx/html/test/Fab.php:9]
#1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
#2  fab(4) called at [/search/nginx/html/test/Fab.php:11]

对该输出的解释(注意输出的前两列):由于程序需要计算fab(4)的值。而fab(4)的值依赖于fab(3)和fab(2)的值,因而无法直接计算fab(4)的值,需要将其压入栈中,对应下图中的1。fab(4)的左分支为fab(3),而fab(3)的值也无法直接计算,因而需要将fab(3)也压入栈中,对应下图中的2,同理fab(2)也需要压入栈中,直到递归树的叶子节点。计算完叶子节点后,依次退栈,直到栈为空,如下图所示:

性能表现-递归效率分析

  昨天在翻阅朴灵的《深入浅出NODE.js》的时候,看到作者对不同的语言做性能测试时给出的测试结果。大致是:通过简单的斐波那契数列的递归计算,测试不同语言的计算时间,从而大致评估不同语言的计算性能。其中PHP的计算时间让我极为吃惊:在n=40的情况下,PHP计算斐波那契数列的耗时为1m17.728s也就是77.728s,与c语言的0.202s相比,足足差了约380倍!(测试结果可见下图)

我们知道,PHP代码的执行过程是经过扫描代码、词法分析、语法分析等过程,将PHP程序编译成中间代码(Opcode字节码),然后由zend核心引擎负责执行,因而从本质上说,PHP是封装在C语言基础上的一个高级语言实现。这样,由于PHP编译过程并没有做过多的编译优化,加之需要在Zend虚拟机上运行,效率与原生C语言相比,必然要大打折扣,但是,居然会有如此大的差距,还是难免让人匪夷所思。

PHP中递归的效率为何如此低下(其中一个需要知道的是PHP中不支持尾递归优化,这样会导致树形递归的反复迭代和重复计算,因而递归的效率大大下降,能够容忍的递归层次也大大降低。在c/c++中,使用gcc -O2等级以上的编译时,编译会对递归做相应的优化)?在这篇文章(PHP函数的实现原理及性能分析)中,作者的一个解释是:“函数递归是通过堆栈来完成的。在php中,也是利用类似的方法来实现。Zend为每个php函数分配了一个活动符号表(active_sym_table),记录当前函数中所有局部变量的状态。所有的符号表通过堆栈的形式来维护,每当有函数调用的时候,分配一个新的符号表并入栈。
当调用结束后当前符号表出栈。由此实现了状态的保存和递归。 对于栈的维护,zend在这里做了优化。预先分配一个长度为N的静态数组来模拟堆栈,这种通过静态数组来模拟动态数据结构的手法在我们自己的程序中也经常有使用,这种方式避免了每次调用带来的内存分配、销毁。ZEND只是在函数调用结束时将当前栈顶的符号表数据clean掉即可。因为静态数组长度为N,一旦函数调用层次超过N,程序不会出现栈溢出,这种情况下zend就会进行符号表的分配、销毁,因此会导致性能下降很多。在zend里面,N目前取值是32。因此,我们编写php程序的时候,函数调用层次最好不要超过32。

另外,php bug中也有说明:“PHP 4.0 (Zend) uses the stack for intensive data, rather than using the heap. That means that its tolerance recursive functions is significantly

lower than that of other languages ”

SO, 在PHP中,如果不是非常必要,我们建议,最好尽量少使用递归,尤其是在递归层次较大或者无法估算递归的层次时。

由于时间仓促,文中难免有错误,敬请指出,不甚感激。

参考文献:

1.  http://www.csharpwin.com/csharpspace/12292r4006.shtml

2. http:/devzone.zend.com/283/recursion-in-php-tapping-unharnessed-power/

3. http://blog.csdn.net/heiyeshuwu/article/details/5840025

4. http:/www.nowamagic.net/librarys/veda/detail/2336

5. http://www.cnblogs.com/JeffreyZhao/archive/2009/03/26/tail-recursion-and-continuation.html

6. http://wenku.baidu.com/view/7fb00bc2d5bbfd0a7956737d.html

【PHP】php 递归、效率和分析相关推荐

  1. 递归优化php,PHP 递归效率分析

    PHP的递归效率一般认为是低效的.大概一年前,我写了一篇博文,对三种遍历树的方法进行了比较,发现递归算法的效率最低. 而且是差了3倍的效率.所以,PHP中的递归一定要小心的对待. 最近写了一个快速排序 ...

  2. c语言折半查找递归程序,C语言数据结构中二分查找递归非递归实现并分析

    C语言数据结构中二分查找递归非递归实现并分析 前言: 二分查找在有序数列的查找过程中算法复杂度低,并且效率很高.因此较为受我们追捧.其实二分查找算法,是一个很经典的算法.但是呢,又容易写错.因为总是考 ...

  3. 【编译原理笔记05】语法分析:FIRST集和FOLLOW集的计算,[非]递归的预测分析法,预测分析中的错误处理

    本次笔记内容: 4-4 FIRST集和FOLLOW集 4-5 递归的预测分析法 4-6 非递归的预测分析法 4-7 预测分析法中的错误处理 本节课幻灯片,见于我的 GitHub 仓库:第5讲 语法分析 ...

  4. 斐波那契数列使用递归的运行时间分析

    前言 在这学期(大三上)去"蹭"了一次校招面试题,编程题中就有一道关于斐波那契数列的编程问题.如果不选择递归求解,就需要说明原因.当时还没怎么接触算法这方面的知识(笔者非科班),关 ...

  5. php遍历数组哪个效率高,PHP遍历数组的三种方法及效率对比分析

    PHP遍历数组的三种方法及效率对比分析 发布于 2015-03-04 21:55:27 | 129 次阅读 | 评论: 0 | 来源: 网友投递 PHP开源脚本语言PHP(外文名: Hypertext ...

  6. 【星云测试】精准测试的软件产品质量效率变化分析

    2019独角兽企业重金招聘Python工程师标准>>> 精准测试的软件产品质量效率变化分析 伴随着软件规模的扩大和软件快速迭代的双重业务加速要求,软件质量控制的压力也越来越明显.但黑 ...

  7. 转载 foreach比递归效率低

    转载原文于phpchina 刚在phpcms函数文件看到一个将数组转换为序列化的url函数,使用了三层foreach实现,我就很奇怪,为什么不用递归,群里面问了问,有几位朋友说是递归效率差,所以就做了 ...

  8. 【编译原理笔记09】语法制导翻译:语法制导翻译方案,在非递归的预测分析过程中进行翻译

    本次笔记内容: 5-5 语法制导翻译方案 5-6 在非递归的预测分析过程中进行翻译 本节课幻灯片,见于我的 GitHub 仓库:第9讲 语法制导翻译_2 文章目录 语法制导翻译方案 语法制导翻译方案 ...

  9. c语言编程效率的分析,C语言编程效率的分析.pdf

    信息管理 年第 期 青海科技 2006 5 C语言编程效率的分析 卫 良 青海师范大学数学与信息科学系,青海 西宁 ) ( 810008 摘 要:文章结合实例探讨了 语言编程中的执行效率问题,并提出了 ...

最新文章

  1. [sinatra] Just Do It: Learn Sinatra, Part One Darren Jones
  2. Redis 安装详细过程(redis基本使用(服务端和客户端)、修改密码)
  3. Win7下运行VC程序UAC权限问题
  4. HDU - 5592 ZYBs Premutation(线段树,逆序对)
  5. 利用VC检测程序内存溢出(转)
  6. MVC阻止用户注入JavaScript代码或者Html标记
  7. 如何在MySQL中重置AUTO_INCREMENT?
  8. IOS学习笔记-UINavgationController
  9. java统计汉字个数_java统计汉字字数的方法示例
  10. 苹果系统连接服务器打印机,Mac系统怎么连接打印机
  11. ping 命令的用法大全(图文详解)
  12. uni-app APP支付 uni.requestPayment APP微信支付
  13. Linux rar 压缩 解压文件
  14. Java五子棋(人机版),昨天买的棋子今天就用不上了
  15. java jlist 添加滚动条_JList滚动条问题
  16. 如何利用微信答题小程序实现盈利呢
  17. 六、C++离散傅里叶逆变换
  18. wifi mesh组网
  19. 【教程】微信公众号如何添加文档附件,如word、excel、pdf等?
  20. linux qt fscanf,fscanf QT小部件C++

热门文章

  1. 【ESP32_8266_BT篇(三)】GATTATT协议规范
  2. 3D数学基础及坐标系统
  3. Python+班级管理系统 毕业设计-附源码171809
  4. 电信增值业务学习笔记(转)
  5. ESP-IDF的下载,设置,编译,烧录和监控
  6. switch-case的使用
  7. “百度杯”CTF比赛 十一月场 - 敲击
  8. c语言中缺少参数怎么弄,printf参数不足
  9. STP特性(Cisco)
  10. jupyter notebook报错:ModuleNotFoundError: No module named ‘cufflinks‘