feat: Traditional Chinese version (#1163)

* First commit

* Update mkdocs.yml

* Translate all the docs to traditional Chinese

* Translate the code files.

* Translate the docker file

* Fix mkdocs.yml

* Translate all the figures from SC to TC

* 二叉搜尋樹 -> 二元搜尋樹

* Update terminology.

* Update terminology

* 构造函数/构造方法 -> 建構子
异或 -> 互斥或

* 擴充套件 -> 擴展

* constant - 常量 - 常數

* 類	-> 類別

* AVL -> AVL 樹

* 數組 -> 陣列

* 係統 -> 系統
斐波那契數列 -> 費波那契數列
運算元量 -> 運算量
引數 -> 參數

* 聯絡 -> 關聯

* 麵試 -> 面試

* 面向物件 -> 物件導向
歸併排序 -> 合併排序
范式 -> 範式

* Fix 算法 -> 演算法

* 錶示 -> 表示
反碼 -> 一補數
補碼 -> 二補數
列列尾部 -> 佇列尾部
區域性性 -> 區域性
一摞 -> 一疊

* Synchronize with main branch

* 賬號 -> 帳號
推匯 -> 推導

* Sync with main branch

* First commit

* Update mkdocs.yml

* Translate all the docs to traditional Chinese

* Translate the code files.

* Translate the docker file

* Fix mkdocs.yml

* Translate all the figures from SC to TC

* 二叉搜尋樹 -> 二元搜尋樹

* Update terminology

* 构造函数/构造方法 -> 建構子
异或 -> 互斥或

* 擴充套件 -> 擴展

* constant - 常量 - 常數

* 類	-> 類別

* AVL -> AVL 樹

* 數組 -> 陣列

* 係統 -> 系統
斐波那契數列 -> 費波那契數列
運算元量 -> 運算量
引數 -> 參數

* 聯絡 -> 關聯

* 麵試 -> 面試

* 面向物件 -> 物件導向
歸併排序 -> 合併排序
范式 -> 範式

* Fix 算法 -> 演算法

* 錶示 -> 表示
反碼 -> 一補數
補碼 -> 二補數
列列尾部 -> 佇列尾部
區域性性 -> 區域性
一摞 -> 一疊

* Synchronize with main branch

* 賬號 -> 帳號
推匯 -> 推導

* Sync with main branch

* Update terminology.md

* 操作数量(num. of operations)-> 操作數量

* 字首和->前綴和

* Update figures

* 歸 -> 迴
記憶體洩漏 -> 記憶體流失

* Fix the bug of the file filter

* 支援 -> 支持
Add zh-Hant/README.md

* Add the zh-Hant chapter covers.
Bug fixes.

* 外掛 -> 擴充功能

* Add the landing page for zh-Hant version

* Unify the font of the chapter covers for the zh, en, and zh-Hant version

* Move zh-Hant/ to zh-hant/

* Translate terminology.md to traditional Chinese
This commit is contained in:
Yudong Jin
2024-04-06 02:30:11 +08:00
committed by GitHub
parent 33d7f8a2e5
commit 5f7385c8a3
1875 changed files with 102923 additions and 18 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -0,0 +1,394 @@
# 雜湊演算法
前兩節介紹了雜湊表的工作原理和雜湊衝突的處理方法。然而無論是開放定址還是鏈式位址,**它們只能保證雜湊表可以在發生衝突時正常工作,而無法減少雜湊衝突的發生**。
如果雜湊衝突過於頻繁,雜湊表的效能則會急劇劣化。如下圖所示,對於鏈式位址雜湊表,理想情況下鍵值對均勻分佈在各個桶中,達到最佳查詢效率;最差情況下所有鍵值對都儲存到同一個桶中,時間複雜度退化至 $O(n)$ 。
![雜湊衝突的最佳情況與最差情況](hash_algorithm.assets/hash_collision_best_worst_condition.png)
**鍵值對的分佈情況由雜湊函式決定**。回憶雜湊函式的計算步驟,先計算雜湊值,再對陣列長度取模:
```shell
index = hash(key) % capacity
```
觀察以上公式,當雜湊表容量 `capacity` 固定時,**雜湊演算法 `hash()` 決定了輸出值**,進而決定了鍵值對在雜湊表中的分佈情況。
這意味著,為了降低雜湊衝突的發生機率,我們應當將注意力集中在雜湊演算法 `hash()` 的設計上。
## 雜湊演算法的目標
為了實現“既快又穩”的雜湊表資料結構,雜湊演算法應具備以下特點。
- **確定性**:對於相同的輸入,雜湊演算法應始終產生相同的輸出。這樣才能確保雜湊表是可靠的。
- **效率高**:計算雜湊值的過程應該足夠快。計算開銷越小,雜湊表的實用性越高。
- **均勻分佈**:雜湊演算法應使得鍵值對均勻分佈在雜湊表中。分佈越均勻,雜湊衝突的機率就越低。
實際上,雜湊演算法除了可以用於實現雜湊表,還廣泛應用於其他領域中。
- **密碼儲存**:為了保護使用者密碼的安全,系統通常不會直接儲存使用者的明文密碼,而是儲存密碼的雜湊值。當用戶輸入密碼時,系統會對輸入的密碼計算雜湊值,然後與儲存的雜湊值進行比較。如果兩者匹配,那麼密碼就被視為正確。
- **資料完整性檢查**:資料傳送方可以計算資料的雜湊值並將其一同傳送;接收方可以重新計算接收到的資料的雜湊值,並與接收到的雜湊值進行比較。如果兩者匹配,那麼資料就被視為完整。
對於密碼學的相關應用,為了防止從雜湊值推導出原始密碼等逆向工程,雜湊演算法需要具備更高等級的安全特性。
- **單向性**:無法透過雜湊值反推出關於輸入資料的任何資訊。
- **抗碰撞性**:應當極難找到兩個不同的輸入,使得它們的雜湊值相同。
- **雪崩效應**:輸入的微小變化應當導致輸出的顯著且不可預測的變化。
請注意,**“均勻分佈”與“抗碰撞性”是兩個獨立的概念**,滿足均勻分佈不一定滿足抗碰撞性。例如,在隨機輸入 `key` 下,雜湊函式 `key % 100` 可以產生均勻分佈的輸出。然而該雜湊演算法過於簡單,所有後兩位相等的 `key` 的輸出都相同,因此我們可以很容易地從雜湊值反推出可用的 `key` ,從而破解密碼。
## 雜湊演算法的設計
雜湊演算法的設計是一個需要考慮許多因素的複雜問題。然而對於某些要求不高的場景,我們也能設計一些簡單的雜湊演算法。
- **加法雜湊**:對輸入的每個字元的 ASCII 碼進行相加,將得到的總和作為雜湊值。
- **乘法雜湊**:利用乘法的不相關性,每輪乘以一個常數,將各個字元的 ASCII 碼累積到雜湊值中。
- **互斥或雜湊**:將輸入資料的每個元素透過互斥或操作累積到一個雜湊值中。
- **旋轉雜湊**:將每個字元的 ASCII 碼累積到一個雜湊值中,每次累積之前都會對雜湊值進行旋轉操作。
```src
[file]{simple_hash}-[class]{}-[func]{rot_hash}
```
觀察發現,每種雜湊演算法的最後一步都是對大質數 $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` 的分佈存在某種週期性時,對合數取模更容易出現聚集現象。
總而言之,我們通常選取質數作為模數,並且這個質數最好足夠大,以儘可能消除週期性模式,提升雜湊演算法的穩健性。
## 常見雜湊演算法
不難發現,以上介紹的簡單雜湊演算法都比較“脆弱”,遠遠沒有達到雜湊演算法的設計目標。例如,由於加法和互斥或滿足交換律,因此加法雜湊和互斥或雜湊無法區分內容相同但順序不同的字串,這可能會加劇雜湊衝突,並引起一些安全問題。
在實際中,我們通常會用一些標準雜湊演算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它們可以將任意長度的輸入資料對映到恆定長度的雜湊值。
近一個世紀以來,雜湊演算法處在不斷升級與最佳化的過程中。一部分研究人員努力提升雜湊演算法的效能,另一部分研究人員和駭客則致力於尋找雜湊演算法的安全性問題。下表展示了在實際應用中常見的雜湊演算法。
- MD5 和 SHA-1 已多次被成功攻擊,因此它們被各類安全應用棄用。
- SHA-2 系列中的 SHA-256 是最安全的雜湊演算法之一,仍未出現成功的攻擊案例,因此常用在各類安全應用與協議中。
- SHA-3 相較 SHA-2 的實現開銷更低、計算效率更高,但目前使用覆蓋度不如 SHA-2 系列。
<p align="center"> 表 <id> &nbsp; 常見的雜湊演算法 </p>
| | MD5 | SHA-1 | SHA-2 | SHA-3 |
| -------- | ------------------------------ | ---------------- | ---------------------------- | ------------------- |
| 推出時間 | 1992 | 1995 | 2002 | 2008 |
| 輸出長度 | 128 bit | 160 bit | 256/512 bit | 224/256/384/512 bit |
| 雜湊衝突 | 較多 | 較多 | 很少 | 很少 |
| 安全等級 | 低,已被成功攻擊 | 低,已被成功攻擊 | 高 | 高 |
| 應用 | 已被棄用,仍用於資料完整性檢查 | 已被棄用 | 加密貨幣交易驗證、數字簽名等 | 可用於替代 SHA-2 |
## 資料結構的雜湊值
我們知道,雜湊表的 `key` 可以是整數、小數或字串等資料型別。程式語言通常會為這些資料型別提供內建的雜湊演算法,用於計算雜湊表中的桶索引。以 Python 為例,我們可以呼叫 `hash()` 函式來計算各種資料型別的雜湊值。
- 整數和布林量的雜湊值就是其本身。
- 浮點數和字串的雜湊值計算較為複雜,有興趣的讀者請自行學習。
- 元組的雜湊值是對其中每一個元素進行雜湊,然後將這些雜湊值組合起來,得到單一的雜湊值。
- 物件的雜湊值基於其記憶體位址生成。透過重寫物件的雜湊方法,可實現基於內容生成雜湊值。
!!! tip
請注意,不同程式語言的內建雜湊值計算函式的定義和方法不同。
=== "Python"
```python title="built_in_hash.py"
num = 3
hash_num = hash(num)
# 整數 3 的雜湊值為 3
bol = True
hash_bol = hash(bol)
# 布林量 True 的雜湊值為 1
dec = 3.14159
hash_dec = hash(dec)
# 小數 3.14159 的雜湊值為 326484311674566659
str = "Hello 演算法"
hash_str = hash(str)
# 字串“Hello 演算法”的雜湊值為 4617003410720528961
tup = (12836, "小哈")
hash_tup = hash(tup)
# 元組 (12836, '小哈') 的雜湊值為 1029005403108185979
obj = ListNode(0)
hash_obj = hash(obj)
# 節點物件 <ListNode object at 0x1058fd810> 的雜湊值為 274267521
```
=== "C++"
```cpp title="built_in_hash.cpp"
int num = 3;
size_t hashNum = hash<int>()(num);
// 整數 3 的雜湊值為 3
bool bol = true;
size_t hashBol = hash<bool>()(bol);
// 布林量 1 的雜湊值為 1
double dec = 3.14159;
size_t hashDec = hash<double>()(dec);
// 小數 3.14159 的雜湊值為 4614256650576692846
string str = "Hello 演算法";
size_t hashStr = hash<string>()(str);
// 字串“Hello 演算法”的雜湊值為 15466937326284535026
// 在 C++ 中,內建 std:hash() 僅提供基本資料型別的雜湊值計算
// 陣列、物件的雜湊值計算需要自行實現
```
=== "Java"
```java title="built_in_hash.java"
int num = 3;
int hashNum = Integer.hashCode(num);
// 整數 3 的雜湊值為 3
boolean bol = true;
int hashBol = Boolean.hashCode(bol);
// 布林量 true 的雜湊值為 1231
double dec = 3.14159;
int hashDec = Double.hashCode(dec);
// 小數 3.14159 的雜湊值為 -1340954729
String str = "Hello 演算法";
int hashStr = str.hashCode();
// 字串“Hello 演算法”的雜湊值為 -727081396
Object[] arr = { 12836, "小哈" };
int hashTup = Arrays.hashCode(arr);
// 陣列 [12836, 小哈] 的雜湊值為 1151158
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode();
// 節點物件 utils.ListNode@7dc5e7b4 的雜湊值為 2110121908
```
=== "C#"
```csharp title="built_in_hash.cs"
int num = 3;
int hashNum = num.GetHashCode();
// 整數 3 的雜湊值為 3;
bool bol = true;
int hashBol = bol.GetHashCode();
// 布林量 true 的雜湊值為 1;
double dec = 3.14159;
int hashDec = dec.GetHashCode();
// 小數 3.14159 的雜湊值為 -1340954729;
string str = "Hello 演算法";
int hashStr = str.GetHashCode();
// 字串“Hello 演算法”的雜湊值為 -586107568;
object[] arr = [12836, "小哈"];
int hashTup = arr.GetHashCode();
// 陣列 [12836, 小哈] 的雜湊值為 42931033;
ListNode obj = new(0);
int hashObj = obj.GetHashCode();
// 節點物件 0 的雜湊值為 39053774;
```
=== "Go"
```go title="built_in_hash.go"
// Go 未提供內建 hash code 函式
```
=== "Swift"
```swift title="built_in_hash.swift"
let num = 3
let hashNum = num.hashValue
// 整數 3 的雜湊值為 9047044699613009734
let bol = true
let hashBol = bol.hashValue
// 布林量 true 的雜湊值為 -4431640247352757451
let dec = 3.14159
let hashDec = dec.hashValue
// 小數 3.14159 的雜湊值為 -2465384235396674631
let str = "Hello 演算法"
let hashStr = str.hashValue
// 字串“Hello 演算法”的雜湊值為 -7850626797806988787
let arr = [AnyHashable(12836), AnyHashable("小哈")]
let hashTup = arr.hashValue
// 陣列 [AnyHashable(12836), AnyHashable("小哈")] 的雜湊值為 -2308633508154532996
let obj = ListNode(x: 0)
let hashObj = obj.hashValue
// 節點物件 utils.ListNode 的雜湊值為 -2434780518035996159
```
=== "JS"
```javascript title="built_in_hash.js"
// JavaScript 未提供內建 hash code 函式
```
=== "TS"
```typescript title="built_in_hash.ts"
// TypeScript 未提供內建 hash code 函式
```
=== "Dart"
```dart title="built_in_hash.dart"
int num = 3;
int hashNum = num.hashCode;
// 整數 3 的雜湊值為 34803
bool bol = true;
int hashBol = bol.hashCode;
// 布林值 true 的雜湊值為 1231
double dec = 3.14159;
int hashDec = dec.hashCode;
// 小數 3.14159 的雜湊值為 2570631074981783
String str = "Hello 演算法";
int hashStr = str.hashCode;
// 字串“Hello 演算法”的雜湊值為 468167534
List arr = [12836, "小哈"];
int hashArr = arr.hashCode;
// 陣列 [12836, 小哈] 的雜湊值為 976512528
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode;
// 節點物件 Instance of 'ListNode' 的雜湊值為 1033450432
```
=== "Rust"
```rust title="built_in_hash.rs"
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let num = 3;
let mut num_hasher = DefaultHasher::new();
num.hash(&mut num_hasher);
let hash_num = num_hasher.finish();
// 整數 3 的雜湊值為 568126464209439262
let bol = true;
let mut bol_hasher = DefaultHasher::new();
bol.hash(&mut bol_hasher);
let hash_bol = bol_hasher.finish();
// 布林量 true 的雜湊值為 4952851536318644461
let dec: f32 = 3.14159;
let mut dec_hasher = DefaultHasher::new();
dec.to_bits().hash(&mut dec_hasher);
let hash_dec = dec_hasher.finish();
// 小數 3.14159 的雜湊值為 2566941990314602357
let str = "Hello 演算法";
let mut str_hasher = DefaultHasher::new();
str.hash(&mut str_hasher);
let hash_str = str_hasher.finish();
// 字串“Hello 演算法”的雜湊值為 16092673739211250988
let arr = (&12836, &"小哈");
let mut tup_hasher = DefaultHasher::new();
arr.hash(&mut tup_hasher);
let hash_tup = tup_hasher.finish();
// 元組 (12836, "小哈") 的雜湊值為 1885128010422702749
let node = ListNode::new(42);
let mut hasher = DefaultHasher::new();
node.borrow().val.hash(&mut hasher);
let hash = hasher.finish();
// 節點物件 RefCell { value: ListNode { val: 42, next: None } } 的雜湊值為15387811073369036852
```
=== "C"
```c title="built_in_hash.c"
// C 未提供內建 hash code 函式
```
=== "Kotlin"
```kotlin title="built_in_hash.kt"
val num = 3
val hashNum = num.hashCode()
// 整數 3 的雜湊值為 3
val bol = true
val hashBol = bol.hashCode()
// 布林量 true 的雜湊值為 1231
val dec = 3.14159
val hashDec = dec.hashCode()
// 小數 3.14159 的雜湊值為 -1340954729
val str = "Hello 演算法"
val hashStr = str.hashCode()
// 字串“Hello 演算法”的雜湊值為 -727081396
val arr = arrayOf<Any>(12836, "小哈")
val hashTup = arr.hashCode()
// 陣列 [12836, 小哈] 的雜湊值為 189568618
val obj = ListNode(0)
val hashObj = obj.hashCode()
// 節點物件 utils.ListNode@1d81eb93 的雜湊值為 495053715
```
=== "Ruby"
```ruby title="built_in_hash.rb"
```
=== "Zig"
```zig title="built_in_hash.zig"
```
??? pythontutor "視覺化執行"
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
在許多程式語言中,**只有不可變物件才可作為雜湊表的 `key`** 。假如我們將串列(動態陣列)作為 `key` ,當串列的內容發生變化時,它的雜湊值也隨之改變,我們就無法在雜湊表中查詢到原先的 `value` 了。
雖然自定義物件(比如鏈結串列節點)的成員變數是可變的,但它是可雜湊的。**這是因為物件的雜湊值通常是基於記憶體位址生成的**,即使物件的內容發生了變化,但它的記憶體位址不變,雜湊值仍然是不變的。
細心的你可能發現在不同控制檯中執行程式時,輸出的雜湊值是不同的。**這是因為 Python 直譯器在每次啟動時,都會為字串雜湊函式加入一個隨機的鹽(salt)值**。這種做法可以有效防止 HashDoS 攻擊,提升雜湊演算法的安全性。
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,108 @@
# 雜湊衝突
上一節提到,**通常情況下雜湊函式的輸入空間遠大於輸出空間**,因此理論上雜湊衝突是不可避免的。比如,輸入空間為全體整數,輸出空間為陣列容量大小,則必然有多個整數對映至同一桶索引。
雜湊衝突會導致查詢結果錯誤,嚴重影響雜湊表的可用性。為了解決該問題,每當遇到雜湊衝突時,我們就進行雜湊表擴容,直至衝突消失為止。此方法簡單粗暴且有效,但效率太低,因為雜湊表擴容需要進行大量的資料搬運與雜湊值計算。為了提升效率,我們可以採用以下策略。
1. 改良雜湊表資料結構,**使得雜湊表可以在出現雜湊衝突時正常工作**。
2. 僅在必要時,即當雜湊衝突比較嚴重時,才執行擴容操作。
雜湊表的結構改良方法主要包括“鏈式位址”和“開放定址”。
## 鏈式位址
在原始雜湊表中,每個桶僅能儲存一個鍵值對。<u>鏈式位址(separate chaining</u>將單個元素轉換為鏈結串列,將鍵值對作為鏈結串列節點,將所有發生衝突的鍵值對都儲存在同一鏈結串列中。下圖展示了一個鏈式位址雜湊表的例子。
![鏈式位址雜湊表](hash_collision.assets/hash_table_chaining.png)
基於鏈式位址實現的雜湊表的操作方法發生了以下變化。
- **查詢元素**:輸入 `key` ,經過雜湊函式得到桶索引,即可訪問鏈結串列頭節點,然後走訪鏈結串列並對比 `key` 以查詢目標鍵值對。
- **新增元素**:首先透過雜湊函式訪問鏈結串列頭節點,然後將節點(鍵值對)新增到鏈結串列中。
- **刪除元素**:根據雜湊函式的結果訪問鏈結串列頭部,接著走訪鏈結串列以查詢目標節點並將其刪除。
鏈式位址存在以下侷限性。
- **佔用空間增大**:鏈結串列包含節點指標,它相比陣列更加耗費記憶體空間。
- **查詢效率降低**:因為需要線性走訪鏈結串列來查詢對應元素。
以下程式碼給出了鏈式位址雜湊表的簡單實現,需要注意兩點。
- 使用串列(動態陣列)代替鏈結串列,從而簡化程式碼。在這種設定下,雜湊表(陣列)包含多個桶,每個桶都是一個串列。
- 以下實現包含雜湊表擴容方法。當負載因子超過 $\frac{2}{3}$ 時,我們將雜湊表擴容至原先的 $2$ 倍。
```src
[file]{hash_map_chaining}-[class]{hash_map_chaining}-[func]{}
```
值得注意的是,當鏈結串列很長時,查詢效率 $O(n)$ 很差。**此時可以將鏈結串列轉換為“AVL 樹”或“紅黑樹”**,從而將查詢操作的時間複雜度最佳化至 $O(\log n)$ 。
## 開放定址
<u>開放定址(open addressing</u>不引入額外的資料結構,而是透過“多次探測”來處理雜湊衝突,探測方式主要包括線性探查、平方探測和多次雜湊等。
下面以線性探查為例,介紹開放定址雜湊表的工作機制。
### 線性探查
線性探查採用固定步長的線性搜尋來進行探測,其操作方法與普通雜湊表有所不同。
- **插入元素**:透過雜湊函式計算桶索引,若發現桶內已有元素,則從衝突位置向後線性走訪(步長通常為 $1$ ),直至找到空桶,將元素插入其中。
- **查詢元素**:若發現雜湊衝突,則使用相同步長向後進行線性走訪,直到找到對應元素,返回 `value` 即可;如果遇到空桶,說明目標元素不在雜湊表中,返回 `None`
下圖展示了開放定址(線性探查)雜湊表的鍵值對分佈。根據此雜湊函式,最後兩位相同的 `key` 都會被對映到相同的桶。而透過線性探查,它們被依次儲存在該桶以及之下的桶中。
![開放定址(線性探查)雜湊表的鍵值對分佈](hash_collision.assets/hash_table_linear_probing.png)
然而,**線性探查容易產生“聚集現象”**。具體來說,陣列中連續被佔用的位置越長,這些連續位置發生雜湊衝突的可能性越大,從而進一步促使該位置的聚堆積生長,形成惡性迴圈,最終導致增刪查改操作效率劣化。
值得注意的是,**我們不能在開放定址雜湊表中直接刪除元素**。這是因為刪除元素會在陣列內產生一個空桶 `None` ,而當查詢元素時,線性探查到該空桶就會返回,因此在該空桶之下的元素都無法再被訪問到,程式可能誤判這些元素不存在,如下圖所示。
![在開放定址中刪除元素導致的查詢問題](hash_collision.assets/hash_table_open_addressing_deletion.png)
為了解決該問題,我們可以採用<u>懶刪除(lazy deletion</u>機制:它不直接從雜湊表中移除元素,**而是利用一個常數 `TOMBSTONE` 來標記這個桶**。在該機制下,`None``TOMBSTONE` 都代表空桶,都可以放置鍵值對。但不同的是,線性探查到 `TOMBSTONE` 時應該繼續走訪,因為其之下可能還存在鍵值對。
然而,**懶刪除可能會加速雜湊表的效能退化**。這是因為每次刪除操作都會產生一個刪除標記,隨著 `TOMBSTONE` 的增加,搜尋時間也會增加,因為線性探查可能需要跳過多個 `TOMBSTONE` 才能找到目標元素。
為此,考慮線上性探查中記錄遇到的首個 `TOMBSTONE` 的索引,並將搜尋到的目標元素與該 `TOMBSTONE` 交換位置。這樣做的好處是當每次查詢或新增元素時,元素會被移動至距離理想位置(探測起始點)更近的桶,從而最佳化查詢效率。
以下程式碼實現了一個包含懶刪除的開放定址(線性探查)雜湊表。為了更加充分地使用雜湊表的空間,我們將雜湊表看作一個“環形陣列”,當越過陣列尾部時,回到頭部繼續走訪。
```src
[file]{hash_map_open_addressing}-[class]{hash_map_open_addressing}-[func]{}
```
### 平方探測
平方探測與線性探查類似,都是開放定址的常見策略之一。當發生衝突時,平方探測不是簡單地跳過一個固定的步數,而是跳過“探測次數的平方”的步數,即 $1, 4, 9, \dots$ 步。
平方探測主要具有以下優勢。
- 平方探測透過跳過探測次數平方的距離,試圖緩解線性探查的聚集效應。
- 平方探測會跳過更大的距離來尋找空位置,有助於資料分佈得更加均勻。
然而,平方探測並不是完美的。
- 仍然存在聚集現象,即某些位置比其他位置更容易被佔用。
- 由於平方的增長,平方探測可能不會探測整個雜湊表,這意味著即使雜湊表中有空桶,平方探測也可能無法訪問到它。
### 多次雜湊
顧名思義,多次雜湊方法使用多個雜湊函式 $f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$ 進行探測。
- **插入元素**:若雜湊函式 $f_1(x)$ 出現衝突,則嘗試 $f_2(x)$ ,以此類推,直到找到空位後插入元素。
- **查詢元素**:在相同的雜湊函式順序下進行查詢,直到找到目標元素時返回;若遇到空位或已嘗試所有雜湊函式,說明雜湊表中不存在該元素,則返回 `None`
與線性探查相比,多次雜湊方法不易產生聚集,但多個雜湊函式會帶來額外的計算量。
!!! tip
請注意,開放定址(線性探查、平方探測和多次雜湊)雜湊表都存在“不能直接刪除元素”的問題。
## 程式語言的選擇
各種程式語言採取了不同的雜湊表實現策略,下面舉幾個例子。
- Python 採用開放定址。字典 `dict` 使用偽隨機數進行探測。
- Java 採用鏈式位址。自 JDK 1.8 以來,當 `HashMap` 內陣列長度達到 64 且鏈結串列長度達到 8 時,鏈結串列會轉換為紅黑樹以提升查詢效能。
- Go 採用鏈式位址。Go 規定每個桶最多儲存 8 個鍵值對,超出容量則連線一個溢位桶;當溢位桶過多時,會執行一次特殊的等量擴容操作,以確保效能。
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+578
View File
@@ -0,0 +1,578 @@
# 雜湊表
<u>雜湊表(hash table</u>,又稱<u>散列表</u>,它透過建立鍵 `key` 與值 `value` 之間的對映,實現高效的元素查詢。具體而言,我們向雜湊表中輸入一個鍵 `key` ,則可以在 $O(1)$ 時間內獲取對應的值 `value`
如下圖所示,給定 $n$ 個學生,每個學生都有“姓名”和“學號”兩項資料。假如我們希望實現“輸入一個學號,返回對應的姓名”的查詢功能,則可以採用下圖所示的雜湊表來實現。
![雜湊表的抽象表示](hash_map.assets/hash_table_lookup.png)
除雜湊表外,陣列和鏈結串列也可以實現查詢功能,它們的效率對比如下表所示。
- **新增元素**:僅需將元素新增至陣列(鏈結串列)的尾部即可,使用 $O(1)$ 時間。
- **查詢元素**:由於陣列(鏈結串列)是亂序的,因此需要走訪其中的所有元素,使用 $O(n)$ 時間。
- **刪除元素**:需要先查詢到元素,再從陣列(鏈結串列)中刪除,使用 $O(n)$ 時間。
<p align="center"> 表 <id> &nbsp; 元素查詢效率對比 </p>
| | 陣列 | 鏈結串列 | 雜湊表 |
| -------- | ------ | ------ | ------ |
| 查詢元素 | $O(n)$ | $O(n)$ | $O(1)$ |
| 新增元素 | $O(1)$ | $O(1)$ | $O(1)$ |
| 刪除元素 | $O(n)$ | $O(n)$ | $O(1)$ |
觀察發現,**在雜湊表中進行增刪查改的時間複雜度都是 $O(1)$** ,非常高效。
## 雜湊表常用操作
雜湊表的常見操作包括:初始化、查詢操作、新增鍵值對和刪除鍵值對等,示例程式碼如下:
=== "Python"
```python title="hash_map.py"
# 初始化雜湊表
hmap: dict = {}
# 新增操作
# 在雜湊表中新增鍵值對 (key, value)
hmap[12836] = "小哈"
hmap[15937] = "小囉"
hmap[16750] = "小算"
hmap[13276] = "小法"
hmap[10583] = "小鴨"
# 查詢操作
# 向雜湊表中輸入鍵 key ,得到值 value
name: str = hmap[15937]
# 刪除操作
# 在雜湊表中刪除鍵值對 (key, value)
hmap.pop(10583)
```
=== "C++"
```cpp title="hash_map.cpp"
/* 初始化雜湊表 */
unordered_map<int, string> map;
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map[12836] = "小哈";
map[15937] = "小囉";
map[16750] = "小算";
map[13276] = "小法";
map[10583] = "小鴨";
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
string name = map[15937];
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.erase(10583);
```
=== "Java"
```java title="hash_map.java"
/* 初始化雜湊表 */
Map<Integer, String> map = new HashMap<>();
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map.put(12836, "小哈");
map.put(15937, "小囉");
map.put(16750, "小算");
map.put(13276, "小法");
map.put(10583, "小鴨");
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
String name = map.get(15937);
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.remove(10583);
```
=== "C#"
```csharp title="hash_map.cs"
/* 初始化雜湊表 */
Dictionary<int, string> map = new() {
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
{ 12836, "小哈" },
{ 15937, "小囉" },
{ 16750, "小算" },
{ 13276, "小法" },
{ 10583, "小鴨" }
};
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
string name = map[15937];
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.Remove(10583);
```
=== "Go"
```go title="hash_map_test.go"
/* 初始化雜湊表 */
hmap := make(map[int]string)
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
hmap[12836] = "小哈"
hmap[15937] = "小囉"
hmap[16750] = "小算"
hmap[13276] = "小法"
hmap[10583] = "小鴨"
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
name := hmap[15937]
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
delete(hmap, 10583)
```
=== "Swift"
```swift title="hash_map.swift"
/* 初始化雜湊表 */
var map: [Int: String] = [:]
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map[12836] = "小哈"
map[15937] = "小囉"
map[16750] = "小算"
map[13276] = "小法"
map[10583] = "小鴨"
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
let name = map[15937]!
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.removeValue(forKey: 10583)
```
=== "JS"
```javascript title="hash_map.js"
/* 初始化雜湊表 */
const map = new Map();
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map.set(12836, '小哈');
map.set(15937, '小囉');
map.set(16750, '小算');
map.set(13276, '小法');
map.set(10583, '小鴨');
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
let name = map.get(15937);
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.delete(10583);
```
=== "TS"
```typescript title="hash_map.ts"
/* 初始化雜湊表 */
const map = new Map<number, string>();
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map.set(12836, '小哈');
map.set(15937, '小囉');
map.set(16750, '小算');
map.set(13276, '小法');
map.set(10583, '小鴨');
console.info('\n新增完成後,雜湊表為\nKey -> Value');
console.info(map);
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
let name = map.get(15937);
console.info('\n輸入學號 15937 ,查詢到姓名 ' + name);
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.delete(10583);
console.info('\n刪除 10583 後,雜湊表為\nKey -> Value');
console.info(map);
```
=== "Dart"
```dart title="hash_map.dart"
/* 初始化雜湊表 */
Map<int, String> map = {};
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map[12836] = "小哈";
map[15937] = "小囉";
map[16750] = "小算";
map[13276] = "小法";
map[10583] = "小鴨";
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
String name = map[15937];
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.remove(10583);
```
=== "Rust"
```rust title="hash_map.rs"
use std::collections::HashMap;
/* 初始化雜湊表 */
let mut map: HashMap<i32, String> = HashMap::new();
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map.insert(12836, "小哈".to_string());
map.insert(15937, "小囉".to_string());
map.insert(16750, "小算".to_string());
map.insert(13279, "小法".to_string());
map.insert(10583, "小鴨".to_string());
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
let _name: Option<&String> = map.get(&15937);
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
let _removed_value: Option<String> = map.remove(&10583);
```
=== "C"
```c title="hash_map.c"
// C 未提供內建雜湊表
```
=== "Kotlin"
```kotlin title="hash_map.kt"
/* 初始化雜湊表 */
val map = HashMap<Int,String>()
/* 新增操作 */
// 在雜湊表中新增鍵值對 (key, value)
map[12836] = "小哈"
map[15937] = "小囉"
map[16750] = "小算"
map[13276] = "小法"
map[10583] = "小鴨"
/* 查詢操作 */
// 向雜湊表中輸入鍵 key ,得到值 value
val name = map[15937]
/* 刪除操作 */
// 在雜湊表中刪除鍵值對 (key, value)
map.remove(10583)
```
=== "Ruby"
```ruby title="hash_map.rb"
```
=== "Zig"
```zig title="hash_map.zig"
```
??? pythontutor "視覺化執行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
雜湊表有三種常用的走訪方式:走訪鍵值對、走訪鍵和走訪值。示例程式碼如下:
=== "Python"
```python title="hash_map.py"
# 走訪雜湊表
# 走訪鍵值對 key->value
for key, value in hmap.items():
print(key, "->", value)
# 單獨走訪鍵 key
for key in hmap.keys():
print(key)
# 單獨走訪值 value
for value in hmap.values():
print(value)
```
=== "C++"
```cpp title="hash_map.cpp"
/* 走訪雜湊表 */
// 走訪鍵值對 key->value
for (auto kv: map) {
cout << kv.first << " -> " << kv.second << endl;
}
// 使用迭代器走訪 key->value
for (auto iter = map.begin(); iter != map.end(); iter++) {
cout << iter->first << "->" << iter->second << endl;
}
```
=== "Java"
```java title="hash_map.java"
/* 走訪雜湊表 */
// 走訪鍵值對 key->value
for (Map.Entry <Integer, String> kv: map.entrySet()) {
System.out.println(kv.getKey() + " -> " + kv.getValue());
}
// 單獨走訪鍵 key
for (int key: map.keySet()) {
System.out.println(key);
}
// 單獨走訪值 value
for (String val: map.values()) {
System.out.println(val);
}
```
=== "C#"
```csharp title="hash_map.cs"
/* 走訪雜湊表 */
// 走訪鍵值對 Key->Value
foreach (var kv in map) {
Console.WriteLine(kv.Key + " -> " + kv.Value);
}
// 單獨走訪鍵 key
foreach (int key in map.Keys) {
Console.WriteLine(key);
}
// 單獨走訪值 value
foreach (string val in map.Values) {
Console.WriteLine(val);
}
```
=== "Go"
```go title="hash_map_test.go"
/* 走訪雜湊表 */
// 走訪鍵值對 key->value
for key, value := range hmap {
fmt.Println(key, "->", value)
}
// 單獨走訪鍵 key
for key := range hmap {
fmt.Println(key)
}
// 單獨走訪值 value
for _, value := range hmap {
fmt.Println(value)
}
```
=== "Swift"
```swift title="hash_map.swift"
/* 走訪雜湊表 */
// 走訪鍵值對 Key->Value
for (key, value) in map {
print("\(key) -> \(value)")
}
// 單獨走訪鍵 Key
for key in map.keys {
print(key)
}
// 單獨走訪值 Value
for value in map.values {
print(value)
}
```
=== "JS"
```javascript title="hash_map.js"
/* 走訪雜湊表 */
console.info('\n走訪鍵值對 Key->Value');
for (const [k, v] of map.entries()) {
console.info(k + ' -> ' + v);
}
console.info('\n單獨走訪鍵 Key');
for (const k of map.keys()) {
console.info(k);
}
console.info('\n單獨走訪值 Value');
for (const v of map.values()) {
console.info(v);
}
```
=== "TS"
```typescript title="hash_map.ts"
/* 走訪雜湊表 */
console.info('\n走訪鍵值對 Key->Value');
for (const [k, v] of map.entries()) {
console.info(k + ' -> ' + v);
}
console.info('\n單獨走訪鍵 Key');
for (const k of map.keys()) {
console.info(k);
}
console.info('\n單獨走訪值 Value');
for (const v of map.values()) {
console.info(v);
}
```
=== "Dart"
```dart title="hash_map.dart"
/* 走訪雜湊表 */
// 走訪鍵值對 Key->Value
map.forEach((key, value) {
print('$key -> $value');
});
// 單獨走訪鍵 Key
map.keys.forEach((key) {
print(key);
});
// 單獨走訪值 Value
map.values.forEach((value) {
print(value);
});
```
=== "Rust"
```rust title="hash_map.rs"
/* 走訪雜湊表 */
// 走訪鍵值對 Key->Value
for (key, value) in &map {
println!("{key} -> {value}");
}
// 單獨走訪鍵 Key
for key in map.keys() {
println!("{key}");
}
// 單獨走訪值 Value
for value in map.values() {
println!("{value}");
}
```
=== "C"
```c title="hash_map.c"
// C 未提供內建雜湊表
```
=== "Kotlin"
```kotlin title="hash_map.kt"
/* 走訪雜湊表 */
// 走訪鍵值對 key->value
for ((key, value) in map) {
println("$key -> $value")
}
// 單獨走訪鍵 key
for (key in map.keys) {
println(key)
}
// 單獨走訪值 value
for (_val in map.values) {
println(_val)
}
```
=== "Ruby"
```ruby title="hash_map.rb"
```
=== "Zig"
```zig title="hash_map.zig"
```
??? pythontutor "視覺化執行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
## 雜湊表簡單實現
我們先考慮最簡單的情況,**僅用一個陣列來實現雜湊表**。在雜湊表中,我們將陣列中的每個空位稱為<u>桶(bucket</u>,每個桶可儲存一個鍵值對。因此,查詢操作就是找到 `key` 對應的桶,並在桶中獲取 `value` 。
那麼,如何基於 `key` 定位對應的桶呢?這是透過<u>雜湊函式(hash function</u>實現的。雜湊函式的作用是將一個較大的輸入空間對映到一個較小的輸出空間。在雜湊表中,輸入空間是所有 `key` ,輸出空間是所有桶(陣列索引)。換句話說,輸入一個 `key` ,**我們可以透過雜湊函式得到該 `key` 對應的鍵值對在陣列中的儲存位置**。
輸入一個 `key` ,雜湊函式的計算過程分為以下兩步。
1. 透過某種雜湊演算法 `hash()` 計算得到雜湊值。
2. 將雜湊值對桶數量(陣列長度)`capacity` 取模,從而獲取該 `key` 對應的陣列索引 `index` 。
```shell
index = hash(key) % capacity
```
隨後,我們就可以利用 `index` 在雜湊表中訪問對應的桶,從而獲取 `value` 。
設陣列長度 `capacity = 100`、雜湊演算法 `hash(key) = key` ,易得雜湊函式為 `key % 100` 。下圖以 `key` 學號和 `value` 姓名為例,展示了雜湊函式的工作原理。
![雜湊函式工作原理](hash_map.assets/hash_function.png)
以下程式碼實現了一個簡單雜湊表。其中,我們將 `key` 和 `value` 封裝成一個類別 `Pair` ,以表示鍵值對。
```src
[file]{array_hash_map}-[class]{array_hash_map}-[func]{}
```
## 雜湊衝突與擴容
從本質上看,雜湊函式的作用是將所有 `key` 構成的輸入空間對映到陣列所有索引構成的輸出空間,而輸入空間往往遠大於輸出空間。因此,**理論上一定存在“多個輸入對應相同輸出”的情況**。
對於上述示例中的雜湊函式,當輸入的 `key` 後兩位相同時,雜湊函式的輸出結果也相同。例如,查詢學號為 12836 和 20336 的兩個學生時,我們得到:
```shell
12836 % 100 = 36
20336 % 100 = 36
```
如下圖所示,兩個學號指向了同一個姓名,這顯然是不對的。我們將這種多個輸入對應同一輸出的情況稱為<u>雜湊衝突(hash collision</u>。
![雜湊衝突示例](hash_map.assets/hash_collision.png)
容易想到,雜湊表容量 $n$ 越大,多個 `key` 被分配到同一個桶中的機率就越低,衝突就越少。因此,**我們可以透過擴容雜湊表來減少雜湊衝突**。
如下圖所示,擴容前鍵值對 `(136, A)` 和 `(236, D)` 發生衝突,擴容後衝突消失。
![雜湊表擴容](hash_map.assets/hash_table_reshash.png)
類似於陣列擴容,雜湊表擴容需將所有鍵值對從原雜湊表遷移至新雜湊表,非常耗時;並且由於雜湊表容量 `capacity` 改變,我們需要透過雜湊函式來重新計算所有鍵值對的儲存位置,這進一步增加了擴容過程的計算開銷。為此,程式語言通常會預留足夠大的雜湊表容量,防止頻繁擴容。
<u>負載因子(load factor</u>是雜湊表的一個重要概念,其定義為雜湊表的元素數量除以桶數量,用於衡量雜湊衝突的嚴重程度,**也常作為雜湊表擴容的觸發條件**。例如在 Java 中,當負載因子超過 $0.75$ 時,系統會將雜湊表擴容至原先的 $2$ 倍。
+9
View File
@@ -0,0 +1,9 @@
# 雜湊表
![雜湊表](../assets/covers/chapter_hashing.jpg)
!!! abstract
在計算機世界中,雜湊表如同一位聰慧的圖書管理員。
他知道如何計算索書號,從而可以快速找到目標圖書。
+47
View File
@@ -0,0 +1,47 @@
# 小結
### 重點回顧
- 輸入 `key` ,雜湊表能夠在 $O(1)$ 時間內查詢到 `value` ,效率非常高。
- 常見的雜湊表操作包括查詢、新增鍵值對、刪除鍵值對和走訪雜湊表等。
- 雜湊函式將 `key` 對映為陣列索引,從而訪問對應桶並獲取 `value`
- 兩個不同的 `key` 可能在經過雜湊函式後得到相同的陣列索引,導致查詢結果出錯,這種現象被稱為雜湊衝突。
- 雜湊表容量越大,雜湊衝突的機率就越低。因此可以透過擴容雜湊表來緩解雜湊衝突。與陣列擴容類似,雜湊表擴容操作的開銷很大。
- 負載因子定義為雜湊表中元素數量除以桶數量,反映了雜湊衝突的嚴重程度,常用作觸發雜湊表擴容的條件。
- 鏈式位址透過將單個元素轉化為鏈結串列,將所有衝突元素儲存在同一個鏈結串列中。然而,鏈結串列過長會降低查詢效率,可以透過進一步將鏈結串列轉換為紅黑樹來提高效率。
- 開放定址透過多次探測來處理雜湊衝突。線性探查使用固定步長,缺點是不能刪除元素,且容易產生聚集。多次雜湊使用多個雜湊函式進行探測,相較線性探查更不易產生聚集,但多個雜湊函式增加了計算量。
- 不同程式語言採取了不同的雜湊表實現。例如,Java 的 `HashMap` 使用鏈式位址,而 Python 的 `Dict` 採用開放定址。
- 在雜湊表中,我們希望雜湊演算法具有確定性、高效率和均勻分佈的特點。在密碼學中,雜湊演算法還應該具備抗碰撞性和雪崩效應。
- 雜湊演算法通常採用大質數作為模數,以最大化地保證雜湊值均勻分佈,減少雜湊衝突。
- 常見的雜湊演算法包括 MD5、SHA-1、SHA-2 和 SHA-3 等。MD5 常用於校驗檔案完整性,SHA-2 常用於安全應用與協議。
- 程式語言通常會為資料型別提供內建雜湊演算法,用於計算雜湊表中的桶索引。通常情況下,只有不可變物件是可雜湊的。
### Q & A
**Q**:雜湊表的時間複雜度在什麼情況下是 $O(n)$ ?
當雜湊衝突比較嚴重時,雜湊表的時間複雜度會退化至 $O(n)$ 。當雜湊函式設計得比較好、容量設定比較合理、衝突比較平均時,時間複雜度是 $O(1)$ 。我們使用程式語言內建的雜湊表時,通常認為時間複雜度是 $O(1)$ 。
**Q**:為什麼不使用雜湊函式 $f(x) = x$ 呢?這樣就不會有衝突了。
在 $f(x) = x$ 雜湊函式下,每個元素對應唯一的桶索引,這與陣列等價。然而,輸入空間通常遠大於輸出空間(陣列長度),因此雜湊函式的最後一步往往是對陣列長度取模。換句話說,雜湊表的目標是將一個較大的狀態空間對映到一個較小的空間,並提供 $O(1)$ 的查詢效率。
**Q**:雜湊表底層實現是陣列、鏈結串列、二元樹,但為什麼效率可以比它們更高呢?
首先,雜湊表的時間效率變高,但空間效率變低了。雜湊表有相當一部分記憶體未使用。
其次,只是在特定使用場景下時間效率變高了。如果一個功能能夠在相同的時間複雜度下使用陣列或鏈結串列實現,那麼通常比雜湊表更快。這是因為雜湊函式計算需要開銷,時間複雜度的常數項更大。
最後,雜湊表的時間複雜度可能發生劣化。例如在鏈式位址中,我們採取在鏈結串列或紅黑樹中執行查詢操作,仍然有退化至 $O(n)$ 時間的風險。
**Q**:多次雜湊有不能直接刪除元素的缺陷嗎?標記為已刪除的空間還能再次使用嗎?
多次雜湊是開放定址的一種,開放定址法都有不能直接刪除元素的缺陷,需要透過標記刪除。標記為已刪除的空間可以再次使用。當將新元素插入雜湊表,並且透過雜湊函式找到標記為已刪除的位置時,該位置可以被新元素使用。這樣做既能保持雜湊表的探測序列不變,又能保證雜湊表的空間使用率。
**Q**:為什麼線上性探查中,查詢元素的時候會出現雜湊衝突呢?
查詢的時候透過雜湊函式找到對應的桶和鍵值對,發現 `key` 不匹配,這就代表有雜湊衝突。因此,線性探查法會根據預先設定的步長依次向下查詢,直至找到正確的鍵值對或無法找到跳出為止。
**Q**:為什麼雜湊表擴容能夠緩解雜湊衝突?
雜湊函式的最後一步往往是對陣列長度 $n$ 取模(取餘),讓輸出值落在陣列索引範圍內;在擴容後,陣列長度 $n$ 發生變化,而 `key` 對應的索引也可能發生變化。原先落在同一個桶的多個 `key` ,在擴容後可能會被分配到多個桶中,從而實現雜湊衝突的緩解。