1. 程式人生 > >從“數學歸納法”到理解“遞迴演算法”!

從“數學歸納法”到理解“遞迴演算法”!


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

前言

相信大家在面試或者工作中偶爾會遇到遞迴演算法的提問或者程式設計,我們今天來聊一聊從數學歸納法到理解遞迴演算法。如有錯誤還請大家及時指出~

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

1. 數學歸納法

1.1 簡介

來源百度百科

數學歸納法(Mathematical Induction, MI)是一種數學證明方法,通常被用於證明某個給定命題在整個(或者區域性)自然數範圍內成立。除了自然數以外,廣義上的數學歸納法也可以用於證明一般良基結構,例如:集合論中的樹。這種廣義的數學歸納法應用於數學邏輯和電腦科學領域,稱作結構歸納法。在數論中,數學歸納法是以一種不同的方式來證明任意一個給定的情形都是正確的(第一個,第二個,第三個,一直下去概不例外)的數學定理。雖然數學歸納法名字中有“歸納”,但是數學歸納法並非不嚴謹的歸納推理法,它屬於完全嚴謹的演繹推理法。事實上,所有數學證明都是演繹法。

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

1.2 推演步驟

簡單瞭解數學歸納法的概念後,我們來看看數學歸納法的推演步驟。

我們知道數學歸納法用來證明任意一個給定的情形都是正確的,也就是說,第一個,第二個,一直到所有情形,概不例外。

其證明步驟如下:

  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、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.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成為了起始柱子!

重複1、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程式設計
獲取最新原創技術文章和相關免費學習資料,隨時隨地學習技術知識!

參考文章:

https://www.cnblogs.com/ysocean/p/8005694.html

http://www.nowamagic.net/librarys/veda/detail/2314

推薦閱讀

一篇帶你讀懂TCP之“滑動視窗”協議

帶你瞭解資料庫中JOIN的用法

深入淺出瞭解“裝箱與拆箱”

相關推薦

數學歸納法”到理解演算法

每章一點正能量:人的一生可能燃燒也可能腐朽。 前言 相信大家在面試或者工作中偶爾會遇到遞迴演算法的提問或者程式設計,我們今天來聊一聊從數學歸納法到理解遞迴演算法。如有錯誤還請大家及時指出~ 本文已同步至 GitHub/Gitee/公眾號,感興趣的同學幫忙點波關注~ 1. 數學歸納法 1.1 簡

數學歸納法函式

1.什麼是數學歸納法? 數學歸納法用於證明在自然數上的一些斷言是否成立。 怎麼證明斷言對所有自然數成立? 第一步:證明N=1是成立的 第二步:證明N>1時,如果對於N-1成立,那麼對於N成立 (第二步不是直接證明,而是先假設N-1成立,再利用這個結論證明N是成立的) 例

怎麼更好地終極理解演算法【轉】

遞迴真是個奇妙的思維方式。對一些簡單的遞迴問題,我總是驚歎於遞迴描述問題和編寫程式碼的簡潔。但是總感覺沒能融會貫通地理解遞迴,有時嘗試用大腦去深入“遞迴”,層次較深時便常產生進不去,出不來的感覺。這種狀態也導致我很難靈活地運用遞迴解決問題。有一天,我看到一句英文:“To

徹底理解的本質說起

遍歷二叉樹,是學習樹這種資料結構首先要理解的一種基本操作。比較簡單地方式就是用遞迴去遍歷,鑑於遞迴這種呼叫方法有一定的特殊性,今天還是想來講講怎麼去理解遞迴遍歷。本文針對想理解遞迴的過程的朋友,因為本人在學到這一部分的時候也糾結了很久,其實只要理解了過程,那以後寫遞迴的程式碼

