散列表原理及实现
散列表原理
散列表:使用算术操作将键转化为数组的索引来访问数组中的键值对, 使用散列表,可以实现常数级别的查找和插入.
使用散列的查找算法主要要解决的两个问题:
- 散列函数的设计(即如何用散列函数将被查找的键转化为数组的一个索引).
- 处理碰撞冲突的过程(即处理两个或多个键的散列值相同的情况).
PS:处理碰撞冲突的方法主要有拉链法和线性探测法.
散列函数的设计
实现散列函数的指导思想 :
设计的散列函数能够均匀并独立地将所有的键散布于0 ~ M-1之间.(其中M为存放键值的数组的大小)
优秀的散列方法需要满足三个条件:
- 一致性 : 等价的键必然产生相等的散列值
- 高效性 : 计算简便
- 均匀性 : 均匀地散列所有的键
散列方法举例—hashCode()方法结合除留余数法
将默认的hashCode()方法与除留余数法结合起来产生一个0~M-1的整数,一般会将数组的大小M取为素数以充分利用原散列值的所有位
private int hash(Key key){
return (key.hashCode()&0x7fffffff % M);
}
处理碰撞冲突
碰撞处理就是处理两个或多个键的散列值相同的情况
拉链法
将大小为M的数组中的每个元素指向一条链表,链表的每个节点都存储了散列值为该元素的索引的键值对.
使用拉链法查找键值的过程 :
- 根据散列值找到对应链表
- 沿着对应链表查找相对应的键
拉链法实现
1. 拉链法基础方法
public class SeparateChainingHashST<Key extends Comparator<? super Key>,Value> {
private int N;//键值对总数
private int M;//散列表使用的数组大小
private SequentialSearchST<Key,Value>[] st;//存放链表对象的数组,实现可参照<算法第四版SequentialSearchST.class>
public SeparateChainingHashST(){
this(997);
}
public SeparateChainingHashST(int M){
/** 创建M条链表 */
this.M=M;
st=(SequentialSearchST<Key,Value>[])new SequentialSearchST[M];
for(int i=0;i<M;i++){
st[i]=new SequentialSearchST();
}
}
}
2. 拉链法中的hash函数实现
/**
* hash函数
* @param key 要插入的键
* @return hash值
*/
private int hash(Key key){
return (key.hashCode()&0x7fffffff % M);
}
3. 拉链法的put()和get()方法
public void put(Key key,Value value){
st[hash(key)].put(key,value);
}
public Value get(Key key){
return (Value)st[hash(key)].get(key);
}
线性探测法
开放地址散列表 : 用大小为M的数组保存N个键值对,其中M>N,依靠数组中的空位来解决碰撞冲突(线性探测法是最简单的开放地址散列表)
线性探测法原理 :
当碰撞发生时(当一个键的散列值已经被另外一个不同的键占用),直接检查散列表的下一个位置(索引值+1)直到遇到相同的键或者遇到了空缺的位置
遇到相同的键则替换其值,遇到空缺位置则把键值写入即可.
线性探测法实现
1. 线性探测法基础方法
public class LinearProbingHashST <Key,Value>{
private int N;//存放的键值对的总数
private int M;//线性检测表的大小
private Key[] keys;//键
private Value[] values;//值
public LinearProbingHashST(){
keys=(Key[]) new Object[M];
values=(Value[])new Object[M];
}
private LinearProbingHashST(int size){
this.M=size;
keys=(Key[]) new Object[M];
values=(Value[])new Object[M];
}
/**
* resize()方法就是新建一个线性探测表,然后将原表的数据插入
* @param size 线性探测表的大小
*/
private void resize(int size){
LinearProbingHashST newTable=new LinearProbingHashST<Key,Value>(size);
for(int i=0;i<M;i++){
if(keys[i]!=null){
newTable.put(keys[i],values[i]);
}
}
keys=(Key[])newTable.keys;
values=(Value[])newTable.values;
M=newTable.M;
}
}
2. 线性检测法中的hash函数实现(与拉链法相同)
private int hash(Key key){
return (key.hashCode()&0x7fffffff)%M;
}
3. 线性检测法中的put()和get()方法实现
public void put(Key key,Value value){
if(N>=M){
resize(2*M);
}
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++;
}
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;
}
4. 线性检测法中的delete()方法实现
public void delete(Key key){
/** 没有找到key */
if(get(key)==null){
return;
}
/** 找到key对应的数组索引位置,并将其键与值删除 */
int i=hash(key);
while(!keys[i].equals(key)){
i=(i+1)%M;
}
keys[i]=null;
values[i]=null;
/** 将被删除键的右侧连续的所有键重新插入 */
while(keys[i]!=null){
Key keyToRedo=keys[i];
Value valueToRedo=values[i];
keys[i]=null;
values[i]=null;
N--;
put(keyToRedo,valueToRedo);
i=(i+1)%M;
}
N--;
if(N>0&&N==M/8) resize(M/2);
}
拉链法与线性检测法的性能差距
拉链法为每个键值对都分配了一小块内存,而线性探测法则为整张表使用了两个很大的数组,但是两者的性能差距还是因场景不同而有所变化,最好的方法还是去实践一下.