This commit is contained in:
krahets
2026-07-19 03:42:10 +08:00
parent cb5235bc3f
commit 2b8612bc41
238 changed files with 7477 additions and 3 deletions
+108
View File
@@ -0,0 +1,108 @@
---
comments: true
hide: [edit]
---
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
# 6.5 &nbsp; 練習
## 6.5.1 &nbsp; 知識鞏固
### 1. &nbsp; 發生雜湊衝突後怎樣查詢
一個雜湊表有 5 個桶,雜湊函式為 $h(x)=x \bmod 5$,衝突時把元素依次放進該桶的串列中。
依次插入 `[1, 6, 11, 7]`
<!-- numbered-subquestions -->
1. 寫出 04 號桶中的內容;
2. 查詢 6 時會先進入哪個桶,並依次檢查哪些元素?
3. 根據第 1 問寫出的桶內容,後插入的元素是否覆蓋了先插入的元素?結合這種衝突處理方式說明理由。
??? success "參考答案"
1. 因為 $1\bmod5=6\bmod5=11\bmod5=1$,而 $7\bmod5=2$,各桶為:
```text
0: []
1: [1, 6, 11]
2: [7]
3: []
4: []
```
2. 查詢 6 時先進入 1 號桶,再依次比較 1、6,第二次比較時找到目標。
3. 雜湊值相同只表示它們落入同一個桶,並不表示這些元素相等。鏈式位址把衝突元素都儲存在桶內,
查詢時再逐個比較,因此 1、6、11 不會互相覆蓋。
### 2. &nbsp; 雜湊表擴容後元素去哪兒
一個使用鏈式位址的雜湊表原有 5 個桶,雜湊函式為 $h(x)=x\bmod5$。
鍵 `[1, 6, 11]` 都在 1 號桶中。
現在把雜湊表擴容為 7 個桶,雜湊函式相應變為 $h(x)=x\bmod7$
<!-- numbered-subquestions -->
1. 分別計算 1、6、11 的新桶號。
2. 擴容後哪些桶中有元素?
3. 擴容時能否把原來 1 號桶中的串列原樣複製到新的 1 號桶?結合第 1、2 問的結果說明理由。
??? success "參考答案"
1. 新桶號分別為:
- $1\bmod7=1$
- $6\bmod7=6$
- $11\bmod7=4$。
2. 1 號桶儲存 1,4 號桶儲存 11,6 號桶儲存 6。三個鍵不再擠在同一個桶中。
3. 不能原樣複製。桶號由“鍵對桶數取餘”得到。桶數從 5 變成 7 後,同一個鍵的新桶號可能改變,
因此必須重新計算每個鍵的位置。若把舊的 1 號桶原樣複製過去,之後按新公式查詢 6 和 11 時,
會分別前往 6 號桶和 4 號桶,從而找不到它們。
### 3. &nbsp; 刪除 6 後還能找到 11 嗎
一個雜湊表有 5 個位置,索引為 `04`,雜湊函式為 $h(x)=x\bmod5$。
發生衝突時,從雜湊函式算出的索引開始,向右尋找第一個空位。
依次插入 `[1, 6, 11]`
<!-- numbered-subquestions -->
1. 三個數最終分別放在哪個索引?
2. 查詢 11 時,會依次檢查哪些索引?
3. 如果刪除 6 時直接把它的位置改成“從未使用的空位”,而查詢遇到空位就停止,
再查詢 11 時會發生什麼?這個查詢結果是否正確?如果有問題,應怎樣避免?
??? success "參考答案"
1. 1 放在索引 1。6 也對映到索引 1,發生衝突後放在索引 2。
11 同樣從索引 1 開始,依次跳過已佔用的索引 1、2,最終放在索引 3。
2. 查詢 11 時依次檢查索引 `1、2、3`,在索引 3 找到它。
3. 如果把索引 2 改成表示“從未使用”的空位,查詢 11 時檢查索引 1 後就會在索引 2 停止,
從而錯誤地認為 11 不存在。刪除時應留下“已刪除”標記:
查詢遇到該標記時繼續檢查下一個索引(越過索引 4 後回到索引 0),而以後的插入仍可重新使用這個位置。
## 6.5.2 &nbsp; 程式設計練習
### 1. &nbsp; 比較兩個字串的字元組成
給定兩個只含小寫英文字母的字串 `s` 和 `t`。
可以任意調整 `s` 中字元的位置,但不能新增、刪除或替換字元。
請判斷調整後能否得到 `t`;可以則返回 `true`,否則返回 `false`。
請使用雜湊表記錄各字母的出現次數,不對字串中的字元排序。
??? tip "解題提示"
1. 兩個字串長度不同,它們的字元組成一定不同
2. 用雜湊表記錄每種字母的數量;掃描 s 時把對應計數加 1
3. 掃描 t 時把對應計數減 1;所有計數最終都為 0,字元組成才相同
[LeetCode](https://leetcode.cn/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [題目解析](https://leetcode.cn/problems/valid-anagram/solutions/2362065/242-you-xiao-de-zi-mu-yi-wei-ci-ha-xi-bi-cch7/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
---
comments: true
icon: material/table-search
---
# 第 6 章 &nbsp; 雜湊表
![雜湊表](../assets/covers/chapter_hashing.jpg){ class="cover-image" }
!!! abstract
在計算機世界中,雜湊表如同一位聰慧的圖書管理員。
他知道如何計算索書號,從而可以快速找到目標圖書。
## 本章內容
- [6.1 &nbsp; 雜湊表](hash_map.md)
- [6.2 &nbsp; 雜湊衝突](hash_collision.md)
- [6.3 &nbsp; 雜湊演算法](hash_algorithm.md)
- [6.4 &nbsp; 小結](summary.md)
- [6.5 &nbsp; 練習](exercises.md)
+55
View File
@@ -0,0 +1,55 @@
---
comments: true
---
# 6.4 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 輸入 `key` ,雜湊表能夠在 $O(1)$ 時間內查詢到 `value` ,效率非常高。
- 常見的雜湊表操作包括查詢、新增鍵值對、刪除鍵值對和走訪雜湊表等。
- 雜湊函式將 `key` 對映為陣列索引,從而訪問對應桶並獲取 `value`
- 兩個不同的 `key` 可能在經過雜湊函式後得到相同的陣列索引,導致查詢結果出錯,這種現象被稱為雜湊衝突。
- 雜湊表容量越大,雜湊衝突的機率就越低。因此可以透過擴容雜湊表來緩解雜湊衝突。與陣列擴容類似,雜湊表擴容操作的開銷很大。
- 負載因子定義為雜湊表中元素數量除以桶數量,反映了雜湊衝突的嚴重程度,常用作觸發雜湊表擴容的條件。
- 鏈式位址透過將單個元素轉化為鏈結串列,將所有衝突元素儲存在同一個鏈結串列中。然而,鏈結串列過長會降低查詢效率,可以透過進一步將鏈結串列轉換為紅黑樹來提高效率。
- 開放定址透過多次探測來處理雜湊衝突。線性探查使用固定步長,缺點是不能刪除元素,且容易產生聚集。多次雜湊使用多個雜湊函式進行探測,相較線性探查更不易產生聚集,但多個雜湊函式增加了計算量。
- 不同程式語言採取了不同的雜湊表實現。例如,Java 的 `HashMap` 使用鏈式位址,而 Python 的 `Dict` 採用開放定址。
- 在雜湊表中,我們希望雜湊演算法具有確定性、高效率和均勻分佈的特點。在密碼學中,雜湊演算法還應該具備抗碰撞性和雪崩效應。
- 雜湊演算法通常採用大質數作為模數,以最大化地保證雜湊值均勻分佈,減少雜湊衝突。
- 常見的雜湊演算法包括 MD5、SHA-1、SHA-2 和 SHA-3 等。MD5 常用於校驗檔案完整性,SHA-2 常用於安全應用與協議。
- 程式語言通常會為資料型別提供內建雜湊演算法,用於計算雜湊表中的桶索引。通常情況下,只有不可變物件是可雜湊的。
### 2. &nbsp; 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` ,在擴容後可能會被分配到多個桶中,從而實現雜湊衝突的緩解。
**Q**:如果為了高效的存取,那麼直接使用陣列不就好了嗎?
當資料的 `key` 是連續的小範圍整數時,直接用陣列即可,簡單高效。但當 `key` 是其他型別(例如字串)時,就需要藉助雜湊函式將 `key` 對映為陣列索引,再透過桶陣列儲存元素,這樣的結構就是雜湊表。