斐波那契数列从O(2^n)到O(1)

2,933

0.斐波那契数列定义

a1=1,a2=1,an+2=an+1+an(n>=1)a_1=1, a_2=1,a_{n+2}=a_{n+1}+a_{n}(n>=1)

常见的问题有爬楼梯问题:有n级楼梯,每次可以爬1级或者2级,问爬上n级有多少种爬法?这个问题与斐波那契数列只是初始条件不一样。

1.递归法

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

递归法简单易懂,只是时间复杂太高。 比如求fib(10)fib(10)

fib(10)=fib(9)+fib(8)fib(10)=fib(9)+fib(8)

fib(9)=fib(8)+fib(7)fib(9)=fib(8)+fib(7)

fib(8)=fib(7)+fib(6)fib(8)=fib(7)+fib(6)

从上面三个式子中发现fib(8)fib(7)fib(8)、fib(7)被计算了两次,当n较大时,会有更多的子项被多次计算,造成了计算冗余,时间复杂度高达O(2n)O(2^n)

2.备忘录递归法

既然发现fib(8)fib(7)fib(8)、fib(7)被重复计算了,那么我们用额外的空间将其结果进行存储,如果fib(8)fib(8)没有被计算过,则进行计算,并将其存储,下次碰到fib(8)fib(8)的时候直接从存储空间中获取,这样会节省很多时间。

function fib2(n) {
   let memory = new Array(n + 1).fill(0);
   const memo = (memory, k) => {
      if (k === 1 || k === 2) return 1;
      if (memory[k]) return memory[k];
      memory[k] = memo(memory, k - 1) + memo(memory, k - 2);
      return memory[k];
   };
   return memo(memory, n);
}

时间复杂度O(n)O(n),空间复杂度O(n)O(n)

3.自底向上备忘录法

递归法在计算fib(n)fib(n)时,都是从大的数进行计算,直到fib(2)fib(1)fib(2)、fib(1),既然可以从大到小,那么也可以从小到大来填充备忘数组。

function fib3(n) {
   let memory = new Array(n + 1).fill(1);
   for (let i = 2; i < n; i++) {
      memory[i] = memory[i - 1] + memory[i - 2];
   }
   return memory[n - 1];
}

这里memory[0]=>fib2(1),memory[i]=>fib(i+1)memory[0]=>fib2(1), memory[i]=>fib(i+1),所以fib(n)=>memory[n1]fib(n) => memory[n-1]。时间复杂度O(n)O(n),空间复杂度O(n)O(n)

4.丢掉备忘录吧

其实无需额外的空间,用变量记住前两个值,当前值等于前两个值相加,然后再更新前两个值,这样可将空间复杂度下降到O(1)O(1)

function fib4(n) {
   if(n === 1 || n === 2) return 1;
   let pre = 1, cur = 1, sum = 2;
   for(let i = 3; i <= n; i++) {
       sum = pre + cur;
       pre = cur;
       cur = sum
   }
   return cur;
}

5.通项公式法

梦回高中吧!(如果你正在读高中,当我没说)

已知:a1=1,a2=1,an+2=an+1+an(n>=1)已知:a_1=1, a_2=1,a_{n+2}=a_{n+1}+a_{n}(n>=1)

构造:an+2tan+1=k(an+1tan)构造:a_{n+2} - ta_{n+1} = k(a_{n+1}-ta_n)

=>an+2=(t+k)an+1tkan=>a_{n+2}=(t+k)a_{n+1}-tka_n

对比已知得:t+k=1,tk=1对比已知得:t+k=1, tk=-1

=>t(1t)=1=>t2t1=0=>t(1-t)=-1=>t^2-t-1=0

=>t1=1+52,k1=152;t2=152,k2=1+52=>t_1=\frac{1+\sqrt{5}}{2},k_1=\frac{1-\sqrt{5}}{2};t_2=\frac{1-\sqrt{5}}{2},k_2=\frac{1+\sqrt{5}}{2}

bn=an+1tan,bn+1=an+2tan+1,b1=a2ta1=1t=k令b_n=a_{n+1}-ta_n,则b_{n+1}=a_{n+2}-ta_{n+1},b_1=a_2-ta_1=1-t=k

于是bn+1=kbn=knb1=kn+1于是b_{n+1} = kb_n=k^nb_1=k^{n+1}

=>bn=kn=>an+1tan=kn=>b_n=k^n=>a_{n+1}-ta_n=k^n

=>=>

an+1t1an=k1na_{n+1}-t_1a_n=k_1^n\cdots①

an+1t2an=k2na_{n+1}-t_2a_n=k_2^n\cdots②

=>(t2t1)an=k1nk2n①-②=>(t_2-t_1)a_n=k_1^n-k_2^n

an=k1nk2nt2t1=(1+5)n(15)n2n5a_n=\frac{k_1^n-k_2^n}{t_2-t_1}=\frac{(1+\sqrt5)^n-(1-\sqrt5)^n}{2^n\sqrt5}

function fib5(n) {
   let sqrt5 = Math.sqrt(5);
   return (Math.pow(1 + sqrt5, n) - Math.pow(1 - sqrt5, n)) / (sqrt5 * Math.pow(2, n));
}

由于精度问题可能会算出小数,这里暂不处理。

6.耗时测试

主要比较fib4与fib5的耗时,因为fib5虽然时间复杂度是O(1)O(1),但是也是需要计算的。 测试代码:

const test = [
   1000000, 10000000, 100000000, 1000000000
];
for (let i = 0; i < test.length; i++) {
     let n = test[i];
     t1 = new Date().getTime();
     fib4(n);
     t2 = new Date().getTime();
     fib5(n);
     t3 = new Date().getTime();
     console.log("fib4", t2 - t1);
     console.log("fib5", t3 - t2);
}

测试结果截图

image.png

100000010000000100000001000000000
fib419681721607
fib50000

由此可见,确实是O(1)O(1)的牛逼些!