1. 什麼是哈希表:
Hash,一般翻譯做“散列”,也有直接音譯爲“哈希”的,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。
2. 哈希表的優點:
就是把數據的存儲和查找消耗的時間大大降低,幾乎可以看成是常數時間O(1);而代價僅僅是消耗比較多的內存。
3. 哈希表的基本原理:
使用一個下標範圍比較大的數組來存儲元素。可以設計一個函數(哈希函數,也叫做散列函數),使得每個元素的關鍵字都與一個函數值(即數組下標,hash值)相對應,於是用這個數組單元來存儲這個元素;也可以簡單的理解爲,按照關鍵字爲每一個元素“分類”,然後將這個元素存儲在相應“類”所對應的地方,稱爲桶。
但是,不能夠保證每個元素的關鍵字與函數值是一一對應的,因此極有可能出現對於不同的元素,卻計算出了相同的函數值,這樣就產生了“衝突”,換句話說,就是把不同的元素分在了相同的“類”之中。 總的來說,“直接定址”與“解決衝突”是哈希表的兩大特點。
其插入過程是:
(1). 得到key
(2). 通過hash函數得到hash值
(3). 得到桶號(一般都爲hash值對桶數求模)
(4). 存放key和value在桶內。
其取值過程是:
(1). 得到key
(2). 通過hash函數得到hash值
(3). 得到桶號(一般都爲hash值對桶數求模)
(4). 比較桶的內部元素是否與key相等,若都不相等,則沒有找到。
(5). 取出相等的記錄的value。
4. 常用哈希算法:
(1)直接定址法 :地址集合 和 關鍵字集合大小相同
(2)數字分析法 :根據需要hash的 關鍵字的特點選擇合適hash算法,儘量尋找每個關鍵字的 不同點
(3)平方取中法:取關鍵字平方之後的中間極爲作爲哈希地址,一個數平方之後中間幾位數字與數的每一位都相關,取得位數由表長決定。比如:表長爲512,=2^9,可以取平方之後中間9位二進制數作爲哈希地址。
(4)摺疊法:關鍵字位數很多,而且關鍵字中每一位上的數字分佈大致均勻的時候,可以採用摺疊法得到哈希地址,
(5)除留取餘法除P取餘,可以選P爲質數,或者不含有小於20的質因子的合數
(6)隨機數法:通常關鍵字不等的時候採用此法構造哈希函數較恰當。
實際工作中需要視不同的情況採用不同的hash函數:
(1)考慮因素:計算哈希函數所需要的時間,硬件指令等因素。
(2)關鍵字長度
(3)哈希表大小
(4)關鍵字分佈情況
(5)記錄查找的頻率。(huffeman樹)
元素特徵轉變爲數組下標的方法就是散列法。散列法當然不止一種,下面列出三種比較常用的:
(1)除法散列法
最直觀的一種,上圖使用的就是這種散列法,公式:index = value % 16
學過彙編的都知道,求模數其實是通過一個除法運算得到的,所以叫“除法散列法”。
(2)平方散列法
求index是非常頻繁的操作,而乘法的運算要比除法來得省時(對現在的CPU來說,估計我們感覺不出來),所以我們考慮把除法換成乘法和一個位移操作。公式: index = (value * value) >> 28 (右移,除以2^28。記法:左移變大,是乘。右移變小,是除。)
如果數值分配比較均勻的話這種方法能得到不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都是0——非常失敗。也許你還有個問題,value如果很大,value * value不會溢出嗎?答案是會的,但我們這個乘法不關心溢出,因爲我們根本不是爲了獲取相乘結果,而是爲了獲取index。
(3)斐波那契(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
如果用這種斐波那契散列法的話,那上面的圖就變成這樣了:
注:用斐波那契散列法調整之後會比原來的取摸散列法好很多。
(4)"Timers33"算法
幾乎所有的流行的hash map都採用了DJB hash function,俗稱“Times33”算法。
times33的算法也很簡單,就是不斷的乘33。nHash = nHash*33 + *key++;
經典times33算法如下:
inline UINT CMyMap::HashKey(LPCTSTR key) const
{
UINT nHash = 0;
while (*key)
nHash = (nHash<<5) + nHash + *key++;
return nHash;
}
5. 解決衝突的兩種方法:
(1)分離鏈接: 就是將數據實際存放在與 hash 表存儲單元相鏈接的鏈表中,而不是 hash 的存儲單元中。、
當產生衝突的時候,將兩個數據都鏈接在同一 hash 存儲單元保存的鏈表中。當一個存儲單元保存的鏈表中有多個數據的時候,對於鏈表後面的數據的查找添加和刪除就是不是嚴格意義上的 O(1) 了。一個好的 hash 函數可以使得這個鏈表很短。最壞情況下,當所有的數據都保存在一個 hash 單元指定的鏈表中的時候,那麼這個 hash 就和鏈表一樣了。
(2)開放地址: 使用開放地址方法解決衝突的時候,數據仍然保存在 hash 表的存儲單元中,但是當衝突發生的時候,要再次計算新的地址。常用的開放地址法是線性探查,就是當對一個數據進行插入刪除或者查找的時候,通過 hash 函數計算,發現這個位置不是要找的數據,這時候就檢查下一個存儲單元,一直找到要操作的數據爲止。 除了線性探查外還有二次探查,再 hash 等等方法,都是當一次計算得到的位置不是要找到的數據的時候,怎樣再次確定新的位置。
6. hash使用實例
工作中使用中見過兩種方法:
(1)特定的數據 % 整個size。
(2)使用斐波那契(Fibonacci)散列法,即對我們常見的32位整數而言,公式: index = (value * 2654435769) >> 28