沒有退出條件的遞歸代碼示例

讓我們再來看一個遞歸問題。例如,考慮計算斐波那契數列。大家應該都記得,斐波那契數列是一個數列,其中前兩個數字是0和1,後面的每個數字都等於前兩個數字的和。

讓我們編寫代碼來計算和顯示這些數字:

public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacci(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }
}

在第一次調用遞歸printFibonacci方法之前,打印序列的前兩個數字:零和一。我們需要這樣做,因為遞歸方法只顯示輸入參數的總和,而不是參數本身。

代碼看起來沒問題:我們得到兩個數字,計算它們的和,將其打印在控制台上,然後再次遞歸調用printFibonacci方法。我們將前一個數字 (previous) 和當前數字 (current) 作為參數傳遞。

實際上,代碼有兩個錯誤。如果您運行代碼,您可以看到它們。

第一個錯誤是long類型溢出。我們序列中的第 104 個數字是負數,表明long類型溢出了。

第二個錯誤是不同的。在計算了大約 12,000 個數字後,我們得到:

線程“main”中的異常 java.lang.StackOverflowError

現在是時候回憶一下 Java 中的方法調用棧是什麼了。Java 機器保留所有函數調用的記錄。為此,它使用一種稱為堆棧的特殊集合。當一個函數調用另一個函數時,Java 機器會將一個新的 StackTraceElement 壓入堆棧。當函數結束時,這個元素從棧中移除。因此,堆棧始終存儲有關函數調用堆棧當前狀態的最新信息。StackTraceElement 的文檔說它“在由於應用程序遞歸太深而發生堆棧溢出時拋出”。運行中的 JVM 有一塊特殊的內存區域用於存儲方法調用堆棧。此內存區域的大小取決於操作系統和 JVM 設置。除了方法調用棧本身,原始變量(方法參數的具體值)和引用變量的地址(在稱為“堆”的內存區域中)存儲在這個特殊的內存區域中。調用堆棧的訪問遵循後進先出原則。

具有退出條件的更正示例

讓我們從修復代碼中的第二個問題開始。

讓我們嘗試正面解決問題:如果堆棧太小,那麼讓我們把它變大。為此,使用“-Xss”標誌啟動 JVM 並指定為堆棧分配多少內存。讓我們嘗試分配 5 兆字節。這就是它在 IDEA 中的樣子:

我們設法增加了輸出的長度。現在可以計算超過 49,000 個數字的序列而不是限於大約 12,000 個數字。但在某些時候,我們仍然會收到StackOverflowError

您可以嘗試增加堆棧的大小,但這並不能解決根本問題。所以,讓我們從邏輯上尋找問題。遞歸停止時必須有一個點。換句話說,必須有一些條件來確定何時不再調用遞歸方法,從而允許調用堆棧展開。要定義這樣的條件,讓我們明確我們的目標:只要其數字小於Integer.MAX_VALUE就顯示斐波那契數列。

讓我們編寫一個新的printFibonacciWithCondition方法來考慮這個條件。我們將在 main 方法中調用新的更正方法。

public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
//        printFibonacci(0, 1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
    }
}

運行代碼後,我們看到輸出以數字1836311903結尾。這個數字之前的數字是1134903170。這些數字的總和是2_971_215_073,確實大於Integer.MAX_VALUE(2_147_483_647 )

此更改自動修復了溢出錯誤。如果您需要計算更多的系列,您將需要使用其他數據類型,例如BigInteger

遞歸下降和展開

下面一步步分析一下我們的代碼是如何執行的。為此,我們將添加一個echo方法,並在遞歸調用printFibonacciWithCondition方法之前和之後調用它。

public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        echo(true, penultimate, previous);
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
        echo(false, penultimate, previous);
    }

    private static void echo(boolean isBeforeRecursiveCall, long penultimate, long previous) {
        if (isBeforeRecursiveCall) {
            System.out.printf("Before method call with args: %d, %d. Current number = ", penultimate, previous);
        } else {
            System.out.printf("After method call with args: %d, %d\n", penultimate, previous);
        }
    }
}

該程序為我們提供了以下輸出:

