java--基础--9.9--集合--Map--哈希表(散列表),哈希冲突

文章详细介绍了哈希表的概念,以及在Java中如何利用哈希表实现Map。讨论了哈希冲突的现象,并提出了直接定址法、除留余数法和开放定址法(线性探测、二次探测、随机探测)以及链地址法(拉链法)这四种解决冲突的方法,分析了各自的优缺点和适用场景。
摘要由CSDN通过智能技术生成

java–基础–9.9–集合–Map–哈希表(散列表),哈希冲突


1、哈希表(散列表)

  • 简单来说就是给一个key,就可以找到对应的key的存储位置,就像身份证对应一个人一样
  • 存储位置 = f(key)
  • hashMap的key就是用到散列表

2、哈希冲突

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

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

3、解决哈希冲突的方法

3.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

3.1.1、优缺点

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

3.2、除留余数法

3.2.1、公式

f( key ) = key mod p 

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

3.2.2、举例

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

这个方法有点极端,因为关键字对应的下标都为0,因为所有值都是12的整数倍

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

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

3.2.3、如何合理选取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个组合:如2323,2329

3.3、开放定址法

3.3.1、定义

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

3.3.2、公式为

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

3.3.3、解决冲突的做法

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

我们的关键字集合为{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这种本来都不是同义词却需要争夺一个地址的情况,我们称这种现象为堆积。
缺陷:堆积的出现,使得我们需要不断处理冲突,无论是存入还是査找效率都会大大降低。

3.3.3.2、开放定址法:二次探测法

考虑深一步,如果发生这样的情况,当最后一个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))
3.3.3.3、开放定址法:随机探测法

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

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

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

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

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

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

3.4.1、定义

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

3.4.2、案例

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

3.4.3、做法

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

3.4.4、优点

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

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

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

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


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

3.4.5、缺点

和开放定址法对比:

  1. 结点规模较小时,开放定址法较为节省空间
  2. 结点规模大时,扩大开放定址法散列表的规模(容量),将装填因子变小,这会减少开放定址法中的冲突,从而提高开放定址法平均查找速度。

​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值