演算法-分治】陣列中取出n個元素的所有組合(需要深入理解

本文為轉載,原文章出處http://www.cnblogs.com/shuaiwhu/archive/2012/04/27/2473788.html 如陣列為{1, 2, 3, 4, 5, 6},那麼從它中取出3個元素的組合有哪些,取出4個元素的組合呢? 比如取3個元素

漢諾塔演算法理解及實現

漢諾塔:(Hanoi)是一種玩具,如圖: 從左到右 A B C 柱 大盤子在下, 小盤子在上, 藉助B柱將所有盤子從A柱移動到C柱, 期間只有一個原則: 大盤子只能在小盤子的下面. 問題理解與描述: 1.問題的理解與描述 問題的形式化表示為:

“漢諾塔”看演算法

  遞迴演算法是《資料結構與演算法》中最簡潔的演算法之一,它可以非常簡明地描述“減而治之”(decrease and conquer)和“分而治之”(divide and conquer)這兩種演算法思想。遞迴演算法雖然從程式碼角度來看非常簡單,但對於新手理解起

漫談的思想 用歸納法理解

為什麼要用遞迴 程式設計裡面估計最讓人摸不著頭腦的基本演算法就是遞迴了。很多時候我們看明白一個複雜的遞迴都有點費時間,尤其對模型所描述的問題概念不清的時候,想要自己設計一個遞迴那麼就更是有難度了。 很多不理解遞迴的人(今天在csdn裡面看到一個初學者的留言),總認為遞迴完全沒必要,用迴圈就

使用演算法給定樹上任意幾個節點將這幾個節點的所有下級 和 上級返回出來

首先,建立名稱為Node的節點類,用來存放節點屬性import java.util.ArrayList; public class Node { public String ID = null; //節點id public String PID = null; //父節點id publ

7-9 採用後序遍歷非演算法輸出根節點到每個葉子節點的路徑逆序列

//採用後序遍歷非遞迴演算法輸出從根節點到每個葉子節點的路徑逆序列 #include "btree.cpp" typedef struct { BTNode *data[MaxSize]; //存放棧中的資料元素 int top; //存放棧頂指標,即棧頂

《程式設計師的數學》:漢諾塔問題(Hanoi問題)的演算法與非演算法總結

從被呼叫函式返回呼叫函式前,系統也應完成3件事: ①儲存被呼叫函式的結果; ②釋放被呼叫函式的資料區; ③依照被呼叫函式儲存的返回地址將控制轉移到呼叫函式。 當有多個函式構成巢狀呼叫時,按照“後呼叫先返回”的原則(LIFO),上述函式之間的資訊傳遞和控制轉移必須通過“棧”來實現,即系統將整個程式執行時所需的

java資料結構與演算法思維(讓我們更通俗地理解)

關聯文章:   本篇是資料結構與演算法的第6篇,從這篇種我們將深入瞭解遞迴演算法,這可能是一篇分水嶺的博文,因為只有在理解遞迴的基礎上,我們才可能更輕鬆地學習樹的資料結構,實際上資料結構系列書籍中遞歸併沒有講得特別通俗易懂,博主目前看過的書籍中分析遞迴

求和的三種方式理解思想

#include <stdio.h> int f1(int *a, int begin, int end) //折半遞迴求和 { int mid; if(be

遞迴演算法問題: 有一對兔子,出生後第3個月起每個月都生一對兔子,小兔子長到第三個月後每個月又生一對兔子,假如兔子都不會死。問: 第20個月的兔子總對數為多少?(提示:先分析兔子的增長規律)

遞迴演算法問題: 有一對兔子,從出生後第3個月起每個月都生一對兔子,小兔子長到第三個月後每個月又生一對兔子,假如兔子都不會死。問: 第20個月的兔子總對數為多少?(提示:先分析兔子的增長規律) 程式碼

演算法理解方式

對於遞迴演算法相信有很多的同學會有這樣的疑惑,這裡先舉一個例子(通過“遞迴”與“迴圈”的對比)來增加理解。 我們一定要記住遞迴是可以來實現我們對於重複的子問題進行呼叫,但是需要注意的是我們不斷地遞迴進行下去一定要一步一步的回去,就像我們走進一間房子,開啟第一個大門後發現有第

二叉樹遍歷之演算法

作者:石鍋拌飯  原文連結 二叉樹的遍歷演算法有多種,典型的有先序遍歷、中序遍歷、後序遍歷以及層序遍歷。而且這些遍歷的遞迴演算法較為簡單,程式碼很少,容易實現,本文就是彙總二叉樹遍歷的遞迴演算法,非遞迴演算法將在下一篇文章中進行總結。本文中用到的二叉樹例項如下:

Python漢諾塔問題演算法與程式

漢諾塔問題: 問題來源:漢諾塔來源於印度傳說的一個故事,上帝創造世界時作了三根金剛石柱子,在一根柱子上從上往下從小到大順序摞著64片黃金圓盤。上帝命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一回只能移動一個圓盤,只能移動在最頂端的圓盤。有預言說

2018.10.31 演算法總結

///十進位制轉二進位制 void dectobin( int n ) { if(n==0) return; dectobin(n/2); printf("%d",n%2); } ///遞迴求斐波那契數列 int fib(int n) { if(n==1 ||

[計算機程式設計C++] Fibonaci數列的與非演算法實現

本文是對西安交通大學C++慕課第三章程式設計練習的16題的講解。 參考部落格:https://blog.csdn.net/zombie_slicer/article/details/38871799 題目內容: 編寫程式,顯示Fibonaci序列的前n項(從

【資料結構週週練】016 利用演算法及孩子兄弟表示法建立樹、遍歷樹並求樹的深度

一、前言 從今天起,就給大家分享一些樹的程式碼啦,不僅僅是二叉樹,我們要弄明白,普通的樹用資料結構怎麼儲存,它有哪些操作,它可以實現哪些功能? 可能大家要問了,二叉樹不是還沒有寫完嗎,線索二叉樹呢?二叉排序樹呢?平衡二叉樹呢?大家不要急,我們通過二叉樹來入門樹的演算法及程式碼實現,然後學