【STL】用哈希表(桶)封装出unordered_set和unordered_map

news2024/12/24 3:08:52

⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++进阶
⭐代码仓库:C++进阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!

用哈希表(桶)封装出unordered_set和unordered_map

  • 一、所用的哈希代码
  • 二、哈希模板参数
    • 1、T模板参数
    • 2、仿函数
    • 3、string类型无法取模
  • 三、哈希表成员函数
    • 1、构造函数
    • 2、拷贝构造函数
    • 3、赋值运算符重载
    • 4、析构函数
    • 5、哈希桶的正向迭代器
      • 搭个框架
      • 构造函数
      • 正向迭代器解引用操作
      • 正向迭代器指针指向操作
      • 判断两个正向迭代器是否相等
      • operator++运算符
    • 6、const迭代器
    • 7、哈希表中的操作
      • (1)begin() 和 end()
      • (2)GetNextPrime
      • (3)Find函数修改
      • (4)Insert函数修改
      • (5)Erase函数修改
  • 四、Unordered_set和Unordered_map封装
  • 五、代码汇总
    • HashTable.h
    • Unordered_map.h
    • Unordered_set.h


一、所用的哈希代码

我们要实现KV模型的哈希并且进行封装unordered_set和unordered_map,那么我们就用哈希桶来解决这个问题。

#include<vector>
#include<string>
using namespace std;

namespace HashT
{
	// 将kv.first强转成size_t
	// 仿函数
	template<class K>
	struct DefaultHashTable
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	// 模版的特化(其他类型) 
	template<>
	struct DefaultHashTable<string>
	{
		size_t operator()(const string& str)
		{
			// BKDR
			size_t hash = 0;
			for (auto ch : str)
			{
				// 131是一个素数,也可以用其他一些素数代替,主要作用防止两个不同字符串计算得到的哈希值相等
				hash *= 131;
				hash += ch;
			}

			return hash;
		}
	};


	template<class K, class V>
	struct HashNode
	{
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{}

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

	//哈希表
	template<class K, class V, class HashFunc = DefaultHashTable<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_table.resize(10, nullptr);
		}

		// 插入
		bool Insert(const pair<K, V>& kv)
		{
			// 1查看哈希表中是否存在该键值的键值对,若已存在则插入失败
			// 2判断是否需要调整负载因子,若负载因子过大都需要对哈希表的大小进行调整
			// 3将键值对插入哈希表
			// 4哈希表中的有效元素个数加一


			HashFunc hf;
			// 1、查看键值对,调用Find函数
			Node* key_ = Find(kv.first);
			if (key_) // 如果key_是存在的则插入不了
			{
				return false; // 插入不了
			}

			// 2、判断负载因子,负载因子是1的时候进行增容
			if (_n == _table.size()) // 整个哈希表都已经满了
			{
				// 增容
				// a.创建一个新表,将原本容量扩展到原始表的两倍
				int HashiNewSize = _table.size() * 2;
				vector<Node*> NewHashiTable;
				NewHashiTable.resize(HashiNewSize, nullptr);

				// b.遍历旧表,顺手牵羊,将原始表数据逐个头插到新表中
				for (size_t i = 0; i < _table.size(); ++i)
				{
					if (_table[i]) // 这个桶中的有数据/链表存在
					{
						Node* cur = _table[i]; // 记录头结点
						while (cur)
						{
							Node* next = cur->_next; // 记录下一个结点
							size_t hashi = hf(kv.first) % _table.size(); // 记录一下新表的位置
							// 头插到新表中
							cur->_next = _table[hashi];
							_table[hashi] = cur;

							cur = next; // 哈希这个桶的下一个结点
						}
						_table[i] = nullptr;
					}
				}
				// c.交换两个表
				_table.swap(NewHashiTable);
			}

			// 3、将键值对插入到哈希表中
			size_t hashii = hf(kv.first) % _table.size();
			Node* newnode = new Node(kv);
			// 头插法
			newnode->_next = _table[hashii];
			_table[hashii] = newnode;

			// 4、将_n++
			++_n;
			return true;
		}

