php遞歸堆棧簡介

php 遞歸、效率和分析

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

蓋章的故事雖然索然無味(誰的大學(xué)生活沒有點(diǎn)悲催的事情呢?不悲催,怎么證明我們年輕過),但卻很好的體現(xiàn)了遞歸的基本思想,也就是遞歸的兩個(gè)基本條件:
  1. 遞歸的退出條件,這是遞歸能夠正常執(zhí)行的必要條件,也是保證遞歸能夠正確返回的必要條件。如果缺乏這個(gè)條件,遞歸就會(huì)無限進(jìn)行下去,直到系統(tǒng)給予的資源耗盡
(在大多數(shù)語言中,都是堆棧空間耗盡),因此,如果你在編程中碰到類似“stack overflow”(C語言中,即棧溢出)和“max nest level of 100 reached”
(php中,超出遞歸限制)等錯(cuò)誤,多半是沒有正確的退出條件,導(dǎo)致了遞歸深度過大或者無限遞歸。
  2. 遞推過程。由一層函數(shù)調(diào)用進(jìn)入下一層函數(shù)調(diào)用的遞推。以n!為例。在n>1的情況下。N! = N*(N-1)! 便是該遞歸函數(shù)的遞推過程,我們也可以簡單的稱為“遞歸公式”。
有了這兩個(gè)基本條件,我們便得到了遞歸的一般模式, 用代碼可以描述為:

function Recur(  param ){
    if(  reach the baseCondition ){
        Calu();//計(jì)算
        return ;
    }
    //else just do it recursively
    param = modify(param)/修改參數(shù),準(zhǔn)備進(jìn)入下層調(diào)用
    Recur(param);
}

