预测未来最好的方法就是去创造未来。——<亚伯拉罕·林肯>
递归的定义
给定数字的阶乘 n! = n * (n - 1) * ... * 1
是标准示例。
1 | function factorial(n) { |
上面显示的示例只是阶乘函数的最幼稚实现。
为了完整起见,让我们看一下它是如何执行的 n = 6:
- 阶乘(6)
- 6 *阶乘(5)
- 5 *阶乘(4)
- 4 *阶乘(3)
- 3 *阶乘(2)
- 2 *阶乘(1)
- 1 *阶乘(0)
- 1个
- (恢复先前的执行)1 * 1 = 1
- 1 *阶乘(0)
- (正在恢复…)2 * 1 = 2
- 2 *阶乘(1)
- (…)3 * 2 = 6
- 3 *阶乘(2)
- …4 * 6 = 24
- 4 *阶乘(3)
- 5 * 24 = 120
- 5 *阶乘(4)
- 6 * 120 = 720
- 6 *阶乘(5)
- 阶乘(6)= 720
现在,我们必须对正在发生的事情非常谨慎,这样我们才能了解接下来会发生什么。
当我们调用一个函数时,会同时发生几件事。保存了调用函数后必须返回的位置以及当前帧的信息(即n的值)。然后为新功能分配空间,并诞生一个新的框架。
如此反复,我们继续堆叠这些帧,然后展开该堆叠,用函数返回的值替换函数调用。
还要注意的另一件事是我们的函数所生成的过程的形状。如果我将此形状称为递归,您可能不会感到惊讶。因此,我们有一个 递归过程。
让我们看一下该函数的第二种实现。
1 | function factorial(n, res) { |
我们可以通过定义内部函数来进一步封装功能。
1 | function factorial(n) { |
让我们看一下它是如何执行的:
- 阶乘(6)
- 内部匿名函数(iaf)被调用(n = 6,res = 1)
- iaf(5,1 * 6)
- iaf(4,6 * 5)
- iaf(3,30 * 4)
- iaf(2,120 * 3)
- iaf(1,360 * 2)
- iaf(0,720)
- 720
- 720
- iaf(0,720)
- 720
- iaf(1,360 * 2)
- 720
- iaf(2,120 * 3)
- 720
- iaf(3,30 * 4)
- 720
- iaf(4,6 * 5)
- 720
- iaf(5,1 * 6)
- iaf(6,1)= 720
- 内部匿名函数(iaf)被调用(n = 6,res = 1)
- 阶乘(6)= 720
您可能会注意到,展开堆栈后,我们不需要执行任何计算。我们刚刚返回了一个值。但是,根据我们的规则,即使状态链后面没有任何用处,我们也必须将状态保存为堆栈框架。
但是,我们的规则并不适用于所有语言。实际上,在Scheme中,必须通过尾部调用优化来优化此类链。这样可以确保我们的堆栈不会充满不必要的框架。因此,我们以前的计算看起来像这样:
- 阶乘(6)
- iaf(6,1)
- iaf(5,6)
- iaf(4、30)
- iaf(3,120)
- iaf(2,360)
- iaf(1,720)
- iaf(0,720)
- 720
反过来看起来非常像
1 | res = 1; |
意味着,即使我们正在使用递归,我们实际上也有一个迭代过程。多么酷啊?
好消息是,这是ES6的功能。只要您的递归调用处于尾部位置并且您的函数具有严格模式,尾部调用优化就会开始执行,并避免出现maximum stack size exceeded
错误。
参考
关注【公众号】,了解更多。
赞赏一下 坚持原创技术分享,您的支持将鼓励我继续创作!