【C++进阶】hash表的封装

文章目录

  • hash表
    • 哈希表的关键组成部分
    • 哈希表的优缺点
      • 优点:
      • 缺点:
    • 常见应用场景
  • 开放定址法实现hash表
    • 负载因子 (Load Factor)
      • 负载因子的意义
      • 负载因子的影响
      • 再散列 (Rehashing)
      • 示例
    • 整体框架
    • insert
    • Find
    • erase
    • hash桶封装
    • 框架
    • insert
    • find
    • erase
    • ~HashTable()
  • 总结

hash表

哈希表是一种数据结构,它通过将键映射到存储桶或槽来快速查找数据。它的核心思想是通过一个哈希函数(Hash Function)将输入数据(键)转换为数组中的索引,以便在常数时间内进行查找、插入和删除操作。

哈希表的关键组成部分

  1. 哈希函数 (Hash Function):将输入的键(key)映射为哈希表的索引。理想的哈希函数应该均匀分布键,避免过多冲突。
  2. 存储桶 (Bucket):每个哈希表的槽位。如果两个不同的键通过哈希函数得到了相同的索引(称为哈希冲突),多个键可以通过链表或其他方式存储在同一个槽中。
  3. 哈希冲突 (Hash Collision):当不同的键映射到同一个存储桶时,发生冲突。常见的解决方法有:
    • 链地址法 (Separate Chaining):在每个槽中存储一个链表,冲突的键会被添加到链表中。
    • 开放地址法 (Open Addressing):通过重新计算索引(如线性探测、二次探测等),将冲突的键存储在下一个可用的槽位中。

哈希表的优缺点

优点:

  • 平均情况下,哈希表的查找、插入和删除操作都能在 O(1) 时间复杂度内完成。

缺点:

  • 当发生大量冲突时,查找和插入的性能可能退化到 O(n)
  • 哈希表的空间利用率不如树结构等其他数据结构高。

常见应用场景

哈希表常用于需要快速查找的场景,如数据库索引、缓存、字典等。

接下来我们将分别用开放定址法和哈希桶的方法来实现hash表

开放定址法实现hash表

首先我们在封装hash表之前要了解什么是负载因子。

负载因子 (Load Factor)

负载因子是指哈希表中已存储元素的数量与哈希表大小(存储桶数量)的比值,公式为:

α = n m \alpha = \frac{n}{m} α=mn

  • n:哈希表中已存储的元素数量。
  • m:哈希表的总存储桶(bucket)数量。

例如,如果哈希表有 100 个存储桶,已存储了 60 个元素,那么负载因子为:

α = 60 100 = 0.6 \alpha = \frac{60}{100} = 0.6 α=10060=0.6

负载因子的意义

  1. 负载因子越小:意味着哈希表中空槽越多,冲突(collisions)的概率越低,查找和插入操作的性能更好。
  2. 负载因子越大:意味着哈希表中存储的元素越来越多,冲突的概率增加,查找和插入操作的效率下降。

负载因子的影响

  • 如果负载因子过高(接近 1 或大于 1),冲突会变得频繁,导致性能下降。这时通常会进行再散列 (rehashing),即扩展哈希表的大小,并重新计算所有元素的哈希值。
  • 在一些常见的哈希表实现中,通常当负载因子超过一定的阈值(如 0.75)时,会触发再散列操作,以保证哈希表的操作性能。

再散列 (Rehashing)

当负载因子达到阈值时,哈希表会增大存储桶的数量(通常是倍增),并重新计算所有已存储元素的哈希值,将它们放入新的存储桶中。这一操作虽然代价较高,但可以避免长期的性能退化。

示例

假设一个哈希表的存储桶数量为 10:

  • 当插入 5 个元素时,负载因子为:

α = 5 10 = 0.5 \alpha = \frac{5}{10} = 0.5 α=105=0.5

  • 当插入 9 个元素时,负载因子为:

α = 9 10 = 0.9 \alpha = \frac{9}{10} = 0.9 α=109=0.9 此时可能需要进行再散列操作来扩展哈希表的大小。

整体框架

	//三种状态enum State{EXIST,//存在EMPTY,//空DELETE//删除};template<class K, class V>struct HashData{pair<K, V> _kv;//初始状态是空State _state = EMPTY;};template<class K, class V>class HashTable{public:private://用一个Hash表来存储vector<HashData<K, V>> _tables;//表中插入的数据个数size_t _n = 0;};

在开放定址法中,我们需要用一个状态来表示hash表中每个位置的状态,由于每个位置的状态不止两种,所以我们不能使用布尔值来表示每个位置的状态,应该使用enum来定义多种状态,我们定义三种状态(EMPTY,EXIST,DELETE)分别表示空,存在,删除。
在HashTables中_n表示hash表中插入了多少个数。
接下来我们来封装insert

insert

