1. 程式人生 > >演算法 遞迴 迭代 動態規劃 斐波那契數列 MD

演算法 遞迴 迭代 動態規劃 斐波那契數列 MD

Markdown版本筆記 我的GitHub首頁 我的部落格 我的微信 我的郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 [email protected]

演算法 遞迴 迭代 動態規劃 斐波那契數列 MD


目錄

目錄
遞迴和迭代
什麼是遞迴
什麼是迭代法
遞迴和迭代的區別
動態規劃

基本思想
適用條件
斐波那契數列
遞迴法實現
迭代法實現
動態規劃實現

遞迴和迭代

什麼是遞迴

遞迴的基本概念:程式呼叫自身的程式設計技巧稱為遞迴

一個函式在其定義中直接或間接呼叫自身的一種方法,它通常把一個大型的複雜的問題轉化為一個與原問題相似的規模較小的問題來解決,可以極大的減少程式碼量。

遞迴的能力在於用有限的語句來定義物件的無限集合.

由於遞迴引起一系列的函式呼叫,並且有可能會有一系列的重複計算,遞迴演算法的執行效率相對較低。

遞迴的優劣
優點

  • 大問題化為小問題,可以極大的減少程式碼量
  • 用有限的語句來定義物件的無限集合
  • 程式碼更簡潔清晰,可讀性更好

缺點

  • 遞迴容易產生"棧溢位"錯誤(stack overflow)。因為需要同時儲存成百上千個呼叫記錄,所以遞迴非常耗費記憶體。
  • 遞迴可能存在冗餘計算。比如最典型斐波那契數列,計算第6個需要計算第4個和第5個;而計算第5個還需要計算第4個,所處會重複。迭代在這方面有絕對優勢。

什麼是迭代法

迭代法也稱輾轉法,是一種不斷用變數的舊值遞推新值的過程,跟迭代法相對應的是直接法,即一次性解決問題。

迭代法是一類利用遞推公式迴圈演算法通過構造序列來求問題近似解的方法。

迭代演算法是用計算機解決問題的一種基本方法,它利用計算機運算速度快、適合做重複性操作的特點,讓計算機對一組指令(或一定步驟)進行重複執行,在每次執行這組指令(或這些步驟)時,都從變數的原值推出它的一個新值

,迭代法又分為精確迭代和近似迭代。比較典型的迭代法如二分法牛頓迭代法屬於近似迭代法。

遞迴和迭代的區別

知乎 上有很多人舉了非常生動的例子來說明他們的區別,下面摘錄一些比較經典的描述。

遞迴就是自己呼叫自己,自己包含自己。
迭代是將輸出做為輸入,再次進行處理。

遞迴過程中, 問題的規模在縮小,這樣最終得到問題的解;
迭代是一種由遠變近的逼近,問題的規模不見得縮小了,但是慢慢在調整接近答案。

遞迴——《盜夢空間》,不斷下潛至底層並最終解決
迭代——《明日邊緣》,不斷回到同一個場景並優化解決

遞迴是一個樹結構,每個分支都探究到最遠,發現無法繼續的時候往回走;
迭代是一個環結構,每次迭代都是一個圈,不會拉掉其中的某一步,然後不斷迴圈;

迭代是更新變數的舊值
遞迴是在內部呼叫自身

迭代是迴圈結構,例如 for while 迴圈
遞迴是選擇結構,例如 if else 呼叫自己

在數學上,遞迴強調的是,新的值與前面計算的好幾個值有關係 F{n} =F{n-1} +F{n-2}
而迭代一般是隻是 x{n+1} 與 x{n} 之間進行計算

迭代是逐漸逼近,用新值覆蓋舊值,直到滿足條件後結束,不儲存中間值,空間利用率高。
遞迴是將一個問題分解為若干相對小一點的問題,遇到遞迴出口再原路返回,因此必須儲存相關的中間值,這些中間值壓入棧儲存,問題規模較大時會佔用大量記憶體。

動態規劃

動態規劃 Dynamic Programming,簡稱DP。通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。

動態規劃程式設計是對解最優化問題的一種途徑、一種方法,而不是一種特殊演算法。不像搜尋或數值計算那樣,具有一個標準的數學表示式和明確清晰的解題方法。

動態規劃程式設計往往是針對一種最優化問題,由於各種問題的性質不同,確定最優解的條件也互不相同,因而動態規劃的設計方法對不同的問題,有各具特色的解題方法,而不存在一種萬能的動態規劃演算法,可以解決各類最優化問題。因此讀者在學習時,除了要對基本概念和方法正確理解外,必須具體問題具體分析處理,以豐富的想象力去建立模型,用創造性的技巧去求解。我們也可以通過對若干有代表性的問題的動態規劃演算法進行分析、討論,逐漸學會並掌握這一設計方法。

