从斐波那契数列中学习尾调用优化

227 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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

  1. 尾调用不能引用当前堆栈帧中的变量(即尾调用的函数不能是闭包)

闭包需要访问包含该尾调用的函数中的变量,尾调用优化就会被取消

function fibonacci() {

  var num = 1,

  fibonacciOther = () => num;

  // 未优化 - 存在闭包

  return fibonacciOther();

}

  1. 使用尾调用的函数在尾调用结束后不能做额外的操作
function fibonacci() {

  // 未优化 - 在函数执行并返回之前有额外的操作

  return 1 + fibonacciOther();

}
  1. 尾调用函数值作为当前函数的返回值
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)

尾调用优化允许某些函数的调用被优化,以便减少调用栈的大小和内存占用,防止堆栈溢出。当符合相应条件时该优化会由引擎自动实现,然而你可以有目的地重写某些函数以便利用它。