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.rhkb.cn/news/432081.html

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

相关文章

JavaWeb - 7 - SpringBootWeb入门

Spring 官网&#xff1a;Spring | Home Spring发展到今天已经形成了一种开发生态圈&#xff0c;Spring提供了若干个子项目&#xff0c;每个项目用于完成特定的功能 SpringBoot SpringBoot可以帮助我们非常快速的构建应用程序、简化开发、提高效率 一.SpringBootWeb入门 需求…

【React】样式控制

1.react组件样式控制方式 行内样式&#xff08;通过style属性&#xff09;&#xff1a;不推荐class类名控制 function App() {const style {color: skyblue,fontSize: 20px}return (<div className"App"><span style{{ color: "pink", fontSiz…

N诺计算机考研-错题

B A.LLC&#xff0c;逻辑链路控制子层。一个主机中可能有多个进程在运行&#xff0c;它们可能同时与其他的一些进程&#xff08;在同一主机或多个主机中&#xff09;进行通信。因此在一个主机的 LLC子层的一个服务访问点&#xff0c;以便向多个进程提供服务。B.MAC地址&#xf…

VSCode#include头文件时找不到头文件:我的解决方法

0.前言 1.在学习了Linux之后&#xff0c;我平常大部分都使用本地的XShell或者VSCode连接远程云服务器写代码&#xff0c;CentOS的包管理器为我省去了不少繁琐的事情&#xff0c;今天使用vscode打开本地目录想写点代码发现#include头文件后&#xff0c;下方出现了波浪线&#…

手机解压软件加密指南:让文件更安全

在数字化时代&#xff0c;文件加密对于保护个人隐私和敏感信息的重要性不言而喻。随着互联网的飞速发展&#xff0c;我们的生活和工作越来越依赖于数字设备和网络。 然而&#xff0c;这也带来了一系列的安全风险&#xff0c;如黑客攻击、数据泄露等。文件加密技术成为了保护我…

keil软件开发流程

1.先建一个文件 2.然后打开keil&#xff0c;打开keil软件新建keil工程 3.确定保存的位置以及工程的名字 4.确定开发工程所用的单片机芯片 5.复制启动文件到工程中选择否 6.创建新的C文件 7.保存C文件 8.写一个代码 9.编译代码 10.勾选设置&#xff0c;生成可执行文件 10.构建代…

无人机之可承受风速的影响因素

无人机可承受风速的影响因素是多方面的&#xff0c;这些因素共同决定了无人机在特定风速条件下的飞行稳定性和安全性。以下是一些主要的影响因素&#xff1a; 一、无人机设计与结构 无人机的大小、形状和重量都会直接影响其抗风能力。大型无人机由于具有更大的表面积和质量&am…

HAproxy-7层负载均衡集群根据不同服务请求分配服务器

搭建HAproxy----7层负载均衡集群的补充 https://blog.csdn.net/qq_73990369/article/details/142500451?spm1001.2014.3001.5501 一、再准备两台虚拟机进行测试 192.168.229.15/24 ----php1 192.168.229.16/24 ----php2 1、PHP1 & php2(192.168.229.15/24 ,192…

SVG之path详解,全面解析椭圆弧命令A

前言&#xff1a; 转载于b站深坑妙脆角&#xff0c;讲解清晰明了&#xff0c;对初使用path圆弧命令非常友好 作者&#xff1a;深坑妙脆角 https://www.bilibili.com/read/cv35872299/?jump_opus1 出处&#xff1a;bilibili 简述&#xff1a; SVG 中的 <path> 元素用于创…

大数据Flink(一百二十三):五分钟上手Flink MySQL连接器

文章目录 五分钟上手Flink MySQL连接器 一、创建数据库表 二、​​​​​​创建session集群 三、源表查询 四、​​​​​窗口计算 五、​​​​​​结果数据写回数据库 五分钟上手Flink MySQL连接器 MySQL Connector可以将本地或远程的MySQL数据库连接到Flink中&#x…

16【Protues51单片机仿真】智能洗衣机倒计时系统

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 用直流电机转动模拟洗衣机。要求 有弱洗、普通洗、强洗三种模式&#xff0c;可通过按键选择。可以设置洗衣时长&#xff0c;通关按键选择15、30、45、60、90分钟。时间到蜂鸣器报警提示。LCD 显示…

Koa (下一代web框架) 【Node.js进阶】

koa (中文网) 是基于 Node.js 平台的下一代 web 开发框架&#xff0c;致力于成为应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石&#xff1b; 利用 async 函数 丢弃回调函数&#xff0c;并增强错误处理&#xff0c;koa 没有任何预置的中间件&#xff0c;可快速…

大数据Hive组件安装

组件版本 组件版本Hadoop3.3.0JDK1.8.0_241Mysql5.7.25Hive3.1.2 Hadoop集群服务分布 Node1Node2Node3NameNode DataNode DataNodeDataNode NodeManager NodeManagerResourceManagerSecondaryNameNode 安装前请确定Hadoop集群服务全部启动&#xff0c;不然后续测试时会报…

Python面向对象编程:类和对象①

文章目录 一、什么是面向对象编程1.1 面向对象编程的基本概念1.2 Python中的类和对象 二、定义类和创建对象2.1 定义类2.2 创建对象2.3 __init__方法2.4 self参数 三、类的属性和方法3.1 类的属性3.1.1 实例属性3.1.2 类属性 3.2 类的方法3.2.1 实例方法3.2.2 类方法3.2.3 静态…

解锁HTML的力量:从基础标签到完整网页构建

在整个学习编程技能的过程中&#xff0c;我们会始终基于编程的本质&#xff1a;输入-》函数处理-》输出 和编程语言的本质&#xff1a;语法糖、变量、基础函数&#xff0c;去理解各种编程技术和学习相关的技能。 今天开始学习编程的第一个技能点&#xff1a;HTML。正如编程的本…

2024 Snap 新款ar眼镜介绍

2024 snap 新款ar眼镜介绍 2024 Snap 新款ar眼镜介绍 助力快速掌握数据集的信息和使用方式。

EasyCVR全方位安全守护智慧电厂:构建高效视频监控系统优势分析

随着信息技术的飞速发展和数字化时代的到来&#xff0c;电厂作为能源供应的重要枢纽&#xff0c;其安全性和管理效率成为社会各界关注的焦点。为了满足电厂对高效、智能、可靠视频监控系统的需求&#xff0c;基于EasyCVR平台建设的电厂视频监控系统应运而生。 一、系统构成 基…

排序个人总结

插入排序 思路&#xff1b;定义 i 和 j&#xff0c;默认 i 前面的数都是有序的&#xff0c;j 定义为 i 的前一个数&#xff0c;把 i 的值给tmp&#xff0c;tmp与j对应的值进行比较&#xff0c;如果arr[j] > tmp,将arr[j] (大的数前移一位)&#xff0c;如下图 代码&#xf…

String类常用的方法

源代码&#xff1a; 输出结果&#xff1a;

从HarmonyOS Next导出手机照片

1&#xff09;打开DevEco Studio开发工具 2&#xff09;插入USB数据线&#xff0c;连接手机 3&#xff09;在DevEco Studio开发工具&#xff0c;通过View -> Tool Windows -> Device File Browser打开管理工具 4&#xff09;选择storage -> cloud -> 100->fi…