用哈希表封装unordered_map(以及unordered_set)【C++】

目录

一,前言

二,封装层框架(哈希底层以哈希桶为例)

三,迭代器

1. operator++

2. operator[]

3. 仿函数优化

3. 解决unordered_set中Key可以修改的Bug

代码区

Hash_map_set.h

HashTable.h

总结:用红黑树还是哈希表??

哈希表优势:

缺点:

红黑树优势:

下节预告:哈希应用!!

结语


一,前言

在学习封装unordered_map与unordered_set前,建议先学习如何封装map & set该篇文章用红黑树封装map&set【C++】-CSDN博客

这样更能理解其中的封装思想(两种封装方式,主题思路大体相似)

同时用哈希表封装 unordered_map和undordered_set,在封装之前,我们首先是要学会哈希表基本的精华,哈希实现建议大家先学习从底层认识哈希表【C++】-CSDN博客

二,封装层框架(哈希底层以哈希桶为例)

由于我们前面已经学习过map与set的封装,在学习unordered_map(set) 封装这里我们直接给出基本的框架代码啦! 

本质上:还是对哈希表进行封装,在unordered_map层面调用哈希底层。

用哈希表封装 unordered_map &  unordered_set,思想核心:就是让unordered_set如何适应unordered_map,看到这里的都是已经学习过红黑树封装map与set的童鞋了吧

namespace hash_map_set
{
	template <class K>
	class unordered_set
	{
	public:
		struct SetkeyofT   // 从T类型中提取Key
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

		bool insert(const K& data)
		{
			return table.insert(data);
		}

	private:
		hash_barrel::HashTable<K, K, SetkeyofT>  table;
	};

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

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

		bool insert(const pair<const K, V>& kv)
		{
			return table.insert(kv);
		}
	private:
		hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT>  table;
	};
}
// 哈希表底层部分变化,为了避免冗余,就只展现部分代码

template <class T>
	struct Node_Data
	{
		typedef Node_Data<T> Node_data;
		T _data;
		Node_data* _downstars = nullptr;

		Node_Data(const T& pa = T())
			:_data(pa)
		{}
	};

