【C++】哈希容器

news2024/9/21 1:32:40

unordered系列关联式容器

        在之前的博文中介绍过关联式容器中的map与set,同map与set一样,unordered_set与unordered_set也是关联式容器。

        在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,查询效率可以达到logN;在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器的使用方法类似,但是其底层的实现不同。

        unordered_map与unordered_set的底层实现都是哈希表。unordered关联容器与其他关联容器的区别是:

  • unordered是单项迭代器
  • unordered遍历出来的数据不是有序的
  • unordered_set只能去重。不可以用于排序
  • unordered_map也不可以用于排序
  • 无序数据可以使用unordered关联容器具有优势,有序数据使用其他容器较快。

unordered_map的介绍

unordered_map的文档介绍

std::unordered_map

  1. unordered_map是存储< key,value >键值对的关联式容器,其允许通过keys快速的索引到与其对应的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. unordered_map的迭代器至少是前向迭代器。

unordered_map的接口说明

unordered_map的构造 

函数说明功能介绍
unordered_map构造不同格式的unordered_map的对象

unordered_map的容量

函数说明功能介绍
bool empty() const noexcept;
检测unordered_map是否为空
size_type size() const noexcept;
获取unordered_map的有效元素个数

unordered_map的迭代器

函数说明功能介绍
begin返回unordered_map第一个元素的迭代器
end返回unordered_map最后一个元素下一个位置的迭代器
rbegin返回unordered_map第一个元素的const迭代器
rend返回unordered_map最后一个元素下一个位置的const迭代器

unordered_map的元素访问

函数说明功能介绍
operator[]返回与key对应的value,没有一个默认值

【注意】:该函数实际上调用了哈希桶的插入操作,用参数key与value构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回value;插入失败,说明key已经在哈希桶值,将key对应的value返回。

unordered_map的查询

函数说明功能介绍
iterator find ( const key_type& k );
返回key在哈希桶中的位置
size_type count ( const key_type& k ) const;
返回哈希桶中关键值为key的键值对的个数

【注意】:unordered_map中的key是不能重复的,因此count函数的返回值最大为1 

unordered_map的修改操作

函数说明功能介绍
insert向容器中插入键值对
erase删除容器中的键值对
clear清空容器中有效元素的个数
swap交换俩个容器中的元素

unordered_map桶操作

函数说明功能介绍

bucket_count

返回哈希桶中的桶的总数

bucket_size

返回n号桶中有效元素的个数

bucket

返回元素key所在的桶号

哈希底层结构

哈希概念

        哈希(散列):存储的值跟存储位置建立出一个对应关系。

顺序容器以及平衡树中,元素关键码与其存储位置之间没有对应关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中的元素的比较次数。

下面介绍一种方法:哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)或者散列表。

该方法可以不经过任何比较,一次直接从表中得到想要的元素,通过某种函数使元素的存储位置与其关键码之间能够建立一一映射的关系,在查找的时候通过该函数可以很快找到该元素。

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

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

哈希设计介绍

哈希函数设计原则:

  • 哈希函数的定义域必须包括存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中。

常见哈希函数:

1.直接定址法

取关键字的某个线性函数为散列地址:Hash(key) = A*key + B;

【优点】:简单,均匀

【缺点】:需要事先知道关键字的分布情况

【使用场景】:适合查找范围小,数据集中的情况。

2.除留余数法

设散列表中允许的地址数为m,取一个不大于m,但是最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key)=key%p(p<=m),将关键码转换成哈希地址。



哈希冲突

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

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

闭散列

闭散列:也叫开放地址法,当发生哈希冲突的时候,如果哈希表未被填满,说明了在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。

1.线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置。

插入方式:

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

删除方式:

  • 采用闭散列处理哈希冲突的时候,不能随便物理删除哈希表中已经存在的元素,这样可能会导致影响其他元素的搜索。
  • 可以采用标记的伪删除的方式来删除一个元素
	enum STATE
	{
		EXIST,
		EMPTY,
		DELETE
	};

 哈希表的扩容

如果使用开放地址法,可以利用载荷因子:

散列表的载荷因子:a = 填入表中的元素个数 / 散列表的长度

