复习HashMap-3

一.哈希表(散列)

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()算法(面试)

回答:减少哈希冲突

	//源码:计算哈希值的方法 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位进行异或运算,混合之后的哈希值,低位也可能掺杂了高位的一部分特性(就是变化性增加了),这样就减少了哈希冲突。

你可能感兴趣的:(哈希算法,散列表,算法)