zoukankan      html  css  js  c++  java
  • [知识点] 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 值能轻松对文件进行识别和校验

  • 相关阅读:
    Python学习 之 流程控制
    Python学习 之 数据类型(邹琪鲜 milo)
    Python学习 之 运算符&表达式
    Python学习 之 编程
    Python学习 之 走进python
    [考试维护]之命名的规范 2015-07-04 21:11 871人阅读 评论(37) 收藏
    [考试维护]修改代码的感受 2015-06-29 11:38 635人阅读 评论(30) 收藏
    如无必要,勿增实体 2015-05-31 20:34 2142人阅读 评论(37) 收藏
    【英语月总】我真的意识到英语的重要性了吗 2015-05-30 21:08 884人阅读 评论(27) 收藏
    selenium测试(Java)--关闭窗口(二十)
  • 原文地址:https://www.cnblogs.com/jinkun113/p/12990275.html
Copyright © 2011-2022 走看看