遞歸就這麼簡單

遞歸介紹

原本預算此章節是繼續寫快速排序的,然而編寫快速排序每每是遞歸來寫的,而且遞歸可能不是那麼好理解,因而就有了這篇文章。java

在上面提到了遞歸這麼一個詞,遞歸在程序語言中簡單的理解是:方法本身調用本身算法

遞歸其實和循環是很是像的,循環能夠改寫成遞歸,遞歸未必能改寫成循環,這是一個充分沒必要要的條件。編程

  • 那麼,有了循環,爲何還要用遞歸呢??在某些狀況下(費波納切數列,漢諾塔),使用遞歸會比循環簡單不少不少
  • 話說多了也無益,讓咱們來感覺一下遞歸吧。

咱們初學編程的時候確定會作過相似的練習:數組

  • 1+2+3+4+....+100(n)求和
  • 給出一個數組,求該數組內部的最大值

咱們要記住的是,想要用遞歸必須知道兩個條件:bash

  • 遞歸出口(終止遞歸的條件)
  • 遞歸表達式(規律)

技巧:在遞歸中經常是將問題切割成兩個部分(1和總體的思想),這可以讓咱們快速找到遞歸表達式(規律)微信

1、求和

若是咱們使用for循環來進行求和1+2+3+4+....+100,那是很簡單的:編程語言

int sum = 0;
    for (int i = 1; i <= 100; i++) {

        sum = sum + i;

    }
    System.out.println("公衆號:Java3y:" + sum);
複製代碼

前面我說了,for循環均可以使用遞歸來進行改寫,而使用遞歸必需要知道兩個條件:一、遞歸出口,二、遞歸表達式(規律)測試

首先,咱們來找出它的規律:1+2+3+...+n,這是一個求和的運算,那麼咱們能夠假設X=1+2+3+...+n,能夠將1+2+3+...+(n-1)當作是一個總體。而這個總體作的事又和咱們的**初始目的(求和)**相同。以咱們的高中數學知識咱們又能夠將上面的式子當作X=sum(n-1)+nspa

好了,咱們找到咱們的遞歸表達式(規律),它就是sum(n-1)+n,那遞歸出口呢,這個題目的遞歸出口就有不少了,我列舉一下:3d

  • 若是n=1時,那麼就返回1
  • 若是n=2時,那麼就返回3(1+2)
  • 若是n=3時,那麼就返回6(1+2+3)

固然了,我確定是使用一個最簡單的遞歸出口了:if(n=1) return 1

遞歸表達式和遞歸出口咱們都找到了,下面就代碼演示:

遞歸出口爲1:

public static void main(String[] args) {
        System.out.println("公衆號:Java3y:" + sum(100));
    }

    /** * * @param n 要加到的數字,好比題目的100 * @return */
    public static int sum(int n) {
        
        if (n == 1) {
            return 1;
        } else {
            return sum(n - 1) + n;
        }
    }

複製代碼

遞歸出口爲4:

public static void main(String[] args) {
        System.out.println("公衆號:Java3y:" + sum(100));
    }

    /** * * @param n 要加到的數字,好比題目的100 * @return */
    public static int sum(int n) {

        //若是遞歸出口爲4,(1+2+3+4)
        if (n == 4) {
            return 10;
        } else {
            return sum(n - 1) + n;
        }
    }

複製代碼

結果都是同樣的。

2、數組內部的最大值

若是使用的是循環,那麼咱們一般這樣實現:

int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2};

    //將數組的第一個假設是最大值
    int max = arrays[0];

    for (int i = 1; i < arrays.length; i++) {

        if (arrays[i] > max) {
            max = arrays[i];
        }
    }

    System.out.println("公衆號:Java3y:" + max);
複製代碼

那若是咱們用遞歸的話,那怎麼用弄呢?首先仍是先要找到遞歸表達式(規律)和遞歸出口

  • 咱們又能夠運用1和總體的思想來找到規律
    • 將數組第一個數->2與數組後面的數->{3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2}進行切割,將數組後面的數當作是一個總體X={3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2},那麼咱們就能夠當作是第一個數和一個總體進行比較if(2>X) return 2 else(2<X) return X
    • 而咱們要作的就是找出這個總體的最大值與2進行比較。找出總體的最大值又是和咱們的**初始目的(找出最大值)**是同樣的
    • 也就能夠寫成if( 2>findMax() )return 2 else return findMax()
  • 遞歸出口,若是數組只有1個元素時,那麼這個數組最大值就是它了。

