【问题标题】:Recursion and the Return Keyword递归和返回关键字
【发布时间】:2015-04-27 18:23:09
【问题描述】:

我目前正在学习 Java 教程,目前正在学习递归。

我有以下代码计算传递给阶乘方法的任何数字的阶乘

public class App {
    public static void main(String[] args) {
        //E.g 4! = 4*3*2*1(factorial 4)
        System.out.println(factorial(4));

    }
    private static int factorial(int value){
        //System.out.println(value);
        if (value == 1){
            return 1;
        }
        return factorial(value - 1)*value;
    }
}

我无法理解该部分

if (value == 1){
    return 1;
}
return factorial(value - 1)*value;

我的理解是 return 关键字只是终止方法,和/或返回方法声明的相同类型的值(即 int、String 等)。

运行以下行时会发生什么?

return factorial(value - 1)*value;

函数返回 (value - 1) * value 的总和,这会给我

(3)*4 = 12
(2)*3 = 6
(1)*2 = 2

每次通过迭代。但是,System.out.println(factorial(4)); 总共给了我24。这个数字是如何从方法中得出的?没有变量来存储值的总和,那么程序将它们存储在哪里?另外,如何从这些值中获取24

(3)*4
(2)*3
(1)*2

虽然我知道24 是从4*3*2*1 派生的,但我看不出如何从上面计算出来。

任何解释将不胜感激。

