hash 碰撞以及常用的解决方法

988 阅读4分钟

⒈ hash 碰撞

  hash 函数接受任意的输入,然后产生一个独一无二的固定长度的值作为输出。现实中,输入可以有无限种可能,但 hash table 的长度是有限的,所以难免不同的输入产生相同的输出,这就产生了 hash 碰撞。

⒉ hash 碰撞产生的概率

   无论 hash table 有多大,hash 冲突都很容易出现。典型的例子是生日悖论, 23 个人中两个人生日相同的概率高达 50%。

⒊ hash 碰撞的解决方法

① 拉链法

   所谓拉链法,即实际发生碰撞的值存储在一个链表中,值的插入、查找、删除都在链表中进行,而 hash table 的每个 slot 中只记录指向该链表的头指针。

   例如,要将序列 50, 700, 76, 85, 92, 73, 101 存入 hash table,hash table 长度为 7,hash 函数为 key % 7。在操作过程中,50、85、92 会产生碰撞;73、101 会产生碰撞。最后产生的 hash table 示意图如下:

② 开放定址法

   所有值都存储在 hash table 中,这样就要求 hash table 的长度必须大于等于值的数量。

  • 当执行 insert 操作时,以 hash 函数得到的输出作为开始位置,持续向后查找,直到找到空的 slot。
  • 当执行 select 操作时,以 hash 函数得到的输 出作为开始位置,持续向后查找,直到找到与输入值相等的项或空 slot。
  • 当执行 delete 操作时,以 hash 函数得到的输出作为开始位置,持续向后查找,直到找到与输入值相等的项,将该 slot 标记为 deleted。之后,insert 操作会在该位置插入新值,但 search 操作在遇到这种 slot 后会继续向后查找。
  1. 线性探查法

   以 hash 函数的输出作为起始位置,依次向后查找,直到找到一个空的 slot,然后将值插入。

   以下为伪代码,其中 x 为输入值,hash(x) 即为查找开始位置,S 为 hash table 的 size。

If slot hash(x) % S is full, then try (hash(x) + 1) % S
If (hash(x) + 1) % S is also full, then try (hash(x) + 2) % S
If (hash(x) + 2) % S is also full, then try (hash(x) + 3) % S 

   仍以序列 50, 700, 76, 85, 92, 73, 101 为例,以下为示意图:

   线性探查法由于在发生碰撞时值会连续存储,所以有很高的缓存性能;但正是由于值的连续存储,造成集群现象,导致查找和插入比较耗时。

  1. 二次探查法

   以 hash 函数的输出作为起始位置,以偏移量的二次方向后查找,直到找到一个空的 slot,然后将值插入。

   仍以 x 为输入值,hash(x) 作为查找起始位置,S 为 hash table 的长度。以下为伪代码:

If slot hash(x) % S is full, then try (hash(x) + 1*1) % S
If (hash(x) + 1*1) % S is also full, then try (hash(x) + 2*2) % S
If (hash(x) + 2*2) % S is also full, then try (hash(x) + 3*3) % S

   二次探查法在发生碰撞时,值的连续性不如线性探查法,所以缓存性能也不如线性探查法,但也不会像线性探查法那样出现集群现象,所以查找和插入不会像线性探查法那样耗时。

  1. 再 hash 法

   以 hash 函数的输出作为起始位置,如果该位置对应的 slot 不为空,则对输入再次进行 hash。

   仍以 x 为输入值,hash(x) 作为查找起始位置,S 为 hash table 的长度。hash2 为再 hash 函数,以下为伪代码:

If slot hash(x) % S is full, then try (hash(x) + 1*hash2(x)) % S
If (hash(x) + 1*hash2(x)) % S is also full, then try (hash(x) + 2*hash2(x)) % S
If (hash(x) + 2*hash2(x)) % S is also full, then try (hash(x) + 3*hash2(x)) % S

   再 hash 法的值存储的连续行最差,所以缓存性能也最差。同时,由于需要进行两次 hash ,增加了计算量。但再 hash 不会出现集群现象。

③ 拉链法和开放定址法的比较

拉链法开放定址法
实现简单计算量大
不需要担心 hash table 被填满hash table 可能会被填满
对 hash 函数和装填因子不敏感需要额外关注装填因子,同时避免出现集群
在对 key 的数量和频次不确定时使用在对 key 的数量和频次确定时使用
缓存性能差缓存性能比较好
链表需要额外占用空间不需要额外占用空间