文章目录
- 一、什么是哈希表?
- 二、什么是哈希冲突?怎样解决?
- 三、哈希表的大小为什么是质数?
- 四、链表法
- 五、开放地址法
- 线性探测法
- 平方探测法
- 双哈希(Double Hashing)
- 六、哈希表满了怎么办?
- 七、完美哈希
- 八、一些使用哈希解决的算法题
在我以往的认知中,哈希就是进行唯一映射,指定一个关键字,就能映射唯一的值。平时经常使用linux下的
md5sum
命令用来比较两个文件是否相同。至于如何哈希原理,它们是怎样进行唯一映射的,一直没有去梳理,如今该填的洞还是得补上。
一、什么是哈希表?
我们知道,普通数组能够直接寻址,在O(1)时间内能访问到数组中的任意位置。它需要足够大的空间,为每一个关键字保留一个位置。当关键字取值的范围很大,储存空间又有限时,能不能同样用数组的形式实现O(1)查找?
哈希表(Hash Table)就是这样的数据结构,当实际储存的关键字集合,比所有可能的关键字的全集小许多时,使用一个长度有限的数组去储存这些关键字,从而节省大量的空闲空间。它也被称作为散列表,因为它的键是分散存储在数组中的。
如图,黄色区域为键的全集,范围为0~99,绿色区域为实际存储的键。直接寻址需要为所有的键开辟空间,而哈希映射仅需为需要储存的键分配空间,储存空间比直接寻址少得多。
哈希表的核心思想是将键转换为数组的索引位置,以便快速查找对应的值。它使用哈希函数将键映射到数组的索引位置,这个索引位置称为哈希值。哈希映射是唯一映射,一个键永远只能产生一个哈希值,这种映射关系,就是我们所说的哈希函数。常见的哈希函数包括MD5、SHA-1、SHA-256等。
二、什么是哈希冲突?怎样解决?
通过取某运算,保持了哈希表中查找一个元素只要O(1)时间的优势。但细心的朋友会发现,上图中当键为7,17时,两个键会映射到同一个哈希值。我们称这种情形为哈希冲突,由于哈希函数不够“随机”,不同的键通过哈希函数计算得到相同的哈希值。
有人会说,简单,将哈希表增大,某上一个更大的值函数不就行了吗?可是可以,但哈希的精髓在于尽可能的节省空间与查找时间。最普通的哈希表,就是一个直接寻址的数组。不过,还有其它更高效的方法解决哈希冲突,如链表法,开放地址法等,这个后面再讲。
当哈希表中存储的键越来越多时,冲突的概率会不断增大,总会有哈希表再也装不下键的时候。所以,合理的控制一个哈希表的大小也极为重要。通常,将哈希表的大小设置为一个质数,是需要储存的键的数量的两倍。
三、哈希表的大小为什么是质数?
当哈希表的大小选择为一个合数(非质数)时,键的哈希值可能与哈希表的大小存在公因子。这种情况下,多个键的哈希值可能会映射到相同的索引位置,导致哈希冲突的概率增加。
让我们通过一个例子来说明这个问题。假设我们有一个哈希表大小为10,键的哈希函数将键映射到0到9的索引位置。我们有两个键:15和25。它们的哈希值分别为5和5。由于它们的哈希值都映射到相同的索引位置5,发生了哈希冲突。
倘若我们选择一个质数作为哈希表的大小,比如13,再次计算这两个键的哈希值。键15的哈希值为2,键25的哈希值为12。由于13是质数,与13存在公因子的数字较少,因此键的哈希值在13以内相同的概率会少很多。
通过选择质数作为哈希表的大小,可以减少键的哈希值与哈希表大小之间存在公因子的情况,从而减少哈希冲突的概率。这样可以更均匀地分布键值对,提高哈希表的性能和效率。
四、链表法
链表法其实很简单,就是将映射到同一地址的元素都放在一个链表中。哈希表中每个槽都有一个指针,指向所有映射到同一位置的元素链表表头,如果当前槽位没有这样的元素,头指针为空。
采用双向链表结构是为了删除与插入操作都能控制时间在O(1)范围内。每一个槽位链表的长度就是它的负载因子
。通过上述结构不难看出,查找需要遍历链表,查找的时间取决于链表的长度。并且,当散列不均匀时,某一些链过长,有一些链很短,查找的效率也是不稳定的。
所以,一个好的哈希函数不应该将太多的输入映射到相同的输出。
五、开放地址法
除了链表法解决哈希冲突外,还可以通过开放地址的方式来解决哈希冲突。之所以叫开放地址,是因为它们都是通过将冲突的键,存放到哈希表空闲的其它地址中去,将未使用的地址开放出来,直到表空间耗尽。开方地址有多种方法,下面一一介绍。
线性探测法
探测(Probe),是指当地址冲突后,会继续去寻找表中可用的地址。线性探测,则是顺着当前位置向后查找。
如图,将关键字以线性探测的方式插入到哈希表中,哈希表的长度为13。
不难看出,线性探测随着填充因子的增加,冲突会聚集在一起,形成聚集效应。负载因子需要控制在70%左右,否则冲突次数飙升。数据分布本身不均匀也会导致冲突分布不均匀。线性探测法实现简单,但随着负载增加,性能下降明显。
平方探测法
如果遇到冲突,就往(原始位置 + i2)的位置寻找空位,i为查找次数。
如图:
平方探测法是解决哈希冲突的一种开放地址法。当发生哈希冲突时,它按一定的平方序列探测下一个位置。
优点:
- 降低了聚集效应。冲突分配更加均匀。
- 减少了聚簇内的查找时间。
- 可以提高负载因子。平均查找长度较短。
缺点:
- 需要计算平方序列值,实现较为复杂。
- 当Step超过表大小时计算会溢出。
- 随机性较差,数据分布不均会影响性能。
- 二次方探测发生冲突的概率较大。
- 删除操作会产生空洞,需要惰性删除避免影响。
总体而言,平方探测法通过牺牲部分实现复杂度和冲突计算时间来减轻聚集效应,在较高负载下性能较好。仍需要考虑非全域性问题。
什么是惰性删除?
惰性删除(Lazy deletion) 是一种常用的哈希表优化方法,主要应用于开放定址法中的删除操作。其基本思路是:
- 当删除键值对时,先将其标记为"已删除",而不直接从散列表中删除记录。
- 对后续插入操作,会扫描标记的已删除槽位并进行插入。
- 直到被后续插入覆盖,标记的删除状态才真正删除。
优点是可以避免删除后产生的空槽位,保证使用空间的连续性,减少冲突次数,提高效率。
缺点是需要扫描已删除槽位,增加查询的开销。
主要应用在平方探测法、线性探测法等开放定址中的删除操作,使得开放定址法的执行效率大为提升。是哈希表优化的重要手段之一。也可视作是一种延迟紧缩、空间换时间的实现。
双哈希(Double Hashing)
除了第一个哈希函数外,还有一个哈希函数用于解决冲突。
如果遇到冲突,新位置 = 原始位置 + i * hash2(key)
,i为查找次数。
hash2(key) = R - (key % R)
R要取比数组尺寸小的质数。
例如,哈希表长度为13,R = 7,hash2(key) = 7 - (key % 7)
也就是说,第二次哈希结果在1-7之间,不会等于0。
举例:
双哈希法的主要优点:
- 采用双重散列,冲突分布更加均匀。
- 相比线性探测,可以有效减少聚集问题。
- 指数增长级别更低,平均查找长度较短。
- 算法和实现都比较简单。
- 允许删除而不产生过多空槽。
双哈希法通过两次散列的思想,改进了性能和冲突分布,是一种效率较好的开方地址方法。
六、哈希表满了怎么办?
再哈希(rehash)
-
当哈希表的插槽被占用了70%后,查找效率会严重下降,需要对哈希表进行扩展。
-
新表长度为原来长度的2倍,且为质数。
-
将旧表中的关键字,通过新的哈希函数,重新填充到新表中。
由此,我们也能看到哈希表有以下缺点:
-
当表中的元素逐渐增多时,哈希冲突的概率会增加,查找效率会下降。
-
哈希表扩展时,需要将旧表的关键字重新哈希,迁移到新表中,性能成本大。
-
一个好的哈希表,依赖哈希函数的合理性。
七、完美哈希
完美哈希(Perfect Hashing),是一种解决哈希冲突的方法,它通过构建一个无冲突的哈希函数来实现。
完美哈希通过两级哈希的方式来解决哈希冲突。首先,使用一个一级哈希函数将键映射到一组桶(bucket)中。然后,在每个桶中,使用二级哈希函数将键映射到具体的索引位置。通过这种方式,每个键都被映射到唯一的索引位置,从而实现了完全散列。
完美哈希在某些特定场景下非常有用,特别是当键的集合是已知的且静态的情况下。它可以在预处理阶段构建完全散列函数,然后在查询时实现O(1)的查找时间复杂度,而无需处理冲突。然而,构建完美哈希函数的过程可能会比较复杂且耗时,特别是在键的集合较大的情况下。
需要注意的是,完美哈希并不适用于动态的键集合,因为当键的数量发生变化时,可能需要重新构建哈希函数。
八、一些使用哈希解决的算法题
-
两数之和:给定一个整数数组和一个目标值,找出数组中和为目标值的两个数的索引。可以使用哈希表来存储数组元素和其索引的映射关系,然后遍历数组,查找目标值与当前元素的差值是否存在于哈希表中。
两数之和 https://blog.csdn.net/qq_41099859/article/details/112464038 -
两个数组的交集:给定两个整数数组,求它们的交集。可以使用哈希集合来存储一个数组中的元素,然后遍历另一个数组,判断元素是否存在于哈希集合中。
两个数组的交集 https://blog.csdn.net/ShenHang_/article/details/107319514