导语
哈希表是我们在开发中经常使用到的一种数据结构。它的结构也决定了它不可避免地会产生冲突。那么,如何解决哈希冲突呢?
我搜索了一些资料,如今结合自己的理解做一个学习记录。
我们先给出一个产生哈希冲突的例子:
(16、25、3、29、5、2、19、17、34)
Hash(key)= key % 11。
对应的地址应该是:
5、3、3、7、5、2、8、6、1
很显然存在哈希冲突。
接下来我们来看一下不同方法分别是如何处理这种冲突的:
(一)开放定址法
第一种方法是开放定址法,也就是说当冲突发生时,使用某种探查技术生成一个探查序列,再按照该序列逐个寻找。
插入:
- 寻找一个开放的地址,将待插入节点存入该地址单元,插入成功;
- 如果查找到了最后一个地址仍未找到,则代表该表已满,插入失败。
查找:
- 寻找给定的关键字,查找成功;
- 如果找到了开放的地址,或者查找到了最后一个地址,说明表中无待查关键字,查找失败。
有以下几种常见的探查技术:
(1)线性探查法
h(key) = d;
探查序列为:d、d+1、d+2、…、0、1、…、d-1。
解决方案:
(16、25、3、29、5、2、19、17、34)
Hash(key)= key % 11。
哈希值: 5、3、3 4、7、5 6、2、8、6 7 8 9、1
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
– | 34 | 2 | 25 | 3 | 16 | 5 | 29 | 19 | 17 | – |
缺点:
- 容易造成堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。这样一来,再有新的记录时更容易发生冲突,也就是说非同义词也可能发生冲突,进一步造成堆聚。
- 删除工作困难,只能标记该节点已删除,而不能真正置空该位置因为会截断其后的同义词的查找。这是所有开放定址法的共同缺陷。
- 如果发生溢出,要另外设立顺序溢出表。
(2)线性补偿探查法
h(key)=d;
哈希表长度为n,q与n互质(能保证遍历所有位置)。
探查序列为:di = (d+q)%n(0<=i<=n-1)
解决方案:
(16、25、3、29、5、2、19、17、34)
Hash(key)= key % 11。
不妨取q = 5。
哈希值: 5、3、3 8、7、5 10、2、8 2 7 1、6、1 6 0。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
34 | 19 | 2 | 25 | – | 16 | 17 | 29 | 3 | – | 5 |
缺点:
- 同线性探测,删除困难。
(3)随机探测
随机探测就是将线性探测的步长从常数改为随机数。
不同的关键字随机产生不同的探测序列,可以减少堆聚。
优点:
- 减少堆聚
缺点:
- 尽管不同关键字具有不同的探测序列,但同一关键字的探测序列却是固定的,所以仍然不能直接删除,而只能打上删除标记。
(4)二次探查
探查序列为:di = h(key) + i2(i>=0).
不能遍历所有空位,但当用该办法查找不到空位时,也是该扩充哈希表的时候。
(二)再哈希法
再哈希法也叫再散列法,是指当发生冲突时,对得到的哈希值进行再次哈希,直至不再发生冲突为止。需要多个不同的哈希函数。
h1(key) = d1;
h2(key) = d2;
…
hi(key) = di;
…
解决方案:
(16、25、3、29、5、2、19、17、34)
Hash1(key)= key % 11;
Hash2(key)= 3key % 7;
Hash3(key)= key2 % 3;
…
哈希值: 5、3、3 2、7、5 1、2 10、8、6、1 4。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
– | 5 | 3 | 25 | 34 | 16 | 17 | 29 | 19 | – | 2 |
缺点:
- 每次冲突都要重新散列,计算时间增加。
(三)链地址法
链地址法也叫拉链法,即 将具有相同哈希值的结点链接到对应地址上的单链表上。这样,整个哈希表实际是一个链表数组。
解决方案:
(16、25、3、29、5、2、19、17、34)
Hash(key)= key % 11。
不妨取q = 5。
哈希值:
5、3、 3、7、 5、2、8、6、1
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
34 | 2 | 25 | 16 | 17 | 29 | 19 | ||||
3 | 5 |
优点:
- 处理冲突简单,且无堆聚现象,平均查找长度较短;
- 灵活,适合于造表前无法确定表长的情况;
- 能负担大于1的装载因子,且结点较大时,指针域所占空间可以忽略不计;
- 删除结点的操作易于实现。
缺点:
- 当结点空间较小时,指针域所占空间不可忽略。
(四)建立公共溢出区
解决方案:
(16、25、3、29、5、2、19、17、34)
Hash(key)= key % 11;
哈希值: 5、3、3 0、7、5 1、 2、8、6、 1。
哈希表 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
34 | 2 | 25 | 16 | 17 | 29 | 19 |
公共表 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
3 | 5 |
结束语
用例子推演了一遍,才发现自己之前对再哈希法的理解有误。
之前误以为是将哈希之后得到的值再哈希,遇到了哈希前哈希后值相同的问题,造成逻辑上的死循环。
推演时发现了该问题,实则是使用其他的哈希函数对原关键字进行再哈希。