a是散列表装满程度的标志因子,由于散列表的长度是定值,a与“填入表中的元素个数”成正比。

  • 负载因子越大,冲突概率越大,空间利用率更高
  • 负载因子越小,冲突概率越小,空间利用率更低(空间浪费越多)

哈希表不能满了再扩容,控制负载因子再0.7-0.8之间。如果超过0.8,查表时的CPU缓存不命中,按照指数曲线上升。

在扩容之后映射关系需要改变,重新映射,这样也会导致哈希冲突的变化。

线性探测的实现

#include<iostream>
#include<string>

using namespace std;

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;
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);
		}

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

			// 扩容
			//if ((double)_n / (double)_table.size() >= 0.7)
			if (_n * 10 / _table.size() >= 7)
			{
				size_t newSize = _table.size() * 2;
				// 遍历旧表,重新映射到新表
				HashTable<K, V, HashFunc> newHT;
				newHT._table.resize(newSize);

				// 遍历旧表的数据插入到新表即可
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state == EXIST)
					{
						newHT.Insert(_table[i]._kv);
					}
				}

				_table.swap(newHT._table);
			}

			// 线性探测
			HashFunc hf;
			size_t hashi = hf(kv.first) % _table.size();
			while (_table[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _table.size();
			}

			_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)
			{
				if (_table[hashi]._state == EXIST
					&& _table[hashi]._kv.first == key)
				{
					return (HashData<const K, V>*) & _table[hashi];
				}

				++hashi;
				hashi %= _table.size();
			}

			return nullptr;
		}

		// 按需编译
		bool Erase(const K& key)
		{
			HashData<const K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;

				return true;
			}

			return false;
		}

	private:
		vector<HashData<K, V>> _table;
		size_t _n = 0; // 存储有效数据的个数
	};
}

【线性探测的优点】:实现简单

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

2.二次探测

线性探测的缺点是产生冲突之后,会将数据堆积在一起,这与其找下一个空位置有关系,查找数据需要逐个进行查找,可以使用二次探测解决问题。

二次探测:

找下一个空位置的方法:

hashi = key % n

hashi + i ^2(i为移动的位置)

i>=0

【研究表明】:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5如果超出必须考虑增容。

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

开散列

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

【说明】开散列使用的是哈希桶,如果不扩容,就会不断插入,某些桶就会越来越长,效率得不到保障,负载因子适当放大,一般负载因子控制到1,平均下来,每一个桶一个数据。

开散列的实现

// 泛型编程:不是针对某种具体类型,针对广泛的类型(两种及以上) -- 模板
namespace hash_bucket
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
	};

	// 前置声明
	template<class K, class T, class KeyOfT, class HashFunc>
	class HashTable;

	template<class K, class T, class KeyOfT, class HashFunc>
	struct HTIterator
	{
		typedef HashNode<T> Node;
		typedef HTIterator<K, T, KeyOfT, HashFunc> Self;

		Node* _node;
		HashTable<K, T, KeyOfT, HashFunc>* _pht;

		HTIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht)
			:_node(node)
			, _pht(pht)
		{}

		T& operator*()
		{
			return _node->_data;
		}

		T* operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			if (_node->_next)
			{
				// 当前桶还没完
				_node = _node->_next;
			}
			else
			{
				KeyOfT kot;
				HashFunc hf;
				size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
				// 从下一个位置查找查找下一个不为空的桶
				++hashi;
				while (hashi < _pht->_table.size())
				{
					if (_pht->_table[hashi])
					{
						_node = _pht->_table[hashi];
						return *this;
					}
					else
					{
						++hashi;
					}
				}

				_node = nullptr;
			}

			return *this;
		}

		bool operator!=(const Self& s)
		{
			return _node != s._node;
		}
	};

	// set -> hash_bucket::HashTable<K, K> _ht;
	// map -> hash_bucket::HashTable<K, pair<K, V>> _ht;
	template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		typedef HashNode<T> Node;

		// 友元声明
		template<class K, class T, class KeyOfT, class HashFunc>
		friend struct HTIterator;
	public:
		typedef HTIterator<K, T, KeyOfT, HashFunc> iterator;

		iterator begin()
		{
			// 找第一个桶
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				if (cur)
				{
					return iterator(cur, this);
				}
			}

			return iterator(nullptr, this);
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}

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

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

				_table[i] = nullptr;
			}
		}

		bool Insert(const T& data)
		{
			KeyOfT kot;

			if (Find(kot(data)))
			{
				return false;
			}

			HashFunc hf;

			// 负载因子到1就扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);

				// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插到新表
						size_t hashi = hf(kot(cur->_data)) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

					_table[i] = nullptr;
				}

				_table.swap(newTable);
			}

			size_t hashi = hf(kot(data)) % _table.size();
			// 头插
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

		Node* Find(const K& key)
		{
			HashFunc hf;
			KeyOfT kot;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return cur;
				}

				cur = cur->_next;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashFunc hf;
			KeyOfT kot;
			size_t hashi = hf(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					--_n;
					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)
				{
					cout << cur->_kv.first << ":" << cur->_kv.second << "->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
			cout << endl;
		}

	private:
		vector<Node*> _table; // 指针数组
		size_t _n = 0; // 存储了多少个有效数据
	};
}

