1.问题描述html
一只青蛙一次能够跳上 1 级台阶,也能够跳上2 级。求该青蛙跳上一个n 级的台阶总共有多少种跳法。java
2.问题分析
设f(n)表示青蛙跳上n级台阶的跳法数。当只有一个台阶时,
即n = 1时, 只有1中跳法;
当n = 2时,有两种跳法;
当n = 3 时,有3种跳法;
当n很大时,青蛙在最后一步跳到第n级台阶时,有两种状况:
一种是青蛙在第n-1个台阶跳一个台阶,那么青蛙完成前面n-1个台阶,就有f(n-1)种跳法,这是一个子问题。
另外一种是青蛙在第n-2个台阶跳两个台阶到第n个台阶,那么青蛙完成前面n-2个台阶,就有f(n-2)种状况,这又是另一个子问题。ios
两个子问题构成了最终问题的解,因此当n>=3时,青蛙就有f(n)=f(n-1)+f(n-2)种跳法。上面的分析过程,其实咱们用到了动态规划的方法,找到了状态转移方程,用数学方程表达以下:算法
仔细一看,这不就是传说中的著名的斐波那契数列,可是与斐波那契数列的仍是有一点区别,斐波那契数列从0开始,f(0)=0,f(1)=1,f(2)=1。斐波那契数列(Fibonacci Sequence),又称黄金分割数列,由于当n趋于无穷大时,前一个数与后一个数的比值无限接近于黄金比例(√5−12√5−12的无理数,0.618…)。编程
3.递归实现
有了初始状态和状态转移方程,那么编程实现求解就不难了,参考下面的递归实现。多线程
int fib(int n){ if (n <= 0) return -1; if (1 == n) return 1; if (2 == n) return 2; return fib(n-1)+fib(n-2); }
3.1时间复杂度分析
以递归实现斐波那契数,效率是很是低下的,由于对子问题的求解fib(n-1)和fib(n-2)二者存在重叠的部分,对重叠的部分重复计算形成了浪费。但递归求解其优势也是显而易见的,代码简单,容易理解。并发
设f(n)为参数为n时的时间复杂度,很明显:f(n)=f(n-1)+f(n-2)变为f(n)-f(n-1)+f(n-2)=0,仔细一看,这就是数学上的二阶线性常系数齐次差分方程,求该差分方程的解,就是求得f(n)的非递归表达式,也就获得了上面递归算法的时间复杂度。关于齐次二阶常系数线性差分方程可能你们已经没有什么概念了,乍一听一脸懵逼,包括我本身,大学的高数基本已经还给老师了,可是涉及到算法,数学仍是至关的重要而且扮演者不可替代的角色。这里简单解释一下我本身温习后对齐次二阶常系数线性差分方程的理解,不清楚的,你们仍是要搜索相关资料,恶补一下吧!函数
差分概念:
“二阶线性常系数齐次”是对差分方程的修饰,“差分”也是对方程的修饰,先看一下差分的概念:
给定函数:ft=f(t),t=0,1,2...ft=f(t),t=0,1,2...,注意t的取值是离散的
一阶差分:Δyt=yt+1−yt=f(t+1)−f(t)Δyt=yt+1−yt=f(t+1)−f(t) 优化
差分方程的定义:
含有自变量t和两个或两个以上的函数值yt,yt+1,...,yt+nyt,yt+1,...,yt+n的方程,称为差分方程。出如今差分方程中的未知函数下标的最大差称为差分方程的阶。差分方程中函数值ytyt的指数为1,称为线性查分方程,函数值ytyt的系数为常量,称为常系数查分方程。差分方程能够化简为形如:this
若是f(t)=0f(t)=0,那么上面就是n阶线性齐次差分方程;
若是f(t)=0f(t)=0,那么上面就是n阶线性非齐次差分方程。
也就是说查分方程的常数项为0,就是齐次,非零就是非齐次。
若是查分方程中函数值ytyt前的系数是常量的话,那么就是常系数查分方程。
差分方程的表达式能够定义以下:
好了,了解了差分方程的阶,常系数,齐次,线性的概念,下面来辨识一下不一样的差分方程吧。
有了关于差分方程的一些定义和概念,如今应该知道为何f(n)-f(n-1)+f(n-2)=0叫做二阶线性常系数齐次差分方程了吧。由于n-(n-2)=2,因此是二阶,函数值f(n),f(n-1)和f(n-2)的指数是1,且系数均是常数,因此是线性常系数,又由于常数项为0,即等号右边为0,因此是齐次的。由于是根据函数值的表达式求函数的表达式,因此差分的,因此该方程就是恶心的二阶线性常系数齐次差分方程。
差分方程求解:
对于二阶线性常系数齐次差分方程的求解过程是,肯定特征方程->求特征方程的根->由求特征方程的根肯定通解的形式->再由特定值求得特解。
下面给出f(n)-f(n-1)+f(n-2)=0的解过程。
设f(n)=λnf(n)=λn,那么f(n)-f(n-1)+f(n-2)=0的特征方程就是:λ2−λ+1=0λ2−λ+1=0,求解得:λ=(1±√5)/2λ=(1±√5)/2。因此,f(n)的通解为:
由f(1)=1,f(2)=2可解得c1=(5+√5)/10, c2 ==(5-√5)/10,最终可得时间复杂度为:
我知道时间度的复杂常见的有且依序复杂度递增:
O(1), O(lgn),O(n‾√)O(n),O(n),O(nlgn),O(n2)O(n2),O(n3)O(n3),O(2n)O(2n),O(n!)。
那么上面求得的算法时间复杂度是归于哪一个级别。很明显是O(2n)O(2n)。也就是说斐波那契数列递归求解的算法时间复杂度是O(2n)O(2n)。
关于斐波那契数列递归求解的期间复杂度咱们简化其求解过程,按照以下方式求解。
递归的时间复杂度是: 递归次数*每次递归中执行基本操做的次数。因此时间复杂度是: O(2^n)。
3.2空间复杂度
每一次递归都须要开辟函数的栈空间,递归算法的空间复杂度是:
递归深度N∗每次递归所要的辅助空间
递归深度N∗每次递归所要的辅助空间
若是每次递归所需的辅助空间是常数,则递归的空间复杂度是 O(N)。由于上面的递归实现,虽然每次递归都会有开辟两个分支,按理说递归调用了 多少次,就开辟了多大的栈空间,按照这个逻辑,那么空间复杂度与时间复杂应该是同样的, 都是O(2^n)。那么这个逻辑错在了哪里呢?首先咱们要知道函数的调用过程大概是什么样的,调用者(caller)将被调用者(callee)的实参入栈,call被调用者,被调用者中保留caller的栈底指针EBP,将ESP赋给EBP开始一个新的栈帧,函数结束后清理栈帧,pop原函数栈底指针EBP到ESP,这一步也就是恢复函数调用的现场。如今再来看看上面斐波那契数列的递归实现,由于是单线程执行,以Fib(5)为例,函数执行的过程应该是以下图所示:
可见递归的深度越深,开辟的形参栈空间就会越大。图中最深处的开辟了最大的辅助空间,当函数执行的流程向上回溯时,你就会发现,后面开辟的辅助栈空间都是在前面开辟的栈空间上开辟的,也就是空间的重复利用,因此说递归算法的空间复杂度是递归最大的深度*每次递归开辟的辅助空间,因此斐波那契数列的递归实现的空间复杂度是O(n)。
图中示例的是单线程状况下递归时的函数执行流程,可是在多线程的状况下,就不是这个样子,由于每一个线程函数并发执行,拥有本身的函数栈,因此空间复杂度要另当计算,这里就不作深究,有兴趣的读者可自行研究。
4.迭代实现
递归实现虽然简单易于理解,可是O(2^n)的时间复杂度和O(n)的空间却让人没法接受,下面迭代法的具体实现,比较简单,就再也不赘述实现步骤。时间复杂度为O(n),空间复杂度为O(1)。
int fibIteration(int n){ if (n <= 0) return -1; if (1 == n) return 1; if (2 == n) return 2; int res=0,a=1,b=2; for(int i=3;i<=n;++i){ res=a+b; a=b; b=res; } return res; }
这个方法是求斐波那契数列的最快方法吗?固然不是,最快的应该是下面的矩阵法。
根据上面的递归公式,咱们能够获得。
于是计算f(n)就简化为计算矩阵的(n-2)次方,而计算矩阵的(n-2)次方,咱们又能够进行分解,即计算矩阵(n-2)/2次方的平方,逐步分解下去,因为折半计算矩阵次方,于是时间复杂度为O(logn)。
下面给出网友beautyofmath在文章关于斐波那契数列三种解法及时间复杂度分析中的实现。
#include <iostream> using namespace std; class Matrix { public: int n; int **m; Matrix(int num) { m=new int*[num]; for (int i=0; i<num; i++) { m[i]=new int[num]; } n=num; clear(); } void clear() { for (int i=0; i<n; ++i) { for (int j=0; j<n; ++j) { m[i][j]=0; } } } void unit() { clear(); for (int i=0; i<n; ++i) { m[i][i]=1; } } Matrix operator=(const Matrix mtx) { Matrix(mtx.n); for (int i=0; i<mtx.n; ++i) { for (int j=0; j<mtx.n; ++j) { m[i][j]=mtx.m[i][j]; } } return *this; } Matrix operator*(const Matrix &mtx) { Matrix result(mtx.n); result.clear(); for (int i=0; i<mtx.n; ++i) { for (int j=0; j<mtx.n; ++j) { for (int k=0; k<mtx.n; ++k) { result.m[i][j]+=m[i][k]*mtx.m[k][j]; } } } return result; } }; int main(int argc, const char * argv[]) { unsigned int num=2; Matrix first(num); first.m[0][0]=1; first.m[0][1]=1; first.m[1][0]=1; first.m[1][1]=0; int t; cin>>t; Matrix result(num); result.unit(); int n=t-2; while (n) { if (n%2) { result=result*first; } first=first*first; n=n/2; } cout<<(result.m[0][0]+result.m[0][1])<<endl; return 0; }
有兴趣的读者可自行给出实现,本人后续再补充代码。
6.问题拓展
青蛙跳台阶问题能够引伸为以下问题:
一只青蛙一次能够跳上1级台阶,也能够跳上2 级,……,也能够跳上n 级,此时该青蛙跳上一个n级的台阶总共有多少种跳法?
6.1问题分析
当n = 1 时, 只有一种跳法,即1阶跳:Fib(1) = 1;
当n = 2 时, 有两种跳的方式,一阶跳和二阶跳:Fib(2) = Fib(1) + Fib(0) = 2;
当n = 3 时,有三种跳的方式,第一次跳出一阶后,后面还有Fib(3-1)中跳法; 第一次跳出二阶后,后面还有Fib(3-2)中跳法,一次跳到第三台阶,Fib(3) = Fib(2) + Fib(1)+Fib(0)=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(0),又由于Fib(n-1)=Fib(n-2)+Fib(n-3)+…+Fib(0),两式相减得:Fib(n)-Fib(n-1)=Fib(n-1),因此Fib(n) = 2*Fib(n-1),n >= 2。递归等式以下:
递归等式是一个以2为公比的等比数列,因此递归和迭代实现起来都比较简单,参考以下:
//递归法 //时间复杂度O(n),空间复杂度O(n) int fib(int n){ if (1 == n) return 1; return 2*fib(n-1); } //迭代法 //时间复杂度O(n),空间复杂度O(1) int fib(int n){ int res=1; if (1 == n) return res; for(int i=2;i<=n;++i) res=2*res; return res; }
历时两天,参考了不少博文资料,即当中也遇到了不少不解的问题,很痛苦,尤为是研究已经忘记了的差分方程,不过仍是坚持了下来。本篇力求较全面的给出青蛙跳台阶问题分析,各类解法以及时间复杂度和空间复杂度的分析,让你们可以不留疑惑的了解斐波那契数列的求解。
转载于 https://blog.csdn.net/K346K346/article/details/52576680
[1]斐波那契数列.百度百科
[2]青蛙跳台阶问题
[3]关于斐波那契数列三种解法及时间复杂度分析
[4]差分方程的基本概念
[5]二阶线性常系数齐次差分方程的求解
[6]时间复杂度&空间复杂度分析
补充
java 代码实现
一只青蛙一次能够跳上1级台阶,也能够跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(前后次序不一样算不一样的结果)。
1 非递归
//利用斐波那契数列从下往上算,避免重复计算,提升效率 //这个问题用递归确实开销会很大,由于递归里面有不少重复计算,最好用迭代。 public class Solution1 { public int JumpFloor(int target) { if (target <= 0) { return 0; } if (target == 1) { return 1; } if (target == 2) { return 2; } int one = 1; int two = 2; int result = 0; for (int i = 2; i < target; i++) { result = one + two; one = two; two = result; } return result; } }
2 递归
public class Solution { public int JumpFloor(int target) { if(target<=0){ return 0; } if(target==1){ return 1; } if(target==2){ return 2; } return JumpFloor(target-1)+JumpFloor(target-2); } }
题目升级:
一只青蛙一次能够跳上1级台阶,也能够跳上2级……它也能够跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
方法1:
public class Solution { public int JumpFloorII(int target) { if (target == 0 || target == 1) { return 1; } int sum = 1; for (int i = 2; i <= target; i++) { sum = 2 * sum; } return sum; } }
方法2:
public class Solution1 { public int JumpFloorII(int target) { if(target == 0) { return 0; } int[] dp = new int[target + 1]; dp[0] = 1; dp[1] = 1; for(int i = 2;i <= target;i++) { dp[i] = 0; for(int j = 0;j < i;j++) { dp[i] += dp[j]; } } return dp[target]; } }
方法3 递归:
//递归方法 /* 假设一共有n阶,一样共有f(n)种跳法,那么这种状况就比较多, 最后一步超级蛙能够从n-1阶往上跳,也能够n-2阶,也能够n-3…等等等,一次类推。 因此,可知: 式1: f(n) = f(n-1) + f(n-2) + ... + f(2) + f(1) 并且,容易得出: 式2: f(n-1) = f(n-2) + f(n-3) + ... + f(2) + f(1) 将式1中的f(n-2) + f(n-3) + … + f(2) + f(1) 替换成式2,可知: */ public class Solution2 { public int JumpFloorII(int target) { if (target == 1) { return 1; } else { return 2 * JumpFloorII(target - 1); } } }
方法4 :
左移
public class Solution { /* 实际上是隔板问题,假设n个台阶,有n-1个空隙,能够用0~n-1个隔板分割,c(n-1,0)+c(n-1,1)+...+c(n-1,n-1)=2^(n-1),其中c表示组合。 有人用移位1<<--number,这是最快的。直接连续乘以2不会慢多少,编译器会自动优化。不过移位仍是最有启发的 */ public int JumpFloorII(int target) { if(target<=0) return 0; return 1<<(target-1); } }