[数据结构] 开散列法 闭散列法 模拟实现哈希结构(一)

news2024/12/26 11:31:15

标题:[数据结构] 开散列法 && 闭散列法 模拟实现哈希结构

个人主页:@水墨不写bug



目录

一、闭散列法

核心逻辑的解决

        i、为什么要设置位置状态?(伪删除法的使用)

        ii、哈希函数的设计

接口的实现 

1、插入(Insert)

仿函数取数值域的key

插入的 扩容逻辑

2、删除(Erase)

3、查找(Find)


正文开始:

        在之前的讲解中,我们已经了解到哈希结构的基本实现思路,具体讲解见这一篇:

《[数据结构] 哈希结构的哈希冲突&&解决哈希冲突》。实现哈希的方法主要区别在与哈希冲突的解决不同,我们知道常见的解决哈希冲突的方法有:

        闭散列法:找到下一个空位置存储;

        开散列法(链地址法、开链法):通过一个链表(哈希桶)来解决哈希冲突的问题。

        本文将分别实现上述两种方法,以及其各自的实现思路,主要实现哈希表的主要功能:插入(Insert)、删除(Erase)、查找(Find)等的主要逻辑。


一、闭散列法

        闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。


核心逻辑的解决

        i、为什么要设置位置状态?(伪删除法的使用)

        对于一个位置,如果发生哈希冲突,就需要把数据存放到下一个空位置中去,当我们下一次查找这个数据的时候,就需要走一遍和存储时一样的操作。

        比如,我们要在下面的这样的一个哈希表中存储一个数据:

        14%10 = 4:

        向后探测的条件可以猜测为:

        如果表中的不为空,就向后探测。

        但是当我们删除一个元素:这个元素是探测路径上的一个元素,那么下一次当我们查找14的时候,就找不到了。

        所以,我们在设计哈希数据的时候,就需要在每一个数据结构中设置一个状态值:

//哈希表位置的状态
enum State{EXIST,EMPTY,DELETE};

        这样,我们的状态体系就实现了:哈希表一个位置的默认状态为“EMPTY”;如果插入一个数据,那么这个位置的状态就需要更新为“EXIST”,在删除了一个数据之后,需要将这个位置的状态设置为“DELETE”。

        这样,我们就完美解决了删除数据后导致查找数据无法找到的问题。

        实现闭散列哈希需要一个顺序表,这里用vector即可。每一个数据位置存储的是一个自定义类型哈希数据(HashData),这个结构内部存储:

数据域、状态域

         数据域根据传入的模板参数决定:(比如传入的是int,或者pair<int,int>)

    //哈希表的数据
	template<class T>
	struct HashData
	{
		//默认构造,一般不会用到
		HashData()
			:_state(EMPTY)
			,_data(T())
		{}
		//插入数据时的构造函数
		HashData(const T& data)
			:_state(EXIST)
			,_data(data)
		{}
		T _data;
		State _state;
	};

        ii、哈希函数的设计

        对于一般的自定义类型(整形,浮点型,指针类型)可以强制转换为size_t类型,我们可以将默认的哈希函数设计为直接强制类型转换为size_t;

        对于常用自定义类型string, 我们设计如下的哈希类仿函数的特化版本:

//特化string版本
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (const auto& e : s)
		{
			hash += e;
			hash *= 31;
		}
		return hash;
	}
};

接口的实现 

1、插入(Insert)

仿函数取数值域的key

        在插入逻辑之前,我们首先需要知道 插入的数据类型V是不确定的,是通过类模板的参数决定的。(这涉及到了unordered_map和unordered_set的需要复用同一个哈希的封装)

        我们暂时不直接看底层实现,只需要知道有一种仿函数,可以取得数据域的key:

/*模板参数介绍
 *关键值:K 
 *数值域:V
 *得到Key的仿函数:KeyOfV 
 *hash值计算仿函数:Hash
 */
template<class K,class V,class KeyOfV,class Hash = HashFunc<K>>
class HashTable
{
    // 具体实现  
};

        由于插入的逻辑比较简单,这里不再解释,直接给出插入的核心逻辑:

bool Insert(const V& data)
{    
    //仿函数,得到数据的关键值
    KeyOfV kov;
    Hash hs;

    //如果有重复数据,插入失败
    //Find是通过key来查找的,如果插入的时候哈希表中已经存在,则返回false,插入失败
    if (Find(kov(data)) != nullptr)
	    return false;

    //...

    size_t hashi = hs(kov(data)) % _table.size();
    while (_table[hashi]._state == EXIST)
    {
	    ++hashi;
	    hashi %= _table.size();
    }
    _table[hashi]._data = data;
    _table[hashi]._state = EXIST;
    _n++;
    
    return 0;
}

插入的 扩容逻辑

        《[数据结构] 哈希结构的哈希冲突&&解决哈希冲突》讲解中,我们了解到载荷因子是反应哈希表装载程度的量:用载荷因子和size()的比值来决定什么时候要扩容。

        我们界定:载荷因子*10/size() >= 0.7的时候需要扩容。

        扩容就需要进行数据的转移。

        为了体现复用(实际上是懒),我们可以new一个新的表,再遍历旧表的数据,一个一个插入新表即可,最终交换新表和旧表的vector即可:

//检查是否需要扩容
if (_n * 10 / _table.size() >= 7)
{
	//新建哈希表,容量为2倍
	HashTable<K,V,KeyOfV> newHT;
	newHT._table.resize(_table.size() * 2);

	//将旧表的数据移动到新表
	for (int i = 0; i < _table.size(); ++i)
	{
		if (_table[i]._state == EXIST)
		{
			newHT.Insert(_table[i]._data);
		}
	}
	_table.swap(newHT._table);
}

插入:

//插入数据
bool Insert(const V& data)
{
	//仿函数,得到数据的关键值
	KeyOfV kov;
	Hash hs;

	//如果有重复数据,插入失败
	if (Find(kov(data)) != nullptr)
		return false;

	//检查是否需要扩容
	if (_n * 10 / _table.size() >= 7)
	{
		//新建哈希表,容量为2倍
		HashTable<K,V,KeyOfV> newHT;
		newHT._table.resize(_table.size() * 2);

		//将旧表的数据移动到新表
		for (int i = 0; i < _table.size(); ++i)
		{
			if (_table[i]._state == EXIST)
			{
				newHT.Insert(_table[i]._data);
			}
		}
		_table.swap(newHT._table);
	}

	size_t hashi = hs(kov(data)) % _table.size();
	while (_table[hashi]._state == EXIST)
	{
		++hashi;
		hashi %= _table.size();
	}
	_table[hashi]._data = data;
	_table[hashi]._state = EXIST;
	_n++;

	return true;
}

2、删除(Erase)

         删除的逻辑比较简单,我们需要先借助find根据key找到需要删除的数据的应该存在的哈希表的位置,再一个一个向后限行探测即可;如果找到,伪删除,修改状态为DELETE,如果没有找到,返回nullptr:

bool Erase(const K key)
{
	HashData<V>* ret = Find(key);
	if (ret == nullptr)
		return false;
	else
	{//修改数据状态表示删除
		ret->_state = DELETE;
		return true;
	}
}

3、查找(Find)

         查找的逻辑也是不复杂的;首先找到数据应该存在的表中的位置,如果一个一个向后限行探测即可;唯一需要注意的是状态为DELETE的时候,需要继续往后查找:

//根据key查找数据
HashData<V>* Find(const K key)
{
	KeyOfV kov;//得到data的关键值
	Hash hs;//哈希函数

	size_t hashi = hs(key) % _table.size();
	while (_table[hashi]._state != EMPTY)
	{
		if (_table[hashi]._state == EXIST && kov(_table[hashi]._data) == key)
			return &_table[hashi];
		hashi++;
		hashi %= _table.size();
	}
	return nullptr;
}

 待续~

未经作者同意禁止转载

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

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

相关文章

Linux 常用命令 - tail 【显示文件最后几行内容】