		// 查找
		HashNode<K, V>* Find(const K& key)
		{
			//1通过哈希函数计算出对应的哈希地址
			//2通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();

			Node* cur = _table[hashi]; // 刚好到哈希桶的位置
			while (cur)
			{
				// 找到匹配的了
				if (cur->_kv.first == key)
				{
					return (HashNode<K, V>*)cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		// 删除
		bool Erase(const K& key)
		{
			//1通过哈希函数计算出对应的哈希桶编号
			//2遍历对应的哈希桶,寻找待删除结点
			//3若找到了待删除结点,则将该结点从单链表中移除并释放
			//4删除结点后,将哈希表中的有效元素个数减一
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			// prev用来记录前面一个结点,有可能是删除的是哈希桶第一个结点
			// cur记录的是当前结点,当然是要删除的结点
			Node* prev = nullptr;
			Node* cur = _table[hashi];

			while (cur) // 遍历到结尾
			{
				if (cur->_kv.first == key) // 刚好找到这个值
				{
					// 第一种情况:这个要删除的值刚好是哈希桶的头结点
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					// 第二种情况:这个要删除的值不是哈希桶的头结点,而是下面挂的值
					else // (prev != nullptr)
					{
						prev->_next = cur->_next;
					}
					delete cur; // 删除cur结点
					_n--;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

	public:
		// 打印一下
		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");
			}
		}

	private:
		vector<Node*> _table; //哈希表
		size_t _n = 0; //哈希表中的有效元素个数
	};
}

二、哈希模板参数

我们根据之前所学习的内容,明白unordered_set是K模型,unordered_map是KV模型,而我们想要实现一个哈希桶封装这两个容器,那必然需要牺牲一个容器的利益,那么就是牺牲set的利益,让set跟着map来,大不了两个模版参数一样不就好了吗,ok!

1、T模板参数

我们为了和哈希表原函数的模板做区分,所以我们将hash的第二个参数变成T模版:

在这里插入图片描述

而我们的上层使用的是unordered_set的话,我们就是K模型,那么我们传入hash中应该是key,key模型,如下:

template<class K>
class Unordered_Set
{
public:

private:
	HashT::HashTable<K, K> _ht; // 传到hash中是key, key模型
};

我们的上层使用的是unordered_map的话,我们就是key_value模型,那么我们传入hash中应该是key, 以及key_value的键值对模型,如下:

template<class K, class V>
class Unordered_Map
{
public:

private:
	HashT::HashTable<K, pair<K, V>> _ht; // 传到hash中是pair<K, V>模型的键值对
};

我们来一个关系图表现一下关系结构:

T的类型是啥,取决于上层传来的是啥类型,上层传来key类型,那么就是T接收的是key,上层传来的是pair<key, value>键值对的话,T接收的就是pair类型。哈希结点的模板参数也应该由原来的K、V变为T,因为结点只接收一个类型,key就是键值,key,value就是键值对。
在这里插入图片描述

我们有了上面的的例子,我们的哈希桶结点也需要重新定义一下:

	template<class T>
	struct HashNode
	{
		HashNode(const T data)
			:_kv(kv)
			, _next(nullptr)
		{}

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

2、仿函数

为什么有个仿函数在这里呢?因为我们的map是键值对,也就是pair<k,v>结构的,我们主要要拿到的是键值key,而我们的编译器/容器底层根本分不清哪个是键值对哪个是键值,也并不知道哈希节点存的到底是个啥类型,所以我们要向上层提供一个仿函数来告诉上层我们所要取到的是键值key,所以此时引入一个仿函数如下:

template<class K, class V>
class Unordered_Map
{
	struct KeyOfMap // 仿函数,取key
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;  // 取key
		}
	};
public:

private:
	HashT::HashTable<K, pair<K, V>, KeyOfMap> _ht; // 传到hash中是pair<K, V>模型的键值对
};

同样了,set也需要引入仿函数,也就是一个陪跑作用,因为它本身存储的是键值,没存储键值对,但因为编译器分不清,所以还是跟着陪跑,写一下仿函数。还是一个很好理解的例子,女朋友map去逛街,作为男朋友set必须得去陪着,不然挨一顿骂。

template<class K>
class Unordered_Set
{
	struct KeyOfSet // 仿函数
	{
		const K& operator()(const K& key)
		{
			return key; // 陪跑功能
		}
	};
public:
	
private:
	HashT::HashTable<K, K> _ht; // 传到hash中是key, key模型
};

我们加一个仿函数用这个仿函数模板来接收。
在这里插入图片描述

3、string类型无法取模

字符串无法取模,是哈希问题中最常见的问题,string类的不能取模,无法比较该咋办呢?

因为是整型能够通过仿函数直接拿到,整型类的仿函数我们能够直接写出来并拿到,但是我们日常生活中用string类的还是挺多的,就比如我们的字典需要用英文的字母组词,所以我们用各个英文名做键值,而字符串并不是整型,也就意味着字符串不能直接用于计算哈希地址,我们需要通过某种方法将字符串转换成整型后,才能代入哈希函数计算哈希地址。而有人就提出来了,我们按照字母进行转化成数字就好了,可是计算机中存储整型毕竟是有限的,而字母组成是无限的,这就导致了我们无论用什么方法进行转化成整型,其哈希冲突是不可避免的,也就是哈希桶下必然会挂结点,而我们需要考虑的是怎么样让哈希冲突更小,概率更低。

经过前辈们实验后发现,BKDRHash算法无论是在实际效果还是编码实现中,效果都是最突出的。该算法由于在Brian Kernighan与Dennis Ritchie的《The C Programing Language》一书被展示而得名,是一种简单快捷的hash算法,也是Java目前采用的字符串的hash算法

那我们就加一个仿函数吧!一个用来控制将string转化成整型,另一个就用来默认的整形,也就是其本身传入的就是整型,那么就不需要过多地进行转换,直接是整型即可。

	// 将kv.first强转成size_t
	// 仿函数
	template<class K>
	struct DefaultHashTable
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	// 模版的特化(其他类型) 
	template<>
	struct DefaultHashTable<string>
	{
		size_t operator()(const string& str)
		{
			// BKDR
			size_t hash = 0;
			for (auto ch : str)
			{
				// 131是一个素数,也可以用其他一些素数代替,主要作用防止两个不同字符串计算得到的哈希值相等
				hash *= 131;
				hash += ch;
			}

			return hash;
		}
	};

三、哈希表成员函数

1、构造函数

构造函数中有两个成员变量,一个是哈希表,一个是数量,我们将其进行定义:
第一个是vector类型的,直接能进行默认拷贝构造,下一个给一个缺省的哈希表中的有效元素个数为0。

在这里插入图片描述

但因为后续我们要进行拷贝构造,编写了拷贝构造函数后,默认的构造函数就不会生成了,此时我们需要使用default关键字显示指定生成默认构造函数。

在这里插入图片描述

在这里插入图片描述

2、拷贝构造函数

这个拷贝需要深拷贝,因为要一个个点挂过去,浅拷贝拷贝出来的哈希表和原哈希表中存储的都是同一批结点。

拷贝构造的逻辑如下:

  1. 将哈希表调整到ht._table.size()一样的大小。
  2. 将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中
  3. 更改哈希表中有效的数据的量
		// 拷贝构造
		HashTable(const HashTable& ht)
		{
			//1. 将哈希表调整到ht._table.size()一样的大小。
			//2. 将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中
			//3. 更改哈希表中有效的数据的量
			_table.resize(ht._table.size());

			for (size_t i = 0; i < ht._table.size(); ++i)
			{
				if (ht._table[i]) // 桶不为空的时候
				{
					Node* cur = ht._table[i];
					while (cur) // 这个桶的往后结点遍历完
					{
						Node* copy = new Node(cur->_data); // 拷贝结点的创建
						// 头插到桶中
						copy->_next = _table[i];
						_table[i] = cur;
						cur = cur->_next;
					}
				}
			}
			_n = ht._n;
		}

3、赋值运算符重载

直接交换即可,因为是我们依据参数进行间接调用拷贝构造,之后将拷贝构造出来的哈希表和当前哈希表的两个成员变量分别进行交换即可。

		// 运算符重载
		HashTable& operator=(HashTable ht)
		{
			// 通过间接调用拷贝构造进行运算符赋值重载
			_table.swap(ht._table);
			swap(_n, ht._n);
			return *this;
		}

4、析构函数

因为之前我们的哈希表中的结点是new出来的,所以我们就简单地遍历将每一个结点都delete即可。

		// 析构函数
		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_table[i]) // 桶结点不为空
				{
					Node* cur = _table[i]; // 定义当前桶的头结点
					while (cur)
					{
						Node* next = cur->_next;
						delete cur;
						cur = next;
					}
					_table[i] = nullptr; // 哈希桶置空
				}
			}
		}

