当前位置: 首页 > article >正文

C++ unordered_set、unordered_map哈希使用及其封装

目录

一、unordered_set、unordered_map的使用

unordered_set类的介绍

unordered_set和set的使⽤差异

unordered_map介绍以及差异

unordered_multimap/unordered_multiset

二、哈希表实现

1、哈希概念

1.1直接定址法:

 1.2 哈希冲突

1.3 负载因子

1.4将关键字转为整数

2、哈希函数

2.1除留余数法/除法散列法

2.2乘法散列法

2.3全域散列法

3、处理哈希冲突

3.1开放定址法

3.1.1线性探测

3.1.2二次探测

3.1.3开放定址法代码实现

3.2链地址法

3.2.1链地址法代码实现

三、哈希封装


一、unordered_set、unordered_map的使用

unordered_set类的介绍

template < class Key,                        // unordered_set::key_type/value_type
           class Hash = hash<Key>,           // unordered_set::hasher
           class Pred = equal_to<Key>,       // unordered_set::key_equal
           class Alloc = allocator<Key>      // unordered_set::allocator_type
           > class unordered_set;

1、unordered_set的声明如下,Key就是unordered_set底层关键字的类型

2、unordered_set默认要求Key⽀持转换为整形,如果不⽀持或者想按⾃⼰的需求⾛可以⾃⾏实现⽀持将Key转成整形的仿函数传给第⼆个模板参数

3、unordered_set默认要求Key⽀持⽐较相等,如果不⽀持或者想按⾃⼰的需求⾛可以⾃⾏实现⽀持将Key⽐较相等的仿函数传给第三个模板参数

4、unordered_set底层存储数据的内存是从空间配置器申请的,如果需要可以⾃⼰实现内存池,传给第四个参数。⼀般情况下,我们都不需要传后三个模板参数

5、unordered_set底层是⽤哈希桶实现,增删查平均效率是 O (1),迭代器遍历不再有序,为了跟set 区分,所以取名unordered_set。

unordered_set和set的使⽤差异

1、查看⽂档我们会发现unordered_set的⽀持增删查且跟set的使⽤⼀模⼀样

2、unordered_set和set的第⼀个差异是对key的要求不同,set要求Key⽀持⼩于⽐较,⽽unordered_set要求Key⽀持转成整形且⽀持等于⽐较

3、unordered_set和set的第⼆个差异是迭代器的差异,set的iterator是双向迭代器unordered_set 是单向迭代器,其次set底层是红⿊树,红⿊树是⼆叉搜索树,⾛中序遍历是有序的,所以set迭代 器遍历是有序+去重。⽽unordered_set底层是哈希表,迭代器遍历是⽆序+去重

4、unordered_set和set的第三个差异是性能的差异,整体⽽⾔⼤多数场景下,unordered_set的增删 查改更快⼀些,因为红⿊树增删查改效率是O(logN) ,⽽哈希表增删查平均效率是O(1) ,具体可以参看下⾯代码的演⽰的对⽐差异。

#include<unordered_set>
#include<unordered_map>
#include<set>
#include<iostream>
using namespace std;
int test_set2()
{
	const size_t N = 1000000;
	unordered_set<int> us;
	set<int> s;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(rand()); // N⽐较⼤时,重复值⽐较多
		//v.push_back(rand() + i); // 重复值相对少
		//v.push_back(i); // 没有重复,有序
	}
	size_t begin1 = clock();
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();
	cout << "set insert:" << end1 - begin1 << endl;
	size_t begin2 = clock();
	us.reserve(N);
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();
	cout << "unordered_set insert:" << end2 - begin2 << endl;
	int m1 = 0;
	size_t begin3 = clock();
	for (auto e : v)
	{
		auto ret = s.find(e);
		if (ret != s.end())
		{
			++m1;
		}
	}
	size_t end3 = clock();
	cout << "set find:" << end3 - begin3 << "->" << m1 << endl;
	int m2 = 0;
	size_t begin4 = clock();
	for (auto e : v)
	{
		auto ret = us.find(e);
		if (ret != us.end())
		{
			++m2;
		}
	}
	size_t end4 = clock();
	cout << "unorered_set find:" << end4 - begin4 << "->" << m2 << endl;
	cout << "插入数据个数:" << s.size() << endl;
	cout << "插入数据个数:" << us.size() << endl << endl;
	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();
	cout << "set erase:" << end5 - begin5 << endl;
	size_t begin6 = clock();
		for (auto e : v)
		{
			us.erase(e);
		}
	size_t end6 = clock();
	cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
	return 0;
}


