触发我这次总结的原因
在xxx的一次面试中,面试官的第一道题目就是手写一个HashMap的,因为之前也看过类似的分析,大概知道HashMap的实现原理,但是实际手写一个的时候发现并不是那么容易。
Hash散列算法的原理
Hash散列思想的出现
在《数据结构》中专门有一章节介绍了查找的算法,主要有:顺序查找、折半查找、AVL树查找……,但是这些查找都需要对原来数据结构中的元素进行遍历(无非是在算法上记性了
改进,减少了遍历的次数而已)。后来有人提出能不能建立一种被查找的值与存储位置的映射关系,然后直接可以一步到位呢。基于这种思想,Hash散列的思想出现了。
Hash散列的思想原理
其实Hash散列的原理很简单,就是建立了
location<----->值之间的映射函数f(key),某人在查询一个值的时候,可以直接放到函数f(key)中,直接返回location,然后一个return:
举个例子:
我们现在有很多的地理数据,比如:北京、上海、成都、重庆,利用普通的存储,比如一般的数组,那么存储的方式就是
array=[北京,上海,成都,重庆],比如想查找北京,那只能从第一个元素以此向后比对,如果array[i]的值和我们预期的一样,那就找到了
找到北京需要一次比较,但是找到重庆就需要四次比较了。所以查找的时间复杂度为O(n)
如果利用散列的话,可以考虑这么搞
设计散列函数f(key)={取出第一个汉子的首字母,然后利用首字母在字母表中的序号作为这个key的hans值}
则
[北京,上海,成都,重庆] 中的每个元素的Hash值为
[2,19,3,3],则数组的第二个位置存放北京,第19个位置存放上海
如果我要查询到上海的值,那么直接通过f(key=‘上海’)=19,直接返回上海的存储位置19,我们直接去19号位置取出来,就是上海的元素。
java中的HashMap的原理
通过上面的Hash散列的原理介绍知道,hash表其实就是实现了映射关系
key<----------->存储位置,但是java中的HashMap稍有区别,那就是java中的HashMap是通过散列之后的hashcode计算location的,不是key直接计算的。
对比一下
普通散列 | key | location | |
java中HashMap | key | hashcode=hash(key) | location=index(hashcode) |
所以我这边的titile使用的是二次散列,即key值并没有直接转换为location位置,而是先进行了一步散列值计算后,再利用散列值再计算得到的存储位置。
为什么这个费事的需要两步计算才能得到location的值呢,下面看源码分析
源码分析
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
HashMap在进行插入数据的时候(也就是put操作),会首先调用我们自己Key的hashcode方法得到一个hash值,然后再利用这个hashcode的值,再计算得到在内部存储的位置。
现在从源码角度回答一下为什么这样搞
首先从刚才哪个例子(北京、上海哪个例子)中知道,最终location的结果都是int的,因为只有数组才具有随机读取的优点(其他的像链表啊二叉树啊都没有给定下标就直接get的优点)。那么我怎么知道我的HashMap中存的是个啥(Object、String)这些不确定因素怎么才能转成最终的int呢,答案就是java的上帝Object自带了一个hashcode的方法,这个方法的声明就决定了不管谁来,我都能给你一个int的输出,至于输出的int你想怎么搞,那你定。说实话这玩意就是专门给散列用的,也就是为了方便计算location用的。
这样一来问题就解决了。
答案
hashcode函数或者先计算hash值的原因就是为了方便转成int,以方便下一步更好的计算出location的存储位置。
冲突消除
可以简单想一下,相同的key值,hashcode也相同(只针对String,Object的话另说,这也可能是java设计Object的hashcode方法的精妙吧),所以为了避免冲突,先比对下看查询或者put的这个key的hashcode是否相同,不相同那就是没有。如果说是hashcode相同的,那这个时候还的看看key值是不是我要的哪个key,因为不同的key也会产生相同的hashcode。所以才会有了为什么要让“你”重写equals方法。