5、哈希桶的正向迭代器

因为是哈希的正向迭代器实际上是封装了哈希结点指针,但由于++等的操作,可能需要去找哈希桶中下一个不为空的位置,所以每一个迭代器中都含有存储哈希表的地址pht!

搭个框架

	// 哈希正向迭代器
	template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
	struct HTIterator
	{
		typedef HashNode<T> Node; // 哈希结点类型
		typedef HashTable<K, T, KeyOfT, HashFunc> HT; // 哈希表类型
		typedef HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self; // 迭代器类型
		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;  // 迭代器,用来传参的
		
		// 成员变量
		Node* _node;
		const HT* _pht;
	};

构造函数

因为是既有结点也有哈希桶的地址,所以两个都需要进行初始化。

		// 构造函数
		HTIterator(Node* node, const HT* pht)
			:_node(node)
			,_pht(pht)
		{}

正向迭代器解引用操作

返回结点数据的引用即可。

		// Ref
		Ref operator*()
		{
			return _node->_data; // 返回引用
		}

正向迭代器指针指向操作

返回结点数据的地址即可。

		// Ptr
		Ptr operator->()
		{
			return &_node->_data; // 返回地址
		}

判断两个正向迭代器是否相等

直接判断地址即可,因为是只需要看这两个迭代器封装的结点的类型即可。

		// 判断相等
		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		// 判断不相等
		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}

operator++运算符

哈希桶++的逻辑那可比树能简单不少,逻辑很简单:

  1. 若当前结点不是当前哈希桶中的最后一个结点,则++后走到当前哈希桶的下一个结点
  2. 若当前结点是当前哈希桶的最后一个结点,则++后走到下一个非空哈希桶的第一个结点

