哈希表
文章目录
为什么有哈希?
在普通的查找方法中,查找的效率都依赖于查找过程中的比较次数。在顺序查找中,依次比较"=“或者”!=",在折半查找、二叉排序树查找等过程中依旧需要去不断比较。我们理想的情况是想着不经过任何比较,一次就找到所查记录。例如:
假如我们要设计一个学生管理系统,将学号作为主键记录学生信息记录,我们希望高效的插入学生和学生的信息(插入)、搜索学生获取学生信息记录(查找)、删除学生以及学生相关信息(删除)。
我们可以使用不同的数据结构来存储处理:数组、链表、AVL树等。但是对于数组、链表和平衡二叉树我们知道查找时间复杂度很高(数组使用二分查找时间复杂度 O ( l o g n ) O(log n) O(logn) , 但是由于需要维护数组有序插入删除的时间复杂度 O ( n ) O(n) O(n) ;平衡二叉树的插入、查找和删除操作的时间复杂度均为 O ( l o g n ) O(log n) O(logn) )。这看起来已经很不错了,但是还有更好的数据结构,就是我们的哈希表。
学号 | 记录地址 |
---|---|
… | NULL |
2020001 | 0x0001 |
2020002 | 0x00C9 |
2020003 | 0x0191 |
… | … |
2021065 | 0x0961 |
我们都知道excel表格,当我们用一个普通的表来存学生信息的时候,首先需要重建一个大数组,数组下标就是学号。所有学好都要在大表中,如果没有该学生则对应的记录地址填充NULL
;如果要查找一个学生的信息,那么直接去大表的相应学号下标查找即可。
这样一来,插入、查找和删除的时间复杂度都降到 O ( 1 ) O(1) O(1) 了,但是这种直接访问大表的方法在实践中的弊端也很明显,需要申请很大的额外空间,存在大量空间浪费(比如这里的学号有7位,总共要申请 1 0 7 10^7 107 条记录,但是我们的学生可能只有两千)。
为了解决上述的问题,我们不能直接利用一个大表,所以提出了哈希这个数据结构,并且和数组、链表等数据结构相比性能更好。通过哈希我们可以在 O ( 1 ) O(1) O(1) 时间复杂度内实现插入、查找和删除,在最坏的情况下也不会超过 O ( n ) O(n) O(n) 。
哈希是对直接访问表的该进。使得通过哈希函数将我们给定的键转为范围更小的数字,通过这个数字作为哈希表的索引(哈希值)来查找信息。
什么是哈希函数?
哈希函数用于将一个大数或者字符串映射为一个可以作为哈希索引的较小的整数的函数。Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。比如开发中经常用到的MD5和SHA都是经典的Hash算法。
一个好的哈希函数,必须满足四个条件:
执行效率高(效率高)。
对于关键字集合中的任一关键字,经过哈希函数映射到哈希表地址集合中任一一个地址的概率都是相等的。换句话就是关键字经过哈希函数得到一个随机的地址,这样就可以使得整个哈希表上的数据均匀分布。输出越均匀,冲突越少。(同一性)
微小的输入变化要使得输出发生巨大差别(雪崩效应)。
不可以从哈希函数的输出值推出原始数据(不可反向推导)。
如何构造哈希函数?
哈希函数的构造原则是:函数本身便于计算、计算出来的地址分布均匀。
1、直接定址法
最容易想到的是直接用关键字做或者关键字的某个线性函数值作为哈希地址。即:
H
a
s
h
(
k
e
y
)
=
k
e
y
或
H
a
s
h
(
k
e
y
)
=
a
∗
k
e
y
+
b
Hash(key)=key 或 Hash(key)=a*key+b
Hash(key)=key或Hash(key)=a∗key+b
由于直接定址法所得地址集合和关键字集合大小相同。所以不同关键字不会发生冲突,但是我们一般不会这么用。
2、数学分析法
如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。
例如,有80个记录,关键字为8位十进制整数d1d2d3…d7d8,如哈希表长取100,则哈希表的地址空间为:00~99,则可取两位十进制数组成哈希地址。
第1位 | 第2位 | 第3位 | 第4位 | 第5位 | 第6位 | 第7位 | 第8位 |
---|---|---|---|---|---|---|---|
8 | 1 | 3 | 4 | 6 | 5 | 3 | 2 |
8 | 1 | 3 | 7 | 2 | 2 | 4 | 2 |
8 | 1 | 3 | 8 | 7 | 4 | 2 | 2 |
8 | 1 | 3 | 0 | 1 | 3 | 6 | 7 |
8 | 1 | 3 | 2 | 2 | 8 | 1 | 7 |
8 | 1 | 3 | 3 | 8 | 9 | 6 | 7 |
8 | 1 | 3 | 5 | 4 | 1 | 5 | 7 |
8 | 1 | 3 | 6 | 8 | 5 | 3 | 7 |
8 | 1 | 4 | 1 | 9 | 3 | 5 | 5 |
假设经过分析,各关键字中 d4和d7的取值分布较均匀,则哈希函数为:h(key)=h(d1d2d3…d7d8)=d4d7。例如,h(81346532)=43,h(81301367)=06。
相反,假设经过分析,各关键字中 d1和d8的取值分布极不均匀, d1 都等于5,d8 都等于2,此时,如果哈希函数为:h(key)=h(d1d2d3…d7d8)=d1d8,
则所有关键字的地址码都是52,显然不可取。
3、平放取中法
当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。
这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。
由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如图所示。
关键字 | 内部编码 | 内部编码的平方值 | H(k)关键字的哈希地址 |
---|---|---|---|
KEYA | 11050201 | 122157778355001 | 778 |
KYAB | 11250102 | 126564795010404 | 795 |
AKEY | 01110525 | 001233265775625 | 265 |
BKEY | 02110525 | 004454315775625 | 315 |
4、折叠法
这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
具体方法有折叠法与移位法。移位法是将分割后的每部分低位对齐相加,折叠法是从一端向另一端沿分割界来回折叠(奇数段为正序,偶数段为倒序),然后将各段相加。
key=12360324711202065,哈希表长度为1000,则应把关键字分成3位一段,在此舍去最低的两位65,分别进行移位叠加和折叠叠加,求得哈希地址为105和907
(a)移位叠加
1 2 3
6 0 3
2 4 7
1 1 2
0 2 0
——————
1 1 0 5
(b) 折叠叠加
1 2 3
3 0 6
2 4 7
2 1 1
0 2 0
——————
9 0 7
5、除留余数法
取关键词被某个不大于哈希表表长 “
m
m
m” 的数
p
p
p 除后所得的余数位哈希地址。这个
p
p
p 我们一般选择文件质数或者不小于20的质因数的合数。即:
H
a
s
h
(
k
e
y
)
=
k
e
y
m
o
d
p
,
p
<
=
m
Hash(key)=key\mod p, p<=m
Hash(key)=keymodp,p<=m
这是最简单也是最常用的构造哈希函数的方法。它不仅可以对关键字取模,还可以在折叠、平方取中等运算后取模。
什么是哈希表?
哈希表和我们普通的表类似,不同的是他的数组下标是经过哈希函数映射后的输出值(哈希值)。
什么是哈希冲突?
由于哈希的本质就是将一个输入控件较大的值映射导hash空间中一个较小的值,那么就会出现两个不同的输入值被映射到了同一个较小的输出值。当一个新插入的值被哈希函数映射到了哈希表中一个已经被占用的槽,就认为产生了 Hash 冲突(也叫Hash碰撞)。
哈希冲突只能尽可能的减少,不可能避免。
由于哈希函数的原理是将输入空间一个较大的值映射到一个较小的 Hash 空间内,而 Hash空间一般远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成同一输出的情况。
何为抽屉原理?
桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是“抽屉原理”。抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理
什么是装填因子?
在评价查找操作时,我们引入了一个平均查找长度ASL(Average Search Length)的概念。同样的我们也需要一个指标来衡量哈希表的装填程度(填满程度):装填因子(load factor)
α
=
n
/
m
\alpha = n/m
α=n/m
其中
m
m
m 表示哈希表的槽位数,
n
n
n 表示插入到哈希表中的关键字的数目。
直观的讲, α \alpha α 越小,冲突发生的可能性越小; α \alpha α 越大,表示哈希表中已填入的记录越多,再填入记录发生冲突的可能性就越大。
后边分析冲突不同解决方案时会用到。
怎么解决哈希冲突?
- 链地址法
- 开放地址法
- 再哈希
详细请参考:图解:什么是哈希? (qq.com)
哈希算法有哪些应用?
哈希算法在日常生活中的应用非常普遍,包括信息摘要(Message Digest),密码校验(Password Verification),数据结构(编程语言),编译操作(Compiler Operation),Rabin-Karp 算法(模式匹配算法),负载均衡等等。
当你注册一个网站在客户端输入邮箱和密码时,客户端会对用户输入的密码进行 hash 运算,然后在服务端的数据库中保存用户密码的 Hash 值,从而保护用户的数据。
C++ 当中的 unordered_set & unordered_map
,以及 Java 中 HashSet & HashMap
和 Python 中的 dict
等各种编程语言均实现了哈希表数据结构。
Rabin-Karp 算法利用哈希来查找字符串的任意一组模式,并且可以用来检查抄袭。
可以利用一致性哈希解决负载均衡问题。
同样可以用于版本校验、防篡改、密码校验,此外还广泛应用于各类数据库当中(包括MySQL、Redis等等)。
关于哈希算法应用的详细内容大家可以看看参考资料[2]。
参考资料
[3] 严蔚敏老师的《数据结构(C语言)》版