基于算法专题 之 散列表(上)我们对什么是散列表以及如何构造一个合适的散列函数进行了详细讲解,这一节将解决散列冲突的核心问题,并提供一些现有可供选择的合理方案。01冲突的发生冲突(collision),其核心就是两个元素被映射到同一个槽中,所谓一山难容二虎,因此发生冲突上一节中,我们讲解了如何精心设计散列函数,让分布尽均匀。事实上,再好的散列函数都无法避免散列冲突,因为对于散列表而言,无论设置的存储区域(n)有多大,当需要存储的数据个数大于 n 时,那么必然会存在哈希值相同的情况。从而产生散列冲突。因此我们要直面冲突从而针对冲突提供解决方案。常见有三类方法:开放寻址法、链地址法和公共溢出区法。
02开放寻址法2.1 定义

将散列函数扩展定义成探查序列,即每个关键字有一个探查序列h(k,0)、h(k,1)、…、h(k,m-1),这个探查序列一定是0….m-1的一个排列(一定要包含散列表全部的下标,不然可能会发生虽然散列表没满,但是元素不能插入的情况),如果给定一个关键字k,首先会看h(k,0)是否为空,如果为空,则插入;如果不为空,则看h(k,1)是否为空,以此类推。

开放寻址的核心思想:如果出现了散列冲突,我们就重新探测一一个空闲位置,将其插入。比较经典的有线性探测方法(Linear Probing)、二次探测(Quadratic probing)和 双重散列(Double hashing)方法。

(1)线性探测方法(Linear Probing)

说明当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置(index)开始,依次向index-1,index+1位置查找,index-2,index+2,依次类推。特点:当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,线性探测的时间就会越来越久。最坏情况下的时间复杂度为 O(n)。拓展如何从基于线性探索的散列表中删除一个键?如果你想到直接将该键所在的位置设为null是不行的,因为这会使得在此位置之后的元素无法被查找。算法专题 之 散列表(下)_散列表定如上图中显示的键值和哈希值,O的散列值为4,但是这个位置已经被H占用,实际存储在7号位置上。如果用置为null的方法删除键O前面的任何一个,例如键E,然后查找O,get()方法将无法找到O。基于上面的问题,可以考虑有两个方案:方法1:被删的槽中置一个特定的值 DELETED,而不是 NIL。哈希插入时做相应调整,使得 DELETED 标志的槽位仍可放入新的元素。哈希查询时遇到特定值也继续查找。方法2:将簇中被删除键的右侧的所有键重新插入散列表。(2)二次探测(Quadratic probing)二次探测是二次方探测法的简称。顾名思义,使用二次探测进行探测的步长变成了原来的“二次方”,也就是说,它探测的下标序列为 hash(key)+0,hash(key)+1^2或[hash(key)-1^2],hash(key)+2^2或[hash(key)-2^2]。

(3)双重散列(Double hashing)

所谓双重散列,意思就是不仅要使用一个散列函数,而是使用一组散列函数 hash1(key),hash2(key),hash3(key)......,先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。


03链地址法3.1 原理

如果遇到冲突,在原地址新建一个空间,然后以链表结点的形式插入到该空间。当插入的时候,只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可。

基于上面的描述可以很好理解,就是把得出来相同散列值存在一个链表中。具体如下图所示:算法专题 之 散列表(下)_数据_023.2 特点
用链地址法散列的最坏情况是所有的关键字 n 都散列到同一个槽中,从而产生出一个长度为 n 的链表,这时最坏情况下查找的时间为 O(n)。

04公共溢出区法4.1 原理为所有冲突的关键字记录建立一个公共的溢出区来存放。在查找时,对给定关键字通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表进行顺序查找。

基于上面的描述,可以理解为把有冲突的单独拎出来做特殊处理,具体如下图所示:

算法专题 之 散列表(下)_散列函数_03

4.2 特点

如果相对于基本表而言,在有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。


05拓展(d-left hashing)5.1 原理

d-left hashing中的d表示多个,我们先简化看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,f1和f2。在存储一个新的key时,同 时用两个哈希函数进行计算,得出两个地址f1[key]和f2[key]。这时需要检查T1中的f1[key]位置和T2中的f2[key]位置,哪一个 位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key 存储在左边的T1子表中。在查找一个key时,必须进行两次hash,同时查找两个位置。

5.2 特点

可以一定程度降低冲突的概率,并且可以通过选择来均衡负载,但是实现相对复杂一点,可以根据具体场景选择。


06

散列表和二叉树的选择

散列表和二叉树都是两个非常棒的设计,散列表的优点很明显,理想情况下查询时间为常数1,而二叉树的查询速度也很快,为log(n),相对慢于散列表。上面阐述了散列表比二叉树的优势是查询速度较快,二叉树查找比散列表的优势有哪些?


  • 列表中的数据是无序存储的
  • 散列表存在哈希冲突,且扩容耗时
  • 散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,不然会浪费一定的存储空间。

基于上面的总结,平衡二叉查找树在某些方面还是优于散列表的,但是这两者的存在并不冲突,可以根据场景合理选择。


​​算法专题 之 散列表(下)_散列表_04