来个图好理解:
在这里插入图片描述

		Self& operator++()
		{
			//1. 若当前结点不是当前哈希桶中的最后一个结点,则++后走到当前哈希桶的下一个结点
			//2. 若当前结点是当前哈希桶的最后一个结点,则++后走到下一个非空哈希桶的第一个结点

			if (_node->_next) // 桶后面的结点还有
			{
				_node = _node->_next; // ++操作就是这个桶的当前结点的下一个结点
			}
			else // 正好到这个桶的最后一个结点
			{
				HashFunc hf;
				KeyOfT kot;
				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;
					}
					++hashi; // 为空则++
				}
				_node = nullptr;
			}
			return *this;
		}

6、const迭代器

		// const
		HTIterator(const Iterator& it)
			:_node(it._node)
			, _pht(it._pht)
		{}

7、哈希表中的操作

  1. 进行正向和const迭代器类型的typedef,需要注意的是,为了让外部能够使用typedef后的正向迭代器类型iterator,我们需要在public区域进行typedef。
  2. 由于正向迭代器中++运算符重载函数在寻找下一个结点时,会访问哈希表中的成员变量_table,而_table成员变量是哈希表的私有成员,因此我们需要将正向迭代器类声明为哈希表类的友元。

在这里插入图片描述

(1)begin() 和 end()

begin():返回哈希桶中第一个有值结点的第一个位置的迭代器。
end():返回哈希桶最后一个结点的值的后一个元素,也就是空。

		// 正向迭代器的begin()
		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);
		}

		// 正向迭代器的end()
		iterator end()
		{
			// 是最后一个结点的下一个位置也就是空位置
			return iterator(nullptr, this);
		}

		// const迭代器的begin()
		const_iterator begin() const
		{
			// 找第一个有值的头结点桶
			for (size_t i = 0; i < _table.size(); ++i)
			{
				Node* cur = _table[i];
				if (cur)
				{
					return const_iterator(cur, this);
				}
			}
			return const_iterator(nullptr, this);
		}

		// const迭代器的end()
		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}

(2)GetNextPrime

我们在上一篇博客讲过,素数因为它的因子少,所以更加适合当做容量来进行操作(因为哈希桶挂的少,效率高),所以我们总结以2倍的素数进行排列,每次增容的时候取表中的数据即可:

		size_t GetNextPrime(size_t prime)
		{
			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
			};

			size_t i = 0;
			for (; i < PRIMECOUNT; ++i)
			{
				if (primeList[i] > prime)
					return primeList[i];
			}

			return primeList[i];
		}

		// 构造函数
		HashTable()
		{
			_table.resize(GetNextPrime(1), nullptr);
		}

(3)Find函数修改

将哈希表中查找函数返回的结点指针,改为返回由结点指针和哈希表地址构成的正向迭代器

在这里插入图片描述

(4)Insert函数修改

		// 插入
		pair<iterator, bool> Insert(const T& data)
		{
			// 1查看哈希表中是否存在该键值的键值对,若已存在则插入失败
			// 2判断是否需要调整负载因子,若负载因子过大都需要对哈希表的大小进行调整
			// 3将键值对插入哈希表
			// 4哈希表中的有效元素个数加一

			KeyOfT kot;
			HashFunc hf;
			// 1、查看键值对,调用Find函数
			iterator key_ = Find(kot(data));
			if (key_ != end())
			{
				return make_pair(key_, false);
			}

			// 2、判断负载因子,负载因子是1的时候进行增容
			if (_n == _table.size()) // 整个哈希表都已经满了
			{
				// 增容
				// a.创建一个新表,将原本容量扩展到原始表的两倍
				size_t HashiNewSize = GetNextPrime(_table.size());
				vector<Node*> NewHashiTable;
				NewHashiTable.resize(HashiNewSize, nullptr);

				// b.遍历旧表,顺手牵羊,将原始表数据逐个头插到新表中
				for (size_t i = 0; i < _table.size(); ++i)
				{
					if (_table[i]) // 这个桶中的有数据/链表存在
					{
						Node* cur = _table[i]; // 记录头结点
						while (cur)
						{
							Node* next = cur->_next; // 记录下一个结点
							size_t hashi = hf(kot(data)) % _table.size(); // 记录一下新表的位置
							// 头插到新表中
							cur->_next = _table[hashi];
							_table[hashi] = cur;

							cur = next; // 哈希这个桶的下一个结点
						}
						_table[i] = nullptr;
					}
				}
				// c.交换两个表
				_table.swap(NewHashiTable);
			}

			// 3、将键值对插入到哈希表中
			size_t hashii = hf(kot(data)) % _table.size();
			Node* newnode = new Node(data);
			// 头插法
			newnode->_next = _table[hashii];
			_table[hashii] = newnode;

			// 4、将_n++
			++_n;
			return make_pair(iterator(newnode, this), true);
		}

