• 当前位置 >
  • 关于建站

Hash与HashMap简单介绍

深圳市凯恒科技有限公司    {{formatDate(infoObj.addTime,'yyyy-MM-dd HH:mm:ss')}}

一、散列函数

散列函数的概念

散列函数常见的几种构造方法

二、哈希冲突

哈希冲突的概念

哈希冲突的常见解决办法

三、HashMap

HashMap的简单介绍

HashMap的几个特点

HashMap的两个关键因子

HashMap查找的时间复杂度分析

四、问题小结

HashMap中哈希函数的实现方式是什么?为什么要这样实现?

HashMap中哈希表的容量为什么一定要是2的整数次幂?

HashMap中hashcode()方法和equals()方法有什么用?

一、散列函数

1.散列函数的概念

散列函数,又称哈希函数。它是一种映射关系,根据数据元素的关键字key作为自变量,通过一定的函数关系,计算出该元素存储地址的值

表示为:address = H[key]

就是把任意长度的输入值通过哈希(散列)算法转换,得到固定长度的输出值,即哈希值。

著名的MD5和SHA1算法等等都是目前应用最广泛的hash算法。

2.散列函数常见的几种构造方法

哈希函数的构造方法有很多,总的一个原则就是要尽可能的减少冲突。

除留余数法

取关键字key被某一个p值取余而得到的值作为散列地址(散列值),其中p不大于散列表长度n 

即 H(key)=key%p, p<=m

直接寻址法

取关键字key或关键字key的某个线性函数的值作为散列地址(散列值) 

即 H(key) = key 或者 H(key) = a * key + b,其中 a 和 b 为常数

数字分析法

当关键字key的位数大于地址的位数,对key的各位分布进行分析,选出分布均匀的任意几位作为散列值 

仅适用于所有关键字都已知的情况下,根据实际情况应用确定要选取的部分,尽量减少冲突的发生

平方取中法

先计算出关键字key值的平方,然后取平方值中间任意几位作为散列地址

折叠法(叠加法)

将关键字key分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址 

用于关键字位数较多,并且关键字中每一位上的数字分布大致均匀

随机数法

选择一个随机函数,把关键字key的随机函数值作为它的散列值 

通常当关键字的长度不相等时使用这种方法

二、哈希冲突

1.哈希冲突的概念

不同的两个关键字key,因为选用哈希函数而计算出相同的哈希值,被映射到表的同一位置上,称为冲突或碰撞(collision)

发生冲突的两个关键字key称为该哈希函数的同义词(synonym)

2.哈希冲突的常见解决办法

拉链法(链接法)

将所有关键字key为同义词的结点链接在同一个单链表中。若选定的散列表长度为 m ,则可将散列表定义为一个由 m 个头指针组成的指针数组 T[0 .. m - 1]。凡是散列地址(散列值)为 i 的结点,均插入到以 T[i] 为头指针的单链表中。指针数组 T 中各分量的初值均应为空指针。

开放定址法

当冲突发生时,使用某种探测技术在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。 

根据形成探测序列的方法不同,将开放定址法区分为线性探查法,二次探查法,双重散列法等。

这里介绍一下线性探查法:

Hi = ( H(key) + i ) % m, 0 <= i <= m-1 

探测时从地址d开始,首先探查T[d],然后依次探查T[d+1]…,直到T[m-1],然后又循环到T[0]、T[1],直到探测到有空余地址或者到T[d-1]为止。

三、HashMap

1.HashMap的简单介绍

HashMap是一个采用Hash表实现的键值对集合,继承自AbstractMap,实现了Map接口。

HashMap特殊的存储结构使得在获取指定元素时先经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量的比较即可获取到元素,因此,HashMap的查找效率较高。

在HashMap中,发生哈希冲突的时候,采用拉链法进行解决,因此HashMap的底层实现是数组+链表。

jdk1.8之后,HashMap中如果一个桶(bucket)中的元素个数超过TREEIFY_THRESHOLD(默认为8)时,就会使用红黑树来替换链表,提高查找速度,称作桶的树形化。

2.HashMap的几个特点

