哈希冲突的常见解决方式

哈希

Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

1.什么是哈希码
在Java中,哈希码代表的是一个对象的特征。它由哈希函数计算而来,设计良好的哈希函数会让不同的对象根据自己不同的特征来生成不同的哈希码。就像人的身份证号一样,根据每个人的特征生成,通过身份证号就可以知道这个人来自哪个区域,出生日期,性别信息等等。

2.什么是哈希函数
哈希函数又称“散列函数”,通过某种算法,可以将任意长度的输入转换为固定长度的输出。
如在Java中,哈希码用整型int表示,这意味着一共只有232 个哈希码,而对象的个数可以理解为无限个,哈希函数可以将无限个对象实例转换成有限个的哈希码。Object::hashCode默认就是通过对象所在的内存地址经过处理计算而得出的。

如果开发者觉得默认的哈希函数无法满足自己的要求,也可以重写哈希函数,如下就是IDEA自动生成的哈希函数,根据每个属性的哈希码计算而得:

class Person{
	private String name;
	private Integer age;
	private Boolean sex;

	@Override
	public int hashCode() {
		int result = name != null ? name.hashCode() : 0;
		result = 31 * result + (age != null ? age.hashCode() : 0);
		result = 31 * result + (sex != null ? sex.hashCode() : 0);
		return result;
	}
}

3.什么是哈希冲突
哈希函数的目的是将任意长度的输入转换为固定长度的输出,这就意味着,不同的输入可能会转换成相同的输出,这就导致了「哈希冲突」,也叫「哈希碰撞」。

k1 != k2 && hash(k1) == hash(k2)

哈希冲突是客观事实,只能尽量避免,无法彻底解决。


处理哈希冲突

一个设计良好的哈希函数,应该让不同特征的对象具有不同的哈希码,尽可能的避免哈希冲突。
但是哈希冲突是客观事实,只能尽量避免,无法彻底解决。
因此,一旦发生了哈希冲突,如何解决就变得非常重要了。

哈希冲突常见的解决方式有如下四种:

1.开放定址法

线性探测
哈希表中已经插入8、9元素,此时再插入14,下标2已经被8给占用了,出现哈希冲突。
线性探测会环形寻找next节点,先找到下标3,被9占用了,依然冲突,再找到下标4,没有被占用,即没有发生冲突,则将14放入下标4的节点中。
在这里插入图片描述
二次探测
也称二元探测,如果默认的哈希函数计算出的哈希码发生了哈希冲突,则哈希函数升级为:

(hash(key) + d) % table.length;
d = 1^2, -1^2, 2^2, -2^2, 3^2......

随机探测
和二次探测类似,只是d会更换为一组伪随机数列。

(hash(key) + d) % table.length;
d = 一组伪随机数列

开放定址法的优点就是:只要哈希表还有位置,通过不断的探测,总能找到合适的位置。
缺点是探测的次数不可控,一旦探测次数骤增,会严重影响哈希表的读写性能。

ThreadLocalMap就是用的「线性探测」技术解决哈希冲突的,当线程的ThreadLocal实例数量较多时,ThreadLocal的读效率会下降,因此Netty、Dubbo才会编写自己的ThreadLocal实现。

2.再散列法

提供一组哈希函数,而不是一个。
如果第一个哈希函数计算的哈希码发生冲突了,就采用第二个哈希函数重新计算哈希码,直到不冲突为止。、
查询时也是一样,依次调用不同的哈希函数计算哈希码,直到Key相等。
这种方式会增加哈希计算的开销,影响读写的效率。

int hash = hash1(key)hash2(key)hash3(key)......

3.链地址法

将哈希码对应一个链表,插入元素时,如果哈希码冲突了,就将元素插入到链表,可选头插或尾插。
查询时,遍历哈希码对应的链表。
HashMap采用的就是这种方式。

这种方式的缺点是:一旦哈希冲突多了,哈希表会退化成链表,查询效率会从O(1)变为O(n)。JDK8的HashMap针对这种情况有做优化,冲突超过8个会将链表转换为红黑树,提高查询效率。

4.公共溢出区

在创建哈希表的同时,再额外创建一个公共溢出区,专门用来存放发生哈希冲突的元素。查找时,先从哈希表查,查不到再去公共溢出区查。

这种方式的缺点是:哈希冲突多了,公共溢出区会膨胀的非常厉害,查询的效率也有影响。


你可能感兴趣的文章:

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
哈希冲突是指不同的键值对被哈希函数映射到了同一个桶中,解决哈希冲突的方法主要有以下几种: 1. 链地址法(Chaining):在每个哈希桶中维护一个链表,发生冲突时将新的键值对插入链表中。这种方法简单易懂,但是可能会导致链表过长,影响查找效率。 2. 开放地址法(Open Addressing):当一个哈希桶被占用时,尝试将键值对插入到其他未被占用的桶中,直到找到一个空桶或者所有桶都被占用。常见的开放地址法包括线性探测、二次探测和双重哈希等。 3. 建立公共溢出区(Overflow Area):当哈希桶被占用时,将冲突的键值对存储在一个公共的溢出区中。这种方法需要额外的存储空间,而且查找效率可能会降低。 举个例子,假设我们有一个哈希表,其中存储了以下键值对: ``` {"apple": 3, "banana": 5, "orange": 2, "pear": 4} ``` 如果我们使用简单的取余哈希函数将键映射到桶中,可能会出现以下情况: ``` hash("apple") % 4 = 3 hash("banana") % 4 = 1 hash("orange") % 4 = 2 hash("pear") % 4 = 0 ``` 可以看到,"apple" 和 "pear" 映射到了同一个桶中,发生了哈希冲突。如果我们使用链地址法来解决冲突,可以在第三个桶中维护一个链表,将 "apple" 和 "pear" 存储在链表中。如果我们使用开放地址法来解决冲突,可以尝试将 "apple" 插入到第四个桶中,但发现已经被占用了,于是继续尝试第一个桶,发现空闲,于是将 "apple" 存储在第一个桶中。这样就避免了哈希冲突

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值