2021-09-12

哈希表是一种通过hash函数实现快速查找的数据结构,如C++的unordered_map。理想情况下,查询复杂度为O(1)。常见的hash函数包括平方取中、折叠法和除留余数法。冲突处理方法主要有开放寻址法(如线性探测再散列)和链地址法。开放寻址法空间利用率高但查询效率可能较低,链地址法则相反。除留余数法中,质数作为模数能更好地避免键值和模数的最大公约数不为1导致的映射问题。
摘要由CSDN通过智能技术生成

hash表学习笔记

目录

hash表及其相关介绍

  hash表也叫散列表,是一种通过hash函数实现映射的数据结构,在C++中unordered_map底层就是是通过hash表实现,这种实现方式使得在查询时效率很高,理想情况下可以达到O(1),但众所周知理想始终是理想,实现还是很难得。


  hash函数:要把键值映射到相应地址需要通过hash函数,或者说hash函数能把键值映射到地址(H(key) = 地址)。hash函数的选择也很多,下面就介绍几种常见的hash函数构造方法:

  • 平方取中法
      当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
  • 折叠法
      将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
  • 除留余数法(最常用)
      取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD M,M<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词(至于为什么选质数后面再说)。

  碰撞(冲突):通过上面的解释我们基本了解了什么时hash函数,如果数据量不大或者空间足够大,那么hash函数的映射绝对没什么问题,查找时间也是很理想的O(1),但是工程中常见情况时数据多且一次能开辟的空间有限,这种情况下就很可能发生碰撞*(冲突),也就是H(key1) = H(key2), key1 = key2,这就出现问题了,就如同人一样,说好一人一套房,凭什么他来得早就有房我来晚了就没有了。面对这个问题,在此介绍两种解决方案——开放寻址法链地址法

  • 开放寻址法
      如果有一个hash函数H()以及键值keym表长,那么所求地址Hi = H(key)(本文使用除留余数法),如果存在冲突,那么使得Hi = (H(key) + di)% m,其中di 为增量序列,这种方法就叫开放寻址法,一般有三种取法:
      1. 线性探测再散列:di = c * i
      2. 平方探测再散列:di = 12,-12,22,-22,…,k2,(k<=m/2)
      3. 伪随机探测再散列:di是一组伪随机数列
    在此对线性探测再散列法进行过程演示:
    数据用例为:11, 22,8, 101,92,57,58,66,31,50
    H(key) = key % m
    Hi = (H(key) + di) % m
    di = 1
Hi0123456789
key(插入11)11
key (插入22)22
key (插入8)8
key (插入101)Hi =1冲突Hi =2冲突101
key (插入92)Hi =2冲突Hi =3冲突92
key (插入57)57
key (插入58)Hi =8冲突58
key (插入66)66
key (插入31)Hi =1冲突Hi =2冲突Hi =3冲突Hi =4冲突31
key (插入66)50
最后结果)50112210192316658858

  以上就是线性探测再散列di = 1时的插入过程,其他方法大致类似,计算出地址,如果冲突再在原地址的基础上加上增量序列,然后再进行计算,直到找到不冲突地址为止。

  • 链地址法
      开放寻地址法是在有限的空间内进行冲突解决,而链地址法则是在冲突地址出开辟出一块新的地址用来存放冲突数据,冲突地址指向新开辟空间,开辟出的空间甚至可以用新的hash函数实现映射,从而提高查找效率。

在这里插入图片描述
  总结一下两种方法,开放地址法:空间利用率高,但查询效率可能稍差;链地址法:查询效率高,但是可能存在空间浪费。两种方法的具体使用还是要结合实际情况来决定,平衡好空间和效率两方面因素,做出最后的决定。

关于除留余数法分母的选择

  前文提到的取模的数也就是M的选择最好选择质数,但原因没有说明,因为我觉得这个点还是挺有意思的所以就想拿出好好说说。这段内容主要是由知乎的回答整理而来,最后我会附上原文地址。
  所谓hash,就是把一个集合A映射到另一个集合B,如{0,1,…,M-1,…,3M+1}映射到{0,1,…,M-1}。如果A集合的分布如{0,1,2,4,…}这样的话,那么无论M取质数还是合数都可以,最后都会被映射到{0,1,2,…,M}。
  但是很多时候元素的步长并不为1,如A={0,6,12,18,24,30,32,…},M取合数10,那么最终的映射集合是{0,6,2,8,4,0,2,4},{0,2,4,6,8},这和我们期望的{0,1,2,3,4,5,6,7,8}并不一样。
  如果key和M的最大公约树为1,那么key%M后得到的所有余数r的集合R={0,1,…,m-1},每个r代表一个集合,如上面例子中的余数0的集合就是{0,30},这个集合称为 “同余类”。余数集合R中由M个余数,每个r都代表一个同余类,那么理想的方式就是A集合映射到R集合,也就是令R=B。
  下面我们来看看有时候无法完全利用集合R的原因是什么。假设key和m的最大公约数不是1,那么就有key=kn,M=km,此时可以将key % M= r转化为key = Mq + r,即kn = kmq + r,其中q是商,r是余数。接下来再对式子进行变换n = mq+r/k,由于n和mq是整数,所以r/k也是整数,此时我们再看(r/k)的取值范围{0, 1, 2, …, m} = {0, 1, 2, …, M/k},接着我们恢复原式,那么此时实际的r取值范围就是{0,k,2k,…,mk},也即是说实际映射到的余数数量缩小了k倍。
  由此我们就知道了为什么要选择是质数来取模,就是为了保证键值和M最大公约数为1。

引用文章:

https://baike.baidu.com/item/哈希表/5981869?fromtitle=Hash表&fromid=8485677&fr=aladdin

https://www.zhihu.com/question/20806796/answer/159392465

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值