若知道一个数组中元素的索引,可以使用索引在O(1)时间内获得元素。若将map中值存储在数组当中,并使map的键通过一个函数映射到值的索引上的话,就能利用键快速获取值。以上,存储了具有键值对的对象的数组称为散列表,映射函数称为散列函数。散列是一种无需执行搜索即可通过从键得到的索引来获取值的技术。java中,map是一种使用散列实现的数据结构。
典型的散列函数首先将搜索键转换为一个称为散列码的整数值,再将散列码压缩为散列表的索引。
Java的根类Object具有hashcode方法,该方法返回一个散列码,默认是对象的内存地址。当equals方法被重写时,也应该重写hashCode方法,这样可以保证当两个对象equals为true时hashcode也相同。
基本数据类型的散列码:
byte,short,int以及char类型的搜索键,散列码即它们转为int后的值。
对于float类型的搜索键,使用Float.floatToIntBits(key)作为散列码。
对于long类型的搜索键,因为其有64位,所以会分为两部分,各32位并执行异或操作将两部分结合,这个过程称为折叠:
int hashcode = (int)(key^(key>>32));
对于double类型的搜索键,首先使用Double.doubleToLongBits方法转换为long值,再折叠。
字符串的散列码:
这里的为s.charAt(i),b为某个正数。长字符串可能会导致溢出,但是Java会忽略算术溢出。应该选择合适的b值来最小化冲突,实验显示较好的取值有31,33,37,39,41。String类中采用的是31。
压缩散列码:
键的散列码可能会是一个很大的数值,从而超过散列表的索引范围,因此需将其缩小到适合范围。假设索引0-N-1,则
int index = hashcode % N;
对N的取值,一般会选择素数来保证索引均匀展开,但是当散列表很大时,选取一个大于最大索引的素数会很耗时。因此在HashMap中,将N设置为2的整数次幂,这样能够通过&操作把散列码压缩为散列表索引值,如下式,比直接用%(取余)操作快很多。此外,也避免取余操作中,当hashcode为负值导致索引值也为负值的的问题,即下式的值永远为正。
int index = hashcode & (N-1);
Hash冲突
Hash冲突即两个不同的键值映射到同一个索引上。通常有两种方法处理:开放地址法和分离链接法。
1.开放地址法:冲突发生时,在散列表再找一个开放位置的过程。
常见的开放地址法有线性探测法,二次探测法和双重散列法
线性探测法:当发生冲突时,按顺序找到下一个可用位置。若在索引[k%N]发生冲突,则检查[(k+1)%N]是否可用以此类推。若检查到表的终点则返回表的起点继续检查,直到找到一个不冲突的位置。
查找散列表条目时,先按照散列函数获得键相对的索引,再比对索引处的对象的键是否与要查找的键相等,若不是,则按顺序找下一个位置直到找到。删除条目时,和查找一样先找到条目的位置,若找到,则在该位置放一个特殊的标记表示该位置可用。所以散列表中的每个单元可能有三个状态:被占,标记,可用。
缺点:易导致散列表中连续的单元组被占用。每个连续的单元组称为簇,当簇的大小增大时,可能会和其他簇合并,从而放慢查找时间。
二次探测法:若索引[k%N]产生冲突,二次探测法将从[(k+j^2)%N]开始检查,其中j=1,2,3,……。二次探测法避免了成簇问题,但是又二次成簇的问题,即产生冲突的键在寻找下一个可用位置时总是采用通用的探测序列。此外,线性探测法可以保证只要表不是满的,总能找到可用单元,二次探测法无法保证。
双重散列法:以上两种方法的探测序列都是对k加上一个增量来得到的,且增量独立于键。双重探测法运用了第二个散列函数来确定增量。探测序列索引为(k+j*h2(key))%N,这里的h2即第二个散列函数,j取值为0,1,2,3,……从而避免成簇问题。第二个散列函数的设计应该要能使探测序列覆盖整个散列表,且不能有0值,因为增量不能是0。
2.分离链接法:将产生冲突的键值对都放置在相同索引处,并使用“桶”来放置同位置的多个键值对。
“桶”可以采用ArrayList或者LinkedList实现。
装填因子和再散列
装填因子用于衡量一个散列表有多满,若溢出,则要复制原散列表的所有条目到新的更大的散列表中,这个过程称为再散列。
装填因子是现有条目数目与散列表大小的比例。对于开放地址法,需维持装填因子在0.5以下,对于分离链表法,维持在0.9以下。再散列的代价比较大,为了减小再散列的可能性,应该至少将散列表的大小翻倍。
HashMap采用了分离链接法处理冲突,用LinkeList实现桶,且装填因子为0.75。set也是使用散列技术实现的。HashSet是基于HashMap实现的,将元素保存于map的key中,而value使用一个static final的Object对象标识。插入新元素时通过判断HashMap中是否有key与新元素相等,若无则添加,若有则跳过,以此保证不重复性。相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成。