1. 程式人生 > >從斐波那契開始瞭解尾遞迴

從斐波那契開始瞭解尾遞迴

從斐波那契開始瞭解尾遞迴

尾遞迴(tail recursive),看名字就知道是某種形式的遞迴。簡單的說遞迴就是函式自己呼叫自己。那尾遞迴和遞迴之間的差別就只能體現在引數上了。

尾遞迴wiki解釋如下:

尾部遞迴是一種程式設計技巧。遞迴函式是指一些會在函式內呼叫自己的函式,如果在遞迴函式中,遞迴呼叫返回的結果總被直接返回,則稱為尾部遞迴。尾部遞迴的函式有助將演算法轉化成函式程式語言,而且從編譯器角度來說,亦容易優化成為普通迴圈。這是因為從電腦的基本面來說,所有的迴圈都是利用重複移跳到程式碼的開頭來實現的。如果有尾部歸遞,就只需要疊套一個堆疊,因為電腦只需要將函式的引數改變再重新呼叫

一次。利用尾部遞迴最主要的目的是要優化,例如在Scheme語言中,明確規定必須針對尾部遞迴作優化。可見尾部遞迴的作用,是非常依賴於具體實現的。

我們還是從簡單的斐波那契開始瞭解尾遞迴吧。

用普通的遞迴計算Fibonacci數列:

#include "stdio.h"
#include "math.h"

int factorial(int n);

int main(void)
{
    int i, n, rs;

    printf("請輸入斐波那契數n:");
    scanf("%d",&n);

    rs = factorial(n);
    printf
("%d \n", rs); return 0; } // 遞迴 int factorial(int n) { if(n <= 2) { return 1; } else { return factorial(n-1) + factorial(n-2); } }

程式執行結果如下:

請輸入斐波那契數n:20
6765

Process returned 0 (0x0)   execution time : 3.502 s
Press any key to continue.

上邊的遞迴需要花費3.502s

下面我們看看如何用尾遞迴實現斐波那契數:

#include "stdio.h"
#include "math.h"

int factorial(int n);

int main(void)
{
    int i, n, rs;

    printf("請輸入斐波那契數n:");
    scanf("%d",&n);

    rs = factorial_tail(n, 1, 1);
    printf("%d ", rs);

    return 0;
}

int factorial_tail(int n,int acc1,int acc2)
{
    if (n < 2)
    {
        return acc1;
    }
    else
    {
        return factorial_tail(n-1,acc2,acc1+acc2);
    }
}

程式執行結果如下:

請輸入斐波那契數n:20
6765
Process returned 0 (0x0)   execution time : 1.460 s
Press any key to continue.

從上邊的結果可以看到尾遞迴的效率比一般遞迴的效率高很多

我們可以列印一下程式的執行過程,函式加入下面的列印語句:

int factorial_tail(int n,int acc1,int acc2)
{
    if (n < 2)
    {
        return acc1;
    }
    else
    {
        printf("factorial_tail(%d, %d, %d) \n",n-1,acc2,acc1+acc2);
        return factorial_tail(n-1,acc2,acc1+acc2);
    }
}

程式的執行結構如下:

請輸入斐波那契數n:10
factorial_tail(9, 1, 2)
factorial_tail(8, 2, 3)
factorial_tail(7, 3, 5)
factorial_tail(6, 5, 8)
factorial_tail(5, 8, 13)
factorial_tail(4, 13, 21)
factorial_tail(3, 21, 34)
factorial_tail(2, 34, 55)
factorial_tail(1, 55, 89)
55
Process returned 0 (0x0)   execution time : 1.393 s
Press any key to continue.

從上面的除錯就可以很清晰地看出尾遞迴的計算過程了。acc1就是第n個數,而acc2就是第n與第n-1個數的和,這就是我們前面講到的“迭代”的精髓,計算結果參與到下一次的計算,從而減少很多重複計算量。

fibonacci(n-1,acc2,acc1+acc2)真是神來之筆,原本樸素的遞迴產生的棧的層次像二叉樹一樣,以指數級增長,但是現在棧的層次卻像是陣列,變成線性增長了,實在是奇妙,總結起來也很簡單,原本棧是先擴充套件開,然後邊收攏邊計算結果,現在卻變成在呼叫自身的同時通過引數來計算

小結

尾遞迴的本質是:將單次計算的結果快取起來,傳遞給下次呼叫,相當於自動累積

Java命令式語言中,尾遞迴使用非常少見,因為我們可以直接用迴圈解決。而在函式式語言中,尾遞迴卻是一種神器,要實現迴圈就靠它了。

很多人可能會有疑問,為什麼尾遞迴也是遞迴,卻不會造成棧溢位呢?因為編譯器通常都會對尾遞迴進行優化。編譯器會發現根本沒有必要儲存棧資訊了,因而會在函式尾直接清空相關的棧

本文轉載至:http://www.nowamagic.net/librarys/veda/detail/2325