斐波那契数列,由数学家 列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为“兔子数列”。一般我们见到的兔子繁殖问题、以1阶或2阶方式上台阶的问题,本质上都是斐波那契数列问题。
斐波那契数列递推公式:
F
(
n
)
=
{
0
,
n
=
0
1
,
n
=
0
F
(
n
−
1
)
+
F
(
n
−
2
)
,
n
⩾
2
或
F
(
n
)
=
{
1
,
n
=
1
1
,
n
=
2
F
(
n
−
1
)
+
F
(
n
−
2
)
,
n
⩾
3
F(n) = \begin{cases} 0, & n=0 \\ 1, & n = 0 \\ F(n-1) + F(n-2), & n \geqslant 2 \end{cases} \\或 \\ F(n) = \begin{cases} 1, & n=1 \\ 1, & n = 2 \\ F(n-1) + F(n-2), & n \geqslant 3 \end{cases}
F(n)=⎩⎪⎨⎪⎧0,1,F(n−1)+F(n−2),n=0n=0n⩾2或F(n)=⎩⎪⎨⎪⎧1,1,F(n−1)+F(n−2),n=1n=2n⩾3
通常,好像我们也没考虑第0项。
斐波那契数列求解,在计算机领域,我们可以通过多种不同的方法实现,因此也涉及到多种算法,这些算法性能不一,笔者进行了如下归纳记录,欢迎阅读指正。
递归法
时间复杂度:O(2n)
Java实现:
private static long fib(int n) {
if (n <= 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
实现方式非常简单粗暴,但因为每次递归调用都会对下面的 n-1,n-2项进行计算,重复计算很多,而且呈指数增长,性能不佳。以计算第6项为例:
光看这个图可能不直观。还是看一下实际运行情况吧,以笔者的电脑为例:mbp 2019, CPU:i7 6核,运行情况如下:
第1项:1 递归次数: 1 time cost: 0ms
第2项:1 递归次数: 1 time cost: 1ms
第3项:2 递归次数: 3 time cost: 0ms
第4项:3 递归次数: 5 time cost: 0ms
第5项:5 递归次数: 9 time cost: 0ms
第6项:8 递归次数: 15 time cost: 0ms
# 省略部分中间结果
第26项:121393 递归次数: 242785 time cost: 0ms
第27项:196418 递归次数: 392835 time cost: 1ms
第28项:317811 递归次数: 635621 time cost: 2ms
第29项:514229 递归次数: 1028457 time cost: 2ms
第30项:832040 递归次数: 1664079 time cost: 3ms
第31项:1346269 递归次数: 2692537 time cost: 5ms
第32项:2178309 递归次数: 4356617 time cost: 8ms
第33项:3524578 递归次数: 7049155 time cost: 13ms
第34项:5702887 递归次数: 11405773 time cost: 19ms
第35项:9227465 递归次数: 18454929 time cost: 27ms
第36项:14930352 递归次数: 29860703 time cost: 39ms
第37项:24157817 递归次数: 48315633 time cost: 62ms
第38项:39088169 递归次数: 78176337 time cost: 113ms
第39项:63245986 递归次数: 126491971 time cost: 167ms
第40项:102334155 递归次数: 204668309 time cost: 270ms
第41项:165580141 递归次数: 331160281 time cost: 446ms
第42项:267914296 递归次数: 535828591 time cost: 674ms
第43项:433494437 递归次数: 866988873 time cost: 1056ms
第44项:701408733 递归次数: 1402817465 time cost: 1713ms
第45项:1134903170 递归次数: 2269806339 time cost: 2771ms
可以看到:从计算第27项开始,这个递归方法就开始显得越来越力不从心了,性能下降非常明显。
递推法(动态规划)
这里和从大到小的递归法不同,用从最小的项开始往后计算的方式。因为我们只需要最后的第n项的值,所以只需要在计算过程始终保留第 n-1 ,第 n-2 项的值就够用了,而其余中间结果不需要保留。
时间复杂度:O(n)
Java实现:
private static long fib(int n) {
if (n <= 2) {
return 1;
}
long sub1 = 1;
long sub2 = 1;
long total = 2;
for (int i = 2; i < n; i++) {
total = sub1 + sub2;
sub2= sub1;
sub1 = total;
}
return total;
}
通项公式法
斐波那契数列第n项的通项公式为:
a
n
=
1
5
⋅
[
(
1
+
5
2
)
n
−
(
1
−
5
2
)
n
]
a_n = \frac{1}{\sqrt{5}} \cdot \left[\left(\frac{1+\sqrt{5}}{2}\right)^n - \left(\frac{1 - \sqrt{5}}{2}\right)^n\right]
an=51⋅[(21+5)n−(21−5)n]
推导过程可以参考:通项公式推导。
注意: 虽然是公式一步得到结果,但是注意时间复杂度可并不是 O(1) 哦,因为这里用到了幂运算,而幂运算可以通过时间复杂度为 O(logN) 的二分法实现。
时间复杂度:O(logN)
Java实现:
private static double SQRT_5 = Math.sqrt(5);
private static double fib3(int n) {
return 1 / SQRT_5 * (Math.pow(((1 + SQRT_5) / 2f), n) - Math.pow(((1 - SQRT_5) / 2f), n));
}
扩展-幂运算(Math.pow())
除了调用Math.pow()
库函数外,我们也可以手动实现时间复杂度为 O(logN) 的幂运算(二分法)。
假设我们要计算a的n次方,当n为偶数时:
a
n
=
a
⋅
a
⋯
a
⏟
n
=
a
⋅
a
⋯
a
⏟
n
2
⋅
a
⋅
a
⋯
a
⏟
n
2
=
(
a
n
/
2
)
2
a^n = \begin{matrix} \underbrace{ a\cdot a \cdots a } \\ n \\ \end{matrix} = \begin{matrix} \underbrace{ a\cdot a \cdots a } \\ \frac{n}{2} \\ \end{matrix} \cdot \begin{matrix} \underbrace{ a\cdot a \cdots a } \\ \frac{n}{2} \\ \end{matrix} =(a^{n/2})^2
an=
a⋅a⋯an=
a⋅a⋯a2n⋅
a⋅a⋯a2n=(an/2)2
若n为奇数,那么n-1就为偶数,就可以用上面的公式计算 a 的 n-1 次方,然后我们再乘一个a,就得到了 a 的 n 次方。
所以,虽然是要计算a的n次方,但我们只要把它的一半(an/2)计算出来,就可以了。然后我们用分治的思想,把 an/2 再一分为二,以此类推,所以时间复杂度是 O(logN)。
Java实现:
private static double pow(double a, int n) {
if (a == 0) {
return 0;
}
if (n == 0) {
return 1;
}
if (n == 1) {
return a;
}
// 考虑指数为负数的情况
if (n < 0) {
a = 1 / a;
n = -n;
}
double half = pow2(a, n / 2);
// 如果是奇数次方,就用偶数次方的结果再乘一个a
return (n & 1) == 0 ? half * half : half * half * a;
}
加速幂运算
通过位运算,还可以写出比上面二分法更快的幂运算,指数n的二进制表示中有多少位是1,就只需要计算多少次。
Java实现:
private static int pow(int a, int n) {
if (n == 0) {
return 1;
}
int res = 1;
int tmp = a;
while (n != 0) {
if ((n & 1) != 0) {
res *= tmp;
}
n >>= 1;
tmp *= tmp;
}
return res;
}
矩阵相乘法
矩阵乘法规则:
[
a
11
a
12
a
21
a
22
]
⋅
[
b
11
b
12
b
21
b
22
]
=
[
a
11
⋅
b
11
+
a
12
⋅
b
21
a
11
⋅
b
12
+
a
12
⋅
b
22
a
21
⋅
b
11
+
a
22
⋅
b
21
a
21
⋅
b
12
+
a
22
⋅
b
22
]
\begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{bmatrix} \cdot \begin{bmatrix} b_{11} & b_{12} \\ b_{21} & b_{22} \end{bmatrix} = \begin{bmatrix} a_{11} \cdot b_{11} + a_{12} \cdot b_{21} & a_{11} \cdot b_{12} + a_{12} \cdot b_{22} \\ a_{21} \cdot b_{11} + a_{22} \cdot b_{21} & a_{21} \cdot b_{12} + a_{22} \cdot b_{22} \end{bmatrix}
[a11a21a12a22]⋅[b11b21b12b22]=[a11⋅b11+a12⋅b21a21⋅b11+a22⋅b21a11⋅b12+a12⋅b22a21⋅b12+a22⋅b22]
基于以上矩阵乘法规则,结合斐波那契数列F(n) = F(n-1) + F(n-2)
的推导公式,如果将斐波那契数列第n项和第n-1项以2x1的矩阵表示,则可以得到以下推导:
推导参考:浅谈斐波那契数列——从递推到矩阵乘法
[
F
n
F
n
−
1
]
=
[
F
n
−
1
+
F
n
−
2
F
n
−
1
]
=
[
1
×
F
n
−
1
+
1
×
F
n
−
2
1
×
F
n
−
1
+
1
×
0
]
=
[
1
1
1
0
]
⋅
[
F
n
−
1
F
n
−
2
]
=
[
1
1
1
0
]
n
−
1
⋅
[
F
1
F
0
]
=
[
1
1
1
0
]
n
−
1
⋅
[
1
0
]
\begin{aligned} \begin{bmatrix} F_n \\ F_{n-1} \end{bmatrix} & = \begin{bmatrix} F_{n-1}+F_{n-2} \\ F_{n-1} \end{bmatrix} \\ & = \begin{bmatrix} 1×F_{n-1}+1×F_{n-2} \\ 1×F_{n-1}+1×0 \end{bmatrix} \\ & = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \cdot \begin{bmatrix} F_{n-1} \\ F_{n-2} \end{bmatrix} \\& = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1} \cdot \begin{bmatrix} F_1 \\ F_0 \end{bmatrix} \\& = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1} \cdot \begin{bmatrix} 1 \\ 0 \end{bmatrix} \end{aligned}
[FnFn−1]=[Fn−1+Fn−2Fn−1]=[1×Fn−1+1×Fn−21×Fn−1+1×0]=[1110]⋅[Fn−1Fn−2]=[1110]n−1⋅[F1F0]=[1110]n−1⋅[10]
根据矩阵乘法特性,一个m行的矩阵与一个n列的矩阵相乘,将得到一个m行、n列的矩阵。
因为
[
1
1
1
0
]
n
−
1
\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1}
[1110]n−1中,相乘的所有矩阵均为2x2,所以我们可以确定
[
1
1
1
0
]
n
−
1
\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1}
[1110]n−1的结果也是一个2x2的矩阵,我们暂且将这个结果矩阵表示为
[
a
b
c
d
]
\begin{bmatrix} a & b \\ c & d \end{bmatrix}
[acbd],那么就可以继续上面的推导:
[
F
n
F
n
−
1
]
=
[
a
b
c
d
]
⋅
[
1
0
]
=
[
a
c
]
\begin{bmatrix} F_n \\ F_{n-1} \end{bmatrix} = \begin{bmatrix} a & b \\ c & d \end{bmatrix} \cdot \begin{bmatrix} 1 \\ 0 \end{bmatrix} = \begin{bmatrix} a \\ c \end{bmatrix}
[FnFn−1]=[acbd]⋅[10]=[ac]
所以,
[
1
1
1
0
]
n
−
1
\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1}
[1110]n−1结果矩阵中的第1行、第一列的值,就是我们要计算的斐波那契数列第n项了。
最终还是变成了一个求a的n次方问题,那就可以参考上面的幂运算或加速幂运算方法了,只不过由数值计算变成了矩阵计算。
时间复杂度:O(logN)
Java实现:
private static int fib(int n) {
int[][] fibMagicMatrix = {
{1, 1},
{1, 0}
};
if (n <= 2) {
return fibMagicMatrix[0][0];
}
// int[][] res = matrixPow(fibMagicMatrix, n - 1);
int[][] res = matrixPowFast(fibMagicMatrix, n - 1);
return res[0][0];
}
/**
* 矩阵幂运算,二分法
* @param matrix 矩阵
* @param n 指数
* @return 结果矩阵
*/
private static int[][] matrixPow(int[][] matrix, int n) {
if (n == 0) {
//返回单位矩阵
return new int[][] {{1, 0},{0, 1}};
}
if (n == 1) {
return matrix;
}
int[][] half = matrixPow(matrix, n / 2);
int[][] res = matrixMultiply(half, half);
return (n & 1) == 0 ? res : matrixMultiply(res, matrix);
}
/**
* 矩阵幂运算,快速幂方法
* @param matrix 矩阵
* @param n 指数
* @return 结果矩阵
*/
private static int[][] matrixPowFast(int[][] matrix, int n) {
if (n == 0) {
//返回单位矩阵
return new int[][] {{1, 0},{0, 1}};
}
// 结果初始为单位矩阵
int[][] res = {{1, 0},{0, 1}};
int[][] tmp = matrix;
while (n != 0) {
if ((n & 1) != 0) {
// 这里矩阵相乘的顺序很重要。不同于普通数值计算(axb=bxa)
res = matrixMultiply(tmp, res);
}
tmp = matrixMultiply(tmp, tmp);
n >>= 1;
}
return res;
}
/**
* 2x2矩阵相乘
* @param ma 第一个矩阵
* @param mb 第二个矩阵
* @return 矩阵相乘结果,也是一个2x2的矩阵
*/
private static int[][] matrixMultiply(int[][] ma, int[][] mb) {
int[][] res = new int[2][2];
res[0][0] = ma[0][0] * mb[0][0] + ma[0][1] * mb[1][0];
res[0][1] = ma[0][0] * mb[0][1] + mb[0][1] * mb[1][1];
res[1][0] = ma[1][0] * mb[0][0] + ma[1][1] * mb[1][0];
res[1][1] = ma[1][0] * mb[0][1] + ma[1][1] * mb[1][1];
return res;
}
查表法
这里再记录一种非常规思路的方法。细心的读者可能注意到,在写通项公式法的时候,为了减少计算,我们是把
5
\sqrt5
5的值提前计算好作为常量直接带入的。那我们也可以把这种思路发挥到极致,直接把斐波那契数列的各项值都提前算好,要用的时候直接查出来就好了。
调试打印一下,我们就能发现,如果用 int 类型来保存,最多只能保存到第46项,第47项就溢出了:
即便用范围更大的 long 类型保存,最多也只能保存到第92项,第93项就溢出了:
所以用switch/case来实现的话,最多也就…92个case语句吧。
这种方法速度最快,O(1)时间复杂度即可解决。在某些场景下,也不失为一种解决办法,虽然和算法、代码美观等考量已经没什么关系了。
时间复杂度:O(1)
Java实现:
/**
* 通过查表直接获取第n项的值,不需要任何计算
* @param n 第n项
* @return 斐波那契数列第n项值
*/
private static long fibFastest(int n) {
switch (n) {
case 1:
case 2:
return 1;
case 3:
return 2;
case 4:
return 3;
case 5:
return 5;
// 省略其他 case 值...
case 46:
return 1836311903;
// 省略其他 case 值...
case 92:
return 7540113804746346429L;
default:
break;
}
return 0;
}