【查找算法】哈希查找

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

何为哈希查找?

先看定义:

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

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

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

散列表

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

计算元素存储地址的基本思想是:记录的存储位置与关键字之间存在对应关系,这个对应关系称为一个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. 二次探测法:即以二次序列(如1^2^、-1^2^、2^2^、-2^2^、...、q^2^)为间隔寻找下一个散列地址
  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−α) (随机探测法)
相关推荐
XuanRanDev3 小时前
【数据结构】树的基本:结点、度、高度与计算
数据结构
王老师青少年编程3 小时前
gesp(C++五级)(14)洛谷:B4071:[GESP202412 五级] 武器强化
开发语言·c++·算法·gesp·csp·信奥赛
DogDaoDao3 小时前
leetcode 面试经典 150 题:有效的括号
c++·算法·leetcode·面试··stack·有效的括号
Coovally AI模型快速验证4 小时前
MMYOLO:打破单一模式限制,多模态目标检测的革命性突破!
人工智能·算法·yolo·目标检测·机器学习·计算机视觉·目标跟踪
可为测控5 小时前
图像处理基础(4):高斯滤波器详解
人工智能·算法·计算机视觉
Milk夜雨5 小时前
头歌实训作业 算法设计与分析-贪心算法(第3关:活动安排问题)
算法·贪心算法
Ai 编码助手5 小时前
在 Go 语言中如何高效地处理集合
开发语言·后端·golang
小丁爱养花5 小时前
Spring MVC:HTTP 请求的参数传递2.0
java·后端·spring
BoBoo文睡不醒5 小时前
动态规划(DP)(细致讲解+例题分析)
算法·动态规划
Channing Lewis5 小时前
什么是 Flask 的蓝图(Blueprint)
后端·python·flask