只有当桶内所有 s 个表项位置都放满表项后再加入表项才会产生溢出。
通常桶的大小 s 取的比较小, 因此在桶内大多采用顺序搜索。
闭散列就是处理溢出的一种常用的方法, 也叫做开地址法。
在闭散列情形, 所有的桶都直接放在散列表数组中。因此每个桶只有一个表项 (s = 1)。
若设散列表中各桶的编址为 0 到 m-1, 当要加入一个表项 R2时, 用它的关键码 R2.key, 通过散列函数 hash ( R2.key ) 的计算,得到它的存放桶号 j。
(1) 线性探查法 (Linear Probing)
但在存放时发现这个桶已经被另一个表项 R1 占据,发生了冲突,必须处理溢出。为此,需把 R2 存放到表中“下一个”空桶中。如果表未被装满,则在允许的范围内必定还有空桶。
可得 Hash (Burke) = 1 Hash (Ekers) = 4
Hash (Broad) = 1 Hash (Blum) = 1
Hash (Attlee) = 0 Hash (Hecht) = 7
Hash (Alton) = 0 Hash (Ederly) = 4
设散列表为HT[26],m = 26。采用线性探查法处理溢出,则上述关键码在散列表中散列位置如图所示。
需要搜索或加入一个表项时,使用散列函数计算桶号:
H0 = hash ( key )
一旦发生冲突,在表中顺次向后寻找“下一个”空桶 Hi 的递推公式为:
Hi = ( Hi-1 +1 ) % m, i =1, 2, …, m-1
即用以下的线性探查序列在表中寻找“下一个”空桶的桶号:
H0 +1, H0 +2, …, m-1, 0, 1, 2, …, H0-1
亦可写成如下的通项公式:
Hi = ( H0 + i ) % m, i =1, 2, …, m-1
当发生冲突时, 探查下一个桶。当循环 m-1次后就会回到开始探查时的位置, 说明待查关键码不在表内, 而且表已满, 不能再插入新关键码。
用平均搜索长度ASL (Averagy Search Length)衡量散列方法的搜索性能。
根据搜索成功与否,它又有搜索成功的平均搜索长度ASLsucc和搜索不成功的平均搜索长度ASLunsucc之分。
搜索成功的平均搜索长度 ASLsucc 是指搜索到表中已有表项的平均探查次数。它是找到表中各个已有表项的探查次数的平均值。
搜索不成功的平均搜索长度 ASLunsucc 是指在表中搜索不到待查的表项,但找到插入位置的平均探查次数。它是表中所有可能散列到的位置上要插入新元素时为找到空桶的探查次数的平均值。
在使用线性探查法对示例进行搜索时,搜索成功的平均搜索长度为:
搜索不成功的平均搜索长度为:
下面给出用线性探查法在散列表 ht 中搜索给定值 x 的算法。如果查到某一个 j,使得 ht[j].info == Active 同时 ht[j].Element == x,则搜索成功;否则搜索失败。造成失败的原因可能是表已满,或者是原来有此表项但已被删去,或者是无此表项且找到空桶。
在利用散列表进行各种处理之前,必须首先将散列表中原有的内容清掉。只需将表中所有表项的info域置为Empty即可。
散列表存放的表项不应有重复的关键码。在插入新表项时,如果发现表中已经有关键码相同的表项,则不再插入。
在闭散列情形下不能真正删除表中已有表项。删除表项会影响其它表项的搜索。若把关键码为Broad的表项真正删除,把它所在位置的info域置为Empty,以后在搜索关键码为Blum和Alton的表项时就查不下去,从而会错误地判断表中没有关键码为Blum和Alton的表项。
若想删除一个表项,只能给它做一个删除标记deleted,进行逻辑删除,不能把它真正删去。
逻辑删除的副作用是:在执行多次删除后,表面上看起来散列表很满,实际上有许多位置没有利用。
算法分析
设散列表的装填因子为 = n / (s*m),其中 n 是表中已有的表项个数,s 是每个桶中最多可容纳表项个数,m 是表中的桶数。
可用 表明散列表的装满程度。 越大,表中表项数越多,表装得越满,发生冲突可能性越大。
通过对线性探查法的分析可知,为搜索一个关键码所需进行的探查次数的期望值 P 大约是 (2-)/(2-2)。虽然平均探查次数较小,但在最坏情况下的探查次数会相当大。
(2) 二次探查法 (quadratic probing)
为改善“堆积”问题,减少为完成搜索所需的平均探查次数,可使用二次探查法。
通过某一个散列函数对表项的关键码 x 进行计算,得到桶号,它是一个非负整数。
H0 = hash(x)
二次探查法在表中寻找“下一个”空桶的公式为:
Hi = (H0 + i 2 ) % m,
Hi = (H0 - i 2 ) % m, i = 1, 2, …, (m-1)/2
式中的 m 是表的大小,它应是一个值为 4k+3 的质数,其中 k 是一个整数。这样的质数如 3, 7, 11, 19, 23, 31, 43, 59, 127, 251, 503, 1019, …。
探查序列形如 H0, H0+1, H0-1, H0+4, H0-4, …。
在做 (H0 - i2 ) % m 的运算时,当 H0 - i2 < 0 时,运算结果也是负数。实际算式可改为
j = (H0 - i2 ) % m, while ( j < 0 ) j += m
示例:给出一组关键码 { Burke, Ekers, Broad, Blum, Attlee, Alton, Hecht, Ederly }。
散列函数为:Hash (x)=ord (x)-ord (''''A'''')
用它计算可得
Hash (Burke) = 1 Hash (Ekers) = 4
Hash (Broad) = 1 Hash (Blum) = 1
Hash (Attlee) = 0 Hash (Hecht) = 7
Hash (Alton) = 0 Hash (Ederly) = 4
因为可能桶号是 0 25, 取满足 4k+3 的质数,表的长度为TableSize = 31,利用二次探查法得到的散列结果如图所示。
使用二次探查法处理溢出时的搜索成功的平均搜索长度为:
搜索不成功的平均搜索长度为:
只要知道上一次的桶号 和 ,当 i 增加 1 时可以从 和 简单地导出 和 ,不需要每次计算 i 的平方。
在溢出处理算法 Find 中,首先求出 H0 作为当前桶号 CurrentPos,当发生冲突时求“下一个”桶号,i = 1。
此时用一个标志 odd 控制是加 i2 还是减 i2 。
若 odd == 0 加 i 2,并置 odd = 1;
若 odd == 1 减 i 2,并置 odd = 0。
下次 i 进一后,又可由 odd 控制先加后减。
在搜索时可以不考虑表装满的情况;但在插入时必须确保表的装填因子不超过0.5。如果超出,必须将表长度扩充一倍,进行表的分裂。
在删除一个表项时,为确保搜索链不致中断,也只能做表项的逻辑删除,即将被删表项的标记info改为Deleted。
(3) 双散列法
使用双散列方法时,需要两个散列函数。
第一个散列函数 Hash( ) 按表项的关键码 key 计算表项所在的桶号 H0 = Hash(key)。
一旦冲突,利用第二个散列函数 ReHash( ) 计算该表项到达“下一个”桶的移位量。它的取值与 key 的值有关,要求它的取值应当是小于地址空间大小 TableSize,且与 TableSize 互质的正整数。
若设表的长度为 m = TableSize,则在表中寻找“下一个”桶的公式为:
j = H0 = Hash(key), p = ReHash(key);
j = ( j + p ) % m;
p是小于m且与m互质的整数
利用双散列法,按一定的距离,跳跃式地寻找“下一个”桶,减少了“堆积”的机会。
双散列法的探查序列也可写成:
Hi = (H0 + i * ReHash(key) ) % m,
i =1, 2, …, m-1
最多经过m-1次探查,它会遍历表中所有位置,回到H0 位置。
示例:给出一组表项关键码{ 22, 41, 53, 46, 30, 13, 01, 67 }。散列函数为:
Hash(x)=(3x) % 11。
散列表为 HT[0..10],m = 11。因此,再散列函数为 ReHash(x) = (7x) % 10 +1。
Hi = ( Hi-1 + (7x) % 10 +1 ) % 11, i = 1, 2,
H0(22) = 0 H0(41) = 2 H0(53) = 5
H0(46) = 6 H0(30) = 2 冲突 H1 = (2+1) = 3
H0(13) = 6 冲突 H1 = (6+2) = 8
H0(01) = 3 冲突 H1 = (3+8) = 0
H2 = (0+8) = 8 H3 = (8+8) = 5
H4 = (5+8) = 2 H5 = (2+8) = 10
搜索成功的平均搜索长度
搜索不成功的平均搜索长度
每一散列位置的移位量有10种:1, 2, , 10。先计算每一散列位置各种移位量情形下找到下一个空位的比较次数,求出平均值;
再计算各个位置的平均比较次数的总平均值。
Rehash( )的取法很多。例如,当m是质数时,可定义
ReHash(key) = key % (m-2) +1
ReHash(key) = key / m % (m-2)+1
当 m 是 2 的方幂时,ReHash(key) 可取从 0 到m-1 中的任意一个奇数。
处理溢出的开散列方法 — 链地址法
开散列方法首先对关键码集合用某一个散列函数计算它们的存放位置。
若设散列表地址空间的所有位置是从0到m-1,则关键码集合中的所有关键码被划分为m个子集,具有相同地址的关键码归于同一子集。我们称同一子集中的关键码互为同义词。每一个子集称为一个桶。
通常各个桶中的表项通过一个单链表链接起来,称之为同义词子表。所有桶号相同的表项都链接在同一个同义词子表中,各链表的表头结点组成一个向量。
向量的元素个数与桶数一致。桶号为 i 的同义词子表的表头结点是向量中的第 i 个元素。
示例:给出一组表项关键码{ Burke, Ekers, Broad, Blum, Attlee, Alton, Hecht, Ederly }。散列函数为:Hash (x)=ord (x)-ord (''''A'''')。
用它计算可得:
Hash (Burke) = 1 Hash (Ekers) = 4
Hash (Broad) = 1 Hash (Blum) = 1
Hash (Attlee) = 0 Hash (Hecht) = 7
Hash (Alton) = 0 Hash (Ederly) = 4
散列表为 HT[0..25],m = 26。
采用开散列法处理溢出,则上述关键码在散列表中的散列位置如图所示。
通常,每个桶中的同义词子表都很短,设有n 个关键码通过某一个散列函数,存放到散列表中的 m 个桶中。那么每一个桶中的同义词子表的平均长度为 n / m。这样,以搜索平均长度为 n / m 的同义词子表代替了搜索长度为 n 的顺序表,搜索速度快得多。
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上,由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装填因子 0.5,而表项所占空间又比指针大得多,所以使用链地址法反而比开地址法节省存储空间。
散列表分析
散列表是一种直接计算记录存放地址的方法,它在关键码与存储位置之间直接建立了映象。
当选择的散列函数能够得到均匀的地址分布时,在搜索过程中可以不做多次探查。
由于很难避免冲突,就增加了搜索时间。冲突的出现,与散列函数的选取 (地址分布是否均匀),处理溢出的方法 (是否产生堆积) 有关。
在实际应用中,在使用关键码进行散列时,如在用作关键码的许多标识符具有相同的前缀或后缀,或者是相同字符的不同排列的场合,不同的散列函数往往导致散列表具有不同的搜索性能。
下图给出一些实验结果,列出在采用不同的散列函数和不同的处理溢出的方法时,搜索关键码所需的对桶访问的平均次数。实验数据为 { 33575, 24050, 4909, 3072, 2241, 930, 762, 500 }。
当装填因子 较高时,选择的散列函数不同,散列表的搜索性能差别很大。在一般情况下多选用除留余数法,其中的除数在实用上应选择不含有20以下的质因数的质数。
对散列表技术进行的实验评估表明,它具有很好的平均性能,优于一些传统的技术,如平衡树。
但散列表在最坏情况下性能很不好。如果对一 个有 n 个关键码的散列表执行一次搜索或插入操作,最坏情况下需要 O(n) 的时间。
Knuth对不同的溢出处理方法进行了概率分析。
若设 是散列表的装填因子:
用地址分布均匀的散列函数Hash( )计算桶号。
Sn 是搜索一个随机选择的关键码 xi (1 i n) 所需的关键码比较次数的期望值
Un 是在长度为 m 的散列表中 n 个桶已装入表项的情况下,装入第 n+1 项所需执行的关键码比较次数期望值。
前者称为在 = n / m 时的搜索成功的平均搜索长度,后者称为在 = n / m时的搜索不成功的平均搜索长度。
用不同的方法溢出处理冲突时散列表的平均搜索长度如图所示。
散列表的装填因子 表明了表中的装满程度。越大,说明表越满,再插入新元素时发生冲突的可能性就越大。
散列表的搜索性能,即平均搜索长度依赖于散列表的装填因子,不直接依赖于 n 或 m。
不论表的长度有多大,我们总能选择一个合适的装填因子,以把平均搜索长度限制在一定范围内。
小结 需要复习的知识点
掌握:静态索引结构,包括线性索引、倒排索引、静态索引树的搜索和构造方法。
掌握:动态索引结构,包括B_树、B+树的搜索; B_树的插入和删除方法; B+树的插入与删除方法。
掌握:散列表与散列方法,包括散列函数的构造、处理溢出的闭散列方法;处理溢出的开散列方法;散列表分析。
B_树的定义、平衡 m 路搜索树与 m 阶B_树的关系
有 n 个关键码的 m 阶B_树的最大高度和最小高度
最大高度为 logm/2((n+1)/2) + 1
最小高度为 logm(n + 1)
(包括失败结点所在层次)
B_树的插入 (包括结点分裂)、删除 (包括结点调整与合并)方法
散列表中散列函数的设计原则及除留余数法的设计 (注意除数的选择)
解决地址冲突的线性探查法的运用,平均探查次数计算
线性探查法的删除问题、散列表中必须为各地址设置的三个状态 Active, Empty, Deleted
解决地址冲突的双散列法的运用,平均探查次数的计算
双散列法中再散列函数的设计:要求 计算结果与表长 m 互质 (m 设计为质数)
解决地址冲突的二次散列法的运用,平均探查次数计算
二次散列法中装填因子 与表长 m 的设置: 不大于0.5, m为满足 4 j + 3 的质数
例 求散列表大小并设计散列函数
设有一个含200个表项的散列表,用二次探查法解决冲突,按关键码查询时找到一个新表项插入位置的平均探查次数不超过1.5,则散列表项应能够至少容纳多少个表项。再设计散列函数(设搜索不成功的平均搜索长度为 Un=1 / (1 -α), 其中 α为装填因子)
解答:设表中表项个数 n = 200,搜索不成功的平均搜索长度
Un=1 / (1 -α) 1.5 1/3
n / m = 200 / m = 1/3 m 600
又 二次散列要求 m 必须是满足 4j+3的质数,故 m 可以取 607
散列函数 hash( key ) = key % m