初识C++之unordered_map与unordered_set

目录

一、unordered系列关联式容器介绍

二、unordered容器简单介绍

1.unordered_map 

1.1 unordered_map的特点

1.2unordered_map和map的模板区别

2.unordered_set

2.1unordered_set和set的模板区别

3.效率对比

3.1 插入效率对比

3.2 搜索效率对比

三、模拟实现unordered_map和unordered_set

1.哈希桶的改造

1.1 结构设置

1.2 构造函数和析构函数

1.3 数据插入

1.4 数据查找

1.5 数据删除

1.6 迭代器实现

1.7 哈希桶的整体实现

2. unordered_map的实现

3. unordered_set的实现


一、unordered系列关联式容器介绍

在C++98中,stl提供了一红黑树为底层的一系列关联式容器,如set和map。这些容器在搜索数据时的效率可以达到logN,即最差情况下需要比较红黑树的高度次。但是,当树中的节点非常多的时候,它的搜索效率其实也不是非常理想。而最理想的搜索效率,就是能够在进行非常少的比较次数的情况下就找到对应元素,即O(1)

因此,在C++11中,stl又提供了4个unordered系列的关联式容器,这4个容器与红黑树结构的关联式容器使用方式基本类似,但是其底层结构完全不同。,使用的是哈希结构,其搜索效率可以达到O(1)。

二、unordered容器简单介绍

从接口上看,unordered容器和stl中的其他容器保持着高度的一致,所以大家只要会使用stl中的其他容器,学习unordered系列容器的使用成本就会非常低。因此,在这里只是简单的介绍一下unordered系列容器的部分使用,重点并不在这上面

1.unordered_map 

从名字上就可以看出来,unordered_map其实就是与map相对应的一个容器。学过map就知道,map的底层是一个红黑树,通过中序遍历的方式可以以有序的方式遍历整棵树。而unordered_map,正如它的名字一样,它的数据存储其实是无序的,这也和它底层所使用的哈希结构有关。而在其他功能上,unordered_map和map基本上就是一致的。

1.1 unordered_map的特点

(1)unordered_map是用于存储键值对的关联式容器,它允许通过key快速的索引到对应的value

(2)在内部,unorder_map没有对按照任何特定的顺序排序,为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中

(3)unordered_map容器的搜索效率比map快,但它在遍历元素自己的范围迭代方面效率就比较低。

(4)它的迭代器只能向前迭代,不支持反向迭代

1.2unordered_map和map的模板区别

初识C++之unordered_map与unordered_set_第1张图片

初识C++之unordered_map与unordered_set_第2张图片

从大结构上看,unordered_map和map的模板其实没有太大差距。学习了map和set我们就应该知道,map是通过T来告诉红黑树要构造的树的存储数据类型的,unordered_map也是一样的,但是它的参数中多了Hash和Pred两个参数,这两个参数都传了仿函数,主要和哈希结构有关。这里先不过多讲解。

2.unordered_set

unordered_set其实也是一样的,从功能上来看和set并没有什么区别,只是由于地层数据结构的不同,导致unordered_set的数据是无序的,但是查找效率非常高。

2.1unordered_set和set的模板区别

初识C++之unordered_map与unordered_set_第3张图片

初识C++之unordered_map与unordered_set_第4张图片

很明显,unordered_set相较于set,多了Hash和Pred两个参数。这两个参数都是传了仿函数,和unordered_map与map之间的关系都是一样的,这里先不过多讲解。

3.效率对比

前文中也说了,unordered_map和unordered_set从功能上来讲,与map和set并没有太大的差别。除了数据是否有序外,主要就体现搜索效率上。这里为了方便测试,所以就直接用unordered_set和set进行对比。

3.1 插入效率对比

首先写出下面的测试代码: 

初识C++之unordered_map与unordered_set_第5张图片

通过这段代码,测试set和unordered_set在插入时效率差距。

运行该程序:

可以看到,在插入10w个数时,unordered_set的插入效率略高与set。

此时我们再将需要插入的数据增加到100w个再看看:

此时差距进一步扩大。此处set的插入效率比unordered_set效率低的原因就在于set的底层是一棵红黑树,在插入数据时可能需要进行旋转,当需要插入的数据越多,需要旋转的次数也就越多。而unordered_set的底层是哈希桶,虽然在插入中也需要调整,但是效率损失上就比set低。

当然,插入效率虽然有差距,但并不算大。而前文也说过了,unordered_set和set的主要效率差距体现在搜索上。因此我们再来对unordered_set和set的搜索效率进行对比。

