0
点赞
收藏
分享

微信扫一扫

【每日算法】NC65 斐波那契数列

在觉 2022-01-21 阅读 76

描述

大家都知道斐波那契数列,现在要求输入一个正整数 n ,请你输出斐波那契数列的第 n 项。

斐波那契数列是这样一个数列:1、1、2、3、5、8、13、21、34…

就是第一和第二位都是 1,后面的数都是前面两个数之和。

数据范围:1 ≤ n ≤ 39

要求:空间复杂度 O(1),时间复杂度 O(n) ,本题也有时间复杂度 O(logn)的解法

输入描述:

一个正整数n

返回值描述:

输出一个正整数。

做题

做这种题,肯定不用递归啦,递归🐕都不用。

肯定需要两个变量:former、latter,前者用来存储比较旧的数据,后者用来存储当前位置的数据,当要求出下一位斐波那契数,就只需要把这两个变量相加就可以了,所以这两个变量是肯定要创建的,创建数组就没必要了,我们只是要一个数而已。

两个变量相加的结果就直接存放到 latter,这里想要在保存原来 latter 的值,就只能再创建一个变量来存储了。

还有循环,直接从 2 开始循环,当 n 小于或等于 2 时,就不走循环,直接返回 latter(1)。

public int Fibonacci(int n) {
    int former=1;
    int latter=1;
    int temp=0;
    for(int i=2;i<n;i++){
        temp=latter;
        latter+=former;
        former=temp;
    }
    return latter;
}

运行!

image.png

运行速度还是挺不错的,占用内存就比较拉跨了。

再试

网上说的斐波那契数组最佳的解的时间复杂度是 log(n),我们肯定要想最优的解看齐啦。

我也找了很多网上的博客,很多是跟线性代数有关的,但更多的是拿时间度为 n 的来凑数,搞得我看了半天,最后这不是我已经写出来的吗?!

线性代数

线性代数怎么来的?

这肯定就要说一下线性方程组啦,那什么是线性方程组呢?

{
a1x1 + a2x2 + a3x3 = b1,

a4x4 + a5x5 + a6x6 = b2,


}

就是类似这种的方程组,线性方程就是其中的一行。

方程组越写越长,越写越多,于是干脆就把线性方程组简写成了矩阵

|

a1 a2 a3

a4 a5 a6

|

这里写出来的效果不太好,同学们可以自行百度搜索一下。

矩阵的出现就帮助我们计算更加复杂算法,让一代代大学生都倒在线性代数下。

大学里学的线性代数都差不多还给老师了,这里就不误人子弟了,这里参考的链接放在文章最末尾了,感兴趣的同学可以去看一下,这里我们只需要知道我们即将用到矩阵就行。

本文需要使用到矩阵的乘法运算,运算如下:

|

|a1 , a2|,

|a3 , a4|

| × |

|b1 , b2|,

|b3 , b4|

| = |

|a1 b1 + a2 b3 , a1 b2 + a2 b4|,

|a3 b1 + a4 b3 , a3 b2 + a4 b4||

二阶常系数齐次线性递推数列

斐波那契数列 属于 二阶常系数齐次线性递推数列。

二阶常系数齐次线性递推数列是什么?

二阶常系数齐次线性递推数列就是满足an = c1a(n-1) + c2a(n-2),其中 c1,c2 为常数,这里的()内的数为下标。

好了,如果是二阶常系数齐次线性递推数列,那就会存在一个二阶的矩阵,可以满足这个数列的规律:|F(n) F(n-1)| = |F(n-1) F(n-2)| * A。A 是一个矩阵。

不懂也没关系,我们可以类比成 X 的 N 次方,我们可以通过移动比特位的方式去降低时间复杂度了,而不是一次次地相乘。

时间复杂度降低到log(n)的秘诀

我们来写一个 X 的 N 次方。

先是用普通的 for 循环来写一遍:

private long calculationPower(long base, long power) {
    long result = 1;
    for (; power != 0; power--) {
        result *= base;
    }
    return result;
}

如果是 2 的 100 次方,那就要运行 100 次,时间复杂度为 n。

当我们优化成通过移动比特位的方式:

