unordered_map/set(底层实现)——C++

目录

前言:

1.开散列

1. 开散列概念

2. 开散列实现

2.1哈希链表结构体的定义

2.2哈希表类即私有成员变量

2.3哈希表的初始化

2.4迭代器的实现

1.迭代器的结构

2.构造

3.*

4.->

5.++

6.!=

2.5begin和end

2.6插入

2.7Find查找

2.8erase删除

3.unordered_map的封装

4.unordered_set的封装

5.5. 开散列与闭散列比较


前言:

我建议大家看过我《哈希表的底层实现之闭散列(C++)》、《红黑树模拟实现STL中的map与set——C++》这两篇文章之后再来看这一篇,因为这一篇的一部分哈希实现和一部分unordered_map和unordered_set的实现原理是建立在这两篇的基础之上的。

1.开散列

1. 开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

2. 开散列实现

2.1哈希链表结构体的定义

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

跟闭散列不同,这回我们的结构体里就没有状态码的设置了,因为我们这回是以链表的方式进行哈希桶的实现来处理哈希冲突,所以我们需要next指针指向产生哈希冲突的元素。

2.2哈希表类即私有成员变量

template<class K,class T,class KeyofT,class Hash>
class Hashtable
{
	//typedef HashNode<T> Node;
	using Node = HashNode<T>;

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

我们的私有成员变量就两个,一个是std库里的vector容器,它里面的元素就是我们的一个个链表;然后再来一个_n代表有效元素的个数。

大家可能发现了我们的重命名是using Node = HashNode<T>;这个写法是C++11的语法,我们后面会专门用一篇文章来讲解C++11的语法。

2.3哈希表的初始化

	//初始化
	Hashtable()
		:_n(0)
	{
		_tables.resize(10,nullptr);
	}

我们这里就不弄的那么复杂,我们就简单开一个10空间大小的vector就可以了。

2.4迭代器的实现

在讲哈希表的增删查改之前我们要先来讲一下迭代器,因为我们后续的实现是建立在迭代器之上的。

我们本篇采用内部类的方式来进行迭代器的思想,将迭代器包在哈希表实现的里面。

1.迭代器的结构

	//内部类实现迭代器
	template<class Ptr,class Ref>
	struct __HIterator
	{
		typedef HashNode<T> Node;
		typedef __HIterator<Ptr,Ref> Self;

		Node* _node;
		const Hashtable* _pht;

	};

这里Ptr和Ref的作用还是跟之前一样,分别代表数据的指针和引用。然后里面我们对哈希链表和迭代器本身进行重命名,我们还需要哈希链表类型的变量以及哈希表指针类型的变量,具体都有什么作用,在后续的讲解中我们就会知道了。

2.构造

//构造
		__HIterator(Node* node, const Hashtable* pht)
			:_node(node)
			,_pht(pht)
		{}

构造我们就采用哈希链表的节点指针以及整个哈希表的指针来进行初始化。因为迭代器的操作实际上都是对哈希表进行操作,所以我们需要知道具体的哈希表是哪个,然后确定是哪个节点。

3.*

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

解引用就返回我们的数据就好了。

4.->

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

->的作用自然就是返回我们的数据的地址。

5.++

		//++
		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				Hash hs;
				KeyofT kot;
				size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();
				++hashi;
				while (hashi<_pht->_tables.size())
				{
					if (_pht->_tables[hashi])
						break;
					hashi++;
				}
				if (hashi == _pht->_tables.size())
				{
					_node = nullptr;
				}
				else
				{
					_node = _pht->_tables[hashi];
				}
			}
			return *this;
		}

++的操作就是为了让我们找到下一个元素。我们是以一个个链表的形式进行存储的,所以我们要先在本条链表里访问完,再进行其它链表的访问。因此,我们的第一步就是访问本条链表的next节点,如果next节点有数据,我们就返回访问到这个数据的迭代器,如果next节点的位置为NULL,我们就要计算当前节点的哈希值,计算出来后继续访问下一个哈希值,如果下一个哈希值所指向的位置是空的,那么我们就要将hash++,继续往下找直到不为空。倘若后续没有数据了,我们就将_node设为空,否则将数据赋值给_node。

6.!=

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

!=操作就是把两迭代器的_node进行一下比对就好了。

2.5begin和end

	typedef __HIterator<T*, T&> iterator;
	typedef __HIterator<const T*, const T&> const_iterator;

	//begin
	iterator begin()
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i])
			{
				return iterator(_tables[i], this);
			}
		}
		return end();
	}
	//end
	iterator end()
	{
		return iterator(nullptr, this);
	}

	//const  begin
	const_iterator begin() const
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i])
			{
				return const_iterator(_tables[i], this);
			}
		}
		return end();
	}
	//const  end
	const_iterator end() const
	{
		return const_iterator(nullptr, this);
	}

我们先将我们所写的普通迭代器和const迭代器进行一下重命名,然后进行接下来的操作。

我们的begin()就是哈希值为0的元素,end()就是为空的元素。我们再分别为他们实现const修饰的版本。

2.6插入

    //插入
	pair<iterator,bool> Insert(const T& data)
	{
		Hash hs;
		KeyofT kot;
		iterator it = Find(kot(data));
		if (it!=end())
		{
			return make_pair(it,false);
		}
		if (_n == _tables.size())
		{
			vector<Node*> newtables(_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)) % newtables.size();
					cur->_next = newtables[hashi];
					newtables[hashi] = cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
			_tables.swap(newtables);
		}
		size_t hashi = hs(kot(data)) % _tables.size();
		Node* newNode = new Node(data);
		//头插
		newNode->_next = _tables[hashi];
		_tables[hashi] = newNode;
		++_n;
		return make_pair(iterator(_tables[hashi], this), true);
	}

在解决完前置内容之后,我们就要来进行插入操作的讲解了。我们先来看插入操作的返回值类型,大家是不是感到很疑惑?为什么是pair类型的呢?我也不卖关子,这实际上是为了我们后续对其进行封装时能够更加方便而设计的。我们先来把当个功能的思路搞懂,等我后面封装的时候大家就很清楚了。

我们先用Find函数来查找一下这个元素是否存在,如果存在,我们直接返回,如果不存在,我们就可以进行后续的操作了。如果有效元素个数_n跟表的有效空间相等,我们就要进行扩容操作。我们先创个两倍大小的新表,然后将旧表的内容拷贝到新表中,同时不要忘记了我们拷贝到新表的时候要重新计算哈希值,因为我们的表的有效空间已经变了。

扩完容之后我们在进行插入,我们先计算要插入的数据的哈希值,然后以头插的方式插入到哈希表当中。将有效元素++就好了。

2.7Find查找

	//查找
	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 end();
	}

查找就很简单了,我们只需要计算哈希值,然后循环查找就可以了。返回的类型为迭代器也是为了我们后续的封装。

2.8erase删除

	//删除
	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] = nullptr;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				_n--;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}
		return false;
	}

删除操作也不复杂,我们只需要多一个prev节点记录当前节点的上一个节点就好了,我们的思路是这样的:我们先计算哈希值,然后用cur去指向哈希值所在的位置,如果当前位置没有数据,我们直接返回false,如果有我们就要看看是否与要删除的key值相等,若相等我们就要判断prev是否为空,为空就说明它是当前哈希值的第一个元素,我们就直接将当前哈希值的链表设为空就行,如果不为空,我们就要用prev链接cur的下一个节点。删除成功后不要忘记将n--。

3.unordered_map的封装

	template<class K,class V,class Hash=HashFunc<K>>
	class unordered_map
	{
		struct MapkeyofT
		{
			const K& operator()(const pair<K,V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename Hashtable<K, pair<const K, V>, MapkeyofT, Hash>::iterator iterator;
		typedef typename Hashtable<K, pair<const K, V>, MapkeyofT, Hash>::const_iterator const_iterator;
		//begin()
		iterator begin()
		{
			return _ht.begin();
		}
		//end()
		iterator end()
		{
			return _ht.end();
		}
		//begin() const
		const_iterator begin() const
		{
			return _ht.begin();
		}
		//end() const
		const_iterator end() const
		{
			return _ht.end();
		}


		//[]
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = Insert(make_pair(key, V()));
			return ret.first->second;
		}
		//插入
		pair<iterator, bool> Insert(const pair<K,V>& kv)
		{
			return _ht.Insert(kv);
		}
	private:
		Hashtable< K, pair<const K, V>, MapkeyofT, Hash> _ht;
	};

unordered_map跟我之前那篇文章里map的封装非常类似,我这里就说说【】重载吧,insert为什么返回值是这个类型我们是参照标准库来定义的。(如图所示)

insert函数这么设计可以让我们重载【】的时候更加方便,我们可以直接复用它。

在说明【】前我们要知道【】能做什么?很简单,就是根据我们的key来返回对应的值。

我们知道插入成功或失败(表里有同样的数据)的时候都会返回它这个键值对,所以我们可以里利用这一点直接复用我们的insert然后取出我们的值就可以了。

4.unordered_set的封装

	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>::const_iterator const_iterator;
		//begin()
		iterator begin()
		{
			return _ht.begin();
		}
		//end()
		iterator end()
		{
			return _ht.end();
		}
		//begin() const
		const_iterator begin() const
		{
			return _ht.begin();
		}
		//end() const
		const_iterator end() const
		{
			return _ht.end();
		}


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

		//find
		iterator Find(const K& key)
		{
			return _ht.Find(key);
		}
		//erase
		bool Erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		Hashtable< K,const K, SetkeyofT, Hash> _ht;
	};

unordered_set的封装跟set也是非常相似,没有什么新鲜的东西,只要看过我 《红黑树模拟实现STL中的map与set——C++》这篇文章的同学看这里的代码就几乎没什么难度。

5.5. 开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a ,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

回答: 在C++中,mapunordered_mapsetunordered_set都是STL(标准模板库)中的容器。它们都用于存储一组数据,并提供了不同的功能和性能特点。 map是一个有序的关联容器,它使用红黑树实现,可以根据键值进行快速查找。map中的元素按照键值的大小进行排序,并且每个键值只能出现一次。\[1\]unordered_map是一个无序的关联容器,它使用哈希表实现,可以根据键值进行快速查找。unordered_map中的元素没有特定的顺序,并且每个键值只能出现一次。\[2\] set是一个有序的容器,它使用红黑树实现,可以存储不重复的元素。set中的元素按照值的大小进行排序,并且每个值只能出现一次。\[3\]unordered_set是一个无序的容器,它使用哈希表实现,可以存储不重复的元素。unordered_set中的元素没有特定的顺序,并且每个值只能出现一次。 在使用这些容器时,可以使用insert()函数插入元素,使用find()函数查找元素,使用erase()函数删除元素。此外,mapunordered_map还提供了count()函数来计算特定键值的出现次数。 总结来说,mapunordered_map适用于需要根据键值进行快速查找的场景,setunordered_set适用于需要存储不重复元素的场景。具体选择哪个容器取决于你的需求和性能要求。 #### 引用[.reference_title] - *1* *3* [C++map,unordered_map,setunordered_set的用法和区别](https://blog.youkuaiyun.com/bryant_zhang/article/details/111600209)[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^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [初识C++unordered_mapunordered_set](https://blog.youkuaiyun.com/Masquerena114514/article/details/129938734)[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^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值