數據結構與算法的重溫之旅(番外篇1)——談談斐波那契數列

在講斐波那契數列以前,咱們先回顧一下以前在第一篇文章講複雜度分析裏,談到時間複雜度的時候,講到時間複雜度有七種,分別是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)。那麼還有更快的嗎?那確定是有的,在數學裏,求解斐波那契數列有一個通項公式,利用特徵方程來求解的,公式以下:

f_{n}=\frac{1}{\sqrt{5}[(\frac{1+\sqrt{5}}{2})^{n}-(\frac{1-\sqrt{5}}{2})^{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),求冪運算裏嵌套一個求根運算,就是logn^{2},能夠轉換爲2logn,雖說去掉常數2最後的時間複雜度也是O(logn),可是爲了更快咱們能夠經過矩陣運算,來構建斐波那契數列的矩陣形態,而後經過矩陣乘法的結合性,把斐波那契轉換成矩陣的冪運算,這一點咱們能夠把非遞歸循環的方法加以改寫,經過矩陣乘法來求解,而後再利用上面的求冪公式獲得結果。按照這個思路,咱們假設一個矩陣x,使得a1矩陣乘以x等於a2矩陣。公式以下:

\begin{bmatrix} a &b \\ 0 &0 \end{bmatrix}*x=\begin{bmatrix} b &a+b \\ 0 & 0 \end{bmatrix}

經過矩陣乘法,咱們能夠求得x的值以下:

\begin{bmatrix} a &b \\ 0 &0 \end{bmatrix}*\begin{bmatrix} 0&1 \\ 1& 1 \end{bmatrix}=\begin{bmatrix} b &a+b \\ 0 & 0 \end{bmatrix}

獲得這個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)。

相關文章
相關標籤/搜索