【C++进阶学习】第九弹——哈希的原理与实现——开放寻址法的讲解

news2024/9/17 7:27:41

前言:

在前面,我们已经学习了很多存储机构,包括线性存储、树性存储等,并学习了多种拓展结构,效率也越来越高,但是是否有一种存储结构可以在大部分问题中都一次找到目标值呢?哈希可能能实现

目录

一、哈希的概念

二、哈希冲突

三、哈希冲突解决

3.1 开放寻址法

节点结构

插入操作

查找操作

删除操作

打印操作

3.2 链地址法

四、测试代码(开放寻址法)

五、总结


一、哈希的概念

哈希就是一种特殊的存储结构,通过特定的函数,使得数据的存储位置与它的关键码之间建立一种一一映射的关系,这样在查找数据时就可以直接通过关键值来快速查找

通过这种方法:

当我们向其中插入数据时,就可以利用此特定函数插入到关键码对应的位置下

当我们搜索数据时,通过关键码,进行相应的处理就可以找到要找的数据

这种方法就叫做哈希,特定的函数就是哈希函数,这种方法所建立的结构就叫做哈希函表

我们来看这样一个例子:

对于这样一个数组{1,5,3,17,19,0},按照上述规则我们首先要先找一个合适的哈希函数,

这里我们哈希函数可以设为:hashi(key)=key%capacity;capacity为存储空间底层空间总的大小

现在我们根据上面这个例子来思考这样一个问题,如果有这样一个数据,比如13,通过上面的哈希函数计算得我们应该把它放在关键码为3的位置上,但是此时这个位置上已经有数据了,我们应该如何解决呢?这样的问题就叫做哈希冲突

二、哈希冲突

哈希冲突指的是在使用哈希表进行数据存储和查找时,不同的关键字通过哈希函数计算得到了相同的哈希值。

哈希函数是将关键字映射到哈希表中的某个位置的函数。由于哈希表的存储空间是有限的,而可能的关键字数量是无限的,所以不同的关键字有可能被映射到相同的位置,这就产生了哈希冲突。

哈希冲突会影响哈希表的性能,比如增加查找、插入和删除操作的时间复杂度。

常见的解决哈希冲突的方法有(这两种方法会在后面详细讲解):

  1. 开放寻址法:当发生冲突时,通过一定的探查方式在哈希表中寻找其他空闲的位置来存储冲突的元素。
  2. 链地址法:在哈希表的每个位置上建立一个链表,将所有哈希值相同的元素都存储在这个链表中。

三、哈希冲突解决

解决哈希冲突常见的两种方法主要是:开放寻址法和链地址法

3.1 开放寻址法

开放定址法是解决哈希冲突的一种方法,其基本思想是当发生冲突时,通过某种系统的方法在哈希表中寻找下一个空槽位,并将冲突的关键码存储在这个槽位中。下面我们先来看一下开放寻址法的重点:

  1. 探测序列:开放定址法中,探测序列决定了当发生冲突时如何查找下一个槽位。常见的探测序列方法有:

    • 线性探测:当冲突发生时,顺序检查表中的下一个槽位,直到找到空槽位。
    • 二次探测:探测序列为 1, 4, 9, 16, ...,即探测位置是 i^2 的倍数,其中 i 是从0开始的整数。
    • 伪随机探测:使用伪随机数生成器来确定探测序列。
  2. 删除操作:在开放定址法中,删除元素比较复杂,因为不能简单地将槽位置为空,否则会影响后续的查找操作。通常,删除一个元素时,将其标记为已删除,但在查找时跳过已删除的元素。

  3. 装填因子:装填因子是哈希表中已存储元素个数与哈希表大小的比值。开放定址法中,装填因子不宜过高,否则冲突概率增加,查找效率下降。(因为这个原因,所以需要扩容)

节点结构

因为我们并不知道插入要操作何种类型的数据,可能是整形,浮点型或string的,所以我们可以选择将它们全转化为整形来处理,这里就需要我们借助仿函数和模板特化来实现

	template<class K>       
struct HashFunc         //仿函数,这里的功能是将其他类型转化为整形
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>     //特化
struct HashFunc<string>    //string类的不可以直接转化为整形,所以需要特殊处理
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}
		return hash;
	}
};

enum Status
	{
		EMPTY,     //此位置为空
		EXIST,     //此位置不为空
		DELETE     //此位置数据已被删除
	};
	template<class K, class V>     //因为不能确定我们要处理什么类型的数据,所以我们采用类模板的形式
	struct HashData
	{
		pair<K, V> _kv;   
		Status _s;        //此位置的状态
	};
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()     //初始化HashTable
		{
			_tables.resize(10);
		}

		
	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;        //存储个数
	};

插入操作

开放寻址法的关键就在于数据的插入,在这里我们重点讲解一下线性探测的思想

这就是线性探测的思路,同时我们还要在装填因子足够大的时候进行扩容,比如上面这个例子,此时10个位置中已经填入7个因子,我们就可以进行按2倍扩容:

代码实现如下:

		//插入
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			//负载因子0.7就扩容
			if (_n * 10 / _tables.size() == 7)
			{
				size_t newSize = _tables.size() * 2;
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(newSize);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._s == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newHT._tables);    //直接交换就可以得到扩容后的结果
			}

			Hash hf;
			//线性探测
			size_t hashi = hf(kv.first) % _tables.size();  //这里涉及到模板的特化,后面会讲
			while (_tables[hashi]._s == EXIST)    //当此位置不为空时,就往后找
			{                                     //当为空或已删除时都是可以放入数据的
				hashi++;
				hashi %= _tables.size();        //这里进行这个操作的目的是防止hashi大于_tables.size()
			}
			_tables[hashi]._kv = kv;     //找到后将这个位置值改为插入值
			_tables[hashi]._s = EXIST;   //状态改为存在
			_n++;

			return true;
		}

查找操作

上面的插入操作中,我们首先就先用查找操作看是否已经有这个数据,因为哈希是不允许存在重复数据的,这里我们就来看一下这个查找操作

		//查找
		HashData<K, V>* Find(const K& key)
		{
			Hash hf;      //仿函数
			size_t hashi = hf(key) % _tables.size();   //所有计算都要用仿函数将key转换为整形
			while (_tables[hashi]._s == EXIST)      //从不为空的位置开始找
			{
				if (_tables[hashi]._s != EMPTY
					&& _tables[hashi]._kv.first == key)
					return &_tables[hashi];
				hashi++;
				hashi %= _tables.size();
			}
			return NULL;
		}

删除操作

我们删除一个数据时,并不能简单的找到这个数据就进行删除,这样会对我们后序很多操作带来不少麻烦,比如我们把4删除了,再找44就会很不容易,这也就是我们前面在定义节点结构是要定义状态的原因,我们删除一个操作后可以把它的状态更改,这样我们在进行其他操作的时候就可以直到这个位置上的值已被删除

		//删除
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_s = DELETE;
				_n--;
				return 0;
			}
			else
				return true;
		}

打印操作

		//打印
		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._s == EXIST)     //当这个位置有数据时,打印出有效数据
					cout << "[" << i << "]->" << _tables[i]._kv.first << ":"
					<< _tables[i]._kv.second << endl;
				else if (_tables[i]._s == EMPTY)
					printf("[%d]->EMPTY\n", i);
				else
					printf("[%d]->DELETE\n", i);
			}
		}

3.2 链地址法

链地址法有些东西与上面的代码有些冲突,不好测试,我们放在下一篇讲

四、测试代码(开放寻址法)

我们给出几个测试用例检验一下上面的开放寻址法是否有误:

测试一:

		void TestHT1()
	{
		HashTable<int, int> j;
		int a[] = { 4,14,24,34,5,7,1 };
		for (auto e : a)
		{
			j.Insert(make_pair(e, e));
		}
		j.Insert(make_pair(3, 3));
		j.Print();


		j.Insert(make_pair(3, 3));     //这里有一个隐藏的bug
		if (j.Find(3))
			cout << "3存在" << endl;
	}

运行结果:

测试二:

	void TestHT2()    //测试string
	{
		string arr[] = { "香蕉","甜瓜","苹果","香蕉","苹果","苹果" };
		HashTable<string, int> ht;
		for (auto e : arr)
		{
			auto ret = ht.Find(e);
			if (ret)
				ret->_kv.second++;
			else
			{
				ht.Insert(make_pair(e, 1));
			}
		}
		ht.Print();
	}

运行结果:

五、总结

以上就是用开放寻址法来创建一个哈希表的完整代码,由于排版原因,整体看起来可能有些乱,有不懂的地方欢迎与我私信交流!!

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

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

相关文章

npm国内淘宝镜像registry镜像过期

我们在使用npm的时候会遇到淘宝镜像安装过期的问题 首先了解 npm 淘宝镜像是一个 npm 注册表的镜像&#xff0c;用于加速 npm 包的下载。 一、如何设置&#xff1f; 如何设置淘宝镜像&#xff1f;淘宝镜像已经从 registry.npm.taobao.org 切换到了 registry.npmmirror.com n…

【书生大模型实战营(暑假场)】入门任务一 Linux+InternStudio 关卡

入门任务一 LinuxInternStudio 关卡 参考&#xff1a; 教程任务 1 闯关任务 1.1 基于 VScode 的 SSH 链接 感谢官方教程的清晰指引&#xff0c;基于VS code 实现 SSH 的链接并不困难&#xff0c;完成公钥配之后&#xff0c;可以实现快速一键链接&#xff0c;链接后效果如下…

搭建自动化 Web 页面性能检测系统 —— 部署篇

作为一个前端想去做全栈的项目时&#xff0c;可能第一个思路是 node vue/react。一开始可能会新建多个工程目录去实现&#xff0c;假设分别为 web 和 server&#xff0c;也许还有管理后台的代码 admin&#xff0c;那么就有了三个工程的代码。此时为了方便管理就需要在远程仓库…

JSON介绍及使用

目录 什么是JSON JSON在JavaScript中的使用 JSON的定义 JSON的访问 JSON的两个常用方法 JSON在Java中的使用 JavaBean和JSON的相互转换 List和JSON的相互转换 Map和JSON的相互转换 什么是JSON JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的…

No module named pip._internal.cli.main

conda 创建的虚拟化python2.7,里面没有pip报错&#xff1a;No module named pip._internal.cli.main 解决办法 使用 ensurepip 安装 pip python -m ensurepip --upgrade确认 pip 已正确安装 pip --version然后又遇到报错 此时我们只需要更新一下pip python -m pip install --up…

使用 Python 中的 ELSER 进行Serverless 语义搜索:探索夏季奥运会历史

作者&#xff1a;来自 Elastic Essodjolo Kahanam 本博客介绍如何使用语义搜索以自然语言表达形式从 Elasticsearch 索引中获取信息。我们将创建一个无服务器 Elasticsearch 项目&#xff0c;将之前的奥运会数据集加载到索引中&#xff0c;使用推理处理器和 ELSER 模型生成推理…

洛谷 P1883 【模板】三分 | 函数

原题 题目描述 给定 n 个二次函数 f1​(x),f2​(x),…,fn​(x)&#xff08;均形如 ax2bxc&#xff09;&#xff0c;设 &#x1d439;(&#x1d465;)F(x)max{f1​(x),f2​(x),...,fn​(x)}&#xff0c;求 &#x1d439;(&#x1d465;)F(x) 在区间[0,1000] 上的最小值。 输入…

PHP师生荣誉管理系统—计算机毕业设计源码10079

目 录 摘要 1 绪论 1.1 研究背景 1.2论文结构与章节安排 2 师生荣誉管理系统系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据增加流程 2.2.2 数据修改流程 2.2.3 数据删除流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 非功能性分析 2.4 系统用例分析 2.…

Hive命令创建数据库和表(内置数据库)

【实验目的】 1) 了解hive操作命令 2) 熟悉hive数据库的操作 【实验原理】 配置完毕hive环境之后&#xff0c;通过hive指令进行创建数据库和表&#xff0c;这些信息被存放在metadata和hdfs上面&#xff0c;当执行操作之后&#xff0c;会在hdfs上有目录结构变化&#xff0c…

windows C/C++系列 64位汇编

Visual Studio 包括 32 位和 64 位托管版本的 MASM&#xff08;Microsoft 宏汇编程序&#xff09;&#xff0c;面向 x64 代码。 它的名称为 ml64.exe&#xff0c;是接受 x64 汇编程序语言的汇编程序。 当你在 Visual Studio 安装期间选择 C 工作负荷时&#xff0c;会安装 MASM …

