Bootstrap

数据结构之(一)Hash(散列)

最近一直在准备面试,借此机会把数据结构相关整理一下,方便自己和其他人查阅。
该系列第一篇为Hash,主要考察点相对集中,对研发和测试的面试来说深度要求也不算太高,因此主要整理Hash数据结构的相关知识点,追求广度和部分深度。下面以基础概念(Hash(散列,下文统一称hash、hash表)、hash表(散列表)、常用哈希构造方法及函数、避免哈希冲突常用方法)、几种常用的查找数据结构的对比、STL常见的hash实现。

基础概念


哈希:哈希是一种用以常数平均时间执行插入、删除和查找的技术。但是,一般不支持诸如FindMin、FindMax以及以线性时间按排序顺序将整个表进行打印的操作。进一步讲,hash就是把任意长度的输入(预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。

哈希表:利用hash技术实现,理想情况下为一个包含有关键字(key-indexed,可以为整数、字符串等)的具有固定大小的数组。输入待查找的值即关键字,即可查找到其对应的数据元素。

哈希函数:建立起数据元素的存放位置与数据元素的关键字之间的对应关系的函数。即使用哈希函数可将被查找的键转换为数组的索引。理想情况下它应该运算简单并且保证任何两个不同的关键字映射到不同的单元(索引值)。但是,这是不可能的,很多时候我们都需要处理多个键被哈希到同一个索引值的情况,即哈希碰撞冲突

那么为什么很难达到理想情况呢?
我们考虑如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1)。但实际上我们很难忽略内存的影响,在例如处理海量数据时,hash往往是一种很好的方式,但这时内存一般不能满足数据要求,所以就需要考虑解决冲突。

哈希函数


如上所述,我们通常希望尽量寻找一个hash函数,该函数尽量在单元之间均匀地分配关键字,并且计算简单。综合不同因素,常用的哈希函数构造方法有以下几种:

1、直接寻址法:以关键字的某个线性函数值为哈希地址,可以表示为hash(K)=aK+C
优点是不会产生冲突,缺点是空间复杂度可能会较高,可能会造成空间的大量浪费,适用于元素较少的情况

2、数字分析法:该方法是取数据元素关键字中某些取值较均匀的数字来作为哈希地址的方法,这样可以尽量避免冲突。
缺点是该方法只适合于能预先估计出全体关键字的每一位上各种数字出现的频度。对于想要设计出更加通用的哈希表并不适用。

例如,要构造一个数据元素个数n=80,哈希长度m=100的哈希表。不失一般性,我们这里只给出其中8个关键字进行分析,8个关键字如下所示:

K1=61317602 K2=61326875 K3=62739628 K4=61343634

K5=62706815 K6=62774638 K7=61381262 K8=61394220

分析上述8个关键字可知,关键字从左到右的第1、2、3、6位取值比较集中,不宜作为哈希地址,剩余的第4、5、7、8位取值较均匀,可选取其中的两位作为哈希地址。设选取最后两位作为哈希地址,则这8个关键字的哈希地址分别为:2,75,28,34,15,38,62,20。

3、除留余数法:是由数据元素关键字除以某个常数所留的余数为哈希地址,该方法计算简单,适用范围广,是经常使用的一种哈希函数,可以表示为:hash(K)=K mod C
该方法的关键是常数的选取,一般要求是接近或等于哈希表本身的长度,研究理论表明,该常数选素数时效果最好

4、平方取中法:对关键字计算平方,然后根据可使用空间的大小取中间分布较均匀的几位,散列到相应的位置。
这样计算的原因是因为关键字的大多数位或所有位对结果都有贡献,并且通过取平方扩大差别,平方值的中间几位(位数可用lgN计算)和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。

例如,设哈希表长为1000则可取关键字平方值的中间三位,如下:

关键字 关键字的平方 哈希函数值
1234 1522756 227
2143 4592449 924
4132 17073424 734
3214 10329796 297

有人曾用“轮盘赌”的统计分析方法对它们进行了模拟分析,结论是平方取中法最接近“随机化”。

此法也可应用于字符串的散列
例如,设有一组关键字值为ABC,BCD,CDE,DEF其相应的机内码分别为010203,020304,030405,040506。假设可利用地址空间大小为1000,平方后取平方数的中间三位作为相当记录的存储地址。如下所示:

关键字 机内码 机内码的平方 哈希地址
ABC 010203 0104101209 101
BCD 020304 0412252416 252
CDE 030405 0924464025 464
DEF 040506 1640736036 736


5、折叠法:所谓折叠法是将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位),这方法称为折叠法。两种叠加处理的方法:移位叠加:将分割后的几部分低位对齐相加;边界叠加:从一端沿分割界来回折叠,然后对齐相加。

例如,当哈希表长为1000时,关键字key=110108331119891,允许的地址空间为三位十进制数,则这两种叠加情况如图(2):

   移位叠加                                 边界叠加

   8 9 1                                     8 9 1

   1 1 9                                     9 1 1

   3 3 1                                     3 3 1

   1 0 8                                     8 0 1

+  1 1 0                                   + 1 1 0              

   (1) 5 5 9                     (2)0 4 4

这种方法适用于关键字位数较多,而且关键字中每一位上数字分布大致均匀的情况。同时,此法也可应用于字符串的散列,计算的方法是将字符串的 ASCII 值累加起来,对 M 求模。

6、随机数法:设定哈希函数为:H(key) = Rand(key),其中,Rand 为伪随机函数
此法适合于对长度不等的关键字构造哈希函数。

7.旋转法:旋转法是将数据的键值中进行旋转。旋转法通常并不直接使用在哈希函数上,而是搭配其他哈希函数使用。

例如,某学校同一个系的新生(小于100人)的学号前5位数是相同的,只有最后2位数不同,我们将最后一位数,旋转放置到第一位,其余的往右移。

新生学号 旋转过程 旋转后的新键值
5062101 506210 1 1506210
5062102 506210 2 2506210
5062103 506210 3 3506210
5062104 506210 4 4506210
5062105 506210 5 5506210


运用这种方法可以只输入一个数值从而快速地查到有关学生的信息。

实际造表时,采用何种构造哈希函数的方法取决于建表的关键字集合的情况(包括关键字的范围和形态),以及哈希表长度(哈希地址范围),总的原则是使产生冲突的可能性降到尽可能地小。

8、相乘取整法:首先用关键字key乘上某个常数A(0< A<1),并抽取出key.A的小数部分;然后用m乘以该小数后取整

函数冲突处理方法


1、开放地址法(再散列法):当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:

Hi=Hkey+di%mi=12n

其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下几种:

(1)线性探测法

di=123m1

这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
建立在两个假设基础上:(1)表格足够大(2)每个元素都能够独立

(2)二次探测法

di=121222
;