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(符号表)的更多相关文章
- 符号表 symbol table 符号 地址 互推
https://zh.wikipedia.org/wiki/符号表 https://en.wikipedia.org/wiki/Symbol_table 在计算机科学中,符号表是一种用于语言翻译器(例 ...
- 符号表实现(Symbol Table Implementations)
符号表的实现有很多方式,下面介绍其中的几种. 乱序(未排序)数组实现 这种情况,不需要改变数组,操作就在这个数组上执行.在最坏的情况下插入,搜索,删除时间复杂度为O(n). 有序(已排序)数组实现 这 ...
- 符号表(Symbol Tables)
小时候我们都翻过词典,现在接触过电脑的人大多数都会用文字处理软件(例如微软的word,附带拼写检查).拼写检查本身也是一个词典,只不过容量比较小.现实生活中有许多词典的应用: 拼写检查 数据库管理应用 ...
- 《Algorithms 4th Edition》读书笔记——3.1 符号表(Elementary Symbol Tables)-Ⅳ
3.1.4 无序链表中的顺序查找 符号表中使用的数据结构的一个简单选择是链表,每个结点存储一个键值对,如以下代码所示.get()的实现即为遍历链表,用equals()方法比较需被查找的键和每个节点中的 ...
- 《Algorithms 4th Edition》读书笔记——3.1 符号表(Elementary Symbol Tables)-Ⅲ
3.1.3 用例举例 在学习它的实现之前我们还是应该先看看如何使用它.相应的我们这里考察两个用例:一个用来跟踪算法在小规模输入下的行为测试用例和一个来寻找更高效的实现的性能测试用例. 3.1.3.1 ...
- 《Algorithms 4th Edition》读书笔记——3.1 符号表(Elementary Symbol Tables)-Ⅱ
3.1.2 有序的符号表 典型的应用程序中,键都是Comparable的对象,因此可以使用a.compare(b)来比较a和b两个键.许多符号表的实现都利用Comparable接口带来的键的有序性来更 ...
- 《Algorithms 4th Edition》读书笔记——3.1 符号表(Elementary Symbol Tables)-Ⅰ
3.1符号表 符号表最主要的目的就是将一个键和一个值联系起来.用例能够将一个键值对插入符号表并希望在之后能够从符号表的所有键值对中按照键值姐找到对应的值.要实现符号表,我们首先要定义其背后的数据结构, ...
- C/C++编译和链接过程详解 (重定向表,导出符号表,未解决符号表)
详解link 有 些人写C/C++(以下假定为C++)程序,对unresolved external link或者duplicated external simbol的错误信息不知所措(因为这样的错 ...
- ELF Format 笔记(七)—— 符号表
最是那一低头的温柔,像一朵水莲花不胜凉风的娇羞,道一声珍重,道一声珍重,那一声珍重里有蜜甜的忧愁 —— 徐志摩 ilocker:关注 Android 安全(新手) QQ: 2597294287 符号表 ...
随机推荐
- 我最喜欢的模板jade(pug)学习和使用
由于版权问题,现已改名pug.但无须担心,几乎没什么区别.就算依然使用jade也不会有太大影响. 慢慢迁移过渡即可 # 官网 https://pugjs.org # github https://gi ...
- Mac下配置mnmp环境
虽然比较喜欢玩下新语言, 但是php还是常会用到的. lnmp很多人都听过, 但是不能用在Mac上面, 另外还有个mnpp但在osx 10.8.3下面跑不起来.所以自己手动一步步安装, 整理了方便安装 ...
- synchronized加static区别
在多线程中,在synchronise方法上加上static 表示的是类锁,锁住的是整个类.而synchroinized 锁住的是当前方法,当前对象.
- 解决 三星Note3 桌面小部件不实时更新/不刷新 的问题
机型及问题描述:我的是三星note3 (国行 SM-N9008V),已ROOT,安装了LBE安全大师.在桌面小部件中,有些不会实时更新.比如有 滴答清单(办过的事项无法勾选),百度云音乐(歌曲播放更新 ...
- 成长这事儿,不可不说-------Day36
事实上我一直都有一个观点,从我当年刚学抛物线那会就有:人生事实上就是一条轨迹,无非是一些点的集合.只是有些在低谷,有些在高峰,放形象了看,有些熠熠生辉,有些暗淡的几若消逝,有些人总喜欢回头数着过往的痕 ...
- 使用Highcharts实现柱状图展示
第一步 新建页面line.html,引入HighCharts核心js文件 <script type="text/javascript" src="../../js/ ...
- .Net 开发Windows Service
1.首先创建一个Windows Service 2.创建完成后切换到代码视图,代码中默认有OnStart和OnStop方法执行服务开启和服务停止执行的操作,下面代码是详细解释: using Syste ...
- ios -仿微信有多级网页时,显示返回跟关闭按钮
@property (nonatomic, copy) NSString * url; @interface WebViewController ()<UIWebViewDelegate,UIG ...
- Obj-C数组以及字符串拼接与分割
本文转载至 http://mobile.51cto.com/iphone-392148.htm Obj-C只是增加了一点“特殊语料”的C语言,所以可以用printf()代替NSLog().但我们建议使 ...
- Aeroplane chess(简单概率dp)
Hzz loves aeroplane chess very much. The chess map contains N+1 grids labeled from 0 to N. Hzz start ...