int main()
{
	test_set2();
	return 0;
}

重复值较多: 

重复值较少:

插入有序数据:

unordered_map介绍以及差异

template < class Key,                                 //unordered_map::key_type
           class T,                                   //unordered_map::mapped_type
           class Hash = hash<Key>,                    // unordered_map::hasher
           class Pred = equal_to<Key>,                //unordered_map::key_equal
     class Alloc = allocator< pair<const Key,T>>//unordered_map::allocator_type
           > class unordered_map;

 和unordered_set与set差异相同;本身与map使用差不多;

unordered_multimap/unordered_multiset

unordered_multimap/unordered_multiset跟multimap/multiset功能完全类似,⽀持Key冗余。

二、哈希表实现

1、哈希概念

哈希(hash)⼜称散列,是⼀种组织数据的⽅式。从译名来看,有散乱排列的意思。本质就是通过哈希 函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进行快速查找。

1.1直接定址法:

当关键字的范围⽐较集中时,直接定址法就是⾮常简单⾼效的⽅法,⽐如⼀组关键字都在[0,99]之间,那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。再⽐如⼀组关键字值都在 [a,z]的⼩写字⺟,那么我们开⼀个26个数的数组,每个关键字acsii码-a ascii码就是存储位置的下标也就是说直接定址法本质就是⽤关键字计算出⼀个绝对位置或者相对位置。

直接定址法的缺点也⾮常明显,当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤。假设我们只有数据范围是[0, 9999]的N个值,我们要映射到⼀个M个空间的数组中(⼀般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这⾥要注意的是h(key)计算出的值必须在[0, M)之间。

class Solution {
public:
	int firstUniqChar(string s) {
		// 每个字⺟的ascii码-'a'的ascii码作为下标映射到count数组,数组中存储出现的次数
		int count[26] = { 0 };
		// 统计次数
		for (auto ch : s)
		{
			count[ch - 'a']++;
		}
		for (size_t i = 0; i < s.size(); ++i)
		{
			if (count[s[i] - 'a'] == 1)
				return i;
		}
		return -1;
	}
}

 1.2 哈希冲突

这⾥存在的⼀个问题就是,两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案。

1.3 负载因子

假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么 ,负载因⼦有些地⽅也翻译为载荷因⼦/装载因⼦等,他的英⽂为load factor。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;

1.4将关键字转为整数

我们将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整数,这个细节我们后⾯代码实现中再进⾏细节展⽰。下⾯哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的Key是关键字转换成的整数。

2、哈希函数

2.1除留余数法/除法散列法

除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。
当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 ,那么key %
本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:
{63 , 31}看起来没有关联的值,如果M是16,也就是 ,那么计算出的哈希值都是15,因为63的⼆
进制后8位是 00111111,31的⼆进制后8位是 00011111。如果是 ,就更明显了,保留的都是
10进值的后x位,如:{112, 12312},如果M是100,也就是 ,那么计算出的哈希值都是12。
当使⽤除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数)。
需要说明的是,实践中也是⼋仙过海,各显神通,Java的HashMap采⽤除法散列法时就是2的整数
次冥做哈希表的⼤⼩M,这样玩的话,就不⽤取模,⽽可以直接位运算,相对⽽⾔位运算⽐模更⾼
效⼀些。但是他不是单纯的去取模,⽐如M是2^16次⽅,本质是取后16位,那么⽤key’ =
key>>16,然后把key和key' 异或的结果作为哈希值。也就是说我们映射出的值还是在[0,M)范围
内,但是尽量让key所有的位都参与计算,这样映射出的哈希值更均匀⼀些即可。

