哈希表(数据结构11章——读书笔记)


哈希表是一种数据结构,它可以提供快速的插入操作和查找操作。
哈希表不仅速度快,编程实现也相对容易。
哈希表的缺点:
它是基于数组的,数组创建后难于扩展。某些哈希表被基本填满时,性能下降的非常严重,所以程序员必须清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。
而且也没有一种简便的方法可以以任何一种顺序(例如从小到大)遍历表中数据项。如果需要这种能力,就只能选择其他数据结构。

如果不需要有序遍历数据,并且可以提前预测数据量的大小,那么哈希表在速度和易用性方面是无与伦比的。

一. 哈希化简介

重要概念之一:如何把关键字转换成数组下标。
–在哈希表中,这个转换通过哈希函数来完成。然而,对于特定的关键字,并不需要哈希函数:关键字的值可以直接用于数组下标。

关键字作为索引

1.1 有序

公司以职员工号为数组下标,存放职员信息,可以通过直接检索数组下标获取数据。新增项只需插到最后一个数据项的后面。
数组需要比当前数据大一些,为扩展留出空间,但不能指望可以大量扩展数组容量。

1.2 字典

1.3 哈希化

哈希表还在另一个类似的领域得到广泛应用,这就是高级计算机语言的编译器,它们通常用哈希表保留符号表。符号表记录了程序员声明的所有变量和函数名,以及它们在内存中的地址。程序需要快速地访问这些名字,所以哈希表是理想的数据结构。
假设把0-199的数字(用变量largeNumber代表),压缩为0-9的数字(用变量smallNumber代表),smallNumber有10个数,因此变量smallRange为10。这个转换的Java表达式为:
smallNumber=largeNumber % smallRange
这就是一种哈希函数。它把一个大范围的数字哈希成一个小范围的数字。这个小范围对应着数组下标。使用哈希函数向数组插入数据后,这个数组就称为哈希表。

1.4 冲突

把巨大的数字空间压缩成较小的数字空间必然要付出代价,即不能保证,每个单词都映射到数组的空白单元。
假设要在数组中插入单词melioration。通过哈希函数得到了它的数组下标后,发现那个单元已经有一个单词demysify了,因为这个单词哈希化后得到的数组下标与melioration相同。这种情况称为冲突。
冲突的可能性会导致哈希化方案无法实施。
当冲突发生时:
一个方法是通过系统的方法找到数组的一个空位,并把这个单词填入,而不再用哈希函数得到的数组下标。这个方法叫做开放地址法
第二种方法是创建一个存放单词链表的数组,数组内不直接存储单词。这样,当发生冲突时,新的数据项直接接到这个数组下标所指的链表中。这种方法叫做链地址法

二. 开放地址法

在开放地址法中,若数据不能直接放在由哈希函数计算出来的数组下标所指的单元时,就要寻找数组的其他位置。下面要探索开放地址法的三种方法,它们在找下一个空白单元时使用的方法不同。这三种方法分别是线性探测、二次探测和再哈希法。

2.1 线性探测

在线性探测中,线性地查找空白单元。如果5421是要插入数据的位置,它已经被占用了,那么就使用5422,然后是5423,依此类推,数组下标一直递增,直到找到空位。这就叫做线性探测。查找合适单元的过程叫做探测。它们走过的步数称为探索长度。

2.1.1 聚集

哈希表中,一串连续的已填充单元叫做填充序列。增加越来越多的数据项时,填充序列变的越来越长。这叫做聚集。当哈希表变得越来越满时,聚集变得越来越严重,性能下降也会很严重。存取序列最后的单元会非常耗时。
当数据项目数目占哈希表长的一半,或最多到三分之二时,哈希表的性能最好。哈希表在几乎被填满的数组中添加数据项,效率非常低。如果哈希表被完全填满,算法就会停止工作。
因此设计哈希表的关键是确保它不会超过整个数组容量的一半,最多到三分之二。

