C++第四十弹---从零开始:模拟实现C++中的unordered_set与unordered_map

news2024/11/18 9:24:35

 ✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】

目录

1 哈希概念

2 哈希冲突

3 哈希函数

4 哈希冲突解决

4.1 闭散列

4.1.1. 线性探测

4.1.2. 二次探测

4.2 开散列

4.2.1. 开散列概念

4.2.2. 开散列实现

4.2.3. 开散列增容

4.2.4. 开散列的思考

4.2.5. 开散列与闭散列比较

5. 模拟实现

5.1 哈希表的改造

5.2 unordered_set

5.3 unordered_map

6 完整代码


unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。


1 哈希概念


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


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

当向该结构中:

  • 插入元素

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

  • 搜索元素

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


该方式即为哈希(散列)方法哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表(Hash Table)(或者称散列表)。

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity;  capacity为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。


问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题

2 哈希冲突


对于两个数据元素的关键字k_i和 k_j(i != j),有k_i != k_j,但有:Hash(k_i) ==Hash(k_j),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
 

发生哈希冲突该如何处理呢?


3 哈希函数


引起哈希冲突的一个原因可能是:哈希函数设计不够合理
哈希函数设计原则:

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

常见哈希函数
1. 直接定址法--(常用)

  • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
  • 优点:简单、均匀
  • 缺点:需要事先知道关键字的分布情况
  • 使用场景:适合查找比较小且连续的情况


2. 除留余数法--(常用)

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


3. 平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。


4. 折叠法--(了解)


折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。


5. 随机数法--(了解)


选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。
通常应用于关键字长度不等时采用此法。


6. 数学分析法--(了解)


设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:

假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况。


注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。


4 哈希冲突解决


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


4.1 闭散列


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


4.1.1. 线性探测


比如1(哈希概念)中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

插入

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

删除

  • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

线性探测的实现

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state;
};

template<class K, class V>
class HashTable
{
public:
    // 构造,默认开辟10个大小空间
    HashTable()
    {
	    _tables.resize(10);
    }

    bool Insert(const pair<K, V>& kv)
    {
        // 检测哈希表底层空间是否充足
        // _CheckCapacity();
        size_t hashi = kv.first % _tables.size();

        // 线性探测
	    while (_tables[hashi]._state == EXIST)
	    {
		    ++hashi;
		    hashi %= _tables.size();
	    }

	    // 更新数据
	    _tables[hashi]._kv = kv;
	    _tables[hashi]._state = EXIST;
    	++_n;

	    return true;
    }
    HashData<K, V>* Find(const K& key)
	{
		Hash hs;
		size_t hashi = key % _tables.size();
		// 线性探测,存在/删除状态均需查找
		while (_tables[hashi]._state != EMPTY)
		{
			// 删除状态不能查找
			if (_tables[hashi]._state != DELETE &&
				_tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];
			}

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

		return nullptr;
	}
	bool Erase(const K& key)
	{
		// 查找是否有该key值,有则将状态修改为删除并--n,没有则返回false
		HashData<K, V>* ret = Find(key);
		if (ret == nullptr)
			return false;
		else
		{
			ret->_state = DELETE;
			--_n;

			return true;
		}
	}
private:
   vector<HashData<K, V>> _tables;
   size_t _n = 0; // 有效数据个数
};

思考:哈希表什么情况下进行扩容?如何扩容?

void CheckCapacity()
{
	if (_n * 10 / _tables.size() >= 7)
	{
		 方式一:创建新的顺序表
		//size_t newsize = _tables.size() * 2;
		//vector<HashData<K, V>> newtables(newsize);

		 旧表重新计算负载到新表
		//for (size_t i = 0; i < _tables.size(); i++)
		//{}

		// 方式二:创建新的哈希表,复用Insert函数
		HashTable<K, V, Hash> newHT;
		newHT._tables.resize(_tables.size() * 2);
		for (size_t i = 0; i < _tables.size(); i++)
		{
			// 当前位置值存在则插入
			if (_tables[i]._state == EXIST)
			{
				newHT.Insert(_tables[i]._kv);
			}
		}
		// 交换两个哈希表的地址
		_tables.swap(newHT._tables);
	}
}

