C++哈希

Posted DR5200

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++哈希相关的知识,希望对你有一定的参考价值。

文章目录

一.unordered系列关联式容器

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

unordered_map 常见接口总览

// 检测unordered_map是否为空
bool empty() const;
// 获取unordered_map的有效元素个数
size_t size() const;
// 返回unordered_map第一个元素的迭代器
iterator begin() noexcept;
// 返回unordered_map最后一个元素下一个位置的迭代器
iterator end() noexcept;
// 返回unordered_map第一个元素的const迭代器
const_iterator cbegin() const noexcept;
// 返回unordered_map最后一个元素下一个位置的const迭代器
const_iterator cend() const noexcept;
// 返回与key对应的value,没有一个默认值
mapped_type& operator[] ( const key_type& k );
// 返回key在哈希桶中的位置
iterator find ( const key_type& k );
// 返回哈希桶中关键码为key的键值对的个数
size_type count ( const key_type& k ) const;
// 向容器中插入键值对
pair<iterator,bool> insert ( const value_type& val );
// 删除容器中的键值对
iterator erase ( const_iterator position );
// 清空容器中有效元素个数
void clear() noexcept;
// 交换两个容器中的元素
void swap ( unordered_map& ump ); 

使用

#include<iostream>
#include<unordered_map>
using namespace std;
int main()

	unordered_map<int,int> um;
	um.insert(make_pair(1,1));
	um.insert(make_pair(10,10));
	um.insert(make_pair(8,8));
	um.insert(make_pair(5,5));
	um.insert(make_pair(2,2));
	
	cout<<um.empty()<<endl;
	cout<<um.size()<<endl;
	
	auto ret = um.find(10);
	if(ret != end()) cout<<ret->first<<" "<<ret->second<<endl;
	else cout<<"没找到"<<endl;
	
	for(auto it = um.begin();it != um.end();it++)
	
		cout<<it->first<<" "<<it->second<<endl;
	

unordered_set 介绍和使用

unordered_set 常见接口总览

// 检测unordered_set是否为空
bool empty() const;
// 获取unordered_set的有效元素个数
size_t size() const;
// 返回unordered_set第一个元素的迭代器
iterator begin() noexcept;
// 返回unordered_set最后一个元素下一个位置的迭代器
iterator end() noexcept;
// 返回unordered_set第一个元素的const迭代器
const_iterator cbegin() const noexcept;
// 返回unordered_set最后一个元素下一个位置的const迭代器
const_iterator cend() const noexcept;
// 返回key在哈希桶中的位置
iterator find ( const key_type& k );
// 返回哈希桶中关键码为key的键值对的个数
size_type count ( const key_type& k ) const;
// 向容器中插入键值对
pair<iterator,bool> insert ( const value_type& val );
// 删除容器中的键值对
iterator erase ( const_iterator position );
// 清空容器中有效元素个数
void clear() noexcept;
// 交换两个容器中的元素
void swap ( unordered_set& ump ); 

使用

#include<iostream>
#include<unordered_set>
using namespace std;
int main()

	unordered_set<int> us;
	us.insert(1);
	us.insert(10);
	us.insert(8);
	us.insert(5);
	us.insert(2);

	cout << us.empty() << endl;
	cout << us.size() << endl;

	auto ret = us.find(10);
	if (ret != us.end()) cout << *ret << endl;
	else cout << "没找到" << endl;

	for (auto it = us.begin(); it != us.end(); it++)
	
		cout << *it << endl;
	

二.哈希概念

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

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。(哈希函数)

常见哈希函数
直接定址法 : 该方法是取关键字的某个线性函数值为哈希地址,适用于整数且数据范围比较集中

缺陷 :
(1). 如果数据范围大,直接定址法会浪费很多空间
(2). 不能处理浮点数,字符串等等场景

优点 : 速度快( O(1) ),每一个值都对应一个唯一位置

除留余数法 : 解决数据范围很大的情况

如 1 5 10 100000 100 18 15 7这8个值,使用直接定址法,由于100000的存在浪费了很多空间,采用除留余数法把数据映射到有效的空间里面,将值模10作为地址,但这样会导致不同的值映射到同一位置(哈希冲突)