template <class K, class T, class KeyofT ,class Hashstr = Hashstr<K>>
	class HashTable
	{
	public:
		typedef Node_Data<T> Node_Data;

.......

三,迭代器

首先我们先完成 迭代器基本框架 和 基本“ * ”, “ & ”,“ ->”, " != "实现,代码其实比较简单,重要的是逻辑如何行云流水的形成,多写多看多思考吧!!

    template <class T>
	struct Node_Data
	{
		typedef Node_Data<T> Node_data;
		T _data;
		Node_data* _downstars = nullptr;

		Node_Data(const T& pa = T())
			:_data(pa)
		{}
	};

	template <class type>
	struct Hashstr
	{
		int operator()(const type& sd)
		{
			return sd;
		}
	};

	template<>
	struct Hashstr<string>
	{
		size_t operator()(const string& str)
		{
			size_t sum = 0;
			for (auto e : str)
			{
				sum += e;
				sum *= 31;   // 别问,问就是实验的结果
			}
			return sum;
		}
	};

	// 由于迭代器需要使用HashTable作为成员变量,因此需要前置声明
	template <class K, class T, class KeyofT, class Hashstr = Hashstr<K>>
	class HashTable;

	template <class K, class T, class KeyofT, class Ref, class Ptr, class Hash>
	struct HT_iterator
	{
		typedef Node_Data<T> Node_Data;
		typedef HashTable<K, T, KeyofT, Hash> HashTable;
		typedef HT_iterator<K, T, KeyofT, Ref, Ptr, Hash> HT_iter; // 正常迭代器

		Node_Data* _node;  // 当前位置
		HashTable* _ht;    // 当前哈希表
		size_t _bucket = -1;     // 当前桶

		HT_iterator(Node_Data* node, HashTable* ht)
			:_node(node)
			, _ht(ht)
		{
			Hash hashstr;
			KeyofT keyofT;
			if (node) // node为空的时候,我们会对nullptr进行访问,所以规避一下
			_bucket = hashstr(keyofT(_node->_data)) % _ht->Get_tables().size();
		}

		// HT_iterator(const HT_iterator)

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

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

		bool operator!=(const HT_iter& it)
		{
			return _node != it._node;
		}
	};

1. operator++

思路:从第一个桶开始访问数据,访问完一个数据后,接着下一个桶,直至结束

思路比较简单,但迭代器设计,逻辑比较难处理。


        // 哈希迭代器只往前单向
		HT_iter operator++()
		{
			// 三种情况:
			// 1. 下一个为空,下一个桶存在
			// 2. 下一个为空,下一个不存在
			// 3. 下一个存在
			if (_node->_downstars)
			{ // 3.
				_node = _node->_downstars;
				return *this;
			}
			else
			{   // 1 , 2
				 寻找下一个不为空的桶
				++_bucket;
				while (_bucket < _ht->Get_tables().size())
				{
					_node = (_ht->Get_tables())[_bucket];
					if (_node)  // 找到后中断寻找
						break;
					++_bucket;
				}
				return *this;
			}
		}

2. operator[]

在实现operator[]时,我们需要先将insert,find函数进行优化:

让insert返回值从bool ——> 返回pair<iterator,  bool>;

find()从返回数据指针——>  返回迭代器;

// Hashtable——哈希类中
        T& operator[](const T& pa)
		{
			pair<HT_iterator, bool> it = insert(pa);
			return *(it.first);
		}

// 封装层 unordered_map
        V& operator[](const K& pa)
		{
			pair<HT_iterator, bool> ret = insert(make_pair(pa, V()));
			return ret.first->second;
		}
        
// unordered_set
        K& operator[](const K& pa)
		{
			return table[pa];
		}

3. 仿函数优化

在哈希实现过程中,我们实现了该仿函数,目的就是为了解决当 K不是int的时候(比如string类)无法转化为size_t类型,来寻找哈希地址。 

那么问题来了!!  我如果想将Day类型(处理年,月,日的类)作为K呢?? 请问这怎么将这个日期类转化为size_t???

为了方便分析,我将这个将其他数据转化为size_t的类叫做转化类 

我们会发现,封装在底层的中转化类(int与string类)在目前看来,应该将其放在unordered_map与unordered_set的封装层;同时在两个封装层添加一个新的参数,作为转化类的行参

这样做的好处是,在我们不知道K类型时,用户可以导入其自己设定的转化类,来实现各种类(各种类型)转化为size_t类型。

3. 解决unordered_set中Key可以修改的Bug

在前面的文章中(用红黑树封装map&set【C++】-CSDN博客),我们已经修复过一次了,这里就只展现修改位置了。

代码区

Hash_map_set.h

namespace hash_map_set
{

	template <class type>
	struct Hashstr
	{
		int operator()(const type& sd)
		{
			return sd;
		}
	};

	template<>
	struct Hashstr<string>
	{
		size_t operator()(const string& str)
		{
			size_t sum = 0;
			for (auto e : str)
			{
				sum += e;
				sum *= 31;   // sum为啥都要乘31这个数??1.减少数据聚集在一个桶中。2.31这个数是大量实验的最佳数
			}
			return sum;
		}
	};

	template <class K, class Hash = Hashstr<K>>
	class unordered_set
	{
	public:
		struct SetkeyofT   // 从T类型中提取Key
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

		// 关于为什么要这样操作?原因:底层(HT_iterator)实例化后,再取得模板,而不是自己设定
		typedef typename hash_barrel::HashTable<K, K, SetkeyofT, Hash>::const_HT_iterator  HT_iterator;
		typedef typename hash_barrel::HashTable<K, K, SetkeyofT, Hash>::const_HT_iterator const_HT_iterator;

		const_HT_iterator begin() const
		{
			return table.begin();
		}

		const_HT_iterator end() const 
		{
			return table.end();
		}

		HT_iterator begin()
		{
			return table.begin();
		}

		HT_iterator end()
		{
			return table.end();
		}

		pair<HT_iterator, bool> insert(const K& data)
		{
			return table.insert(data);
		}

		K& operator[](const K& pa)
		{
			return table[pa];
		}

		HT_iterator find(const K& pa)
		{
			return table.find(pa);
		}

		bool erase(const K& pa)
		{
			return table.erase(pa);
		}

	private:
		hash_barrel::HashTable<K, K, SetkeyofT, Hash>  table;
	};

	template <class K, class V, class Hash = Hashstr<K>> 
	class unordered_map
	{
	public:

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

		typedef typename hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT, Hash>::HT_iterator  HT_iterator;
		typedef typename hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT, Hash>::const_HT_iterator  const_HT_iterator;

		const_HT_iterator begin() const 
		{
			return table.begin();
		}

		const_HT_iterator end() const 
		{
			return table.end();
		}

		HT_iterator begin()
		{
			return table.begin();
		}

		HT_iterator end()
		{
			return table.end();
		}

		pair<HT_iterator, bool> insert(const pair<const K, V>& kv)
		{
			return table.insert(kv);
		}

		V& operator[](const K& pa)
		{
			pair<HT_iterator, bool> ret = insert(make_pair(pa, V()));
			return ret.first->second;
		}

		HT_iterator find(const K& pa)
		{
			return table.find(pa);
		}

		bool erase(const K& pa)
		{
			return table.erase(pa);
		}

	private:
		hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT, Hash>  table;
	};
}

HashTable.h

namespace hash_barrel
{
	template <class T>
	struct Node_Data
	{
		typedef Node_Data<T> Node_data;
		T _data;
		Node_data* _downstars = nullptr;

		Node_Data(const T& pa = T())
			:_data(pa)
		{}
	};

	// 由于迭代器需要使用HashTable作为成员变量,因此需要前置声明
	template <class K, class T, class KeyofT, class Hashstr>
	class HashTable;

	template <class K, class T, class KeyofT, class Ref, class Ptr, class Hash>
	struct HT_iterator
	{
		typedef Node_Data<T> Node_Data;
		typedef HashTable<K, T, KeyofT, Hash> HashTable;
		typedef HT_iterator<K, T, KeyofT, Ref, Ptr, Hash> HT_iter; // 正常迭代器
		typedef HT_iterator<K, T, KeyofT, T&, T*, Hash> iterator; 

		Node_Data* _node;  // 当前位置
		const HashTable* _ht;    // 当前哈希表,添加const原因:1.迭代器不会修改哈希表 2.const迭代器传入的是const的哈希表,所以需要适配
		size_t _bucket = -1;     // 当前桶

		HT_iterator(Node_Data* node, const HashTable* ht)
			:_node(node)
			, _ht(ht)
		{
			Hash hashstr;
			KeyofT keyofT;
			if (node) // node为空的时候,我们会对nullptr进行访问,所以规避一下
				_bucket = hashstr(keyofT(_node->_data)) % _ht->Get_tables().size();
		}

		// 1. 当对象是普通迭代器时,这是拷贝构造
		// 2. 当对象是const迭代器时,是利用普通迭代器构造const迭代器的构造函数
		HT_iterator(const iterator& it)
			:_node(it._node)
			,_ht(it._ht)
			,_bucket(it._bucket)
		{}

		// HT_iterator(const HT_iterator)

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

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

		bool operator!=(const HT_iter& it)
		{
			return _node != it._node;
		}

		// 哈希迭代器只往前单向
		HT_iter operator++()
		{
			// 三种情况:
			// 1. 下一个为空,下一个桶存在
			// 2. 下一个为空,下一个不存在
			// 3. 下一个存在
			if (_node->_downstars)
			{ // 3.
				_node = _node->_downstars;
				return *this;
			}
			else
			{   // 1 , 2
				 寻找下一个不为空的桶
				++_bucket;
				while (_bucket < _ht->Get_tables().size())
				{
					_node = (_ht->Get_tables())[_bucket];
					if (_node)
						break;
					++_bucket;
				}
				return *this;
			}
		}
	};



	template <class K, class T, class KeyofT ,class Hash>
	class HashTable
	{
	public:
		typedef Node_Data<T> Node_Data;
		typedef HT_iterator<K, T, KeyofT, const T&, const T*, Hash>  const_HT_iterator;  // const迭代器
		typedef HT_iterator<K, T, KeyofT, T&, T*, Hash> HT_iterator;    // 普通迭代器
		
		~HashTable()
		{
			for (auto bucket : _tables)
			{
				Node_Data* cur = bucket;
				while (cur)
				{
					Node_Data* next = cur->_downstars;
					delete (cur);
					cur = next;
				}
			}
		}

		const_HT_iterator begin() const 
		{
			size_t i = 0;
			Node_Data* cur = nullptr;
			for (; i < _tables.size(); i++)
			{
				cur = _tables[i];

				if (cur)
					break;
			}

			return const_HT_iterator(cur, this);  // this就是哈希表对象
		}

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

		HT_iterator begin()
		{
			size_t i = 0;
			Node_Data* cur = nullptr;
			for (; i < _tables.size(); i++)
			{
				cur = _tables[i];

				if (cur)
					break;
			}
			return HT_iterator(cur, this);  // this就是哈希表对象
		}

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

		// 在实现 operator[]时,底层用到insert(),没找到,插入新值返回迭代器;找到直接返回迭代器
		pair<HT_iterator,bool> insert(const T& pa)
		{
			KeyofT keyofT;
			HT_iterator ret = _find(keyofT(pa));
			if (ret != end())
				return make_pair(ret, true);  

			// 考虑扩容:负载因子为1,2,3都可以
			Hash hashstr;
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 10)
			{
				size_t  new_size = _tables.size() == 0 ? 10 : _tables.size() * 2;
			     // 开始扩容	
				vector<Node_Data*> new_tables;
				new_tables.resize(new_size);
				for (auto& data : _tables)
				{
					// 处理桶内的数据,重新插入新节点
					Node_Data* cur = data;
					while (cur)
					{
						Node_Data* room = cur->_downstars;
						size_t new_hashi = hashstr(keyofT(cur->_data)) % new_tables.size();
						// 处理结点关系
						Node_Data* tmp = new_tables[new_hashi];
						new_tables[new_hashi] = cur;
						cur->_downstars = tmp;
						cur = room;
					}
				}
				_tables.swap(new_tables);
			}

			size_t hashi = hashstr(keyofT(pa)) % _tables.size();
			Node_Data* new_node = new Node_Data(pa);
			new_node->_downstars = _tables[hashi];
			_tables[hashi] = new_node;
			_n++;
			return make_pair(HT_iterator(new_node, this), true);
		}

		HT_iterator find(const K& order)
		{
			return _find(order);
		}

		bool erase(const K& order)
		{
			Hash hashstr;
			HT_iterator cur = find(order);
			if (!(cur._node))
			{
				cout << "擦除失败: 不存在" << endl;
				return false;
			}
			size_t index = hashstr(order) % _tables.size();
			Node_Data* tmp = _tables[index];

			while (tmp)
			{
				if (cur._node == tmp)
				{
					break;
				}
				else if (cur._node == tmp->_downstars)
				{
					break;
				}
				tmp = tmp->_downstars;
			}
			// 开始处理节点
			if (tmp == _tables[index])  // 如果擦除的是头,那要置空的包括指针数组
			{
				_tables[index] = cur._node->_downstars;
			}
			else
			{
				tmp->_downstars = cur._node->_downstars;
			}
			
			delete (cur._node);
			cout << "擦除成功" << endl;
			return true;
		}

		T& operator[](const T& pa)
		{
			pair<HT_iterator, bool> it = insert(pa);
			return *(it.first);
		}

		size_t Get_n()
		{return _n; }

		 vector<Node_Data*>& Get_tables()
		{
			return _tables;
		}

		 // 需要为const 对象进行重载
		 const vector<Node_Data*>& Get_tables() const 
		 {
			 return _tables;
		 }

	private:
		// Node_Data* _find(const K& order)
		HT_iterator _find(const K& order)
		{
			if (!_tables.size())
				return end();

			Hash hashstr;
			KeyofT keyofT;
			size_t hashi = hashstr(order) % _tables.size();
			auto cur = _tables[hashi];
			while (cur)
			{
				if (keyofT(cur->_data) == order)
					return HT_iterator(cur, this);
				cur = cur->_downstars;
			}
			return end();
		}

		vector<Node_Data*> _tables;  // 存放各个哈希地址的第一个结点地址的指针数组
		size_t _n = 0;                  // 哈希桶中,数据个数
	};
}

总结:用红黑树还是哈希表??

哈希表优势:

哈希表的查找,插入,删除操作平均为O(1);红黑树的查找,插入,删除平均O(logn)。因此哈希表会有更高的效率。

缺点:

1. 哈希表存在,哈希冲突,而解决方案有 哈希桶法(链表法),开放定址法。如果超过负载值,将对全部数据进行重新分配,这会导致性能下降。

2. 占用更多的内存

红黑树优势:

1. 相比于哈希表红黑树占用内存比较少。

2. 相比于哈希表插入可能需要重新分配哈希桶,红黑树的插入效率不差,也更稳定。

因此:选择哈希表还是红黑树作为map的底层数据结构取决于具体的应用场景。如果对性能要求较高且不需要有序性,可以选择哈希表。如果需要有序性或者对性能要求不是特别高,可以选择红黑树。

 

下节预告:哈希应用!!

结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值