C++实现闭散列/开放定址法

news2024/11/27 5:24:47

前言

哈希冲突是无法避免的,只能尽可能的减少冲突的可能性,通常我们可以设计适合的哈希函数。但是,哈希冲突还是会发生,那我们如何解决呢?
我们可以使用闭散列/开放定址法的方法,解决哈希冲突

在这里插入图片描述

文章目录

  • 前言
  • 一. 闭散列
    • (1). 线性探测
    • (2). 二次探测
  • 二. 闭散列的实现
    • (1). 结构
    • (2). 插入
    • (3). 查找
    • (4). 删除
    • (5). 测试
  • 三. 完整代码
  • 结束语

一. 闭散列

闭散列:又叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明哈希表中还有空位置,那么可以把冲突的元素放到冲突位置的 “ 下一个 ” 空位
“ 下一个 ” 空位的寻找,有以下几种常用方法:

(1). 线性探测

比如这样一个数组:{1 , 77 , 6 , 14 , 5 , 9}
在这里插入图片描述
初次的映射没有产生哈希冲突,但是当我们再插入一个44时,哈希冲突就产生了。

而线性探测:从发生冲突的位置,依次向后探测,直到寻找到下一个空位置为止

插入

  1. 通过哈希函数获取待插入元素在哈希表中的位置
  2. 如果该位置没有元素则直接插入新元素,如果该位置中有元素,发生哈希冲突,那么使用线性探测,找到下一个空位,插入新元素
    在这里插入图片描述

删除
我们不可以采用覆盖的方法,因为任何值都可以是插入的值,无法分辨该值是插入还是删除,所以我们可以采用状态标记的方法,标记一个值的删除

(2). 二次探测

线性探测的缺陷是,产生冲突的数据堆积在一起,则线性探测的次数会变得很多。
而二次探测是将每次向后探测的位置由1,2,3,4这样的线性变化,变成1,4,9,16,这样的非线性变化,使得每次产生哈希冲突后,下一个空位分部散乱,再冲突的可能性降低

二. 闭散列的实现

接下来,我们使用代码将其实现
我们采用线性探测的方式,和除留余数法的哈希函数

(1). 结构

首先,我们可以使用一个枚举定义哈希表结点的状态:1.存在值 2. 删除值 3. 空
结点存储的是键值对
闭散列中,使用一个vector,存储哈希表结点,并有一个记录当前存储个数的成员变量

代码如下:

//状态标记位
enum State
{
	EMPTY,//空
	EXIST,//存在
	DELETE//删除
};

//哈希结点
template<class K,class V>
struct HashNode
{
	pair<K, V>_kv;//键值对
	State _state = EMPTY;//状态标记位
};

//闭散列
//哈希表
template<class K,class V>
class HashTable
{
	typedef HashNode<K, V> Node;
private:
	vector<Node> _tables;//线性表
	size_t _n = 0;//大小
};

(2). 插入

根据上述所讲述的步骤
插入

  1. 通过哈希函数获取待插入元素在哈希表中的位置
  2. 如果该位置没有元素则直接插入新元素,如果该位置中有元素,发生哈希冲突,那么使用线性探测,找到下一个空位,插入新元素
	//插入
	bool Insert(const pair<K,V>kv)
	{
		//1. 映射位置
		size_t hashi = kv.first % _tables.size();
		size_t indix = hashi;
		size_t i = 1;//线性探索的距离
		//2.线性探索
		while (_tables[indix]._state == EXIST)
		{
			indix = hashi + i;
			indix %= _tables.size();
			i++;
		}

		_tables[indix]._kv = kv;
		_tables[indix]._state = EXIST;
		_n++;

		return true;
	}

但是,这份代码还存在诸多问题

  1. 最开始哈希表的大小为0,那么就会出现除零异常
  2. 如果哈希表的容量满了,那么是否需要扩容

我们解决了扩容的问题,最开始容器为空时就会自动扩容,除零异常自然就解决了。
STL中,vector的扩容,是重新申请更大的空间,然后将原本空间的数据拷贝到新空间
但是哈希表的扩容不能如此,因为我们采用的是除留余数法,当容量发生改变后,不同关键字根据哈希函数得出的哈希地址就不同
还是这个图
在这里插入图片描述
当前容量为10,所以14的哈希地址是4,但是如果容量扩大到了20,那么14的哈希地址就变成14了。所以每一次扩容,原先的映射关系就会发生改变,不能直接拷贝

同时,我们可以控制负载因子,来决定什么时候扩容
负载因子/载荷因子

负载因子α = 当前插入元素个数 / 容量
由于表长是定址,α与 “ 填入表中的元素个数 ”成正比,所以,α越大,表面填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,表面填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是负载因子的函数,只是不同处理冲突的方法有不同的函数
对于开放定址法,负载因子是特别重要的因素,严格限制在0.7~0.8以下,超过0.8,查表时的CPU缓存不命中,按照指数曲线上升。因此,超过时需要即使扩容

扩容,我们可以直接重新建立一个哈希表,然后调用遍历原先容器,将值重新映射新的哈希表中,最后交换一下新旧表的vector,就完成了扩容

代码如下:

//插入
	bool Insert(const pair<K,V>kv)
	{

		//判断是否需要扩容
		//当前大小为0 || 负载因子超过0.7
		if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
		{
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			HashTable<K, V>newTable;
			//重新构造一个HashTable完成映射
			newTable._tables.resize(newSize);

			//重新映射
			for (auto &data : _tables)
			{
				if (data._state == EXIST)
				{
					newTable.Insert(data._kv);
				}

			}
			//交换新旧表的vector
			_tables.swap(newTable._tables);
		}
		
		//插入新元素
		
		//映射位置
		size_t hashi = kv.first % _tables.size();
		size_t indix = hashi;
		//线性探索的距离
		size_t i = 1;
		//线性探索
		while (_tables[indix]._state == EXIST)
		{
			indix = hashi + i;
			indix %= _tables.size();
			i++;
		}

		_tables[indix]._kv = kv;
		_tables[indix]._state = EXIST;
		_n++;

		return true;
	}

(3). 查找

哈希表最高效的就是查找
而查找的逻辑,其实依照根据哈希函数实现的。
首先,我们先查看,查找的值经过哈希函数产生的哈希地址,是否直接就存储着该元素,如果是,那么直接返回
如果不是,那么根据线性探测,依次向后查找,直到查找到,代表当前哈希表没有该元素
代码如下:

//查找
	Node* Find(const K&key)
	{
		//空表直接返回空指针
		//保证不会出现除0异常
		if (_tables.size() == 0)
			return nullptr;
		
		//根据哈希函数求得哈希地址
		size_t hashi = key % _tables.size();
		size_t indix = hashi;
		size_t i = 1;
		//直到查找到空为止
		while (_tables[indix]._state!=EMPTY)
		{
			//可能查找的是已经删除的数据
			if (_tables[indix]._state == EXIST &&
			 _tables[indix]._kv.first == key)
			{
				return &_tables[indix];
			}
			indix = hashi + i;
			indix %= _tables.size();
			++i;

			//可能出现全是删除和存在的情况
			if (hashi == indix)
			{
				//说明已经查了一圈了
				return false;
			}
		}

		//到这就是没找到
		return nullptr;
	}

需要注意的是

  1. 可能出现当前为空表,但还是查找的情况,这会出现除零异常,我们单独判断一下
  2. 线性探测的过程中,如果一个元素是删除的,我们只会将其状态标志位改为DELETE,其值还存在,所以我们查找的到元素需要是EXIST的,才算是查找成功
  3. 可能出现当前表删除一些元素,再插入一些元素,负载因子并未超过0.7,但是插入的元素刚好将所有EMPTY的位置都占满,导致哈希表只有DELETE和EXIST的情况,这时会导致死循环,所以当indix==hashi时,代表已经查找完一整圈了,就直接返回,没有找到
    在这里插入图片描述
    如图,此时哈希表中只有DELETE和EXIST,没有EMPTY。

我们还可以在插入中,复用查找
在这里插入图片描述

如果返回的是,不是空指针,说明该值在哈希表中存在,直接返回假

(4). 删除

删除的逻辑,只要将其标志位改为DELETE就好
代码如下:

	//删除
	bool Erase(const K&key)
	{
		Node*ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			_n--;
		}
		else
		{
			return false;
		}

		return true;
	}

(5). 测试

我们编写一个遍历的算法
只访问标记位为EXIST的元素

	//遍历
	void traverse()
	{
		if (_tables.size() == 0)
		{
			cout << "当前表为空" << endl;
			return;
		}

		for (int i = 0; i < _tables.size(); i++)
		{
			if (_tables[i]._state == EXIST)
			{
				cout << _tables[i]._kv.first << " : " << _tables[i]._kv.second << endl;
			}
		}
	}

测试如下:

void TestHashTable1()
{
	int a[] = { 3, 33, 2, 13, 5, 12, 1002 ,15};
	HashTable<int, int> ht;
	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}

	ht.traverse();

	cout << "-------------------" << endl;
	if (ht.Find(2))
	{
		cout << "2存在" << endl;
	}
	else
	{
		cout << "2不存在" << endl;
	}

	ht.Erase(2);

	ht.traverse();

}

运行结果如下:
在这里插入图片描述

三. 完整代码

头文件请在.cpp中自行包含

#pragma once

//状态标记位
enum State
{
	EMPTY,//空
	EXIST,//存在
	DELETE//删除
};

//哈希结点
template<class K,class V>
struct HashNode
{
	pair<K, V>_kv;
	State _state = EMPTY;
};

//哈希表
template<class K,class V>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
	//插入
	bool Insert(const pair<K,V>kv)
	{
		//可以复用查找看一下当前值是否已经存在
		if (Find(kv.first))
			return false;

		//判断是否需要扩容
		//当前大小为0 || 负载因子超过0.7
		if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
		{
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			HashTable<K, V>newTable;
			//重新构造一个HashTable完成映射
			newTable._tables.resize(newSize);

			//重新映射
			for (auto &data : _tables)
			{
				if (data._state == EXIST)
				{
					newTable.Insert(data._kv);
				}

			}

			_tables.swap(newTable._tables);
		}


		//映射位置
		size_t hashi = kv.first % _tables.size();
		size_t indix = hashi;
		//线性探索的距离
		size_t i = 1;
		//线性探索
		while (_tables[indix]._state == EXIST)
		{
			indix = hashi + i;
			indix %= _tables.size();
			i++;
		}

		_tables[indix]._kv = kv;
		_tables[indix]._state = EXIST;
		_n++;

		return true;
	}

	//查找
	Node* Find(const K&key)
	{
		//空表直接返回空指针
		//保证不会出现除0异常
		if (_tables.size() == 0)
			return nullptr;

		//根据哈希函数求得哈希地址
		size_t hashi = key % _tables.size();
		size_t indix = hashi;
		size_t i = 1;
		//直到查找到空为止
		while (_tables[indix]._state!=EMPTY)
		{
			//可能查找的是已经删除的数据
			if (_tables[indix]._state == EXIST && _tables[indix]._kv.first == key)
			{
				return &_tables[indix];
			}
			indix = hashi + i;
			indix %= _tables.size();
			++i;

			//可能出现全是删除和存在的情况
			if (hashi == indix)
			{
				//说明已经查了一圈了
				return false;
			}
		}

		//到这就是没找到
		return nullptr;
	}

	//删除
	bool Erase(const K&key)
	{
		Node*ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			_n--;
		}
		else
		{
			return false;
		}

		return true;
	}

	//遍历
	void traverse()
	{
		if (_tables.size() == 0)
		{
			cout << "当前表为空" << endl;
			return;
		}

		for (int i = 0; i < _tables.size(); i++)
		{
			if (_tables[i]._state == EXIST)
			{
				cout << _tables[i]._kv.first << " : " << _tables[i]._kv.second << endl;
			}
		}
	}

private:
	vector<Node> _tables;//线性表
	size_t _n = 0;//大小
};

结束语

本篇内容到此就结束了,感谢你的阅读!

如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

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

相关文章

