散列 哈希

散列是一种以常数平均时间执行插入删除和查找的技术;散列表无法寻找最大最小值;与排序相关的应当选择二叉查找树
  1. 实现散列表的方法:
    1. 理想的散列表数据结构只是一个包含一些项的具有固定大小的数组
    2. 散列函数:解决关键字映射数组的下标问题,
      1. 一种方法是将将关键字用ASCII码加起来得到一个整数,再去对表的长度取余数
      2. 一种方法是将将关键字用ASCII码乘起来得到一个整数,再去对表的长度取余数
      3. horner法则: 计算一个31的n次多项式,31是一个质数,同时它的值不大不小可以减少冲突域,31以上的素数也行
        1. String中的hashCode()
        2. public static int hash( String key ,int tablesize){
               int hashVal=0;
               for(int i=0;i<key.length();i++){
                   hashVal=31*hashVal+key.charAt(i);
               }
               hashVal%=tablesize;
              if(hashVal<0)
                   hashVal+=tablesize;
             return hashVal;
          }

           

  2. 解决散列冲突(哈希冲突):如果一个元素插入时,该位置已经有值,那么就造成冲突
    1. 分离链接法:将发生冲突的元素保留在一个表中
      1. 因为使用双向链表,如果空间不允许应当避免使用
      2. 也就是散列表存储数组声明成链表数组,数组的每一个值为链表的表头
      3. 当插入新元素时候,使用头插法,使得新元素在前面
      4. 装填因子:表中元素数量  /  表的大小。一般差不多为1左右,如果超过就需要rehash()扩大散列表的大小
    2. 探测散列法:装填因子小于0.5
      1. 线性探测法:通过哈希计算出下表后,检测是否哈希冲突,是的话就在那个位置下寻找另一个空闲位置
        1. 一次聚集:散列到区块中的任何关键字都需要多次试选单元才能解决冲突,然后改关键字被添加到对应的区块中
      2. 平方探测法:发生哈希冲突时,f(i)=i^2下一个不为空则寻找哈希值平方位置(表的长度为素数)
        1. 定理:如果表的大小是素数,那么当表至少有一半为空的时候总能够插入一个新元素
        2. 即使仅仅比一半多一个,插入都有可能失败
        3. 二次聚集:散列到同一位置的那些元素将探测相同的备选单元
      3. 探测散列法中表的标准删除操作不能进行。需要懒惰删除,即用个标示符表示删除,允许被插入
      4. 装填因子大于0.5的话需要将散列表放大,也就是在散列
      5. 双散列法:发生冲突后,一种流行的方式是f(i)=i*hash2(x);再次哈希,
        1. 也就是说Hash1()计算后得到的位置如果占有了,那么就再用hash2()计算,再在hash1的结果上增加一个增量,增量就是hash2的结果
        2. hash2(x)选择不正确会引发灾难性结果
        3. 表的长度必须是素数
    3. 再散列:由平方探测法的散列表装得太满(超过一半)那么就需要扩大散列表,要不然会导致运行时间过长,插入可能失败
      1. 思路:
        1. 创建一个新的散列表,大小约为原来的两倍的素数
        2. 扫描原有散列数组,将未删除的重新计算新的散列值。并将插入新散列中     
      2. 再散列的时机:
        1. 表满到一半时
        2. 只有插入失败时
        3.  途中策略:当散列达到某一个装填因子时进行再散列   
      3. 装填因子的增长确实导致散列表的性能下降。所以途中策略的截止手段可能是最好的策略
  3. 标准库中的散列表
      1. HashSet和HashMap     
      2. HashSet中的项或HashMap 中的关键字必须提供equals方法和hashcode()方法:如String
      3. 实现方法:分离链接散列法
      4. HashMap性能常常优于TreeMap
      5. String 中hashCode()优化:String对象初始化的hashCode值为0,调用hashCode函数后这个值就被记住,可以避免String对象的第二次计算,直接使用就可以,这个技巧称为闪存散列代码,而且表示一种经典的时空交换
        1. 之所以闪存散列代码有效,是因为String是不可改变的,改变String时,hashCode重置0
  4. 最坏情形下O(1)访问散列表
    1. 完美散列:与分离链接法的区别在于发生冲突后不是使用链表,而是创建一个二级散列表去解决冲突,使得原散列表作为一个槽
      1. 也就是将映射到槽的元素再从该槽二级散列表的全域散列函数簇选取出一个散列函数对元素进行再次散列,最终确定目标位置。
    2. 布谷鸟散列:利用两张散列表,两个独立散列函数分别计算每个项分别在了两张表中的位置
      1. 每个元素都有两个哈希值分别代表在表中的位置,当项插入时候,先插入表1,没冲突就插入,如果发生冲突,就把原来那个踢去表2,再插入,如果那个元素在表2中冲突,就把表2那个踢去表1。。。循环
      2. 有分析证明,当装填因子小于0.5的时候,循环的概率非常低,接近0.5就快速恶化
      3. 如果若干次替换被检测到,简单的做法就是替换散列函数,重新建立表
      4. 也可以不拘谨于两张表,而是再使用多一些表 
      5. 许多常用的散列函数都不满足
      6. 实现:
        1. 一般限制0.4,超出就扩大表
        2. 需要一个一个散列函数的集合,因为任何hashCode冲突将导致集合中所有散列函数冲突
        3. 插入的过程:
          1. 存在就返回
          2. 如果表满载(也就是装填因子过大)就扩展
          3. 声明变量追踪尝试插入的次数,达到一定界限就再散列
          4. 为空就插入
          5. 否则就替换,踢皮球。再不然就替换散列函数
          6. 限制循环次数为奇数(3),超过就再散列
    3. 跳房子散列:尝试对线性探测法的改进。给探测的范围一个常数界限,
      1. 主要两点:
        1. 确定最长探测距离
        2. 找到可以替换的数
      2. 如果插入的位置离散列的位置太远,那么可以掉头向散列的位置走去,替换潜在的项,并保证那些被替换掉的项不会离他散列的位置太远
      3. 即给定一个散列函数,那些项可以被替换,或者不可以。不可以的话就说明这表太挤了,该再散列了
  5. 通用散列函数
    1. 散列函数必须可在线性时间内计算(与项数无关)
    2. 散列函数必须将项均匀分布到数组单元中
    3. 要设计一个简单通用的散列函数,我们将首先假设会把非常大的整数映射到0~M-1的范围内比较小的整数,令P为比最大输入值大的一个素数得散列族
    4. H={  H(a,b)(x)=((ax+b)%p)%M       1<=a<=p-1   0<=b<=p-1 }a,b随机的
 
 
posted @ 2020-04-19 22:35  浪波激泥  阅读(172)  评论(0)    收藏  举报