想要知道什么是哈希表,得先了解哈希函數
哈希函數
地址index=H(key)
說白了,hash函數就是根據key計算出應該存儲地址的位置,而哈希表是基於哈希函數建立的一種查找表
幾種常見的哈希函數(散列函數)構造方法
直接定址法
- 取關鍵字或關鍵字的某個線性函數值為散列地址。
- 即 H(key) = key 或 H(key) = a*key + b,其中a和b為常數。
除留余數法
- 取關鍵字被某個不大於散列表長度 m 的數 p 求余,得到的作為散列地址。
- 即 H(key) = key % p, p < m。
數字分析法
- 當關鍵字的位數大於地址的位數,對關鍵字的各位分布進行分析,選出分布均勻的任意幾位作為散列地址。
- 僅適用於所有關鍵字都已知的情況下,根據實際應用確定要選取的部分,盡量避免發生沖突。
平方取中法
- 先計算出關鍵字值的平方,然后取平方值中間幾位作為散列地址。
- 隨機分布的關鍵字,得到的散列地址也是隨機分布的。
折疊法(疊加法)
- 將關鍵字分為位數相同的幾部分,然后取這幾部分的疊加和(舍去進位)作為散列地址。
- 用於關鍵字位數較多,並且關鍵字中每一位上數字分布大致均勻。
隨機數法
- 選擇一個隨機函數,把關鍵字的隨機函數值作為它的哈希值。
- 通常當關鍵字的長度不等時用這種方法。
構造哈希函數的方法很多,實際工作中要根據不同的情況選擇合適的方法,總的原則是盡可能少的產生沖突。
通常考慮的因素有關鍵字的長度和分布情況、哈希值的范圍等。
如:當關鍵字是整數類型時就可以用除留余數法;如果關鍵字是小數類型,選擇隨機數法會比較好。
什么是哈希表
哈希表(Hash table,也叫散列表),具有像數組那樣根據隨機訪問的特性,是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
我們通過一個具體實例來理解散列表。當學校舉辦校運動會,每個運動員都有一個號碼牌,這個號碼牌的是根據“年級+班級+需序號”組成,比如初一三班的小島的號碼牌為 070311
,其中 07
表示七年級即初一, 03
表示三班, 11
表示班上第11個班上參加運動會的序號。
這個時候我們如何存儲運動員的信息,來實現通過號碼牌來查找運動員的信息?
按照以往的經驗,我們可以通過使用數組來存儲,其中號碼牌即為數組的下標,數組的值為運動員的信息。但是這里有一個問題,運動員的號碼牌不是連續的,而申請數組的時候內存空間是連續的,因此會有很多內存空間浪費。
這個時候就可以使用散列表,處理過程如下所示:
從上圖可以觀察到,我們在存儲運動員信息的時候,不是將整個號碼牌作為數組的下標,而是將號碼牌先進行 hash
函數(對100取余)處理后得到的數作為數組的下標,這樣就可以數組的大小大大減小,並且在查找到時候也可以通過號碼牌來查找對應的運動員的信息。
index=H(key)
這里的對應關系H稱為散列函數,又稱為哈希(Hash函數),采用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間稱為散列表或哈希表(Hash table)。
Hash Table的查詢速度非常的快,幾乎是O(1)的時間復雜度。
hash就是找到一種數據內容和數據存放地址之間的映射關系。
哈希沖突
細心的小伙伴觀察到,如果 hash
函數處理后的余數一樣該怎么辦?
即不同key值產生相同的地址,H(key1)=H(key2)
比如我們上面說的存儲3 6 9
3 MOD 3 == 6 MOD 3 == 9 MOD 3
此時3 6 9都發生了hash沖突
解決沖突
設計合理的哈希函數可以減少沖突,但不能完全避免沖突。
所以需要有解決沖突的方法,常見有兩類
開放定址法
如果兩個數據元素的哈希值相同,則在哈希表中為后插入的數據元素另外選擇一個表項。
當程序查找哈希表時,如果沒有在第一個對應的哈希表項中找到符合查找要求的數據元素,程序就會繼續往后查找,直到找到一個符合查找要求的數據元素,或者遇到一個空的表項。
例子
若要將一組關鍵字序列 {1, 9, 25, 11, 12, 35, 17, 29} 存放到哈希表中。
采用除留余數法構造哈希表;采用開放定址法處理沖突。
不妨設選取的p和m為13,由 f(key) = key % 13 可以得到下表。
需要注意的是,在上圖中有兩個關鍵字的探查次數為 2 ,其他都是1。
這個過程是這樣的:
① 12 % 13 結果是12,而它的前面有個 25 ,25 % 13 也是12,存在沖突。
我們使用開放定址法 (12 + 1) % 13 = 0,沒有沖突,完成。
② 35 % 13 結果是 9,而它的前面有個 9,9 % 13也是 9,存在沖突。
我們使用開放定址法 (9 + 1) % 13 = 10,沒有沖突,完成。
拉鏈法(鏈表的數組)
將哈希值相同的數據元素存放在一個鏈表中,在查找哈希表的過程中,當查找到這個鏈表時,必須采用線性查找方法。
在這種方法中,哈希表中每個單元存放的不再是記錄本身,而是相應同義詞單鏈表的頭指針。
例子
如果對開放定址法例子中提到的序列使用拉鏈法,得到的結果如下圖所示:
我們根據元素的一些特征把元素分配到不同的鏈表中去,也是根據這些特征,找到正確的鏈表,再從鏈表中找出這個元素。
元素特征轉變為數組下標的方法就是散列法. 散列法當然不止一種,下面列出三種比較常用的:
除法散列法
最直觀的一種,公式: index = value % 16
學過匯編的都知道,求模數其實是通過一個除法運算得到的,所以叫“除法散列法”。
平方散列法
求index是非常頻繁的操作,而乘法的運算要比除法來得省時(對現在的CPU來說,估計我們感覺不出來),所以我們考慮把除法換成乘法和一個位移操作。公式:
index = (value * value) >> 28 (右移,除以2^28。記法:左移變大,是乘。右移變小,是除。)
如果數值分配比較均勻的話這種方法能得到不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都是0——非常失敗。也許你還有個問題,value如果很大,value * value不會溢出嗎?答案是會的,但我們這個乘法不關心溢出,因為我們根本不是為了獲取相乘結果,而是為了獲取index。
斐波那契(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。
Hash的應用
1、Hash主要用於信息安全領域中加密算法,它把一些不同長度的信息轉化成雜亂的128位的編碼,這些編碼值叫做Hash值. 也可以說,Hash就是找到一種數據內容和數據存放地址之間的映射關系。
2、查找:哈希表,又稱為散列,是一種更加快捷的查找技術。我們之前的查找,都是這樣一種思路:集合中拿出來一個元素,看看是否與我們要找的相等,如果不等,縮小范圍,繼續查找。而哈希表是完全另外一種思路:當我知道key值以后,我就可以直接計算出這個元素在集合中的位置,根本不需要一次又一次的查找!
舉一個例子,假如我的數組A中,第i個元素里面裝的key就是i,那么數字3肯定是在第3個位置,數字10肯定是在第10個位置。哈希表就是利用利用這種基本的思想,建立一個從key到位置的函數,然后進行直接計算查找。
3、Hash表在海量數據處理中有着廣泛應用。
問題實例(海量數據處理)
我們知道hash 表在海量數據處理中有着廣泛的應用,下面,請看另一道百度面試題:
題目:海量日志數據,提取出某日訪問百度次數最多的那個IP。
方案:IP的數目還是有限的,最多2^32個,所以可以考慮使用hash將ip直接存入內存,然后進行統計。