顺序查找(无序链表):
符号表中使用的数据结构的一个简单选择就是链表,每个结点存储一个键值对。get()方法和put()方法的实现即为遍历链表。代码实现如下:
public class SequentialSearchST<Key,Value>{
private Node first;//链表首节点
private class Node{
//链表结点的定义
Key key;
Value val;
Node next;
public Node(Key key,Value val,Node next) {
this.key=key;
this.val=val;
this.next=next;
}
}
public Value get(Key key) {
//查找给定的键,返回键的值
for(Node x=first;x!=null;x=x.next) {
if(key.equals(x.key)) {
return x.val;//命中
}
}
return null; //未命中
}
public void put(Key key,Value val) {
for(Node x=first;x!=null;x=x.next) {
if(key.equals(x.key)) {
x.val=val;return; //命中,更新
}
}
first=new Node(key,val,first); //将put进的结点设置为first结点,并指向原first结点
}
}
性能分析:
在表中查找一个不存在的键时,我们会将表中每个键和给定的键比较(N)。因为不允许出现重复的键,每次插入操作之前我们都需要这样查找一遍。
推论: 向一个空表中插入N个不同的键需要~N²/2次比较。
链表的实现以及顺序查找是非常低效的,无法满足Frequency-Counter处理庞大输入问题的需求。比较的总次数和查找次数与插入次数的乘积成正比。
二分查找(有序数组):
它使用的数据结构是一对平行的数组,一个存储键一个存储值。算法先保证数组中的Comparable类型的键有序,然后再使用数组的索引来高效的实现get()和其他操作。
这份实现的核心是rank()方法,它返回表中小于给定键的键的数量。对于get()方法,只要给定的键存在于表中,rank()方法就能够精确地告诉我们在哪里能找到它(如果找不到,那它肯定不在表中了)。
对于put()方法,只要给定的键存在于表中,rank()方法就能够精确地告诉我们到哪里去更新他的值,以及当键不在表中时将键存储到标的何处。我们将所有更大的键向后移动一格来腾出位置。
实现代码如下:
public class BinarySearchST<Key extends Comparable<Key>,Value>{
private Key[] keys;
private Value[] vals;
private int N;//存储当前字符表的大小
public BinarySearchST(int capacity) {
keys=(Key[])new Comparable[capacity];
vals=(Value[])new Object[capacity];
}
public int size() {
return N;
}
public Value get(Key key) {
if(isEmpty())return null;
int i=rank(key);
if(i<N&&keys[i].compareTo(key)==0)return vals[i];
else return null;
}
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;
}
}
性能分析:在N个键的有序数组中进行二分超找最多需要(lgN+1)次比较(无论是否成功)。
二分查找减少了比较的次数但无法减少运行所需的时间,因为它无法改变以下事实:在键是随机排列的情况下,构造一个基于有序数组的符号表所需要的访问数组的次数是数组长度的平方级别。
一般情况下二分查找都比顺序查找快得多,它也是众多实际应用程序的最佳选择,对于一个静态表(不允许插入)来说,将其在初始化时就排序是值得的。即使查找前所有的键值对已知,为BinarySearchST添加一个能够初始化并将符号表排序的构造函数也是有意义的。
但是,二分查找也适合很多应用。我们需要在构造庞大的符号表的同时能够任意插入(删除)键值对,同时也能够完成查找操作。