【数据结构】什么是哈希表(散列表)?

news2024/10/6 3:03:18

🦄个人主页:修修修也

🎏所属专栏:数据结构

⚙️操作环境:Visual Studio 2022


目录

📌哈希表的概念

📌哈希函数的构造方法

🎏直接定址法

🎏除留余数法

🎏平方取中法

🎏折叠法

🎏随机数法

🎏数学分析法

📌哈希冲突

📌哈希冲突的处理方法

🎏闭散列

🕹️线性探测

🕹️二次探测

🎏开散列

🎏开散列与闭散列比较

结语


        在正式开始深入了解哈希表之前呢, 我想带大家先回忆一下生活中咱们的这个"老朋友"。可能你会感到诧异, 我怎么会和它是"老朋友"呢? 别急, 其实你的生活中常常会出现哈希的身影,只是你没有细心观察罢了,不信你看下面几个场景对你来说是不是非常熟悉呢:

        事实上,上面列出的生活中的例子都或多或少的借助了哈希的思想来使得查找和定位变得更加快捷和方便,那么它是具体怎么做到的呢?下面就带大家揭开哈希表神秘的面纱:


📌哈希表的概念

        在我们之前学习过的各种数据结构(线性表/树)中,元素在结构中的相对位置是随机的, 它的关键码(Key)和其存储位置之间没有任何的对应关系。我们在存入时是无逻辑的, 因此在后续查找一个元素时, 往往需要进行多次关键码(Key)的比较, 一般来讲,顺序表查找元素的时间复杂度为O(N), 而二分查找则是O(log_{2}N),平衡树则是它的树高,一般为O(log_{2}N), 查找元素的效率取决于查找过程中元素的比较次数。

        那么有没有理想的情况是不经过任何比较, 一次存取就能得到我们想要的元素?答案是有的,只需要我们在元素的存储位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。这时我们在查找时, 只要根据这个对应关系f找到给定值K的映像f(K)。如果K在这个结构中,那么它一定就在f(K)的存储位置上,因此我们就不需要进行比较就可以直接取到所查的元素。在这个过程中, 我们称这个对应关系F为哈希(Hash)函数, 按照这个思想建立的表结构为哈希表

        只看概念有些抽象,拿上面生活中取奶茶的场景给大家举个例子吧:

        假设我们今天点了一杯奶茶,准备一会儿去店里取,下单后系统提示取餐码是:570。我们到店之后发现取餐台是按照尾号取餐的, 所以计算之后自然一眼就看到了0号位置自己点的奶茶, 向店员展示取餐码后就成功将奶茶取走了,这个过程中是这样运用哈希思想的:

        也就是说,如果我们构造一种存储结构,通过某种函数(hash func)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

        向这个结构中插入元素:

  • 根据待插入元素的关键码, 以此函数计算出该元素的存储位置并按此位置进行存放。

        向这个结构中搜索元素:

  • 对元素的关键码进行同样的计算, 把求得的函数值当作元素的存储位置,在结构中按此位置取元素进行比较,若关键码相等, 则搜索成功。

        这个方式即为哈希(散列)方法, 哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者散列表)。


📌哈希函数的构造方法

        在概念部分,我们频繁的提到了哈希函数, 它是建立起关键码和存储位置映射的桥梁, 无疑是非常重要的。但是从上面生活场景中可以看出, 哈希函数是一个映射, 并且它的设定是很灵活的, 只要使得任何关键字由此所得的哈希函数值都落在表长允许范围之内即可;

        有多么灵活呢?它甚至可以不是通俗意义上的数学函数, 比如上面取奶茶的例子, 假设有家奶茶店的取餐台不是按尾号排布而是按下单顾客的姓氏呢?这种也算是合理的哈希映射,但在实际操作中会有更多的不便之处, 比如有些大姓氏的顾客太多,分配的放置区域不够用; 还比如有些太冷门的姓氏从来都没有被使用过, 但是却白白占着放置区域不让别人使用。

        在对比中, 足以显示出选取合适的哈希函数对效率提升的重要性。我们下面要介绍几种常见的哈希函数设计方法。

        首先明确哈希函数的设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能几乎均匀分布在整个空间中
  • 哈希函数应该比较简单

