1. 程式人生 > >數學歸納法和遞迴函式

數學歸納法和遞迴函式

1.什麼是數學歸納法?

數學歸納法用於證明在自然數上的一些斷言是否成立。

怎麼證明斷言對所有自然數成立?
第一步:證明N=1是成立的
第二步:證明N>1時,如果對於N-1成立,那麼對於N成立

(第二步不是直接證明,而是先假設N-1成立,再利用這個結論證明N是成立的)

例子:
用數學歸納法證明 1+2+3+…+n = n(n+1)/2
第一步:
1 = 1*2/2
第二步:
①假設對於n-1的情況下成立
1+2+3+…+(n-1) = (n-1)n/2
②利用假設結論帶進去
1+2+3…+(n-1)+n = (n-1)n/2+n = n(n+1)/2

2.什麼是遞迴函式?

當一個函式直接或間接地呼叫自身定義時就稱為遞迴(recursive)。在思想上遞迴類似於數學歸納法。

編寫遞迴程式的時候,關鍵是要牢記遞迴的四條基本法則:

1.基準情形。 必須有某些基準情形不用遞迴就能求解。
2.不斷推進。 對於那些需要遞迴求解的情形。遞迴呼叫必須總能朝著基準情形的方向邁進。
3.設計法則。 假設所有的遞迴呼叫都能執行。
4.合成效益法則。 在求解一個問題的同一例項時,切勿在不同的遞迴調動中做重複性的工作。(攤還分析)

實現遞迴的三要素

1.方法中出現自己呼叫自己
2.要有分支
3.要有結束條件

3.遞迴的實現的例子

爬樓梯演算法

已知一個樓梯有n個臺階,每次可以選擇邁上一個或者兩個臺階,求走完一共有多少種不同的走法。

	public static int climbStairs(int n) {

        if(n<=0)
            return 0;
        if(n==1){
            return 1;
        }
        if(n==2){
            return 2;
        }
        else
            return climbStairs(n-1)+climbStairs(n-2);
    }

分析一下這個演算法:
A:如果有0個臺階,那麼有0種走法,這個不用多說;
B:如果有1個臺階,那麼有1種走法;
C:如果有2個臺階,那麼有2種走法(一次走1個,走兩次;一次走兩個);
以上的B和C就是基礎情形。
D:接下來就是遞迴了,如果臺階數目多於2個,那麼首先第一步就有兩種選擇:第一次走1個,或者第一次走兩個。這樣除了第一次後邊的走法就有了兩種情形:climbStairs(n-1)和climbStairs(n-2)。這樣一直遞迴下去,直到出現到了基礎情形(即n=1或n=2的情形),遞迴到這個地方(基礎情形),然後開始回溯 ,這就是所說的和遞迴密切相關的“回溯”了。回溯,顧名思義就是從結果倒著回去,找到整個過程,進而分析這個路徑或者說是實現的過程。

需要注意的是,這個演算法實現思路上簡單,但是複雜度並沒有降低,還牽扯回溯儲存堆疊問題(其實遞迴的設計儘量避免這種巢狀兩個的遞迴方式(climb(n)中包含climb(n-1)和climb(n-2)),這種操作會使得堆疊開闢空間隨著n的增大以指數型增長,最終程式很容易崩潰),而且在臺階數目多到一定數量的時候會越界(走法次數會超出int的範圍),所以遞迴程式很大程度上就是思想實現設計上簡單理解一些。

漢諾塔問題

一次只能移動一個盤子;不能把大盤子放在小盤子上;除去盤子在兩個柱子之間移動的瞬間,盤子必須都在柱子上。(在這三點要求下把盤子從起始柱子A全部移動到目標柱子C上)

在這裡插入圖片描述

程式碼如下:

基礎情形:n==1的時候終止遞迴,進行回溯。

/**
 * 漢諾塔問題
 *
 *  n個的移動次數=(n-1)的移動次數+1+(n-1)的移動次數
 *
 *  1個的時候是1次
 *  2個的時候是 2*1+1
 *  3個的時候是 2*(2*1+1)+1
 *  ...
 *  n個的時候是 2^n-1
 *
 */
public class Hanoi {

    /**
     *
     * @param n 盤子的數目
     * @param origin 源座
     * @param assist 輔助座
     * @param destination 目的座
     */
    public void hanoi(int n, char origin, char assist, char destination) {
        if (n == 1) {
            move(origin, destination);
        } else {
            hanoi(n - 1, origin, destination, assist);
            move(origin, destination);
            hanoi(n - 1, assist, origin, destination);
        }
    }

    // Print the route of the movement
    private void move(char origin, char destination) {
        System.out.println("Direction:" + origin + "--->" + destination);
    }

    public static void main(String[] args) {
        Hanoi hanoi = new Hanoi();
        hanoi.hanoi(4, 'A', 'B', 'C');
    }


}

4.遞迴和迴圈

如果我們要重複地多次計算相同的問題,通常可以選擇用遞迴或者迴圈兩種不同的方法。遞迴是在一個函式的內部呼叫這個函式自身。而迴圈這是通過設定計算的初始值及終止條件,在一個範圍內重複計算。比如求1+2+3+…+n,我們可以用遞迴或者迴圈兩種方式求出結果。對應的程式碼如下:

 public  int AddFrom1ToN_Recursive(int n) {
    return n <= 0 ? 0 :n + AddFrom1ToN_Recursive(n - 1);
}

public int AddFrom1ToN_Iternative(int n) {
    int result = 0;
    for (int i = 0; i <= n; ++i)
        result += i;
    return result;
}

5.遞迴的缺點

遞迴雖然有簡潔的優點,但它同時也有顯著的缺點。

遞迴由於是函式呼叫自身,而函式呼叫是有空間和時間的消耗的:每一次函式呼叫,都需要在記憶體棧中分配空間以儲存引數、返回的地址及臨時變數,而且往棧裡壓入資料和彈出資料都需要時間。這就不難理解上述的例子中遞迴實現的效率不如迴圈。

另外,遞迴中有可能很多計算都是重複的,從而對效能帶來很大的負面影響。遞迴的本質是把一個問題分解成兩個或多個小問題。如果多個小問題存在相互重疊的部分,那麼就存在重複的計算。

除了效率以外,遞迴還有可能引起更嚴重的問題:呼叫棧溢位。前面分析中提到需要為每一次函式呼叫在記憶體棧中分配空間,而每個程序的棧的容量是有限的。當遞迴呼叫的層級太多時,就會超出棧的容量,從而導致棧溢位。在上述的例子中,如果輸入的引數比較小,如10,它們都能返回結果55.但如果輸入的引數很大,比如5000,那麼遞迴程式碼在執行的時間就會出錯,但執行迴圈的程式碼能得到正確的結果12502500.
  
參考
https://www.cnblogs.com/vincently/p/4191734.html
https://blog.csdn.net/ares_xxm/article/details/68957829