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

news2024/12/26 1:00:02

文章目录

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


哈希概念

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和_n
				if (_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;
			}
			else
				return 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.coloradmin.cn/o/1227206.html

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

相关文章

【Linux进阶之路】动静态库

文章目录 回顾一. 静态库1.代码传递的方式2.简易制作3.原理 二. 动态库1.简易制作2.基本原理 尾序 回顾 前面在gcc与g的使用中&#xff0c;我们简单的介绍了动态库与静态库的各自的优点与区别&#xff1a; 动态链接库&#xff0c;也就是所有的程序公用一份代码,虽然方便省空间&…

ACWSpring1.3

首先,前端写ajax写上我们的访问路径(就在我们前端的源代码里面),我们建了两个包pkController用于前端页面url映射过来一层一层找到我们的RestController返回bot1里面有键值,返回的这就是一个session对象bot1这个map.前端拿到我们bot1里的两个值给到我们前端显示出来 1准备页面:…

《Fine-Grained Image Analysis with Deep Learning: A Survey》阅读笔记

论文标题 《Fine-Grained Image Analysis with Deep Learning: A Survey》 作者 魏秀参&#xff0c;南京理工大学 初读 摘要 与上篇综述相同&#xff1a; 细粒度图像分析&#xff08;FGIA&#xff09;的任务是分析从属类别的视觉对象。 细粒度性质引起的类间小变化和类内…

2023年【广东省安全员C证第四批(专职安全生产管理人员)】考试题库及广东省安全员C证第四批(专职安全生产管理人员)考试试卷

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 广东省安全员C证第四批&#xff08;专职安全生产管理人员&#xff09;考试题库根据新广东省安全员C证第四批&#xff08;专职安全生产管理人员&#xff09;考试大纲要求&#xff0c;安全生产模拟考试一点通将广东省安…

网络协议入门 笔记一

一、服务器和客户端及java的概念 JVM (Java Virtual Machine) : Java虚拟机&#xff0c;Java的跨平台:一次编译&#xff0c;到处运行&#xff0c;编译生成跟平台无关的字节码文件 (class文件)&#xff0c;由对应平台的JVM解析字节码为机器指令 (010101)。 如下图所示&#xff0…

【数据结构】C语言实现队列

目录 前言 1. 队列 1.1 队列的概念 1.2 队列的结构 2. 队列的实现 2.1 队列的定义 2.2 队列的初始化 2.3 入队 2.4 出队 2.5 获取队头元素 2.6 获取队尾元素 2.7 判断空队列 2.8 队列的销毁 3. 队列完整源码 Queue.h Queue.c &#x1f388;个人主页&#xff1a…

100.相同的树(LeetCode)

关于树的递归问题&#xff0c;永远考虑两方面&#xff1a;返回条件和子问题 先考虑返回条件&#xff0c;如果当前的根节点不相同&#xff0c;那就返回false&#xff08;注意&#xff0c;不要判断相等时返回什么&#xff0c;因为当前相等并不能说明后面节点相等&#xff0c;所以…

BatchNormalization:解决神经网络中的内部协变量偏移问题

ICML2015 截至目前51172引 论文链接 代码连接(planing) 文章提出的问题 减少神经网络隐藏层中的”内部协变量偏移”问题。 在机器学习领域存在“协变量偏移”问题,问题的前提是我们划分数据集的时候,训练集和测试集往往假设是独立同分布(i.i.d)的,这种独立同分布更有利于…

Java面向对象(高级)-- 类的成员之四:代码块

文章目录 一、回顾&#xff08;1&#xff09;三条主线&#xff08;2&#xff09;类中可以声明的结构及作用1.结构2.作用 二、代码块&#xff08;1&#xff09;代码块的修饰与分类1. 代码块的修饰2. 代码块的分类3. 举例 &#xff08;2&#xff09; 静态代码块1. 语法格式2. 静态…

2023年高压电工证考试题库及高压电工试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年高压电工证考试题库及高压电工试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人员上岗证考试大纲随机出的高压…

2023年【G1工业锅炉司炉】报名考试及G1工业锅炉司炉理论考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 G1工业锅炉司炉报名考试是安全生产模拟考试一点通生成的&#xff0c;G1工业锅炉司炉证模拟考试题库是根据G1工业锅炉司炉最新版教材汇编出G1工业锅炉司炉仿真模拟考试。2023年【G1工业锅炉司炉】报名考试及G1工业锅炉…

SQL INSERT INTO 语句详解:插入新记录、多行插入和自增字段

SQL INSERT INTO 语句用于在表中插入新记录。 INSERT INTO 语法 可以以两种方式编写INSERT INTO语句&#xff1a; 指定要插入的列名和值&#xff1a; INSERT INTO 表名 (列1, 列2, 列3, ...) VALUES (值1, 值2, 值3, ...);如果要为表的所有列添加值&#xff0c;则无需在SQL…

vscode c++ 报错identifier “string“ is undefined

vscode c 报identifier “string” is undefined 问题 新装了电脑, 装好vsc和g等, 发现报错 但开头并没问题 解决 shiftctrlp选择 C/C Edit:COnfigurations (JSON)自动生成打开 c_cpp_properties.json添加g路径等 "cStandard": "c11","cppStanda…

c盘清除文件

打开设置 搜索存储

跟随鼠标的粒子特效分享

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 广告打完,我们进入正题,先看效果: 上代码: html, body {padding: 0;margin: 0;overflow: hidden; }import * as PIXI from https://cdn.skypack.dev/pixi.js@7.2.…

⑩⑥ 【MySQL】详解 触发器TRIGGER,协助 确保数据的完整性,日志记录,数据校验等操作。

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 触发器 ⑩⑥ 【MySQL】触发器详解1. 什么是触发…

鲜花植物展示预约小程序的作用有哪些

同城需求量高&#xff0c;所面对的消费者一般都是一束或几束订购需求&#xff0c;采购也有但少&#xff0c;同时还有新店开业花篮、新人结婚布置婚车等服务&#xff0c;零售与增值服务较多&#xff0c;尤其遇上节日单子增多&#xff0c;则制作鲜花、服务排序不清&#xff0c;微…

轻松掌控财务,分析账户花销,明细记录支出情况

随着科技的发展&#xff0c;我们的生活变得越来越智能化。然而&#xff0c;对于许多忙碌的现代人来说&#xff0c;管理财务可能是一件令人头疼的事情。复杂的账单、花销、收入&#xff0c;这些可能会让你感到无从下手。但现在&#xff0c;我们有一个全新的解决方案——一款全新…

【算法挨揍日记】day23——740. 删除并获得点数、LCR 091. 粉刷房子

740. 删除并获得点数 740. 删除并获得点数 题目描述&#xff1a; 给你一个整数数组 nums &#xff0c;你可以对它进行一些操作。 每次操作中&#xff0c;选择任意一个 nums[i] &#xff0c;删除它并获得 nums[i] 的点数。之后&#xff0c;你必须删除 所有 等于 nums[i] - 1…

十一周阅读记录

Neural Scene Graphs for Dynamic Scenes&#xff1a;动态场景的神经场景图 提出了一种将动态场景分解为场景图的神经渲染方法。提出了一种学习的场景图表示&#xff0c;它编码了物体的变换和辐射&#xff0c;以便高效地渲染场景的新排列和视图。为此&#xff0c;隐式学习场景…