哈希表
哈希表支持一种最有效的检索方法: 散列。
从根本上说,一个哈希表包含一个数组,通过特殊的索引值(键)来访问数组中的元素。
哈希表的主要思想是通过一个 哈希函数,在所有可能的键与槽位之间建立一张映射表。哈希函数每次接受一个键将返回与键相对应的哈希编码或哈希值。键的类型可能多种多样,但哈希值的类型只能是整型。
由于计算哈希值何在数组中进行索引都只消耗固定的时间,因此哈希表的最大亮点在于: 它是一种运行时间在常量级的检索方法。
在理想状态,哈希函数能够保证不同的键生成的哈希值互不相同。但是在实际运用过程中,能够直接寻址的结果非常少。绝大多数哈希函数会将一些不同的键映射到相同的槽位上,当两个键映射到同一个槽位时,它们就产生了冲突。一个好的哈希函数能最大限度地减少冲突。但冲突不可能完全消除。
两种哈希表(解决冲突的方式不同):
- 链式哈希表:将数据存储在“桶”(bucket)中的哈希表。每个“桶”都是一个哈希表,且链表的容量能够随着冲突的增加而增大。
- 开地址哈希表:将数据存储在表中,,而不是“桶”中的哈希表,它通过各种它探查方法来避免冲突问题
哈希算法的核心问题:选择哈希函数。 将键随机地分散到表中,使冲突最小化。
哈希表的应用
- 数据库系统:也就是追求高效的随机访问的地方。数据库系统主要有两种优化数据访问的方法:随机访问和顺序访问。哈希表是高效的随机访问方法的一个重要组成部分,因为它能在固定时间内定位数据。
- 符号表:符号表是编译器用来维护程序中符号信息的表。编译器会频繁地访问符号信息,因此,用更为高效的方法来实现符号表是非常重要的。
- 数据字典:一种支持数据的添加、删除和检索的数据结构。
- 关联数组
链式哈希表
链式哈希表从根本上来说是由一组链表组成。
每个链表看做是一个“桶”,我们将所有的元素通过散列的方式放到具体的不同的桶中。插入元素时,首先将其键传入一个哈希函数,函数通过散列的方式告知元素属于哪个“桶”,然后在其相应的链表头插入元素。查找或删除元素时,也是以相同的方式,先找到属于哪个“桶”,然后遍历相应的链表找到该元素。每个“桶”都是一个链表,所以链式哈希表并不限制包含元素的个数。然而如果表变得太大,它的性能将会降低。
链式哈希表解决冲突的方法
哈希表中两个键散列到一个相同的槽位时,这两个键就会产生冲突。链式哈希表解决冲突的办法:当冲突发生时,他就将元素放入已经准备好的“桶”中。但是也会带来一个问题:当过多的冲突发生在同一个槽位时,此位置的“桶”将会变得很深,从而造成访问此位置的元素所需的时间越来越长。
我们的目标就是尽可能均匀和随机地分配表中的元素。这种情况在理论上称为均匀散列。
如果想插入表中的元素数量远大于表中“桶”的数量,那么即使是均匀散列,其性能也会迅速降低。因此我们必须要注意一个哈希表的负载因子,其定义为:
负载因子α=表中元素个数n / “桶”的个数m
在均匀散列的情况下,负载因子能告诉我们表中“桶”能装下元素个数的最大值。
选择哈希函数
一个好的哈希函数旨在近似均匀散列。也就是尽可能以均匀和随机的方式散布一个哈希表中的元素。定义一个哈希函数h,它将键映射到哈希表中的位置x,x称为k的哈希编码,正式的表述为:
h(k)=x
取余法
有一个整型键k,一种最简单地将k映射到m槽位的散列方法是计算k除以m所得到的余数。取余法表达式:
h(k) = k mod m
乘法
乘法将整型键k乘以一个常数A(0<A<1),取结果的小数部分,然后乘以m取结果的整数部分。通常情况A取0.618,表达式:
h(k) = 取整(kA mod 1),A=0.618
开地址哈希表
在链式哈希表中,元素存放着每个地址的“桶”中。而在开地址哈希表中,元素存放在表本身,这种特性对于某些依赖于固定大小表的应用来说非常有效。
开地址哈希表冲突解决方法
在开地址哈希表中解决方式就是探查这个表,直到找到一个可以放置元素的槽,例如要插入一个元素,我们探查槽位直到找到空槽,然后将元素插入槽中。如果要删除或查找一个元素,我们探查槽位直到定位到该元素或直到找到一个空槽位。如果在找到元素之前找到一个空槽位或遍历完所有槽位,那么说明此元素不存在。
究竟经过多少次探查就停止,取决于两件事:
- 哈希表的负载因子
- 元素均匀分布的程度
负载因子α=元素个数/槽位个数,根据开地址哈希表的定义,它所包含的元素不可能大于表中槽位的数量(n>m),所以通常开地址的哈希表的负载因子通常小于或等于1.
假设进行均匀散列,我们能在一个开地址哈希表中探查的槽位个数是:
1/(1-α)
例如,对于一个半满状态的开地址哈希表来说(其负载系数为0.5),我们希望能探查的槽位个数为1/(1-0.5)=2,当负载因子趋近于1时,我们期望探查的槽位数量显著增大。在一个对时间敏感的应用中,就可以通过增加哈希表的空间来提高探查的效率。
开地址哈希表的哈希函数定义:
h(k,i) = x
k是键,i是到目前为止的探查次数。x是得到的哈希编码
线性探查
优点是简单,它对m没有限制,可以保证所有槽位都被探查到。但是线性探查并不能近似均匀散列,特别是当遇到一种称为基本聚集的情况时,基本聚集会产生很长的探查序列,从而使表变得越来越大,会降低标的性能。
双散列探查
最有效的探查方法之一,通过两个辅助哈希函数的和来得到哈希编码。优点是能够在表中探查并产生较好的元素分布,缺点是要限制m的值。
一个开哈希表的本质来说是一个数组构成。
1.在链式哈希表中,用于访问哈希表的哈希编码实际上是对表取模后的值,这是为什么?
进行这种转换是为了确保哈希编码不会处于表的末尾。虽然哈希函数保证这一点。在开地址哈希表进行双散列时,取模的原因是为了产生的哈希编码不超过表的边界。因为开地址哈希表的哈希编码是两个辅助哈希函数产生的哈希编码相加得到的。
2.为什么哈希表适用于随机访问,不适用于顺序访问?
哈希表是通过哈希函数,输入键,输出哈希编码,散列的每个键所得到的结果恰恰是我们在表中访问的数据,后者发生冲突时,多进行查找几次也能找到。**哈希表不支持顺序访问。**在散列到某个槽之后,我们也没法确定下一个键所处的槽位。
3. 在链式哈希表中查找元素时,什么是最坏的性能,如何预防?
当所有的元素都散列到一个“桶”时,链式哈希表的性能最差。这种情况下,查找一个元素的复杂度是O(n),其中,n是表中元素的个数。产生这种情况的哈希函数是h(k)=c,c是哈希表范围内的常数。我们要选择一个好的哈希函数,确保近似于均匀散列的分布。
4.在开地址哈希表中查找元素时,什么是最坏的性能,如何预防?
在开地址表中查找一个元素时,如果表已经满了,而且要查找的元素不在表中,此时性能最差。负载度为O(m),m为表中槽位的个数。这种情况在任何哈希表中都有可能发生,为了保证有较好的性能,通常不会让元素的个数超过表容量的80%,还要选择一个近似均匀散列的哈希函数,期望在一段固定的时间内找到元素。