我们已经习惯了数组根据下标快速访问的能力....
什么是哈希表
数组中可以通过下标(i)快速计算到对应值的地址,从而读取值,这极大的提升了我们对于数据随机访问的性能。
不过实际场景中我们更经常见的是通过某一些关键词找到对应的详细信息,比如通讯录中点击你的名字,就能看到你的电话、地址等一系列信息。这种场景如果直接使用数组或者任意其它数据结构都少不了查找的步骤。
我们知道,数组中随机访问的本质其实就两点:
- 分配一片连续空间
- 使用下标计算偏移地址
那假若我们按照数组的思想,可以根据关键字计算出偏移地址,同时也使用一片连续的空间,那是不是就可以根据关键字实现快速随机访问了?
答案是肯定的,这种存储结构就是哈希表。
哈希表的关键就是如何通过关键字计算地址偏移量,这里通常使用的技术就是哈希函数,它可以将关键字映射到一片连续存储空间中的某个地址。
即 f(keyword) = addr
哈希函数
广泛的讲哈希函数(散列函数)能够将任意长度的值转变成固定长度的值,该值称为哈希值,输出值通常为字母与数字组合。
常见的哈希算法
1. 直接地址法
直接使用关键字的值或者某个线性函数值作为哈希地址
适用于键的取值范围较小且已知的情况。在直接地址法中,哈希表的大小与键的取值范围相同,每个键值对应哈希表中的一个位置,即直接将键映射到哈希表的索引位置。
2.除留余数法:
它的计算方式是将键除以表的大小,然后取余数作为哈希值。公式为:hash(key) = key % table_size。
通常用于整数关键字
4.平方取中法
对关键字做平方,然后取中间几位作为哈希值。
5.折叠法
基本思想是将输入数据分成几段,然后将这些段相加或异或,得到哈希值。
例如关键字123456 三位一段,分成两段,然后将这些段相加,从而计算出哈希值。
123456 -> (123, 456) -> 123 + 456 = 579 -> 579 % hash_table.size -> addr
优势:即使输入数据的分布不均匀,折叠法也能产生较为均匀的哈希值分布。
6.随机数法
基本思想是利用随机数生成器为每个输入数据生成一个随机哈希值。
优势:由于随机数的分布通常是均匀的,随机数法能产生较为均匀的哈希值分布。
上面的示例都是对于数字类型的哈希计算方法,对于字符串关键字的哈希函数设计也类似,常见的方法包括以下几种:
- 简单求和法:将字符串中每个字符的ASCII码值相加,得到的和作为哈希值。这种方法简单直观,但可能导致哈希冲突较多。
- 加权法:为字符串中的每个字符赋予一个权重,然后将字符的ASCII码值乘以相应的权重后相加,得到的和作为哈希值。通过合理选择权重,可以减少哈希冲突。
- 取模法:将字符串中每个字符的ASCII码值相加,然后对一个较大的素数取模,得到的余数作为哈希值。这种方法可以减少哈希冲突。
- 多项式法:将字符串看作一个多项式,每个字符的ASCII码值作为多项式的系数,然后计算多项式的值作为哈希值。这种方法可以有效地将字符串映射到哈希表中。
- 循环移位法:将字符串中的字符进行循环移位操作,然后将移位后的字符ASCII码值相加,得到的和作为哈希值。这种方法可以增加字符串的变化性,减少哈希冲突。
- MD5、SHA-1等加密哈希函数:对于安全性要求较高的场景,可以使用MD5、SHA-1、SHA-256等加密哈希函数来处理字符串关键字。这些哈希函数生成的哈希值具有较高的随机性和唯一性。
上面的哈希算法,无论使用哪一种,多个关键字都有可能计算出相同的地址,因为哈希的本质其实就是对数据分组。所以现实情况下根据数据特点,选择合适的哈希算法是非常重要的。应该尽量避免多个关键字分配到相同的组,也就是说尽量避免哈希冲突。
哈希冲突
那既然哈希冲突不可避免,那如何解决这个问题,接下来我们讲几种多个关键字如何共用一个存储地址的方法。
1.开放地址法
它通过在哈希表中寻找其他空槽位来存放冲突的元素,当发生哈希冲突时,会根据一定的探测序列来寻找下一个可用的槽位。
几种常见的探测序列:
- 线性探测(Linear Probing) :线性探测是最简单的开放地址法方法,当发生冲突时,依次检查下一个槽位,直到找到空槽位为止。线性探测的探测序列为:h(key), h(key)+1, h(key)+2, ...,其中h(key)是哈希函数计算得到的初始位置。
- 二次探测(Quadratic Probing) :二次探测通过二次探测序列来寻找下一个空槽位,探测序列为:h(key), h(key)+1^2, h(key)-1^2, h(key)+2^2, h(key)-2^2, ...。
- 双重散列(Double Hashing) :双重散列使用两个哈希函数来计算探测序列,当发生冲突时,通过第二个哈希函数计算出一个步长,然后依次检查下一个槽位。探测序列为:h1(key), h1(key)+i*h2(key),其中h1和h2是两个不同的哈希函数。
优势:不需要额外的空间来存储链表或其他数据结构,节省了内存空间。
劣势:可能会导致聚集(Clustering)现象,即连续的槽位被占据,影响查找效率。
注意在设计开放地址法时,合理选择探测序列,以减少聚集现象的发生。
2.链地址法
它通过在哈希表中的每个槽位存储一个链表(或其他数据结构),将哈希冲突的元素存储在同一个槽位对应的链表中。当发生哈希冲突时,新元素会被插入到对应槽位的链表中,而不会覆盖原有元素。
优势:
- 简单高效:链地址法实现简单,易于理解和实现。
- 适用性广泛:适用于各种哈希表大小和负载因子。
- 动态性好:可以动态地调整哈希表大小,适应数据量的变化。
劣势:
- 额外空间开销:需要额外的空间来存储链表结构,可能会占用较多内存。
- 内存访问不连续:链表中的元素在内存中存储不连续,可能影响缓存性能。
- 性能受链表长度影响:当链表长度过长时,查找、插入、删除操作的时间复杂度会增加。
在实际应用中,链地址法通常适用于处理哈希冲突频繁、数据量较大且分布不均匀的情况。通过合理设计哈希函数和链表结构,可以提高链地址法的效率和性能。
3.公共溢出区法
它是开放地址法和链地址法的一种结合形式。在公共溢出区法中,哈希表中的每个槽位都可以存储一个元素,当发生哈希冲突时,将冲突的元素存储在一个公共的溢出区域中,而不是放入其他槽位或链表中。
优势:
- 简单高效:公共溢出区法实现简单,不需要额外的链表结构,减少了内存开销。
- 避免聚集现象:通过将冲突元素存储在公共溢出区域中,可以避免聚集现象的发生。
- 适用性广泛:适用于各种哈希表大小和负载因子。
劣势:
- 查找效率降低:当发生哈希冲突时,需要额外的查找操作来定位到公共溢出区域,可能会降低查找效率。
- 空间利用率降低:公共溢出区域可能会占用较多的空间,降低哈希表的空间利用率。
在实际应用中,公共溢出区法通常适用于需要简单高效处理哈希冲突的场景,同时又希望避免聚集现象的情况。通过合理设计哈希函数和溢出区域的管理策略,可以提高公共溢出区法的效率和性能。
4.再散列法
通过重新计算哈希值并找到新的槽位来存储冲突的元素。当发生哈希冲突时,再散列法会使用一个不同的哈希函数来计算新的哈希值,然后将元素存储在新的槽位上。
优势:
- 避免聚集现象:通过重新计算哈希值,可以避免聚集现象的发生,提高哈希表的性能。
- 灵活性:可以根据需要选择不同的哈希函数,适应不同的数据分布情况。
劣势:
- 哈希函数选择:选择合适的哈希函数对再散列法的效果至关重要,不同的哈希函数可能会导致不同的性能表现。
- 性能开销:重新计算哈希值可能会带来一定的性能开销,特别是在哈希表较大时。
在实际应用中,再散列法通常作为解决哈希冲突的一种补充方法,可以与其他解决冲突的方法结合使用,以提高哈希表的性能和效率。
效率衡量
哈希表效率衡量的几个因素
- 哈希函数选择,是否足够均匀,减少冲突
- 哈希冲突的处理方式,足够均匀,避免转成线性
- 加载因子(已存元素数量/哈希表总容量),衡量空间利用率,越大冲突概率越大,越小空间浪费越多,一般来说,加载因子的合理范围是0.7到0.8之间。