世界超高清大会发布重大技术成果:博冠自主创新推动8K摄像机攻关

一、世界超高清大会背景介绍&#xff1a; 近日&#xff0c;由工业和信息化部、国家广播电视总局、中央广播电视总台、广东省人民政府主办的2023世界超高清视频产业发展大会在广州越秀国际会议展览中心盛大召开。自2018年创办以来&#xff0c;大会已成功举办四届&#xff0c;成…

第08讲:搭建 SkyWalking 源码环境,开启征途

搭建 SkyWalking 源码环境 下载 SkyWalking 源码 执行 git clone 命令从 GitHub下载 SkyWalking 源码&#xff0c;如下所示 &#xff1a; git clone gitgithub.com:apache/skywalking.git 切换分支 等待 clone 完成之后&#xff0c;我们通过命令行窗口进入 SkyWalking 源码根…

SSM 三大框架原理、核心技术,运行流程讲解

作者:arrows 来源:https://www.cnblogs.com/arrows/p/10537733.html 一、Spring部分 1、 Spring的运行流程 第一步&#xff1a;加载配置文件ApplicationContext ac new ClassPathXmlApplicationContext(“beans.xml”); &#xff0c;ApplicationContext接口&#xff0c;它由…

存储卡目录变成未知文件?这些技巧能让你恢复数据!

当存储卡的目录变成未知文件时&#xff0c;我们无法直接访问存储卡中的数据。但是&#xff0c;这并不意味着这些数据永远无法恢复。以下是几种可能恢复存储卡数据的方法&#xff1a; 使用数据恢复软件。从互联网上下载并安装专业的数据恢复软件这些软件可以扫描存储卡&#xf…

分布式接口幂等性设计实现

面对分布式架构和微服务复杂的系统架构和网络超时服务器异常等带来的系统稳定性问题&#xff0c;分布式接口的幂等性设计显得尤为重要。本文简要介绍了几种分布式接口幂等性设计实现&#xff0c;包括Token去重机制、乐观锁机制、数据库主键和状态机实现等&#xff0c;以加深理解…

面板安全增强,网站支持反向代理设置,1Panel开源面板v1.2.0发布

2023年5月15日&#xff0c;现代化、开源的Linux服务器运维管理面板1Panel正式发布v1.2.0版本。 在这一版本中&#xff0c;1Panel着重增强了安全方面的功能&#xff0c;包括安全入口访问、面板SSL设置、网站密码访问等&#xff0c;同时网站新增支持反向代理设置&#xff0c;并带…

JVM学习(三)

1. JAVA 四中引用类型 1.1. 强引用 在 Java 中最常见的就是强引用&#xff0c; 把一个对象赋给一个引用变量&#xff0c;这个引用变量就是一个强引 用。当一个对象被强引用变量引用时 &#xff0c;它处于可达状态&#xff0c;它是不可能被垃圾回收机制回收的&#xff0c;即…

Java阶段二Day21

Java阶段二Day21 文章目录 Java阶段二Day21整合Lombok基础组件1 Lombok简介2 安装和配置 Lombok3 Lombok 注解及其用法3.1 Getter 和 Setter3.2 ToString3.3 AllArgsConstructor 和 NoArgsConstructor3.4 Data 4. 总结5 微博项目优化 Knife4j1 Knife4j的优点2 Knife4j快速上手2…

使用Docker构建的MySQL主从架构:高可用性数据库解决方案

前言 MySQL主从架构&#xff0c;我们已经在vmware虚拟机上实践过了&#xff0c;接下来我们一起探讨在docker中如何使用MySQL主从架构。 &#x1f3e0;个人主页&#xff1a;我是沐风晓月 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是沐风晓月&#xff0c;阿里云社…

《论文阅读》基于提示的知识生成解决对话情感推理难题

《论文阅读》基于提示的知识生成解决对话情感推理难题 前言摘要作者新观点问题定义模型框架Global ModelLocal ModelPrompt Based Knowledge Generation分类器实验结果问题前言 你是否也对于理解论文存在困惑? 你是否也像我之前搜索论文解读,得到只是中文翻译的解读后感到失…