如何解决哈希冲突?
一. 闭散列(开放地址法)
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置中去
(1). 线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

线性探测缺点 : 某些连续位置出现冲突,出现踩踏效应

此时如果再插入模10为0的值,插入和查找的时间复杂度就退化成O(N)了

(2). 二次探测
二次探测为了避免由于线性探测找空位置一个一个往后找所导致的踩踏效应,找下一个空位置的方法为 : Hi = (H0 + i^2) % m

负载因子/载荷因子 = 存储的有效数据个数 / 空间的大小
负载因子越大,冲突的概率越高,增删查改的效率越低
负载因子越小,冲突的概率越低,增删查改的效率越高,但空间利用率低

一个类型去做 map/set 的 Key 有什么要求?
能支持比较大小
一个类型去做 unordered_map / unordered_set 的 Key 有什么要求?
能支持转换成整数 + 相等比较

二. 开散列(哈希桶/拉链法)

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

闭散列的开放定址法,负载因子不能超过1,建议控制在[0.0,0,7]左右
开散列的拉链法,负载因子可以超过1,建议控制在[0.0,1]左右

实际中哈希桶结构更实用
(1). 空间利用率高
(2). 极端情况下还有解决方案(数据不多,负载因子低,但是这些数据大部分冲突了),将冲突数据多的这个桶改成红黑树结构

闭散列实现
(1). 每一个节点由数据和数据的状态构成,默认数据的状态为空(EMPTY)
(2). KeyOfT仿函数用来获取data中的key值
(3). HashFunc仿函数用来将key值转换成整形以此来取模
(4). 插入时使用 key % _table.size() 来获取对应映射位置,如发生冲突采用线性探测或二次探测的方法,当负载因子大于 0.7 时,冲突的概率会较高,因此需要增容并重新计算每个数据对应的映射位置
(5). 查找时也采用线性探测或二次探测的方法,数据状态不为空就继续向后查找,找到后返回节点地址,没找到返回空
(6). 删除只需要修改节点的状态为DELETE即可

namespace CloseHash

	// 节点状态 
	enum State
	
		EMPTY,  // 空 
		EXIST,  // 存在
		DELETE, // 删除
	;
	// 节点定义
	template<class T>
	struct HashData
	
		T _data;
		State _state = EMPTY;
	;

	template<class K>
	struct Hash
	
		int operator()(const K& key)
		
			return key;
		
	;
	// 模板特化来处理字符串的情况
	template<>
	struct Hash<string>
	
		size_t operator()(const string& s)
		
			size_t val = 0;
			for (auto& e : s)
			
				val = val * 131 + e;
			
			return val;
		
	;
	
	
	template<class K, class T,class KeyOfT,class HashFunc = Hash<K>>
	class HashTable
	
	public:
		bool Insert(const T& data)
		
			HashData<T>* ret = Find(data);
			// 不能出现重复数据
			if (ret) return false;
			// 第一次插入先开空间
			if (_table.size() == 0)
			
				_table.resize(10);
			
			// 负载因子大于 0.7 开始扩容
			if ((double)_n / (double)_table.size() > 0.7)
			
				HashTable<K, T,KeyOfT,HashFunc> newHT;
				newHT._table.resize(_table.size() * 2);
				for (auto& e : _table)
				
					if (e._state == EXIST)
						newHT.Insert(e._data);
				
				_table.swap(newHT._table);
			
			HashFunc hf;
			KeyOfT kot;
			// 获取对应映射的起始位置
			size_t start = hf(kot(data)) % _table.size();
			size_t index = start;
			size_t i = 1;
			// 线性探测
			while (_table[index]._state == EXIST)
			
				index = start + i;
				index %= _table.size();
				i++;
			
			// 放入数据
			_table[index]._data = data;
			// 修改数据状态为存在
			_table[index]._state = EXIST;
			// 数据个数++
			_n++;
			return true;
		
		HashData<T>* Find(const K& key)
		
			if (_table.size() == 0)
			
				return nullptr;
			
			HashFunc hf;
			KeyOfT kot;
			size_t start = hf(key) % _table.size();
			size_t index = start;
			size_t i = 1;
			while (_table[index]._state != EMPTY)
			
				if (_table[index].kot(data) == key && _table[index]._state != DELETE) return &_table[index];

				index = start + i;
				index %= _table.size();
				i++;
			
			return nullptr;
		
		bool Erase(const K& key)
		
			HashData<T>* ret = Find(key);
			if (ret)
			
				ret->_state = DELETE;
				_n--;
				return true;
			
			else
			
				return false;
			
		
	private:
		vector<HashData<T>> _table;
		size_t _n = 0; // 存储的有效数据个数
	;