2.1.2 冲突

根据冲突的位置,查找算法只是沿着数组一个个地查看每个单元。如果在找到目标关键字前遇到一个空位,说明查找失败,不需要再做查找。因为插入算法本应该把这个数据项插在那个空位上。

2.1.3 删除

删除一个数据项不是简单地把某个单元的数据项删除,变成空白。因为在插入操作中,探测过程走过一系列单元,如果在此期间有一个空白单元,查找算法会在走到空白单元时放弃查找,即便最终本可以到达目标单元。
因此要用一个有特殊关键字值的数据项替代被删除的数据项,以此标识此处的数据已不存在。插入操作将在第一个空白单元或者值为特殊关键字的位置插入新数据项。查找操作会把该特殊值作为一个已存在的项,以便跨过它查找后面的数据项。
如果做了很多次删除操作,哈希表就会充满值为特殊关键字的数据项,这使得哈希表效率下降。因此许多哈希表不允许删除操作。如果实现了删除,应该尽量有节制地使用。

2.1.4 是否允许重复值

可以重写查找算法,使它可以找到所有具有相同关键字的项,而不是只找到第一个。然而这需要搜索它遇到的每一个线性序列,即使不存在重复关键字。由于要搜索整个表,所以耗时非常大。在大多数情况下,可能会禁止重复的关键字。

2.1.5 扩展数组

当哈希表变得太满时,一个选择是扩展数组。在Java中数组有固定的大小,而且不能扩展。编程时只能另外创建一个新的更大的数组,然后把旧的数组中所有内容重新插入到新的数组中。
注意,哈希函数根据数组大小计算给定数据项的位置,所以这些数据项插入新数组时,不能再放在原数组中相同的位置上。因此不能简单的拷贝数据,需要按顺序遍历原数组,用插入法向新数组中插入每个数据项。这叫做重新哈希化。这是一个耗时的过程,但如果数组要进行扩展,这个过程就是必要的。
扩展后的数组容量通常是原来的2倍。实际上,因为数组容量应该是一个质数,所以新数组要比两倍的容量多一点。计算新数组的容量是重新哈希化的一部分。

2.2 二次探测

前面已经看到,在开放地址法的线性探测中会发生聚集。一旦聚集形成,它会变得越来越大。聚集越大,它增长的也越快。
就像人群,当某个人在商场晕倒,人群就慢慢聚集。人群聚集的越大,吸引的人就会越多。
已填入哈希表的数据项和表长的比率叫做装填因子。有10000个单元的哈希表填入6667个数据后,它的装填因子是2/3。
loadFactor=nItems / arraySize;
当装填因子不太大时,聚集分布的比较连贯。哈希表的某个部分可能包含大量的聚集,而另一个部分还很稀疏。聚集降低了哈希表的性能。
二次探测是防止聚集产生的一种尝试。思想是探测相隔较远的单元,而不是和原始位置相邻的单元。

2.2.1 步骤是步数的平方

在线性探测中,如果哈希函数计算的原始下标是x,线性探测就是x+1,x+2,x+3,依此类推。而在二次探测中,探测的过程是x+1,x+4,x+9,x+16,x+25,依此类推。到原始位置的距离是步数的平方:x+12,x+22,x+32,x+34,x+35,等等。
探测序列每增加一步,跨国的单元就越多。如果序列太长,步长最终会超过整型变量的范围,从而导致方法停止工作。

2.2.2 二次探测的问题

二次探测消除了在线性探测中产生的聚集问题。这种聚集叫做原始聚集。然而,二次探测产生了另外一种,更细的聚集问题,之所以会发生,是因为所有映射到同一个位置的关键字在寻找空位时,探测的单元都是一样的。
比如将182,302,420和544依次插入表中,它们都映射到7。那么302需要以1为步长的探测,420需要以4为步长的探测,544需要以9为步长的探测。只要有一项,其关键字映射到7,就需要更长步长的探测。这个现象叫做二次聚集。
二次聚集不是一个严重的问题,但是二次探测不会经常使用,因为还有稍微好些的解决方案。

