從「數學概括法」到理解「遞歸算法」!

關注我
從「數學概括法」到理解「遞歸算法」!

每章一點正能量:人的一輩子可能燃燒也可能腐朽。html

前言

相信你們在面試或者工做中偶爾會遇到遞歸算法的提問或者編程,咱們今天來聊一聊從數學概括法到理解遞歸算法。若有錯誤還請你們及時指出~git

本文已同步至 GitHub/Gitee/公衆號,感興趣的同窗幫忙點波關注~github

1. 數學概括法

1.1 簡介

來源百度百科面試

數學概括法(Mathematical Induction, MI)是一種數學證實方法,一般被用於證實某個給定命題在整個(或者局部)天然數範圍內成立。除了天然數之外,廣義上的數學概括法也能夠用於證實通常良基結構,例如:集合論中的樹。這種廣義的數學概括法應用於數學邏輯和計算機科學領域,稱做結構概括法。在數論中,數學概括法是以一種不一樣的方式來證實任意一個給定的情形都是正確的(第一個,第二個,第三個,一直下去概不例外)的數學定理。雖然數學概括法名字中有「概括」,可是數學概括法並不是不嚴謹的概括推理法,它屬於徹底嚴謹的演繹推理法。事實上,全部數學證實都是演繹法。算法

天然數是指表示物體個數的數,即由0開始,0,1,2,3,4,……一個接一個,組成一個無窮的集體,即指非負整數。數據庫

1.2 推演步驟

簡單瞭解數學概括法的概念後,咱們來看看數學概括法的推演步驟。編程

咱們知道數學概括法用來證實任意一個給定的情形都是正確的,也就是說,第一個,第二個,一直到全部情形,概不例外。bash

其證實步驟以下:微信

  1. 證實基本狀況(一般是N = 1 的時候)是否成立。 證實對於N=1成立。咱們只須要先從最小的天然數開始證實。這一步一般很是簡單。關鍵是證實第二步。函數

  2. 證實N > 1 時,假設 N - 1 成立,那麼對於N成立(N爲任意大於1的天然數)。 這一步並非直接證實的,而是假設N-1成立,利用這個結論推出N是成立的。若是可以推出的話,就能夠說:對於全部的天然數都成立。由於證實了對1成立,那麼對2成立,對3也成立。那麼就證實了對全部天然數都成立。 咱們會發現數學概括法它很合適用來證實,例如常見的等差、等比、以及平方、立方數列的求和等等。

1.3 小栗子

咱們來舉一個小栗子,回顧下咱們高中時期所學的數學概括法是如何進行證實。

例子:

證實: 1+2+3+...+n = n(n+1)/2

複製代碼

咱們來將上面 1.2 推演步驟 用起來。

  • 第一步: 證實基本狀況(一般是N = 1 的時候)是否成立。

咱們把N=1同時代入等號左邊和右邊,得

1 = 1*(1+1)/2

複製代碼

成立!

  • 第二步: 證實N > 1 時,假設 N - 1 成立,那麼對於N成立(N爲任意大於1的天然數)。

這裏咱們須要分兩步。

  • ① 假設對於N-1的狀況下成立

咱們依然將N-1同時代入等號的左邊和右邊,得:

1+2+3+...+(n-1) = (n-1)n/2
    
複製代碼
  • ② 將假設結論代入,同時加N

咱們假設N-1是成立的,那麼咱們在等號左邊與右邊同時加N,確定也是成立的,得:

1+2+3...+(n-1)+n = (n-1)n/2+n 
    
複製代碼

化簡右邊得:n(n+1)/2,那麼咱們最後證實的結果就是成立的!

即:1+2+3+...+n = n(n+1)/2 成立。經過以上步驟,咱們能夠證實這個公式是成立的。

1.4 小結

概括法適用於想解決一個問題轉化爲解決他的子問題,而他的子問題又變成子問題的子問題,並且咱們發現這些問題其實都是一個模型,也就是說存在相同的邏輯概括處理項。

接下來咱們來看看,咱們寫程序和數學概括法的關聯。

2. 遞歸

提及遞歸算法,其實咱們每一個開發人員都確定聽過或者寫過。記得我最開始接觸遞歸算法的時候,仍是大一學習譚浩強老師寫的那本C語言時,裏面介紹了遞歸算法。給個人印象就是:本身調用本身。後來在工做中,用到的地方也很少,印象中只有一次寫級聯菜單的時候用到了遞歸算法。(是否是我寫的代碼太水,你們也能夠說說哪裏用到過遞歸算法)本章就來經過數學概括法來回顧下咱們曾經學過的遞歸算法。

2.1 理解遞歸

遞歸的基本思想:以此類推

具體來說就是把規模大的問題轉化爲規模小的類似的子問題來解決。在函數實現時,由於解決大問題的方法和解決小問題的方法每每是同一個方法,因此就產生了函數調用它自身的狀況。另外這個解決問題的函數必須有明顯的結束條件,這樣就不會產生無限遞歸的狀況了。仔細觀察遞歸,就會發現:遞歸的數學模型其實就是概括法

2.2 遞歸條件

咱們在使用遞歸的時候須要知足一些基本條件,若是不知足的話,就有可能出現無限遞歸,最後會致使堆棧溢出了。

知足條件:

  1. 嚴格定義遞歸函數做用,包括參數,返回值,其餘變量。
  2. 先通常狀況,後特殊狀況。
  3. 有退出條件。在通常狀況下,能讓遞歸正常退出的條件。
  4. 每次調用必須縮小問題規模,且新問題與原問題有着相同的形式,即規律。

上面的條件一環扣一環,也能夠縮減成兩個主要條件:有規律,有退出條件。咱們以上面的條件,來結合案例進行理解。

2.3 小栗子

2.3.1 遞歸求和

例題:

1+2+3+...+n=? 

複製代碼

第一步: 嚴格定義遞歸函數做用,包括參數,返回值,其餘變量。

咱們初看題目,能夠知道這是一個簡單的求和,即從1開始:1+2+3+...一直加到n。因此咱們能夠定義一個入參爲n,返回值類型爲int的一個方法,既然是遞歸求和,咱們的方法名就叫recursionSum。

public static int recursionSum(int n) { //爲了方便調用,我用了static
    
    return 0;
}

System.out.println("公衆號:Coder編程:" + recursionSum(0));

複製代碼

那麼咱們第一步就作完了。

第二步: 先通常狀況,後特殊狀況。

咱們先用通常的狀況進行求和計算,例如代入1,2,3這樣的通常狀況。即:

public static int recursionSum(int n) { 
    if(n == 1) {
        return 1;
    }
    
    if(n == 2) {
        return 1+2;
    }
    
    if(n == 3) {
        return 1+2+3;
    }
    return 0;
}

System.out.println("公衆號:Coder編程:" + recursionSum(3));

複製代碼

第三步: 有退出條件。在通常狀況下,能讓遞歸正常退出的條件。

其實,咱們作完第二步,就會發現已經把第三步作完了。即有了讓遞歸正常退出的條件!

第四步: 每次調用必須縮小問題規模,且新問題與原問題有着相同的形式,即規律。

這一步是最關鍵的,也是最核心的!咱們須要找到其規律,而且能縮小問題的規模。咱們會發現,當咱們須要求第N個數的和的時候,咱們必須知道前N-1個數的和,即 sum(N-1)。前N個數的和就是sum(N-1)+N。找到這個規律後,咱們就能夠定義一個臨時變量sum來接收前N個數的和了。

public static int recursionSum(int n) {

    if(n == 1) {
        return 1;
    }
    
    if(n == 2) {
        return 1+2;
    }
    
    if(n == 3) {
        return 1+2+3;
    }
    
    int sum = recursionSum(n-1)+n;
    return sum;
}

System.out.println("公衆號:Coder編程:前5個數的和" + recursionSum(5));

複製代碼

輸出結果:15

咱們優化一下:

public static int recursionSum(int n) {

    if (n < 0){
       throw new Exception("參數不能爲負!");
    }
    if(n == 1) {
        return 1;
    }
    
    return recursionSum(n-1)+n;
}

System.out.println("公衆號:Coder編程:前5個數的和" + recursionSum(5));

複製代碼

