哈希表
什么是哈希表
- 哈希表(Hash Table),也可以称为散列表或者 Hash 表。
- 哈希表用的是数组支持按照下标访问数据的特性,所以哈希表其实就是数组的一种扩展,由数组演化而来。
- 是根据键(Key)而直接访问在内存存储位置的数据结构。
过程:它通过映射函数 输入key 输出数组下标的方式,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表
为什么需要哈希呢?
链表和哈希表做对比
比如 现在我们将每个人的性别作为数据进行存储,键为人名,值为对应的性别,其中 M 表示性别为男,F 表示性别为女。
查询数组中数据
为了和哈希表进行对比,我们先将这些数据存储在数组中。
此处准备了6个箱子(即长度为6的数组)来存储数据,假设我们需要查询 Ally 的性别,由于不知道 Ally 的数据存储在哪个箱子里,所以只能从头开始查询,这个操作便叫作线性查找。一般来说,我们可以把键当成数据的标识符,把值当成数据的内容。
从 0 号箱子开始查找,发现 0 号箱子中存储的键是 Joe 而不是 Ally,因此接着查找 1 号箱子。
哦豁,1 号箱子中的也不是 Ally,没办法,只能接着往下找。
有点小糟糕,2 号、3 号箱子中的也都不是 Ally。
功夫不负有心人,当我们查找到 4 号箱子的时候,发现其中数据的键为 Ally,把键对应的值取出,我们就知道 Ally 的性别为女(F)。
哈希表存储
通过上面的查找过程,我们发现数据量越多,线性查找耗费的时间就越长。由此可知:由于数据的查询较为耗时,所以此处并不适合使用数组来存储数据。
但使用哈希表便可以解决这个问题,首先准备好数组,这次我们用 5 个箱子的数组来存储数据。
尝试把 Joe 存进去,使用哈希函数(Hash)计算 Joe 的键,也就是字符串 Joe 的哈希值,比如得到的结果为4928。
将得到的哈希值除以数组的长度 5,求得其余数,这样的求余运算叫作mod运算,此处mod运算的结果为3。
因此,我们将 Joe 的数据存进数组的 3 号箱子中,重复前面的操作,将其他数据也存进数组中。
Sue 键的哈希值为 7291, mod 5 的结果为 1,将 Sue 的数据存进 1 号箱中。
Dan 键的哈希值为 1539, mod 5 的结果为 4,将 Dan 的数据存进 4 号箱中。
Nell 键的哈希值为 6276, mod 5 的结果为 1,本应将其存进数组的 1 号箱中,但此时 1 号箱中已经存储了 Sue 的数据,这种存储位置重复了的情况便叫作冲突(哈希冲突)。
遇到这种情况,其中一种方法用链表法,用链表在已有数据的后面继续存储新的数据。
Ally 键的哈希值为 9143, mod 5 的结果为 3,本应将其存储在数组的 3 号箱中,但 3 号箱中已经有了 Joe 的数据,所以使用链表,在其后面存储 Ally 的数据。
Bob 键的哈希值为 5278, mod 5 的结果为 3,本应将其存储在数组的 3 号箱中,但 3 号箱中已经有了 Joe 和 Ally 的数据,所以使用链表,在 Ally 的后面继续存储 Bob 的数据。
像这样存储完所有数据,哈希表也就制作完成了。
查询哈希表中的数据
接下来讲解数据的查询方法,假设我们要查询 Dan 的性别。
为了知道 Dan 存储在哪个箱子里,首先需要算出 Dan 键的哈希值,然后对其进行 mod 运算,最后得到的结果为 4,于是我们知道了它存储在 4 号箱中。
查看 4 号箱可知,其中的数据的键与 Dan 一致,于是取出对应的值,由此我们便知道了 Dan 的性别为男(M)。
那么,想要查询 Ally 的性别时该怎么做呢?为了找到它的存储位置,先要算出 Ally 键的哈希值,再对其进行 mod 运算,最终得到的结果为 3。
然而 3 号箱中数据的键是 Joe 而不是 Ally,此时便需要对 Joe 所在的链表进行线性查找。
于是我们找到了键为 Ally 的数据,取出其对应的值,便知道了 Ally 的性别为女(F)。
哈希冲突的解决办法
链地址法
可以利用链表在存储数据后面加一个指针,指向后面冲突的数据来解决冲突,这种方法被称为链表法,也被称为链地址法。
有一组数据
19 01 23 14 55 68 11 86 37要存储在表长11的数组中,其中H(key)=key MOD 11
开放地址法(开放寻址法)
这种方法是指当冲突发生时,立刻计算出一个候补地址(数组上的位置)并将数据存进去。如果仍然有冲突,便继续计算下一个候补地址,直到有空地址为止,可以通过多次使用哈希函数或线性探测法等方法计算候补地址。
首先有一个H(key)的哈希函数
有三种取法
1)线性探测再散列
2)平方探测再散列
3)随机探测在散列(双探测再散列)
三种开放定址法解决冲突方案的例子
有一组数据
19 01 23 14 55 68 11 86 37要存储在表长11的数组中,其中H(key)=key MOD 11
那么按照上面三种解决冲突的方法,存储过程如下:
(表格解释:从前向后插入数据,如果插入位置已经占用,发生冲突,冲突的另起一行,计算地址,直到地址可用,后面冲突的继续向下另起一行。最终结果取最上面的数据(因为是最“占座”的数据))
线性探测再散列
我们取di=1,即冲突后存储在冲突后一个位置,如果仍然冲突继续向后
平方探测再散列
先走线性探测,如果冲突然后向这个点左探测,这样来回探测
随机探测在散列(双探测再散列) 发生冲突后 根据 伪随机函数 生成对应的随机数,就是他的索引,则有如下结果:
公共溢出区法
建立一个特殊存储空间,专门存放冲突的数据。此种方法适用于数据和冲突较少的情况。
再散列法
准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数,第二个也冲突,使用第三个……
重点了解一下开放定制法和链地址法
哈希函数的构造方法
直接定制法
数字分析法
平方取中法
折叠法
除留余数法
H(key)=key MOD p (p<=m m为表长)
很明显,如何选取p是个关键问题。
使用举例
比如我们存储3 6 9,那么p就不能取3
因为 3 MOD 3 == 6 MOD 3 == 9 MOD 3
p应为不大于m的质数或是不含20以下的质因子的合数,这样可以减少地址的重复(冲突)
比如key = 7,39,18,24,33,21时取表长m为9 p为7 那么存储如下
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
key | 7 | 21(冲突后移) | 24 | 39 | 18(冲突后移) | 33(冲突后移) |
随机数法 H(key) =Random(key) 取关键字的随机函数值为它的散列地址
哈希表的查找
查找过程和造表过程一致,假设采用开放定址法处理冲突,则查找过程为:
对于给定的key,计算hash地址index = H(key)
如果数组arr【index】的值为空 则查找不成功
如果数组arr【index】== key 则查找成功
否则 使用冲突解决方法求下一个地址,直到arr【index】== key或者 arr【index】==null
哈希表的删除
首先链地址法是可以直接删除元素的,但是开放定址法是不行的,拿前面的双探测再散列来说,假如我们删除了元素1,将其位置置空,那 23就永远找不到了。正确做法应该是删除之后置入一个原来不存在的数据,比如-1
备注:
借鉴了:
https://blog.csdn.net/u011109881/article/details/80379505
https://zhuanlan.zhihu.com/p/107326081