2.3 再哈希法

为了消除原始聚集和二次聚集,可以使用另外的一个方法:再哈希法。二次聚集产生的原因是,二次探测的算法产生的探测序列步长总是固定的:1,4,6,9,16,依此类推。
现在需要的一种方法是产生一种依赖关键字的探测序列,而不是每个关键字都一样。那么,不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。
方法是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长。对指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。
经验说明,第二个哈希函数必须具备如下特点:

  • 和第一个哈希函数不同
  • 不能输出0(否则,将没有步长:每次探测都是原地踏步,算法将陷入死循环)。

专家们已经发现以下形式的哈希函数工作的非常好:
stapSize=constant * (key % constant);
其中,constant是质数,且小于数组容量。
不同的关键字,可能映射到相同的数组下标,但是,它们会(很有可能)产生不同的步长。

2.3.1 表的容量是一个质数

再哈希法要求哈希表的容量是一个质数。为了考察为什么会有这个限制,假设表的容量不是质数。例如,假设表长是15(下标从0到14),有一个特定关键字映射到0,步长为5.探测序列是0,5,10,0,5,10,依此类推,一直循环下去。算法只尝试这三个单元,所以不可能找到某些空白单元,例如位置1,2,3或其他位置。算法最终会导致崩溃。
如果数组容量是13,即一个质数,探测序列最终会访问所有单元。即0,5,10,2,7,12,4,9,1,6,11,3,一直下去。只要表中有一个空位,就可以探测到它。用质数作为数组容量使得任何数想整除它都是不可能的,因此探测序列最终会检查所有单元。
类似的影响在二次探测中也存在。然而由于每步的步长都在变化,且最终会超出变量的范围,所以避免了无限的循环。
使用开放地址策略时,探测序列通常用再哈希法生成。

三. 链地址法

开放地址法,是通过在哈希表中再寻找一个空位解决冲突问题。另一个方法是在哈希表每个单元中设置链表。某个数据项的关键字值还是像通常一样映射到哈希表的单元,而数据项本身插入到这个单元的链表中。其他同样映射到这个位置的数据项只需要加到链表中;不需要在原始的数组中寻找空位。
链地址法在概念上比开放地址法中的几种探测策略要简单。然而,代码会比其他的长,因为必须要包含链表机制,这就要在程序中增加一个类。

3.1 装填因子

链地址方法中的装填因子与开放地址法的不同。在链地址法中,需要在有N个单元的数组中装入N个或更多的数据项;因此,装填因子一般为1,或比1大。这没有问题,因为某些位置包含的链表中包含两个或两个以上的数据项。
当然,如果链表中有许多项,存取时间就会变长,因为存取特定数据项平均需要搜索链表的一半的数据项。找到初始的单元需要O(1)的时间级,而搜索链表的时间与M成正比,M为链表包含的平均项数。即O(M)的时间级。因此,并不希望链表太满。
在开放地址法中,当装填因子超过三分之一或三分之二后,性能下降的很快。在链地址法中,装填因子可以达到1以上,且对性能影响不大。因此链地址法是更健壮的机制,特别是当事先难以确定哈希表中要存储多少数据时更是如此。

3.2 重复值

这里允许重复值,在填入过程中可以填重复出现的值。所有相同关键字值的项都放在同一链表中。所以如果需要找到所有项,不管查找是否成功,都要搜索整个链表。这会使性能略微下降。

3.3 删除

链地址法中,删除并没有像开放地址法那样的问题。算法找到正确的链表,从链表中删除数据项。因为不需要探测,无所谓链表中的某一项是否为空。

3.4 表的容量

