没有退出条件的递归代码示例

让我们再来看一个递归问题。例如,考虑计算斐波那契数列。大家应该都记得,斐波那契数列是一个数列,其中前两个数字是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
在方法之前使用参数调用:2584、4181。当前编号 = 6765
使用参数调用方法之前:4181、6765。当前编号 = 10946 使用参数
调用方法之前:6765、10946。当前编号 = 17711
使用参数调用方法之前: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