8.散列表:散列函数、冲突解决与扩容策略

散列表利用数组支持按下标随机访问的时候,时间复杂度为O(1)的特性,

  • 存储时通过散列函数把键值转化为下标,将数据存储在数组中对应下标的位置

  • 查询时也同样利用散列函数计算出下标,取出数据

  • 散列表三个关键

    • 散列函数:hash值尽可能分布均匀,同时不能太复杂影响效率

    • 装载因子:根据响应时间是否敏感控制大小(执行效率vs内存空间)

    • 冲突解决:保证最坏情况下的查询效率,抵御散列表碰撞攻击(跳表、红黑树)

  • 散列函数设计的基本要求

    • 散列值是一个非负整数

    • 如果key1 = key2,那么hash(key1) == hash(key2)(相同键值映射到相同表项)

    • 如果key1 != key2,那么hash(key1) != hash(key2)(但散列冲突是无法完全避免的)

    • 构造哈希函数的几个方法

      • 数字分析法:数据关键字存在分布比较均匀的若干位,那么将这若干位提取出作为哈希地址(如学号长六位:年级-班级-序号,显而易见可以将序号抽取出作为学号的哈希地址)。

      • 平方区中法:取关键字平方的中间几位作为哈希地址,这样哈希地址与关键字的多位值均有关,使得分布较均匀。

      • 折叠法:将关键字分割成长度相同的多段,然后将它们的叠加和作为哈希值。这种方法和平方取中理念相同,使关键字各位值都对哈希值有影响。

      • 除留余数法:将关键字除以一个不大于哈希表长度的正整数P,所得余数作为哈希地址。这种方法的好坏取决于P的选取,实践证明,当P取小于哈希表长的最大质数时,产生的哈希函数较好。Java1.8中String对象的哈希函数采用变体的折叠法加除留余数法

  • 装载因子

    • 表示散列表中空位的多少

    • 散列表的装载因子 = 填入表中的元素个数 / 散列表的长度

    • 当散列表的装载因子超过某个阈值时,就需要进行扩容。如果内存空间不紧张,对执行效率要求较高,可以降低装载因子的阈值;相反,如果内存空间紧张,对执行的效率要求又不高,可以适当增加阈值,甚至可以大于1

  • 散列冲突解决

    • 开放寻址法

      • 线性探测:当发生散列冲突后,从当前位置开始,依次往后找,直到找到或到达空闲位置为止(这种方法需要注意删除时不能直接将值设为空,查找时遇到空会结束查找,使得后面的数据无法被探测到)

      • 二次探测:线性探测的步长为1,二次探测步长为原来的二次方

      • 双重散列:使用一组散列函数,第一组散列冲突了换第二组……

    • 链表法:所有散列值相同的元素放到相应槽位的链表中。当然,这里链表可以换成跳表、红黑树等效率更高的数据结构

  • 扩容策略:当负载因子达到设定的阈值那么就需要扩容操作来防止散列表查询性能下降

    • 一次性扩容:需要扩容时申请一个n倍当前大小的空间,将原来散列表的所有数据全部重新计算散列值,加入到新表中,最后将要插入的新值加入。

      • 有了前面数组插入分析的基础,这种方式散列表插入的均摊时间复杂度为O(1)

      • 这种策略下,大多数时候插入数据都很快,但在需要扩容时,插入数据就会变得很慢,甚至无法接受(假设当前散列表大小1GB,那插入时需要重新计算1GB数据的哈希值,并移到新的散列表,听起来就十分耗时)

    • 穿插扩容:将扩容过程穿插在插入过程中。负载达到阈值后,新数据都插入新散列表中并且从原来的散列表取出一个数据插入新表,经过多次插入过程,原来的散列表就慢慢的转移到新表中了

      • 插入时间复杂度仍为O(1)

      • 采用这种方式时,查询数据首先在原来的散列表中查询,如果没有则在新表中查询

转载请注明原文地址:https://www.cnblogs.com/codespoon/p/13253849.html

你可能感兴趣的:(8.散列表:散列函数、冲突解决与扩容策略)