【C++进阶学习】第十弹——哈希的原理与实现——链地址法的原理与讲解

news2024/9/17 7:13:57

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

前言:

哈希的整体思想就是建立映射关系,前面的开放地址法的讲解中,也对哈希的原理做了详细的讲解,今天就来讲解一下实现哈希的另一种主要方法——链地址法

目录

一、链地址法的基本思想

二、链地址法的实现步骤

节点结构

构造和析构

插入操作

查找操作

删除操作

打印操作

三、测试代码

四、总结


一、链地址法的基本思想

前面所讲的开放地址法,我们是通过建立一种映射的关系来存储数据

这种方法时常会遇到图中的这种情况,有利有弊

链地址法则是另一种思路:将哈希表的每个槽指向一个链表(或其他数据结构,如动态数组,红黑树等),所有哈希到同一个槽的元素都存储在这个链表中。这样,即使发生了哈希冲突,也可以通过链表来存储多个元素。

二、链地址法的实现步骤

首先,我们先来看一下链地址法的重点:

  1. 定义哈希表结构:哈希表通常包含一个数组,数组的每个元素是一个链表的头节点。
  2. 哈希函数:设计一个哈希函数,将键映射到数组的索引位置。
  3. 插入操作
    • 计算键的哈希值,得到索引位置。
    • 将键值对插入到对应索引位置的链表中。
  4. 查找操作
    • 计算键的哈希值,得到索引位置。
    • 在对应索引位置的链表中查找键值对。
  5. 删除操作
    • 计算键的哈希值,得到索引位置。
    • 在对应索引位置的链表中删除键值对。

节点结构

与开放寻址法一样,因为我们并不知道插入要操作何种类型的数据,可能是整形,浮点型或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;
	}
};


    template<class K,class V>
	struct HashNode
	{
		HashNode* next;
		pair<K, V> _kv;
		HashNode(const pair<K,V>& kv)    //构造函数
			:next(nullptr)     //初始化列表
			,_kv(kv)
		{}
	};

	template<class K,class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		
	private:
		//vector<list> _tables;    //这也是一种思路
		vector<Node*> _tables;
		size_t _n;
	};

构造和析构

因为在节点中我们使用了指针类型的数据,所以我们尽量将构造和析构函数自己定义,这里没啥难度,看代码即可:

        HashTable()
		{
			_tables.resize(10);     //初始化大小为19
		}
		~HashTable()
		{
			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];     //每个链表的头节点
				while (cur)     //遍历链表,清空链表中的所有元素
				{
					Node* next = cur->next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

插入操作

链地址法插入操作的基本思路就是:

 1、选择合适的哈希函数,确定数组大小

2、通过哈希函数找到自己所对应的位置,并进行头插

3、当负载因子过大时进行扩容

		bool Insert(const pair<K, V>& kv)
		{
			Hash hf;
			if (Find(kv.first))
				return false;

			//负载因子最大到1,到1时进行扩容
			//我们提供这样一个思路:如果数据真的非常多的时候,用链表来存储,因为要
			//                    考虑负载因子的原因,其实是比较浪费空间的,我们
			//                    可以把节点结构进行更改,改成红黑树的结构
			if (_n == _tables.size())
			{
				扩容
				//size_t newSize = _tables.size() * 2;
				//HashTable<K, V> newHT;
				//newHT._tables.resize(newSize);
				遍历旧表
				//for (size_t i = 0; i < _tables.size(); i++)
				//{
				//	Node* cur = _tables[i];
				//	while (cur)
				//	{
				//		newHT.Insert(cur->_kv);
				//		cur = cur->next;
				//	}
				//}
				//_tables.swap(newHT._tables);

				//方法二
				vector<Node*> newTables;
				newTables.resize(_tables.size() * 2);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->next;
						size_t hashi = hf(cur->_kv.first) % newTables.size();
						cur->next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}
			size_t hashi = hf(kv.first) % _tables.size();     //哈希函数
			Node* newnode = new Node(kv);
			//头插
			newnode->next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return true;
		}

查找操作

上面的插入操作中,我们首先就先用查找操作看是否已经有这个数据,因为哈希是不允许存在重复数据的,这里我们就来看一下这个查找操作,首先是先通过哈希函数找到对应的头节点,然后在对应的链表中进行查找

	    Node* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
					return cur;
				cur = cur->next;
			}
			return nullptr;
		}

删除操作

删除操作也是先通过哈希函数找到删除元素的头节点,然后就是链表中元素的删除那一套操作

		bool Erase(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (parent == nullptr)
					{
						_tables[hashi] = cur->next;
					}
					else
					{
						parent->next = cur->next;
					}
					delete cur;
					cur = nullptr;
					return true;
				}
				parent = cur;
				cur = cur->next;
			}
			return false;
		}

打印操作

