散列 哈希
散列函數的設計
過于復雜的散列函數,勢必會消耗很多計算時間,散列函數生成的值要盡可能隨機并且均勻分布。
如今的一些散列函數:直接尋址法、平方取中法、折疊法、隨機數法等。
直接尋址法:直接定址法是以數據元素關鍵字k本身或它的線性函數作為它的哈希地址。鍵字的元素很少是連續的。用該方法產生的哈希表會造成空間大量的浪費。
平方取中法:先取關鍵字的平方,然后根據可使用空間的大小,選取平方數是中間幾位為哈希地址。
哈希函數 H(key)=“key平方的中間幾位”因為這種方法的原理是通過取平方擴大差別,平方值的中間幾位和這個數的每一位都相關,則對不同的關鍵字得到的哈希函數值不易產生沖突,由此產生的哈希地址也較為均勻。
折疊法:將關鍵字分割成位數相同的幾部分(最后一部分的位數可以不同),然后取這幾部分的疊加和(舍去進位)。
隨機數法:除留余數法:取關鍵字被某個不大于散列表 表長m的數p除后所得的余數為散列地址。即 H(key) = key MOD p
。不僅可以對關鍵字直接取模,也可在折疊、平方取中等運算之后取模。對p的選擇很重要,一般取,素數,質數(減少散列沖突),或m,若p選的不好,容易產生同義詞。
哈希算法:將任意長度的二進制值串映射為固定長度的二進制值串。
要求:
從哈希值不能反向推導出原始數據(所以哈希算法也叫單向哈希算法);
對輸入數據非常敏感,哪怕原始數據只修改了一個 Bit,最后得到的哈希值也大不相同;
散列沖突的概率要很小,對于不同的原始數據,哈希值相同的概率非常小;
哈希算法的執行效率要盡量高效,針對較長的文本,也能快速地計算出哈希值。
應用一:安全加密
MD5 消息摘要算法 SHA(Secure Hash Algorithm,安全散列算法)DES(Data Encryption Standard,數據加密標準)、AES(Advanced Encryption Standard,高級加密標準)。
應用二:唯一標識,圖片搜索
應用三:數據校驗,下載文件拼裝校驗
應用四:散列函數,
應用五:負載均衡,輪詢,隨機,加權輪詢。通過哈希算法計算數據,分配訪問的機器,這樣統一用戶訪問的機器都是同一臺。
應用六:數據分片,對數據分片,計算哈希值,看看該數據應該由那臺機器處理
應用七:分布式存儲
一致性哈希算法(當要增加節點時)https://www.sohu.com/a/158141377_479559 //漫畫理解
假設我們有 k 個機器,數據的哈希值的范圍是 [0, MAX]。我們將整個范圍劃分成 m 個小區間(m 遠大于 k),每個機器負責 m/k 個小區間。當有新機器加入的時候,我們就將某幾個小區間的數據,從原來的機器中搬移到新的機器中。這樣,既不用全部重新哈希、搬移數據,也保持了各個機器上數據數量的均衡。
散列表
用的就是數組支持按照下標隨機訪問的時候,時間復雜度是 O(1) 的特性。我們通過散列函數把元素的鍵值映射為下標,然后將數據存儲在數組中對應下標的位置。當我們按照鍵值查詢元素時,我們用同樣的散列函數,將鍵值轉化數組下標,從對應的數組下標的位置取數據。
散列函數,顧名思義,它是一個函數。我們可以把它定義成hash(key),其中 key 表示元素的鍵值,hash(key)的值表示經過散列函數計算得到的散列值。
散列函數設計的基本要求:
散列函數計算得到的散列值是一個非負整數;
如果 key1 = key2,那 hash(key1) ==hash(key2);
如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
散列沖突
1.開放尋址法(適用于數據量比較小,裝載因子小的場景)
優點:散列表中的數據都存儲在數組中,可以有效地利用 CPU 緩存加快查詢速度。而且,這種方法實現的散列表,序列化起來比較簡單。
缺點:用開放尋址法解決沖突的散列表,刪除數據的時候比較麻煩,需要特殊標記已經刪除掉的數據。而且,在開放尋址法中,所有的數據都存儲在一個數組中,比起鏈表法來說,沖突的代價更高。裝載因子的上限不能太大。
如果出現了散列沖突,我們就重新探測一個空閑位置,將其插入。
線性探測(最壞的時間復雜度位O(n))
當我們往散列表中插入數據時,如果某個數據經過散列函數散列之后,存儲位置已經被占用了,我們就從當前位置開始,依次往后查找,看是否有空閑位置,直到找到為止。
查找元素的過程
找元素的鍵值對應的散列值,然后比較數組中下標為散列值的元素和要查找的元素。如果相等,則說明就是我們要找的元素;否則就順序往后依次查找。如果遍歷到數組中的空閑位置,還沒有找到,就說明要查找的元素并沒有在散列表中。
刪除元素的過程
找到之后直接刪除會有問題(查到的時候發現有位置空,會認定原來存在的數據不存在)
我們可以將刪除的元素,特殊標記為 deleted。當線性探測查找的時候,遇到標記為 deleted 的空間,并不是停下來,而是繼續往下探測。
二次探測
二次探測探測的步長就變成了原來的“二次方”
,也就是說,它探測的下標序列就是 hash(key)+0,hash(key)+1的平方,hash(key)+2的平方…
雙重散列
不僅要使用一個散列函數。我們使用一組散列函數
hash1(key),hash2(key),hash3(key)……我們先用第一個散列函數,如果計算得到的存儲位置已經被占用,再用第二個散列函數,依次類推,直到找到空閑的存儲位置。
盡可能的保證散列表中有一定比例的空閑槽位。
裝載因子
裝載因子越大,說明空閑位置越少,沖突越多,散列表的性能會下降。
當裝載因子過大時,我們也可以進行動態擴容,重新申請一個更大的散列表,將數據搬移到這個新散列表中。
數據的搬移需要通過散列函數重新計數每個數據的存儲位置。
如:
插入的時間復雜度:如果不需要動態擴容,所以時O(1),如果需要動態擴容,所以時間復雜度時O(n),攤還分析之后,時間復雜度時O(1)。插入數據也可以這樣做:當超過閾值的時候,只申請空間,然后每次插入新的數據時,把新數據放入新的空間,同時在原散列表中拿出一個數據放入新的散列表。這樣每次插入操作都可以很快速。查找的話先從新散列表查找,然后再從舊得散列表查找。
如果對空間有要求,在刪除較多數據之后,可以啟動縮容。
2.鏈表法(更常用)
優點:鏈表法對內存的利用率比開放尋址法要高。,只要散列函數的值隨機均勻,即便裝載因子變成 10,也就是鏈表的長度變長了而已,雖然查找效率有所下降,但是比起順序查找還是快很多。
缺點:鏈表因為要存儲指針,所以對于比較小的對象的存儲,是比較消耗內存的,而且,因為鏈表中的結點是零散分布在內存中的,不是連續的,所以對 CPU 緩存是不友好的,這方面對于執行效率也有一定的影響。如果存放的時大對象,指針的內存消耗可以忽略。
在散列表中,每個“桶(bucket)”或者“槽(slot)”會對應一條鏈表,所有散列值相同的元素我們都放到相同槽位對應的鏈表中。
插入
只需要通過散列函數計算出對應的散列槽位,將其插入到對應鏈表中即可,所以插入的時間復雜度是 O(1)。
查找和刪除
同樣通過散列函數計算出對應的槽,然后遍歷鏈表查找或者刪除。
這兩個操作的時間復雜度跟鏈表的長度 k 成正比,也就是 O(k)。對于散列比較均勻的散列函數來說,
理論上講,k=n/m,其中 n 表示散列中數據的個數,m 表示散列表中“槽”的個數。
散列表碰撞攻擊
通過一定設計的數據,將數據存入散列表,使所有數據都散列到同一個槽中,此時散列表就會退化成鏈表。
但是如果對鏈表法稍微改造,可以實現一個更加高效的散列表。那就是,我們將鏈表法中的鏈表改造為其他高效的動態數據結構,比如跳表、紅黑樹。這樣,即便出現散列沖突,極端情況下,所有的數據都散列到同一個桶內,那最終退化成的散列表的查找時間也只不過是 O(logn)。這樣也就有效避免了前面講到的散列碰撞攻擊。
HashMap的設計(數據+鏈表+紅黑樹)
1.初始大小
HashMap 默認的初始大小是 16,如果事先知道大概的數據量有多大,可以通過修改默認初始大小,減少動態擴容的次數,這樣會大大提高 HashMap 的性能。
2. 裝載因子和動態擴容
最大裝載因子默認是 0.75,當 HashMap 中元素個數超過 0.75*capacity(capacity 表示散列表的容量)的時候,就會啟動擴容,每次擴容都會擴容為原來的兩倍大小。
3. 散列沖突解決方法
HashMap 底層采用鏈表法來解決沖突。一旦出現拉鏈過長(默認超過 8)鏈表就轉換為紅黑樹。我們可以利用紅黑樹快速增刪改查的特點,提高 HashMap 的性能。當紅黑樹結點個數少于 8 個的時候,又會將紅黑樹轉化為鏈表。
int hash(Object key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}
先補充下老師使用的這段代碼的一些問題:在JDK HashMap源碼中,是分兩步走的:
hash值的計算,源碼如下:
static final int hash(Object key) {
int hash;
return key == null ? 0 : (hash = key.hashCode()) ^ hash >>> 16;
}
在插入或查找的時候,計算Key被映射到桶的位置:
int index = hash(key) & (capacity - 1)
JDK HashMap中hash函數的設計,確實很巧妙:
首先hashcode本身是個32位整型值,在系統中,這個值對于不同的對象必須保證唯一(JAVA規范),這也是大家常說的,重寫equals必須重寫hashcode的重要原因。
獲取對象的hashcode以后,先進行移位運算,然后再和自己做異或運算,即:hashcode ^ (hashcode >>> 16),這一步甚是巧妙,是將高16位移到低16位,這樣計算出來的整型值將“具有”高位和低位的性質加粗樣式
最后,用hash表當前的容量減去一,再和剛剛計算出來的整型值做位與運算。進行位與運算,很好理解,是為了計算出數組中的位置。但這里有個問題:
為什么要用容量減去一?
因為 A % B = A & (B - 1),所以,(h ^ (h >>> 16)) & (capitity -1) = (h ^ (h >>> 16)) % capitity,可以看出這里本質上是使用了除留余數法
綜上,可以看出,hashcode的隨機性,加上移位異或算法,得到一個非常隨機的hash值,再通過「除留余數法」,得到index,整體的設計過程與老師所說的“散列函數”設計原則非常吻合!
**一致性哈希:**原文 :https://www.cnblogs.com/lpfuture/p/5796398.html
一致性Hash性質
考慮到分布式系統每個節點都有可能失效,并且新的節點很可能動態的增加進來,如何保證當系統的節點數目發生變化時仍然能夠對外提供良好的服務,這是值得考慮的,尤其實在設計分布式緩存系統時,如果某臺服務器失效,對于整個系統來說如果不采用合適的算法來保證一致性,那么緩存于系統中的所有數據都可能會失效(即由于系統節點數目變少,客戶端在請求某一對象時需要重新計算其hash值(通常與系統中的節點數目有關),由于hash值已經改變,所以很可能找不到保存該對象的服務器節點),因此一致性hash就顯得至關重要,良好的分布式cahce系統中的一致性hash算法應該滿足以下幾個方面:
平衡性(Balance)
平衡性是指哈希的結果能夠盡可能分布到所有的緩沖中去,這樣可以使得所有的緩沖空間都得到利用。很多哈希算法都能夠滿足這一條件。
單調性(Monotonicity)
**單調性是指如果已經有一些內容通過哈希分派到了相應的緩沖中,又有新的緩沖區加入到系統中,那么哈希的結果應能夠保證原有已分配的內容可以被映射到新的緩沖區中去,而不會被映射到舊的緩沖集合中的其他緩沖區。**簡單的哈希算法往往不能滿足單調性的要求,哈希結果的變化意味著當緩沖空間發生變化時,所有的映射關系需要在系統內全部更新。而在P2P系統內,緩沖的變化等價于Peer加入或退出系統,這一情況在P2P系統中會頻繁發生,因此會帶來極大計算和傳輸負荷。單調性就是要求哈希算法能夠應對這種情況。
分散性(Spread)
在分布式環境中,終端有可能看不到所有的緩沖,而是只能看到其中的一部分。當終端希望通過哈希過程將內容映射到緩沖上時,由于不同終端所見的緩沖范圍有可能不同,從而導致哈希的結果不一致,最終的結果是相同的內容被不同的終端映射到不同的緩沖區中。這種情況顯然是應該避免的,因為它導致相同內容被存儲到不同緩沖中去,降低了系統存儲的效率。分散性的定義就是上述情況發生的嚴重程度。好的哈希算法應能夠盡量避免不一致的情況發生,也就是盡量降低分散性。
負載(Load)
負載問題實際上是從另一個角度看待分散性問題。既然不同的終端可能將相同的內容映射到不同的緩沖區中,那么對于一個特定的緩沖區而言,也可能被不同的用戶映射為不同的內容。與分散性一樣,這種情況也是應當避免的,因此好的哈希算法應能夠盡量降低緩沖的負荷。
平滑性(Smoothness)
平滑性是指緩存服務器的數目平滑改變和緩存對象的平滑改變是一致的。
設計的一個工業級的散列函數
要求:
支持快速的查詢、插入、刪除操作;
內存占用合理,不能浪費過多的內存空間;
性能穩定,極端情況下,散列表的性能也不會退化到無法接受的情況;
設計思路:
設計一個合適的散列函數;
定義裝載因子閾值,并且設計動態擴容策略;
選擇合適的散列沖突解決方法。
總結
- 上一篇: 保姆级安装,在FusionCompute
- 下一篇: 团队目标WBS及具体任务分工