数据结构哈希(hash)

ops/2024/9/25 5:53:29/

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

数据结构哈希(hash)

收录于专栏 [C++进阶学习]
本专栏旨在分享学习C++的一点学习笔记,欢迎大家在评论区交流讨论💌

目录

1. 哈希的概念

2. 哈希冲突

3. 哈希函数

常见哈希函数 

4. 哈希冲突解决

4.1 闭散列(开放地址法)

1. 线性探测

2. 二次探测

4.2 开散列(链地址法)

1. 开散列的概念

2. 开散列的增容

3. 开散列和闭散列的比较 

5. unordered系列关联式容器介绍 


1. 哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N)平衡树中为树的高度,即 O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

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

当向该结构中:

插入元素

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

搜索元素

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

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

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。 

 

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 

2. 哈希冲突

对于两个数据元素的关键字k_i和 k_j(i != j),有k_i != k_j,但有:Hash(k_i) == Hash(k_j),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。 

3. 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

哈希函数设计原则:

1. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间

2. 哈希函数计算出来的地址能均匀分布在整个空间中

3. 哈希函数应该比较简单 

常见哈希函数 

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


2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key % p(p <= m), 将关键码转换成哈希地址


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


4. 折叠法--(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况


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


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

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

4. 哈希冲突解决

4.1 闭散列(开放地址法)

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

1. 线性探测

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

 

插入

通过哈希函数获取待插入元素在哈希表中的位置

如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素 

 

删除

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

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE}; 

哈希表的扩容问题:

但我们插入4, 44, 444, 4444, 44444, 444444, 4444444, 44444444, 444444444时,这些数字会全部冲突,我们寻找444444444时,先找到了4这个位置,然后一直向后寻找,这就会导致我们的哈希表的时间复杂度退化成O(N),所以我们的哈希表需要考虑扩容的问题

扩容条件

1.负载因子(Load Factor):

负载因子是哈希表中元素数量与槽位数量的比率。一般来说,当负载因子超过某个预设的阈值(例如 0.7 或 0.75)时,会进行扩容。
负载因子的计算公式为:
​填入表中的元素个数 / 散列表的长度

2.冲突频率:

如果频繁发生冲突,导致查找性能显著下降,也可能考虑扩容。

扩容方法

1. 确定新大小:

通常,新槽位的数量会选择为当前槽位数的两倍,以保持良好的性能。新大小一般是下一个素数,以减少哈希冲突的可能性。
2. 创建新的哈希表:

创建一个新的、大小为新槽位数的哈希表。
3. 重新哈希(Rehash):

将原哈希表中的所有元素重新插入新哈希表中。这是因为哈希函数通常依赖于表的大小,因此在扩容后,元素的哈希值可能会改变。
对每个元素,计算新的槽位并插入到新的哈希表中。
4. 替换旧表:

将新哈希表替换为旧的哈希表。

示例

1. 假设当前哈希表大小为 10,当前存储元素为 8(负载因子为 0.8)。
2. 当负载因子超过 0.7 时,决定进行扩容。
3. 新哈希表大小可以选择 20(下一个素数),然后为每个元素重新计算哈希位置并插入。

扩容的过程虽然消耗时间,但通常在整体性能上是可接受的,因为它不会频繁发生,并且有助于维持哈希表的效率。

 开放地址法-线性探测-总结:

线性探测优点:实现非常简单,

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

2. 二次探测

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

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

因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。 

4.2 开散列(链地址法)

1. 开散列的概念

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

 

插入44后:

 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

2. 开散列的增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可 能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希 表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可 以给哈希表增容。

3. 开散列和闭散列的比较 

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

5. unordered系列关联式容器介绍 

unordered系列关联式容器是C++标准库中提供的一组哈希表实现,用于存储键值对并提供快速的查找、插入和删除操作。主要包括以下几种容器:

unordered_set:

存储唯一的键,没有重复元素。
主要用于快速查找、插入和删除操作,平均时间复杂度为O(1)。

unordered_map:

存储键值对,每个键对应一个值,键必须唯一。
适用于需要快速访问值的场景,平均时间复杂度也是O(1)。

unordered_multiset:

类似于 unordered_set,但允许重复元素。
用于需要存储多个相同键的场景。

unordered_multimap:

类似于 unordered_map,但允许同一键对应多个值。
适合于需要关联多个值的情况。


特点:

哈希表实现:使用哈希函数将键映射到槽位,提供高效的访问速度。
无序性:元素的存储顺序不保证与插入顺序相同,适合不需要有序的场景。
负载因子与扩容:当负载因子超过特定阈值时,会自动进行扩容,以维持高效性能。

这些容器适用于需要快速查找和插入的应用场景,尤其在处理大量数据时表现优越。

 参考:

unordered_map - C++ Reference

unordered_set - C++ Reference

unordered_multimap - C++ Reference

unordered_multiset - C++ Reference


http://www.ppmy.cn/ops/115641.html

相关文章

js 将二进制文件流,下载为excel文件

吃西瓜 现成的粒子 二进制流&#xff0c;是一种计算机文件格式&#xff0c;它的数据以二进制形式存储&#xff0c;与文本文件不同&#xff0c; 二进制文件可以包含任意类型的数据&#xff0c;例如&#xff1a;图像、音频、视频、可执行文件、压缩文件等&#xff0c;而文本文…

18.1 k8s服务组件之4大黄金指标讲解

本节重点介绍 : 监控4大黄金指标 Latency&#xff1a;延时Utilization&#xff1a;使用率Saturation&#xff1a;饱和度Errors&#xff1a;错误数或错误率 apiserver指标 400、500错误qps访问延迟队列深度 etcd指标kube-scheduler和kube-controller-manager 监控4大黄金指标 …

【nvm管理多版本node】下载安装以及常见问题和解决方案

nvm管理多版本node nvm 下载安装下载安装 nvm 常用命令其他常用命令 常见问题 nvm 下载安装 下载 nvm下载地址 每个版本下都有Assets&#xff0c;根据需要下载一个。 node下载地址 根据自己需要,可以下载可执行文件或者压缩包 安装 按提示安装即可。 安装过程中&#xff…

grafana 使用常见问题

一、点击 panel 没有反应&#xff0c;没有出现 edit 选项。 方法一 将鼠标放在 panel 的任意位置&#xff0c;然后键盘输入 "e"&#xff0c;然后再次点击 title&#xff0c;即可出现选项框。 方法二 F12 查看当前 panel id&#xff0c;然后在浏览器 url 地址上拼接…

Oracle 物化视图创建(materialized)

要想创建 “物化视图&#xff0c;至少具有 ‘CREATE MATERIALIZED VIEW’ 权限” -- 权限查询&#xff0c;非 DBA 用户&#xff0c;则使用 user_sys_privs 即可 SELECT * FROM dba_sys_privs t WHERE t.privilege LIKE %MATERIALIZED%; grant create materialized view to sco…

指令个人记录

grep 返回file_name中包括11111的文本行 grep 11111 file_name grep "11111" file_name #字符串中含空格用双引号括起来 grep 11111 file_1 file_2 file_3-E正则表达式匹配 grep -E "[1-9]" -v返回file_name中不包括11111的文本行 grep -v 11111 file…

Spark Streaming 容错机制详解

Spark Streaming 是 Spark 生态系统中用于处理实时数据流的模块。它通过微批处理&#xff08;micro-batch&#xff09;的方式将实时流数据进行分片处理&#xff0c;每个批次的计算本质上是 Spark 的批处理作业。为了保证数据的准确性和系统的可靠性&#xff0c;Spark Streaming…

Oracle数据库的比较运算符Comparison Operators

Comparison operators compare one expression to another. The result is always either TRUE, FALSE, or NULL. If the value of one expression is NULL, then the result of the comparison is also NULL. 如果一个表达式的值为NULL&#xff0c;那么比较的结果也是NULL。 …