解决哈希冲突的常用方法之开放寻址法

       解决哈希冲突的方法一般有:开放寻址法、链地址法(拉链法)、再哈希法和建立公共溢出区等方法。在 Java中为了解决Hash碰撞,ThreadLocalMap采用线性探测再散列的开放寻址法,LinkedHashMap采用链表法。本文介绍其中的开放寻址法。

开放寻址法概念

       开放寻址法:又称开放定址法,当哈希碰撞发生时,从发生碰撞的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。这个空闲单元又称为开放单元或者空白单元。

       查找时,如果探查到空白单元,即表中无待查的关键字,则查找失败。开放寻址法需要的表长度要大于等于所需要存放的元素数量,非常适用于装载因子较小(小于0.5)的散列表。

开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记,直到有下个元素插入才能真正删除该元素。

       可以把开放寻址法想象成一个停车问题。若当前车位已经有车,则继续往前开,直到找到一个空停车位。

        再散列法:Hi=RHi(key), i=1,2,…,k. RHi均是不同的散列函数,即在key产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。

开放寻址法函数

       开放寻址法的基本函数是:

Hi(key)=(H(key) + f(i)) MOD m, i=0,1,2,…, k(k<=m-1),

        其中,m 为散列表长度,一般为素数;

       H(key) 为散列函数,用于计算索引,key为关键字值;

       f(i) 为增量序列,用于解决冲突,且f(0) = 0,i为已经尝试计算索引的次数。

       当散列值H0(key)发生冲突时,再计算H1(key)……,直到不冲突为止。

        实现步骤

             * 得到给定的 key;

             * 根据函数计算得 hashValue;

             * 若不冲突,则把关键字值存入下标为hashValue的桶;

             * 若冲突,则使 i++ ,也就是往后找,直到找到第一个空桶并填入当前key。若到了尾部则循环到前面。

       下面根据增量序列f(i)的不同构造方法,来选择另外的空单元,介绍在开放寻址法中解决冲突的三种方法:线行探测再散列、平方探测再散列和双散列。 

线性探测法

       冲突函数:是i的一次多项式,典型取法为f(i)=i。

       线行探查法(Linear Probing)是开放定址法中最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。

       对于一个散列表,在散列过程中,某些元素形成一些区块,这种现象称作一次聚集(primary clustering)。就是说,散列到区块中的任何关键字都需要多次探测才可以解决哈希碰撞,然后,把该关键字添加到相应区块的桶中。

平方探测法

       冲突函数:是i的二次多项式,典型取法为f(i)=i^2。

       平方探测法(Quadratic Probing)即是发生冲突时,用发生冲突的单元H(key), 加上 1²、 2²等,即H(key) + 1²,H(key) + 2²,H(key) + 3²...直到找到空闲单元。f(i)也可以构造为:±i^2,i=1,2,3,...,k。

       在实际操作中,平方探测法不能探查到全部剩余的桶。不过在实际应用中,散列表如果大小是素数,并且至少有一半是空的,那么,总能够插入一个新的关键字。若探查到一半桶仍未找一个空闲的,表明此散列表太满,应该重哈希。平方探测法是解决线性探测中一次聚集问题的解决方法,但是,她引入了被称为二次聚集的问题——散列到同一个桶的那些元素将探测到相同的备选桶。下面的技术将会排除这个遗憾,不过要付出计算一个附加的哈希函数的代价。

双散列

       冲突函数:f(i) = i * hash2(key),典型取法是令hash2(key)=PRIME – (key % PRIME),其中 PRIME 是小于散列表大小的质数。

       双散列(double hashing)使用两个散列函数H(key)和hash2(key)。hash2(key)也以关键字为自变量,产生一个l至m-1之间的、并和m互素的数(即m不能被该数整除)作为探查序列的地址增量(即步长)。

       无论是线性探测还是二次探测,当装载因子过高时,哈希表能否动态增长?

       要扩充哈希表,首先必须找下一个新的且够大(大约2倍)的质数,然后必须考虑重哈希的成本。我们不可能原封不动的拷贝,必须要检验旧表格中的每个元素,计算其在新表格中的位置,然后再插入到新表格中。

开放寻址法实战

       下面看一个哈希表的考研真题,题目摘自2010年全国硕士研究生入学统一考试计算机科学与技术学科联考计算机学科专业基础综合试题:

       将关键字序列(7、8、30、11、18、9、14)散列存储到散列表中。散列表的存储空间是一个下标从0开始的一维数组,散列函数为H(key) = (keyx3) MOD 7,处理冲突采用线性探测再散列法,要求装填(载)因子为0.7。

       (1) 请画出所构造的散列表。

       (2) 分别计算等概率情况下查找成功和查找不成功的平均查找长度。

 ************************************

       解析:(1) 根据题意,我们可以确定哈希表的长度为 L = 7/0.7 = 10,因此此题需要构建的哈希表是下标为0~9的一维数组,记作array。

       当key=7时,根据散列函数H(key) = (keyx3) MOD 7, 可得 H(7) = (7x3)%7 =0,其他关键字的索引可同理计算,故得到如下散列函数值表:

key

7

8

30

11

18

9

14

H(key)

0

3

6

5

5

6

0

                       表1 散列函数值表

       表中有三组关键字7和14,30和9, 11和18的H(key)值相同,即在构建散列表时产生了哈希碰撞,所以要通过一定的冲突处理方法来解决这个问题。按关键字序列依次采用线性探测再散列法处理冲突,所构造的散列表为:

地址

0

1

2

3

4

5

6

7

8

9

关键字

7

14

 

8

 

11

30

18

9

 

                     表2 地址与关键字映射关系表

       下面详细介绍如何构建散列表:

       第一个key 7,它的地址是0,array[0]上没有关键字,因此没有冲突可以直接填入位置0;

       第二个key 8,它的地址是3,array[3]上没有关键字,因此没有冲突可以直接填入;

       第三个key 30,它的地址是6,没有冲突,因此放到散列表的数组下标为6的位置;

       第四个key 11,它的地址是5,没有冲突,因此放到array[5];

       第五个key 18,它的地址是5,因为array[5]已经有关键字11,出现哈希碰撞,此时我们根据线性探测再散列法来处理这个冲突,探测下一个位置6, array[6]上已经存在关键字30则继续增加步长1,因为array[7]上没有关键字,放入即可,至此冲突已经解决;

       第六个key 9,它的地址是6,因此应该放到散列表的数组下标为6的位置,但是这个位置上已经有关键字30,遇到了冲突,探测下一个位置7, 而array[7]=18需继续增加步长1,array[8]上没有关键字,放入即可;

       第七个key 14,它的地址是0,但array[0]=7,探测下一个位置array[1], 桶中没有关键字,放入即可。

       到这一步所有关键字均已填入,散列表已经构造完成,如表2所示。

 

       (2)关于等概率情况下查找成功平均查找长度ASL,可以根据第一问的构造过程求解:

       key 7一次就填入了表中,因此查找次数为1,同理8, 30, 11查找次数均为1; key 18 进行了3次放入操作,探测位置分别是5,6,7 ,因此查找次数为3;key 9也是3次;key 14 进行了两次探测,因此查找次数为2。次数表如表3所示:

key

7

8

30

11

18

9

14

count

1

1

1

1

3

3

2

                                  表3 key-count mapping

       所以ASLsuccess= (1+1+1+1+3+3+2)/ 7 = 12/7。 

       接下来讨论概率情况下查找不成功的平均查找长度, 看表2,计算查找不成功的次数就直接找关键字到第一个地址上关键字为空的距离即可,但根据哈希函数地址为MOD7,因此初始只可能在0~6的位置。等概率情况下,查找0~6位置查找失败的查找次数为:

地址0,到第一个关键字为空的地址2的距离为3,因此查找不成功的次数为3;    

地址1, 到第一个关键字为空的地址2的距离为2,因此查找不成功的次数为2;

地址2,  到第一个关键字为空的地址2的距离为1,因此查找不成功的次数为1;

地址3,到第一个关键字为空的地址4的距离为2,因此查找不成功的次数为2;

地址4,到第一个关键字为空的地址4的距离为1,因此查找不成功的次数为1;

地址5,到第一个关键字为空的地址2(注意不是地址9,因为初始只可能在0~6之间,因此循环回去)的距离为5,因此查找不成功的次数为5;

地址6,到第一个关键字为空的地址2(注意不是地址9,因为初始只可能在0~6之间,因此循环回去)的距离为4,因此查找不成功的次数为4。

因此查找不成功的次数表如下表所示

key

7

8

30

11

18

9

14

count

3

2

1

2

1

5

4

                                  表4 表3 key-count error mapping

 

      所以ASLunsuccess= (3+2+1+2+1+5+4)/ 7 = 18/7。

  

Reference

 https://www.cnblogs.com/ricklz/p/9006424.html

https://www.cnblogs.com/heyanan/p/6912870.html

数据结构与算法分析(Java语言描述) 第三版

posted @ 2020-03-29 21:13  楼兰胡杨  阅读(11979)  评论(2编辑  收藏  举报