【算法】遞歸

遞歸

  • 遞歸實現的原理:
    一個遞歸函數的調用過程相似於多個函數的嵌套的調用,只不過調用函數和被調用函數是同一個函數。爲了保證遞歸函數的正確執行,系統需設立一個工做棧。具體地說,遞歸調用的內部執行過程以下:
    1. 運動開始時,首先爲遞歸調用創建一個工做棧,其結構包括值參局部變量返回地址
    2. 每次執行遞歸調用以前,把遞歸函數的值參局部變量的當前值以及調用後的返回地址壓棧;
    3. 每次遞歸調用結束後,將棧頂元素出棧,使相應的值參和局部變量恢復爲調用前的值,而後轉向返回地址指定的位置繼續執行。
  • 注意:在咱們瞭解了遞歸的基本思想及其數學模型以後,咱們如何才能寫出一個漂亮的遞歸程序呢?我認爲主要是把握好以下三個方面:
    1. 明確遞歸函數的做用;
    2. 明確遞歸終止條件與對應的解決辦法;
    3. 找出函數的等價關係式,提取重複的邏輯縮小問題規模。
  • 遞歸三步走:
    • 明確函數功能:要清楚你寫這個函數是想要作什麼;
    • 尋找遞歸出口:遞歸必定要有結束條件,否則會永遠遞歸下去,禁止套娃
    • 找出遞推關係:開始實現遞歸,一步一步遞推出最終結果。

明確函數功能

第一步,明確這個函數的功能是什麼,它要完成什麼樣的一件事。
而這個功能,是徹底由你本身來定義的。也就是說,咱們先無論函數裏面的代碼是什麼、怎麼寫,而首先要明白,你這個函數是要用來幹什麼的。數組

例如,求解任意一個數的階乘:
要作出這個題,
第一步,要明確即將要寫出的這個函數的功能爲:算n的階乘。函數

//算n的階乘(假設n不爲0)
int f(int n) {
    
}

尋找遞歸出口(初始條件)

遞歸:就是在函數實現的內部代碼中,調用這個函數自己。因此,咱們必需要找出遞歸的結束條件,否則的話,會一直調用本身,一直套娃,直到內存充滿。優化

  • 必須有一個明確的結束條件。由於遞歸就是有「遞」「歸」,因此必須又有一個明確的點,到了這個點,就不用「遞下去」,而是開始「歸來」。

第二步,咱們須要找出當參數爲什麼值時,遞歸結束,以後直接把結果返回。
通常爲初始條件,而後從初始條件一步一步擴充到最終結果code

注意:這個時候咱們必須能根據這個參數的值,可以直接知道函數的結果是什麼。blog

讓咱們繼續完善上面那個階乘函數。
第二步,尋找遞歸出口:
當n=1時,咱們可以直接知道f(1)=1
那麼遞歸出口就是n=1時函數返回1。
以下:遞歸

//算n的階乘(假設n不爲0)
int f(int n) {
    if(n == 1) {
        return 1;
    }
}

固然,當n=2時,咱們也是知道f(2)等於多少的,n=2也能夠做爲遞歸出口。遞歸出口可能並不惟一的。內存

找出遞推關係

第三步,咱們要從初始條件一步一步遞推到最終結果。(能夠類比數學概括法,多米諾骨牌)數學

  • 初始條件:f(1) = 1
  • 遞推關係式:f(n) = f(n-1)*n

這樣就能夠從n=1,一步一步推到n=2,n=3...變量

// 算n的階乘(假設n不爲0)
int f(int n) {
    if(n = 1) {
        return n;
    }
    // 把f(n)的遞推關係寫進去
    return f(n-1) * n;
}

到這裏,遞歸三步走就完成了,那麼這個遞歸函數的功能咱們也就實現了。
可能初學的讀者會感受很奇妙,這就能算出階乘了?原理

那麼,咱們來一步一步推一下。
f(1)=1
f(2)=f(1)*2=2
f(3)=f(2)*3=2*3=6
...
你看看是否是解決了,n都能遞推出來!

例題

斐波那契數列

斐波那契數列的是這樣一個數列:一、一、二、三、五、八、1三、2一、34....,即第一項 f(1) = 1,第二項 f(2) = 1.....,第 n 項目爲 f(n) = f(n-1) + f(n-2)。求第 n 項的值是多少。

  • 明確函數功能:f(n)爲求第n項的值

    // 1.f(n)爲求第n項的值
    int f(int n) {
    
    }
  • 尋找遞歸出口:f(1)=1,f(2)=1

    // 1.f(n)爲求第n項的值
    int f(int n) {
        // 2.遞歸出口
        if(n <= 2) {
            return 1;
        }
    }
  • 找出遞推關係:f(n) = f(n-1)+f(n-2)

    // 1.f(n)爲求第n項的值
    int f(int n) {
        // 2.遞歸出口
        if(n <= 2) {
            return 1;
        }
        // 3.遞推關係
        return f(n-1) + f(n-2);
    }

小青蛙跳臺階

一隻青蛙一次能夠跳上1級臺階,也能夠跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

  • 明確函數功能:f(n)爲青蛙跳上一個n級的臺階總共有多少種跳法

    int f(int n) {
    
    }
  • 尋找遞歸出口:f(0)=0,f(1)=1

    int f(int n) {
        // 遞歸出口
        if(n <= 1) {
            return n;
        }
    }
  • 找出遞推關係:f(n) = f(n-1)+f(n-2)

    int f(int n) {
        // 遞歸出口
        if(n <= 2) {
            return 1;
        }
        // 遞推關係
        return f(n-1) + f(n-2);
    }

遞歸優化思路

自頂向下

上面說了那麼多,都是自底向上
(我比較習慣自底向上,由於比較符合數學概括法,順着推)

例如,階乘能夠理解爲f(n)一步一步分解爲f(n-1)...直到f(1),一步步化小,這樣也是能夠的。

重複計算

其實遞歸當中有不少子問題被重複計算。

對於斐波那契數列,f(n) = f(n-1)+f(n-2)。
遞歸調用的狀態圖以下:

其中,遞歸計算時f(6)、f(5)...都被重複了不少次,這是極大的浪費,當n越大,因重複計算浪費的就越多,因此咱們必需要進行優化。

  • 優化思路:
    • 創建一個數組,將子問題的計算結果保存起來。
    • 判斷以前是否計算過:
      • 計算過,取出來用
      • 沒有計算過,再遞歸計算
  • 實例:
    • 把n做爲數組下標,f(n)做爲值。
      例如arr[n] = f(n)。
    • f(n)尚未計算過的時候,咱們讓arr[n]等於一個特殊值。
      例如arr[n] = -1。
    • 當咱們要判斷的時候,
      • 若是 arr[n] = -1,則證實f(n)沒有計算過;
      • 不然,f(n)就已經計算過了,且f(n) = arr[n]。
        直接把值取出來用就好了。

代碼以下:

// 咱們實現假定 arr 數組已經初始化好的了。
int f(int n) {
    if(n <= 1) {
        return n;
    }
    //先判斷有沒計算過
    if(arr[n] != -1) {
        //計算過,直接返回
        return arr[n];
    }else {
        // 沒有計算過,遞歸計算,而且把結果保存到 arr數組裏
        arr[n] = f(n-1) + f(n-1);
        reutrn arr[n];
    }
}
相關文章
相關標籤/搜索