散列表总结(散列函数,拉链法,线性探测法)
散列表类似于数组,可以把散列表的散列值看成数组的索引值,访问散列表和访问数组元素一样快速,他可以在常数时间内实现查找和插入操作。
由于无法通过散列值知道键的大小,因此散列表无法实现有序性操作。
散列函数
对于一个大小为M的散列表,散列函数能够把任意的数转换成【0,M-1】内的正整数,该正整数即为hash值。
散列表存在冲突,也就是两个不同的键可能有相同的hash值。
散列函数应该满足三个条件:
一致性:相等的键应当有相等的hash值,两个键相等表示调用equals返回的值相等
高效性:计算应当简便,有必要的话可以把hash值缓存起来,在调用hash函数时直接返回。
均匀性:所有键的hash值应当均匀地分布在【0,m-1】之间,如果不能满足这个条件,有可能产生很多冲突,从而导致散列表的性能下降。
除留余数法将整数散列到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 最好是一个素数,否则无法利用键包含的所有信息。例如 M 为 10k,那么只能利用键的后 k 位。
对于其它数,可以将其转换成整数的形式,然后利用除留余数法。例如对于浮点数,可以将其的二进制形式转换成整数。
对于多部分组合的类型,每个部分都需要计算 hash 值,这些 hash 值都具有同等重要的地位。为了达到这个目的,可以将该类型看成 R 进制的整数,每个部分都具有不同的权值。
例如,字符串的散列函数实现如下:
int hash = 0;
for (int i = 0; i < s.length(); i++)
hash = (R * hash + s.charAt(i)) % M;
Copy to clipboardErrorCopied
再比如,拥有多个成员的自定义类的哈希函数如下:
int hash = (((day * R + month) % M) * R + year) % M;
Copy to clipboardErrorCopied
R 通常取 31。
java中的hashcode(),实现了哈希函数,但是默认使用对象内存地址值,在使用hashCode()时,使用除留余数法,因为内存地址32,符号位一位,只需要31即可。
int hash = (x.hashCode() & 0x7fffffff) % M;
使用 Java 的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode():
public class Transaction {
private final String who;
private final Date when;
private final double amount;
public Transaction(String who, Date when, double amount) {
this.who = who;
this.when = when;
this.amount = amount;
}
public int hashCode() {
int hash = 17;
int R = 31;
hash = R * hash + who.hashCode();
hash = R * hash + when.hashCode();
hash = R * hash + ((Double) amount).hashCode();
return hash;
}
}
拉链法:
拉链法使用链表来存储hash值相同的键,从而解决冲突。
查找需要分两步:
首先查找Key所在的链表,然后在链表中顺序查找。
对于N个键,M条链表(N>M)如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 ~N/M。
线性探测法:
使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。
使用线性探测法,数组的大小M应当大于N。
public class LinearProbingHashST<Key,Value> implements UnorderedST<Key,Value> { private int N=0; private int M=16; private Key[] keys; private Value[] values; public LinearProbingHashST(){ init(); } public LinearProbingHashST(int M){ this.M=M; init(); } private void init() { keys=(Key[]) new Object[M]; values=(Value[]) new Object[M]; } private int hash(Key key){ return (key.hashCode()&0x7fffffff)%M; } @Override public int size() { return N; } @Override public Value get(Key key) { for(int i=hash(key);keys[i]!=null;i=(i+1)%M){ if(keys[i].equals(key)){ return values[i]; } } return null; } @Override public void put(Key key, Value value) { resize(); putInternal(key,value); } private void resize() { //调整数组大小 if(N>=M/2){ resize(2*M); }else if(N<=M/8){ resize(M/2); } } private void resize(int ix) { LinearProbingHashST<Key,Value> t=new LinearProbingHashST<>(ix); for(int i=0;i<M;i++){ if(keys[i]!=null){ t.putInternal(keys[i],values[i]); } } keys=t.keys; values=t.values; M=t.M; } private void putInternal(Key key, Value value) { int i; for( i=hash(key);keys[i]!=null;i=(i+1)%M){ if(keys[i].equals(key)){ values[i]=value; return; } keys[i]=key; values[i]=value; N++; } } @Override public void delete(Key key) { int i=hash(key); while (keys[i]!=null&&!key.equals(keys[i])){ i=(i+1)%M; } if(keys[i]==null){ return; } keys[i]=null; values[i]=null; //将之后相连的键值对重新插入 i=(i+1)%M; while(keys[i]!=null){ Key key1=keys[i]; Value value1=values[i]; keys[i]=null; values[i]=null; N--; putInternal(key1,value1); i=(i+1)%M; } N--; resize(); } }
1. 符号表算法比较
算法 | 插入 | 查找 | 是否有序 |
---|---|---|---|
链表实现的无序符号表 | N | N | yes |
二分查找实现的有序符号表 | N | logN | yes |
二叉查找树 | logN | logN | yes |
2-3 查找树 | logN | logN | yes |
拉链法实现的散列表 | N/M | N/M | no |
线性探测法实现的散列表 | 1 | 1 | no |
应当优先考虑散列表,当需要有序性操作时使用红黑树。
Java符号表的实现
java.util.TreeMap:红黑树
java.util.HashMap :拉链法散列表
稀疏向量乘法
当向量为稀疏向量时,可以使用符号表来存储向量中的非0索引和值,使得乘法运算只需要对哪些非0元素进行即可。
public class SparseVector {
private HashMap<Integer, Double> hashMap;
public SparseVector(double[] vector) {
hashMap = new HashMap<>();
for (int i = 0; i < vector.length; i++)
if (vector[i] != 0)
hashMap.put(i, vector[i]);
}
public double get(int i) {
return hashMap.getOrDefault(i, 0.0);
}
public double dot(SparseVector other) {
double sum = 0;
for (int i : hashMap.keySet())
sum += this.get(i) * other.get(i);
return sum;
}
}