2.2乘法散列法

乘法散列法对哈希表⼤⼩M没有要求,⼤思路第⼀步:⽤关键字 K 乘上常数 A (0<A<1),并抽
取出 k*A 的⼩数部分。第⼆步:后再⽤M乘以k*A 的⼩数部分,再向下取整。
h ( key ) = floor ( M × (( A × key)%1.0)), 其中floor表⽰对表达式进⾏下取整,A∈(0,1),这⾥
最重要的是A的值应该如何设定,Knuth认为 A = ( 5 − 1)/2 = 0.6180339887....(⻩⾦分割点])⽐较好。
乘法散列法对哈希表⼤⼩M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A*key
= 762.6539420558,取⼩数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =
669.6366651392,那么h(1234) = 669。

2.3全域散列法

如果存在⼀个恶意的对⼿,他针对我们提供的散列函数,特意构造出⼀个发⽣严重冲突的数据集,
⽐如,让所有关键字全部落⼊同⼀个位置中。这种情况是可以存在的,只要散列函数是公开且确定
的,就可以实现此攻击。解决⽅法⾃然是⻅招拆招,给散列函数增加随机性,攻击者就⽆法找出确
定可以导致最坏情况的数据。这种⽅法叫做全域散列。
h ab ( key ) = (( a × key + b )% P )% M,P需要选⼀个⾜够⼤的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了⼀个P*(P-1)组全域散列函数组。假设P=17,M=6,a = 3, b = 4, 则 h34 (8) = ((3 × 8 + 4)%17)%6  = 5 。
需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使⽤,后续增删查
改都固定使⽤这个散列函数,否则每次哈希都是随机选⼀个散列函数,那么插⼊是⼀个散列函数,
查找⼜是另⼀个散列函数,就会导致找不到插⼊的key了。

3、处理哈希冲突

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法。

3.1开放定址法

在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于1的。这⾥的规则有三种:线性探测、⼆次探测、双重探测。
3.1.1线性探测
从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛
到哈希表尾,则回绕到哈希表头的位置。
h ( key ) = hash 0 = key % M,hash0位置冲突了,则线性探测公式为:hc ( key , i ) = hashi = ( hash 0 + i ) % M i = {1, 2, 3, ..., M − 1},因为负载因⼦⼩于1,则最多探测M-1次,⼀定能找到⼀个存储key的位置。
线性探测的⽐较简单且容易实现,线性探测也存在问题。假设,hash0位置连续冲突,hash0,hash1, hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做群集/堆积。下⾯的⼆次探测可以⼀定程度改善这个问题。
下⾯演⽰ {19,30,5,36,13,20,21,12} 等这⼀组值映射到M=11的表中。
3.1.2二次探测
从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为
⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表
尾的位置;
h ( key ) = hash 0 = key % M , hash0位置冲突了,则⼆次探测公式为:hc ( key , i ) = hashi = ( hash 0 ± i ^ 2 ) % M i = {1, 2, 3, ..., M/2}
⼆次探测当 hashi = ( hash 0 − i ^ 2 )% M 时,当hashi<0时,需要hashi += M
下⾯演⽰ {19,30,52,63,11,22} 等这⼀组值映射到M=11的表中。
3.1.3开放定址法代码实现
#include<iostream>
#include<vector>
using namespace std;


enum Stat
{
	Empty,
	Exist,
	Delete
};

template<class K,class V>
struct HashData
{
	pair<K, V> _kv;
	Stat _st;//哈希表里面每个位置的状态
};

