Hash表從瞭解到深刻(淺談)

·    Hasn表,將一個數據進行Value化,再進行一個映射關係到Key直接進行訪問的一個數據結構,這樣能夠經過直接的計算進行數據的訪問和插入。關於Hash表的基本概念這裏就不一一敘述,能夠經過百度瞭解Hash的一些基本概念。今天這裏主要講2個點,Hash衝突與Hash構建函數算法。算法

 

1,一個基本的Hash表是什麼?編程

  不少人若是隻是簡單瞭解過Hash表的結構,可能提到這個數據結構的第一個印象是Hash是由一個數組和一個鏈表(鏈表這裏可能有不少種形態)組成的數據結構。大錯特錯,我的感受這徹底是一種錯誤的植入方式,首先Hash自己的描述僅僅只是一個數組,咱們根據每次要插入或者訪問的數據,經過Value->Key的一個映射,經過Key直接訪問數組下標進行數據插入或訪問,在插入數據時,若是當前下標已經放入了某個數值,則在這個下標的基礎上向後移動,直到某一個塊尚未被插入數據,就將當前須要插入的數據放入當前塊。而之因此不少不太瞭解Hash表的人將其說成一個數組加一個鏈表,是由於這種數據結構是採用Hash的拉鍊法進行一個解決Hash衝突的方法而已。數組

 

2,什麼是Hash衝突?服務器

  Hash衝突簡單來講就是兩個數據,雖然Value不相同(某些存儲也會須要存入相同的Value),但經過Hash映射函數所獲得的Key是相同的,這兩個數據都要存入Hash表中時,就會產生衝突。而咱們要解決衝突,就須要拉鍊法(經常使用且簡單)或一些其餘的方法,進行衝突解決,若是Key相同,拉鍊法是在當前哈希表(數組)存放一個鏈表的頭,若兩個數據的Key均相同,則將數據分別存放在鏈表中。數據結構

 

3,常看法決衝突的方法?負載均衡

  常看法決除了上述的拉鍊法外,還有一些比較經常使用的方法像「開放定址法」,「再哈希法」,「創建公共溢出區」。這三種方法的實現百度就有不少,在此處不詳細介紹,我的比較喜歡拉鍊法,也是比較容易實現的一種,就是拉下去會增長空間的開銷。函數

 

4,裝載因子是什麼?大數據

  裝載因子 = 添加的數據總數 / 哈希表的大小,稱爲飽和程度。通常來講飽和程度越小,對於咱們Hash搜索的效率也是越高的,但一樣,這就表明這空間的代價也是很高的,每每不少空間是浪費掉沒有使用的。好比裝載因子爲0.2,就表示哈希表只有20%的空間被使用,其他浪費。因此爲了平衡這個點,大部分採用0.75做爲裝載因子,若是超過,就動態進行哈希的重構,增長哈希表的大小。spa

 

5,哈希表的大小怎麼設定最好?設計

  在開始構造哈希表的時候,因爲哈希表的大小是固定的,雖然能夠動態調整,但重構起來是比較浪費時間的,因此通常在開始須要根據實際項目狀況定好一個大小值。至於大小取什麼比較合適,網上有一種說法是取質數,我的理解這種取質數的方法僅僅適用於 Key = Value % Buff 。先討論一下這種狀況,算法導論中有提到,針對於取餘這種狀況,能夠選擇遠離2的冪次方的質數,其次是普通質數,最後纔是恰好爲2^冪次數。但在我看來,這徹底是一種錯誤指向。舉個例子:爲何最差的宣發是2^冪次,假設對4/8/16等等取餘爲1,那麼對2取餘一樣是1,這樣的一個理解特別容易令人進入一個誤區,以爲只要是2^冪次數大小,就會形成不均勻。看似有點道理,但用機率論的想法去思考這個問題,感受徹底是因果沒有任何關係,牛脣不對馬嘴。

  由於隨着哈希表大小的增長,取餘的範圍也會增長,對於任意常熟對其取餘,所獲得的機率應該均是相等纔對,好比1-100的天然數,不管取餘多少,都會是相似於桶的一種塞法,每一個桶先塞一個,輪詢完後回來從第一個繼續塞,因此我的認爲不管對於取餘這種狀況,哈希大小選定只要是某個區間內是合適的,就應該是能夠的。不在意是否爲2^冪次仍是質數(有特殊規律的數據羣,須要依靠其規律設計大小)。這一段僅僅時候從常熟機率相同的角度分析,看來是很容易分析到的一種現象,但一樣,後面經過一些公式,咱們又能證實出另外一種可能性。

  一個數對一個2^冪次取餘其實就是 Value % Key = Value & (Key - 1),因此也就是說這個餘數只是Value二進制中的後幾位而已,若後幾位相同的狀況下,前面的位數不管取何值,結果都是相同的,這樣產生衝突的可能性就大了一些。

  綜上所述,若是是已經知道了是一個天然數均勻分佈的數據羣,那不管取質數或是2^冪次數都是沒什麼影響的,但若是是某些特殊羣,好比後幾位或前幾位常常會出現相同值,那咱們使用遠離2^次冪的質數大小就能有效的減小衝突的產生。

(這段講的真的很亂,就是轉折來轉折去,最終結論仍是建議能夠遵照遠離2^次冪的質數大小)。

 

6,Hash構建函數-常見的?

  第一個問題提到的插入方法就是最基本的構建方法,直接尋址法,百度中也有一些基本的Hash構造方法,好比:

