一、散列表的基本概念
散列表是根据关键码值 ( K e y (Key (Key v a l u e ) value) value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
- 散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数。记为 H a s h ( k e y ) = A d d r Hash(key) = Addr Hash(key)=Addr(可以是数组下标、索引或内存地址)
- 散列表:根据关键字而直接进行访问的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系。
其他术语:
- 冲突:散列函数可能会把两个或两个以上的不同关键字映射到统一地址。
- 同义词:发生碰撞的不同关键词。
二、散列函数的构造方法
2.1 构造散列函数的注意点
- 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。
- 散列函数计算出来的地址应该能够
等概率、均匀
地分布在整个地址空间中,从而减少冲突的发生。 - 散列函数应该尽可能简单,能够在较短的时间内计算出任意一个关键字对应的散列地址。
2.2 常用的散列函数
2.2.1 直接定址法
直接取关键字的某个线性函数值为散列地址,散列函数为:
H
(
k
e
y
)
=
k
e
y
H(key)=key
H(key)=key或
H
(
k
e
y
)
=
a
×
k
e
y
+
b
H(key)=a×key+b
H(key)=a×key+b
特点:
- 简单,不会产生冲突
- 适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会浪费存储空间。
2.2.2 除留余数法
这是一种最简单、最常用的方法,假定散列表表长为
m
m
m,取一个不大于
m
m
m但最接近或等于
m
m
m的质数
p
p
p,利用以下公式把关键字转换为散列地址。散列函数为:
H
(
k
e
y
)
=
k
e
y
%
p
H(key)=key \% p
H(key)=key%p
除留余数法的关键是选好
p
p
p,使得每个关键字通过该函数转换后等概率地映射到散列空间上的任意一个的地址,从而尽可能减少冲突的可能性。
2.2.3 数学分析法
如果关键字由多位字符或者数字组成,就可以考虑抽取其中的 2 2 2 位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。
特点:
- 适用于已知关键字集合,若更换了关键字,则需要重新构造新的散列函数。
举例:
- 手机号前三位是接入号,中间四位是 H L R HLR HLR 识别号,只有后四位才是真正的用户号,也就是说,如果手机号作为关键字,那么极有可能前 7 7 7位是相同的此时我们可以选择后四位作为散列地址。
2.2.4 平方取中法
即取关键字平方的中间位数作为散列地址。
特点:
- 适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
- 适合于不知道关键字的分布,而位数又不是很大的情况。
举例:
假设关键字是
4321
4321
4321,那么它的平方就是
18671041
18671041
18671041,抽取中间的
3
3
3 位就可以是
671
671
671,也可以是
710
710
710,用做散列地址。
2.2.5 随机数法
选择一个随机数,取关键字的随机值作为它的散列地址,散列函数为
H
(
k
e
y
)
=
r
a
n
d
o
m
(
k
e
y
)
H(key)=random(key)
H(key)=random(key)
特点:
- 适用于关键字的长度不等的情况
2.2.6 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
举例:
-
假设关键字是 9876543210 9876543210 9876543210,散列表表长为三位,则我们可以将它分为四组 987 ∣ 654 ∣ 321 ∣ 0 987|654|321|0 987∣654∣321∣0,然后将它们叠加求和 987 + 654 + 321 + 0 = 1962 987+654+321+0=1962 987+654+321+0=1962,再取后 3 3 3 位得到散列地址即为 962 962 962。
-
有时可能这还不能够保证分布均匀,那么也可以尝试从一端到另一端来回折叠后对齐相加,比如讲 987 987 987 和 321 321 321 反转,再与 654 654 654和 0 0 0相加,变成 789 + 654 + 123 + 0 = 1566 789+654+123+0=1566 789+654+123+0=1566,此时散列地址为 566 566 566。
特点:
- 折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况
三、处理冲突的方法
3.1 开放定址法
将可存放新表项的空闲地址既向它的同义词开放,又向它的非同义词开放。数学递推公式为:
H
i
=
(
H
(
k
e
y
)
+
d
i
)
%
m
H_i=(H(key)+d_i)\%m
Hi=(H(key)+di)%m
其中,
m
m
m散列表表长,
d
i
d_i
di表示增量序列。
3.3.1 线性探测法
当 d i = 0 , 1 , 2 , . . . , m − 1 d_i=0,1,2,...,m-1 di=0,1,2,...,m−1时,成为线性探测法。
冲突发生时,顺序查看表中下一个单元(探测到表尾地址 m − 1 m-1 m−1时,下一个探测地址是表首地址 0 0 0),直到找出一个空闲单元(当表未填满时一定能够找到一个空闲单元)或查遍全表。
特点:
- 空间利用率高,但容易发生聚集现象
3.3.2 平方探测法
当 d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , k 2 , − k 2 d_i=0^2,1^2,-1^2,2^2,-2^2,...,k^2,-k^2 di=02,12,−12,22,−22,...,k2,−k2时,成为平方探测法。其中 k < = m / 2 k<=m/2 k<=m/2,散列表长度 m m m必须是一个可以表示成 4 k + 3 4k+3 4k+3的素数,又称为二次探测法。
特点:
- 可以避免出现"堆积"现象
- 不能探测到散列表上的所有单元,但至少能探测到一半单元。
3.3.3 伪随机序列法
增量 d i di di为伪随机数时,称为伪随机序列法。
3.3.4 双散列法
当
d
i
=
H
a
s
h
2
(
k
e
y
)
d_i=Hash_2(key)
di=Hash2(key)时,称为双散列法。需要使用两个散列函数,当通过第一个散列函数
H
(
k
e
y
)
H(key)
H(key)得到的地址发生冲突时,则利用第二个散列函数
H
a
s
h
2
(
k
e
y
)
Hash_2(key)
Hash2(key)计算该关键字的地址增量。其具体函数形式如下:
H
i
=
(
H
(
k
e
y
)
+
i
×
H
a
s
h
2
(
k
e
y
)
)
%
m
H_i=(H(key)+i×Hash_2(key))\%m
Hi=(H(key)+i×Hash2(key))%m
初始探测位置
H
0
=
H
(
k
e
y
)
%
m
H_0=H(key)\%m
H0=H(key)%m,
i
i
i是冲突次数,初始为
0
0
0。在双散列法中,最多经过
m
−
1
m-1
m−1次探测就会遍历表中所有位置,回到
H
0
H_0
H0位置。
举例:
在长度为
11
11
11 的哈希表中已填写好
17
17
17、
60
60
60和
29
29
29 这
3
3
3 个数据,其中采用的哈希函数为:
H
(
k
e
y
)
=
k
e
y
%
11
H(key)=key \%11
H(key)=key%11,
现有第
4
4
4 个数据
38
38
38 ,当通过哈希函数求得的哈希地址为
5
5
5,与
60
60
60 冲突,则分别采用以上前
3
3
3种方式求得插入位置如下图所示:
3.2 拉链法
对于不同的关键字可能会通过散列函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储到一个线性链表中,这个线性链表由其散列地址唯一标识。
举例:
假设关键字序列为
{
19
,
14
,
23
,
01
,
68
,
20
,
84
,
27
,
55
,
11
,
10
,
79
}
\{19,14,23,01,68,20,84,27,55,11,10,79\}
{19,14,23,01,68,20,84,27,55,11,10,79},散列函数为
H
(
k
e
y
)
=
k
e
y
%
13
H(key)=key\%13
H(key)=key%13,用拉链法处理,建立的表如下图所示:
四、散列查找及性能分析
散列表的查找过程与构造散列表的过程基本一致。对于一个给定的关键字 k e y key key,根据散列函数可以计算出其散列地址,执行步骤如下:
-
初始化: A d d r = H a s h ( k e y ) Addr=Hash(key) Addr=Hash(key)
- 检测查找表中地址为 A d d r Addr Addr的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与 k e y key key的值,若相等,则返回查找成功标志,否则执行下一步。
- 用给定的处理冲突方法计算“下一个散列地址”,并把 A d d r Addr Addr置为此地址,转入上一步。
举例:
下图为按照散列函数
H
(
k
e
y
)
=
k
e
y
%
13
H(key)=key\%13
H(key)=key%13和线性探测处理冲突构造所得的散列表:
(1)查找
84
84
84:首先求得散列地址
H
(
84
)
=
6
H(84)=6
H(84)=6,因为
L
(
6
)
L(6)
L(6)不为空且
L
(
6
)
!
=
84
L(6)!=84
L(6)!=84,则找到第一次冲突处理后的地址
H
1
=
(
6
+
1
)
%
16
=
7
H_1=(6+1)\%16=7
H1=(6+1)%16=7,而
L
(
7
)
L(7)
L(7)不为空且
L
(
7
)
!
=
84
L(7)!=84
L(7)!=84,则找到第二次冲突处理后的地址
H
2
=
(
6
+
2
)
%
16
=
8
H_2=(6+2)\%16=8
H2=(6+2)%16=8,而
L
(
8
)
L(8)
L(8)不为空且
L
(
8
)
=
84
L(8)=84
L(8)=84,查找成功,返回在表中的序号
8
8
8。
(2)查找
38
38
38:首先求得散列地址
H
(
38
)
=
12
H(38)=12
H(38)=12,因为
L
(
12
)
L(12)
L(12)不为空且
L
(
12
)
!
=
38
L(12)!=38
L(12)!=38,则找到第一次冲突处理后的地址
H
1
=
(
12
+
1
)
%
16
=
13
H_1=(12+1)\%16=13
H1=(12+1)%16=13,而
L
(
13
)
L(13)
L(13)为空,所以不存在关键字为18的记录。
查找各关键字的比较次数如下图:
平均查找长度$
A
S
L
=
(
1
∗
6
+
2
+
3
∗
3
+
4
+
9
)
/
12
=
2.5
ASL=(1*6+2+3*3+4+9)/12=2.5
ASL=(1∗6+2+3∗3+4+9)/12=2.5
对同一组关键字,设定相同的散列函数,则不同的处理冲突的方法得到的散列表不同,它们的平均查找长度也不同。
从散列表的查找过程可见:
(1)虽然散列表在关键字与记录的存储位置之间建立了直接映像,但由于“冲突”的产生,使得散列表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需要以平均查找长度作为衡量散列表的查找效率的度量。
(2)散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子。
- 装填因子。散列表的装填因子一般记为
α
\alpha
α,定义为一个表的装满程度,即
α = 表中记录数 n / 散列表长度 m \alpha=表中记录数n/散列表长度m α=表中记录数n/散列表长度m
散列表的平均查找长度依赖于散列表的装填因子 α \alpha α,而不直接依赖于 n n n或 m m m。直观地看, α \alpha α越大,表示装填的记录越“满”,发生冲突的可能性越大,反之发生冲突的可能性越小。