template<class K>
struct HashFunc
{
	size_t operator()(const K& k)
	{
		return (size_t)k;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& k)
	{
		int hash0 = 0;
		for (auto ch : k)
		{
			hash0 *= 131;
			hash0 += ch;
		}
		return hash0;
	}
};
//插入的值可能是浮点数、负数、字符串,写一个仿函数将它们转为无符号整形
//对于字符串,将每个字符的ASCLL值相加得到对应的无符号整形数字
//但是相同字符组成的不同顺序排列的字符串就是一样的整形,会造成哈希冲突
//所以将每个每次加一个字符之后再乘素数131,保证每个字符串转换之后的独立
//这里需要特化
//对于自定义类型,自己写一个函数去转换为size_t类型的,这里看Data类型
template<class K,class V,class ToSize_t = HashFunc<K>>
class HashTable
{
public:
	HashTable()
		:_t(11)
		,_n(0)
	{}
	inline unsigned long __stl_next_prime(unsigned long n)//库中的素数表
	{
		// Note: assumes long is at least 32 bits.
		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//几乎都是两倍
		};
		const unsigned long* first = __stl_prime_list;
		const unsigned long* last = __stl_prime_list +
			__stl_num_primes;
		const unsigned long* pos = lower_bound(first, last, n);
		return pos == last ? *(last - 1) : *pos;
	}
	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first)) return false;//不支持冗余
		if (10 * _n / _t.size() > 7)//扩容
		{
			HashTable<K, V,ToSize_t> ht;
			ht._t.resize(__stl_next_prime(_t.size() + 1));//返回的是比传入大的一个素数
			for (auto data : _t)
			{
				if (data._st == Exist) 
					ht.Insert(data._kv);
			}
			_t.swap(ht._t);//交换比赋值效率高,不需要深拷贝,只是交换指针
		}
		int i = 1;
		size_t hash0 = ToSize_t()(kv.first) % _t.size();
		size_t hashi = hash0;
		while (_t[hashi]._st == Exist)//找到为空或者删除状态的位置进行插入
		{
			hashi = (hashi + i) % _t.size();
			i++;
		}
		_t[hashi]._kv = kv;
		_t[hashi]._st = Exist;
		++_n;
		return true;
	}
	HashData<K, V>* Find(const K& k)
	{
		int i = 1;
		size_t hash0 = ToSize_t()(k) % _t.size();
		size_t hashi = hash0;
		//若是有k,那么hash0对应的位置不会是Empty
		while (_t[hashi]._st != Empty)
		{
			//不为空可能是删除,删除的这个元素可能是k,所以不能直接用数据是否相等作为条件
			if (_t[hashi]._st == Exist && _t[hashi]._kv.first == k) return &_t[hashi];
			hashi = (hashi + i) % _t.size();
			i++;
		}
		return nullptr;
	}
	bool Erase(const K& k)
	{
		HashData<K, V>* ret = Find(k);//这里必须要用指针类型,否则ret只是一个和_t不相关的HashData
		//此时改变了状态也不会使得_t里面数据状态改变;使用指针解决这个问题
		if (ret) ret->_st = Delete;
		else return false;
	}
private:
	vector<HashData<K, V>> _t;
	size_t _n;//哈希表中实际数据个数
};

3.2链地址法

开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。
下⾯演⽰ {19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到M=11的表中。
h(19) = 8 h(30) = 8 h(5) = 5 h(36) = 3 h(13) = 2 h(20) = 9 h(21) =10, h(12) = 1,h(24) = 2,h(96) = 88
扩容
开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;stl中unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容,我们下⾯实现也使⽤这个⽅式。
极端场景
如果极端场景下,某个桶特别⻓怎么办?其实我们可以考虑使⽤全域散列法,这样就不容易被针对
了。但是假设不是被针对了,⽤了全域散列法,但是偶然情况下,某个桶很⻓,查找效率很低怎么
办?这⾥在Java8的HashMap中当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。
3.2.1链地址法代码实现
#include<iostream>
#include<vector>
using namespace std;


template<class K>
struct ToSize_t
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>//针对于浮点型的模板特化
struct ToSize_t<string>
{
	size_t operator()(const string& key)
	{
		int ret = 0;
		for (int i = 0; i < (int)key.size(); i++)
		{
			ret += key[i];
			ret *= 31;
		}
		return ret;
	}
};

template<class K,class V>
struct HashData
{
	HashData(const pair<K, V>& kv, HashData<K,V>* next = nullptr)
		:_kv(kv)
		,_next(next)
	{}
	pair<K, V> _kv;
	HashData<K, V>* _next;
};

template<class K,class V,class T = ToSize_t<K>>
class HashBucket
{
public:
	HashBucket()
		:_table(__stl_next_prime(0))
		//:_table(11)
		,_n(0)
	{}
	~HashBucket()
	{
		for (int i = 0; i < (int)_table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				_n--;
				cur = next;
			}
			_table[i] = nullptr;
		}
	}
	//具有申请资源的,就要写析构和拷贝构造和赋值重载
	HashBucket(HashBucket<K, V, T>& ht)
	{
		int size = ht._table.size();
		_table.resize(size);
		for (int i = 0; i < size; i++)
		{
			Node* cur = ht._table[i];
			while (cur)
			{
				this->Insert(cur->_kv);
				cur = cur->_next;
			}
		}
	}
	//赋值重载
	HashBucket<K, V, T> operator=(HashBucket<K, V, T>& ht)
	{
		HashBucket<K, V, T> tmp = ht;//拷贝构造一个临时哈希桶
		this->_table.swap(tmp._table);
		this->_n = tmp._n;
		return *this;
	}
	typedef HashData<K, V> Node;
	inline unsigned long __stl_next_prime(unsigned long n)
	{
		// Note: assumes long is at least 32 bits.
		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
		};

		const unsigned long* first = __stl_prime_list;
		const unsigned long* last = __stl_prime_list + __stl_num_primes;
		const unsigned long* pos = lower_bound(first, last, n);
		return pos == last ? *(last - 1) : *pos;
	}
	bool Insert(const pair<K,V>& kv)
	{
		if (_n / _table.size() >= 1)//扩容
		{
//这里扩容不能和开放定址法一样
//原因:若是创建一个新的哈希表,并且遍历旧的哈希表并且重新向新的哈希表里面插入
//意味着在新的哈希表中要创建newnode,而旧的哈希表的vector里面在析构时不会销毁下面挂着的一些节点指针,
//这里选择创建一个顺序表,将原来哈希桶中的节点转移过来,节点指针还是一样的
			vector<Node*> newtable;
			newtable.resize(__stl_next_prime(_table.size() + 1));
			//newtable.resize(2*_table.size());

			for (int i = 0; i < (int)_table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;//记录旧表下一个节点指针,避免逻辑出错
					int hashi = T()(cur->_kv.first) % newtable.size();
					cur->_next = newtable[hashi];
					newtable[hashi] = cur;
					cur = next;
				}
			}
			_table.swap(newtable);
		}
		int hashi = T()(kv.first) % _table.size();
		//找到位置了头插进入
		Node* newnode = new Node(kv);
		newnode->_next = _table[hashi];
		_table[hashi] = newnode;
		_n++;
		return true;
	}
	Node* Find(const K& key)
	{
		int hashi = T()(key) % _table.size();
		Node* cur = _table[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
				return cur;
			cur = cur->_next;
		}
		return nullptr;
	}
	bool Erase(const K& key)
	{
		int hashi = T()(key) % _table.size();
		Node* prev = nullptr;
		Node* cur = _table[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				//分两种情况:可能删的是头部或者中间的
				if (prev == nullptr)//说明进来就是要删除的即头部
				{
					Node* next = cur->_next;
					delete cur;
					_table[hashi] = next;
				}
				else//说明prev已经更新过了,删除的是中间部分的
				{
					Node* next = cur->_next;
					prev->_next = next;
					delete cur;
				}
				_n--;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}
private:
	vector<Node*> _table;
	size_t _n;
};

