在講斐波那契數列以前,咱們先回顧一下以前在第一篇文章講複雜度分析裏,談到時間複雜度的時候,講到時間複雜度有七種,分別是O(1),O(logn),O(n),O(nlogn),O(n^2),O(2^n),O(n!)。前面五種的話其實很容易寫出對應的算法來實現相應時間複雜度。好比O(1)時間複雜度在數組的下標取值,鏈表的插入和刪除都是這個時間複雜度;O(logn)時間複雜度能夠經過二分查找來實現,二分查找會在以後的文章有講;O(n)時間複雜度能夠經過數組的遍歷和鏈表的查詢能夠實現;O(nlogn)時間複雜度能夠經過第十篇文章講的快速排序和歸併排序實現;O(n^2)時間複雜度則能夠經過冒泡排序、插入排序、選擇排序來實現。可是咱們在那篇文章中好像漏了O(2^n)和O(n!)時間複雜度是經過哪一種現實狀況來實現,今天我主要講O(2^n)時間複雜度是如何實現的,至於後面的O(n!)時間複雜度則留到下一篇講排列組合的文章具體分析。算法
咱們先來看看斐波那契數列的定義是什麼,斐波那契的定義是這個數列從第3項開始,每一項都等於前兩項之和。一開始這個數列是應用與兔子繁殖的問題。好比一對成年兔子一年生一對小兔,小兔一年以後長成年。一開始只有一對小兔,求n年以後有多少隻兔子。數組
咱們先來分析一下,第0年的時候沒有生兔子,因此爲0;第一年生了一對兔子,記爲1;因爲小兔要等一年才能成年,因此第二年老兔子仍然只生一對兔子,記爲1;到第三年的時候,第二代的兔子成年並生了一對小兔,算上第一代生的一對小兔,第三年則生了兩對小兔。第四年的時候,第三代兔子成年生了一對小兔,算上今年第一代和第二代小兔生的兔子,第四年總共生了三隊小兔,下面咱們用圖來表示一下這關係:bash
這裏咱們能夠看到,從第三個開始,每一項都等於前兩個數的和。若是用做遞推公式來表示的話咱們能夠用下面這條公式來表示:函數
n=1: f(n) = f(1)優化
n=2: f(n) = f(2)ui
n>2: f(n) = f(n - 1) + f(n - 2)spa
看到這條公式是否是以爲有點似曾相識。在第八篇文章講遞歸的時候走臺階遊戲其實就是斐波那契數列。如今一聽是否是以爲只要瞭解背後的本質,也就是這條遞推公式,無論你怎麼變,其實核心都只是斐波那契數列而已。那如何用代碼實現呢?在第八篇文章的時候其實有講過他的實現方法,解法以下:code
function func(val){
if (val === 1) return 1
if (val === 2) return 2
return func(val - 1) + func(val - 2)
}複製代碼
那麼這種解法有什麼問題呢,這種解法的問題是時間複雜度很是高,是O(2^n),當你輸入40的時候,計算結果已經要花費好幾秒的時間了,爲何會是這樣呢,由於求解F(n),必須先計算F(n-1)和F(n-2),計算F(n-1)和F(n-2),又必須先計算F(n-3)和F(n-4)。。。。。。以此類推,直至必須先計算F(1)和F(0),而後逆推獲得F(n-1)和F(n-2)的結果,從而獲得F(n)要計算不少重複的值,在時間上形成了很大的浪費,算法的時間複雜度隨着N的增大呈現指數增加。下面有一張圖來簡單的表示:cdn
在這張圖咱們能夠看到,咱們計算f(6)的時候樹的層級爲4,在這棵樹當中,咱們能夠發現其實有不少計算都是重複的,這樣的重複計算耗費了大量的時間,那有什麼方法優化呢?這裏能夠利用非遞歸循環的思想來解斐波那契數列,代碼以下:blog
function func(n) {
if (n === 0) {
return 0
}
else if (n < 3) {
return 1
}
let a1 = 1, a2 = 1
for (let i = 1; i < n - 1; i++) {
[a1, a2] = [a2, a1 + a2]
}
return a2
}複製代碼
經過這種算法,咱們減小了每次的重複計算的次數,使得時間複雜度壓縮到O(n)。那麼還有更快的嗎?那確定是有的,在數學裏,求解斐波那契數列有一個通項公式,利用特徵方程來求解的,公式以下:
利用這項公式,咱們能夠獲得代碼:
function func (n) {
return Math.round((Math.pow((1+Math.sqrt(5))/2, n) - Math.pow((1 - Math.sqrt(5))/2, n)) / Math.sqrt(5))
}複製代碼
乍一看好像時間複雜度變爲O(1),其實不是的,這裏的使用了JavaScript函數內置的冪運算Math.pow方法,執行了n次冪,那n次冪的話時間複雜度是O(n)嗎,也不是。在計算機中,求冪能夠經過平方來不斷的接近n,求根則能夠經過二分來不斷的接近要求解的數,求冪和求根的方法的時間複雜度都是O(logn),因此這裏的時間複雜度是O(logn)。
其實因爲IEEE754標準的問題,咱們每次經過計算所獲得的值都要經過Math.round函數來進行一次四捨五入的運算,在計算機中,執行這個方法也是要耗費必定的時間的,其實咱們能夠經過改寫底層的Math.pow方法來使得改方法直接返回一個整數型的數值,這個時候就須要深刻到二進制了。
這個思路是這樣的,對於咱們要求的冪,傳入來的冪若是模以2有餘數的時候,咱們則乘以對應次數的x倍,若是沒有則乘以對應次數的n。代碼以下:
function pow (x, n) {
var r = 1
var v = x
while (n) {
if (n % 2 == 1) {
r *= v
n -= 1
}
v *= v
n = n / 2
}
return r
}複製代碼
就這樣上面的通項公式代碼能夠改寫成以下:
function func (n) {
return (pow((1+Math.sqrt(5))/2, n) - pow((1 - Math.sqrt(5))/2, n)) / Math.sqrt(5)
}
function pow (x, n) {
var r = 1
var v = x
while (n) {
if (n % 2 == 1) {
r *= v
n -= 1
}
v *= v
n = n / 2
}
return r
}複製代碼
上面的方法其實也用到了JavaScript自帶的Math.sqrt求根方法,上面也說到求根運算在計算機中也是時間複雜度爲O(logn),求冪運算裏嵌套一個求根運算,就是,能夠轉換爲2logn,雖說去掉常數2最後的時間複雜度也是O(logn),可是爲了更快咱們能夠經過矩陣運算,來構建斐波那契數列的矩陣形態,而後經過矩陣乘法的結合性,把斐波那契轉換成矩陣的冪運算,這一點咱們能夠把非遞歸循環的方法加以改寫,經過矩陣乘法來求解,而後再利用上面的求冪公式獲得結果。按照這個思路,咱們假設一個矩陣x,使得a1矩陣乘以x等於a2矩陣。公式以下:
經過矩陣乘法,咱們能夠求得x的值以下:
獲得這個x的值咱們能夠獲得一個代碼,以下所示:
function matrixMul (x, y) {
return [
[x[0][0] * y[0][0] + x[0][1] * y[1][0], x[0][0] * y[0][1] + x[0][1] * y[1][1]],
[x[1][0] * y[0][0] + x[1][1] * y[1][0], x[1][0] * y[0][1] + x[1][1] * y[1][1]]
]
}複製代碼
緊接着稍微的改寫一下求冪公式,得:
function pow (x, n) {
var r = [[1,0],[0,1]]
var v = x
while (n) {
if (n % 2 == 1) {
r = matrixMul(r, v)
n -= 1
}
v = matrixMul(v, v)
n = n / 2
}
return r
}複製代碼
這下子咱們就能夠上面說的矩陣乘法,求得等式右邊的值,咱們最後只要右上角的元素,因此代碼以下:
function func (n) {
if (n <= 0) {
return 0
}
else {
return matrixMul([[0,1],[0,0]], pow([[0,1],[1,1]], n - 1))[0][1]
}
}複製代碼
就這樣能夠經過矩陣乘法,進一步的將時間複雜度穩定在O(logn)。