理解遞迴

AdamWong發表於2019-03-14

在初學遞迴的時候, 看到一個遞迴實現, 我們總是難免陷入不停的回溯驗證之中, 因為回溯就像反過來思考迭代, 這是我們習慣的思維方式, 但是實際上遞迴不需要這樣來驗證. 比如, 另外一個常見的例子是階乘的計算. 階乘的定義: “一個正整數的階乘(英語:factorial)是所有小於或等於該數的正整數的積,並且0的階乘為1。” 以下是Ruby的實現:

int factorial(n) 
  if (n <= 1) 
    return 1;
  else
    return n * factorial(n - 1);

我們怎麼判斷這個階乘的遞迴計算是否是正確的呢? 先別說測試, 我說我們讀程式碼的時候怎麼判斷呢?
回溯的思考方式是這麼驗證的, 比如當n = 4時, 那麼factoria(4)等於4 * factoria(3), 而factoria(3)等於3 * factoria(2), factoria(2)等於2 * factoria(1), 等於2 * 1, 所以factoria(4)等於4 * 3 * 2 * 1. 這個結果正好等於階乘4的迭代定義.
用回溯的方式思考雖然可以驗證當n = 某個較小數值是否正確, 但是其實無益於理解.
Paul Graham提到一種方法, 給我很大啟發, 該方法如下:

  1. 當n=0, 1的時候, 結果正確.
  2. 假設函式對於n是正確的, 函式對n+1結果也正確.
    如果這兩點是成立的,我們知道這個函式對於所有可能的n都是正確的。

這種方法很像數學歸納法, 也是遞迴正確的思考方式, 事實上, 階乘的遞迴表達方式就是1!=1,n!=(n-1)!×n. 當程式實現符合演算法描述的時候, 程式自然對了, 假如還不對, 那是演算法本身錯了…… 相對來說, n,n+1的情況為通用情況, 雖然比較複雜, 但是還能理解, 最重要的, 也是最容易被新手忽略的問題在於第1點, 也就是基本用例(base case)要對. 比如, 上例中, 我們去掉if n <= 1的判斷後, 程式碼會進入死迴圈, 永遠不會結束.

使用尾遞迴

我們常見的遞迴函式比如Fabpnpcci函式可以通過尾遞迴來進行優化,尾遞迴能進行優化的原因是編譯器會自動識別尾遞迴併且為其優化。 現在我們來看一下兩個例子

正常遞迴版階乘法
int f(const int n){
    if(n<1)return 0;
    if(n==1)return 1;
    else return f(n-1)*n;
}
尾遞迴版本階乘
int f(const int n,const int res){
    if(n==1||n==2)return 1;
    else return f(n-1,res+n)
}

相關文章