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); } }