求斐波那契数列的几种方法
力扣练习记录
已经使用Java和python在力扣上测试通过
目录
Fabonacci数列介绍
斐波那契数列中的斐波那契数会经常出现在我们的眼前——比如松果、凤梨、树叶的排列、某些花朵的花瓣数(典型的有向日葵花瓣),蜂巢,蜻蜓翅膀,超越数e
(可以推出更多),黄金矩形、黄金分割
、等角螺线等。
斐波那契数还可以在植物的叶、枝、茎等排列中发现。例如,在树木的枝干上选一片叶子,记其为数0,然后依序点数叶子(假定没有折损),直到到达与那些叶子正对的位置,则其间的叶子数多半是斐波那契数。叶子从一个位置到达下一个正对的位置称为一个循回。叶子在一个循回中旋转的圈数也是斐波那契数。在一个循回中叶子数与叶子旋转圈数的比称为叶序(源自希腊词,意即叶子的排列)比。多数的叶序比呈现为斐波那契数的比。
黄金分割。随着数列项数的增加,前一项与后一项之比越来越逼近黄金分割的数值 0.6180339887..…
斐波那契数列通式
对于斐波那契数列1、1、2、3、5、8、13、……
。有如下定义
F(n)=F(n-1)+F(n-2)
F(1)=1
F(2)=1
著名案例-兔子繁殖问题
Fabonacci数列比较著名的是以下这个问题。
兔子出生1个月后长大,2月后就有繁殖能力,有繁殖能力后每个月产1只幼兔。
最开始有1只幼兔,求第n个月时有多少只兔子?
(不用考虑兔子的雌雄和死亡)
经过月数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | … |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
幼仔对数 | 1 | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | … |
成兔对数 | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | … |
总体对数 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 | … |
n=1: F(1)=1;
n=2: F(2)=1;
n>=3: F(n)=F(n-1)+F(n-2)
幼仔对数=前月成兔对数
成兔对数=前月成兔对数+前月幼仔对数
总体对数=本月成兔对数+本月幼仔对数
可以看出幼仔对数、成兔对数、总体对数都构成了一个数列。这个数列有关十分明显的特点,那是:前面相邻两项之和,构成了后一项。
这个数列是意大利中世纪数学家斐波那契在<算盘全书>中提出的,这个级数的通项公式,除了具有a(n+2)=an+a(n+1)
(即满足斐波那契数列)的性质外,还可以证明通项公式为:an=(1/√5)*{[(1+√5)/2]^n-[(1-√5)/2]^n}(n=1,2,3,...)
我尝试的几种方法
前情提要:
按力扣剑指 Offer 10- I 斐波那契数列要求做,使用int类型,并且对其进行溢出处理。
0 <= n <= 100
在JAVA中定义栈大小默认是48K,最小可以设置为32K;要进行溢出处理。
python中可以认为是无限内存,不存在溢出,不做溢出处理
递归的简单介绍
递归的简单复杂度分析
复杂度 | 计算公式 |
---|---|
时间复杂度 | 递归的总次数*每次递归的数量。 |
间复杂度 | 递归的深度*每次递归创建变量的个数 |
概念演示
几种方案
递归
在力扣运行时超时
问题:开辟的栈过多 ,创建的栈成指数增加,时间过长,内存越界
常用的Java递归解法
java代码实现:
/**
* 递归方法实现,使用long扩大计算范围
* f(n) = f(n - 1) + f(n - 2)
* 最高支持 n = 92 ,否则超出 Long.MAX_VALUE
* @param num n
* @return f(n)
*/
public static long Fib(int num) {
if(num < 1)
return 0;
if(num < 3)
return 1;
return Fib(num - 1) + Fib(num - 2);
}
/**
* 递归方法实现,用int
* f(n) = f(n - 1) + f(n - 2)
* 最高支持 1000000007(int 能表示的斐波那契数列最大值),否则超出 取余
* @param num n
* @return f(n)
*/
public class Solution {
public int Fibonacci(int n) {
if(n == 0){
return 0;
}
if((n == 1) || (n == 2)){
return 1;
}
return (Fibonacci(n - 1) + Fibonacci(n - 2) ) % 1000000007;
}
}
演示图:
可以看到,重复计算很多。时间、空间消耗巨大。
递归的复杂度
此种方法,重复求解的次数,太多;且随问题规模增大,时间复杂度和空间复杂度快速增长!
在第一个函数,递归调用过程中Fib(3)被计算了2次,Fib(2)被计算了3次,Fib(1)被调用了5次,Fib(0)被调用了3次。
我们都知道调用函数的开销是很大的,而该种方法就是在不断地调用同一个函数还做着许多无用功,反复求解一些之前求过的,可见它的效率是很低的。
递归的效率低下,但优点是代码简单,容易理解。
第n层节点个数:2^n - 1个
前n层节点个数:1+2+4+……+2^n = (2 ^ n) - 1
- 时间复杂度为(二叉树的节点个数):
O((2^h)-1)=O(2^n)
。 - 空间复杂度为树的高度:
h即O(n)
.
尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。
当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
尾递归函数的特点是在回归过程中不用做任何操作
。最精髓就是 通过参数传递结果,达到不压栈的目的
。
Fib(1) = 1;
Fib(2) = 1;
java代码实现:
int Fib(int n,int a,int b){
if(n == 1) return a;
return Fib(n - 1,a,a + b)
}
c实现:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
long Fib(long first, long second, long n){
if (n < 3)
return 1;
if (3 == n)
{
return first + second;
}
return Fib(second, first + second, n - 1);
}
尾递归的复杂度
尾递归若优化
- 时间复杂度是
O(n)
- 空间复杂度可达到
O(1)
使用循环
时间换空间的 时间复杂度O(N)
根据F(0)和F(1)算出F(2),然后保存中间值F(1)和F(2)再求得F(3),以此类推。
使用自底向上的方法。
开辟一个有三个数字的小数组保存计算过程中的值。
java代码实现:
/**
* 使用数组,记录fib(n)、fib(n-1)、fib(n-2)三个值
*/
public int fib(int n) {
/*使用循环,还有数组*/
if(n<2) return n;
int[] a = {0,1,1};
for(int i=0;i<n;i++){
a[2] = (a[0] + a[1]) % 1000000007;
a[0] = a[1];
a[1] = a[2];
}
return a[0];
}
c实现
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Fib(int first, int second, int n){
int third = 0;
if (n < 3)
return 1;
while (n >3){
int temp = second;
second = first + second;
first = temp;
n--;
}
third = first + second;
return third;
}
python实现
# 从位数考虑
num = int(input('please input a number : '))
def Fib(num):
list1 = []
# 从1开始,递增1,到num-1结束
for i in range(num):
if i <= 1:
list1.append(1)
else:
list1.append(list1[-2] + list1[-1])
return list1
print(Fib(num)) # 输出num位数列
使用循环的复杂度
算法 | 时间复杂度 | 空间复杂度 |
---|---|---|
递归算法 | O(2^n) | O(n) |
使用循环 | O(n) | O(1) |
时间和空间复杂度相当于使用普通的递归有了巨大的提升!