有了遞歸的一般模式,我們便可以輕松實(shí)現(xiàn)大多的遞歸函數(shù)。例如:經(jīng)常提起的斐波那契數(shù)列的遞歸實(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("./");

細(xì)心的同學(xué)可能發(fā)現(xiàn),我們在表述的過程中,多次使用“層”這個(gè)術(shù)語。主要有兩大原因:

  1. 人們在分析遞歸的過程中,經(jīng)常使用遞歸樹的形式來分析遞歸函數(shù)的走向。以斐波那契數(shù)列為例,首先斐波那契數(shù)列的定義為:

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


而遞歸的計(jì)算過程則是從上而下,從左而右,一旦到達(dá)遞歸樹的葉子節(jié)點(diǎn)(也就是遞歸的退出條件),便又層層向上返回。如下圖所示
引用網(wǎng)址:http:/www.csharpwin.com/csharpspace/12292r4006.shtml

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

作為遞歸的基本實(shí)例,以下可用于練習(xí):

  1. 目錄的遞歸遍歷。
  2. 無限分類。
  3. 二分查找和合并排序。
  4. PHP內(nèi)置的與遞歸行為有關(guān)的函數(shù)(如array_merge_recursive,array_walk_recursive,array_replace_recursive等,考慮它們的實(shí)現(xiàn))

理解遞歸-函數(shù)調(diào)用的堆棧跟蹤

在c語言中,可以通過GDB等調(diào)試工具跟蹤函數(shù)調(diào)用的堆棧,從而細(xì)致追蹤函數(shù)的運(yùn)行過程(關(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)鍵點(diǎn)輸出即可。
2.Php內(nèi)置的堆棧跟蹤函數(shù):debug_backtrace 和debug_print_backtrace.
3.xdebug 和xhprof等調(diào)試工具。
為了方便理解,還是以斐波那契數(shù)列為例(這里,我們假設(shè)n一定是非負(fù)數(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]

初看這一堆亂七八糟的輸出,似乎毫無頭緒。其實(shí)對于上述的每一行輸出,都包含如下幾項(xiàng)內(nèi)容:
A. 所在的棧層次,如#0表示是棧頂,#1表示第一層棧幀,#2表示第二層棧幀,依次類推,數(shù)字越大,表示所在的棧幀深度越大。
B. 調(diào)用的函數(shù)和參數(shù)。如fab(4)表示實(shí)際的執(zhí)行函數(shù)是fab函數(shù),4表示函數(shù)的實(shí)參。
C. 調(diào)用的位置:包括文件名和執(zhí)行的行數(shù)。
實(shí)際上,我們加上一些額外的輸出信息,便可以更加清晰的看到函數(shù)的調(diào)用堆棧和計(jì)算過程,例如:我們加上函數(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]

對該輸出的解釋(注意輸出的前兩列):由于程序需要計(jì)算fab(4)的值。而fab(4)的值依賴于fab(3)和fab(2)的值,因而無法直接計(jì)算fab(4)的值,需要將其壓入棧中,對應(yīng)下圖中的1。fab(4)的左分支為fab(3),而fab(3)的值也無法直接計(jì)算,因而需要將fab(3)也壓入棧中,對應(yīng)下圖中的2,同理fab(2)也需要壓入棧中,直到遞歸樹的葉子節(jié)點(diǎn)。計(jì)算完葉子節(jié)點(diǎn)后,依次退棧,直到棧為空,如下圖所示:

性能表現(xiàn)-遞歸效率分析
  昨天在翻閱樸靈的《深入淺出NODE.js》的時(shí)候,看到作者對不同的語言做性能測試時(shí)給出的測試結(jié)果。大致是:通過簡單的斐波那契數(shù)列的遞歸計(jì)算,測試不同語言的計(jì)算時(shí)間,從而大致評估不同語言的計(jì)算性能。其中PHP的計(jì)算時(shí)間讓我極為吃驚:在n=40的情況下,PHP計(jì)算斐波那契數(shù)列的耗時(shí)為1m17.728s也就是77.728s,與c語言的0.202s相比,足足差了約380倍!(測試結(jié)果可見下圖)


我們知道,PHP代碼的執(zhí)行過程是經(jīng)過掃描代碼、詞法分析、語法分析等過程,將PHP程序編譯成中間代碼(Opcode字節(jié)碼),然后由zend核心引擎負(fù)責(zé)執(zhí)行,因而從本質(zhì)上說,PHP是封裝在C語言基礎(chǔ)上的一個(gè)高級語言實(shí)現(xiàn)。這樣,由于PHP編譯過程并沒有做過多的編譯優(yōu)化,加之需要在Zend虛擬機(jī)上運(yùn)行,效率與原生C語言相比,必然要大打折扣,但是,居然會(huì)有如此大的差距,還是難免讓人匪夷所思。

PHP中遞歸的效率為何如此低下(其中一個(gè)需要知道的是PHP中不支持尾遞歸優(yōu)化,這樣會(huì)導(dǎo)致樹形遞歸的反復(fù)迭代和重復(fù)計(jì)算,因而遞歸的效率大大下降,能夠容忍的遞歸層次也大大降低。在c/c++中,使用gcc -O2等級以上的編譯時(shí),編譯會(huì)對遞歸做相應(yīng)的優(yōu)化)?在這篇文章(PHP函數(shù)的實(shí)現(xiàn)原理及性能分析)中,作者的一個(gè)解釋是:“函數(shù)遞歸是通過堆棧來完成的。在php中,也是利用類似的方法來實(shí)現(xiàn)。Zend為每個(gè)php函數(shù)分配了一個(gè)活動(dòng)符號表(active_sym_table),記錄當(dāng)前函數(shù)中所有局部變量的狀態(tài)。所有的符號表通過堆棧的形式來維護(hù),每當(dāng)有函數(shù)調(diào)用的時(shí)候,分配一個(gè)新的符號表并入棧。當(dāng)調(diào)用結(jié)束后當(dāng)前符號表出棧。由此實(shí)現(xiàn)了狀態(tài)的保存和遞歸。 對于棧的維護(hù),zend在這里做了優(yōu)化。預(yù)先分配一個(gè)長度為N的靜態(tài)數(shù)組來模擬堆棧,這種通過靜態(tài)數(shù)組來模擬動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu)的手法在我們自己的程序中也經(jīng)常有使用,這種方式避免了每次調(diào)用帶來的內(nèi)存分配、銷毀。ZEND只是在函數(shù)調(diào)用結(jié)束時(shí)將當(dāng)前棧頂?shù)姆柋頂?shù)據(jù)clean掉即可。因?yàn)殪o態(tài)數(shù)組長度為N,一旦函數(shù)調(diào)用層次超過N,程序不會(huì)出現(xiàn)棧溢出,這種情況下zend就會(huì)進(jìn)行符號表的分配、銷毀,因此會(huì)導(dǎo)致性能下降很多。在zend里面,N目前取值是32。因此,我們編寫php程序的時(shí)候,函數(shù)調(diào)用層次最好不要超過32。”

原文作鏈接:http://www.xuebuyuan.com/2021725.html
參考文獻(xiàn):

  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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容