3.2 搜索效率对比

首先在插入效率对比代码的基础上添加如下代码:

初识C++之unordered_map与unordered_set_第6张图片

 首先用10w个数来测试:

初识C++之unordered_map与unordered_set_第7张图片

 当传入10w个随机数时,一共生成了6w个不同的随机数,此时set的find效率就很明显低于unordered_set。

再传入100w个随机数进行测试:

初识C++之unordered_map与unordered_set_第8张图片

可以看到,当传入100w个随机数时,一共生成了63w个不同的随机数。但是可以发现,此时set的搜索效率已经很低了。但是unordered_set的搜索依然为0。这也就证明了使用哈希为底层数据结构的unordered_set搜索时的时间复杂度为O(1)

三、模拟实现unordered_map和unordered_set

要模拟实现unordered_map和unordered_set的前期条件,就需要我们自己实现一个哈希桶。因此,在这里的第一步就是将上篇文章中的哈希桶进行改造。

1.哈希桶的改造

1.1 结构设置

首先,这里实现的哈希桶需要能够同时支持unordered_map和unordered_set的调用。因此,在传入的数据中就需要有一个T,来标识传入的数据类型。同时,还需要有Hash函数KeyOfT来分别对传入的数据转换为整形获取传入数据的key值,主要是提供给使用了KV模型的数据。

我们还要知道,哈希桶其实是保存在一个顺序表中的,每个下标对应的位置上都是桶的头节点,每个桶中的数据以单链表的方式链接起来。因此,我们就需要一个vector来存储结构体指针,这个结构体中包含了当前节点存储的数据和下一个节点的位置。当然,还有有一个_n来记录顺序表中数据的个数。

namespace BucketHash//哈希桶
{
	//哈希桶内的每个节点/
	template
	struct HashNode
	{
		T _data;//存储数据
		HashNode* _next;//指向下一个节点

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

    template
	class HashBucket
	{
		template
		friend struct HashIterator;//友元声明,让迭代器可以访问哈希桶的私有成员

		typedef HashNode Node;
	public:
		typedef HashIterator iterator;


