| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> 手撕哈希表(HashTable)——C++高阶数据结构详解 -> 正文阅读 |
|
[数据结构与算法]手撕哈希表(HashTable)——C++高阶数据结构详解 |
传统艺能😎小编是双非本科大一菜鸟不赘述,欢迎米娜桑来指点江山哦(QQ:1319365055) 🎉🎉非科班转码社区诚邀您入驻🎉🎉 概念🤔哈希简单来说就是把任意输入通过特定方式(hash函数) 处理后 生成一个值。这个值等同于存放数据的地址,这个地址里面再吧输入的数据进行存储。哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为 O(1),因为哈希表的查找速度非常快,所以在很多程序中都有使用哈希表,例如拼音检查器。 在普通的顺序结构或者平衡树中,因为关键码内容和存储位置之间没有对应关系,所以查找一个元素必须经过关键码的多次比较,顺序结构中查找的时间复杂度为 O(N),平衡树中查找的时间复杂度为树的高度 O(logN) ;而最理想的搜索方法是可以不经过任何比较,直接从表中得到要搜索的元素,即查找的时间复杂度为 O(1) 的哈希!
这种转换思想就是转换函数,哈希方法中称为哈希(散列)函数,构造出来的结构称为哈希表(散列表)。
给定集合 {1, 7, 6, 4, 5, 9},将哈希函数设置为:h a s h ( k e y ) = k e y % c a p a c i t y ,其中capacity为存储元素空间的总大小。 在查找时只需要对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取出元素进行比较,若关键码相等则搜索成功。 哈希碰撞🤔也叫哈希冲突,指不同关键字通过相同哈希函数计算出了相同的哈希地址,比如在上述例子中,再将元素 11 插入当前的哈希表就会产生哈希冲突。 因为元素11通过该哈希函数得到的哈希地址与元素1相同,都是下标为1的位置: hash(11)=11 % 10 = 1 那么这种冲突是否可以避免呢? 答案是只能缓解,不可避免。 由于哈希函数的原理是将输入空间一个较大的值映射到一个较小的 Hash 空间内,而 Hash空间一般远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成同一输出的情况。 抽屉原理
哈希函数🤔不合理的哈希函数就是引发哈希冲突的重要原因,哈希函数设计的越精妙,产生哈希冲突的可能性越低! 哈希函数的设计遵从三大原则: 1. 哈希函数的定义域必须包括需要存储的全部关键码,且如果散列表允许有m个地址,其值域必须在0到m-1之间。 常见的哈希函数有:
取关键字的线性函数作为哈希地址:Hash(Key) = A ? Key + B
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数 Hash(Key) = Key % p (p <= m),将关键码转换成哈希地址
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按哈希表表长,取后几位作为哈希地址
选择一个随机函数,取关键字的随机函数值为它的哈希地址,Hash(Key) = random(Key),其中 random 为随机数函数
设有n个d位数,每一位可能有r种不同的符号,这r中不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,而在某些位上分布不均匀,只有几种符号经常出现。此时,我们可根据哈希表的大小,选择其中各种符号分布均匀的若干位作为哈希地址 举个栗子,假设要存储某家公司员工信息,如果用手机号作为关键字,那么极有可能前 7 位都是相同的,那么我们可以选择后 4 位作为哈希地址
解决哈希冲突🤔解决哈希冲突有两种常见的方法:闭散列和开散列 闭散列😎也叫开放定址法,在发生哈希冲突时,如果哈希表未被装满,说明在哈希表种必然还有空位置,那么可以把产生冲突的元素存放到冲突位置的 “下一个” 空位置中去,寻找“下一个位置”的方式多种多样,常见的方式有以下两种:
当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止。
所以开放定址法的优点就是实现简单,而缺点也显而易见就是冲突一旦发生,极有可能造成数据堆积,不同关键字占据可利用空间,导致查找时需要多次比较,即所谓的踩踏效应,搜索效率下降。 随着数据的增多,哈希冲突的可能性增加,有可能一个位置会发生多次哈希冲突,冲突多次后插入哈希表的元素,在查找时的效率必然也会降低,于是哈希表中又引入了负载因子(载荷因子):负载因子 = 表中有效数据个数 / 空间的大小
因此我们在闭散列(开放定址法)对负载因子的标准定在了 0.7~0.8,一旦大于 0.8 会导致查表时缓存未命中率呈曲线上升;这就是为什么有些哈希库都有规定的负载因子,Java 的系统库就将负载因子定成了 0.75,超过 0.75 就会自动扩容。
二次探测的根本目的是为了避免线性探测可能产生的踩踏效应,他在寻找空位置的方法上进行了改造:
H0:通过哈希函数对元素的关键码进行计算得到的位置。 采用二次探测相比线性探测而言,哈希表中元素的分布会相对稀疏一些,不容易导致数据堆积,当然二次探测也需要考虑负载因子,因此不能看出闭散列最大的缺点就是空间利用率低,其实这也是哈希的老病根。 开散列😎开散列,又叫链地址法(拉链法),首先对关键码集合用哈希函数计算哈希地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶(bucket),各个桶中的元素通过一个链表连接,各链表的头结点存储在哈希表中。 相比闭散列那种报复社会型的小藓钕占座,开散列就显得格局打开了,既然没法坐那我就吊扶手。其中链表的头结点存储在哈希表中的方式,不会影响与自己哈希地址不同的元素的增删查改的效率,因此开散列的负载因子相比闭散列而言,可以稍微大一点。闭散列负载因子不能超过1,一般建议控制在 [0.0, 0.7] 之间;开散列的哈希桶,负载因子可以超过1,一般建议控制在 [0.0, 1.0] 之间。 而且开散列相对闭散列不仅仅只有空间利用率高的优点,还有它处理某些极端情况的能力,比如根据哈希函数计算的哈希地址全部在同一个地址,就是全员冲突,此时效率退化到了 O(N):
闭散列实现🤔首先我们应该知道在闭散列的哈希表中,每个位置除了存储所给数据之外,还应该存储该位置当前的状态,那么状态的存在意义是什么? 比如我需要在哈希表中查找一个数据,这个数据我用哈希函数算出来他的位置是 1 ,但是我们不知道是不是存在哈希冲突,如果冲突就会向后偏移,我们就需要从 1 这个位置开始向后遍历,但是万万不能遍历完整个哈希表,这样就失去了哈希原本的意义,只需要遍历到一个空位置就可以说明他不存在,即可结束。 那如何标识一个空位置?用数字 0 吗?那如果我们要存储的元素就是 0 怎么办?因此我们必须要单独给每个位置设置一个状态字段。 假设哈希表当中箭头所指处有元素存在并将其删除,此时我们要判断当前哈希表当中是否存在元素 101,当我们从 1 下标开始往后找到 2 下标(空位置)时,我们按照原来的逻辑就会停下来,此时并没有找到元素 101!
由此我们需要三个状态:
我们可以用枚举定义这三个状态。
哈希表存储结构:
而为了在插入元素时好计算当前哈希表的负载因子,我们还应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容
数据插入😎哈希表中插入数据的步骤如下:
数据查找😎哈希表中数据的查询首先需要验证此时哈希表的大小是否是 0,是 0 则查找失败,然后再根据哈希函数计算出哈希地址,对应哈希地址在哈希表中进行遍历查找,遇到 EMPTY 位置还没找到则查找失败。 在查找过程中,必须找到位置状态为 EXIST 且 key 值匹配,才算查找成功。若 key 值匹配但该位置状态为 DELETE,则需继续进行查找,因为该位置的元素已经被删除了!
数据删除🤔其实哈希表的数据删除是非常简单的,我们的基本思想就是进行伪删除,也就是我们改变他的状态码即可,待删除位置置为 DELETE 即可,这样既不用大费周章的操作数据,也不会造成空间的浪费:
开散列实现🤔在开散列的哈希表中,哈希表的每个位置存储的实际上是某个单链表的头结点,即每个哈希桶中存储的数据实际上是一个结点类型,该结点类型除了存储所给数据之外,还需要存储一个结点指针用于指向下一个结点:
因为开散列的优势,在发生哈希冲突时不需要进行任何探测来跳到下一个未占用的地址,直接挂桶即可,所以开散列不需要状态码成员,其他的流程就与闭散列一样了,需要根据负载因子来判断当前是否进行哈希增容,且时刻记录当前表中的有效元素个数:
插入数据😎向哈希表中插入数据的步骤如下: 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整,将键值对插入哈希表,最后哈希表中的有效元素个数加一 实际操作中为了降低时间复杂度,在增容时取结点都是从单链表的表头开始向后依次取的,在插入结点时也是直接将结点头插到对应单链表 将键值对插入哈希表的具体步骤如下:
查找数据😎开散列查找数据的方法和闭散列的查找方法是一样的:
数据删除😎开散列查找数据的方法和闭散列的删除方法是一样的:
利用素数来规定哈希表大小🤔其实哈希表在使用除留余数法时,为了减少哈希冲突的次数,很多地方都使用了素数来规定哈希表的大小 下面用合数(非素数)10和素数11来进行说明。 合数10的因子有:1,2,5,10。 我们选取下面这五个序列:
对这几个序列分别放进哈希表,分别观察,不难得出他们的规律:
综上所述,某个随机序列当中,每个元素之间的间隔是不定的,为了尽量减少冲突,我们就需要让哈希表的大小的因子最少,此时素数就可以视为最佳方案。 实现方案😎很明显如果还是采用传统的 2 倍扩容就会不符合素数大小的要求,所以我们不妨直接将素数大小存储在数组里,我们规定下面这个数组即可,其中元素近似 2 倍增长:
在扩容时直接求取下一个素数即可:
aqa 芭蕾 eqe 亏内,代表着开心代表着快乐,ok 了家人们 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 19:41:24- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |