目录
一、散列函数的基知识
散列函数(英语:Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据计算成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(又叫哈希值)(hash values,hash codes,hash sums,或hashes)的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。如果在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。
指纹
在计算机科学中,“指纹”通常指的是一种能够唯一标识某个数据对象的短小、固定长度的值(指纹的关键特点是它简洁且能够代表整个数据,但它本身比原始数据要小得多)。指纹的作用是通过对数据进行某种形式的处理,生成一个简洁的代表,便于快速识别、查找或比较数据。具体来说,指纹可以看作是数据的摘要或标识符。
- 在哈希函数中的指纹
对于哈希函数而言,指纹就是对输入数据(如字符串、文件、数组等)通过哈希算法生成的唯一标识符,这个标识符通常是一个固定长度的数字或字符串。这个指纹具有以下几个特点:
定长:哈希函数通常会把输入数据(无论其大小如何)转换为固定长度的输出(例如,MD5算法输出的是128位的指纹,SHA-256输出的是256位的指纹)。
高效生成:生成这个指纹的过程通常非常高效,可以在常数时间内计算出来。
唯一性:理想情况下,不同的数据应当生成不同的指纹,哈希函数应尽量避免哈希冲突(不同的数据产生相同的指纹)。
不可逆性:哈希函数是一种单向函数,通过哈希值不能反推原始数据。换句话说,从指纹(哈希值)无法恢复出原始数据。
- 举个例子
假设我们有一个字符串
"hello"
,我们应用某种哈希算法(例如 SHA-256)来计算其指纹。
- 输入数据:
"hello"
- 计算得到的哈希值(指纹):
2cf24dba5fb0a30e26e83b2ac5b9e29e1b169e20b0f21d1dcd7f6a1e6c3cfdb3
这里的哈希值
2cf24dba5fb0a30e26e83b2ac5b9e29e1b169e20b0f21d1dcd7f6a1e6c3cfdb3
就是"hello"
这个字符串的指纹。
- 指纹的实际应用
数据查找:哈希表中通过数据的指纹来快速查找数据。
数据验证:当传输文件或数据时,可以计算数据的哈希值,并与接收端计算的哈希值进行比较,如果相同,则说明数据没有被篡改。比如,下载文件时,文件提供商通常会给出文件的哈希值,用户可以下载文件后计算哈希值来确保文件未被篡改。
数字签名:在数字签名中,通过哈希函数将消息转换为指纹,并对指纹进行加密,确保消息的完整性和来源。
负载均衡和分布式系统:例如,在分布式哈希表(DHT)中,哈希函数用来将数据分配到不同的节点上,确保数据均匀分布。
- 总结
在哈希函数中,指纹就是对原始数据进行哈希计算后得到的一个简短、固定长度的数字值,它充当数据的唯一标识符。通过指纹,我们可以高效地查找、验证、比较数据。
如今,散列算法也被用来加密存在数据库中的密码(password)字符串,由于散列算法所计算出来的散列值(Hash Value)具有不可逆(无法逆向演算回原本的数值)的性质,因此可有效的保护密码。

二、散列函数的性质
所有散列函数都有如下一个基本特性:如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。但另一方面,散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为“散列碰撞(collision)”,这通常是两个不同长度的输入值,刻意计算出相同的输出值。输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。
典型的散列函数都有非常大的定义域,比如SHA-2最高接受(2^64-1)/8长度的字节字符串。同时散列函数一定有着有限的值域,比如固定长度的比特串。在某些情况下,散列函数可以设计成具有相同大小的定义域和值域间的单射。在密码学中,散列函数必须具有不可逆性。
- 散列碰撞/散列冲突
在计算机科学中,碰撞或冲突是指两个不同的元素具有相同的哈希值、校验和,数字指纹时发生的情况。当数据量足够多(例如将所有可能的人名和计算机文件名映射到一段字符上)时,碰撞是不可避免的。这仅仅是鸽巢原理的一个实例。
哈希碰撞是指两个不同的输入值经过哈希函数处理后得到相同的输出值。 这种情况在哈希表数据结构中尤为重要,因为它可能影响查找和存储的效率。
哈希碰撞的发生是不可避免的,主要原因如下:
- 输入空间通常大于输出空间:哈希函数将任意长度的输入映射到固定长度的输出,必然会有多个输入对应同一个输出.
- 生日悖论:根据概率论,即使在相对较小的样本空间中,也有较高的概率出现重复.
处理哈希碰撞的主要方法有两种:
- 开放寻址法:当发生碰撞时,继续探测散列表的下一个位置,直到找到空槽.
- 链接法:在每个散列表槽位使用链表存储发生碰撞的元素
一个优秀的哈希函数应该满足以下条件:
- 单向性:难以从哈希值反推原始输入。
- 弱无碰撞性:给定一个输入,难以找到另一个输入产生相同的哈希值。
- 强无碰撞性:难以找到任意两个不同输入产生相同的哈希值.
在实际应用中,哈希碰撞可能被恶意利用。例如,攻击者可能通过制造大量碰撞来增加服务器查询哈希表的时间,从而导致性能下降或服务瘫痪.为了减少哈希碰撞的影响,可以采取以下措施:
- 选择高质量的哈希函数。
- 使用足够大的哈希表以减少碰撞概率。
- 实现有效的碰撞解决策略,如链接法或开放寻址法。
- 在必要时动态调整哈希表大小。
总之,虽然哈希碰撞无法完全避免,但通过合理的设计和实现,可以最大限度地减少其对系统性能的影响。
碰撞的影响依程序而异。当散列函数和数字指纹用于标识相似数据时,程序被设计成尽可能增加相似但不同的数据发生碰撞的可能性;校验和则不同,要求尽可能使得相似的数据输出不同,而不考虑不同数据输出相同的情况。
三、散列函数的应用
由于散列函数的应用的多样性,它们经常是专为某一应用而设计的。例如,加密散列函数假设存在一个要找到具有相同散列值的原始输入的敌人。一个设计优秀的加密散列函数是一个“单向”操作:对于给定的散列值,没有实用的方法可以计算出一个原始输入,也就是说很难伪造。为加密散列为目的设计的函数,如SHA-2,被广泛的用作检验散列函数。这样软件下载的时候,就会对照验证代码之后才下载正确的文件部分。此代码不会因为环境因素的变化,如机器配置或者IP地址的改变而有变动。以保证源文件的安全性。
错误监测和修复函数主要用于辨别数据被随机的过程所扰乱的事例。当散列函数被用于校验和的时候,可以用相对较短(但不能短于某个安全参数, 通常不能短于160位)的散列值来验证任意长度的数据是否被更改过。
3.1 保护资料
散列值可用于唯一地识别机密信息。这需要散列函数是抗碰撞(collision-resistant)的,意味着很难找到产生相同散列值的资料。散列函数分类为密码散列函数和可证明的安全散列函数。第二类中的函数最安全,但对于大多数实际目的而言也太慢。透过生成非常大的散列值来部分地实现抗碰撞。例如,SHA-256是最广泛使用的密码散列函数之一,它生成256比特值的散列值。
3.2 确保传递真实的信息
消息或数据的接受者确认消息是否被篡改的性质叫数据的真实性,也称为完整性。发信人通过将原消息和散列值一起发送,可以保证真实性。
发信人会对消息(或数据)进行散列运算,生成一个散列值(即哈希值)。然后,发信人将这个散列值附加到消息或数据中,一同发送给接收者。接收者收到消息后,会重新计算该消息的散列值,并与接收到的散列值进行比对。如果两者一致,说明消息未被篡改,数据的完整性得到了保证;如果不一致,说明消息可能被修改过。
四、目前常见的散列算法/散列函数
五、 散列表/哈希表
5.1 基本概念
散列表(英语:Hash table)是根据键而直接访问在存储器存储位置的数据结构。也就是说,它通过计算出一个键值的函数,将所需查询的数据映射到表中一个位置来让人访问,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名𝑥到首字母𝐹(𝑥)的一个函数关系),在首字母为W的表中查找“王”姓的电话号码,显然比直接查找就要快得多。这里使用人名作为关键字,“取首字母”是这个例子中散列函数的函数法则𝐹(),存放首字母的表对应散列表。关键字和函数法则理论上可以任意确定。
可以将散列表理解为一串按顺序放的数组,数组的下标是从key经过计算得出,数组每个位置存放 value。这里有很多将key转换为下标的函数,比如取模,md5等。可以在哈希表可视化页面 直观操作,理解这里的数据结构。
- 若关键字为𝑘,则其值存放在𝑓(𝑘)的存储位置上。由此,不需比较便可直接获取所查记录。称这个对应关系𝑓为散列函数,按这个思想建立的表为散列表。
- 对不同的关键字可能得到同一散列地址,即𝑘1≠𝑘2,而𝑓(𝑘1)=𝑓(𝑘2),这种现象称为冲突(英语:Collision)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数𝑓(𝑘)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。
- 若对于关键字集合中的任一个关键字,经散列函数镜像到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。
5.2 处理冲突
- 开放寻址法-----线性探测法
线性探测法(Linear Probing)是一种开放地址法(Open Addressing)中解决哈希冲突的常用方法。当发生冲突时,线性探测法通过检查下一个位置(桶)来找到一个空桶。如果下一个桶已满,则继续检查下一个桶,直到找到一个空桶为止。
如果遇到冲突,就往下一个地址寻找空位。新位置=原始位置 + i(i是查找的次数) 。
例子:
对于数据关键字为:{15,2,38,28,4,12} ,哈希函数为简单的取模运算。
先来看15,15mod13=2,那么15会被放在哈希表中下标为2的位置上。
接下来是2,2mod13=2,由于15已经再下标为2的位置上,此时发生冲突一次,寻找下一个空位,发现下标为3的位置是空的,那么就将2放在下标为3的位置上。
接下来是38,38mod13=12,直接放在哈希表下标为12的位置上。
接下来是28,28mod13=2,15已经占了下标为2的位置,2已经占了下标为3的位置,28需要寻找空位,在寻找空位的过程中发生冲突两次,那么新的位置即为下标为4的位置。
接下来是4,4mod13=4,由于28已经占了下标为4的位置,4需要寻找空位,此时发生冲突一次,发现下标为5的位置为空,因此4会被放置在下标为5的位置上。
接下来是12,12mod13=12,由于38已经占了下标为12的位置,此时发生冲突一次,12寻找空位,发现下标为0的位置为空,因此12会被放在下标为0的位置上。
这就是整个线性探测法的流程。

