feat: Revised the book (#978)

* Sync recent changes to the revised Word.

* Revised the preface chapter

* Revised the introduction chapter

* Revised the computation complexity chapter

* Revised the chapter data structure

* Revised the chapter array and linked list

* Revised the chapter stack and queue

* Revised the chapter hashing

* Revised the chapter tree

* Revised the chapter heap

* Revised the chapter graph

* Revised the chapter searching

* Reivised the sorting chapter

* Revised the divide and conquer chapter

* Revised the chapter backtacking

* Revised the DP chapter

* Revised the greedy chapter

* Revised the appendix chapter

* Revised the preface chapter doubly

* Revised the figures
This commit is contained in:
Yudong Jin
2023-12-02 06:21:34 +08:00
committed by GitHub
parent b824d149cb
commit e720aa2d24
404 changed files with 1537 additions and 1558 deletions

View File

@@ -1,10 +1,10 @@
# 哈希算法
在上两节中,我们了解了哈希表的工作原理和哈希冲突的处理方法。然而无论是开放寻址还是链地址**它们只能保证哈希表可以在发生冲突时正常工作,无法减少哈希冲突的发生**。
前两节介绍了哈希表的工作原理和哈希冲突的处理方法。然而无论是开放寻址还是链地址,**它们只能保证哈希表可以在发生冲突时正常工作,无法减少哈希冲突的发生**。
如果哈希冲突过于频繁,哈希表的性能则会急剧劣化。如下图所示,对于链地址哈希表,理想情况下键值对均分布在各个桶中,达到最佳查询效率;最差情况下所有键值对都存储到同一个桶中,时间复杂度退化至 $O(n)$ 。
如果哈希冲突过于频繁,哈希表的性能则会急剧劣化。如下图所示,对于链地址哈希表,理想情况下键值对均分布在各个桶中,达到最佳查询效率;最差情况下所有键值对都存储到同一个桶中,时间复杂度退化至 $O(n)$ 。
![哈希冲突的最佳与最差情况](hash_algorithm.assets/hash_collision_best_worst_condition.png)
![哈希冲突的最佳情况与最差情况](hash_algorithm.assets/hash_collision_best_worst_condition.png)
**键值对的分布情况由哈希函数决定**。回忆哈希函数的计算步骤,先计算哈希值,再对数组长度取模:
@@ -14,25 +14,25 @@ index = hash(key) % capacity
观察以上公式,当哈希表容量 `capacity` 固定时,**哈希算法 `hash()` 决定了输出值**,进而决定了键值对在哈希表中的分布情况。
这意味着,为了减小哈希冲突的发生概率,我们应当将注意力集中在哈希算法 `hash()` 的设计上。
这意味着,为了降低哈希冲突的发生概率,我们应当将注意力集中在哈希算法 `hash()` 的设计上。
## 哈希算法的目标
为了实现“既快又稳”的哈希表数据结构,哈希算法应包含以下特点。
为了实现“既快又稳”的哈希表数据结构,哈希算法应具备以下特点。
- **确定性**:对于相同的输入,哈希算法应始终产生相同的输出。这样才能确保哈希表是可靠的。
- **效率高**:计算哈希值的过程应该足够快。计算开销越小,哈希表的实用性越高。
- **均匀分布**:哈希算法应使得键值对均分布在哈希表中。分布越均,哈希冲突的概率就越低。
- **均匀分布**:哈希算法应使得键值对均分布在哈希表中。分布越均,哈希冲突的概率就越低。
实际上,哈希算法除了可以用于实现哈希表,还广泛应用于其他领域中。
- **密码存储**:为了保护用户密码的安全,系统通常不会直接存储用户的明文密码,而是存储密码的哈希值。当用户输入密码时,系统会对输入的密码计算哈希值,然后与存储的哈希值进行比较。如果两者匹配,那么密码就被视为正确。
- **数据完整性检查**:数据发送方可以计算数据的哈希值并将其一同发送;接收方可以重新计算接收到的数据的哈希值,并与接收到的哈希值进行比较。如果两者匹配,那么数据就被视为完整
- **数据完整性检查**:数据发送方可以计算数据的哈希值并将其一同发送;接收方可以重新计算接收到的数据的哈希值,并与接收到的哈希值进行比较。如果两者匹配,那么数据就被视为完整。
对于密码学的相关应用,为了防止从哈希值推导出原始密码等逆向工程,哈希算法需要具备更高等级的安全特性。
- **单向性**:无法通过哈希值反推出关于输入数据的任何信息。
- **抗碰撞性**:应当极其困难找到两个不同的输入,使得它们的哈希值相同。
- **抗碰撞性**:应当极难找到两个不同的输入,使得它们的哈希值相同。
- **雪崩效应**:输入的微小变化应当导致输出的显著且不可预测的变化。
请注意,**“均匀分布”与“抗碰撞性”是两个独立的概念**,满足均匀分布不一定满足抗碰撞性。例如,在随机输入 `key` 下,哈希函数 `key % 100` 可以产生均匀分布的输出。然而该哈希算法过于简单,所有后两位相等的 `key` 的输出都相同,因此我们可以很容易地从哈希值反推出可用的 `key` ,从而破解密码。
@@ -42,7 +42,7 @@ index = hash(key) % capacity
哈希算法的设计是一个需要考虑许多因素的复杂问题。然而对于某些要求不高的场景,我们也能设计一些简单的哈希算法。
- **加法哈希**:对输入的每个字符的 ASCII 码进行相加,将得到的总和作为哈希值。
- **乘法哈希**:利用乘法的不相关性,每轮乘以一个常数,将各个字符的 ASCII 码累积到哈希值中。
- **乘法哈希**:利用乘法的不相关性,每轮乘以一个常数,将各个字符的 ASCII 码累积到哈希值中。
- **异或哈希**:将输入数据的每个元素通过异或操作累积到一个哈希值中。
- **旋转哈希**:将每个字符的 ASCII 码累积到一个哈希值中,每次累积之前都会对哈希值进行旋转操作。
@@ -52,9 +52,9 @@ index = hash(key) % capacity
观察发现,每种哈希算法的最后一步都是对大质数 $1000000007$ 取模,以确保哈希值在合适的范围内。值得思考的是,为什么要强调对质数取模,或者说对合数取模的弊端是什么?这是一个有趣的问题。
先抛出结论:**当我们使用大质数作为模数,可以最大化地保证哈希值的均匀分布**。因为质数不与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。
先抛出结论:**使用大质数作为模数,可以最大化地保证哈希值的均匀分布**。因为质数不与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。
举个例子,假设我们选择合数 $9$ 作为模数,它可以被 $3$ 整除那么所有可以被 $3$ 整除的 `key` 都会被映射到 $0$、$3$、$6$ 这三个哈希值。
举个例子,假设我们选择合数 $9$ 作为模数,它可以被 $3$ 整除那么所有可以被 $3$ 整除的 `key` 都会被映射到 $0$、$3$、$6$ 这三个哈希值。
$$
\begin{aligned}
@@ -64,7 +64,7 @@ $$
\end{aligned}
$$
如果输入 `key` 恰好满足这种等差数列的数据分布,那么哈希值就会出现聚堆,从而加重哈希冲突。现在,假设将 `modulus` 替换为质数 $13$ ,由于 `key``modulus` 之间不存在公约数,输出的哈希值的均匀性会明显提升。
如果输入 `key` 恰好满足这种等差数列的数据分布,那么哈希值就会出现聚堆,从而加重哈希冲突。现在,假设将 `modulus` 替换为质数 $13$ ,由于 `key``modulus` 之间不存在公约数,因此输出的哈希值的均匀性会明显提升。
$$
\begin{aligned}
@@ -74,7 +74,7 @@ $$
\end{aligned}
$$
值得说明的是,如果能够保证 `key` 是随机均匀分布的,那么选择质数或者合数作为模数都可以,它们都能输出均匀分布的哈希值。而当 `key` 的分布存在某种周期性时,对合数取模更容易出现聚集现象。
值得说明的是,如果能够保证 `key` 是随机均匀分布的,那么选择质数或者合数作为模数都可以,它们都能输出均匀分布的哈希值。而当 `key` 的分布存在某种周期性时,对合数取模更容易出现聚集现象。
总而言之,我们通常选取质数作为模数,并且这个质数最好足够大,以尽可能消除周期性模式,提升哈希算法的稳健性。
@@ -82,12 +82,12 @@ $$
不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。
在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2、SHA3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。
在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2、SHA-3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。
近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。下表展示了在实际应用中常见的哈希算法。
- MD5 和 SHA-1 已多次被成功攻击,因此它们被各类安全应用弃用。
- SHA-2 系列中的 SHA-256 是最安全的哈希算法之一,仍未出现成功的攻击案例,因此常用在各类安全应用与协议中。
- SHA-2 系列中的 SHA-256 是最安全的哈希算法之一,仍未出现成功的攻击案例,因此常用在各类安全应用与协议中。
- SHA-3 相较 SHA-2 的实现开销更低、计算效率更高,但目前使用覆盖度不如 SHA-2 系列。
<p align="center"> 表 <id> &nbsp; 常见的哈希算法 </p>
@@ -95,7 +95,7 @@ $$
| | MD5 | SHA-1 | SHA-2 | SHA-3 |
| -------- | ------------------------------ | ---------------- | ---------------------------- | -------------------- |
| 推出时间 | 1992 | 1995 | 2002 | 2008 |
| 输出长度 | 128 bits | 160 bits | 256 / 512 bits | 224/256/384/512 bits |
| 输出长度 | 128 bits | 160 bits | 256/512 bits | 224/256/384/512 bits |
| 哈希冲突 | 较多 | 较多 | 很少 | 很少 |
| 安全等级 | 低,已被成功攻击 | 低,已被成功攻击 | 高 | 高 |
| 应用 | 已被弃用,仍用于数据完整性检查 | 已被弃用 | 加密货币交易验证、数字签名等 | 可用于替代 SHA-2 |
@@ -105,7 +105,7 @@ $$
我们知道,哈希表的 `key` 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法,用于计算哈希表中的桶索引。以 Python 为例,我们可以调用 `hash()` 函数来计算各种数据类型的哈希值。
- 整数和布尔量的哈希值就是其本身。
- 浮点数和字符串的哈希值计算较为复杂,有兴趣的同学请自行学习。
- 浮点数和字符串的哈希值计算较为复杂,有兴趣的读者请自行学习。
- 元组的哈希值是对其中每一个元素进行哈希,然后将这些哈希值组合起来,得到单一的哈希值。
- 对象的哈希值基于其内存地址生成。通过重写对象的哈希方法,可实现基于内容生成哈希值。
@@ -130,7 +130,7 @@ $$
str = "Hello 算法"
hash_str = hash(str)
# 字符串 Hello 算法 的哈希值为 4617003410720528961
# 字符串Hello 算法的哈希值为 4617003410720528961
tup = (12836, "小哈")
hash_tup = hash(tup)
@@ -158,7 +158,7 @@ $$
string str = "Hello 算法";
size_t hashStr = hash<string>()(str);
// 字符串 Hello 算法 的哈希值为 15466937326284535026
// 字符串Hello 算法的哈希值为 15466937326284535026
// 在 C++ 中,内置 std:hash() 仅提供基本数据类型的哈希值计算
// 数组、对象的哈希值计算需要自行实现
@@ -181,7 +181,7 @@ $$
String str = "Hello 算法";
int hashStr = str.hashCode();
// 字符串 Hello 算法 的哈希值为 -727081396
// 字符串Hello 算法的哈希值为 -727081396
Object[] arr = { 12836, "小哈" };
int hashTup = Arrays.hashCode(arr);
@@ -209,7 +209,7 @@ $$
string str = "Hello 算法";
int hashStr = str.GetHashCode();
// 字符串 Hello 算法 的哈希值为 -586107568;
// 字符串Hello 算法的哈希值为 -586107568;
object[] arr = [12836, "小哈"];
int hashTup = arr.GetHashCode();
@@ -243,7 +243,7 @@ $$
let str = "Hello 算法"
let hashStr = str.hashValue
// 字符串 Hello 算法 的哈希值为 -7850626797806988787
// 字符串Hello 算法的哈希值为 -7850626797806988787
let arr = [AnyHashable(12836), AnyHashable("小哈")]
let hashTup = arr.hashValue
@@ -283,7 +283,7 @@ $$
String str = "Hello 算法";
int hashStr = str.hashCode;
// 字符串 Hello 算法 的哈希值为 468167534
// 字符串Hello 算法的哈希值为 468167534
List arr = [12836, "小哈"];
int hashArr = arr.hashCode;
@@ -323,7 +323,7 @@ $$
let mut str_hasher = DefaultHasher::new();
str.hash(&mut str_hasher);
let hash_str = str_hasher.finish();
// 字符串 Hello 算法 的哈希值为 16092673739211250988
// 字符串Hello 算法的哈希值为 16092673739211250988
let arr = (&12836, &"小哈");
let mut tup_hasher = DefaultHasher::new();