首页 > 代码库 > Symbol Table(符号表)

Symbol Table(符号表)

一、定义

符号表是一种存储键值对的数据结构并且支持两种操作:将新的键值对插入符号表中(insert);根据给定的键值查找对应的值(search)。

 

二、API

1、无序符号表

技术分享

几个设计决策:

A、泛型

在设计方法时没有指定处理对象,而是使用了泛型。

并且将键(Key)和值(Value)区分开来。

B、重复键的处理

规则:

每个值(Value)都只对应一个键(Key)(即不允许存在重复的键)。

当用例代码向表中存入的键值对和表中的已有的键(以及关联的值)冲突时,新的值替代旧的值。

C、Null 键

键不能为空,否则会抛出空指针异常

D、Null 值

同样规定值不能为Null,因为API中get函数,如果键不存在会返回null,如果在表中的值可以为null的话,就会产生混淆。

这个规定产生两个结果:

可以用get()方法是否返回null来判断给定的key是否存在表中。

可以用put(key, null)来实现删除。

E、便捷方法

可以由其他函数来实现,如:

    //shorthand methods
    public boolean contains(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function contains");
        
        return get(key) != null;
    }
    
    //shorthand methods
    public boolean isEmpty() {
        return size() == 0;
    }

F、键的等价性

对象的equals方法。

最好用不可变的数据类型作为键,否则表的一致性是不可保证的。

 

2、有序符号表

技术分享

对于实现了Comparable的键,符号表可利用这一特性来保持键的有序性

这就大大拓展了API,根据键的相对位置定义更多使用的操作。这种表就叫有序符号表

只要符号表出现了泛型变量Key extends Comparable<Key>,那么这个符号表就实现了有序性。

A、最大键和最小键

在有序符号表中有获取最大(最小)键操作和删除最大(最小)键操作。

之前的Priority Queue也有类似的操作,主要区别在优先队列中可以存在重复的键而符号表不行。

B、向上取整和向下取整

向下取整:floor, find the largest key that is less than or equal to the given key.

向上取整:ceiling, find the smallest key that is greater than or equal to the given key.

C、排名和选择

rank(key), 小于key的键的个数。

select(k), 返回第k大的键,即符号表中有k个键小于返回的键(k的取值范围为0到st.size)。

i == rank(select(i)),key = select(rank(key))

上述两个等式可以更好的理解这两个操作。

D、范围查找

对于给定的范围有多少个键在这个范围,或者哪些键在这些范围?

public int size(Key lo, Key hi)

public Iterable<Key> keys(Key lo, Key hi)

这两个函数实现了上述操作。

E、异常情况

当方法需要返回一个键,但符号表没有符合的键时,抛出一个异常。

或者返回null。

F、便捷方法

可以由其他函数来实现,如:

  //shorthand methods
    public void deleteMin() {
        if(isEmpty())
            throw new NoSuchElementException("underflow error");
        delete(min());
    }
    
    //shorthand methods
    public void deleteMax() {
        if(isEmpty())
            throw new NoSuchElementException("underflow error");
        delete(max());
    }
    
    //shorthand methods
    public int size(Key lo, Key hi) {
        
        if (lo == null)
            throw new IllegalArgumentException("first argument to size() is null"); 
        if (hi == null)
            throw new IllegalArgumentException("second argument to size() is null"); 
        
        if(hi.compareTo(lo) < 0)
            return 0;
        else if(contains(hi))
            return rank(hi) - rank(lo) + 1;
        else 
            return rank(hi) - rank(lo);
    }
    
    //shorthand methods
    public Iterable<Key> keys() {
        if(isEmpty())
            return new LinkedList<>();
        return keys(min(), max());
    }

 G、键的等价性

任何一个Comparable类型的两个值a和b都要保证(a.compareTo(b) == 0)和a.equals(b)的返回值相等。

为了避免二义性,在有序符号表中只使用compareTo()方法来比较两个键,即a.compareTo(b) == 0来表示a和b是否相等。

H、成本模型

无论是用a.compareTo(b) == 0还是用a.equals(b),都用比较来表示一个符号表条目和一个被查找的键的比较操作。

如果比较操作不在内循环,则统计数组的访问次数

 

三、测试用例

两个用例:一个用来跟踪在小规模输入下的行为测试用例,一个用来寻找更高效实现的性能测试用例。

