一般的查找,可以通过挨个比较:字符 x 中的字母是否与表中的某个字符 a[i] 匹配。
)更方便一点的方法是二分查找,可以在有序表中,通过比较表中间位置 a[(0+len)/2] 与 x 的大小关系,接下来找它所属的部分,再用同样的二分查找,接着快速找到它的位置。
这样看来,比较是不可避免的,可不可以直接通过 x 的关键字来查找呢?
散列表查找定义
可以通过查找关键词不需要比较就可以获得需要记录的存储位置。这就是一种新的存储技术——散列技术。散列技术既是一种存储方法,也是一种查找方法。
散列技术是在记录的存储位置和它的关键词之间建立一个确定的对应关系 f,使得每个关键字 key 对应一个存储位置 f(key)。查找是,根据这个确定的对应关系找到给定值 key 的映射 f(key),若查找集合中存在这个记录,则必定在 f(key)的位置上。
这里的对应关系 f 成为散列函数,又称为哈希(Hash)函数。按这个思想,采用散技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列地址。
散列表查找步骤有两步:存储和查找。
散列函数
一个好的散列函数:首先要计算简单,如果保证所有的关键字都不产生冲突,会需要复杂的计算,也会消耗很多时间,但是如果需要频繁地查找,就会大大降低查找的效率了;然后是散列地址分布均匀,尽量让散列地址均匀地分布在存储空间中,这样可以有效理由存储空间,减少为处理冲突而消耗的时间。
实际上要根据不同的情况采用不同的散列函数,可以根据以下因素作为参考:
(1)计算散列地址所需的时间
(2)关键字的长度
(3)散列表的大小
(4)关键字的分布情况
(5)记录查找的频率
直接定址法
对于 0~100 岁的人口数字统计表,年龄这个关键字就可以直接用年龄的数字。此时 f(key)=key.
如果要统计的是 1980 年后出生年份的人口数,可以对出生年份减去 1980 来作为地址。此时 f(key)=key-1980.
也就是,可以取关键字的某个线性函数值作为散列地址,即: f(key)=a * key + b.
数字分析法
如果关键字是数字较多的数字,比如是我们的 11 位手机号 “ 123 1234 1234 ”,其中前三位是不同运营商的子品牌,中间四位是 HLK 识别码,表示用户号的归属地;后四位是真正的用户号,如果现在要用手机号作为关键字,前 7 位作为关键字可能会出现相同的情况,而后 4 位重复的几率小很多。如果还是容易出现冲突问题,还可以对抽取的数字进行反转(如 1234 改成 4123)、左环位移、甚至前两数与后两数叠加(如 1234 改为 12+34=46)等方法。总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各位置。
平方取中法
假设关键字是 1234,它的平方就是 1522756,抽取中间 3 位就是 227,用作散列地址;如果关键字是 4321,它的平方就是 18671041,再抽取中间的 3 位就是可以是 671,也可以是 710,用作散列地址。
这种方法比较适合用于不知道关键字的分布,而位数不是很多的情况。
折叠法
是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时,可以短些),然后将这几部分叠加求和,并按散列表表长,取最后几位作为散列地址。
比如关键字是 9876543210,散列表表长为 3 位,我们将它分为 4 组,987|654|321|0,然后将它们叠加起来求和 987+654+321+0=1962,再求后三位得到散列地址为 962.
有时可能这还不能够保证分布均匀,不放从一端向另一端来回折叠后对齐相加。比如我们将 987 和 321 反转,再与 654 和 0 相加,变成 789+654+123+0=1566,此时散列地址为 566.
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
除留余数法
对于散列表长为 m 的散列函数公式为:f(key)% p (要满足 p<=m)
不仅可以对关键词直接取模,也可以折叠、平方之后再取模。
要选取合适的 p,若是 p选择得不好,就会产生同义词。
根据经验,若散列表长为 m,通常 p 为小于或等于表长(最好接近 m)的最小质数不包含小于 20 质因子的合数。
随机数法
选择一个随机数,去关键字的随机函数值为它的散列函数,也就是 f(key)= random(key)。这里 random 是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
但是如果关键字是字符串该怎么处理?其实无论是英文字符还是中文字符,也包括各种各样的符号,它们都可以转化为某种数字来对待,比如 ASCII 码值或者 Unicode 码等。
处理散列冲突的方法
在使用散列函数后发现两个关键字 key1 != key2,但是却有 f(key1)= f(key2),在有冲突时,怎么办呢?
开放定值法
所谓开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
它的公式是:
(key)=(f(key)+ )% m
( =1,2,3,4,5,…,m-1)
比如说,我们的关键字集合为 {12,67,56,16,25,37,22,29,15,47,48,34},表长为 12。我们用散列函数 f(key)= key % 12.
当计算前 5 个数 {12,67,56,16,25} 时,都是没有冲突的散列地址,直接存入,如图所示:
计算 key=37 时,发现 f(37)=1,此时与 25 所在的位置冲突,于是我们应用上面的公式 f(37)= ( f(37)+1)% 12 = 2;
于是将 37 存入下标为 2 的位置,接下来的 22,29,15,47 都没有冲突,如下图:
到了 key = 48,计算得到 f(48)=0,与 12 所在的位置冲突了,于是 f(48)=(f(48)+2)% 12=2,还是冲突,一直到 f(48)=(f(48)+6)% 2 时才有空位,接着存入。
这种解决冲突的开放定址法称为线性探测法。
在解决冲突时,碰到如 37 和 48 这种本来都不是同义词,却需要争夺一个地址的情况,这种叫做堆积。
到了最后一个 key=34,f(key)=10,与 22 所在的位置冲突,可是 22 后面没有空位置了,反而在前面有一个位置,尽管可以不断地取余数得到最后的结果,但是效率很低。所以可以改进为: = ,-, ,-,…,,-(q<=m/2),这样可以双向找到空位置。
增加平方运算的目的是为了不让关键字都聚集在某一块区域。这个被称为二次探测法。
(key)=(f(key)+)% m
( = ,-, ,-,…,,-(q<=m/2))
还有一种解决冲突的方法是随机探测法。
我们设置随机的种子,则不断调用随机函数可以生成不会重复的数列,在查找时可以使用相同的随机种子,它每次得到的数列是相同的,相同的 当然可以得到相同的散列地址。
(key)=(f(key)+)% m
(是一个随机数列)
总之,在散列表未填满时,开放定址法总是能找到不发生冲突的地址,是我们常用的解决冲突的方法。
再散列函数法
我们可以把前面说的除留余数法、折叠、平方取中全部用上,每当发生冲突时,就换一个散列函数计算,总有一个可以解决冲突。(这个方法可以使关键词不聚集,但是会增加计算的时间)
链地址法
发生了冲突不一定要换地方,可以直接在原地想办法,将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。对于关键字集合{12,67,56,16,25,37,22,29,15,47,48,34},我们可以用前面的 12 为除数,进行除留取余法,可以得到下图:
公共溢出区法
凡是冲突的单独建立一个公共的溢出区来存放。就前面的那个例子,一共有三个关键字重复{37,48,34}与之前的关键字位置有冲突,那就把它们存储在溢出表里,如下图: