散列表之散列函數
我們在之前的文章《散列表之鏈接法》中已經提到過,散列函數是散列表的一個難點,一個好的散列可以很大程度上提升散列表的查找和刪除操作的速度,而一個設計差勁的散列表的,查找和刪除操作的運行時間將和鏈式鏈表一樣,將達到
什麼是好的散列函數
一個好的散列函數應該滿足簡單均勻散列
的假設:
每一個關鍵字都被等可能的散列到m個槽中的任何一個,並與其它關鍵字散列到那個槽無關。
在實際應用中,我們可以通過利用關鍵字有用的分佈信息來設計一個表現良好的散列函數。假設現在我們需要散列一些英文字符串,其中有一些比較相近的單詞,比如the
,then
,he
等等,好的散列函數是將這些鍵值比較相近的字符串映射到相同槽的可能性最小化。
將關鍵字轉化爲自然數
什麼?關鍵字除了自然數難道還有其他麼?是的,關鍵字除了自然數還有許多其他的種類,比如字符串,浮點數,一個類對象,一些可以將自身轉化爲自然數的類型。轉化爲自然數以後,進行一些計算映射工作就得到槽的位置了。
比如對於一個字符串來說,我們將每個字符的ASCII碼相加就得到一個自然數。一個小數的話,可以進行小數的截斷或者通過乘
舉個例子:
class Person
{
//menbers
int age;
string name;
//function 轉爲自然數
size_t toN() const
{
/** 將age和name進行一些計算以後返回一個自然數 */
}
}
散列函數的三種設計方法
接下來我們將介紹三種散列函數的設計方法,他們分別是:
- 除法散列法
- 乘法散列法
- 全域散列法
除法散列法
除法散列法的定義,假設有一元素
比如
除法散列法注意事項:
在應用觸發散列法時,要避免m 的某些值,例如m 不應該爲2的冪,因爲如果m=2p ,則h(k) 就是k 的最低p 位數字(因爲我們除以2p 就是將k 右移p 位,那麼取餘就是k 的最低p 位數字),除非已經知道各種最低的p 位的排列形式是等可能的,否則在設計散列函數的時候,最好考慮關鍵字的所有位
乘法散列法
構造散列函數的乘法散列法包含了兩個步驟。第一步,用關鍵字
全域散列法
假設現在你寫了一款軟件,打算賣給一家公司,可是另外一個人也寫了一款可以實現和你相同功能的軟件,也打算賣給這家公司。於是公司就叫你們在看的見對方全部源碼的情況下爲對方寫測試用例,最後那個軟件運行速度快就買那個。假設雙方在代碼裏面都使用了散列表,並且都設計了對應的散列函數,那麼你的競爭對手就會針對你的散列函數寫一個將全部鍵值都映射到同一個槽的惡意測試用例,而你也會這樣寫一組針對對方散列函數的惡意測試用例。那你要怎麼才能在這場競爭中勝出呢?
沒錯,那就是隨機,你寫了若干個性能優良的散列函數,然後再運行的時候隨機在裏面選出一個散列函數進行映射,這樣你的競爭對手就無法寫出針對你的散列函數的惡意測試用例了
在前面我們講過的除法散列法
和乘法散列法
都是一個固定的散列函數,在全域散列法裏面,散列函數都是隨機的,不過這些隨機的散列函數可不是任意設計的。
設
H 是一組有限散列函數集合,它將給定的關鍵字全域U 映射到{0,1,2,3,...,m−1} ,這樣的一個函數稱爲全域的,如果對於每一對不同的關鍵字k,l∈U ,滿足h(k)=h(l) 的散列函數h∈H 的個數至多是|H|/m ,也就是說,從H 中選取一個散列函數h ,在k≠l 的情況下,h(k)=h(l) 的概率不大於1/m 。這也正好是從集合0,1,2,3,...,m−1 中獨立的隨機選取h(k) 和h(l) 發生衝突的概率。
那要怎麼設計一個全域散列函數類呢?
- 首先選取一個足夠大的素數
p ,使得每一個可能的關鍵字落到0 到p−1 的範圍內。設Zp={0,1,2,...,p−1} ,Z∗p={1,2,3,...,p−1} - 現在對於
a∈Z∗p ,b∈Zp ,定義散列函數hab .利用一次線性變換,進行模m 和模p 的歸約,有
hab(k)=((ak+b)modp))modm
於是構成了這樣的散列函數簇:
Hpm={hab:a∈Z∗p,b∈Zp}
定理:Hpm 是全域的
證明:考慮Zp 中兩個不同的關鍵字k 和l ,即k≠l ,對於某一個給定的散列函數hab ,設:
r=(ak+b)modps=(al+b)modp
上面兩式相減:
r−s≡a(k−l)modp
因爲p 是素數,且a modp≠0 和(k−l) modp≠0 的,所以a(k−l)modp≠0 ,即r≠s
當r 和s 爲隨機選取不同的值時,不同的關鍵字k 和l 發生衝突的概率爲r≡s(modp) 的概率,對於某個給定的r 值,s 的可能取值就是餘下的p−1 種,其中滿足s≠r 且s≡r(modm) 的s 值的數目至多是:
⌈p/m⌉−1≤((p+m−1)/m)−1=(p−1)/m
所以有:
Prhab(k)=hab(l)≤1/m