【c++】哈希---unordered容器+闭散列+开散列

news2024/11/25 22:36:19

1.unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

unordered_set:

1.  unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。
2.  在unordered_set中,元素的值同时也是唯一地标识它的key。
3.  在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找

4.  到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。
5.  unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。
6.  它的迭代器至少是前向迭代器。

 

 部分接口的使用:

#include <iostream>
#include <unordered_set>
using namespace std;

int main()
{
	unordered_set<int> us;
	//插入元素(去重)
	us.insert(1);
	us.insert(4);
	us.insert(3);
	us.insert(3);
	us.insert(2);
	us.insert(2);
	us.insert(3);
	
	for (auto e : us)
	{
		cout << e << " ";
	}
	cout << endl; 
}

 unordered_map

1.  unordered_map是存储<key, value>键值对的关联式容器,其允许通过key值快速的索引到与其对应是value。
2.  在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
3.  在内部,unordered_map没有对<key, value>按照任何特定的顺序排序,为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4.  unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5.  unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
6.  它的迭代器至少是前向迭代器。

代码练习:

int main()
{
	unordered_map<string, int> countMap;
	string arr[] = { "苹果","香蕉","苹果" ,"西瓜","西瓜"};
	for (auto& e : arr)
	{
		auto it = countMap.find(e);
		countMap[e]++;
	}
	for (auto& kv : countMap)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	return 0;
}

unordered_map容器当中还实现了[ ]运算符重载函数,该重载函数的功能非常强大:[key]

若当前容器中已有键值为key的键值对,则返回该键值对value的引用。
若当前容器中没有键值为key的键值对,则先插入键值对<key, value()>,然后再返回该键值对中value的引用。


2.哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
哈希映射:key值跟存储位置建立关联关系。

哈希冲突:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 。

哈希函数:
1. 直接定址法
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2. 除留余数法
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,
按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址

3.解决哈希冲突

解决哈希冲突两种常见的方法是:闭散列开散列。

1.闭散列——开放定址法

插入数据

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。常用的寻找下一个位置的方法是线性探测法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

删除:

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索,线性探测采用标记的伪删除法来删除一个元素

扩容:

在有限的空间内,随着我们插入的数据越来越多,冲突的概率也越来越大,查找效率越来越低,所以闭散列的冲突表不可能让它满了,所以引入了负载因子:负载因子/载荷因子:等于表中的有效数据个数/表的大小,衡量表的满程度,在闭散列中负载因子不可能超过1(1代表满了)。一般情况下,负载因子一般在0.7左右。负载因子越小,冲突概率也越小,但是消耗的空间越大,负载因子越大,冲突概率越大,空间的利用率越高。当负载因子大于0.7的时候就需要进行扩容了:扩容不能进行直接拷贝,空间的大小发生变化映射的位置也会随着改变,所以需要重新计算映射的位置。

仿函数:考虑到统计出现次数:因为字符串不能够取模,所以我们可以给HashTable增加一个仿函数Hash,其可以将不能取模的类型转成可以取模的类型,同时把string特化出来解决字符串不能取模的问题

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

代码实现

#include <vector>
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 hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		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 Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashData<K, V> Data;
	public:
		HashTable()
			:_n(0)
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))   //不允许有相同数据的元素插入
				return false;
			if (_n * 10 / _tables.size() >= 7)    //负载因子 到达比例就开始扩容
			{
				HashTable<K, V, Hash> newHT;  
				newHT._tables.resize(_tables.size() * 2);
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHT.Insert(e._kv);
					}
				}
				_tables.swap(newHT._tables);
			}
			Hash hf;
			size_t hashi = hf(kv.first)% _tables.size();  //求出原本该在的位置
			while (_tables[hashi]._state == EXIST)  //开始寻找没有被占用的位置
			{
				++hashi;
				hashi %= _tables.size();   //始终保持在vector内
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;
			return true;
		}

		Data* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			size_t starti = hashi;
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state == EXIST && key == _tables[hashi]._kv.first)
				{
					return &_tables[hashi];
				}
				++hashi;
				hashi %= _tables.size();
				//如果数据不存在就会导致一直循环的找,最多一圈
				if (hashi == starti)
				{
					break;
				}
			}
			return nullptr;
		}

		bool Erase(const K& key)  //伪删除
		{
			Data* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		vector<Data> _tables;   
		size_t _n = 0;   //有效数据的个数
	};

	

2.开散列——开链法

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

 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。由于桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容。开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突。所以在元素个数刚好等于桶的个数时,可以给哈希表增容 。研究分析表明:素数作为哈希表的长度可以尽可能减小哈希冲突。所以可提前定义一个素数表。

代码实现:

#include <vector>
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 hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};
namespace buckethash
{
	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()
			:_n(0)
		{
			_tables.resize(__stl_next_prime(0));
		}

		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}

				_tables[i] = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			if (_tables.size() == _n)
			{
                
                //相当于是将节点从旧的哈希桶中搬到新的哈希桶中
				vector<Node*> newTables;
				newTables.resize(__stl_next_prime(_tables.size()), nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = Hash()(cur->_kv.first) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}

			size_t hashi = Hash()(kv.first) % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

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

		bool Erase(const K& key)
		{
			size_t hashi = Hash()(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (cur == _tables[hashi])
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}
		inline unsigned long __stl_next_prime(unsigned long n)
		{
			static const int __stl_num_primes = 28;
			static const unsigned long __stl_prime_list[__stl_num_primes] =
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};

			for (int i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return __stl_prime_list[__stl_num_primes - 1];
		}

	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};
	
}

3.开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销 由于开地址法必须保持大量的空闲空间以确保搜索效率,如线性探查法要求负载因子 <= 0.7 ,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。


 

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

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

相关文章

FPGA开发基本流程详解

FPGA是一种可编程逻辑器件&#xff0c;与传统的硬连线电路不同&#xff0c;它具有高度的可编程性和灵活性。FPGA的设计方法包括硬件设计和软件设计两部分&#xff0c;硬件设计包括FPGA芯片电路、存储器、输入输出接口电路等等&#xff0c;软件设计则是HDL程序开发&#xff0c;以…

openCV 第四篇 角点检测、图像特征、图片拼接

本文原本打算直接简单介绍一下harris和sift&#xff0c;之后进行特征匹配&#xff0c;来一波图像拼接。 想来想去还是先介绍下原理吧&#xff0c;虽然没人看QAQ。可以直接点击右侧目录跳转到代码区。 本文可以完成&#xff1a; 角点检测 和 图像特征提取(就几行代码) 以及进…

Win32子窗口创建,子窗口回调函数,消息堆栈,逆向定位子窗口消息处理过程

本专栏上一篇文章中我们讲解了Win32程序入口识别&#xff0c;定位回调函数&#xff0c;具体事件处理的定位&#xff0c;这一章节中我们来讲解一下子窗口的创建&#xff0c;子窗口的回调函数&#xff0c;并且逆向分析子窗口消息处理过程。 文章目录 一.子窗口按钮的创建- 创建子…

charge pump的分析与应用

春节前最后一更&#xff0c;提前祝大家新春快乐&#xff0c;阖家安康&#xff0c;工作顺利&#xff01; 定义&#xff1a; 电荷泵是利用电容的充放电来实现电压的转换的&#xff0c;输入回路和输出回路轮流导通。通过调节占空比来调节输出电压。 它们能使输入电压升高或降低&…

基于PyQt5连接本地SQLite实现简单人力资源管理系统

人力资源管理系统 使用环境&#xff1a;Python3.86 PyQt5.15.4 sqlite3 记录一下最近学校举办的一个程序设计比赛&#xff0c;题目是实现一个简单的人力资源管理系统&#xff0c;文末有效果展示 我认为程序是面向人类而不是面向机器的&#xff0c;所以我使用了PyQt5封装了一…

SpringCloud源码分析 (Eureka-Server-处理客户端续约请求) (七)

文章目录 1.处理客户端续约请求1.1 InstanceResource.renewLease()1.2 InstanceRegistry.renew()1.3 PeerAwareInstanceRegistryImpl.renew()1.4 AbstractInstanceRegistry.renew()1.6 PeerAwareInstanceRegistryImpl.replicateToPeers()1.7 PeerEurekaNode.headbeat() 1.处理客…

大数据Doris(二十二):Rollup物化索引创建与操作

文章目录 Rollup物化索引创建与操作 一、创建测试表 二、创建Rollup物化索引表

岗位少,竞争激烈,这是今年软件测试就业的真实写照,也是所有岗位的真实写照。

前两天跟一个HR朋友聊天&#xff0c;她表示刚在boss上发布了一个普通测试岗位&#xff0c;不到一小时竟然收到了几百份简历。而且简历质量极高&#xff0c;这是往年不敢想象的。岗位少&#xff0c;竞争激烈&#xff0c;这是今年软件测试就业的真实写照&#xff0c;也是所有岗位…

