Java 递归:幕后

Java 递归:幕后,java,recursion,Java,Recursion,众所周知,递归是“一种调用自身的方法”,但我倾向于想知道实际发生了什么。以经典的阶乘为例: public static int fact(int n) { if(n == 0) return 1; else return n * fact(n - 1); } 事实(5) 我知道它是这样的:(等号表示为该值调用函数时发生的情况) 为什么递归的功能是这样的?计算机的哪一方面使它能像这样通过自身反向工作?幕后发生了什么 作为一名学生,我觉得我们所学的

众所周知,递归是“一种调用自身的方法”,但我倾向于想知道实际发生了什么。以经典的阶乘为例:

public static int fact(int n) {
    if(n == 0)
        return 1;
    else
        return n * fact(n - 1);
}
事实(5)

我知道它是这样的:(等号表示为该值调用函数时发生的情况)

为什么递归的功能是这样的?计算机的哪一方面使它能像这样通过自身反向工作?幕后发生了什么


作为一名学生,我觉得我们所学的递归是肤浅而笼统的。我希望这里的优秀社区能够帮助我从机器本身的角度来理解它。谢谢大家!

如果跟踪函数调用,您将看到它是如何工作的

例如

fact(3)
将返回
3*fact(2)
。因此java将调用
fact(2)

fact(2)
将返回
2*fact(1)
。因此java将调用
fact(1)

事实(1)
将返回
1*事实(0)
。因此java将调用
fact(0)

事实(0)
将返回
1

然后
fact(1)
将返回
1*1=1

然后
fact(2)
将返回
2*1=2

然后
fact(3)
将返回
3*2=6



Java像调用任何其他方法一样调用递归方法

以下是调用方法时发生的情况的简要概述:

  • 从堆栈中为该方法分配一个帧
  • 框架包含方法的所有局部变量、参数和返回值
  • 该框架放置在调用此方法的当前方法的框架顶部
  • 当该方法返回时,与该方法相关的帧从堆栈中弹出,并且调用方法返回到操作中,如果有返回值,则从上一个方法获取返回值
您可以在此处了解有关框架的更多信息-

在递归的情况下,同样的事情也会发生。就目前而言,请忘记您正在处理递归,并将每个递归调用视为对不同方法的调用。因此,在
factorial
情况下,堆栈的增长如下:

fact(5)
  5 * fact(4)
    4 * fact(3)
      3 * fact(2)
        2 * fact(1) 
          1 * fact(0)  // Base case reached. Stack starts unwinding.
        2 * 1 * 1
      3 * 2 * 1 * 1
    4 * 3 * 2 * 1 * 1
  5 * 4 * 3 * 2 * 1 * 1  == Final result

您可能听说过一种叫做“堆栈”的东西,它是用来存储方法状态的

我相信它还存储了调用线路,这样函数就可以返回到它的调用者

因此,假设您调用了一个递归函数

 - int $input = 5
 - stack.Push L
 - GOTO FOO
 - Label L
您的递归函数(没有基本情况)可能类似于以下内容

 - Label FOO
 - int in = $input 
 - input = in - 1
 - stack.Push in
 - stack.Push L2
 - goto FOO
 - Label L2
 - in = stack.Pop in
 - output *= in
 - goto stack.POP

也许下面的内容可以帮助你理解。计算机不在乎他是否调用了相同的函数,它只是在计算。一旦你了解了递归是什么,以及它为什么能处理很多事情,比如列表、自然数等,它们本身就是结构递归的,那么递归就没有什么神奇之处了

  • 定义:0的阶乘是1
  • 定义:大于0的数n的阶乘是该数与其前一个数的阶乘的乘积
  • 因此

    所以,如果你听说过归纳法证明,它是这样的:

    fact(5)
      5 * fact(4)
        4 * fact(3)
          3 * fact(2)
            2 * fact(1) 
              1 * fact(0)  // Base case reached. Stack starts unwinding.
            2 * 1 * 1
          3 * 2 * 1 * 1
        4 * 3 * 2 * 1 * 1
      5 * 4 * 3 * 2 * 1 * 1  == Final result
    
  • 我们证明了一个基本情形的一些性质
  • 我们证明,如果这个性质对n是真的,那么它对n的后继者也是真的
  • 我们的结论是,这证明了该属性适用于基本情况和所有后续情况
  • 示例:通过归纳法证明偶数的平方是4的倍数

  • 0的平方是0,是4的倍数
  • 设n为偶数,其平方n²为4的倍数。然后
    (2+n)*(2+n)
    =
    4+2n+2n+n²
    。这是4的倍数,因为根据我们的假设,n²是1,
    2n+2n=4n
    也是4的倍数,根据分布定律,4的倍数之和是4的倍数:
    4a+4b=4(a+b)
  • Q.E.D.该属性适用于0(我们的基本情况)和(2+n),前提是它适用于n。因此它适用于2、4、6、8。。。。和所有其他偶数
  • (一个更容易的证明是观察到
    (2a)²
    =
    4*a*a
    ,这是4的倍数。)

    编写递归程序非常类似于通过归纳进行证明:

  • 我们为基本情况编写了计算程序
  • 对于非基本情况,我们知道如何计算结果(例如,我们知道
    n!=n*(n-1)!
    ,因此我们只写下它,因为我们需要的函数就是我们刚刚编写的函数
  • 我们可以得出结论,我们的程序将为基本情况和基本情况的任何后续情况计算正确的值。如果
    678!
    仍然无法计算正确的答案,那么这与我们使用的数据类型如
    int
    不太适合大数字有关(或者,换言之,计算moulo 2^32的所有数据)此外,软件坚持将可用数据的一半解释为负数
  • 这项工作的原因与计算机硬件或编程语言无关:正如我前面所说的,这是手头上项目(列表、树、集合、自然数)递归结构的结果


    新手常犯的一个错误是忽略基本情况而迷失在复杂性中。我总是建议从基本情况开始,一旦你有了基本情况,你就可以假设函数存在,并且可以在更复杂的情况下使用它。

    “递归为什么会这样?”因为这就是你告诉它要做的?这是由代码规定的简单、循序渐进的方法?调用函数,等待其结果。冲洗,重复。看看是什么使递归以这种方式计算?没有“计算机的方面”这使得它的工作原理是这样的——递归代码和其他代码一样,机器处理r