【C++】哈希表原理与实战:从冲突解决到性能优化

序言

今天我们来学习哈希表,在介绍它之前,我们先来看看unordered_set和unordered_map

一、unordered_set与unordered_map

1. unordered_set

cpp 复制代码
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;
  • 这是unordered_set的声明,Key就是其底层关键字的类型
  • 它要求Key支持转成整型 ,如果不支持,需要传第二个仿函数参数去实现。
  • 它要求Key支持比较相等 ,如果不支持,需要传第三个仿函数参数去实现。
  • 第四个参数为内存池
  • unordered_set底层是哈希桶,增删查改平均效率为O(1),迭代器遍历不再有序

2. unordered_map

cpp 复制代码
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类似,Key也是要求转整型且支持等于比较,它的具体情况我们后面学习底层原理时再深入学习

3. unordered_set/unordered_map与set/map差异

对比项 set/map unordered_set/unordered_map
底层结构 红黑树 哈希表
数据是否有序 ✔ 有序 ✘ 无序
插入效率 O(logN) O(1)(平均)
查找效率 O(logN) O(1)(平均)
删除效率 O(logN) O(1)(平均)
最坏时间复杂度 O(logN) O(N)
支持范围查找
支持自定义排序
适用场景 需要排序 只关心查找速度

二、哈希

1. 概念

哈希(hash)又称散列 ,其本质是通过哈希函数把关键字Key跟存储位置建立一个映射关系 ,查找时通过这个哈希函数计算出Key存储的位置,进行快速查找

2.直接定址法

当关键字范围比较集中时,直接定址法就是非常简单高效的方法,比如0,99的数字、a,z的字母等等。它的本质是用关键字计算出一个绝对或相对位置。这个方法在计数排序中也有使用。

3. 哈希冲突

直接定址法有明显的缺点,就是如果数据比较分散,就要消耗大量空间 ,比如说,有0,99999的N个数,要映射到大小为M的数组中,此时我们就要用到哈希函数(hash function)hf ,关键字Key被放到数组的h(0<=h<M)位置。

哈希冲突就是指两个不同的Key被放到了同一个位置 ,实际上哈希冲突时不可避免的,因此就需要我们去设计一个好的哈希函数去尽量减少冲突次数以及解决冲突的方案

4. 负载因子

假设哈希表中已经映射了N个值,哈希表大小为M ,那么负载因子=负载因子越大,哈希冲突概率越高,空间利用率也就越高;越低则反之

5. 哈希函数

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

  • 假设哈希表大小为M,那么通过key除以M的余数作为映射位置的下标,即哈希函数hf(key)=key%M。
  • 使用除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等等,假设M=2X,那么key%2X就相当于取key的后X位,后X位相同的key就会造成大量的哈希冲突,10的幂也是同理。
  • 使用除法散列法时,建议M取不接近2的整数次幂的一个质数
  • 这里说明一下,实践中并不一定要严格按照上面的规则来取M,在Java中,M就是取2的幂,举个例子,M=216,本质是取key的后16位,那么用新的key'=key>>16,再将key'与key异或得到哈希值,无论是哪种设计都是让key的分布尽量均匀以减少哈希冲突

5.2 乘法散列法(了解)

  • 乘法散列法对哈希表大小M没有要求,他的大思路第一步:用关键字K乘上常数A(0<A<1),并抽取出kA的小数部分。第二步:后再用M乘以k*A的小数部分,再向下取整。
  • h(key) = floor(M × ((A × key)%1.0)) ,其中floor表示对表达式进行下取整,A∈(0,1),这里最重要的是A的值应该如何设定,Knuth认为(黄金分割点)
    A = ( 5 − 1)/2 = 0.6180339887...比较好。

5.3 全域散列法(了解)

  • 如果存在⼀个恶意的对手,他针对我们提供的散列函数,特意构造出⼀个发生严重冲突的数据集,比如,让所有关键字全部落入同⼀个位置中。此时就要用到全域散列法,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。
  • h(key) = ((a * key + b)%P )%M ,P需要选⼀个足够大的质数,a可以随机选1,P-1之间的任意整数,b可以随机选0,P-1之间的任意整数,这些函数构成了⼀个P*(P-1)组全域散列函数组。

5.4 其他方法(了解)

其他方法可参考《算法导论》、《殷人昆 数据结构:用面向对象方法与C++语言描述(第二版)》和《数据结构(C语言版).严蔚敏_吴伟民》等书

6. 处理哈希冲突

解决哈希冲突,主要有两种方法:开放地址法链地址法

