哈希表和哈希冲突

194 阅读5分钟

参考

哈希相关的基本概念

  • 哈希表:
    • 给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
  • 哈希算法:
    • 根据设定的哈希函数f(key)和处理冲突方法将一组关键字映象到一个有限的地址区间上的算法。也称为散列算法、杂凑算法。
  • 哈希冲突:
    • 由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。
    • 例如:
      hash表的大小为9(即有9个槽),现在要把一串数据存到表里5,28,19,15,20,33,12,17,10
      f(5)=5, 所以数据5应该放在hash表的第5个槽里;
      f(28)=1,所以数据28应该放在hash表的第1个槽里;
      f(19)=1,也就是说,数据19也应该放在hash表的第1个槽里——于是就造成了碰撞(也称为冲突,collision)。
      

hash 函数的构造

  • 原则:
    • 计算简单:散列函数的计算时间不应该超过其他查找技术和关键字比对时间
    • 地址均匀分布
  • 直接定址法
    • 哈希函数为关键字的线性函数
  • 数字分析法
  • 平方取中法
  • 折叠法
  • 除留余数法
  • 这里具体参考这篇文章

解决哈希冲突的方法

什么是哈希冲突?

  • 对于给定的关键值value1,value2 (value1 != value2),根据哈希函数计算 f(value1) == f(value2),因此存储位置冲突,那么就需要采取方法解决这个问题。
  • 解决哈希冲突的方法一般有:开放定址法、链地址法(拉链法)、再哈希法、建立公共溢出区等方法。

开放定址法

  • 从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。
  • 开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记。只到有下个元素插入才能真正删除该元素。
  • 线行探查法
    • 线行探查法是开放定址法中最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。
    • 当新值到达时,发现目标位置非空,因此依次往后找,直到空闲的单元进行存储。 1.jpeg 2.jpeg
  • 平方探查法
    • 平方探查法即是发生冲突时,用发生冲突的单元d[i], 加上 1²、 2²等。即d[i] + 1²,d[i] + 2², d[i] + 3²…直到找到空闲单元。
    • 在实际操作中,平方探查法不能探查到全部剩余的单元。不过在实际应用中,能探查到一半单元也就可以了。若探查到一半单元仍找不到一个空闲单元,表明此散列表太满,应该重新建立。

链地址法(拉链法)

  • 链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
    • Redis采用的方法就是这种拉链法。来看下面例子。新键值对计算应该存到二号,二号此时已经有一个键值对了。因此,直接通过链表的方式挂到二号键值对1的下面。 3.jpeg
    • 对于新的键值对也是如此,通过链表的方式挂到二号键值对2的下面。 4.jpeg
  • Rehash
    • 负载因子 = 散列表内元素个数/散列表的长度
    • 如果负载因子高(大于1)就说明哈希冲突概率大,查找效率低
    • 如果负载因子低(小于0.1)就说明哈希表占用了太多空间,大部分都是空的。
    • 为了使负载因子值在合理范围内,程序需要对哈希表进行扩展或收缩。由于空间变大或缩小,之前的键在老表的存储位置,在新表中就不一定一样了,需要重新计算。这个重新计算,并把老表元素转移到新表元素的过程就叫做rehash

再哈希法

  • 事先准备多个散列函数, 每当发生散列冲突就换一个散列计算函数, 当然也相应的增加了计算时间.

建立公共溢出区

  • 将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。