- 開題引入斐波那契
- 代碼演示: 遞歸、循環
- 遞歸 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/