线性探测优点:实现非常简单。
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?


4.1.2. 二次探测


线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:H_i = (H_0 + i^2 )% m, 或者:H_i = (H_0 - i^2 )% m。其中:i =1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
对于1(哈希概念)中如果要插入44,产生冲突,使用解决后的情况为:

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

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

4.2 开散列

4.2.1. 开散列概念


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

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

4.2.2. 开散列实现

// 哈希桶结点定义
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 HashTable
{
	typedef HashNode<K, V> Node;
public:
    // 构造函数,先开辟10个大小空间,并将有效数据个数初始化为0
	HashTable()
	{
		_tables.resize(10, nullptr);
		_n = 0;
	}
	bool Insert(const pair<K, V>& kv)
	{
		// 不能重复插入
		if (Find(kv.first))
			return false;

		// 扩容...
        // _CheckCapacity();
		
		size_t hashi = kv.first % _tables.size();
		Node* newnode = new Node(kv);
		// 头插
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_n;

		return true;
	}

	bool Erase(const K& key)
	{
		// 找到在哪个桶
		size_t hashi = key % _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[hashi];
		while (cur)
		{
			// 找到该值
			if (cur->_kv.first == key)
			{
				// 删除的是第一个节点
				if (prev == nullptr)
				{
					_tables[hashi] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				--_n;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}

	Node* Find(const K& key)
	{
		size_t hashi = key % _tables.size();
		// 找到桶的起始地址
		Node* cur = _tables[hashi];
		// 遍历桶
		while (cur)
		{
			if (cur->_kv.first == key)
				return cur;

			cur = cur->_next;// 迭代往后走
		}
		return nullptr;
	}
private:
	vector<Node*> _tables;
	size_t _n = 0;
};

4.2.3. 开散列增容


桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

void _CheckCapacity()
{
	// 扩容,负载因子为1
	if (_n == _tables.size())
	{
		// 将原节点链接到新表
		vector<Node*> newTables(_tables.size() * 2, nullptr);
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				// 头插新表的位置
				size_t hashi = hs(kv.first) % newTables.size();
				cur->_next = newTables[hashi];
				newTables[hashi] = cur;

				cur = next;
			}
			_tables.swap(newTables);
		}
	}
}

4.2.4. 开散列的思考


1. 只能存储key为整形的元素,其他类型怎么解决?

哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法

// 整形数据不需要转化
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};

// 特化,string类型
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}

		return hash;
	}
};

4.2.5. 开散列与闭散列比较


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

5. 模拟实现


5.1 哈希表的改造


1. 模板参数列表的改造

// K:关键码类型
// V: 不同容器V的类型不同,如果是unordered_map,V代表一个键值对,如果是unordered_set,V 为 K
// KeyOfT: 因为V的类型不同,通过value取key的方式就不同,详细见unordered_map/set的实现
// Hash: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能取模
template<class K,class T,class KeyOfT,class Hash>
class HashTable;

2. 增加迭代器操作

为什么要获取哈希桶数据的指针?

因为当一个桶的中的数据遍历完之后,想要遍历下一个桶的数据,只有通过哈希桶这个表才能获取,因此我们需要一个哈希桶的指针。

// 前置声明,为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,
template<class K, class T, class KeyOfT, class Hash>
class HashTable;

template<class K, class T, class KeyOfT, class Hash>
struct __HTIterator
{
	typedef HashNode<T> Node;
	typedef __HTIterator<K, T, KeyOfT, Hash> Self;
	
	Node* _node;
	// 获取哈希桶数据的指针
	HashTable<K, T, KeyOfT, Hash>* _pht;

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

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

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

