一.哈希表(散列)
1.什么是哈希表
根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数H(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数H(key)为哈希(Hash)函数。
2.什么是哈希冲突(面试题)
根据一定的规则放进存放哈希值的数组中,然后下标为1的数组已经有值了,后面根据规则,判定某个数也需要放到下标为1的数组中,这样就导致了只有一个位置两个人都要坐,就引起了冲突。(不同的key值产生的H(key)是一样的)。
3.解决哈希冲突的方法(面试题)
(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<=m/2); 冲突发生时,在表的左右进行跳跃式探测,比较灵活。
③随机探查
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。
(2) 再哈希法
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,....,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
(3) 链地址法
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。
(4)建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
二.HashMap
1.HashMap的hash()算法(面试)
回答:减少哈希冲突
java
//源码:计算哈希值的方法 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
进行 与
运算,计算出储存下标的位置。具体原理如下:
java
综下所述 储存下标 = 哈希值 & 数组长度 - 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
,计算下标如下
java
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
,之后在进行下标计算
java
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位进行异或运算,混合之后的哈希值,低位也可能掺杂了高位的一部分特性(就是变化性增加了),这样就减少了哈希冲突。