hash 表在 go 語言中的實現
雜湊表,是根據 key 值直接進行資料訪問的資料結構。即通過一個 hash 函式,將 key 轉換成換成陣列的索引值,然後將 value 儲存在該陣列的索引位置。如下圖:
在 hash 表的結構設計中一般有 3 個關鍵問題需要解決:
- hash 衝突。即不同的 key 通過 hash 函式,會生成相同的 hash 值,即對映到相同的陣列索引中。
- 空間浪費。即如果兩個 key 值,hash 之後,生成的索引值差距較大,就會對陣列空間產生浪費。
- 擴容問題。即當現有的陣列空間被填充滿時,如何儲存更多的鍵值。
hash 衝突的解決一般採用拉鍊法(當然還有開放地址法等)。即當有兩個不同的 key,經過 hash 函式,被 hash 到同一個位置的時候,不直接儲存在該索引下,而是將該值加到連結串列中,以免覆蓋第一個具有相同 hash 的 key 值。如下圖,假設 a 和 b 的 hash 值相同。
對於第二個問題,在 go 中是通過位操作來解決的。 即將 key 轉換成 hash 值後,並不直接用 hash 作為索引,而是用 hash 和一個掩碼值(一般是和底層陣列個數或其相關的一個值)進行取模或位操作後得到對應陣列的索引值。
第三個問題是涉及到空間增長和資料遷移,即重新分配更大的空間,將原有的 key 重新 hash 到新的空間的索引位置上。
本文主要介紹在 go 中實現 hash 表的底層資料結構以及 hash 衝突的解決。。
map 資料結構
首先,整體來看下 go 中整體 map 的資料結構。如下圖:
如上圖,我們得知在 map 的資料結構中主要包含 hmap,bmap 兩個結構體。
hmap 結構體
在 go 中,我們初始化或建立一個 map 時,實際上是建立了一個 hmap 結構體。hmap 的完整資料結構如下:
type hmap struct {
count int //map中的元素個數
flags uint8
B uint8 //log_2的對數,即buckets的個數為2^B次方
noverflow uint16
hash0 uint32 //hash種子
buckets unsafe.Pointer //bucket陣列指標
oldbuckets unsafe.Pointer //
nevacuate uintptr
extra *mapextra //溢位的buckets
}
例如我們用如下語句建立一個 map 變數:
//建立一個容量為10的map
m := make(map[string]int, 16)
建立的 hmap 結構如下:
在 hmap 結構中,有以下幾個重要的欄位:
- B :log_2 的對數,即 bucket 的個數=2^B 次方
- hash0:隨機數的種子。Go 執行時環境避免 hash 衝突使用。
- buckets:底層的 buckets 陣列。
- extra:溢位的 buckets 陣列。
資料結構中的 B 欄位及其作用
根據上面的資料結構,我們可知,bucket 的個數=2^B 次方。那我們為什麼需要這個 B 值呢? *因為我們需要用 B 值和 hash 值經過一定的運算後,得到 bucket 陣列範圍內的索引 index *。
我們在用 map 的時候,key 是一個字串,經過 hash 函式後轉換成陣列的索引。但這個雜湊後的數字不一定在 buckets 的陣列範圍內。比如,我們的 buckets 陣列個數是 8 個,一個 key 經過雜湊函式轉換成的雜湊值是 1378,那這個雜湊值就不能直接作為 buckets 陣列的索引來儲存該 value。而且,我們也不能直接擴充套件該陣列的空間來儲存該值,這樣將會浪費太多的空間。
所以,我們需要 B 和 hash 進行按位與操作,以此將 hash 值落到 bucket 陣列的範圍之內。在 go 中程式碼實現如下:
index := hash & (1 << B - 1)
buckets
buckets 是 map 結構中的底層儲存結構,buckets 本質上一個 bmap 型別的陣列,即每個 bucket 指向一個 bmap 結構體。陣列大小由 B 欄位值決定。
type bmap struct {
tophash [8]uint8 //容量為8的陣列,儲存hash值的高位
keys [8]keyType //該欄位是在執行時階段自動加入的,在原始碼中並沒有。
values [8]valueType //該欄位是在執行時階段自動加入的,在原始碼中並沒有。
}
在 bmap 結構體中,tophash 是一個固定容量的陣列。值得注意的是 keys 和 values 的儲存結構。key-value 的儲存並不是我們常見的 key-value/key-value 儲存,而是以 key0/key1/key2/.../key7/value0/value1/.../value7 格式儲存的。即先存 8 個 key,再存 8 個 value。這主要是考慮在記憶體對齊方面,可以避免浪費記憶體。
賦值操作
map 的賦值操作如下:
m['apple'] = 'mac'
賦值操作的目標,是將 apple 經過 hash 之後,找到對應的 bucket,並儲存到 bmap 結構體中。
計算步驟如下: 1、根據 key 生成 hash 值 2、根據 hash 和 B 計算 bucket 的索引 3、根據 bucket 索引和 bucketsize 計算得到 buckets 陣列的起始地址 4、計算 hash 的高位值 top 5、在 tophash 陣列中依次該 tophash 值是否存在,如果存在,並且 key 和儲存的 key 相等,則更新該 key/value。如果不存在,則從 tophash 陣列查詢第一個空位置,儲存該 tophash 和 key/value
場景一:tophash 陣列未滿,且 k 值不存在時,則查詢空閒空間,直接賦值
場景二:tophash 陣列未滿,且 k 值已經存在,則更新該 k
場景三:tophash 陣列已滿,且 k 值不在當前的 bucket 的 tophash 中,則從 bmap 結構體中的 buoverflowt 中查詢,並做更新或新增
hash 衝突
由上面的賦值操作可知,當遇到 hash 衝突的時候,go 的解決方法是先在 tophash 的陣列中查詢空閒的位置,如果有空閒的位置則存入。如果沒有空閒位置,則在 bmap 的 overflow 指標指向的 bucket 中的 tophash 中繼續查,依次迴圈,直到找不等於該 key 的空閒位置,依次迴圈,直到從 tophash 中找到一個空閒位置為止。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Go語言中時間輪的實現Go
- 如何在Go語言中實現表單驗證?整一個validator吧!Go
- 在Go語言中,怎樣使用Json的方法?GoJSON
- Go 語言中的方法Go
- Go 語言中的 切片 --sliceGo
- Go 語言中的 collect 使用Go
- Go 語言中的外掛Go
- 在C語言中實現泛型程式設計C語言泛型程式設計
- 在 Go 語言中,我為什麼使用介面Go
- 在 go 語言中利用反射精簡程式碼Go反射
- 為什麼在Go語言中要慎用interface{}Go
- Go語言中的併發模式Go模式
- GO 語言中的物件導向Go物件
- 【Go】四捨五入在go語言中為何如此困難Go
- Go 語言中使用 ETCDGo
- 認識 Go 語言中的陣列Go陣列
- Go語言中的變數作用域Go變數
- Go語言中的單元測試Go
- go語言使用切片實現線性表Go
- static在C語言中的作用C語言
- 聊聊Go語言中的陣列與切片Go陣列
- 9.Go語言中的流程控制Go
- 詳細解讀go語言中的chnanelGoNaN
- Go 語言中的格式化輸出Go
- Go 語言中的兩種 slice 表示式Go
- Go語言中defer的一些坑Go
- Go 語言中 strings 包常用方法Go
- Go語言中JSON標籤的用法與技巧GoJSON
- go 語言中的 rune,獲取字元長度Go字元
- Go語言中切片slice的宣告與使用Go
- Go 語言中常見的幾種反模式Go模式
- go 語言中預設的型別識別Go型別
- Go語言中的加解密利器:go-crypto庫全解析Go解密
- go語言中遍歷陣列的方法有哪些Go陣列
- 理解 Go 語言中的組合字面量(Composite Literal)Go
- 聊聊 Go 語言中的物件導向程式設計Go物件程式設計
- go語言中的封裝,繼承和多型Go封裝繼承多型
- Go語言中的TCP/IP網路程式設計GoTCP程式設計