(5)Erase函数修改


		// 删除
		bool Erase(const K& key)
		{
			//1通过哈希函数计算出对应的哈希桶编号
			//2遍历对应的哈希桶,寻找待删除结点
			//3若找到了待删除结点,则将该结点从单链表中移除并释放
			//4删除结点后,将哈希表中的有效元素个数减一
			KeyOfT kot;
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			// prev用来记录前面一个结点,有可能是删除的是哈希桶第一个结点
			// cur记录的是当前结点,当然是要删除的结点
			Node* prev = nullptr;
			Node* cur = _table[hashi];

			while (cur) // 遍历到结尾
			{
				if (kot(cur->_data) == key) // 刚好找到这个值
				{
					// 第一种情况:这个要删除的值刚好是哈希桶的头结点
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					// 第二种情况:这个要删除的值不是哈希桶的头结点,而是下面挂的值
					else // (prev != nullptr)
					{
						prev->_next = cur->_next;
					}
					delete cur; // 删除cur结点
					_n--;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

四、Unordered_set和Unordered_map封装

namespace JRH
{
	template<class K>
	class Unordered_Set
	{
		struct KeyOfSet // 仿函数
		{
			const K& operator()(const K& key)
			{
				return key; // 陪跑功能
			}
		};
	public:
		typedef typename HashT::HashTable<K, K, KeyOfSet>::iterator iterator;
		typedef typename HashT::HashTable<K, K, KeyOfSet>::const_iterator const_iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

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

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

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

		// 查找
		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		// 删除
		void erase(const K& key)
		{
			_ht.Erase(key);
		}

	private:
		HashT::HashTable<K, K, KeyOfSet> _ht; // 传到hash中是key, key模型
	};
}
namespace JRH1
{
	template<class K, class V>
	class Unordered_Map
	{
		struct KeyOfMap // 仿函数,取key
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;  // 取key
			}
		};
	public:
		//现在没有实例化,没办法到HashTable里面找iterator,所以typename就是告诉编译器这里是一个类型,实例化以后再去取
		typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::iterator iterator;
		typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::const_iterator const_iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

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

		const_iterator end() const
		{
			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 = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

		// 查找
		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		// 删除
		void erase(const K& key)
		{
			_ht.Erase(key);
		}

	private:
		HashT::HashTable<K, pair<K, V>, KeyOfMap> _ht; // 传到hash中是pair<K, V>模型的键值对
	};
}

五、代码汇总

HashTable.h

#include<vector>
#include<string>
using namespace std;

namespace HashT
{
	// 将kv.first强转成size_t
	// 仿函数
	template<class K>
	struct DefaultHashTable
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	// 模版的特化(其他类型) 
	template<>
	struct DefaultHashTable<string>
	{
		size_t operator()(const string& str)
		{
			// BKDR
			size_t hash = 0;
			for (auto ch : str)
			{
				// 131是一个素数,也可以用其他一些素数代替,主要作用防止两个不同字符串计算得到的哈希值相等
				hash *= 131;
				hash += ch;
			}

			return hash;
		}
	};


	template<class T>
	struct HashNode
	{
		HashNode(const T data)
			:_data(data)
			, _next(nullptr)
		{}

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

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

	// 哈希正向迭代器
	template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
	struct HTIterator
	{
		typedef HashNode<T> Node; // 哈希结点类型
		typedef HashTable<K, T, KeyOfT, HashFunc> HT; // 哈希表类型
		typedef HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self; // 迭代器类型
		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;  // 迭代器,用来传参的
		
		// 成员变量
		Node* _node;
		const HT* _pht;

		// 构造函数
		HTIterator(Node* node, const HT* pht)
			:_node(node)
			,_pht(pht)
		{}

		// const
		HTIterator(const Iterator& it)
			:_node(it._node)
			, _pht(it._pht)
		{}

		// Ptr
		Ptr operator->()
		{
			return &_node->_data; // 返回地址
		}

		// Ref
		Ref operator*()
		{
			return _node->_data; // 返回引用
		}

		// 判断相等
		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		// 判断不相等
		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}

		Self& operator++()
		{
			//1. 若当前结点不是当前哈希桶中的最后一个结点,则++后走到当前哈希桶的下一个结点
			//2. 若当前结点是当前哈希桶的最后一个结点,则++后走到下一个非空哈希桶的第一个结点

			if (_node->_next) // 桶后面的结点还有
			{
				_node = _node->_next; // ++操作就是这个桶的当前结点的下一个结点
			}

			else // 正好到这个桶的最后一个结点
			{
				HashFunc hf;
				KeyOfT kot;
				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;
					}
					++hashi; // 为空则++
				}
				_node = nullptr;
			}
			return *this;
		}

	};

	//哈希表
	template<class K, class T, class KeyOfT, class HashFunc = DefaultHashTable<K>>
	class HashTable
	{
		typedef HashNode<T> Node;

		// 友元声明
		template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
		friend struct HTIterator;

	public:
		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;
		typedef HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator;

		// 正向迭代器的begin()
		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);
		}

		// 正向迭代器的end()
		iterator end()
		{
			// 是最后一个结点的下一个位置也就是空位置
			return iterator(nullptr, this);
		}

		// const迭代器的begin()
		const_iterator begin() const
		{
			// 找第一个有值的头结点桶
			for (size_t i = 0; i < _table.size(); ++i)
			{
				Node* cur = _table[i];
				if (cur)
				{
					return const_iterator(cur, this);
				}
			}
			return const_iterator(nullptr, this);
		}

		// const迭代器的end()
		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}



		size_t GetNextPrime(size_t prime)
		{
			const int PRIMECOUNT = 28;
			//素数序列
			const size_t primeList[PRIMECOUNT] =
			{
				53ul, 97ul, 193ul, 389ul, 769ul,
				1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
				49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
				1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
				50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
				1610612741ul, 3221225473ul, 4294967291ul
			};

			size_t i = 0;
			for (i = 0; i < PRIMECOUNT; i++)
			{
				if (primeList[i] > prime)
					return primeList[i];
			}
			return primeList[i];
		}

		// 构造函数
		HashTable()
		{
			_table.resize(GetNextPrime(1), nullptr);
		}

		//构造函数
		//HashTable() = default; //显示指定生成默认构造函数

		// 拷贝构造
		HashTable(const HashTable& ht)
		{
			//1. 将哈希表调整到ht._table.size()一样的大小。
			//2. 将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中
			//3. 更改哈希表中有效的数据的量
			_table.resize(ht._table.size());

			for (size_t i = 0; i < ht._table.size(); ++i)
			{
				if (ht._table[i]) // 桶不为空的时候
				{
					Node* cur = ht._table[i];
					while (cur) // 这个桶的往后结点遍历完
					{
						Node* copy = new Node(cur->_data); // 拷贝结点的创建
						// 头插到桶中
						copy->_next = _table[i];
						_table[i] = cur;
						cur = cur->_next;
					}
				}
			}
			_n = ht._n;
		}

		// 运算符重载
		HashTable& operator=(HashTable ht)
		{
			// 通过间接调用拷贝构造进行运算符赋值重载
			_table.swap(ht._table);
			swap(_n, ht._n);
			return *this;
		}

		// 析构函数
		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_table[i]) // 桶结点不为空
				{
					Node* cur = _table[i]; // 定义当前桶的头结点
					while (cur)
					{
						Node* next = cur->_next;
						delete cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
			}
		}

		// 插入
		pair<iterator, bool> Insert(const T& data)
		{
			// 1查看哈希表中是否存在该键值的键值对,若已存在则插入失败
			// 2判断是否需要调整负载因子,若负载因子过大都需要对哈希表的大小进行调整
			// 3将键值对插入哈希表
			// 4哈希表中的有效元素个数加一

			KeyOfT kot;
			HashFunc hf;
			// 1、查看键值对,调用Find函数
			iterator key_ = Find(kot(data));
			if (key_ != end())
			{
				return make_pair(key_, false);
			}

			// 2、判断负载因子,负载因子是1的时候进行增容
			if (_n == _table.size()) // 整个哈希表都已经满了
			{
				// 增容
				// a.创建一个新表,将原本容量扩展到原始表的两倍
				size_t HashiNewSize = GetNextPrime(_table.size());
				vector<Node*> NewHashiTable;
				NewHashiTable.resize(HashiNewSize, nullptr);

				// b.遍历旧表,顺手牵羊,将原始表数据逐个头插到新表中
				for (size_t i = 0; i < _table.size(); ++i)
				{
					if (_table[i]) // 这个桶中的有数据/链表存在
					{
						Node* cur = _table[i]; // 记录头结点
						while (cur)
						{
							Node* next = cur->_next; // 记录下一个结点
							size_t hashi = hf(kot(data)) % _table.size(); // 记录一下新表的位置
							// 头插到新表中
							cur->_next = _table[hashi];
							_table[hashi] = cur;

							cur = next; // 哈希这个桶的下一个结点
						}
						_table[i] = nullptr;
					}
				}
				// c.交换两个表
				_table.swap(NewHashiTable);
			}

			// 3、将键值对插入到哈希表中
			size_t hashii = hf(kot(data)) % _table.size();
			Node* newnode = new Node(data);
			// 头插法
			newnode->_next = _table[hashii];
			_table[hashii] = newnode;

			// 4、将_n++
			++_n;
			return make_pair(iterator(newnode, this), true);
		}

		// 查找
		iterator Find(const K& key)
		{
			//1通过哈希函数计算出对应的哈希地址
			//2通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可
			HashFunc hf;
			KeyOfT kot;
			size_t hashi = hf(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)
		{
			//1通过哈希函数计算出对应的哈希桶编号
			//2遍历对应的哈希桶,寻找待删除结点
			//3若找到了待删除结点,则将该结点从单链表中移除并释放
			//4删除结点后,将哈希表中的有效元素个数减一
			KeyOfT kot;
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			// prev用来记录前面一个结点,有可能是删除的是哈希桶第一个结点
			// cur记录的是当前结点,当然是要删除的结点
			Node* prev = nullptr;
			Node* cur = _table[hashi];

			while (cur) // 遍历到结尾
			{
				if (kot(cur->_data) == key) // 刚好找到这个值
				{
					// 第一种情况:这个要删除的值刚好是哈希桶的头结点
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					// 第二种情况:这个要删除的值不是哈希桶的头结点,而是下面挂的值
					else // (prev != nullptr)
					{
						prev->_next = cur->_next;
					}
					delete cur; // 删除cur结点
					_n--;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

	public:
		// 打印一下
		void Print()
		{
			// 大循环套小循环
			for (size_t i = 0; i < _table.size(); ++i)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				// 小循环
				while (cur)
				{
					//cout << cur->_data;
					cur = cur->_next;
				}
				printf("NULL\n");
			}
		}

	private:
		vector<Node*> _table; //哈希表
		size_t _n = 0; //哈希表中的有效元素个数
	};
}

Unordered_map.h

#include"HashTable.h"

namespace JRH1
{
	template<class K, class V>
	class Unordered_Map
	{
		struct KeyOfMap // 仿函数,取key
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;  // 取key
			}
		};
	public:
		//现在没有实例化,没办法到HashTable里面找iterator,所以typename就是告诉编译器这里是一个类型,实例化以后再去取
		typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::iterator iterator;
		typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::const_iterator const_iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

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

		const_iterator end() const
		{
			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 = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

		// 查找
		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		// 删除
		void erase(const K& key)
		{
			_ht.Erase(key);
		}

	private:
		HashT::HashTable<K, pair<K, V>, KeyOfMap> _ht; // 传到hash中是pair<K, V>模型的键值对
	};
}