bool Insert(const pair<K, V>& kv)
{//去冗余if (Find(kv.first)){return false;}//扩容,通过负载因子来进行扩容if (_n * 10 / _tables.size() == 7)//负载因子为0.7的时候进行扩容{//不能直接进行resize,因为扩容之后空间变化了,映射也要跟着变化//创建一个新表是以前表的二倍HashTable<K, V>  newHT;newHT._tables.resize(_tables.size() * 2);for (size_t i = 0;i < _tables.size();i++){if (_tables[i]._state == EXIST){//直接复用insert//这里不是自己调用自己,因为这里是两个对象newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}Hash hs;//算起始位置size_t hashi = hs(kv.first) % _tables.size();//状态存在则继续while (_tables[hashi]._state == EXIST){hashi++;//防止hashi越界hashi %= _tables.size();}//添加值并且状态改为存在_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}

在这里插入图片描述
要进行插入我们首先需要找到插入位置,假如当我们找到位置的时候,当前位置已经被占据了,所以我们只能向后移动。
在这里插入图片描述
插入的及基本逻辑经济就是如此,既然有插入那肯定有插入位置满的时候,所以插入逻辑中应该涉及到扩容,但是我们不能直接进行扩容,因为当我们扩容的时候,每个data的映射的位置也会随之而变化,这就涉及到我们应该找到新的映射位置,但是对于开放定址法来说,我们不是在插入位置满的时候扩容,而是在负载因子到达0.7的时候进行扩容。
在这里插入图片描述
在扩容的时候,我们可以直接创建一个新表,然后在新表中重新映射数据,这里我们可以直接复用insert。(注意:这里调用的不是同一个insert,因为是两个不同的对象)

Find

HashData<K, V>* Find(const K& key)
{Hash hs;//算起始位置size_t hashi = hs(key) % _tables.size();//状态存在则继续while (_tables[hashi]._state != EMPTY){//这个状态不是DELETE才能进行查找if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key){return &_tables[hashi];}hashi++;//防止hashi越界hashi %= _tables.size();}return nullptr;
}

对于查找来说我们只需要找到当前需要查找的数据的所在的位置,然后从查找的位置对这个表进行遍历,如果遇到和当前值相等的则直接返回当前位置的地址,需要注意的是:我们还要考虑状态,当状态是DELETE的时候,但是当前值又等于查找的值,这个值是不能返回的,因为当前值已经删除了,所以这种情况需要排除,所以前面需要添加一个判断条件。

erase

bool Erase(const K& key)
{//找到删除位置HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;return true;}return false;
}

对于删除某个值,我们首先需要查找,所以这里我们可以复用查找,用HashData<K,V>接收一下,如果当前指针是空指针就直接返回false,说明需要删除的值没在表中,如果当前指针不是空指针,则将状态设置为DELETE状态。

hash桶封装

框架

template<class K,class V>
struct HashNode
{pair<K,V> _kv;HashNode<T>* _next;HashNode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}
};
template<class K, class V>
class HashTable
{
public:typedef HashNode<K,V> Node;
private://hash桶//hash桶当中的Node节点是new出来的,需要写析构函数vector<Node*> _tables;//表中存储的数据个数size_t _n = 0;
};

用hash桶来封装hash表我们需要一个结构体,结构体中包含指向下一个结构体的指针,还有一个存放数据的kv。
在HashTable中我们需要一个vector,这个vector的类型是Node*,相当于存放的是指针。
在这里插入图片描述
上述代码描述的结构就是上面这种结构。

insert

bool Insert(const pair<K,V>& kv)
{if (Find()) {return false;}//找到插入的位置size_t hashi = kv.first % _tables.size();//需要控制负载因子,hash桶的负载因子需要控制在1if (_n == _tables.size()){vector<Node*> newtables(_tables.size() * 2, nullptr);for (size_t i = 0;i < _tables.size();i++){//将旧表中的节点进行复用,头插Node* cur = _tables[i];while (cur){Node* next = cur->_next;//重新计算插入位置size_t hashi = cur->_kv.first % newtables.size();cur->_next = newtables[hashi];//先指向第一个newtables[hashi] = cur;//然后再成为第一个cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}//头插,尾插也可以,但是尾插需要找到尾,所以这里我们选择头插不用找尾Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;_n++;return true;
}

对于插入来说,我们可以直接算出插入位置,然后在当前桶中进行头插,为什么进行头插而不进行尾插呢?
因为头插可以直接插,而尾插则需要找到尾之后才能插入,大大影响了我们的插入效率,所以我们进行头插。
插入的逻辑说完之后,,我们也要扩容,对于hash桶实现hash表来说,我们的扩容的前提是当每个_n== 1的时候,意思就是每个桶都有一个节点的时候,_n==1的时候最好的情况是每个桶有一个节点,,这个情况的前提就是保证每个桶都有节点。

