问题描述
如果我们有一个永远不会变满的理论堆栈内存,并且我们有一个简单的递归函数
recurse(n):
if n > 0:
recurse(n-1)
recurse(n-2)
return
认为理论堆栈在 recurse(n)
的执行过程中的任何时刻最多有 n 个堆栈帧是否合理,因为 recurse(k)
不可能打开recurse(i)
的顶部如果 0 recurse(i) 调用了 recurse(k)
(基于函数体这是不可能的)。如果我的推理是正确的,那么最大深度一定是函数栈如下所示:
(BottOM-MOST)|recursion(n)|recursion(n-1)|...|recursion(2)|recursion(1) (TOP-MOST)
解决方法
当 n = 0
函数调用本身有一个堆栈帧时 - 任何 n
的堆栈帧都不能少于一个 - 所以最大堆栈帧数的公式是 { {1}},不完全是 max(1,n+1)
。否则你的推理是正确的,这个公式可以用归纳法证明:
- 在
n
的基本情况下,有一个堆栈帧,它等于n <= 0
,因为max(1,n+1)
。 - 否则当
n+1 <= 1
时,会进行两次递归调用,根据归纳假设,一次的堆栈深度为n >= 1
,另一次的堆栈深度为max(1,n)
。因此,包括max(1,n-1)
上调用的当前堆栈帧在内的最大堆栈深度等于n
。这可以简化为1 + max(max(1,n),max(1,n-1))
,因为1+n
是n
操作数中最大的一个,并且max
根据需要等于1+n
。
这很容易证明。为自己绘制地图 -
recurse(0)
recurse(1)
recurse(0)
recurse(-1)
recurse(2)
recurse(1)
recurse(0)
recurse(-1)
recurse(0)
recurse(3)
recurse(2)
recurse(1)
recurse(0)
recurse(-1)
recurse(0)
recurse(1)
recurse(0)
recurse(-1)
recurse(4)
recurse(3)
recurse(2)
recurse(1)
recurse(0)
recurse(-1)
recurse(0)
recurse(1)
recurse(0)
recurse(-1)
recurse(2)
recurse(1)
recurse(0)
recurse(-1)
recurse(0)
这就是为什么 fib(n)
对于大 n
不会溢出堆栈,而是长时间占用您的 CPU。对于小n
,例如n = 20
,结果以1,048,576 步计算,但仅使用20 帧-
function fib(x)
{ if (x < 2n)
return x
else
return fib(x - 1n) + fib(x - 2n)
}
console.log("result %s",fib(20n))
// result 6765
对于像 n
这样更大的 n = 200
,它需要惊人的 1,606,938,044,258,990,275,541,962,092,341,162,602,522,202,993,782,792,18 计算,但不会导致堆栈溢出,736018 次溢出。然而,即使每秒 1,000,000 次计算,也需要 50,955,671,114,250,072,156,268,658,377,807,020,642 年才能完成 -
function fib(x)
{ if (x < 2n)
return x
else
return fib(x - 1n) + fib(x - 2n)
}
console.log("result %s",fib(200n))
// result ...
如果您尝试运行上面的 fib(200)
,JavaScript 将导致您的浏览器挂起,直到太阳死了很久。刷新此选项卡,我们可以在 1 毫秒内计算出答案。 fib
的这种重写使用线性递归,计算 n = 200
将只需要 200 步和 200 帧 -
function fib(x,a = 0n,b = 1n)
{ if (x == 0n)
return a
else
return fib(x - 1n,b,a + b)
}
console.time("fib(200)")
console.log("result %s",fib(200n))
console.timeEnd("fib(200)")
// result 280571172992510140037611932413038677189525
// fib(200): 1.000ms
如果您使用 while
循环,则可以在 200 步和 1 帧内完成。但这不是递归,所以在这篇文章中可能不值得讨论。