Unordered_set.h

#include"HashTable.h"

namespace JRH
{
	template<class K>
	class Unordered_Set
	{
		struct KeyOfSet // 仿函数
		{
			const K& operator()(const K& key)
			{
				return key; // 陪跑功能
			}
		};
	public:
		typedef typename HashT::HashTable<K, K, KeyOfSet>::iterator iterator;
		typedef typename HashT::HashTable<K, K, KeyOfSet>::const_iterator const_iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

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

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

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

		// 查找
		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		// 删除
		void erase(const K& key)
		{
			_ht.Erase(key);
		}

	private:
		HashT::HashTable<K, K, KeyOfSet> _ht; // 传到hash中是key, key模型
	};
}

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

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

相关文章

如何在企业网站里做好网络安全

在当今数字时代&#xff0c;网站不仅仅是企业宣传和产品展示的平台&#xff0c;更是日常生活和商业活动中不可或缺的一部分。然而&#xff0c;随着网络技术不断发展&#xff0c;网站的安全问题日益凸显。保护网站和用户数据的安全已经成为至关重要的任务&#xff0c;以下是一些…

C#学生选课及成绩查询系统

一、项目背景 学生选课及成绩查询系统是一个学校不可缺少的部分&#xff0c;传统的人工管理档案的方式存在着很多的缺点&#xff0c;如&#xff1a;效率低、保密性差等&#xff0c;所以开发一套综合教务系统管理软件很有必要&#xff0c;它应该具有传统的手工管理所无法比拟的…

进程间通信-内存映射

内存映射是通过将一个进程的虚拟内存空间映射到另一个进程的虚拟内存空间来实现的。这样&#xff0c;两个进程可以共享同一块物理内存&#xff0c;从而实现数据的共享。内存映射通常通过操作系统提供的特定系统调用来完成。 下面是使用内存映射进行进程间通信的一般步骤&…

29 drf-Vue个人向总结-2

文章目录 drf项目总结2重写create自定义验证类获取个性化内容 与 lookup_field 的用处重写get_queryset&#xff0c;get_serializer_class类docs帮助文档支付宝支付原理&#xff08;微信同原理&#xff09;使用流程创建公钥私钥使用的理论介绍使用的代码介绍支付宝与Drf的联合使…

扫雷 | C语言 | 简单易懂 | 扫雷相关知识点总结

扫雷思路 相信大家都有玩过扫雷吧&#xff01;其实在我们学习完C语言中函数和数组之后&#xff0c;我们就有能力制作一个简单的扫雷小游戏了。 先考虑扫雷游戏的思路&#xff1a; 扫雷游戏我们需要利用二维数组将其进行初始化以及赋值“雷”&#xff0c;就以9*9个雷盘来分析…

集合-Map系列

系列文章目录 1.集合-Collection-CSDN博客​​​​​​ 2.集合-List集合-CSDN博客 3.集合-ArrayList源码分析(面试)_喜欢吃animal milk的博客-CSDN博客 4.数据结构-哈希表_喜欢吃animal milk的博客-CSDN博客 5.集合-set系列集合-CSDN博客 6.集合-Map系列-CSDN博客 文章目…

指针详解第三部分

1. 字符指针变量 一种指针类型为字符指针 char*&#xff08;下面有两个代码表示了两种不同的用法&#xff09; int main() {char ch w;char* pc &ch;*pc w;return 0; }int main() {const char* pstr "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了…

什么是JWT?深入理解JWT从原理到应用

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《ELement》。&#x1f3af;&#x1f3af; &#x1…

使用 Python 和 wxPython 进行批量文件扩展名替换

引言&#xff1a; 在日常的文件管理中&#xff0c;有时我们需要将一大批文件的扩展名进行替换。手动一个个重命名文件是一项繁琐的任务&#xff0c;但是使用 Python 编程语言和 wxPython 模块可以轻松地实现这一功能。本文将介绍如何使用 Python 和 wxPython 创建一个简单的图形…

Python学习笔记之分支结构与循环结构

Python学习笔记之分支结构与循环结构 一、分支结构 使用关键字if、elif、else 练习1&#xff1a;使用分支结构实现分段函数求值 """分段函数求值""" x float(input("x "))if x > 1:y 3 * x - 5 elif x < -1:y 5 * x 3…

开启创意思维,畅享Mindomo Desktop for Mac思维导图之旅

在数字化时代&#xff0c;我们需要一个强大而直观的工具来整理和展现我们的思维。Mindomo Desktop for Mac&#xff08;Mindomo&#xff09;作为一款免费的思维导图软件&#xff0c;将为您提供卓越的创意思维体验。 Mindomo拥有直观的界面和丰富的功能&#xff0c;让您能够方便…

手搭手Mybatis-Plus多数据源异构数据迁移案例

环境介绍 技术栈 springbootmybatis-plusdruidbaomidoumysqloracledm 软件 版本 mysql 8 IDEA IntelliJ IDEA 2022.2.1 JDK 1.8 Spring Boot 2.7.13 mybatis 2.3.1 pom.xml所需依赖 <dependencies><dependency><groupId>org.springframework.…

【Golang】包

包 Go语言是使用包来组织源代码的&#xff0c;包&#xff08;package&#xff09;是多个 Go 源码的集合&#xff0c;是一种高级的代码复用方案 Go语言中为我们提供了很多内置包&#xff0c;如 fmt、os、io 等。 任何源代码文件必须属于某个包&#xff0c;同时源码文件的第一…

【2】c++设计模式——>UML表示类之间的继承关系

继承也叫作泛化&#xff08;Generalization&#xff09;&#xff0c;用于描述父子类之间的关系&#xff0c;父类又称为基类或者超类&#xff0c;子类又称作派生类。在UML中&#xff0c;继承关系用带空心三角形的实线来表示。 关于继承关系一共有两种&#xff1a;普通继承关系和…

【共享模型-----管程】

文章目录 1. 为什么会出现线程安全问题2. synchronized 解决方案2.1 线程八锁 3. 变量的线程安全分析3.1 局部变量线程安全分析3.2 常见线程安全类 1. 为什么会出现线程安全问题 一段代码块内如果存在对共享资源的多线程读写操作&#xff0c;称这段代码块为临界区 共享资源&a…

一张图看懂 8 种网络协议

什么是网络协议&#xff1f; 网络协议就是计算机之间沟通的语言&#xff0c;为了有效地交流&#xff0c;计算机之间需要一种共同的规则或协议&#xff0c;就像我们和老外沟通之前&#xff0c;要先商量好用哪种语言&#xff0c;要么大家都说中文&#xff0c;要么大家都说英语&a…

优思学院|六西格玛将烹饪和美味提升至极致

最近&#xff0c;我们曾提到一个美国男子如何利用六西格玛来控制糖尿病。这表明六西格玛逐渐被认为是一个不仅可以在工作场所之外使用&#xff0c;尤其不仅限于制造业的系统。 六西格玛的核心理念是改进过程的质量&#xff0c;从而改善最终结果。如果你做了晚餐或尝试了一道新…

ESP32设备驱动-OLED显示BME280传感器数据

OLED显示BME280传感器数据 文章目录 OLED显示BME280传感器数据1、BME280介绍2、硬件准备3、软件准备4、代码实现在本文中,我们将介绍如何使用OLED显示BME280传感器的数据。 1、BME280介绍 BME280 传感器用于测量有关环境温度、大气压力和相对湿度的读数。 它主要用于以低功耗…

2023/10/4 -- ARM

今日任务&#xff1a;QT实现TCP服务器客户端搭建的代码&#xff0c;现象 ser&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);server new QTcpSe…

JMeter性能分析实战一:日常登录接口

负载测试 日常需求&#xff1a;负载测试&#xff01; 对于桥的负载测试&#xff1a;我给你20t的一排车辆&#xff0c;看你能不能撑得住20t&#xff01; 对于系统的负载测试&#xff1a; 逐步增加负载&#xff0c;便于问题的发现和定位&#xff0c;不要操之过急。逐步增加负载…