C++--哈希表--散列--冲突--哈希闭散列模拟实现

文章目录

  • 哈希概念
  • 一、哈希表闭散列的模拟实现
  • 二、开散列(哈希桶)的模拟实现
    • 数据类型定义
    • 析构函数
    • 插入
    • 查找
    • 删除


哈希概念

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素

当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述

问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?
哈希冲突
对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?
哈希函数:
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
  • 域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见的哈希函数:

  1. 直接定址法–(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况
    在这里插入图片描述
    在这里插入图片描述
  1. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

哈希冲突解决
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
线性探测插入:
在这里插入图片描述

上面这个场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入通过哈希函数获取待插入元素在哈希表中的位置。如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

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


提示:以下是本篇文章正文内容,下面案例可供参考

一、哈希表闭散列的模拟实现

哈希表中元素状态

enum State
{EMPTY,//空EXIST,//存在DELETE,//删除
};

存储类型用pair即可,但是数据中要包含状态,我们进行一次封装

//由于数据需要一个状态,所以需要将pair<K,V>封装一层template<class K,class V>struct HashDate{pair<K, V>_kv;State _state;};

构建哈希表的内容

template<class K,class V>class HashTable{public:bool Insert(const pair<K,V>& kv);HashDate<K, V>* find(const K& key);bool Erase(const K& key);private:vector<HashDate<K,V>> _table;size_t _n= 0;};

闭散列的插入:

		bool insert(const pair<K, V>& kv){if (Find(kv.first))return false;if (_table.size() == 0 || 10 * _n / _table.size() >= 0.7) //如果hash表的负载因子 >= 0.7 || hash表一开始为空{size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;HashTable<K, V> NewHashTable;NewHashTable._table.resize(newsize);for (auto& e : _table)  //这里的e每一个vector里面的值-》pair AND _s{if (e._s == EXIST){NewHashTable.insert(e._kv);  //对象不同,调用的成员函数里面的内容也不同}}//走到这里,说明已经将原来的vector里面的内容拷贝到现在newHashTable里面_table.swap(NewHashTable._table);  //vector析构的时候会调用HashData<K, V>的析构函数,但是HashData<K, V>里面没有动态开辟的内存,所以不需要在HashData里面写一个析构函数}HashFuni<K> hashfuni;size_t hashi = hashfuni(kv.first) % _table.size();  //计算表中的下标while (_table[hashi]._s == EXIST){hashi++;hashi %= _table.size();}_table[hashi]._kv = kv;_table[hashi]._s = EXIST;_n++;return true;}

闭散列的查找:

		HashData<K, V>* Find(const K& key){HashFuni<K> hashfuni;size_t hashi = hashfuni(key) % _table.size();while (_table[hashi]._s != EMPTY){//这里必须使用两个条件,因为我们的HashTable的删除并不是真正的删除,仅仅只是修改状态_s和_nif (_table[hashi]._kv.first == key && _table[hashi]._s == EXIST){return &_table[hashi];}hashi++;hashi %= _table.size();}return nullptr;}

闭散列的删除:

		bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_s = DELETE;_n--;return true;}elsereturn false;}

二、开散列(哈希桶)的模拟实现

开散列:又叫链地址法(开链法),首先对key值集合用哈希函数计算映射下标,具有相同下标的key值归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
在这里插入图片描述

如上图所示,此时的哈希表中存放的是一个单链表的头指针。

  • 不同的数据,根据哈希函数算出的映射位置发生哈希碰撞时,这些碰撞的数据会挂在哈希表对应位置指向的单链表中。这些单链表被形象称为桶。
  • 每个桶中放的都是发生哈希冲突的元素。
  • 当有新数据插入时,进行头插。

如上图中所示,7,27,57,根据哈希函数都映射到哈希表下标为7的位置,这几个数据按照头插的顺序以单链表的形式挂在哈希表下标为7的位置。

新插入的数据如果尾插的话,在找单链表的尾部时,会有效率损失,由于没有排序要求,所以头插是效率最高的。

闭散列的方法,通常被称为哈希桶,使用的也最广泛,能够解决闭散列中空间利用率不高的问题。

数据类型定义

在这里插入图片描述
采用哈希同的方式来解决哈希碰撞时,哈希表中存放的数据是单链表的头节点,如上图所示。

  • 链表节点中,有键值对,还有下一个节点的指针。
  • 仍然使用闭散列中转换整形的仿函数。

析构函数

在哈希桶的构造函数中,哈希表的初始大小是10个元素,每个元素都是nullptr,因为此时还没有桶。
在这里插入图片描述
哈希桶必须有析构函数,闭散列的方式,默认生成的析构函数就能满足要求,但是哈希桶不可以。
如果只使用默认生成的析构函数,在哈希桶销毁的时候,默认的析构函数会调用vector的析构函数。
vector的析构函数只会释放vector的本身,而不会释放vector上挂着的桶。

插入

在这里插入图片描述

  • 在经过哈希函数映射后,将键值对插入到哈希表对应位置除的桶中。
  • 插入到桶中时,使用头插,如上图中蓝色框所示,这俩步的不能反,必须按照这个顺序,否则无法实现头插。

插入的扩容

一般情况下,当哈希表的负载因子等于1的时候,发生扩容。
在这里插入图片描述

  • 当负载因子等于1时,也就是数据个数和哈希表大小相等的时候进行扩容。
  • 扩容和闭散列类似,将旧的哈希表中的数据插入到新哈希表中,复用Insert函数,然后旧表被释放,新表留下来。
    但是这种方式不是很好,有很大的开销,效率有所损失:
    在这里插入图片描述

在将旧表中的数据插入新表的时候,每插入一个,新表就需要new一个节点,旧表中的所有节点都会被new一遍。

然后将旧表中的所有节点再释放,这里做了没必要的工作。相同的一个节点,会先在新表中new一个,再释放旧表的。

新表中完全可以不再new新的节点,直接使用旧表中的节点。

  • 旧表中可以直接复用的节点是:改变了哈希表容量以后,映射关系不变的节点。
  • 比如节点27,哈希表的容量从10变成20,但是映射后的下标仍然是7,这样的节点就可以复用。

那些映射关系变了的节点就不可以直接复用了,需要改变所在桶的位置。

如节点18,哈希表的容量从10变成20,映射后的下标从8变成18,此时就需要改变18所在的桶了。

在这里插入图片描述

这里不用创建新的哈希桶结构,只创建底层的vector就可以,因为不再复用Insert了。将旧表中的数据一个个拿出来,通过哈希函数重新计算映射关系,并且头插到新新表的桶中。
旧表的每个桶中的数据处理完后,必须把表中的单链表头置空,因为此时新表和旧表都指向这些桶,否则在旧表析构的时候会析构掉所有桶,导致新表中没有数据。

查找

在这里插入图片描述

删除

在这里插入图片描述
如上图所示,使用Find先找到key值所在哈希表中的位置,然后删除。

哈希表挂的桶是单链表,只指定要删除节点是无法进行删除的,必须指定前一个节点,否则无法再链接。

所以上面的方法是不能用的,只能拿着key值通过哈希函数重新寻找哈希表中的key值,在这个过程中同时记录前一个节点prev。

		bool Erase(const K& key){HashFuni<K> hashfuni;size_t hashi = hashfuni(key) % _table.size();  //计算表中的下标Node* cur = _table[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (cur == _table[hashi]){_table[hashi] = cur->_next;delete cur;}else{prev->_next = cur->_next;delete cur;}return true;}else{prev = cur;cur = cur->_next;}}return false;}

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

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

相关文章

C#WPF文本转语音实例

本文介绍C#WPF文本转语音实例 实现方法:使用类库(SpeechSynthesizer )实现的。 一、首先是安装程序包。 二、创建项目 需要添加引用using System.Speech.Synthesis; UI界面 <Windowx:Class="TextToSpeechDemo.MainWindow"xmlns="http://schemas.micr…

JVM垃圾回收相关概念

目录 一、System.gc()的理解 二、内存溢出与内存泄露 &#xff08;一&#xff09;OOM &#xff08;二&#xff09;内存泄露 三、StopTheWorld 四、垃圾回收的并行与并发 五、安全点与安全区域 &#xff08;一&#xff09;安全点 &#xff08;二&#xff09;安全区域 …

【JavaEE初阶】计算机是如何工作的

一、计算机发展史 计算的需求在⼈类的历史中是广泛存在的&#xff0c;发展大体经历了从⼀般计算⼯具到机械计算机到目前的电子计算机的发展历程。 人类对计算的需求&#xff0c;驱动我们不断的发明、改善计算机。目前这个时代是“电子计算机”的时代&#xff0c;发展的潮流是…

Qt按钮大全续集(QCommandLinkButton和QDialogButtonBox )

## QCommandLinkButton 控件简介 QCommandLinkButton 控件中文名是“命令链接按钮”。QCommandLinkButton 继承QPushButton。CommandLinkButton 控件和 RadioButton 相似,都是用于在互斥选项中选择一项。表面上同平面按钮一样,但是 CommandLinkButton 除带有正常的按钮上的文…

通过汇编理解cortex-m3:第0章

第0章&#xff1a;准备工作 基本想法&#xff1a;利用汇编和gdb调试&#xff0c;来学习cortex-m3汇编指令&#xff0c;以及一些寄存器的功能。 软件和硬件&#xff1a; 硬件&#xff1a;韦东山瑞士军刀中的最小核心板&#xff08;STM32F103C8T6&#xff09; STLINK-V2&#…

初刷leetcode题目(3)——数据结构与算法

&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️Take your time ! &#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️…

ROS参数服务器(Param):通信模型、Hello World与拓展

参数服务器在ROS中主要用于实现不同节点之间的数据共享。 参数服务器相当于是独立于所有节点的一个公共容器&#xff0c;可以将数据存储在该容器中&#xff0c;被不同的节点调用&#xff0c;当然不同的节点也可以往其中存储数据。 使用场景一般存储一些机器人的固有参数&…

C语言实现冒泡排序(超详细)

排序算法 - 冒泡排序 什么是冒泡排序&#xff1f;冒泡排序有啥用呢&#xff1f;冒泡排序的实现代码讲解冒泡排序的总结 什么是冒泡排序&#xff1f; 冒泡排序是一种简单的排序算法&#xff0c;它重复地遍历要排序的列表&#xff0c;一次比较两个元素&#xff0c;如果它们的顺序…

OpenHarmony源码下载

OpenHarmony源码下载 现在的 OpenHarmony 4.0 源码已经有了&#xff0c;在 https://gitee.com/openharmony 地址中&#xff0c;描述了源码获取的方式&#xff0c;但那是基于 ubuntu 或者说是 Linux 的下载方式。在 windows 平台下的下载方式没有做出介绍。 我自己尝试了 wind…

Python 爬虫入门

文章目录 Python 爬虫入门requests 库beautifulsoup4库函数findall()&#xff0c;find()函数get() 爬虫实例 1&#xff1a;抓小说爬虫实例 2&#xff1a;抓豆瓣 top 250 的电影信息后记 Python 爬虫入门 Python 的爬虫功能使得程序员可以快速抓取并分析网页中的信息&#xff0…

Spring Cloud学习(十)【Elasticsearch搜索功能 分布式搜索引擎02】

文章目录 DSL查询文档DSL查询分类全文检索查询精准查询地理坐标查询组合查询相关性算分Function Score Query复合查询 Boolean Query 搜索结果处理排序分页高亮 RestClient查询文档快速入门match查询精确查询复合查询排序、分页、高亮 黑马旅游案例 DSL查询文档 DSL查询分类 …

大数据HCIE成神之路之数学(2)——线性代数

线性代数 1.1 线性代数内容介绍1.1.1 线性代数介绍1.1.2 代码实现介绍 1.2 线性代数实现1.2.1 reshape运算1.2.2 转置实现1.2.3 矩阵乘法实现1.2.4 矩阵对应运算1.2.5 逆矩阵实现1.2.6 特征值与特征向量1.2.7 求行列式1.2.8 奇异值分解实现1.2.9 线性方程组求解 1.1 线性代数内…

谈谈如何写作(二)

序言 没有什么比一套好理论更有用了。——库尔特勒温 谈谈如何写作系列今天进入第二篇&#xff0c;第一篇请速戳&#xff1a;谈谈如何写作&#xff08;一&#xff09; 今天&#xff0c;博主从如何写报告讲起。 Q&#xff1a;如何写报告 如何写报告呢&#xff1f; 当每位盆友接到…

【华为HCIP | 华为数通工程师】刷题日记1116(一个字惨)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

阿里云ECS11月销量王 99元/年

这一波好像真没得说&#xff0c;老用户居然都有份&#xff0c;买来练习、测试冒似已经够了&#xff01; 阿里云ECS11月销量王 99元/年 2核2G 3M固定带宽不限流量&#xff0c;新老同享&#xff0c;新购、续费同价&#xff0c;开发必备&#xff01; 活动规则 云服务器ECS 云创季…

DevToys:开发者的多功能瑞士军刀,让编程更高效!

DevToys&#xff1a;开发者的多功能瑞士军刀&#xff0c;让编程更高效&#xff01; DevToys 是一款专为开发者设计的实用工具&#xff0c;它能够帮助用户完成日常的开发任务&#xff0c;如格式化 JSON、比较文本和测试正则表达式&#xff08;RegExp&#xff09;。它的优势在于…

OpenAI Assistants-API简明教程

OpenAI在11月6号的开发者大会上&#xff0c;除了公布了gpt4-v、gpt-4-turbo等新模型外&#xff0c;还有一个assistants-api&#xff0c;基于assistants-api开发者可以构建自己的AI助手&#xff0c;目前assistants-api有三类的工具可以用。首先就是之前大火的代码解释器(Code In…

leetcode刷题日志-68.文本左右对齐

给定一个单词数组 words 和一个长度 maxWidth &#xff0c;重新排版单词&#xff0c;使其成为每行恰好有 maxWidth 个字符&#xff0c;且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单词&#xff1b;也就是说&#xff0c;尽可能多地往每行中放置单词。必要时可…

『亚马逊云科技产品测评』活动征文|借助AWS EC2搭建服务器群组运维系统Zabbix+spug

授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 Developer Centre, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道。 本文基于以下软硬件工具&#xff1a; aws ec2 frp-0.52.3 zabbix 6…

【STM32】ADC(模拟/数字转换)

一、ADC的简介 1.什么是ADC 1&#xff09;将【电信号】-->【电压】-->【数字量】 2&#xff09;ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字量&#xff0c;建立模拟电路到数字电路的桥梁。 3&#xff09;12位逐次逼近型ADC&#xff0c;1us转换时间&#xf…