CS-Notes/docs/notes/算法 - 符号表.md

948 lines
28 KiB
Java
Raw Normal View History

2019-04-21 10:36:08 +08:00
<!-- GFM-TOC -->
2019-03-27 20:57:37 +08:00
* [前言](#前言)
* [初级实现](#初级实现)
* [1. 链表实现无序符号表](#1-链表实现无序符号表)
* [2. 二分查找实现有序符号表](#2-二分查找实现有序符号表)
* [二叉查找树](#二叉查找树)
* [1. get()](#1-get)
* [2. put()](#2-put)
* [3. 分析](#3-分析)
* [4. floor()](#4-floor)
* [5. rank()](#5-rank)
* [6. min()](#6-min)
* [7. deleteMin()](#7-deletemin)
* [8. delete()](#8-delete)
* [9. keys()](#9-keys)
* [10. 分析](#10-分析)
* [2-3 查找树](#2-3-查找树)
* [1. 插入操作](#1-插入操作)
* [2. 性质](#2-性质)
* [红黑树](#红黑树)
* [1. 左旋转](#1-左旋转)
* [2. 右旋转](#2-右旋转)
* [3. 颜色转换](#3-颜色转换)
* [4. 插入](#4-插入)
* [5. 分析](#5-分析)
* [散列表](#散列表)
* [1. 散列函数](#1-散列函数)
* [2. 拉链法](#2-拉链法)
* [3. 线性探测法](#3-线性探测法)
* [小结](#小结)
* [1. 符号表算法比较](#1-符号表算法比较)
* [2. Java 的符号表实现](#2-java-的符号表实现)
* [3. 稀疏向量乘法](#3-稀疏向量乘法)
2019-04-21 10:36:08 +08:00
<!-- GFM-TOC -->
2019-03-27 20:57:37 +08:00
# 前言
符号表Symbol Table是一种存储键值对的数据结构可以支持快速查找操作
符号表分为有序和无序两种有序符号表主要指支持 min()max() 等根据键的大小关系来实现的操作
有序符号表的键需要实现 Comparable 接口
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public interface UnorderedST<Key, Value> {
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
int size();
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
Value get(Key key);
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
void put(Key key, Value value);
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
void delete(Key key);
2019-03-08 23:06:28 +08:00
}
```
```java
2019-03-27 20:57:37 +08:00
public interface OrderedST<Key extends Comparable<Key>, Value> {
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
int size();
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
void put(Key key, Value value);
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
Value get(Key key);
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
Key min();
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
Key max();
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
int rank(Key key);
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
List<Key> keys(Key l, Key h);
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
# 初级实现
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 1. 链表实现无序符号表
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public class ListUnorderedST<Key, Value> implements UnorderedST<Key, Value> {
private Node first;
private class Node {
Key key;
Value value;
Node next;
Node(Key key, Value value, Node next) {
this.key = key;
this.value = value;
this.next = next;
}
}
@Override
public int size() {
int cnt = 0;
Node cur = first;
while (cur != null) {
cnt++;
cur = cur.next;
}
return cnt;
}
@Override
public void put(Key key, Value value) {
Node cur = first;
// 如果在链表中找到节点的键等于 key 就更新这个节点的值为 value
while (cur != null) {
if (cur.key.equals(key)) {
cur.value = value;
return;
}
cur = cur.next;
}
// 否则使用头插法插入一个新节点
first = new Node(key, value, first);
}
@Override
public void delete(Key key) {
if (first == null)
return;
if (first.key.equals(key))
first = first.next;
Node pre = first, cur = first.next;
while (cur != null) {
if (cur.key.equals(key)) {
pre.next = cur.next;
return;
}
pre = pre.next;
cur = cur.next;
}
}
@Override
public Value get(Key key) {
Node cur = first;
while (cur != null) {
if (cur.key.equals(key))
return cur.value;
cur = cur.next;
}
return null;
}
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 2. 二分查找实现有序符号表
2019-03-08 23:06:28 +08:00
使用一对平行数组一个存储键一个存储值
2019-03-27 20:57:37 +08:00
二分查找的 rank() 方法至关重要当键在表中时它能够知道该键的位置当键不在表中时它也能知道在何处插入新键
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
二分查找最多需要 logN+1 次比较使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的但是插入操作需要移动数组元素是线性级别的
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public class BinarySearchOrderedST<Key extends Comparable<Key>, Value> implements OrderedST<Key, Value> {
private Key[] keys;
private Value[] values;
private int N = 0;
public BinarySearchOrderedST(int capacity) {
keys = (Key[]) new Comparable[capacity];
values = (Value[]) new Object[capacity];
}
@Override
public int size() {
return N;
}
@Override
public int rank(Key key) {
int l = 0, h = N - 1;
while (l <= h) {
int m = l + (h - l) / 2;
int cmp = key.compareTo(keys[m]);
if (cmp == 0)
return m;
else if (cmp < 0)
h = m - 1;
else
l = m + 1;
}
return l;
}
@Override
public List<Key> keys(Key l, Key h) {
int index = rank(l);
List<Key> list = new ArrayList<>();
while (keys[index].compareTo(h) <= 0) {
list.add(keys[index]);
index++;
}
return list;
}
@Override
public void put(Key key, Value value) {
int index = rank(key);
// 如果找到已经存在的节点键为 key就更新这个节点的值为 value
if (index < N && keys[index].compareTo(key) == 0) {
values[index] = value;
return;
}
// 否则在数组中插入新的节点,需要先将插入位置之后的元素都向后移动一个位置
for (int j = N; j > index; j--) {
keys[j] = keys[j - 1];
values[j] = values[j - 1];
}
keys[index] = key;
values[index] = value;
N++;
}
@Override
public Value get(Key key) {
int index = rank(key);
if (index < N && keys[index].compareTo(key) == 0)
return values[index];
return null;
}
@Override
public Key min() {
return keys[0];
}
@Override
public Key max() {
return keys[N - 1];
}
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
# 二叉查找树
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
**二叉树** 是一个空链接或者是一个有左右两个链接的节点每个链接都指向一颗子二叉树
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c11528f6-fc71-4a2b-8d2f-51b8954c38f1.jpg" width="180"/> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
**二叉查找树** BST是一颗二叉树并且每个节点的值都大于等于其左子树中的所有节点的值而小于等于右子树的所有节点的值
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
BST 有一个重要性质就是它的中序遍历结果递增排序
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ef552ae3-ae0d-4217-88e6-99cbe8163f0c.jpg" width="200"/> </div><br>
2019-03-08 23:06:28 +08:00
基本数据结构
```java
2019-03-27 20:57:37 +08:00
public class BST<Key extends Comparable<Key>, Value> implements OrderedST<Key, Value> {
protected Node root;
protected class Node {
Key key;
Value val;
Node left;
Node right;
// 以该节点为根的子树节点总数
int N;
// 红黑树中使用
boolean color;
Node(Key key, Value val, int N) {
this.key = key;
this.val = val;
this.N = N;
}
}
@Override
public int size() {
return size(root);
}
private int size(Node x) {
if (x == null)
return 0;
return x.N;
}
protected void recalculateSize(Node x) {
x.N = size(x.left) + size(x.right) + 1;
}
2019-03-08 23:06:28 +08:00
}
```
为了方便绘图下文中二叉树的空链接不画出来
2019-03-27 20:57:37 +08:00
## 1. get()
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
- 如果树是空的则查找未命中
- 如果被查找的键和根节点的键相等查找命中
- 否则递归地在子树中查找如果被查找的键较小就在左子树中查找较大就在右子树中查找
2019-03-08 23:06:28 +08:00
```java
@Override
2019-03-27 20:57:37 +08:00
public Value get(Key key) {
return get(root, key);
2019-03-08 23:06:28 +08:00
}
2019-03-27 20:57:37 +08:00
private Value get(Node x, Key key) {
if (x == null)
return null;
int cmp = key.compareTo(x.key);
if (cmp == 0)
return x.val;
else if (cmp < 0)
return get(x.left, key);
else
return get(x.right, key);
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 2. put()
2019-03-08 23:06:28 +08:00
当插入的键不存在于树中需要创建一个新节点并且更新上层节点的链接指向该节点使得该节点正确地链接到树中
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/58b70113-3876-49af-85a9-68eb00a72d59.jpg" width="200"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
@Override
public void put(Key key, Value value) {
root = put(root, key, value);
}
private Node put(Node x, Key key, Value value) {
if (x == null)
return new Node(key, value, 1);
int cmp = key.compareTo(x.key);
if (cmp == 0)
x.val = value;
else if (cmp < 0)
x.left = put(x.left, key, value);
else
x.right = put(x.right, key, value);
recalculateSize(x);
return x;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 3. 分析
2019-03-08 23:06:28 +08:00
二叉查找树的算法运行时间取决于树的形状而树的形状又取决于键被插入的先后顺序
2019-03-27 20:57:37 +08:00
最好的情况下树是完全平衡的每条空链接和根节点的距离都为 logN
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c395a428-827c-405b-abd7-8a069316f583.jpg" width="200"/> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
在最坏的情况下树的高度为 N
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5ea609cb-8ad4-4c4c-aee6-45a40a81794a.jpg" width="200"/> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 4. floor()
2019-03-08 23:06:28 +08:00
floor(key)小于等于键的最大键
2019-03-27 20:57:37 +08:00
- 如果键小于根节点的键那么 floor(key) 一定在左子树中
- 如果键大于根节点的键需要先判断右子树中是否存在 floor(key)如果存在就返回否则根节点就是 floor(key)
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public Key floor(Key key) {
Node x = floor(root, key);
if (x == null)
return null;
return x.key;
}
private Node floor(Node x, Key key) {
if (x == null)
return null;
int cmp = key.compareTo(x.key);
if (cmp == 0)
return x;
if (cmp < 0)
return floor(x.left, key);
Node t = floor(x.right, key);
return t != null ? t : x;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 5. rank()
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
rank(key) 返回 key 的排名
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
- 如果键和根节点的键相等返回左子树的节点数
- 如果小于递归计算在左子树中的排名
- 如果大于递归计算在右子树中的排名加上左子树的节点数再加上 1根节点
2019-03-08 23:06:28 +08:00
```java
@Override
2019-03-27 20:57:37 +08:00
public int rank(Key key) {
return rank(key, root);
2019-03-08 23:06:28 +08:00
}
2019-03-27 20:57:37 +08:00
private int rank(Key key, Node x) {
if (x == null)
return 0;
int cmp = key.compareTo(x.key);
if (cmp == 0)
return size(x.left);
else if (cmp < 0)
return rank(key, x.left);
else
return 1 + size(x.left) + rank(key, x.right);
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 6. min()
2019-03-08 23:06:28 +08:00
```java
@Override
2019-03-27 20:57:37 +08:00
public Key min() {
return min(root).key;
2019-03-08 23:06:28 +08:00
}
2019-03-27 20:57:37 +08:00
private Node min(Node x) {
if (x == null)
return null;
if (x.left == null)
return x;
return min(x.left);
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 7. deleteMin()
2019-03-08 23:06:28 +08:00
令指向最小节点的链接指向最小节点的右子树
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/31b7e8de-ed11-4f69-b5fd-ba454120ac31.jpg" width="450"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public void deleteMin() {
root = deleteMin(root);
2019-03-08 23:06:28 +08:00
}
2019-03-27 20:57:37 +08:00
public Node deleteMin(Node x) {
if (x.left == null)
return x.right;
x.left = deleteMin(x.left);
recalculateSize(x);
return x;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 8. delete()
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
- 如果待删除的节点只有一个子树 那么只需要让指向待删除节点的链接指向唯一的子树即可
- 否则让右子树的最小节点替换该节点
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/23b9d625-ef28-42b5-bb22-d7aedd007e16.jpg" width="400"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public void delete(Key key) {
root = delete(root, key);
}
private Node delete(Node x, Key key) {
if (x == null)
return null;
int cmp = key.compareTo(x.key);
if (cmp < 0)
x.left = delete(x.left, key);
else if (cmp > 0)
x.right = delete(x.right, key);
else {
if (x.right == null)
return x.left;
if (x.left == null)
return x.right;
Node t = x;
x = min(t.right);
x.right = deleteMin(t.right);
x.left = t.left;
}
recalculateSize(x);
return x;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 9. keys()
2019-03-08 23:06:28 +08:00
利用二叉查找树中序遍历的结果为递增的特点
```java
@Override
2019-03-27 20:57:37 +08:00
public List<Key> keys(Key l, Key h) {
return keys(root, l, h);
}
private List<Key> keys(Node x, Key l, Key h) {
List<Key> list = new ArrayList<>();
if (x == null)
return list;
int cmpL = l.compareTo(x.key);
int cmpH = h.compareTo(x.key);
if (cmpL < 0)
list.addAll(keys(x.left, l, h));
if (cmpL <= 0 && cmpH >= 0)
list.add(x.key);
if (cmpH > 0)
list.addAll(keys(x.right, l, h));
return list;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 10. 分析
2019-03-08 23:06:28 +08:00
二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比
2019-03-27 20:57:37 +08:00
# 2-3 查找树
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
2-3 查找树引入了 2- 节点和 3- 节点目的是为了让树平衡一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1097658b-c0e6-4821-be9b-25304726a11c.jpg" width="160px"/> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 1. 插入操作
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
插入操作和 BST 的插入操作有很大区别BST 的插入操作是先进行一次未命中的查找然后再将节点插入到对应的空链接上但是 2-3 查找树如果也这么做的话那么就会破坏了平衡性它是将新节点插入到叶子节点上
2019-03-08 23:06:28 +08:00
根据叶子节点的类型不同有不同的处理方式
2019-03-27 20:57:37 +08:00
- 如果插入到 2- 节点上那么直接将新节点和原来的节点组成 3- 节点即可
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c6f9930-8704-4a54-af23-19f9ca3e48b0.jpg" width="350"/> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
- 如果是插入到 3- 节点上就会产生一个临时 4- 节点时需要将 4- 节点分裂成 3 2- 节点并将中间的 2- 节点移到上层节点中如果上移操作继续产生临时 4- 节点则一直进行分裂上移直到不存在临时 4- 节点
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7002c01b-1ed5-475a-9e5f-5fc8a4cdbcc0.jpg" width="460"/> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 2. 性质
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
2-3 查找树插入操作的变换都是局部的除了相关的节点和链接之外不必修改或者检查树的其它部分而这些局部变换不会影响树的全局有序性和平衡性
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
2-3 查找树的查找和插入操作复杂度和插入顺序无关在最坏的情况下查找和插入操作访问的节点必然不超过 logN 含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
# 红黑树
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
红黑树是 2-3 查找树但它不需要分别定义 2- 节点和 3- 节点而是在普通的二叉查找树之上为节点添加颜色指向一个节点的链接颜色如果为红色那么这个节点和上层节点表示的是一个 3- 节点而黑色则是普通链接
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f1912ba6-6402-4321-9aa8-13d32fd121d1.jpg" width="240"/> </div><br>
2019-03-08 23:06:28 +08:00
红黑树具有以下性质
2019-03-27 20:57:37 +08:00
- 红链接都为左链接
- 完美黑色平衡即任意空链接到根节点的路径上的黑链接数量相同
2019-03-08 23:06:28 +08:00
画红黑树时可以将红链接画平
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f5cb6028-425d-4939-91eb-cca9dd6b6c6c.jpg" width="220"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public class RedBlackBST<Key extends Comparable<Key>, Value> extends BST<Key, Value> {
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
private static final boolean RED = true;
private static final boolean BLACK = false;
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
private boolean isRed(Node x) {
if (x == null)
return false;
return x.color == RED;
}
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 1. 左旋转
2019-03-08 23:06:28 +08:00
因为合法的红链接都为左链接如果出现右链接为红链接那么就需要进行左旋转操作
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f4d534ab-0092-4a81-9e5b-ae889b9a72be.jpg" width="480"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
recalculateSize(h);
return x;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 2. 右旋转
2019-03-08 23:06:28 +08:00
进行右旋转是为了转换两个连续的左红链接这会在之后的插入过程中探讨
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/63c8ffea-a9f2-4ebe-97d1-d71be71246f9.jpg" width="480"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
recalculateSize(h);
return x;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 3. 颜色转换
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的分裂 4- 节点除了需要将子节点的颜色由红变黑之外同时需要将父节点的颜色由黑变红 2-3 树的角度看就是将中间节点移到上层节点
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/094b279a-b2db-4be7-87a3-b2a039c7448e.jpg" width="270"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 4. 插入
2019-03-08 23:06:28 +08:00
先将一个节点按二叉查找树的方法插入到正确位置然后再进行如下颜色操作
2019-03-27 20:57:37 +08:00
- 如果右子节点是红色的而左子节点是黑色的进行左旋转
- 如果左子节点是红色的而且左子节点的左子节点也是红色的进行右旋转
- 如果左右子节点均为红色的进行颜色转换
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4c457532-550b-4eca-b881-037b84b4934b.jpg" width="430"/> </div><br>
2019-03-08 23:06:28 +08:00
```java
@Override
2019-03-27 20:57:37 +08:00
public void put(Key key, Value value) {
root = put(root, key, value);
root.color = BLACK;
}
private Node put(Node x, Key key, Value value) {
if (x == null) {
Node node = new Node(key, value, 1);
node.color = RED;
return node;
}
int cmp = key.compareTo(x.key);
if (cmp == 0)
x.val = value;
else if (cmp < 0)
x.left = put(x.left, key, value);
else
x.right = put(x.right, key, value);
if (isRed(x.right) && !isRed(x.left))
x = rotateLeft(x);
if (isRed(x.left) && isRed(x.left.left))
x = rotateRight(x);
if (isRed(x.left) && isRed(x.right))
flipColors(x);
recalculateSize(x);
return x;
2019-03-08 23:06:28 +08:00
}
```
可以看到该插入操作和二叉查找树的插入操作类似只是在最后加入了旋转和颜色变换操作即可
2019-03-27 20:57:37 +08:00
根节点一定为黑色因为根节点没有上层节点也就没有上层节点的左链接指向根节点flipColors() 有可能会使得根节点的颜色变为红色每当根节点由红色变成黑色时树的黑链接高度加 1.
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 5. 分析
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
一颗大小为 N 的红黑树的高度不会超过 2logN最坏的情况下是它所对应的 2-3 构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点
2019-03-08 23:06:28 +08:00
红黑树大多数的操作所需要的时间都是对数级别的
2019-03-27 20:57:37 +08:00
# 散列表
2019-03-08 23:06:28 +08:00
散列表类似于数组可以把散列表的散列值看成数组的索引值访问散列表和访问数组元素一样快速它可以在常数时间内实现查找和插入操作
由于无法通过散列值知道键的大小关系因此散列表无法实现有序性操作
2019-03-27 20:57:37 +08:00
## 1. 散列函数
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
对于一个大小为 M 的散列表散列函数能够把任意键转换为 [0, M-1] 内的正整数该正整数即为 hash
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
散列表存在冲突也就是两个不同的键可能有相同的 hash
2019-03-08 23:06:28 +08:00
散列函数应该满足以下三个条件
2019-03-27 20:57:37 +08:00
- 一致性相等的键应当有相等的 hash 两个键相等表示调用 equals() 返回的值相等
- 高效性计算应当简便有必要的话可以把 hash 值缓存起来在调用 hash 函数时直接返回
- 均匀性所有键的 hash 值应当均匀地分布到 [0, M-1] 之间如果不能满足这个条件有可能产生很多冲突从而导致散列表的性能下降
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
除留余数法可以将整数散列到 [0, M-1] 之间例如一个正整数 k计算 k%M 既可得到一个 [0, M-1] 之间的 hash 注意 M 最好是一个素数否则无法利用键包含的所有信息例如 M 10<sup>k</sup>那么只能利用键的后 k
2019-03-08 23:06:28 +08:00
对于其它数可以将其转换成整数的形式然后利用除留余数法例如对于浮点数可以将其的二进制形式转换成整数
2019-03-27 20:57:37 +08:00
对于多部分组合的类型每个部分都需要计算 hash 这些 hash 值都具有同等重要的地位为了达到这个目的可以将该类型看成 R 进制的整数每个部分都具有不同的权值
2019-03-08 23:06:28 +08:00
例如字符串的散列函数实现如下
```java
2019-03-27 20:57:37 +08:00
int hash = 0;
for (int i = 0; i < s.length(); i++)
hash = (R * hash + s.charAt(i)) % M;
2019-03-08 23:06:28 +08:00
```
再比如拥有多个成员的自定义类的哈希函数如下
```java
2019-03-27 20:57:37 +08:00
int hash = (((day * R + month) % M) * R + year) % M;
2019-03-08 23:06:28 +08:00
```
2019-03-27 20:57:37 +08:00
R 通常取 31
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
Java 中的 hashCode() 实现了哈希函数但是默认使用对象的内存地址值在使用 hashCode() 应当结合除留余数法来使用因为内存地址是 32 位整数我们只需要 31 位的非负整数因此应当屏蔽符号位之后再使用除留余数法
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
int hash = (x.hashCode() & 0x7fffffff) % M;
2019-03-08 23:06:28 +08:00
```
2019-03-27 20:57:37 +08:00
使用 Java HashMap 等自带的哈希表实现时只需要去实现 Key 类型的 hashCode() 函数即可Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数Java 中的 StringInteger 等对象的 hashCode() 都能实现这一点以下展示了自定义类型如何实现 hashCode()
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public class Transaction {
private final String who;
private final Date when;
private final double amount;
public Transaction(String who, Date when, double amount) {
this.who = who;
this.when = when;
this.amount = amount;
}
public int hashCode() {
int hash = 17;
int R = 31;
hash = R * hash + who.hashCode();
hash = R * hash + when.hashCode();
hash = R * hash + ((Double) amount).hashCode();
return hash;
}
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
## 2. 拉链法
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
拉链法使用链表来存储 hash 值相同的键从而解决冲突
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
查找需要分两步首先查找 Key 所在的链表然后在链表中顺序查找
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
对于 N 个键M 条链表 (N>M)如果哈希函数能够满足均匀性的条件每条链表的大小趋向于 N/M因此未命中的查找和插入操作所需要的比较次数为 \~N/M
2019-03-08 23:06:28 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cbbfe06c-f0cb-47c4-bf7b-2780aebd98b2.png" width="330px"> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 3. 线性探测法
2019-03-08 23:06:28 +08:00
线性探测法使用空位来解决冲突当冲突发生时向前探测一个空位来存储冲突的键
2019-03-27 20:57:37 +08:00
使用线性探测法数组的大小 M 应当大于键的个数 NM>N)
2019-03-08 23:06:28 +08:00
2019-04-21 15:53:21 +08:00
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0dbc4f7d-05c9-4aae-8065-7b7ea7e9709e.gif" width="350px"> </div><br>
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public class LinearProbingHashST<Key, Value> implements UnorderedST<Key, Value> {
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
private int N = 0;
private int M = 16;
private Key[] keys;
private Value[] values;
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
public LinearProbingHashST() {
init();
}
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
public LinearProbingHashST(int M) {
this.M = M;
init();
}
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
private void init() {
keys = (Key[]) new Object[M];
values = (Value[]) new Object[M];
}
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
private int hash(Key key) {
return (key.hashCode() & 0x7fffffff) % M;
}
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
#### 3.1 查找
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public Value get(Key key) {
for (int i = hash(key); keys[i] != null; i = (i + 1) % M)
if (keys[i].equals(key))
return values[i];
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
return null;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
#### 3.2 插入
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public void put(Key key, Value value) {
resize();
putInternal(key, value);
2019-03-08 23:06:28 +08:00
}
2019-03-27 20:57:37 +08:00
private void putInternal(Key key, Value value) {
int i;
for (i = hash(key); keys[i] != null; i = (i + 1) % M)
if (keys[i].equals(key)) {
values[i] = value;
return;
}
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
keys[i] = key;
values[i] = value;
N++;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
#### 3.3 删除
2019-03-08 23:06:28 +08:00
删除操作应当将右侧所有相邻的键值对重新插入散列表中
```java
2019-03-27 20:57:37 +08:00
public void delete(Key key) {
int i = hash(key);
while (keys[i] != null && !key.equals(keys[i]))
i = (i + 1) % M;
// 不存在,直接返回
if (keys[i] == null)
return;
keys[i] = null;
values[i] = null;
// 将之后相连的键值对重新插入
i = (i + 1) % M;
while (keys[i] != null) {
Key keyToRedo = keys[i];
Value valToRedo = values[i];
keys[i] = null;
values[i] = null;
N--;
putInternal(keyToRedo, valToRedo);
i = (i + 1) % M;
}
N--;
resize();
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
#### 3.5 调整数组大小
2019-03-08 23:06:28 +08:00
2019-04-21 15:53:21 +08:00
线性探测法的成本取决于连续条目的长度连续条目也叫聚簇当聚簇很长时在查找和插入时也需要进行很多次探测例如下图中 2\~4 位置就是一个聚簇
2019-04-25 18:43:33 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ace20410-f053-4c4a-aca4-2c603ff11bbe.png" width="340px"> </div><br>
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
α = N/M α 称为使用率理论证明 α 小于 1/2 时探测的预计次数只在 1.5 2.5 之间为了保证散列表的性能应当调整数组的大小使得 α [1/4, 1/2] 之间
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
private void resize() {
if (N >= M / 2)
resize(2 * M);
else if (N <= M / 8)
resize(M / 2);
2019-03-08 23:06:28 +08:00
}
2019-03-27 20:57:37 +08:00
private void resize(int cap) {
LinearProbingHashST<Key, Value> t = new LinearProbingHashST<Key, Value>(cap);
for (int i = 0; i < M; i++)
if (keys[i] != null)
t.putInternal(keys[i], values[i]);
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
keys = t.keys;
values = t.values;
M = t.M;
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
# 小结
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 1. 符号表算法比较
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
| 算法 | 插入 | 查找 | 是否有序 |
| :---: | :---: | :---: | :---: |
| 链表实现的无序符号表 | N | N | yes |
| 二分查找实现的有序符号表 | N | logN | yes |
| 二叉查找树 | logN | logN | yes |
| 2-3 查找树 | logN | logN | yes |
| 拉链法实现的散列表 | N/M | N/M | no |
| 线性探测法实现的散列表 | 1 | 1 | no |
2019-03-08 23:06:28 +08:00
应当优先考虑散列表当需要有序性操作时使用红黑树
2019-03-27 20:57:37 +08:00
## 2. Java 的符号表实现
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
- java.util.TreeMap红黑树
- java.util.HashMap拉链法的散列表
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
## 3. 稀疏向量乘法
2019-03-08 23:06:28 +08:00
2019-03-27 20:57:37 +08:00
当向量为稀疏向量时可以使用符号表来存储向量中的非 0 索引和值使得乘法运算只需要对那些非 0 元素进行即可
2019-03-08 23:06:28 +08:00
```java
2019-03-27 20:57:37 +08:00
public class SparseVector {
private HashMap<Integer, Double> hashMap;
public SparseVector(double[] vector) {
hashMap = new HashMap<>();
for (int i = 0; i < vector.length; i++)
if (vector[i] != 0)
hashMap.put(i, vector[i]);
}
public double get(int i) {
return hashMap.getOrDefault(i, 0.0);
}
public double dot(SparseVector other) {
double sum = 0;
for (int i : hashMap.keySet())
sum += this.get(i) * other.get(i);
return sum;
}
2019-03-08 23:06:28 +08:00
}
```
2019-03-27 20:57:37 +08:00
2019-10-28 00:25:00 +08:00
2019-10-29 09:21:49 +08:00
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-1.png"></img></div>