每章一點正能量:人的一生可能燃燒也可能腐朽。
前言
相信大家在面試或者工作中偶爾會遇到遞歸算法的提問或者編程,我們今天來聊一聊從數學歸納法到理解遞歸算法
。如有錯誤還請大家及時指出~
1. 數學歸納法
1.1 簡介
來源百度百科
數學歸納法(Mathematical Induction, MI)是一種數學證明方法,通常被用於證明某個給定命題在整個(或者局部)自然數范圍內成立。除了自然數以外,廣義上的數學歸納法也可以用於證明一般良基結構,例如:集合論中的樹。這種廣義的數學歸納法應用於數學邏輯和計算機科學領域,稱作結構歸納法。在數論中,數學歸納法是以一種不同的方式來證明任意一個給定的情形都是正確的(第一個,第二個,第三個,一直下去概不例外)的數學定理。雖然數學歸納法名字中有“歸納”,但是數學歸納法並非不嚴謹的歸納推理法,它屬於完全嚴謹的演繹推理法。事實上,所有數學證明都是演繹法。
自然數是指表示物體個數的數,即由0開始,0,1,2,3,4,……一個接一個,組成一個無窮的集體,即指非負整數。
1.2 推演步驟
簡單了解數學歸納法的概念后,我們來看看數學歸納法的推演步驟。
我們知道數學歸納法用來證明任意一個給定的情形都是正確的,也就是說,第一個,第二個,一直到所有情形,概不例外。
其證明步驟如下:
-
證明基本情況(通常是N = 1 的時候)是否成立。
證明對於N=1成立。我們只需要先從最小的自然數開始證明。這一步通常非常簡單。關鍵是證明第二步。 -
證明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 遞歸條件
我們在使用遞歸的時候需要滿足一些基本條件,如果不滿足的話,就有可能出現無限遞歸,最后會導致堆棧溢出了。
滿足條件:
- 嚴格定義遞歸函數作用,包括參數,返回值,其他變量。
- 先一般情況,后特殊情況。
- 有退出條件。在一般情況下,能讓遞歸正常退出的條件。
- 每次調用必須縮小問題規模,且新問題與原問題有着相同的形式,即規律。
上面的條件一環扣一環,也可以縮減成兩個主要條件:有規律
,有退出條件
。我們以上面的條件,來結合案例進行理解。
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、1、2、3、5、8、13、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 將A柱子的1號盤子直接移動到C柱子中。
1.2 結束。 -
兩個盤子的情況:
2.1 將A柱子的1號盤子移動到B柱子。
2.2 將A柱子的2號盤子移動到C柱子。
2.3 將B柱子的1號盤子移動到C柱子。
2.4 結束。 -
三個盤子的情況:
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即可。即規律
。
當我們有三個盤子的時候,我們會發現一個問題: 角色變化
-
將A塔座的第(N-1)~1個盤子看成是一個盤子,放到中柱子B上,然后將第N個盤子放到目標柱子C上。這時候
柱子A空了!柱子A成為中介柱子,柱子B成為起始柱子
。 -
柱子B這時候有N-1個盤子,將第(N-2)~1個盤子看成是一個盤子,放到中介柱子A上,然后將柱子B的第(N-1)號盤子放到目標柱子C上。這時候
柱子B空了!柱子B又成為了中介柱子,A成為了起始柱子
!
重復1、2步驟,直到所有盤子都放到目標塔座C上結束。
總結一下:
- 從初始柱子A上移動包含n-1個盤子到中介柱子B上。
- 將初始柱子A上剩余的一個盤子(最大的一個盤子)放到目標柱子C上。
- 將中介柱子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編程
獲取最新原創技術文章和相關免費學習資料,隨時隨地學習技術知識!
參考文章:
https://www.cnblogs.com/ysocean/p/8005694.html
http://www.nowamagic.net/librarys/veda/detail/2314