unordered_map与unordered_set的实现

目录

1.底层结构

1)方式

2)哈希冲突

3)哈希函数

4)哈希冲突的解决

1.闭散列

1)线性探测

扩容:

2)二次探测

2.开散列

1)概念

2)实现

插入操作:

删除操作:

查找操作:

哈希桶的销毁:

3)迭代器的实现

4)begin -- end

2.unordered_set的实现

3.unordered_map的实现


1.底层结构

两种容器的底层结构均为哈希表

概念:通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立

一一映射的关系,那么在查找时通过该函数可以很快找到该元素

1)方式

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

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

2)哈希冲突

在上述的操作中,难免会出现不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

而哈希冲突的发生是不可避免的

3)哈希函数

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

哈希函数设计原则
1.哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在0 m-1 之间
2.哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数应该比较简单
常见的哈希哈数:
直接定址法、除留余数法、平方取中法、折叠法、随机数法、数学分析法

4)哈希冲突的解决

解决哈希冲突 两种常见的方法是: 闭散列 开散列
1.闭散列
闭散列:也叫 开放定址法 ,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的 “下一个” 空位置中去。
1)线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
例如下面的元素44,在位置冲突的情况下,就需要去找到下一个空位

比如上述,如果删除了6号元素,直接标记空的话:
查询44号元素的话,就会查找失败
扩容:

扩容有个前提条件,即载荷因子超出预定值时进行扩容

散列表中载荷因子的定义为:α = 填入表中的元素个数 / 散列表的长度

α 是散列表装满程度的标志因子。由于表长是定值,α 与“填入表中的元素个数”成正比,所以,α 越大,表明填入表中的元素越多,产生冲突的可能性就越大:反之,越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子 α 的函数,只是不同处理冲突的方法有不同的函数。

对于开放定址法,载荷因子一般 严格控制在0.7 - 0.8以下。超过0.8的话,查表的效率会大大降低
2)二次探测

为了避免线性探测的逐个寻找空位,即有了二次探测:

若冲突,则:

(x + i ^ 1)/ m、(x + i ^ 2)、......i为1,2,3,4,5.....

2.开散列
1)概念
开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中
2)实现
    template<class T>
	struct HashNode
	{
		HashNode<T>* _next;
		T _data;

		HashNode(const T& data)
			:_next(nullptr)
			, _data(data)
		{}
	};

    template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<T> Node;
    public:
		HashTable()
		{
			_tables.resize(10, nullptr);
		}

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

底层即为vector,vector存储的每个数据为节点的指针

HashTable的构造函数即为重要,因为要事先开辟一些空间,防止访问空指针导致错误

插入操作:

未达到载荷因子就无须扩容,直接头插即可,若达到,则扩容后插入即可

		pair<Iterator, bool> Insert(const T& data)
		{
			Hash hs;
			KeyOfT kot;
			Node* cur = nullptr;

			Iterator it = Find(kot(data));
			if (it != End())
				return make_pair(it, false);

			if (_n == _tables.size())
			{
				// 扩容
                // ......
			}
			else // 正常找空位插入即可
			{
				size_t hashi = hs(kot(data)) % _tables.size();
				if (_tables[hashi] == nullptr)
				{
					_tables[hashi] = new Node(data);
					cur = _tables[hashi]; // 这里cur一开始忘了给值,小修bug
				}
				else
				{
					cur = new Node(data);
					Node* tmp = _tables[hashi];

					cur->_next = tmp;
					_tables[hashi] = cur;
				}
				_n++;
			}
			return make_pair(Iterator(cur, this), true);
		}

返回值用pair<Iterator, bool> 是因为方便unordered_map的 [] 的实现

扩容操作:
创建一个新表,然后遍历旧表,将旧表的值一个个按照新表的规则插入新表中,最后将需要插入的值正常插入即可

				// 扩容
				size_t newsize = _tables.size() * 2;
				vector<Node*> newtables(newsize, nullptr);

				for (int i = 0; i < _tables.size(); ++i)
				{
					if (_tables[i])
					{
						cur = _tables[i];
						while (cur)
						{
							Node* next = cur->_next; // 先保存下一个位置

							size_t hashi = hs(kot(cur->_data)) % newsize;
							if (newtables[hashi] == nullptr)
							{
								cur->_next = nullptr; // cur的next应该及时处理,不然容易出现死循环
								newtables[hashi] = cur;
							}
							else
							{
								// 头插
								cur->_next = newtables[hashi];
								newtables[hashi] = cur;
							}
							cur = next;
						}
						_tables[i] = nullptr;
					}
				}
				_tables.swap(newtables);
				Insert(data);

注意,每当拿出来一个节点时,要将其_next置空或者赋值,否则会保留原表中的链接逻辑,导致后续错乱

删除操作:

删除相比插入起来就简单多了

只需要找到相应的节点,然后复原链接关系,最后delete即可:

两种情况:

1.删除头部

直接将_tables[hashi] 赋值给next然后delete掉头即可

2.删除非头部

