目录
定义
哈希表是一个用于储存”键值对“的的基本数据结构。在C++当中哈希表使用的是哈希函数计算出数组的索引,然后通过索引查找对应索引的值,计算索引的值的过程就被称为”哈希“。
哈希表的储存方式
构造哈希函数的最常用的方法就是使用除留余数法:
f(key)%m (m不大于表的长度)
但是通常情况下,在数据比较特殊或则数据量比较多的情况下,就难免会出现不同的输入对应着相同的输出的问题,这就是哈希冲突。
但是,如果输入的是相同的参数,那么输出就一定是相同的,没有任何随机成分,这就是”哈希表“查找的原理了。
哈希冲突(哈希碰撞)
但由于通过哈希函数产生的哈希值是有限的(数组大小有限),而数据可能比多,导致经过哈希函数处理后有不同的学号对应相同的索引值。这时候就产生了哈希冲突 (两个值都需要同一个地址索引位置)。
也就是说,如果某个输入的参数经过哈希函数的计算和表中存在的key值相同,就出现了哈希冲突。
哈希冲突的解决方案
-
线性探测再散列:
fi(key) = (f(key)+di)%m (di = 1,2,3,……,m-1)
简单解释一下就是如果出现了哈希冲突,就会遍历di列表当中的值,一直找到一个空缺的位置,将值存入。
就比如有一个键值集合为我们有键值集合(12,67,56,16,25,37,22,29,15,47,48,34)表长为12。我们用散列函数f(key)= key%12。当存入前5个{12,67,56,16,25}时,都是没有冲突的散列地址,直接存入:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 16 | 67 | 56 |
但是之后加入48之后就f(key)= key%12 = 0 就与0 所在的位置冲突了,所以就通过线性再探测,di = 1时为1,这时还是冲突,所以只能继续探测di+1,找到值为2,没有冲突,直接存入这个位置。
缺点:
-
但是,如果空位置在这个冲突key的前面,查找的效率就会大打折扣,可能就需要遍历一圈才能取余得到这个空位置。
-
并且,这种线性探测的方法很容易会产生聚集现象。
举个例子就是
当使用线性探测法来解决哈希冲突时,可能会出现聚集现象。假设我们有一个大小为10的哈希表,并使用线性探测法来处理哈希冲突。
现在,我们插入一系列元素:
- 将关键字10哈希到位置0,插入成功。
- 将关键字20哈希到位置0,发生冲突,继续探测,在位置1插入成功。
- 将关键字30哈希到位置0,发生冲突,继续探测,在位置2插入成功。
- 将关键字40哈希到位置0,发生冲突,继续探测,在位置3插入成功。
- 将关键字50哈希到位置0,发生冲突,继续探测,在位置4插入成功。
- 将关键字60哈希到位置0,发生冲突,继续探测,在位置5插入成功。
在这个例子中,我们会发现,由于使用了线性探测法,发生冲突时我们只是简单地往后探测,导致连续的位置都被占用了,形成了一个聚集现象。当往哈希表中插入新的元素时,由于聚集现象的存在,新元素更容易发生新的哈希冲突,需要经过多次探测才能找到合适的位置插入,降低了哈希表的效率。
-
平方探测法
平方探测法的di增量为 di = 1,-1,4,-4,9,-9……q^2, -q^2 (q<=m/2)。
这种探测就有效的避免了聚集现象,但是可能不能完全探测到哈希表上的所有存储单元。
-
随机探测法
随机探测法是解决哈希冲突的一种方法,它也属于开放寻址法的一种。与线性探测法和平方探测法不同,随机探测法在解决冲突时并不遵循固定的探测序列,而是通过随机选择下一个位置来插入元素。
具体来说,当发生哈希冲突时,随机探测法会在哈希表中随机选择一个位置进行探测,如果该位置已经被占用,则继续随机选择下一个位置,直到找到一个空闲的位置插入元素。这样可以避免聚集现象,因为每次探测的位置都是随机选择的,不会形成连续的聚集。
优点:能够有效地减少哈希冲突,并且不容易受到特定数据分布的影响,因为每次探测都是随机的。
缺点:实现起来可能比较复杂,需要额外的随机数生成器来确定下一个探测位置,而且在实际应用中可能会引入一定的性能开销。
-
再哈希法
再哈希法(也称为双重散列)是一种解决哈希冲突的方法,它不同于开放寻址法中的线性探测、平方探测和随机探测,而是利用多个哈希函数来解决冲突。
具体来说,当发生哈希冲突时,再哈希法会使用第二个哈希函数来计算一个增量,然后将这个增量加到当前位置,得到下一个探测位置。如果在这个位置上还有冲突,就再次应用第二个哈希函数来计算增量,直到找到一个空闲位置插入元素。
再哈希法的优势在于可以通过多个哈希函数的组合减少哈希冲突的概率,因为即使两个关键字在第一个哈希函数下产生了相同的哈希值,但在第二个哈希函数下可能会有所不同,从而找到不同的探测位置。这样可以减少聚集现象,提高哈希表的性能。
-
链地址法
前面提到的方法都是,如果发生了哈希冲突,就在表中找其他空位置,放入元素。链地址法就不会再占用其他的空位置,而是在当前的位置维护一个链表,如果发生哈希冲突时,新的元素会被插入到对应槽位的链表中,而不是直接放置到该位置。这样,即使多个关键字哈希到了同一个位置,它们仍然可以通过链表进行存储和查找,避免了数据的覆盖。
虽然这种方法的原理很简单,易于扩展,并且适用于不同负载因子的哈希表。同时,即使哈希冲突较多时,每个槽中的链表长度也能保持在较小的范围内,从而保持较好的性能。此外,链地址法还能够很好地处理大量数据插入和删除操作。
但是链地址法也存在一些缺点:
其中最主要的是对内存的额外消耗。由于需要额外的存储空间来存储链表节点,当哈希表中元素较多时,可能会导致内存占用过高。此外,在查找元素时需要沿着链表进行遍历,可能会增加查找的时间复杂度。
-
示例结构如下
下标 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
关键字 | 链表头1 | 链表头2 | 链表头3 | 链表头4 | 链表头5 |
48 | 37 | 15 | 16 | ||
12 | 25 |
- 公共溢出区
公共溢出区的方法和链地址法有相同之处,但是公共溢出区不是用链表维护了,而是额外在其他空间建立一块区域存储这些关键字。
公共溢出区的实现方式是,在哈希表中预留一个独立的存储区域,被称为“溢出区”或“附加区”。当发生哈希冲突时,冲突的元素会被放置到这个公共溢出区中,而不是直接放置到原始的哈希表槽中。这样,即使哈希冲突发生,也不会影响原始哈希表结构,而是将冲突的元素统一放置到公共的溢出区中。
公共溢出区的优点在于它避免了链地址法中可能产生的链表结构,从而减少了额外的指针开销。此外,它也可以更好地利用内存空间,因为所有的溢出元素都集中存储在一个区域,而不会分散在各个链表中。
然而,公共溢出区也存在一些缺点。最主要的是,当溢出区的大小不够时,可能会导致溢出区本身的哈希冲突,需要额外的处理。另外,对于查找操作,需要先在原始哈希表中查找,如果未找到则再去溢出区中查找,这可能会增加查找的时间复杂度。