哈希表查找
本节提要
- 哈希查找的概念。
- 哈希函数的构造方法。
- 处理冲突的方法。
- 哈希查找的过程。
哈希存储、查找的概念
哈希存储是一种由关键字自身决定其存储位置的存储结构。通过哈希函数 ( H(k) ) 计算关键字 ( k ) 的存储地址。理想情况下,哈希查找的时间复杂度为 ( O(1) )。
H(k) 称为哈希函数。这种存储结构称为哈希表。
哈希函数的特点
- Hash( 输入 ) = 输出
- 输入:可为不固定长度的值。
- 输出:固定长度的值。
- 单向不可逆:无法从输出值反推出输入值。
- 输出值能反映输入值的大部分特征。
哈希函数的应用
- 验证用户输入密码是否正确。
- 判断文件、消息等数据的完整性。
- 分布式应用,如BT下载。
- 比特币、区块链。
哈希函数示例
已知地址区间为:0~6,关键字序列为:{20, 30, 70, 12, 18}。哈希函数为 ( h(k) = k % 7 ),则构造的哈希表如下:
地址 0 1 2 3 4 5 6
关键字 18 20 30 70 12
哈希函数问题
如果继续添加新的关键字14,会出现冲突。冲突是指关键字不同,但哈希函数值相同。构造哈希表时需要设计良好的哈希函数与处理冲突的方法。
冲突:关键字不同,哈希函数值相同。冲突的关键字称为同义词。
冲突无法避免,因此构造哈希表时需要同时设计良好的哈希函数与处理冲突的方法
哈希表设计方法
哈希表设计主要需要解决哈希冲突。实际中哈希冲突是难以避免的,主要与3个因素有关:
-
与装填因子有关。
装填因子α=存储的记录个数/哈希表的大小=n/m α越小,冲突的可能性就越小; α越大(最大可取1),冲突的可能性就越大。通常使最终的控制在0.6~0.9的范围内。 -
与所采用的哈希函数有关。
好的哈希函数会减少冲突的发生;不好的哈希函数会增加冲突的发生。 -
与解决冲突方法有关。
好的哈希冲突解决方法会减少冲突的发生。 -
哈希表设计的重点:
-
尽可能设计好的哈希函数,尽可能使哈希地址出现在表中任意位置的概率均等,从而减少冲突。
-
设计解决冲突的方法。
-
哈希函数构造方法:
- 直接定址法
- 除留余数法
- 数字分析法
- 平方取中法
- 折叠移位法
-
解决冲突的方法:
- 开放定址法
- 线性探查法
- 平方探查法
- 双散列函数探查法
- 链地址法
直接定址法
取关键字本身或关键字的某个线性函数值作为哈希地址。例如,( h(key) = key ) 或 ( h(key) = a*key + b )。
取关键字本身或关键字的某个线性函数值作为哈希地址。
h(key)=key 或 h(key)=a*key+b (a 和 b 均为常数)
示例:利用哈希表保存从2000年到2019年每年出生的人数。关键字k为年份,共有20年,地址区间为0~19。
用直接定址法设置哈希函数为:
h(k)=k-2000
哈希表如下:
适用场景:关键字和地址一一对应,不会产生冲突。
只适用于关键字基本连续的情况。
如关键字序列为{1, 45, 99},哈希函数为h(k)=k,则地址区间为(1~99),但只存放3个关键字,造成空间严重浪费。
除留余数法
以关键字被某个整数 ( m ) 除后所得余数作为哈希地址。( h(k) = k % m ),其中 ( m ) 应小于或等于表长。以关键字被某个整数m除后所得余数作为哈希地址。
h(k) = k % m (m<=表长)
余数区间为 (0~m-1),与哈希表地址区间一致。
m为质数(素数)时,冲突的可能性相对较少。
除留余数法为常用方法。
示例:
已知连续的地址区间为0~6,
关键字k的序列为{20,30,70,12,18},
哈希函数为h(k)=k%7,则构造的哈希表为:
其中,k%7就是用除留余数法设计的哈希函数,7是一个等于表长的质数。
数字分析法
如果关键字的数位相同,且取值事先知道,则可对关键字进行分析,取其中分布均匀的若干位或它们的组合作为哈希地址。
如果可能出现的关键字的数位相同,且取值事先知道,则可对关键字进行分析,取其中“分布均匀”的若干位或它们的组合作为哈希地址。
示例:通过分析如下一组关键字可知:
关键字的第1,2,3位和第6位取值较集中,不宜作为哈希地址。
第4,5,7和8位取值较分散,可根据实际需要取其中的若干位作为哈希地址。
处理冲突的方法
-
开放定址法:
- 使用某种探查技术,在哈希表中形成一个探查序列,当冲突发生时,沿此序列查找空闲单元地址。
h0 = h(k)
hi = ( h0 + di ) % m ( 1 ≤ i ≤ m-1 )
hi 为第 i 次冲突时探查的下一地址 - 线性探查法:di = i 即依次为:1, 2, 3, …, m-1
- 平方探查法: di = ±i2即依次为:+1, -1, +4, -4, …
- 双哈希函数探查法:di = i * h2(k)
- 示例:已知哈希表长13,地址区间0~12,关键字序列{15,59,22,34,20,35},哈希函数h(k)=k%13,采用线性探查法处理冲突,构造哈希表。
h(15)=15%13=2,存至A[2]; h(59)=59%13=7,存至A[7]。
h(22)=22%13=9,存至A[9]; h(34)=34%13=8,存至A[8]。
h(20)=20%13=7,存至A[7]。 冲突;
h0=7; h1=(h0+1)%13=8,仍冲突;
h2=(h0+2)%13=9,仍冲突;h3=(h0+3)%13=10,存至A[10];
h(35)=35%13=9,冲突;(同义词引起的冲突)
h0=9; h1=(h0+1)%13=10,仍冲突(非同义词);
h2=(h0+2)%13=11,存至A[11];
h(20)=20%13=7
h(35)=35%13=9
上例中,35与20的哈希地址不同,但争夺同一个后继地址。这种现象称为聚集。
聚集将造成不是同义词的元素也处在同一个探查序列中,从而增加了查找时间。
如发现聚集较严重,可改用平方探查法。
- 使用某种探查技术,在哈希表中形成一个探查序列,当冲突发生时,沿此序列查找空闲单元地址。
-
链地址法:
- 哈希地址相同的元素放置于一个单链表,链表的头指针放置于对应的哈希地址处。
哈希表的查找
- 开放定址法:
- 计算待查找元素 ( k ) 的哈希地址 ( H(k) )。
- 如果该位置没有元素,则查找失败。
- 该位置有元素,与 ( k ) 相比较:
- 如果相等,查找成功。
- 如果不相等,则按建表时设定的冲突处理方法找下一个地址。
- 回到步骤 2 和 3,继续查找。如果探查序列上的全部元素都比较完后,仍未找到,则查找失败。
- 链地址法:
- 计算待查找元素 ( k ) 的哈希地址 ( H(k) )。
- 如果该位置头指针为空,则查找失败。
- 头指针不空,对应链表中的元素依次与 ( k ) 相比较:
- 如果相等,查找成功。
- 如果对应链表的全部元素都比较完后,仍未找到,则查找失败。
练习
已知哈希表地址区间为 0~10,给定关键字序列 {20, 30, 70, 15, 8, 12, 18, 63, 19}。哈希函数为 ( h(k) = k % 11 ),分别采用线性探查法和链地址法处理冲突,构造哈希表。
线性探查法:
哈希表:
链地址法:
总结
- 哈希函数的特点以及构造方法。
- 冲突产生的原因以及处理冲突的方法。
- 哈希查找的方法。
练手题目
1.已知哈希函数为H(k)=k%11,线性探测法解决冲突,根据哈希表的状态图,完成下面问题,并写出过程及结果。
0 1 2 3 4 5 6 7 8 9 10
23 13 45 15 16 26 18 20 31
1)在上述哈希表的状态图中,空位置上标记空标志NULLFLAG,查找45;
0 1 2 3 4 5 6 7 8 9 10
NULLFLAG 23 13 45 15 16 26 18 NULLFLAG 20 31
H(45)=45%11=1,地址被占用,线性探查下一地址;
H1 =(H(45)+1)%11=2,占用;
H2 =(H(45)+2)%11=3,查找成功。
2)查找5,若5不存在,则将其插入;
H(5)=5%11=5,地址被占用,线性探查下一地址;
H1 =(H(5)+1)%11=6,占用;
H2 =(H(5)+2)%11=7,占用;
H3 =(H(5)+3)%11=8,空,查找失败。
0 1 2 3 4 5 6 7 8 9 10
NULLFLAG 23 13 45 15 16 26 18 5 20 31
3)查找26,若26存在,则将其删除,并在相应位置设置删除标志DELETEFLAG;
H(26)=26%11=4,地址被占用,线性探查下一地址;
H1 =(H(26)+1)%11=5,占用;
H2 =(H(26)+2)%11=6,查找成功。
0 1 2 3 4 5 6 7 8 9 10
NULLFLAG 23 13 45 15 16 DELETEFLAG 18 5 20 31
4)查找5,若5存在,则将其删除;
H(5)=5%11=5,地址被占用,线性探查下一地址;
H1 =(H(5)+1)%11=6,删除标志,继续探查;
H2 =(H(5)+2)%11=7,占用;
H3 =(H(5)+3)%11=8,查找成功
0 1 2 3 4 5 6 7 8 9 10
NULLFLAG 23 13 45 15 16 DELETEFLAG 18 DELETEFLAG 20 31
5)查找10,若10不存在,则将其插入;
H(10)=10%11=10,地址被占用,线性探查下一地址;
H1 =(H(10)+1)%11=0,空,查找失败。
0 1 2 3 4 5 6 7 8 9 10
10 23 13 45 15 16 DELETEFLAG 18 DELETEFLAG 20 31
6)查找27,若27不存在,则将其插入;
H(27)=27%11=5,地址被占用,线性探查下一地址;
H1 =(H(27)+1)%11=6,删除标志,继续探查;
H2 =(H(27)+2)%11=7,占用;
H3 =(H(27)+3)%11=8,删除标志,继续探查;
H4 =(H(27)+4)%11=9,占用;
H5 =(H(27)+5)%11=10,占用;
H6 =(H(27)+6)%11=0,占用;
H7 =(H(27)+7)%11=1,占用;
H8 =(H(27)+8)%11=2,占用;
H9 =(H(27)+9)%11=3,占用;
H10 =(H(27)+4)%11=4,占用;查找次数11与哈希表容量11相同,查找失败,插入到第一个删除标志处。
0 1 2 3 4 5 6 7 8 9 10
10 23 13 45 15 16 27 18 DELETEFLAG 20 31
总结查找、插入、删除等操作成功与失败的条件,并在hash项目中利用my_hash1验证上述过程。
3.在hash项目中利用my_hash2验证上述过程,回答以下问题:
1)my_hash1和my_hash2这两个哈希表,哪个可以直接判断同义词存在?哪个不可以?
答:my_hash1不可以判断出同义词。因为开放定址法处理冲突,使到同义词可能分散在数组的不同位置,无法判断。
my_hash2 可以判断出同义词。因为同义词在其相应位置使用链表链接起来了。
2)对比链地址法处理冲突时,哈希表的查找、插入、删除等操作与第1题开放定址法处理冲突的区别。
链地址法处理冲突时,查找、插入、删除等操作只在与哈希值对应的链表中进行操作。