hash 表在 go 語言中的實現

yudotyang發表於2021-04-16

雜湊表,是根據 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 的 bucket 指標的 tophash 中繼續查,依次迴圈,直到找不等於該 key 的空閒位置,依次迴圈,直到從 tophash 中找到一個空閒位置為止。

更多原創文章乾貨分享,請關注公眾號
  • hash 表在 go 語言中的實現
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章