哈希表
1. 定义
散列存储即在存储位置和存储内容之间建立一个对应关系f,使得存储位置 = f (key)
,这个f 即被称为哈希(Hash)函数,又称散列函数,采用这种存储方式将记录存储在一块连续的存储空间中,这块空间即称为哈希表(Hash table) 或 散列表,关键字对应的记录位置称为散列地址。
2. 哈希函数的构造方法
冲突:当 k e y 1 ≠ k e y 2 \sf key1\not =key2 key1=key2,但 f ( k e y 1 ) = f ( k e y 2 ) \sf f(key1) = f(key2) f(key1)=f(key2)时,即为发生了冲突,所以在设计哈希函数时要使这种情况尽可能地少。
f(key) = a*key+b \colorbox{#D2E4EF}{\sf{f(key) = a*key+b}} f(key) = a*key+b
适用于关键字位数较大,且分布比较均匀时(例如电话号码),可以抽取关键字中的一部分(例如后四位)用于计算散列地址
适用于不知道关键字分布,重复频率又比较高的情况,例如若key = 1234,而12342 = 1522756,则取中间3为227为散列地址
适用于关键字位数较大,且不知道分布时,可以将关键字分为相等的几部分,作和并按哈希表表长取后几位为散列地址。
对于哈希表长为m的散列函数公式为
f(key)=key
mod
p
(p
<
=
m)
\colorbox{#D2E4EF}{\sf f(key)=key\ mod\ p\ \ \ \ (p <= m)}
f(key)=key mod p (p <= m)
为了尽量减少冲突,使p为小于或等于表长m的最小质数或不包含小于20质因子的合数
适用于关键字长度不等的情况
f(key)=random(key)
\colorbox{#D2E4EF}{\sf{}f(key)=random(key)}
f(key)=random(key)
3. 处理冲突的方法
1)开放定址法
- 线性探测法:一旦发生冲突,就去找下一个地址,直到找到空位为止。公式:
f i ( k e y ) = ( f ( k e y ) + d i ) m o d m ( d i = 1 , 2 , 3 , … , m − 1 ) \sf{}f_i(key)=(f(key)+d_i)\mod m\ \ \ \ (d_i=1,2,3,…,m-1) fi(key)=(f(key)+di)modm (di=1,2,3,…,m−1)
例如若关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},我们使 f ( k e y ) = k e y m o d 12 \sf f(key)=key\ mod\ 12 f(key)=key mod 12,在存储前5个数时都没有产生冲突,如表
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 16 | 67 | 56 |
但是当key = 37时,37 mod 12 = 1,与25所在的位置冲突,于是我们用上面的公式, f ( 37 ) = ( f ( 37 ) + 1 ) m o d 12 = 2 \sf{}f(37)=(f(37)+1)\ mod\ 12=2 f(37)=(f(37)+1) mod 12=2,所以将37存入下标为2的位置。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 37 | 16 | 67 | 56 |
接下来22, 29, 15, 47都没有冲突
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 37 | 15 | 16 | 29 | 67 | 56 | 22 | 47 |
当key = 48时, f ( 48 ) = 0 \sf{}f(48)=0 f(48)=0,与12所在位置冲突,则 f ( 48 ) = ( f ( 48 ) + 1 ) m o d 12 = 1 \sf{}f(48)=(f(48)+1)\ mod\ 12=1 f(48)=(f(48)+1) mod 12=1,又与25所在位置冲突,继续 f ( 48 ) = ( f ( 48 ) + 2 ) m o d 12 = 2 \sf{}f(48)=(f(48)+2)\ mod\ 12=2 f(48)=(f(48)+2) mod 12=2,还是与37所在位置冲突…这样直到 f ( 48 ) = ( f ( 48 ) + 6 ) m o d 12 = 6 \sf{}f(48)=(f(48)+6)\ mod\ 12=6 f(48)=(f(48)+6) mod 12=6,才不再冲突,存入下标为6的位置。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 37 | 15 | 16 | 29 | 48 | 67 | 56 | 22 | 47 |
这种解决冲突的开放定址法称为线性探测法。而48本来和37不是同义词,但却需要争夺同一个地址,这种现象称为堆积。
- 二次探测法
增加平方运算使得关键字不会聚集在某一块区域,并且双向寻找可能的空位置,公式:
f i ( k e y ) = ( f ( k e y ) + d i ) m o d m ( d i = 1 2 , − ( 1 2 ) , 2 , − ( 2 2 ) , … , q 2 , − ( q 2 ) , q ≤ m / 2 \sf{}f_i(key)=(f(key)+d_i)\mod m\ \ \ \ (d_i=1^2,-(1^2),2,-(2^2),…,q^2,-(q^2),q\le m/2 fi(key)=(f(key)+di)modm (di=12,−(12),2,−(22),…,q2,−(q2),q≤m/2
- 随机探测法
令di为一组伪随机数列
2)再散列法
f i ( k e y ) = R H i ( k e y ) ( i = 1 , 2 , … k ) \sf{}f_i(key)=RH_i(key)\ \ \ \ (i=1,2,…k) fi(key)=RHi(key) (i=1,2,…k)
即事先准备多个散列函数,如果发生冲突就替换至RHI
3)链地址法
我们可以先将所有同义词存在同一个单链表中,即为同义词子表,然后在哈希表中存储所有同义词子表的头指针即可。
4)公共溢出区法
将所有有冲突的关键字存储在溢出区内,在查找使先在基本表中查,如果查不到再顺序查找溢出区,适用于冲突较少的情况。
4. 性能分析
影响平均查找长度的因素:
如果没有冲突,散列查找的时间复杂度为O(1)
- 散列函数是否均匀
但不同的散列函数对同一组随机的关键字,产生冲突的可能性时相同的,因此我们可以不考虑它对平均查找长度的影响。 - 处理冲突的方法
线性探测法处理冲突时产生堆积的可能性就比二次探测法要大,而链地址法不会产生任何堆积,因而具有更加的平均查找性能。 - 装填因子
α
\alpha
α
α = \sf{}\alpha= α= 填入表中的记录个数/散列表长度,标志着散列表的装满程度, α \alpha α越大,产生冲突的可能性越大。例如散列表长度为12,填入的关键字个数为11,那么此时 α = 11 / 12 = 0.9167 \sf\alpha=11/12=0.9167 α=11/12=0.9167,那么填入最后一个关键字时产生冲突的可能性就非常大了。
来源:《大话数据结构》