This commit is contained in:
krahets
2023-08-22 13:50:12 +08:00
parent 0c9bf14e20
commit 92a0853ab8
64 changed files with 478 additions and 479 deletions
+2 -2
View File
@@ -6,11 +6,11 @@ comments: true
在上两节中,我们了解了哈希表的工作原理和哈希冲突的处理方法。然而无论是开放寻址还是链地址法,**它们只能保证哈希表可以在发生冲突时正常工作,但无法减少哈希冲突的发生**。
如果哈希冲突过于频繁,哈希表的性能则会急剧劣化。如图所示,对于链地址哈希表,理想情况下键值对平均分布在各个桶中,达到最佳查询效率;最差情况下所有键值对都被存储到同一个桶中,时间复杂度退化至 $O(n)$ 。
如果哈希冲突过于频繁,哈希表的性能则会急剧劣化。如图 6-7 所示,对于链地址哈希表,理想情况下键值对平均分布在各个桶中,达到最佳查询效率;最差情况下所有键值对都被存储到同一个桶中,时间复杂度退化至 $O(n)$ 。
![哈希冲突的最佳与最差情况](hash_algorithm.assets/hash_collision_best_worst_condition.png)
<p align="center"> 图哈希冲突的最佳与最差情况 </p>
<p align="center"> 图 6-7 &nbsp; 哈希冲突的最佳与最差情况 </p>
**键值对的分布情况由哈希函数决定**。回忆哈希函数的计算步骤,先计算哈希值,再对数组长度取模:
+4 -4
View File
@@ -15,11 +15,11 @@ comments: true
## 6.2.1 &nbsp; 链式地址
在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。图展示了一个链式地址哈希表的例子。
在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。图 6-5 展示了一个链式地址哈希表的例子。
![链式地址哈希表](hash_collision.assets/hash_table_chaining.png)
<p align="center"> 图链式地址哈希表 </p>
<p align="center"> 图 6-5 &nbsp; 链式地址哈希表 </p>
链式地址下,哈希表的操作方法包括:
@@ -1163,11 +1163,11 @@ comments: true
- **插入元素**:通过哈希函数计算数组索引,若发现桶内已有元素,则从冲突位置向后线性遍历(步长通常为 $1$ ),直至找到空位,将元素插入其中。
- **查找元素**:若发现哈希冲突,则使用相同步长向后线性遍历,直到找到对应元素,返回 `value` 即可;如果遇到空位,说明目标键值对不在哈希表中,返回 $\text{None}$ 。
图展示了一个在开放寻址(线性探测)下工作的哈希表。
6-6 展示了一个在开放寻址(线性探测)下工作的哈希表。
![开放寻址和线性探测](hash_collision.assets/hash_table_linear_probing.png)
<p align="center"> 图开放寻址和线性探测 </p>
<p align="center"> 图 6-6 &nbsp; 开放寻址和线性探测 </p>
然而,线性探测存在以下缺陷:
+10 -10
View File
@@ -6,19 +6,19 @@ comments: true
「哈希表 hash table」,又称「散列表」,其通过建立键 `key` 与值 `value` 之间的映射,实现高效的元素查询。具体而言,我们向哈希表输入一个键 `key` ,则可以在 $O(1)$ 时间内获取对应的值 `value`
图所示,给定 $n$ 个学生,每个学生都有“姓名”和“学号”两项数据。假如我们希望实现“输入一个学号,返回对应的姓名”的查询功能,则可以采用哈希表来实现。
如图 6-1 所示,给定 $n$ 个学生,每个学生都有“姓名”和“学号”两项数据。假如我们希望实现“输入一个学号,返回对应的姓名”的查询功能,则可以采用图 6-1 所示的哈希表来实现。
![哈希表的抽象表示](hash_map.assets/hash_table_lookup.png)
<p align="center"> 图哈希表的抽象表示 </p>
<p align="center"> 图 6-1 &nbsp; 哈希表的抽象表示 </p>
除哈希表外,数组和链表也可以实现查询功能,它们的效率对比如表所示。
除哈希表外,数组和链表也可以实现查询功能,它们的效率对比如表 6-1 所示。
- **添加元素**:仅需将元素添加至数组(链表)的尾部即可,使用 $O(1)$ 时间。
- **查询元素**:由于数组(链表)是乱序的,因此需要遍历其中的所有元素,使用 $O(n)$ 时间。
- **删除元素**:需要先查询到元素,再从数组(链表)中删除,使用 $O(n)$ 时间。
<p align="center"> 表元素查询效率对比 </p>
<p align="center"> 表 6-1 &nbsp; 元素查询效率对比 </p>
<div class="center-table" markdown>
@@ -462,11 +462,11 @@ index = hash(key) % capacity
随后,我们就可以利用 `index` 在哈希表中访问对应的桶,从而获取 `value` 。
设数组长度 `capacity = 100` 、哈希算法 `hash(key) = key` ,易得哈希函数为 `key % 100` 。图以 `key` 学号和 `value` 姓名为例,展示了哈希函数的工作原理。
设数组长度 `capacity = 100` 、哈希算法 `hash(key) = key` ,易得哈希函数为 `key % 100` 。图 6-2 以 `key` 学号和 `value` 姓名为例,展示了哈希函数的工作原理。
![哈希函数工作原理](hash_map.assets/hash_function.png)
<p align="center"> 图哈希函数工作原理 </p>
<p align="center"> 图 6-2 &nbsp; 哈希函数工作原理 </p>
以下代码实现了一个简单哈希表。其中,我们将 `key` 和 `value` 封装成一个类 `Pair` ,以表示键值对。
@@ -1499,19 +1499,19 @@ index = hash(key) % capacity
20336 % 100 = 36
```
图所示,两个学号指向了同一个姓名,这显然是不对的。我们将这种多个输入对应同一输出的情况称为「哈希冲突 hash collision」。
如图 6-3 所示,两个学号指向了同一个姓名,这显然是不对的。我们将这种多个输入对应同一输出的情况称为「哈希冲突 hash collision」。
![哈希冲突示例](hash_map.assets/hash_collision.png)
<p align="center"> 图哈希冲突示例 </p>
<p align="center"> 图 6-3 &nbsp; 哈希冲突示例 </p>
容易想到,哈希表容量 $n$ 越大,多个 `key` 被分配到同一个桶中的概率就越低,冲突就越少。因此,**我们可以通过扩容哈希表来减少哈希冲突**。
图所示,扩容前键值对 `(136, A)` 和 `(236, D)` 发生冲突,扩容后冲突消失。
如图 6-4 所示,扩容前键值对 `(136, A)` 和 `(236, D)` 发生冲突,扩容后冲突消失。
![哈希表扩容](hash_map.assets/hash_table_reshash.png)
<p align="center"> 图哈希表扩容 </p>
<p align="center"> 图 6-4 &nbsp; 哈希表扩容 </p>
类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时。并且由于哈希表容量 `capacity` 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步提高了扩容过程的计算开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容。