C++·哈希

news2025/1/15 12:53:41

1. unordered系列关联式容器

        在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到logN。后来在C++11中STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的使用方法类似,但是底层结构不同,事实上底层是哈希表,因此查询时效率可达到O(1)

        这四个容器分别叫 unordered_set 、unordered_multiset 、unordered_map 、unordered_multimap。它们的函数接口就不展示了,我们在set与map中基本上都涉及过了,还有几个哈希结构特有的函数接口我们在讲底层的时候在说。

Containers - C++ Referenceicon-default.png?t=N7T8https://legacy.cplusplus.com/reference/stl/

2. 底层结构

        一般来讲搜索的效率与关键码的比较次数有关,顺序表的关键码值最坏比较次数是N,平衡搜索树的最坏比较次数是logN。那最理想的搜索方案就是不经过任何比较,直接一次找到需要的关键码。

        如果构造一种储存结构,通过某种方法使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快的找到该元素。

2.1 哈希概念

        插入元素:根据待插入元素的关键码,以此计算该元素的存储位置并按此位置存放。

        搜索元素:对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码值相等,则搜索成功。

        该方式为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或称散列表)。

        哈希是一种映射的思想,这种思想其实我们在排序那节中就接触过了,计数排序的时候我们让数字去映射数组的下标,每出现一次该数字就在下标对应位置+1,最后得到数字出现频率的高低顺序。

2.2 哈希函数与哈希冲突

        我们可以借助上面的思想,让数据的关键码值作为数组的下标让数组形成一个哈希表,但是这么做会存在一个要求的数据关键码值不集中就会导致浪费空间的问题,关于这个问题我们解决办法就是让数据的关键码值取模哈希表所开空间大小 hash(key) = key%capacity 

        这个hash()函数我们称之为哈希函数,它用来控制映射的规则

        如果我们再向上面的哈希表中插入一个36会怎样,6中已经存上数据了此时两个数据的存储发生了冲突,这个冲突就叫哈希冲突

        引发哈希冲突的原因就是哈希函数设置的不合理。

        哈希函数的设置原则:

        1. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间,就是说哈希函数映射出的位置必须在哈希表中。

        2. 哈希函数计算出来的地址能均匀分布在整个空间中。

        3. 哈希函数应该比较简单

2.3 解决哈希冲突

        解决哈希冲突的两种常用办法是:闭散列、开散列

2.3.1 闭散列(开放地址法)

        闭散列也叫开放地址法,当发生哈希冲突时,如果哈希表为被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置的"下一个"空位中去。这个寻找空位的也有两种方案,线性探测和二次探测

        线性探测

        线性探测就是从被占的关键码值位置一个一个挨着向后看,遇到空位置就把这个元素插入,就比如我们现在想插入36,但是6的位置发生哈希冲突了,那就往后看,7是空着的就存到7的位置上去。

        36存进去之后又要存37,那往后找至顺序表结束之后要有回溯的操作,从顺序表头开始向后找。

       但是这个方案会造成数据拥堵的问题,某个范围内的数据会异常爆满,但别的地方还很空旷,这不符合哈希函数设置的第二条原则

        二次探测

        每次从映射位置向后找 i的二次方 的位置是否为空,第一次找映射位置后的 1^2 位置,第二次找映射位置后的 2^2 位置,第三次找射位置后的 3^2 位置。

        这个方案一定程度上解决了局部数据拥堵的问题,但还是治标不治本。

        不过不用担心,解决哈希冲突还有另一类方法:开散列,或者叫哈希桶或拉链法,这个方案可以治本。但是先不急,我们先实现一下线性探测的方案

线性探测的实现

        线性探测的思路中还有一个坑没解决,比如此时要查找36。按哈希函数的逻辑来走,36的映射位置是6,6位置被占用了,而且不是36,那就往后走,发现36找到了。

        但如过我们现在把10086删除了,这个位置现在是空的,然后再查找36,按哈希函数逻辑来走,映射6位置,但6位置为空,那就说明36不在哈希表中,没找到退出了。这个结果明显是错的,为了解决这个问题我们对每个位置引入3个状态,存在、空、删除。

        此时哈希表的框架就可以写成这样

                                

        插入数据

                ​​​​​​​​​​​​​​

        插入数据时先算出应该的存放位置,然后判断如果位置被占了就一直向后走知道有一个空位置,或被删除的位置,寻找的位置超出容器长度就重回第一个位置找。

        但是这么写是不对的,如果此时哈希表已经满了,那这个函数就会陷入死循环,因此我们要加入扩容逻辑。当然还有插入成功和失败的逻辑也还没写。