	private:
		vector _bucket;//存储数据的指针数组
		size_t _n;
	};
}

在类的模板参数中,Hash为将数据转化为整形的仿函数KeyOfT是返回键值的仿函数。

注意,上面的代码中有一个迭代器的重命名和迭代器结构体的友元声明。重命名是为了方便后续的使用。友元声明则和迭代器的实现有关,这里先不过多讲解。

1.2 构造函数和析构函数

注意,这里没有实现拷贝构造。并不是不需要实现,实际上,虽然哈希桶内的成员是自定义类型的,会去调自己的拷贝构造,但是这里只会进行浅拷贝,不满足需要。如果有需要,可以自己实现拷贝构造,实现起来也很简单。

HashBucket()
	:_n(0)
{
	_bucket.resize(10);//构建时默认开10个空间
}

~HashBucket()//析构函数
{
	for (auto& cur : _bucket)
	{
		while (cur)
		{
			Node* prev = cur;
			cur = cur->_next;

			delete prev;
			prev = nullptr;
		}
	}
}

1.3 数据插入

insert()函数返回的是pair,这是为了后续实现[]重载做准备。

在插入时,首先要先查看哈希桶中是否存在相同键值,存在就直接返回当前位置。第二步就是要查看哈希桶中的元素个数与哈希桶的容量之间的负载因子,如果等于1,就需要进行扩容。第三步则是开始插入节点。先找到映射位置,然后新建一个节点连接到对应的数组下标的空间中即可。

pair insert(const T& data)//插入数据
{
	KeyOfT kt;
	iterator it = find(kt(data));
	if (it != end())//不允许数据重复,找到相同的返回
		return make_pair(it, false);

	if (_bucket.size() == _n)//负载因子设置为1,超过就扩容
	{
		vector newbucket;//这种方式就无需再开空间拷贝节点
		newbucket.resize(NextPrime(_bucket.size()));//开一个素数大小的空间
		for (size_t i = 0; i < _bucket.size(); ++i)
		{
			Node* cur = _bucket[i];
			while (cur)
			{
				Node* next = cur->_next;

				size_t hashi = Hash()(kt(cur->_data)) % newbucket.size();//找映射位置
				cur->_next = newbucket[hashi];//头插到新哈希表
				newbucket[hashi] = cur;

				cur = next;
			}

			_bucket[i] = nullptr;//将原哈希表中的指针置为空
		}

		_bucket.swap(newbucket);//将newbucket中的节点全部交换给_bucket
	}

	Hash knt;
	size_t hashi = knt(kt(data)) % _bucket.size();//找映射位置

	Node* newnode = new Node(data);
	newnode->_next = _bucket[hashi];//头插,让插入的节点的下一个指针指向头节点
	_bucket[hashi] = newnode;//让头节点指向插入的节点
	++_n;

	return make_pair(iterator(newnode, this), true);
}

1.4 数据查找

查找数据很简单。先通过hash函数计算出要查找的数据键值所对应的位置,如果对应的位置上存储的是空,说明不存在,直接返回。如果不为空,则比对键值,相同返回,不相同向下找直到为空。

iterator find(const K& key)//查找
{
	size_t pos = Hash()(key) % _bucket.size();//找映射位置	

	Node* cur = _bucket[pos];
	while (cur)
	{
		if (KeyOfT()(cur->_data) == key)
			return iterator(cur, this);
		else
			cur = cur->_next;
	}

	return end();
}

1.5 数据删除

哈希桶与哈希表不同,要删除数据时不能使用find()函数搜索。因为哈希桶中的数据是用单链表链接起来的,用find()就无法知道节点的父节点,进而无法链接链表。

因此,在删除时,首先要调用hash函数获取关键码,根据关键码蛆对应位置上找。如果该位置存的数据为空,则表示不存在,返回;如果不为空,就比较键值,相同为找到,删除,不同就继续向下找直到为空。

bool erase(const K& key)//删除
{
	size_t hashi = Hash()(key) % _bucket.size();//找映射位置
	Node* cur = _bucket[hashi];
	Node* prev = nullptr;

	while (cur)
	{
		if (KeyOfT()(cur->_data) == key)
		{
			if (cur == _bucket[hashi])
				_bucket[hashi] = cur->_next;
			else
				prev->_next = cur->_next;

			delete cur;
			--_n;
			cur = nullptr;

			return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
		}
	}
	return false;
}

1.6 迭代器实现

哈希桶中的迭代器实现方式比较复杂。要使用迭代器,首先就要有能够遍历哈希桶的手段。因此,为了便于遍历,迭代器的结构体中首先要有哈希桶的指针。当然,迭代器的结构体中还需要有一个数据的指针,用于初始化迭代器,获取对应位置上的内容。

要遍历哈希桶很简单,和find()的逻辑差不多。直接判断当前节点是否为空,不为空则返回;为空就说明当前下标对应的位置上已经没有数据了,就调用hash函数获取该节点的关键码。++关键码走向下一个下标,不为空则返回;为空则继续走,直到找到不为空的位置或结束。

template
class HashBucket;//前置声明,因为迭代器中使用了HashBucket的模板,但是迭代器在它之前无法找到,所以要前置声明

template
struct HashIterator
{
	typedef HashNode Node;
	typedef HashIterator Self;
	typedef HashBucket HB;//将哈希桶传进来

	HashIterator(Node* node, HB* hb)
		:_node(node)
		,_hb(hb)
	{}

	T& operator*()//解引用
	{
		return _node->_data;
	}

	T* operator->()//运算符->重载
	{
		return &_node->_data;
	}

	bool operator!=(const Self& sl) const//判断是否相等
	{
		return _node != sl._node;
	}

	Self operator++()//运算符++重载
	{
		if (_node->_next)//节点的下一个位置不为空
		{
			_node = _node->_next;
		}
		else//节点的下一个位置为空,说明该位置上已经没有值,找下一个不为空的桶节点
		{
			KeyOfT kt;
			size_t hashi = Hash()(kt(_node->_data)) % _hb->_bucket.size();
			++hashi;
			while (hashi < _hb->_bucket.size())//当前位置小于桶的数量
			{
				if (_hb->_bucket[hashi])//如果hashi位置上不为空,则修改_node的值
				{
					_node = _hb->_bucket[hashi];
					break;
				}
				else
					++hashi;
			}

			if (hashi == _hb->_bucket.size())//如果hashi的位置与桶的数量相当,说明没有找到
				_node = nullptr;
		}
		return *this;
	}