如果我们直接扩容的话也不是不行,但是会很浪费我们的空间,所以我们可以不释放当前节点,直接把旧表的节点插入到新表映射的位置上去,就不用浪费空间了。

find

Node* Find(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_data == key){return cur}cur = cur->_next;}return nullptr;
}

对于查找来说直接找到映射位置遍历hash桶即可。

erase

bool Erase(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_data == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;
}

对于删除的逻辑我们先找到映射位置,然后先定义一个prev标记前一个指针,然后遍历当前桶,如果当前位置的值和需要删除的值相同,则可以分出两种情况,第一种情况是头删,头删就说明prev是nullptr,则可以直接令头指针等于下一个节点,如果是删除中间的,则可以令prev指向cur的下一个节点,然后释放当前节点,注意:释放当前节点的时候需要_n—。

~HashTable()

因为每个节点都是我们new出来的,所以需要我们手动释放,所以写一个析构函数,释放每个节点,遍历每个桶逐个释放

~HashTable()
{for (size_t i = 0;i < _tables.size();i++){Node* cur = _tables[i];while(cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}
}

总结

通过对哈希表的封装,我们不仅提升了代码的可读性和复用性,还通过合理设计哈希函数和处理冲突机制,优化了哈希表的性能。封装让我们可以将底层的细节隐藏起来,提供一个简洁高效的接口,便于后续在项目中的使用。在实际开发中,理解负载因子、再散列等概念,并针对具体场景合理调整这些参数,能够确保哈希表在性能和内存占用上的平衡。掌握了这些知识后,相信你能够更加自信地在复杂应用中使用哈希表。

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

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

相关文章

银行结算业务

1.1 银行本票 银行本票是由银行签发的,承诺自己在见票时无条件支付票款给收款人或持票人的业务。银行本票按票面划分为定额本票和不定额本票,按币种划分为人民币银行本票和外币银行本票。人民币银行本票仅在同一交换区域内使用,资金清算利用当地人民银行组织的资金清算形式…

多个vue项目部署到nginx服务器

文章目录 需求一、项目打包1.vue.config.js2.request.js文件3.打包 二、nginx配置 需求 同一个域名安装多个vue项目。 比如&#xff1a;域名为 https://domain.com 后缀。那么通过不同的后缀就能去访问不同的项目地址。 https://domain.com&#xff0c;不加任何后缀&#x…

【第0006页 · 数组】寻找重复数

【前言】本文以及之后的一些题解都会陆续整理到目录中&#xff0c;若想了解全部题解整理&#xff0c;请看这里&#xff1a; 第0006页 寻找重复数 今天想讨论的一道题在 LeetCode 上评论也是颇为“不错”。有一说一&#xff0c;是道好题&#xff0c;不过我们还是得先理解了它才…

微信小程序中如何监听元素进入目标元素

Page({onLoad: function(){// 如果目标节点&#xff08;用选择器 .target-class 指定&#xff09;进入显示区域以下 100px 时&#xff0c;就会触发回调函数。wx.createIntersectionObserver().relativeToViewport({bottom: 100}).observe(.target-class, (res) > {res.inter…

合宙4G模组Air780EX——产品规格书

Air780EX 是合宙通信推出的LTE Cat.1 bis通信模块&#xff1b; Air780EX采用移芯EC618平台&#xff0c;支持LTE 3GPP Rel.13 技术&#xff1b; Air780EX 是4G全网通模块&#xff0c;可适应不同的运营商和产品&#xff0c;确保产品设计的最大灵活性。 其主要特点和优势可以总…

maven配置文件常用模板

注释很详细&#xff0c;直接上代码 项目结构 内容 父项目 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi…

高德地图SDK Android版开发 10 InfoWindow

高德地图SDK Android版开发 10 InfoWindow 前言相关类和方法默认样式Marker类AMap类AMap.OnInfoWindowClickListener 接口 自定义样式(视图)AMap 类AMap.ImageInfoWindowAdapter 接口 自定义样式(Image)AMap.ImageInfoWindowAdapter 接口 示例界面布局MapInfoWindow类常量成员变…

【数学建模国赛思路预约】2024全国大学生数学建模竞赛助攻思路、代码、论文

2024年全国大学生数学建模大赛马上就要开始了&#xff0c;大家有没有准备好呢&#xff0c;今年将会和之前一样&#xff0c;将会在比赛赛中时期为大家提供比赛各题的相关解题思路、可运行代码参考以及成品论文。 一、分享计划表如下所示 1、 赛中分享内容包括&#xff08;2023国…

高并发内存池(二):​整体框架的介绍与ThreadCache的实现

目录 整体框架介绍 ThreadCache的主体框架 自由链表-FreeList 内存对齐-RoundUp 计算桶位置-Index 基础版 进阶版 线程局部存储 __declspec(thread) 关键字 实现线程无锁 申请内存-Allocate 释放内存-Deallocate 从中心缓存中申请内存 整体框架介绍 高并发内存池…

机器学习引领未来:赋能精准高效的图像识别技术革新

图像识别技术近年来取得了显著进展,深刻地改变了各行各业。机器学习,特别是深度学习的突破,推动了这一领域的技术革新。本文将深入探讨机器学习如何赋能图像识别技术,从基础理论到前沿进展,再到实际应用与挑战展望,为您全面呈现这一领域的最新动态和未来趋势。 1. 引言 …

kubernetes集群部署Confluence 7.2.0+mysql 5.7(自测有效)

背景介绍&#xff1a; Confluence是一个专业的企业知识管理与协同软件。使用简单&#xff0c;但它强大的编辑和站点管理特征能够帮助团队成员之间共享信息、文档协作、集体讨论&#xff0c;信息推送。 这里介绍的使用的是Confluence 7.2.0版本的。 一、在kubernetes集群部署 1…

本地零阶提示优化

本文探讨了如何优化大型语言模型&#xff08;LLM&#xff09;中的提示&#xff08;prompt&#xff09;&#xff0c;以更有效地利用这些黑盒模型的能力。传统的优化方法倾向于寻找全局最优解&#xff0c;但在某些情况下这种做法可能表现不佳。通过对提示优化进行深入的研究&…

01 Docker概念和部署

目录 1.1 Docker 概述 1.1.1 Docker 的优势 1.1.2 镜像 1.1.3 容器 1.1.4 仓库 1.2 安装 Docker 1.2.1 配置和安装依赖环境 1.3镜像操作 1.3.1 搜索镜像 1.3.2 获取镜像 1.3.3 查看镜像 1.3.4 给镜像重命名 1.3.5 存储&#xff0c;载入镜像和删除镜像 1.4 Doecker…

汽车功能安全--TC3xx之PBIST、MONBIST

目录 1.PMS 电源监控速览 2.PBIST 3.MONBIST 4.小结 1.PMS 电源监控速览 英飞凌TC3xx芯片的四种硬件机制&#xff0c;分别是&#xff1a; PMS:PBIST: Power Built-in Self Test. MCU:LBIST: Logic Built-in Self Test. PMS:MONBIST: Monitor Built-in Self Test. VMT:MBI…

史上最全的Linux常用命令汇总(超全面!超详细!)收藏这一篇就够了!

command &#xff1a;命令名&#xff0c;相应功能的英文单词或单词的缩写[-options] &#xff1a;选项&#xff0c;可用来对命令进行控制&#xff0c;也可以省略parameter &#xff1a;传给命令的参数&#xff0c;可以是 零个、一个 或者 多个 查阅命令帮助信息 -help 说明&…

【高阶数据结构】B树、B+树、B*树

B树、B树、B*树 1. 常见的搜索结构2. B树概念3. B树的插入分析4. B树的插入实现4.1 B树的节点设计4.2 B树的部分插入实现14.3 B树的查找4.4 B树的部分插入实现24.5 插入key的过程4.7 B树的插入完整代码4.8 B树的简单验证4.9 B树的删除4.10 B树的性能分析 5. B树6. B*树7. 总结8…

【C++】STL学习——list模拟实现

目录 list介绍list结构介绍节点类的实现迭代器的实现构造函数运算符重载--运算符重载运算符重载!运算符重载*运算符重载->运算符重载 const迭代器的实现多参数模板迭代器list函数接口总览默认成员函数构造函数1构造函数2构造函数3 析构函数拷贝构造函数赋值重载函数 迭代器b…

开放式系统互连(OSI)模型的实际意义

0 前言 开放式系统互连&#xff08;OSI&#xff0c;Open Systems Interconnection&#xff09;模型&#xff0c;由国际标准化组织&#xff08;ISO&#xff09;在1984年提出&#xff0c;目的是为了促进不同厂商生产的网络设备之间的互操作性。 定义了一种在层之间进行协议实现…

【C++】STL容器详解【下】

目录 一、list容器 1.1 list基本概念 1.2 lsit构造函数 1.3 list数据元素插入和删除操作 1.4 list大小操作 1.5 list赋值操作 1.6 list数据的存取 1.7 list反转排序 二、set/multiset容器 2.1 set/multiset基本概念 2.2 set构造函数 2.3 set赋值操作 2.4 set大小操…

数据库的操作:SQL语言的介绍

一.前言 SQL是一种结构化查询语言。关系型数据库中进行操作的标准语言。 二.特点 ①对大小写不敏感 例如&#xff1a;select与Select是一样的 ②结尾要使用分号 没有分号认为还没结束; 三.分类 ①DDL&#xff1a;数据定义语言&#xff08;数据库对象的操作&#xff08;结…