目录
1、定义
符号表是一种存储键值对的数据结构,支持两种操作:插入(put),即将一组新的键值 对存入表中;查找(get),即根据给定的键得到相应的值。
2、用途
符号表主要的目的就是将一组键和值对应起来,并将多组键值对存储起来,方便后期查找的时候我们可以根据相应的键找到对应的值。
3、特点
我们的符号表需要遵循以下的规则:
- 没有重复的key。
- key不能为null。
- value也不能为null,这样设计的目的为了可以通过get()方法返回值是否为null来判断key是否存在,也可以通过put(key,null)来删除key这个键值对。
4、方案一:基于无序链表
我们对于符号表键值对的存储方式的数据结构的简单选择就是:链表。用一个结点来存储键和值。{如果对链表不太熟悉可以看算法-3-链表栈(最优设计方案)}
插入(put):需要从头结点遍历下去,直到找到相同的key,更新key对应的value;如果没有就利用该键值对创建新的头结点。
查找(get):也需要从头结点遍历下去,直到找到对应的key返回对应的value,如果没有则返回null。
public class NodeMap<Key, Value> {
private Node first;//头结点
class Node {
Key key;
Value value;
Node next;
public Node(Key key, Value value, Node next) {
this.key = key;
this.value = value;
this.next = next;
}
}
public void put(Key key, Value value) {
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) { //如果链表中含有相同的key更新对应的value即可
x.value = value;
return;
}
}
first = new Node(key, value, first);//链表中没有该key的话,利用该键值对创建新的头结点
}
public Value get(Key key) {
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
return x.value;
}
}
return null;
}
}
基于链表实现符号表首先它是无序的,然后它的每次插入和查找的最差时间复杂度都是N,平均情况下查找的时间复杂度为N/2,插入还是N,这显然是非常低效的设计方案。
对于无序列表低效的插入查找方式,我们可以利用有序表来进行改进,对于有序表的查找,我们可以利用二分查找法来将时间复杂度提升到lgN级别。同时它还支持有序相关的大部分操作,比如获取最大最小值等。
5、方案二:基于有序数组的二分查找
对于有序符号表的实现,我们采用的数据结构是 一对平行的数组,一个存储key,一个存储value。
插入(put):首先我们利用二分查找法,查找数组keys中是否有该key值,如果有则返回在keys数组中的下标,如果没有的话则返回大于等于该key的最小的key的下标k,然后将该键值对插入到下标k的位置,并将原来数组下标k以及后面的元素后移一位。
查找(get):我们利用二分查找法,在lgN的时间复杂度内就能得到我们想要的value。
该方案在最坏情况下,查找一个元素需要lgN+1 次比较;向大小为N的有序数组插入一个新的元素,最坏情况下需要访问数组~2N次(如果插入的位置为0,那么key和value数组都需要后移N次)。
public class BinarySearchArray<Key extends Comparable<Key>, Value> {
private Key[] keys;
private Value[] values;
private int N;//存储键值对的数量
public BinarySearchArray(int size) {
keys = (Key[]) new Comparable[size];
values = (Value[]) new Object[size];
}
public void put(Key key, Value value) {
/*
* 先查找keys数组中是否用相同的key如果有更新它对应的value就行
*/
int k=rank(key);
if (k<N&&key.compareTo(keys[k])==0){
values[k]=value;
return;
}
/*
* 如果keys数组没有对应key,就需要将这个键值对插入到下标为k的位置,
* 然后将原来数组下标为k以及k后面的元素平移到后面。
*/
for (int i=N;i>k;i--){
keys[i]=keys[i-1];
values[i]=values[i-1];
}
keys[k]=key;
values[k]=value;
N++;
}
public Value get(Key key) {
if (N == 0) return null;
int k = rank(key);
if (k < N && key.compareTo(keys[k]) == 0) {
return values[k];
}
return null;
}
/**
* 返回大于等于该key的最小key的下标
* 就比如数组keys{1,3,4,7,9},我要利用二分查找法寻找5这个key,并返回大于等于5的最小的key的下标。
* 第一步:mid=0+(4-0)/2=2; keys[2]<5; lo=2+1=3;
* 第二步:mid=3+(4-3)/2=3; keys[3]>5; hi=4-1=3;
* 第三步:mid=3+(3-3)/2=3; keys[3]>5; hi=3-1=2;lo=3;
* 第四步:hi<lo跳出while循环,返回lo=3, 7就是大于等于5的最小key
*/
private int rank(Key key) {
int lo = 0;
int hi = N;
while (lo <= hi) {
int mid = (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;
}
}
6、符号表的各种实现的优缺点
显然上面的两种方案都没有达到理想的时间复杂度,后面我们会将表格中更优化的实现方案。