递归的定义

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

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

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

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

  1. function Recur(  param ){
  2. if(  reach the baseCondition ){
  3. Calu();//计算
  4. return ;
  5. }
  6. //else just do it recursively
  7. param = modify(param)/修改参数,准备进入下层调用
  8. Recur(param);
  9. }

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

  1. function ScanDir($path){
  2. if(is_dir($path)){
  3. $handler = opendir($path);
  4. while($dir = readdir($handler)){
  5. if($dir == '.' || $dir == '..'){
  6. continue;
  7. }
  8. if(is_dir($path."/".$dir)){
  9. ScanDir($path."/".$dir."/");
  10. }else{
  11. echo "file: ".$path."/".$dir.PHP_EOL;
  12. }
  13. }
  14. }
  15. }
  16. 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一定是非负数):

  1. function fab($n){
  2. debug_print_backtrace();
  3. if($n == 1 || $n == 0){
  4. return $n;
  5. }
  6. return fab($n - 1) + fab($n - 2);
  7. }
  8. 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. 调用的位置:包括文件名和执行的行数。

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

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

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

  1. ---- n = 4 ---------------------------------------------
  2. #0  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  3. ---- n = 3 ---------------------------------------------
  4. #0  fab(3) called at [/search/nginx/html/test/Fab.php:9]
  5. #1  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  6. ---- n = 2 ---------------------------------------------
  7. #0  fab(2) called at [/search/nginx/html/test/Fab.php:9]
  8. #1  fab(3) called at [/search/nginx/html/test/Fab.php:9]
  9. #2  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  10. ---- n = 1 ---------------------------------------------
  11. #0  fab(1) called at [/search/nginx/html/test/Fab.php:9]
  12. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
  13. #2  fab(3) called at [/search/nginx/html/test/Fab.php:9]
  14. #3  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  15. ---- n = 0 ---------------------------------------------
  16. #0  fab(0) called at [/search/nginx/html/test/Fab.php:9]
  17. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
  18. #2  fab(3) called at [/search/nginx/html/test/Fab.php:9]
  19. #3  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  20. ---- n = 1 ---------------------------------------------
  21. #0  fab(1) called at [/search/nginx/html/test/Fab.php:9]
  22. #1  fab(3) called at [/search/nginx/html/test/Fab.php:9]
  23. #2  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  24. ---- n = 2 ---------------------------------------------
  25. #0  fab(2) called at [/search/nginx/html/test/Fab.php:9]
  26. #1  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  27. ---- n = 1 ---------------------------------------------
  28. #0  fab(1) called at [/search/nginx/html/test/Fab.php:9]
  29. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
  30. #2  fab(4) called at [/search/nginx/html/test/Fab.php:11]
  31. ---- n = 0 ---------------------------------------------
  32. #0  fab(0) called at [/search/nginx/html/test/Fab.php:9]
  33. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]
  34. #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中,如果不是非常必要,我们建议,最好尽量少使用递归,尤其是在递归层次较大或者无法估算递归的层次时。

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

