哈希桶的模拟实现【C++】

news2025/1/14 18:37:02

文章目录

  • 哈希冲突解决
    • 闭散列 (开放定址法)
    • 开散列 (链地址法、哈希桶)
      • 开散列实现(哈希桶)
        • 哈希表的结构
        • Insert
        • Find
        • Erase

哈希冲突解决

闭散列 (开放定址法)

发生哈希冲突时,如果哈希表未被装满,说明在哈希表种必然还有空位置,那么可以把产生冲突的元素存放到冲突位置的“下一个”空位置中去

如何寻找“下一个位置”
1、线性探测
发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止

Hi=(H0+i)%m ( i = 1 , 2 , 3 , . . . )

H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过线性探测后得到的存放位置
m:表的大小。

举例:
用除留余数法将序列{1,111,4,7,15,25,44,9}插入到表长为10的哈希表中,当发生哈希冲突时我们采用闭散列的线性探测找到下一个空位置进行插入,插入过程如下:

使用除留余数法
1%10 =1 ,111 %10 =1
即111和1发生了哈希冲突 ,所以111找到1的下一个空位置插入
在这里插入图片描述

将数据插入到有限的空间,那么空间中的元素越多,插入元素时产生冲突的概率也就越大,冲突多次后插入哈希表的元素,在查找时的效率必然也会降低。
介于此,哈希表当中引入了负载因子(载荷因子):

负载因子 = 表中有效数据个数 / 空间的大小
不难发现:
负载因子越大,产出冲突的概率越高,查找的效率越低
负载因子越小,产出冲突的概率越低,查找的效率越高

负载因子越小,也就意味着空间的利用率越低,此时大量的空间都被浪费了。对于闭散列(开放定址法)来说,负载因子是特别重要的因素,一般控制在0.7~0.8以下
采用开放定址法的hash库,如JAVA的系统库限制了负载因子为0.75,当超过该值时,会对哈希表进行增容

线性探测的缺点:一旦发生冲突,所有的冲突连在一起,容易产生数据“堆积”,即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要多次比较(踩踏效应),导致搜索效率降低
2、二次探测

二次探测为了避免该问题,找下一个空位置的方法为

Hi=(H0+i ^2 )%m ( i = 1 , 2 , 3 , . . . )

H0:通过哈希函数对元素的关键码进行计算得到的位置
Hi:冲突元素通过二次探测后得到的存放位置
m:表的大小

相比线性探测而言,二次探测i是平方,采用二次探测的哈希表中元素的分布会相对稀疏一些,不容易导致数据堆积

template <class K>
struct  DefaultHashFunc
{
	size_t operator() (const K& key)
	{
		return (size_t)key;
	}
};

template <>
struct DefaultHashFunc<string>
{
	size_t  operator() (const string& str)
	{
		//BKDR,将输入的字符串转换为哈希值
		size_t hash = 0;
		for (auto ch : str)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

namespace open_address 
{
	enum  STATE
	{
		EXIST,
		EMPTY,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		STATE _state = EMPTY;
	};


	struct StringHashFunc
	{
		size_t operator()(const string& str)
		{
			return str[0];
		}
	};

	//template<class K, class V>
	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);
		}
		bool insert(const pair<K, V> kv)
		{
			//扩容 
			if ((double)_n / (double)_table.size() >= 0.7)
			{
				HashTable<K, V>  newHT;
				size_t newSize = _table.size() * 2;
				newHT._table.resize(newSize);
				//遍历旧表的数据,将旧表的数据重新映射到新表中

				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state == EXIST)
					{
						newHT.insert(_table[i]._kv);//插入的写成kv不行?
					}

				}
				_table.swap(newHT._table);
			}





			//线性探测

			HashFunc hf;

			size_t  hashi = hf(kv.first) % _table.size();

			//如果该位置没有元素,则直接插入元素 ,如果该位置有元素,找到下一个空位置,插入新元素
			while (_table[hashi]._state == EXIST)//不是EMPTY和DELETE这两种情况
			{
				++hashi;
				hashi %= _table.size();
			}
			//是EMPTY和DELETE这两种情况
			_table[hashi]._kv = kv;
			_table[hashi]._state = EXIST;
			++_n;
			return true;
		}

		HashData<const K, V>* Find(const K& key)
		{
			HashFunc hf;
			//线性探测 
		//如果该位置没有元素,则直接插入元素 ,如果该位置有元素,找到下一个空位置,插入新元素
			size_t hashi = hf(key) % _table.size();
			while (_table[hashi]._state != EMPTY) //DELETE和EXIST
			{
				if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
				{
					return  (HashData<const K, V>*) & _table[hashi];
				}
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			//先找到
			HashData<const K, V>* ret = Find(key);
			//再删除 
			if (ret != nullptr)
			{
				ret->_state = DELETE;
				_n--;
				return true;
			}
			//没找到 
			return false;
		}
	public:
		vector<HashData<K, V>> _table;
		size_t  _n = 0; //存储有效数据的个数

	};

}

闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

开散列 (链地址法、哈希桶)

开散列,又叫哈希桶,首先对关键码集合用哈希函数计算哈希地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中

举例:
用除留余数法将序列{1,111,4,7,15,25,44,9}插入到表长为10的哈希表中,当发生哈希冲突时我们采用开散列的方式进行插入,插入过程如下:
在这里插入图片描述
将相同哈希地址的元素通过单链表链接起来,然后将链表的头结点存储在哈希表中的方式,不会影响与自己哈希地址不同的元素的增删查改的效率,因此开散列的负载因子相比闭散列而言,可以稍微大一点

闭散列的开放定址法,负载因子不能超过1,一般建议控制在[0.0, 0.7]

开散列的哈希桶,负载因子可以超过1,一般建议控制在[0.0, 1.0]

在实际中,开散列的哈希桶结构比闭散列更实用,主要原因有两点:
哈希桶的负载因子可以更大,空间利用率高
哈希桶在极端情况下还有可用的解决方案

开散列实现(哈希桶)

哈希表的结构
struct HashNode
	{
		pair<K, V>  _kv;
		HashNode<K,V>* _next;
		HashNode(  const pair<K, V> & kv)
			:_kv(kv)
			,_next(nullptr)
		{
			
		}
	};
Insert
	bool Insert(const pair<K,V> & kv)
		{
			
			size_t hashi = kv.first % _table.size();
			//负载因子到1就扩容 
			if (_n == _table.size())
			{
				size_t 	newsize = _table.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newsize, nullptr);

			
				//遍历旧表,将原哈希表当中的结点插入到新哈希表
				for (int i = 0; i <= _table.size(); i++)
				{
					Node* cur = _table[i];
					//插入到新哈希表
					while (cur != nullptr)
					{
						Node* next = cur->_next;
						// 重新分配hashi
						size_t hashi = cur->_kv.first % _table.size();
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

				}
			}
	
			//头插 
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			return true;
		}

在这里插入图片描述

Find
	Node *   Find(const K & key)
		{
			size_t hashi = key % _table.size();
			Node* cur = _table[hashi];
			while (cur != nullptr)
			{
				if (key == cur->_kv.first)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}
Erase

32.png)

		bool Erase(const K & key)
		{
			size_t hashi = key % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur != nullptr)
			{
				if (key == cur->_kv.first)
				{
					  if(prev==nullptr)//第二种情况 ,prev是nullptr ,就是头删
					  {
						_table[hashi] = cur->_next;
					  }
					  else//第一种情况 ,cur是头节点
					  {
						  prev->_next = cur->_next;
					  }
					  delete cur;
					return  true; 
				}
				prev = cur;
				cur = cur->_next;
			}
		

			//没找到 
			return false;
		}
namespace hash_bucket
{
	template <class K ,class V> 
	struct HashNode
	{
		pair<K, V>  _kv;
		HashNode<K,V>* _next;
		HashNode(  const pair<K, V> & kv)
			:_kv(kv)
			,_next(nullptr)
		{
			
		}
	};
	template<class K,class V> 
	class HashTable
	{
	public:
		typedef HashNode<K,V>  Node;

		//iterator begin()
		//{

		//}
		//iterator end()
		//{

		//}
		//const_iterator begin()
		//{

		//}
		//const_iterator end()
		//{

		//}
		//GetNextPrime()
		//{

		//}
		HashTable()
		{
			_table.resize(10, nullptr);
		}
		~HashTable()
		{

		}
		//bool Insert(const pair<K, V>  kv)
		//{
		//	//负载因子到1就扩容 
		//	if (_n == _table.size())
		//	{
		//		size_t 	newsize = _table.size() * 2;
		//		vector<Node*> newtable;
		//		newtable.resize(newsize, nullptr);
		//	}
		//	size_t hashi = kv.first % _table.size();
		//	//头插 
		//	Node* newnode = new Node(key);
		//	newnode->_next = _table[hashi];
		//	_table[hashi] = newnode;
		//	++_n;
		//	return true;
		//}
		bool Insert(const pair<K,V> & kv)
		{
			
			size_t hashi = kv.first % _table.size();
			//负载因子到1就扩容 
			if (_n == _table.size())
			{
				size_t 	newsize = _table.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newsize, nullptr);

			
				//遍历旧表,将原哈希表当中的结点插入到新哈希表
				for (int i = 0; i <= _table.size(); i++)
				{
					Node* cur = _table[i];
					//插入到新哈希表
					while (cur != nullptr)
					{
						Node* next = cur->_next;
						// 重新分配hashi
						size_t hashi = cur->_kv.first % _table.size();
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

				}
			}
	
			//头插 
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			return true;
		}

