符号表的应用
0.前言
经过几篇文章的分析,我们已经清楚了符号表的几种实现方法了。本文将着重介绍符号表的实际应用。在本文的代码中,如果不特别强调,其符号表的实现方式都是红黑树来实现的。
1.集合set
集合最大的特征就是{in, not in}两种属性,因此非常适合做关键字筛选/过滤。关键字筛选/过滤在实际应用中非常广泛,例如垃圾邮件过滤、拼写纠正、浏览器痕迹等等。
我们模拟一个场景:输入一个文件和一些关键字,白名单将筛选出这些关键字并显示,黑名单将过滤这些关键字。
//白名单
public class WhiteList{
public static void main(String[] args){
SET<String> set = new SET<String>();
In in = new In(args[0]);
//读入关键字,存储在白名单中
while (!in.isEmpty())
set.add(in.readString());
//读入文件,在白名单中匹配
while (!StdIn.isEmpty()){
String word = StdIn.readString();
if (set.contains(word))
//输出匹配的
StdOut.println(word);
}
}
}
//黑名单
public class BlackList{
public static void main(String[] args){
SET<String> set = new SET<String>();
In in = new In(args[0]);
//读入关键字,存储在白名单中
while (!in.isEmpty())
set.add(in.readString());
//读入文件,在白名单中匹配
while (!StdIn.isEmpty()){
String word = StdIn.readString();
if (!set.contains(word))
//输出不匹配的
StdOut.println(word);
}
}
}
模拟运行结果如下图:
2.字典查找dictionary client
在实际应用中,经常需要挑选出一份文件的一列作为key,另若干列作为value,以方便分析或生成报表。字典查找就可以完成此类工作。
//针对CSV文件,用逗号分隔字符串的文件
public class LookupCSV{
public static void main(String[] args){
//读入文件
In in = new In(args[0]);
//读入key列
int keyField = Integer.parseInt(args[1]);
//读入value列
int valField = Integer.parseInt(args[2]);
//定义符号表
ST<String, String> st = new ST<String, String>();
while (!in.isEmpty()){
String line = in.readLine();
String[] tokens = line.split(",");
String key = tokens[keyField];
String val = tokens[valField];
//符合条件的插入符号表
st.put(key, val);
}
//输出显示
while (!StdIn.isEmpty()){
String s = StdIn.readString();
if (!st.contains(s)) StdOut.println("Not found");
else StdOut.println(st.get(s));
}
}
}
模拟运行结果如下图:
3.索引indexing client
在实际应用中,关键字索引也是很常用的。输入关键字,输出关键字所在文件或者所在的句子。
//索引关键字所在的文件
public class FileIndex{
public static void main(String[] args){
//符号表,key存字符串,value存文件File集合
ST<String, SET<File>> st = new ST<String, SET<File>>();
//遍历每一个文件
for (String filename : args) {
File file = new File(filename);
//读入文件内容
In in = new In(file);
while (!in.isEmpty()){
String key = in.readString();
//如果key不存在,则创建节点
if (!st.contains(key))
st.put(key, new SET<File>());
//如果key已经存在,则往文件集合里添加文件
SET<File> set = st.get(key);
set.add(file);
}
}
//输出显示
while (!StdIn.isEmpty()){
//输入关键字
String query = StdIn.readString();
StdOut.println(st.get(query));
}
}
}
//索引关键字所在的句子
public class Concordance{
public static void main(String[] args){
//读入文件
In in = new In(args[0]);
//预先读取文件的所有字符串
String[] words = in.readAllStrings();
//符号表,key为字符串,value为整数集合
ST<String, SET<Integer>> st = new ST<String, SET<Integer>>();
for (int i = 0; i < words.length; i++){
String s = words[i];
//如果字符串不存在,则创建节点
if (!st.contains(s))
st.put(s, new SET<Integer>());
//如果字符串已经存在,则添加索引到整数集合
SET<Integer> set = st.get(s);
set.add(i);
}
//输出显示
while (!StdIn.isEmpty()){
//输入关键字
String query = StdIn.readString();
SET<Integer> set = st.get(query);
for (int k : set)
// 输入关键字位置的前后4个字符串(形成一句话)
}
}
}
模拟运行结果如下图:
4.稀疏向量sparse vectors
常见的矩阵运算很容易实现:
...
double[][] a = new double[N][N];
double[] x = new double[N];
double[] b = new double[N];
...
//初始化 a[][] 和 x[]
...
//行循环,时间复杂度的平方级的N^2
for (int i = 0; i < N; i++){
sum = 0.0;
//列循环
for (int j = 0; j < N; j++)
sum += a[i][j]*x[j];
b[i] = sum;
}
但是,对于稀疏矩阵,其本身的存储就很浪费空间。所谓的稀疏矩阵是指少数项存放了有效数字,大多数的项都为零的矩阵。
分析稀疏矩阵之前,我们先来看看只有一行的情况,即稀疏向量。
稀疏向量
使用一维数组来存储稀疏向量,其占用空间为向量大小N。
使用符号表来存储稀疏向量,key表示index,value表示项数据,其占用空间正比于向量的非零项的个数。
public class SparseVector{
//定义哈希表,可以存储index,value存储项数据
private HashST<Integer, Double> v;
public SparseVector(){
v = new HashST<Integer, Double>();
}
//插入key-value对
public void put(int i, double x){
v.put(i, x);
}
//根据index查找
public double get(int i){
if (!v.contains(i)) return 0.0;
else return v.get(i);
}
//迭代key
public Iterable<Integer> indices(){
return v.keys();
}
//点积
public double dot(double[] that){
double sum = 0.0;
for (int i : indices())
sum += that[i]*this.get(i);
return sum;
}
}
稀疏矩阵
使用二维数组来存储稀疏矩阵,即每一行都是一个数组,其占用空间正比于矩阵大小N^2。
使用符号表来存储稀疏矩阵,即每一行都是一个稀疏向量,其占用空间正比于N+非零项的个数(N是行数组的大小,即符号表的个数)。关键字筛选