C++——关联式容器(5):哈希表

7.哈希表

7.1 哈希表引入

        哈希表的出现依旧是为了查找方便而设计的。在顺序结构中,查询一个值需要一一比较,复杂度为O(N);在平衡树中,查询变为了二分查找,复杂度为O(logN);而对于哈希表,我们可以通过特殊的映射关系,将其查询的复杂度变为O(1)。

        因此,哈希(散列)的主要思想是用于查找,希望可以不通过比较,一次性拿到要搜索的元素。为了实现这个目的,可以考虑将带存储的元素按照某种哈希方法(散列方法)映射到指定位置,这样就在存储元素和存储位置之间产生了一一映射关系。通过这种关系,我们可以根据待搜索的元素通过哈希函数直接计算出存储位置。类比于数学中一种特殊的映射关系——函数:给定一个自变量x(给一个带存储的元素的key),通过函数关系f(通过哈希函数Hash),得到一个因变量y(得到key对应的值,作为存储位置的标识)。

        通过以上的方法构建出来的结构即为哈希表(散列表)

        哈希函数用于计算不同的元素应该存储的位置。常见的哈希函数 Hash(key) :

①直接定址法:即线性关系确定位置 Hash(key) = A*key+B

②除留余数法:即对哈希表的长度取模 Hash(key) = key%m

        如同函数一样,一个多个自变量可能映射到同一个因变量。哈希函数虽然决定了映射关系,但问题也很明显,即无法避免位置的冲突,我们一般将不同关键码而具有相同哈希地址的现象称为哈希冲突。为了解决哈希冲突,可以采取闭散列或者开散列两种方法。

7.2 闭散列(开放地址法)

        闭散列规定,当发生哈希冲突时,可以把当前的key存储到冲突位置的“下一个”位置。

7.2.1 结构设计

        对于开放地址法,哈希表采用数组的方式进行哈希表的组织,即数组的每个位置存放一个元素。当发生冲突时就按照既定的规则,找寻数组合适的空位。

        当存在这样“寻找”的操作时,因为单纯依靠数组中的值没有办法判断当前位置是否可用,因此需要通过一个标记来指明对应位置的状态。哈希表中的元素包括:空、占用、删除三种状态。空——位置没有元素,可以存放新元素;占用——当前位置已存在有效元素;删除——当前位置元素无效。给定这三种状态将使得我们的插入、删除、查找操作变得容易起来。

        给出如下基本结构,使用结构体将值和状态组织起来,存在数组中构成哈希表,_num成员记录存入的元素数量。

	//因为哈希冲突的存在,我们在占用对应位置时需要先对哈希表中对应位置的数据进行性质判断//哈希表中的元素包括 有效、已删除、空 三种状态,通过枚举常量来定义出这三种状态enum state{EMPTY,DELETE,EXIST};//哈希表元素既要包含key-value,还需要包含状态,因此使用结构体template <class K, class V>struct HashData{pair<K, V> _kv;state _state = EMPTY;};template<class K, class V, class HashFunc = HashFunc<K>>class HashTable {public://构造函数HashTable(){_table.resize(10);}private:vector<HashData<K, V>> _table;size_t _num = 0;};

7.2.2 哈希表模板

    template<class K, class V, class HashFunc = HashFunc<K>>

         对于这个模板参数,重点解释一下其中的HashFunc的作用。我们的哈希表采用除留取余的方法计算映射位置,采取这个哈希方法就表明我们的key必须要是可以取模的值,也就是整型。但是key不一定都是整型,例如string类型的key也是完全可以的。所以设定了HashFunc这个模板参数,用于接受一个仿函数,这个仿函数可以将key转化为可以进行取模运算的形式。

	//需要注意,当前哈希表的哈希函数是直接对key取模//但是当哈希表元素的key是其他类型,如string时,很明显这种取模运算就会产生错误,因此我们需要想办法将string对象转化为可以取模运算的数值//我们通过模板参数HashFunc来处理,这是一个仿函数类,通过这个仿函数我们可以得到到对应对象的可进行模运算的数字//这是哈希表缺省仿函数类,当key类型可以转换成size_t时即可不传参采取默认的仿函数template<class K>struct HashFunc {size_t operator()(const K& key){return (size_t)key;}};//类模板的特化,这样string类型也可以采取默认的仿函数了template<>struct HashFunc<string> {size_t operator()(const string& s){size_t ret = 0;for (const auto& e : s){ret = ret * 31 + e;	//string对象采取每次乘31再加下一个字符的策略}return ret;}};

        上述代码给出了HashFunc的模板类,对于一般的key直接尝试强制类型转换。另外可以对一些特殊情况,如string写出其模板特化,人为的将其转化为可计算的形式。

        使用了特化的形式就可以在哈希表的模板参数处给定缺省值,这样可以在实例化模板时不需要考虑string的HashFunc问题,自动使用缺省值的特化。

7.2.3 insert插入

        插入元素的操作有多个注意点,我们一一列举。

        ①哈希冲突处理方式

        在插入元素的过程中,势必会出现哈希冲突,而闭散列规定,当发生哈希冲突时,把当前的值存储到冲突位置的“下一个”位置。所以问题就转化为了如何去找这“下一个位置”。

        处理的方案有很多,此处只介绍两种方法。一般常用的方法是线性探测,即当发现当前位置冲突时,就+1尝试存在后一个位置,如果仍然冲突就继续向后+1寻找,本文采用的即为这种方法。另一种是很相似的二次探测,其冲突后所加的值为二次序列,即1 4 9 16...。

        当状态是EXIST的时候就说明被占用发生冲突,需要持续寻找直到找到非EXIST的位置。

        ②扩容

        如此向哈希表中存储肯定有容量满的时刻,因此我们还需要处理扩容逻辑。规定负载因子(实际就是数组占用率)大于等于0.7的时候就进行扩容。

        因为我们使用的是除留余数法,是根据哈希表的长度来进行取模映射的,所以当哈希表长度变化时,也就代表我们需要重新映射元素位置了。重新映射实际上也就是将原来的元素依次插入新扩容的表,于是我们可以复用Insert函数,代替我们完成插入操作。虽然效率没有变化,但是少写了部分代码,还是很棒的。

        ③不允许重复元素

        哈希表和set、map一样不允许重复元素的出现,因此需要通过Find来排除重复元素的情况。

		//插入bool Insert(const pair<K,V>& kv){//使用find函数排除重复的情况if (Find(kv.first)){return false;}//处理扩容的情况//当负载因子(已存储元素/哈希表总长度)>= 0.7时就进行扩容//已存储元素由成员_num记录,每次成功insert后_num++if (_num * 10 / _table.size() >= 7){//因为哈希函数计算中参考了哈希表长度,所以在扩容后会使得映射关系发生变化,所以需要一个一个重新调整//扩容方法:定义一个原先size大小二倍的数组,遍历原来的哈希表,将值一一映射到新的哈希表中,最后将这个局部变量与旧表交换,出函数作用域销毁旧表留下新表//我们发现上述方法中,将原值重新映射到新的哈希表中实际上就是对新表的insert,于是我们可以复用insert逻辑//由于在此阶段,新表不会涉及到重复或扩容问题,所以实际复用的是之后正常的插入逻辑HashTable<K, V> newtable;newtable._table.resize(2 * _table.size());for (int i = 0; i < _table.size(); i++){if (_table[i]._state == EXIST){newtable.Insert(_table[i]._kv);}}_table.swap(newtable._table);}HashFunc hfs;size_t hashi = hfs(kv.first) % _table.size();//当发生哈希冲突时,采取线性探测的方法找寻下一个空位置//线性探测:从发生冲突的位置开始,依次向后逐个检查是否有空位置//与之相似的探测方法还有二次探测:从发生冲突的位置开始,按二次方的规律找后1、4、9、16…的位置是否有空位置while (_table[hashi]._state == EXIST){hashi = (hashi + 1) % _table.size();}//找到空位(DELETE、EMPTY),修改对应地址下的数据_table[hashi]._kv = kv;_table[hashi]._state = EXIST;_num++;return true;}