openEuler 成功适配 LeapFive InFive Poros 开发板

近日&#xff0c;openEuler RISC-V 23.03 创新版本在跃昉科技的 Poros 开发板上成功运行。 openEuler 在 Poros 上适配成功&#xff0c;XFCE 桌面启动正常&#xff0c;文件系统、终端模拟器和输入法等相关 GUI 应用也运行流畅&#xff0c;Chromium 浏览器和 LibreOffice 等应用…

【Pm4py第三讲】关于Output

本节用于介绍pm4py中的输出函数&#xff0c;包括日志输出、模型输出、面向对象日志输出等。 1.函数概述 本次主要介绍Pm4py中一些常见的输入函数&#xff0c;总览如下表&#xff1a; 函数名说明write_bpmn()用于写入bpmn模型write_dfg()用于写入dfg模型write_pnml() 用于写入p…

面试之高手回答

1.int与Integer的区别 int与Integer的区别有很多&#xff0c;我简单罗列三个方面 第一个作为成员变量来说Integer的初始值是null&#xff0c;int的初始值是0&#xff1b; 第二个Integer存储在堆内存&#xff0c;int类型是在直接存储在栈空间&#xff1b; 第三个integer是个对象…

项目管理6大避坑技巧

1、拒绝错位战略目标 明确目标方向 做项目&#xff0c;首先需要明确项目目标。项目中有很多目标都很重要&#xff0c;但只有一两个目标是最重要的。在任何时刻&#xff0c;我们主要精力都应该集中在一到两个最重要的目标上。 一般最重要的目标具有以下特点&#xff1a;能够给组…

CSS--空间转换及动画

01-空间转换 空间转换简介 空间&#xff1a;是从坐标轴角度定义的 X 、Y 和 Z 三条坐标轴构成了一个立体空间&#xff0c;Z 轴位置与视线方向相同。空间转换也叫 3D转换属性&#xff1a;transform 平移 transform: translate3d(x, y, z); transform: translateX(); transfor…

能源硕士为何受热捧?社科院与杜兰大学能源管理硕士项目为你解惑

能源行业可谓是全球最具发展前景的行业之一&#xff0c;能源管理硕士更是近几年被争相推荐的“大热门”。广泛的就业选择、较高且稳定的收入&#xff0c;是该专业的特点之一&#xff0c;毕业后可选择在政府相关机构、能源监管部门、全国节能减排领域的各类研究机构工作&#xf…

Linux学习 Day3

目录 1. 时间相关的指令 2. cal指令 3. find指令&#xff1a;&#xff08;灰常重要&#xff09; -name 4. grep指令 5. zip/unzip指令 6. tar指令&#xff08;重要&#xff09;&#xff1a;打包/解包&#xff0c;不打开它&#xff0c;直接看内容 7. bc指令 8. uname –…

Shell基础学习---1、Shell概述、脚本入门、变量

1、Shell 概述 Shell是一个命令解释器&#xff0c;它接收应用程序/用户命令&#xff0c;然后调用操作系统内核。 说明&#xff1a;Shell是一个功能相当强大的编程语言&#xff0c;易编写&#xff0c;易调试、灵活性强。 1、 提供的Shell解释器 2、bash和sh的关系 3、CentOS…

简述-关于Kmeans轮廓系数随着聚类个数的增加后减少的问题

当我们在做Kmeans聚类的准备工作时&#xff0c;通常会用到手肘法&#xff08;elbow method&#xff09;或者轮廓系数&#xff08;silhouette score&#xff09;去找到最佳簇类个数。 对于轮廓系数寻找法&#xff0c;理论上来说&#xff0c;轮廓系数会随着聚类个数的增加而增加…

云渲染是什么?云渲染和传统渲染农场有什么区别?

云渲染是什么&#xff1f;云渲染和传统渲染农场有什么区别&#xff1f; 今天云渲染小编就来和大家说一说云渲染以及它和传统渲染农场的区别。 一、什么是云渲染&#xff1f;云渲染什么意思&#xff1f; 首先云渲染云渲染是一种依托于云计算的云端服务&#xff0c;用户将本地…