【16】 散列表(中):如何打造一个工业级水平的散列表?

1. 如何设计散列函数?

  1. 散列函数的设计不能太复杂,太复杂会消耗更多时间,也会影响到散列表的性能。
  2. 散列函数生成的值要尽可能随机并且均匀分布,尽可能减少散列冲突,即便冲突之后,分配到每个槽内的数据也比较均匀。
  3. 常见的散列函数设计方法:直接寻址法、平方取中法、折叠法、随机数法等。

2. 装载因子过大怎么办?(如何根据装载因子动态扩容?)

  1. 如何设置装载因子阈值?
    1)可以通过设置装载因子的阈值来控制是扩容还是缩容,支持动态扩容的散列表,插入数据的时间复杂度使用摊还分析法。
    2)装载因子阈值的设置要权衡时间、空间复杂度。如果内存空间不紧张,对执行效率要求很高,可以降低负载因子的阈值;相反,如果内存空间紧张,对执行效率要求又不高,可以增加负载因子的值,甚至可以大于 1。
  2. 如何避免低效扩容?一次性扩容,分批搬运
    利用均摊思想。
    1)分批搬运的插入操作:当有新数据要插入时,我们将数据插入新的散列表,并且从老的散列表中拿出一个数据放入新散列表。每次插入都重复上面的过程。这样插入操作就变得很快了。
    2)分批搬运的查询操作:先查新散列表,再查老散列表。
    3)通过分批搬运的方式,任何情况下,插入一个数据的时间复杂度都是O(1)。

3. 如何选择散列冲突解决方法?

  1. 开放寻址法
    【优点】
    1)开放寻址法不像链表法,需要拉很多链表。散列表中的数据都存储在数组中,可以有效地利用 CPU 缓存加快查询速度。
    2)序列化起来比较简单。链表法包含指针,序列化起来就没那么容易。
    【缺点】
    1)删除数据比较麻烦
    2)冲突的代价更高
    3)装载因子不能过高
    【适用场景】
    当数据量比较小、装载因子小的时候,适合采用开放寻址法。
  2. 链表法
    【优点】
    1)链表法对内存的利用率比开放寻址法要高。
    2)链表法比起开放寻址法,对大装载因子的容忍度更高。
    【缺点】
    1)对CPU缓存不友好
    2)不利于序列化
    【适用场景】
    基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

4. 如何设计一个工业级的散列函数?

思路:
何为一个工业级的散列表?工业级的散列表应该具有哪些特性?结合学过的知识,我觉的应该有这样的要求:

  1. 支持快速的查询、插入、删除操作;
  2. 内存占用合理,不能浪费过多空间;
  3. 性能稳定,在极端情况下,散列表的性能也不会退化到无法接受的情况。
    方案:
    如何设计这样一个散列表呢?根据前面讲到的知识,我会从3个方面来考虑设计思路:
  4. 设计一个合适的散列函数;
  5. 定义装载因子阈值,并且设计动态扩容策略;
  6. 选择合适的散列冲突解决方法。
    关于散列函数、装载因子、动态扩容策略,还有散列冲突的解决办法,具体如何选择,还要结合具体的业务场景、具体的业务数据来具体分析。不过只要我们朝这三个方向努力,就离设计出工业级的散列表不远了。

5. 思考

  1. 在你熟悉的编程语言中,哪些数据类型底层是基于散列表实现的?散列函数是如何设计的?散列冲突是通过哪种方法解决的?是否支持动态扩容呢?
    Python中的dict就是基于散列表实现。

6. 参考资料

  1. 王争老师在极客时间的专栏《数据结构与算法之美》
  2. 专栏下的所有评论

7. 声明

本文章是学习王争老师在极客时间专栏——《数据结构与算法之美》的学习总结,文章很多内容直接引用了专栏下的回复,推荐大家购买王争老师的专栏进行更加详细的学习。本文仅供学习使用,勿作他用,如侵犯权益,请联系我,立即删除。

发布了128 篇原创文章 · 获赞 157 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_27283619/article/details/102311285
16
今日推荐