算法小记——从计算Fibonacci第N+1项谈起

背景

Fibonacci是编程基础经典题目,对于博主这样的新手码农来说,通过这道题目,对于递归和循环递推等思想的理解和体会都有非常大的帮助。Fibonacci数列其基本形式如下:

F(0)=F(1)=1

F(n)=F(n1)+F(n2)

公式计算法

为了计算数列第N+1项(第一项为F(0),故F(n)为第N+1项),最直观的方式是构造其通项公式,然后带入数据进行计算,Fibonacci通项公式的推导过程这里不再赘述(详情可参考这里),推导之后的通项公式如下:

F(n)=(1+52)n(152)n5

通过带入具体的N值,可以快速计算得到第N+1项的值,但是这种方法存在一些问题,首先,推导的公式相对比较繁琐,公式的结果也相对复杂,如果不理解推导过程直接记忆公式,会非常吃力且容易记错,而且该公式只能针对

F(n)=F(n1)+F(n2)

这样的递推公式,其灵活性会非常差(例如,如果将最后递推公式最后一项改为F(n-3),该公式将不再适用)

递归计算法

递归的核心思想即是通过调用自身的方式来缩减问题规模,数列的递推公式已经提供了一种天然的递归调用的思路,demo代码(Java实现)如下:

    public int Fibonacci(int n){
        if (n < 2){
            return 1;
        }
        return Fibonacci(n-1)+Fibonacci(n-2);
    }

这种方式可以以非常少的代码实现第N+1项的计算,但是与之相对的,这种算法的代价也非常的高,计算机每次调用函数时,为了保证函数执行完毕,都必须要将函数调用时的程序现场入栈保存,递归存在大量的自身调用,势必会产生非常庞大的函数现场栈来记录数据。另外,回到算法的本身上来,以计算F(10)项为例,为了计算F(10)项,必须知道F(9)项和F(8)项的值,而为了计算F(9)项,又必须知道F(8)项和F(7)项的值……整个过程可以用下面的树形图来进行表示(图片摘自tuzhutuzhu的专栏):
树形图

我们可以看到,图中存在大量的重复节点需要计算,从而进一步导致整个算法的效率非常低下,事实上,整个算法的时间复杂度和空间复杂度都是 2n 级别的

非递归计算法

为了提高算法的性能,最直观的做法,就是将递归转为循环,我们知道,递归从逻辑上讲,是一个逆向推导的过程,而循环通常都代表着顺序的思维,这两种方法好比一个事物的两面,经常可以互相转化来解决问题。从递推公式我们可以看出,Fibonacci数列从第三项起,其值始终为该项的前两项的和,利用这个性质,我们可以从头用代码推导第N+1项的值,demo代码(Java实现)如下:

    public int Fibonacci(int n){
        int firstPre = 1, secondPre = 1;
        for (int i = 1;i < n;i++){ //起始firstPre为F(1),故i从1开始
            int tmp = firstPre+secondPre;
            secondPre = firstPre;
            firstPre = tmp;
        }
        return firstPre;
    }

通过利用循环的方式,可以将算法的空间复杂度降至O(1),时间复杂度降至O(n),相对于递归算法,这一优化的效果已经是非常显著的了,但是如果我们需要计算的n值非常大的话(例如将n取遍int的非负范围),这种算法任然会产生很高昂的代价。

矩阵快速幂计算法

我们可以发现,循环算法主要的代价在于N-1次的循环,如果可以将N-1次的加法运算进一步缩减的话,就可以进一步优化我们的算法。为了达到这个目的,首先我们需要学习一种快速计算乘法运算的技巧:

我们知道计算两个数的乘积a*b,传统的做法事实上是将b个a相加得到结果,如果用代码的思维来看,这一过程需要循环执行b次加a操作,然而如果我们换一种角度来看,以7*3为例,结果也可以表示为7*2+7*1,即如果我们将3用二进制表示的话,结果等于7*10+7*01(3的二进制为11),这个新的算式可以理解为,a乘以所有b的不为0的二进制位的对应权值的和(3的二进制11,最右位代表1,从右开始第二位代表2),且因为二进制相邻的位置,左边的位置权值始终是右边的2倍,所以如果我们每次都判断b的最右二进制位是否为1,且每次都将b右移一位,a乘以2的话,我们就可以将乘法运算的次数缩减为log(b)

根据以上思路,我们可以定义一种高效的乘法函数如下(Java实现):

    public int multiply(int a, int b){
        int sum = 0;
        while (b != 0){
            if (b&1 == 1){ //将b与1按位与可以判断最右位是否为1
                sum += a;           
            }
            a *= 2;
            b>>1; //二进制下的右移一位相当于除以2
        }
        return sum;
    }

我们可以看到,利用这种算法,原本需要b次才能完成的乘法操作,现在只需要log(b)就可以完成了。

幂运算,通常也被成为乘法的乘法,同样,也可以利用这样的思路,进行快速计算,也就是所谓的快速幂。demo代码如下(Java实现):

    public int multiply(int a, int b){
        int sum = 1;
        while (b != 0){
            if (b&1 == 1){
                sum *= a;
            }
            a = a * a;
            b>>1;
        }
        return sum;
    }

当然,我们这份代码里的普通乘法运算,同样可以利用快速幂的思想进行进一步优化。这样,我们就可以在log(n)的时间内完成n次幂运算。

接下来,我们需要考虑如何将通项公式

F(n)=F(n1)+F(n2)

转化为只包含乘法或幂运算的算式,我们来看如下矩阵运算:
[1110][AB]=[A+BA00]

不难看出,我们的递推公式可以转化为这个特殊的矩阵算子

[1110]

通过向初始矩阵乘以n-1次该矩阵,我们就可以得到F(n)的解,而利用我们上面提到的快速幂算法,这个过程可以在O(log(n))的时间内完成,从而进一步完成了对算法的优化。完整的demo代码如下(Java实现):

    public int Fibonacci(int n){
        long[][] sum = new long[][]{{1,0},{0,1}};//初始化为单位矩阵,类比普通快速幂的sum=1
        long[][] basic = new long[][]{{1,1},{1,0}};//初始化算子
        while (n > 1){// 计算n-1次
            if (n&1 == 1){
                res = multiply(sum, basic, 2);
            }
            basic = multiply(basic, basic, 2);
            n>>1;
        }
        return (int)sum[0][0];
    }

    //矩阵乘法运算
    public long[][] multiply(long[][] a, long[][] b, int N){
        long[][] result = new long[N][N];
        for (int k = 0;k < N;k++){
            for (int i = 0;i < N;i++){
                for (int j = 0;j < N;j++){
                    result[k][i] += a[k][j]*b[j][i];
                }
            }
        }
        return result;
    }

P.S. 后记:要灵活

上面博主整理的方法都只针对了Fibonacci数列,亦即递推形为

F(n)=F(n1)+F(n2)

的数列的第N+1项求值,但这些方法绝不仅限于此。例如我们如果将该数列递推改为
F(0)=F(1)=F(2)=1

F(n)=F(n1)+F(n3)

的话,我们同样可以利用上述方法实现F(n)求值(Java实现):

    // 递归实现
    public int Fabonicci(int n){
        if (n < 3){
            return 1;
        }
        return Fabonicci(n-1)+Fabonicci(n-3);
    }
    // 循环实现
    public int Fabonicci(int n){
        int thirdPre = 1,secondPre=1,firstPre=1;
        for (int i = 1;i < n;i++){
            int tmp = thirdPre + firstPre;
            thirdPre = secondPre;
            secondPre = firstPre;
            firstPre = tmp;
        }
        return firstPre;
    }
    // 矩阵快速幂实现
    public int Fabonicci(int n){
        //博主在这里体会了好久才初窥了矩阵设计的奥秘,希望觉得本文有用的童鞋们也可以好好想一下,为什么这里的矩阵不是单位矩阵了,换成单位矩阵可以不?
        long[][] sum = new long[][]{{1,0,0},{1,0,0},{1,0,0}};
        long[][] basic = new long[][]{{1,0,1},{1,0,0},{0,1,0}};

        while (n > 1){
            if (n&1 == 1){
                sum = multiply(basic, sum, 3);
            }
            basic = multiply(basic, basic, 3);
            n>>1;
        }
        return (int)sum[0][0];
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值