散列表(哈希表)+散列冲突的解决方法

转载http://www.nowamagic.net/academy/detail/3008060

1散列表

1简单来说就是给一个key,就可以找到对应的key的存储位置,就像身份证对应一个人一样

存储位置 = f(key)

2hashMap的key就是用到散列表

1.1散列冲突

我们时常会碰到两个关键字key1 ≠ key2,但是却有f (key1) = f (key2),这种现象我们称为冲突(collision),

打个比方说:加上f的函数是这样的f(key)=key%10;那么f(1)=f(11),这就是散列冲突

2解决散列冲突方法

2.1直接定址法

如果我们现在要对0-100岁的人口数字统计表,那么我们对年龄这个关键字就可以直接用年龄的数字作为地址。

此时f(key) = key。

这个时候,我们可以得出这么个哈希函数:f(0) = 0,f(1) = 1,……,f(20) = 20。

这个是根据我们自己设定的直接定址来的。人数我们可以不管,我们关心的是如何通过关键字找到地址。

如果我们现在要统计的是80后出生年份的人口数,那么我们对出生年份这个关键字可以用年份减去1980来作为地址。此时f (key) = key-1980。

假如今年是2000年,那么1980年出生的人就是20岁了,此时 f(2000) = 2000 - 1980,可以找得到地址20,地址20里保存了数据“人数500万”。

也就是说,我们可以取关键字的某个线性函数值为散列地址,即:

f(key) = a × key + b

这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合査找表较小且连续的情况。由于这样的限制,在现实应用中,直接定址法虽然简单,但却并不常用。

2.2除留余数法

2.21公式:f( key ) = key mod p 

本方法的关键就在于选择合适的p, p如果选得不好,就可能会容易产生同义词

一个例子

有一个关键字,它有12个记录,现在我们要针对它设计一个散列表。如果采用除留余数法,那么可以先尝试将散列函数设计为f(key) = key mod 12的方法。这个方法有点极端,因为关键字对应的下标都为0

但是我们如果不选用p=12来做除留余数法,而选用p=11,则结果如下:

这个时候就只有12和144有冲突,相对来说,就要好很多了。

2.22如何合理选取p值

使用除留余数法的一个经验是,若散列表表长为m(上一个案例表长为12),通常p为小于或等于表长(最好接近m)的最大质数或不包含小于20 质因子的合数。

  • 举个例子:某散列表的长度为100,散列函数H(k)=k%P,则P通常情况下最好选择哪个呢?A、91 B、93 C、97 D、99

  • 实践证明,当P取小于哈希表长的最大质数时,产生的哈希函数较好。我选97,因为它是离长度值最近的最大质数

  • 不包含小于20 质因子的合数23,29,31,37,41等等某2个组合:如23*23,23*29

2.3开放定址法

定义:就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

公式为:fi(key) = (f(key)+di) MOD m (di=1,2,3,......,m-1)

解决冲突的做法:

  • 当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。
  • 沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。
  • 查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。

2.31开放定址法:线性探测

案例

我们的关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12。 我们用散列函数f(key) = key mod 12

当计算前S个数{12,67,56,16,25}时,都是没有冲突的散列地址,直接存入:

计算key = 37时,发现f(37) = 1,此时1位置已经存在25了,发生位置冲突。

于是我们应用上面的公式f(37) = (f(37)+1) mod 12 = 2。于是将37存入下标为2的位置。

接下来22,29,15,47都没有冲突,正常的存入:

到了 key=48,我们计算得到f(48) = 0,此时0位置存在12了,发生位置冲突,不要紧,我们f(48) = (f(48)+1) mod 12 = 1,此时又1位置也有内容了,发生位置冲突。于是f(48) = (f(48)+2) mod 12=2,还是冲突……一直到 f(48) = (f(48)+6) mod 12 = 6时,才有空位,机不可失,赶快存入:

堆积

定义:案例中48和37这种本来都不是同义词却需要争夺一个地址的情况,我们称这种现象为堆积。

缺陷:堆积的出现,使得我们需要不断处理冲突,无论是存入还是査找效率都会大大降低。

2.32开放定址法:二次探测法

考虑深一步,如果发生这样的情况,当最后一个key=34,f(key)=10,与22所在的位置冲突,可是22后面没有空位置了,反而它的前面有一个空位置,尽管可以 不断地求余数后得到结果,但效率很差。

因此我们可以改进di = 1^2, -1^2, 2^2, -2^2,……, q^2, -q^2 (q <= m/2),这样就等于是可以双向寻找到可能的空位置。

对于34来说,我 们取di即可找到空位置了。另外增加平方运算的目的是为了不让关键字都聚集在 某一块区域。我们称这种方法为二次探测法。

fi(key) = (f(key)+di) MOD m ( di = 1^2, -1^2, 2^2, -2^2,……, q^2, -q^2 (q <= m/2))

2.33开放定址法:随机探测法

还有一种方法是,在冲突时,对于位移量 di 采用随机函数计算得到,我们称之为随机探测法。

此时一定会有人问,既然是随机,那么查找的时候不也随机生成办吗?如何可以获得相同的地址呢?这是个问题。这里的随机其实是伪随机数。

伪随机数是说,如果我们设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,我们在査找时,用同样的随机种子,它每次得到的数列是相同的,相同的 di 当然可以得到相同的散列地址。

fi(key) = (f(key)+di) MOD m (di是一个随机数列)

总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是我们常用的解决冲突的办法。

2.4链地址法(拉链法,hashMap就是这种结构)

定义:将所有关键字为同义词的记录存储在一个单链表(同义词子表)中,在散列表中只存储所有同义词子表的头指针。

案例

对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:

做法:

  • 将所有关键字为同义词的结点链接在同一个单链表中。
  • 若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。
  • 凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。
  • T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。

拉链法的优势与缺点

优点:

  • 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

  • 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

  • 开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

  • 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

  • 对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了査找时需要遍历单链表的性能损耗,不过性能损耗在很多场合下也不是什么大问题。

缺点:

指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

猜你喜欢

转载自blog.csdn.net/zhou920786312/article/details/83505565