从菲波那切数列看尾部调用优化

1、菲波那切数列

  在数学上,斐波那契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2,n∈N*),用文字来说,就是斐波那契数列列由 0 和 1 开始,之后的斐波那契数列系数就由之前的两数相加。形如:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……

  数学上的计算公式是:

   

  读书时候还有利用线性方程推算过这个公式,不过现在都忘了差不多了 ~~泪奔~~。

  其实用代码描述兔子生娃的故事也没少干,常见算法有递归法和递推法。

2、递归

function fibonacci(n){
    if(n === 1 || n === 0 ) return n;
    return fibonacci(n-1) + fibonacci(n-2);
} 

说明: 

  递归造成了大量的重复计算,使用递归计算大数字时,性能会特别低。

  函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。因而,当递归层数过大之后,就可能造成调用栈占用内存过大或者溢出。

3、递推法

function fibonacci(n) {
    let current = 0;
    let next = 1;
    for(let i = 0; i < n; i++){
        [current, next] = [next, current + next];
    }
    return current;
}

4、 尾调用优化

 尾调用优化是指某个函数的最后一步是调用另一个函数。

最简单模式:

function f(x){
  return g(x);
}

  结合上面的递归说明,尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。   

菲波那切数列改写成尾调用写法:

'use strict'
function fibonacci(n, current = 0, next = 1) {
	if(n === 1) return next;
	if(n === 0) return 0;
	return fibonacci(n - 1, next, current + next);
}

5、动态规划 

function fibonacci(n) {
  var n1 = 1, n2 = 1, sum;
  for (let i = 2; i < n; i++) {
    sum = n1 + n2
    n1 = n2
    n2 = sum
  }
  return sum
}

 参考:阮一峰--《尾调用优化》

 

原文地址:https://www.cnblogs.com/leaf930814/p/8644370.html