從斐波那契算法再看時間複雜度

  • 開題引入斐波那契
    • 代碼演示: 遞歸、循環
  • 遞歸 vs 循環
    • 時間複雜復高,指數型O(2^n); 推導過程
    • 佔用線程堆棧, 可能導致棧滿異常
  • 壓測直觀演示

打入門軟件開發,斐波那契數列便是繞不過去的簡單編程算法。

一個老生常談的思路是遞歸,另外是循環,今天藉此機會回顧並演示時間複雜度在編程中的重要性。

斐波那契 遞歸算法 1,1,2,3,5,8,13,21,34,55

遞歸算法的應用場景是:

  • 將大規模問題,拆解成幾個小規模的同樣問題
  • 拆解具備終止場景
func Fibonacci(n int) (r int) {
	if n == 1 || n == 2 {
		r = 1
		return
	} else {
		return Fibonacci(n-1) + Fibonacci(n-2)
	}
}

爲什麼能想到循環, 斐波那契數組也有循環的含義:
第n個數字是循環指針i從第1個數字移動到第n-2個數字時, 第n-2個數字pre+第n-1個數字next的和。

func Fibonacci2(n int) (r int) {
   if n==1 || n==2  {
     return 1
   }
   pre,next int :=1,1
   for i:=0; i<=n-1-2; i++ {
       tmp:= pre+next
       pre = next
       next = tmp
   }
}

單元測試如下:

func TestFibonacci(t *testing.T) {
	t.Logf("Fibonacci(1) = %d, Fibonacci2(1)= %d ", Fibonacci(1), Fibonacci2(1))
	t.Logf("Fibonacci(3) = %d, Fibonacci2(3)= %d ", Fibonacci(3), Fibonacci2(3))
	t.Logf("Fibonacci(8) = %d, Fibonacci2(8)= %d ", Fibonacci(8), Fibonacci2(8))
}

go test ./ -v
=== RUN   TestFibonacci
    m_test.go:8: Fibonacci(1) = 1, Fibonacci2(1)= 1 
    m_test.go:9: Fibonacci(3) = 2, Fibonacci2(3)= 2 
    m_test.go:10: Fibonacci(8) = 21, Fibonacci2(8)= 21 
--- PASS: TestFibonacci (0.00s)
PASS
ok      example.github.com/test 3.359s

遞歸的問題在於:

(1) 函數調用存在壓棧過程,會在線程棧(一般2M)上留下棧幀,斐波那契人遞歸算法:是函數自己調用自己,在終止條件後棧幀開始收斂,但是在此之前有可能已經撐爆線程棧。

棧幀中維持着函數調用所需要的各種信息,包括函數的入參、函數的局部變量、函數執行完成後下一步要執行的指令地址、寄存器信息等。

(2) 斐波那契遞歸調用存在重複計算,時間複雜度是O(2^n),隨着n的增加,需要計算的次數陡然增大(業內稱爲指數型變化)。

 f(n) = f(n-1)+f(n-2)                 // 1次計算
      = f(n-2)+f(n-3)+f(n-3)+f(n-4)   // 3次計算
      = f(n-3)+f(n-4)+f(n-4)+f(n-5)+f(n-4)+f(n-5)+f(n-5)+f(n-6)                               // 7次計算
      =......
      = f(1)+......                   //  2^n-1次計算
      
 故爲斐波那契遞歸時間複雜度爲 O(2^n)     

而我們的循環算法不存在以上問題, 第n個數字需要n -2次計算, 時間複雜度是O(n)

有些童鞋可能沒意識到指數型的威力,舉個例子, 斐波那契遞歸算法,第20個數字需要2^20次運算; 循環算法只要18次運算。

使用基準測試壓測:

func BenchmarkF1(b *testing.B) {
	for i := 1; i < b.N; i++ {
		Fibonacci(20) //  時間複雜度  O(2^n)
	}
}

func BenchmarkF2(b *testing.B) {
	for i := 1; i < b.N; i++ {
		Fibonacci2(20) // 時間複雜度 O(n)
	}
}


go test  -bench=.  -benchmem  ./
goos: darwin
goarch: amd64
pkg: example.github.com/test
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkF1-8              55039             20740 ns/op               0 B/op          0 allocs/op
BenchmarkF2-8           196663548                6.080 ns/op           0 B/op          0 allocs/op
PASS
ok      example.github.com/test 3.744s

單次執行效率相形見絀,甚至斐波那契遞歸n=50+ 不一定能計算出來。


ok, that'all 本次快速重溫遞歸算法相比循環的兩點劣勢,這裏面重要的常見時間複雜度變化曲線, 需要程序員必知必會。
https://adrianmejia.com/most-popular-algorithms-time-complexity-every-programmer-should-know-free-online-tutorial-course/

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