好多人都在刷leetcode,今天我也註冊了一個玩玩,發現裏面好多都是算法題,好吧,畢業十來年,學的那點可憐的數學知識,全都還給學校了。好了閒話少說,言歸正傳,讓咱們看看今天在裏面我嘗試的第一道題,有點意思, 不僅是單純的算法,還有數據和是否適合的問題。html
點開題庫,看了第一題,咱們看看這道題:golang
給定一個整數數組和一個目標值,找出數組中和爲目標值的兩個數。
你能夠假設每一個輸入只對應一種答案,且一樣的元素不能被重複利用。
示例:
給定 nums = [2, 7, 11, 15], target = 9
由於 nums[0] + nums[1] = 2 + 7 = 9
因此返回 [0, 1]
用了這麼多文字描述,其實總結起來就是:數組裏那兩個數想加等於目標值,找出來這兩個數的索引。算法
題是不難,leetcode給出了兩種算法:數組
我用Go語言(golang)實現了暴力法,下面看看代碼。app
func TwoSum1(nums []int, target int) []int { n:=len(nums) for i,v:=range nums { for j:=i+1;j<n;j++ { if v+nums[j] == target { return []int{i,j} } } } return nil }
兩層循環嵌套,很黃很暴力。這個算法是若是運氣好了,循環兩遍就出來結果了,若是運氣很差,要找的元素正好在最後兩位,那麼真的是O(n^2)了。性能
Go語言裏有map類型,這個默認的Hash實現,基於這個咱們用Golang實現哈希法。測試
func TwoSum2(nums []int, target int) []int { m:=make(map[int]int,len(nums)) for i,v:=range nums { sub:=target-v if j,ok:=m[sub];ok{ return []int{j,i} }else{ m[v]=i } } return nil }
這個算法中規中矩,時間和空間複雜度都是O(n),若是運氣好,數組內重複的元素多,空間佔用還會再少一些。網站
寫好了算法,還要測試一下,要保證結果是正確的,不能搞烏龍。spa
package main import ( "flysnow.org/hello/lib" "fmt" ) func main(){ r1:=lib.TwoSum1([]int{2, 7, 11, 15},9) fmt.Println(r1) r2:=lib.TwoSum2([]int{2, 7, 11, 15},9) fmt.Println(r2) }
運行輸出:code
[0 1] [0 1]
和指望的結果同樣,說明咱們的算法沒有問題。
這兩種算法,leetcode也給了空間和時間複雜度,從咱們本身的代碼實現分析看,也是第二種哈希法要比暴力法好的多,真實的狀況真的是這樣嗎?咱們用Go語言的基準測試(Benchmark),測試一下。
關於基準測試(Benchmark)能夠參考 Go語言實戰筆記(二十二)| Go 基準測試 ,這裏再也不詳述。
func BenchmarkTwoSum1(b *testing.B) { b.ResetTimer() for i:=0;i<b.N;i++{ TwoSum1([]int{2, 7, 11, 15},9) } } func BenchmarkTwoSum2(b *testing.B) { b.ResetTimer() for i:=0;i<b.N;i++{ TwoSum2([]int{2, 7, 11, 15},9) } }
運行➜ lib go test -bench=. -benchmem -run=none
命令查看Golang Benchmark 測試的結果。
pkg: flysnow.org/hello/lib BenchmarkTwoSum1-8 50000000 26.9 ns/op 16 B/op 1 allocs/op BenchmarkTwoSum2-8 20000000 73.9 ns/op 16 B/op 1 allocs/op
我用的測試用例,直接用題中給的,咱們發如今這種測試用例的狀況下,咱們不看好的暴力法,反而性能比哈希法高出2.5倍,好像和咱們想的有點不同。
咱們看測試的數組,答案就在數組的前兩位,這對於暴力法來講,的確有優點,咱們把這兩個答案二、7調整到數組的末尾,也就是測試數組爲{11, 15, 2, 7}
,看看測試結果。
BenchmarkTwoSum1-8 50000000 29.1 ns/op 16 B/op 1 allocs/op BenchmarkTwoSum2-8 10000000 140 ns/op 16 B/op 1 allocs/op
好吧,這一調,暴力法仍是一如既往的堅挺,可是哈希法的性能降低了1倍,把哈希法給調死了。
咱們發現,數組個數少的時候,暴力法是佔有優點的,性能是最好的。下面咱們調整下數組的個數,再進行測試。
const N = 10 func BenchmarkTwoSum1(b *testing.B) { nums:=[]int{} for i:=0;i<N;i++{ nums=append(nums,rand.Int()) } nums=append( nums,7,2) b.ResetTimer() for i:=0;i<b.N;i++{ TwoSum1(nums,9) } } func BenchmarkTwoSum2(b *testing.B) { nums:=[]int{} for i:=0;i<N;i++{ nums=append(nums,rand.Int()) } nums=append( nums,7,2) b.ResetTimer() for i:=0;i<b.N;i++{ TwoSum2(nums,9) } }
仔細看上面的代碼,我採用自動隨機生成數組元素的方式,可是爲了保證答案,數組的最後兩位仍是7,2
。
先測試下數組大小爲10個的狀況。
BenchmarkTwoSum1-8 20000000 73.3 ns/op 16 B/op 1 allocs/op BenchmarkTwoSum2-8 2000000 660 ns/op 318 B/op 2 allocs/op
10個元素是,暴力法比哈希法的性能快10倍。
繼續調整數組大小爲50,直接修改常量N
就行了,測試50個元素的狀況。
BenchmarkTwoSum1-8 2000000 984 ns/op 16 B/op 1 allocs/op BenchmarkTwoSum2-8 500000 3200 ns/op 1451 B/op 6 allocs/op
隨着數組大小的增長,哈希法的優點開始凸現,50個數組元素時,相差只有4倍。
從不斷的增長數組的大小開始,在個人電腦上,當數組的大小爲300時,二者打平,性能同樣。
當數組大小爲1000時,哈希法的性能已是暴力法的4倍,反過來了。
當數組大小爲10000時,哈希法的性能已是暴力法的20倍,測試數據以下:
BenchmarkTwoSum1-8 100 21685955 ns/op 16 B/op 1 allocs/op BenchmarkTwoSum2-8 2000 641821 ns/op 322237 B/op 12 allocs/op
從基準測試的數據來看,數組越大,每次操做耗費的時間越長,可是暴力法的耗時增加太大,致使性能低下。
從數據中也能夠看出,哈希法是空間換時間的方式,內存佔用和分配都比較大。
從這測試和性能分析來看,不存在最優的算法,只存在最合適的。
若是你的數組元素比較少,那麼暴力算法是更適合你的。
若是數組元素很是多,那麼採用哈希算法就是一個比較好的選擇了。
因此,根據咱們本身系統的實際狀況,來選擇合適的算法,好比動態判斷數組的大小,採用不一樣的算法,達到最大的性能。
本文爲原創文章,轉載註明出處,「總有爛人抓取文章的時候還去掉個人原創說明」歡迎掃碼關注公衆號
flysnow_org
或者網站
http://www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註
*……&¥」以爲好的話,順手分享到朋友圈吧,感謝支持。