散列表

作者: 蹩脚的小三 | 来源:发表于2019-12-02 22:08 被阅读0次

    一、散列表的由来?

    1. 散列表来源于数组,它借助散列函数对数组这种数据结构进行扩展,利用的是数组支持按照下标随机访问元素的特性。
    2. 需要存储在散列表中的数据我们称为键,将键转化为数组下标的方法称为散列函数,散列函数的计算结果称为散列值。
    3. 将数据存储在散列值对应的数组下标位置。

    二、如何设计散列函数?

    总结3点设计散列函数的基本要求

    1. 散列函数计算得到的散列值是一个非负整数。
    2. 若key1=key2,则hash(key1)=hash(key2)
    3. 若key≠key2,则hash(key1)≠hash(key2)
      正是由于第3点要求,所以产生了几乎无法避免的散列冲突问题。

    三、散列冲突的解放方法?

    1. 常用的散列冲突解决方法有2类:开放寻址法(open addressing)和链表法(chaining)
    2. 开放寻址法
    • 核心思想:如果出现散列冲突,就重新探测一个空闲位置,将其插入。
    • 线性探测法(Linear Probing):
      插入数据:当我们往散列表中插入数据时,如果某个数据经过散列函数之后,存储的位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
      查找数据:我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素是否相等,若相等,则说明就是我们要查找的元素;否则,就顺序往后依次查找。如果遍历到数组的空闲位置还未找到,就说明要查找的元素并没有在散列表中。
      删除数据:为了不让查找算法失效,可以将删除的元素特殊标记为deleted,当线性探测查找的时候,遇到标记为deleted的空间,并不是停下来,而是继续往下探测。
      结论:最坏时间复杂度为O(n)
    • 二次探测(Quadratic probing):线性探测每次探测的步长为1,即在数组中一个一个探测,而二次探测的步长变为原来的平方。
    • 双重散列(Double hashing):使用一组散列函数,直到找到空闲位置为止。
    • 线性探测法的性能描述:
      用“装载因子”来表示空位多少,公式:散列表装载因子=填入表中的个数/散列表的长度。
      装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
    1. 链表法(更常用)
      插入数据:当插入的时候,我们需要通过散列函数计算出对应的散列槽位,将其插入到对应的链表中即可,所以插入的时间复杂度为O(1)。
      查找或删除数据:当查找、删除一个元素时,通过散列函数计算对应的槽,然后遍历链表查找或删除。对于散列比较均匀的散列函数,链表的节点个数k=n/m,其中n表示散列表中数据的个数,m表示散列表中槽的个数,所以是时间复杂度为O(k)。

    四、思考

    1. 散列冲突的解放方法有几种
    2. hash函数的设计目标是什么
    3. Word文档中单词拼写检查功能是如何实现的?
      字符串占用内存大小为8字节,20万单词占用内存大小不超过20MB,所以用散列表存储20万英文词典单词,然后对每个编辑进文档的单词进行查找,若未找到,则提示拼写错误。
    4. 假设我们有10万条URL访问日志,如何按照访问次数给URL排序?
      字符串占用内存大小为8字节,10万条URL访问日志占用内存不超过10MB,通过散列表统计url访问次数,然后用TreeMap存储散列表的元素值(作为key)和数组下标值(作为value)
    5. 有两个字符串数组,每个数组大约有10万条字符串,如何快速找出两个数组中相同的字符串?
      分别将2个数组的字符串通过散列函数映射到散列表,散列表中的元素值为次数。注意,先存储的数组中的相同元素值不进行次数累加。最后,统计散列表中元素值大于等于2的散列值对应的字符串就是两个数组中相同的字符串。
    6. 为什么散列表和链表经常会一起使用?
    7. LRU 缓存淘汰算法
    8. linkHashMap的实现
    9. Redis 有序集合
    10. 假设猎聘网有 10 万名猎头,每个猎头都可以通过做任务(比如发布职位)来积累积分,然后通过积分来下载简历。假设你是猎聘网的一名工程师,如何在内存中存储这 10 万个猎头 ID 和积分信息,让它能够支持这样几个操作:
    • 根据猎头的 ID 快速查找、删除、更新这个猎头的积分信息;
    • 查找积分在某个区间的猎头 ID 列表;
    • 查找按照积分从小到大排名在第 x 位到第 y 位之间的猎头 ID 列表

    面试题目:如何设计一个工业级的散列函数?(思考hashmap源码的设计)

    思路:

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

    1. 支持快速的查询、插入、删除操作:设计一个合适的散列函数;
    2. 内存占用合理,不能浪费过多空间:定义装载因子阈值,并且设计动态扩容策略;
    3. 性能稳定,在极端情况下,散列表的性能也不会退化到无法接受的情况:选择合适的散列冲突解决方法。
    方案:

    设计散列函数

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

    根据装载因子动态扩容

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

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

    • 常见的2中方法:开放寻址法和链表法。
    • 大部分情况下,链表法更加普适。而且,我们还可以通过将链表法中的链表改造成其他动态查找数据结构,比如红黑树、跳表,来避免散列表时间复杂度退化成O(n),抵御散列冲突攻击。
    • 但是,对于小规模数据、装载因子不高的散列表,比较适合用开放寻址法。
      Java 中 LinkedHashMap 就采用了链表法解决冲突,ThreadLocalMap 是通过线性探测的开放寻址法来解决冲突

    相关文章

      网友评论

          本文标题:散列表

          本文链接:https://www.haomeiwen.com/subject/wpnowctx.html