符號表主要就是用來將一個鍵和一個值聯繫起來。
定義:符號表是一種存儲鍵值對的數據結構,支持兩種操作:插入(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;
}
}
// 未命中,新建節點
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種符號表的預覽: