diff --git a/notes/pics/st-hash-probe-delete.png b/notes/pics/st-hash-probe-delete.png new file mode 100644 index 00000000..81ee04c1 Binary files /dev/null and b/notes/pics/st-hash-probe-delete.png differ diff --git a/notes/pics/st-rb-insert.png b/notes/pics/st-rb-insert.png index e8b12d92..012626ab 100644 Binary files a/notes/pics/st-rb-insert.png and b/notes/pics/st-rb-insert.png differ diff --git a/notes/pics/st-rb-rotate-right.png b/notes/pics/st-rb-rotate-right.png index d53a0d6b..d248c018 100644 Binary files a/notes/pics/st-rb-rotate-right.png and b/notes/pics/st-rb-rotate-right.png differ diff --git a/notes/算法 - 符号表.md b/notes/算法 - 符号表.md index df4e9f91..426e94ef 100644 --- a/notes/算法 - 符号表.md +++ b/notes/算法 - 符号表.md @@ -160,7 +160,7 @@ public class ListUnorderedST implements UnorderedST { 使用一对平行数组,一个存储键一个存储值。 -二分查找的 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 +二分查找的 rank() 方法至关重要,它返回表中小于 key 的键的数量:当键在表中时,这就是该键的位置;当键不在表中时,这就是新键应插入的位置。例如 keys 为 [A, C, E, H] 时,rank(E) 和 rank(D) 都返回 2。 二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 @@ -248,11 +248,11 @@ public class BinarySearchOrderedST, Value> implement ## 二叉查找树 -**二叉树** 是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一棵子二叉树。 +**二叉树**是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一棵子二叉树。

-**二叉查找树** (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() -- 如果待删除的节点只有一个子树, 那么只需要让指向待删除节点的链接指向唯一的子树即可; -- 否则,让右子树的最小节点替换该节点。 +- 如果待删除的节点最多只有一个子树,那么只需要让指向待删除节点的链接指向这个子树(或空链接)即可; +- 否则,用右子树的最小节点(后继节点)替换该节点,并从右子树中删除这个最小节点。

@@ -496,28 +505,28 @@ private Node delete(Node x, Key key) { ```java @Override public List keys(Key l, Key h) { - return keys(root, l, h); + List list = new ArrayList<>(); + keys(root, list, l, h); + return list; } -private List keys(Node x, Key l, Key h) { - List list = new ArrayList<>(); +private void keys(Node x, List 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 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- 节点,而黑色则是普通链接。

-红黑树具有以下性质: +一般所说的红黑树(例如 java.util.TreeMap 的实现)有以下五条性质: + +- 每个节点是红色或黑色; +- 根节点是黑色; +- 叶子节点(空节点)是黑色; +- 红色节点的两个子节点都是黑色; +- 从任一节点到其后代叶子节点的每条路径上,黑色节点的数量相同。 + +它对应 2-3-4 树,允许红色的右子节点。左倾红黑树在此基础上要求红链接都为左链接,使它与 2-3 树一一对应,实现的代码更短。 + +左倾红黑树具有以下性质: - 红链接都为左链接; - 没有任何一个节点同时和两条红链接相连; - 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。 -画红黑树时可以将红链接画平。 +画红黑树时可以将红链接画平。实现时把链接的颜色保存在它指向的节点中:节点的 color 为红色,表示父节点指向它的链接是红链接。

@@ -575,6 +594,8 @@ public class RedBlackBST, Value> extends BSTk,那么只能利用键的后 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。

### 3. 线性探测法 -线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。 +线性探测法使用空位来解决冲突,当冲突发生时,依次检查下一个位置(到末尾后回到 0),直到找到空位来存储冲突的键。 -使用线性探测法,数组的大小 M 应当大于键的个数 N(M\>N)。 +使用线性探测法,数组的大小 M 应当大于键的个数 N(M > N)。

@@ -798,6 +819,11 @@ public class LinearProbingHashST implements UnorderedST 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 删除 -删除操作应当将右侧所有相邻的键值对重新插入哈希表中。 +删除操作应当将右侧所有相邻的键值对重新插入哈希表中。这是因为查找遇到空位就停止,直接把被删除的位置置空,会把一个聚簇切成两段,后半段中的键就再也找不到了。 + +

```java public void delete(Key key) { @@ -875,7 +903,7 @@ public void delete(Key key) {

-α = 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)