哈希表和hashmap()中关于hash函数理解。
概述
哈希表又称散列表。
基本思路是:把n个关键字为 k i k_i ki(i=1,2,3…n)的元素,根据关键字 k i k_i ki 在哈希函数上的映射,存到长度为 m 的连续内存单元中去,这样的线性存储结构为哈希表。
哈希函数hash() :——以关键字
k
i
k_i
ki为自变量,通过hash()映射到内存单元地址
h
(
k
i
)
h(k_i)
h(ki),即哈希地址为因变量,取值为(0-m-1)。,在HashMap中
k
i
k_i
ki一般为key对象的hashcode() 。
问题:在映射过程中,可能出现关键字 k i k_i ki不同,但得到的哈希地址相同的情况,这种现象叫做哈希冲突。我们需要尽量减少这种冲突,使得哈希地址尽量不一样,这样线性表才会更加散列,均匀。
核心:哈希函数
1. 直接定址法
哈希函数 为:
h
(
k
)
=
k
+
c
h(k) = k+c
h(k)=k+c
适用于:关键字分布连续的情况。较少用。
2. 除留取余法
哈希函数 为:
h
(
k
)
h(k)
h(k) =
k
k
k
m
o
d
mod
mod p
(
p
<
=
m
)
(p<=m)
(p<=m) m为线性表长度
除留取余法计算简单,较为常用,p取奇数比偶数好,当p取<=m的素数时效果最好。
3.改进1
改进后哈希函数 为:
h
(
k
)
h(k)
h(k) =
k
k
k &
(
m
−
1
)
(m-1)
(m−1) m为线性表长度。
原因:1、除法效率低。& 运算比 % 效率高
2、
k
k
k &
(
m
−
1
)
(m-1)
(m−1) =
k
k
k
m
o
d
mod
mod p
&运算结果和除留取余法一样的。
前提: m=
2
x
2^x
2x ,即线性表长度必须为2的整数倍幂
效率更高,绝大多数情况下length一般都小于2^16=65536
为什么必须 是2的整数幂?
因为
m
=
2
x
m=2^x
m=2x 时,m-1表示为二进制数才为 x个1。
例如:
2
3
−
1
=
7
2^3-1=7
23−1=7,转化为二进制为1111,3个1.
2
16
−
1
=
65535
2^{16}-1=65535
216−1=65535,转化为二进制位1111 1111 1111 1111, 16个1。
这样
k
k
k &
(
m
−
1
)
(m-1)
(m−1) 就总是将k的 低x位 和 x 个 1 进行 与运算。即将k的低16位与线性表长度-1(m-1)进行与运算。这样才能保留低位的1。
4.改进2
为了达到更好的散列效果:原来是将key的低16位与线性表长度-1(m-1)进行与运算。但是一般Map中存放value的线性表长度很小,那么与运算的结果就会很多值是相同的,达不到散列的效果,所以让key值的高16位与key本身进行异或运算这样,得到的结果重复值会较少,散列效果好。所以哈希函数为:
h
(
k
)
h(k)
h(k) =
k
k
k ^
(
k
>
>
>
16
)
(k>>>16)
(k>>>16)
在java 中查看 HashMap()类的源码发现 hash()函数是这样定义的:
// hash()方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
其中 ^ 异或运算, >>> 是右移位
java中int类型占4个字节,共32位。
7 >>>2 是将7的二进制向右移动两位,左边高位补0。如下:
0000 0000 0000 0000 0000 0000 0000 0111
>>>2
0000 0000 0000 0000 0000 0000 0000 0001
所以h>>>16就是将h向右移动16位,即取h的高16位。
^ 异或运算比 & 和 | 运算效果更好。因为 & | 运算结果会趋于0 或1 。
HashMap之所以能根据get(key)直接拿到value,原因是它内部通过用一个大数组存储所有value,并根据hashcode()函数计算出value应该存储在数组中的索引,这样通过哈希表实现查找。
在Map<key,value>中,一个Entry对象包括:key,value,hash值, 指向下一个Entry对象的引用。两个相同的Key对象(指内容相同即调用equals方法返回true,所以如果key是自定义的类型,必须重写equals方法) 返回的数组索引,即hash值一定是相同的。但两个不同的key对象返回的数组索引应该尽量不同(为什么是尽量?因为会出现不同的key值对应的索引是相同的,这就是冲突。)
遇到冲突怎么办?
这时,就把重复的添加到同一hash值的元素的后面,让 next引用指向它,这样形成一个链表。所以数组索引处可以认为存放的就是一个链表。没有发生冲突的索引处就是只有一个元素的链表。JDK8以后,当链表长度大于8时,就转化为红黑树,这样就大大提高了查找效率。
所以HashMap要想快速查找关键就是让hash值不冲突,这样就不用遍历链表,才能提高效率。