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

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


一、所用的哈希代码

我们要实现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模型
	};
}

猜你喜欢

转载自blog.csdn.net/m0_70088010/article/details/133513599