三、哈希封装

哈希封装底层是哈希桶,迭代器是单向的,并且实现迭代器的++时逻辑和map一块的也不一样;

先引入unordered_xxx的头文件

#pragma once
#include"HashBucket.h"

template<class K,class Hash = HashFun<K>>
class UnOrderedSet
{
	struct SetKoT
	{
		const K& operator()(const K& data)
		{
			return data;
		}
	};
public:
	typedef typename HashBucket<K, const K, SetKoT, Hash> :: Iterator iterator;//unordered_set的值不支持修改,因为这个值同时也是key
	typedef typename HashBucket<K, const K, SetKoT, Hash> :: ConstIterator const_iterator;

	iterator begin()
	{
		return _ht.Begin();
	}
	iterator end()
	{
		return _ht.End();
	}
	const_iterator cbegin()const
	{
		return _ht.cBegin();
	}
	const_iterator cend()const
	{
		return _ht.cEnd();
	}
	pair<iterator,bool> insert(const K& data)
	{
		return _ht.Insert(data);
	}
	iterator find(const K& k)
	{
		return _ht.Find(k);
	}
	bool erase(const K& k)
	{
		return _ht.Erase(k);
	}
private:
	HashBucket<K, const K, SetKoT,Hash> _ht;
};
#pragma once
#include"HashBucket.h"

template<class K,class V,class Hash = HashFun<K>>
class UnOrderedMap
{
	struct MapKoT
	{
		const K& operator()(const pair<const K,V>& data)
		{
			return data.first;
		}
	};
public:
	typedef typename HashBucket<K, pair<const K, V>, MapKoT, Hash> ::Iterator iterator;
	typedef typename HashBucket<K, pair<const K, V>, MapKoT, Hash> ::ConstIterator const_iterator;

	iterator begin()
	{
		return _ht.Begin();
	}
	iterator end()
	{
		return _ht.End();
	}
	const_iterator cbegin()const
	{
		return _ht.cBegin();
	}
	const_iterator cend()const
	{
		return _ht.cEnd();
	}
	pair<iterator,bool> insert(const pair<const K, V>& data)
	{
		return _ht.Insert(data);
	}
	iterator find(const K& k)
	{
		return _ht.Find(k);
	}
	bool erase(const K& k)
	{
		return _ht.Erase(k);
	}
	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
		return ret.first->second;//return *(ret.first).second//*的优先级高于.
	}
private:
	HashBucket<K, pair<const K,V>, MapKoT,Hash> _ht;
};

这里封装的MapKoT和SetKoT是因为哈希表进行比较时比较键值key,所以这里传一个类,将数据里面的key拿出来;

这里unordered_set模板参数传两个K是为了迎合unordered_map;

Hash这个类是将key转为整形方便取模放入哈希桶;

封装的这两个类底层都是一个哈希桶,即_ht;

pair<>只是类型不是一个值,要是要返回一个pair<>类型的值,可以使用{}进行隐式类型转换或者调用make_pair()函数。

#pragma once
#include<iostream>
#include<vector>
using namespace std;

//HashFun将K转为无符号整形方便取模
template<class K>
struct HashFun
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>//针对于浮点型的模板特化
struct HashFun <string>
{
	size_t operator()(const string& key)
	{
		int ret = 0;
		for (int i = 0; i < (int)key.size(); i++)
		{
			ret += key[i];
			ret *= 31;
		}
		return ret;
	}
};
/
//哈希桶的数据
//这里是T因为HashData类里面存了真实数据,T可以是set的v也可以是map的pair
template<class T>
struct HashData
{
	HashData(const T& data, HashData<T>* next = nullptr)
		:_data(data)
		, _next(next)
	{}
	T _data;
	HashData<T>* _next;
};
//
template<class K, class T, class KoT,class Hash>
class HashBucket;//哈希桶类模板前置声明,因为在迭代器实现中使用到了哈希桶类
                 //而编译器是只会向上查找有无声明定义的
//迭代器实现
template<class K, class T,class Ref,class Ptr, class KoT, class Hash = HashFun<K>>
struct HTIterator
{
	typedef HashData<T> Node;
	typedef HashBucket<K, T, KoT, Hash> HT;
	typedef HTIterator<K, T, Ref, Ptr, KoT, Hash> Self;

	//迭代器构造函数
	HTIterator(Node* node = nullptr , const HT* ht=nullptr)
		:_node(node)
		, _ht(ht)
	{}
	Ref operator*()
	{
		return _node->_data;
	}
	Ptr operator->()
	{
		return &(_node->_data);
	}
	bool operator==(const Self& it)
	{
		return _node == it._node;
	}
	bool operator!=(const Self& it)
	{
		return !(_node == it._node);
	}
	Self& operator++()
	{
		if (_node->_next)//_node下一个有挂着的节点
			_node = _node->_next;
		else
		{
			size_t hashi = Hash()(KoT()(_node->_data)) % _ht->_table.size();
			hashi++;
			while (hashi < _ht->_table.size())
			{
				if (_ht->_table[hashi])
				{
					_node = _ht->_table[hashi];
					break;//找到了下一条不为空的链表的首节点
				}
				hashi++;
			}
			if (hashi == _ht->_table.size())//可能越过哈希桶的大小
				_node = nullptr;
		}
		return *this;
	}

	Node* _node;
	const HT* _ht;

};
//
//哈希桶实现
template<class K, class T, class KoT,class Hash >
class HashBucket
{
	template<class K, class T, class Ref, class Ptr, class KoT, class Hash >
	friend struct HTIterator;//迭代器中需要使用_table私有成员,所以将迭代器给成友元类
public:
	typedef HTIterator<K, T, T&, T*, KoT, Hash> Iterator;
	typedef HTIterator<K, T, const T&,const T*, KoT, Hash> ConstIterator;

	Iterator Begin()
	{
		if (_n == 0) return End();
		//有数据就找第一个不为空的桶的首节点
		size_t hashi = 0;
		while (_table[hashi] == nullptr)
		{
			hashi++;
		}
		return Iterator( _table[hashi],this );
	}
	Iterator End()
	{
		return Iterator( nullptr,this );
	}
	ConstIterator cBegin()const
	{
		if (_n == 0) return cEnd();
		//有数据就找第一个不为空的桶的首节点
		size_t hashi = 0;
		while (_table[hashi] == nullptr)
		{
			hashi++;
		}
		return ConstIterator( _table[hashi],this );
	}
	ConstIterator cEnd()const
	{
		return ConstIterator( nullptr,this );
	}