	Node* _node;//节点指针
	HB* _hb;//哈希桶的指针
};

注意,因为模板是向上寻找的,在迭代器的类模板中使用了哈希桶的类模板,所以如果迭代器类模板在哈希桶类模板之上,就需要进行类声明,让迭代器类模板能找到哈希桶类模板的位置。反之亦然。

注意,因为迭代器中传入了哈希桶,要对哈希桶进行遍历。但因为哈希桶中的成员变量都被设置为了私有,所以可以将迭代器声明为哈希桶的友元类,也可以单独提供一个获取哈希桶指针的函数

有了迭代器的类模板后,哈希桶的迭代器实现起来就很轻松了。

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

	return iterator(nullptr, this);
}

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

注意,哈希桶的begin()要返回的是第一个不为空的桶,而不是第一个节点

1.7 哈希桶的整体实现

有了上面的内容,就可以写出一个比较完整,能够同时提供给unordered_map和unordered_set同时使用的哈希桶了:

#pragma once
#include
#include
#include

using namespace std;

namespace BucketHash//哈希桶
{
	//哈希桶内的节点/
	template
	struct HashNode
	{
		T _data;//存储数据
		HashNode* _next;//指向下一个节点

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

	///哈希函数,用于将数据类型转换为整形
	template
	struct HashFunc//用于提供将可以转换成整形的数据进行转换
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	template<>
	struct HashFunc//模板特化,用于将字符串转为整形
	{
		size_t operator()(const string& str)
		{
			int hash = 0;
			for (const auto& e : str)
			{
				hash = hash * 131 + e;
			}

			return hash;
		}

	};

	哈希桶的迭代器实现//
	template
	class HashBucket;//前置声明,因为迭代器中使用了HashBucket的模板,但是迭代器在它之前无法找到,所以要前置声明

	template
	struct HashIterator
	{
		typedef HashNode Node;
		typedef HashIterator Self;
		typedef HashBucket HB;//将哈希桶传进来

		HashIterator(Node* node, HB* hb)
			:_node(node)
			,_hb(hb)
		{}

		T& operator*()//解引用
		{
			return _node->_data;
		}

		T* operator->()//运算符->重载
		{
			return &_node->_data;
		}

		bool operator!=(const Self& sl) const//判断是否相等
		{
			return _node != sl._node;
		}

		Self operator++()//运算符++重载
		{
			if (_node->_next)//节点的下一个位置不为空
			{
				_node = _node->_next;
			}
			else//节点的下一个位置为空,说明该位置上已经没有值,找下一个不为空的桶节点
			{
				KeyOfT kt;
				size_t hashi = Hash()(kt(_node->_data)) % _hb->_bucket.size();
				++hashi;
				while (hashi < _hb->_bucket.size())//当前位置小于桶的数量
				{
					if (_hb->_bucket[hashi])//如果hashi位置上不为空,则修改_node的值
					{
						_node = _hb->_bucket[hashi];
						break;
					}
					else
						++hashi;
				}

				if (hashi == _hb->_bucket.size())//如果hashi的位置与桶的数量相当,说明没有找到
					_node = nullptr;
			}
			return *this;
		}

		Node* _node;//节点指针
		HB* _hb;//哈希桶的指针
	};

	///哈希桶实现/
	template
	class HashBucket
	{
		template
		friend struct HashIterator;//友元声明,让迭代器可以访问哈希桶的私有成员

		typedef HashNode Node;
	public:
		typedef HashIterator iterator;
		///默认成员函数//
		HashBucket()
			:_n(0)
		{
			_bucket.resize(10);//构建时默认开10个空间
		}

		~HashBucket()//析构函数
		{
			for (auto& cur : _bucket)
			{
				while (cur)
				{
					Node* prev = cur;
					cur = cur->_next;

					delete prev;
					prev = nullptr;
				}
			}
		}

		HashBucket(HashBucket& hb)//拷贝构造
			:_n(0)
		{
			_bucket.resize(hb._bucket.size());
			for (int i = 0; i < hb._bucket.size(); ++i)
			{
				Node* cur = hb._bucket[i];
				while (cur)
				{					
					insert(cur->_data);
					cur = cur->_next;
				}
			}
		}

		/迭代器提供///
		iterator begin()
		{
			for (size_t i = 0; i < _bucket.size(); ++i)
			{
				if (_bucket[i])
				{
					return iterator(_bucket[i], this);
				}
			}

			return iterator(nullptr, this);
		}

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