⑴ 數字分析法:先對可能插入的數據進行簡單的分析,好比是數字,那就看有幾位,那幾位基本是固定的,那咱們就用剩下位數的數字做爲Key來存放數據。舉個例子,好比存放出生日期,那年月日中3個參數中,年份這個參數前兩個參數的數據基本是一致的,那就能夠直接拋棄,只以十位和個位的數來做爲Key進行判斷。

⑵ 除留餘數法:此方法爲 Key = Value % Buff 進行計算的,整體來講這個簡單的構建方法也是我比較喜歡的,若是不是作一些大項目,僅僅用於小的數據存儲的話,徹底可使用這種構建方法進行Hash構建。

 

7,好一些的Hash構建算法?

  在這個大數據橫飛的時代,若是僅僅使用一些簡單的Hash構造那確定是不能知足咱們的要求,因此前人們就研究出了許多適用於各類環境下的Hash構造算法,都是很高效,而且如今的項目也在使用的一些算法。

⑴ BKDRHash構造:

  BKDR算法,主要應用場景是對字符串數據進行Hash構造的方法,咱們先拋開算法一說,從最初開始想辦法。首先要將字符串Value化,最經常使用的方法就是字符串中的每一個字符進行相加,獲得的值就是Value,若是直接使用Value做爲Key,那麼Value(ad) = a + d = 97 + 100 = 197 = 98 + 99 = Value(bc)。很明顯,當即就產生了碰撞,這固然不是咱們想要的一種構造方法了,那繼續進行下去,根據高數中的離散性分析,在無係數線性方程中加入係數管控是能夠必定程度上加大離散性。因此咱們將Value的計算方法進一步修改成:

Value(ad) = k1 * a + k2 * d這樣得出來的Value就沒那麼容易產生衝突了,固然也不能徹底避免,至於如何最大化的下降衝突的機率,咱們就須要研究一下 k1,k2 係數的取值爲多少時,能夠最大化的下降衝突機率。

  係數取值:首先因爲字符串沒法去評估它的長度,有多是一個很長的字符串,咱們若是人爲的去設定 k1,k2 的定值,一旦字符串長了,咱們就須要預先設定不少值,這樣無疑很麻煩。因此咱們能夠給出一個固定的係數值k,方程式中的 k1,k1 徹底能夠替換爲 k^0,k^1.....。替換後的方程就是 Value(ad) = k^1 * a + k^0 * d,其中 k ≠ 0,1。設字符串爲數組p,元素個數爲n,就有公式: 

(Ps:這個博客竟然不能插入數學公式,我服)

  繼續取值的問題,到這步咱們只須要對 k 進行一個合理的取值就OK。網上看過有人對這個值進行討論是分爲2的冪的偶數、非2的冪的偶數、奇數三個部分討論。但做爲一名學數學的人,總以爲喜歡分爲偶數和奇數兩種進行討論,而實驗以後發現也徹底是能夠推導下去的(網上之因此分了2的冪,應該是爲了逐步說明,這裏就不逐步了)。

  假設Value的存儲類型爲Hash默認的 unsigned int 類型。若是取 k 爲偶數,k 老是能分解爲 k = 2 * k1(偶數或奇數)。如果字符串的長度大於等於33時,第一位的係數就會變成 k = 2 * k1 ^ 32 = 2 ^ 32 * k1 ^ 32,而熟悉編程的人都知道,若是某個數不斷增長,超過類型範圍會被直接拋棄,因此不管哪一個字符串在乘以 2 ^ 32 以後第一位就會被拋棄,就形成了只要後面32位字符不變化,前面的字符不管怎麼改變,都會發生衝突。因此係數去偶數是行不通的。

  那再來討論 k 取奇數,k = 2 * k1 + 1。則 k ^ n = 2 * k1 ^ n + 1。係數後面永遠有一個1,因此就能夠保證每一個字符的改變都會影響到Value值,這樣就保證了咱們的衝突可能降到最小。上述的方法就是BkdrHash構造算法。

⑵ 一致性Hash構造算法:

  一致性Hash的構造常常被用來進行服務器分配,好比一個服務器集羣組,能夠經過這種Hash分配的方法將新連入的請求分配給某個服務器進行處理。具體的一個原理是:在移除或添加一個Cache時,它可以儘量小的改變已存在Key映射關係,知足單調性。

  對於一致性Hash,咱們能夠把它想象成一個環形的控件,首爲0,尾爲2^32 - 1。首先當請求到來時,先將請求映射到Hash空間中,這時候再把服務器的節點映射到Hash空間中,到這裏咱們先看幾個簡單的圖,來實例一下大體如何映射。

                         

      4個請求經過Hash函數映射到哈希表中                  3個服務器也進行映射

   觀察第二張圖的箭頭,好比 Key1 會被分到 Cache A 處理,Key 4 會被分到 Cache B 處理, Key 2 和 Key 3 會被分給 Cache C處理。若是其中一個服務器掛掉,如Cache B掛掉,就會將 Key 4 的請求交給 Cache C 

                  

  固然若是說某種狀況下致使服務器不均勻,就須要進行負載均衡的原理進行從新搭建虛擬映射,對服務進行統計劃分後,將服務器的已存在的映射進行再次虛擬映射,使得每一個服務器所接受到的請求基本是平均的

                       

本次的分享大體就是這些,基本都是對Hash有過基本的瞭解後,經過本次文章,能夠大體瞭解到一些實用的Hash構建方法以及怎麼保證衝突最小化的辦法。

相關文章
相關標籤/搜索