直接prev->next = cur->next然后delete掉cur即可

	    bool Erase(const K& key)
		{
			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;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}
查找操作:

找到头节点后遍历即可:

		Iterator Find(const K& key)
		{
			Hash hs;
			KeyOfT kot;

			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;
			}
			return Iterator(nullptr, this);
		}
哈希桶的销毁:
		// 哈希桶的销毁
		~HashTable()
		{
			for (int i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i])
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						delete cur;
						cur = next;
					}
				}
				_tables[i] = nullptr;
			}
		}
3)迭代器的实现

为了实现++,我们需要获取到_tables(属于哈希的底层结构),因此必须设置友元或者实现内部类

	template<class K, class T, class KeyOfT, class Hash, class Ref, class Ptr>
	struct __HTIterator
	{
		typedef __HTIterator<K, T, KeyOfT, Hash, Ref, Ptr> Self;
		typedef HashNode<T> Node;
		Node* _node;
		HashTable<K, T, KeyOfT, Hash>* _pht;
		KeyOfT kot;
		Hash hs;

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

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

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

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

构造时,本来只需要node节点的,但是由于需要_tables的原因,需要拿到一个HashTable的指针方便访问。

这里的迭代器中,难度稍微高点的依旧是++操作:

1.若该桶未走完,则next即可

2.若该桶走完,则寻找下一个头不为空的点返回其头部即可

3.若所有桶走完,返回nullptr即可

		Self& operator++()
		{
			if (_node->_next)
			{
				// 1.当前桶没走完,找当前桶的下一个节点
				_node = _node->_next;
			}
			else
			{
				// 2.当前桶走完了,找下一个不为空的桶的第一个节点
				KeyOfT kot;
				Hash hs;
				size_t i = hs(kot(_node->_data)) % _pht->_tables.size();
				++i;
				for (; i < _pht->_tables.size(); i++)
				{
					if (_pht->_tables[i])
						break;
				}

				if (i == _pht->_tables.size())
				{
					// 3.所有桶都走完了,最后一个的下一个用nullptr标记
					_node = nullptr;
				}
				else
				{
					_node = _pht->_tables[i];
				}
			}

			return *this;
		}

逻辑理清楚了,实现起来不会太难

4)begin -- end
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<T> Node;
		// 友元声明
		template<class K, class T, class KeyOfT, class Hash, class Ref, class Ptr>
		friend struct __HTIterator;

	public:
		typedef __HTIterator<K, T, KeyOfT, Hash, T&, T*> Iterator;
		typedef __HTIterator<K, const T, KeyOfT, Hash, const T&, const T*> Const_Iterator;

		Iterator Begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
				{
					return Iterator(cur, this);
				}
			}
			return End();
		}

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

		Const_Iterator Begin() const
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
				{
					return Const_Iterator(cur, this);
				}
			}
			return End();
		}

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

2.unordered_set的实现

template<class K>
class myunordered_set
{
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:
	typedef typename Hash_bucket::HashTable<K, const K, SetKeyOfT>::Iterator iterator;
	typedef typename Hash_bucket::HashTable<K, const K, SetKeyOfT>::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);
	}

	bool erase(const K& key)
	{
		return _ht.Erase(key);
	}
private:
	Hash_bucket::HashTable<K, const K, SetKeyOfT> _ht;
};

3.unordered_map的实现


template<class K, class V>
class myunordered_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>::Iterator iterator;
	typedef typename Hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::Const_Iterator const_iterator;

public:
	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>& data)
	{
		return _ht.Insert(data);
	}

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

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

	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> _ht;
};

具体代码参考 ---- Hash

  • 18
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
unordered_map与unordered_set有什么区别? 回答: unordered_map和unordered_set都是关联式容器,类似于键值对 (key-value) 的模型。它们的底层实现方式不同,unordered_map使用哈希表作为底层数据结构,而unordered_set也是使用哈希表。unordered_map和unordered_set的区别在于它们存储的类型不同,即unordered_map存储键值对,而unordered_set存储单个元素。此外,unordered_map和unordered_set在功能上也有一些区别。unordered_map提供了以键为索引的查找功能,而unordered_set则提供了判断元素是否存在的功能。从效率上来看,unordered_map和unordered_set的增删查改操作的时间复杂度都是O(1),即常数时间。而mapset的时间复杂度为O(logN),其中N是容器中的元素数量。所以在对效率要求较高的情况下,选择unordered_map和unordered_set会更合适。但是,unordered_map和unordered_set相比于mapset会消耗更多的内存空间。因此,在对数据有排序或者对空间有要求的情况下,选择mapset;而对于对效率有要求的情况,选择unordered_map和unordered_set更合适。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [STL详解(十二)—— unordered_set、unordered_map的介绍及使用](https://blog.csdn.net/chenlong_cxy/article/details/122277348)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [模拟实现unordered_map和unordered_set超详解(C++)](https://blog.csdn.net/m0_67430750/article/details/124760725)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [unordered_set和unordered_map的使用【STL】](https://blog.csdn.net/m0_63312733/article/details/128000844)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值