算法预备军(5)~散列表

362 阅读8分钟

散列表又称为Hash表,核心体现在Hash算法上,而Hash算法又是加密算法的一种,所以我们很有必要去了解一下散列表。

一些概念

我们看一个公式:存储位置=f(关键字),我们将根据这个公式来理解散列技术与散列表的概念。

散列技术:散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。

哈希函数:我们把上述公式中的对应关系f称为散列函数,又称为哈希函数。

散列表(哈希表):采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表,关键字对应的记录存储位置我们称为散列地址。

散列技术既是一种存储方法,也是一种查找方法,不过有一点需要注意的是,散列技术的记录之间不存在什么逻辑关系,它只与关键字有关联。虽说散列技术既是存储技术又是查找技术,但是散列主要是面向查找的存储结构。

散列技术最适合的求解问题是查找与给定值相等的记录。不适合求解一个关键字对应多个记录的问题,也不适合查找范围问题,最大值问题。

哈希冲突:在理想情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,但是这只是理想情况。我们时常会碰到两个关键字key1!=key2,但是却有f(key1)=f(key2),这种现象我们称为冲突,并把key1和key2称为这个散列函数的同义词。在Java的集合框架中,HashMap、Hashtable、ConcurrentHashMap都通过拉链法来解决的冲突。

散列技术的关键之处在于如何设计一个简单、均匀、存储利用率高的散列函数。

散列函数的好坏我们可以根据两个原则来衡量:(1)计算是否简单(2)散列地址是否分布的均匀。本来我们使用散列函数就是为了减少查找所需的时间,如果计算哈希值的过程复杂,耗费的时间较长,就失去了散列函数的意义。散列地址的均匀分布是为了减少冲突的发生,如果发生冲突,处理冲突也要花一定的时间。

常用的散列函数的构造方法

常用的散列函数的构造方法有:直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法。

直接定址法:我们可以取关键字的某个线性函数值为散列地址,即f(key)=a*key+b(a,b为常数,注意a不能为0)。这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。

数字分析法:数字分析法是指抽取关键字的一部分来计算散列存储位置的方法,如散列手机号码,根据手机号的特点,我们可以选取后四位来做散列运算。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。

平方取中法:这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用作散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。

折叠法:折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。123/456/789/0
,将它们求和为1368,再求后3位得到散列地址为368。折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。

除留余数法:此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:f(key)=key mod p(p<=m),mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。很显然,本方法的关键就在于选择合适的p,p如果选择不好,就可能会容易产生同义词。根据前辈们的经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

随机数法:选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key)=random(key)。这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

现实中,应该视不同的情况采用不同的散列函数。这里只能给出一些考虑的因素来提供参考:
1.计算散列地址所需的时间
2.关键字的长度
3.散列表的大小
4.关键字的分布情况
5.记录查找的频率
综合这些因素,才能决策选择哪种散列函数更合适。

处理散列冲突的方法

处理散列冲突的方法常用的有:开放定址法、再散列函数法、链地址法、公共溢出区法。

开放定址法:所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。它的公式为: fi(key)=(f(key)+di) MOD m。根据d的取值方式可以有线性探测法、二次探测法、随机探测法等。
当d=1,2,3,4....,m-1时,我们称上述公式解决冲突的开放定址法为线性探测法。
当d=11,-11,22,-22,...,qq,-qq,q<=m/2时,我们称上述公式解决冲突的开放定址法为二次探测法,这里增加平方运算的目的是为了不让关键字都聚集在某一块区域。
当d为采用随机函数计算得到时,我们称上述公式解决冲突的开放定址法为随机探测法。
在用开放定址法处理冲突时,会遇到堆积现象。堆积是指本来不是同义词的关键字却需要争夺同一个地址的情况。总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,时我们常用的解决冲突的办法。

再散列函数法:再散列函数法是指我们事先准备多个散列函数,当发生散列地址冲突时,就换一个散列函数计算,相信总会有一个可以把冲突解决掉。这种方法能够使得关键字不产生聚集,当然,相应地也增加了计算的时间。其公式如下:
fi(key)=RHi(key)(i=1,2,...,k),这里RH就是不同的散列函数,可以将前面说的折叠、平方取中等方法都用上。

链地址法:读过HashMap源码的同学肯定对这种方法很熟悉,链地址法是在冲突的位置上引入单链表来解决冲突的,这种方法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链表的性能损耗。

公共溢出区法:为所有冲突的关键字建立一个公共的溢出区来存放,在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果查找不相等,则到溢出表去进行顺序查找。

散列表的平均查找长度取决于装填因子,而不是取决于查找集合中的记录个数。所谓的装填因子a=填入表中的记录个数/散列表长度。a标志着散列表的装满的程度。当填入表中的记录越多,a就越大,产生冲突的可能性就越大。

这个系列在第一篇文章里就说了,内容是针对《大话数据结构》的笔记。转载请注明出处:blog.csdn.net/android_jia…