散列表的概念
散列法
散列法又称哈希法或杂凑法,它在元素的存储位置与元素关键码之间建立一个确定的对应函数关系Hash(),使得每个关键码与结构中的一个唯一的存储位置相对应:
A
d
d
r
e
s
s
=
H
a
s
h
(
k
e
y
)
Address = Hash(key)
Address=Hash(key)
在插入时,依此函数计算存储位置并按此位置存放。
在查找时,对元素的关键码进行同样的函数计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则查找成功。这种方法就称作散列法。
散列函数
在散列法中使用的转换函数叫做散列函数。
散列表
按照散列法构造出来的表或结构就叫做散列表。
散列表是根据关键字直接进行访问的数据结构,也就是说,散列表建立了关键字和存储位置之间的一种直接映射关系。
常见的散列函数
<1> 直接定址法
对关键码做一个线性计算,把计算结果当作散列地址。
H
(
k
e
y
)
=
a
×
k
e
y
+
b
H(key) = a × key + b
H(key)=a×key+b
其中,
a
a
a和
b
b
b都是常数。
这种方式计算简单,且不会产生冲突。它适合关键字的分布基本连续的情况;若关键分布不连续,空位较多,则会造成存储空间的浪费。
<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,使得每个关键字通过该函数转换后等概率地映射到散列空间上地任一地址,从而尽可能减少冲突的可能性。
<3> 数字分析法
设关键字是r进制数,而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较均匀的若干位作为散列地址。
这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
<4> 平方取中法
这种方法取关键字的平方值的中间几位作为散列地址。
这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值不够均匀或均小于散列地址所需的位数。
<5> 折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以短一些),然后取这及部分的叠加和作为散列地址,这种方法称为折叠法。
当关键字位数很多,而且关键字中的每位上数字分布大致均匀时,可以采用折叠法得到散列地址。
处理冲突的方法
可以注意到,任何设计出来的散列函数都不可能绝对地避免冲突。为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个“空”的Hash地址。
处理冲突的开放地址法
所谓开放地址法,是指可存放新元素的空闲位置既向它的同义词元素开放,又向它的非同义词元素开放。就是说,所有元素都放在一个散列表的基本空间之内。这里的同义词指的是那些散列地址相同的不同关键码。
其数学递归公式是:
H
i
=
(
H
(
k
e
y
)
+
d
i
)
%
m
H_i = (\ H(key) + d_i\ )\ \% \ m
Hi=( H(key)+di ) % m
其中
H
i
H_i
Hi 表示发生冲突后第
i
i
i 次探测的散列地址;
i
=
0
,
1
,
2
,
.
.
.
,
k
(
k
≤
m
−
1
)
i=0,1,2,...,k(k≤m-1)
i=0,1,2,...,k(k≤m−1);
m
m
m表示散列表表长;
d
i
d_i
di为增量序列
确定某一增量序列后,对应的处理方法就是确定的。通常由以下4中取法:
<1> 线性探测法
当 d i = 0 , 1 , 2 , . . . , m − 1 d_i = 0,1,2,...,m-1 di=0,1,2,...,m−1时,称为线性探测法。
这种方法的特点:冲突发生时,顺序查看表中下一个单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。
缺点:线性探测法可能使第 i i i个散列地址的同义词存入第 i + 1 i+1 i+1个散列地址,这样本应存入第 i + 1 i+1 i+1个散列地址的元素就争夺第 i + 2 i+2 i+2个散列地址的元素的地址……从而造成大量元素在相邻的散列地址上“聚集”起来,大大降低了查找效率。
例如,关键码为
{
37
,
25
,
14
,
36
,
49
,
68
,
57
,
11
}
\{37,25,14,36,49,68,57,11\}
{37,25,14,36,49,68,57,11},散列函数为
H
a
s
h
(
k
e
y
)
=
k
e
y
%
11
Hash(key) = key \% 11
Hash(key)=key%11,则各关键码计算出的地址为:
H
a
s
h
(
37
)
=
4
Hash(37)=4
Hash(37)=4,
H
a
s
h
(
25
)
=
3
Hash(25)=3
Hash(25)=3,
H
a
s
h
(
14
)
=
3
Hash(14)=3
Hash(14)=3,
H
a
s
h
(
36
)
=
3
Hash(36)=3
Hash(36)=3,
H
a
s
h
(
49
)
=
5
Hash(49)=5
Hash(49)=5,
H
a
s
h
(
68
)
=
2
Hash(68)=2
Hash(68)=2,
H
a
s
h
(
57
)
=
2
Hash(57)=2
Hash(57)=2,
H
a
s
h
(
11
)
=
0
Hash(11)=0
Hash(11)=0
得到的散列结果如图所示:
散列地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 11 | 68 | 25 | 37 | 14 | 36 | 49 | 57 | ||||
比较次数 | 1 | 1 | 1 | 1 | 3 | 4 | 3 | 7 |
那么 A S L 成 功 = 1 8 × ( 1 + 1 + 1 + 1 + 3 + 4 + 3 + 7 ) = 21 8 ASL_{成功} = \frac{1}{8} × (1+1+1+1+3+4+3+7) = \frac{21}{8} ASL成功=81×(1+1+1+1+3+4+3+7)=821
<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,散列表长度必须是一个可以表示成 4 k + 3 4k+3 4k+3的素数,又称为二次探测法。
平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。
例如,关键码为
{
37
,
25
,
14
,
36
,
49
,
68
,
57
,
11
}
\{37,25,14,36,49,68,57,11\}
{37,25,14,36,49,68,57,11},散列函数为
H
a
s
h
(
k
e
y
)
=
k
e
y
%
19
Hash(key) = key \% 19
Hash(key)=key%19,则各关键码计算出的地址为:
H
a
s
h
(
37
)
=
18
Hash(37)=18
Hash(37)=18,
H
a
s
h
(
25
)
=
6
Hash(25)=6
Hash(25)=6,
H
a
s
h
(
14
)
=
14
Hash(14)=14
Hash(14)=14,
H
a
s
h
(
36
)
=
17
Hash(36)=17
Hash(36)=17,
H
a
s
h
(
49
)
=
11
Hash(49)=11
Hash(49)=11,
H
a
s
h
(
68
)
=
11
Hash(68)=11
Hash(68)=11,
H
a
s
h
(
57
)
=
0
Hash(57)=0
Hash(57)=0,
H
a
s
h
(
11
)
=
11
Hash(11)=11
Hash(11)=11
得到的散列结果如图所示:
散列地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 57 | 25 | 11 | 49 | 68 | 14 | 36 | 37 | |||||||||||
比较次数 | 1 | 1 | 3 | 1 | 2 | 1 | 1 | 1 |
那么
A
S
L
成
功
=
1
8
×
(
1
+
1
+
3
+
1
+
2
+
1
+
1
+
1
)
=
11
8
ASL_{成功} = \frac{1}{8} × (1+1+3+1+2+1+1+1) = \frac{11}{8}
ASL成功=81×(1+1+3+1+2+1+1+1)=811
<3> 再散列法
当 d i = H a s h 2 ( k e y ) d_i = Hash_2(key) di=Hash2(key)时,称为再散列法,又称为双散列法。
需要使用两个散列函数,当通过第一个散列函数
H
a
s
h
(
k
e
y
)
Hash(key)
Hash(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
a
s
h
(
k
e
y
)
%
m
H_0 = Hash(key)\%m
H0=Hash(key)%m。
i
i
i是冲突次数,初始为0。
在散列法中,最多经过
m
−
1
m-1
m−1次探测就会遍历表中的所有位置,回到
H
0
H_0
H0位置。
例如,关键码为
{
37
,
25
,
14
,
36
,
49
,
68
,
57
,
11
}
\{37,25,14,36,49,68,57,11\}
{37,25,14,36,49,68,57,11},散列函数为
H
a
s
h
(
k
e
y
)
=
k
e
y
%
11
Hash(key) = key \% 11
Hash(key)=key%11,则各关键码计算出的地址为:
H
a
s
h
(
37
)
=
4
Hash(37)=4
Hash(37)=4,
H
a
s
h
(
25
)
=
3
Hash(25)=3
Hash(25)=3,
H
a
s
h
(
14
)
=
3
Hash(14)=3
Hash(14)=3,
H
a
s
h
(
36
)
=
3
Hash(36)=3
Hash(36)=3,
H
a
s
h
(
49
)
=
5
Hash(49)=5
Hash(49)=5,
H
a
s
h
(
68
)
=
2
Hash(68)=2
Hash(68)=2,
H
a
s
h
(
57
)
=
2
Hash(57)=2
Hash(57)=2,
H
a
s
h
(
11
)
=
0
Hash(11)=0
Hash(11)=0
取第二个散列函数为
H
a
s
h
2
(
k
e
y
)
=
7
−
(
k
e
y
%
7
)
Hash_2(key) = 7-(key\%7)
Hash2(key)=7−(key%7),各关键码相关的地址增量为
H
a
s
h
2
(
37
)
=
5
Hash_2(37)=5
Hash2(37)=5,
H
a
s
h
2
(
25
)
=
3
Hash_2(25)=3
Hash2(25)=3,
H
a
s
h
2
(
14
)
=
7
Hash_2(14)=7
Hash2(14)=7,
H
a
s
h
2
(
36
)
=
6
Hash_2(36)=6
Hash2(36)=6,
H
a
s
h
2
(
49
)
=
7
Hash_2(49)=7
Hash2(49)=7,
H
a
s
h
2
(
68
)
=
2
Hash_2(68)=2
Hash2(68)=2,
H
a
s
h
2
(
57
)
=
6
Hash_2(57)=6
Hash2(57)=6,
H
a
s
h
2
(
11
)
=
3
Hash_2(11)=3
Hash2(11)=3
得到的散列结果如图所示:
散列地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 11 | 68 | 25 | 37 | 49 | 57 | 36 | 14 | |||
比较次数 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 |
那么
A
S
L
成
功
=
1
8
×
(
1
+
1
+
1
+
1
+
1
+
2
+
2
+
2
)
=
11
8
ASL_{成功} = \frac{1}{8} × (1+1+1+1+1+2+2+2) = \frac{11}{8}
ASL成功=81×(1+1+1+1+1+2+2+2)=811
<4> 伪随机序列法
当
d
i
=
伪
随
机
数
序
列
d_i = 伪随机数序列
di=伪随机数序列时,称为伪随机序列法。
注意!!!!
在开放定址的情形下,不能随便物理删除表中已有元素,因为若删除元素,则会截断其他具有相同散列地址的元素的查找位置。
因此,要删除一个元素时,可给它作一个删除标记,进行逻辑删除。但这样做的副作用是,执行多次删除后,表面上看起来散列表很满,实际上有许多个位置未利用,因此需要定期维护散列表,要把删除标记的元素物理删除。
故当散列表经常变动时,最好用拉链法来处理冲突。
处理冲突的拉链法
显然,对于不同的关键字可能会通过散列函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。
假设散列地址为
i
i
i 的同义词链表的头指针存放在散列表的第
i
i
i 个单元中,因而查找、插入和删除操作主要在同义词链中进行。
拉链法适用于经常进行插入和删除的情况。
例如,关键字序列为
{
12
,
15
,
16
,
22
,
25
,
29
,
34
,
37
,
47
,
48
,
56
,
67
}
\{12,15,16,22,25,29,34,37,47,48,56,67\}
{12,15,16,22,25,29,34,37,47,48,56,67},散列函数为
H
a
s
h
(
k
e
y
)
=
k
e
y
%
13
Hash(key) = key \% 13
Hash(key)=key%13,用拉链法处理冲突,建立的表如下图所示:
散列查找及性能分析
散列表的查找过程与构造散列表的过程基本一致。
对于一个给定的关键字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置为此地址,转入步骤①。
散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子。
装填因子
散列表的装填因子一般记为
α
\alpha
α,定义为一个表的装满程度,即
α
=
表
中
记
录
数
n
散
列
表
长
度
m
\alpha = \frac{表中记录数n}{散列表长度m}
α=散列表长度m表中记录数n
散列表的平均查找长度依赖于散列表的装填因子
α
\alpha
α,而不直接依赖于
n
n
n或
m
m
m。
直观来看,
α
\alpha
α越大,表示装填的记录越“满”,发生冲突的可能性越大,反之发生冲突的可能性越小。