7.2.4 Find查找

        查找逻辑需要依照哈希函数找到开始查找的位置,也需要安装哈希冲突的处理方法既定的逻辑,按图索骥寻找可能的位置。在此过程中只有碰到了EMPTY才说明查找完毕,因为DELETE是被删除位置,它的“下一个位置”仍可能是通过哈希冲突占用的元素。

		//查找HashData<K, V>* Find(const K& key){HashFunc hsf;size_t hashi = hsf(key) % _table.size();while (_table[hashi]._state != EMPTY)	//注意DELETE不能作为判断查找完毕的标志{if (_table[hashi]._kv.first == key){return &_table[hashi];}hashi = (hashi + 1) % _table.size();}return nullptr;}

 7.2.5 Erase删除

        删除操作只需要找到位置后,将状态置为DELETE即可。

		//删除//删除只需要将对应位置的状态置为删除即可bool Erase(const K& key){if (HashData<K, V>* dst = Find(key)){dst->_state = DELETE;_num--;return true;}else{return false;}}

7.3 开散列(链地址法/拉链法)

        开散列在哈希表的每个位置定义一个哈希桶(也就是链表),在发生冲突时将后来的结点链接在当前位置的链表后,即一个位置可以有多个由链表链接起来的元素。

7.3.1 结构设计

        拉链法的哈希表每个位置由一个哈希桶组成,因此按照单链表的方法定义链表节点即可。由于每个元素均是new的结点,因此需要一个析构函数遍历整个哈希表的所有哈希桶来析构结点。

	template <class K, class V>struct HashNode{//结点的构造函数HashNode(const pair<K, V>& kv):_kv(kv),_next(nullptr){}pair<K, V> _kv;HashNode<K, V>* _next;};template<class K, class V, class HashFunc = HashFunc<K>>class HashTable {public:typedef HashNode<K, V> Node;//构造函数HashTable(){_table.resize(10, nullptr);}//析构函数~HashTable(){for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* tmp = cur;cur = cur->_next;delete tmp;}_table[i] = nullptr;}}private:vector<Node*> _table;size_t _num = 0;};

        拉链法的哈希表模板参数与开放地址法相同,不再解释。

7.3.2 Insert插入 

        和开放地址法相似的逻辑,也不允许重复元素,除此之外还有一些值得注意的问题。

        ①扩容

        首先是负载因子,拉链法由于采取链表的方式,因此不会存在存不进去的问题。哈希表短,元素多只会导致链表过长效率变低,因此仍然使用负载因子,当其大于1时就扩容。

        如果扩容逻辑参照开放地址法,创建新表再复用Insert函数,看似方便,但实际存在效率问题。因为拉链法采取的是链表形式,所以Insert操作需要new结点,而在全部插入后又将原来的所有节点都释放了,相当于把旧有结点全部释放然后又重新申请。

        为了避免这个问题,可以考虑直接转移结点,即根据结点的值直接更改结点的链接_next,这样就避免了重复的new和delete。

        ②插入方法

        对于一个哈希桶,当做一个单链表即可,最方便的插入元素方式即为头插。

		//插入bool Insert(const pair<K, V>& kv){//使用find函数排除重复的情况if (Find(kv.first)){return false;}HashFunc hfs;size_t hashi = hfs(kv.first) % _table.size();//处理扩容的情况//当负载因子(已存储元素/哈希桶总数量(也即哈希表长度))>= 1时就进行扩容,//已存储元素由成员_num记录,每次成功insert后_num++if (_num  / _table.size() >= 1){//同样的,在扩容后会使得映射关系发生变化,需要一个一个重新调整//扩容方法:定义一个原先size大小二倍的数组,遍历原来的哈希表,将值一一映射到新的哈希表中,最后将这个局部变量与旧表交换,出函数作用域销毁旧表留下新表//我们采取老方法,将这个过程看作向新表插入数据,复用insert//但是这种方法需要将原哈希表的结点全部释放后再new一遍,效率低/*HashTable<K, V> newtable;newtable._table.resize(2 * _table.size(), nullptr);for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){newtable.Insert(cur->_kv);cur = cur->_next;}}_table.swap(newtable._table);*///我们可以采取直接转移结点的方法,遍历原哈希表,将其中的结点直接链在新哈希表中,实际上就是把Insert的复用部分再写一遍vector<Node*> newtable;	//新建一个vector作为新的tablenewtable.resize(2 * _table.size(), nullptr);for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* tmp = cur->_next;//记录cur的nextsize_t hashi = hfs(cur->_kv.first) % newtable.size();//根据新表计算地址//头插cur->_next = newtable[hashi];newtable[hashi] = cur;cur = tmp;}_table[i] = nullptr;//原表置空是好习惯,此处换出去的是vector,析构时析构的也是vector<Node*>不会调用到哈希表的析构函数,所以不会释放结点,不置空也不会有问题}_table.swap(newtable);}//找到了对应位置后直接头插即可Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;_num++;return true;}

7.3.3 Find查找

        在拉链法下查找,只需要根据哈希函数找到对应位置,然后遍历哈希桶即可。

		//查找//哈希函数定位到位置,遍历哈希桶寻找Node* Find(const K& key){HashFunc hsf;size_t hashi = hsf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}

7.3.4 Erase删除

        删除操作实际就只是单链表的删除操作,根据哈希函数确定单链表,然后进行删除即可,这是单链表的基操。

		//删除//因为是单链表的删除,需要遍历桶找到前驱节点,并且考虑头删的问题bool Erase(const K& key){HashFunc hsf;size_t hashi = hsf(key) % _table.size();Node* cur = _table[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;_num--;return true;}else{prev = cur;cur = cur->_next;}}return false;}

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

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

相关文章

SpringBoot简易商品管理系统

> 这是一个基于SpringBootThymeleaf实现的简易商品管理系统。 > 包含基本的登录/注册与商品管理功能。 > 界面简洁美观&#xff0c;代码结构清晰&#xff0c;适用于JAVA初学者在此基础上进行二次开发。 一、项目演示 二、技术框架 框架描述Spring Boot容器管理 S…

毫米波雷达预警功能 —— 开门预警(DOW)

文档声明&#xff1a; 以下资料均属于本人在学习过程中产出的学习笔记&#xff0c;如果错误或者遗漏之处&#xff0c;请多多指正。并且该文档在后期会随着学习的深入不断补充完善。感谢各位的参考查看。 笔记资料仅供学习交流使用&#xff0c;转载请标明出处&#xff0c;谢谢配…

16代现场实拍图

64*32 全彩 LED 点阵显示屏 无线通信868M&#xff0c;跳频通信 通信速率200K/50K 覆盖20-30米以上的通信半径 尺寸&#xff1a;192*96mm 供电方式&#xff1a;24V外置电源 储存温度&#xff1a;-40℃~80℃ 工作温度&#xff1a;-20℃~50℃ 自定义双向通信协议&#xff…

工业无线路由器组网方案:简单方便的工业组网方案

​一、项目背景 随着工业互联网的发展&#xff0c;越来越多的企业开始寻求高效、稳定的网络解决方案&#xff0c;以支持其生产和管理的数字化转型。工业无线路由器在这一过程中扮演着重要的角色。本文将详细介绍基于星创易联SR500工业无线路由器的组网方案&#xff0c;适用于制…

解锁2024年翻译在线Top4,让每一次交流都精准无误

现在世界就像个大家庭&#xff0c;交流多了&#xff0c;语言不通就成了问题。有道翻译在线就像桥梁&#xff0c;帮我们和全世界的朋友沟通。对企业来说&#xff0c;翻译准确太重要了&#xff0c;一句话翻错可能损失巨大。有道翻译在线技术强&#xff0c;各种语言都能搞定&#…

亲测好用,ChatGPT 3.5/4.0新手使用手册,最好论文指令手册~

本以为遥遥领先的GPT早就普及了&#xff0c;但小伙伴寻找使用的热度一直高居不下&#xff0c;其实现在很简单了&#xff01; 国产大模型快200家了&#xff0c;还有很多成熟的国内AI产品&#xff0c;跟官网一样使用&#xff0c;还更加好用~ ① 3.5 大多数场景是够用的&#xff…

【贪心算法】贪心算法二

贪心算法二 1.最长递增子序列2.递增的三元子序列3.最长连续递增序列 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.最长递增子序列 题目链…

从零基础到大模型大师,看完这篇,效率提高99%!!

一、 前置知识 - 数学&#xff1a;主要包括线性代数、概率统计等内容。 - 自然语言处理&#xff1a;主要包括Word2vec、Seq2seq等内容。 - Python&#xff1a;Python语言是大模型应用的编程基础&#xff0c;需要熟练掌握深度学习相关的框架&#xff0c;如PyTorch、TensorFlow。…

【C++ Primer Plus习题】17.1

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: #include <iostream> using namespace std;int main() {char …

智慧城市主要运营模式分析

(一)运营模式演变 作为新一代信息化技术落地应用的新事物,智慧城市在建设模式方面借鉴了大量工程建设的经验,如平行发包(DBB,Design-Bid-Build)、EPC工程总承包、PPP等模式等,这些模式在不同的发展阶段和条件下发挥了重要作用。 在智慧城市发展模式从政府主导、以建为主、…

[数据集][目标检测]中草药类型识别检测数据集VOC+YOLO格式7976张45类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;7976 标注数量(xml文件个数)&#xff1a;7976 标注数量(txt文件个数)&#xff1a;7976 标注…

音乐项目,总结

今天的写的思路都挺简单的但是比较繁琐&#xff0c;这个查找&#xff0c;传文件的话可以了&#xff0c;但是没有用分片传送&#xff0c;然后在写音乐播放的处理&#xff0c;<歌单&#xff0c;二级评论&#xff0c;歌曲歌词滚轮播放>三个还没有实现&#xff0c;时间挺紧张…

[Angular] 从零开始使用 Angular CLI 创建 Angular 项目

一、安装 Node.js &#x1f4da; Node.js 是一个运行 JavaScript 代码的 JavaScript 运行时&#xff0c;它允许我们在服务器端运行 JavaScript 代码。以下是安装 Node.js 的步骤&#xff1a; &#x1f310; 访问 Node.js 国内网站&#xff1a;https://nodejs.cn/ &#x1f4…

【2023工业图像异常检测文献】SimpleNet

SimpleNet:ASimpleNetworkforImageAnomalyDetectionandLocalization 1、Background 图像异常检测和定位主要任务是识别并定位图像中异常区域。 工业异常检测最大的难题在于异常样本少&#xff0c;一般采用无监督方法&#xff0c;在训练过程中只使用正常样本。 解决工业异常检…

【CSS in Depth 2 精译_034】5.4 Grid 网格布局的显式网格与隐式网格(下)

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一章 层叠、优先级与继承&#xff08;已完结&#xff09; 1.1 层叠1.2 继承1.3 特殊值1.4 简写属性1.5 CSS 渐进式增强技术1.6 本章小结 第二章 相对单位&#xff08;已完结&#xff09; 2.1 相对…

Qt-QTextEdit的输入类控件(30)

目录 描述 相关属性 相关信号 使用 文本内容改变时触发 选中内容时发生改变 光标位置发生改变时触发 可复制&#xff0c;可撤销&#xff0c;可恢复发生改变时触发 undo撤销 redo恢复 copy复制 描述 这是一个多行输入框 有两个很像的&#xff0c;需要注意一下&…

边缘计算网关在工业中的应用

在工业4.0和智能制造的浪潮中&#xff0c;边缘计算网关扮演着至关重要的角色。AIoTedge边缘计算网关&#xff0c;作为工业互联网的关键组件&#xff0c;通过其强大的数据处理能力和智能分析功能&#xff0c;正在改变工业生产的面貌。 边缘计算网关的定义与角色 边缘计算网关是…

Python学习——【4.3】数据容器:str字符串

文章目录 【4.3】数据容器&#xff1a;str字符串一、再识字符串二、字符串的下标&#xff08;索引&#xff09;三、字符串的常用操作四、字符串的遍历五、字符串的特点 【4.3】数据容器&#xff1a;str字符串 一、再识字符串 虽然之前已经学习过字符串&#xff0c;但此处我们需…

自制工具 | 局域网文本、文件传输与共享,跨设备剪切板(预览版)

本文将介绍一款自制文本和文件传输工具&#xff0c;可实现局域网内文本/文件传输与共享。支持传输文本&#xff0c;可一键复制、粘贴&#xff0c;文本字数无限制&#xff0c;以缩略形式展示。支持传输文件&#xff0c;文件大小无限制。工作在局域网&#xff0c;含操作校验&…

买家账号被feng?探讨亚马逊、Lazada和速卖通的解决方案

账号登录的安全注意事项 当亚马逊买家账号在多个手机或电脑上进行登录时&#xff0c;这些设备的独特硬件网络参数&#xff0c;如设备型号、地区码、监管码、主板码、WIFI地址等&#xff0c;均会被亚马逊系统读取并记录。特别是在风控加强期间&#xff0c;平台会深入分析这些参…