0
1
在使用 args 進行方法調用之前:0、1。當前編號 = 1 在使用
args 進行方法調用之前:1、1。當前編號 = 2
在使用 args 進行方法調用之前:1、2。當前編號 = 3
在使用 args 進行方法調用之前: 2、3。當前編號 = 5
在使用 args 進行方法調用之前:3、5。當前編號 = 8 在使用
args 進行方法調用之前:5、8。當前編號 = 13
在使用 args 進行方法調用之前:8、13。當前編號 = 21
在使用 args 進行方法調用之前:13、21。當前編號 = 34 在使用
args 進行方法調用之前:21、34。當前編號 = 55
在使用 args 進行方法調用之前:34、55。當前編號 = 89
在使用 args 進行方法調用之前:55, 89. 當前人數 = 144
在使用 args 進行方法調用之前:89、144。當前編號 = 233 在
使用 args 進行方法調用之前:144、233。當前編號 = 377
在使用 args 進行方法調用之前:233、377。當前編號 = 610
在使用 args 進行方法調用之前:377, 610. 當前編號 = 987
在使用 args 進行方法調用之前:610、987。當前編號 = 1597 在使用
args 進行方法調用之前:987、1597。當前編號 = 2584 在使用
args 進行方法調用之前:1597、2584。當前編號 = 4181
在方法之前使用 args 調用:2584、4181。當前編號 = 6765
在使用 args 進行方法調用之前:4181、6765。當前編號 = 10946 在使用
args 進行方法調用之前:6765、10946。當前編號 = 17711
在使用 args 進行方法調用之前:10946、17711。當前數量 = 28657
在使用 args 進行方法調用之前:17711、28657。當前編號 = 46368
在使用 args 進行方法調用之前:28657、46368。當前編號 = 75025 在使用
args 進行方法調用之前:46368、75025。當前編號 = 121393 在使用 args 進行
方法調用之前:75025, 121393。當前號碼 = 196418
在使用參數調用方法之前:121393、196418。當前號碼 = 317811 在使用參數
調用方法之前:196418、317811。當前號碼 = 514229 在使用參數調用方法之前:317811、514229。當前
號碼 = 83204 0
方法前使用 args 調用:514229、832040。當前編號 = 1346269 在使用
args 進行方法調用之前:832040、1346269。當前編號 = 2178309 在使用
args 進行方法調用之前:1346269、2178309。當前編號 = 3524578
在使用 args 進行方法調用之前:2178309、3524578。當前編號 = 5702887 在使用
args 進行方法調用之前:3524578、5702887。當前編號 = 9227465 在使用 args 進行方法調用之前:5702887、9227465。當前編號 = 14930352 在使用 arg 進行
方法
調用之前小號:9227465, 14930352。當前號碼 = 24157817 在使用
參數調用方法
之前:14930352、24157817。當前號碼 = 39088169 在使用參數調用方法之前:24157817、39088169。當前號碼 = 63245986 在使用參數調用方法之前:39088169
, 63245986. 當前號碼 = 102334155
方法前使用 args 調用:63245986、102334155。當前號碼 = 165580141
在使用 args 方法調用之前:102334155、165580141。當前號碼 = 267914296
在使用參數調用方法之前:165580141、267914296。當前號碼 = 433494437
在使用參數調用方法之前:267914296、433494437。當前號碼 = 701408733 在使用參數調用方法之前:433494437、701408733。當前號碼
= 1134903170 在使用參數
調用方法之前:701408733, 1134903170。當前號碼 = 1836311903
使用 args 進行方法調用後:701408733、113490317
使用 args 進行方法調用後:433494437、701408733 使用 args 進行方法調用後:267914296、433494437 使用 args 進行方法調用
後gs
: 165580141, 267914296
在使用 args 調用方法後: 102334155, 165580141
使用參數調用方法後:63245986、102334155
使用參數調用方法後:39088169、63245986
帶參數的方法調用後:24157817、39088169
帶參數的方法調用後:14930352、24157817 帶
參數的方法調用後:9227465、14930352 帶參數的方法調用後:5702887、9227465 帶參數的
方法
調用後:352 4578、5702887
方法調用後帶參數: 2178309, 3524578
方法調用後參數:1346269, 2178309
方法調用後參數:832040, 1346269
方法調用後參數:514229, 832040 方法調用後參數:317811
, 514229
方法調用後: args: 196418, 317811
之後帶參數的方法調用:121393、196418
帶參數的方法調用之後:75025、121393
帶參數的方法調用之後:46368、75025
在使用 args 的方法調用之後:28657、46368
在使用 args 的方法調用之後:17711、28657 在使用
args 的方法調用之後:10946、17711 在使用
args 的方法調用之後:6765、10946 在使用 args 的
方法調用之後:4181、6765
在使用 args 的方法調用之後: 2584, 4181
方法調用後,參數:1597, 2584
方法調用後,參數:987, 1597
方法調用後,
參數:610, 987 方法調用後,參數:377, 610
方法調用後,參數:233, 377
之後帶參數的方法調用:144、233
帶參數的方法調用之後:89、144 帶
參數的方法調用之後:55、89 帶
參數的方法調用之後:34、55 帶參數的
方法調用之後:21、34
方法調用後參數:13、21
方法調用後參數:8、13
方法調用後參數:5、8
方法調用後參數:3、5
方法調用後參數:2、3
方法調用後參數: : 1, 2
方法調用後參數:1, 1
方法調用後參數:0, 1

這是正在發生的事情的可視化。

再說一遍:調用了printFibonacciWithCondition方法。它計算當前數字。如果它適合我們,那麼我們會顯示它並使用新參數再次調用printFibonacciWithCondition方法。

只要調用遞歸方法,就稱為“遞歸下降”。當遞歸終止並且方法調用返回時,我們說調用堆棧正在“展開”。

遞歸是編程中一個有趣的話題。為了更好地處理材料,讓我們稍微改變一下我們的任務。新任務是按降序輸出斐波那契數列中不超過Integer.MAX_VALUE的所有數字。我們已經編寫了此任務所需的所有代碼。剩下的就是交換顯示當前數字和調用遞歸方法的順序。也就是說,在第一個例子中,計算出的數字是在“下降”時顯示的,但現在我們需要“下降到最底部”,然後在“返回的途中”顯示數字。當然,在main方法中,我們在調用遞歸方法後顯示它們後交換序列的兩個初始數字(零和一)。為了可讀性,方法。

public class Fibonacci {
    public static void main(String[] args) {
        printFibonacciWithCondition(0, 1);
        System.out.println(1);
        System.out.println(0);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        printFibonacciWithCondition(previous, current);
        System.out.println(current);
    }
}

輸出將是:

1836311903
1134903170
701408733
433494437
267914296
165580141
102334155
63245986
39088169
24157817
14930352
9227465
5702887
3524578
2178309
1346269
832040
514229
317811
196418
121393
75025
46368
28657
17711
10946
6765
4181
2584
1597
987
610
377
233
144
89
55
34
21
13
8
5
3
2
1
1
0