6.1 开放地址法

将所有元素映射到哈希表内,发生冲突时,我们就需要按照某种规则找到一个没有存储数据的位置进行存储,开放地址法的负载因子一定是小于1的。上面说的规则有:线性探测、二次探测、双重探测

线性探测
  • 规则:从发生冲突的位置开始向后探测,直到遇到空的位置即可,如果走到表尾,则返回到表的开头。
  • 哈希函数:h(key)=hash0=key%M,hash0冲突了,则探测公式为hc(key,i)=hahsi=(hash0+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的表中

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(key)=hash0=hash%M,hash0冲突了,二次探测公式为:hc(key,i)=hashi=(hash0 ± i2)%M,i={1,2,3,...,M/2}
  • 当hashi=(hash0-i2)%M时,当hashi<0时,需要hashi+=M

举个例子:{19,30,52,63,11,22},M=11

h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0

6.2 代码实现思路

结构
cpp 复制代码
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 HashTable
{ 
private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0; // 表中存储数据个数
};

注意,我们要给每个存储值的位置加一个状态情况,否则删除之后会影响后面冲突的查找

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =
10,h(12) = 1

如上图,如果我们直接删除30,在查找20时,发现9为空,导致查找失败,因此删除30不用直接删除值,将其状态改为DELETE即可

扩容

这里我们将哈希表的负载因子控制在0.7,当负载因子到0.7以后我们就需要扩容了,我们可以按二倍扩容,但是我们要保证哈希表大小为质数,这怎么办呢?一种方案就是采取与Java类似的方法,大小为2的幂;另一种方案是sgi版本的哈希表的使用的方法,给了一个近似2倍的质数表,每次扩容从表中取即可

cpp 复制代码
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;
}

它返回的是表中第一个大于等于n的质数

key不能取模的问题

当key不支持取模时,我们需要给哈希表增加一个仿函数,将key转换成一个可以取模的整型且转换后不易冲突,那么这个仿函数的要求就是尽量让key的每位数都参与运算。在写这个仿函数时,我们可以将一些常见的需要特殊处理的类型进行特化,比如string

cpp 复制代码
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
// 特化
template<>
struct HashFunc<string>
{
// 字符串转换成整形,可以把字符ascii码相加即可
// 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的
// 这⾥我们使⽤BKDR哈希的思路,⽤上次的计算结果去乘以⼀个质数,这个质数⼀般去31, 131等效果会⽐较好
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 131;
			hash += e;
		} 
		return hash;
	}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{ 
public:
private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0; // 表中存储数据个数
}

这样开放地址法实现哈希表就大功告成了,具体代码这里就不呈现了。

6.3 链地址法

处理哈希冲突的方式

链地址法与开放地址法的区别在于处理哈希冲突的方式,链地址法的哈希表是存储一个指针(这个哈希表是一个指针数组 ),没有数据映射到这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成一个链表,挂在这个位置下面 ,链地址法也称拉链法哈希桶

以{ 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,stl中的unordered_xxx最大负载因子控制在1左右,因此我们就采用1为扩容阈值,当负载因子大于等于1我们就扩容,链地址法和开放地址法扩容过程都是要采用rehash操作进行重新定址。

极端情况

如果某个桶的长度特别长该怎么办呢?在Java8中,当一个桶的长度达到8时,会转换成红黑树;但是要注意,这种情况一般极其罕见,在C++slt中,不会采取树化的措施,它主要是看当负载因子load_factor() > max_load_factor()时,进行rehash操作,为每个元素重新分配地址,这样就解决了单个桶过长的问题

代码呈现
cpp 复制代码
namespace HashBucket
{
	template<typename K,typename V>
	struct HashNode
	{
		HashNode(const pair<K,V>& kv)
			:_kv(kv),_next(nullptr)
		{}

		typedef HashNode<K, V> Node;
		pair<K, V> _kv;
		Node* _next;
	};

	template<typename K>
	struct HashFunc
	{
		size_t operator()(const K& k) { return (size_t)k; }
	};

	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t sum=0;
			for (auto i : s)
				sum += (int)i;
			return sum;
		}
	};

	template<typename K,typename V,class Hash=HashFunc<K>>
	class HashTable
	{
	public:
		typedef HashNode<K, V> Node;

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

		HashTable()
		{
			_table.resize(__stl_next_prime(0), nullptr);
		}

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

		bool insert(const pair<K, V>& kv)
		{
			Hash hs;
			if (_n == _table.size())
			{
				//扩容
				vector<Node*> newtable(__stl_next_prime(_table.size()+1));
				Node* cur;
				for (int i = 0; i < _table.size(); i++)
				{
					cur = _table[i];
					while (cur)
					{
						int hash0 = hs(cur->_kv.first) % newtable.size();
						cur->_next = newtable[hash0];
						newtable[hash0] = cur;
						cur = cur->_next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newtable);
			}
			int hash1 = hs(kv.first) % _table.size();
			Node* newnode = new Node(kv);
			newnode->_next = _table[hash1];
			_table[hash1] = newnode;
			++_n;
			return true;
		}

		bool erase(const K& k)
		{
			Hash hs;
			int hash = hs(k) % _table.size();
			Node* cur = _table[hash],prev=nullptr;
			while (cur)
			{
				if (cur->_kv.first == k)
				{
					if (prev == nullptr)
						_table[hash] = cur->_next;
					else
						prev->_next = cur->_next;
					delete cur;
					--_n;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		Node* find(const K& k)
		{
			Hash hs;
			int hash = hs(k) % _table.size();
			Node* cur = _table[hash];
			while (cur)
			{
				if (cur->_kv.first == k)
					return cur;
				cur = cur->_next;
			}
			return nullptr;
		}

		V& operator[](const K& k)
		{
			if (find(k) == nullptr)
				insert(make_pair(k, V()));
			else
				return find(k)->_kv.second;
		}

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

具体代码可见:

github:HashTable

gitee:HashTable

三、封装unordered_set和unordered_map

1.架构分析

源码与map/set类似,都是复用一个hashtable控实现key和key/value结构,源码的命名风格比较乱,set用的是value,map用的是Key和T,可读性较差,我们无需理会,接下来,我们将自己实现一份。

2. 模拟实现unordered_set和unordered_map

2.1 基本结构

我们调整一下,key用K,value用V,哈希表数据用T。

分析

  • HashTable实现了泛型,不知道T是K还是pair,我们就可以通过T的传值控制HashTable是set还是map。
  • 因为我们HashTable中用的是T取取模和比较相等,但是pair<K, V>只需要K去完成,因此,我们需要传一个KeyOfT的仿函数取出K,所以我们在unordered_set和unordered_map层分别实现一个MapKeyOfT和SetKeyOfT的仿函数传给KeyOfT

实现

cpp 复制代码
// MyUnorderedSet.h
namespace bit
{
	template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
		}
	};
	public:
		bool insert(const K& key)
		{
			return _ht.Insert(key);
		}
	private:
		hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
	};
} 
// MyUnorderedMap.h
namespace bit
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		bool insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}
	private:
		hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT, Hash> _ht;
	};
} 
// HashTable.h
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
namespace hash_bucket
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;
		HashNode(const T& data)
		:_data(data)
		,_next(nullptr)
	{}
};
// 实现步骤:
// 1、实现哈希表
// 2、封装unordered_map和unordered_set的框架 解决KeyOfT
// 3、iterator
// 4、const_iterator
// 5、key不⽀持修改的问题
// 6、operator[]
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
	typedef HashNode<T> Node;
public:
	HashTable()
	{
		_tables.resize(__stl_next_prime(_tables.size()), nullptr);
	} 
}

2.2 iterator实现

2.2.1 iterator源码

cpp 复制代码
template <class Value, class Key, class HashFcn,
	class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
	typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>
		hashtable;
	typedef __hashtable_iterator<Value, Key, HashFcn,
									ExtractKey, EqualKey, Alloc>
		iterator;
	typedef __hashtable_const_iterator<Value, Key, HashFcn,
										ExtractKey, EqualKey, Alloc>
		const_iterator;
	typedef __hashtable_node<Value> node;
	
	typedef forward_iterator_tag iterator_category;
	typedef Value value_type;
	node* cur;
	hashtable* ht;
	__hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}
	__hashtable_iterator() {}
	reference operator*() const { return cur->val; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
	pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
	iterator& operator++();
	iterator operator++(int);
	bool operator==(const iterator& it) const { return cur == it.cur; }
	bool operator!=(const iterator& it) const { return cur != it.cur; }
};
template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++()
{
	const node* old = cur;
	cur = cur->next;
	if (!cur) {
		size_type bucket = ht->bkt_num(old->val);
		while (!cur && ++bucket < ht->buckets.size())
			cur = ht->buckets[bucket];
	} 
	return *this;
}

2.2.2 实现思路分析

  • iterator大框架与list的iterator一致,用一个类型封装节点的指针,再通过运算符重载实现迭代器像指针一样的访问行为 ,哈希表的迭代器是单向迭代器
  • operator++的实现是一个难点,若当前桶下面还有节点,则节点的指针指向下一个即可,如果当前桶走完了,则需要想办法找到下一个桶。
  • 它的结构设计就比较困难了,参考上面的源码,iterator中有一个结点的指针和一个哈希表对象的指针,这样的设计就能满足++的要求
  • begin()返回第一个结点指针构造的迭代器,end()返回空
  • unordered_set的key不支持修改,我们把它的第二个模板参数改成const K 即可,即:HashTable<K, const K, SetKeyOfT, Hash> _ht
  • unordered_map中pair<K, V>的K不支持修改,V可以修改,我们把它的第二个模板参数改成**pair<const K, V>** 即可,即:HashTable<K, pair<const K, V>,MapKeyOfT,Hash> _ht

2.3 代码实现

unordered_set.h

cpp 复制代码
#include "HashTable.h"
namespace zzt
{
	template<class K,class Hash=HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key) { return key; }
		};
		typedef typename HashBucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
		typedef typename HashBucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;
	public:
		unordered_set(initializer_list<K> il)
		{
			for (auto i : il)
				this->insert(i);
		}
		iterator begin() { return _ht.Begin(); }
		const_iterator begin() const { return _ht.Begin(); }
		iterator end() { return _ht.End(); }
		const_iterator end() const { return _ht.End(); }
		pair<iterator, bool> insert(const K& k) { return _ht.insert(k); }
		bool erase(const K& k) { return _ht.erase(k); }
		iterator find(const K& k) { return _ht.find(k); }
		const_iterator find(const K& k) const { return _ht.find(k); }
	private:
		HashBucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
	};
}

unordered_map.h

cpp 复制代码
#include "HashTable.h"
namespace zzt
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K,V>& kv) { return kv.first; }
		};
		typedef typename HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
		typedef typename HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;
	public:
		iterator begin() { return _ht.Begin(); }
		const_iterator begin() const { return _ht.Begin(); }
		iterator end() { return _ht.End(); }
		const_iterator end() const { return _ht.End(); }
		pair<iterator, bool> insert(const pair<const K, V>& kv) { return _ht.insert(kv); }
		bool erase(const K& k) { return _ht.erase(k); }
		iterator find(const K& k) { return _ht.find(k); }
		const_iterator find(const K& k) const { return _ht.find(k); }
		V& operator[](const K& k)
		{
			pair<iterator, bool> ret = _ht.insert(pair<const K, V>(k, V()));
			return ret.first->second;
		}
	private:
		HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};
}

HashTable.h

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template<typename K>
struct HashFunc
{
	size_t operator()(const K& k) { return (size_t)k; }
};

template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t sum = 0;
		for (auto i : s)
			sum += (int)i;
		return sum;
	}
};
namespace HashBucket
{
	template<typename T>
	struct HashNode
	{
		HashNode(const T& data)
			:_data(data),_next(nullptr)
		{}

		typedef HashNode<T> Node;
		T _data;
		Node* _next;
	};

	template<typename K, typename T, class KeyOfT, class Hash = HashFunc<K>>
	class HashTable;

	template<typename K,typename T,typename Ref,typename Ptr,typename KeyOfT,typename Hash>
	struct Hash_Iterator
	{
		typedef HashNode<T> Node;
		typedef Hash_Iterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;

		Hash_Iterator(const HashTable<K, T, KeyOfT, Hash>* pht, Node* node)
			:_pht(pht),_node(node)
		{}

		Ref operator*() { return _node->_data; }
		Ptr operator->() { return &_node->_data; }
		bool operator==(const Self& s) { return _node == s._node; }
		bool operator!=(const Self& s) { return _node != s._node; }
		Self& operator++()
		{
			if (_node->_next)
				_node = _node->_next;
			else
			{
				Hash hs;
				KeyOfT kot;
				int k = hs(kot(_node->_data)) % _pht->_table.size()+1;
				while (k < _pht->_table.size() && _pht->_table[k] == nullptr)
					k++;
				if (k < _pht->_table.size())
					_node = _pht->_table[k];
				else
					_node = nullptr;
			}
			return *this;
		}

		const HashTable<K, T, KeyOfT, Hash>* _pht;
		Node* _node;
	};

	template<typename K,typename T,class KeyOfT,class Hash>
	class HashTable
	{
		typedef HashNode<T> Node;