若依框架快速开发项目(避坑超详细)

若依框架快速开发项目&#xff08;避坑超详细&#xff09; 初衷&#xff1a; 若依框架使用及其普遍&#xff0c;是一个非常优秀的开源框架&#xff0c;框架本身的权限系统&#xff0c;字典设置以及相关封装&#xff0c;安全拦截相当完善&#xff0c;本人受益匪浅&#xff0c;学…

Python进阶实际应用开发实战(一)

目录 原型设计和环境环境设置创建新项目 原型设计和环境 原书第一章内容 环境设置 对于一个项目我们需要安装库并管理依赖项&#xff0c;这意味着需要有一个虚拟环境。我们使用pipenv来指定依赖项。 python -m pip install --user pipenv在命令行中启动Python脚本的时候&am…

分布式锁概念

什么是分布式锁 方案一&#xff1a;SETNX EXPIRE 方案二&#xff1a;SETNX value值是&#xff08;系统时间过期时间&#xff09; 方案三&#xff1a;使用Lua脚本(包含SETNX EXPIRE两条指令) 方案四&#xff1a;SET的扩展命令&#xff08;SET EX PX NX&#xff09; 方案五…

chatgpt赋能Python-aidlearning安装pycharm

Aid Learning: 如何安装PyCharm PyCharm是一款由JetBrains开发的用于Python编程的集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的编辑器和调试工具&#xff0c;可以帮助开发者更高效地编写Python代码。本文将介绍如何安装PyCharm。 安装前准备工作 在安装PyCharm…

vue diff算法与虚拟dom知识整理(8) 手写patch实现简易版的节点第一次上dom树

上一文 我们整理了一下 patch 函数的整体过程 我不知道大家有没有保留我们之前学手写H函数时的那个案例 我们要将index.jsi还原成这样 参考代码如下 import h from "./snabbdom/h";const dom1 h("div",{props: {class: "dom"} },"文本测试…

Qt QJson 使用

文章目录 1. 简介2. 简单实例3. 结果 1. 简介 QJson 是一个用于 Qt 应用程序的 JSON 解析和生成库。JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;具有良好的可读性和可扩展性&#xff0c;常用于 Web 应用程序中。QJson 将 …

Python通过natcap.invest库调用InVEST模型批处理数据(Carbon Storage and Sequestration模块)

InVEST&#xff08;Integrated Valuation of Ecosystem Servicesand Tradeoffs&#xff09;生态系统服务和权衡的综合评估模型&#xff0c;旨在通过模拟不同土地覆被情景下生态系统物质量和价值量的变化。它提供了多种生态系统服务功能评估&#xff0c;包括了淡水生态系统评估、…

Liunx压缩命令 - gz

gz命令详解及使用 Linux中gz命令的全称是gzip&#xff0c;它是一种常用的压缩程序&#xff0c;可将文件或目录压缩为.gz格式&#xff0c;以节省存储空间&#xff0c;同时也可以通过解压缩操作重新获取原始文件掌握 gzip 命令&#xff0c;可以帮助我们更加高效地进行文件压缩与…

视频编码测试平台CodecWar

​在视频编码的研究和开发中&#xff0c;我们经常需要对编码器的性能进行比较&#xff0c;一般的做法是构建一个数据集&#xff0c;然后将两个编码器在同一个配置上&#xff08;LD,RA,AI等&#xff09;使用不同参数&#xff08;CQP下一般为4个QP值&#xff09;编码&#xff0c;…

Py之pymc:pymc的简介、安装、使用方法之详细攻略

Py之pymc&#xff1a;pymc的简介、安装、使用方法之详细攻略 目录 pymc的简介 pymc的安装 pymc的使用方法 1、时序性任务 (1)、使用 Euler-Maruyama 方案推断 SDE 的参数 pymc的简介 PyMC&#xff08;以前称为PyMC3&#xff09;是一个专注于高级马尔科夫链蒙特卡洛&#x…

⑤电子产品拆解分析-人体感应灯

⑤电子产品拆解分析-人体感应灯 一、功能介绍二、电路分析以及器件作用三、原理图复现与学习 一、功能介绍 ①感应人体活动亮灯20S&#xff1b;②Micro-USB进行锂电池充电&#xff1b; 二、电路分析以及器件作用 三、原理图复现与学习 R1为下拉电阻&#xff0c;防止上电因芯…