【PHP】php 递归、效率和分析(转)

递归的定义

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

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

[plain] view plaincopyprint?

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

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

[php] view plaincopyprint?

  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. }

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

[php] view plaincopyprint?

  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一定是非负数):

[php] view plaincopyprint?

  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. 调用的位置:包括文件名和执行的行数。

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

[php] view plaincopyprint?

  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)之后的调用堆栈为:

[plain] view plaincopyprint?

  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中,如果不是非常必要,我们建议,最好尽量少使用递归,尤其是在递归层次较大或者无法估算递归的层次时。

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

时间: 2024-10-19 10:14:50

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

递归调用的分析

摘自 编程之美 编程之美的第3.8题求二叉树中节点的最大距离中,在总结的时候,作者讲了一个分析递归问题的体会: 1.首先弄清楚递归的顺序. 在递归的实现中,往往需要假设后续的调用已经完成,在此基础之上,才实现递归的逻辑. 2.分析清楚递归体的逻辑然后写出来. 3.考虑清楚递归退出的边界条件.也就是说,那些地方应该写return. 递归调用的分析

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

一.  矩阵乘法串行实现 例子选择两个1024*1024的矩阵相乘,根据矩阵乘法运算得到运算结果.其中,两个矩阵中的数为double类型,初值由随机数函数产生.代码如下: #include <iostream> #include <omp.h> // OpenMP编程需要包含的头文件 #include <time.h> #include <stdlib.h> using namespace std; #define MatrixOrder 1024 #def

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

1.关于SQL查询效率,100w数据,查询只要1秒,与您分享: 机器情况p4: 2.4内存: 1 Gos: windows 2003数据库: ms sql server 2000目的: 查询性能测试,比较两种查询的性能 SQL查询效率 step by step -- setp 1.-- 建表create table t_userinfo(userid int identity(1,1) primary key nonclustered,nick varchar(50) not null defa

多线程程序跑久了效率下降分析

最近在写一个搜索引擎,有个中间程序是分析分词结果文件,建立倒排索引.最初写的是单线程的,效率低到无语,于是又改成多线程的了.本以为万事大吉,可是在分析了将近2000文件的时候,效率低的和单线程的没什么区别了.打开任务管理器,线程数显示3(我设置的子线程数量最高为15,加上启动就有的,程序刚运行的时候线程数可以达到20个). 百度了下,Windows单个程序的线程数是有上限的,一般只能开到2000个左右.而我的程序中为了方便,把每个子线程都设置为detach状态了.这个状态下,线程结束时其他线程并

谨慎地使用递归之斐波那契递归实现的分析

[斐波那契函数的定义] 斐波那契数列,又称黄金分割数列,指的是这样一个数列:1.1.2.3.5.8.13.21.--在数学上,斐波纳契数列以如下被以递归的方法定义:F0=1,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*). [用递归求解斐波那契函数的弊端] 斐波那契函数用递归实现如下面的代码: 计算斐波那契的自然递归程序效率时很低的,上面的程序虽然写法简单,看上去时递归的聪明的使用,但其实效率是及其低下的.特别是当n=40以后,效率衰减的特别明显.为了计算 fib( n ) ,

算法效率的分析--【以选择排序与冒泡排序为基础】

在前面我们实现了选择排序与冒泡排序的具体实现,现在我们从数学的角度分析下算法的效率问题: 首先我们把两种排序算法的概念重温一遍: 选择排序:在每一轮中,找出最小的元素放在他最终的位置.挺起来有些别扭,我们举个实例: eg:        8, 5,1, 20, 6, 3 1ed······  1, 5,8, 20, 6, 3 2ed······  1, 3, 8,20, 6,5 3ed······  1, 3,5, 20, 6, 8 4ed······  1, 3,  5, 6,  20,8 5

递归调用实例分析2.在函数中间的递归与在函数尾部的递归

上一篇最后给出了用递归完成字符串逆置的代码,但是没有分析它的具体算法,今天做了如'abcde'字符串递归翻转的图跟大家分享(画的比较烂,具体思路还是有的,详情见附件) 这里的递归调用没有出现在函数末尾,二前面几个递归都出现在函数末尾,所以说递归可以分为在函数末尾的递归与在函数中的递归.其差别如下. 1.在函数尾部的递归,都可以用循环的方式做下去,这样的递归大多数情况只能造成代码的简洁,并不利于机器的运算. 2.在函数中间的递归,不一定能用循环的方式做下去,这样的递归有的可以转化成在函数末尾的递归

Solrj API索引效率对比分析

测试软件环境: 1.16G windows7 x64  32core cpu . 2.jdk 1.7  tomcat 6.x  solr 4.8 数据库软件环境: 1.16G windows7 x64  32core cpu . 2.Oracle 11g 一.Solr默认索引工具DIH. 使用Solr DIH索引数据,一千九百万数据,耗时45分钟左右,每秒钟6500条/s,合计39w条没分钟. 相关jvm最大堆内存为4G,solr index config使用默认参数. Solr DIH 导入截

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

1.关于SQL查询效率,100w数据,查询只要1秒,与您分享: 机器情况p4: 2.4内存: 1 Gos: windows 2003数据库: ms sql server 2000目的: 查询性能测试,比较两种查询的性能 SQL查询效率 step by step -- setp 1.-- 建表create table t_userinfo(userid int identity(1,1) primary key nonclustered,nick varchar(50) not null defa