在計算機中,程式執行中的函式呼叫是借助棧實現的:每當進入乙個新的函式呼叫,棧就會增加一層棧幀,每當函式返回,棧就會減少一層棧幀。這個棧的大小是有限的(貌似是1m或者2m)。所以在執行遞迴的過程中遞迴的次數是有限度的,超過某個不是很大的值就會爆棧(棧溢位)。
以求解fabonacci問題為例:
使用遞迴的方式實現fabonacci問題的**如下:
1 #include 2 #include 3 #include 4在**中加入了計時函式,記錄遞迴所用的時間。using
namespace
std;
56 unsigned long
long fabonacci( int
n )7
1314
intmain()
15
當輸入n=46是程式輸出:
計算的結果為1836311903,整個遞迴用時12.04s.佔整個execute time的絕大部分。
這個求解過程大致如此:(以求fabonacci(6)為例)
f(6)這個求解的過程更像是對一棵以子函式構成的一棵樹的後序遍歷。向下遞迴,向上返回。=f(5) + f(4)
=(f(4) + f(3)) + f(4)
=((f(3) + f(2)) + f(3)) + f(4)
=(((f(2) + f(1)) + f(2)) + f(3)) + f(4)
=(((1 + 1) + 1) + f(3)) + f(4)
=(3 +(f(2) + f(1)) + f(4)
=(3 +(1 +1)+f(4)
=5 +(f(3) + f(2))
=5 +((f(2) + f(1)) + f(2))
=5 +(( 1 +1) +1)
=5 +3
=5
這樣的話,求解的fabonacci數每增加1,需要遍歷的樹的層數就會增加一層。這是乙個指數函式的複雜度增長(貌似沒那麼誇張,留坑,待研究)。
總之,這種方法對於n>50的情況是很難快速的到解的。
而如果使用尾遞迴,則會大大地避免這種情況。
「在電腦科學裡,尾呼叫是指乙個函式裡的最後乙個動作是乙個函式呼叫的情形:即這個呼叫的返回值直接被當前函式返回的情形。這種情形下稱該呼叫位置為尾位置。若這個函式在尾位置呼叫本身(或是乙個尾呼叫本身的其他函式等等),則稱這種情況為尾遞迴,是遞迴的一種特殊情形。」
尾呼叫的重要性在於它可以不在呼叫棧上面新增乙個新的堆疊幀——而是更新它,如同迭代一般。尾遞迴因而具有兩個特徵:
呼叫自身函式(self-called);
計算僅占用常量棧空間(stack space)。
「形式上只要是最後乙個return
語句返回的是乙個完整函式,它就是尾遞迴」
以上來自維基百科對尾呼叫(遞迴)的定義。
因為尾遞迴是當前函式最後乙個動作,所以當前函式幀上的區域性變數(全域性變數儲存在堆中)等大部分的東西都不需要了儲存了,所以當前的函式幀經過適當的更動以後可以直接當作被尾呼叫的函式的幀使用。因此整個過程只要使用乙個棧幀,在函式棧中不用新開闢棧空間。省去了向上返回->計算所用的時間,這樣的話整個的計算過程就變成了線性的時間複雜度。
那麼求解fabonacci數列尾遞迴版的寫法是:
1 #include 2 #include 3 #include 4注意在fabonacci函式返回的時候將本層函式和上一層的計算結果傳遞給了下一層,用於下一層的計算,並且設定乙個計數器來判斷是否到達了底部。判斷計算結束後,直接返回結果,而不用一層一層向上返回。using
namespace
std;
56 unsigned long
long fabonacci( int i,int num, unsigned long
long pre1, unsigned long
long
pre2 )713
14int
main()
15
這樣的話,求解的過程就變成了:
f(3,6,1,1)最終返回5+3=8=f(4,6,2,1)
=f(5,6,3,2)
=f(6,6,5,3)
這樣,即使是fabonacci(1000),也可以很快求出答案
這裡計時函式還沒有抓取到就計算完成了。。。
尾遞迴的另乙個優化顯然就是棧空間上的優化。開頭說到程式的函式棧空間是有限的,使用尾遞迴顯然可以避免由於遞迴層數過多而產生的爆棧。
然而,不幸的是。不同的編譯器(直譯器)會有不同的選擇。對於python這種語言的直譯器不會進行尾遞迴優化,即使你寫成了尾遞迴形式的**,他依然為你分配相應的棧空間。
c則比較奇怪,我對這兩個程式進行了測試。普通遞迴版的程式可以計算到fabonacci(65141)(當然不可能等到其輸出結果,但是在輸入後相當長的一段時間裡程式都沒有爆棧,輸入65142則會直接爆棧),而尾遞迴版的程式只可以計算到fabonacci(32572),反而不如普通遞迴版的計算的多。
探索c 之尾遞迴編譯器優化
遞迴運用 乙個函式直接或間接的呼叫自身,這個函式即可叫做遞迴函式。遞迴主要功能是把問題轉換成較小規模的子問題,以子問題的解去逐漸逼近最終結果。遞迴最重要的是邊界條件,這個邊界是整個遞迴的終止條件。static int recfact int x recfact 10 上面是個經典階乘函式的實現。這裡...
編譯器優化
常量摺疊 a 1 2 由於結果可預見,編譯器直接生成a 3 常量傳播a 1 若後續 沒有更改a,則編譯器將a直接用其值1代替 減少變數 對於x和y的比較,可以轉換成if i j x i2 y j 2 if x y 複寫傳播 類似於常量長傳,不過傳播的是變數 若後續 未修改a的值,則編譯器用m代替a ...
C 編譯器的遞迴深度與程式優化思考
遞迴演算法具有程式容易編寫的特點 然而,由於編譯器預分配堆疊空間的限制,遞迴深度並不是無限制的。在遞迴過程中,系統將對當前程式執行狀態儲存 壓入堆疊 並將引數壓棧,然後遞迴。在遞迴完成後,則做出棧操作。當遞迴深度很深時,由於堆疊滿,遞迴無法繼續。那麼,遞迴深度到底是多少呢?如果我們建立無引數傳遞的遞...