1 Hash function
1.1 Introduction
在解題甚至是一般應用中,我們常常需要執行下列這些操作:
1. 檢索 (ex. 詢問某元素是否在某集合內) 2. 比較 (ex. 比較兩字串是否相同)
3. 映射 (ex. 把一個集合 S 映射到 {1 · · · n})
我們先前分別已經學過了一些工具可以解決上述的幾個問題 (如二元搜尋樹和 Trie 用於檢索、KMP 用於比較等等),但在很多情況下如果元素的結構非常複雜,以上的 操作難度就會大幅增加。舉例來說,許多人常常用 BitTorrent 等 P2P 界面下載影片,
影片來源這麼多,到底要怎麼確定兩個巨大的影片檔案是不是同一個檔案呢?一個檔 案大小動輒數 GB,如果對於所有檔案都還要 O(n) 比較一下位元,將會非常耗費時 間。類似這樣的情形,我們通常會需要把原先複雜的資料型態轉換為較簡單的資料型 態,而轉換過程中使用的函數我們就稱為 hash function (雜湊函數),映射後的所有可 能形成的狀態空間則稱為 hash table。
對於一個資料型態 T ,一個好的雜湊函數 f 通常需要滿足以下條件:
1. 對於一個 T 型態的元素 x ,f (x) 為一個 T � 型態的元素,且比較兩 T � 型態元素 是否相等的複雜度低於比較 T 型態。
2. 對於兩個 T 型態的元素 x, y ,若 x = y ,則 f (x) = f (y) 。
3. 對於兩個 T 型態的元素 x, y ,若 x̸= y ,則 f(x) ̸= f(y) 成立的機率應盡量高。
注意到我們並不要求第三點的機率等於 1 ,因為將複雜的型態轉為簡單的型態往 往會丟失部份訊息,從而降低代表性。舉例而言,如果我們想要針對僅含有小寫字 母,長度為 L 的字串設計一個雜湊函數將之映射到 [0, 1000000) 內的整數,則原始輸 入資料有 26L 種可能,映射到的對象卻只有 1000000 種可能,根據鴿籠定理必定會有 許多元素映射到同一個整數。然而這並不影響雜湊函數的作用,至少雜湊函數已經減 少許多不必要的比較。
底下我們介紹一些雜湊函數設計的例子,注意到雜湊函數設計的方式有很多種,很 多時候優劣性也不容易直觀判斷,所以在實務上使用雜湊函數時很多時候是需要一些 經驗與靈感的。
例題 1
給定 C 語言中 int 範圍內的整數 k ,請設計雜湊函數 f 將 k 映射到 [0, M ) 內 的整數。
解答
令 f (k) = k mod M 。
這是一個很常見的問題,例如排列組合問題中,解答常常是個很大的數值,為了避 免大家要寫大數的困擾,我們當然會希望輸出會是一個範圍內的數值。最常見的作法 就是取除以 M 的餘數,而這顯然滿足雜湊函數的前兩個要求。而第三個要求效果是 否良好則仰賴於 M 的選擇,根據數學性質我們知道 M 為質數且數值較大時,映射到 同一個元素的情形會較不嚴重,雜湊函數的正確性較高。
例題 2
給定字串 s = s1· · · sn ,字元集合大小為 C ,試設計雜湊函數 f 將 s 映射到一 個整數。
解答
令 f (s) = ∑n
i=1
si× Cn−i 。
此函數在字串演算法中扮演著重要的角色,稱為 rolling hash 。該函數實際上等價 於把字串作為一種 C 進位的數字系統,除了雜湊函數的前兩要素都符合外,連第三個 要素都能滿足機率為 1,理論上是個絕佳的雜湊函數。然而由於映射過後得到的整數 往往非常龐大,實務上我們往往還是需要如例題 1 一樣,將運算結果除以 M 取餘數,
導致雜湊函數的精確度瓶頸仰賴於設計者選取的質數 M 。 例題 3
給定 k 個長度為 n 的序列 A1· · · Ak,每次詢問其中兩序列是否為相同的集合 (即包含的元素集合完全相同)。
解答
令 f (A) = A[1]⊕ A[2] ⊕ · · · ⊕ A[n] 。(⊕ 表示布林運算的 XOR)
如果我們直接每次都比較兩個序列,那麼複雜度是 O(n)。由於我們想知道的是元 素組成是否相同,元素的順序並不重要,因此可以設計如上的雜湊函數 f ,並且先
O(kn) 預處理所有序列的 f 函數,之後對於每組詢問直接檢查兩序列的 f 函數值是否
相等即可,詢問複雜度為 O(1)。當然,這犧牲了一些正確性。
例題 4
給定 k 個長度為 n 的序列 A1· · · Ak,每次詢問其中兩序列是否為相同的序列 (即元素順序也須完全相同)。
解答
接續例題 3,先令序列 B ={A[1], A[2] − A[1], · · · , A[n] − A[n − 1]},即 A 序列 中相鄰兩數的差,再令雜湊函數 g(A) = f (A) + f (B)。
相較於上題,本題的要求更嚴格,連元素之間的順序關係也是需要考慮的部份。一 個解決的辦法是把前一題的想法強化,先定義序列 B ,再將兩序列的 f 函數值相加。
如此一來如果兩序列 A1, A2 元素組成相同但順序不同,則 f (A1)和 f (A2) 會相等,但 f (B1) 和 f (B2) 很可能不相等,導致 g(A1) ̸= g(A2) 的機率較高,達到雜湊函數的需 求。
1.2 Conflict
在上一小節中,我們已經提到 hash function 的侷限性:由於丟失部份資料,且必 須滿足映射到的型態較原型態更易操作,難免會有兩個不同的元素在經過轉換後變成 相同的元素。我們稱這種情形為「碰撞」(hash conflict)。為了避免碰撞造成的誤判,
我們最常見的處理方式有如下四種:
1. 視而不見:
總是假設碰撞不存在。無視並不代表消極處理,而是代表我們沒必要或者沒辦法 處理此時的碰撞情形。舉例而言,前面提到的 P2P 檔案辨識問題,如果兩個檔 案經過 hash function 得到的驗證碼是相同的,理論上並不代表兩個檔案就是相 同的檔案,但在實務上我們會如此默認,因為確認的成本過於高昂且發生的機率 過低。
2. 閉合雜湊 (closed hashing):
如果把映射後的結果看成座位,則碰撞可以看成是「有人搶了自己的座位」。這 時候閉合雜湊的解決方法是,如果真的在自己座位上的人不是自己 (即碰撞確實 成立),那麼當前元素就按照某些特定的規則再去搶別人的空位。這樣做的好處 是使用的空間量較固定,壞處則是好的搶奪規則不好設計,且在碰撞嚴重時效率 不佳。
3. 開放雜湊 (open hashing):
相較於閉合雜湊,開放雜湊在遭遇碰撞時的處理方式是「既然命運決定我們同個 位子,那只好擠一擠了」。開放雜湊的實做方式是在 hash table 的每個狀態裡都 維護一個 list,記錄所有同屬此狀態的相異元素。如果有某個元素已知映射到某 個狀態 S ,則為了避免誤判,我們可以進一步檢查 S 內 list 中的所有相異元素 來確定元素是否相等。由於實做相對方便,且可配合諸多優化方式 (如將 list 改 為二元平衡樹),在解題實務上我們相較於閉合雜湊更常使用開放雜湊。
4. 多重雜湊:
一個便當吃不飽,可以吃兩個;一個雜湊函數不夠準確,我們當然也可以用兩 個!假如兩個雜湊函數 f, g 互相獨立,f 的誤判率為 p1 ,g 的誤判率為 p2 ,則 兩者同時誤判的機率就大幅下降為 p1× p2。類似地,若有更多彼此獨立的雜湊 函數,則誤判的機率為各個雜湊函數誤判率的乘積。多重雜湊的好處在於誤判率 足夠低後,我們甚至可以略去判斷碰撞的過程 (即視而不見),壞處則為運算常 數增加,且設計許多獨立的雜湊函數並不容易。
※ 例題 4 實際上就是多重雜湊的一個應用,只是為了方便起見將兩個雜湊函數 直接相加起來。
雜湊函數本身並不穩定,相較於其他資料結構效能分析也更困難,因此一開始會不 容易掌握;但在平均效率上,雜湊函數卻有相當驚人的表現,實務上也隨處可見。希 望大家可以不要太過害怕其不穩定性,多多研究與嘗試,累積一些經驗後將會成為好 使又強力的工具。
習題
1. 請依照需求設計雜湊函數。請注意,以下題目並沒有標準答案,也不需要太在意運 算的時間複雜度,但是請大家盡量避免碰撞,例如令 ∀x, f(x) = 1 是不會得到任何 分數的 (笑)。
(a) (15 pts) 請設計一個雜湊函數 f 將一個簡單凸多邊形 P = {(x1, y1),· · · , (xn, yn)} 映射到一個整數,以判斷兩多邊形是否全等 (即在平面上經過平移或翻轉後兩 者完全疊合),其中 xi、yi 皆為整數。另外,請保證 P1, P2 映射到不同的整數,
其中 P1 = {(0, 0), (5, 0), (5, 5), (0, 5)}, P2 = {(0, 0), (5, 0), (8, 4), (3, 4)} ,否則只 能得到部份分數。
※ 簡單凸多邊形:內角小於等於 180 度,且除了相鄰的邊相交於頂點之外,所 有邊彼此不相交。
(b) (15 pts) 請設計一個雜湊函數 f 將一個簡單凸多邊形 P = {(x1, y1),· · · , (xn, yn)} 映射到一個整數,以判斷兩多邊形是否相似 (即在平面上經過平移、翻轉或縮 放後兩者完全疊合),其中 xi、yi 皆為整數。另外,請保證 P1, P2 映射到不同 的整數,其中 P1 ={(0, 0), (5, 0), (5, 5), (0, 5)}, P2 ={(0, 0), (5, 0), (8, 4), (3, 4)} , 否則只能得到部份分數。
(c) (15 pts) 我們說兩棵 (節點和邊皆沒有權重的) 二元樹 T1, T2 「同構」(T1 = T2),
若且唯若 T1, T2 滿足以下條件其中之一:
i. T1 = NULL 且 T2 = NULL
ii. T1 ̸= NULL 且 T2 ̸= NULL 且 T1.lef t = T2.lef t 且 T1.right = T2.right 請設計一個雜湊函數 f 將一棵二元樹映射到一個整數,以判斷兩棵二元樹是否 同構。
2. 在例題中,我們提到一個字串 s = s1s2· · · sn 的 rolling hash 為 H(s) =
∑n i=1
si× Cn−i mod M 其中 C 為字元集合大小,M 為設計者選擇的一個常數。
(a) (10 pts) 請描述如何在 O(n) 的時間內得到 H(s) 的值。
(b) (15 pts) 令字元集合為英文小寫字母,a = 0, b = 1,· · · , z = 25,C = 26,M = 1000007。請列出三個互不相同,但長度相等的字串 s1, s2, s3,滿足 H(s1) = H(s2) = H(s3)。(你可能會想要寫點 code。)
(c) (15 pts) 令 s[l : r] = slsl+1· · · sr 為 s 的一個長度為 k = (r− l + 1) 的子字串。
已知 s[l : r] 的 rolling hash 值 H(s[l : r]) = x ,且 H(s[l + 1 : r + 1]) = y ,請 以 x, si, k, C, M 表示 y 。(以數學形式表達即可,不需要考慮程式上的細節) (d) (15 pts) 給定兩個長度分別為 n 和 m (n ≤ m) 的字串 s, t ,請描述如何使用
rolling hash 在 O(n + m) 的時間內,判斷 s 是否為 t 的子字串。為了方便起見,
可以假設 hash value 不會發生碰撞。