【剑指Offer】斐波那契数列
题目描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39
解法1 递归
解题前先简单说明一下斐波那契数列,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……,因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为兔子数列。可以表示为F(n) = F(n-1) + F(n-2)
。这道题在不考虑效率的情况下,最直接的解法是用递归,代码如下
实现代码
public int Fibonacci(int n)
{
if (n == 0)
{
return 0;
}
else if (n == 1 || n == 2)
{
return 1;
}else
{
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
}
解法2 动态规划
解法1使用递归虽然很直观,简单,但是效率太低。在n <= 39的情况下,运行时间为1277ms,究其原因还是算法中存在大量重复运算。以求解斐波那契数列第6项的过程来说明,如下图,在求解F6的过程中,F4会被重复计算2次,F3会被重复计算3次,这都导致了多余的消耗,且随着n越来越大冗余计算的增长是爆炸性的。
递归的思想是自顶向下的,Fn的求解基于Fn-1和Fn-2,Fn-1的求解又基于Fn-2和Fn-3等等依次类推。而现在我们可以反过来,自底向上,在已知F1 = 1,F2 = 1的情况下求解F3,再利用F3和F2求解F4直到求出Fn。即不使用递归,使用循环迭代的方式。相比于解法1,优化后的算法运行时间只有39ms。
实现代码
public int FibonacciOptimize(int n)
{
if (n == 0)
{
return 0;
}
int fibl = 1, fibn = 1;
for(int i = 2; i < n; i++)
{
fibn = fibl + fibn;
fibl = fibn - fibl;
}
return fibn;
}
//或者是更简洁一点的写法
public int FibonacciOptimize2(int n)
{
int f = 0, g = 1;
while(n -- > 0)
{
g += f;
f = g - f;
}
return f;
}
动态规划
上面不使用递归,而使用循环的方式,我们可以给它起一个高大上的名字,动态规划。什么叫做动态规划呢,其实和它本身字面上的意思并没有太大关系。
对于递归算法,编译器常常都只能做很低效的处理,递归算法如此慢的原因在于,编译器模拟的递归不能保留预先算出来的值,对已经求解过的子问题仍在递归的进行调用,导致了大量的冗余计算,比如上面的斐波那契递归算法。当我们想要改善这种情况时,可以将递归算法改成非递归算法,让后者把那些子问题的答案系统地记录下来,利用这种方法的一种技巧就叫做动态规划。比如上面的代码,我们都是用了两个变量把上一次的计算结果记录了下来,避免了重复计算。
可能上面的算法对动态规划的体现并不是那么直观,可以看下面这段代码。我们用一个数组,将每次求解出来的Fn都记录了下来,当一个子问题被求解过以后,下一次就可以直接通过索引访问数组得到,而避免了再次求解。
public int FibonacciOptimize3(int n)
{
if (n == 0)
{
return 0;
}
int[] array = new int[n + 1];
array[0] = 1;
array[1] = 1;
for(int i = 2; i < n; i++)
{
array[i] = array[i - 1] + array[i - 2];
}
return array[n - 1];
}
解法3
除了使用递归和动态规划外,我们还可以使用矩阵来求解斐波那契数列。对于矩阵这里不再进行扩展,只介绍本算法会用到的基本概念。如下所示的M就是一个2x2的矩阵,2行2列。
矩阵和矩阵之间可以相乘,一个rxn的矩阵M和一个nxc的矩阵N相乘,它们的结果MN将会是一个rxc大小的矩阵。注意如果两个矩阵的行列不满足上面的规定,则这两个矩阵就不能相乘。怎样计算新的矩阵MN呢,可以用一个简单的方式描述:对于每个元素cij,我们找到M中的第i行和N中的第j列,然后把它们对应元素相乘后再加起来,这个和就是cij,对于有矩阵M,N如下
则MN为
那么斐波那契数列和矩阵有什么关系呢?
我们已知斐波那契第n项,Fn = F(n - 1) + F(n - 2),可以将它们转换成如下所示的矩阵形式
即
以此类推
所以要求斐波那契的第n项,我们只需要求得F1和F0构成的矩阵与特定矩阵的n-1次方相乘后的矩阵,然后取该矩阵的第一行第一列的元素值就是Fn
现在引入了一个新的问题,怎样求特定矩阵的n-1次方,即矩阵的快速幂
矩阵的快速幂
在了解矩阵的快速幂之前,我们先看普通整数的快速幂
求解整数m的n次方,一般是mn = m * m * m .....,连乘n次,算法复杂度是O(n),这样的算法效率太低,我们可以通过减少相乘的次数来提高算法效率,即快速幂
对于n我们可以用二进制表示,以14为例,14 = 1110
可以发现这样的规律,指数n的二进制从低位到高位依次对应底数m的1次方,2次方,4次方,8次方...,当该二进制位是1的时候,则乘以底数对应的次方数,如果该二进制位是0,则表示乘以1。使用快速幂后,原本需要14次连乘,现在只需要4次连乘。
那么怎样得到一个整数的二进制位呢,又怎样判断该二进制位是0还是1呢
可以使用与运算和右移运算,例如对于14 = 1110
- 和1按位与得到0,即第一个二进制位是0
- 1110右移一位,得到0111,和1按位与得到1,即第二个二进制位是1
- 0111右移一位,得到0011,和1按位与得到1,即第三个二进制位是1
- 0011右移一位,得到0001,和1按位与得到1,即第四个二进制位是1
- 0001右移一位,得到0000,等于0则,算法结束
对应的代码如下
public int pow(int m, int n)
{
int ret = 1;
while(n > 0)
{
if ((n & 1) > 0)
{
ret = ret * m;
}
m *= m;
n >>= 1;
}
return ret;
}
对应矩阵的快速幂就是
// 简单实现了2*2矩阵的乘法
public int[,] matrixMul(int[,] m, int[,] n)
{
int[,] ret = {
{ m[0,0] * n[0,0] + m[0,1] * n[1,0], m[0,0] * n[0,1] + m[0,1] * n[1,1]} ,
{ m[1,0] * n[0,0] + m[1,1] * n[1,0], m[1,0] * n[0,1] + m[1,1] * n[1,1]}
};
return ret;
}
// 矩阵的快速幂
public int[,] matrixPow(int[,] m, int n)
{
// 单位矩阵,作用相当于整数乘法中的1
int[,] ret = { { 1, 0 }, { 0, 1 } };
while(n > 0)
{
if ((n & 1) > 0)
{
ret = matrixMul(m, ret);
}
m = matrixMul(m, m);
n >>= 1;
}
return ret;
}
实现代码
在已经知道矩阵的快速幂之后,求解Fn就可以直接代入公式
实现代码如下
public int FibonacciOptimize4(int n)
{
if (n == 0)
{
return 0;
}
int[,] matrix = { { 1, 1 }, { 1, 0 } };
// 这里的F1和F0矩阵多加了一列0,0,不会影响最终结果,是因为matrixMul只实现了2*2矩阵的乘法
int[,] unit = { { 1, 0 }, { 0, 0 } };
// 调用前面代码的矩阵乘法和矩阵快速幂
int[,] ret = matrixMul(matrixPow(matrix, n - 1), unit);
return ret[0, 0];
}
更多题目的完整描述,AC代码,以及解题思路请参考这里