装载因子越大,说明散列表中的元素越多,空闲位置越少,散列冲突的几率就越大。java
不只插入数据的过程要屡次寻址或者拉很长的链,查找的过程也会所以变得很慢。api
对于没有频繁插入和删除的静态数据集合来讲,很容易根据数据的特色、分布等,设计出完美的、极少冲突的散列函数,由于毕竟以前数据都是已知的。数组
对于动态散列表来讲,数据集合是频繁变更的,事先没法预估将要加入的数据个数,因此也没法事先申请一个足够大的散列表。缓存
随着数据慢慢加入,装载因子就会慢慢变大。当装载因子大到必定程度以后,散列冲突就会变得不可接受。数据结构
这个时候,进行动态扩容,从新申请一个更大的散列表,将数据搬移到这个新散列表中。函数
假设每次扩容咱们都申请一个原来散列表大小两倍的空间。性能
若是原来散列表的装载因子是 0.8,那通过扩容以后,新散列表的装载因子就降低为原来的一半,变成了0.4。大数据
针对数组的扩容,数据搬移操做比较简单。可是,针对散列表的扩容,数据搬移操做要复杂不少。由于散列表的大小变了,数据的存储位置也变了,因此须要经过散列函数从新计算每一个数据的存储位置。优化
以下图所示在原来的散列表中,21这个元素原来存储在下标为0的位置,搬移到新的散列表中,存储在下标为7的位置。this
对于支持动态扩容的散列表,插入一个数据,最好状况下,不须要扩容,最好时间复杂度是 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; }