每篇一句:一个真正的快乐的人,是能够享受他的创造的人。那些像海绵一样,只取不予的人,只会失去快乐。 ——《洛克菲勒留给儿子的38封信》
1.基本思想
有序符号表使用的数据结构是一对平行的数组,一个存储键一个存储值。在 put 时可以保证数组键有序,然后使用数组的索引高效实现 get() 和其他操作
2.有序符号表的 API
/ | 方法 | 描述 |
---|---|---|
void | put(Key key, Value value) | 将键值对存入表中 |
Value | get(Key key) | 获取键 key 对应的值 |
boolean | contains(Key key) | 键 key 是否存在于表中 |
boolean | isEmpty() | 表是否为空 |
int | size() | 表中的键值对数量 |
Key | min() | 最小的键 |
Key | max() | 最大的键 |
Key | floor(Key key) | 小于等于 key 的最大键 |
Key | ceiling(Key key) | 大于等于 key 的最小键 |
int | rank(Key key) | 小于 key 的键的数量 |
Key | select(int k) | 排名为 k |
void | deleteMin() | 删除最小的键 |
void | deleteMax() | 删除最大的键 |
int | size(Key lo, Key hi) | [lo···hi]之间键的数量 |
Iterable | keys(Key lo, Key hi) | [lo···hi]之间键的数量 |
Iterable | keys() | 表中所有键的集合,已排序 |
3.代码实现
package symboltable;
import com.sun.org.apache.xpath.internal.functions.FuncFloor;
import edu.princeton.cs.algs4.Queue;
public class BinarySearchST<Key extends Comparable<Key>, Value> {
private Key[] keys; //这里使用两个数组来保存键和值
private Value[] values;
private int N;
@SuppressWarnings("unchecked")
public BinarySearchST(int capacity) {
keys = (Key[]) new Comparable[capacity];
values = (Value[]) new Object[capacity];
}
public void put(Key key, Value value) {
int i = rank(key);
if(i < N && keys[i].compareTo(key) == 0) {
values[i] = value; //如果找到匹配的值则更新
}
for(int j = N; j > i; j--) { //将所有较大的元素全部向后移动一位
keys[i] = keys[j-1];
values[j] = values[j-1];
}
keys[i] = key;
values[i] = value;
N++;
}
public Value get(Key key) {
if(isEmpty())
return null;
int i = rank(key); //返回小于它的元素数量
if(i < N && keys[i].compareTo(key) == 0)
return values[i];
else
return null;
}
public Key delete(Key key) {
int i = rank(key);
if(keys[i].compareTo(key) == 0) { //如果找到元素,则将后面的元素向前移动一位
for(int j = i; j < N - 1; j++) {
keys[j] = keys[j + 1];
values[j] = values[j + 1];
}
N--;
return keys[i];
}
return null;
}
public boolean contains(Key key) {
int i = rank(key);
return keys[i].equals(key);
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public Key min() {
return keys[0];
}
public Key max() {
return keys[N-1];
}
public Key floor(Key key) {
int i = rank(key);
for(int j = i; j >= 0; j--) {
if(select(j).compareTo(key) != 1)
return select(j);
}
return null;
}
public Key ceiling(Key key) {
int i = rank(key);
return keys[i];
}
public int rank(Key key) {
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; //如果找到该键,rank() 会返回该键 的位置,也就是表中小于它的键的数量
}
return lo; //如果不存在,lo 就是表中小于它的键的数量
}
public Key select(int k) {
return keys[k];
}
public void deledtMin() {
delete(min());
}
public void deleteMax() {
delete(max());
}
public int size(Key lo, Key hi) {
if(hi.compareTo(lo) < 0)
return 0;
else if(contains(hi))
return rank(hi) - rank(lo) + 1;
else {
return rank(hi) - rank(lo);
}
}
public Iterable<Key> keys(Key lo, Key hi){
Queue<Key> queue = new Queue<Key>();
for(int i = rank(lo); i < rank(hi); i++) { //将 lo~hi(不包括hi)的元素入队
queue.enqueue(keys[i]);
}
if(contains(hi)) //判断表中是否包含 hi
queue.enqueue(keys[rank(hi)]);
return queue;
}
public Iterable<Key> keys(){
return keys(min(), max());
}
public static void main(String[] args) {
BinarySearchST<Integer, String> binarySearchST = new BinarySearchST<>(10);
for(int i = 0; i < 5; i++) {
binarySearchST.put(i, "Timber" + i);
}
System.out.println("size = " + binarySearchST.size());
for(int k : binarySearchST.keys()) {
System.out.println("key:" + k + ", value:" + binarySearchST.get(k));
}
binarySearchST.delete(3);
System.out.println("删除后:");
for(int k : binarySearchST.keys()) {
System.out.println("key: " + k + ", value: " + binarySearchST.get(k));
}
System.out.println("小于等于 3 的最大键: " + binarySearchST.floor(3));
System.out.println("大于等于 3 的最小键: " + binarySearchST.ceiling(3));
}
}
3.结果展示
4.rank() 方法分析
这份实现的核心就是 rank() 方法,它返回表中小于给定键的数量。它首先将 key 和中间键比较,如果相等则返回其索引,如果小于中间键则在左半部分查找,大于则在右半部分查找。
public int rank(Key key){
int lo = 0, hi = N -1;
while(lo <= hi){
int mic = 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;
}
}
}
非递归版本的二分查找的性质:
- 如果表中存在该键,rank() 返回该键的位置,也就是表中小于该键的键的数量;
- 如果表中不存在该键,rank() 还是应该返回表中小于它的键的数量,也就是在循环结束时 lo 的值正好等于表中小于 被查找的键 的键的数量。
在有序数组中使用二分查找排名的轨迹如下图。(图片来自 Algorithms, 4th Edition )
5.性能分析
在 N 个键的有序数组中进行二分查找最多需要 (lgN + 1) 次比较 (无论是否成功)。而向其中插入一个新的元素在最坏情况需要访问 ~2N 次数组,因此向一个空符号表中插入 N 个元素在最坏情况下需要访问 ~2N 次数组。
其具体方法的操作成本如下:
方法 | 运行所需时间的增长数量级 |
---|---|
put() | N |
get() | logN |
delete() | N |
contains() | logN |
size() | 1 |
min() | 1 |
max() | 1 |
floor() | logN |
ceiling() | logN |
rank() | logN |
select() | 1 |
deleteMin() | N |
deleteMax() | 1 |
6.顺序查找和二分查找的比较
一般来说,二分查找都比顺序查找快得多。但是,二分查找也不适合很多应用。例如,无法处理 Leipzig Corpora 数据库,因为查找和插入操作是混合进行的,而且符号表也太大。
下表列出了顺序查找和二分查找的性能特点,表中是运行时间的增长数量级(二分查找是数组的访问次数,其他的则是比较次数):
算法 | 最坏情况下(N次插入后) | 平均情况下(N次插入后) | 是否高效支持有序性操作 | ||
---|---|---|---|---|---|
查找 | 插入 | 查找 | 查找 | ||
顺序查找 | N | N | N/2 | N | 否 |
二分查找 | lgN | 2N | lgN | N | 是 |
7.写在最后
如果有什么不对或建议,欢迎批评指正。