描述
大家都知道斐波那契数列,现在要求输入一个正整数 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;
}
运行!
运行速度还是挺不错的,占用内存就比较拉跨了。
再试
网上说的斐波那契数组最佳的解的时间复杂度是 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 倍!
我刚学的时候也有一点不懂。
- 为啥 temp 在每次循环中都要自乘?
- 为啥在
(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]
这一部分,我们也可以把它写在代码里,不过这样就只能用于二阶矩阵的相乘了。
执行!
不用看了,毫无疑问肯定是可以通过的,不能通过我还会写上来?
但是这运行时间怎么和动态规划一样啊?!
冷静冷静,能学到东西就行。
两种算法的比较
时间复杂度为 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) 是非常快的。
好了,本文结束了。
这里是程序员徐小白,【每日算法】是我新开的一个专栏,在这里主要记录我学习算法的日常,也希望我能够坚持每日学习算法,不知道这样的文章风格您是否喜欢,不要吝啬您免费的赞,您的点赞、收藏以及评论都是我下班后坚持更文的动力。