标签:代码 一半 探测 插入数据 ret object 攻击 响应 功能
装载因子越大,说明散列表中的元素越多,空闲位置越少,散列冲突的概率就越大。
不仅插入数据的过程要多次寻址或者拉很长的链,查找的过程也会因此变得很慢。
对于没有频繁插入和删除的静态数据集合来说,很容易根据数据的特点、分布等,设计出完美的、极少冲突的散列函数,因为毕竟之前数据都是已知的。
对于动态散列表来说,数据集合是频繁变动的,事先无法预估将要加入的数据个数,所以也无法事先申请一个足够大的散列表。
随着数据慢慢加入,装载因子就会慢慢变大。当装载因子大到一定程度之后,散列冲突就会变得不可接受。
这个时候,进行动态扩容,重新申请一个更大的散列表,将数据搬移到这个新散列表中。
假设每次扩容我们都申请一个原来散列表大小两倍的空间。
如果原来散列表的装载因子是 0.8,那经过扩容之后,新散列表的装载因子就下降为原来的一半,变成了0.4。
针对数组的扩容,数据搬移操作比较简单。但是,针对散列表的扩容,数据搬移操作要复杂很多。因为散列表的大小变了,数据的存储位置也变了,所以需要通过散列函数重新计算每个数据的存储位置。
如下图所示在原来的散列表中,21这个元素原来存储在下标为0的位置,搬移到新的散列表中,存储在下标为7的位置。
对于支持动态扩容的散列表,插入一个数据,最好情况下,不需要扩容,最好时间复杂度是 O(1)。
最坏情况下,散列表装载因子过高,启动扩容,需要重新申请内存空间,重新计算哈希位置,并且搬移数据,所以时间复杂度是O(n)。
均摊情况下,时间复杂度接近最好情况,就是O(1)。
实际上,对于动态散列表,随着数据的删除,散列表中的数据会越来越少,空闲空间会越来越多。
如果对空间消耗非常敏感,可以在装载因子小于某个值之后,启动动态缩容。
当然,如果更加在意执行效率,能够容忍多消耗一点内存空间,那就可以不用费劲来缩容了。
当散列表的装载因子超过某个阈值时,就需要进行扩容。所以装载因子阈值需要选择得当。如果太大,会导致冲突过多;如果太小,会导致内存浪费严重。
装载因子阈值的设置要权衡时间、空间复杂度。
如果内存空间不紧张,对执行效率要求很高,可以降低负载因子的阈值;相反,如果内存空间紧张,对执行效率要求又不高,可以增加负载因子的值,甚至可以大于1。
大部分情况下,动态扩容的散列表插入一个数据都很快,但是在特殊情况下,当装载因子已经到达阈值,需要先进行扩容,再插入数据。
这个时候,插入数据就会变得很慢,甚至会无法接受。
举一个极端的例子,如果散列表当前大小为1GB,要想扩容为原来的两倍大小,那就需要对1GB的数据重新计算哈希值,并且从原来的散列表搬移到新的散列表,这个操作很耗时。
如果业务代码直接服务于用户,尽管大部分情况下,插入一个数据的操作都很快,但是,极个别非常慢的插入操作,也会让用户崩溃。这个时候,“一次性”扩容的机制就不合适了。
为了解决一次性扩容耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。
当装载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。
当有新数据要插入时,将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。
经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次性数据搬移,插入操作就都变得很快了。
这期间对于查询操作,为了兼容了新、老散列表中的数据,先从新散列表中查找,如果没有找到,再去老的散列表中查找。
通过这样均摊的方法,将一次性扩容的代价,均摊到多次插入操作中,就避免了一次性扩容耗时过多的情况。
这种实现方式,任何情况下,插入一个数据的时间复杂度都是O(1)。
两种主要的散列冲突的解决办法,开放寻址法和链表法。
这两种冲突解决办法在实际的软件开发中都非常常用。比如,Java中LinkedHashMap 就采用了链表法解决冲突,ThreadLocalMap 是通过线性探测的开放寻址法来解决冲突。
优点:
缺点:
总结一下,当数据量比较小、装载因子小的时候,适合采用开放寻址法。这也是 Java 中的 ThreadLocalMap 使用开放寻址法解决散列冲突的原因。
总结一下,基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。
Java 中的 HashMap 是一个经常用到的散列表,来具体看下,这些技术是怎么应用的。
int hash(Object key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}
// 其中, hashCode() 返回的是 Java 对象的 hash code。比如 String 类型的对象的 hashCode() 就是下面这样:
public int hashCode() {
int var1 = this.hash;
if(var1 == 0 && this.value.length > 0) {
char[] var2 = this.value;
for(int var3 = 0; var3 < this.value.length; ++var3) {
var1 = 31 * var1 + var2[var3];
}
this.hash = var1;
}
return var1;
}
标签:代码 一半 探测 插入数据 ret object 攻击 响应 功能
原文地址:https://www.cnblogs.com/xiexiandong/p/13369708.html