		Node *   Find(const K & key)
		{
			size_t hashi = key % _table.size();
			Node* cur = _table[hashi];
			while (cur != nullptr)
			{
				if (key == cur->_kv.first)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}
		bool Erase(const K & key)
		{
			size_t hashi = key % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur != nullptr)
			{
				if (key == cur->_kv.first)
				{
					  if(prev==nullptr)//第二种情况 ,prev是nullptr ,就是头删
					  {
						_table[hashi] = cur->_next;
					  }
					  else//第一种情况 ,cur是头节点
					  {
						  prev->_next = cur->_next;
					  }
					  delete cur;
					return  true; 
				}
				prev = cur;
				cur = cur->_next;
			}
		

			//没找到 
			return false;
		}
		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur != nullptr)
				{
				
					cout << cur->_kv.first << "->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
			cout << endl;
		}
	private:
		vector<Node*> _table;//指针数组
		size_t  _n = 0;//存储有效数据

	};
}

如果你觉得这篇文章对你有帮助,不妨动动手指给点赞收藏加转发,给鄃鳕一个大大的关注
你们的每一次支持都将转化为我前进的动力!!

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

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

相关文章

【CSS】基础知识梳理和总结

1. 前言 CSS&#xff08;Cascading Style Sheets&#xff0c;层叠样式表&#xff09;&#xff0c;用来为HTML文档添加样式的计算机语言。HTML中加载样式的方法有三种&#xff1a; 通过<link>标签加载外部样式表&#xff08;External Style Sheet&#xff09;&#xff0c…

【论文阅读】Realtime multi-person 2d pose estimation using part affinity fields

OpenPose&#xff1a;使用PAF的实时多人2D姿势估计。 code&#xff1a;GitHub - ZheC/Realtime_Multi-Person_Pose_Estimation: Code repo for realtime multi-person pose estimation in CVPR17 (Oral) paper&#xff1a;[1611.08050] Realtime Multi-Person 2D Pose Estima…

SpringBoot多线程与任务调度总结

一、前言 多线程与任务调度是java开发中必须掌握的技能&#xff0c;在springBoot的开发中&#xff0c;多线程和任务调度变得越来越简单。实现方式可以通过实现ApplicationRunner接口&#xff0c;重新run的方法实现多线程。任务调度则可以使用Scheduled注解 二、使用示例 Slf…

磁盘管理 :逻辑卷、磁盘配额

一 LVM可操作的对象&#xff1a;①完成的磁盘 ②完整的分区 PV 物理卷 VG 卷组 LV 逻辑卷 二 LVM逻辑卷管理的命令 三 建立LVM逻辑卷管理 虚拟设置-->一致下一步就行-->确认 echo "- - -" > /sys/class/scsi_host/host0/scan;echo "- -…

【SpringBoot】第2章 SpringBoot核心配置与注解

学习目标 熟悉SpringBoot全局配置文件的使用 熟悉SpringBoot自定义配置 掌握SpringBoot配置文件属性值注入 掌握Profile多环境配置 了解随机值设置以及参数间引用 2.1 全局配置文件 全局配置文件能够对一些默认配置进行修改。SpringBoot使用一个application.properties…

设计模式(4)--对象行为(7)--观察者

1. 意图 定义对象间的一种一对多的依赖关系&#xff0c; 当一个对象的状态改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。 2. 四种角色 抽象目标(Subject)、具体目标(Concrete Subject)、抽象观察者(Observer)、 具体观察者(Concrete Observer) 3. 优点 3.1 …

数据结构学习 Leetcode494 目标和

关键词&#xff1a;动态规划 01背包 dfs回溯 一个套路&#xff1a; 01背包&#xff1a;空间优化之后dp【target1】&#xff0c;遍历的时候要逆序遍历完全背包&#xff1a;空间优化之后dp【target1】&#xff0c;遍历的时候要正序遍历 题目&#xff1a; 解法一&#xff1a; …

国际物流公司科普_集装箱种类区分和介绍_箱讯科技

集装箱运输的不断发展&#xff0c;为适应装载不同种类货物的需要&#xff0c;因而出现了不同种类的集装箱。今天和大家一起来总结一下。 按使用材料分类 根据箱子主体部件&#xff08;侧壁、端壁、箱顶等&#xff09;采用什么材料&#xff0c;就叫做什么材料制造的集装箱&…

pybullet安装时出现fatal error C1083: 无法打开包括文件: “string.h”: No such file or directory

