符号表主要就是用来将一个键和一个值联系起来。
定义:符号表是一种存储键值对的数据结构,支持两种操作:插入(put),即将一组新的键值对存入表中;查找(get),即根据给定的键得到相应的值。
在实现前,我们遵循以下规则:
- 每个键只对应一个值;
- 当向表中存入的键值对和表中的已有的键冲突时,新的值会替代旧的值。
- 键不能为空
- 值不能为空
无序链表,java实现:
import java.util.LinkedList;
import java.util.Queue;
/**
* @author yuan
* @date 2019/2/25
* @description 符号表,键值对(Map),无序链表实现
*/
public class SequentialSearchST<Key,Value> {
/**
* 链表首节点
*/
private Node first;
/**
* 大小
*/
private int n;
private 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 Value get(Key key) {
if (key == null) {
throw new IllegalArgumentException("argument to get() is null");
}
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
return x.value;
}
}
return null;
}
public void put(Key key, Value value) {
if (key == null) {
throw new IllegalArgumentException("first argument to put() is null");
}
if (value == null) {
// 防止value为null,如果value为null,删除对应key
delete(key);
return;
}
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++;
}
public boolean contains(Key key) {
if (key == null) {
throw new IllegalArgumentException("argument to contains() is null");
}
return get(key) != null;
}
public int size(){
return n;
}
public boolean isEmpty(){
return size() == 0;
}
public void delete(Key key) {
if (key == null) {
throw new IllegalArgumentException("argument to contains() is null");
}
first = delete(first, key);
}
private Node delete(Node x, Key key) {
if (x == null) {
return null;
}
if (key.equals(x.key)) {
--n;
return x.next;
}
x.next = delete(x.next, key);
return x;
}
public Iterable<Key> keys(){
Queue<Key> queue = new LinkedList<>();
for (Node x = first; x != null; x = x.next) {
queue.offer(x.key);
}
return queue;
}
public static void main(String[] args) {
SequentialSearchST<String, Integer> map = new SequentialSearchST<>();
map.put("aaa", 1);
map.put("bbb", 3);
map.put("ccc", 2);
System.out.println(map.get("aaa")); // 1
System.out.println(map.size()); // 3
System.out.println(map.get("bbb")); // 3
map.delete("aaa");
System.out.println(map.get("aaa")); // null
}
}
在含用N对键值的基于(无序)链表的符号表中,未命中的查找和插入操作都需要N次比较。
基于链表实现的顺序查找是非常低效的。
下面使用数组来实现,同时保证键的有序
使用一对平行数组,一个存储键一个存储值。
由于需要对键进行比较,所有键都必须是Comparable的对象,才可以使用a.compareTo(b)来比较a和b两个键。
实现的核心是rank()操作,它返回表中小于给定键的键的数量。
然后实现get()方法就容易了:只用通过rank()判断给定的键是否存储在表中即可。
然后put()方法,如果给定的键存储在表中,则更新;否则,将更大的键向后移动一格,腾出位置并将键值对插入合适的位置。
实现代码:
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
/**
* @author yuan
* @date 2019/2/25
* @description 基于有序数组的有序符号表
*/
public class BinarySearchST<Key extends Comparable<Key>, Value> {
private Key[] keys;
private Value[] vals;
/**
* 大小
*/
private int n;
/**
* 默认大小
*/
private static final int DEFAULT_CAPACITY = 2;
public BinarySearchST() {
this(DEFAULT_CAPACITY);
}
public BinarySearchST(int capacity) {
keys = (Key[]) new Comparable[capacity];
vals = (Value[]) new Object[capacity];
}
public int size(){
return n;
}
public boolean isEmpty() {
return size() == 0;
}
public Value get(Key key) {
if (key == null) {
throw new IllegalArgumentException("argument to get() is null");
}
if (isEmpty()) {
return null;
}
int i = rank(key);
if (i < n && keys[i].compareTo(key) == 0) {
return vals[i];
}
return null;
}
/**
* 基于有序数组的二分查找
* @param key
* @return 返回小于键的数量
*/
public int rank(Key key) {
if (key == null) {
throw new IllegalArgumentException("argument to rank() is null");
}
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
int cmp = key.compareTo(keys[mid]);
if (cmp < 0) {
r = mid - 1;
} else if (cmp > 0) {
l = mid + 1;
} else {
return mid;
}
}
return l;
}
public void put(Key key, Value val) {
if (key == null) {
throw new IllegalArgumentException("first argument to put() is null");
}
if (val == null) {
delete(key);
return;
}
int i = rank(key);
if (i < n && keys[i].compareTo(key) == 0) {
// 如果键存在,更新值
vals[i] = val;
return;
}
// 如果容量满,扩容
if (n == keys.length) {
resize(2 * keys.length);
}
// 移动数组,腾出位置给新的键值对
for (int j = n; j > i; j--) {
keys[j] = keys[j - 1];
vals[j] = vals[j - 1];
}
keys[i] = key;
vals[i] = val;
++n;
}
/**
* 调整大小
* @param capacity
*/
private void resize(int capacity) {
assert capacity >= n;
Key[] tempk = (Key[]) new Comparable[capacity];
Value[] tempv = (Value[]) new Object[capacity];
for (int i = 0; i < n; i++) {
tempk[i] = keys[i];
tempv[i] = vals[i];
}
keys = tempk;
vals = tempv;
}
public boolean contains(Key key) {
if (key == null) {
throw new IllegalArgumentException("argument to contains() is null");
}
return get(key) != null;
}
public void delete(Key key) {
if (key == null) {
throw new IllegalArgumentException("argument to delete() is null");
}
if (isEmpty()) {
return;
}
int i = rank(key);
// 如果键不存在,返回
if (i == n || keys[i].compareTo(key) != 0) {
return;
}
// 将数组往前移动
for (int j = i; j < n - 1; j++) {
keys[j] = keys[j + 1];
vals[j] = vals[j + 1];
}
// 赋值为null,让系统回收空间
keys[n] = null;
vals[n] = null;
--n;
// 如果已用空间只占总容量的1/4
if (n > 0 && n == keys.length / 4) {
// 缩小容量空间
resize(keys.length / 2);
}
}
/**
* 获取最小键的值
* @return
*/
public Key min() {
if (isEmpty()) {
throw new NoSuchElementException("called min() with empty symbol table");
}
return keys[0];
}
/**
* 获取最大键的值
* @return
*/
public Key max() {
if (isEmpty()) {
throw new NoSuchElementException("called max() with empty symbol table");
}
return keys[n-1];
}
/**
* 获取第k个键的值
* @param k
* @return
*/
public Key select(int k) {
if (k < 0 || k >= size()) {
throw new IllegalArgumentException("called select() with invalid argument: " + k);
}
return keys[k];
}
/**
* 获取所有的keys
* @return
*/
public Iterable<Key> keys(){
Queue<Key> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
queue.offer(keys[i]);
}
return queue;
}
public static void main(String[] args) {
BinarySearchST<String, Integer> map = new BinarySearchST<>();
map.put("A", 3);
map.put("Z", 5);
map.put("T", 8);
map.put("S", 4);
map.put("E", 1);
for (String key : map.keys()) {
System.out.println("key=" + key + ",value=" + map.get(key) + ",size=" + map.size());
map.delete(key);
}
}
}
对于put()方法,虽然二分查找减少了比较次数,但无法减少运行所需时间:因为最坏情况下需要移动整个数组。
两者的对比:
可以看到两种方法都无法实现插入和查找都是O(lgN)级别的。
为了实现更高效的插入和查找操作,我们需要用到二叉查找树。
6种符号表的预览: