【查找算法】哈希查找

本篇文章将介绍一种查找算法------哈希查找。

何为哈希查找?

先看定义:

哈希查找是通过计算数据元素的存储地址进行查找的一种方法。

哈希查找通过给定的哈希函数构造哈希表(也叫散列表),然后通过计算存储地址进行元素查找。

所以我们先来聊聊散列表。

散列表

散列是一种新的存储方式,它既不是按给定形式顺序存储,也不是毫无规律地进行存储,而是通过计算元素的存储地址实现存储。

计算元素存储地址的基本思想是:记录的存储位置与关键字之间存在对应关系,这个对应关系称为一个hash函数。

举个例子,现有一个数据元素序列,{1,3,5,7,9},若规定每个元素k的存储地址H(k) = k,则其存储结构如下: 这样的存储方式在查找元素的时候是非常方便的,比如想要查找元素值7,就把7代入hash函数,得到存储地址为7,此时比较下标为7的元素值,若相等,查找成功;或不相等,查找失败。

由此可以发现,哈希查找法的查找效率是非常高的,甚至可以达到O(1), 但是缺点也很明显,比较浪费存储空间。

冲突

散列表在构造过程中难免会产生一些冲突,何为冲突?

冲突指的是不同的元素值被映射到了同一个散列地址。

举个例子,有一个数据元素序列,{25,21,39,9,23,11},其hash函数为H(k) = k mod 7,则其存储结构为: 该元素序列中的每个元素求模7,其结果均在0~6之间,先看第一个元素25,因为25 % 7 = 4,所以25存放在下标为4的位置: 因为21 % 7 = 0,所以21存放在下标为0的位置: 因为39 % 7 = 4,按理说39应该存放在下标为4的位置,但你会发现,下标为4的位置已经有元素值了,而你第二次准备存入的元素值又跟其不同,所以冲突便产生了。

这些具有相同函数值的关键字被称为同义词。

构造散列函数

我们说冲突是不同的元素映射到了同一个散列地址,而地址是由hash函数决定,所以想要避免冲突,就需要设计完美的hash函数。

但这是无法实现的,在散列查找方法中,冲突是不可避免的,只能说是尽可能地避免冲突的产生。

为了尽可能减少冲突,hash函数的设计就有一些讲究:

  1. 所选函数尽可能简单,以便提高转换速度
  2. 所选函数对元素值计算出的地址,应在散列地址中均匀分布,以减少空间浪费

构造hash函数需要考虑的因素有:

  • 元素长度
  • 散列表的大小
  • 元素的分布情况
  • 查找频率

这里介绍两种构造hash函数的方式:

  1. 直接定址法
  2. 除留余数法

直接定址法

先看第一种方式,直接定址法。

<math xmlns="http://www.w3.org/1998/Math/MathML"> H a s h ( k e y ) = a ∗ k e y + b ( a 、 b 为常数 ) Hash(key) = a * key + b(a、b为常数) </math>Hash(key)=a∗key+b(a、b为常数)

它的优点是:以关键码key的某个线性函数值为散列地址,不会产生冲突。

除留余数法

这种方法较直接定址法更为常用一些。

<math xmlns="http://www.w3.org/1998/Math/MathML"> H a s h ( k e y ) = k e y m o d p ( p 为整数 ) Hash(key) = key mod p(p为整数) </math>Hash(key)=keymodp(p为整数)

该方法的关键是如何能够取到合适的p值?

若表长为m,则p应取小于等于m的质数。

解决冲突的方式

前面说过了,冲突是无法避免的,那么在产生冲突时该如何解决如何处理呢?

这里介绍两种常见的方式:

  1. 开放地址法
  2. 链地址法

开放地址法

开放地址法的基本思想为:有冲突时就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素存入。

这下一个散列地址该如何找呢?也可以分为三种方式:

  1. 线性探测法:即以线性序列(如1、2、3、...、m - 1)为间隔寻找下一个散列地址
  2. 二次探测法:即以二次序列(如12、-12、22、-22、...、q2)为间隔寻找下一个散列地址
  3. 伪随机探测法:即以一个伪随机数为间隔寻找下一个散列地址

举个例子,比如有一个序列{47,7,29,11,16,92,22,8,3},散列表的表长为m = 11,hash函数为Hash(key) = key mod 11,我们试着用线性探测法来解决一下存储过程中的冲突。 现在准备存入元素47,因为47 % 11 = 3,所以将47存放在下标为3的位置: 因为7 % 11 = 7,所以将7存放在下标为7的位置: 因为29 % 11 = 7,所以将29存放在下标为7的位置,但该位置已经有了元素值,且两者不相同,此时产生冲突。现在我们利用线性探测法,即在该位置上加上一个序列值,7 + 1 = 8,所以将其存放到下标为8的位置: 接下来是11、16、92,它们在存放过程中均没有产生冲突,直接存入即可: 下一个元素是22,因为22 % 11 = 0,而下标0的位置已经有了元素值,所以采用线性探测法,在此位置的基础上加上一个序列值,0 + 1 = 1,所以将其存放到下标为1的位置: 元素8也是如此,它应该存放到下标为9的位置: 最后看元素3,因为3 % 11 = 3,所以存放位置应为下标3,但下标3位置已有元素,于是采用线性探测法,加上一个序列值,3 + 1 = 4,但下标4的位置仍然有元素,那就加第二个序列值,3 + 2 = 5,下标5的位置还是有元素,那就再加第三个序列值,3 + 3 = 6,直至找到空的散列地址。

此时下标为6的位置是空的,所以存入元素3: 查找过程也是一样的,比如11,让其与11求模,余数为0,所以到下标为0的位置找出元素值比较,相等则查找成功;

或者8,让其与11求模,余数为8,所以到下标为8的位置找出元素值比较,发现不相等,根据线性探测法,你就需要到下一个序列值对应的位置查找,也就是下标9,此时元素值相等,查找成功。

还有两种方式,二次探测法和伪随机探测法,原理是一样的,无非就是地址间隔不一样,就不重复讲解了。

链地址法

第二种处理冲突的方法就是链地址法。

其基本思想为:相同散列地址的记录链成一个单链表,m个散列地址就有m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构。

比如一个元素序列{19,14,23,1,68,20,84,27,55,11,10,79},其hash函数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> H a s h ( k e y ) = k e y m o d 13 Hash(key) = key mod 13 </math>Hash(key)=keymod13

这里面很多元素就会产生冲突,比如元素14、1、27、79求模13的结果都是1,所以我们可以将这些同义词链成一个单链表,然后存储在数组中对应的位置,其函数结果为1,就将单链表的表头指针存储在数组下标为1的位置。 其建立散列表的方式也非常简单:

  1. 取数据元素的关键字key,计算其散列函数值。若该地址对应的链表为空,则将该元素插入此链表,否则执行步骤2解决冲突
  2. 根据选择的冲突解决方案,计算关键字key的下一个存储地址。若该地址对应的链表不为空,则利用链表的前插法或后插法将该元素插入到此链表中

查找效率分析

若使用平均查找长度ASL来衡量查找算法,ASL取决于:

  • hash函数
  • 处理冲突的方法
  • 散列表的装填因子α

其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> α = 表中填入的记录数 哈希表的长度 α = \frac{表中填入的记录数}{哈希表的长度} </math>α=哈希表的长度表中填入的记录数

α越大,表中记录数越多,说明表装得越满,发生冲突的可能性就越大,查找时比较次数就越多;反之,相反。

所以哈希查找的效率只是理论上能达到O(1),根据解决冲突的方式不同,查找效率也有所不同:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> A S L ≈ 1 + α 2 ASL ≈ 1 + \frac{α}{2} </math>ASL≈1+2α (链地址法)
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> A S L ≈ 1 2 ( 1 + 1 1 − α ) ASL ≈ \frac{1}{2}(1 + \frac{1}{1 - α}) </math>ASL≈21(1+1−α1) (线性探测法)
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> A S L ≈ − 1 α I n ( 1 − α ) ASL ≈ -\frac{1}{α}In(1 - α) </math>ASL≈−α1In(1−α) (随机探测法)
相关推荐
Asthenia041243 分钟前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom1 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04123 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