紙上談兵 雜湊表 hash table

2021-07-07 09:09:17 字數 3469 閱讀 9049

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: 一位整數

1044

876   

6192  

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表被廣泛的用於搜尋。設定集合a為搜尋物件,集合b為儲存位置,利用hash函式將搜尋物件與儲存位置對應起來。這樣,我們就可以通過一次hash,將物件所在位置找到。一種常見的情形是,將集合b設定在陣列下標。由於陣列可以根據陣列下標進行隨機訪問(random access,演算法複雜度為1),所以搜尋操作將取決於hash函式的複雜程度。

比如我們以人名(字串)為鍵值,以陣列下標為hash值。每個陣列元素中儲存有乙個指標,指向記錄 (有人名和**號碼)。

下面是乙個簡單的hash函式:

#define hashsize 1007

/* by vamei

* hash function

*/int hash(char *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:

我們在搜尋的時候,先根據hash值找到鍊錶,再根據key值遍歷搜尋鍊錶,直到找到記錄。我們可以用其他資料結構代替鍊錶。

open 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

紙上談兵 佇列 queue

佇列 queue 是乙個簡單而常見的資料結構。佇列也是有序的元素集合。佇列最大的特徵是first in,first out fifo,先進先出 即先進入佇列的元素,先被取出。這一點與棧 stack 形成有趣的對比。佇列在生活中很常見,排隊買票 排隊等車 先到的人先得到服務並離開佇列,後來的人加入到佇...

紙上談兵 佇列 queue

佇列 queue 是乙個簡單而常見的資料結構。佇列也是有序的元素集合。佇列最大的特徵是first in,first out fifo,先進先出 即先進入佇列的元素,先被取出。這一點與棧 stack 形成有趣的對比。佇列在生活中很常見,排隊買票 排隊等車 先到的人先得到服務並離開佇列,後來的人加入到佇...

紙上談兵 棧 stack

棧 stack 是簡單的資料結構,但在計算機中使用廣泛。它是有序的元素集合。棧最顯著的特徵是lifo last in,first out,後進先出 當我們往箱子裡存放一疊書時,先存放的書在箱子下面,我們必須將後存放的書取出來,才能看到和拿出早先存放的書。棧中的每個元素稱為乙個frame。而最上層元素...