哈希表(hash)詳解

 哈希表結構講解:

 哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。java

 

記錄的存儲位置 = function(關鍵字)git

這裏的對應關係function稱爲散列函數,又稱爲哈希(Hash函數),採用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間稱爲散列表或哈希表(Hash table)。程序員

 

哈希表hashtable(key,value) 就是把Key經過一個固定的算法函數function既所謂的哈希函數轉換成一個整型數字,而後就將該數字對數組長度進行取餘,取餘結果就看成數組的下標,將value存儲在以該數字爲下標的數組空間裏。(或者:把任意長度的輸入(又叫作預映射, pre-image),經過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,而不可能從散列值來惟一的肯定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。) 而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換爲對應的數組下標【仍經過映射哈希函數function】,並定位到該空間獲取value,如此一來,就能夠充分利用到數組的定位性能進行數據定位。 面試

 

  Hash的應用:

一、Hash主要用於信息安全領域中加密算法,它把一些不一樣長度的信息轉化成雜亂的128位的編碼,這些編碼值叫作Hash值. 也能夠說,Hash就是找到一種數據內容和數據存放地址之間的映射關係。算法

 

二、查找:數據庫

哈希表,又稱爲散列,是一種更加快捷的查找技術。咱們以前的查找,都是這樣一種思路:集合中拿出來一個元素,看看是否與咱們要找的相等,若是不等,縮小範圍,繼續查找。而哈希表是徹底另一種思路:當我知道key值之後,我就能夠直接計算出這個元素在集合中的位置,根本不須要一次又一次的查找!編程

 

舉一個例子,假如個人數組A中,第i個元素裏面裝的key就是i,那麼數字3確定是在第3個位置,數字10確定是在第10個位置。哈希表就是利用利用這種基本的思想,創建一個從key到位置的函數,而後進行直接計算查找。數組

 

三、Hash表在海量數據處理中有着普遍應用。緩存

 

 Hash的特色:

Hash Table的查詢速度很是的快,幾乎是O(1)的時間複雜度。安全

 

hash就是找到一種數據內容和數據存放地址之間的映射關係。

 

散列法:元素特徵轉變爲數組下標的方法。

我想你們都在想一個很嚴重的問題:「若是兩個字符串在哈希表中對應的位置相同怎麼辦?」,畢竟一個數組容量是有限的,這種可能性很大。解決該問題的方法不少,我首先想到的就是用「鏈表」。我遇到的不少算法均可以轉化成鏈表來解決,只要在哈希表的每一個入口掛一個鏈表,保存全部對應的字符串就OK了。

 

散列表的查找步驟 

當存儲記錄時,經過散列函數計算出記錄的散列地址

當查找記錄時,咱們經過一樣的是散列函數計算記錄的散列地址,並按此散列地址訪問該記錄

 

關鍵字——散列函數(哈希函數)——散列地址

優勢:一對一的查找效率很高;

 

缺點:一個關鍵字可能對應多個散列地址;須要查找一個範圍時,效果很差。

 

散列衝突:不一樣的關鍵字通過散列函數的計算獲得了相同的散列地址。

 

好的散列函數=計算簡單+分佈均勻(計算獲得的散列地址分佈均勻)

 

哈希表是種數據結構,它能夠提供快速的插入操做和查找操做。 

 

 Hash優缺點:

優勢:不論哈希表中有多少數據,查找、插入、刪除(有時包括刪除)只須要接近常量的時間即O(1)的時間級。實際上,這隻須要幾條機器指令。

 

哈希表運算得很是快,在計算機程序中,若是須要在一秒種內查找上千條記錄一般使用哈希表(例如拼寫檢查器)哈希表的速度明顯比樹快,樹的操做一般須要O(N)的時間級。哈希表不只速度快,編程實現也相對容易。

 

若是不須要有序遍歷數據,而且能夠提早預測數據量的大小。那麼哈希表在速度和易用性方面是無與倫比的。

 

