--- comments: true --- # 6.3 雜湊演算法 前兩節介紹了雜湊表的工作原理和雜湊衝突的處理方法。然而無論是開放定址還是鏈式位址,**它們只能保證雜湊表可以在發生衝突時正常工作,而無法減少雜湊衝突的發生**。 如果雜湊衝突過於頻繁,雜湊表的效能則會急劇劣化。如圖 6-8 所示,對於鏈式位址雜湊表,理想情況下鍵值對均勻分佈在各個桶中,達到最佳查詢效率;最差情況下所有鍵值對都儲存到同一個桶中,時間複雜度退化至 $O(n)$ 。 ![雜湊衝突的最佳情況與最差情況](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" }
圖 6-8 雜湊衝突的最佳情況與最差情況
**鍵值對的分佈情況由雜湊函式決定**。回憶雜湊函式的計算步驟,先計算雜湊值,再對陣列長度取模: ```shell index = hash(key) % capacity ``` 觀察以上公式,當雜湊表容量 `capacity` 固定時,**雜湊演算法 `hash()` 決定了輸出值**,進而決定了鍵值對在雜湊表中的分佈情況。 這意味著,為了降低雜湊衝突的發生機率,我們應當將注意力集中在雜湊演算法 `hash()` 的設計上。 ## 6.3.1 雜湊演算法的目標 為了實現“既快又穩”的雜湊表資料結構,雜湊演算法應具備以下特點。 - **確定性**:對於相同的輸入,雜湊演算法應始終產生相同的輸出。這樣才能確保雜湊表是可靠的。 - **效率高**:計算雜湊值的過程應該足夠快。計算開銷越小,雜湊表的實用性越高。 - **均勻分佈**:雜湊演算法應使得鍵值對均勻分佈在雜湊表中。分佈越均勻,雜湊衝突的機率就越低。 實際上,雜湊演算法除了可以用於實現雜湊表,還廣泛應用於其他領域中。 - **密碼儲存**:為了保護使用者密碼的安全,系統通常不會直接儲存使用者的明文密碼,而是儲存密碼的雜湊值。當用戶輸入密碼時,系統會對輸入的密碼計算雜湊值,然後與儲存的雜湊值進行比較。如果兩者匹配,那麼密碼就被視為正確。 - **資料完整性檢查**:資料傳送方可以計算資料的雜湊值並將其一同傳送;接收方可以重新計算接收到的資料的雜湊值,並與接收到的雜湊值進行比較。如果兩者匹配,那麼資料就被視為完整。 對於密碼學的相關應用,為了防止從雜湊值推導出原始密碼等逆向工程,雜湊演算法需要具備更高等級的安全特性。 - **單向性**:無法透過雜湊值反推出關於輸入資料的任何資訊。 - **抗碰撞性**:應當極難找到兩個不同的輸入,使得它們的雜湊值相同。 - **雪崩效應**:輸入的微小變化應當導致輸出的顯著且不可預測的變化。 請注意,**“均勻分佈”與“抗碰撞性”是兩個獨立的概念**,滿足均勻分佈不一定滿足抗碰撞性。例如,在隨機輸入 `key` 下,雜湊函式 `key % 100` 可以產生均勻分佈的輸出。然而該雜湊演算法過於簡單,所有後兩位相等的 `key` 的輸出都相同,因此我們可以很容易地從雜湊值反推出可用的 `key` ,從而破解密碼。 ## 6.3.2 雜湊演算法的設計 雜湊演算法的設計是一個需要考慮許多因素的複雜問題。然而對於某些要求不高的場景,我們也能設計一些簡單的雜湊演算法。 - **加法雜湊**:對輸入的每個字元的 ASCII 碼進行相加,將得到的總和作為雜湊值。 - **乘法雜湊**:利用乘法的不相關性,每輪乘以一個常數,將各個字元的 ASCII 碼累積到雜湊值中。 - **互斥或雜湊**:將輸入資料的每個元素透過互斥或操作累積到一個雜湊值中。 - **旋轉雜湊**:將每個字元的 ASCII 碼累積到一個雜湊值中,每次累積之前都會對雜湊值進行旋轉操作。 === "Python" ```python title="simple_hash.py" def add_hash(key: str) -> int: """加法雜湊""" hash = 0 modulus = 1000000007 for c in key: hash += ord(c) return hash % modulus def mul_hash(key: str) -> int: """乘法雜湊""" hash = 0 modulus = 1000000007 for c in key: hash = 31 * hash + ord(c) return hash % modulus def xor_hash(key: str) -> int: """互斥或雜湊""" hash = 0 modulus = 1000000007 for c in key: hash ^= ord(c) return hash % modulus def rot_hash(key: str) -> int: """旋轉雜湊""" hash = 0 modulus = 1000000007 for c in key: hash = (hash << 4) ^ (hash >> 28) ^ ord(c) return hash % modulus ``` === "C++" ```cpp title="simple_hash.cpp" /* 加法雜湊 */ int addHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = (hash + (int)c) % MODULUS; } return (int)hash; } /* 乘法雜湊 */ int mulHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = (31 * hash + (int)c) % MODULUS; } return (int)hash; } /* 互斥或雜湊 */ int xorHash(string key) { int hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash ^= (int)c; } return hash & MODULUS; } /* 旋轉雜湊 */ int rotHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS; } return (int)hash; } ``` === "Java" ```java title="simple_hash.java" /* 加法雜湊 */ int addHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = (hash + (int) c) % MODULUS; } return (int) hash; } /* 乘法雜湊 */ int mulHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = (31 * hash + (int) c) % MODULUS; } return (int) hash; } /* 互斥或雜湊 */ int xorHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash ^= (int) c; } return hash & MODULUS; } /* 旋轉雜湊 */ int rotHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS; } return (int) hash; } ``` === "C#" ```csharp title="simple_hash.cs" /* 加法雜湊 */ int AddHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = (hash + c) % MODULUS; } return (int)hash; } /* 乘法雜湊 */ int MulHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = (31 * hash + c) % MODULUS; } return (int)hash; } /* 互斥或雜湊 */ int XorHash(string key) { int hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash ^= c; } return hash & MODULUS; } /* 旋轉雜湊 */ int RotHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS; } return (int)hash; } ``` === "Go" ```go title="simple_hash.go" /* 加法雜湊 */ func addHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = (hash + int64(b)) % modulus } return int(hash) } /* 乘法雜湊 */ func mulHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = (31*hash + int64(b)) % modulus } return int(hash) } /* 互斥或雜湊 */ func xorHash(key string) int { hash := 0 modulus := 1000000007 for _, b := range []byte(key) { fmt.Println(int(b)) hash ^= int(b) hash = (31*hash + int(b)) % modulus } return hash & modulus } /* 旋轉雜湊 */ func rotHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus } return int(hash) } ``` === "Swift" ```swift title="simple_hash.swift" /* 加法雜湊 */ func addHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = (hash + Int(scalar.value)) % MODULUS } } return hash } /* 乘法雜湊 */ func mulHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = (31 * hash + Int(scalar.value)) % MODULUS } } return hash } /* 互斥或雜湊 */ func xorHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash ^= Int(scalar.value) } } return hash & MODULUS } /* 旋轉雜湊 */ func rotHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS } } return hash } ``` === "JS" ```javascript title="simple_hash.js" /* 加法雜湊 */ function addHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* 乘法雜湊 */ function mulHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (31 * hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* 互斥或雜湊 */ function xorHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash ^= c.charCodeAt(0); } return hash & MODULUS; } /* 旋轉雜湊 */ function rotHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; } return hash; } ``` === "TS" ```typescript title="simple_hash.ts" /* 加法雜湊 */ function addHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* 乘法雜湊 */ function mulHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (31 * hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* 互斥或雜湊 */ function xorHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash ^= c.charCodeAt(0); } return hash & MODULUS; } /* 旋轉雜湊 */ function rotHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; } return hash; } ``` === "Dart" ```dart title="simple_hash.dart" /* 加法雜湊 */ int addHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = (hash + key.codeUnitAt(i)) % MODULUS; } return hash; } /* 乘法雜湊 */ int mulHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = (31 * hash + key.codeUnitAt(i)) % MODULUS; } return hash; } /* 互斥或雜湊 */ int xorHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash ^= key.codeUnitAt(i); } return hash & MODULUS; } /* 旋轉雜湊 */ int rotHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS; } return hash; } ``` === "Rust" ```rust title="simple_hash.rs" /* 加法雜湊 */ fn add_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = (hash + c as i64) % MODULUS; } hash as i32 } /* 乘法雜湊 */ fn mul_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = (31 * hash + c as i64) % MODULUS; } hash as i32 } /* 互斥或雜湊 */ fn xor_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash ^= c as i64; } (hash & MODULUS) as i32 } /* 旋轉雜湊 */ fn rot_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS; } hash as i32 } ``` === "C" ```c title="simple_hash.c" /* 加法雜湊 */ int addHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = (hash + (unsigned char)key[i]) % MODULUS; } return (int)hash; } /* 乘法雜湊 */ int mulHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = (31 * hash + (unsigned char)key[i]) % MODULUS; } return (int)hash; } /* 互斥或雜湊 */ int xorHash(char *key) { int hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash ^= (unsigned char)key[i]; } return hash & MODULUS; } /* 旋轉雜湊 */ int rotHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS; } return (int)hash; } ``` === "Kotlin" ```kotlin title="simple_hash.kt" /* 加法雜湊 */ fun addHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = (hash + c.code) % MODULUS } return hash.toInt() } /* 乘法雜湊 */ fun mulHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = (31 * hash + c.code) % MODULUS } return hash.toInt() } /* 互斥或雜湊 */ fun xorHash(key: String): Int { var hash = 0 val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = hash xor c.code } return hash and MODULUS } /* 旋轉雜湊 */ fun rotHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS } return hash.toInt() } ``` === "Ruby" ```ruby title="simple_hash.rb" [class]{}-[func]{add_hash} [class]{}-[func]{mul_hash} [class]{}-[func]{xor_hash} [class]{}-[func]{rot_hash} ``` === "Zig" ```zig title="simple_hash.zig" [class]{}-[func]{addHash} [class]{}-[func]{mulHash} [class]{}-[func]{xorHash} [class]{}-[func]{rotHash} ``` ??? pythontutor "視覺化執行" 觀察發現,每種雜湊演算法的最後一步都是對大質數 $1000000007$ 取模,以確保雜湊值在合適的範圍內。值得思考的是,為什麼要強調對質數取模,或者說對合數取模的弊端是什麼?這是一個有趣的問題。 先丟擲結論:**使用大質數作為模數,可以最大化地保證雜湊值的均勻分佈**。因為質數不與其他數字存在公約數,可以減少因取模操作而產生的週期性模式,從而避免雜湊衝突。 舉個例子,假設我們選擇合數 $9$ 作為模數,它可以被 $3$ 整除,那麼所有可以被 $3$ 整除的 `key` 都會被對映到 $0$、$3$、$6$ 這三個雜湊值。 $$ \begin{aligned} \text{modulus} & = 9 \newline \text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline \text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \} \end{aligned} $$ 如果輸入 `key` 恰好滿足這種等差數列的資料分佈,那麼雜湊值就會出現聚堆積,從而加重雜湊衝突。現在,假設將 `modulus` 替換為質數 $13$ ,由於 `key` 和 `modulus` 之間不存在公約數,因此輸出的雜湊值的均勻性會明顯提升。 $$ \begin{aligned} \text{modulus} & = 13 \newline \text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline \text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \} \end{aligned} $$ 值得說明的是,如果能夠保證 `key` 是隨機均勻分佈的,那麼選擇質數或者合數作為模數都可以,它們都能輸出均勻分佈的雜湊值。而當 `key` 的分佈存在某種週期性時,對合數取模更容易出現聚集現象。 總而言之,我們通常選取質數作為模數,並且這個質數最好足夠大,以儘可能消除週期性模式,提升雜湊演算法的穩健性。 ## 6.3.3 常見雜湊演算法 不難發現,以上介紹的簡單雜湊演算法都比較“脆弱”,遠遠沒有達到雜湊演算法的設計目標。例如,由於加法和互斥或滿足交換律,因此加法雜湊和互斥或雜湊無法區分內容相同但順序不同的字串,這可能會加劇雜湊衝突,並引起一些安全問題。 在實際中,我們通常會用一些標準雜湊演算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它們可以將任意長度的輸入資料對映到恆定長度的雜湊值。 近一個世紀以來,雜湊演算法處在不斷升級與最佳化的過程中。一部分研究人員努力提升雜湊演算法的效能,另一部分研究人員和駭客則致力於尋找雜湊演算法的安全性問題。表 6-2 展示了在實際應用中常見的雜湊演算法。 - MD5 和 SHA-1 已多次被成功攻擊,因此它們被各類安全應用棄用。 - SHA-2 系列中的 SHA-256 是最安全的雜湊演算法之一,仍未出現成功的攻擊案例,因此常用在各類安全應用與協議中。 - SHA-3 相較 SHA-2 的實現開銷更低、計算效率更高,但目前使用覆蓋度不如 SHA-2 系列。表 6-2 常見的雜湊演算法