斐波那契数列
在学习JAVA的过程中,自己做了一些笔记,借鉴了一些别的大佬的一些见解,做了一些笔记整理。将这篇笔记传到我的博客上来,希望能够对你有所帮助,同时我自己还有些不理解的地方,也做了标注,大家可以和我一起讨论,关于斐波那契数列的问题很早之前整理的,打算发博客后,将斐波那契数列作为第一篇博客发上来,可能有一些不足之处,希望大家多多包涵。主要列举了一些关于力扣上用到斐波那契数列的题目,还有一些扩展。
1、斐波那契算法
:
题目:剑指 Offer 10- I. 斐波那契数列
难度简单
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解题思路:
斐波那契数列的定义是 f(n + 1) = f(n) + f(n - 1),生成第 n项的做法有以下几种:
1、递归法:
原理: 把 f(n)问题的计算拆分成 f(n-1) 和 f(n-2) 两个子问题的计算,并递归,以 f(0) 和 f(1) 为终止条件。
缺点: 大量重复的递归计算,例如 f(n) 和 f(n−1) 两者向下递归需要 各自计算f(n−2) 的值。
2、记忆化递归法:
原理: 在递归法的基础上,新建一个长度为 n 的数组,用于在递归时存储 f(0) 至 f(n) 的数字值,重复遇到某数字则直接从数组取用,避免了重复的递归计算。
缺点: 记忆化存储需要使用 O(N) 的额外空间。
3、动态规划:
原理: 以斐波那契数列性质 f(n + 1) = f(n) + f(n - 1)为转移方程。
从计算效率、空间复杂度上看,动态规划是本题的最佳解法。
下图帮助理解递归法的 “重复计算” 概念。
动态规划解析:
状态定义: 设 dp 为一维数组,其中 dp[i] 的值代表 斐波那契数列第 i 个数字 。
转移方程: dp[i + 1] = dp[i] + dp[i - 1] ,即对应数列定义 f(n + 1) = f(n) + f(n - 1);
初始状态: dp[0] = 0, dp[1] = 1 ,即初始化前两个数字;
返回值: dp[n] ,即斐波那契数列的第 n 个数字。
空间复杂度优化:
若新建长度为 n的 dp 列表,则空间复杂度为 O(N) 。
由于 dp列表第 i 项只与第 i-1 和第 i-2 项有关,因此只需要初始化三个整形变量 sum, a, b ,利用辅助变量 sum 使 a, b 两数字交替前进即可 (具体实现见代码) 。
节省了 dp 列表空间,因此空间复杂度降至 O(1) 。
循环求余法:
大数越界: 随着 n 增大, f(n)会超过 Int32 甚至 Int64 的取值范围,导致最终的返回值错误。
求余运算规则: 设正整数 x, y, p ,求余符号为t⊙ ,则有(x+y)⊙p=(x⊙p+y⊙p)⊙p 。
解析: 根据以上规则,可推出 f(n)⊙p=[f(n−1)⊙p+f(n−2)⊙p]⊙p ,从而可以在循环过程中每次计算 sum=(a+b)⊙1000000007 ,此操作与最终返回前取余等价。
//解法1:
public static int fib1(int n) {
int[] f=new int[n+1];//n+1的空间可以存放0-n
//初始条件
if(n==0){
return 0;
}
f[1]=1;
for(int i=2;i<=n;++i){
f[i]=f[i-1]+f[i-2];
f[i]%=1000000007;
}
return f[n];
}
解法2:优化后
时间复杂度:O(n)
空间复杂度:O(1)
public static int fib2(int n) {
if (n == 0) {
return 0;
}
int a = 0;
int b = 1;
for (int i = 2; i <= n; ++i) {//如果从i=2开始循环,返回b,b代表前面两值相加后值
int sum = a + b;
a = b;
b = sum % 1000000007;
}
return b;//如果n=1,则跳过循环直接返回b=1;
}
public static int fib(int n) {
int a = 0, b = 1, sum;
for(int i = 0; i < n; i++){////如果从i=0开始循环,返回a,a代表还没进行相加,是上次的
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;//如果n=0,则直接跳过循环返回a=0;
}
题目:剑指offer-矩形覆盖
https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking&tab=answerKey
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?
比如n=3时,23的矩形块有3种覆盖方法:
解题思路:
n=1时,显然只有一种方法
n=2时,如图有2种方法
n=3,如图有3中方法
n=4,如图有4种方法。
那我们就再分析以下,从n=3到n=4,怎么来的呢?
这里有2种情况:
直接在n=3的情况下,再后面中添加一个竖着的。这个很显然成立,有3种情况(重复情况去掉)
然后横着的显然能添加到n-2的情况上,也就是在n=2后面,添加2个横着的。有2种情况
通过以上分析,发现刚好和图中的个数一样。
所以总结:f [n]表示2n大矩阵 的方法数。
可以得出:f[n] = f[n-1] + f[n-2],初始条件f[1] = 1, f[2] =2
另一种思路:
要覆盖 2n 的大矩形,可以先覆盖 21 的矩形,再覆盖 2(n-1) 的矩形;或者先覆盖 22 的矩形,再覆盖 2(n-2) 的矩形。而覆盖 2*(n-1) 和 2*(n-2) 的矩形可以看成子问题。该问题的递推公式如下:
所以代码可用递归,记忆递归,和动态规划和递推
//优化后:同斐波列契
public int rectCover(int target) {
if(target==0||target==1||target==2){//初始条件
return target;
}
int a=1;
int b=2;
for(int i=3;i<=target;++i){
int sum=a+b;
a=b;
b=sum;
}
return b;
}
时间复杂度:O(n)
空间复杂度:O(1)
题目:力扣70. 爬楼梯
难度简单
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。则和斐波列那有点不同
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
解法:斐波列那问题:每一步有两种走法:走一步和走两步,所以这两种方法相加
f[i]是走到第i布的方法:f[i]=f[i-1]+f[i-2];
public int climbStairs(int n) {
//优化前的方法就不写了,同斐波列那方法
//int[] f=new int[n+1];
// f[0]=1;
// f[1]=1;
// for(int i=2;i<=n;++i){
// f[i]=f[i-1]+f[i-2];
// }
// return f[n];
//优化后
if(n==1){//需要base case n=1,只有一种方法
return 1;
}
//初始化
int a=1;
int b=2;
/*
不建议为f[0]设置值的,循环里面应该从3开始。 题目说到了n是一个正整数,
那么n就不会是0,那么如果循环里面,以2为开始,就会面临f[2] = f[1] + f[0]
所以也就违背了函数的入参为正整数的原则。 如果为f[0]指定值的话,通过下面的方程,
可能会改变f[2]的值,因为不知道应该为f[0]设置0还是1, 但是我们可以明确的知道f[2]就是有2种
方法,所以我们可以直接赋值f[2]=2,尽量避免其他可变因素。 而f[n]也正好为第n阶所求的
*/
for(int i=3;i<=n;++i){
int sum=a+b;
a=b;
b=sum;
}
return b;//如果n=2,直接等于b=2
}
题目:剑指 Offer 10- II. 青蛙跳台阶问题
难度简单
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解题思路:
此类求 多少种可能性 的题目一般都有 递推性质 ,即 f(n) 和 f(n-1)…f(1)之间是有联系的。
设跳上 n级台阶有 f(n) 种跳法。在所有跳法中,青蛙的最后一步只有两种情况: 跳上 1 级或 2 级台阶。
当为 1 级台阶: 剩 n-1 个台阶,此情况共有 f(n-1) 种跳法;
当为 2 级台阶: 剩 n-2 个台阶,此情况共有 f(n-2) 种跳法。
f(n) 为以上两种情况之和,即 f(n)=f(n-1)+f(n-2) ,以上递推性质为斐波那契数列。本题可转化为 求斐波那契数列第 n 项的值 ,与 面试题10- I. 斐波那契数列 等价,唯一的不同在于起始数字不同。
青蛙跳台阶问题: f(0)=1 , f(1)=1 , f(2)=2 ;
斐波那契数列问题: f(0)=0 , f(1)=1 , f(2)=1
public static int numWays(int n) {
// n=0,没有意义
int a = 1;
int b = 1;
for (int i = 2; i <= n; ++i) {//如果从i=2开始循环,返回b,b代表前面两值相加后值
int sum = a + b;
a = b;
b = sum % 1000000007;
}
return b;//如果n=1,则跳过循环直接返回b=1;
}
题目:剑指offer变态跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解题思路:分析:用Fib(n)表示跳上n阶台阶的跳法数。如果按照定义,Fib(0)肯定需要为0,否则没有意义。
但是我们设定Fib(0) = 1;n = 0是特殊情况,通过下面的分析就会知道,强制令Fib(0) = 1很有好处。
ps. Fib(0)等于几都不影响我们解题,但是会影响我们下面的分析理解。
当n = 1 时, 只有一种跳法,即1阶跳:Fib(1) = 1;
当n = 2 时, 有两种跳的方式,一阶跳和二阶跳:Fib(2) = 2;
到这里为止,和普通跳台阶是一样的。
当n = 3 时,有三种跳的方式,第一次跳出一阶后,对应Fib(3-1)种跳法; 第一次跳出二阶后,
对应Fib(3-2)种跳法;第一次跳出三阶后,只有这一种跳法。Fib(3) = Fib(2) + Fib(1)+ 1 = Fib(2) +
Fib(1) + Fib(0) = 4;
当n = 4时,有四种方式:第一次跳出一阶,对应Fib(4-1)种跳法;第一次跳出二阶,对应Fib(4-2)种跳法;
第一次跳出三阶,对应Fib(4-3)种跳法;第一次跳出四阶,只有这一种跳法。所以,Fib(4) = Fib(4-1) +
Fib(4-2) + Fib(4-3) + 1 = Fib(4-1) + Fib(4-2) + Fib(4-3) + Fib(4-4) 种跳法。
当n = n 时,共有n种跳的方式,第一次跳出一阶后,后面还有Fib(n-1)中跳法; 第一次跳出二阶后,
后面还有Fib(n-2)中跳法…第一次跳出n阶后,后面还有 Fib(n-n)中跳法。
Fib(n) = Fib(n-1)+Fib(n-2)+Fib(n-3)+…+Fib(n-n) = Fib(0)+Fib(1)+Fib(2)+…+Fib(n-1)。
通过上述分析,我们就得到了通项公式:
Fib(n) = Fib(0)+Fib(1)+Fib(2)+…+ Fib(n-2) + Fib(n-1)
因此,有 Fib(n-1)=Fib(0)+Fib(1)+Fib(2)+…+Fib(n-2)
两式错位相减得:Fib(n)-Fib(n-1) = Fib(n-1) ----》 Fib(n) = 2*Fib(n-1) n >= 3
这就是我们需要的递推公式:Fib(n) = 2*Fib(n-1) n >= 3
递归方法:
public static int JumpFloorIIj(int target) {
if (target == 0 || target == 1)
return 1;
if (target == 2)
return 2;
int sum = 0;
for (int i = 0; i < target; i++) {
sum += JumpFloorIIj(i);
}
return sum;
}
//动态规划
public static int JumpFloorII(int target) {
if(target==0){ //如果为0层台阶时,返回0
return 0;
}
int a[] = new int[target+2]; //加2的原因是下面的a数组要初始化到第三个元素
a[0]=1;
a[1]=1;
a[2]=2;
if(target<3&&target>0){
return a[target];
}
for(int i=3;i<=target;i++){
a[i]=2*a[i-1];
}
return a[target];
}
//动态规划的优化
public static int jump2(int num){
int a=1;
int b=1;
for(int i=2;i<=num;++i){
b=2*a;
a=b;
}
return b;
}
要跳上第 n 级台阶,可以从第 n−1 级台阶一次跳上来,也可以可以从第 n−2 级台阶一次跳上来,也可以可以从第 n−3 级台阶一次跳上来,…,也可以可以从第 1 级台阶一次跳上来。那么问题就很简单啦,同样的,令 f(n) 表示从第一级台阶跳上第 n 级台阶有几种跳法。则有如下递推公式:
f(n)=f(n−1)+f(n−2)+…+f(1)
同时,f(n−1) 也可以表示如下:
f(n−1)=f(n−2)+f(n−3)+…+f(1)
所以,由上面两个公式可知:
f(n)=2f(n−1)=4f(n−2)=8f(n−3)=…
因为 f(1)=1,所以 f(n)=2^n-1。
public static double Jump2(int num){
if(num==0||num==1){
return num;
}
return Math.pow(2,num-1);
}
题目: 青蛙跳台阶变种
一只青蛙一次可以跳上1级台阶,也可以跳上2级,但年幼的青蛙不能连续的跳2级,
求年幼青蛙跳上一个n级的台阶总共有多少种方法?
解释:如果上一次跳了一级:则这次可以选择跳一级或者两级
如果上一次跳了两级,这次只能跳一级了
https://blog.youkuaiyun.com/qq_34499130/article/details/80136565
// n:剩余台阶级数
// two:为1表示上一步跳了2级,为0表示上一步跳了1级
//方法1 递归解法: 不考虑超时
public static int jump(int n, int pre){
if(n<=1){
return n;
}
if(n==2){
if(pre==0){//如果上一次跳了一级,则剩下两级台阶有两种跳法
return 2;
}
if(pre==1){
return 1;//如果上一次跳了2级,则剩下两级台阶有1种跳法
}
}
return pre==1? jump(n-1,pre):jump(n-1,pre)+jump(n-2,pre);
//如果上一次跳了一级,则剩下跳一级和两级方法
//如果上一次跳了2级,则只剩下跳一级
}
```java
public static int jump(int n) {
int[][] v=new int[n+1][2];
// v[i][0]表示上一跳为1级时,剩余i级台阶有v[i][0]种跳法
// v[i][1]表示上一跳为2级时,剩余i级台阶有v[i][1]种跳法
//如果有一级台阶,则只有一种方法
v[1][0] = 1;
v[1][1] = 1;
//如果有两级台阶,则分别赋初始值
if (n >= 2) {
v[2][0] = 2;
v[2][1] = 1;
}
for (int i = 3; i <= n; ++i) {
v[i][0] = v[i - 1][0] + v[i - 2][1];//如果上一次跳了一级台阶
v[i][1] = v[i - 1][0];//如果上一次跳了两级台阶
}
return v[n][0];//这里不明白为什么返回v[n][0]
}
public static void main(String[] args) {
int jump = jump(4);
System.out.println(jump);
}
/