缺點:它是基於數組的,數組建立後難於擴展,某些哈希表被基本填滿時,性能降低得很是嚴重,因此程序員必需要清楚表中將要存儲多少數據(或者準備好按期地把數據轉移到更大的哈希表中,這是個費時的過程)。 

 

 常見的散列法:

  元素特徵轉變爲數組下標的方法就是散列法。散列法固然不止一種,下面列出三種比較經常使用的:

 

1、除法【求餘】散列法 

最直觀的一種,公式: 

 index = value % 16 

學過彙編的都知道,求模數實際上是經過一個除法運算獲得的,因此叫「除法散列法」。

通常哈希表的大小爲素數,由於素數不存在因子,因此大大減小了位置衝突的機率

 

2MAD

除餘法存在的不足

除餘法雖能必定程度保證詞條均勻分佈,但從關鍵碼空間到散列地址空間依然殘留有必定的連續性,如 相鄰關鍵碼對應散列地址也相鄰。

所以便有mad法,若常數ab選取得當,能夠很好地克服除餘法的這種連續性。除餘法也能夠看做Mad法a=1和b=0的特例,只是兩個常數並未發揮實質做用。

 

 

 

表達式

hash(key) = (a*key+b) % M 其中M仍爲素數,a>0,b>0,且a % M != 0

 