线性探测法的优点:
-
简单易实现: 线性探测法的实现非常简单,因为只需要在发生冲突时,按顺序查找下一个桶即可,不需要复杂的计算或额外的哈希函数。
-
内存局部性好: 由于探测过程是顺序进行的,因此其在内存中的访问模式较为连续,能够充分利用缓存,提高内存的访问效率。
-
空间效率高: 线性探测法不需要额外的数据结构(如链表或树),所有元素都存储在同一个哈希表中,因此空间开销较小。
-
适用于载荷因子较低的情况: 在载荷因子较低时,线性探测法能有效减少冲突的发生,查找效率较高。
线性探测法的缺点:
-
聚集问题(Clustering): 线性探测法的一个主要缺点是“聚集问题”或“一次性聚集”现象。即在某些情况下,相邻的多个桶都被占用,这会导致探测过程中的连续冲突区域变大,影响查找效率。随着载荷因子的增加,聚集问题变得更加严重。
-
查找性能下降: 随着哈希表的填充程度增加,冲突的可能性增大,线性探测法的查找时间可能会变得较长。特别是当载荷因子接近 1 时,查找性能可能急剧下降。
-
插入和删除操作的复杂性: 由于线性探测法会在哈希表中“打散”桶的位置(通过顺序查找空桶),删除一个元素后,可能需要对后续的元素进行重新定位(即“重新探测”),以避免破坏现有的数据结构。这会使得删除操作变得较为复杂。
-
载荷因子过高时效率低下: 当载荷因子过高时(接近1),线性探测法的效率会显著下降。为了避免这一点,通常需要定期扩容哈希表,这会增加开销。
结论:
线性探测法是一种简单有效的哈希冲突解决方法,适用于载荷因子较低的情况,并且在内存访问上具有较好的局部性。然而,当哈希表的载荷因子较高时,线性探测法容易出现性能下降,特别是由于聚集问题导致查找效率降低。在这种情况下,可能需要考虑其他的探测方法(如二次探测或双重哈希)。
- 开放寻址法-----平方探测法
平方探测法(Quadratic Probing)是开放地址法(Open Addressing)中另一种解决哈希冲突的方式,它通过在发生冲突时,按照一定的平方步长(而非线性步长)查找空桶。具体地,假设当前冲突的位置为 h(k),则第 iii 次探测的位置为:h(k,i)=(h(k)+i^2) mod m
其中,i 从 0 开始递增(即:0, 1, 4, 9, 16...)。平方探测法的核心是通过步长的平方增加来避免线性探测法中的“聚集问题”。
例子:
数据关键字为:{15,2,28,19,10} ,哈希函数为简单的取模运算。
初始哈希表为空,15mod13=2后被放置在下标为2的位置上。
接下来是2,2mod13=2,但15已经把下标为2的位置占了,发生冲突一次,需要寻找新的空位, 新位置=2(原始位置)+1^2(已发生的冲突的次数的平方)=3,发现下标为3这个位置目前为空,那么将2放在下标为3的位置上。
接下来是28,28mod13=2,但15已经把下标为2的位置占了,发生冲突一次,需要寻找新的空位,新位置=2(原始位置)+1^2(已发生的冲突的次数的平方)=3,发现下标为3这个位置被2占了,发生冲突两次,重新寻找新的空位,新位置=2(原始位置)+2^2(已发生的冲突的次数的平方)=6,下标为6这个位置为空,那么将28放在下标为6这个位置上。
接下来是19,19mod13=6,下标为6这个位置已经被28占了,发生冲突一次,需要寻找新的空位,新位置=6(原始位置)+1^2(已发生的冲突的次数的平方)=7,发现下标为7这个位置为空,则将19放在7这个位置上。
接下来是10,10mod13=10,下标为10这个位置为空,直接将10放置在下标为10这个位置上即可。
这就是平方探测的流程。
平方探测法的优点:
-
避免聚集问题: 线性探测法容易导致连续的冲突形成长链(即聚集现象),使得查找效率显著下降。平方探测法通过步长的平方增加,避免了这种连续冲突的聚集现象,因此在查找时更为均匀分布,减少了“聚集”的影响。
-
较好的查找效率: 由于避免了聚集现象,平方探测法通常能提供更均匀的分布,相比于线性探测法,在较高载荷因子的情况下,它的查找效率表现得更好。
-
较好的空间局部性: 虽然比线性探测法稍差,但平方探测法仍然会在内存中连续查找下一个可能的空桶,因此仍然具有一定的空间局部性,能够比较高效地利用缓存。
-
适用于载荷因子较高的情况: 平方探测法在载荷因子较高时,相较于线性探测法,能够保持更好的查找性能。它在载荷因子接近 1 的情况下,表现得比线性探测法更为稳定。
平方探测法的缺点:
-
探测空间不完全: 平方探测法不能保证在所有情况下都能探测到哈希表的每个桶,尤其是当哈希表大小 m 为质数时,可能会导致无法探测到所有的桶。这意味着在某些情况下,平方探测法可能无法找到空桶,特别是在表的载荷因子较高时。
-
删除操作复杂: 删除元素时,平方探测法的难度较大。删除一个元素后,可能会破坏后续探测的路径,导致查找的元素不能找到。因此,删除操作需要特别处理,通常需要重新探测并调整哈希表中的其他元素。
-
计算成本较高: 相比线性探测法,平方探测法每次探测时需要进行平方计算,虽然这是常数时间操作,但在某些高效要求较严格的场景下,仍可能略微增加计算开销。
-
需要适当选择哈希表大小: 平方探测法要求哈希表的大小 m 需要合适,通常最好选择质数大小,这样能够避免探测空间不完全的问题。选错大小可能导致探测不充分,影响查找效率。
总结:
平方探测法在哈希冲突的处理上,相比于线性探测法具有显著的优势,特别是在避免冲突聚集和提高查找效率方面,表现更为优越。它适用于载荷因子较高的情况,并能够保持较好的性能。然而,它也有一定的缺点,如删除操作复杂、探测空间不完全等问题。因此,在使用平方探测法时,需要注意表的大小选择和删除操作的特殊处理。
- 开放寻址法-----双哈希法
双重哈希(Double Hashing)是一种开放地址法的哈希冲突处理方法,它通过使用两个哈希函数来解决冲突。双重哈希的基本思想是,当发生哈希冲突时,不仅仅使用一个哈希函数来确定下一个桶的位置,而是使用第二个哈希函数来计算一个步长,从而在哈希表中跳转到另一个桶。
双重哈希的工作原理:
假设我们有一个哈希表,大小为 m,两个哈希函数 h1 和 h2:
-
哈希函数 h1 用于计算初始的桶位置:h1(k)=(k mod m)其中,k 是键值,m 是哈希表的大小。
-
如果桶 h1(k) 已经被占用(发生冲突),则使用第二个哈希函数 h2 来计算步长。通常,h2 是一个与 h1 完全不同的哈希函数,它通常满足以下条件:h2(k)=1+(k mod (m−1)),其中,m−1 确保步长不会为零。
-
探测过程:当发生冲突时,使用双重哈希来计算下一个桶的位置。假设当前桶位置为 i,则下一个桶的位置为:i_next=(h1(k)+j⋅h2(k)) mod m,其中,j 是探测次数,从 0 开始递增。
举例 :
对于关键字:{15,2,18,28},第一个哈希函数为简单的取模运算。第二个哈希函数为7-(关键字 mod 7)。
初始哈希表为空,15mod13=2,则将15存放在下标为2的位置上。
接下来是2,2mod13=2,但15已经把下标为2的位置占了,发生冲突一次,启用第二个哈希函数,7-(2 mod 7)=5,由于2(原始位置)+1(冲突次数)×5(第二次哈希函数的值)=7这个位置为空,则可以将2放在下标为7这个位置上。
接下来是18,18mod13=5,下标为5的位置为空,直接将18放置在下标为5的位置上。
接下来是28,28mod13=2,下标为2的位置已经被15占了,发生第一次冲突,启用第二个哈希函数,7-(28 mod 7)=7,由于2(原始位置)+1(冲突次数)×7(第二次哈希函数的值)=9 这个位置为空,则可以将2放在下标为9这个位置上。
以上就是双重哈希解决哈希冲突的流程。
双重哈希的优点:
- 双重哈希减少了哈希冲突的聚集效应。在某些情况下,相比于线性探测和二次探测,双重哈希能提供更均匀的分布,避免了连续的冲突聚集现象。
- 它通过第二个哈希函数提供了更大的跳跃范围,从而增加了找到空桶的可能性。
双重哈希的缺点:
- 需要额外的哈希函数来计算步长,这会增加一些计算开销。
- 如果第二个哈希函数的设计不当,可能会导致步长分布不均匀,影响性能。
双重哈希是解决哈希冲突的一种有效方法,尤其是在载荷因子较高时,可以显著提高查找性能。
参考:https://www.bilibili.com/video/BV1MC4y1p7rP/?spm_id_from=333.337.search-card.all.click&vd_source=fb7bfda367c76676e2483b9b60485e57
5.3 查找效率
散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
- 散列函数是否均匀;
- 处理冲突的方法;
- 散列表的载荷因子(英语:load factor)。
5.4 载荷因子
散列表的载荷因子定义为:𝛼 = 填入表中的元素个数 / 散列表的长度,𝛼是散列表装满程度的标志因子。由于表长是定值,𝛼 与“填入表中的元素个数”成正比,所以,𝛼越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,𝛼越小,表明填入表中的元素越少,产生冲突的可能性就越小。
散列表的载荷因子(Load Factor)是散列表中元素的数量与散列表的桶(或槽)数量之比。它衡量了散列表的装载程度,决定了哈希表的效率和性能。载荷因子的计算公式为:
载荷因子=元素数量/桶的数量
- 高载荷因子:表明哈希表中有许多元素,这可能导致冲突增多,从而影响查找、插入等操作的性能。为了解决这个问题,通常需要扩展散列表。
- 低载荷因子:虽然可以减少冲突,但可能导致哈希表空间浪费,影响内存利用率。
通常,载荷因子设置为一个合适的值(如0.7或0.75),当载荷因子超过某个阈值时,哈希表会进行扩容,增加桶的数量,以保持操作的效率。
对于开放寻址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放寻址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。
举例:(面试题:哈希表满了该怎么办?哈希表扩容/再次哈希)
下图为9个元素、7个桶的哈希表,在该哈希表中使用单链表法处理冲突问题。哈希函数为简单的取余操作。载荷因子=9/7>1,载荷因子的值较高,表明目前的哈希表中有许多元素,影响查找、插入操作的性能。如何解决:哈希表扩容。

我们将哈希表中桶的数量设置为原来的一倍,即14,此时载荷因子=9/14≈0.643。载荷因子大致符合理想。扩容后的哈希表如下所示。

5.5 哈希表的平均查找长度(ASL)
散列表的平均查找长度是载荷因子𝛼的函数,只是不同处理冲突的方法有不同的函数。
哈希表的平均查找长度(Average Search Length, ASL)是指在进行查找操作时,所需的平均比较次数。它是衡量哈希表查找性能的一个重要指标,尤其在发生冲突时。平均查找长度的计算取决于哈希冲突的处理方式。
- 对于不同的冲突处理方法,ASL的计算方法如下:
-
链式地址法(Separate Chaining):
- 链式地址法将所有发生冲突的元素存储在一个链表中。
- 假设哈希表中有 n 个元素,桶的数量为 m(桶的数量通常大于等于元素数量)。
- 每个桶中平均包含 n/m 个元素。
- 在查找时,最坏情况是链表中包含所有元素,因此查找一个元素所需的比较次数与链表长度成正比。
- 平均查找长度(ASL)大约为 1+(n/m)/2,其中 1 是查找的第一步(直接定位桶)。
-
开放地址法(Open Addressing):
- 在开放地址法中,所有元素都存储在哈希表本身中,发生冲突时会寻找下一个空桶。
- 计算开放地址法中的ASL较为复杂,通常需要根据使用的具体探测方法(如线性探测、二次探测、双重哈希等)来分析。
- 假设表的载荷因子为 α=n/m,其中 n 是元素数量,m 是桶的数量。
- 线性探测(Linear Probing):在最坏情况下,查找长度通常为 1/(1−α),随着载荷因子增大,查找长度增大。
- 二次探测(Quadratic Probing) 和 双重哈希(Double Hashing):这两种方法的平均查找长度也与载荷因子有关,通常比线性探测更高效,避免了线性探测中可能出现的聚集现象。
总结:
- 链式地址法的平均查找长度通常较低,特别是在桶数较多时,每个桶中的元素数较少,查找时平均比较次数较少。
- 开放地址法的平均查找长度随着载荷因子的增大而增加,当载荷因子过高时,查找性能会显著下降。
5.5 哈希表应用举例:Linux内核的bcache
Linux操作系统在物理文件系统与块设备驱动程序之间引入了“缓冲区缓存”(Buffer Cache,简称bcache)。当读写磁盘文件的数据,实际上都是对bcache操作,这大大提高了读写数据的速度。如果要读写的磁盘数据不在bcache中,即缓存不命中(miss),则把相应数据从磁盘加载到bcache中。一个缓存数据大小是与文件系统上一个逻辑块的大小相对应的(例如1KiB字节),在bcache中每个缓存数据块用struct buffer_head
记载其元信息:
整个bcache以struct buffer_head
为基本数据单元,组织为一个封闭寻址(close addressing,即“单独链表法”解决冲突)的散列表struct buffer_head * hash_table[NR_HASH];
散列函数的输入关键字是b_blocknr(逻辑块号)与b_dev(设备号)。计算hash值的散列函数表达式为:
(b_dev ^ b_blocknr) % NR_HASH
其中NR_HASH是散列表的条目总数。发生“ 冲突”的struct buffer_head
,以b_prev与b_next指针组成一个双向(不循环)链表。bcache中所有的struct buffer_head
,包括使用中不空闲与未使用空闲的struct buffer_head
,以b_prev_free和b_next_free指针组成一个双向循环链表free_list,其中未使用空闲的struct buffer_head
放在该链表的前部。