斐波那契数列的N种解法

简述

1,1,2,3,5,8,13,21......
上面的数列叫做斐波那契(Fibonacci)数列,这是一个很有趣的数列,经常为人们所使用,其数学表达式很简单,就是f(n)=f(n-1)+f(n-2)
在算法学习中,随着水平不断提升,斐波那契的解法也是不断进步。

几种常见的斐波那契解法

新手(暴力递归)

int fib(int n){
   if(n<2)return 1;
   else return fib(n-1)+fib(n-2);
}

这是斐波那契的递归写法,十分的通俗易懂,经常被作为递归算法的例子。虽然代码十分简短,它的时空复杂度却异常的大,达到O(2^n)。仔细一看,我们会发现这代码的效率低下的主要原因就是因为多了很多重复的计算。比如调用fib(10)的时候,会递归调用2次fib(8),3次fib(7),而fib(1)将会被调用55次。

入门(优雅递归)

上面简单的代码背后竟如此的复杂,我们不禁思考:是否可以避免递归中的重复计算呢?
答案是可以的,可以使用一个备忘录来记录。实现很简单,定义一个数组来保存数据,递归时先判断数组的当前下标是否有效值,如已经是有效值,直接返回数据,不再需要重新计算。这里就不附上代码了。

进阶(动态规划)

很多时候,我们看到递归,总要想想能不能使用动态规划来优化,对于斐波那契,显然是可以的,其状态转换方程正是f(n)=f(n-1)+f(n-2)

int fib(int n){
    int[] f = new int[n+1];
    f[0] = 1;
    f[1] = 1;
    for (int i = 2;i<=n;i++)f[i] = f[i-1]+f[i-2];
    return f[n];
}

在使用动态规划后,时间和空间复杂度均为O(n),和原来相比是大大优化了。同时我们发现其实随着程序的进行,我们用得到的数据其实就是n-2,n-1,n这3个位置的值,那可以只使用3个变量,将空间复杂度优化到O(1)。

int fib(int n){
    int a = 1, b = 1, c;
    for (int i = 1;i<=n;i++){
        c = a+b;
        a = b;
        b = c;
    }
    return a;
}

高手(快速幂动态规划)

曾经我以为O(n)的时间复杂度已经是极限了,然而事实上还有O(logN)的做法,用到的正是大学里学到的矩阵乘法。

如图所示,我们可以将问题转化为求出矩阵的n次幂,而求n次幂可以用二分,使得时间复杂度达到O(logN)。

int fib3(int n){
    if(n<2)return 1;
    int[][] re = pow(n);
    return re[0][0];
}
int[][] pow(int n){
    if (n==1)return new int[][]{{1,1},{1,0}};
    int[][] tmp = pow(n/2);
    int[][] re = new int[2][2];
    re[0][0] = tmp[0][0]*tmp[0][0]+tmp[0][1]*tmp[1][0];
    re[0][1] = tmp[0][0]*tmp[0][1]+tmp[0][1]*tmp[1][1];
    re[1][0] = tmp[1][0]*tmp[0][0]+tmp[1][1]*tmp[1][0];
    re[1][1] = tmp[1][0]*tmp[0][1]+tmp[1][1]*tmp[1][1];
    if (n%2==1){
        re[0][0] = re[0][0]+re[0][1];
        re[0][1] = re[0][1]+re[1][1];
        re[1][1] = re[1][0];
        re[1][0] = re[0][1];
    }
    return re;
}

在测试中,当n达到1e8时,使用O(n)动态规划的运行时间是54ms,而使用O(nlogn)矩阵乘法的运行时间是1ms。

总结

斐波那契数列是一个很有趣的数列,它的解法也是五花八门。这里最后一种解法用到了矩阵乘法,这是以前所没有接触过的解法,以前对线性代数的认识就只是一门学科,不知道它有什么用。
平时学到的知识并不是没用,而是我们不懂如何去使用,多进行知识储备,终有一日会用得上的。

posted @ 2021-12-30 21:02  freya9  阅读(267)  评论(0)    收藏  举报