探索斐波那契數列的新航線

只有用水將心上的霧氣淘洗乾淨,榮光纔會照亮最初的夢想。

                                                                                          ——加西亞·馬爾克斯

最近沉迷於算法,總是想着實現與優化,今天想到了斐波那契數列,一起看看能不能輸出點新花樣?!

什麼是斐波那契數列大家都知道,我最開始瞭解以及實現的時候寫的是遞歸加法實現,如下代碼都不陌生:

func recursive(num int) int {
	if num < 1 {
		return 0
	}

	if num == 1 || num == 2 {
		return 1
	}
	return recursive(num-1) + recursive(num-2)
}

思路分析:傳入一個數,它是自頂向下開始算的,比如傳入40,它會先得出第40-1和第40-2,即第29和第28這兩個數, 隨着傳入到函數recursive的值逐漸減少,直到減少到1或2時才真正的返回結果。整個過程實際上是個二叉樹。

過程分析:以傳入5爲例:因爲5!=1因此進行遞歸,開始計算第4和第3這兩個數,即在代碼中是return recursive(4) + recursive(3) ,而和的左半部分recursive(4)即傳入4,因爲4!=1,因此繼續遞歸,代碼中會執行return recursive(3) + recursive(2),注意這裏執行了一次recursive(2) ;和的右半部分recursive(3),因爲3!=1,因此繼續遞歸,代碼中是return recursive(2) + recursive(1),注意這裏又執行了一次recursive(2) ,換句話說,傳入一個較小的數,其中相同的計算就可能會執行多次,如果你傳入的較大,那麼非常非常多的recursive(xx)及其分解之後的recursive(xx-1)、recursive(xx-2)等等將執行很多次。如果傳入的是40,經過推算,畫出的二叉樹如下:

圈圈的部分都會重複計算多次。傳入的數很小那麼沒什麼大問題,如果值越大,這棵樹將極其龐大,顯然重複計算的值將非常多。

改良方案:既然會產生這麼多次計算都是相同的過程,那我計算第一次的時候可以先記錄下來嘛,下次再執行的時候去查一下,如果有這個值我取過來直接相加不就好咯?!

實現如下:

func testFib1(num int) int {
	if num < 1 {
		return 0
	}

	backMap := make(map[int]int)
	return test(backMap, num)
}

func test(backMap map[int]int, num int) int {
	if num == 1 || num == 2 {
		return 1
	}

	if backMap[num] != 0 { // 已經計算過的直接獲取之前的計算結果,避免重複計算
		return backMap[num]
	}

	// 如果傳入的是40,那麼對於3-40之間每個test(x)來說只執行了一遍,test方法共執行40-2=38次,時間複雜度爲O(n)
	backMap[num] = test(backMap, num-1) + test(backMap, num-2) // 如果沒有,那麼算一下,存到map裏記錄下來
	return backMap[num]
}

這種思路依舊是自頂向下計算的,避免了大量的重複計算過程,當然我們還可以反推,自底向上實現一下,讓傳入的值作爲循環條件:

func testFib2(num int) int {
	if num < 1 {
		return 0
	}
	if num == 1 || num == 2 {
		return 1
	}

	// left 和 right,每次較大的是right,因爲right不僅要和left相加,還會和他的right相加
	left, right, res := 1, 1, 0
	// 最多循環num-3+1次,如果傳入的是40,那麼循環了38次,時間規模即循環次數,時間複雜度爲O(n)
	// 循環從第3個數開始,i=3執行結束時保證產生的res也就是第3個數=2即可,這樣邏輯立即就浮現在眼前了
	for i := 3; i <= num; i++ {
		res = left + right
		left,right = right,res
	}

	return res
}

到此,兩種改良方案就出來了,到了見證奇蹟的時刻!來比一下三種方式的時間消耗(這裏看數值較大的時候,因爲傳入值很小時都影響不大):

func main() {
	t1 := time.Now().UnixNano()
	fmt.Println(recursive(40))
	t2 := time.Now().UnixNano()
	fmt.Println("傳統計算(自頂向下計算)耗時:", (t2-t1)/1e6, "毫秒")

	t3 := time.Now().UnixNano()
	fmt.Println(testFib1(40))
	t4 := time.Now().UnixNano()
	fmt.Println("改良後(自頂向下計算)耗時:", (t4-t3)/1e6, "毫秒")

	t5 := time.Now().UnixNano()
	fmt.Println(testFib2(40))
	t6 := time.Now().UnixNano()
	fmt.Println("改良後(自底向上計算)耗時:", (t6-t5)/1e6, "毫秒")
}

控制檯打印:

102334155
傳統計算(自頂向下計算)耗時: 631 毫秒
102334155
改良後(自頂向下計算)耗時: 0 毫秒
102334155
改良後(自底向上計算)耗時: 0 毫秒

Process finished with exit code 0

美滋滋!!!

總結:當你想優化一件事,或者想把某件事做的更好的時候,先仔細捋一下之前的過程,看看有什麼弊端和影響,有弊端就找對策就完事了。

歡迎各位仁兄一起討論!

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章