设计一个工业级应用的散列表
如何设计散列表
如何设计一个散列表能够在散列冲突的情况下应对散列表性能急剧下降,并且能够抵抗散列碰撞攻击
1、散列函数的设计不能太复杂,散列函数生成的值要尽可能随机并且均匀分布
2、考虑关键字的长度 特点 分布还有散列表的大小
装载因子过大怎么办?
装载因子 = 散列表存放的元素个数/散列表大小
装载因子过大说明散列表中元素越多,空闲位置越少,当装载因子大到一定程度的时候散列冲突就变得不可接受,我们可以进行动态扩容。散列表的动态扩容与数组不同,散列表的大小变了,数据的存储位置也发生了改变,因此需要通过散列函数重新计算元素的存储位置。
如何避免低效的扩容?
动态扩容的散列表插入一个数据都很快,但是在特殊情况下,当装载因子已经到达阈值,需要先进行扩容,再插入数据。这个时候,插入数据就会变得很慢,甚至会无法接受。为了解决一次性扩容耗时过多的情况,我们可以将扩容操作穿插在插入操作的过程中,分批完成。
当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,我们都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次性数据搬移,插入操作就都变得很快了。
如何选择冲突的解决方法
1、开放定址法
优点:
①存在数组中可以有效利用CPU缓存加速查询数据。
②序列化起来比较容易。
缺点:
①数据的时候比较麻烦,需要特殊标记已经删除掉的数据。
②放寻址法中,所有的数据都存储在一个数组中,比起链表法来说,冲突的代价更高。所以,使用开放寻址法解决冲突的散列表,装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间。
总结:当数据量比较小,装载因子比较小的时候适合使用开放定址法。
2、链表法
优点:
①内存使用率比开放寻址法高。因为链表结点可以使用的时候再创建,不需要事先申请额外的内存空间
②链表法比起开放寻址法,对大装载因子的容忍度更高。开放寻址法只能适用装载因子小于 1 的情况。接近 1 时,就可能会有大量的散列冲突,导致大量的探测、再散列等,性能会下降很多。但是对于链表法来说,只要散列函数的值随机均匀,即便装载因子变成 10,也就是链表的长度变长了而已,虽然查找效率有所下降,但是比起顺序查找还是快很多。
缺点:
①链表因为要存储指针如果存放的是小对象,消耗的内存会翻倍。
②链表结点是分散存储在内存中的不是连续的,因此对CPU的缓存不太友好,对执行效率也有影响。
总结:我总结一下,基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表
工业级散列表举例分析
java的hashMap:
1、初始大小
初始大小默认为16,如果知道数据大小可以自定义初始大小,减少动态扩容的次数。
2、装载因子和动态扩容
装载因子默认为0.75,动态扩容为原来的两倍
3、散列冲突的解决方法
HashMap底层采用链表来解决冲突,即使负载因子和散列函数设计的再合理都无法避免出现拉链过长的情况,一旦拉链过长会严重影响HashMap的性能。于是在JDK1.8中,引入红黑树代替链表结点,当链表结点超过8时转换为红黑树。当红黑树结点个数小于8个时转换为链表。因为红黑树的个数结点小于8个时,比起链表来性能优势并不明显。
4、散列函数