private long calculationPower1(long base, long power) {
    long result = 1;
    long temp = base;
    for (; power != 0; power = power >> 1) {
        if ((power & 1) != 0) {
            result *= temp;
        }
        temp *= temp;
    }
    return result;
}

代码都是可以运行的,结果准确无误。

100 的二进制等于 1100100,也就是说,本来要循环 100 次,现在只需要循环 7 次!

效率提升了 14 倍!

我刚学的时候也有一点不懂。

  1. 为啥 temp 在每次循环中都要自乘?
  2. 为啥在(power & 1) != 0时才result *= temp;

第一点,因为每次 power 右移的时候,其实是在给我们的次方数除于 2,temp 存储的是当前比特位是 X 的 n 次方的结果。最直观的例子就是拿二进制来看一下,6 =110,这三位二进制数分别代表 4 2 1,代入到 2 ^ 6 中,到二进制110中最前面的 1 时,2 =10,result = 4,就是才乘了两次,剩下的这个 1 就代表这 2 ^ 4,而这时的 temp 在上一次循环中做了一次自乘(4 * 4),已经达到了 2 ^ 4,所以 temp 要自乘。

第二点,就跟转换二进制到十进制一样,肯定是当前比特位为 1 才能算是有啊。

(第一点写的我心力憔悴,愣是想了很久才表达出来,个人感觉只要举得例子能看懂就行,其他的可能我个人表达也有问题。)

斐波那契数列的二阶的矩阵

那么就要求出斐波那契数列的二阶的矩阵 A 了。

已知,F(3) = F(2) + F(1),F(4) = F(3) + F(2) = 2F(2) + F(1), F(2) = 1,F(1) = 1。

代入二阶常系数齐次线性递推数列的规律,得:

|F(3) F(2)| = |F(2) F(1)| * A

|F(4) F(3)| = |F(3) F(2)| * A

这里的|F(2) F(1)||F(3) F(2)|也是一个矩阵,

二阶矩阵 A 为

||a b|,

|c d||

我们运算时把 F(3)、F(2)、F(1) 都转换成数字 2、1、1。

|F(3) F(2)| = |1 1| * A = | a + c , b + d|

|F(4) F(3)| = |2 1| * A = | 2a + c, 2b + d |

于是我们得出四条方程式:

a + c = 2,
b + d = 1,
2a + c = 3,
2b + d = 2.

计算得出

a = 1, b = 1, c = 1, d = 0.

于是我们的矩阵 A 为

||1 1|,

|1 0||

有了矩阵 A,我们就可以通过移动比特位的方式极大地减少时间复杂度,也就是达到 log(n)。

那么最后一个问题来了,初始的基数是啥?

初始的基数

如果是求 X 的 N 次方,那么我们可以让调用者来输入,但这里是斐波那契数列啊,我们就得把基数确定下来。

我们依照二阶常系数齐次线性递推数列的规律:|F(n) F(n-1)| = |F(n-1) F(n-2)| * A,就可以得出我们的基数使用的就是|F(2) F(1)|。

But,我们还要创建一个矩阵相乘的方法,如果使用基数:|F(2) F(1)|,那我们就无法写一个能够通用的方法,所以我们同样要找一个二阶的矩阵作为基数。

于是乎,可以使用 ||F(2) F(1)|,|0 0|| 来作为基数,我们只取第一列的值就行。

好了,开始敲代码。

/**
 * 时间复杂度O(log(n))
 *
 * @param n
 * @return
 */
public int fibonacci(int n) {
    if (n <= 2) {
        return 1;
    }
    int[][] base = {{1, 1}, {0, 0}};
    int[][] A = {{1, 1}, {1, 0}};

    base = multiMatrix(base, A, n - 2);
    return base[0][0];
}

/**
 * base 矩阵 和 A 矩阵的 n 次方相乘
 *
 * @param base
 * @param A
 * @param n
 * @return
 */
private int[][] multiMatrix(int[][] base, int[][] A, int n) {
    //这里就可以模仿 calculationPower1 了,只不过原来的 temp = base,这里就改成了 = A,
    // 因为不是自乘了,而是跟矩阵 A 相乘
    int[][] temp = A;
    for (; n != 0; n = n >> 1) {
        if ((n & 1) != 0) {
            base = multiMatrix(base, temp);
        }
        temp = multiMatrix(temp, temp);
    }
    return base;
}

