最近看到一篇有關 Perfect Hash 生成算法的文章,感受頗有必要寫篇文章推薦下:
http://ilan.schnell-web.net/p...。web
先解釋下什麼是 Perfect Hash:Perfect Hash 是這樣一種算法,能夠映射給定 N 個 keys 到 N 個不一樣的的數字
裏。因爲沒有 hash collision,這種 Hash 在查找時時間複雜度是真正的 O(1)
。外加一個「最小」前綴,則是要
求生成的 Perfect Hash 映射結果的上界儘量小。舉個例子,假設我有 100 個字符串,若是存在這樣的最小
Perfect Hash 算法,能夠把 100 個字符串一一映射到 0 ~99 個數裏,我就能用一個數組存儲所有的字符串,然
後在查找時先 hash 一下,取 hash 結果做爲下標即可知道給定字符串是否在這 100 個字符串裏。總時間複雜度
爲 O(n)
的 hash 過程 + O(1)
的查找,而所佔用的空間只是一個數組(外加一個圖 G,後面會講到)。算法
聽到前面的描述,你可能想到 trie (前綴樹)和相似的 AC 自動機算法。不過討論它們之間的優劣和應用場景不
是本文的主題(也許之後我有機會能夠寫一下)。本文的主題在於介紹一種生成最小 Perfect Hash 算法。數組
這種算法出自於一篇 1992 年的論文《An optimal algorithm for generating minimal perfect hash functions》。
算法的關鍵在於把判斷某個 hash 算法是否爲 perfect hash 算法的問題變成一個判斷圖是否無環的問題。
注意該算法最終生成的圖 G 在不一樣的運行次數裏大小可能不同,你可能須要多跑幾回結果生成多個 G,取其中最小者
。app
如下就是算法的步驟:函數
假設你有 K 個 keys,好比 apple
,boy
,cat
,dog
。測試
apple 0 boy 1 cat 2 dog 3
f1(x) = (x[0] + x[1] + x[2] + ...) % N f2(x) = (x[0] * x[1] * x[2] * ...) % N
之因此隨機選擇 hash 函數,是爲了讓每次生成的圖 G 不同,好找到一個最小的。.net
好比前面咱們挑的函數裏,f1(x) 和 f2(x) 的結果以下表:設計
key f1(x) f2(x) apple 2 0 boy 0 0 cat 0 0 dog 2 0
生成的圖是這樣的:code
2 --- apple ------ | | --- dog ---------0 -- boy - | | --- cat -
沿用前面的例子,當 N 爲 7 時,f1(x) 和 f2(x) 的結果以下表:字符串
key f1(x) f2(x) apple 5 0 boy 1 0 cat 4 3 dog 6 4
生成的圖是這樣的:
0 --- apple --- 5 | ---- boy --- 1 4 --- cat --- 3 | ---- dog --- 6
顯然上圖是無環的。接下來的工做,就是給各個節點賦值,確保同一條邊兩個節點的值的和爲該邊的 ID。
即 0 號節點的值 + 5 號節點的值爲 apple 的 ID 0。
咱們能夠每次選擇一個沒被賦值的節點,賦值爲 0,而後遍歷其相鄰節點,確保這些節點和隨機選擇的節點的值的
和爲該邊的 ID,直到全部節點都被賦值。這裏咱們假設隨機選取了 5 號節點和 3 號節點,賦值後的圖是這樣的:
0(0) --- apple --- 5(0) | ---- boy --- 1(1) 4(2) --- cat --- 3(0) | ---- dog --- 6(1)
如今圖 G 能夠這麼表示:
int G[7] = { 0, // 0 號節點值爲 0 1, 0, // 2 號節點沒有用到,能夠取任意值 0, 2, 0, 1 // 6 號節點值爲 1 }
最終獲得的最小 Perfect Hash 算法以下:
P(x) = (G[f1(x)] + G[f2(x)]) % N
# N = 7 key f1(x) f2(x) G[f1(x)] G[f2(x)] P(x) apple 5 0 0 0 0 boy 1 0 1 0 1 cat 4 3 2 0 2 dog 6 4 1 2 3
P(x)
返回的值正好是 key 的 ID,因此拿這個 ID 做爲 keys 的 offset 就能取出對應的 key 了。
注意,若是輸入 x 不必定是 keys 中的一個 key,則 P(x)
的算出來的 offset 取出來的 key 不必定匹配輸入
的 x。你須要匹配下x 和 key 兩個字符串。
關於圖 G,有兩點須要解釋下:
這個算法背後的數學原理並不深奧。
若是你能找到這樣的 P(key)
,令 P(key)
的結果剛好等於 key
在 keys
裏面的 offset,則 P(key)
必然是最小 Perfect Hash 算法。由於 keys[P(key)]
只能是 key
,不可能會有兩個結果;並且也找不到比
比 keys 的個數更小的 Perfect Hash 了,再小下去必然會有 hash collision。
若是咱們設計出這樣的一個圖 G,它有 K 條邊,每條邊對應一個 key,邊的兩端節點的和爲該邊(key)的 offset
,則 P(x) 就是先算得兩端節點的值,而後求和。兩端節點的值能夠經過隨機選取一個節點爲 0,而後給每一個相鄰
節點賦值的方式決定,前提是這個圖必須是無環的,不然一個節點就可能被賦予兩個值。因此咱們首先要檢查生成
出來的圖 G 是不是無環的。
你可能會問,爲何生成出來的 P(x) 是 (G[f1(x)] + G[f2(x)]) % N
,而不是 G[f1(x)] + G[f2(x)]
?我看
了原文裏面的代碼實現(就在本文開頭所給的連接裏),他在計算每一個節點值時,不容許值爲負數。好比節點 A 爲 5,
邊的 ID 爲 3,N 爲 7,則另外一端的節點 B 爲 9(而不是 -2)。之因此這麼作,是由於論文裏面說 G(x)
是一個映射x
到 [0,K]
的函數,而後 P(x) 裏面須要 % K
。而代碼裏則把 G(x)
實現成映射 x 到 [0,N]
的函數,順理
成章地後面就要 % N
了。
但其實若是咱們容許值爲負數,則 G[f1(x)] + G[f2(x)]
就能知足該算法背後的數學原理了。這麼改的好處在
於計算時能夠省掉一個相對昂貴的取餘操做。
我改動了下代碼實現,改動後的結果也能經過全部的測試(我另外還添了個 fuzzy test),因此這麼改應該沒有問題。