哈希表是最經常使用的數據結構之一,對於其用法,你們都很是熟悉,這裏詳細探討一下其原理。哈希表的底層其實是基於數組來存儲的,當插入鍵值對時,並非直接插入該數組中,而是經過對鍵進行Hash運算獲得Hash值,而後和數組容量取模,獲得在數組中的位置後再插入。取值時,先對指定的鍵求Hash值,再和容量取模獲得底層數組中對應的位置,若是指定的鍵值與存貯的鍵相匹配,則返回該鍵值對,若是不匹配,則表示哈希表中沒有對應的鍵值對。這樣作的好處是在查找、插入、刪除等操做能夠作到\(O(1)\),最壞的狀況是\(O(n)\),固然這種是最極端的狀況,極少遇到。
無論哪門語言,實現一個HashMap的過程都可分爲三大步驟:程序員
Hash函數很是重要,一個好的Hash函數不只性能優越,並且還會讓存儲於底層數組中的值分配的更加均勻,減小衝突發生。之因此是減小衝突,是由於取Hash的過程,其實是將輸入鍵(定義域)映射到一個很是小的空間中,因此衝突是沒法避免的,能作的只是減小Hash碰撞發生的機率。具體實現時,哈希函數算法可能不一樣,在Rust及不少語言的實現中,默認選擇SipHash哈希算法。算法
默認狀況下,Rust的HashMap使用SipHash哈希算法,其旨在防止哈希表碰撞攻擊,同時在各類工做負載上提供合理的性能。雖然 SipHash 在許多狀況下表現出競爭優點,但其中一個比其它哈希算法要慢的狀況是使用短鍵,例如整數。這就是爲何 Rust 程序員常常觀察到 HashMap 表現不佳的緣由。在這些狀況下,常常推薦 FNV 哈希,但請注意,它不具有與 SipHash 相同的防碰撞性。後端
影響Hash碰撞(衝突)發生的除了Hash函數自己意外,底層數組容量也是一個重要緣由。很明顯,極端狀況下若是數組容量爲1,哪必然發生碰撞,若是數組容量無限大,哪碰撞的機率很是之低。因此,哈希碰撞還取決於負載因子。負載因子是存儲的鍵值對數目與數組容量的比值,好比數組容量100,當前存貯了90個鍵值對,負載因子爲0.9。負載因子決定了哈希表何時擴容,若是負載因子的值太大,說明存儲的鍵值對接近容量,增長碰撞的風險,若是值過小,則浪費空間。數組
因此,既然衝突沒法避免,就必需要有解決Hash衝突的機制方法。微信
主要有四類處理衝突的方法:數據結構
主要思想是基於數組和鏈表的組合來解決衝突,桶(Bucket)中不直接存儲鍵值對,每一個Bucket都連接一個鏈表,當發生衝突時,將衝突的鍵值對插入鏈表中。外部拉鍊法的有點在於方法簡單,非同義詞之間也不會產生彙集現象(相比於開放定址法),而且其空間結構是動態申請的,因此比較適合沒法肯定表長的狀況:缺點是鏈表指針須要額外的空間,遇到碰撞拒絕服務時會退化爲單鏈表。分佈式
同義詞:兩個元素經過Hash函數獲得了相同的索引地址,這兩個元素就叫作同義詞。函數
下面是外部拉鍊法的兩種實現方法,主要區別在於桶(Bucket)中是否存儲數據。
性能
主要思想是發生衝突時,直接去尋找下一個空的地址,只要底層的表足夠大,就總能找到空的地址。這個尋找下一個地址的行爲,叫作探測。 \({hash_{i}=(hash(key)+d_{i})\,{\bmod {\,}}m}, i=1,2...k\,(k\leq m-1)\)`,其中\(hash(key)\)爲哈希函數,\(m\)爲哈希表長,\(d_{i}\)爲增量序列,\(i\)爲已發生衝突的次數。根據增量序列取法的不一樣有多種探測方法:學習
下圖爲線性探測:
主要思想是創建一個獨立的公共區,把衝突的鍵值對都放在其中。不經常使用,這裏再也不細述。
主要思想是有衝突時,換另一個Hash函數來算Hash值。不經常使用,這裏再也不細述。
主要是:
其中最重要的是插入、查找、刪除這三個操做。
參考文檔Hash table
關注微信公衆號,推送經常使用數據結構、TCP/IP、分佈式、後端開發等技術分享,共同窗習進步!
---恢復內容結束---