⒈ 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 后会继续向后查找。
- 线性探查法
以 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 为例,以下为示意图:
线性探查法由于在发生碰撞时值会连续存储,所以有很高的缓存性能;但正是由于值的连续存储,造成集群现象,导致查找和插入比较耗时。
- 二次探查法
以 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
二次探查法在发生碰撞时,值的连续性不如线性探查法,所以缓存性能也不如线性探查法,但也不会像线性探查法那样出现集群现象,所以查找和插入不会像线性探查法那样耗时。
- 再 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 的数量和频次确定时使用 |
缓存性能差 | 缓存性能比较好 |
链表需要额外占用空间 | 不需要额外占用空间 |