3、數字分析法(selecting digits

注:如下各方法爲保證落在合法的散列地址空間上,最後一般還需對錶長M取餘。

思路

從關鍵碼key特定進制的展開中抽取特定的若干位,構成整型地址。

表達式

例:選取key十進制展開中的奇數位

hash(123456789) = 13579

 

4、平方取中法(mid-square

思路

從關鍵碼key的平方的十進制或二進制展開中取居中的若干位,構成一個整型地址。

表達式

例:取平方並用十進制展開中的居中3位做爲散列地址

123^2 = 15129,hash(123) = 512

 

5、摺疊法(folding

思路

將關鍵碼的十進制或二進制展開分割成等寬的若干段,取其總和做爲散列地址。

表達式

例:以十進制三個數位爲分割單位

hash(123456789) = 123+456+789 = 1368

 

6、異或法(xor

思路

將關鍵碼的二進制展開分割成等寬的若干段,經異或運算獲得散列地址。

表達式

例:以二進制三個數位爲分割單位

hash(411) = hash(110011011b) = 110^011^011 = 110b = 6

 

7、平方散列法 

求index是很是頻繁的操做,而乘法的運算要比除法來得省時(對如今的CPU來講,估計咱們感受不出來),因此咱們考慮把除法換成乘法和一個位移操做。公式: 

      index = (value * value) >> 28   

(右移,除以2^28。記法:左移變大,是乘。右移變小,是除。)

若是數值分配比較均勻的話這種方法能獲得不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都是0——很是失敗。也許你還有個問題,value若是很大,value * value不會溢出嗎?答案是會的,但咱們這個乘法不關心溢出,由於咱們根本不是爲了獲取相乘結果,而是爲了獲取index。

 

8、斐波那契(Fibonacci)散列法

平方散列法的缺點是顯而易見的,因此咱們能不能找出一個理想的乘數,而不是拿value自己看成乘數呢?答案是確定的。

 

 

 

1,對於16位整數而言,這個乘數是40503 

2,對於32位整數而言,這個乘數是2654435769 

3,對於64位整數而言,這個乘數是11400714819323198485

 

    這幾個「理想乘數」是如何得出來的呢?這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典表達式無疑就是著名的斐波那契數列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377, 610, 987, 1597, 2584, 4181, 6765, 10946…。另外,斐波那契數列的值和太陽系八大行星的軌道半徑的比例出奇吻合。

 

    對咱們常見的32位整數而言,公式: 

            index = (value * 2654435769) >> 28

 

    若是用這種斐波那契散列法的話,那上面的圖就變成這樣了:

 

 

注:用斐波那契散列法調整以後會比原來的取摸散列法好不少。 

 

適用範圍

    快速查找,刪除的基本數據結構,一般須要總數據量能夠放入內存。

 

基本原理及要點

    hash函數選擇,針對字符串,整數,排列,具體相應的hash方法。 

碰撞處理,一種是open hashing,也稱爲拉鍊法;另外一種就是closed hashing,也稱開地址法,opened addressing。 

 

 散列衝突的解決方案:

1.創建一個緩衝區,把凡是拼音重複的人放到緩衝區中。當我經過名字查找人時,發現找的不對,就在緩衝區裏找。

 

2.進行再探測。就是在其餘地方查找。探測的方法也能夠有不少種。

 

(1)在找到查找位置的index的index-1,index+1位置查找,index-2,index+2查找,依次類推。這種方法稱爲線性再探測。

 

(2)在查找位置index周圍隨機的查找。稱爲隨機在探測。

 

(3)再哈希。就是當衝突時,採用另一種映射方式來查找。

 

這個程序中是經過取模來模擬查找到重複元素的過程。對待重複元素的方法就是再哈希:對當前key的位置+7。最後,能夠經過全局變量來判斷須要查找多少次。我這裏經過依次查找26個英文字母的小寫計算的出了總的查找次數。顯然,當總的查找次數/查找的總元素數越接近1時,哈希表更接近於一一映射的函數,查找的效率更高。 

 

 哈希衝突解決辦法:

衝突必然的

由於用短位(散列地址空間)表示長位數據(關鍵碼空間),確定會出現衝突。好比 常見的 MD5 碼,一共就128bit,但卻要表示無限的數據的散列碼,所以必然會出現不一樣數據具備相同MD5碼的狀況。

若是遇到衝突,哈希表通常是怎麼解決的呢?具體方法有不少,百度也會有一堆,最經常使用的就是開發定址法和鏈地址法

 

衝突排解策略分爲如下兩種類型:

  1. 開放定址(open addressing) / 閉散列(closed hashing):散列地址空間對全部詞條開放(即 桶單元容許裝hash(key)不對應的詞條);詞條存儲地址(散列地址)僅限於散列表所覆蓋的範圍以內。
    如:線性試探、查找鏈法等。
    注:因閉散列不得使用附加空間的緣由,裝填因子一般<=0.5

 

  1. 封閉定址(closed addressing) / 開散列(open hashing):散列地址空間只對對應的詞條開放;詞條存儲地址不侷限於散列表範圍以內。
    如:多槽位法、獨立鏈法、公共溢出區等

 

1、多槽位法(multiple slots

思路

 

 

 

每一個桶自己再細分爲若干槽位,用於存放彼此衝突的詞條。每一個桶槽位的詞典結構爲向量,所以總體物理存儲結構相似於二維數組。

如:put操做,首先經過hash(key)定位到對應的桶單元,並在該桶內部槽位中進一步查找key,若沒找到,則建立新詞條插入到該桶的空閒槽位中。

缺點

·絕大多數的槽位都處於空閒狀態,形成空間浪費。若桶被細分爲k個槽位,則裝填因子將直接下降爲原來的1/k.

·很難實現肯定應該細分爲多少個槽位,才能保證夠用。

 

2 獨立鏈法(separate chaining) / 拉鍊法

思路

 

 

 

與多槽位思想相似,但每一個桶的子詞典是使用鏈表實現,令彼此衝突的詞條互相串接。

優勢:

能靈活動態地調整子詞典的規模,有效地使用空間。

缺點

空間未必連續分佈,會致使系統緩存失效。

 

 

 

 

3、公共溢出區

原理

 

 

 

在原散列表以外另設一個詞典結構$D_{overflow}$,插入詞條一旦發生衝突,則轉存到該詞典中。$D_{overflow}$至關於存放衝突詞條的公共緩衝池。

 

4.線性探查法 Linear Probing

當獲得key的hash值H(key),可是表中下標爲H(key)的位置已經被某個其餘元素使用了,那麼就檢查下一個位置H(key) + 1 是否被佔,若是沒有,就使用這個位置;不然就繼續檢查下一個位置(也就是將hashH(key)不斷加1)。若是檢查過程當中超過了表長,那麼就回到表的首位繼續循環直到找到一個可使用的位置,或者是發現表中全部位置都已被使用。顯然,這個作法容易致使扎堆,即表中連續若干個位置都被使用,這在必定程度上會下降效率。

 

5.採用平方探查法【Quadratic Probing

經過將給定元素值對表長的餘數做爲在哈希表中的插入位置,若是出現衝突,採用平方探查法解決。平方探查法的具體過程是,假設給定元素值爲a,表長爲M,插入位置爲a%M,假設a%M位置已有元素,即發生衝突,則查找

(a+1^2)%M,(a-1^2)%M,   (a+2^2)%M,(a-2^2)%M,⋯⋯,   (a+k^2)%M,(a-k^2)%M直至查找到一個可進行插入的位置,不然當查找到(a+k^2)%M,(a-k^2)%M仍然不能插入則該元素插入失敗。其中 k<=M/2 【有的是k<M】

 

擴展 

具體查找邏輯

查找鏈(probing chain):對於待查找的key,從hash(key)桶單元開始,直接空桶結束的順序序列。

  1. 經hash(key)算得的當前桶單元,若關鍵碼相等,則成功返回。
  2. 當前桶單元非空,但關鍵碼不等,則轉入下一桶單元繼續試探。
  3. 當前桶爲空,則返回查找失敗。

注:相互衝突的關鍵碼比屬於同一查找鏈(即中途不包含空桶),但同一查找鏈的關鍵碼未必相互衝突。多組各自衝突的關鍵碼所對應的查找鏈,有可能相互交織和重疊。

優勢

具體由良好的數據局部性,試探地桶單元在物理空間上依次連貫,系統緩存能發揮做用。

 

懶惰刪除:

定義:

從詞典刪除詞條時,暫時並不實際將桶置空,而是額外維護一個刪除標記Bitmap,標記該桶已刪除。

爲何須要懶惰刪除?

由於查找鏈中任何一環的缺失,都會致使後續詞條的「丟失」,即沒法找到已存在詞條;同時由於開銷問題,不可能每次刪除操做都對查找鏈進行維護重建(在擴容時,才重建鏈)。

所以懶惰刪除機制既能保證查找鏈的完整,也不須要太多開銷。

加入懶惰刪除後,操做邏輯的變化:

  1. 在刪除等操做查詢指定詞條時,判斷失敗的條件變爲:爲空且不帶懶惰刪除標記。
  2. 在插入操做時,找空桶過程當中,判斷桶爲空條件爲:帶有懶惰標記或當前桶爲空。

 

 

d-left hashing

其中的d是多個的意思,咱們先簡化這個問題,看一看2-left hashing。2-left hashing指的是將一個哈希表分紅長度相等的兩半,分別叫作T1和T2,給T1和T2分別配備一個哈希函數,h1和h2。在存儲一個新的key時,同 時用兩個哈希函數進行計算,得出兩個地址h1[key]和h2[key]。這時須要檢查T1中的h1[key]位置和T2中的h2[key]位置,哪個 位置已經存儲的(有碰撞的)key比較多,而後將新key存儲在負載少的位置。若是兩邊同樣多,好比兩個位置都爲空或者都存儲了一個key,就把新key 存儲在左邊的T1子表中,2-left也由此而來。在查找一個key時,必須進行兩次hash,同時查找兩個位置。 

 

問題實例(海量數據處理) 

    咱們知道hash 表在海量數據處理中有着普遍的應用,下面,請看另外一道百度面試題:

題目:海量日誌數據,提取出某日訪問百度次數最多的那個IP。

方案:IP的數目仍是有限的,最多2^32個,因此能夠考慮使用hash將ip直接存入內存,而後進行統計。

 

 

 hash_map的使用:

hash函數系統自帶的類型函數:

  1 struct hash<char*>
  2 struct hash<const char*>
  3 struct hash<char> 
  4 struct hash<unsigned char> 
  5 struct hash<signed char>
  6 struct hash<short>
  7 struct hash<unsigned short> 
  8 struct hash<int> 
  9 struct hash<unsigned int>
 10 struct hash<long> 
 11 struct hash<unsigned long>
 12 
 13 
 14 普通的hash使用:
 15 
 16 hash_map<string, int>map1;
 17 
 18 自定義hash函數的使用:
 19 
 20 hash_map<int, string, hash<int>, equal_to<int> > mymap;
 21 
 22  
 23 
 24 hash函數的自定義與默認函數:
 25 
 26  struct str_hash
 27 
 28  {      //自寫hash函數    
 29 
 30          size_t operator()(const string& str) const  
 31 
 32          {   
 33 
 34             unsigned long __h = 0;
 35 
 36             for (size_t i = 0 ; i < str.size() ; i ++)   
 37 
 38              {   
 39 
 40                       __h = 107*__h + str[i];    
 41 
 42              }   
 43 
 44              return size_t(__h);   
 45 
 46           }    
 47 
 48  };
 49 
 50  
 51 
 52 / struct str_hash
 53 
 54 // {    //自帶的string hash函數   
 55 
 56 //          size_t operator()(const string& str) const    
 57 
 58 //          {  
 59 
 60 //                   return __stl_hash_string(str.c_str());  
 61 
 62 //          }    
 63 
 64 // };
 65 
 66  
 67 
 68 //  struct hash<int>
 69 
 70 //  {        //自帶的int hash函數   
 71 
 72 //     size_t operator()(int __x) const { return __x; }
 73 
 74 // };
 75 
 76  
 77 
 78  struct str_equal  //即壓入數據時,去重的比較函數
 79 
 80  {      //string 判斷相等函數  
 81 
 82          bool operator()(const string& s1,const string& s2) const  
 83 
 84          {    
 85 
 86                  return s1==s2;   
 87 
 88          }    
 89 
 90  };
 91 
 92  
 93 
 94 注意hash函數只指的是第一個元素類型
 95 
 96  
 97 
 98 hash_map<string,int,str_hash,str_equal> map2;   
 99 
100  
101 
102 }

 

 hash_map的案例——布隆過濾器

不安全網頁的黑名單包含100億個黑名單網頁,每一個網頁的URL最多佔用64B。如今想要實現一種網頁過濾系統,能夠根據網頁的URL判斷該網頁是否在黑名單上,請設計該系統。

要求以下:

該系統容許有萬分之一如下的判斷失誤率。

使用的額外空間不要超過30GB。

若是將這100億個URL經過數據庫或哈希表保存起來,就能夠對每條URL進行查詢,可是每一個URL有64B,數量是100億個,因此至少須要640GB的空間,不知足要求2。

若是面試者遇到網頁黑名單系統、垃圾郵件過濾系統,爬蟲的網頁判重系統等題目,又看到系統容忍必定程度的失誤率,可是對空間要求比較嚴格,那麼極可能是面試官但願面試者具有布隆過濾器的知識。一個布隆過濾器精確地表明一個集合,並能夠精確判斷一個元素是否在集合中。注意,只是精確表明和精確判斷,到底有多精確呢?則徹底在於你具體的設計,但想作到徹底正確是不可能的。布隆過濾器的優點就在於使用不多的空間就能夠將準確率作到很高的程度。該結構由Burton Howard Bloom於1970年提出。

那麼什麼是布隆過濾器呢?

假設有一個長度爲m的bit類型的數組,即數組的每一個位置只佔一個bit,若是咱們所知,每個bit只有0和1兩種狀態,如圖所示:

 

 

 

 再假設一共有k個哈希函數,這些函數的輸出域S都大於或等於m,而且這些哈希函數都足夠優秀且彼此之間相互獨立(將一個哈希函數的計算結果乘以6除以7得出的新哈希函數和原函數就是相互獨立的)。那麼對同一個輸入對象(假設是一個字符串,記爲URL),通過k個哈希函數算出來的結果也是獨立的。可能相同,也可能不一樣,但彼此獨立。對算出來的每個結果都對m取餘(%m),而後在bit array 上把相應位置設置爲1(咱們形象的稱爲塗黑)。如圖所示

 

 

 

 咱們把bit類型的數組記爲bitMap。至此,一個輸入對象對bitMap的影響過程就結束了,也就是bitMap的一些位置會被塗黑。接下來按照該方法,處理全部的輸入對象(黑名單中的100億個URL)。每一個對象均可能把bitMap中的一些白位置塗黑,也可能遇到已經塗黑的位置,遇到已經塗黑的位置讓其繼續爲黑便可。處理完全部的輸入對象後,可能bitMap中已經有至關多的位置被塗黑。至此,一個布隆過濾器生成完畢,這個布隆過濾器表明以前全部輸入對象組成的集合。

那麼在檢查階段時,如何檢查一個對象是不是以前的某一個輸入對象呢(判斷一個URL是不是黑名單中的URL)?假設一個對象爲a,想檢查它是不是以前的輸入對象,就把a經過k個哈希函數算出k個值,而後把k個值都取餘(%m),就獲得在[0,m-1]範圍傷的k個值。接下來在bitMap上看這些位置是否是都爲黑。若是有一個不爲黑,說明a必定再也不這個集合裏。若是都爲黑,說明a在這個集合裏,但可能誤判。

再解釋具體一點,若是a的確是輸入對象 ,那麼在生成布隆過濾器時,bitMap中相應的k個位置必定已經塗黑了,因此在檢查階段,a必定不會被漏過,這個不會產生誤判。會產生誤判的是,a明明不是輸入對象,但若是在生成布隆過濾器的階段由於輸入對象過多,而bitMap太小,則會致使bitMap絕大多數的位置都已經變黑。那麼在檢查a時,可能a對應的k個位置都是黑的,從而錯誤地認爲a是輸入對象(便是黑名單中的URL)。通俗地說,布隆過濾器的失誤類型是「寧肯錯殺三千,毫不放過一個」。

布隆過濾器到底該怎麼生成呢?只需記住下列三個公式便可:

對於輸入的數據量n(這裏是100億)和失誤率p(這裏是萬分之一),布隆過濾器的大小m:m = - (n*lnp)/(ln2*ln2),計算結果向上取整(這道題m=19.19n,向上取整爲20n,即須要2000億個bit,也就是25GB)

須要的哈希函數的個數k:k = ln2 * m/n = 0.7 * m/n(這道題k = 0.7 * 20n/n = 14)

因爲前兩步都進行了向上取整,那麼由前兩步肯定的布隆過濾器的真正失誤率p:p = (1 - e^(-nk/m))^k

一致性哈希算法的基本原理

題目

工程師常使用服務器集羣來設計和實現數據緩存,如下是常見的策略:

不管是添加、查詢仍是珊瑚數據,都先將數據的id經過哈希函數換成一個哈希值,記爲key

若是目前機器有N臺,則計算key%N的值,這個值就是該數據所屬的機器編號,不管是添加、刪除仍是查詢操做,都只在這臺機器上進行。

請分析這種緩存策略可能帶來的問題,並提出改進的方案。

解析

題目中描述的緩存從策略的潛在問題是,若是增長或刪除機器時(N變化)代價會很高,全部的數據都不得不根據id從新計算一遍哈希值,並將哈希值對新的機器數進行取模啊哦作。而後進行大規模的數據遷移。

爲了解決這些問題,下面介紹一下一致性哈希算法,這時一種很好的數據緩存設計方案。咱們假設數據的id經過哈希函數轉換成的哈希值範圍是2^32,也就是0~(2^32)-1的數字空間中。如今咱們能夠將這些數字頭尾相連,想象成一個閉合的環形,那麼一個數據id在計算出哈希值以後認爲對應到環中的一個位置上,如圖所示

 

 

 

 接下來想象有三臺機器也處在這樣一個環中,這三臺機器在環中的位置根據機器id(主機名或者主機IP,是主機惟一的就行)設計算出的哈希值對2^32取模對應到環上。那麼一條數據如何肯定歸屬哪臺機器呢?咱們能夠在該數據對應環上的位置順時針尋找離該位置最近的機器,將數據歸屬於該機器上:

 

 

 

 這樣的話,若是刪除machine2節點,則只需將machine2上的數據遷移到machine3上便可,而沒必要大動干戈遷移全部數據。當添加節點的時候,也只需將新增節點到逆時針方向新增節點前一個節點這之間的數據遷移給新增節點便可。

但這時仍是存在以下兩個問題:

機器較少時,經過機器id哈希將機器對應到環上以後,幾個機器可能沒有均分環

 

 

 

 那麼這樣會致使負載不均。

增長機器時,可能會打破現有的平衡:

 

 

 

 爲了解決這種數據傾斜問題,一致性哈希算法引入了虛擬節點機制,即對每一臺機器經過不一樣的哈希函數計算出多個哈希值,對多個位置都放置一個服務節點,稱爲虛擬節點。具體作法:好比對於machine1的IP192.168.25.132(或機器名),計算出192.168.25.132-一、192.168.25.132-二、192.168.25.132-三、192.168.25.132-4的哈希值,而後對應到環上,其餘的機器也是如此,這樣的話節點數就變多了,根據哈希函數的性質,平衡性天然會變好:

 

 

 

 

此時數據定位算法不變,只是多了一步虛擬節點到實際節點的映射,好比上圖的查找表。當某一條數據計算出歸屬於m2-1時再根據查找表的跳轉,數據將最終歸屬於實際的m1節點。

基於一致性哈希的原理有不少種具體的實現,包括Chord算法、KAD算法等,有興趣的話能夠進一步學習。

 

 RandomPool

設計一種結構,在該結構中有以下三個功能:

inserrt(key):將某個key加入到該結構中,作到不重複加入。

delete(key):將本來在結構中的某個key移除。

getRandom():等機率隨機返回結構中的任何一個key。

要求:insert、delete和getRandom方法的時間複雜度都是O(1)

思路:使用兩個哈希表和一個變量size,一個表存放某key的標號,另外一個表根據根據標號取某個key。size用來記錄結構中的數據量。加入key時,將size做爲該key的標號加入到兩表中;刪除key時,將標號最大的key替換它並將size--;隨機取key時,將size範圍內的隨機數做爲標號取key。

  1 template<class T>
  2 
  3 class RandomPool
  4 
  5 {
  6 
  7 public:
  8 
  9 void insert(T key);
 10 
 11 void del(T key);
 12 
 13 T getRandom();
 14 
 15 void getPrint(T key);
 16 
 17 void getPrint(int index);
 18 
 19  
 20 
 21 private:
 22 
 23 hash_map<T, int>KeyMap;
 24 
 25 hash_map<int, T>IndexMap;
 26 
 27 int size = 0;
 28 
 29 };
 30 
 31  
 32 
 33  
 34 
 35 template<class T>
 36 
 37 void RandomPool<T>::insert(T key)
 38 
 39 {
 40 
 41 if (KeyMap.find(key) == KeyMap.end())
 42 
 43 {
 44 
 45 KeyMap[key] = this->size;
 46 
 47 IndexMap[this->size] = key;
 48 
 49 ++(this->size);
 50 
 51 cout << "add succeed!" << endl;
 52 
 53 }
 54 
 55 else
 56 
 57 cout << "add filed!" << endl;
 58 
 59 }
 60 
 61  
 62 
 63  
 64 
 65 template<class T>
 66 
 67 void RandomPool<T>::del(T key)
 68 
 69 {
 70 
 71 auto ptr = KeyMap.find(key);
 72 
 73 if (ptr == KeyMap.end())
 74 
 75 {
 76 
 77 cout << "delete filed! there is not exsite the key!" << endl;
 78 
 79 return;
 80 
 81 }
 82 
 83 //交換查找到元素與最後一個元素
 84 
 85 T temp = IndexMap[--(this->size)];//最後一個元素的關鍵詞,同時將hash表中的元素刪除了
 86 
 87 int index = KeyMap[key];//要刪除元素的位置
 88 
 89 KeyMap[temp] = index;
 90 
 91 IndexMap[index] = temp;//將最後一個元素替換要刪除元素的位置
 92 
 93 //正式刪除
 94 
 95 KeyMap.erase(ptr);
 96 
 97 IndexMap.erase(IndexMap.find(index));
 98 
 99 }
100 
101  
102 
103 template<class T>
104 
105 T RandomPool<T>::getRandom()
106 
107 {
108 
109 if (this->size == 0)
110 
111 {
112 
113 cout << "the map is empty!" << endl;
114 
115 }
116 
117 else
118 
119 {
120 
121 int index = (int)((rand() % (99 + 1) / (double)(99 + 1))*(this->size));//隨機生成一個位置
122 
123 return IndexMap[index];
124 
125 }
126 
127 }
128 
129  
130 
131 template<class T>
132 
133 void RandomPool<T>::getPrint(T key)
134 
135 {
136 
137 if (KeyMap.find(key) == KeyMap.end())
138 
139 cout << "the key is not exsite!" << endl;
140 
141 else
142 
143 cout << KeyMap[key] << endl;
144 
145 }
146 
147  
148 
149 template<class T>
150 
151 void RandomPool<T>::getPrint(int index)
152 
153 {
154 
155 if (IndexMap.find(index) == IndexMap.end())
156 
157 cout << "the key is not exsite!" << endl;
158 
159 else
160 
161 cout << IndexMap[index] << endl;
162 
163 }
164 
165  
166 
167 <code class="lang-java">import java.util.HashMap;
168 
169  
170 
171 public class RandomPool {
172 
173     public int size;
174 
175     public HashMap<Object, Integer> keySignMap;
176 
177     public HashMap<Integer, Object> signKeyMap;
178 
179  
180 
181     public RandomPool() {
182 
183         this.size = 0;
184 
185         this.keySignMap = new HashMap<>();
186 
187         this.signKeyMap = new HashMap<>();
188 
189     }
190 
191  
192 
193     public void insert(Object key) {
194 
195         //不重複添加
196 
197         if (keySignMap.containsKey(key)) {
198 
199             return;
200 
201         }
202 
203         keySignMap.put(key, size);
204 
205         signKeyMap.put(size, key);
206 
207         size++;
208 
209     }
210 
211  
212 
213     public void delete(Object key) {
214 
215         if (keySignMap.containsKey(key)) {
216 
217             Object lastKey = signKeyMap.get(--size);
218 
219             int deleteSign = keySignMap.get(key);
220 
221             keySignMap.put(lastKey, deleteSign);
222 
223             signKeyMap.put(deleteSign, lastKey);
224 
225             keySignMap.remove(key);
226 
227             signKeyMap.remove(lastKey);
228 
229         }
230 
231     }
232 
233  
234 
235     public Object getRandom() {
236 
237         if (size > 0) {
238 
239             return signKeyMap.get((int) (Math.random() * size));
240 
241         }
242 
243         return null;
244 
245     }
246 
247  
248 
249 }

 

 認識一致性哈希

 

使用二分法,找到hash值大於等於該值的hash值的服務器即爲管理該值的服務器

一致性哈希的應用:

當咱們要添加或減小服務器時,通常操做是將已存的值從新計算hash值,而後取mod,來決定管理該數據的服務器,代價過高,時間太長

 

因而使用一個環來減小操做:

將hash範圍組成一個環,如圖所示,而後將服務器按其hash值按順序數組中,

而後每輸入一個數據,根據其hash值,使用二分法將其放入剛剛>=該數hash值的服務器。

當添加服務器時,【假如添加一個服務器mx在m1-m2之間】只須要將原m1-m2中一部分小於mx的數據改成有mx來管理。

如何確保服務器負載均衡【由於服務器的hash值不均勻】?

使用一致hash

將每一個服務器產生N個虛擬服務器,如1000個,則有3個服務器就有3000個虛擬服務器,而後讓3000個虛擬去負載整個hash範圍,那麼這三個服務器的虛擬服務器所管理的數據就幾乎均分分佈在整個hash域中

那麼就認爲這3個服務器的負載均衡。

添加與刪除原理也是如此。

相關文章
相關標籤/搜索