符号表
符号表最主要的目的就是将键与值联系起来
定义:符号表是一种储存键值对的数据结构,支持两种操作:插入(put),即将一组新的键值对存入表中;查找(get),即根据提供的键得到相应的值
API
返回值 | 名称 | 作用 |
构造函数 | ST() | 创建一张符号表 |
void | put(Key key,Value val) | 将键值对放入表中(若为空值,则删除这个键值对) |
Value | get(Key key) | 获取键key对应的值,若不存在则返回null值 |
void | delete(Key key) | 删除键key对应的键值对 |
boolean | contains(Key key) | 判断key是否存在于表中 |
boolean | isEmpty() | 表是否为空 |
int | size() | 表中键值对数量 |
Iterable | keys() | 表中所有键的集合 |
关于符号表的一些规定
- 采用泛型构造符号表有利于扩展用例
- 对于重复的键我们规定每个键值对应一个值,当向符号表中存入已有的值时会将新值覆盖旧值
- 禁止空键的产生
- 同样禁止空值的产生,一旦赋予空值,按时删除这个键值对
查找的成本模型
成本模型的计算,通过统计比较次数(等价性测试,或是键的相互比较)。在内循环不进行比较(极少)的情况下,通过统计访问数组的次数来确定成本模型
实现二分查找(基于有序数组)
package cn.ywrby.Searches;
//二分查找,基于有序数组
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import java.util.NoSuchElementException;
public class BinarySearchST<Key extends Comparable<Key>,Value> {
private static final int INIT_CAPACITY=2; //定义数组的初始大小
private Key[] keys; //键数组
private Value[] vals; //值数组
private int n=0 ; //数组大小
//无参构造函数
public BinarySearchST(){
this(INIT_CAPACITY); //利用固定初始大小调用含餐构造函数
}
//含参构造函数
public BinarySearchST(int capacity){
keys=(Key[]) new Comparable[capacity];
vals=(Value[])new Object[capacity];
}
//动态调整数组大小
private void resize(int capacity){
assert capacity>=n:"不可以动态将数组调大"; //判断传入容量大小,不可以将容量调大。只通过put增加数组大小
//assert [boolean 表达式] : [错误表达式 (日志)] 返回一个报错
Key[] tempKey=(Key[]) new Comparable[capacity];
Value[] tempValue=(Value[]) new Object[capacity];
for(int i=0;i<n;i++){ //逐个复制放入新数组
tempKey[i]=keys[i];
tempValue[i]=vals[i];
}
vals=tempValue;
keys=tempKey;
}
public int size(){return n;}
public boolean isEmpty(){return size()==0;}
//判断键是否存在
public boolean contains(Key key) {
//如果键为null即报错,警告键不可能为null
if (key == null) throw new IllegalArgumentException("argument to contains() is null");
return get(key) != null;
}
public Value get(Key key){
if (key == null) throw new IllegalArgumentException("argument to get() is null"); //防止搜索null键
if(isEmpty()) return null; //防止搜索空数组
int i=rank(key); //获取对应键的索引值
if(i<n && keys[i].compareTo(key)==0) return vals[i]; //返回对应值
return null; //未找到
}
/*
*查找目标键的值
* 如果存在,返回的对应键的索引值
*如果不存在,返回表中小于它的键的数量(即新键的位置)
* */
public int rank(Key key){
if(key==null) throw new IllegalArgumentException("argument to rank() is null"); //避免索引null值
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; //while循环结束条件是lo>hi此时lo的值即为小于key的键的数量
}
//插入元素
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(keys.length*2);
}
//将值按键序放入
for(int j=n;j>i;j--){
keys[j]=keys[j-1];
vals[j]=vals[j-1];
}
keys[i]=key;
vals[i]=val;
n++;
assert check();
}
//删除对应键的元素
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];
}
n--;
keys[n]=null;
vals[n]=null;
//删减后动态调整数组大小
if (n > 0 && n == keys.length/4) resize(keys.length/2);
assert check();
}
//删除最小元素
public void deleteMin() {
if (isEmpty()) throw new NoSuchElementException("Symbol table underflow error");
delete(min());
}
//删除最大元素
public void deleteMax() {
if (isEmpty()) throw new NoSuchElementException("Symbol table underflow error");
delete(max());
}
//返回数组最小值
public Key min() {
if (isEmpty()) throw new NoSuchElementException("called min() with empty symbol table");
return keys[0];
}
//返回数组最大值
public Key max() {
if (isEmpty()) throw new NoSuchElementException("called max() with empty symbol table");
return keys[n-1];
}
//返回数组第k个键
public Key select(int k) {
if (k < 0 || k >= size()) {
throw new IllegalArgumentException("called select() with invalid argument: " + k);
}
return keys[k];
}
//向下取整,找出小于等于该键的最大键
public Key floor(Key key) {
if (key == null) throw new IllegalArgumentException("argument to floor() is null");
int i = rank(key);
if (i < n && key.compareTo(keys[i]) == 0) return keys[i];
if (i == 0) return null;
else return keys[i-1];
}
//向上取整,找出大于等于该键的最小键
public Key ceiling(Key key) {
if (key == null) throw new IllegalArgumentException("argument to ceiling() is null");
int i = rank(key);
if (i == n) return null;
else return keys[i];
}
//返回表中所有键的集合(已排序)
public Iterable<Key> keys() {
return keys(min(), max());
}
//返回表中从lo至hi的所有键的集合
public Iterable<Key> keys(Key lo, Key hi) {
if (lo == null) throw new IllegalArgumentException("first argument to keys() is null");
if (hi == null) throw new IllegalArgumentException("second argument to keys() is null");
Queue<Key> queue = new Queue<Key>();
if (lo.compareTo(hi) > 0) return queue;
for (int i = rank(lo); i < rank(hi); i++)
queue.enqueue(keys[i]);
if (contains(hi)) queue.enqueue(keys[rank(hi)]);
return queue;
}
//检查数组是否符合规定条件
private boolean check() {
return isSorted() && rankCheck();
}
//检查是否已经排序
private boolean isSorted() {
for (int i = 1; i < size(); i++)
if (keys[i].compareTo(keys[i-1]) < 0) return false;
return true;
}
//检查是否出现重复的键
private boolean rankCheck() {
for (int i = 0; i < size(); i++)
if (i != rank(select(i))) return false;
for (int i = 0; i < size(); i++)
if (keys[i].compareTo(select(rank(keys[i]))) != 0) return false;
return true;
}
public static void main(String[] args) {
BinarySearchST<String, Integer> st = new BinarySearchST<String, Integer>();
for (int i = 0; !StdIn.isEmpty(); i++) {
String key = StdIn.readString();
st.put(key, i);
}
for (String s : st.keys())
StdOut.println(s + " " + st.get(s));
}
}
上述实现中,rank方法可以利用递归实现,更容易理解函数调用过程
public int rank(Key key,int lo,int hi){
if(hi<lo) return lo;
int mid=lo+(hi-lo)/2;
int cmp=key.compareTo(keys[mid]);
if(cmp<0) return rank(key,lo,mid-1);
else if(cmp>0)return rank(key,mid+1,hi);
else(return mid;)
}
算法分析
在N个键的有序数组中进行二分查找最多需要(lgN+1)次比较(无论是否成功)
显然易知C(0)=0,C(1)=1,且对于N>0可以利用递归方式归纳得知
C(N)=C([N/2]_{向下取整})+1
特殊的,当N=2^n-1时
C(2^n-1)<=C(2^{n-1}-1)+1
利用上式不断替换得到
C(2^n-1)<=C(2^0)+n
易得
C(N)<=n+1<lgN+1
对于其他一般形式也可得类似结果
二分查找很好的解决了查找元素的问题,但没有解决插入元素的难题,向有序数组中插入元素,最坏情况下需要访问数组~2N次,因此向一个空数组中插入N个元素,最坏情况需要N^2次访问。