目錄
1 Hash函數
2 Hash沖突
3 一致性hash
hash表
1 hash函數
地址index=H(key)即根據key計算出應該存儲地址的位置,而哈希表是基於哈希函數建立的一種查找表。
1.1 hash函數的性質
(1)輸入域是無窮的,但是輸出域是有限的
(2)不是隨機產生的輸出,相同的輸入一定對應相同的輸出
(3)不同的輸入可能會導致相同的輸出(hash碰撞)
(4)輸出的值在整個輸出域幾乎是均勻分布的(離散性)
1.2 hash函數設計的考慮因素
- 計算散列地址所需要的時間(即hash函數本身不要太復雜)
- 關鍵字的長度
- 表長
- 關鍵字分布是否均勻,是否有規律可循
- 設計的hash函數在滿足以上條件的情況下盡量減少沖突
2 hash沖突
不同key值產生相同的地址即:H(key1)=H(key2)
2.1 解決hash沖突的方法
- 鏈地址法
- 再哈希法
- 探測法
- 建立公共溢出區
2.1.1 鏈地址法(拉鏈法、鏈接法)
HashMap,HashSet其實都是采用的拉鏈法來解決哈希沖突的,就是在每個位桶實現的時候,我們采用鏈表(jdk1.8之后采用鏈表+紅黑樹)的數據結構來去存取發生哈希沖突的輸入域的關鍵字(也就是被哈希函數映射到同一個位桶上的關鍵字)。首先來看使用拉鏈法解決哈希沖突的幾個操作:
①插入操作:在發生哈希沖突的時候,我們輸入域的關鍵字去映射到位桶(實際上是實現位桶的這個數據結構,鏈表或者紅黑樹)中去的時候,我們先檢查帶插入元素x是否出現在表中,很明顯,這個查找所用的次數不會超過裝載因子(n/m:n為輸入域的關鍵字個數,m為位桶的數目),它是個常數,所以插入操作的最壞時間復雜度為O(1)的。
②查詢操作:和①一樣,在發生哈希沖突的時候,我們去檢索的時間復雜度不會超過裝載因子,也就是檢索數據的時間復雜度也是O(1)的
③刪除操作:如果在拉鏈法中我們想要使用鏈表這種數據結構來實現位桶,那么這個鏈表一定是雙向鏈表,因為在刪除一個元素x的時候,需要更改x的前驅元素的next指針的屬性,把x從鏈表中刪除。這個操作的時間復雜度也是O(1)的。
存儲數據結構如下圖所示:
每個字符是隨機均勻分布在長度為10的數組上的,因為字符多,那么在相同位置字符使用鏈表連接的(JDK1.8之前,從1.8之后用紅黑樹連接)
Java中有hashMap和hashSet這兩個其實從結構上來說是一樣的,可以認為value只不過是key的伴隨數據而已,也就是封裝的對象不同,但是基本的存儲結構還是一樣的。
2.1.2 再hash法
這種方式是同時構造多個哈希函數,當產生沖突時,計算另一個哈希函數的值。這種方法不易產生聚集,但增加了計算時間。
例題:
1 如果采用哈希表組織100萬條記錄,以支持字段A快速查找,那么以下描述中,正確的是()。
A.理論上可以在常數時間內找到特定記錄 B.所有記錄必須存在內存中
C.拉鏈式哈希法的最壞查找時間復雜度是O(n) D.哈希函數的選擇與字段A無關
A:對於哈希表而言,散列沖突的問題需要解決,尤其是當數據量大的時候,散列沖突的現象將更加明顯,因此,不能在常數的時間找到特定記錄。B:哈希表中的數據既可以在內存中,也可以被映射到外存中(例如文件)。
C:在最壞的情況下,每個記錄都有散列沖突,在這種情況下,査找的效率與線性查找的效率是一樣的,時間復雜度為O(n)。D:哈希函數的選擇跟字段A有直接的關系,根據A的數據類型的不同,需要選擇不同的哈希函數。哈希函數的好壞對查找性能有着直接的影響。
2.1.3 探測法
一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入
公式為:fi(key) = (f(key)+di) MOD m (di=1,2,3,……,m-1)
※ 用開放定址法解決沖突的做法是:當沖突發生時,使用某種探測技術在散列表中形成一個探測序列。沿此序列逐個單元地查找,直到找到給定的關鍵字,或者碰到一個開放的地址(即該地址單元為空)為止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探測到開放的地址則表明表中無待查的關鍵字,即查找失敗。
比如說,我們的關鍵字集合為{12,67,56,16,25,37,22,29,15,47,48,34},表長為12。 我們用散列函數f(key) = key mod 12
當計算前S個數{12,67,56,16,25}時,都是沒有沖突的散列地址,直接存入:
計算key = 37時,發現f(37) = 1,此時就與25所在的位置沖突。
於是我們應用上面的公式f(37) = (f(37)+1) mod 12 = 2。於是將37存入下標為2的位置:
可能出現的問題及解決:
當裝不下時:需要進行擴容
刪除數據:當需要刪除數據時,有可能會造成查找不到數據比如刪除了25,那么需要查37時直接就查不到了。解決方法:對刪除的位置進行標記
3 一致性hash
Distributed Hash Table(DHT) 是一種哈希分布方式,其目的是為了克服傳統哈希分布在服務器節點數量變化時大量數據遷移的問題。
基本原理
將哈希空間 [0, 2n-1] 看成一個哈希環,每個服務器節點都配置到哈希環上。每個數據對象通過哈希取模得到哈希值之后,存放到哈希環中順時針方向第一個大於等於該哈希值的節點上。
最初hash用來解決服務器負載均衡問題的場景:
上圖中保證了服務其存儲的信息是基本均勻分布的,但是這樣有一個問題,當我需要加增加一個服務器或者減少一個服務器時那么就需要重新對所有的信息取模,這樣做成本太大了,因此上面的方法只適合服務器台數固定的場景使用。
為了解決上述問題提出了環形結構,按照順時針方向把數據存儲到相應的服務器中。考慮當新來一個服務器m4時,只需要把原本在m3中存儲的m2~m4這段數據存儲在m4中,這樣只是修改了m3服務器上的部分數據,所花費的代價很少。
在底層實現時,是利用數組來實現環形結構,將服務器得到的hash值進行排序,得到一個有序數組,那么新來m4時,只需要二分查找比其大的最近的服務器hash值即可。
上面的方法解決了服務器數量變化的問題的,但是出現了另外兩個問題:
(1)當服務器數目過少時,因為hash函數本身的性質很有可能造成某幾個服務器得到hash值距離太近,這樣就會導致負載不均衡問題。
(2)就算第一步問題解決了,當新增加一個服務器是,還會造成負載不均衡問題。
為了解決上面的新的分布不均的問題,提出了虛擬節點技術:三個服務器各自分配1萬個虛擬節點,將虛擬節點計算出的hash值對應在環上,這樣數目足夠大就可以均勻分布了。
當新增一個服務器時,同樣設置1萬個虛擬節點,還是按照上面的順時針遷移數據的方法,這時m1、m2、m3只需要各自移動1/12的數據即可,這樣的性能還是可以接受的。
虛擬節點具體實現:
一個服務器上的對應着多個ip地址,選擇其中的一萬個,分別計算出hash值,將這些hash值對應在環上,即為虛擬節點