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

🦄个人主页:修修修也

🎏所属专栏:数据结构

⚙️操作环境: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.rhkb.cn/news/440723.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Bolt.new:终极自动化编程工具

兄弟们&#xff0c;终极写代码工具来了—— Bolt.new&#xff01;全方位的编程支持&#xff1a; StackBlitz 推出了 Bolt․new&#xff0c;这是一款结合了 AI 与 WebContainers 技术的强大开发平台&#xff0c;允许用户快速搭建并开发各种类型的全栈应用。 它的主要特点是无需…

前端reactvue3——实现滚动到底加载数据

文章目录 ⭐前言⭐react 实现滚动加载⭐vue3 实现滚动加载⭐总结⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享 前端react&vue3——实现滚动加载&#xff08;到底部加载&#xff09; scrollTop 属性 一个双精度浮点值&#xff0c;表示元素当前从原点垂直…

安全运营中心 (SOC) 团队对其安全工具感到失望

Vectra AI 表示&#xff0c;安全运营中心 (SOC) 从业人员认为&#xff0c;由于太多孤立的工具和缺乏准确的攻击信号&#xff0c;他们在检测和确定真实威胁的优先级方面正在失败。 人们对供应商的不信任感日益加深&#xff0c;认为供应商的工具在发现真正的攻击方面起的阻碍作用…

ctfshow-web入门(信息收集,持续更新中。。)

写在之前:近期打了个比赛,备受打击,入手了vip账号进修,加油! 文章目录 ctfshow-web1查看源代码ctfshow-web2burp抓包ctfshow-web3burp抓包ctfshow-web4访问robots.txtctfshow-web5dirscarch扫描PHPS文件泄露ctfshow-web6dirscarch扫描ctfshow-web7dirscarch扫描ctfshow-w…

【STM32开发之寄存器版】(六)-通用定时器中断

一、前言 STM32定时器分类 STM32103ZET6具备8个定时器TIMx(x 1,2,...,8)。其中&#xff0c;TIM1和TIM8为高级定时器&#xff0c;TIM2-TIM6为通用定时器&#xff0c;TIM6和TIM7为基本定时器&#xff0c;本文将以TIM3通用定时器为例&#xff0c;分析STM32定时器工作的底层寄存器…

You must konw JS!!(超详细的javascript套餐,适合计算机专业有基础的,包含常见前端开发面试题)

1.起源 JavaScript 起源于 1995 年&#xff0c;当时它主要是为了满足网页交互的需求而被创建。它最初的设计目的是为了让网页开发者能够在网页中添加一些简单的交互效果和动态内容。在那个时期&#xff0c;网页大多是静态的&#xff0c;而 JavaScript 的出现为网页带来了新的活…

jmeter学习(7)beanshell

beanshell preprocessor 发送请求前执行 beanshell postprocessor 发送请求前执行 获取请求相关信息 String body sampler.getArguments().getArgument(0).getValue(); String url sampler.getPath(); 获取响应报文 String responseprev.getResponseDataAsString(); 获…

CMake 教程跟做与翻译

目录 STEP 1: 入门与理解 cmake_minimum_required设置CMake版本的最小值 project声明工程属性 add_executable添加可执行文件 使用CMake构建工程 根据自己的构建工具自行构建 Reference STEP 1: 入门与理解 我们起手的&#xff0c;最基本的 CMake 项目是从单个源代码文件…

【Blender Python】1.概述和基础使用

概述 众所周知&#xff0c;Blender是一款开源免费的3D建模软件&#xff08;当然不限于3D建模&#xff09;。在Blender中&#xff0c;可以使用其内置的Python解释器执行Python代码&#xff0c;用于程序化的生成网格以及其他内容。你可以基于此创建Blender插件。 这个系列就是快…

Electron桌面应用打包现有的vue项目

