背景
标题中的几个关键字在许多文章都会看到,但是我们并不知道它们是什么意思,下来一起来学习一下。
参考《HashMap实现原理及源码分析》 侵删
参考《Java中HashMap的实现原理》 侵删
参考《谈谈哈希表》 侵删
哈希表
Hash table(也称为散列表、哈希表),当我们需要存储集合或字典,使用线性表、树去存储时,元素在存储结构中的位置与元素的关键码之间不存在直接的对应关系,所以在要搜索一个元素时都需要进行一系列的关键码比较,搜索的效率取决于搜索过程进行的比较次数。而散列表(Hash table)是表示集合和字典的另一种有效方法,它提供了一种完全不同的存储和搜索方式,通过关键码映射到表中某个位置来存储元素,然后根据关键码用同样的方式直接访问。
用自己的话解析
存储数据时,用其他结构都需要对关键码一一比较,搜索效率可能比较低。而哈希表是将关键码和存储位置建立一个映射关系,如存储时,关键字.hashCode()的方法得到一个数组的下标,然后把该Entry(存储对象)存储在该Bucket(桶)里,那么就能大大提高搜索效率。由于hashCode()的计算规则,有可能不同的Entry得到的下标是一样的(哈希冲突),Bucket的结构就设计成单链表,存储时,如果Bucket不为空,则把Entry放进单链表里。在单链表搜索时对比关键码获得Entry。
为什么重写类的equals方法时,也需要重写hashCode方法
正确的状态:当两个不对象相等,即equals方法返回false时,但两对象hashCode方法返回的值相等,那岂不是天下大乱了?
通过hashCode就无法在哈希表中获取到正确的对象。所以一定要保证不相等的对象,返回的hashCode也不一致。另外,两个对象的hashCode()的数值相等,但两个对象用equals方法不一定相等。
equals和==
==用于比较引用和比较基本数据类型时具有不同的功能:
比较基本数据类型,如果两个值相同,则结果为true
而在比较引用时,如果引用指向内存中的同一对象,结果为true;
equals()
作为方法,实现对象的比较。由于==运算符不允许我们进行覆盖,也就是说它限制了我们的表达。因此我们复写equals()方法,达到比较对象内容是否相同的目的。而这些通过==运算符是做不到的。
object类的equals()方法的比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)
String s1=new String("abc");
String s2=new String("abc");
System.out.println(s1==s2); //返回false
System.out.println(s1.equals(s2)); //返回true
一些哈希函数的实现方法
直接定址法:这个方法我们在上述例子中有用到过,取关键字的或关键的某一个线性函数值为哈希地址。即:Hash(key)=key或者Hash(key)=a*key+b。这种构造方法下由于地址集合和关键字集合大小一样,所以并不会有冲突的发生。但是这种方法并不常用。
除留余数法:这个方法我们在上述例子中也有用到过,取关键字被某一个不大于哈希表长m的数p除后所余得的数为哈希地址。即Hash(key)=key MOD p,p<=m(m为哈希表长)。这种方法比较简单也很常用。
数字分析法:就是取关键字的若干位数组成哈希地址。当然我们在取的时候要对位数进行分析,尽量减少冲突。
平方取中法:取关键字平方后的中间若干位组成哈希地址。相比于数字分析法优点在于一个数平方之后的中间的几位数和原来的数都有关,因此哈希地址是随机的。
可以看到:在哈希函数构造的时候都是使用的数字,但如果我们的关键字不为数字的时候,我们要先将关键字转化为数字,再用哈希函数得到哈希地址。如上述例子中我们利用学生姓名作为关键字,我们首先要得到学生姓名小写拼音的ASCII码,再用ASCII码作为哈希函数的自变量,从而得到哈希地址。
哈希表处理冲突的方法
之前讲过哈希表的冲突是不可避免的只能减少的,那么如何处理冲突也成了一个不可避免的问题。
什么是处理冲突呢?:由关键字得到的哈希地址上已经有记录存在了,那么“处理冲突”就是要为该关键字的记录找到一个“没有记录存在”的哈希地址。在处理冲突的过程中可能得到一个地址序列Hi(i=1,2,3,4,5,……)。我们在处理哈希冲突的时候可能得到的另一个哈希地址H1也是有记录的,我们要求下一个地址H2,如果H2依然冲突,则找H3,依次类推,直至Hi上没有记录为止。
(1)开放地址法:Hi=(Hash(key)+di)MOD m
其中:Hash(key)为哈希函数,di为增量序列,m为哈希表长。
1、di=1,2,3,4,5……,m-1时称线性探测再散列;
2、di=12,-12,22,-22……….,+k2,-k2(k<=m/2)称之为二次探测再散列。
3、di=伪随机数序列,称之为随机探测在散列。
(2)再哈希法:Hi=RHashi(key) i=0,1,2,3,4….,k. RHashi均是不同的哈希函数,就是说在同义词产生地址冲突的时候用另一个哈希函数去求得另一个哈希地址,一直到不再有冲突
(3)链地址法:链地址法我们在上述例子有用到过。
(4)建立公共溢出区:设哈希函数产生的哈希地址集为[0,m-1],则分配两个表:
一个基本表ElemType base_tbl[m];每个单元只能存放一个元素;
一个溢出表ElemType over_tbl[k];只要关键码对应的哈希地址在基本表上产生冲突,则所有这样的元素一律存入该表中。查找时,对给定值kx 通过哈希函数计算出哈希地址i,先与基本表的base_tbl[i]单元比较,若相等,查找成功;否则,再到溢出表中进行查找。
HashMap的resize
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap什么时候进行扩容呢?
当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。