开散列与闭散列的比较

优先使用开散链的方法。

虽然链地址的方法处理溢出,需要增设链接指针,但是开地址法必须保持大量的空闲空间以确保搜索效率,而表项所占的空间比指针大,所以使用链地址法比开地址法节省空间。

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

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

相关文章

详解HTTP代理与SOCKS代理之间的差异

代理服务器在网络安全和隐私保护方面&#xff0c;发挥着十分重要的作用。其中&#xff0c;HTTP代理和SOCKS代理是两种常见的代理方式&#xff0c;它们在原理、功能和应用场景上有着明显的差异。本文将为大家深入分析这两种代理之间的区别&#xff0c;帮助大家更好地选择适合自己…

Linux中如何添加磁盘分区

在Linux中添加分区通常涉及到几个步骤&#xff0c;包括识别磁盘、创建分区、格式化分区&#xff0c;以及挂载或将其用作特定的文件系统类型&#xff08;如LVM、RAID等&#xff09;。以下是一个基本的步骤指南&#xff0c;假设你正在使用命令行界面&#xff08;CLI&#xff09;和…

《技术人求职之道》之简历优化篇(下):技能与项目亮点,如何让你的简历熠熠生辉

摘要 本文将深入探讨技术求职者在撰写简历时的关键要素,包括专业技能的表达和项目经验的描述。文章首先提出专业技能描述的六条基本原则,包括统一技术词汇、标点符号一致性、技术关键字的驼峰命名法、技术分类、技术热度和掌握度排序以及慎用“精通”。接着,指导读者如何美…

FFmpeg Windows安装教程

一. 下载ffmpeg 进入Download FFmpeg网址&#xff0c;点击下载windows版ffmpeg。 下载第一个essentials版本就行。 二. 环境配置 上面源码解压后如下 将bin添加到系统环境变量 验证安装是否成功&#xff0c;输入ffmpeg –version&#xff0c;显示版本即为安装成功。

必应Bing国内广告开户、投放流程和避坑攻略!

必应Bing作为微软旗下的搜索引擎&#xff0c;不仅在全球范围内拥有庞大的用户群体&#xff0c;在中国也有着稳定的市场份额。为了让企业更好地利用必应Bing在国内的广告资源&#xff0c;云衔科技提供了全面的广告开户及代运营服务&#xff0c;帮助企业轻松驾驭数字化营销之路。…

c语言指针3

文章目录 前言一、数组名的理解1.数组名正常情况是首元素的地址2.数组名不是首元素地址的情况2. 1 sizeof(arr)中的数组名2. 2 &arr中的arr代表整个数组 3. 结论 二、使用指针访问数组1.使用指针输入输出数组中的数 三、一维数组传参的本质四、冒泡排序五、二级指针5.1 二级…

betterZip免费版怎么下载 如何安装下载和激活BetterZip教程 BetterZip注册码密钥

BetterZip是一款功能齐全且对用户友好的Mac系统解压缩工具&#xff0c;它具备压缩文件及文件夹&#xff0c;解压压缩包&#xff0c;在线预览和编辑压缩包内文件等一系列功能。此外&#xff0c;BetterZip还有简洁的界面和操作&#xff0c;可以通过拖拽或右键菜单来压缩或解压文件…

深度体验AI计算平台:超算互联网模型服务与加速卡