【问题讨论】:

    标签: java recursion


    【解决方案1】:

    你误解了

    return factorial(value - 1)*value;
    

    相乘的值为factorial(value - 1)value。换句话说,你可以这样重写它:

    return (factorial(value - 1)) * value;
    

    所以当你通过 4 时,你会得到这个:

    factorial(3) * 4;
    

    相当于

    (factorial(2) * 3) * 4;
    

    相当于

    ((factorial(1) * 2) * 3) * 4;
    

    相当于

    1 * 2 * 3 * 4;
    

    如果您使用调试器单步执行代码,您可以很容易地看到它的工作方式,如下所示:

    1. 对函数的第一次调用传递4。该函数评估 if,然后调用自身,传递 3。 (第一个函数调用的状态保存在堆栈上,所以当这个调用返回时,我们可以从中断的地方继续,现在我们有了函数调用的结果。这个“堆栈”抽象实际上不需要理解递归。)

    2. 第二个函数调用评估 if 并调用自身,传递 2

    3. 第三个函数调用计算 if 并调用自身,传递 1
    4. 第四个函数调用计算 if 并返回 1。
    5. 然后第三个函数调用继续,将刚刚返回的函数的返回值 (1) 与其参数的值 (2) 相乘,返回结果 (2)。
    6. 然后第二个函数调用继续,将刚刚返回的函数的返回值 (2) 与其参数的值 (3) 相乘,返回结果 (6)。
    7. 然后第一个函数调用继续,将刚刚返回的函数的返回值 (6) 与其参数的值 (4) 相乘,返回结果 (24)。

    一些优化编译器会将递归调用更改为循环,但通常不可能将递归调用更改为固定表达式,例如1 * 2 * 3 * 4,因为在编译时您通常不知道递归的深度。

    如果您按如下方式修改代码,然后使用调试器单步执行,所有这些都会非常清楚:

    private static int factorial(int value){
        if (value == 1){
            return 1;
        }
        int recursiveResult = factorial(value - 1);
        return recursiveResult * value;
    }
    

    请注意,对于每个递归调用,我们必须将等待调用结果的“暂停”方法的状态存储在堆栈上。出于这个原因,如果一个方法递归地调用自己(或者一个方法链以相互递归的方式调用自己),堆栈就有可能变满。这称为堆栈溢出。这通常是由于函数中不正确的逻辑导致递归循环:

    int stackOverflow() { return stackOverflow(); }
    

    它也可能是由一个没有逻辑循环的函数引起的,但由于传递给它的数据而多次调用自身。例如,递归函数在处理树数据结构时很有用;如果树太高,可能会导致堆栈溢出。以下内容也是如此,有一些论点,但没有其他论点:

    void possibleStackOverflow(int arg) { if (arg == 0) return; possibleStackOverflow(arg - 1); }
    

    如果你调用possibleStackOverflow(10) 可能没问题,但possibleStackOverflow(-1) 会抛出异常。

    另外,由于 VM 实现的限制,调用 possibleStackOverflow(Integer.MAX_VALUE) 会抛出 StackOverflowException

    【讨论】:

    • 您好 Phoog,感谢您的回答。我说得对吗,当我运行阶乘(4)时,程序只是像您在答案中写的那样继续扩展/递归运行,直到它达到计数 1,然后它评估括号中的所有内容(1 * 2 * 3 )*4,给出答案 24?
    • 并非如此。它的作用是使用堆栈。 (在某些情况下,编译器可以用循环替换多个堆栈帧,但在这种情况下不会。)我会在我的答案中添加一点来解释。
    【解决方案2】:

    您的 return 子句返回 factorial(value-1)*value 的结果,每个 factorial(value-1) 将被方法调用的结果替换。

    这意味着阶乘(4)是:

    (阶乘(1) * (阶乘(2 -1) * (阶乘(3 -1) * 阶乘(4 - 1)))) * 4

    这将是

    (1 * (2 * 3)) * 4 即 24

    【讨论】:

    • 这不太正确。表达式 (factorial(1) * (factorial(2 -1) * (factorial(3 -1) * factorial(4 - 1)))) * 4 实际上计算为 1 * 1 * 2 * 6 * 4,即 48。
    【解决方案3】:
    return factorial(value - 1)*value;
    

    返回value - 1 乘以value阶乘

    是这样的:

    阶乘(4)

    =阶乘(3) * 4

    = 阶乘(2) * 3 * 4

    = 阶乘(1) * 2 * 3 * 4

    = 1 * 2 * 3 * 4 = 24

    【讨论】:

      【解决方案4】:

      要了解递归,请尝试绘制调用和参数树:

      factorial(4)  = 4 * factorial(4-1)
                                              |
                                      3 * factorial(3-1)
                                                     |
                                             2 * factorial(2-1)
                                                            |
                                                            1
      

      用递归斐波那契公式试试。

      【讨论】:

        【解决方案5】:

        “return”这个词确实打破了当前方法并返回了一个值/结果。但是,返回“语句”中有一个“表达式”,需要在“返回”退出之前对其进行评估。

        例如,返回 1 + 1;需要在返回之前评估操作“+”。 当你调用 return func(arguments); java 必须在返回之前调用该函数。在这种情况下,它递归地通过这个“调用堆栈”(函数调用被放在一个“堆栈”上,其中最后一个是第一个评估的)。

        所以,要真正描述这里发生的事情:

        1) return 语句识别表达式

        2) 表达式调用堆栈

        上的函数

        3) 堆栈上的函数命中另一个返回值并使用另一个函数进行评估

        4) ...

        5) 达到“基本情况”,找到“1”。

        6) 函数调用在堆栈中向下执行以评估表达式

        【讨论】:

          【解决方案6】:

          Java 使用堆栈来存储递归调用之间的信息。请参阅https://en.wikipedia.org/wiki/Call_stack 了解更多信息。

          【讨论】:

            【解决方案7】:

            您的递归方法只能以两种方式结束。 value 是 1 并返回 1,或者它使用较小的 value-1 调用自身并返回该结果与当前 value 的乘积。

            这个方法可以写成这样扩展:

            private static int factorial(int value){
                if (value == 1){
                    return 1;
                }
                int a = factorial(value - 1);
                return a * value;
            }
            

            从堆栈的角度来看,它看起来像这样。我插入了虚构变量abc 来指示递归调用返回时的值。

            factorial(4)
              |-- a = factorial(4-1=3)
              |    |-- b = factorial(3-1=2)
              |    |    |-- c = factorial(2-1=1)
              |    |    |    +-- return 1          // <-- deepest point in the stack
              |    |    +-- return c * 2 = 2
              |    +-- return b * 3 = 6
              +-- return a * 4 = 24
            

            如果你输入一个更大的起始数字,堆栈就会变得更深,反复调用factorial(value-1),直到它最终到达1,然后堆栈展开以显示答案。如果你放 much much 更大的数字,你最终会出现 stackoverflow(这个网站的名字!),因为你会运行内存不足,无法保存所有需要的变量。

            【讨论】:

              【解决方案8】:

              如我所想,返回 f();表示执行 f() 并返回 f() 的结果。在递归中,返回结果;只是在递归函数 f() 之后运行的程序语句。所以它的执行顺序与调制递归函数相反。

              【讨论】:

                猜你喜欢
                • 2012-11-28
                • 1970-01-01
                • 2014-12-25
                • 2019-09-12
                • 1970-01-01
                • 2021-12-16
                • 2014-04-18
                • 2010-10-30
                • 2018-08-16
                相关资源
                最近更新 更多