Java方法的嵌套與遞歸調用

Java方法的嵌套與遞歸調用

本文關鍵字:方法、嵌套、遞歸、經典問題

一、方法的嵌套

1. 概念解讀

方法嵌套的概念其實比較好理解,就是在調用方法的過程中又遇到了方法的調用,在剛開始接觸的時候雖然在邏輯上能夠理解爲什麼運行結果是這樣的,但是對於代碼執行的過程還是感覺有些繞。

2. 方法嵌套

在編程中最常見的就是方法與方法之間的調用嵌套,因爲通常情況下,我們解決一個問題不會只靠一個方法。而且如果一個方法所提供的功能十分強大,那勢必其中的代碼邏輯和參數列表也會變的相對複雜,不利於修改和使用,所以我們希望,每個方法都是一個個小小的利刃,用來解決特定的問題,通過組合使用的方式來完成一個較爲複雜的功能,就像雷恩的七星刀一樣。
Java方法的嵌套與遞歸調用
比如,我們已經有了兩個方法:分別用於計算圓的面積和計算矩形的面積,如果我們現在需要算一個圓柱的表面積,我們還需要把整個方法重寫一遍嗎?當然不需要,因爲圓柱的表面積的計算剛好可以通過兩個圓柱底面積(圓)加圓柱側面積(矩形)得到,我們只需要合理的傳入參數和進行值的返回即可實現。

public class Test{
    public static void main(String[] args){
        // 計算一個圓柱的面積,已知底面半徑和高
        int radius = 5;
        int height = 10;
        // 調用計算圓柱表面積
        double area = getColumnArea(radius,height);
        // 輸出結果
        System.out.println("圓柱的表面積爲:" + area);
    }
    public static double getCircleArea(double radius){
        // 根據圓的半徑計算面積後返回
        return Math.PI * Math.pow(radius,2);
    }
    public static double getRectangleArea(double width,double height){
        // 根據寬和高計算面積後返回
        return width * height;
    }
    public static double getColumnArea(double radius,double height){
        // 計算得到底面積 -> 剛好是側面積的寬
        double baseArea = getCircleArea(radius);
        // 計算得到側面積
        double lateralArea = getRectangleArea(baseArea,height);
        // 根據底面積及側面積計算後返回
        return baseArea * 2 + lateralArea;
    }
}

那麼,整個方法的執行過程是怎樣的呢?其實依然是個順序結構,當一個被調用的方法完全執行後纔會繼續進行後續的步驟,我們可以將這個過程描述如下:
Java方法的嵌套與遞歸調用

3. 構造嵌套

在之前的文章中已經向大家介紹了構造器的重載,可以適用於對不同個數的屬性進行初始化,直擊傳送門:Java初始化對象的工具 - 構造器。但是在使用時我們會發現一個問題,構造器的主要用途是爲屬性賦值,但是在構造器重載時會發現,一樣有代碼的冗餘,會出現爲很多相同的賦值語句,作爲強迫症的重度患者,這是不能忍受的,看下面的例子:

public class Person{
    // 一參構造器
    public Person(String name){
        this.name = name;
    }
    // 兩參構造器,可以給name和age屬性賦值
    public Person(String name,int age){
        this.name = name;
        this.age = age;
    }
    // 三參構造器,可以給name、age和job屬性賦值
    public Person(String name,int age,String job){
        this.name = name;
        this.age = age;
        this.job = job;
    }
    public String name;
    public int age;
    public String job;
}

在上面的例子中一共定義了三個構造器,分別滿足不同的初始化需要(當然,我們還可以定義的更多),但是可以發現很多賦值語句都是重複的,我們可以通過構造器互相調用的方式來減少代碼量。在當前類中構造器進行相互調用,使用this()的方式來完成,括號中填入相應的參數,修改後代碼如下。

public class Person{
    // 一參構造器
    public Person(String name){
        this.name = name;
    }
    // 兩參構造器,可以給name和age屬性賦值
    public Person(String name,int age){
        this(name);
        this.age = age;
    }
    // 三參構造器,可以給name、age和job屬性賦值
    public Person(String name,int age,String job){
        this(name,age);
        this.job = job;
    }
    public String name;
    public int age;
    public String job;
}

假如在測試類中使用三參構造器來初始化一個Person對象:Person person = new Person("小張",25,”工程師“);則執行過程如下:
Java方法的嵌套與遞歸調用

二、方法的遞歸

1. 概念解讀

遞歸是一種計算過程或方法,是一種將問題分解爲同類的子問題來解決問題的方法,那麼什麼是同類子問題呢?就是對一個大問題進行拆解,而得到的子問題又是同一規則,或同一種操作,比如最簡單的階乘計算。假如我們需要計算4的階乘,直接用數學的方式寫出來是4! = 4 x 3 x 2 x 1。
那麼我們如何用計算機解決這個問題呢?當然,我們可以使用循環,從給定的數一直乘到1爲止:

public class Test{
    public static void main(String[] args){
        int n = 4;
        int result = 1;
        for(int i = n;i <= 1;i--){
            result *= i;
        }
        System.out.println(result);
    }
}

但是其實這可以總結或者分解爲一個規律:n! = n x (n - 1)!,n ≥ 2;n! = 1,n = 1。那這和循環又有什麼區別呢?區別在於我們在使用循環時,我們自己將這個計算過程完全翻譯成了計算機可以讀懂和直接執行的代碼,而卻沒有了原本的意義,並且在某些情況下,並不是所有問題都可以通過循環結構實現。另外一方面,計算理論可以證明遞歸的作用可以完全取代循環,但是出於性能的考慮,我們也不會刻意的用遞歸去代替循環,而更偏向於使用遞歸去解決某一類特定的問題。

2. 遞歸思想