【PHP】php 递归、效率和分析(转)的更多相关文章

  1. 2014年第五届蓝桥杯国赛 Log大侠(区间合并+Java递归效率分析)

    1678: Log大侠 java 时间限制: 2 Sec  内存限制: 256 MB提交: 20  解决: 1 题目描述     atm参加了速算训练班,经过刻苦修炼,对以2为底的对数算得飞快,人称L ...

  2. SQL语句执行效率及分析(note)

    1.关于SQL查询效率,100w数据,查询只要1秒,与您分享: 机器情况p4: 2.4内存: 1 Gos: windows 2003数据库: ms sql server 2000目的: 查询性能测试, ...

  3. 字符串数组初始化0 与memset 0 效率的分析

    转自:http://www.xuebuyuan.com/1722207.html 结合http://blog.sina.com.cn/s/blog_59d470310100gov8.html来看. 最 ...

  4. Solrj和Solr DIH索引效率对比分析

    测试软件环境: 1.16G windows7 x64  32core cpu . 2.jdk 1.7  tomcat 6.x  solr 4.8 数据库软件环境: 1.16G windows7 x64 ...

  5. 基于OpenMP的矩阵乘法实现及效率提升分析

    一.  矩阵乘法串行实现 例子选择两个1024*1024的矩阵相乘,根据矩阵乘法运算得到运算结果.其中,两个矩阵中的数为double类型,初值由随机数函数产生.代码如下: #include <i ...

  6. SQL语句执行效率及分析

    查询效率分析:子查询为确保消除重复值,必须为外部查询的每个结果都处理嵌套查询.在这种情况下可以考虑用联接查询来取代.如果要用子查询,那就用EXISTS替代IN.用NOT EXISTS替代NOT IN. ...

  7. memoization提升递归效率

    从开通博客到目前为止,也有一年了,刚开始的写了一篇工作的感想,然后就一直不知道写什么,看园子里的文章实在是很专业,怕自己写的太水.但是,写一些东西总归是好的,于是就当作是记笔记一样,开始写第一篇技术类 ...

  8. 提升JavaScript递归效率:Memoization技术详解[转载]

    递归是拖慢脚本运行速度的大敌之一,太多的递归会让浏览器变得越来越慢直到死掉或者莫名其妙的突然自动退出.这里我们可以通过memoization技术来替代函数中太多的递归调用,提升JavaScript效率 ...

  9. HashMap与HashTable源码学习及效率比较分析

    一.个人学习后的见解: 首先表明学习源码后的个人见解,后续一次依次进行分析: 1.线程安全:HashMap是非线程安全的,HashTable是线程安全的(HashTable中使用了synchroniz ...

随机推荐

  1. android-LinearLayout 控件占满父容器位置实现

    经常碰到需要把一个控件放在手机底部的情况,以前都是在LinearLayout尝试使用gravity="bottom" ,但是,没有效果,后来在网上查到了方法,如下 <Line ...

  2. js面向对象3-继承

    一.了解继承  首先我们一起了解下js中继承,其实继承就是后辈继承前辈的属性和方法. 二.继承的方法 从父类继承属性和方法 这是对象冒充的方法,模仿java的继承方法.实现的原理是,通过改变父类的执行 ...

  3. 一个理性战胜感性的成功案例:P2P投资和活期理财,纠结中提炼出来的1个数学问题

    我经常是投资了P2P,然后用钱,因而损失了一部分收益. 这是一个让我纠结的问题,为了解决这个问题,我不再凭感觉,而是从现实情况,提炼出来1个数学题,解答我的疑惑. 这是一个理性战胜感性的成功案例~ P ...

  4. Unity实现发送QQ邮件功能

    闲来无聊,用Unity简单实现了一个发送邮件的功能,希望与大家互相交流互相进步,大神勿喷,测试的是QQ邮件用到的是MailMessage类和SmtpClient类首先如果发送方使用的是个人QQ邮箱账号 ...

  5. 【Codeforces Round #451 (Div. 2) A】Rounding

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 模拟 [代码] /* 1.Shoud it use long long ? 2.Have you ever test several ...

  6. Spring CORS

    转载:Spring MVC 4.2 增加 CORS 支持 http://spring.io/blog/2015/06/08/cors-support-in-spring-framework http: ...

  7. C++基于矢量图形库cairo画图图形

    //sudo apt-get install libcairo2-dev //pkg-config --cflags --libs cairo //-I/usr/include/cairo -I/us ...

  8. 终于研究出如何设置新版paypal付款时汇率损失方的问题了

    http://bbs.55haitao.com/thread-1686005-1-1.html 终于研究出如何设置新版paypal付款时汇率损失方的问题了 登录paypal后,选"设置&qu ...

  9. Android学习笔记之网络接口(Http接口,Apache接口,Android接口)

    目前Android平台有三种网络接口可以使用,他们分别是:Java.NET.*(标准Java接口),org.apache(Apache接口),和android.Net.*(android网络接口). ...

  10. 让ie6 7 8 9支持原生html5 websocket

      让ie6 7 8 9支持原生html5 websocket   从github上的 web-socket-js(socket.io好像也是用这个做的他们的flash替代传输方式)改过来的.不过值得 ...