哈希表的扩容

        哈希表中有一个控制扩容的参数叫 载荷因子 一旦载荷因子超过限定值就要进行扩容。

                载荷因子\alpha  =  表中现有元素个数  /  哈希表的长度

        \alpha是哈希表装满程度的标志因子。由于表长是定值,\alpha与 表中现有元素成正比,因此\alpha越大,表中元素越多,插入新元素时产生冲突的概率越大;反之\alpha越小,产生冲突的可能性就越小。实际上,哈希表的平均查找长度是载荷因子\alpha的函数,只是不同处理冲突的方法有不同的函数

        对于开放地址法,载荷因子是特别重要的因素,应严格限制在 0.7~0.8 以下。超过 0.8 查表时的CPU缓存不命中概率指数级上升。因此,一些采用开放定址法的hash库,比如Java的系统库限制了载荷因子为 0.75 ,超过此值将resize扩容。

        ​​​​​​​        ​​​​​​​

        我们修改一下默认构造函数,让它一上来就先开一部分空间,然后扩容的时候因为我们的 _n 是无符号整形,运算不出0.7这样的小数,所以我们干脆给它乘10,让它的结果变成整数来比较。

         扩容逻辑内部我们可以复用insert函数,这不是递归,然后用现代写法交换一下容器就可以完成扩容了。

        查找数据

        查找数据需要注意查找到的数据不能是删除的状态。

        删除数据

        ​​​​​​​        ​​​​​​​        

自定义哈希码值

        至此我们的哈希表还没有完全完成,比如现在插入pair<string, string>类型的元素就会报错,因为string类型无法进行取模,也就没办法找映射的存放位置。此时就要借助别仿函数,生成一个整形哈希关键码值。

        如果是可以强转成整形的数据我们就直接强转

        如果是string类型(库中支持转成整形了),或类似的不能强转的自定义类型元素,我们就要自己写一个强转的仿函数。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

        但是库中是支持string类转成整形关键码的,原理就是用到了模板的特化,所以我们在使用库中的哈希容器时是可以不用传第三个模板参数的。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

完整代码
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

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

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t n = 0;
		for (auto e : key)
		{
			n += e;
		}
		return n;
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	HashTable()
	{
		_tables.resize(10);
	}
	bool insert(const pair<K, V>& kv)
	{
		Hash hs;
		//不接收冗余
		if (Find(kv.first))
			return false;

		//扩容
		if (_n * 10 / _tables.size() >= 7)
		{
			HashTable<K, V, Hash> newHT;
			newHT._tables.resize(_tables.size() * 2);
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					newHT.insert(_tables[i]._kv);
				}
			}
			_tables.swap(newHT._tables);
		}

		size_t hashi = hs(kv.first) % _tables.size();
		while (_tables[hashi]._state == EXIST)
		{
			++hashi;
			hashi %= _tables.size();
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}

	HashData<K, V>* Find(const K& key)
	{
		Hash hs;
		size_t hashi = hs(key) % _tables.size();
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._kv.first == key && _tables[hashi]._state == EXIST)
			{
				return &_tables[hashi];
			}
			++hashi;
			hashi %= _tables.size();
		}
		return nullptr;
	}

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);
		if (ret == nullptr)
		{
			return false;
		}
		else
		{
			ret->_state = DELETE;
			return true;
		}
	}

private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0;	//哈希表中有效数据个数
};

2.3.2 开散列(拉链法)

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

        开散列的实现逻辑与闭散列基本一致,唯一需要注意的是开散列的插入不要完全用现代写法,否则在表中数据量较大的情况下,new节点和delete节点的消耗巨大,不如直接把原表的节点直接拿到新表中去,拿完的时候原表也空了,这时再交换,完全省去了new和delete节点的消耗。

完整代码
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};
	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& key)
		{
			size_t n = 0;
			for (auto e : key)
			{
				n += e;
			}
			return n;
		}
	};

	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 Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_tables.resize(10, nullptr);
		}
		~HashTable()
		{
			//依次把每个桶置空
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur != nullptr)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			//不接收冗余
			if (Find(kv.first))
				return false;

			Hash hs;
			//负载因子==1 时扩容
			if (_n  == _tables.size())
			{
				vector<Node*> newtables(_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						//将旧表中的节点直接拿到新表中去
						size_t hashi = hs(cur->_kv.first) % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newtables);
			}
			
			size_t hashi = hs(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 hs;
			size_t hashi = hs(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 hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next; 					
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

	private:
		vector<Node*> _tables;
		size_t _n = 0;	//表中存储数据个数
	};

3. 其他接口

        剩余这些与set和map有区别的接口其实就是针对哈希桶和哈希结构本身的一些功能。

        bucket_count顾名思义就是返回该哈希结构中桶的个数,bucket_size是给它一个关键码,它返回关键码所在桶的长度,bucket是给一个关键码,所在的桶是第几号桶

        load_factor返回哈希结构当前的载荷因子,max_load_factor返回设置的或者说最大的载荷因子,剩下两个是控制哈希结构的空间大小用的,如果在使用哈希结构之前我们知道大概需要多大空间,就可以使用这两个接口先把空间开好,避免后期扩容消耗。

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

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

相关文章

【C++】类和对象——Lesson2

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C &#x1f680;本系列文章为个人学习笔记…

最全架构学习路线图,海量大厂架构案例

很多读者经常抱怨&#xff0c;工作中涉及不到太多架构设计&#xff0c;对于架构的理解少之又少。 零散地做过一些架构工作&#xff0c;但完全不知道架构设计的全流程是怎样的。 想要成长为架构师&#xff0c;缺乏系统的方法论指导。 无论是程序员&#xff0c;还是产品经理&a…

数字图像边缘曲率计算及特殊点检测

一、曲率和数字图像边缘曲率检测常用方法简介 边缘曲率作为图像边缘特征的重要参数&#xff0c;不仅反映了边缘的几何形状信息&#xff0c;还对于图像识别、图像分割、目标跟踪等任务具有显著影响。 曲线的曲率&#xff08;curvature&#xff09;就是针对曲线上某个点的切线方向…

只有4%知道的Linux,看了你也能上手Ubuntu桌面系统,Ubuntu简易设置,源更新,root密码,远程服务...

创作不易 只因热爱!! 热衷分享&#xff0c;一起成长! “你的鼓励就是我努力付出的动力” 最近常提的一句话&#xff0c;那就是“但行好事&#xff0c;莫问前程"! 与辉同行的董工说​&#xff1a;​守正出奇。坚持分享&#xff0c;坚持付出&#xff0c;坚持奉献&#xff0c…

患者特征对AI算法在解释阴性筛查数字乳腺断层摄影研究中的表现的影响| 文献速递-AI辅助的放射影像疾病诊断

Title 题目 Patient Characteristics Impact Performance of AI Algorithm in Interpreting Negative Screening Digital Breast Tomosynthesis Studies 患者特征对AI算法在解释阴性筛查数字乳腺断层摄影研究中的表现的影响 Background 背景 Artificial intelligence (AI)…

什么是云边协同?

当今信息技术高速发展的时代&#xff0c;"云边协同"&#xff08;Edge Cloud Collaboration&#xff09;已经成为一个备受关注的话题。它涉及到云计算和边缘计算的结合&#xff0c;为数据处理、存储和应用提供了全新的可能性。本文将介绍云边协同的概念、优势以及在不…

Learning vtkjs之LookUpTable

颜色映射表 LookUpTable 介绍 先看官方的介绍&#xff1a; vtkLookupTable is a 2D widget for manipulating a marker prop vtkLookupTable 是一个用于操纵标记属性的2维的小部件。 一般可以用来进行颜色刻度的显示。它会帮我们进行颜色线性插值计算 代码效果 其实设置一个…

C++必修:STL之vector的了解与使用

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C学习 贝蒂的主页&#xff1a;Betty’s blog 1. C/C中的数组 1.1. C语言中的数组 在 C 语言中&#xff0c;数组是一组相同类型…

顺序消费rocketMQ(FIFO先进先出)和小技巧 取模运算的周期性特征来将数据分组。

20240801 一、顺序消费MQ&#xff08;FIFO先进先出&#xff09;介绍 二、一个小技巧&#xff0c;对于取模运算&#xff0c;用来在几以前进行随机选取&#xff0c;取模运算的周期性特征来将数据分组&#xff0c;使用场景对于取模会重复问题 一、顺序消费MQ&#xff08;FIFO先进先…

openeuler下载docker

https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/rhel/9/x86_64/stable/ #清华的网址 https://mirrors.aliyun.com/docker-ce/linux/rhel/9/x86_64/stable/ #阿里云的网址 开始配置 vim /etc/yum.repos.d/docker-ce.repo #写仓库&#xff0c;我这里…

【初阶数据结构篇】归并排序和计数排序(总结篇)

文章目录 归并排序和计数排序前言代码位置归并排序计数排序排序性能比较排序算法复杂度及稳定性分析 归并排序和计数排序 前言 本篇以排升序为例 代码位置 gitee 前篇&#xff1a;【初阶数据结构篇】冒泡排序和快速排序 中篇&#xff1a;【初阶数据结构篇】插入、希尔、选择…

【Qt】QDateTimeEdit

在Qt中&#xff0c;QDateEdit是用于选择日期的微调框&#xff0c;QTimeEdit是用于选择小时和分钟的微调框 QDateTimeEdit则是基于QDateEdit和QTimeEdit的组合控件&#xff0c;能够同时显示日期和时间&#xff0c;并允许用户以交互方式编辑日期 常用属性 属性说明dateTime时间…

electron-updater实现electron全量更新和增量更新——渲染进程UI部分

同学们可以私信我加入学习群&#xff01; 正文开始 前言更新功能所有文章汇总一、两个同心球效果实现二、球内进度条、logo、粒子元素实现2.1 球内包含几个元素&#xff1a;2.2 随机粒子生成方法generateRandomPoint2.3 创建多个粒子的方法createParticle 三、gsap创建路径动画…

基于python的百度迁徙迁入、迁出数据分析(六)

书接上回&#xff0c;苏州市我选取了2024年5月1日——5月5日迁入、迁出城市前20名并求了均值&#xff0c;从数据中可以看出苏州市与上海市的关系还是很铁的&#xff0c;都互为对方的迁入、迁出的首选且迁徙比例也接近4分之一&#xff0c;名副其实的老铁了&#xff1b; 迁出城市…

Seurat-SCTransform与harmony整合学习续(亚群分析)

目录 提取细胞亚群 SCTransform-harmony技术路线 ①亚群SCTransform标准化 ②harmony去批次 这里对上一章的内容进行补充&#xff1a; Seurat-SCTransform与harmony整合学习-CSDN博客 提取细胞亚群 rm(list ls()) library(Seurat)#好像先后需要先后加载 library(patchw…

【Jenkins】在linux上通过Jenkins编译gitee项目

因项目需求近期在linux服务器上部署了Jenkins来自动编译gitee上的项目源码&#xff0c;期间踩到了一些坑&#xff0c;花费了不少时间来处理&#xff0c;特此记录。 所需资源下载列表&#xff1a; Jenkins &#xff1a;https://mirrors.tuna.tsinghua.edu.cn/jenkins/war/2.46…

文件系统 --- 重定向,缓冲区

序言 本篇文章的内容和上一篇文章 &#x1f449;点击查看 紧密相连&#xff0c;所以为了更好的理解本篇文章&#xff0c;需要大家将前置知识准备好哦&#x1f607;。  本文主要向大家介绍文件的重定向&#xff0c;以及基于用户级别的缓冲区和基于操作系统级别的缓冲区。原来看…

AI技术和大模型对人才市场的影响

012024 AI技术和大模型 2024年AI技术和大模型呈现出多元化和深入融合的趋势&#xff0c;以下是一些关键的技术方向和特点&#xff1a; 1. 生成式AI 生成式AI&#xff08;Generative AI&#xff09;在2024年继续快速发展&#xff0c;它能够创造全新的内容&#xff0c;而不仅仅…

Redis——有序集合

目录 1. 添加元素 ZADD 2. 查看全部元素 ZRANGE 3. 查看某个元素的分数 ZSCORE 4. 查看元素的排名 ZRANK SortedSet 也叫 ZSet ,即有序集合&#xff0c; 有序集合与集合的区别&#xff1a; 有序集合的每个元素都会关联一个浮点类型的分数&#xff0c;依赖该分数的的大小对…