Update 算法 - 符号表.md

补充二叉查找树 max 方法与红黑树性质,调整线性探测删除机制与使用率说明。新增线性探测删除配图,并重新绘制红黑树插入与旋转配图。
This commit is contained in:
CyC2018
2026-10-30 20:19:20 +08:00
parent 6f5635d1de
commit 69c6f4efb5
4 changed files with 59 additions and 27 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 114 KiB

+59 -27
View File
@@ -160,7 +160,7 @@ public class ListUnorderedST<Key, Value> implements UnorderedST<Key, Value> {
使用一对平行数组,一个存储键一个存储值。
二分查找的 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。
二分查找的 rank() 方法至关重要,它返回表中小于 key 的键的数量:当键在表中时,这就是该键的位置;当键不在表中时,这就是新键应插入的位置。例如 keys 为 [A, C, E, H] 时,rank(E) 和 rank(D) 都返回 2。
二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。
@@ -248,11 +248,11 @@ public class BinarySearchOrderedST<Key extends Comparable<Key>, Value> implement
## 二叉查找树
**二叉树** 是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一棵子二叉树。
**二叉树**是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一棵子二叉树。
<div align="center"> <img src="pics/st-binary-tree.png" width="300px"/> </div><br>
**二叉查找树** (BST)是一棵二叉树,并且每个节点的键都大于其左子树中所有节点的键,小于其右子树中所有节点的键。
**二叉查找树**(BST)是一棵二叉树,并且每个节点的键都大于其左子树中所有节点的键,小于其右子树中所有节点的键。
BST 有一个重要性质,就是它的中序遍历结果递增排序。
@@ -393,7 +393,7 @@ private Node floor(Node x, Key key) {
### 5. rank()
rank(key) 返回 key 的排名。
rank(key) 返回树中小于 key 的键的数量,也就是 key 从 0 开始的排名。
- 如果键和根节点的键相等,返回左子树的节点数;
- 如果小于,递归计算在左子树中的排名;
@@ -433,6 +433,15 @@ private Node min(Node x) {
return x;
return min(x.left);
}
// max() 与 min() 对称,沿右链接走到底
@Override
public Key max() {
Node x = root;
while (x.right != null)
x = x.right;
return x.key;
}
```
### 7. deleteMin()
@@ -457,8 +466,8 @@ public Node deleteMin(Node x) {
### 8. delete()
- 如果待删除的节点只有一个子树, 那么只需要让指向待删除节点的链接指向唯一的子树即可;
- 否则,让右子树的最小节点替换该节点。
- 如果待删除的节点最多只有一个子树,那么只需要让指向待删除节点的链接指向这个子树(或空链接)即可;
- 否则,用右子树的最小节点(后继节点)替换该节点,并从右子树中删除这个最小节点。
<div align="center"> <img src="pics/st-bst-delete.png" width="800px"/> </div><br>
@@ -496,28 +505,28 @@ private Node delete(Node x, Key key) {
```java
@Override
public List<Key> keys(Key l, Key h) {
return keys(root, l, h);
List<Key> list = new ArrayList<>();
keys(root, list, l, h);
return list;
}
private List<Key> keys(Node x, Key l, Key h) {
List<Key> list = new ArrayList<>();
private void keys(Node x, List<Key> list, Key l, Key h) {
if (x == null)
return list;
return;
int cmpL = l.compareTo(x.key);
int cmpH = h.compareTo(x.key);
if (cmpL < 0)
list.addAll(keys(x.left, l, h));
keys(x.left, list, l, h);
if (cmpL <= 0 && cmpH >= 0)
list.add(x.key);
if (cmpH > 0)
list.addAll(keys(x.right, l, h));
return list;
keys(x.right, list, l, h);
}
```
### 10. 分析
二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。
二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比,范围查询 keys() 还要再加上返回的键的数量。
## 2-3 查找树
@@ -541,23 +550,33 @@ private List<Key> keys(Node x, Key l, Key h) {
### 2. 性质
2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。
2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。如果一直分裂到根节点,根节点分裂成 3 个 2- 节点,树高加 1,所有空链接到根节点的距离同时加 1,因此树始终是完美平衡的。
2-3 查找树的查找和插入操作复杂度和插入顺序无关,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。
## 红黑树
红黑树是 2-3 查找树,但它不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
本节介绍的是左倾红黑树(Left-Leaning Red-Black Tree),它用二叉查找树表示 2-3 查找树,不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
<div align="center"> <img src="pics/st-rb-23.png" width="520px"/> </div><br>
红黑树具有以下性质:
一般所说的红黑树(例如 java.util.TreeMap 的实现)有以下五条性质:
- 每个节点是红色或黑色;
- 根节点是黑色;
- 叶子节点(空节点)是黑色;
- 红色节点的两个子节点都是黑色;
- 从任一节点到其后代叶子节点的每条路径上,黑色节点的数量相同。
它对应 2-3-4 树,允许红色的右子节点。左倾红黑树在此基础上要求红链接都为左链接,使它与 2-3 树一一对应,实现的代码更短。
左倾红黑树具有以下性质:
- 红链接都为左链接;
- 没有任何一个节点同时和两条红链接相连;
- 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。
画红黑树时可以将红链接画平。
画红黑树时可以将红链接画平。实现时把链接的颜色保存在它指向的节点中:节点的 color 为红色,表示父节点指向它的链接是红链接。
<div align="center"> <img src="pics/st-rb-flat.png" width="680px"/> </div><br>
@@ -575,6 +594,8 @@ public class RedBlackBST<Key extends Comparable<Key>, Value> extends BST<Key, Va
}
```
下面只实现了插入。继承自二叉查找树的 delete()、deleteMin() 不维护链接的颜色,会破坏黑色平衡,红黑树的删除需要另外实现。
### 1. 左旋转
因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。
@@ -684,7 +705,7 @@ private Node put(Node x, Key key, Value value) {
## 哈希表
哈希表类似于数组,可以把哈希表的哈希值看成数组的索引值。访问哈希表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。
哈希表类似于数组,可以把键的哈希值看成数组的索引值。访问哈希表和访问数组元素一样快速,它可以在平均常数时间内实现查找和插入操作。
由于无法通过哈希值知道键的大小关系,因此哈希表无法实现有序性操作。
@@ -696,8 +717,8 @@ private Node put(Node x, Key key, Value value) {
哈希函数应该满足以下三个条件:
- 一致性:相等的键应当有相等的 hash 值,两个键相等表示调用 equals() 返回的值相等。
- 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回。
- 一致性:相等的键应当有相等的 hash 值,两个键相等表示调用 equals() 返回 true;
- 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回;
- 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,如果不能满足这个条件,有可能产生很多冲突,从而导致哈希表的性能下降。
除留余数法可以将整数映射到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 即可得到一个 [0, M-1] 之间的 hash 值。注意 M 最好是一个素数,否则无法利用键包含的所有信息。例如 M 为 10<sup>k</sup>,那么只能利用键的后 k 位。
@@ -760,15 +781,15 @@ public class Transaction {
查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。
对于 N 个键,M 条链表 (N\>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。
对于 N 个键,M 条链表(N > M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。
<div align="center"> <img src="pics/st-hash-chaining.png" width="560px"/> </div><br>
### 3. 线性探测法
线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。
线性探测法使用空位来解决冲突,当冲突发生时,依次检查下一个位置(到末尾后回到 0),直到找到空位来存储冲突的键。
使用线性探测法,数组的大小 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>
@@ -798,6 +819,11 @@ public class LinearProbingHashST<Key, Value> implements UnorderedST<Key, Value>
private int hash(Key key) {
return (key.hashCode() & 0x7fffffff) % M;
}
@Override
public int size() {
return N;
}
}
```
@@ -837,7 +863,9 @@ private void putInternal(Key key, Value value) {
##### 3.3 删除
删除操作应当将右侧所有相邻的键值对重新插入哈希表中。
删除操作应当将右侧所有相邻的键值对重新插入哈希表中。这是因为查找遇到空位就停止,直接把被删除的位置置空,会把一个聚簇切成两段,后半段中的键就再也找不到了。
<div align="center"> <img src="pics/st-hash-probe-delete.png" width="660px"/> </div><br>
```java
public void delete(Key key) {
@@ -875,7 +903,7 @@ public void delete(Key key) {
<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 探测次数增长越快。因此下面的代码在 N 达到 M/2 时把数组扩大一倍,降到 M/8 时缩小一半,使 α 保持在 1/8 到 1/2 之间。
```java
private void resize() {
@@ -910,12 +938,14 @@ private void resize(int cap) {
| 拉链法实现的哈希表 | N/M | N/M | no |
| 线性探测法实现的哈希表 | 1 | 1 | no |
表中二叉查找树和哈希表是平均情况,最坏情况下(键按顺序插入、大量冲突)都与 N 成正比;2-3 查找树(红黑树)最坏也是 logN。
应当优先考虑哈希表,当需要有序性操作时使用红黑树。
### 2. Java 的符号表实现
- java.util.TreeMap:红黑树
- java.util.HashMap:拉链法的哈希表
- java.util.HashMap:拉链法的哈希表(JDK 8 起,链表过长时转为红黑树)
### 3. 稀疏向量乘法
@@ -949,3 +979,5 @@ public class SparseVector {
- Sedgewick R, Wayne K. 算法(第 4 版)[M]. 人民邮电出版社, 2012.
- [Algorithms, 4th Edition](https://algs4.cs.princeton.edu/home/)
- Cormen T H, Leiserson C E, Rivest R L, Stein C. 算法导论(原书第 3 版)[M]. 机械工业出版社, 2013.
- [TreeMap (Java SE 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/TreeMap.html)