	Self& operator++()
	{
		// 当前桶没走完,找当前桶的下一个结点
		if (_node->_next)
		{
			_node = _node->_next;
		}
		else
		{
			Hash hs;
			KeyOfT kot;
			// 当前桶走完了,找下一个不为空的桶的第一个结点
			// 1、找当前结点在桶的哪个位置
			size_t i = hs(kot(_node->_data)) % _pht->_tables.size();
			++i;
			for (; i < _pht->_tables.size(); i++)
			{
				if (_pht->_tables[i])
					break;
			}
			// 循环结束有两种情况,一种break结束,一种循环结束
			// 循环结束,该结点的下一个为nullptr
			if (i == _pht._tables.size())
			{
				_node = nullptr;
			}
			else
			{
				_node = _pht->_tables[i];
			}
		}
	}
	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}
};

3. 增加通过key获取value操作

template<class K,class T,class KeyOfT,class Hash>
class HashTable
{
	typedef HashNode<T> Node;
public:
	typedef __HTIterator<T&,T*> iterator;
	typedef __HTIterator<const T&, const T*> const_iterator;

	const_iterator begin() const
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			if (cur)
			{
				// this -> const HashTable*
				return const_iterator(cur, this);
			}
		}
		return end();
	}

	const_iterator end() const
	{
		return const_iterator(nullptr, this);
	}

	iterator begin()
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			if (cur)
			{
				// this -> HashTable*
				return iterator(cur, this);
			}
		}
		return end();
	}

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

	HashTable()
	{
		_tables.resize(10, nullptr);
		_n = 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;
		}
	}
	pair<iterator,bool> Insert(const T& data)
	{
		KeyOfT kot;
		// 不能重复插入
		//if (Find(kot(data)))
		//	return false;

		iterator it = Find(kot(data));
		if(it != end())
			return make_pair(it,false);

		Hash hs;
		// 扩容,负载因子为1
		if (_n == _tables.size())
		{
			// 将原节点链接到新表
			vector<Node*> newTables(_tables.size() * 2, nullptr);
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					// 头插新表的位置
					size_t hashi = hs(kot(cur->_data)) % newTables.size();
					cur->_next = newTables[hashi];
					newTables[hashi] = cur;

					cur = next;
				}
				_tables[i] = nullptr;
			}
			_tables.swap(newTables);
		}

		size_t hashi = hs(kot(data)) % _tables.size();
		Node* newnode = new Node(data);
		// 头插
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_n;

		return make_pair(iterator(newnode,this),true);
	}

	iterator Find(const K& key)
	{
		KeyOfT kot;
		Hash hs;
		size_t hashi = hs(key) % _tables.size();
		// 找到桶的起始地址
		Node* cur = _tables[hashi];
		// 遍历桶
		while (cur)
		{
			if (kot(cur->_data) == key)
				return iterator(cur, this);

			cur = cur->_next;// 迭代往后走
		}
		// 没找到返回end()
		return end();
	}

	bool Erase(const K& key)
	{
		KeyOfT kot;
		Hash hs;
		// 找到在哪个桶
		size_t hashi = hs(key) % _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[hashi];
		while (cur)
		{
			// 找到该值
			if (kot(cur->_data) == key)
			{
				// 删除的是第一个节点
				if (prev == nullptr)
				{
					_tables[hashi] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				--_n;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}

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

5.2 unordered_set

template<class K,class Hash = HashFunc<K>>
class unordered_set
{
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:
	typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;
	typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;

	const_iterator begin() const
	{
		return _ht.begin();
	}
	const_iterator end() const
	{
		return _ht.end();
	}

	iterator begin()
	{
		return _ht.begin();
	}
	iterator end()
	{
		return _ht.end();
	}
	pair<iterator,bool> insert(const K& key)
	{
		return _ht.Insert(key);
	}

private:
	hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;

5.3 unordered_map

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:
	typedef typename hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT, Hash>::iterator iterator;
	typedef typename hash_bucket::HashTable<K, pair<const K,const V>, MapKeyOfT, Hash>::const_iterator const_iterator;

	const_iterator begin() const
	{
		return _ht.begin();
	}
	const_iterator end() const
	{
		return _ht.end();
	}
	iterator begin()
	{
		return _ht.begin();
	}
	iterator end()
	{
		return _ht.end();
	}
	pair<iterator,bool> insert(const pair<K,V>& kv)
	{
		return _ht.Insert(kv);
	}

	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = insert(make_pair(key, V()));
		return ret.first->second;
	}
private:
	hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT, Hash> _ht;
};

6 完整代码

HashTable.h

#pragma once
#include<vector>

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

// 特化,string类型
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 open_address
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state;
	};



	// 单独string版本仿函数
	struct StringHashFunc
	{
		// abcd
		// bcad
		// aadd
		// BKDR
		size_t operator()(const string& key)
		{
			size_t hash = 0;
			for (auto ch : key)
			{
				hash *= 131;
				hash += ch;
			}

			return hash;
		}
	};

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

		bool Insert(const pair<K, V>& kv)
		{
			// 有该则返回false
			if (Find(kv.first))
				return false;

			// 扩容,载荷因子(有效数据个数/总的数据个数) >= 0.7则扩容
			// 整数相除还是整数,解决办法 : 1、*10 >= 7    2、*1.0成浮点数 
			if (_n * 10 / _tables.size() >= 7)
			{
				 方式一:创建新的顺序表
				//size_t newsize = _tables.size() * 2;
				//vector<HashData<K, V>> newtables(newsize);

				 旧表重新计算负载到新表
				//for (size_t i = 0; i < _tables.size(); i++)
				//{}

				// 方式二:创建新的哈希表,复用Insert函数
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(_tables.size() * 2);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					// 当前位置值存在则插入
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}
				// 交换两个哈希表的地址
				_tables.swap(newHT._tables);
			}

			// 计算放置数据的下标,key值可能不是整型,使用仿函数
			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();

			// 线性探测
			while (_tables[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _tables.size();
			}

			// 更新数据
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;

			return true;
		}
		HashData<K, V>* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			// 线性探测,存在/删除状态均需查找
			while (_tables[hashi]._state != EMPTY)
			{
				// 删除状态不能查找
				if (_tables[hashi]._state != DELETE &&
					_tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}

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

			return nullptr;
		}
		bool Erase(const K& key)
		{
			// 查找是否有该key值,有则将状态修改为删除并--n,没有则返回false
			HashData<K, V>* ret = Find(key);
			if (ret == nullptr)
				return false;
			else
			{
				ret->_state = DELETE;
				--_n;

				return true;
			}
		}
	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;// 有效数据个数
	};
	void TestHT1()
	{
		int a[] = { 10001,11,55,24,19,12,31 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		cout << ht.Find(55) << endl;
		cout << ht.Find(31) << endl;

		ht.Erase(55);
		cout << ht.Find(55) << endl;
		cout << ht.Find(31) << endl;
	}
	void TestHT2()
	{
		int a[] = { 10001,11,55,24,19,12,31 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(32, 32));
		ht.Insert(make_pair(32, 32));
	}
	// key不支持强转整形取模,那么就要自己提供转换成整形仿函数
	void TestHT3()
	{
		HashTable<string, int> ht;
		ht.Insert(make_pair("sort", 1));
		ht.Insert(make_pair("left", 1));
		ht.Insert(make_pair("insert", 1));

		cout << StringHashFunc()("bacd") << endl;
		cout << StringHashFunc()("abcd") << endl;
		cout << StringHashFunc()("aadd") << endl;
	}
	void test_map1()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
	"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
		unordered_map<string, int> countMap;
		for (auto& e : arr)
		{
			countMap[e]++;
		}

		cout << countMap.load_factor() << endl;
		cout << countMap.max_load_factor() << endl;
		cout << countMap.size() << endl;
		cout << countMap.bucket_count() << endl;
		cout << countMap.max_bucket_count() << endl;

		for (auto& kv : countMap)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;
	}
}

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 Hash>
	class HashTable;

	//template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
	//struct __HTIterator
	//{
	//	typedef HashNode<T> Node;
	//	typedef __HTIterator<K, T, KeyOfT, Hash> Self;
	//	
	//	Node* _node;
	//	// 获取哈希桶数据的指针
	//	HashTable<K, T, KeyOfT, Hash>* _pht;

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

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

	//	Self& operator++()
	//	{
	//		// 当前桶没走完,找当前桶的下一个结点
	//		if (_node->_next)
	//		{
	//			_node = _node->_next;
	//		}
	//		else
	//		{
	//			Hash hs;
	//			KeyOfT kot;
	//			// 当前桶走完了,找下一个不为空的桶的第一个结点
	//			// 1、找当前结点在桶的哪个位置
	//			size_t i = hs(kot(_node->_data)) % _pht->_tables.size();
	//			++i;
	//			for (; i < _pht->_tables.size(); i++)
	//			{
	//				if (_pht->_tables[i])
	//					break;
	//			}
	//			// 循环结束有两种情况,一种break结束,一种循环结束
	//			// 循环结束,该结点的下一个为nullptr
	//			if (i == _pht._tables.size())
	//			{
	//				_node = nullptr;
	//			}
	//			else
	//			{
	//				_node = _pht->_tables[i];
	//			}
	//		}
	//	}
	//	bool operator!=(const Self& s)
	//	{
	//		return _node != s._node;
	//	}
	//};

	template<class K,class T,class KeyOfT,class Hash>
	class HashTable
	{
		typedef HashNode<T> Node;
	public:
		// 友元类 __HTIterator可以访问HashTable私有成员
		//template<class K, class T, class KeyOfT, class Hash>
		//friend struct __HTIterator;

		// 内部类
		template<class Ref,class Ptr>
		struct __HTIterator
		{
			typedef HashNode<T> Node;
			typedef __HTIterator Self;

			Node* _node;
			// 获取哈希桶数据的指针
			const HashTable* _pht;// 加const时const迭代器才能构造成功

			__HTIterator(Node* node, const HashTable* pht)
				:_node(node)
				, _pht(pht)
			{}

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

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

			Self& operator++()
			{
				// 当前桶没走完,找当前桶的下一个结点
				if (_node->_next)
				{
					_node = _node->_next;
				}
				else
				{
					Hash hs;
					KeyOfT kot;
					// 当前桶走完了,找下一个不为空的桶的第一个结点
					// 1、找当前结点在桶的哪个位置
					size_t i = hs(kot(_node->_data)) % _pht->_tables.size();
					++i;
					for (; i < _pht->_tables.size(); i++)
					{
						if (_pht->_tables[i])
							break;
					}
					// 循环结束有两种情况,一种break结束,一种循环结束
					// 循环结束,该结点的下一个为nullptr
					if (i == _pht->_tables.size())
					{
						_node = nullptr;
					}
					else
					{
						_node = _pht->_tables[i];
					}
				}
				return *this;
			}
			bool operator!=(const Self& s)
			{
				return _node != s._node;
			}
		};

		typedef __HTIterator<T&,T*> iterator;
		typedef __HTIterator<const T&, const T*> const_iterator;


		const_iterator begin() const
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
				{
					// this -> const HashTable*
					return const_iterator(cur, this);
				}
			}
			return end();
		}

		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}

		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
				{
					// this -> HashTable*
					return iterator(cur, this);
				}
			}
			return end();
		}

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

		HashTable()
		{
			_tables.resize(10, nullptr);
			_n = 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;
			}
		}
		pair<iterator,bool> Insert(const T& data)
		{
			KeyOfT kot;
			// 不能重复插入
			//if (Find(kot(data)))
			//	return false;

			iterator it = Find(kot(data));
			if(it != end())
				return make_pair(it,false);

			Hash hs;
			// 扩容,负载因子为1
			if (_n == _tables.size())
			{
				// 方式一:调用Insert函数,会对所有结点右一份拷贝,效率较低
				//HashTable<K, V> newHT;
				//newHT._tables.resize(_tables.size() * 2);
				//for (size_t i = 0; i < _tables.size(); i++)
				//{
				//	Node* cur = _tables[i];
				//	// 将每个桶中不为空的结点插入
				//	while (cur)
				//	{
				//		newHT.Insert(cur->_kv);
				//		cur = cur->_next;
				//	}
				//}
				 交换两个哈希表的地址
				//_tables.swap(newHT._tables);

				// 方式二:将原节点链接到新表
				vector<Node*> newTables(_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						// 头插新表的位置
						size_t hashi = hs(kot(cur->_data)) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}

			size_t hashi = hs(kot(data)) % _tables.size();
			Node* newnode = new Node(data);
			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return make_pair(iterator(newnode,this),true);
		}

		iterator Find(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			// 找到桶的起始地址
			Node* cur = _tables[hashi];
			// 遍历桶
			while (cur)
			{
				if (kot(cur->_data) == key)
					return iterator(cur, this);

				cur = cur->_next;// 迭代往后走
			}
			// 没找到返回end()
			return end();
		}

		bool Erase(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			// 找到在哪个桶
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				// 找到该值
				if (kot(cur->_data) == key)
				{
					// 删除的是第一个节点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

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

		//vector<list<pair<K, V>>> _tables;
	};
	//void TestHT1()
	//{
	//	int a[] = { 10001,11,55,24,19,12,31,4,34,44};
	//	HashTable<int, int> ht;
	//	for (auto e : a)
	//	{
	//		ht.Insert(make_pair(e, e));
	//	}

	//	ht.Insert(make_pair(32, 32));
	//	ht.Insert(make_pair(32, 32));

	//	ht.Erase(31);
	//	ht.Erase(11);
	//}
	//void TestHT2()
	//{
	//	HashTable<string, int> ht;
	//	ht.Insert(make_pair("sort", 1));
	//	ht.Insert(make_pair("left", 1));
	//	ht.Insert(make_pair("insert", 1));
	//}
}

my_unordered_set.h

#pragma once
#include "HashTable.h"

namespace lin
{
	template<class K,class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;
		typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;

		const_iterator begin() const
		{
			return _ht.begin();
		}
		const_iterator end() const
		{
			return _ht.end();
		}

		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		pair<iterator,bool> insert(const K& key)
		{
			return _ht.Insert(key);
		}

	private:
		hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
	};
	void Func(const unordered_set<int>& s)
	{
		unordered_set<int>::iterator it = s.begin();
		while (it != s.end())
		{
			//*it = 1;
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
	void test_unordered_set()
	{
		unordered_set<int> s;
		s.insert(31);
		s.insert(11);
		s.insert(5);
		s.insert(15);
		s.insert(25);

		unordered_set<int>::iterator it = s.begin();
		while (it != s.end())
		{
			//*it = 1;
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : s)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

my_unordered_map.h

#pragma once
#include "HashTable.h"

namespace lin
{
	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:
		typedef typename hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT, Hash>::iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K,const V>, MapKeyOfT, Hash>::const_iterator const_iterator;


		const_iterator begin() const
		{
			return _ht.begin();
		}
		const_iterator end() const
		{
			return _ht.end();
		}
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		pair<iterator,bool> insert(const pair<K,V>& kv)
		{
			return _ht.Insert(kv);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return ret.first->second;
		}
	private:
		hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT, Hash> _ht;
	};
	void test_unordered_map()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
	"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
		unordered_map<string, int> countMap;
		for (auto& e : arr)
		{
			countMap[e]++;
		}

		unordered_map<string, int>::iterator it = countMap.begin();
		while (it != countMap.end())
		{
			//it->first += 'x'; // key不能修改
			it->second += 1;  // value可以修改
			cout << it->first << ":" << it->second << endl;
			++it;
		}
		cout << endl;

		for (auto& kv : countMap)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;
	}
}

Test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

#include "HashTable.h"
#include "my_unordered_set.h"
#include "my_unordered_map.h"
int main()
{
	//open_address::TestHT3();
	//open_address::test_map1();
	//hash_bucket::TestHT2();
	//lin::test_unordered_set();
	lin::test_unordered_map();
	return 0;
}

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

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

相关文章

安卓逆向百例十-币coin

typora-root-url: ./pic安卓逆向百例十-币coin 现在售价依旧是99&#xffe5;,计划更新100案例&#xff0c;平均一个案例1块钱&#xff0c;要什么自行车&#xff01; 案例起源&#xff1a; 有位老哥 经过炒币失败后想做一个这种查询接口的app&#xff0c;但是苦于他的资金比…

windows系统搭建WSUS更新服务问题整理

1、连接微软更新服务器时&#xff0c;总是连接失败&#xff0c;采取了指定TLS1.2连接的方法&#xff0c;还是连接失败。 解决方法&#xff1a;当时服务器的操作系统为windows server 2012 R2&#xff0c;将操作系统换成windows server 2016可以解决这个问题。 2、客户端怎么配…

Linux发送邮件:如何配置SMTP服务器发信?

linux发送邮件至多个收件人的方法&#xff1f;如何用Linux命令&#xff1f; 在Linux系统中&#xff0c;邮件发送是一个常见且重要的功能&#xff0c;无论是用于系统监控通知还是日常通信。AokSend将详细介绍如何在Linux环境下配置SMTP服务器&#xff0c;以确保您的邮件发送既高…

探索RAG与Multi-Agent的结合:解决复杂任务的新方法

最近帮企业定制了一个langgraphrag的项目&#xff0c;跟大家简单介绍一下设计架构和具体的应用。如果大家有兴趣&#xff0c;我也可以出一期视频&#xff0c;给大家详细介绍一下。我们会一步步探讨如何构建一个可以控的Agent&#xff0c;以执行RAG任务&#xff0c;并最终展示一…

C#为复杂属性提供下拉式编辑框和弹出式编辑框

一.为属性提供编辑类 弹出式和下拉式是如何实现的呢&#xff0c;这需要为属性提供一个专门的编辑类。.Net为我们提供了一个System.Drawing.Design.UITypeEditor类&#xff0c;它是所有编辑类的基类&#xff0c;从他继承出了诸如ColorEditor、FontEditor的类&#xff0c;因此我们…

信刻光盘摆渡系统安全合规实现跨网数据单向导入/导出

在当今信息化、数字化时代&#xff0c;各种数据传输和储存技术发展迅速&#xff0c;各安全领域行业对跨网数据交互需求日益迫切&#xff0c;数据传输的安全可靠性对于整个过程的重要性不可忽视。应如何解决网络安全与效率之间的矛盾&#xff0c;如何安全合规地实现跨网数据单向…

Spring Cloud Gateway 之动态路由

前言 熟悉 Spring Cloud Gateway 的人都知道 Gateway 提供了鉴权、路由等功能&#xff0c;本篇我们重点分析的是 Gateway 的动态路由功能。 Gateway Actuator API 方法源码解析 Spring Cloud Gateway 的 Actuator 端点允许监视 Spring Cloud Gateway 应用程序并与之交互&…

k8s学习(三十八) 使用OpenTelemetry+jaeger实现链路追踪

文章目录 前言一、安装jaeger二、安装cert-manager三、安装OpenTelemetry Operator四、配置 OpenTelemetry Collector五、配置 Instrumentation六、编写java示例程序并测试调用链跟踪 前言 OpenTelemetry 可以用于从应用程序收集数据。它是一组工具、API 和 SDK 集合&#xff…

《黑神话:悟空》Steam全球评价出炉:18个语言区好评率超90%

《黑神话&#xff1a;悟空》Steam在线人数已经不断打破纪录&#xff0c;连续三天刷榜&#xff0c;目前最高成绩超过241万。这个成绩也稳坐总榜第二&#xff0c;同时也是单机游戏的历史第一。 除了游玩人数之外&#xff0c;该作的评价口碑也非常出色&#xff0c;根据媒体汇总的数…

Flutter->`Flutter` 通过`ffi`调用`Rust`编译生成的产物.so文件(Android)和.a文件(iOS)接口方法

flutter_rust_ffi Flutter 通过ffi调用Rust编译生成的产物.so文件(Android)和.a文件(iOS)接口方法; 拾用本文您将获取以下技能: Rust编译.so文件的能力;Rust编译.a文件的能力;Flutter调用.so文件的能力;Flutter调用.a文件的能力; 附加Buff: Flutter环境安装指南;Rust环境安…

游戏行业如此竞争激烈,个人开发者是否仍存机会?

在当今这个数字化时代&#xff0c;游戏行业以其庞大的市场规模、高速的增长速度以及无限的创意空间&#xff0c;吸引了无数开发者投身其中。然而&#xff0c;随着技术的进步、资本的涌入以及大型游戏公司的强势扩张&#xff0c;游戏行业的竞争日益激烈&#xff0c;似乎形成了一…

选择合适系统

选择合适系统 原厂SDK系统 硬件兼容性 ⭐⭐⭐⭐⭐软件功能完善度 ⭐⭐⭐⭐⭐开发使用难度 ⭐⭐⭐⭐⭐烧写工具 全志自家烧录器。 TinaSDK-4.0 TF卡系统镜像 tina_v851se-tinyvision_uart0.img 默认TinaSDK编译出来 支持ADB 和默认SDK兼容性最好 tina-4.0_cameratest_ti…

使用知识图谱,大幅提升RAG准确性

大家好&#xff0c;图形检索—增强生成&#xff08;GraphRAG&#xff09;的发展势头日益强劲&#xff0c;已成为传统向量搜索检索方法的有力补充。这种方法利用图数据库的结构化特性&#xff0c;将数据组织为节点和关系&#xff0c;从而增强了检索信息的深度和上下文关联性。 知…

Stablediffusion有哪几种模型,小白入门必看!

前言 在Stable Diffusion中&#xff0c;模型有好几种&#xff0c;不同插件有不同的模型&#xff0c;分别作用于不同的功能。 今天卧龙君就带着大家一起来了解一下。 大模型&#xff1a;Stable Diffusion StableDiffusion大模型&#xff0c;可以理解为绘画风格集合&#xff…

16:【stm32】I2C的使用一:I2C片上外设的使用

I2C 1、片上外设1.1&#xff1a;寄存器与内部结构 2、通过I2C向外发送数据2.1&#xff1a;I2C的初始化2.1.1&#xff1a;初始化SCL和SDA2.1.2&#xff1a;使能时钟PCLK1&#xff08;APB1&#xff09;2.1.3&#xff1a;配置I2C1的参数 2.2&#xff1a;发送数据2.2.1&#xff1a;…

P2P 文件共享:现代网络中的高效文件传输

在互联网的世界中&#xff0c;不同应用程序的数据传输方法各异。P2P文件共享&#xff08;Peer-to-Peer File Sharing&#xff09; 作为一种高效的文件传输方式&#xff0c;使得用户可以在没有中央服务器的情况下直接进行文件交换。本文将详细介绍P2P文件共享的基本原理、优势及…

【通俗理解】CNN复杂度——卷积神经网络的计算成本解析

【通俗理解】CNN复杂度——卷积神经网络的计算成本解析 关键词提炼 #CNN复杂度 #卷积神经网络 #计算成本 #输入数据尺寸 #卷积核大小 #卷积核数量 #复杂度公式 第一节&#xff1a;CNN复杂度的类比与核心概念【尽可能通俗】 1.1 CNN复杂度的类比 CNN的复杂度就像是烹饪一道大…

引擎切换pdf识别简历分析

文章目录 1.EasyCode生成interview_history的crud1.在模板设置中手动指定逻辑删除的值2.生成代码&#xff0c;进行测试 2.PDF识别关键字1.引入依赖2.代码概览3.PDFUtil.java4.keyword1.EndType.java2.FlagIndex.java3.WordType.java4.KeyWordUtil.java 3.策略模式实现引擎切换&…

QT-window记事本

QT-window记事本 一、演示效果二、核心代码三、下载连接 一、演示效果 二、核心代码 #include <QMessageBox> #include <QFileDialog> #include <QDebug> #include <QProcess> #include <QDesktopServices> #include <QDateTime> #includ…

孙宇晨:区块链领域的坚韧领航者,以智慧铸就行业基石

​ 在区块链领域&#xff0c;孙宇晨以其卓越的智慧与不懈的韧性&#xff0c;成为行业内备受瞩目的领军人物。从创立波场 TRON 到引领去中心化的变革&#xff0c;孙宇晨始终以坚定的信念和独特的战略眼光推动着区块链技术的发展。 孙宇晨的成功不仅仅是因为他对技术的深入…