[数据结构

一、什么是哈希表?

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系 f,使得每个关键字 key 对应一个存储位置 f(key)。

我们把这种对应关系 f 称为哈希函数(也叫散列函数)。采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表(Hash table,也叫散列表)。关键字对应的记录存储位置我们称为散列地址

整个散列过程其实就是两步:

  • 插入元素时:根据待插入元素的关键码,用哈希函数计算出该元素的存储位置并按此位置进行存放;
  • 查找元素时:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则查找成功。

顺序查找的时间复杂度为 O(N) ,二分查找查找树的时间复杂度为 O(logN),而哈希表的时间复杂度为 O(1) 。不过这只是理想状态,实际并不那么完美。

二、哈希函数的构造方法

根据前人经验,统计出如下几种常用 hash 函数的构造方法:


1. 直接定址法

取关键字的某个线性函数值为散列地址,即:f(key) = a*key + b (a、b为常数)

这样的散列函数优点就是简单、均匀,也不会产生冲突。但问题是这需要事先知道关键字的分布情况适合查找表较小且连续的连续的情况。因此,在现实应用中,此方法虽然简单,但却并不常用


2. 数字分析法

抽取方法是使用关键字的一部分来计算散列存储位置的方法,通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。

比如我们现在要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是相同的。那么我们选择后面的四位成为散列地址就是不错的选择。如果这样的抽取工作还是容易出现冲突问题,还可以对抽取出来的数字再进行反转(如 1234 改成 4321)、右环位移(如 1234 改成 4123)等方法。总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各位置。


3. 平方取中法

关键字平方后再抽取中间几位。比较适合于不知道关键字的分布,而位数又不是很大的情况。

比如关键字是 4321,那么它的平方就是 18671041,抽取中间的 3 位就可以是 671,也可以是 710,用做散列地址。


4. 折叠法

将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址

比如我们的关键字是 9876543210,散列表表长为三位,我们将它分为四组,987|654|321|0,然后将它们叠加求和 987+654+321+0=1962,再求后 3 位得到散列地址为 962。

有时可能这还不能够保证分布均匀,不妨从一端向另一端来回折叠后对齐相加。比如我们将 987 和 321 反转,再与 654 和 0 相加,变成 789+654+123+0=1566,此时散列地址为 566。

折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况


5. 除留余数法

对于散列表长为m的散列函数公式为:f(key)=key mod p(p≤m)

该方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模

本方法的关键就在于选择合适的pp如果选得不好,就可能会容易产生同义词

根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数


6. 随机数法

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

那如果关键字是字符串如何处理?其实无论是英文字符,还是中文字符,也包括各种各样的符号,它们都可以转化为某种数字来对待,比如 ASCII 码或者 Unicode 码等,因此也就可以使用上面的这些方法。


总结

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


三、处理哈希冲突的方法

我们时常会碰到两个关键字key1≠key2,但是却有f(key1)=f(key2),这种现象我们称为哈希冲突(collision),并把key1和key2称为这个哈希函数的同义词


1. 开放定址法

所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

它的公式是:fi(key)=(f(key)+di) MOD m(di=1,2,3,......,m-1)

比如说,我们的关键字集合为 {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表长为 12。我们用散列函数f(key)=key mod 12


2. 再散列函数法

对于我们的散列表来说,我们可以事先准备多个散列函数:fi(key)=RHi(key)(i=1,2,...,k)

这里 RHi 就是不同的散列函数,每当发生散列地址冲突时,就换一个散列函数计算,相信总会有一个可以把冲突解决掉。这种方法能够使得关键字不产生聚集,当然,相应地也增加了计算的时间。


3. 链地址法

将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。

对于关键字集合 {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},我们用前面同样的 12 为除数,进行除留余数法,可得到如下图结构,此时,已经不存在什么冲突换址问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题


链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链表的性能损耗


4. 公共溢出区法

为所有冲突的关键字建立了一个公共的溢出区来存放。

在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表去进行顺序查找。如果对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。


四、哈希表查找性能分析

如果没有冲突,哈希表查找是所有查找中效率最高的,因为它的时间复杂度为O(1)。可惜,冲突是无法避免的。哈希表查找的平均长度取决于以下因素:

1、哈希表是否均匀

哈希函数的好坏直接影响着出现冲突的频繁程度,不过,由于不同的哈希函数对同一组随机的关键字,产生冲突的可能性是相同的,因此我们可以不考虑它对平均查找长度的影响

2、处理冲突的方法

相同的关键字、相同的哈希函数,但处理冲突的方法不同,会使得平均查找长度不同。比如线性探测处理冲突可能会产生堆积,显然就没有二次探测法好,而链地址法处理冲突不会产生任何堆积,因而具有更佳的平均查找性能。

3、哈希表的装填因子

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

不管记录个数 n 有多大,我们总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围之内此时我们散列查找的时间复杂度就真的是 O(1) 了。通常我们都将哈希表的空间设置得比查找集合大,此时虽然是浪费了一定的空间,但换来的是查找效率的大大提升。


参考:

《大话数据结构 - 第8章》 查找


原文地址:https://www.cnblogs.com/linuxAndMcu/p/11558619.html