開篇語
Map是一種常用的kv資料結構,程式設計中經常使用,且作為一種最基礎的資料結構,很多程式語言本身提供的api都會有實現,Go也不例外,今天我們將從一下三個方面為大家分析Go中的Map。
- 什麼是Map?
- Go中如何使用Map?
- 以及Go的Map實現機制是什麼樣?
希望透過這幾個方面的講解,讓大家真正理解Go的Map使用和實現。
什麼是Map
key,value儲存
最通俗的話說Map是一種透過key來獲取value的一個資料結構,其底層儲存方式為陣列,在儲存時key不能重複,當key重複時,value進行覆蓋,我們透過key進行hash運算(可以簡單理解為把key轉化為一個整形數字)然後對陣列的長度取餘,得到key儲存在陣列的哪個下標位置,最後將key和value組裝為一個結構體,放入陣列下標處,看下圖:
-
length = len(array) = 4 hashkey1 = hash(xiaoming) = 4 index1 = hashkey1% length= 0 hashkey2 = hash(xiaoli) = 6 index2 = hashkey2% length= 2
hash衝突
如上圖所示,陣列一個下標處只能儲存一個元素,也就是說一個陣列下標只能儲存一對key,value, hashkey(xiaoming)=4佔用了下標0的位置,假設我們遇到另一個key,hashkey(xiaowang)也是4,這就是hash衝突(不同的key經過hash之後得到的值一樣),那麼key=xiaowang的怎麼儲存?
hash衝突的常見解決方法
- 開放定址法
也就是說當我們儲存一個key,value時,發現hashkey(key)的下標已經被別key佔用,那我們在這個陣列中空間中重新找一個沒被佔用的儲存這個衝突的key,那麼沒被佔用的有很多,找哪個好呢?常見的有線性探測法,線性補償探測法,隨機探測法,這裡我們主要說一下線性探測法線性探測,字面意思就是按照順序來,從衝突的下標處開始往後探測,到達陣列末尾時,從陣列開始處探測,直到找到一個空位置儲存這個key,當陣列都找不到的情況下回擴容(事實上當陣列容量快滿的時候就會擴容了);查詢某一個key的時候,找到key對應的下標,比較key是否相等,如果相等直接取出來,否則按照順尋探測直到碰到一個空位置,說明key不存在。如下圖:
首先儲存key=xiaoming在下標0處,當儲存key=xiaowang時,hash衝突了,按照線性探測,儲存在下標1處,(紅色的線是衝突或者下標已經被佔用了)
再者key=xiaozhao儲存在下標4處,當儲存key=xiaoliu是,hash衝突了,按照線性探測,從頭開始,儲存在下標2處 (黃色的是衝突或者下標已經被佔用了)- 拉鍊法
何為拉鍊,簡單理解為連結串列,當key的hash衝突時,我們在衝突位置的元素上形成一個連結串列,透過指標互連線,當查詢時,發現key衝突,順著連結串列一直往下找,直到連結串列的尾節點,找不到則返回空,如下圖:
- 開放定址(線性探測)和拉鍊的優缺點
- 由上面可以看出拉鍊法比線性探測處理簡單
- 線性探測查詢時會被拉鍊法會更消耗時間
- 線性探測會更加容易導致擴容,而拉鍊不會
- 拉鍊儲存了指標,所以空間上會比線性探測佔用多一點
- 拉鍊是動態申請儲存空間的,所以更適合鏈長不確定的
Go中Map的使用
-
直接用程式碼描述,直觀,簡單,易理解
-
//直接建立初始化一個mao var mapInit = map[string]string {"xiaoli":"湖南", "xiaoliu":"天津"} //宣告一個map型別變數, //map的key的型別是string,value的型別是string var mapTemp map[string]string //使用make函式初始化這個變數,並指定大小(也可以不指定) mapTemp = make(map[string]string,10) //儲存key ,value mapTemp["xiaoming"] = "北京" mapTemp["xiaowang"]= "河北" //根據key獲取value, //如果key存在,則ok是true,否則是flase //v1用來接收key對應的value,當ok是false時,v1是nil v1,ok := mapTemp["xiaoming"] fmt.Println(ok,v1) //當key=xiaowang存在時列印value if v2,ok := mapTemp["xiaowang"]; ok{ fmt.Println(v2) } //遍歷map,列印key和value for k,v := range mapTemp{ fmt.Println(k,v) } //刪除map中的key delete(mapTemp,"xiaoming") //獲取map的大小 l := len(mapTemp) fmt.Println(l)
-
看了上面的map建立,初始化,增刪改查等操作,我們發現go的api其實挺簡單易學的
Go中Map的實現原理
知其然,更得知其所以然,會使用map了,多問問為什麼,go底層map到底怎麼儲存呢?接下來我們一探究竟。
map的原始碼位於 src/runtime/map.go中 筆者go的版本是1.12
在go中,map同樣也是陣列儲存的的,每個陣列下標處儲存的是一個bucket,這個bucket的型別見下面程式碼,每個bucket中可以儲存8個kv鍵值對,當每個bucket儲存的kv對到達8個之後,會透過overflow指標指向一個新的bucket,從而形成一個連結串列,看bmap的結構,我想大家應該很納悶,沒看見kv的結構和overflow指標啊,事實上,這兩個結構體並沒有顯示定義,是透過指標運算進行訪問的。
-
//bucket結構體定義 b就是bucket type bmap{ // tophash generally contains the top byte of the hash value for each key in this bucket. If tophash[0] < minTopHash, tophash[0] is a bucket evacuation state instead. //翻譯:top hash通常包含該bucket中每個鍵的hash值的高八位。如果tophash[0]小於mintophash,則tophash[0]為桶疏散狀態 //bucketCnt 的初始值是8 tophash [bucketCnt]uint8 // Followed by bucketCnt keys and then bucketCnt values. NOTE: packing all the keys together and then all the values together makes the code a bit more complicated than alternating key/value/key/value/... but it allows us to eliminate padding which would be needed for, e.g., map[int64]int8. Followed by an overflow pointer. //翻譯:接下來是bucketcnt鍵,然後是bucketcnt值。注意:將所有鍵打包在一起,然後將所有值打包在一起,使得//程式碼比交替鍵/值/鍵/值/更復雜。但它允許//我們消除可能需要的填充,例如map[int64]int8./後面跟一個溢位指標 }
看上面程式碼以及註釋,我們能得到bucket中儲存的kv是這樣的,tophash用來快速查詢key值是否在該bucket中,而不同每次都透過真值進行比較;還有kv的存放,為什麼不是k1v1,k2v2..... 而是k1k2...v1v2...,我們看上面的註釋說的 map[int64]int8,key是int64(8個位元組),value是int8(一個位元組),kv的長度不同,如果按照kv格式存放,則考慮記憶體對齊v也會佔用int64,而按照後者儲存時,8個v剛好佔用一個int64,從這個就可以看出go的map設計之巧妙。
最後我們分析一下go的整體記憶體結構,閱讀一下map儲存的原始碼,如下圖所示,當往map中儲存一個kv對時,透過k獲取hash值,hash值的低八位和bucket陣列長度取餘,定位到在陣列中的那個下標,hash值的高八位儲存在bucket中的tophash中,用來快速判斷key是否存在,key和value的具體值則透過指標運算儲存,當一個bucket滿時,透過overfolw指標連結到下一個bucket。
歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來
本作品採用《CC 協議》,轉載必須註明作者和本文連結