1. 程式人生 > >遞迴替換演算法之尾遞迴

遞迴替換演算法之尾遞迴

遞迴在很多時候被視為洪水猛獸。它的名聲狼籍,好像永遠和低效聯絡在一起。 
其實,對一些如樹的遞迴結構,遞迴演算法是又自然又好用。 
如果看看一些用來代替遞迴的技術,(漢諾塔的迭代演算法不去說它,那是真正的演算法的革命,除了佩服沒啥好說的),一般來說只不過是自己模擬堆疊,編起來費勁,讀起來費勁,維護起來更費勁。而模擬堆疊的效果,相比於簡單的遞迴,好處在哪裡呢? 
1。不使用程序堆疊,不會耗盡堆疊空間(雖然可能耗盡堆空間) 
2。可以有選擇地把真正有用的東西壓棧,而不用什麼pc, sp, 所有的區域性變數都壓棧。這樣節省了一些記憶體(不過,仍然在一個數量級,遞迴是O(N),模擬遞迴仍然是O(N))。 不過這也不絕對,編譯器的優化可以識別一些不需要儲存的區域性變數的。(這叫變數生存期分析) 


那麼這樣做又沒有壞處呢?除了上面的程式碼複雜度的問題(我想很多搞嵌入式系統,實時系統的C高手都對此不屑一顧),還有一個效率上的缺點: 
模擬的遞迴使用堆空間,它的new/delete都比直接在堆疊上分配空間慢得多。而且很容易產生記憶體碎片。 
所以說,模擬堆疊只不過是犧牲了一定的效率來換取了一部分空間而已。是否值得,嘿嘿,就得看具體應用了。 

好,閒話說完,下面言歸正傳。 

話說大家都知道函式呼叫要壓棧。不過這是有幾個例外的。 
1。函式是inline的。 
2。語言採用的函式呼叫方式是continuation, 而不是activation record. 這種模式中語言可以使用堆而不是棧來完成函式呼叫。 
3。尾呼叫。也就是說,函式調用出現在呼叫者函式的尾部,因為是尾部,所以根本沒有必要去儲存任何區域性變數,sp, pc的資訊。直接讓被呼叫的函式返回時越過呼叫者,返回到呼叫者的呼叫者去。 
比如: 
f(){g();} g(){h();}


這裡,h()函式返回時可以繞過g()函式,f()函式而直接返回到f()函式的呼叫者。 
注意,尾呼叫優化不是什麼很複雜的優化,實際上幾乎所有的現代的高階語言編譯器都支援尾呼叫這個很基本的優化。 
實現層面上,只需要把彙編程式碼call改成jmp, 並放棄所有區域性變數壓棧處理,就可以了。所以,很簡單。 
我們不考慮continuation這種情況,因為c/c++/java等流行的語言都不是這種模式。 
對於遞迴,inline也不能使用。因為你不知道你會遞迴呼叫多少次。 
於是,就剩下遞三條:尾呼叫。而一個對自己本身的遞迴尾呼叫,就叫做尾遞迴。 
那麼,當尾遞迴時,我們就沒有前面分析的遞迴呼叫的佔用堆疊的缺點,因為每次呼叫都是尾呼叫,所以堆疊根本就沒有被佔用,每次呼叫都是重新使用呼叫者的堆疊。 
有些看過ml, haskell這種functional language的程式設計師可能會奇怪為什麼他們不支援迴圈。 
現在讓我們看看他們會怎麼實現迴圈的功能。 


考慮一個簡單的例子,求從一加到一百的和。 

用遞迴, 我們也許可以這樣做: