【 C++ 】闭散列哈希表的模拟实现

哈希节点状态

我们都很清楚数组里的每一个值无非三种状态:

  1. 如果某下标没有值,则代表空EMPTY。
  2. 如果有值在代表存在EXIST。
  3. 如果此位置的值被删掉了,则表示为DELETE。

而这三种状态我们可以借助enum枚举来帮助我们表示数组里每个位置的状态。这里我们专门封装一个类来记录每个位置的状态,以此汇报给后续的哈希表。

enum State
{
	EMPTY,
	EXIST,
	DELETE
};

//哈希节点
template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;//记录每个位置的状态,默认给空
};

//哈希表
template<class K, class V, class HashFunc = DefaultHash<K>>//添加仿函数便于把其他类型的数据转换为整型数据
class HashTable
{
	typedef HashData<K, V> Data;
public:
	//相关功能的实现……
private:
	vector<Data> _tables;
	size_t _n = 0;//记录存放的有效数据的个数
};

实现好了哈希节点的类,就能够很好的帮助我们后续的查找,示例:

在这里插入图片描述

  • 查找50:

50%10=0,下标0的值不是50,继续++下标往后查找,直至下标3的下标为止。

  • 查找60:

60%10=0,下标0不是,往后++下标继续查找,找到下标4发现状态为EMPTY空,此时停止查询,因为往后就不可能出现了

  • 删除10,再查找50:

50%10=0,下标0的值不是,++下标到下标1,发现状态为DELETE删除,继续++下标直至下标3的值为50,找到了。

哈希表的扩容

  • 散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长度。
  • α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,表明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数。
  • 对于开放定址法(闭散列),载荷因子是特别重要因素,应严格限制在0.7 ~ 0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照质数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了载荷因子为0.75,超过此值将resize散列表。

综上,我们在后续的插入操作中,必然要考虑到扩容的情况,我们直接把负载因子控制在0.7,超过了就扩容。具体操作见下文哈希表的插入操作。

构建仿函数把所有数据类型转换为整型并特化

在我们后续的插入操作中,插入的数据类型如果是整数,那么可以直接建立映射关系,可若是字符串,就没那么容易了,因此,我们需要套一层仿函数,来帮助我们把字符串类型转换成整型的数据再建立映射关系。主要分为以下三类需要写仿函数的情况:

  1. key为整型,为默认仿函数的情况。
    此时的数据类型为整型,直接强转size_t随后返回。

  2. key为字符串,单独写个字符串转整型的仿函数。
    针对于字符串转整型,我们推出下面两种方法,不过都是会存在问题的:

    • 只用首字母的ascii码来映射,此法不合理,因为"abc"和"axy"本是俩不用字符串,经过转换,会引发冲突。
    • 字符串内所有字符ASCII码值之和,此法也会产生冲突,因为"abcd"和"bcad"在此情况就会冲突。

为了避免冲突,几位大佬推出多种算法思想,下面我取其中一种算法思想来讲解:

BKDR哈希算法:
hash = hash * 131 + ch;   // 也可以乘以31、131、1313、13131、131313..  

为了能够让我们的哈希表能够自动识别传入数据的类型,不用手动声明,这里我们可以借助特化来解决,仿函数+特化总代码如下:

//利用仿函数将数据类型转换为整型
template<class K>
struct DefaultHash
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//模板的特化
template<>
struct DefaultHash<string>
{
	size_t operator()(const string& key)
	{
		//BKDR哈希算法
		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;//把所有字符的ascii码值累计加起来
		}
		return hash;
	}
};

哈希表的插入

哈希表的插入主要是三大步骤:

  • 去除冗余
  • 扩容操作
  • 插入操作

下面分开来演示。

1、去除冗余:

  1. 复用Find查找函数,去帮助我们查找插入的值是否存在 。
  2. 若存在,直接返回false 。
  3. 不存在,再进行后续的插入操作。

2、扩容操作:

  1. 如果哈希表一开始就为空,则要扩容。
  2. 如果填入表中的元素个数*10 再 / 表的大小>=7,就扩容(*10是为了避免出现size_t的类型相除不会有小数的情况)。
  3. 扩容以后要重新建立映射关系。
  4. 创建一个新的哈希对象,扩容到先前旧表扩容的大小。
  5. 遍历旧表,把旧表每个存在的元素插入到新表,此步骤让新表自动完成映射关系,无序手动构建。
  6. 利用swap函数把新表交换到旧表那,此时的旧表就是已经扩好容且建立号映射关系的哈希表。

3、插入操作:

  1. 借助仿函数把插入的数据类型转为整型并定义变量保存插入键值对的key。
  2. 用此变量%=哈希表的size(),不能是capacity(),因为[ ]运算符会判断下标是否小于size,且对于哈希表,应该尽量控制size和capacity一样大。
  3. 遍历进行线性探测 / 二次探测,如果这个位置的状态为EXIST存在,说明还要往后遍历查找。
  4. 遍历结束,说明此位置的状态为空EMPTY或删除DELETE,可以放值。
  5. 把插入的值放进该位置,更新状态为EXIST,有效数据个数++。
//插入
bool Insert(const pair<K, V>& kv)
{
	//1、去除冗余
	if (Find(kv.first))
	{
		//说明此值已经有了,直接返回false
		return false;
	}
	//2、扩容
		//负载因子超过0.7,就扩容
	if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
	{
		size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		//扩容以后,需要重新建立映射关系
		HashTable<K, V, HashFunc> newHT;
		newHT._tables.resize(newSize);
		//遍历旧表,把旧表每个存在的元素插入newHT
		for (auto& e : _tables)
		{
			if (e._state == EXIST)
			{
				newHT.Insert(e._kv);
			}
		}
		newHT._tables.swap(_tables);//建立映射关系后交换
	}
	//3、插入
	HashFunc hf;
	size_t starti = hf(kv.first);//取出键值对的key,并且避免了负数的情况,借用仿函数确保是整型数据
	starti %= _tables.size();
	size_t hashi = starti;
	size_t i = 1;
	//线性探测/二次探测
	while (_tables[hashi]._state == EXIST)
	{
		hashi = starti + i;//二次探测改为 +i^2
		++i;
		hashi %= _tables.size();//防止hashi超出数组
	}
	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	_n++;
	return true;
}

哈希表的查找

查找的核心逻辑就是找到key相同,就返回此对象的地址,找到空就返回nullptr,具体细分规则如下:

  1. 先去判断表的大小是否为0,为0直接返回nullptr。
  2. 按照线性探测 / 二次探测的方式去遍历,遍历的条件是此位置的状态不为空EMPTY。
  3. 如果遍历到某哈希表中的对象的值等于要查找的值(前提是此位置的状态不为DELETE删除),返回此对象的地址。
  4. 当遍历结束后,说明此位置的状态为空EMPTY,哈希表没有我们要查找的值,返回nullptr。
//查找
Data* Find(const K& key)
{
	//判断表的size是否为0
	if (_tables.size() == 0)
	{
		return nullptr;
	}
	HashFunc hf;
	size_t starti = hf(key);//通过仿函数把其它类型数据转为整型数据
	starti %= _tables.size();
	size_t hashi = starti;
	size_t i = 1;
	//线性探测/二次探测
	while (_tables[hashi]._state != EMPTY)//不为空就继续
	{
		if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];//找到了就返回此对象的地址
		}
		hashi = starti + i;//二次探测改为 +i^2
		++i;
		hashi %= _tables.size();//防止hashi超出数组
	}
	return nullptr;
}

哈希表的删除

删除的逻辑很简单,遵循下面的规则:

  1. 复用Find函数去帮我们查找删除的位置是否存在。
  2. 若存在,把此位置的状态置为DELETE即可,此时表中的有效数据个数_n需要减减,最后返回true。
  3. 若不存在,直接返回false。
//删除
bool Erase(const K& key)
{
	//复用Find函数去帮助我们查找删除的值是否存在
	Data* ret = Find(key);
	if (ret)
	{
		//若存在,直接把此位置的状态置为DELETE即可
		ret->_state = DELETE;
		return true;
	}
	else
	{
		return false;
	}
}

完整代码

#pragma once
#include<iostream>
#include<string>
#include<vector>
using namespace std;

//利用仿函数将数据类型转换为整型
template<class K>
struct DefaultHash
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//模板的特化
template<>
struct DefaultHash<string>
{
	size_t operator()(const string& key)
	{
		//BKDR哈希算法
		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;//把所有字符的ascii码值累计加起来
		}
		return hash;
	}
};

//闭散列哈希表的模拟实现
namespace CloseHash
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};
	//哈希节点状态的类
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;//记录每个位置的状态,默认给空
	};


	//哈希表的类
	template<class K, class V, class HashFunc = DefaultHash<K>>//添加仿函数便于把其他类型的数据转换为整型数据
	class HashTable
	{
		typedef HashData<K, V> Data;
	public:
		//插入
		bool Insert(const pair<K, V>& kv)
		{
			//1、去除冗余
			if (Find(kv.first))
			{
				//说明此值已经有了,直接返回false
				return false;
			}
			//2、扩容
				//负载因子超过0.7,就扩容
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				//扩容以后,需要重新建立映射关系
				HashTable<K, V, HashFunc> newHT;
				newHT._tables.resize(newSize);
				//遍历旧表,把旧表每个存在的元素插入newHT
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHT.Insert(e._kv);
					}
				}
				newHT._tables.swap(_tables);//建立映射关系后交换
			}
			//3、插入
			HashFunc hf;
			size_t starti = hf(kv.first);//取出键值对的key,并且避免了负数的情况,借用仿函数确保是整型数据
			starti %= _tables.size();
			size_t hashi = starti;
			size_t i = 1;
			//线性探测/二次探测
			while (_tables[hashi]._state == EXIST)
			{
				hashi = starti + i;//二次探测改为 +i^2
				++i;
				hashi %= _tables.size();//防止hashi超出数组
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			_n++;
			return true;
		}
		//查找
		Data* Find(const K& key)
		{
			//判断表的size是否为0
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			HashFunc hf;
			size_t starti = hf(key);//通过仿函数把其它类型数据转为整型数据
			starti %= _tables.size();
			size_t hashi = starti;
			size_t i = 1;
			//线性探测/二次探测
			while (_tables[hashi]._state != EMPTY)//不为空就继续
			{
				if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];//找到了就返回此对象的地址
				}
				hashi = starti + i;//二次探测改为 +i^2
				++i;
				hashi %= _tables.size();//防止hashi超出数组
			}
			return nullptr;
		}
		//删除
		bool Erase(const K& key)
		{
			//复用Find函数去帮助我们查找删除的值是否存在
			Data* ret = Find(key);
			if (ret)
			{
				//若存在,直接把此位置的状态置为DELETE即可
				ret->_state = DELETE;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		vector<Data> _tables;
		size_t _n = 0;//记录存放的有效数据的个数
	};
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RK3568平台开发系列讲解(基础篇)如何快速学习一套 Linux开发板源码

🚀返回专栏总目录 文章目录 一、基础代码二、驱动代码沉淀、分享、成长,让自己和他人都能有所收获!😄 拿到一份源码和一块评估板,如何快速找到与这块板相关的源码,是很多研发人员都曾遇到过的问题。如果对内核源码结构有大概了解,要完成这些事情也不难,通常可按照基础…

ASLR 和 PIE

前言 ASLR&#xff08;Address Space Layout Randomization&#xff0c;地址空间随机化&#xff09;是一种内存攻击缓解技术&#xff0c;是一种操作系统用来抵御缓冲区溢出攻击的内存保护机制。这种技术使得系统上运行的进程的内存地址无法被预测&#xff0c;使得与这些进程有…

高性能 Kafka 及常见面试题

Kafka 是一种分布式的&#xff0c;基于发布/订阅的消息系统&#xff0c;原本开发自 LinkedIn&#xff0c;用作 LinkedIn 的事件流&#xff08;Event Stream&#xff09;和运营数据处理管道&#xff08;Pipeline&#xff09;的基础。 基础原理详解可见 Kafka 基本架构及原理 基础…

事件循环解析

浏览器的进程模型 何为进程&#xff1f; 程序运行需要有它自己专属的内存空间&#xff0c;可以把这块内存空间简单的理解为进程 每个应用至少有一个进程&#xff0c;进程之间相互独立&#xff0c;即使要通信&#xff0c;也需要双方同意。 何为线程&#xff1f; 有了进程后&…

Java根据excel模版导出Excel(easyexcel、poi)——含项目测试例子拿来即用

Java根据excel模版导出Excel&#xff08;easyexcel、poi&#xff09;——含项目测试例子拿来即用 1. 前言1.1 关于Excel的一般导出2.2 关于easyexcel的根据模版导出 2. 先看效果2.1 模版2.2 效果 3. 代码实现&#xff08;核心代码&#xff09;3.1 项目代码结构3.2 静态填充例子…

全域增长方法论:帮助品牌实现科学经营,助力长效生意增长

前两年由于疫情反复、供给需求收缩等条件制约&#xff0c;品牌业务均受到不同程度的影响。以双十一和618电商大促为例&#xff0c;就相比往年颇显“惨淡”&#xff0c;大多品牌营销都无法达到理想预期。 随着市场环境不断开放&#xff0c;2023年营销行业开始从低迷期走上了高速…

RPA中国 x UiPath | 第六届RPA极客挑战赛,3月16日上海开赛!

随着人工智能技术的不断进步以及数字化转型的深入&#xff0c;企业对于高效、精准、自动化的业务流程需求日益迫切。RPA技术作为连接人类工作与机器操作的桥梁&#xff0c;正逐渐从规则驱动发展为智能决策的助手。 由RPA中国联合UiPath共同主办的【第六届RPA极客挑战赛】将于2…

高性能API云原生网关 APISIX安装与配置指南

Apache APISIX是Apache软件基金会下的顶级项目&#xff0c;由API7.ai开发并捐赠。它是一个高性能的云原生API网关&#xff0c;具有动态、实时等特点。 APISIX网关可作为所有业务的流量入口&#xff0c;为用户提供了丰富的功能&#xff0c;包括动态路由、动态上游、动态证书、A…

【LeetCode每日一题】938. 二叉搜索树的范围和

2024-2-26 文章目录 [938. 二叉搜索树的范围和](https://leetcode.cn/problems/range-sum-of-bst/)思路&#xff1a;写法一&#xff1a;在中间累加写法二&#xff1a;在最后累加 938. 二叉搜索树的范围和 思路&#xff1a; 1.在二叉搜索树中&#xff1a;左子树的结点都小于根节…

【Excel PDF 系列】POI + iText 库实现 Excel 转换 PDF

你知道的越多&#xff0c;你不知道的越多 点赞再看&#xff0c;养成习惯 如果您有疑问或者见解&#xff0c;欢迎指教&#xff1a; 企鹅&#xff1a;869192208 文章目录 前言转换前后效果引入 pom 配置代码实现 前言 最近遇到生成 Excel 并转 pdf 的需求&#xff0c;磕磕碰碰总…

UE5 C++ Widget练习 Button 和 ProgressBar创建血条

一. 1.C创建一个继承Widget类的子类&#xff0c; 命名为MyUserWidget 2.加上Button 和 UserWidget的头文件 #include "CoreMinimal.h" #include "Components/Button.h" #include "Blueprint/UserWidget.h" #include "MyUserWidget.genera…

2步破解官方sublime4

sublime简要破解流程 1.下载sublime官方最新版2. 破解流程 1.下载sublime官方最新版 打开 官方网站下载 portable version 版&#xff0c;省的安装。。解压到任意位置&#xff0c;备份 sublime_text.exe 文件 2. 破解流程 打开网址把文件 sublime_text.exe 拖入网页搜索替换…

飞书公式留存

飞书公式留存 if(and(month([完成日期])>7,month([完成日期])<9),"Q3季度"&#xff0c;if(and(month([完成日期])>10,month([完成日期])<12),"Q4季度"&#xff0c;""))

基于YOLOv5+PySide6的火灾火情火焰检测系统设计深度学习

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;225火灾 获取完整源码源文件已标注的数据集&#xff08;1553张&#xff09;配置跑起来说明 可有偿49yuan一对一远程操作&#xff0c;在你电脑跑起来 效果展示&#xff1a; ​数据集在下载的文件夹&#xff1a;yolov5-5.0\…

抽象的后端

Connection refused: no further information 出现这条代码的核心是你使用redis&#xff0c;但是本地没有开启redis服务 如何启动redis服务 第一步&#xff1a;确定你安装了对应的框架 以spring为例 <dependency><groupId>org.springframework.boot</group…

详解POCV/SOCV的时序报告

​POCV/SOCV的时序报告中有如下变量&#xff1a; Mean: 高斯分布中的μ值&#xff08;平均值&#xff09; Sensit: sensitivity&#xff0c;也就是1个Sigma的值&#xff1b; Corner: Sigma边界的最差值 cell的delay Delay mean N * Delay sigma; cell 的Transition Sl…

十一、Qt自定义Widget组件、静态库与动态库

一、自定义Widget组件 1、自定义Widget组件 使用步骤采用提升法&#xff08;promotion&#xff09;重新定义paintEvent事件 2、实现程序 &#xff08;1&#xff09;创建项目&#xff0c;基于QWidget &#xff08;2&#xff09;添加类&#xff0c;为Widget组件提升类 #inclu…

嵌入式C语言(三)

typeof() 使用typeof可以获取一个变量或表达式的类型。 typeof的参数有两种形式&#xff1a;表达式或类型。 int i;typeof(i) j 20; --> int j 20;typeof(int *) a; -->int *a; int f(); -->typeof(f()) k;--? int k我们可以看出通过typeof获取一个变量的…

【iOS ARKit】ARWorldMap

ARWorldMap 用于存储 ARSession 检测扫描到的空间信息数据&#xff0c;包括地标&#xff08;Landmark&#xff09;、特征点&#xff08;Feature Point&#xff09;、平面&#xff08;Plane&#xff09;等&#xff0c;以及使用者的操作信息&#xff0c;如使用者添加的 ARAnchor …

LabVIEW高精度闭式微小型循环泵性能测试

LabVIEW高精度闭式微小型循环泵性能测试 开发了一套基于LabVIEW的高精度闭式微小型循环泵性能测试系统&#xff0c;旨在通过先进的测试技术和虚拟仪器技术&#xff0c;对微小型循环泵的性能进行精确测量和分析&#xff0c;从而优化泵的设计和性能&#xff0c;提高其在航空、机…