🎏直接定址法

  • 取关键字的某个线性函数为散列地址:Hash(Key)=A*Key+B (A,B为常数)
  • 优点:简单、均匀
  • 缺点:需要事先知道关键字的分布情况
  • 使用场景:适合查找比较小且连续的情况

        例如我们现在要对0~100岁的人口数字做统计表, 那年龄这个关键字就可以直接使用年龄的数字作为地址。此时f(key)=key:

        对于直接定值法, 我们之前在讲排序时其实也曾经借助过这个思想, 那就是计数排序。

        感兴趣的朋友可以移步这篇博客:【数据结构】八大排序之计数排序算法


🎏除留余数法

        此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:

f(key) = key % p (p<=m)

        %运算符是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。
        很显然,本方法的关键就在于选择合适的p, p如果选得不好,就可能会容易产生同义词。
        例如表8-10-4,我们对于有12个记录的关键字构造散列表时,就用了f (key)=key%12的方法。比如 29 % 12=5,所以它存储在下标为5的位置。

        

        不过这也是存在冲突的可能的,因为12=2×6=3×4。如果关键字中有像18(3×6)、30 (5×6)、42(7×6)等数字,它们的余数都为6,这就和78所对应的下标位置冲突了。
        甚至极端一些,对于表8-10-5的关键字,如果我们让p为12的话,就可能出现下面的情况,所有的关键字都得到了0这个地址数,这未免也太糟糕了点。

        如果我们不选用p=12来做除留余数法,而选用p=11,如表8-10-6所示。

        此就只有12和144有冲突,相对来说,就要好很多。
        根据前辈们的经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。


🎏平方取中法

  • 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
  • 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址;
  • 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

🎏折叠法

        折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

        比如我们的关键字是9876543210,散列表表长为三位,我们将它分为四组:

987 | 654 | 321 | 0

        然后将它们叠加求和987+654+321+0=1962,再求后3位得到散列地址为962。
        有时可能这还不能够保证分布均匀,不妨从一端向另一端来回折叠后对齐相加。比如我们将987和321反转,再与654和0相加,变成789+654+123+0=1566,此时散列地址为566。

        折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况


🎏随机数法

  • 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
  • 通常应用于关键字长度不等时采用此法。

🎏数学分析法

        设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:

        假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。
        数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突


📌哈希冲突

        不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
        把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

        还拿买奶茶做例子, 假设今天买奶茶的人特别多, 取餐台上的奶茶堆积成了这个样子, 那尾号是1,2,3,5,8,9号的顾客还是可以一眼就找到自己的奶茶,因为映射的哈希地址只有自己一份奶茶, 但是0号位置和6号位置的顾客就不像其他顾客那样幸运了,他们必须要在分辨一下这个位置里的几杯奶茶哪杯是自己的,之后才能取到正确的奶茶 :

        可能0号位置的两个顾客的取餐码一个是570,一个是580,他们计算出的哈希地址都是0,因此他们就被放在了一个存储地址空间里,这种现象就被称为哈希冲突


📌哈希冲突的处理方法

🎏闭散列

        闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

        下面介绍两种闭散列的冲突找"下一个"空位置的具体做法:

🕹️线性探测

        比如下图中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在4的位置,但是该位置已经放了值为4的元素,即此时发生哈希冲突。


        线性探测解决方法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  • 插入

        通过哈希函数获取待插入元素在哈希表中的位置
        如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

  • 删除

        采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影
响。因此线性探测采用标记的伪删除法来删除一个元素

  • 线性探测优点实现非常简单,按顺序向后找即可。
  • 线性探测缺点一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

🕹️二次探测

        线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:, 或者:。其中:i =1,2,3…, H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

        研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
        因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。


