16 散列表(中):如何打造一个工业级水平的散列表?
1. 如何设计散列函数?
- 散列函数的设计不能太复杂,太复杂会消耗更多时间,也会影响到散列表的性能。
- 散列函数生成的值要尽可能随机并且均匀分布,尽可能减少散列冲突,即便冲突之后,分配到每个槽内的数据也比较均匀。
- 常见的散列函数设计方法:直接寻址法、平方取中法、折叠法、随机数法等。
2. 装载因子过大怎么办?(如何根据装载因子动态扩容?)
- 如何设置装载因子阈值?
1)可以通过设置装载因子的阈值来控制是扩容还是缩容,支持动态扩容的散列表,插入数据的时间复杂度使用摊还分析法。
2)装载因子阈值的设置要权衡时间、空间复杂度。如果内存空间不紧张,对执行效率要求很高,可以降低负载因子的阈值;相反,如果内存空间紧张,对执行效率要求又不高,可以增加负载因子的值,甚至可以大于 1。 - 如何避免低效扩容?一次性扩容,分批搬运
利用均摊思想。
1)分批搬运的插入操作:当有新数据要插入时,我们将数据插入新的散列表,并且从老的散列表中拿出一个数据放入新散列表。每次插入都重复上面的过程。这样插入操作就变得很快了。
2)分批搬运的查询操作:先查新散列表,再查老散列表。
3)通过分批搬运的方式,任何情况下,插入一个数据的时间复杂度都是O(1)。
3. 如何选择散列冲突解决方法?
- 开放寻址法
【优点】
1)开放寻址法不像链表法,需要拉很多链表。散列表中的数据都存储在数组中,可以有效地利用 CPU 缓存加快查询速度。
2)序列化起来比较简单。链表法包含指针,序列化起来就没那么容易。
【缺点】
1)删除数据比较麻烦
2)冲突的代价更高
3)装载因子不能过高
【适用场景】
当数据量比较小、装载因子小的时候,适合采用开放寻址法。 - 链表法
【优点】
1)链表法对内存的利用率比开放寻址法要高。
2)链表法比起开放寻址法,对大装载因子的容忍度更高。
【缺点】
1)对CPU缓存不友好
2)不利于序列化
【适用场景】
基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。
4. 如何设计一个工业级的散列函数?
思路:
何为一个工业级的散列表?工业级的散列表应该具有哪些特性?结合学过的知识,我觉的应该有这样的要求:
- 支持快速的查询、插入、删除操作;
- 内存占用合理,不能浪费过多空间;
- 性能稳定,在极端情况下,散列表的性能也不会退化到无法接受的情况。
方案:
如何设计这样一个散列表呢?根据前面讲到的知识,我会从3个方面来考虑设计思路: - 设计一个合适的散列函数;
- 定义装载因子阈值,并且设计动态扩容策略;
- 选择合适的散列冲突解决方法。
关于散列函数、装载因子、动态扩容策略,还有散列冲突的解决办法,具体如何选择,还要结合具体的业务场景、具体的业务数据来具体分析。不过只要我们朝这三个方向努力,就离设计出工业级的散列表不远了。
5. 思考
- 在你熟悉的编程语言中,哪些数据类型底层是基于散列表实现的?散列函数是如何设计的?散列冲突是通过哪种方法解决的?是否支持动态扩容呢?
Python中的dict就是基于散列表实现。
6. 参考资料
- 王争老师在极客时间的专栏《数据结构与算法之美》
- 专栏下的所有评论
7. 声明
本文章是学习王争老师在极客时间专栏——《数据结构与算法之美》的学习总结,文章很多内容直接引用了专栏下的回复,推荐大家购买王争老师的专栏进行更加详细的学习
。本文仅供学习使用,勿作他用,如侵犯权益,请联系我,立即删除。