是否是忽然發現遞歸其實也沒想的那麼難?

2.3.2 觸類旁通?

接下來咱們難度進行升級!看你們能不能都理解了。我就不像上面求和那麼囉嗦了!

2.3.2.1 求階乘

例題:求n的階乘(n>1,n是正整數)

階乘的遞推公式爲:factorial(n)=n*factorial(n-1),其中n爲非負整數,且0!=1,1!=1 這裏就不作過多說明,跟求後過程一致,能夠模仿求和的過程,你們能夠先本身嘗試寫下,下面我直接貼代碼了:

public static int factorial(int n) throws Exception {
    if (n < 0){
        throw new Exception("參數不能爲負!");
    }else if (n == 1 || n == 0) {
        return 1;
    }else {
        return n * factorial(n - 1);
    }
}

System.out.println("公衆號:Coder編程:3的階乘:" + factorial(3));


複製代碼

輸出結果: 公衆號:Coder編程:3的階乘:6

2.3.2.2 斐波那契數列

斐波那契數列 我想你們一樣熟悉瞭解,下面咱們繼續回顧一下斐波那契數列究竟是什麼?

斐波那契數列圖

斐波那契數列: 一、一、二、三、五、八、1三、21.....

能夠看出從第三位起:第三項等於前兩項之和。總結遞推公式::Fib(n)=Fib(n-1)+Fib(n-2)。因此咱們能夠將前兩位做爲退出遞歸的條件。即:if(n==1) retrun 1 if(n==2) return 1

所以咱們能夠直接用公式(規律)和退出條件,寫出編程代碼:

public static int fib(int n) throws Exception {
    if (n < 0) {
        throw new Exception("參數不能爲負!");
    }else if (n == 0 || n == 1){
        return n;
    }else {
        return fib(n - 1) + fib(n - 2);
    }
}

System.out.println("公衆號:Coder編程:斐波那契數列:" + fib(3));

複製代碼
2.3.2.3 漢諾塔問題

相傳在古印度聖廟中,有一種被稱爲漢諾塔(Hanoi)的遊戲。該遊戲是在一塊銅板裝置上,有三根杆(編號A、B、C),在A杆自下而上、由大到小按順序放置不一樣個數的金盤(以下圖)。

遊戲的目標:把A杆上的金盤所有移到C杆上,並仍保持原有順序疊好。

操做規則:每次只能移動一個盤子,而且在移動過程當中三根杆上都始終保持大盤在下,小盤在上,操做過程當中盤子能夠置於A、B、C任一杆上。

漢諾塔圖

在總結規律和寫代碼以前,咱們先來玩幾把簡單的(先通常後特殊):

注:咱們以數字的大小做爲盤子的大小。

  1. 一個盤子的狀況:

    1.1 將A柱子的1號盤子直接移動到C柱子中。 1.2 結束。

  2. 兩個盤子的狀況:

    2.1 將A柱子的1號盤子移動到B柱子。 2.2 將A柱子的2號盤子移動到C柱子。 2.3 將B柱子的1號盤子移動到C柱子。 2.4 結束。

  3. 三個盤子的狀況:

    3.1 將A柱子的1號盤子移動到C柱子。 3.2 將A柱子的2號盤子移動到B柱子。 3.3 將C柱子的1號盤子移動到B柱子。 3.4 將A柱子的3號盤子移動到C柱子。 3.5 將B柱子的1號盤子移動到A柱子。 3.6 將B柱子的2號盤子移動到C柱子。 3.7 將A柱子的1號盤子移動到C柱子。 3.8 結束。


咱們會發現,隨着盤子數量的增長,盤子移動的難度也開始加大。

這時候不要懼怕,咱們回過頭再來看這個問題:當盤子的數量是4個、5個...N個的時候,咱們該如何解決呢?咱們是否是能夠用數學概括法的思想或者遞歸的思想去解決呢?答案是:確定的。這時候咱們須要去找到他們的規律在哪?

咱們再觀察下上面在通常狀況下移動盤子的規律在哪?

  • 1.當只有一個盤子的時候,能夠將盤子直接移動到目標柱子C中。即退出條件
  • 2.當只有兩個盤子的時候,咱們只須要將B柱子做爲中介,將盤子1先放到中介柱子B上,而後將盤子2放到目標柱子C上,最後將中介柱子B上的盤子放到目標柱子C上便可。

