为什么要构建哈希表?
现在有一组数据,我们想查找一个值(x)是否在这组数据中,通常来说,我们需要把这组数据遍历一遍,来看看有没有x这个值。
这时,我们发现这样查找数据要花费的时间复杂度为O(n),链表、顺序表都是如此。
为了降低查找数据的时间复杂度,那我们就不能去遍历所有的数据来查找,我们需要找到新的方法来查找数据,这时我们就引入了哈希表。
文章目录
哈希
哈希就是根据设定好的哈希函数和处理发生哈希冲突的解决方法,哈希是一种存储方法,也是一种查找方法,算法中只要用到这种哈希思想,就可以叫做哈希算法(hash)
哈希函数
1.定义
将一组数据的关键字映射到连续且有限的地址集(区间)可以将映射到地址集上的像记录在表中,这个表就是我们说的哈希表,这个映射的过程一般叫做哈希造表 或者叫散列,这个映射关系叫做哈希函数或者散列
2.构造方法
1)直接定址法
取其关键字或者关键字的某个线性函数值为哈希地址。即:
f
(
k
e
y
)
=
a
×
k
e
y
+
b
(
a
、
b
为常数
)
f(k e y)=a \times k e y+b \quad(a 、 b \text { 为常数 })
f(key)=a×key+b(a、b 为常数 )
例如:现在有一个0~100岁的人口统计表,其中年龄作为关键字哈希函数取关键字:
f
(
k
e
y
)
=
k
e
y
f(k e y)= k e y
f(key)=key
如果我们要统计1980年后出现的人口数,那么我们就可以对出生年份这个关键字减去1980来作为地址,即:
f
(
k
e
y
)
=
k
e
y
−
1980
f(k e y)= k e y-1980
f(key)=key−1980
2)数字分析法
假设关键字是以r为基的数(如:以10为基的十进制数),并且哈希表可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
例如:我们的11位手机号“130xxxx1234”,前三位表示不同运营商公司的子品牌,中间四位是HLR识别号,表示用户的归属地,后面四位才是真正的用户号,如下图所示
那么如果我们要存储某家公司员工登记表,如果用手机号作为关键字,那么很有可能前7位是相同的,那么选取后四位作为哈希地址就是不错的选择。
3)平方取中法
这个方法计算比较简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做哈希地址。这种方法比较适合不知道关键字的分布,而位数又不是很大的情况。
4)折叠法
折叠法是将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和,并按照哈希表表长,取后几位作为哈希地址。
比如我们的关键字是9876543210,哈希表表长为3位,我们将其分为四组987|654|321|0,然后将它们叠加求和987+654+321+0=1962,在求后三位得到哈希地址为962.
折叠法事先不需要知道关键字的分布,适合关键字位数比较多的情况。
5)除留余数法
此方法是最常用的构造哈希函数的方法。对于哈希表长为m的哈希函数公式为:
f
(
key
)
=
key
m
o
d
p
(
p
⩽
m
)
f(\text { key })=\text { key } \bmod p(p \leqslant m)
f( key )= key modp(p⩽m)
mod是取模(求余数)的意思,并且此方法不仅可以直接对关键字取模,也可以在折叠、平方取中后再取模。此方法的关键在于选取合适的p,如果选的不好很容易造成哈希冲突。
如对下表,我们对于有12个记录的关键字构造哈希表时,就用了
f
(
key
)
=
key
.
m
o
d
12
f(\text { key })=\text { key } .\bmod 12
f( key )= key .mod12的方法,比如
29
m
o
d
12
=
5
29mod12=5
29mod12=5,所以它存储在下表为5的位置。
6)随机数法
选取一个随机数,取关键字的随机函数值作为它的哈希地址,也就是 f ( k e y ) = r a n d o m ( k e y ) f(key)=random(key) f(key)=random(key).这里 r a n d o m random random是随机函数。当关键字的长度不等时,采用这个方法比较合适。
哈希冲突
1.定义
不同数据的关键字通过哈希函数得到一个相同的地址,这时就发生冲突,这种冲突一般叫做哈希冲突(存放位置有值)。
2.构造方法
1)开放定址法
此方法就是一旦发生了冲突,就去寻找下一个空的哈希地址,只要哈希列表足够大,空的哈希地址总能找到,并将记录存入。
公式为:
f
i
(
key
)
=
(
f
(
key
)
+
d
i
)
MOD
m
(
d
i
=
1
,
2
,
3
,
⋯
⋯
,
m
−
1
)
f_{i}(\text { key })=\left(f(\text { key })+d_{i}\right) \quad \operatorname{MOD} m\left(d_{i}=1,2,3, \cdots \cdots, m-1\right)
fi( key )=(f( key )+di)MODm(di=1,2,3,⋯⋯,m−1)
2)再哈希函数法
对于我们的哈希函数来说,我们可以事先准备多个哈希函数
f
i
(
key
)
=
R
H
i
(
key
)
(
i
=
1
,
2
,
⋯
,
k
)
f_{i}(\text { key })=R H_{i}(\text { key }) \quad(i=1,2, \cdots, k)
fi( key )=RHi( key )(i=1,2,⋯,k)
这里
R
H
i
R H_{i}
RHi就是不同的哈希函数,可以把我们之前说的除留余数、折叠、平方取中等方法全部用上。每当哈希地址冲突时,就换一个哈希函数计算,相信总会有一个可以把冲突解决掉,当然这种方法会相应的增加计算的时间。
3)链地址法
我们将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。对于关键字集合{12,67,56,16,25,37,22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法,可得到如下图结构,此时,已经不存在冲突换址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。
链地址法对于可能会造成很多冲突的哈希函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链表的性能损耗。
4)公共区溢出法
这个方法就是建立一个溢出表,把所有冲突的数据都存进去,我们给所有冲突的关键字建立了一个公共溢出区来存放。