开散列实现 :
(1). 迭代器类对节点指针进行了封装,重载了++ / * / -> / != / == 运算符
(2). 插入操作将节点头插即可,当负载因子等于1时,需要将哈希桶进行扩容
(3). 删除/查找操作和单链表的删除/查找操作一致

namespace OpenHash

	template<class T>
	struct HashNode
	
		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		
		HashNode<T>* _next;
		T _data;
	;
	// 将key值转换成整形以便取模
	template<class K>
	struct Hash
	
		int operator()(const K& key)
		
			return key;
		
	;
	// 模板特化,将string转换成整形
	template<>
	struct Hash<string>
	
		size_t operator()(const string& s)
		
			int val = 0;
			for (auto& e : s)
			
				val = val * 131 + e;
			
			return val;
		
	;
	// 前置声明
	template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
	class HashTable;

	template<class K,class T,class KeyOfT,class HashFunc = Hash<K>>
	struct __HashIterator
	
		typedef HashNode<T> Node;
		typedef __HashIterator<K, T,KeyOfT,HashFunc> Self;
		typedef HashTable<K, T, KeyOfT,HashFunc> HT;

		Node* _node;
		HT* _ht;

		__HashIterator(Node* node,HT* ht)
			:_node(node)
			,_ht(ht)
		
		Self& operator++()
		
			// 遍历当前桶
			if (_node->_next)
			
				_node = _node->_next;
			
			// 当前桶遍历完毕,找下一个桶
			else
			
				HashFunc hf;
				KeyOfT kot;
				
				size_t index = hf(kot(_node->_data)) % _ht->_table.size();
				++index;

				while (index < _ht->_table.size() && _ht->_table[index] == nullptr) ++index;

				if (index == _ht->_table.size()) _node = nullptr;
				else _node = _ht->_table[index];
			
			return *this;
		
		// 返回数据的引用
		T& operator*()
		
			return _node->_data;
		
		// 返回数据的地址
		T* operator->()
		
			return &_node->_data;
		
		// 比较指针的指向是否不相等即可
		bool operator!=(const Self& s)const
		
			return _node != s._node;
		
		// 比较指针的指向是否相等即可
		bool operator==(const Self& s)const
		
			return _node == s._node;
		
	;

	template<class K, class T,class KeyOfT,class HashFunc = Hash<K>>
	class HashTable
	
	public:
		typedef HashNode<T> Node;
		typedef __HashIterator<K, T, KeyOfT, HashFunc> iterator;
		// 友元
		template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
		friend struct __HashIterator;
	public:
		// 返回哈希表中第一个节点的迭代器
		iterator begin()
		
			for (size_t i = 0; i < _table.size(); i++)
			
				if (_table[i])
					return iterator(_table[i],this);
			
			return end();
		
		iterator end()
		
			return iterator(nullptr,this);
		
		pair<iterator,bool> Insert(const T& data)
		
			// 第一次插入先扩容
			if (_table.size() == 0)
			
				_table.resize(10);
			
			KeyOfT kot;
			HashFunc hf;
			// 不能出现重复的数据
			iterator ret = Find(kot(data));
			if (ret != end())
				以上是关于C++哈希的主要内容,如果未能解决你的问题,请参考以下文章

算法小讲堂之哈希表|散列表|考研笔记

室友竟只在2021的最后一天就学会了哈希表

哈希表

数据结构之哈希表

算法哈希表的诞生(Java)

C++数据结构——哈希表