資料結構 - 雜湊表,再探

IT规划师發表於2024-10-29

書接上回,我們繼續來聊雜湊表。

從上面的章節不難發現,無論雜湊函式怎麼構建總會發生碰撞,最多隻能降低碰撞機率,但是並不能杜絕碰撞,因此如何解決碰撞問題成了雜湊重中之重。

01、碰撞解決方案

下面我們一起來看看幾種碰撞處理策略。

1、鏈式法

鏈式法的核心思想可以理解為把碰撞的key直接放到一個大的集合中;

我們先來回顧一下何為碰撞,碰撞就是兩個不同的key,透過雜湊函式計算出相同的雜湊值。

為什麼發生碰撞就會出問題呢?因為雜湊表本質上是陣列,陣列一個下標只能對應一個值,如果key1和key2發生碰撞,意味著key2對應的value2會把key1對應的value1覆蓋掉,這就會導致了資料丟失。

想解決這個問題我直觀想法是如果發生碰撞時value2不是覆蓋value1,而是兩者同時儲存下來,是不是這個問題就解決了呢?

答案是肯定的,但是裡面還有很多細節要處理。如一個陣列元素裡怎麼再存放多個元素呢?對於發生碰撞的key如何查詢呢?

首先一個陣列元素裡怎麼存放多個元素?其實方法有很多,比如陣列元素中直接存放一個陣列,比如陣列中存放一個連結串列表頭指向一個連結串列等等。

在雜湊表中,每個陣列元素所在的位置,我們稱為“ 桶” 或者 “ 槽”,而每個桶(槽)又對應一條連結串列,發生碰撞的所有key都放到相同的桶對應的連結串列中,這種碰撞處理策略稱為鏈式法。

當存取元素時分兩步,第一步先找到key所在的桶,第二步再在桶所對應的連結串列中存取元素。

這個方案是非常簡單實用的,而且因為連結串列節點是動態申請這也大大提升了空間利用率。

2、開放定址法

開放定址法的核心思想可以理解為當發生衝突時,如果當前雜湊位置已被使用就按某種方式繼續探測下一個可以用的雜湊位置。

而某種方式繼續探測具體是指當發生衝突時,則在當前雜湊值基礎上再加上指定的步長並判斷當前雜湊位置是否可用,如果不可用則重複此步驟。

因此開放定址法可以概況為:hash_value=(hash(key)+step(i))%m,1≤i≤m-1

如上圖當如果陣列中淺綠代表空地址,淺橙色表示已經被佔用,當傳入key4時,經過雜湊函式計算出所在陣列索引應該為6,而此時索引6已經被佔用,因此繼續往後探測,索引8已經被佔用,繼續往後探測,索引0已經被佔用,繼續往後探測,索引1空閒,存入value4。

而根據不同的步長生成規則,又可以分成以下幾種探測方法:線性探測法、平方探測法、雙重雜湊探測法、隨機探測法等等

(1)線性探測法

線性探測法是透過在雜湊表中線性探測下一個可用位置來解決碰撞。

當發生碰撞時,則按順序探測下一個元素位置,直至查詢到可用的雜湊地址或者排查完全表。因此當探查到表尾地址時,並不是結束探測而是回到表首繼續探測,直至探測到起始探測位置。

其公式可表示為:hash_value=(hash(key)+step)%m,1≤step≤m-1

優點:1、實現簡單;2、效率較高,在負載因子較低時特別更為突出。

缺點:可能導致聚集現象,即多個元素在碰撞後會集中在一起,從而導致相鄰位置被佔用,增加探測時間。

(2)平方探測法

平方探測法是透過使用平方增量來解決雜湊衝突,從而有效地分散元素。

如果說線性探測法每次探測的步長是step,那麼平方探測法的探測步長則是step的平方。

其公式可表示為:hash_value=(hash(key)+step^2)%m,1≤step≤m-1

與線性探測法相比,平方探測法主要優勢在於增量平方數使得探測位置分佈更廣,位置也就更分散,從而減少元素的聚焦現象,從而提高查詢效率。因此平方探測法在處理碰撞時通常優於線性探測法,當然選擇哪種方法應根據具體應用場景和效能要求來決定。

優點:減少聚集現象,元素分佈均勻。

缺點:1、仍可能存在二次聚集現象(即某些位置可能因為探測規則而頻繁被訪問)。2、可能無法探測到所有位置,在負載因子較高時尤為突出。

(3)雙重雜湊探測法

雙重雜湊探測法是使用兩個雜湊函式來決定探測序列,從而更有效地分散衝突。也就是當發生碰撞時,會使用第二個雜湊函式來生成增量從而確定下一個要查詢的位置。

其公式可表示為:hash_value=(hash(key)+ i * hash2(key))%m,1≤i≤m-1

優點:1、減少聚集現象,效率高,探測路徑較為隨機。2、可以探測到所有位置,理論上避免了聚集問題。

缺點:實現相對複雜,且需要選擇合適的第二個雜湊函式。

(4)隨機探測法

隨機探測法是透過隨機選擇下一個探測位置來解決衝突。也就是說當發生碰撞時,確定查詢的下一個位置的增量式一個隨機數。

其公式可表示為:hash_value=(hash(key)+ r)%m,1≤r≤m-1

優點:1、大幅減少聚集現象,位置探測隨機性強。2可以有效分散元素,降低碰撞影響。

缺點:1、隨機性可能導致效能不穩定,尤其是在負載因子較高時。2、難以預測和除錯。

總結

3、再雜湊法

再雜湊法的核心思想可用理解為對現有雜湊表進行擴容並重新計算現有元素位置。

我們先來回憶一個概念負載因子,負載因子式用來衡量雜湊表填充程度,透過雜湊表已儲存的元素個數除以雜湊表的大小計算可得。

負載因子過大會導致一些列問題,首先會加大碰撞機率,碰撞機率增大又會導致處理碰撞成本增加,進而導致效能下降。同時也可能導致記憶體碎片化使用率不高,等等問題。

而負載因子就是觸發再雜湊的時機,當負載因子超過某個閾值(一般是0.75)時,進行再雜湊。

而再雜湊的難點在於如何處理新老資料,是當觸發再雜湊時一次性遷移所有老資料到新雜湊表中,還是分批次遷移老資料直至所有老資料遷移完成為止?

一次性遷移可能實現上相對簡單,但是也引發了很多問題,如果遷移過程中又有其他操作怎麼辦?如果資料量很大遷移時間過長怎麼辦?

而分批遷移實現上會更復雜一些,要處理好新老資料共存時的查詢、插入、刪除等操作。

整體思路如下:

(1)當負載因子到達設定的閾值,則重新申請新的記憶體空間,不進行老資料遷移;

(2)當有新資料要插入時,將新資料插入至新的記憶體空間中,並取出一個老資料插入到新的記憶體空間中;

(3)重複步驟(2)直至所有老資料都遷移至新的記憶體空間為止;

(4)當在新老資料共存進行查詢時,可用先在老的空間進行查詢,如果不存在再到新空間中查詢。

:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

相關文章