[数据结构]从散列(Hash)表、哈希函数的构造到解决冲突

一、什么是散列表?

散列表(Hash table,也叫哈希表),是根据关键字码值而直接进行访问的数据结构,也就是通过把关键字码值映射到表中一个位置来访问记录,以加快查找的速度。
哈希函数也叫做散列函数,是将记录的关键字值与记录的存储位置对应起来的关系f,f(关键字)的结果称位哈希地址。

哈希地址(记录的存储位置)= f(关键字),这里对应的关系就叫哈希函数,也可叫散列函数。

在运用散列表来解决问题时,我们务必考虑好下面因素:

(1) 设计一个简单、均匀、存储效率高的散列函数是散列技术中最关键的问题。
(2)冲突问题:在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是一个理想。但是我们经常会碰到两个关键字k1=k2,但是却有f(k1)=f(k2),这种现象我们称为冲突,并把k1和k2称为这个散列函数的同义词。

散列表的应用:
(1)主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
(2)查找:当我们知道key值就可以知道关键字对应的位置。

二、哈希函数的构造方法

构造散列函数的目标是使散列地址尽可能均匀地分布在散列空间上,同时使计算尽可能简单,以节省计算时间。
下面让我们一起来了解一下几种常用的散列函数构造方法。
1、直接定址法:取关键字或关键字的某个线性函数为哈希地址

f(key)=key或f(key)=a*key+b

其中a,b为常数,调整a与b的值可以使哈希地址取值范围与存储空间范围一致。
在这里插入图片描述
这样的散列函数优点就是简单、均匀、也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。此法并不常用。
2、数字分析法:对各个关键字的各个码位进行分析,取关键字中某些取值较分散的数字位作为散列地址的方法
我们的手机号前三位是接入号,一般对应不同运营商公司的子品牌,如130是联通如意通、136是移动神州行、153是电信等,中间4位是HLR识别号,表示用户归属地;后四位才是真正的用户号。如果用我们的手机号作为关键字,那么极有可能前7位都是相同的。我们就可以选择后面的四位成为散列地址,这就是抽取,也就是抽取关键字的一部分来计算散列存储位置的方法,这在散列函数中常常用到的手段。
在这里插入图片描述
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑此法。
3、平方取中法:取关键字平方的中间几位作为散列地址的方法
假设关键字为1234,它的平方就是1522756,再抽取中间的3位就是227,也可以是722,用作散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。
4、折叠法:将关键字从左到右分割成位数相等的几部分(最后一部分不够可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如关键字9876543210,分为四组987|654|321|0,叠加求和987+654+321+0=1962,取最后三位962当作散列地址。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
5、除留余数法:选择一个适当的正整数p,用p去除关键字,取所得余数作为散列地址

f(key) = key % p (p<=m),m为散列表长

事实上此法不仅可以直接取模,也可以在折叠、平方取中后再取摸。本方法的关键在于选择合适的p,p如果选的不好很容易产生同义词。
如下面存在5个记录(表长为5)的散列表,选择下标作为哈希地址,p=11,其中只有12和144这两个关键字地址有冲突。如果选择p=12,我们就会出现很多冲突,构造更好地哈希函数就是为了尽可能地减少冲突。p的选择很重要。若散列表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
在这里插入图片描述
除留余数法是最常用地构造散列函数方法。
6、随机数法:选择一个随机数,取关键字的随机函数值为它的散列地址。

f(key) = random(key),其中random是随机函数。

当关键字的长度不等时,采用这个方法构造散列函数比较合适。

综上地,不同情况采用不同的散列函数,我们可以按照以下因素来选择。
(1)计算散列地址所需地时间
(2)关键字的长度。
(3)散列表的大小。
(4)关键字的分布情况。
(5)记录查找的频率。

三、处理散列冲突的方法

了解了除留余数法的例子可以看出,我们设计再好的散列函数也不可能完全避免冲突,既然冲突不能避免,就要考虑如何处理它。
1、开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要数列表足够大,空的散列地址总能找到,并将记录存入。

fi(key) = (f(key)+di) % m, (di=1,2,3,…,m-1)

比如我们的关键字集合{12 ,67 ,51 2,16 ,37,48 },表长为6,f(key)=f(key)%6.
当计算前面的4个数时,都是没有冲突的散列地址。

下标 0 1 2 3 4 5
关键字 12 67 51 16

当key=37时,发现f(key)=1,此时与67所在位置冲突,f(key)=(f(key)+1)%6=2。

下标 0 1 2 3 4 5
关键字 12 67 37 51 16

当key=48时,f(48)=0,与12所在0的位置发生了冲突,当我们用到以上的公式di=1,还是冲突…di=2…直到di=5,也就是(f(48)+5)=5,才没有冲突。

下标 0 1 2 3 4 5
关键字 12 67 37 51 16 48

我们把这种解决冲突的开放定地址称为线性探测法。从这例子看出,48和37本来都不是同义词却要争夺一个地址的情况,我们称这种现象为堆积。这样不断的处理冲突,无论存入还是查找效率都会大大降低。
为了增进,可以改进这个公式:
①增加平方运算的目的是为了不让关键字都聚集在某一块区域,这种方法叫做二次探测法。

fi(key) = (f(key)+di) % m ,(di=pow(1,2),-pow(1,2),pow(2,2),-pow(2,2),…,pow(q,2),–pow(q,2),q<=m/2)

②在冲突时,位移量di采用随机函数计算得到,我们称此法为随机探测法。

fi(key) = (f(key)+di) % m ,(di为一个随机数列)

开放定址法(我们常用的解决冲突的1办法)适用于只要在散列表未填满,总是能找到不发生冲突的地址。
2、再散列函数法
fi(key) = Hi(key) (i=1,2,3,…,k)
Hi就是不同的散列函数,把前面的什么除留余数、折叠…全部用上,当发生冲突时,就换一个散列函数计算,总有一个可以把冲突解决掉,此法使得关键字不聚集,增加了计算时间。
3、链地址法:将关键字为同义词存储在一个单链表中,我们称为这种表为同义词子表。
对于关键字集合{12,67,56,16,25,37,22,29,15,47,48,34},运用除留余数法得到下面的链表:
在这里插入图片描述
此法不存在冲突换地址的问题了,只不过在当前位置增加结点的问题,链地址提供了绝不会出现炸不到地址的保障。但是查找时遍历单链表造成性能的损耗。
4、公共溢出区法:包含两个表:基本表和溢出表,凡是发生冲突的关键字都存储在溢出表中
在这里插入图片描述
此法用于查找时,现在基本表查找,当关键字相等,则查找成功;如果不等就到溢出表进行顺序查找

四、参考资料

1、大话数据结构
2、https://blog.csdn.net/yyyljw/article/details/80903391

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章