1、Test Client

    public static void main(String[] args) {
        
        ST<String, Integer> st = new ST<String, Integer>();
        
        for(int i = 0; !StdIn.isEmpty(); i++) {
            
            String key = StdIn.readString();
            
            if(key.equals("-")) {
                key = StdIn.readString();
                st.delete(key);
                StdOut.println("delete " + key);
            } else {
                st.put(key, i);
                StdOut.println("put" + " " + key + " " + i);
            }
            
            StdOut.print(st.size() + " key-value pairs:");
            for(String s : st.keys()) 
                StdOut.print(" " + s + " " + st.get(s));
            StdOut.println();
            
        }
        
    }

跟课本的有所不同,加上了delete操作,更好的观测行为。

 

2、Performance Client

    public static void main(String[] args) {
        
        int minlen = Integer.parseInt(args[0]);
        SequentialSearchST<String, Integer> st = new SequentialSearchST<>();
        
        //build symbol table and count frequencies
        while(!StdIn.isEmpty()) {
            String word = StdIn.readString();
            if(word.length() < minlen)
                continue;//ignore short keys
            Integer freq = st.get(word);
            if(freq == null)
                st.put(word, 1);
            else
                st.put(word, freq + 1);
        }
        
        //find a key with the highest frequency count
        String max = "";
        st.put(max, 0);
        for(String word : st.keys())
            if(st.get(word) > st.get(max))
                max = word;
        StdOut.println(max + " " + st.get(max));
        
    }

这个是对课本的用例进行改进后的,消除对contains的调用。

 

本章一直用这个用例来进行性能测试。用例特点:

查找(search)和插入(insert)操作交叉进行。

大量不同的键。

查找(search)比插入(insert)操作多的多。

虽然不可预测,查找和插入的模式并非随机。

 

 四、初级实现

1、无序链表中的顺序查找

顺序查找用的是数据结构是链表,每个节点存储一个键值对。

 

get()的实现为遍历链表,用equals函数比较寻找的键和节点中的键。如果有匹配的键,则返回相应的值。

否则,返回null。

    //search for key, return associated value
    public Value get(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function get");
        
        for(Node current = first; current != null; current = current.next) {
            if(key.equals(current.key))
                return current.val;//search hit
        }
        
        return null;//search miss
    }

 

put()的实现也是遍历链表,如果有匹配的键,则更新相应的值,否则,在链表头插入新的节点。

    public void put(Key key, Value val) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function put");
        
        if(val == null) {
            delete(key);
            return;
        }
        
        for(Node current = first; current != null; current = current.next) {
            if(key.equals(current.key)) {
                current.val = val;//search hit: update val
                return;
            }
        }
        
        first = new Node(key, val, first);//search miss: add new node.
        n++;
        
    }

 

delete()实现:

    public void delete(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function delete");
        
        Node fakehead = new Node(null, null, first);
        
        for(Node prev = fakehead; prev.next != null; prev = prev.next) {
            if(key.equals(prev.next.key)) {
                prev.next = prev.next.next;
                n--;
                break;
            }
        }
        
        first = fakehead.next;
        
    }

 

整个代码:

package com.qiusongde;

import java.util.LinkedList;
import java.util.Queue;

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;

public class SequentialSearchST<Key, Value> {
    
    private Node first;
    private int n;
    
    public SequentialSearchST() {
        first = null;
        n = 0;
    }
    
    public void put(Key key, Value val) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function put");
        
        if(val == null) {
            delete(key);
            return;
        }
        
        for(Node current = first; current != null; current = current.next) {
            if(key.equals(current.key)) {
                current.val = val;//search hit: update val
                return;
            }
        }
        