	HashBucket()
		:_table(__stl_next_prime(0))
		, _n(0)
	{}
	~HashBucket()
	{
		for (int i = 0; i < (int)_table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				_n--;
				cur = next;
			}
			_table[i] = nullptr;
		}
	}
	//具有申请资源的,就要写析构和拷贝构造和赋值重载
	HashBucket(HashBucket<K, T, KoT, Hash>& ht)
	{
		int size = ht._table.size();
		_table.resize(size);
		for (int i = 0; i < size; i++)
		{
			Node* cur = ht._table[i];
			while (cur)
			{
				this->Insert(cur->_data);
				cur = cur->_next;
			}
		}
	}
	//赋值重载
	HashBucket<K, T, KoT,Hash> operator=(HashBucket<K, T, KoT, Hash>& ht)
	{
		HashBucket<K, T, KoT,Hash> tmp = ht;//拷贝构造一个临时哈希桶
		this->_table.swap(tmp._table);
		this->_n = tmp._n;
		return *this;
	}
	typedef HashData<T> Node;
	inline unsigned long __stl_next_prime(unsigned long n)
	{
		// Note: assumes long is at least 32 bits.
		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
		};

		const unsigned long* first = __stl_prime_list;
		const unsigned long* last = __stl_prime_list + __stl_num_primes;
		const unsigned long* pos = lower_bound(first, last, n);
		return pos == last ? *(last - 1) : *pos;
	}
	pair<Iterator,bool> Insert(const T& data)
	{
		Iterator it = Find(KoT()(data));
		if (it!= End())//不支持冗余值
			return make_pair(it, false);
		if (_n / _table.size() >= 1)//扩容
		{
			//这里扩容不能和开放定址法一样
			//原因:若是创建一个新的哈希表,并且遍历旧的哈希表并且重新向新的哈希表里面插入
			//意味着在新的哈希表中要创建newnode,而旧的哈希表的vector里面在析构时不会销毁下面挂着的一些节点指针
			vector<Node*> newtable;
			newtable.resize(__stl_next_prime(_table.size() + 1));
			//newtable.resize(2*_table.size());

			for (int i = 0; i < (int)_table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;//记录旧表下一个节点指针,避免逻辑出错
					size_t hashi = Hash()(KoT()(cur->_data)) % newtable.size();
					cur->_next = newtable[hashi];
					newtable[hashi] = cur;
					cur = next;
				}
			}
			_table.swap(newtable);
		}
		size_t hashi = Hash()(KoT()(data)) % _table.size();
		//找到位置了头插进入
		Node* newnode = new Node(data);
		newnode->_next = _table[hashi];
		_table[hashi] = newnode;
		_n++;
		return make_pair(Iterator(newnode,this),true);
	}
	Iterator Find(const K& key)
	{
		size_t hashi = Hash()(key) % _table.size();
		Node* cur = _table[hashi];
		while (cur)
		{
			if (KoT()(cur->_data) == key)
				return Iterator(cur, this);
			cur = cur->_next;
		}
		return End();
	}
	bool Erase(const K& key)
	{
		int hashi = Hash()(key) % _table.size();
		Node* prev = nullptr;
		Node* cur = _table[hashi];
		while (cur)
		{
			if (KoT()(cur->_data) == key)
			{
				//分两种情况:可能删的是头部或者中间的
				if (prev == nullptr)//说明进来就是要删除的即头部
				{
					Node* next = cur->_next;
					delete cur;
					_table[hashi] = next;
				}
				else//说明prev已经更新过了,删除的是中间部分的
				{
					Node* next = cur->_next;
					prev->_next = next;
					delete cur;
				}
				_n--;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}
private:
	vector<Node*> _table;
	size_t _n;
};


http://www.kler.cn/a/566409.html

相关文章:

  • 计算机毕业设计Python+DeepSeek-R1大模型期货价格预测分析 期货价格数据分析可视化预测系 统 量化交易大数据 机器学习 深度学习
  • REACT学习--钩子剩余部分
  • TCP 三次握手与四次挥手
  • Python测试框架Pytest的参数化
  • ChatGPT Deep Research:重塑智能研究的未来边界
  • C++ 实现 组合
  • Sparsely-Gated Mixture-of-Experts Layer (MoE)论文解读与Pytorch代码实现
  • nginx配置文件分解为多个子配置
  • Maven 插件的使用(二)
  • 安全模块设计:token服务、校验注解(开启token校验、开启签名校验、允许处理API日志)、获取当前用户信息的辅助类
  • 解析excel文件报错java.lang.NoSuchMethodException
  • FPGA 使用门控时钟
  • 8个Linux进程管理命令详解及示例(四):kill、pkill 和 killall 命令
  • 养生保健:为健康生活筑牢基石
  • 人类驾驶的人脑两种判断模式(反射和预判)-->自动驾驶两种AI模式
  • 深度学习笔记17-马铃薯病害识别(VGG-16复现)
  • 验证码识别:一文掌握手机验证码的自动化处理
  • 爬虫下载B站视频简单程序(仅供学习)
  • 【考研】复试相关上机题目
  • 【西瓜书《机器学习》四五六章内容通俗理解】