如果用链地址法的话,表容量是质数的要求就不像在二次探测和再哈希法中显得那么重要。这里没有探测,所以不需要担心探测会因为表容量能被步长整除而陷入无限循环中。

3.5 桶

另一种方法类似于链地址法,它在哈希表的每个单元中使用数组,而不是链表。这样的数组有时称为桶。然而,这个方法不如链表有效,因为桶容量不好选择。如果桶容量太小,可能会溢出。如果太大,又浪费空间。链表是动态分配的,所以没有这个问题。

四. 哈希函数

4.1 快速的计算

好的哈希函数很简单,所以它能快速计算。哈希表的主要优点是它的速度。如果哈希函数运行缓慢,速度就会降低。哈希函数中有许多乘法和除法是不可取的。
哈希函数的目的是得到关键字值的范围,把它用一种方式转化成数组的下标值,这种方法应该使关键字值随机的分布在整个哈希表中。关键字可能完全随机,但也有可能不那么随机。

4.2 随机关键字

所谓完美的哈希函数把每个关键字都映射到表中不同的位置。只有在关键字组织得异乎寻常得好,且它得范围足够小,可以直接用于数组下标的时候,这种情况才可能出现。
大多数情况下,这种情况不会发生,哈希函数需要把大较大的关键字值范围压缩成较小的数组下标的范围。
在特定的数据库中,关键字值的分布决定哈希函数需要做什么。
如果,关键字真的是随机的,得到的下标也是随机的,就会有良好的分布情况。

4.3 非随机关键字

然而,数据通常不是随机分布的。

4.3.1 不要使用无用数据

压缩关键字字段时,要把每个位都计算在内。舍弃无用数据。各种调整位的技术都可以用来压缩关键中的不同字段。

4.3.2 使用所有的数据

关键字的每个部分(除了无用数据)都应该在哈希函数中有所反映。不要只使用头四位数字,或类似的删除其他位的情况。关键字中提供的数据越多,哈希化后,越可能覆盖整个下标范围。
总结:关于哈希函数的窍门是找到既简单又快的哈希函数,而且要去掉关键字中的无用数据,并尽量使用所有的数据。

4.3.3 使用质数作为取模的基数

通常,哈希函数包含对数组容量的取模操作。前面已经看到,使用二次探测和再哈希法时,要求数组容量为质数是多么的重要。然而,如果关键字本身不是随机分布的,不论使用什么哈希化系统,都应该要求数组容量是质数。
这个论述是正确的,因为如果许多关键字共享一个数组容量作为除数,它们会趋向于映射到相同的位置,这会导致聚集。使用质数,可以消除这种可能性。
所以,应该仔细检查关键字,并修正哈希算法,删除任何关键字分布不规则的地方。

4.4 哈希化字符串

Horner方法:
var4n4+var3n3+var2n2+var1n1+var0n0
可以写成下面的形式:
(((var4
n+var3)*n+var2)*n+var1)*n+var0
在Horner公式的每一步中,都可以应用取模操作符(%)。这和最后只应用一次取模操作符的结果是一样的,但是避免了溢出。
这种方法或类似的方法通常用来哈希化字符串。可以使用这个方法把任意长度的字符串转换成适当的数字。字符串可以是单词、名字或其他字符的组合。

4.5 折叠

另一个哈希函数是把关键字分为几组,然后几个组相加。这样做确保了所有数字对哈希值都有贡献。一个组拥有几个数字,是和数组容量相对应的。即,对于有1000项的数组,每组包含3个数字。
例如,假设要哈希化一个9位数的社会安全号码,使用线性探测。如果数组容量是1000项,就把9位数分成三组。如果某个号码是123-45-6789,那么按公式123+456+789=1368计算关键字。可以使用取模操作符截短这个和,使得最大数组下标是999。在这里1368 % 1000=368。如果数组容量是100,就需要把9位数分成4个2位数和1个1位数的组:12+34+56+78+9=189,然后189 % 100=89。
当数组容量是10 的倍数时,容易考虑这个过程如何进行。然而,要得到最好的结果,它应该是质数,正如其他的哈希函一样。

