php递归堆栈简介

php 递归、效率和分析

递归的定义
递归(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 recursively
    param = 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

  1. 堆栈的结构。
    跟递归有关的另一个重要的概念是栈,借用百度百科中关于栈的解释:“在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 现在,皓哥的博客已经迁移至
coolshell.cn)。
而在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。”

原文作链接:http://www.xuebuyuan.com/2021725.html
参考文献:

  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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容