pybullet安装时出现fatal error C1083: 无法打开包括文件: “string.h”: No such file or directory 报错原文&#xff1a; -----CloneTreeCreator.cppD:\Program_Professional\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.38.33130\include\cstring(11): fat…

最短路径(数据结构实训)(难度系数100)

最短路径 描述&#xff1a; 已知一个城市的交通路线&#xff0c;经常要求从某一点出发到各地方的最短路径。例如有如下交通图&#xff1a; 则从A出发到各点的最短路径分别为&#xff1a; B&#xff1a;0 C&#xff1a;10 D&#xff1a;50 E&#xff1a;30 F&#xff1a;60 输…

百度每天20%新增代码由AI生成,Comate SaaS服务8000家客户 采纳率超40%

12月28日&#xff0c;由深度学习技术及应用国家工程研究中心主办的WAVE SUMMIT深度学习开发者大会2023在北京召开。百度首席技术官、深度学习技术及应用国家工程研究中心主任王海峰现场公布了飞桨文心五载十届最新生态成果&#xff0c;文心一言最新用户规模破1亿&#xff0c;截…

PostgreSQL14 Internals 中文版 持续修正...

为了方便自己快速学习&#xff0c;整理了翻译版本&#xff0c;目前翻译的还不完善&#xff0c;后续会边学习边完善。 About This Book 1 Introduction Part I Isolation and MVCC 2 Isolation 3 Pages and Tuples 4 Snapshots 5 Page Pruning and HOT Updates 6 Vacuum…

加强-jdbc与连接池的关系,连接池有哪些

0驱动什么是数据库驱动 开发人员编写好应用程序之后想要操作数据库&#xff0c;平常就了解到有很多种数据库如oracle\mysql\sql server&#xff0c;代码已经写好了是一套总不能在使用不同的数据库技术的时候代码就要写不同方式连接来连接数据库吧&#xff0c;所以开发商在开发数…

产品管理-学习笔记-版本的划分

版本号说明【X.Y.Z_修饰词】 版本号定义原则X表示大版本号&#xff0c;一般当产品出现重大更新、调整、不再向后兼容的情况时我们会在X上加1Y表示功能更新&#xff0c;在产品原有的基础上增加、修改部分功能&#xff0c;且并不影响产品的整体流程或业务Z表示小修改&#xff0c…

Illustrator脚本 #015 自动角线

这是一个在画板上自动生成辅助线和角线的脚本,只要单击最右边按钮运行脚本即可。 绿色的为参考线及出血线。 #target "Illustrator" var settings = {addTrim : true,addBleedGuide : true,addCenterGuide : true,addCover : false,overlapAlert : false,trimma…

联营商自述被坑惨,加盟库迪没有未来?

撰稿 | 多客 来源 | 贝多财经 近日&#xff0c;库迪联营商在社交平台不约而同发出了致库迪咖啡管理层的公开信&#xff0c;两封公开信可谓字字珠玑&#xff0c;没有一句废话&#xff0c;揭开了库迪咖啡在细节、运营、扩张、培训等方方面面的“背后真相”。 两封公开信 折射库…

【PowerMockito:编写单元测试过程中采用when打桩失效的问题】

问题描述 正如上图所示&#xff0c;采用when打桩了&#xff0c;但是&#xff0c;实际执行的时候还是返回null。 解决方案 打桩时直接用any() 但是这样可能出现一个mybatisplus的异常&#xff0c;所以在测试类中需要加入以下代码片段&#xff1a; Beforepublic void setUp() …

如何积极管理日内伦敦银交易?

伦敦银日内交易要做得好&#xff0c;积极的管理是很重要的。不要小看管理这个因素&#xff0c;在日内这种短线交易中&#xff0c;它能对交易结果产生决定性的影响。晚走几秒钟&#xff0c;市场可能就由涨转跌了&#xff0c;投资者就可能由盈转亏了。下面我们就来具体地讨论一下…

Java——值得收藏的Java final修饰符总结!!!

Java final修饰符总结 一、final修饰类二、final修饰方法三、final修饰变量 总结 算下刚转Java到现在也有三个多月了&#xff0c;所以打算对Java的知识进行汇总一下&#xff0c;本篇文章介绍一下Java的final修饰符的作用&#xff0c;final表示最后的、最终的含义&#xff0c;fi…

专题四:前缀和

前缀和 一.一维前缀和(模板)&#xff1a;1.思路一&#xff1a;暴力解法2.思路二&#xff1a;前缀和思路 二. 二维前缀和(模板)&#xff1a;1.思路一&#xff1a;构造前缀和数组 三.寻找数组的中心下标&#xff1a;1.思路一&#xff1a;前缀和 四.除自身以外数组的乘积&#xff…