哈希表数据结构_算法与数据结构-哈希表
前面我們已經講到了數組和鏈表,數組能通過下標 O(1) 訪問,但是刪除一個中間元素卻要移動其他元素,時間 O(n)。 循環雙端鏈表倒是可以在知道一個節點的情況下迅速刪除它,但是吧查找又成了 O(n)。
難道就沒有一種方法可以快速定位和刪除元素嗎?似乎想要快速找到一個元素除了知道下標之外別無他法,于是乎聰明的計算機科學家又想到了一種方法。 能不能給每個元素一種『邏輯下標』,然后直接找到它呢,哈希表就是這種實現。它通過一個哈希函數來計算一個元素應該放在數組哪個位置,當然對于一個 特定的元素,哈希函數每次計算的下標必須要一樣才可以,而且范圍不能超過給定的數組長度。
我們還是以書中的例子說明,假如我們有一個數組 T,包含 M=13 個元素,我們可以定義一個簡單的哈希函數 h
h(key) = key % M
這里取模運算使得 h(key) 的結果不會超過數組的長度下標。我們來分別插入以下元素:
765, 431, 96, 142, 579, 226, 903, 388
先來計算下它們應用哈希函數后的結果:
?
下邊我畫個圖演示整個插入過程(純手工繪制,原諒我字寫得不太優雅):
?
哈希沖突 (collision)
這里到插入 226 這個元素的時候,不幸地發現 h(226) = h(96) = 5,不同的 key 通過我們的哈希函數計算后得到的下標一樣, 這種情況成為哈希沖突。怎么辦呢?聰明的計算機科學家又想到了辦法,其實一種直觀的想法是如果沖突了我能不能讓數組中 對應的槽變成一個鏈式結構呢?這就是其中一種解決方法,叫做 鏈接法(chaining)。如果我們用鏈接法來處理沖突,后邊的插入是這樣的:
?
這樣就用鏈表解決了沖突問題,但是如果哈希函數選不好的話,可能就導致沖突太多一個鏈變得太長,這樣查找就不再是 O(1) 的了。 還有一種叫做開放尋址法(open addressing),它的基本思想是當一個槽被占用的時候,采用一種方式來尋找下一個可用的槽。 (這里槽指的是數組中的一個位置),根據找下一個槽的方式不同,分為:
- 線性探查(linear probing): 當一個槽被占用,找下一個可用的槽。h(k,i)=(h′(k)+i)%m,i=0,1,...,m?1h(k,i)=(h′(k)+i)%m,i=0,1,...,m?1
- 二次探查(quadratic probing): 當一個槽被占用,以二次方作為偏移量。 h(k,i)=(h′(k)+c1+c2i2)%m,i=0,1,...,m?1h(k,i)=(h′(k)+c1+c2i2)%m,i=0,1,...,m?1
- 雙重散列(double hashing): 重新計算 hash 結果。 h(k,i)=(h1(k)+ih2(k))%mh(k,i)=(h1(k)+ih2(k))%m
我們選一個簡單的二次探查函數 h(k,i)=(home+i2)%mh(k,i)=(home+i2)%m,它的意思是如果 遇到了沖突,我們就在原始計算的位置不斷加上 i 的平方。我寫了段代碼來模擬整個計算下標的過程:
?
這段代碼輸出的結果如下:
?
遇到沖突之后會重新計算,每個待插入元素最終的下標就是:
?
Cpython 如何解決哈希沖突
如不同 cpython 版本實現的探查方式是不同的,后邊我們自己實現 HashTable ADT 的時候會模仿這個探查方式來解決沖突。
?
哈希函數
到這里你應該明白哈希表插入的工作原理了,不過有個重要的問題之前沒提到,就是 hash 函數怎么選? 當然是散列得到的沖突越來越小就好啦,也就是說每個 key 都能盡量被等可能地散列到 m 個槽中的任何一個,并且與其他 key 被散列到哪個槽位無關。 如果你感興趣,可以閱讀后邊提到的一些參考資料。視頻里我們使用二次探查函數,它相比線性探查得到的結果沖突會更少。
裝載因子(load factor)
如果繼續往我們的哈希表里塞東西會發生什么?空間不夠用。這里我們定義一個負載因子的概念(load factor),其實很簡單,就是已經使用的槽數比哈希表大小。 比如我們上邊的例子插入了 8 個元素,哈希表總大小是 13, 它的 load factor 就是 8/13≈0.628/13≈0.62。當我們繼續往哈希表插入數據的時候,很快就不夠用了。 通常當負載因子開始超過 0.8 的時候,就要新開辟空間并且重新進行散列了。
重哈希(Rehashing)
當負載因子超過 0.8 的時候,需要進行 rehashing 操作了。步驟就是重新開辟一塊新的空間,開多大呢?感興趣的話可以看下 cpython 的 dictobject.c 文件然后搜索 GROWTH_RATE 這個關鍵字,你會發現不同版本的 cpython 使用了不同的策略。python3.3 的策略是擴大為已經使用的槽數目的兩倍。開辟了新空間以后,會把原來哈希表里 不為空槽的數據重新插入到新的哈希表里,插入方式和之前一樣。這就是 rehashing 操作。
HashTable ADT
實踐是檢驗真理的唯一標準,這里我們來實現一個簡化版的哈希表 ADT,主要是為了讓你更好地了解它的工作原理,有了它,后邊實現起 dict 和 set 來就小菜一碟了。 這里我們使用到了定長數組,還記得我們在數組和列表章節里實現的 Array 吧,這里要用上了。
解決沖突我們使用二次探查法,模擬 cpython 二次探查函數的實現。我們來實現三個哈希表最常用的基本操作,這實際上也是使用字典的時候最常用的操作。
- add(key, value)
- get(key, default)
- remove(key)
?
具體的實現和代碼編寫在視頻里講解。這個代碼可不太好實現,稍不留神就會有錯,我們還是通過編寫單元測試驗證代碼的正確性。公眾號:學習py最風sao的方式歡迎大家繼續關注!
總結
以上是生活随笔為你收集整理的哈希表数据结构_算法与数据结构-哈希表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手机上网流量统计_数据统计 | 上半年手
- 下一篇: Rabbit发送消息,消费者消费异常