C++——unordered_map和unordered_set的封装

unordered_map和unordered_set的底层结构用到的都是在哈希表模拟实现中的哈希桶的实现方式,哈希桶的具体实现我已经在哈希表的模拟实现里做过详细的介绍,这边会引用里面的代码进行改造和封装,同时为了方便操作,同样我采用二倍扩容的方式。

一、哈希桶的基本结构

首先对哈希桶的模版参数进行改造,原本我们是直接采用K_V的结构来定义这个哈希桶,但是在封装的过程中,unordered_map和unordered_set的存储数据是不一样的,unordered_set只存一个Key值,unordered_map存储的是key_value的键值对,但是他们在增删查改的中间的行为又是一样的,所以我们给哈希桶多传入几个参数,T表示的是我们要存储的数据,让上层的容器传入决定,这样就能让unordered_map和unordered_set分别存储不同的数据类型。

KeyOfT的模版参数是一个仿函数,因为我们并不知道T里存储的数据究竟是一个Key还是一个Key、Value,所以我要让上层的结构自己实现出怎么从T中取出Key的方法。这里也有人会疑惑,那既然有了KeyOfT的这个仿函数,那为什么还要传入K的模版参数呢?

比如在使用查找的功能的时候,我们是需要用Key找到对应的Value值,如果我们只有KeyOfT的仿函数的话,我们就必须要求用户传入一个完整的数据才能使用查找功能,这样使用起来就会非常的不方便,所以还要让上层的容器确定它的Key的类型是什么。

cpp 复制代码
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t hashi = 0;
			//BKDR
			for (auto e : s)
			{
				hashi += e;
				hashi *= 131;
			}
			return hashi;
		}

	};

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

		// K 为 T 中key的类型
		// T 可能是键值对,也可能是K
		// KeyOfT: 从T中提取key
		// Hash将key转化为整形,因为哈希函数使用除留余数法
		template<class K, class T, class KeyOfT, class Hash>
		class HashTable
		{
			typedef HashNode<T> Node;

		public:

			HashTable()
			{
				_tables.resize(10, 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;
					}
					_tables[i] = nullptr;
				}
			}

			// 插入值为data的元素,如果data存在则不插入
			bool Insert(const T& data)
			{
				KeyOfT kot;
				Hash hs;


				//插入的数据已经存在
				if (it != End())
					return false;

				//负载因子为1时扩容
				if (_n == _tables.size())
				{
					vector<Node*> newHT(_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)) % newHT.size();
							cur->_next = newHT[hashi];
							newHT[hashi] = cur;

							cur = next;
						}
						_tables[i] = nullptr;
					}

					_tables.swap(newHT);
				}

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

			// 在哈希桶中查找值为key的元素,存在返回true否则返回false
			bool Find(const K& key)
			{
				Hash hs;
				size_t hashi = hs(key) % _tables.size();

				//在对应的桶里查找数据
				Node* cur = _tables[hashi];
				KeyOfT kot;
				while (cur)
				{
					if (kot(cur->_data) == key)
						return true;

					cur = cur->_next;
				}

				return false;
			}

			// 哈希桶中删除key的元素,删除成功返回true,否则返回false
			bool Erase(const K& key)
			{
				Hash hs;
				KeyOfT kot;
				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;			// 表中存储数据个数
		};

二、迭代器

这里的迭代器看似要传入很多的模版参数,但是其实和我们在实现list时实现的迭代器没有什么本质上的区别,只不过多套了几个模版参数,同时这里的迭代器是一个单向迭代器,所以只需要实现++的功能即可。这里需要细讲的其实也就只有这个++的操作。

第一种情况,当前桶里还有数据,那么直接接着访问下一个结点即可。

第二种情况,当前桶里的数据都被访问过了,但是哈希表并没有被遍历完,此时我们就要去到哈希表的下一个位置去找元素,这里我们就需要用到这个哈希表的数据了,这里和之前实现过的迭代器都不同的地方就在这里了。我们需要使用到哈希表里的数据,最重要的是我们需要去访问哈希表底层的那个数组,我们不可能因为这个在迭代器里开一个数组来存储哈希表里每个头结点的信息,这样不仅浪费空间,还会多出很多不必要的操作,这里的解决方案其实很简单,给迭代器增加一个指向哈希表的指针即可。其实这样又引出了两个问题,一个是这样做其实不能完全解决问题,因为我们要访问的是哈希表里底层的数组,这个数组是一个私有的成员变量,我们在外部是没办法访问的,所以我们要把这个迭代器声明成HashTable的友元类,这样我们的迭代器就能访问HashTable的数组了;带模版参数的友元类的声明和之前声明的友元类有一些不同点,我们在声明的时候要把它的类型完整的声明出来;第二个问题就是会出现报错,HashTable是在迭代器后续实现的一个类,编译器在编译的时候是从上往下的顺序进行的,编译进行到HTIterator这里时,它发现在前面的代码中,没有发现HashTable,也就是说编译器不认识这个类型,所以就出现了这个报错,这里解决这个问题的办法也很简单,我们只要在HTIterator之前把HashTable声明了就行,这个做法叫做提前声明,就是告诉编译器这个东西是一个我们自己定义的类,你继续向后编译就好,后面就我们的具体实现。