【运维】远程控制与访问的协议(域,工作组,RDP,ARD,VNC,SSH,SCP)和工具(DDNS,跳板机,堡垒机)

【运维】远程控制与访问的协议&#xff08;域&#xff0c;工作组&#xff0c;RDP&#xff0c;ARD&#xff0c;VNC&#xff0c;SSH&#xff0c;SCP&#xff09;和工具&#xff08;DDNS&#xff0c;跳板机&#xff0c;堡垒机&#xff09; 文章目录 1、远程访问协议1.1 组织&#…

基于 SASL/SCRAM 让 Kafka 实现动态授权认证

一、说明 在大数据处理和分析中 Apache Kafka 已经成为了一个核心组件。然而在生产环境中部署 Kafka 时&#xff0c;安全性是一个必须要考虑的重要因素。SASL&#xff08;简单认证与安全层&#xff09;和 SCRAM&#xff08;基于密码的认证机制的盐化挑战响应认证机制&#xff…

傻瓜式PHP-Webshell免杀学习手册,零基础小白也能看懂

项目描述 一、PHP相关资料 PHP官方手册&#xff1a; https://www.php.net/manual/zh/ PHP函数参考&#xff1a; https://www.php.net/manual/zh/funcref.php 菜鸟教程&#xff1a; https://www.runoob.com/php/php-tutorial.html w3school&#xff1a; https://www.w3school…

网络流量分析>>pcapng文件快速分析有用价值解析

引言 在网络安全和流量管理中&#xff0c;解析网络协议数据包是了解网络行为和检测潜在威胁的关键步骤。本文介绍了如何使用Python解析和分析TCP、UDP和ICMP协议的数据包&#xff0c;并统计端口的访问次数。本文的示例代码展示了如何处理不同协议的数据包&#xff0c;提取关键…

网安零基础入门神书,全面介绍Web渗透核心攻击与防御方式!

Web安全是指Web服务程序的漏洞&#xff0c;通常涵盖Web漏洞、操作系统洞、数据库漏洞、中间件漏洞等。 “渗透测试”作为主动防御的一种关键手段&#xff0c;对评估网络系统安全防护及措施至关重要&#xff0c;因为只有发现问题才能及时终止并预防潜在的安全风险。 根据网络安…

前端面试宝典【vue篇】【3】

欢迎来到《前端面试宝典》,这里是你通往互联网大厂的专属通道,专为渴望在前端领域大放异彩的你量身定制。通过本专栏的学习,无论是一线大厂还是初创企业的面试,都能自信满满地展现你的实力。 核心特色: 独家实战案例:每一期专栏都将深入剖析真实的前端面试案例,从基础知…

phpenv安装redis扩展

1、下载dll文件 https://pecl.php.net/package/redis 我的是php8.1, 安装最新版的 DLL文件 &#xff12;、将dll文件放到php安装目录的ext目录下 3、在php.ini中增加配置后重启服务 [Redis] extension php_redis.dll

自研Vue3开源Tree组件:节点拖拽bug修复

当dropType为after&#xff0c;且dropNode为父节点时&#xff0c;bug出现了&#xff1a; bug原因&#xff1a;插入扁平化列表的位置insertIndex计算的不对&#xff1a; 正确的逻辑&#xff0c;同inner要算上子孙节点所占的位置&#xff1a; bug修复&#xff01;

vue里给img的src绑定数据失效

起因 在v-for遍历数据时想要通过给img的src单向绑定 图片路径时出现问题 解决过程 上网查说是webpack构建时识别不到&#xff0c;直接不单绑数据&#xff0c;写死试试看 解决方案 直接require导入图像文件模块

【C语言】VS的实用调试技巧

0. 前言 VS(Visual Studio)是集成开发环境&#xff0c;其内置了多种调试工具和技巧帮助开发人员在开发过程中解决问题。包含断点、监视窗口、自动窗口、调用堆栈等&#xff0c;通过这些技巧&#xff0c;开发人员可以有效地调试和解决程序中的问题。我们在VS编译器上写代码&…