解决hash冲突的方法有四个:链地址法( 拉链法)、开放地址法、再哈希法和建立公共溢出区法
链地址法
链地址法:将相同hashCode的元素链接在一个链表中。
HashMap就是数组+链表的实现方式,也就是通过“链地址法”来解决hash冲突,如果hash冲突太严重,则会导致链表很长,导致map的性能急剧下降,因此随后hashMap又使用了数组+链表+红黑树的实现方式,当链表的长度超过8,那么采用红黑树来保存node元素。
优点:
- 对于记录总数频繁可变的情况,处理的比较好(也就是避免了动态调整,就是全部再hash的开销)
- 由于记录存储在链表结点中,不会造成内存的浪费,如果记录本身size很大,指针的开销可以忽略不计
- 删除记录时,比较方便,直接通过指针操作即可
缺点:
- 存储的记录是随机分布在内存中的,在查询记录时,相比结构紧凑的数据类型(比如数组),哈希表的跳转访问会带来额外的时间开销
- 如果记录数量是已知的,并不会发生变化,可以创建一个不会产生冲突的完美哈希函数,此时封闭散列的性能将远高于开放散列
- 由于使用指针,记录不容易进行序列化(serialize)操作
开放地址法
开放地址法:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。对应的计算公式为: H_i = (H(key)+d_i)%m 或者 H_i = H(key+d_i)%m,i=1,2,…,n,
其中i表示第i次hash计算,H()表示hash方法,d_i表示增量,根据增量的不同产生方式,又可以分为:
- 线性探测再散列
d_i=1,2,3,…,m-1
这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
- 二次探测再散列
d_i=1^2,-1^2,2^2,-2^2,3^2,-3^2,…,,k^2,-k^2( k<=m/2 )
这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
- 伪随机探测再散列
具体实现时,应建立一个伪随机数发生器,,并给定一个随机数做起点,产生一个伪随机数序列,如d_i=(i+p) % m。然后利用公式进行再hash。
这里会奇怪使用开放地址法怎么查找正确的数据,查找数据时会比较hash值和key本身,二者都相同才会对其进行操作,如果hash值相同而key值不同,那么则继续进行hash计算。
优点:
- 记录更容易进行序列化(serialize)操作(没有指针)
- 如果记录总数可以预知,可以创建完美哈希函数,此时处理数据的效率是非常高的
缺点:
- 记录的数目不能超过桶数组的长度,如果超过就需要扩容,而扩容会导致某次操作的时间成本飙升
- 使用探测序列,有可能由于冲突次数太多导致计算的时间变长,导致哈希表的处理性能降低
- 由于记录是存放在桶数组中的,而桶数组必然存在空槽,空槽占用的空间会导致明显的内存浪费
- 删除记录比较麻烦。比如需要删除记录a,记录b是在a之后插入桶数组的,但是和记录a有冲突,是通过探测序列再次跳转找到的地址,所以如果直接删除a,a的位置变为空槽,而空槽是查询记录失败的终止条件,这样会导致记录b在a的位置重新插入数据前不可见,所以不能直接删除a,而是设置删除标记。这就需要额外的空间和操作
再哈希法
再哈希法:
建立公共溢出区法
建立公共溢出区法:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。