五. 哈希化的效率

已经注意到在哈希表中执行插入和搜索操作可以达到O(1)的时间级。如果没有发生冲突,只需要使用一次哈希函数和数组的引用,就可以插入一个新数据项或找到一个已存在的数据项。这是最小的存取时间级。
如果发生冲突,存取时间就依赖后来的探测长度。在一次探测中,每个单元的存取时间要加上寻找一个空白单元(插入时)或一个已存在单元的时间。在一次存取中,要检查这个单元是不是空的,以及(查找或删除时)这个单元是不是包含要寻找的数据项。
因此,一次单独的查找或插入时间与探测的长度成正比。这里还要加上哈希函数的常量时间。
平均探测长度(以及平均存取时间)取决于装填因子。随着装填因子变大,探测长度也会越来越长。

5.1 开放地址法

随着装填因子变大,效率下降的情况,在不同开放地址法的方案中比链地址法中更严重。
开放地址法中,不成功查找比成功的查找花费更多的时间。在探测序列中,只要找到要查找的数据项,算法就能停止,平均起来,这会发生在探测序列的一半。另一方面,要确定算法不能找到这样的项,就必须走过整个探测序列。
实际情况中,最好的装填因子取决于存储效率和速度之间的平衡。随着装填因子变小,存储效率下降,而速度上升。对于较高的装填因子,对比线性探测,二次探测和再哈希法还是可以忍受的。

5.2 链地址法

对于有序链表,在不成功查找中只需要检查一半的数据项,所以时间与成功查找的时间相同。
在链地址法中,通常装填因子为1(数据项的个数和数组容量相同)。较小的装填因子不能显著地提升性能,但是,所有操作的时间都会随着装填因子的变大而增长,所以不宜把装填因子提升到2。

5.3 开放地址法和链地址法的比较

如果使用开放地址法,对于小型的哈希表,再哈希法似乎比二次探测的效果好。有一个情况例外,就是内存充足,并且哈希表一经创建,就不再改变其容量;在这种情况下线性探测相对容易实现,并且,如果装填因子低于0.5,几乎没有什么性能下降。
如果在哈希表创建时,要填入的项数未知,链地址法要好过开放地址法。如果用开放地址法,随着装填因子变大,性能会下降很快,但是用链地址法,性能只能线性地下降。
当两者都可选时,使用链地址法。它需要使用链表类,但回报是增加比预期更多的数据时,不会导致性能的快速下降。

六. 哈希化和外部存储

6.1 文件指针表

外部哈希化的关键部分是一个哈希表,它包含块成员,指向外部存储器中的块。哈希表有时叫做索引。它可以存储在内存中,如果它太大,也可以存储在磁盘上,而把它的一部分放在内存中。即使把哈希表都放在内存中,也要在磁盘中保存一份备份,文件打开时,把它读入内存。

6.2 未填满块

在外部哈希化中,重要的是块不要填满。
所有关键字映射为同一个值的记录都定位到相同块。为找到特定关键字的一个记录,搜索算法哈希化关键字,用哈希值作为哈希表的下标,得到某个下标中的块号,然后读取这个块。

6.3 填满的块

即使用一个好的哈希函数,块偶尔也会填满。这时,可以使用在内部哈希表中讨论的处理冲突的不同方法:开放地址法和链地址法。
在开放地址法中,插入时,如果发现一个块是满的,算法在相邻的块插入新记录。在线性探测中,这是下一个块,但也可以用二次探测或再哈希法选择。在链地址法中,有一个溢出块:当发现块已满时,新记录插在溢出块中。
填满的块是不合需要的,因为有了它就需要额外的磁盘访问,这就需要两倍的访问时间。然而,如果这种情况不经常发生,也可以接受。

来源:《数据结构》

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值