简介 tail 这个命令源自英文单词 “尾巴”&#xff0c;它的主要功能是显示文件的最后几行内容。通过使用 tail&#xff0c;用户可以查看文件的最新添加内容&#xff0c;特别是对于监控日志文件来说非常有用。tail 命令默认显示文件的最后 10 行&#xff0c;但这可以通过参数调…

走进低代码报表开发(一):探秘报表数据源

在前文当中&#xff0c;我们对勤研低代码平台的流程设计功能进行了介绍。接下来&#xff0c;让我们一同深入了解在企业日常运营中另一个极为常见的报表功能。在当今数字化时代&#xff0c;高效的报表生成对于企业的决策至关重要。勤研低代码开发平台能够以卓越的性能和便捷的操…

Git 学习与使用

0 认识⼯作区、暂存区、版本库 ⼯作区&#xff1a;是在电脑上你要写代码或⽂件的⽬录。 暂存区&#xff1a;英⽂叫stage或index。⼀般存放在.git ⽬录下的index⽂件&#xff08;.git/index&#xff09;中&#xff0c;我们 把暂存区有时也叫作索引&#xff08;index&#xff09;…

LAMP环境下项目部署

目录 1、创建一台虚拟机 centos 源的配置 备份源 修改源 重新加载缓存 安装软件 2、关闭防火墙和selinux 查看防火墙状态 关闭防火墙 查看SELinux的状态 临时关闭防火墙 永久关闭SELinux&#xff1a;编辑SELinux的配置文件 配置文件的修改内容 3、检查系统中是否…

NFTScan | 09.02~09.08 NFT 市场热点汇总

欢迎来到由 NFT 基础设施 NFTScan 出品的 NFT 生态热点事件每周汇总。 周期&#xff1a;2024.09.02~ 2024.09.08 NFT Hot News 01/ 数据&#xff1a;NFT 8 月销售额跌破 4 亿美元&#xff0c;创年内新低 9 月 2 日&#xff0c;数据显示&#xff0c;8 月 NFT 的月销售额仅为 …

直播相关01-录制麦克风声音,QT上 .pro 将 linux,mac和windows上配置为三种可以共享, 在.pro文件中 message 的作用

一 QT 上的 .pro 文件 将 linux&#xff0c;mac和windows上配置设置为可以共享 1. 先来看文件夹布局 2. 再来看 QT 中的 .pro文件 .pro 文件的写法 QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11# The following define makes your compiler …

【FFMPEG】FFplay音视频同步分析(下)

audio_decode_frame函数分析 首先说明一下&#xff0c;audio_decode_frame() 函数跟解码毫无关系&#xff0c;真正的解码函数是 decoder_decode_frame 。 audio_decode_frame() 函数的主要作用是从 FrameQueue 队列里面读取 AVFrame &#xff0c;然后把 is->audio_buf 指向…

多路转接之poll(接口介绍,struct pollfd介绍,实现原理,实现非阻塞网络通信代码)

目录 poll 引入 介绍 函数原型 fds struct pollfd 特点 nfds timeout 取值 返回值 原理 如何实现关注多个fd? 如何确定哪个fd上有事件就绪? 如何区分事件类型? 判断某事件是否就绪的方法 代码 示例 总结 为什么说它解决了fd上限问题? 缺点 poll 引入…

DVWA通关教程

Brute Force Low 先进行一下代码审计 <?php // 检查是否通过GET请求传递了Login参数&#xff08;注意&#xff1a;这里应该是username或类似的&#xff0c;但代码逻辑有误&#xff09; if( isset( $_GET[ Login ] ) ) { // 从GET请求中获取用户名 $user $_GET[ us…

【学习笔记】手写 Tomcat -- 预备知识

目录 一、新建项目 二、IO流 1. 什么是IO流&#xff1f; 2. IO的流向说明图解 3. IO 流的分类 4. 字节流 输出流 字节输出流的细节 输入流 字节输入流的细节 5. 练习 6. 字符流 输入流 字符流读取的细节 字符输入流原理解析 字符输出流原理解析 三、网络编程 …

NVIDIA GH200 超级芯片:重塑超算性能与AI基准的革新之作

Nvidia 正在将其 GH200 芯片应用于欧洲超级计算机&#xff0c;研究人员正在着手研究这些系统并发布带有性能基准的研究论文。 在第一篇论文《理解紧密耦合异构系统中的数据移动&#xff1a;以 Grace Hopper 超级芯片为例》中&#xff0c;研究人员对 GH200 的各种应用进行了基准…

vue2关闭eslint

vue2关闭eslint 1、找到项目build目录下的webpack.base.conf.js文件 2、注释createLintingRule()里面的内容&#xff08;只注释里面的内容&#xff09; 3、重启项目即可

自己动手实现mybatis的底层框架(不用动态代理直接用执行器、用动态代理自己实现。图文分析!)

目录 一.原生mybits框架图分析 自己实现Mybatis框架的分析 两种框架操作数据库的方法&#xff1a; 二.搭建开发环境 1.先创建一个maven项目 2.加入依赖(mysql dom4j junit lombok) 三.mybatis框架的设计思路 具体实现过程 3.1实现任务阶段 1- 完成读取配置文件&#x…

基于 TiDB 资源管控 + TiCDC 实现多业务融合容灾测试

导读 随着金融行业的不断发展&#xff0c;多个业务系统的整合成为了趋势&#xff0c;分布式数据库的应用也愈发广泛。为了应对多业务融合带来的复杂性&#xff0c;金融机构需要在保障各业务系统高效运行的同时&#xff0c;确保 IT 系统的高可用性和稳定性。本文将介绍 TiDB 如…

多输入多输出 | Matlab实现DBO-BP蜣螂算法优化BP神经网络多输入多输出预测

多输入多输出 | Matlab实现DBO-BP蜣螂算法优化BP神经网络多输入多输出预测 目录 多输入多输出 | Matlab实现DBO-BP蜣螂算法优化BP神经网络多输入多输出预测预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 多输入多输出 | Matlab实现DBO-BP蜣螂算法优化BP神经网络…

如何选择合适的数据报表工具?

在企业的日常运营中&#xff0c;数据报表如同企业的“仪表盘”&#xff0c;为管理者提供了关键的业务信息。无论是销售数据、财务状况还是生产进度&#xff0c;都需要通过数据报表进行清晰的呈现。同时&#xff0c;随着企业对数据可视化的需求不断增加&#xff0c;数据看板和数…

Numba最近邻插值(CPU+ GPU + Z轴切块 + XYZ轴切块 + 多线程)

文章目录 最近邻插值&#xff08;加速方法&#xff09;&#xff08;1&#xff09;scipy.ndimage.zoom&#xff08;2&#xff09;Numba-CPU加速&#xff08;3&#xff09;Numba-GPU加速&#xff08;4&#xff09;Numba-CPU加速&#xff08;Z轴切块&#xff09;&#xff08;5&…

docker运行springboot项目

博客中若有侵权或者错误的地方&#xff0c;请及时告知&#xff0c;感谢。 1. 背景 在开发中使用k8s部署&#xff0c;日常也只是写个dockerFile, 没有想过整个部署流程是怎样的。今天我们自己部署docker镜像。 2.实战 2.1 建立springboot项目 (1) JAVA项目打包 (解决no mai…

Minio笔记-Centos搭建Minio

下载 Minio wget https://dl.min.io/server/minio/release/linux-amd64/minio 赋予执行权限 chmod x minio 创建存储目录 mkdir /data 运行 Minio ./minio server /data 默认端口为9000 访问 Minio 控制台&#xff1a;在浏览器中输入 http://your-server-ip:9000 默认…

FPGA Prototyping vs Emulation

FPGA Prototyping vs. Emulation One way to visualize the difference between Prototyping and Emulation is with a “spider chart” (named for its resemblance to a spider’s web). The Prototyping vs. Emulation spider chart below highlights the differences bet…