HashMap、HashTable等中的hash()方法分析
HashMap、HashTable、ConcurrentHashMap是非常常用的、应用广泛的数据类型,关于他们的内部结构网上的文章很多这里就不过多的分析了,本文主要分析其内部的hash()方法。这里首先问下,你知道HashMap中hash方法的具体实现吗?你知道HashTable、ConcurrentHashMap中hash方法的实现以及原因吗?你知道为什么要这么实现吗?你知道为什么JDK 7和JDK 8中hash方法实现的不同以及区别吗?如果你不能很好的回答这些问题,那么你需要好好看看这篇文章。文中涉及到大量代码和计算机底层原理知识。
1. 哈希概述
1.1 哈希定义
Hash,直接翻译过来就是“散列”,也有直接音译为“哈希”的,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
1.2 常见的哈希函数
常见的Hash函数有以下几个:
(1)直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
(2)数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
(3)除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
(4)分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
(5)平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
(6)伪随机数法:采用一个伪随机数当作哈希函数。
1.3 哈希碰撞
两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。即如果两个输入串的hash函数的值一样,则称这两个串是一个碰撞(Collision)。既然是把任意长度的字符串变成固定长度的字符串,所以必有一个输出串对应无穷多个输入串,碰撞是必然存在的。
1.4 碰撞处理
衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。任何哈希函数基本都无法彻底避免碰撞,常见的解决碰撞的方法有:开放寻址(Open Addressing)法、链接(Chaining)法、再哈希法和建立公共溢出区。
1.4.1 开放寻址法
所有的元素都在散列表中,每一个表项或包含动态集合的一个元素,或包含NIL。这种方法中散列表可能被填满,以致于不能插入任何新的元素。在开放寻址法中,当要插入一个元素时,可以连续地检查或探测散列表的各项,直到有一个空槽来放置待插入的关键字为止。有三种技术用于开放寻址法:线性探测、二次探测以及双重探测。
(1)线性探测
给定一个普通的散列函数h’:U —>{0,1,…..,m-1},线性探测方法采用的散列函数为:h(k,i) = (h’(k)+i)mod m,i=0,1,….,m-1
探测时从i=0开始,首先探查T[h’(k)],然后依次探测T[h’(k)+1],…,直到T[h’(k)+m-1],此后又循环到T[0],T[1],…,直到探测到T[h’(k)-1]为止。探测过程终止于三种情况:
(1)若当前探测的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探测的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探测到T[h'(k)-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
线性探测方法较容易实现,但是存在一次群集问题,即连续被占用的槽的序列变的越来越长。采用例子进行说明线性探测过程,已知一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:
散列过程如下图所示:
(2)二次探测
二次探测法的探查序列是:h(k,i) =(h’(k)+i*i)%m ,0≤i≤m-1 。初次的探测位置为T[h’(k)],后序的探测位置在次基础上加一个偏移量,该偏移量以二次的方式依赖于i。该方法的缺陷是不易探查到整个散列空间。
(3)双重散列
该方法是开放寻址的最好方法之一,因为其产生的排列具有随机选择的排列的许多特性。采用的散列函数为:h(k,i)=(h1(k)+ih2(k)) mod m。其中h1和h2为辅助散列函数。初始探测位置为T[h1(k)],后续的探测位置在此基础上加上偏移量h2(k)模m。
1.4.2 链接法
将所有关键字为同义词的结点链接在同一个链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。
举例说明链接法的执行过程,设有一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:
最终散列结果如下: