当前位置: 首页 > news >正文

[C++][STL]unordered_set类和unordered_map类

一、unordered系列容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到。在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同。

unordered_xxx系列与map和set容器的用法上几乎没有任何区别

他们的区别就是

unordered_xxx系列都是哈希表作为底层的,而map和set是用红黑树作为底层的
unordered_xxx系列不排序,只去重
unordered_xxx系列是单项迭代器

二、unordered_set

如下就是unordered_set的文档

unordered_set是一种容器,它以无特定顺序的方式存储唯一的元素,并允许根据元素的值快速检索各个元素。

在unordered_set中,元素的值同时也是它的键,唯一标识该元素。键是不可变的,因此,unordered_set中的元素一旦放入容器后就不能被修改,不过可以插入和删除。

在内部,unordered_set中的元素不按任何特定顺序排序,而是根据它们的哈希值组织成桶,以便通过它们的值(平均具有恒定的平均时间复杂度)直接快速访问各个元素。

对于通过键访问单个元素,unordered_set容器比set容器更快,尽管对于通过子集范围迭代它们通常效率较低。

容器中的迭代器是单向迭代器。

这些接口其实大差不差

void test1()
{
	unordered_set<int> us;
	us.insert(7);
	us.insert(5);
	us.insert(4);
	us.insert(3);
	us.insert(1);
	us.insert(6);

	unordered_set<int>::iterator usit = us.begin();
	while (usit != us.end())
	{
		cout << *usit << endl;
		usit++;
	}
}

我们可以自己试着用一用

三、unordered_map

如下是unordered_map的文档

 unordered_map 是关联容器,用于存储由键值映射值组合形成的元素,并允许根据各个元素的键快速检索各个元素。
在 unordered_map 中,键值通常用于唯一标识元素,而映射值是具有与此关联的内容的对象。映射值的类型可能不同。
在内部,unordered_map中的元素不是根据其值或映射值按任何特定顺序排序的,而是根据其哈希值组织到中,以允许直接通过其键值快速访问各个元素(平均平均时间复杂度保持不变)。
unordered_map 容器通过访问单个元素的速度比 map 容器更快,尽管它们通常对其元素子集进行范围迭代的效率较低。
 unordered_map实现直接访问运算符 (operator[]),它允许使用其键值作为参数直接访问映射值
容器中的迭代器至少是单向迭代器。

我们可以试着用一用

void test2()
{
	unordered_map<string, string> dict;
	dict["insert"] = "插入";
	dict["sort"] = "排序";
	dict["delete"] = "删除";
	dict["string"] = "字符串";
	dict["insert"] = "xxxxx";
	dict.insert(make_pair("iterator", "迭代器"));
	unordered_map<string, string>::iterator umit = dict.begin();
	while (umit != dict.end())
	{
		cout << umit->first << ":" << umit->second << endl;
		umit++;
	}
	cout << endl;
}

四、unordered_set与set的比较

如下所示,我们采用如下代码进行比较

void test3()
{
	const size_t N = 100000;

	unordered_set<int> us;
	set<int> s;

	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(rand());
		//v.push_back(rand()+rand());  //减少重复数据
		//v.push_back(i);		  //有序的时候
	}

	size_t begin1 = clock();
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();
	cout << "set insert:" << end1 - begin1 << endl;

	size_t begin2 = clock();
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();
	cout << "unordered_set insert:" << end2 - begin2 << endl;


	size_t begin3 = clock();
	for (auto e : v)
	{
		s.find(e);
	}
	size_t end3 = clock();
	cout << "set find:" << end3 - begin3 << endl;

	size_t begin4 = clock();
	for (auto e : v)
	{
		us.find(e);
	}
	size_t end4 = clock();
	cout << "unordered_set find:" << end4 - begin4 << endl << endl;

	cout <<"插入数据个数:"<< s.size() << endl;
	cout <<"插入数据个数:" << us.size() << endl << endl;;

	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();
	cout << "set erase:" << end5 - begin5 << endl;

	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();
	cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
}


可以看到,在无序的数据的时候,unordered_set更占优势一些。

但是我们会发现有很多的重复数据,于是我们可以对随机值+随机值以此减少重复数据

可以看到,还是unordered_set占据优势

在有序的数据时,此时set占据优势

因此,如果数据是无序的,unordered_set更优,如果是有序的,使用set更好

五、哈希与各种查找的比较

1.直接查找

就是我们最常见的暴力查找,他的时间复杂度是O(N)

2.二分

不过我们可以对其进行一定程度的优化,即先排序,这样的画他的时间复杂度就变为了logN,但是增删还是不方便,而且排序也需要时间。

3.平衡树

再后来就是使用红黑树,他的效率都是很优秀的,增删查改都是logN

4.哈希

即存储的值和存储位置建立出一个对应关系,这样的话时间复杂度直接变为了O(1),哈希我们也称作散列,哈希的方式有点类似于计数排序

5.STL中的哈希

STL库中,set类和map类都是红黑树作为底层实现的,与之类似,unordered系列的unordered_set类和unordered_map类,都是通过哈希表作为底层来实现的。

六、哈希表的修改

下面是我们之前写的哈希表

namespace hash_bucket
{
	template<class K,class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next = nullptr;
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
		{}
	};

	template<class K>
	struct DefaultHashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};
	template<>
	struct DefaultHashFunc<string>
	{
		size_t operator()(const string& str)
		{
			size_t hashi = 0;
			for (auto ch : str)
			{
				hashi = hashi * 131 + ch;
			}
			return hashi;
		}
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		typedef HashNode<K,V> Node;
	public:
		HashTable()
			:_n(0)
		{
			_table.resize(10, nullptr);
		}
		
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}

			HashFunc hf;
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector<Node*> newTable(newSize, nullptr);
				for (int i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newTable.size();
						
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}

				_table.swap(newTable);
			}
			size_t hashi = hf(kv.first) % _table.size();
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

		void Print()
		{
			for (int i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				printf("[%d]->", i);
				while (cur)
				{
					cout << cur->_kv.first << ":" << cur->_kv.second << "->";
					cur = cur->_next;
				}
				cout << "NULL" << endl;
			}
		}

		Node* Find(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						_table[hashi] = cur->_next;
					}
					delete cur;
					cur = nullptr;
					_n--;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

		~HashTable()
		{
			for (int i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}
	private:
		vector<Node*> _table;
		size_t _n;
	};
}

现在我们需要为了把它改装成unordered系列容器

1.结点

template<class T>
struct HashNode
{
	HashNode<T>* _next;
	T _data;

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

对于哈希表的修改与之前我们用红黑树去封装map和set类似。

首先是将结点都改为T类型的,这个T类型对于set而言是K,对于map而言是pair<K,V>
 

2.迭代器

改造后的哈希表,最重要的功能之一就是支持单向迭代器
 

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 HashIterator
{
	typedef HashNode<T> Node;
	typedef HashTable<K, T, KeyOfT, Hash> Ht;
	typedef HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
	typedef HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;

	Node* _node;
	const Ht* _ht;

	HashIterator(Node* node, const Ht* ht)
		: _node(node)
		, _ht(ht)
	{}

	HashIterator(const Iterator& it)
		: _node(it._node)
		, _ht(it._ht)
	{}

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

	Ptr operator->()
	{
		return &(operator*());
	}

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

	bool operator==(const Self& s)
	{
		return _node == s._node;
	}
};
  1. 这里增加_ht成员变量,这样当一条单链表走到空,可以走到下一个哈希桶的位置,所以需要哈希表的地址
  2. 这里存在相互引用的问题,所以前置声明哈希表
  3. const修饰_ht,使const迭代器能够被构造
  4. 迭代器的拷贝构造函数有两个用途:
    • 以普通迭代器拷贝出普通迭代器(普通迭代器调用时)
    • 以普通迭代器拷贝出const迭代器(const迭代器调用时)

3.operator++

Self& operator++()
{
	if (_node->_next)
	{
		_node = _node->_next;
	}
	else
	{
		int flag = 0;
		size_t hashi = Hash()(KeyOfT()(_node->_data)) % _ht->_tables.size();
		for (size_t i = hashi + 1; i < _ht->_tables.size(); ++i)
		{
			if (_ht->_tables[i])
			{
				_node = _ht->_tables[i];
				flag = 1;
				break;
			}
		}

		if (!flag)
		{
			_node = nullptr;
		}
	}
	return *this;
}

Self operator++(int)
{
	Self tmp = *this;
	++*this;
	return tmp;
}
  1. 前置++的思路:
    • 下一个结点不为空,则跳到下一位
    • 下一个结点为空,则先取模算出哈希地址,再往后探测不为空的哈希桶
  2. 后置++:复用前置++,返回临时对象

4.本体

1.成员变量和默认成员函数

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 HashIterator;
protected:
	typedef HashNode<T> Node;
public:
	HashTable()
	{
		_tables.resize(10);
	}
	
	~HashTable()
	{
		for (auto& cur : _tables)
		{
			while (cur)
			{
				Node* del = cur;
				cur = cur->_next;
				delete del;
			}
		}
	}
protected:
	vector<Node*> _tables;
	size_t _n = 0;//有效数据个数
};
  1. 将迭代器声明为友元,使迭代器内部可操作_tables
  2. 第三个模板参数为KeyOfT(仿函数),用于获取不同数据T的键值key来进行比较
  3. 第四个模板参数为Hash(仿函数),用于将不同类型key转换为整型来进行取模

2.begin和end

typedef HashIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef HashIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;

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

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

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

const_iterator end() const
{
	return const_iterator(nullptr, this);
}
  1. begin返回最开始不为空的哈希桶的迭代器,end返回空迭代器
  2. 构造迭代器需要传入哈希表本身的地址,这里直接传this指针即可

3.Find

iterator Find(const K& key)
{
	size_t hashi = Hash()(key) % _tables.size();
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (KeyOfT()(cur->_data) == key)
		{
			return iterator(cur, this);
		}
		cur = cur->_next;
	}
	return iterator(nullptr, this);
}

  1. 返回迭代器
  2. Hash转整型,KeyOfT获取键值

4.Insert

pair<iterator, bool> Insert(const T& data)
{
	KeyOfT kot;
	iterator it = Find(kot(data));
	if (it._node)//保持key唯一
	{
		return make_pair(it, false);
	}
	
	Hash hash;
	if (_n == _tables.size())//负载因子为1时,扩容
	{
		size_t newsize = _tables.size() * 2;
		vector<Node*> newtables(newsize);
		for (auto& cur : _tables)
		{
			while (cur)
			{
				Node* next = cur->_next;
				//将旧表结点重新映射到新表上
				size_t hashi = hash(kot(cur->_data)) % newsize;
				cur->_next = newtables[hashi];
				newtables[hashi] = cur;
				//跳回旧表的下一结点
				cur = next;
			}
		}
		_tables.swap(newtables);
	}

	size_t hashi = hash(kot(data)) % _tables.size();
	Node* newnode = new Node(data);
	//头插
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	++_n;
	return make_pair(iterator(newnode, this), true);
}
  1. 返回pair,第一个参数为迭代器,第二个参数为布尔值(记录是否插入成功)

5.Erase

bool Erase(const K& key)
{
	size_t hashi = Hash()(key) % _tables.size();
	Node* prev = nullptr;
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (KeyOfT()(cur->_data) == key)
		{
			if (prev == nullptr)
			{
				_tables[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;
			--_n;
			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

5.unordered_set

1 成员变量与仿函数

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:
protected:
	HashTable<K, K, SetKeyOfT, Hash> _ht;
};
  1. unordered_set类仿函数,直接返回参数key
  2. 成员变量的第二个模板参数为K,第三个模板参数为SetKeyOfT
  3. 模板Hash可以根据特定需要而传手动实现的哈希化函数

2 begin和end

typedef typename HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
typedef typename HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;

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

const_iterator begin() const
{
	return _ht.begin();
}

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

const_iterator end() const
{
	return _ht.end();
}

3 find

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

4 insert

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

5 erase

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

6.unordered_map

1 成员变量与仿函数

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<const K, V>& kv)
		{
			return kv.first;
		}
	};
public:
protected:
	HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
  1. unordered_map类仿函数,返回参数pair的first
  2. 成员变量的第二个模板参数为pair,第三个模板参数为MapKeyOfT
  3. 模板Hash可以根据特定需要而传手动实现的哈希化函数

2 begin和end

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;

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

const_iterator begin() const
{
	return _ht.begin();
}

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

const_iterator end() const
{
	return _ht.end();
}

3 find

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

4 insert

pair<iterator, bool> insert(const pair<const K, V>& kv)
{
	return _ht.Insert(kv);
}

5 erase

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

6 operator[ ]

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

相关文章:

  • 【MySQL】从零开始:掌握MySQL数据库的核心概念(五)
  • columns实现瀑布流布局
  • ⑦(ACG-网络配置)
  • 06 单目标定-去畸变
  • React Native与原生Android数据传递
  • 【Unity】 鼠标拖动物体移动速度跟不上鼠标,会掉落
  • 学习《JS数据结构与算法》
  • 同步整流和异步整流区别及其各优点
  • vm虚拟机 Ubuntu ping失败情况解决方法
  • Python的迭代器(Iterator)介绍以及实现多次使用
  • SVTAV1热点函数-svt_ext_all_sad_calculation_8x8_16x16_avx2
  • pip 安装某个包之后,Jupyter Lab仍旧显示包冲突;例如:Numba needs NumPy 2.1 or less. Got NumPy 2.2.
  • 热血传奇2超高清重置UI素材
  • Java并发编程
  • SAP-ABAP:OData 协议深度解析:架构、实践与最佳应用
  • 重学Java基础篇—什么是快速失败(fail-fast)和安全失败(fail-safe)?
  • 【Pandas】pandas Series to_xarray
  • 类和对象—继承(1)
  • 什么是真理?以及人工智能对真理标准的挑战
  • c++ 日志框架G3log介绍及在嵌入式Linux上的移植(交叉编译)
  • 亦庄网站开发公司/厦门seo哪家强
  • 找人做的网站推广被坑/搜索引擎营销的作用
  • 沈阳企业网站模板建站/中国国家人事人才培训网官网
  • 中山小榄网站建设/策划公司
  • 做的网站怎样打开速度快/磁力棒
  • 杭州蚂蚁 做网站的公司/今日头条新闻手机版