1. 程式人生 > >遞迴演算法詳解

遞迴演算法詳解

1. 何為遞迴?

遞迴在我們的生活中其實很常見。假設你去電影院看電影,黑漆漆一片,你不知道自己來到了第幾排,於是你問前面的人他是第幾排,知道了前面的人是第幾排,加一也就是你所在的排數。但前面的人也不知道,於是他也繼續向前問,直到第一排的人回答他在第一排,然後再依次往後傳,最後你就知道了你現在位於第幾排。

  • 遞迴可以分為兩個過程,向前一直推進直到找到一個確定的解答為“遞”,倒推回來得到你想要的答案為“歸”。可以形象地概括為往前”遞“,往後”歸“

2. 遞迴滿足的條件?

  • 一個問題的解可以分解為幾個子問題的解。開篇的例子中,‘”自己在第幾排的問題“可以分解為“前面的人在第幾排”的子問題。

  • 問題和分解後的子問題,除了資料規模不一樣,求解思路完全一樣

  • 存在遞迴終止條件。不能無限制地“遞”下去,第一排的人知道答案後就要“歸”回來。


3. 實現遞迴的關鍵?

  • 基線條件(base case),停止呼叫

  • 遞迴條件(recursive case), 自己呼叫自己

  • 實現遞迴的關鍵就在於找到基線條件和遞迴條件。遞迴條件就是分解子問題的過程,基線條件就是找到遞迴終止的情況。

  • 不要試圖去弄清楚遞迴的詳細過程,解決問題的時候要把重點放在,假設子問題已經得到解決的情況下,我們該如何求解


4. 遞迴的注意事項?

  • 重複計算。斐波那契數列,F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)。其計算過程如下所示,我們可以直觀地看到有一些值被計算了很多次。
    重複計算

針對這種情況,我們可以通過定義一個數據結構(比如散列表)來儲存已經求解過的值,當遞迴呼叫時,我們先檢視當前值是否被計算過,如果是,則直接從散列表中取值返回,來避免重複計算。

  • 堆疊溢位。遞迴需要多次的函式呼叫,而函式呼叫需要棧來儲存臨時變數,因此,當遞迴呼叫的次數非常多時,佔用記憶體很高,就會有堆疊溢位的風險。相應的,其計算時間也可能超出想象的大。

我們可以用尾遞迴來確保最後一步只調用函式自身,優化堆疊使用。以求 5 的階乘為例,尾遞迴的呼叫過程如下所示,fact_tail(5, 1) > fact_tail(4, 5) > fact_tail(3, 20) > fact_tail(2, 60) > fact_tail(1, 120),四次遞迴呼叫後直接返回結果。

int fact_tail(int n, int a)
{
    /*Compute a factorialina tail - recursive manner.*/
     
    if (n < 0)
        return 0;    
    else if (n == 0)
        return 1;    
    else if (n == 1)
        return a;
    else
        return facttail(n - 1, n * a);
 
}

5. 遞迴與迴圈?

  • 籠統地講,所有遞迴程式碼都可以改為迭代迴圈的方式實現。

  • 遞迴程式碼寫起來非常簡潔,程式可能更容易理解;但空間複雜度高、有堆疊溢位風險、時間成本較高。

  • 改用迴圈,則可以減少函式呼叫,程式的效能可能更高,如何選擇要看什麼對你來說更重要。


獲取更多精彩,請關注「seniusen」!
seniusen