近日翻看SICP,看到遞迴的部分,深有感觸,遂記下所想所悟,如果有批評直言,望不吝賜教。
遞迴是實現程式計算過程中的描述過程的基本模式之一,在討論遞迴的問題前我們必須十分小心,因為遞迴包含兩個方面的內容,一個是遞迴的計算過程,一個是遞迴過程,後者是文法上的事實而前者是概念上的計算過程,事實上在程式上我們也許是使用迴圈來實現的。
一般在討論遞迴的時候都喜歡用斐波那契數來作為例子,我們也不要免俗。我們將斐波那契數的定義如來說明:
實現的代碼:
def Fib(n):
if (n<1):
return 0
elif (n<3):
return 1
else:
return Fib(n-1)+Fib(n-2)
我們可以看出,這個演算法幾乎就是對定義的直譯,我們假設n=6,那麼得到的計算過程就是,要計算Fib(6)就得計算Fib(5)和Fib(4),以此類推,如:
我們可以看到過程如同一棵倒置的樹,這種方式被稱之為樹形遞迴,也被稱之為線性遞迴。這種遞迴的方式非常的直白,很好理解其計算過程,一般很多人寫遞迴都會下意識的採用這種方式。但是缺點也是很明顯的,從其計算過程可以看出,經過了很多冗餘的計算,並且消耗了大量的呼叫堆疊,這個消耗是指數級增長的,經常有人說呼叫堆疊很容易在很短的遞迴過程就耗光了,多半就是採用了線性遞迴造成的。線性遞迴的過程可用描述,可以清晰的看到展開收攏的過程:
除了這種遞迴方式還有另外一種實現遞迴的方式,同樣是上面的斐波那契數作為例子,這次我們不按照斐波那契的定義入手,我們從正常產生數列的過程入手來實現,0,1,的情況很簡單可以直接返回,之後的計算過程就是累加,我們在遞迴的過程中要保持狀態,這個狀態要保持三個數,也就是上兩個數和迭代的步數,所以我們定義的方法為
def Fib(n,b1=1,b2=1,c=3):
if n<3:
return 1
else:
if n==c:
return b1+b2
else:
return Fib(n,b1=b2,b2=b1+b2,c=c+1)
這種方法我們在每一次遞迴的過程中保持了上一次計算的狀態,所以稱之為“線性迭代過程”,也就是俗稱的尾遞迴。由於每一步計算都保持了狀態所以消除了冗餘計算,所以這種方式的效率明顯高於前一種,其計算過程如下
fib(6)
fib 0,0,1
fib 0,1,2
fib 1,2,3
fib 2,3,4
fib 3,5,5
fib 5,8,6
這兩種遞迴方式之間是可以轉換的,凡是可以通過固定數量狀態來描述中間計算過程的遞迴過程都可以通過線性迭代來表示。
我們可以發現,其實尾遞迴的過程和迴圈基本上是等價的,我們可以將尾遞迴的過程很方便到用迴圈來代替,所以很多的語言對尾遞迴提供了編譯層級的最佳化,也就是將尾遞迴在編譯期轉化成迴圈的代碼。不過對於沒有提供尾遞迴最佳化的語言來說也是很有意義的,比如python的預設呼叫堆疊長度是1000,如果用線性遞迴很快就會消耗光,但是尾遞迴就不會,比如尾遞迴的Fib函數,用Fib(1001)調用沒問題的而且跑得飛快,Fib(1002)的時候才堆疊溢位。但是如果是線性遞迴的方式計算n=30的時候就能明顯感覺到速度變慢,40以上基本就掛了。
這裡我無意對比兩種方式的優劣,也許線性遞迴效能有差距但是它的可讀性非常的強,幾乎就等同於公式的直接描述,所以可以根據計算規模來合理選用。