我們先來看一下什么是斐波那契數列,這個應該在大一高數時大家都學過。
斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……在數學上,斐波納契數列以如下被以遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
——《百度百科》
具體函數表達參考下面這張圖。
那么我們該如何求解與斐波那契數列相關的問題呢?
先看一下題目描述:
大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項為0)。
具體可以用以下幾種方法求解:
使用遞歸
遞歸能將一個問題劃分成多個子問題進行求解。求F(n)時會轉化成求F(n-1)、F(n-2),以此類推,最后轉化成幾個F(0)、F(1)相加的結果。實現如下:
public class Solution {
public int Fibonacci(int n) {
int result = 0;
if (n <= 1){
return n;
}else{
result = Fibonacci(n - 1) + Fibonacci(n - 2);
}
return result;
}
}
運行時間與占用內存如下:
可是使用遞歸會有一個問題,會重復計算一些子問題。比如計算F(5)需要計算F(4)和F(3),計算F(4)需要計算F(3)和F(2),可以看到F(3)被重復計算了。造成了資源浪費。
所以我們換個思路。
動態規劃
遞歸是將一個問題劃分成多個子問題進行求解。動態規劃相當于是個相反的過程,將子問題的解存儲起來,用來解決大問題,比如已知F(0)、F(1),進行求F(2),再進一步求F(3),以此類推,直至求到F(n)。這樣子就不會有重復求解子問題的煩惱產生。
實現如下:
public class Solution {
public int Fibonacci(int n) {
if (n <= 1){
return n;
}
int[] fib = new int[n+1];
fib[0] = 0;
fib[1] = 1;
for(int i = 2;i < n + 1; i++){
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib[n];
}
}
運行時間與占用內存如下:
這么做比遞歸好很多,但是考慮到第i項只與第i-1和第i-2項有關,因此只需要存儲前兩項的值就能求解第i項,從而將空間復雜度由O(N)降低為O(1)。所以我們可以進一步優化。
動態規劃的進一步優化
使用兩個值存儲i-1和i-2,避免使用數組,浪費更多的空間。實現如下:
public class Solution {
public int Fibonacci(int n) {
if (n <= 1){
return n;
}
int preOne = 1; //存儲i-1
int preTwo = 0; //存儲i-2
int result = 0;
for(int i = 2;i < n + 1; i++){
result = preOne + preTwo;
preTwo = preOne;
preOne = result;
}
return result;
}
}
運行時間與占用內存如下:
接下來我們來看看劍指Offer中其他關于斐波那契數列的運用的題目:
題目一:跳臺階
一只青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先后次序不同算不同的結果)。
簡單分析一下,就可以知道還是上面斐波那契數列的變化,青蛙跳1級臺階有1種跳法,2級臺階有2種跳法,3級臺階時可以從1級臺階跳上來也可以從2級臺階跳上來,即等于1級臺階的跳法加2級臺階的跳法因此n級臺階共有n-2級臺階跳法數+n-1級臺階跳法數。
實現如下:
public class Solution {
public int JumpFloor(int target) {
if(target <= 2)
return target;
int preOne = 2;
int preTwo = 1;
int result = 0;
for(int i = 3;i < target+1 ;i++){
result = preOne + preTwo;
preTwo = preOne;
preOne = result;
}
return result;
}
}
題目二:變態跳臺階
一只青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法
上一題的升級版,跳n級臺階時可以允許跳1~n任意階級的臺階。
先來分析一下
- 跳n級臺階,那么第一步有n種跳法:跳1級、跳2級、到跳n級
- 跳1級,剩下n-1級,則剩下跳法是F(n-1);
- 跳2級,剩下n-2級,則剩下跳法是F(n-2);
- 所以F(n)=F(n-1)+F(n-2)+...+F(1)+1,最后的+1是因為直接跳n級臺階只有一種方法;
- 因為F(n-1)=F(n-2)+F(n-3)+...+F(1)+1;
- 以此類推,得F(n)=2*F(n-1)。
分析后變得比上面一提還要簡單。實現如下:
public class Solution {
public int JumpFloorII(int target) {
if(target <= 2){
return target;
}
int preNum = 2;
int result = 0;
for(int i = 3;i < target + 1;i++){
result = 2 * preNum;
preNum = result;
}
return result;
}
}
題目三:矩陣覆蓋
我們可以用2 * 1的小矩形橫著或者豎著去覆蓋更大的矩形。請問用n個2 * 1的小矩形無重疊地覆蓋一個2 * n的大矩形,總共有多少種方法?
再來分析一下
- 首先從n=1開始,小矩陣只能豎著放,只有一種方法;
- n=2時,大矩陣為2 * 2,小矩陣既可以豎著放也可以橫著放,有兩種方法;
-
當n越來越大時,如果第一步選擇豎著放,如下圖:
第一步豎著放
那么大矩陣的規??s小成2 * (n-1);
-
如果第一步選擇豎著放,那么第二排也只能橫著放,如下圖:
第一步橫著放
那么大矩陣的規模縮小成2 * (n-2);
- 因此,題目又轉化成了與題目一一樣的斐波那契數列了。
實現如下:
public class Solution {
public int RectCover(int target) {
if(target <= 2)
return target;
int preOne = 2;
int preTwo = 1;
int result = 0;
for(int i = 3;i < target+1 ;i++){
result = preOne + preTwo;
preTwo = preOne;
preOne = result;
}
return result;
}
}
以上就是關于斐波那契數列的含義和使用方式,題目一二三都是劍指Offer中的真題,示例中關于運行時間和占用內存是根據牛客網的測試用例得來的。