		//友元 Iterator _pht取值
		template<typename K, typename T, typename Ref, typename Ptr, typename KeyOfT, typename Hash>
		friend struct Hash_Iterator;

	public:
		typedef Hash_Iterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
		typedef Hash_Iterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;

		HashTable()
		{
			_table.resize(__stl_next_prime(_tables.size()), nullptr);
		}

		~HashTable()
		{
			// 依次把每个桶释放
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				} 
				_table[i] = nullptr;
			}
		}

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

		HashTable()
		{
			_table.resize(__stl_next_prime(0), nullptr);
		}

		HashTable(initializer_list<T> il)
		{
			for (auto i : il)
				this->insert(i);
		}

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

		Iterator Begin()
		{
			int hashi = 0;
			Node* cur = _table[hashi];
			while (hashi<_table.size()&&cur == nullptr)
			{
				hashi++;
				cur = _table[hashi];
			}
			if (cur)
				return Iterator(this,cur);
			return Iterator(this, nullptr);
		}

		ConstIterator Begin() const
		{
			int hashi = 0;
			Node* cur = _table[hashi];
			while (hashi < _table.size() && cur == nullptr)
			{
				hashi++;
				cur = _table[hashi];
			}
			if (cur)
				return cur;
			return nullptr;
		}

		Iterator End() { return Iterator(this,nullptr); }

		ConstIterator End() const { return Iterator(this, nullptr); }

		pair<Iterator, bool> insert(const T& data)
		{
			Hash hs;
			KeyOfT kot;
			if (_n == _table.size())
			{
				//扩容
				vector<Node*> newtable(__stl_next_prime(_table.size()+1));
				Node* cur;
				for (int i = 0; i < _table.size(); i++)
				{
					cur = _table[i];
					while (cur)
					{
						int hash0 = hs(kot(cur->_data)) % newtable.size();
						cur->_next = newtable[hash0];
						newtable[hash0] = cur;
						cur = cur->_next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newtable);
			}
			int hash1 = hs(kot(data)) % _table.size();
			Node* newnode = new Node(data);
			newnode->_next = _table[hash1];
			_table[hash1] = newnode;
			++_n;
			return { Iterator(this,newnode),true };
		}

		bool erase(const K& k)
		{
			Hash hs;
			KeyOfT kot;
			int hash = hs(k) % _table.size();
			Node* cur = _table[hash],prev=nullptr;
			while (cur)
			{
				if (kot(cur->_data) == k)
				{
					if (prev == nullptr)
						_table[hash] = cur->_next;
					else
						prev->_next = cur->_next;
					delete cur;
					--_n;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		Iterator find(const K& k)
		{
			Hash hs;
			KeyOfT kot;
			int hash = hs(k) % _table.size();
			Node* cur = _table[hash];
			while (cur)
			{
				if (kot(cur->_data) == k)
					return Iterator(this,cur);
				cur = cur->_next;
			}
			return Iterator(this,nullptr);
		}

		ConstIterator find(const K& k) const
		{
			Hash hs;
			KeyOfT kot;
			int hash = hs(k) % _table.size();
			Node* cur = _table[hash];
			while (cur)
			{
				if (kot(cur->_data) == k)
					return Iterator(this, cur);
				cur = cur->_next;
			}
			return Iterator(this, nullptr);
		}

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

github:stl_myunordered_set&stl_myunordered_map

gitee:stl_myunordered_set&stl_myunordered_map

相关推荐
Cloud_Shy6181 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第七章 Item 48 - 50)
开发语言·人工智能·笔记·python·microsoft·学习方法
秋雨梧桐叶落莳1 小时前
iOS——QQ音乐仿写项目总结
学习·macos·ui·ios·mvc·objective-c·xcode
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章84-包胶有无检测
图像处理·人工智能·opencv·算法·计算机视觉
fpcc1 小时前
并行编程实战——CUDA编程的pipelines
c++·cuda
Irissgwe1 小时前
数据结构-排序
数据结构·算法·排序算法
A__tao2 小时前
告别手写 Go 结构体!推荐一个支持注释解析的 YAML 转 Struct 在线工具
开发语言·后端·golang
小O的算法实验室2 小时前
2025年IEEE TITS,基于动态聚类粒子群算法的无人机任务分配与路径规划
算法
何以解忧,唯有..2 小时前
Go 语言语句分隔符详解:分号、换行与代码规范
开发语言·golang·代码规范
Tairitsu_H2 小时前
[LC优选算法#5] 分治:快排 | 颜色分类 | 排序数组 | 第K大元素
c++·算法·leetcode·排序算法·快速排序