avatar

目录
JAVA 散列

JAVA 散列

简述

JAVA 散列表可以用来以常数平均时间来实现 insert 和 查找操作,但它不能保证存储对象的大小顺序。例如它不能查找最小值、最大值或者找出在某一范围内的项,除非准确使用一个字符串
在使用时,需注意:

  • 当关键字不是短的串或整数时,需要仔细选择散列
  • 使用散列表时需注意诸如装填因子这样的细节是特别重要的,否则时间界将不再有效

其应用场景也很多,包括但不限于:

  • 编译器中的符号表,用于跟踪代码中声明的变量
  • 图论问题
  • 游戏程序中的变换表
  • 在线拼写检验程序

散列函数

key 为整数

  • 保证表的大小为素数

    key 为字符串

  • 将字符串的 ascii 值(或unicode)加起来

    • 缺点:当 tableSize 很大时,不能保证均匀分配,一般会集中在表的前端
  • 计算 key 的前三个字符得到的值然后处理

    • 公式:$x + 27 y + 729 z$
      ps:x,y,z分别表示前三个字符所处字母表的位置,例如字母A,x=1;27表示26个字母加上一个空格

    • 缺点:虽然理论上有$26^3$个组合,但实际上有效组合(构成有意义的单词)只有2851个组合,所以当 tableSize 很大时依旧无法保证均匀分配。

  • 根据Horner 法则计算一个 37 的多项式(涉及关键字中的所有字符,并且一般可以分布得很好,但当字符串很长时,不使用所有字符)

    • 公式: $\sum_{i=0}^{KeySize-1}Key[KeySize-i-1]*37^i$

解决冲突

分离链接法

  • 前提
    被排列的对象必须提供适当equals方法和返回一个 int 型的 hashCode 方法

  • 原理
    将散列到同一个值的所有元素保留到一个表中(可以使用标准库表的实现方法,但如果空间很紧,则应该避免使用它们,因为这些表是双向列表且浪费空间),可以参考下图

    QQ截图20200116214308.jpg

  • 影响查找效率的重要因素 装填因子 $\lambda$

    装填因子 $\lambda$ 为散列表中的元素个数对该表大小的比。
    而一次成功的查找则需要遍历大约 $1+\lambda/2$ 个链

  • 一般法则

    使得表的大小与预料的元素个数大致相等,即 装填因子$\lambda\approx1$
    如果装填因子大于1,则应扩大散列表的大小

  • 缺点

    给新单元分配地址需要时间,因此会导致算法的减慢,同时算法还要求对第二种数据结构的实现

开放定址法

即不用链表的方法(探测散列表)

  • 总体思路
    当发生冲突时,尝试另外一些单元,直到找到空的单元为止。可以利用以下公式来寻找
    $h_i(x)=(hash(x)+f(i))\ \ mod\ \ TableSize$ 且 $f(0)=0$
    其中$f(i)$是冲突解决方法,$h_i(x)$为元素位置
  • 特点
    • 该解决方法所需要的表比分离链接法所需要的表要大,一般来说,对于探测散列表,其装填因子$\lambda$应该低于0.5
    • 在探测散列表中,标准的删除操作不能执行,因为相应的单元可能已经引起过冲突,元素绕过他存在了别处。因此探测散列表需要懒惰删除。

线性探测法

  • $f(i)=i$
  • 成功查找的预期探测次数为 $\frac{1}{2}(1+1/(1-\lambda))$
  • 不成功查找的预期探测次数为 $\frac{1}{2}(1+1/(1-\lambda)^2)$
  • 插入的预期探测次数为 $\frac{1}{2}(1+1/(1-\lambda)^2)$
  • 查找次数随 $\lambda$变化的曲线图
    QQ截图20200116221352.jpg

  • 总结
    线性探测法容易造成聚集,当$\lambda>0.5$时查找次数开始明显随着$\lambda$的增大而快速增多,但当$\lambda<=0.5$时则查找次数较小,例如当$\lambda=0.5$时,不成功查找约2.5次,成功查找约1.5次

平方探测法

  • $f(i)=i^2$
  • 特点
    • 其解决了线性探测法的一次聚集问题
    • 产生了二次聚集问题,即散列到同一位置上的哪些元素将探测相同的备选单元
    • 当表有一半为空,且表的大小为素数时,总能插入一个新元素
    • 一旦表被填充超过一半,或当表的大小不为素数时甚至在表被填充一半之前,就不能保证能插入一个新元素了
    • 如果表的大小是形如 4k+3 的素数,且使用的平方冲突解决方法为$f(i)=\pm i^2$,那么整个表均可被探测到,其代价则是例程要稍微复杂点

双散列

  • $f(i)=i*hash_2(x)$
  • $hash_2(x)$选的不好将会是灾难性的,该函数一定不能算得0值且保证所有单元都能被探测到也是很重要的。
    一般认为$hash_2(x)=R-(x\ mod\ R)$(其中R是小于 TableSize 的素数)将起到良好的作用
  • 应保证两个散列表的大小均为素数,否则就有可能导致无法插入新的元素
  • 当双散列正确实现时,模拟表明,预期的探测次数几乎和随机冲突解决的方法情形相同,这使得双散列算法理论上很具有吸引力。但对于像串这样的关键字,它们的散列函数计算起来相当麻烦,这时可以考虑使用平方探测法

再散列

一般当装载因子$\lambda$>=某个值(例如0.5)时,就可以考虑再散列,扩大表,以防止插入失败或者查找次数增多。
再散列的方法有很多例如,建立另外一个大约两倍大的表,扫描整个原始散列表,计算每个(未删除的)元素的新散列值并将其插入到新表中。
何时使用再散列,一般有三个策略:

  • 只要表满到一半就再散列
  • 只有当插入失败才再散列
  • 途中策略:当散列表到达某一个装填因子时再散列

系统标准库的散列

HashMap类 和 HashSet 类,他们通常是使用分离链接散列来实现的,要求存储的对象必须有:

  • hashCode()方法
  • equal()方法

可拓散列

当数据量过大以至于装不进主存的时候,就需要用到可拓散列了

文章作者: f1rry
文章链接: http://yoursite.com/2020/02/03/java 散列/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 F1rry's blog