🎏开散列

        开散列法又叫链地址法(开链法/拉链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

        之前的冲突通过链地址法解决如下:


🎏开散列与闭散列比较

        应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上, 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间


结语

希望这篇关于 哈希表(散列表) 的简介博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【数据结构】什么是红黑树(Red Black Tree)?

【数据结构】什么是平衡二叉搜索树(AVL Tree)?

【数据结构】C语言实现链式二叉树(附完整运行代码)

【数据结构】什么是二叉搜索(排序)树?

【C++】STL标准模板库容器map

【C++】模拟实现二叉搜索(排序)树

【C++】STL标准模板库容器set

【C++】模拟实现priority_queue(优先级队列)

【C++】模拟实现queue

【C++】模拟实现stack

【C++】模拟实现list

【C++】模拟实现vector

【C++】标准库类型vector

【C++】模拟实现string类

【C++】标准库类型string

【C++】构建第一个C++类:Date类

【C++】类的六大默认成员函数及其特性(万字详解)

【C++】什么是类与对象?


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2191005.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自动驾驶的技术实现及原理

自动驾驶技术是现代科技领域中一项引人注目的创新&#xff0c;它具有变革运输行业并提升道路安全的潜力。随着人工智能、传感器技术以及数据处理能力的不断提升&#xff0c;自动驾驶车辆已经从实验室研究逐渐走向现实应用。 自动驾驶的技术实现及原理 1. 自动驾驶技术的核心…

【深度学习】— 多层感知机介绍、 隐藏层、从线性到非线性、线性模型的局限性

【深度学习】— 多层感知机介绍 4.1 多层感知机4.1.1 隐藏层线性模型的局限性引入隐藏层 4.2 从线性到非线性线性组合的局限性引入非线性堆叠更多隐藏层 4.1 多层感知机 在第 3 节中&#xff0c;我们介绍了 softmax 回归&#xff0c;并实现了其从零开始的实现和基于高级 API 的…

UART通信协议

什么是UART UART ( Universal Asynchronous Receiver/Transmitter&#xff0c; 通用异步收发器) 是一种常用的串行通信协议&#xff0c;用于在 计算机和外部设备之间传输数据。它是一种异步通信协议&#xff0c;也就是说数据的传输不需要事先建立好同步时钟信号。 UART&#xf…

Unity MVC框架演示 1-1 理论分析

本文仅作学习笔记分享与交流&#xff0c;不做任何商业用途&#xff0c;该课程资源来源于唐老狮 1.一般的图解MVC 什么是MVC我就不说了&#xff0c;老生常谈&#xff0c;网上有大量的介绍&#xff0c;想看看这三层都起到什么职责&#xff1f;那就直接上图吧 2.我举一个栗子 我有…

深入理解 JavaScript 事件循环机制:单线程中的异步处理核心

深入理解 JavaScript 事件循环机制&#xff1a;单线程中的异步处理核心 JavaScript 是一门单线程的编程语言&#xff0c;也就是说它在同一时间只能执行一个任务。然而&#xff0c;现代 Web 应用经常需要处理大量的异步操作&#xff0c;如用户输入、网络请求、定时器等。为了确…

Vue的基本用法及模板语法

Vue.js使用了基于 HTML 的模板语法&#xff0c;允许开发者声明式地将 DOM 绑定至底层 Vue实例的数据。所有 Vue.js的模板都是合法的 HTML&#xff0c;所以能被遵循规范的浏览器和 HTML 解析器解析。 在底层的实现上&#xff0c;Vue将模板编译成虚拟 DOM 渲染函数。结合响应系…

实现Xshell与虚拟机中Linux服务器的连接(附常见错误解决)

前言 Xshell是一个强大的安全终端模拟软件&#xff0c;它支持SSH1, SSH2, 以及Microsoft Windows 平台的TELNET 协议。Xshell 通过互联网到远程主机的安全连接以及它创新性的设计和特色帮助用户在复杂的网络环境中享受他们的工作。 本文将介绍Xshell与虚拟机中Linux服务器连接…

前缀线性基——关于目前的理解以及一些样题

怎么说呢&#xff1f;在前几天我总结了了有关线性基的一篇博客&#xff0c;线性基用来去求整个区间的异或最值问题 前缀线性基——用于统计一个区间内的异或最值问题 那么我们如何去统计呢&#xff1f;那么就要去存储一个区间的异或空间线性基&#xff0c;因此我们的思路就是用…

【python】追加写入excel

输出文件运行前&#xff08;有两张表&#xff0c;“表1”和“Sheet1”&#xff09;&#xff1a; 目录 一&#xff1a;写入单表&#xff08;删除所有旧工作表&#xff0c;写入新表&#xff09;二&#xff1a;写入多表&#xff08;删除所有旧工作表&#xff0c;写入新表&#x…

平衡二叉搜索树之 AVL 树的模拟实现【C++】

文章目录 AVL树的简单介绍全部的实现代码放在了文章末尾准备工作包含头文件类的成员变量 构造函数和拷贝构造swap和赋值运算符重载析构函数findinsert[重要]当parent的平衡因子为1/-1时&#xff0c;如何向上更新祖先节点的平衡因子呢&#xff1f;怎么旋转&#xff1f;左单旋右单…

Windows Ubuntu下搭建深度学习Pytorch训练框架与转换环境TensorRT

Windows Ubuntu下搭建深度学习Pytorch训练框架与转换环境TensorRT JetBrains2024&#xff08;IntelliJ IDEA、PhpStorm、RubyMine、Rider……&#xff09;安装包Anaconda Miniconda安装.condarc 文件配置镜像源查看conda的配置和源(channel)自定义conda虚拟环境路径conda常用命…

Chromium 中JavaScript Screen API接口c++代码实现

Screen - Web API | MDN (mozilla.org) Screen Screen 接口表示一个屏幕窗口&#xff0c;往往指的是当前正在被渲染的 window 对象&#xff0c;可以使用 window.screen 获取它。 请注意&#xff1a;由浏览器决定提供屏幕对象&#xff0c;此对象一般通过当前浏览器窗口活动状…

《python语言程序设计》2018版第8章19题几何Rectangle2D类(下)-头疼的几何和数学

希望这个下集里能有完整的代码 一、containsPoint实现 先从网上找一下Statement expected, found Py:DEDENTTAB还是空格呢??小小总结如何拆分矩形的四个点呢.我们来小小的测试一下这个函数结果出在哪里呢???修改完成variable in function should be lowercase 函数变量应该…

No.2 笔记 | 网络安全攻防:PC、CS工具与移动应用分析

引言 在当今数字化时代,网络安全已成为每个人都应该关注的重要话题。本文将总结一次关于网络安全攻防技术的学习内容,涵盖PC端和移动端的恶意程序利用,以及强大的渗透测试工具Cobalt Strike的使用。通过学习这些内容,我们不仅能够了解攻击者的手法,更能提高自身的安全意识和防…

【牛顿迭代法求极小值】

牛顿迭代法求极小值 仅供参考 作业内容与要求 作业内容 作业要求 递交报告 代码 编程实现 计算偏导数 故上述非线性方程组的根可能为 f ( x , y ) f(x, y) f(x,y)的极值点&#xff0c;至于是极小值点还是极大值点或鞍点&#xff0c;就需要使用微积分中的黑塞矩阵来判断了。…

网络基础 【HTTPS】

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;Linux初窥门径⏪   &#x1f69a;代码仓库:Linux代码练习&#x1f69a; &#x1f4bb;操作环境&#xff1a; CentOS 7.6 华为云远程服务器 &#x1f339;关注我&#x1faf5;带你学习更多Linux知识…

Linux之实战命令26:timeout应用实例(六十)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

安卓手机密码忘了怎么办?(只做科普)

注意&#xff1a;只做科普&#xff0c;拒绝利用技术做一些非法事情 科普人&#xff1a;网络安全工程师~DL 科普平台&#xff1a;快手&#xff0c;CSDN&#xff0c;微信公众号&#xff0c;小红书&#xff0c;百度&#xff0c;360 本期文章耗时比较大&#xff0c;如果喜欢&…

数学题-分糖果-答案解析

PDF文档回复:20241005 1[题目描述] 幼儿园有7个小朋友&#xff0c;你是其中之一&#xff0c;有一天你发现无穷多颗糖&#xff0c;最少可以拿16个&#xff0c;最多可以拿23个&#xff0c;你打算拿一些分给小朋友们&#xff0c;分配原则是如果每人(包括你)都可以拿1块糖&#xf…

快速上手C语言【上】(非常详细!!!)

目录 1. 基本数据类型 2. 变量 2.1 定义格式 和 命名规范 2.2 格式化输入和输出&#xff08;scanf 和 printf&#xff09; ​编辑 2.3 作用域和生命周期 3. 常量 4. 字符串转义字符注释 5. 操作符 5.1 双目操作符 5.1.1 算数操作符 5.1.2 移位操作符 5.1.3 位操作符…