什么是哈希表
根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
什么是哈希冲突
根据一定的规则放进存放哈希值的数组中,然后下标为1的数组已经有值了,后面根据规则,判定某个数也需要放到下标为1的数组中,这样就导致了只有一个位置两个人都要坐,就引起了冲突。(不同的key值产生的H(key)是一样的)。
解决哈希冲突的方法
1.开放地址
插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。
Hi=(H(key)+di)%m //开放地址法计算下标公式
Hi:下标(储存的地址)
H(key):哈希函数(计算哈希值)
di:增量
%:取模
m:哈希表的长度
探查方法
线性探查
di=1,2,3,…m-1;冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
二次探查
di=1^2, -1^2, 2^2, -2^2 …k^2, -k^2,(k
随机探查
di=伪随机数序列;冲突发生时,建立一个伪随机数发生器(如i=(i+p) % m),p是质数(在m范围取得质数),生成一个伪随机序列,并给定一个随机数做起点,每次加上伪随机数++就行。
例子
设哈希表长为14,哈希函数为H(key)=key%11。表中现有数据15、38、61和84,其余位置为空,如果用二次探测再散列处理冲突,则49的位置是?使用线性探测法位置是?
解:因为H(key)=key%11
所以15的位置 = 15 % 11=4; 38的位置 = 38 % 11=5; 61的位置 = 61 % 11=6; 84的位置 = 84 % 11=7;(证明哈希表4,5,6,7已经有元素)
因为计算下标的公式为:Hi=(H(key)+di)mod%m
使用二次探测法
H(1) = (49%11 + 1^1) = 6;冲突
H(-1) = (49%11 + (-1^2)) = 4;冲突 注意 -1^2 = -1; (-1)^2 = 1;
H(2) = (49%11 + 2^2) = 9;不冲突
二次探测法49的位置就是哈希表的9。
使用线性探测
H(1) = (49%11 + 1) = 6;冲突
H(2) = (49%11 + 2) = 7;冲突
H(3) = (49%11 + 3) = 8;不冲突
线性探测法49的位置就是哈希表的8。
再哈希法
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
链地址法
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。
比如 66和88这两个元素哈希值都是1,这就发生了哈希冲突,采用链地址法,可以把 66和88放在同一个链表中。如下图
建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
HashMap的hash()算法
为什么不是h = key.hashCode()直接返回,而要 h = key.hashCode() ^ (h >>> 16)来计算哈希值呢?
回答:减少哈希冲突
//源码:计算哈希值的方法 H(key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//^ (异或运算) 相同的二进制数位上,数字相同,结果为0,不同为1。 举例如下:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 1 = 0
1 ^ 0 = 1
// &(与运算) 相同的二进制数位上,都是1的时候,结果为1,否则为零。 举例如下:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
h = key.hashCode() ^ (h >>> 16)意思是先获得key的哈希值h,然后 h 和 h右移十六位 做异或运算,运算结果再和 数组长度 - 1 进行 与 运算,计算出储存下标的位置。具体原理如下:
综下所述 储存下标 = 哈希值 & 数组长度 - 1
//jdk1.7中计算数组下标的HashMap源码
static int indexFor(int h, int length) {
//计算储存元素的数组下标
return h & (length-1);
}
//jdk1.8中去掉了indexFor()函数,改为如下
i = (n - 1) & hash //i就是元素存储的数组下标
某个key的哈希值为 :1111 1111 1110 1111 0101 0101 0111 0101,数组初始长度也是16,如果没有 ^ h >>> 16,计算下标如下
1111 1111 1110 1111 0101 0101 0111 0101 //h = hashcode()
& 0000 0000 0000 0000 0000 0000 0000 1111 //数组长度 - 1 = 15 (15的二进制就是 1111)
------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0101 //key的储存下标为5
由上面可知,只相当于取了后面几位进行运算,所以哈希冲突的可能大大增加。
以上条件不变,加上 异或h >>> 16,之后在进行下标计算
1111 1111 1110 1111 0101 0101 0111 0101 //h = hashcode()
^ 0000 0000 0000 0000 1111 1111 1110 1111 //h >>> 16
------------------------------------------
1111 1111 1110 1111 1010 1010 1001 1010 //h = key.hashCode() ^ (h >>> 16)
& 0000 0000 0000 0000 0000 0000 0000 1111 //数组长度 - 1 = 15 (15的二进制就是 1111)
------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1010 //key的存储下标为10
重要:由上可知,因为哈希值得高16位和低16位进行异或运算,混合之后的哈希值,低位也可能掺杂了高位的一部分特性(就是变化性增加了),这样就减少了哈希冲突。
为什么HashMap的初始容量和扩容都是2的次幂
也是为了减少哈希冲突
原理:
因为判断储存位置的下标为 : i = (n - 1) & hash,n就是数组的长度。
2的次幂 - 1,其二进制都是1,比如 2^4 -1= 15(1111),2^5-1 = 31(11111)。
因为 n-1 和 hash 进行与运算,只有 1 & 1 ,才为1。
因为 n-1永远是2的次幂-1,(n - 1) & hash的结果就是 hash的低位的值。
1111 1111 1110 1111 0101 0101 0111 0101 //hash值
& 0000 0000 0000 0000 0000 0000 0000 1111 //数组长度 - 1 = 15 (15的二进制就是 1111)
------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0101 //高位全部清零,只保留末四位(就相当于保留了hash的低位)
如果容量不是2次幂会怎么样呢?如下图表
2次幂的时候,数组长度为16,n-1 = 16 -1 = 15(1111)
- 非2次幂的时候,数组长度为10,n-1 = 10 -1 = 9(1001)
重要:由上看出,n为2的次幂,哈希冲突会更少,保证元素的均匀插入。
如果指定了不是2的次幂的容量会发生什么
会获得一个大于指定的初始值的最接近2的次幂的值作为初始容量。
比如:输入 9 获得 16,输入 5 获得 8。