php 遞歸、效率和分析
遞歸的定義
遞歸(http:/en.wikipedia.org/wiki/Recursive)是一種函數(shù)調(diào)用自身(直接或間接)的一種機制,這種強大的思想可以把某些復雜的概念變得極為簡單。在計算機科學之外,尤其是在數(shù)學中,遞歸的概念屢見不鮮。例如:最常用于遞歸講解的斐波那契數(shù)列便是一個極為典型的例子,而其他的例如階層(n!)也可以轉(zhuǎn)化為遞歸的定義(n! = n*(n-1)!).即使是在現(xiàn)實生活中,遞歸的思想也是隨處可見:例如,由于學業(yè)問題你需要校長蓋章,然而校長卻說“只有教導主任蓋章了我才會蓋章”,當你找到教導主任,教導主任又說:“只有系主任蓋章了我才會蓋章”...直到你最終找到班主任,在得到班主任豪爽的蓋章之后,你要依次返回到系主任、教導主任、最后得到校長的蓋章,過程如下:

蓋章的故事雖然索然無味(誰的大學生活沒有點悲催的事情呢?不悲催,怎么證明我們年輕過),但卻很好的體現(xiàn)了遞歸的基本思想,也就是遞歸的兩個基本條件:
1. 遞歸的退出條件,這是遞歸能夠正常執(zhí)行的必要條件,也是保證遞歸能夠正確返回的必要條件。如果缺乏這個條件,遞歸就會無限進行下去,直到系統(tǒng)給予的資源耗盡
(在大多數(shù)語言中,都是堆??臻g耗盡),因此,如果你在編程中碰到類似“stack overflow”(C語言中,即棧溢出)和“max nest level of 100 reached”
(php中,超出遞歸限制)等錯誤,多半是沒有正確的退出條件,導致了遞歸深度過大或者無限遞歸。
2. 遞推過程。由一層函數(shù)調(diào)用進入下一層函數(shù)調(diào)用的遞推。以n!為例。在n>1的情況下。N! = N*(N-1)! 便是該遞歸函數(shù)的遞推過程,我們也可以簡單的稱為“遞歸公式”。
有了這兩個基本條件,我們便得到了遞歸的一般模式, 用代碼可以描述為:
function Recur( param ){
if( reach the baseCondition ){
Calu();//計算
return ;
}
//else just do it recursively
param = modify(param)/修改參數(shù),準備進入下層調(diào)用
Recur(param);
}
有了遞歸的一般模式,我們便可以輕松實現(xiàn)大多的遞歸函數(shù)。例如:經(jīng)常提起的斐波那契數(shù)列的遞歸實現(xiàn),再如,目錄的遞歸訪問:
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("./");
細心的同學可能發(fā)現(xiàn),我們在表述的過程中,多次使用“層”這個術(shù)語。主要有兩大原因:
- 人們在分析遞歸的過程中,經(jīng)常使用遞歸樹的形式來分析遞歸函數(shù)的走向。以斐波那契數(shù)列為例,首先斐波那契數(shù)列的定義為:

因此,為了得到Fab(n)的值,我們常常需要展開為“遞歸樹”的形式,如下圖所示:


而遞歸的計算過程則是從上而下,從左而右,一旦到達遞歸樹的葉子節(jié)點(也就是遞歸的退出條件),便又層層向上返回。如下圖所示
引用網(wǎng)址:http:/www.csharpwin.com/csharpspace/12292r4006.shtml
- 堆棧的結(jié)構(gòu)。
跟遞歸有關(guān)的另一個重要的概念是棧,借用百度百科中關(guān)于棧的解釋:“在Windows下,棧是向低地址擴展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存的區(qū)域。這句話的意思是棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預先規(guī)定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數(shù)),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小?!?在linux系統(tǒng)中,也可以通過ulimit –s命令查看系統(tǒng)的最大棧大小。棧的特點是“后進先出”,也就是最后壓入的元素有最高的優(yōu)先權(quán),每次壓入數(shù)據(jù)時,棧層層向上疊放,而取數(shù)據(jù)時,則是從棧頂取出需要的數(shù)據(jù)。正是由于棧的這一特性,使得棧特別適合用于遞歸。具體來說,在遞歸程序運行時,系統(tǒng)會分配額定大小的??臻g,每次函數(shù)調(diào)用的參數(shù)、局部變量、函數(shù)返回地址(稱為一個棧幀)都會被壓入到??臻g中(稱為“保護現(xiàn)場”,以便在合適的時候“返回現(xiàn)場”),每次該層的遞歸調(diào)用結(jié)束后,便無條件(由于無條件,使棧溢出攻擊稱為可能,可參考:http:/wenku.baidu.com/view/7fb00bc2d5bbfd0a7956737d.html 返回到之前保存的返回地址處繼續(xù)執(zhí)行代碼。這樣層層下來,棧的結(jié)構(gòu)恰似一疊有規(guī)律的盤子:

作為遞歸的基本實例,以下可用于練習:
- 目錄的遞歸遍歷。
- 無限分類。
- 二分查找和合并排序。
- PHP內(nèi)置的與遞歸行為有關(guān)的函數(shù)(如array_merge_recursive,array_walk_recursive,array_replace_recursive等,考慮它們的實現(xiàn))
理解遞歸-函數(shù)調(diào)用的堆棧跟蹤
在c語言中,可以通過GDB等調(diào)試工具跟蹤函數(shù)調(diào)用的堆棧,從而細致追蹤函數(shù)的運行過程(關(guān)于GDB的使用,推薦皓哥(@左耳朵耗子)之前的博客:http:/blog.csdn.net/haoel/article/details/2879 現(xiàn)在,皓哥的博客已經(jīng)遷移至
coolshell.cn)。
而在php中,可以使用的調(diào)試方法有:
1.原生的print ,echo ,var_dump,print_r等,通常對于較為簡單的程序,只需要在函數(shù)的 關(guān)鍵點輸出即可。
2.Php內(nèi)置的堆棧跟蹤函數(shù):debug_backtrace 和debug_print_backtrace.
3.xdebug 和xhprof等調(diào)試工具。
為了方便理解,還是以斐波那契數(shù)列為例(這里,我們假設(shè)n一定是非負數(shù)):
function fab($n){
debug_print_backtrace();
if($n == 1 || $n == 0){
return $n;
}
return fab($n - 1) + fab($n - 2);
}
fab(4);
打印出的斐波那契的調(diào)用堆棧是
#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]
初看這一堆亂七八糟的輸出,似乎毫無頭緒。其實對于上述的每一行輸出,都包含如下幾項內(nèi)容:
A. 所在的棧層次,如#0表示是棧頂,#1表示第一層棧幀,#2表示第二層棧幀,依次類推,數(shù)字越大,表示所在的棧幀深度越大。
B. 調(diào)用的函數(shù)和參數(shù)。如fab(4)表示實際的執(zhí)行函數(shù)是fab函數(shù),4表示函數(shù)的實參。
C. 調(diào)用的位置:包括文件名和執(zhí)行的行數(shù)。
實際上,我們加上一些額外的輸出信息,便可以更加清晰的看到函數(shù)的調(diào)用堆棧和計算過程,例如:我們加上函數(shù)層次的基本信息:
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);
則執(zhí)行fab(4)之后的調(diào)用堆棧為:
---- 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)的值,需要將其壓入棧中,對應(yīng)下圖中的1。fab(4)的左分支為fab(3),而fab(3)的值也無法直接計算,因而需要將fab(3)也壓入棧中,對應(yīng)下圖中的2,同理fab(2)也需要壓入棧中,直到遞歸樹的葉子節(jié)點。計算完葉子節(jié)點后,依次退棧,直到棧為空,如下圖所示:
性能表現(xiàn)-遞歸效率分析
昨天在翻閱樸靈的《深入淺出NODE.js》的時候,看到作者對不同的語言做性能測試時給出的測試結(jié)果。大致是:通過簡單的斐波那契數(shù)列的遞歸計算,測試不同語言的計算時間,從而大致評估不同語言的計算性能。其中PHP的計算時間讓我極為吃驚:在n=40的情況下,PHP計算斐波那契數(shù)列的耗時為1m17.728s也就是77.728s,與c語言的0.202s相比,足足差了約380倍?。y試結(jié)果可見下圖)

我們知道,PHP代碼的執(zhí)行過程是經(jīng)過掃描代碼、詞法分析、語法分析等過程,將PHP程序編譯成中間代碼(Opcode字節(jié)碼),然后由zend核心引擎負責執(zhí)行,因而從本質(zhì)上說,PHP是封裝在C語言基礎(chǔ)上的一個高級語言實現(xiàn)。這樣,由于PHP編譯過程并沒有做過多的編譯優(yōu)化,加之需要在Zend虛擬機上運行,效率與原生C語言相比,必然要大打折扣,但是,居然會有如此大的差距,還是難免讓人匪夷所思。
PHP中遞歸的效率為何如此低下(其中一個需要知道的是PHP中不支持尾遞歸優(yōu)化,這樣會導致樹形遞歸的反復迭代和重復計算,因而遞歸的效率大大下降,能夠容忍的遞歸層次也大大降低。在c/c++中,使用gcc -O2等級以上的編譯時,編譯會對遞歸做相應(yīng)的優(yōu)化)?在這篇文章(PHP函數(shù)的實現(xiàn)原理及性能分析)中,作者的一個解釋是:“函數(shù)遞歸是通過堆棧來完成的。在php中,也是利用類似的方法來實現(xiàn)。Zend為每個php函數(shù)分配了一個活動符號表(active_sym_table),記錄當前函數(shù)中所有局部變量的狀態(tài)。所有的符號表通過堆棧的形式來維護,每當有函數(shù)調(diào)用的時候,分配一個新的符號表并入棧。當調(diào)用結(jié)束后當前符號表出棧。由此實現(xiàn)了狀態(tài)的保存和遞歸。 對于棧的維護,zend在這里做了優(yōu)化。預先分配一個長度為N的靜態(tài)數(shù)組來模擬堆棧,這種通過靜態(tài)數(shù)組來模擬動態(tài)數(shù)據(jù)結(jié)構(gòu)的手法在我們自己的程序中也經(jīng)常有使用,這種方式避免了每次調(diào)用帶來的內(nèi)存分配、銷毀。ZEND只是在函數(shù)調(diào)用結(jié)束時將當前棧頂?shù)姆柋頂?shù)據(jù)clean掉即可。因為靜態(tài)數(shù)組長度為N,一旦函數(shù)調(diào)用層次超過N,程序不會出現(xiàn)棧溢出,這種情況下zend就會進行符號表的分配、銷毀,因此會導致性能下降很多。在zend里面,N目前取值是32。因此,我們編寫php程序的時候,函數(shù)調(diào)用層次最好不要超過32?!?/p>
原文作鏈接:http://www.xuebuyuan.com/2021725.html
參考文獻:
- http://www.csharpwin.com/csharpspace/12292r4006.shtml
- http:/devzone.zend.com/283/recursion-in-php-tapping-unharnessed-power/
- http://blog.csdn.net/heiyeshuwu/article/details/5840025
- http:/www.nowamagic.net/librarys/veda/detail/2336
- http://www.cnblogs.com/JeffreyZhao/archive/2009/03/26/tail-recursion-and-continuation.html
- http://wenku.baidu.com/view/7fb00bc2d5bbfd0a7956737d.html