cpp 复制代码
		template<class K, class T, class KeyOfT, class Hash>
		class HashTable;

		template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
		struct HTIterator
		{
			typedef HashNode<T> Node;
			typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
			typedef HashTable<K, T, KeyOfT, Hash> HT;

			HTIterator(Node* node,const HT* ht)
				:_node(node)
				,_ht(ht)
			{}

			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
				{
					KeyOfT kot;
					Hash hs;

					size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();
					++hashi;
					while (hashi < _ht->_tables.size())
					{
						_node = _ht->_tables[hashi];
						if (_node)
							break;
						else
							++hashi;
					}
				}

				return *this;
			}

			Node* _node;
			const HT* _ht;
		};


		template<class K, class T, class KeyOfT, class Hash>
		class HashTable
		{

			template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
			friend struct HTIterator;
        };

三、哈希表里的迭代器调用

这里其实很简单,就是简单的去找哈希表里的第一个数据就是Begin函数需要做的功能,而End其实就是空,主要是我们可以通过迭代器去改造Find函数和Insert的函数,这样不仅能通过这个Insert函数实现后续unordered_map的重载[],还能方便操作。但是其实这里的ConstIterator版本的Begin函数还出现了一个问题,会在后续说明。

cpp 复制代码
		template<class K, class T, class KeyOfT, class Hash>
		class HashTable
		{
		public:

			typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
			typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;

			Iterator Begin()
			{
				if (_n == 0)
					return End();

				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];

					if (cur)
						return Iterator(cur, this);
				}

			}

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

			ConstIterator Begin() const
			{
				if (_n == 0)
					return End();

				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];

					if (cur)
						return ConstIterator(cur, this);
				}

			}

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

			pair<Iterator, bool> Insert(const T& data)
			{
				KeyOfT kot;
				Hash hs;

				Iterator it = Find(kot(data));
				//插入的数据已经存在
				if (it != End())
					return { it ,false };

				//负载因子为1时扩容
				if (_n == _tables.size())
				{
					vector<Node*> newHT(_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)) % newHT.size();
							cur->_next = newHT[hashi];
							newHT[hashi] = cur;

							cur = next;
						}
						_tables[i] = nullptr;
					}

					_tables.swap(newHT);
				}

				size_t hashi = hs(kot(data)) % _tables.size();
				//头插
				Node* newnode = new Node(data);
				newnode->_next = _tables[hashi];
				_tables[hashi] = newnode;
				++_n;
				return { Iterator(newnode,this),true };
			}

			// 在哈希桶中查找值为key的元素,存在返回true否则返回false
			Iterator Find(const K& key)
			{
				Hash hs;
				size_t hashi = hs(key) % _tables.size();

				//在对应的桶里查找数据
				Node* cur = _tables[hashi];
				KeyOfT kot;
				while (cur)
				{
					if (kot(cur->_data) == key)
						return Iterator(cur,this);

					cur = cur->_next;
				}

				return End();
			}
        };

四、封装容器

其实到这里就没有什么可说的了,也没有什么难点了,无非就是实现一个KeyOfT的仿函数然后再对我们实现好的哈希表和迭代的功能进行调用罢了,unordered_set里的细节只有一个,就是在给哈希表传参的时候,我们可以直接给第二个参数直接加上的const的,第二个模版参数对应的是T,unordered_set存储的本来就只有一个Key值,本身就是不支持修改的,加上const修饰也可以防止误操作。

还有一个细节点就是我们在给迭代器传参以及定义指向哈希表的指针的时候,一定要用const修饰,不然就会引发第三点中说的问题ConstIterator版本的Begin和End都是被const修饰的,表示Begin和End中this指针指向的对象是被const的修饰的,这样原来的哈希表就变成了一个被const修饰的哈希表边,此时在调用ConstIterator版本的迭代器的时候就会出现参数类型不匹配的问题;但是如果用const修饰以后,普通版本的迭代器传入的this指向的对象是没有被const修饰的,但是权限是能给缩小的,所以也不会出现错误。

cpp 复制代码
	template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

	public:

		typedef typename HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
		typedef typename HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator 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);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

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

	};

unordered_map这里要多实现一个重载[]的功能,这里这个写法的意思就是,利用Insert的功能去找出这个Key值是否在哈希表中存在,如果存在就返回它对应的结点,如果不存在就会直接插入这个结点,这个结点对应Value值是V类型的默认构造出来的。

return ret.first->second;的第一个.first是取到pair这个对里的第一个类型是这个一个迭代器,->second的意思是:我们的迭代器重载了->这个操作符,这个操作符的作用是取出迭代器指向结点存储的内容,也就是T类型,在unordered_mapT类型是我们的pair<K,V>,所以它的second就是我们对应的V,这样就能做到Key存在时进行修改操作,Key不存在时进行插入和修改的操作了。

cpp 复制代码
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{

	public:
		struct MapKeyOfT
		{
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};

		typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
		typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator 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);
		}

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

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert({ key,V() });
			return ret.first->second;
		}

	private:
		HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};
相关推荐
唐诺36 分钟前
几种广泛使用的 C++ 编译器
c++·编译器
冷眼看人间恩怨2 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客2 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin2 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
yuanbenshidiaos3 小时前
c++---------数据类型
java·jvm·c++
十年一梦实验室4 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
taoyong0014 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
这是我584 小时前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
fpcc4 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
呆萌很4 小时前
C++ 集合 list 使用
c++