斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你 n ,请计算 F(n) 。
示例 1:
输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
- 0 <= n <= 30
分析
这是一道递推逻辑的题目,既然已经知道了初始值,那么如果就可以使用递归一层一层的返回以达到求取结果的目的.
实现
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n-1) + fib(n-2);
}
这种递归的实现方式在有些时候很有用,例如树的遍历等。但是缺陷也很明显:
- 由于执行的函数需要不停的压栈保存现场,因此在递归层数比较多时需要占用较多内存,容易造成内存溢出;
- 递归是函数调用自身,而函数调用是有时间和空间的消耗的;每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址以及临时变量,而往栈中压入数据和弹出数据都需要时间;
- 递归中的很多计算是重复的,例如示例中的 f i b ( n − 1 ) fib(n-1) fib(n−1)和 f i b ( n − 2 ) fib(n-2) fib(n−2)最终都会需要计算 f i b ( 0 ) + f i b ( 1 ) fib(0) + fib(1) fib(0)+fib(1);
- 递归在很多时候阅读起来并不容易。
所以在压栈层数比较多时,应该尽可能规避使用递归算法.
算法复杂度
时间复杂度
从分析可以知道, f i b ( n ) = f i b ( n − 1 ) + f i b ( n − 1 ) fib(n)=fib(n-1)+fib(n-1) fib(n)=fib(n−1)+fib(n−1),而 f i b ( n − 1 ) = f i b ( n − 2 ) + f i b ( n − 3 ) fib(n-1)=fib(n-2)+fib(n-3) fib(n−1)=fib(n−2)+fib(n−3)等,即每一个 f i b fib fib都会分解出两个子运算的和,呈现幂增长趋势,这里可以有两种理解方式:
- 解出通项公式
可知 f ( n ) f(n) f(n)是关于常数的 n n n次幂 - 借助满二叉树的节点数
顶层是f(n),在n减小到一定的成都之前每层翻倍,则所有子节点的和约为2^n级别,但不会满二叉,所以比这小点,而满二叉树的节点的数为 2 n − 1 2^n - 1 2n−1,所以时间复杂度为 O ( 2 n ) O(2^n) O(2n).
空间复杂度
时间复杂度为 O ( 1 ) O(1) O(1)
其他算法
- 使用数组保存每个 f ( n ) f(n) f(n)的值:
int fib(int n) {
int fib(int n) {
int *result = malloc(sizeof(int) * (n + 1));
memset(result, 0, sizeof(int) * (n + 1));
int a[2] = {0, 1};
memcpy(result, a, (n >=1 ? 2 : n) * sizeof(int));
for (int index = 2; index <= n; index++) {
result[index] = result[index - 1] + result[index - 2];
}
return result[n];
}
算法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n).
而事实上,数组保存所有的 f ( n ) f(n) f(n)并没有太大的意义,只需要保存前两项就可以了,其余的空间是不需要的,所以算法可以针对空间复杂度优化一下:
int fib(int n) {
int a[2] = {0, 1};
if (n < 2) {
return a[n];
}
for (int index = 2; index < n; index++) {
a[1] = a[0] + a[1];
a[0] = a[1] - a[0];
}
return a[0] + a[1];
}
这样时间复杂度依然是 O ( n ) O(n) O(n),但是空间复杂度却可以降低为 O ( 1 ) O(1) O(1).