链地址法我们一般需要观测的数据是链表个数,链表长度等(链表在这里也成为桶,即哈希桶),所以我们这里打印的是与链表个数、长度等相关的

		void Some()
		{
			size_t bucketSize = 0;        //桶的个数 
			size_t maxbucketLen = 0;      //最大桶长
			size_t sum=0;                 //总的元素个数
			double averagebucketLen = 0;  //平均桶长

			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
				{
					++bucketSize;
				}
				size_t bucketLen = 0;
				while (cur)
				{
					++bucketLen;
					cur = cur->next;
				}
				sum += bucketLen;
				if (bucketLen > maxbucketLen)
				{
					maxbucketLen = bucketLen;
				}
			}
			averagebucketLen = (double)sum / (double)bucketSize;
			cout << "桶的个数:" << bucketSize << endl;
			cout << "桶的最大长度:" << maxbucketLen << endl;
			cout << "平均桶的长度:" << averagebucketLen << endl;
		}

三、测试代码

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

测试一:

	void TestHT1()   //测试插入,查找和删除操作是否有误
	{
		HashTable<int, int> ht;
		int a[] = { 4,14,24,34,5,7,1 };
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		cout << ht.Find(4) << endl;     //如果成功插入,这里会返回一个地址
		ht.Erase(4);                    //删除节点
		cout << ht.Find(4) << endl;     //删除后会返回nullptr
	}

运行结果:

测试二:

	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.Some();    //通过桶的相关信息可以推断出插入情况
	}

运行结果:

四、总结

以上就是链地址法的内容,链地址法与开放地址法各有千秋,总的来说开放地址法时间复杂度更低,都是当数据过多时需要的空间多,链地址法节省空间但是效率上稍微偏低,在应用时要结合实际情况进行取舍

感谢各位大佬观看,创作不易,还请各位大佬点赞支持!!!

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

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

相关文章

系统移植(八)u-boot源码解析(未整理)

文章目录 一、分析make <board_name>_defconfig执行过程&#xff08;一&#xff09;1. 1. 分析Makefile文件&#xff0c;分析Makefile文件的规则中目标为"<board_name>_defconfig", &#xff08;二&#xff09;&#xff08;三&#xff09; 二、分析make …

【精通Redis】Redis命令详解

引言 Redis是一个内存数据库&#xff0c;在学习它的内部原理与实现之前&#xff0c;我们首先要做到的就是学会使用&#xff0c;学会其丰富的命令操作。 一、字符串 Redis的字符串类型之前笔者的一篇入门介绍中曾经说过&#xff0c;不是简单的只存人可以阅读的字符串&#xf…

JavaWeb笔记_FilterListener

一.过滤器 1.1 过滤器概述 过滤器主要用来拦截目标资源&#xff08;静态资源或动态资源&#xff09;的请求和响应 &#xff08;类似地铁的安检&#xff09; 我们访问动态或静态资源都要通过URL访问&#xff1a;http://localhost:8080/... 所以过滤器本质上拦截的是URL 1.2 过滤…

select ... for update中锁等级转化

一、结论 select ... for update 除了查询功能&#xff0c;还实现了加锁机制&#xff0c;是一种悲观锁。根据是否使用了主键和索引&#xff0c;决定锁等级是表锁还是行锁。如果采用了&#xff0c;则是行锁&#xff0c;否则是表锁。 二、实例 前提条件&#xff1a;将事务自动…

你敢信?1万块存上5年,到手只有900!

1996年的夏天你走进银行&#xff0c;会看到五年期整存整取的利息&#xff0c;可能高达14%左右。1万块存上5年&#xff0c;到手利息高达——7000元。 今天呢&#xff1f;同样的存款方式&#xff0c;5年后&#xff0c;能拿到的利息只有900元。靠吃银行利息就能躺平的年代&#xf…

分布式事务解决方案(一) 2PC、3PC、TCC、Sega

目录 1.绪论 2.2PC 2.1 基本原理 2.1.1 组成 2.1.2 步骤 1.prepare阶段 2.commit阶段 2.2 2PC 存在的问题 2.2.1 阻塞问题 2.2.2 单点故障问题 1. 事务协调器宕机 2.部分数据不一致问题 2.资源管理器宕机 3. 事务协调器和资源管理管理器同时宕机 2.2 实现 2.2.1…

【AI落地应用实战】Amazon Bedrock +Amazon Step Functions实现链式提示(Prompt Chaining)

一、链式提示 Prompt Chaining架构 Prompt Chaining 是一种在生成式人工智能&#xff08;如大型语言模型&#xff09;中广泛使用的技术&#xff0c;它允许用户通过一系列精心设计的提示&#xff08;Prompts&#xff09;来引导模型生成更加精确、丰富且符合特定需求的内容。 P…

freertos-HAL库-STM32Cubemax生成

打开cubemax选好型号配置RCC&#xff08;外部高速时钟&#xff09;这里查看原理图&#xff0c;我们把按键设为输入&#xff0c;led设为输出创建两个新任务&#xff08;default是系统创建的&#xff09;配置时钟&#xff0c;这里HSE是外部高速时钟&#xff0c;HSI是内部的&#…

打卡第27天------贪心算法

再次祈祷上帝,提前预备好自己,希望我可以在机会来临的时候,抓住机会,成功上岸! 一、理论基础 什么是贪心?例如:有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿? 你肯定是每次拿最大的,最终结果就是拿走最大数额的钱了。 每次拿最大的就是局部最优,…

【Python从入门到进阶】61、Pandas中DataFrame对象的操作(二)

接上篇《60、Pandas中DataFrame对象的操作&#xff08;一&#xff09;》 上一篇我们讲解了DataFrame对象的简介、基本操作及数据清洗相关的内容。本篇我们来继续讲解DataFrame对象的统计分析、可视化以及数据导出与保存相关内容。 一、DataFrame的统计分析 在数据分析和处理中…

Selenium Java中的isDisplayed()方法

isDisplayed&#xff08;&#xff09;方法用于确定元素是否可见。本文将详细讨论 的WebElement接口isDisplayed&#xff08;&#xff09;方法。 方法声明- boolean isDisplayed&#xff08;&#xff09;它能做什么&#xff1f;此方法用于判断元素是否显示。这个方法节省了我们…

EasyExcel入门

目录 一、文章简介 二、概念 1.EasyExcel是什么&#xff1f; 2.EasyExcel 能用在哪里&#xff1f; 3.为什么要选用EasyExcel解析excel&#xff1f; 4.如何使用EasyExcel&#xff1f; 三、EasyExcel快速入门 1.环境搭建 2.简单写excel 代码示例 TestFileUtil Employe…

C++(week14): C++提高:(二)C++11线程库

文章目录 一、线程1.C11线程库的概述2.构造函数3.线程启动: 线程入口函数的传递方式4.线程终止5.线程状态6.获取线程id&#xff1a;get_id() 二、互斥锁1.什么是互斥锁2.头文件3.常用函数接口 三、lockguard与unique_lock1.lock_guard2.unique_lock(1)概念(2)函数接口 3.原子数…

Python脚本:使用PyPDF2给一个PDF添加上页数/总页数标签

一、实现代码 import PyPDF2 from PyPDF2 import PdfWriter from PyPDF2.generic import AnnotationBuilder# 指定输入和输出pdf pdf_path rC:\Users\ASUS\Desktop\temp\xxxx.pdf out_path rC:\Users\ASUS\Desktop\temp\xxxx2.pdf# 创建 PdfWriter 对象 writer PdfWriter()…

Python转换Excel文件为SVG文件

SVG&#xff08;Scalable Vector Graphics&#xff09;是一种基于XML的矢量图像格式。这种格式在Web开发和其他图形应用中非常流行&#xff0c;提供了一种高效的方式来呈现复杂的矢量图形。如果我们需要在网页中嵌入Excel表格&#xff0c;或是直接使用Excel工作表制作网页&…

基于元神系统编写“清屏”程序

1. 背景 本文介绍了基于元神系统开发软件的操作流程&#xff0c;并详细介绍了“清空屏幕”程序的编写以及测试结果。 2. 方法 &#xff08;1&#xff09;编写程序 在元神系统0.4版的基础上&#xff0c;用FASM汇编语言进行软件开发。假设屏幕为80列25行的文本显示模式&#…

【更新2022】各省农业科技活动经费(RD)测算 1999-2022 无缺失

各省农业科技活动经费&#xff08;R&D&#xff09;测算数据在农业经济学、政策研究和农村发展规划等领域的论文研究中具有重要应用价值。首先&#xff0c;这些数据可以用于分析不同省份在农业科技投入上的差异及其对农业生产力和产出的影响&#xff0c;帮助揭示不同地区农业…

Node.js版本管理工具之NVM

目录 一、NVM介绍二、NVM的下载安装1、NVM下载2、卸载旧版Node.js3、安装 三、NVM配置及使用1、设置nvm镜像源2、安装Node.js3、卸载Node.js4、使用或切换Node.js版本5、设置全局安装路径和缓存路径 四、常用命令技术交流 博主介绍&#xff1a; 计算机科班人&#xff0c;全栈工…

坐牢十八天 20240729(IO)

一.笔记 1. 有关系统时间的函数 1> 有关时间的函数 #include <time.h> time_t time(time_t *tloc); 功能&#xff1a;获取系统时间&#xff0c;从1970年1月1日0时0分0秒&#xff0c;到目前累计的秒数 参数&#xff1a;用于接收的秒数 返回值&#xff1a;秒数使…