[知识点] 7.2 哈希表

总目录 > 7 数据结构 > 7.2 哈希表

前言

很久很久以前经常听到哈希这个词,后来多多少少有所接触,但并未系统地了解过哈希到底是怎么回事。

更新日志

20200804 - 进行部分调整,增加例子

子目录列表

1、哈希表与数组

2、哈希函数

3、构造哈希函数

4、哈希冲突

5、字符串 hash

6、应用

 

7.2 哈希表

1、哈希表与数组

哈希表(hash table),又称为散列表,是根据关键码值(key)直接进行访问的一种数据结构,也就是说,给定一个 key,则可以通过哈希表的映射关系快速找到其对应的值(value)。这听起来似乎和数组是一个意思 —— 对于数组 a = {2, 5, 8},其元素 a[1] = 2, a[2] = 5, a[3] = 8,没错,数组本身就是一种 key-value 的对应关系,每个元素的编号为 key,值为 value —— 而哈希表在数组的基础上有什么改进?

2、哈希函数

给定一个哈希表,存在函数 f(key),对任意给定的 key 值,通过代入这个函数就能得到包含该 key 的记录在表中的地址,则这个函数叫做哈希函数(hash function)。hash 的目的是让本来复杂的数据以简单的方式体现或访问,举个例子:给定 10 个同年同月同日同地出生的人的身份证号码,并为这些号码编一个号以便以后使用,比如:

a[320115********0105] = 1
a[320115********8577] = 2
...
a[320115********5201] = 10

显然,开一个 18 位数大小的数组是不现实的,我们决定将这些数进行 hash —— 由于这些号码前 14 位都是相同的,我们将这 18 位的 key 值取后 4 位来替换原 key 值进行各类操作,一下就变得现实起来了:

a[105] = 1
a[8577] = 2
...
a[5201] = 10

而后我们需要输出或其他情况时,把前 14 位还原即可。

所以,在生活中,我们常说的手机尾号便是手机号的哈希值,假设尾号为 4 位,其 hash 函数为 f(key) = key % 10 ^ 4;小明在学校的学号为 1810141728,而在班上提交作业时使用的学号为 28,本质是完整学号的哈希值,hash 函数为 f(key) = key % 100。

而对于普通的数组,可以理解成哈希函数为 f(key) = key。

那么,如何构造一个 hash 函数?有什么要求?

 

3、构造哈希函数

① 除留余数法

取 key 被某个不大于表长 m 的数取模后得到的余数作为 hash 值,即 f(key) = key % p。p 的取值非常关键,一般选择质数或者 m,能够降低错误率。

最为常用的构造法,因为我们使用哈希,最关键的原因就在于原值范围太大,难以存储与访问,那么最简单的就是取模以降低数据大小。比如上述学号与手机号的 hash 值便是使用的这种方法(也可以认为结合了数学分析法)。但更多情况下我们碰到的可能是没有太多可循规律的数据,模数的取值就没有限定了。

这里提供一些常用的质数模数:

1e9 + 7, 12255871, 16341163, 21788233, 29050993, 38734667, 51646229, 68861641,  91815541, 1e9 + 9

(看完下面的若干种构造法后也不难发现,除留余数法也是实现起来最简单的)

② 直接寻址法

取 key 或 key 的某个线性函数值作为 hash 值,即 f(key) = a * key + b

构造方便,但适用范围不广。

举例:

小明统计这次高数考试成绩,每 10 分为一个分段。分数为 key,分数段人数为 value,则 hash 函数可以设定为 f(key) = 0.1 * key,通过 hash 使 key 范围缩小至 1 / 10。

③ 数学分析法

通过对 key 值的分析,找到最不可能出现冲突的构造方式,具体情况具体分析。

④ 平方取中法

求出 key 值的平方,取该平方值的中间几位作为 hash 值。听起来也是个比较玄学的构造方法,当然取最中间的数也是不无道理的 —— 它们和 key 的每一位都会相关,出现冲突的概率较低。由于存在平方操作,key 值不能过大。

举例:

key = 77777,key ^ 2 = 6049261729,f(key) 可以取 9261。‬

⑤ 折叠法

将 key 值按照数位平均切割为若干部分,求出每一部分各个数位之和,最后将这些和首尾相连,得到 f(key)。

举例:

key = 123456789,可以拆分成 123, 456, 789,分别求和为 6, 15, 24,再合并成 61524,即 f(key)。

⑥ 随机数法

选择一个随机函数,取随机值作为 hash 值,即 f(key) = random(key)。

