持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第31天,点击查看活动详情
1、用JS计算斐波那契数列(Fibonacci)的第n个值
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
从斐波那契数列的定义中,可以写出一个递归的算法 F(n)=F(n-1)+F(n-2), 其中F(0) = 0、F(1) =1、F(2) =1;
例如:当n = 10
0 1 1 2 3 5 8 13 21 34 55
F(10) = F(9) + F(8)
....
F(2) = F(1) + F(0)
所以,最后的代码如下
function fibonacci(n) {
if (n <= 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
2、分析
上述递归的时间复杂度是O(n ^ 2)
fibonacci(100) // 堆栈溢出
fibonacci(500) // 堆栈溢出
递归实际上是自己调用自己,和循环一样,必须要有个结束条件,否则就会栈溢出,
在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,因此递归次数过多容易造成栈溢出
3、尾调用优化
尾调用指的是一个函数在另外一个函数的尾部被调用
function fibonacci() {
return fibonacciOther();
}
3.1 尾调用优化的条件
尾递归优化只在严格模式下生效 use strict
- 尾调用不能引用当前堆栈帧中的变量(即尾调用的函数不能是闭包)
闭包需要访问包含该尾调用的函数中的变量,尾调用优化就会被取消
function fibonacci() {
var num = 1,
fibonacciOther = () => num;
// 未优化 - 存在闭包
return fibonacciOther();
}
- 使用尾调用的函数在尾调用结束后不能做额外的操作
function fibonacci() {
// 未优化 - 在函数执行并返回之前有额外的操作
return 1 + fibonacciOther();
}
- 尾调用函数值作为当前函数的返回值
function fibonacci() {
// 未优化 - 无返回值
fibonacciOther();
}
3.2 优化
所以fibonacci的优化代码如下
'use strict'
// 初始值 n1 = 0, n2 = 1
function fibonacci(n, n1 = 0, n2 = 1) {
if (n === 0) return n1;
return fibonacci(n - 1, n2, n1 + n2);
}
参数n1负责保存上次计的结果,所以不需要调用其它的函数即可计算下一次的值。
当 n 大于 1 的时候,先进行相加的结果并将值作为第二个参数传入 fibonacci()。
虽然,仍然是递归,但是在时间复杂度上是O(n),
- 每次递归的时候,把本次运算相加的结果作为参数传到下一次的递归计算中
- n逐渐递减,直到n === 0
- n1即为结果值
3.3 总结
在严格模式下 ECMAScript 6 试图利用恰当的尾部函数调用来减少调用栈的大小(非严格模式下的尾调用未被考虑)
该优化使得尾部的函数调用不再增加,而是清除并利用已存在的堆栈帧(stack frame)
尾调用优化允许某些函数的调用被优化,以便减少调用栈的大小和内存占用,防止堆栈溢出。当符合相应条件时该优化会由引擎自动实现,然而你可以有目的地重写某些函数以便利用它。