目录 前言 AI算力性能体验 1、注册/登录 2、购买服务 3、运行的过程记录 4、运行效果 5、运行结果反馈 6、体验总结 番外篇&#xff1a;主流推荐 1、算法模型推荐 2、开源项目推荐 3、数据集推荐 结束语 前言 在人工智能的浪潮中&#xff0c;AI计算平台已成为研究…

系统移植(十一)根文件系统(未整理)

文章目录 一、根文件系统中各个目录文件功能解析&#xff1a;二、对busybox进行配置和编译&#xff08;一&#xff09;执行make help命令获取make的帮助信息&#xff08;二&#xff09;对busybox源码进行配置&#xff0c;配置交叉编译器&#xff08;三&#xff09;执行make men…

kill 命令详解

kill命令其实比较让人难以理解的点在于信号这块&#xff0c;开发中kill -9经常用&#xff0c;但却很少去深入了解其他信号参数的具体作用&#xff0c;本文主要是就信号这块做一个解释。 实验代码 public static void main(String[] args) {Runtime.getRuntime().addShutdownHoo…

文件系统 --- 文件结构体,文件fd以及文件描述符表

序言 在编程的世界里&#xff0c;文件操作是不可或缺的一部分。无论是数据的持久化存储、日志记录&#xff0c;还是简单的文本编辑&#xff0c;文件都扮演着至关重要的角色。然而&#xff0c;当我们通过编程语言如 C、Java 等轻松地进行文件读写时&#xff0c;背后隐藏的复杂机…

C#面向对象补全计划 之 画UML类图(持续更新)

本文仅作学习笔记与交流&#xff0c;不作任何商业用途&#xff0c;作者能力有限&#xff0c;如有不足还请斧正 本系列旨在通过补全学习之后&#xff0c;给出任意类图都能实现并做到逻辑上严丝合缝 学会这套规则&#xff0c;并看完面向对象补全计划文章之后&#xff0c;可以尝试…

Ubuntu20.04安装Angular CLI

一、更换apt-get源 使用原来的apt-get源有几个包报错&#xff0c;下不下来 更换阿里源&#xff08;阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区&#xff09;&#xff0c;使用网站中的内容&#xff0c;在 apt-get update 时总是报错 改用清华源&#xff1a; deb http:/…

多种方式防止表单重复提交

1.前端方案&#xff1a; 通过js将按钮绑定一个方法&#xff0c;点击后3s内将按钮设置成不可用&#xff0c;或者隐藏。 缺点&#xff1a;绕过前端&#xff0c;例如通过postman发请求。 2.hashmap版&#xff1a; 请求携带一个参数&#xff0c;将请求携带的参数&#xff08;可…

牛客JS题(十四)类继承

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; 类的基本使用构造函数实现类原型链的使用 题干&#xff1a; 我的答案 <!DOCTYPE html> <html><head><meta charset"utf-8" /></head><body><script type"tex…

这两个大龄程序员,打算搞垮一个世界软件巨头!

大家都知道&#xff0c;Adobe是多媒体和数字内容创作者的绝对王者&#xff0c;它的旗下有众多大家耳熟能详的软件&#xff1a;Photoshop、Illustrator、Premiere Pro、After Effects、InDegign、Acrobat、Animate等等。 这些软件使用门槛很高&#xff0c;价格昂贵&#xff0c;安…

安装 Terraform for Tencent 使用

第一步 &#xff1a;下载安装包 前往 Terraform 官网&#xff0c;使用命令行直接安装 Terraform 或下载二进制安装文件。 解压并配置全局路径 Linux/MAC&#xff1a;export PATH$PATH:${可执行文件所在目录} 例如&#xff1a;export PATH$PATH:$/usr/bin/terraform Win…

Vulnhub靶机-Jangow 1.0.1

Vulnhub靶机-Jangow 1.0.1 修改为NAT模式 ?buscarecho <?php eval($_POST[cmd])?> >shell.php后面试了试很多网上的方法反弹shell但都不行

LeetCode面试150——122买卖股票的最佳时机II

题目难度&#xff1a;中等 默认优化目标&#xff1a;最小化平均时间复杂度。 Python默认为Python3。 目录 1 题目描述 2 题目解析 3 算法原理及题目解析 3.1 动态规划 3.2 贪心算法 参考文献 1 题目描述 给你一个整数数组 prices &#xff0c;其中 prices[i] 表示某支…