随机大法好。

 

4、hash 冲突

在 2 中举的几个例子,10 个身份证号码的后 4 位理论上是不会有重复的;小明在班上交作业,班上也不会有和他一样尾号为 28 的;但对于手机尾号,假设张三的手机号为 155****1666,李四的手机号为 189****1666,那么他们在讨论手机号时,肯定不会用后 4 位尾号,因为并不能分清到底是谁的手机号,对于这种两个不同的原值通过哈希函数得到的 hash 值相同的情况,我们称之为 hash 冲突

题目对于 hash 函数如何定义并无规定,上面给出的构造 hash 函数的方法任你选择,但显然,我们要保证不出现 hash 冲突的情况,或者尽可能少到忽略不计,即保证其 hash 结果的正确性。而正确性与空间占用往往是成反比的,其正确性越高,hash 值范围越大, 所占用的空间也就越大,所以我们需要在其中找到平衡点,选择最合适的 hash 函数。

而理论上,不论 hash 函数设计得对于契合,对于巧妙,只要数据范围和数据量够大,必然会出现 hash 冲突的情况,那么在遇到冲突时,有如下几种办法解决:

① 开放寻址法

对于 hash 函数 f(key) 和 key 值序列 k[i],假设存在 f(k[1]) = f(k[i]),则将 f(k[i]) 重新构造为:

f(k[i]) = (f(k[i]) + d[i]) % m,m 为表长。

d[i] 可以取:

> 线性探测再哈希:d[i] = c * i,c 为常数

> 平方探测再哈希:d[i] = 1 ^ 2, -1 ^ 2, 2 ^ 2, -2 ^ 2...

要求表长 m 为 4 * j + 3 的质数

> 随机探测再哈希:d[i] 为一组伪随机数列

要求 m 和 d[i] 没有公因子

具体例子暂略。

② 挂链 / 链地址法

所有 hash 值视作一个链表,将所有生成该哈希值的 key 值链在所属链表中。查询时把对应 hash 值的整个链表遍历一遍,对比是否与查询的 key 值相等。

 

5、例子

【例子】给出 10 ^ 6 个数,数据范围为 [1, 10 ^ 9],判定是否出现重复的数。

这里我们使用除留余数法构造 hash 表 + 挂链法解决冲突。

代码:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define MAXN 1000005
 5 #define MOD 12255871
 6 
 7 int n, h[MAXN], a[MAXN], nxt[MAXN];
 8 
 9 int main() { 
10     cin >> n;
11     for (int i = 1; i <= n; i++) {
12         cin >> a[i];
13         int x = a[i] % MOD;
14         if (!h[x]) h[x] = i;
15         else {
16             for (int o = h[x]; o; o = nxt[o])
17                 if (a[o] == a[i])
18                     cout << "yp", exit(0);
19             nxt[i] = h[x], h[x] = i;
20         }    
21     }
22     cout << "nob";
23     return 0;
24 }

h[i] 表示 hash 值为 i 链表头的数,nxt[i] 表示数值为 i 的数所在的链表的下一个数的数值。

 

5、字符串 hash

相比普通的 hash,字符串 hash 多了个字符串转整数的步骤。

详细参见:5.2 字符串 Hash

 

6、应用

密码 hash 函数(Cryptographic Hash Function),是 hash 函数的一种。它是单向函数,也就是说只能从 key 值计算出 hash 值,而很难由 hash 值破译出 key 值,所以可以用于加密,在密码学中使用广泛。

大名鼎鼎的 MD5 便是属于密码 hash 函数。MD5(Message-Digest Algorithm),中文名为信息摘要算法,一种被广泛使用的密码 hash 函数,用于对信息加密,同时保证信息传输完整一致。它在 1992 年公开,用以取代另一种加密算法 MD4。目前,MD5 被广泛用于密码管理,电子签名,垃圾邮件筛选,文件校验等,不过尽管它在 MD2/3/4 的基础上进行大量改进,其安全性并非坚不可摧,在 2004 年已经被证明存在弱点而能被破解,无法防止碰撞,所以不适用于更高级别的安全防护。

举个例子,之前很火的 P2P(Peer-to-peer)对等网络技术被应用于文件下载和共享时,下载软件对文件的识别便是使用的 MD5 值,每一个被上传和下载的文件有其独一无二的 MD5 值,相当于其身份牌,通过比较 MD5 值能轻松对文件进行识别和校验

posted @ 2020-05-29 23:54  jinkun113  阅读(616)  评论(0编辑  收藏  举报