Update 算法 - 符号表.md

重新绘制配图,统一哈希表的术语,更新 hashCode 和红黑树性质的描述,补充参考资料。
This commit is contained in:
CyC2018
2026-10-28 03:19:03 +08:00
parent 16faf18d75
commit d047784c2e
18 changed files with 48 additions and 43 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

+48 -43
View File
@@ -25,14 +25,15 @@
* [3. 颜色转换](#3-颜色转换)
* [4. 插入](#4-插入)
* [5. 分析](#5-分析)
* [散列表](#散列表)
* [1. 散列函数](#1-散列函数)
* [哈希表](#哈希表)
* [1. 哈希函数](#1-哈希函数)
* [2. 拉链法](#2-拉链法)
* [3. 线性探测法](#3-线性探测法)
* [小结](#小结)
* [1. 符号表算法比较](#1-符号表算法比较)
* [2. Java 的符号表实现](#2-java-的符号表实现)
* [3. 稀疏向量乘法](#3-稀疏向量乘法)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -249,13 +250,13 @@ public class BinarySearchOrderedST<Key extends Comparable<Key>, Value> implement
**二叉树** 是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一棵子二叉树。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c11528f6-fc71-4a2b-8d2f-51b8954c38f1.jpg" width="180"/> </div><br>
<div align="center"> <img src="pics/st-binary-tree.png" width="300px"/> </div><br>
**二叉查找树** (BST)是一棵二叉树,并且每个节点的值都大于等于其左子树中的所有节点的值而小于等于右子树的所有节点的值。
**二叉查找树** (BST)是一棵二叉树,并且每个节点的键都大于其左子树中所有节点的键,小于其右子树中所有节点的键。
BST 有一个重要性质,就是它的中序遍历结果递增排序。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ef552ae3-ae0d-4217-88e6-99cbe8163f0c.jpg" width="200"/> </div><br>
<div align="center"> <img src="pics/st-bst-inorder.png" width="420px"/> </div><br>
基本数据结构:
@@ -329,7 +330,7 @@ private Value get(Node x, Key key) {
当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接指向该节点,使得该节点正确地链接到树中。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/58b70113-3876-49af-85a9-68eb00a72d59.jpg" width="200"/> </div><br>
<div align="center"> <img src="pics/st-bst-put.png" width="470px"/> </div><br>
```java
@Override
@@ -358,11 +359,9 @@ private Node put(Node x, Key key, Value value) {
最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c395a428-827c-405b-abd7-8a069316f583.jpg" width="200"/> </div><br>
在最坏的情况下,树的高度为 N。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5ea609cb-8ad4-4c4c-aee6-45a40a81794a.jpg" width="200"/> </div><br>
<div align="center"> <img src="pics/st-bst-shape.png" width="560px"/> </div><br>
### 4. floor()
@@ -440,7 +439,7 @@ private Node min(Node x) {
令指向最小节点的链接指向最小节点的右子树。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/31b7e8de-ed11-4f69-b5fd-ba454120ac31.jpg" width="450"/> </div><br>
<div align="center"> <img src="pics/st-bst-deletemin.png" width="800px"/> </div><br>
```java
public void deleteMin() {
@@ -461,7 +460,7 @@ public Node deleteMin(Node x) {
- 如果待删除的节点只有一个子树, 那么只需要让指向待删除节点的链接指向唯一的子树即可;
- 否则,让右子树的最小节点替换该节点。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/23b9d625-ef28-42b5-bb22-d7aedd007e16.jpg" width="400"/> </div><br>
<div align="center"> <img src="pics/st-bst-delete.png" width="800px"/> </div><br>
```java
public void delete(Key key) {
@@ -524,7 +523,7 @@ private List<Key> keys(Node x, Key l, Key h) {
2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树平衡。一棵完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1097658b-c0e6-4821-be9b-25304726a11c.jpg" width="160px"/> </div><br>
<div align="center"> <img src="pics/st-23-tree.png" width="440px"/> </div><br>
### 1. 插入操作
@@ -534,11 +533,11 @@ private List<Key> keys(Node x, Key l, Key h) {
- 如果插入到 2- 节点上,那么直接将新节点和原来的节点组成 3- 节点即可。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c6f9930-8704-4a54-af23-19f9ca3e48b0.jpg" width="350"/> </div><br>
<div align="center"> <img src="pics/st-23-insert-2node.png" width="560px"/> </div><br>
- 如果是插入到 3- 节点上,就会产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。
- 如果是插入到 3- 节点上,就会产生一个临时 4- 节点,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7002c01b-1ed5-475a-9e5f-5fc8a4cdbcc0.jpg" width="460"/> </div><br>
<div align="center"> <img src="pics/st-23-insert-3node.png" width="640px"/> </div><br>
### 2. 性质
@@ -550,16 +549,17 @@ private List<Key> keys(Node x, Key l, Key h) {
红黑树是 2-3 查找树,但它不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f1912ba6-6402-4321-9aa8-13d32fd121d1.jpg" width="240"/> </div><br>
<div align="center"> <img src="pics/st-rb-23.png" width="520px"/> </div><br>
红黑树具有以下性质:
- 红链接都为左链接;
- 没有任何一个节点同时和两条红链接相连;
- 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。
画红黑树时可以将红链接画平。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f5cb6028-425d-4939-91eb-cca9dd6b6c6c.jpg" width="220"/> </div><br>
<div align="center"> <img src="pics/st-rb-flat.png" width="680px"/> </div><br>
```java
public class RedBlackBST<Key extends Comparable<Key>, Value> extends BST<Key, Value> {
@@ -579,7 +579,7 @@ public class RedBlackBST<Key extends Comparable<Key>, Value> extends BST<Key, Va
因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f4d534ab-0092-4a81-9e5b-ae889b9a72be.jpg" width="480"/> </div><br>
<div align="center"> <img src="pics/st-rb-rotate-left.png" width="600px"/> </div><br>
```java
public Node rotateLeft(Node h) {
@@ -598,7 +598,7 @@ public Node rotateLeft(Node h) {
进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/63c8ffea-a9f2-4ebe-97d1-d71be71246f9.jpg" width="480"/> </div><br>
<div align="center"> <img src="pics/st-rb-rotate-right.png" width="600px"/> </div><br>
```java
public Node rotateRight(Node h) {
@@ -617,7 +617,7 @@ public Node rotateRight(Node h) {
一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/094b279a-b2db-4be7-87a3-b2a039c7448e.jpg" width="270"/> </div><br>
<div align="center"> <img src="pics/st-rb-flip.png" width="460px"/> </div><br>
```java
void flipColors(Node h) {
@@ -635,7 +635,7 @@ void flipColors(Node h) {
- 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转;
- 如果左右子节点均为红色的,进行颜色转换。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4c457532-550b-4eca-b881-037b84b4934b.jpg" width="430"/> </div><br>
<div align="center"> <img src="pics/st-rb-insert.png" width="820px"/> </div><br>
```java
@Override
@@ -672,7 +672,7 @@ private Node put(Node x, Key key, Value value) {
可以看到该插入操作和二叉查找树的插入操作类似,只是在最后加入了旋转和颜色变换操作即可。
根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1.
根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1。
### 5. 分析
@@ -680,31 +680,31 @@ private Node put(Node x, Key key, Value value) {
红黑树大多数的操作所需要的时间都是对数级别的。
## 散列表
## 哈希表
散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。
哈希表类似于数组,可以把哈希表的哈希值看成数组的索引值。访问哈希表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。
由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。
由于无法通过哈希值知道键的大小关系,因此哈希表无法实现有序性操作。
### 1. 散列函数
### 1. 哈希函数
对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。
对于一个大小为 M 的哈希表,哈希函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。
散列表存在冲突,也就是两个不同的键可能有相同的 hash 值。
哈希表存在冲突,也就是两个不同的键可能有相同的 hash 值。
散列函数应该满足以下三个条件:
哈希函数应该满足以下三个条件:
- 一致性:相等的键应当有相等的 hash 值,两个键相等表示调用 equals() 返回的值相等。
- 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回。
- 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,如果不能满足这个条件,有可能产生很多冲突,从而导致散列表的性能下降。
- 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,如果不能满足这个条件,有可能产生很多冲突,从而导致哈希表的性能下降。
除留余数法可以将整数散列到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 最好是一个素数,否则无法利用键包含的所有信息。例如 M 为 10<sup>k</sup>,那么只能利用键的后 k 位。
除留余数法可以将整数映射到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 最好是一个素数,否则无法利用键包含的所有信息。例如 M 为 10<sup>k</sup>,那么只能利用键的后 k 位。
对于其它数,可以将其转换成整数的形式,然后利用除留余数法。例如对于浮点数,可以将其的二进制形式转换成整数。
对于多部分组合的类型,每个部分都需要计算 hash 值,这些 hash 值都具有同等重要的地位。为了达到这个目的,可以将该类型看成 R 进制的整数,每个部分都具有不同的权值。
例如,字符串的散列函数实现如下:
例如,字符串的哈希函数实现如下:
```java
int hash = 0;
@@ -720,7 +720,7 @@ int hash = (((day * R + month) % M) * R + year) % M;
R 通常取 31。
Java 中的 hashCode() 实现了哈希函数,但是默认使用对象的内存地址值。在使用 hashCode() 时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。
Java 中的 hashCode() 实现了哈希函数,在使用 hashCode() 时,应当结合除留余数法来使用。hashCode() 返回 32 位整数,String 等类重写的 hashCode() 可能是负数,例如 "polygenelubricants".hashCode() 为 -2147483648,因此应当屏蔽符号位得到 31 位的非负整数,再使用除留余数法。需要注意的是,Object 默认的 hashCode() 与对象的内存地址无关,HotSpot 默认用线程本地的随机数生成(JDK 21 中 -XX:hashCode 的默认值为 5)。
```java
int hash = (x.hashCode() & 0x7fffffff) % M;
@@ -760,13 +760,13 @@ public class Transaction {
对于 N 个键,M 条链表 (N\>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cbbfe06c-f0cb-47c4-bf7b-2780aebd98b2.png" width="330px"> </div><br>
<div align="center"> <img src="pics/st-hash-chaining.png" width="560px"/> </div><br>
### 3. 线性探测法
线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。
使用线性探测法,数组的大小 M 应当大于键的个数 N(M\>N)。
使用线性探测法,数组的大小 M 应当大于键的个数 N(M\>N)。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0dbc4f7d-05c9-4aae-8065-7b7ea7e9709e.gif" width="350px"> </div><br>
@@ -835,7 +835,7 @@ private void putInternal(Key key, Value value) {
##### 3.3 删除
删除操作应当将右侧所有相邻的键值对重新插入散列表中。
删除操作应当将右侧所有相邻的键值对重新插入哈希表中。
```java
public void delete(Key key) {
@@ -871,9 +871,9 @@ public void delete(Key key) {
线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。例如下图中 2\~4 位置就是一个聚簇。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ace20410-f053-4c4a-aca4-2c603ff11bbe.png" width="340px"> </div><br>
<div align="center"> <img src="pics/st-hash-cluster.png" width="500px"/> </div><br>
α = N/M,把 α 称为使用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。
α = N/M,把 α 称为使用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。为了保证哈希表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。
```java
private void resize() {
@@ -901,19 +901,19 @@ private void resize(int cap) {
| 算法 | 插入 | 查找 | 是否有序 |
| :---: | :---: | :---: | :---: |
| 链表实现的无序符号表 | N | N | yes |
| 链表实现的无序符号表 | N | N | no |
| 二分查找实现的有序符号表 | N | logN | yes |
| 二叉查找树 | logN | logN | yes |
| 2-3 查找树 | logN | logN | yes |
| 拉链法实现的散列表 | N/M | N/M | no |
| 线性探测法实现的散列表 | 1 | 1 | no |
| 拉链法实现的哈希表 | N/M | N/M | no |
| 线性探测法实现的哈希表 | 1 | 1 | no |
应当优先考虑散列表,当需要有序性操作时使用红黑树。
应当优先考虑哈希表,当需要有序性操作时使用红黑树。
### 2. Java 的符号表实现
- java.util.TreeMap:红黑树
- java.util.HashMap:拉链法的散列表
- java.util.HashMap:拉链法的哈希表
### 3. 稀疏向量乘法
@@ -942,3 +942,8 @@ public class SparseVector {
}
}
```
## 参考资料
- Sedgewick R, Wayne K. 算法(第 4 版)[M]. 人民邮电出版社, 2012.
- [Algorithms, 4th Edition](https://algs4.cs.princeton.edu/home/)