【散列表】设计一个工业级别的散列表

  • 工业级的散列表具有的特性

    支持快速的查询、插入、删除操作

    内存占用合理,不能浪费过多的内存空间

    性能稳定,极端情况下,散列表的性能也不会退化到无法接收的情况

  • 散列表的设计思路

    设计一个合适的散列函数

    定义装载因子阈值,并设计动态扩容策略

    选择合适的散列冲突解决方法

  • 装载因子过大怎么办?

    针对散列表,当装载因子过大时,可以进行动态扩容,重新申请一个更大的散列表,将数据搬移到这个新散列表。但是,数据搬移操作对于散列表要复杂很多,因为散列表的大小变了,数据存储位置也变了,所以需要通过散列函数重新设计每个数据的存储位置。这样插入一个数据最坏情况下的时间复杂度为O(n)

    当装载因子超过某个阈值时,就需要扩容。装载因子阈值的设置要权衡时间、空间复杂度。如果内存空间不紧张,对执行效率要求很高,可以降低负载因子的阈值;相反,如果内存空间紧张,对执行效率要求又不高,可以增加负载因子的值,甚至可以大于1(最大装载因子默认是0.75)

  • 如何避免低效地扩容?

    为了解决一次性扩容的机制导致耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。

    当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。经过多次插入操作后,老的散列表中的数据就一点点搬移到新散列表中。

    这期间的查询操作怎么做呢?先从新散列表中查找,如果没有找到则找老散列表。

    通过这样均摊的方法,在任何情况下,插入一个数据的时间复杂度都是O(1)

  • 如何选择冲突解决方法?

    java中LinkedHashMap采用了链表法解决冲突,ThreadLocalMap是通过线性探测的开放寻址法来解决

    • 1.开放寻址法

      优点:

      散列表中的数据都存储在数组中,可以有效的利用CPU缓存加快查询速度,而且序列化起来比较简单。

      缺点:

      删除数据的时候比较麻烦,需要特殊标记已经删除掉的数据。而且所有的数据都存储在一个数组中,比起链表冲突的代价更高。因此它的装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间

      开放寻址法只能适用装载因子小于1的情况。接近1时,就可能会有大量的散列冲突,导致大量的探测、再散列等,性能会下降很多。

      当数据量比较小、装载因子小的时候,适合采用开放寻址法。

    • 2.链表法

      优点:

      链表法对内存的利用率比开放寻址法要高,因为链表结点可以在需要的时候再创建。

      链表法比起开放寻址法,对大装载因子的容忍度更高。只要散列函数的值随机均匀,即便装载因子变成10,也就是链表的长度变长而已,虽然查询效率有所下降,但比起顺序查找还是快很多。

      缺点:

      链表因为要存储指针,所以对于比较小的对象的存储,是比较消耗内存的,还有可能会让内存的消耗翻倍。而且,因为链表中的结点是零散分布在内存中的,不是连续的,所以对CPU缓存不是很友好(但是存储的是大对象时,对象的大小远远大于一个指针的大小,那么链表中指针的内存消耗在大对象面前就忽略了)

      实际上,将链表改成跳表、红黑树,这样即便出现散列冲突,极端情况下,所有的数据都散列到同一个桶内,最终退化成的散列表的查找时间也不过是O(logn),这样就有效避免了散列碰撞攻击

      基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用跳表代替链表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值