1符号表
符号表就是一种存储键值对的数据结构,
支持两种操作:
- 插入(put):即将一组新的键值对存入表中;
- 查找(get):即根据给定的键得到相应的值。
1.1特点
- 每个键只对应着一个值(表中不允许存在重复的键)
- 当插入新的键值对与已有的键冲突时,新的值会代替旧的值
- 键不能为空
- 值不能为空
- 删除的两种方法:
- 延时删除:将值置为空,之后某个时间在删除
- 即时删除:直接删除键值对
1.2有序符号表
许多符号表的实现都利用了Comparable接口
带来的键的有序性来更好地实现put()
和get()
方法。
2无序链表的顺序查找
每个结点存储一个键值对。
实现:
/**
* 基于无序链表的顺序查找
* @Author: AZhu
* @Date: 2021/2/28 21:09
*/
public class SaquentialSearchST<Key, Value> {
private Node first;//链表首结点
//链表结点的定义
private class Node {
Key key;
Value value;
Node next;
public Node(Key key, Value val, Node next) {
this.key = key;
this.value = val;
this.next = next;
}
}
/**
* 查找给定的键,返回对应的值
* @param key
* @return
*/
public Value get(Key key) {
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
return x.value;//命中
}
}
return null;//未命中
}
/**
* 插入键值对,若已存在则更新值,否则新建结点插入。
* @param key
* @param value
*/
public void put(Key key, Value value) {
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {//命中,更新
x.value = value;
return;
}
}
first = new Node(key, value, first);//未命中,新建结点插入链表开头
}
}
性能:
- 在一个含有
N
对键值对的基于无序链表的符号表中:- 未命中的查找和插入都需要
N
次比较
- 未命中的查找和插入都需要
- 命中的查找在最坏的情况下需要
N
次比较 - 向一个空表中插入
N
个不同的键需要~N^2/2
次比较。
3有序数组的二分查找
使用了一对平行数组,一个存储键,一个存储值。
核心是rank()
方法:
实现:
/**
* 基于有序数组的二分查找
* @Author: AZhu
* @Date: 2021/2/28 21:28
*/
public class BinarySearchST<Key extends Comparable<Key>, Value> {
private Key[] keys;//键
private Value[] values;//值
private int N;//符号表的大小
public BinarySearchST(int capacity) {
keys = (Key[]) new Comparable[capacity];
values = (Value[]) new Object[capacity];
}
/**
* 返回表中键值对的数量
* @return
*/
public int size() {
return N;
}
/**
* 判断表是否为空
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 查找给定key所对应的value
* @param key
* @return
*/
public Value get(Key key) {
if (isEmpty()) return null;
int i = rank(key);//查找
if (i < N && keys[i].compareTo(key) == 0) {//找到了
return values[i];
} else {//找不到
return null;
}
}
/**
* 插入键值对,若已存在则更新值,否则插入。
* @param key
* @param value
*/
public void put(Key key, Value value) {
int i = rank(key);
if (i < N && keys[i].compareTo(key) == 0) {//找到了
values[i] = value;//更新
return;
}
for (int j = N; j > i; j--) {//找不到
//后移
keys[j] = keys[j - 1];
values[j] = values[j - 1];
}
//插入
keys[i] = key;
values[i] = value;
N++;
}
/**
* 迭代的二分查找
* @param key
* @return
*/
public int rank(Key key) {
int lo = 0, hi = N - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int cmp = key.compareTo(keys[mid]);
if (cmp < 0) {
hi = mid - 1;
} else if (cmp > 0) {
lo = mid + 1;
} else {
return mid;
}
}
return lo;
}
/**
* 递归的二分查找
* @param key
* @param lo
* @param hi
* @return
*/
public int rank(Key key, int lo, int hi) {
if (hi<lo) return lo;
int mid = lo + (hi - lo) / 2;
int cmp = key.compareTo(keys[mid]);
if (cmp < 0) {
return rank(key, lo, mid - 1);
} else if (cmp > 0) {
return rank(key, mid + 1, hi);
} else {
return mid;
}
}
}
性能:
- 二分查找很快
- 在
N
个键的有序数组中进行二分查找最多需要lgN+1
次比较。 - 但是
put()
方法太慢了:构造一个基于有序数组的符号表需要访问数组的次数是数组长度的平方级别的。
4总结
现代应用需要同时能够支持高效的查找和插入操作的符号表的实现。
简单的符号表实现的成本总结:
算法 | 最坏情况 | 平均情况 | 是否高效的支持有序性相关的操作 | ||
---|---|---|---|---|---|
查找 | 插入 | 查找 | 插入 | ||
顺序查找 | N | N | N/2 | N | 否 |
二分查找 | lgN | 2N | lgN | N | 是 |