基本思想

動態規劃演算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。

動態規劃演算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。

與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重複計算了很多次。如果我們能夠儲存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重複計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以後是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規劃法的基本思路。

具體的動態規劃演算法多種多樣,但它們具有相同的填表格式。

關鍵字:分解成若干個子問題,儲存子問題的解,從子問題的解得到原問題的解

問題特徵
動態規劃常常適用於有最優子結構和重疊子問題性質的問題:

  • 最優子結構:當問題的最優解包含了其子問題的最優解時,稱該問題具有最優子結構性質。
  • 重疊子問題:在用遞迴演算法自頂向下解問題時,每次產生的子問題並不總是新問題,有些子問題被反覆計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只解一次,而後將其解儲存在一個表格中,在以後儘可能通過查表以利用這些子問題的解。

適用條件

適用動態規劃的問題必須滿足最優化原理無後效性

最優化原理(最優子結構性質)
最優化原理可這樣闡述:一個最優化策略具有這樣的性質,不論過去狀態和決策如何,對前面的決策所形成的狀態而言,餘下的諸決策必須構成最優策略。簡而言之,一個最優化策略的子策略總是最優的。一個問題滿足最優化原理又稱其具有最優子結構性質。

個人理解:例如求最短路徑問題,從起點到終點的最短路徑,一定也是這條路徑上任意一點到終點的最短路徑。

無後效性
將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱為無後效性。

個人理解:例如求最短路徑問題,前 N 步的路徑(所謂的以前階段的狀態)並不會對後續最優路徑的選擇產生影響,唯一對後續最優路徑的選擇產生影響的是當前所處的位置(所謂的狀態)

子問題的重疊性
動態規劃將原來具有指數級時間複雜度的搜尋演算法改進成了具有多項式時間複雜度(包括O(lgn)、O(n)、O(n^2)、O(n^3)等)的演算法。其中的關鍵在於解決冗餘,這是動態規劃演算法的根本目的。動態規劃實質上是一種以空間換時間的技術,它在實現的過程中,不得不儲存產生過程中的各種狀態,所以它的空間複雜度要大於其它的演算法。

斐波那契數列

1,1,2,3,5,8,13,21,34,55

耗時時間比較:

n 遞迴法 迭代法 動態規劃
20 0-2 全部不超過 1 全部不超過 1
25 1-4 0 0
30 5-8 0 0
35 50 0 0
40 489 0 0
41 803 0 0
42 1323 0 0
43 2025 0 0
44 3293 0 0
45 5309 0 0

遞迴法實現

public static int recurFib(int n) {
    if (n < 2) {
        return n;
    } else {
        return recurFib(n - 1) + recurFib(n - 2);
    }
}

迭代法實現

方式1:

public static int iterFib(int n) {
    if (n < 2) {
        return n;
    } else {
        int result = 0, a = 0, b = 1;
        for (int i = 2; i <= n; i++) {
            result = a + b; //每次都是最近的兩個值的和
            a = b;// 把最舊的值替換為第二舊的值
            b = result; //把第二舊的值替換為最新的值
        }
        return result;
    }
}

方式2:

public static int iterFib(int n) {
    if (n < 2) {
        return n;
    } else {
        int result = 0, a = 0, b = 1;
        for (int i = 2; i <= n; i++) {
            result = a + b; //每次都是最近的兩個值的和
            if (a < b) a = result; //較小的值是更舊的值,我們把最新的值替換為更舊的值,另一個值保持不變
            else b = result;
        }
        return result;
    }
}

動態規劃實現

public static int dynFib(int n) {
    int[] val = new int[n + 1];
    if (n < 2) {
        return n;
    } else {
        val[1] = 1;
        for (int i = 2; i <= n; i++) {
            val[i] = val[i - 1] + val[i - 2];
        }
        return val[n];
    }
}

在使用動態規劃實現時,我們用陣列保留中間值,這些中間值即是動態規劃定義中的小問題

但需要注意的是,用動態規劃解決斐波那契數列時可以不用陣列,因為在計算某個位置上的數時只需要用到前兩位的值,所以我們只需要動態的保留前兩位的值即可。這樣子的動態規劃的實現就和迭代是一樣的了,但在其他問題上可能是不一樣的。

2018-12-9