        first = new Node(key, val, first);//search miss: add new node.
        n++;
        
    }
    
    //search for key, return associated value
    public Value get(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function get");
        
        for(Node current = first; current != null; current = current.next) {
            if(key.equals(current.key))
                return current.val;//search hit
        }
        
        return null;//search miss
    }
    
    public void delete(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function delete");
        
        Node fakehead = new Node(null, null, first);
        
        for(Node prev = fakehead; prev.next != null; prev = prev.next) {
            if(key.equals(prev.next.key)) {
                prev.next = prev.next.next;
                n--;
                break;
            }
        }
        
        first = fakehead.next;
        
    }
    
    public Iterable<Key> keys() {
        
        Queue<Key> queue = new LinkedList<>();
        
        for(Node cur = first; cur != null; cur = cur.next) {
            queue.add(cur.key);
        }
        
        return queue;
        
    }
    
    //shorthand methods
    public boolean contains(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function contains");
        
        return get(key) != null;
    }
    
    //shorthand methods
    public boolean isEmpty() {
        return size() == 0;
    }
    
    public int size() {
        return n;
    }
    
    //inner class
    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 static void main(String[] args) {
        
        SequentialSearchST<String, Integer> st = new SequentialSearchST<String, Integer>();
        
        for(int i = 0; !StdIn.isEmpty(); i++) {
            
            String key = StdIn.readString();
            
            if(key.equals("-")) {
                key = StdIn.readString();
                st.delete(key);
                StdOut.println("delete " + key);
            } else {
                st.put(key, i);
                StdOut.println("put" + " " + key + " " + i);
            }
            
            StdOut.print(st.size() + " key-value pairs:");
            for(String s : st.keys()) 
                StdOut.print(" " + s + " " + st.get(s));
            StdOut.println();
            
        }
        
    }
    
}

 

测试数据:

A
B
C
D
-
B
C
E
F
-
A
B
-
B
-
F
-
D

 

Test Client的输出结果:

put A 0
1 key-value pairs: A 0
put B 1
2 key-value pairs: B 1 A 0
put C 2
3 key-value pairs: C 2 B 1 A 0
put D 3
4 key-value pairs: D 3 C 2 B 1 A 0
delete B
3 key-value pairs: D 3 C 2 A 0
put C 5
3 key-value pairs: D 3 C 5 A 0
put E 6
4 key-value pairs: E 6 D 3 C 5 A 0
put F 7
5 key-value pairs: F 7 E 6 D 3 C 5 A 0
delete A
4 key-value pairs: F 7 E 6 D 3 C 5
put B 9
5 key-value pairs: B 9 F 7 E 6 D 3 C 5
delete B
4 key-value pairs: F 7 E 6 D 3 C 5
delete F
3 key-value pairs: E 6 D 3 C 5
delete D
2 key-value pairs: E 6 C 5

 

2、无序链表符号表性能分析

结论1:在含有N个键值对的基于无序链表的符号表中,未命中的查找(search miss)和插入(insert)操作都需要N次比较。命中的查找(search hit)在最坏的情况下需要N次比较。

推论:向一个空表中插入N个不同的键需要~N2/2次比较。

 

随机查找命中,即在符号表中查找每个键的可能性是相同的,所以平均比较次数是(1+2+……+N)/N = (N+1)/2 ~ N/2

 

3、有序数组中的二分查找

采用的数据结构是一对平行的数组,一个用于存储键,一个用于存储值。

算法需要保持数组中的Comparable类型的键有序,然后使用数组的索引来高效的实现其他操作。

 

这份实现的核心是rank方法:

   //return the number of keys in the table that are smaller than key
    //the heart method
    public int rank(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function rank");
        
        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;
    }

 

对于get方法,如果键在数组中,调用rank方法即可知道键在数组中的位置。否则不存在,返回null。

    public Value get(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function get");
        
        if(isEmpty())
            return null;
        
        int k = rank(key);
        if(k < n && keys[k].compareTo(key) == 0)
            return vals[k];
        else
            return null;
        
    }

 

 

对于put方法,如果键在数组中,调用rank方法即可知道更新该键值的位置,或者插入的位置。

插入的时候,需要将后边的键都往后移动一个位置(对于大数组,移动的开销将会非常大)。

    public void put(Key key, Value val) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function put");
        
        if(val == null) {
            delete(key);
            return;
        }
        
        //it works when ST is empty(n = 0)
        int k = rank(key);
        if(k < n && keys[k].compareTo(key) == 0) {
            vals[k] = val;//key is already in symbol table
            return;
        }
        
        //move
        for(int j = n; j > k; j--) {
            keys[j] = keys[j-1];
            vals[j] = vals[j-1];
        }
        
        keys[k] = key;
        vals[k] = val;
        n++;
    }

 

delete操作:其中对于大数组,移动的开销也会非常大。

    public void delete(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function delete");
        
        if(isEmpty())
            return;
        
        int k = rank(key);
        
        if(k < n && keys[k].compareTo(key) == 0) {//key is in symbol table
            
            //move
            for(int j = k; j < n - 1; j++) {
                keys[j] = keys[j+1];
                vals[j] = vals[j+1];
            }
            
            n--;
            
            keys[n] = null;// to avoid loitering
            vals[n] = null;
            
        }
        
    }

 