/**
 * 两个矩阵相乘
 *
 * @param base
 * @param A
 * @return
 */
private int[][] multiMatrix(int[][] base, int[][] A) {
    int[][] ints = new int[base.length][base[0].length];
    for (int i = 0; i < ints.length; i++) {
        for (int j=0;j<ints[0].length;j++){
            for (int k=0;k<A[0].length;k++){
                ints[i][j] += base[i][k] * A[k][j];
            }
        }
    }
    return ints;
}

因为个人水平有限,两个矩阵相乘的方法呢,我是抄了别人的写法,然后经过自己的理解后稍微修改了一部分,我无法从矩阵相乘推算出算法,这里只能结合算法和矩阵相乘,推出这个算法到底是怎么算的。

还记得之前说的矩阵的乘法运算吗?

|

|a1 , a2|,

|a3 , a4|

| × |

|b1 , b2|,

|b3 , b4|

| = |

|a1 b1 + a2 b3 , a1 b2 + a2 b4|,

|a3 b1 + a4 b3 , a3 b2 + a4 b4||

这里我们把 an 看成数组 a,把 bn 看成数组 b,最后面这个 |a1 b1 + a2 b3 …| 看成数组 res。

res =

|a1 b1 + a2 b3 , a1 b2 + a2 b4|,

|a3 b1 + a4 b3 , a3 b2 + a4 b4|

=>

|a[0][0] b[0][0] + a[0][1] b[1][0] , a[0][0] b[0][1] + a[0][1] b[1][1]|,

|a[1][0] b[0][0] + a[1][1] b[1][0] , a[1][0] b[0][1] + a[1][1] b[1][1]|

multiMatrix(这一部分说的都是两个矩阵相乘的)其实就是给数组一位一位地去填数,res[0][0] -> res[0][1] -> res[1][0] -> res[1][1]

res[0][0] = a[0][0] b[0][0] + a[0][1] b[1][0],

res[0][1] = a[0][0] b[0][1] + a[0][1] b[1][1],

res[1][0] = a[1][0] b[0][0] + a[1][1] b[1][0],

res[1][1] = a[1][0] b[0][1] + a[1][1] b[1][1].

通过目视法可以得出一个规律:

res[i][j] = a[i][0] b[0][j] + a[i][1] b[1][j]

这是不是就能理解了,这三层 for 循环是怎么计算的?

最内层的 for 循环其实是简化了a[i][0] b[0][j] + a[i][1] b[1][j]这一部分,我们也可以把它写在代码里,不过这样就只能用于二阶矩阵的相乘了。

执行!

不用看了,毫无疑问肯定是可以通过的,不能通过我还会写上来?

image.png

但是这运行时间怎么和动态规划一样啊?!

冷静冷静,能学到东西就行。

两种算法的比较

时间复杂度为 log(n) 的算法究竟比时间复杂度为 n 的算法快多少呢?

public static void main(String[] args) {
    Solution2 solution2 = new Solution2();
    //动态规划算法,时间复杂度为 n
    System.out.println("solution2.Fibonacci(i)");
    long startTime1 = System.nanoTime();
    solution2.Fibonacci(100000000);
    long endTime1 = System.nanoTime();
    System.out.println((endTime1 - startTime1));
    //移动比特位,时间复杂度为 log(n)
    System.out.println("solution2.fibonacci(i)");
    long startTime2 = System.nanoTime();
    solution2.fibonacci(100000000);
    long endTime2 = System.nanoTime();
    System.out.println((endTime2 - startTime2));
}

运行结果:

快了 982.7 倍!!

可见 log(n) 是非常快的。

好了,本文结束了。

这里是程序员徐小白,【每日算法】是我新开的一个专栏,在这里主要记录我学习算法的日常,也希望我能够坚持每日学习算法,不知道这样的文章风格您是否喜欢,不要吝啬您免费的赞,您的点赞、收藏以及评论都是我下班后坚持更文的动力。

举报

相关推荐

0 条评论