因為在刷《劍指offer》的時候又又又又遇到了這個題,腦子里響起了“塔塔開,不塔塔開就無法勝利啊!”,於是我准備好好把斐波那契數列弄明白,然后此文就誕生了。
斐波那契數列簡介
斐波那契數列(Fibonacci sequence),又稱黃金分割數列,因數學家萊昂納多·斐波那契(Leonardo Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:0、1、1、2、3、5、8、13、21、34、……
在數學上,斐波那契數列以如下被以遞推的方法定義:
F(0)=0
F(1)=1
F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N)**
算法部分
一、原版遞歸
long long fibonacci(unsigned int n) {
long long f[2] = {0, 1};
if(n<2)return f[n];
return fibonacci(n-1) + fibonacci(n-2);
}
n = 5
n = 40(嘗試了一下n=100,結果渣機在兩分鍾之內沒跑出來)
#include<iostream>
using namespace std;
int main(void) {
long long out = fibonacci(40);
cout << out;
return 0;
}
消耗時間:0.9454s
當n比較小的時候,運算還是比較愉快的~
但是當n比較大的時候事請就變得嚴重了~
那么,為什么呢?
其實斐波那契數列的遞歸過程可以看作一棵完全二叉樹的建立(以n=5為例):
最后f[5] = f[1] + f[0] + f[1] + f[1] + f[0] + f[1] + f[0] + f[1] = 1 + 0 + 1 + 1 + 0 + 1 + 0 + 1 = 5
我們發現,有好多沒必要的重復計算出現。
比方說f(2)已經求出來了,但是我沒有儲存這個結果,於是計算機就得再求一遍,那么時間就會增加,這好嗎?這不好!
這個例子f(2)求了3遍 f(3)求了2遍,而求f(5)只需要分別求1遍f(4),f(3),f(2)。
這樣遞歸相當於浪費了一半的時間!
於是捏,我們就可以把已經求過了的f給存起來,給計算機減負。
二、尾遞歸(存值版遞歸)
long long fibonacci(int f0,int f1,unsigned int n) {
if(n > 0) {
return fibonacci(f0+f1,f0,n-1);
}
return f0;
}
n = 40
#include<iostream>
using namespace std;
int main(void) {
long long out = fibonacci(0,1,5);//當然f0=0,f1=1咯,帶進去就好了
cout << out;
return 0;
}
消耗的時間:0.2618s
三、雙指針緩存(存值版非遞歸)
long long fibonacci(unsigned int n) {
long long f[100000] = {0, 1};
if(n<2)return f[n];
for(size_t i=2;i<=n;i++){
f[i] = f[i-1] + f[i-2];//每一次循環記錄下新的f的值,不重復計算
}
return f[n];
}
n = 40
#include<iostream>
using namespace std;
int main(void) {
long long out = fibonacci(40);
cout << out;
return 0;
}
消耗時間:0.1321s
存下值后再進行計算是不是快了很多!!!
但是,俗話說得好,活到老,學到老。
所以有沒有更快、更強的方法呢?
別急,馬上安排上!
四、二階矩陣
這個算法的時間復雜度為log(n),但前提是你要有一點點線代的知識,什么,你說沒有?那我現場教你!
在數學中,矩陣(Matrix)是一個按照長方陣列排列的復數或實數集合,最早來自於方程組的系數及常數所構成的方陣。這一概念由19世紀英國數學家凱利首先提出。
矩陣求值:
好了教完了,以上知識均來自於百度,但是我為什么要教你呢?因為你如果不懂這些的話大概率不會自學,不自學就不會往下看,不往下看我就不能裝13,裝不了13我就會很難受,所以為了我能裝13,我必須教大家這個。
首先有這樣一個這個公式:{f(n), f(n-1), f(n-1), f(n-2)} ={1, 1, 1,0}n-1
不知道也很簡單,自己證明一下,上面的知識加上你高中數學知識足夠證明出來。
有了這個公式,要求f(n),我們只需要求矩陣{1, 1, 1,0}的(n-1)次方。
但是如果我們從0開始循環,n次方仍然需要n次運算,和存值版沒啥大的區別。
但是!他是乘方啊,乘方就可以分成兩半,這樣我們就不需要n的時間復雜度了,而是二分法所帶來的趨於log(n)的時間復雜度!!!!
別懵,我來舉個例子。
比方說你要求:26
普通人:2 * 2 = 4
4 * 2 = 8
8 * 2 = 16
16 * 2 = 32
32 * 2 = 64
二分人:22 = 2 * 2 = 4
23 = 22 * 2 = 8
26 = 23 * 23 = 8 * 8 = 64
普通人算了五次,而二分人只算了三次
了解了這些之后我們直接上代碼!
struct Matrix2By2//2x2矩陣
{
Matrix2By2(long long m00 = 0, long long m01 = 0, long long m10 = 0, long long m11 = 0):m_00(m00), m_01(m01), m_10(m10), m_11(m11){}//初始化二元矩陣的四個元素
long long m_00;
long long m_01;
long long m_10;
long long m_11;
};
Matrix2By2 MatrixMultiply(const Matrix2By2& matrix1, const Matrix2By2& matrix2){
return Matrix2By2(matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10,matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11,matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10,matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * matrix2.m_11);
}//矩陣乘法
Matrix2By2 MatrixPower(unsigned int n)
{
Matrix2By2 matrix;
if(n == 1)
{
matrix = Matrix2By2(1, 1, 1, 0);
}
else if(n % 2 == 0)
{
matrix = MatrixPower(n / 2);
matrix = MatrixMultiply(matrix, matrix);
}
else if(n % 2 == 1)
{
matrix = MatrixPower((n - 1) / 2);
matrix = MatrixMultiply(matrix, matrix);
matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0));
}
return matrix;
}//矩陣乘方
int fibonacci(unsigned int n) {
int result[2] = {0, 1};
if(n <= 2)return result[n];
Matrix2By2 PowerNMinus2 = MatrixPower(n-1);
return PowerNMinus2.m_00;//返回的就是f[n],記不得可以回去看下公式
}
n=40
int main(void) {
long long out = fibonacci(40);
cout << out;
return 0;
}
消耗時間:0.09125s
太帥了!
試試之前那哥們兒算不出來的n=100
int main(void) {
unsigned long long out = fibonacci(100);//不加unsigned就溢出啦,可想而知斐波那契數列的威力巨大
cout << out;
return 0;
}
消耗時間:0.1228s
emmmm~
算你勉強合格吧!
大家要好好吃飯,每天都要開開心心的!