第二點能夠當作:當咱們有N個盤子的時候,第N個盤子當作一個盤子,(N-1)個盤子看作成一個盤子。須要將(N-1)個盤子放在中介柱子B上,N個盤子放在目標柱子C便可。即規律

當咱們有三個盤子的時候,咱們會發現一個問題: 角色變化

  1. 將A塔座的第(N-1)~1個盤子當作是一個盤子,放到中柱子B上,而後將第N個盤子放到目標柱子C上。這時候柱子A空了!柱子A成爲中介柱子,柱子B成爲起始柱子

  2. 柱子B這時候有N-1個盤子,將第(N-2)~1個盤子當作是一個盤子,放到中介柱子A上,而後將柱子B的第(N-1)號盤子放到目標柱子C上。這時候柱子B空了!柱子B又成爲了中介柱子,A成爲了起始柱子!

重複一、2步驟,直到全部盤子都放到目標塔座C上結束。

總結一下:

  1. 從初始柱子A上移動包含n-1個盤子到中介柱子B上。
  2. 將初始柱子A上剩餘的一個盤子(最大的一個盤子)放到目標柱子C上。
  3. 將中介柱子B上n-1個盤子移動到目標柱子C上。
move(3,"A","B","C");

/**
 * 漢諾塔問題
 * @param dish 盤子個數(也表示名稱)
 * @param from 初始柱子
 * @param temp 中介柱子
 * @param to   目標柱子
 */
public static void move(int dish,String from,String temp,String to){
    if(dish == 1){
        System.out.println("將盤子"+dish+"從柱子"+from+"移動到目標柱子"+to);
    }else{
        move(dish-1,from,to,temp);//A爲初始柱子,B爲目標柱子,C爲中介柱子
        System.out.println("將盤子"+dish+"從柱子"+from+"移動到目標柱子"+to);
        move(dish-1,temp,from,to);//B爲初始柱子,C爲目標柱子,A爲中介柱子
    }
}

複製代碼
  • move(dish-1,from,to,temp);//A爲初始柱子,B爲目標柱子,C爲中介柱子 這裏須要將n-1以前的盤子都放到B柱子上,最後第n個盤子放到C柱子。

  • move(dish-1,temp,from,to);//B爲初始柱子,C爲目標柱子,A爲中介柱子 這時候B變爲了初始柱子,A成爲了目標柱子。將以前n-1個盤子放到C目標柱子中。

打印結果:

打印結果

文末

本章節主要簡單介紹了數學概括法與遞歸算法的一些思想。但願對你們有所幫助! 從此我會在每張文章開頭增長 每章一點正能量 ,文末增長5個編程相關的英語單詞 學點英語。但願你們和我同樣天天都能積極向上,一塊兒學習一同進步!

學點英語

  • JRE Java Runtime Environment(Java運行環境),運行 JAVA程序所必須的環境的集合,包含JVM標準實現及Java核心類庫。
  • JSDK Java Software Development Kit,和JDK以及J2SE 等同。
  • JDK Java Development Kit(Java開發工具包):包括運行環境 、編譯工具及其它工具、源代碼等,基本上和J2SE等同。
  • J2ME Java 2 Micro Edition(JAVA2精簡版)API規格基 於J2SE ,可是被修改成能夠適合某種產品的單一要求。J2ME使JAVA程序能夠很方便的應用於電話卡、尋呼機等小型設備,它包括兩種類型的組件,即配置 (configuration)和描述(profile)。

歡迎關注公衆號:Coder編程 獲取最新原創技術文章和相關免費學習資料,隨時隨地學習技術知識!

參考文章:

www.cnblogs.com/ysocean/p/8…

www.nowamagic.net/librarys/ve…

微信公衆號

推薦閱讀

一篇帶你讀懂TCP之「滑動窗口」協議

帶你瞭解數據庫中JOIN的用法

深刻淺出了解「裝箱與拆箱」

求關注
相關文章
相關標籤/搜索