其余操作:

    public Key min() {
        if(isEmpty())
            return null;
        return keys[0];
    }
    
    public Key max() {
        if(isEmpty())
            return null;
        return keys[n -1];
    }
    
    public Key select(int k) {
        if(k < 0 || k >= n)
            return null;
        return keys[k];
    }
    
    public Key ceiling(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function ceiling");
        
        if(isEmpty())
            return null;
        
        int k = rank(key);
        if(k == n)
            return null;
        else
            return keys[k];
        
    }
    
    public Key floor(Key key) {
        
        if(key == null)
            throw new IllegalArgumentException("key is null in function floor");
        
        if(isEmpty())
            return null;
        
        int k = rank(key);
        if(k < n && keys[k].compareTo(key) == 0)
            return keys[k];
        else if(k == 0)
            return null;
        else
            return keys[k-1];
        
    }
    
    public Iterable<Key> keys(Key lo, Key hi) {
        
        if(lo == null || hi == null)
            throw new IllegalArgumentException("one of the arguements is null in function keys");
        
        Queue <Key> queue = new LinkedList<>();
        
        if(lo.compareTo(hi) > 0 || isEmpty())
            return queue;//special case
        
        for(int i = rank(lo); i < rank(hi); i++) {
            queue.add(keys[i]);
        }
        if(contains(hi))
            queue.add(keys[rank(hi)]);
        
        return queue;
        
    }
    
    //shorthand methods
    public boolean isEmpty() {
        return size() == 0;
    }
    
    //shorthand methods
    public boolean contains(Key key) {
        return get(key) != null;
    }
    
    public int size() {
        return n;
    }
    
    //shorthand methods
    public void deleteMin() {
        if(isEmpty())
            throw new NoSuchElementException("underflow error");
        delete(min());
    }
    
    //shorthand methods
    public void deleteMax() {
        if(isEmpty())
            throw new NoSuchElementException("underflow error");
        delete(max());
    }
    
    //shorthand methods
    public int size(Key lo, Key hi) {
        
        if (lo == null)
            throw new IllegalArgumentException("first argument to size() is null"); 
        if (hi == null)
            throw new IllegalArgumentException("second argument to size() is null"); 
        
        if(hi.compareTo(lo) < 0)
            return 0;
        else if(contains(hi))
            return rank(hi) - rank(lo) + 1;
        else 
            return rank(hi) - rank(lo);
    }
    
    //shorthand methods
    public Iterable<Key> keys() {
        if(isEmpty())
            return new LinkedList<>();
        return keys(min(), max());
    }

 

Test Client测试结果(数据同上):

put A 0
1 key-value pairs: A 0
put B 1
2 key-value pairs: A 0 B 1
put C 2
3 key-value pairs: A 0 B 1 C 2
put D 3
4 key-value pairs: A 0 B 1 C 2 D 3
delete B
3 key-value pairs: A 0 C 2 D 3
put C 5
3 key-value pairs: A 0 C 5 D 3
put E 6
4 key-value pairs: A 0 C 5 D 3 E 6
put F 7
5 key-value pairs: A 0 C 5 D 3 E 6 F 7
delete A
4 key-value pairs: C 5 D 3 E 6 F 7
put B 9
5 key-value pairs: B 9 C 5 D 3 E 6 F 7
delete B
4 key-value pairs: C 5 D 3 E 6 F 7
delete F
3 key-value pairs: C 5 D 3 E 6
delete D
2 key-value pairs: C 5 E 6

 

 

4、二分查找性能分析

结论1:在N个键的有序数组中进行二分查找最多需要logN + 1次比较(无论是否成功)。

但是put和delete这两个方法太慢了。

结论2:向大小为N的数组中插入一个新元素,在最坏的情况下需要访问~2N次数组。因此向一个空的符号表中插入N个元素,在最坏的情况下需要访问~N2次数组。

 

五、总结

二分查找法适用于静态表(不允许插入),在初始化的时候就进行排序。

但是二分查找法不适用于查找和插入操作是混合进行的,而且符号表非常大的情况。

目前很多应用都需要同时支持高效的查找和插入两种操作

 

如何保证查找和插入操作都是对数级别的算法和数据结构?

首先,要支持高效的插入操作,需要一种链式结构;但是单链接的链表是不能支持二分查找法的。

为了将二分查找法的效率和链表的灵活性结合起来,需要更加复杂的数据结构,二叉查找树

Symbol Table(符号表)