從上面的介紹中可以看到,我們希望通過遞歸的思想盡量的貼近原有問題的描述,並能將問題很好的解決。從代碼的角度來看,遞歸方法一句話來概括就是:<font color="red">自己調用自己</font>。爲什麼這麼說呢?因爲整個的執行過程都是通過重複一個步驟來實現的,每一步結果的產生都來自於上一步或前一步。那麼問題就來了,什麼時候是個頭呢?這就引出了一個概念:遞歸的出口
就像循環需要有一個終止條件一樣,遞歸在不斷的調用自己,去獲取自己所需要的結果,那同樣要有一個終止條件,這個條件的設定通常比較明顯,那就是能得到一個確切的結果時,就不需要再進行遞歸調用了,此時直接將具體結果返回就可以了,比如我們使用遞歸去實現階乘:

public class Test{
    public static void main(String[] args){
        int n = 4;
        int result = getFactorial(n);
        System.out.println(result);
    }
    // 定義一個方法,用於計算n的階乘,不考慮n < 0的情況
    public static int getFactorial(int n){
        // 遞歸的出口
        // 描述當n = 1時,階乘的結果爲1,直接返回確定的結果
        if(n == 1){
            return 1;
        }else{
            // 根據規律,此時應該先獲取到n - 1的階乘的結果
            // 描述當n ≥ 2時,n! = n x (n - 1)!
            return n * getFactorial(n - 1);
        }
    }
}

當我們整理出一個公式或描述出一個規律之後,我們可以嘗試按照如下思路進行思考:

  • 首先需要確定遞歸的出口,也就是判斷條件,通常出口即爲:能夠得到確定值時傳入參數的取值
  • 接下來就是確定出口的內容,也就是符合判斷條件時,得到的確定值
  • 最後就是遞歸調用的部分,根據總結出的規律,用表達式表述出來

3. 執行過程

如果大家理解了這個分解的過程,那麼我們已經從代碼上實現了這個描述,當n = 1時,直接就可以得到確定的結果:1;當n ≥ 2時,通過遞歸調用(調用自己),將n - 1作爲參數傳入,代表想要獲取n - 1的遞歸的值,將n - 1傳入後,如果不能得到確定的結果,就會繼續調用,那麼整體的運算過程可以用下圖來表示:
Java方法的嵌套與遞歸調用

4. 經典問題

  • 斐波那契數列

斐波那契數列是一個很經典的數列,第一項爲1,第二項爲1,從第三項開始,每一項的值都是前兩項的和,用數學的方式整理一下就是:當n = 1或n = 2時,f(n) = 1;當n ≥ 3時,f(n) = f(n - 1) + f(n - 2)。按照之前的步驟,我們可以確定出口爲n = 1或n = 2,得到的確定值爲:1,遞歸調用的部分即爲:f(n - 1) + f(n - 2),據此寫出程序:

public class Test{
    public static void main(String[] args){
        int n = 5;// 自定義一個正整數n
        int result = f(n);
        System.out.println(result);
    }
    public static int f(int n){
        // 遞歸出口:當n = 1或n = 2時終止調用,得到確定的值
        if(n == 1 || n == 2){
            return 1;
        }else{
            // 自第三項開始,結果爲前兩項的加和
            return f(n - 1) + f(n - 2);
        }
    }
}
  • 楊輝三角

楊輝三角是一個很有趣的圖形,一個金字塔的構圖,頂部和兩側的值固定爲1,此時你應該想到什麼?沒錯,遞歸出口!其他部分的值爲上一層中與它最鄰近的兩個值的加和,如:自頂向下(第4層,第3列),它的值爲(第3層,第2列) + (第3層,第3列)。
Java方法的嵌套與遞歸調用
如果我們用變量i代表層,j代表這一層的列,那麼(i,j)的值爲(i - 1,j - 1) + (i - 1,j),這是什麼?沒錯,規律的描述!最後我們只需要搞定遞歸出口的判定條件,一切就大功告成啦!由上面的構圖我們知道,每一層的元素的個數,不會超過這一層的層數,並且剛好相等,所以你知道頂部和兩側該如何描述了嗎?

public class Test{
    public static void main(String[] args){
        int i = 4;// 定義一個正整數i
        int j = 3;// 定義一個正整數j,大小不能超過i
        int result = getN(i,j);
        System.out.println(result);
    }
    public static int getN(int i,int j){
        // 第1層和第1列的值固定爲1,最後一列的值也固定爲1
        if(i == 1 || j == 1 || j == i){
            return 1;
        }else{
            // 使用表達式描述規律
            return getN(i - 1,j - 1) + getN(i - 1,j);
        }
    }
}

以爲這就完了?怎麼會!既然碰到了這麼美麗的圖形,不通過程序打印出來如何能消心頭之癢!與獲得單個的數值不同,打印時要求輸入的是想要顯示的層數,那麼我們就要用到雙重for循環來構建出整個圖形了:

public class Test{
    public static void main(String[] args){
        int n = 5;// 定義一個正整數n
        print(n);
    }
    public static void print(int n){
        for(int i = 1;i <= n;i ++){
            // 在每行前插入空格,空格數量與目標層數相關
            for (int j = i;j <= n;j++){
                System.out.print(" ");
            }
            for (int j = 1;j <= i;j++){
                // 輸出後進行留空
                System.out.print(getN(i,j) + " ");
            }
            // 打印一層後換行
            System.out.println();
        }
    }
    public static int getN(int i,int j){
        // 第1層和第1列的值固定爲1,最後一列的值也固定爲1
        if(i == 1 || j == 1 || j == i){
            return 1;
        }else{
            // 使用表達式描述規律
            return getN(i - 1,j - 1) + getN(i - 1,j);
        }
    }
}

運行結果如下:
Java方法的嵌套與遞歸調用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章