散列表和散列函数hashcode()
关于hash算法可以阅读以下文章
https://www.cnblogs.com/xiohao/p/4389672.html
https://blog.csdn.net/u014209205/article/details/80820263
在数组中查找只需要1次查询就可以得到所需数据,散列表就是希望使用数组提高查询速度。当有一个大小为M的数组时,我们根据键值分配其在数组中的位置,一般情况下我们使用java实现的hashcode()方法得到一个int值,根据hashcode()%M的值来决定在数组中的位置。
很显然当键的数量大于M时,会出现hash冲突,也就是两个不同的键值的散列值相同,我们可以增大数组M来减少冲突,但是没法消除冲突,因为键值是由调用散列表的程序的输入决定的,而且数组的维护需要内存,此时使用拉链法和开放地址法解决冲突。
拉链法
当hash冲突的时候对散列值相等的部分采用链表:首先根据散列值找到链表,然后在链表中顺序查找,拉链法的实现主要有两种:
1 数组中存储链表的结点类型Node,根据散列值找到相应的Node,这个Node就是链表的首节点,然后根据键值key判断首节点键值是否就是目标值,如果是则返回首节点的value,如果不是则根据Node的next属性找到下一个节点进行判断,直到找到对应键值的结点或者链表查找完毕返回null。插入时根据散列值找到对应的Node,如果为空则形成新的结点和引用,如果不为空则判断键值是否相等,如果相等则改变对应Node的value,如果不等则查找下一位,直到遇到null或者键值相等的结点。java的HashMap的源码中就是使用了这种方法,数组tab[]中保存的是链表的Node或者红黑树的TreeNode。
2 数组中存储的是链表,而不是链表的结点。根据散列值直接找到链表,然后使用链表的方法查找、插入和删除。下面的代码就是采用这种方式,代码中LinkedListST访问超链接LinkedListST。
可以看到,拉链法在查找和插入时比较的次数和链表的长度有关,无论键如何分布,链表的平均长度肯定是键的个数和数组的长度的比值:N/M。所以平均情况下的查找和插入最多需要N/M次比较和1次散列值的计算。
/**
* 基于顺序链表查找表实现的拉链法的散列表
*/
import second.LinkedListST;
@SuppressWarnings("unchecked")
public class LinkedChainHashST<Key,Value> {
private LinkedListST<Key, Value>[] tab;//使用链表的数组
private final int M=97;
public LinkedChainHashST(){
tab=(LinkedListST<Key, Value>[])new LinkedListST[M];
for (int i = 0; i < tab.length; i++) {
tab[i]=new LinkedListST<Key, Value>();
}
}
public Value get(Key key){
return (Value)tab[hash(key)].get(key);
}
public void put(Key key,Value value){
tab[hash(key)].put(key,value);
}
public void delete(Key key){
tab[hash(key)].delete(key);
}
public int hash(Key key){
return (key.hashCode() & 0x7fffffff)%M;
}
public void show(){
for (int i = 0; i < tab.length; i++) {
tab[i].show();
}
}
public static void main(String[] args) {
LinkedChainHashST<String, Integer> st=new LinkedChainHashST();
st.put("lm",3);
st.put("sgt",4);
st.put("zx",2);
st.put("sq",1);
st.delete("zx");
st.show();
}
}
查询的速度和链表的长度有关,在链表的长度很长时可以扩大M来减小长度,通常情况下链表的长度保持在2-8之间。N>8*M时M可以增加2倍。
开发地址法
拉链法存储的键值数量N小于数组长度M,在散列值重复的地方形成链表,开放地址法中是M>N,利用数组中的空位解决hash冲突。开放地址散列表最简单的方法为线性探测法:使用两个数组keys[]和values[]分别保存键值key和相应的value。插入时,根据hash值访问数组中的元素,如果该位置为空,那么目前没有和该键冲突的键,直接改变keys[]和values[]中的值,如果该位置不为空则存在冲突,那么判断key是否相等,如果相等则是重复,只改变values[]中的值,如果不相等则判断下一位,直到遇到null或者键值相等,当到达数组末尾时转向从0开始。查找时计算散列值找到元素如果键值相等则返回values[]中的值,如果不相等则查询下一位,遇到相等的键值或者null时结束。
线性探测法可以形象的理解为将拉链法中的链表放在数组中,将这种在数组中的“链表”称为键簇,即键值的散列值相等的键会相邻,将其称为键簇。线性探测法的性能和
α
=
M
/
N
\alpha =M/N
α=M/N有关,我们将其称为使用率,很显然使用率越大,冲突的机会则会变小,查找时键簇变短,查找更快,Kunth在1962年证明了命中所需的比较次数近似为
1
2
(
1
+
1
1
−
α
)
\frac{1}{2}\left ( 1+\frac{1}{1-\alpha } \right )
21(1+1−α1)未命中所需的比较次数近似为
1
2
(
1
+
1
(
1
−
α
)
2
)
\frac{1}{2}\left ( 1+\frac{1}{(1-\alpha) ^{2} } \right )
21(1+(1−α)21)
为了保证查找的速度一般保证$$之间,当N<M/8时checksize()缩短一半,N>M/2checksize()增加一倍。线性探测中删除操作需要处理数组中的后序元素,因为将该键值内容设为null后原来和改键冲突的键值将因为遇到null而不会被访问到。
线性探测法的实现如下:
/**
* 基于线性探测法的散列表
* @author XY
*
*/
@SuppressWarnings("unchecked")
public class LinearProbingHashST<Key,Value> {
private Key[] keys;
private Value[] values;//两个数组保存键和对应的数据
private int M=97;
private int N;
public LinearProbingHashST(){
keys=(Key[])new Object[M];
values=(Value[])new Object[M];
}
public void put(Key key,Value value){
int x=hash(key);
while (keys[x]!=null) {
if(key.equals(keys[x])){//已经存在
values[x]=value;
return;
}else x=(x+1)%M;
}
keys[x]=key;
values[x]=value;
N++;
checksize();
}
public Value get(Key key){
int x=hash(key);
while(keys[x]!=null){
if(key.equals(keys[x])) return values[x];//存在
else {x=(x+1)%M;}
}
return null;
}
public void delete(Key key){
for (int i = hash(key);keys[i]!=null; i=(i+1)%M) {
if(keys[i].equals(key)){
keys[i]=null;
values[i]=null;//该键值存在,删除
rest((i+1)%M);//为了避免此位置的null对后面的造成影响
N--;
checksize();
}
}
}
private void rest(int x){
while(keys[x]!=null){//非空值向前移位,直到遇到null
Key tempk=keys[x];
Value tempv=values[x];
keys[x]=null;
values[x]=null;
N--;
put(tempk, tempv);
x=(x+1)%M;
}
}
private void checksize(){//检查大小,使得使用率在1/2~1/8之间
if(N<M/8){
Key[] skeys=keys;
Value[] svalues=values;
this.M=M/2;
keys=(Key[])new Object[M/2];
values=(Value[])new Object[M/2];
for (int i = 0; i < M; i++) {
if(skeys[i]!=null) put(skeys[i],svalues[i]);
}
}else if (N>M/2) {
Key[] skeys=keys;
Value[] svalues=values;
this.M=M*2;
keys=(Key[])new Object[M*2];
values=(Value[])new Object[M*2];
for (int i = 0; i < M; i++) {
if(skeys[i]!=null) put(skeys[i],svalues[i]);
}
}
}
public int hash(Key key){
return (key.hashCode() & 0x7fffffff)%M;
}
}