1 环境准备 Node&#xff1a;v16.20.2&#xff08;本地vue项目nodejs版本&#xff09;Electron&#xff1a;22.3.7vue&#xff1a;2 版本管理 2 Vue项目准备 更新相关依赖npm install --registry https://registry.npmmirror.com/npm run dev 3、引入Electorn 安装指定版…

算法剖析:双指针

文章目录 双指针算法一、 移动零1. 题目2. 算法思想3. 代码实现 二、 复写零1. 题目2. 算法思想3. 代码实现 三、 快乐数1. 题目2. 算法思想3. 代码实现 四、 盛水最多的容器1. 题目2. 算法思想3. 代码实现 五、有效三角形的个数1. 题目2. 算法思想3. 代码实现 六、 和为 s 的两…

UART驱动学习三(TTY驱动部分源码解析)

目录 全局框架图一、tty_io.c 分析1. 关键数据结构和定义2. 文件操作结构体3. 初始化和注册4. 读写操作5. 挂起和恢复6. 信号处理7. 设备类8. 控制台通知9. 辅助函数10. 代码功能11. 带有注释的部分tty_io.c源码 二、tty_ldisc.c 分析1. 关键数据结构和定义2. 行规程操作函数3.…

Android车载——VehicleHal运行流程(Android 11)

1 概述 本篇主要讲解VehicleHal的主要运行流程&#xff0c;包括设置属性、获取属性、订阅属性、取消订阅、持续上报属性订阅等。 2 获取属性流程 2.1 获取属性流程源码分析 作为服务注册到hwServiceManager中的类是VehicleHalManager&#xff0c;所以&#xff0c;CarServic…

判断是否为二叉排序树(二叉搜索树,二叉查找树)

1.判断给定的二叉树是否为二叉排序树&#xff0c;如果是返回1&#xff0c;不是返回0。 思想&#xff1a; 二叉树是左子树<根<右子树。中序遍历是递增有序&#xff0c;可以通过比较当前结点与前驱关系来进行判断。 代码&#xff1a; //pre为全局变量&#xff0c;保存当…

数学与生活

多学科交叉 信号处理 小波 经济 政策 计算机 统计 信号处理与市场分析 经济与数据分析 政策与统计 过去的数学家没有一个是纯粹的数学家&#xff1b;生活中各方面工程的&#xff0c;物理的&#xff0c;天文&#xff0c;地理的&#xff0c;赌博&#xff0c;政治的&#xff1b…

5-25 JQuery

jQuery简介 jQuery是什么 jQuery基本语法 测试jQuery <head> <meta charset"utf-8"> <title>无标题文档</title><script type"text/javascript" src"jquery-3.5.1.js"></script><script type"tex…

FastAdmin Apache下设置伪静态

FastAdmin Apache下设置伪静态 一、引言 FastAdmin 是一个基于ThinkPHP和Bootstrap框架开发的快速后台开发框架&#xff0c;它以其简洁、高效、易于扩展的特点&#xff0c;广受开发者的喜爱。在部署FastAdmin项目时&#xff0c;为了提高访问速度和用户体验&#xff0c;我们通…

Redis介绍及整合Spring

目录 Redis介绍 Spring与Redis集成 Redis介绍 Redis是内存数据库&#xff0c;Key-value型NOSQL数据库&#xff0c;项目上经常将一些不经常变化并且反复查询的数据放入Redis缓存&#xff0c;由于数据放在内存中&#xff0c;所以查询、维护的速度远远快于硬盘方式操作数据&#…

【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解

目录 1、FileChannel (1&#xff09;获取 FileChannel (2&#xff09;读取文件 (3&#xff09;写入文件 (4&#xff09;关闭通道 (5&#xff09;当前位置与文件大小 (6&#xff09;强制写入磁盘 2、两个 FileChannel 之间的数据传输 (1&#xff09;使用 transferTo()…

leetcode-42. 接雨水 单调栈

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;6 解释&#xff1a;上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表…