使用到數組的時候,咱們一般爲數組設定左邊界和右邊界,這樣比較好地進行切割

  • L表示左邊界,每每表示的是數組第一個元素,也就會賦值爲0(角標爲0是數組的第一個元素)
  • R表示右邊界,每每表示的是數組的長度,也就會賦值爲arrays.length-1(長度-1在角標中才是表明最後一個元素)

那麼能夠看看咱們遞歸的寫法了:

public static void main(String[] args) {

        int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};

        System.out.println("公衆號:Java3y:" + findMax(arrays, 0, arrays.length - 1));


    }
  

    /** * 遞歸,找出數組最大的值 * @param arrays 數組 * @param L 左邊界,第一個數 * @param R 右邊界,數組的長度 * @return */

    public static int findMax(int[] arrays, int L, int R) {

        //若是該數組只有一個數,那麼最大的就是該數組第一個值了
        if (L == R) {
            return arrays[L];
        } else {

            int a = arrays[L];
            int b = findMax(arrays, L + 1, R);//找出總體的最大值

            if (a > b) {
                return a;
            } else {
                return b;
            }
        }

    }
複製代碼

3、冒泡排序遞歸寫法

在冒泡排序章節中給出了C語言的遞歸實現冒泡排序,那麼如今咱們已經使用遞歸的基本思路了,咱們使用Java來重寫一下看看:

冒泡排序:倆倆交換,在第一趟排序中可以將最大值排到最後面,外層循環控制排序趟數,內層循環控制比較次數

以遞歸的思想來進行改造:

  • 當第一趟排序後,咱們能夠將數組最後一位(R)和數組前面的數(L,R-1)進行切割,數組前面的數(L,R-1)當作是一個總體,這個總體又是和咱們的**初始目的(找出最大值,與當前趟數的末尾處交換)**是同樣的
  • 遞歸出口:當只有一個元素時,即不用比較了:L==R
public static void main(String[] args) {

        int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};
        bubbleSort(arrays, 0, arrays.length - 1);
        System.out.println("公衆號:Java3y:" + arrays);


    }

    public static void bubbleSort(int[] arrays, int L, int R) {

        int temp;

        //若是隻有一個元素了,那什麼都不用幹
        if (L == R) ;

        else {
            for (int i = L; i < R; i++) {
                if (arrays[i] > arrays[i + 1]) {
                    temp = arrays[i];
                    arrays[i] = arrays[i + 1];
                    arrays[i + 1] = temp;
                }
            }

            //第一趟排序後已經將最大值放到數組最後面了

            //接下來是排序"總體"的數據了
            bubbleSort(arrays, L, R - 1);

        }
    }


複製代碼

4、斐波那契數列

接觸過C語言的同窗極可能就知道什麼是費波納切數列了,由於每每作練習題的時候它就會出現,它也是遞歸的典型應用。

菲波那切數列長這個樣子:{1 1 2 3 5 8 13 21 34 55..... n }

數學好的同窗可能很容易就找到規律了:前兩項之和等於第三項

例如:

1 + 1 = 2
	2 + 3 = 5
	13 + 21 = 34
複製代碼

若是讓咱們求出第n項是多少,那麼咱們就能夠很簡單寫出對應的遞歸表達式了:Z = (n-2) + (n-1)

遞歸出口在本題目是須要有兩個的,由於它是前兩項加起來才得出第三項的值

一樣地,那麼咱們的遞歸出口能夠寫成這樣:if(n==1) retrun 1 if(n==2) return 2

下面就來看一下完整的代碼吧:

public static void main(String[] args) {

        int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
        //bubbleSort(arrays, 0, arrays.length - 1);

        int fibonacci = fibonacci(10);
        System.out.println("公衆號:Java3y:" + fibonacci);


    }

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

    }

複製代碼

5、漢諾塔算法

圖片來源百度百科:

玩漢諾塔的規則很簡單:

  • 有三根柱子,原始裝滿大小不一的盤子的柱子咱們稱爲A,還有兩根空的柱子,咱們分別稱爲B和C(任選)
  • 最終的目的就是將A柱子的盤子所有移到C柱子中
    • 移動的時候有個規則:一次只能移動一個盤子,小的盤子不能在大的盤子上面(反過來:大的盤子不能在小的盤子上面)

咱們下面就來玩一下:

  • 只有一個盤子:
    • A柱子的盤子直接移動到C柱子中
    • 完成遊戲
  • 只有兩個盤子:
    • 將A柱子上的盤子移動到B柱子中
    • 將A柱子上的盤子移動到C柱子中
    • 最後將在B柱子的盤子移動到C柱子盤子中
    • 完成遊戲
  • 只有三個盤子:
    • 將A柱子的盤子移動到C柱子中
    • 將A柱子上的盤子移動到B柱子中
    • 將C柱子盤子移動到B柱子盤子中
    • 將A柱子的盤子移動到C柱子中
    • 將B柱子的盤子移動到A柱子中
    • 將B柱子的盤子移動到C柱子中
    • 最後將A柱子的盤子移動到C柱子中
    • 完成遊戲

.......................

從前三次玩法中咱們就能夠發現的規律:

  • 想要將最大的盤子移動到C柱子,就必須將其他的盤子移到B柱子處(藉助B柱子將最大盤子移動到C柱子中[除了最大盤子,將全部盤子移動到B柱子中])[遞歸表達式]
  • 當C柱子有了最大盤子時,全部的盤子在B柱子。如今的目的就是藉助A柱子將B柱子的盤子都放到C柱子中(和上面是同樣的,已經發生遞歸了)
  • 當只有一個盤子時,就能夠直接移動到C柱子了(遞歸出口)
    • A柱子稱之爲起始柱子,B柱子稱之爲中轉柱子,C柱子稱之爲目標柱子
    • 從上面的描述咱們能夠發現,起始柱子、中轉柱子它們的角色是會變的(A柱子開始是起始柱子,第二輪後就變成了中轉柱子了。B柱子開始是目標柱子,第二輪後就變成了起始柱子。總之,起始柱子、中轉柱子的角色是不停切換的)

簡單來講就分紅三步:

  1. 把 n-1 號盤子移動到中轉柱子
  2. 把最大盤子從起點移到目標柱子
  3. 把中轉柱子的n-1號盤子也移到目標柱子

那麼就能夠寫代碼測試一下了(回看上面玩的過程):

public static void main(String[] args) {

        int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
        //bubbleSort(arrays, 0, arrays.length - 1);

        //int fibonacci = fibonacci(10);

        hanoi(3, 'A', 'B', 'C');

        System.out.println("公衆號:Java3y" );


    }

    /**
     * 漢諾塔
     * @param n n個盤子
     * @param start 起始柱子
     * @param transfer 中轉柱子
     * @param target 目標柱子
     */
    public static void hanoi(int n, char start, char transfer, char target) {


        //只有一個盤子,直接搬到目標柱子
        if (n == 1) {
            System.out.println(start + "---->" + target);
        } else {

            //起始柱子藉助目標柱子將盤子都移動到中轉柱子中(除了最大的盤子)
            hanoi(n - 1, start, target, transfer);
            System.out.println(start + "---->" + target);

            //中轉柱子藉助起始柱子將盤子都移動到目標柱子中
            hanoi(n - 1, transfer, start, target);

        }
    }

複製代碼

咱們來測試一下看寫得對不對:

參考資料:

6、總結

遞歸的確是一個比較難理解的東西,好幾回都把我繞進去了....

要使用遞歸首先要知道兩件事:

  • 遞歸出口(終止遞歸的條件)
  • 遞歸表達式(規律)

在遞歸中經常用」總體「的思想,在漢諾塔例子中也不例外:將最大盤的盤子當作1,上面的盤子當作一個總體。那麼咱們在演算的時候就很清晰了:將」總體「搬到B柱子,將最大的盤子搬到C柱子,最後將B柱子的盤子搬到C柱子中

由於咱們人腦沒法演算那麼多的步驟,遞歸是用計算機來乾的,只要咱們找到了遞歸表達式和遞歸出口就要相信計算機能幫咱們搞掂。

在編程語言中,遞歸的本質是方法本身調用本身,只是參數不同罷了。

最後,咱們來看一下若是是5個盤子,要運多少次才能運完:

PS:若是有更好的理解方法,或者我理解錯的地方你們能夠在評論下留言一塊兒交流哦!

若是文章有錯的地方歡迎指正,你們互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:Java3y

相關文章
相關標籤/搜索