		获取素数,用于扩容
		inline unsigned long NextPrime(unsigned long n)
		{
			static const int primenum = 28;
			static const unsigned long  primelist[primenum] =
			{
				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
			};

			for (int i = 0; i < primenum; ++i)
			{
				if (primelist[i] > n)
					return primelist[i];
			}

			return primelist[primenum - 1];//如果扩大了最大值,就不再扩容
		}

		///正常提供外部使用的函数///
		pair insert(const T& data)//插入数据
		{
			KeyOfT kt;
			iterator it = find(kt(data));
			if (it != end())//不允许数据重复,找到相同的返回
				return make_pair(it, false);

			if (_bucket.size() == _n)//负载因子设置为1,超过就扩容
			{
				vector newbucket;//这种方式就无需再开空间拷贝节点
				newbucket.resize(NextPrime(_bucket.size()));//开一个素数大小的空间
				for (size_t i = 0; i < _bucket.size(); ++i)
				{
					Node* cur = _bucket[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = Hash()(kt(cur->_data)) % newbucket.size();//找映射位置
						cur->_next = newbucket[hashi];//头插到新哈希表
						newbucket[hashi] = cur;

						cur = next;
					}

					_bucket[i] = nullptr;//将原哈希表中的指针置为空
				}

				_bucket.swap(newbucket);//将newbucket中的节点全部交换给_bucket
			}

			Hash knt;
			size_t hashi = knt(kt(data)) % _bucket.size();//找映射位置

			Node* newnode = new Node(data);
			newnode->_next = _bucket[hashi];//头插,让插入的节点的下一个指针指向头节点
			_bucket[hashi] = newnode;//让头节点指向插入的节点
			++_n;

			return make_pair(iterator(newnode, this), true);
		}

		iterator find(const K& key)//查找
		{
			size_t pos = Hash()(key) % _bucket.size();//找映射位置	

			Node* cur = _bucket[pos];
			while (cur)
			{
				if (KeyOfT()(cur->_data) == key)
					return iterator(cur, this);
				else
					cur = cur->_next;
			}

			return end();
		}

		bool erase(const K& key)//删除
		{
			size_t hashi = Hash()(key) % _bucket.size();//找映射位置
			Node* cur = _bucket[hashi];
			Node* prev = nullptr;

			while (cur)
			{
				if (KeyOfT()(cur->_data) == key)
				{
					if (cur == _bucket[hashi])
						_bucket[hashi] = cur->_next;
					else
						prev->_next = cur->_next;

					delete cur;
					--_n;
					cur = nullptr;

					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

	private:
		vector _bucket;//存储数据的指针数组
		size_t _n;
	};
}

2. unordered_map的实现

有了哈希桶,unordered_map实现起来就很简单了,仅仅只需要在哈希桶的基础上进行封装即可。

#pragma once
#include "HashBucket.h"

namespace MyUnordered_map
{
	template>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair& kv)
			{
				return kv.first;
			}
		};
	public:
		//要加上typename告诉编译器这里是类型,不是实例化对象
		typedef typename BucketHash::HashBucket, Hash, MapKeyOfT>::iterator iterator;

		iterator begin()
		{
			return _mp.begin();
		}

		iterator end()
		{
			return _mp.end();
		}

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

		pair insert(const pair& kv)//插入函数
		{
			return _mp.insert(kv);
		}

		iterator find(const K& key)//查找
		{
			return _mp.find(key);
		}

		bool erase(const K& key)//删除
		{
			return _mp.erase(key);
		}
	private:
		BucketHash::HashBucket, Hash, MapKeyOfT> _mp;
	};
}

在这里面,实现了运算符[]的重载。总的来讲,并没有什么难度。

3. unordered_set的实现

unordered_set要实现起来基本和unordered_map是一样的,仅仅只是修改了下传给哈希桶的数据和不用对[]进行重载。

#pragma once
#include "HashBucket.h"

namespace MyUnordered_set
{
	template>
	class unordered_set
	{
		struct SetKeyOfT//返回键值
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

	public:
		typedef typename BucketHash::HashBucket::iterator iterator;//封装哈希桶的迭代器

		iterator begin()
		{
			return _st.begin();
		}

		iterator end()
		{
			return _st.end();
		}

		pair insert(const K& key)//插入函数
		{
			return _st.insert(key);
		}

		iterator find(const K& key)//查找
		{
			return _st.find(key);
		}

		bool erase(const K& key)//删除
		{
			return _st.erase(key);
		}

	private:
		BucketHash::HashBucket _st;
	};
}

你可能感兴趣的:(C++,#,stl库,c++,哈希算法,开发语言,数据结构)