首页 > 编程知识 正文

处理散列冲突的方法,再散列法解决冲突举例子

时间:2023-05-05 21:01:35 阅读:228092 作者:844

1 散列函数的设计原则

  作为好的散列函数hash(),首先必须具有确定性。无论所含数据项如何,词条E在散列表中的映射地址hash(E.key)必须完全取决于其关键码key。其次映射过程资深不能过于复杂,唯此房能保证散列地址的计算可快速完成,从而保证查询或修改操作整体的O(1)期望执行时间。再次,所有关键码经映射后尽量覆盖整个地址空间[0,M),方可充分利用有限空间。也就是说,函数hash()最好是满射。
  由于定义域规模远远大于取值域规模,hash()不可能是单射。这就意味着,不同的词条可能被映射到同一散列地址(散列冲突)难以避免。所以设计和选择散列函数阶段要做好充分的考量,以便尽可能地降低冲突发生的概率。
  在此,最为重要的一条原则就是,关键码映射到各桶的概率应尽量相等,这等效于将关键码“均匀地”映射到散列地址空间,从而避免导致极端低效的情况,比如,关键码集中分布于某一区间,而加剧散列冲突。
  总而言之,随机越强、规律性越弱的散列函数越好。

2 常见设计方法 2.1 除余法 设计方法

  符合上述设计原则的一种最简单的映射方法,就是将散列表长度M取作素数,并将关键码key映射至key关于M整除的余数:
    hash(key) = key mod M

一般散列表的长度M与词条关键码间隔T之间的最大公约数越大,发生冲突的可能性也将越大,因此将M取作素数

实例分析

  以校园电话簿为例,取M=90001,则以下关键码{6278-5001,5153-1876,6277-0211}—>{54304,51304,38514}

不足之处

  以素数为表长的除余法尽管可在一定程度上保证词条的均匀分布,但从关键码空间到散列地址空间映射的角度看,依然残留某种连续性。比如,相邻关键码所对应的散列地址,总是彼此相邻;极小的关键码,通常都被集中映射到散列表的起始区段一一其中特别是0值居然是一个“不动点”,其散列地址总是0。

2.2 MAD法 设计方法

  所谓的MAD法是multply-add-divide的缩写,其将关键码key映射至key与常数a之积再于常数b之和对M求余数(M的取值方法与除余法相同):
    hash(key) = (a x key + b) mod M
  MAD法尽管运算量略有增加,但只要常数a和b选取得当可以很好地克服除余法原有的连续性缺陷。

实例分析

  当取a=31和b=2时,关键码集合a{2011,2012,2013,2014,2015,2016},经过散列函数得出集合b{1,4,6,9,12,15}。连续的关键码经过散列后并不连续,各关键码散列的均匀性和随机性有了很大的改善。

2.3 数字分析法 设计方法

  数字分析法从关键码key特定进制的展开中抽取出特定的若干位,构成一个整型地址。比如,取十进制展开中奇位数,则有
    hash(123456789) = 13579

2.4 平方取中法 设计方法

  平方取中法,从关键码key的平方的十进制或二进制展开中取居中的若干位,构成一个整型地址。比如,去平方后十进制展开中居中的三位,则有
    hash(123) = 512 (1232=15129)

2.5 折叠法 设计方法

  折叠法,是将关键码的十进制或二进制展开分割成等宽的若干段,取其总和作为散列地址。比如,若以三个数位为分割单位,则有
    hash(123456789) = 123 + 456 + 789 = 1368

2.6 位异或法 设计方法

  位异或法,是将关键码的二进制展开分割成等宽的若干段,经异或运算得到散列地址。比如,仍以三个数位为分割单位,则有
    hash(411) = hash(110011011b) = 110 ^ 011 ^ 011 = 110b = 6

3 散列冲突解决方案 冲突的普遍性

  无论散列函数设计得如何巧妙,也不可能保证不同的关键码之间互不冲突。事实上在实际应用中,不发生任何冲突的概率远远低于我们的想象。我们来看下面这个例子,某课堂的所有学生中,是否有某两位生日相同?需要多少学生才能是这样的概率大于50%呢?
  不难证明上述的答案是,只要学生人数大于23,生日相同的概率将大于50%。若将全年各天视作365个桶,并将学生视作词条,则可按生日将他们组织为散列表,则该问题便可转化为:若长度为365的散列表中存在n个词条,则至少发生一次冲突的概率是多大?由此可见,解决散列冲突是涉及散列表中非常重要的部分。

3.1 多槽位法

  最简单的解决冲突策略是,将彼此冲突的每一组词条组织为一个小规模的子词典,分别存放他们共同对应的桶单元。多槽位法是将各桶细分为更小的槽位,每一组槽位使用向量存储数据。

  多槽位法优点是速度快,但是使用向量需要提前申请空间,导致空间利用率低。

3.2 独立链法

  独立链法与多槽位法是同样的策略,不过子词典采用链表结构。

  采用独立链法可以有效避免向量的空间浪费问题,缺点是操作时间增加。

3.3 公共溢出区法

  公共溢出区法的思路是,在原散列表职位另设一个词典结构,相当于一个公共缓存区,一旦发生冲突就将该词条转存至公共缓存区。

  该策略构思简单、易于实现,在冲突不甚频繁的场合不失为一种好的选择。

闭散列策略

  尽管逻辑结构而言,独立链等策略便捷而紧凑,但绝非上策。比如需要引进次级关联结构,在查找过程中将需要做更多地I/O操作,时间成本大大增加。实际上仅仅依靠基本的散列表结构就地解决冲突,反而是更好的选择。
  闭散列策略,即当词条存在冲突时,只允许在散列表内部为其寻找另一空桶。因闭散列策略不得使用附加控件,装填因子需要适度降低,通常取λ≤0.5

3.4 线性试探法

  线程试探法采用闭散列策略,当关键码key词条插入时,发现桶单元hash(key)已被占用,则转而试探下一个桶单元hask(key)+1, 如此不断试探,直到发现一个可用空桶。当然为确保桶地址的合法,最后还需要统一对M取模。
  在查找过程中只有查找到对应关键码或第一个空桶才停止,否则须转入下一个桶单元继续试探。
  在删除时采用假删除策略,将被删除的桶单元关键码改为特定的数(如-1)表示删除,以便查找时不至于找不到关键码。

  线性试探法在物理上保持了一定的连贯性,具有良好的数据局部性,故系统缓存的作用可以充分发挥。尽管闭散列策略同时也会一定程度上增加冲突发生的可能,但只要散列表的规模不是很小,装填因子不大,则相对于I/O的负担降低而言,这些问题都微不足道。

3.5 平方试探法

  由于数据的局部性,数据往往会聚集在散列表中的某个地方,采用MAD散列函数可缓解一定的聚集现象。而平方试探法,则是更为有效的一种方法。
  在发生冲突时,按如下规则确定第j次试探的桶地址:
    ( hash(key) + j2 ) mod M   (j=0,1,2……)
  平方试探法的删除和查找策略与线性试探法大致相同。

  平方试探法能很好解决词条的聚集现象,但线性试探法中,只要散列表中尚有空桶,则试探过程必将会终止。而平方试探法是否能保证这一点呢?
  事实上平方试探法无法保证有空桶必将终止,但是能保证散列表长度M为素数且装填因子λ≤50%时,必将终止于某个空桶。

3.6 再散列法

  在散列法也是一个解决聚集现象的有效办法。为此,需要选取一个适宜的二级散列函数hash2(),一旦在插入词条时发现桶单元hash(key)已被占用,则以hash2(key)为偏移增量继续尝试,直到发现一个空桶。如此,被尝试的桶地址依次应为:
    [ hash(key) + 1 x hash2(key) ] % M
    [ hash(key) + 2 x hash2(key) ] % M
    [ hash(key) + 3 x hash2(key) ] % M
  可见再散列法也是一种线性试探,只不过不同的key的增量不同。

本文内容来自 zjdxf老师的 《数据结构与算法》

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。