存储的元素是无序的,顺序也会不定时的改变;

底层实现是链表 + 数组,Java8之后加入了红黑树,大大提高了查找效率;

允许有null键和null值存在,但空键只有一个,且放在第一位,看源码可知;

插入、查找的时间复杂度基本为O(1),(前提是经过良好的哈希函数运算,使得元素分布在均匀的位置);

遍历整个Map需要的时间与桶(数组)的长度成正比,因此在初始化HashMap容量 (capacity) 时不宜太大;

key采用Set存放,因此想要做到key不允许重复,需要在key对应的类中重写 hashcode() 和 equals() 方法;

3.HashMap的两个关键因子

初始容量 ( DEFAULT_INITIAL_CAPACITY : 1<<4 ):默认初始容量为16,且必须是2的整数次方

加载因子 ( DEFAULT_LOAD_FACTOR : 0.75f ):默认加载因子的大小为0.75

加载因子太大,发生冲突的可能性会加大,导致查找的效率降低;太小的话会频繁rehash,又会导致性能降低。

这两个关键因子决定了HashMap应该什么时候扩容,而HashMap扩容又伴随着相当大的开销(重新创建数组,重新哈希计算,重新分配等等),因此在设置初始容量时,应该根据实际情况先考虑Map中有多少对键值对,设置合理的加载因子,尽可能的去避免扩容,提高效率。


4.HashMap查找的时间复杂度分析

在HashMap中,查找的时间复杂度O一般跟链表长度有关,因此哈希算法越好,元素分布的越均匀,get()方法就越快。

当HashMap中有大量的元素都存放到同一个桶中,这时候哈希表中只有一个桶,桶下有一条长长的链表,这个时候HashMap与单链表无异,遍历的时间复杂度又回到了O(n),完全失去了优势。

所以Java8以后,HashMap新增了红黑树 (红黑树的结构可以控制时间复杂度为O(logn)) ,优化了这种极端情况下的性能问题。

四、问题小结

1.HashMap中哈希函数的实现方式是什么?为什么要这样实现?

在HashMap的jdk1.8源码中,hash()方法是这样写的:

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

这里同样解释了为什么HashMap允许有null键,因为当键为 null 时,返回了值为0。

通过将传入键的hashcode进行无符号右移16位,然后按位异或,得到该键的哈希值。(无符号右移,高位总补0)由于哈希表中的容量都是2的整数次幂,因此key的hashcode在很多情况下低位总是相同的,这大大加大了冲突的可能性,因此在jdk1.8以后进行了移位操作,将元素的hashcode和自己右移16位后的结果做异或运算,而int类型长度只有32位,因此无符号右移16位就相当于把自己的高位移到低位,然后再与自己的高位做异或运算,得到hash值,最后通过取模运算(n - 1) & hash得到下标值。

这样做可以避免只靠低位数据来计算hash值时导致的冲突,计算结果由高低位共同决定,极大的避免了哈希值分布不均匀,而且,采用位运算的效率非常高。

2.HashMap中哈希表的容量为什么一定要是2的整数次幂?

capacity为2的整数次幂,在计算桶的位置hashcode&(length-1)的时候,相当于hashcode%length,即对length取模,提高了计算效率;

capacity为2的整数次幂,那么capacity-1永远为奇数,则最后一位永远为1,这样就可以保证在进行hashcode&(length-1)计算的时候,得到的hash值的最后一位可能为1也可能为0(取决于hashcode最后一位),即运算完后的hash值既可能为偶数也可能为奇数,保证了散列的均匀性,否则,capacity为奇数的话,capacity-1最后一位永远为0,hashcode&(length-1)最后一位也必为0,即只能为偶数,这样任意的hashcode值只会被散列到数组的偶数下标上,浪费了近一半的空间,也大大增加了碰撞的几率。

3.HashMap中hashcode()方法和equals()方法有什么用?

当插入、查找时需要通过key的hashcode()的值进行hash()计算得到hash值,然后通过 hash&(length-1) 位运算得到下标,从而获得要找的桶的位置。

而当发生冲突时,利用key.equals()方法去链表或树中去找对应的节点。