哈希表与HashMap
1.哈希表是什么?
哈希表通过计算一个以记录的关键字为自变量的函数(哈希函数)来得到该记录的储存地址。
2.哈希冲突是什么?
哈希冲突:对于某个哈希函数H和两个关键字K1和K2,如果K1不等于K2,而H(K1)=H(K2),则称为哈希冲突。
通俗来讲,就是两个关键字映射到了同一个地址上,产生了冲突。
我们也称K1和K2为同义词。
注意:理想情况是哈希地址平均的分散,这样会减少冲突。但冲突无法完全避免,只能减少。因为哈希函数是从关键字集合到地址集合的映像。简单来说,如果关键字和地址是1对1,那么就不会有冲突,然而哈希函数是一种压缩映像函数,就是为了减少存储空间的,所以1对1没有意义。
3.如何处理哈希冲突?
解决冲突,就是给冲突的关键字再找一个空的地址来存放。那么有下面几种处理哈希冲突的方式:
A.开放定址法
Hi=(H(key)+di )%m i=1,2,...,k (k<=m-1)
H(key)为哈希函数,m为哈希表表长,di为增量序列。
增量序列有以下几种:
1.di=1,2,3...,m-1,线性探测再散列
2.di=1²,-1²,2²,-2²,...,±k²(k<= m/2),二次探测再散列
3.di=伪随机数序列,称为随机探测再散列
最简单的是线性探测再散列。
缺点:1.溢出需要单独处理。2.容易产生聚集现象。
二次探测再散列和随机探测再散列可以减少聚集现象。
B.链地址法
也叫拉链法,即在查找表的每个记录中增加一个链域,其中存放下一个具有相同哈希函数值的记录的存储地址。利用链域,就把若干个发生冲突的记录链接在一个链表内。当链域的值为null,表示已没有后继记录了,因此,对于发生冲突时的查找和插入操作就和线性表一样了。
C.再哈希法
Hi=RHi(key) (i=1,2,...,k)
RHi均是不同的哈希函数。即在同义词发生地址冲突时计算另一个哈希函数地址,直到冲突不再发生。这种方法不易产生聚集现象,但增加了计算时间。
D.建立公共溢出区
只要发生冲突,都填入到公共溢出区中。
4.什么是装填因子?
α=表中装入的记录数/哈希表的长度。
α标志着哈希表的装满成都。α越大,表中填入的记录越多,越容易发生冲突,查找给定值需要比较的关键字就越多。
了解了哈希表的知识点,那么理解我们常用的HashMap的实现原理就很简单了。
HashMap也采用哈希存储方式,因此也会产生哈希冲突,HashMap的处理冲突方法就是上面的B.链地址法,通过建立一个链域,来存放冲突的关键字指向的地址。
因此HashMap的源码原理通过猜测就可以得出:
首先有个哈希函数,来计算哈希地址,也就是map.put方法,一个put的操作。
如果这个哈希地址没值,那么就存进去,如果有,就是产生了哈希冲突,就在这个哈希地址链域增加一个指针,指向产生冲突的关键字的哈希地址。
我们查看HashMap的源码就会发现,原理就是我们猜测的大致一样。
HashMap的底层由Entry对象的数组存储,通过hash算法计算在数组中的存储位置,
put方法,根据equals决定其在数组位置上的链域中的存储位置。
put方法有两种处理:1.如果key相同,覆盖。2.如果key不同(哈希冲突),将当前的key-value放入链表中。
HashMap的哈希函数如下:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
Hash的put方法如下
public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 如果发现已有该键值,则存储新的值,并返回原始值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}
Hash的get方法如下
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}