散列概念

散列是一种用于常数平均时间执行插入、删除和查找的技术。理想的散列表数据结构不过是一个包含关键字的具有固定大小的数组。数组的每个位置存储一条信息,并且与一个唯一的关键字有确定的映射关系f,f被称为散列函数。这样在已知关键字k和散列函数f时,就能

唯一确定存储的信息。理想情况下,存储信息的位置是无限的,关键字的个数也是无限的,这样就能得到唯一的映射关系,不存在冲突;但是,实际上,我们在定义散列函数和关键字时,可能并不知道全部的存储信息,所以我们无法保证找到的找到的散列函数是无冲突

的,或者能均匀分布关键字。因此,实际的散列常伴随着冲突的出现。

设所有可能出现的关键字集合记为U(简称全集)。实际发生(即实际存储)的关键字集合记为K(|K|比|U|小得多)。

散列方法是使用函数f将K映射到表T[0..m-1]的下标上(m=O(|K|))。这样以K中关键字为自变量,以f为函数的运算结果就是相应结点的存储地址。从而达到在O(1)时间内就可完成查找。

其中:

  • f:K→{0,1,2,…,m-1} ,通常称f为散列函数(Hash Function)。散列函数f的作用是压缩待处理的下标范围,使待处理的|K|个值减少到m个值,从而降低空间开销。
  • T为散列表(Hash Table)。
  • f(ki)(ki∈K)是关键字为ki结点存储地址(亦称散列值或散列地址)。
  • 将结点按其关键字的散列地址存储到散列表中的过程称为散列(Hashing)

设计散列表

通常,关键字时字符串。

1、除余法 
除余法就是用关键码x除以M(往往取散列表长度),并取余数作为散列地址。除余法几乎是最简单的散列方法,散列函数为:hash(key)=key mod m

M应该选择散列表实际大小的内的一个素数,并且最好不要和2的整数幂接近。这是为了防止表中的元素分布不均匀。

2、乘余取整法 
设 hash(key)=floor(m×(A×key mod 1)),其中floor()表示对表达式进行下取整,A∈(0,1),m如上同样表示散列表的大小,且在这种方法中对m并无任何特殊的要求。
[A×key mod 1]表示将key乘上某个在0~1之间的数并取乘积的小数部分,该表达式等价于A×key-floor(A×key)
这里最重要的是A的值应该如何设定,Don•Knuth建议A=(√5-1)/2比较好。

3、平方取中法 
由于整数相除的运行速度通常比相乘要慢,所以有意识地避免使用除余法运算可以提高散列算法的运行时间。平方取中法的具体实现是:先通过求关键码的平方值,从而扩大相近数的差别,然后根据表长度取中间的几位数(往往取二进制的比特位)作为散列函数值。因

为一个乘积的中间几位数与乘数的每一数位都相关,所以由此产生的散列地址较为均匀。

4、数字分析法 
设有 n 个 d 位数,每一位可能有 r 种不同的符号。这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布均匀些,每种符号出现的几率均等; 在某些位上分布不均匀,只有某几种符号经常出现。可根据散列表的大小,选取其中各种符号分布均匀的若干位

作为散列地址。

5、基数转换法

将关键码值看成另一种进制的数再转换成原来进制的数,然后选其中几位作为散列地址。

6、折叠法 
有时关键码所含的位数很多,采用平方取中法计算太复杂,则可将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址,这方法称为折叠法。

7、ELFhash字符串散列函数 
ELFhash函数在UNIX系统V 版本4中的“可执行链接格式”( Executable and Linking Format,即ELF )中会用到,ELF文件格式用于存储可执行文件与目标文件。ELFhash函数是对字符串的散列。它对于长字符串和短字符串都很有效,字符串中每个字符都有同样的作用,它

巧妙地对字符的ASCII编码值进行计算,ELFhash函数对于能够比较均匀地把字符串分布在散列表中。

冲突处理

当两个key关键字通过散列函数计算映射到同一个存储位置时,就会发生冲突,如何处理这种冲突呢?

冲突解决技术可以分为两类:开散列方法( open hashing,也称为拉链法,separate chaining )和闭散列方法( closed hashing,也称为开地址方法,open addressing )。这两种方法的不同之处在于:开散列法把发生冲突的关键码存储在散列表主表之外,而闭散列法把发生

冲突的关键码存储在表中另一个位置。

开散列方法:

1、拉链法

开散列方法的一种简单形式是把散列表中的每个位置定义为一个链表的表头。散列到一个特定位置的所有记录都放到这个位置的链表中。图9-5说明了一个开散列的散列表,这个表中每一个位置存储一个记录和一个指向链表其余部分的指针。这7个数存储在有11个位置的散列表中,使用的散列函数是h(K) = K mod 11。数的插入顺序是77、7、110、95、14、75和62。有2个值散列到第0个位置,1个值散列到第3个位置,3个值散列到第7个位置,1个值散列到第9个位置。

2、桶式散列

桶式散列方法的基本思想是把一个文件的记录分为若干存储桶,每个存储桶包含一个或多个页块,一个存储桶内的各页块用指针连接起来,每个页块包含若干记录。散列函数f把关键码值K转换为存储桶号,即h(K)表示具有关键码值K的记录所在的存储桶号。 图9-6表示了一个具有B个存储桶的散列文件组织。有一个存储桶目录表,存放B个指针,每个存储桶一个,每个指针就是所对应存储桶的第一个页块的地址。

有些存储桶仅仅由一个页块组成,如下图中的1号存储桶。有的存储桶由多个页块组成,每一个页块的块头上有一个指向下一个页块的指针,例如,如下图中的第B-1号存储桶由b4,b5,b6三个页块组成,每个存储桶中最后一个页块的头上为空指针。

闭散列方法:

闭散列方法把所有记录直接存储在散列表中。每个记录关键码key有一个由散列函数计算出来的基位置,即f(key)。如果要插入一个关键码,而另一个记录已经占据了R的基位置(发生碰撞),那么就把R存储在表中的其它地址内,由冲突解决策略确定是哪个地址。

闭散列表解决冲突的基本思想是:当冲突发生时,使用某种方法为关键码K生成一个散列地址序列d0,d1,d2,... di ,...dm-1。其中d0=h(K)称为K的基地址地置( home position );所有di(0< i< m)是后继散列地址。当插入K时,若基地址上的结点已被别的数据元素占用,则按上述地址序列依次探查,将找到的第一个开放的空闲位置di作为K的存储位置;若所有后继散列地址都不空闲,说明该闭散列表已满,报告溢出。相应地,检索K时,将按同值的后继地址序列依次查找,检索成功时返回该位置di ;如果沿着探查序列检索时,遇到了开放的空闲地址,则说明表中没有待查的关键码。删除K时,也按同值的后继地址序列依次查找,查找到某个位置di具有该K值,则删除该位置di上的数据元素(删除操作实际上只是对该结点加以删除标记);如果遇到了开放的空闲地址,则说明表中没有待删除的关键码。因此,对于闭散列表来说,构造后继散列地址序列的方法,也就是处理冲突的方法。

形成探查的方法不同,所得到的解决冲突的方法也不同。下面是几种常见的构造方法。

1、线性探查法

将散列表看成是一个环形表,若在基地址d(即h(K)=d)发生冲突,则依次探查下述地址单元:d+1,d+2,......,M-1,0,1,......,d-1直到找到一个空闲地址或查找到关键码为key的结点为止。当然,若沿着该探查序列检索一遍之后,又回到了地址d,则无论是做插入操作还是做检索操作,都意味着失败。 用于简单线性探查的探查函数是: p(K,i) = i

例9.7 已知一组关键码为(26,36,41,38,44,15,68,12,06,51,25),散列表长度M= 15,用线性探查法解决冲突构造这组关键码的散列表。 因为n=11,利用除余法构造散列函数,选取小于M的最大质数P=13,则散列函数为:h(key) = key%13。按顺序插入各个结点: 26: h(26) = 0,36: h(36) = 10, 41: h(41) = 2,38: h(38) = 12, 44: h(44) = 5。 插入15时,其散列地址为2,由于2已被关键码为41的元素占用,故需进行探查。按顺序探查法,显然3为开放的空闲地址,故可将其放在3单元。类似地,68和12可分别放在4和13单元中.

2、二次探查法

二次探查法的基本思想是:生成的后继散列地址不是连续的,而是跳跃式的,以便为后续数据元素留下空间从而减少聚集。二次探查法的探查序列依次为:12,-12,22 ,-22,...等,也就是说,发生冲突时,将同义词来回散列在第一个地址的两端。求下一个开放地址的公式为:

3、随机探查法

理想的探查函数应当在探查序列中随机地从未访问过的槽中选择下一个位置,即探查序列应当是散列表位置的一个随机排列。但是,我们实际上不能随机地从探查序列中选择一个位置,因为在检索关键码的时候不能建立起同样的探查序列。然而,我们可以做一些类似于伪随机探查( pseudo-random probing )的事情。在伪随机探查中,探查序列中的第i个槽是(h(K) + ri) mod M,其中ri是1到M - 1之间数的“随机”数序列。所有插入和检索都使用相同的“随机”数。探查函数将是 p(K,i) = perm[i - 1], 这里perm是一个长度为M - 1的数组,它包含值从1到M – 1的随机序列。

4、双散列探查法

伪随机探查和二次探查都能消除基本聚集——即基地址不同的关键码,其探查序列的某些段重叠在一起——的问题。然而,如果两个关键码散列到同一个基地址,那么采用这两种方法还是得到同样的探查序列,仍然会产生聚集。这是因为伪随机探查和二次探查产生的探查序列只是基地址的函数,而不是原来关键码值的函数。这个问题称为二级聚集( secondary clustering )。

为了避免二级聚集,我们需要使得探查序列是原来关键码值的函数,而不是基位置的函数。双散列探查法利用第二个散列函数作为常数,每次跳过常数项,做线性探查。例如:一种流行的选择是F(i) = i * hash2(X);

评价散列表

填装因子

散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度

α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。

因此链表大小应该根据填入表中的元素调整合适大小,否则就容易冲突;最坏的散列表就是所有的元素都映射到一个位置,这就退化成了一个链表。

再散列

当填装因子较大时,散列表中的元素很拥挤,这样每次插入查找会消耗大量时间,散列表的效率变差,此时,需要扩充散列表的大小,将现有元素重新插入到新表中。这样的操作就是再散列。

显然再散列开销较大,时间复杂度O(n);n表示散列表中元素的个数。

再散列的实现方法有三种:1.当装填到表的一半时进行此操作;2.当插入失败时进行此操作;3.当装填因子达到某个临界值时进行此操作。显然第三种最好。

可扩散列