作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段聲明。謝謝!
HASH
雜湊表(hash table)是從一個集合A到另一個集合B的映射(mapping)。映射是一種對應關係,而且集合A的某個元素只能對應集合B中的一個元素。但反過來,集合B中的一個元素可能對應多個集合A中的元素。如果B中的元素只能對應A中的一個元素,這樣的映射被稱為一一映射。這樣的對應關係在現實生活中很常見,比如:
A -> B
人 -> 社會安全號碼
日期 -> 星座
上面兩個映射中,人 -> 社會安全號碼是一一映射的關係。在雜湊表中,上述對應過程稱為hashing。A中元素a對應B中元素b,a被稱為索引值(key),b被稱為a的hash值(hash value)。
韋小寶的hash值
映射在數學上相當於一個函數f(x):A->B。比如 f(x) = 3x + 2。雜湊表的核心是一個雜湊函數(hash function),這個函數規定了集合A中的元素如何對應到集合B中的元素。比如:
A: 三位整數 hash(x) = x % 10 B: 一位整數
104 4
876 6
192 2
上述對應中,雜湊函數表示為hash(x) = x % 10。也就是說,給一個三位元,我們取它的最後一位作為該三位元的hash值。
雜湊表在電腦科學中應用廣泛。比如:
Ethernet中的FCS:參看小喇叭開始廣播 (乙太網路與WiFi協議)
IP協議中的checksum:參看我儘力 (IP協議詳解)
git中的hash值:參看版本管理三國志
上述應用中,我們用一個hash值來代表索引值。比如在git中,檔案內容為索引值,並用SHA演算法作為hash function,將檔案內容對應為固定長度的字串(hash值)。如果檔案內容發生變化,那麼所對應的字串就會發生變化。git通過比較較短的hash值,就可以知道檔案內容是否發生變動。
再比如電腦的登陸密碼,一般是一串字元。然而,為了安全起見,電腦不會直接儲存該字串,而是儲存該字串的hash值(使用MD5、SHA或者其他演算法作為hash函數)。當使用者下次登陸的時候,輸入密碼字串。如果該密碼字串的hash值與儲存的hash值一致,那麼就認為使用者輸入了正確的密碼。這樣,就算駭客闖入了資料庫中的密碼記錄,他能看到的也只是密碼的hash值。上面所使用的hash函數有很好的單向性:很難從hash值去推測索引值。因此,駭客無法獲知使用者的密碼。
(之前有報道多家網站使用者密碼泄露的時間,就是因為這些網站儲存純文字密碼,而不是hash值,見多家網站捲入CSDN泄密事件 純文字密碼成爭議焦點)
注意,hash只要求從A到B的對應為一個映射,它並沒有限定該對應關係為一一映射。因此會有這樣的可能:兩個不同的索引值對應同一個hash值。這種情況叫做hash碰撞(hash collision)。比如網路通訊協定中的checksum就可能出現這種狀況,即所要校正的內容與原文並不同,但與原文產生的checksum(hash值)相同。再比如,MD5演算法常用來計算密碼的hash值。已經有實驗表明,MD5演算法有可能發生碰撞,也就是不同的純文字密碼產生相同的hash值,這將給系統帶來很大的安全性漏洞。(參考hash collision)
HASH與搜尋
hash表被廣泛的用於搜尋。設定集合A為搜尋對象,集合B為儲存位置,利用hash函數將搜尋對象與儲存位置對應起來。這樣,我們就可以通過一次hash,將對象所在位置找到。一種常見的情形是,將集合B設定在數組下標。由於數組可以根據數組下標進行隨機存取(random access,演算法複雜度為1),所以搜尋操作將取決於hash函數的複雜程度。
比如我們以人名(字串)為索引值,以數組下標為hash值。每個數組元素中儲存有一個指標,指向記錄 (有人名和電話號碼)。
下面是一個簡單的hash函數:
#define HASHSIZE 1007
/* By Vamei
* hash function
*/int hash(char *p){ int value=0; while((*p) != '\0') { value = value + (int) (*p); // convert char to int, and sum p++; } return (value % HASHSIZE); // won's exceed HASHSIZE}
hash value of "Vamei": 498
hash value of "Obama": 480
我們可以建立一個HASHSIZE大小的數組records,用於儲存記錄。HASHSIZE被選擇為質數,以便hash值能更加均勻的分布。在搜尋"Vamei"的記錄時,可以經過hash,得到hash值498,再直接讀取records[498],就可以讀取記錄了。
(666666是Obama的電話號碼,111111是Vamei的電話號碼。純屬杜撰,請勿當真)
hash搜尋
如果不採用hash,而只是在一個數組中搜尋的話,我們需要依次訪問每個記錄,直到找到目標記錄,演算法複雜度為n。我們可以考慮一下為什麼會有這樣的差別。數組雖然可以隨機讀取,但數組下標是隨機的,它與元素值沒有任何關係,所以我們要逐次訪問各個元素。通過hash函數,我們限定了每個下標位置可能儲存的元素。這樣,我們利用索引值和hash函數,就可以具備相當的先驗知識,來選擇適當的下標進行搜尋。在沒有hash碰撞的前提下,我們只需要選擇一次,就可以保證該下標指向的元素是我們想要的元素。
解決衝突
hash函數需要解決hash衝突的問題。比如,上面的hash函數中,"Obama"和"Oaamb"有相同的hash值,發生衝突。我們如何解決呢?
一個方案是將發生衝突的記錄用鏈表儲存起來,讓hash值指向該鏈表,這叫做open hashing:
open hashing
我們在搜尋的時候,先根據hash值找到鏈表,再根據key值遍曆搜尋鏈表,直到找到記錄。我們可以用其他資料結構代替鏈表。
open hashing需要使用指標。我們有時候想要避免使用指標,以保持隨機儲存的優勢,所以採用closed hashing的方式來解決衝突。
closed hashing
這種情況下,我們將記錄放入數組。當有衝突出現的時候,我們將衝突記錄放在數組中依然閑置的位置,比中Obama被插入後,隨後的Oaamb也被hash到480位置。但由於480被佔據,Oaamb探測到下一個閑置位置(通過將hash值加1),並記錄。
closed hashing的關鍵在如何探測下一個位置。上面是將hash值加1。但也可以有其它的方式。概括的說,在第i次的時候,我們應該探測POSITION(i)=(h(x) + f(i)) % HASHSIZE的位置。上面將hash值加1的方式,就相當於設定f(i) = 1。當我們在搜尋的時候,就可以利用POSITION(i),依次探測記錄可能出現的位置,直到找到記錄。
(f(i)的選擇會帶來不同的結果,這裡不再深入)
如果數組比較滿,那麼closed hashing需要進行許多次探測才能找到空位。這樣將大大減小插入和搜尋的效率。這種情況下,需要增大HASHSIZE,並將原來的記錄放入到新的比較大的數組中。這樣的操作稱為rehashing。
總結
hash表,搜尋
hash衝突, open hashing, closed hashing
歡迎繼續閱讀“紙上談兵: 演算法與資料結構”系列。