C++学习笔记---024
- C++之unordered_set和unordered_map的介绍与应用
- 1、unordered_set和unordered_map的简单介绍
- 1.1、unordered_set和unordered_map的基本概念
- 1.2、unordered_set和unordered_map的基本特性
- 2、unordered_set的基本操作
- 2.1、unordered_set的定义
- 2.2、unordered_set的基本接口使用
- 2.3、unordered_multiset的基本介绍
- 3、unordered_map的基本操作
- 3.1、unordered_map的定义
- 3.2、unordered_map的基本接口使用
- 3.3、unordered_multimap的基本介绍
- 4、map/set 和 unordered_map/unordered_set的区别
- 4.1、性能测试
- 4.2、总结
C++之unordered_set和unordered_map的介绍与应用
前言:
前面篇章学习了C++对于AVL树的知识认识和了解,接下来继续学习,C++的unordered_set和unordered_map等知识。
/知识点汇总/
1、unordered_set和unordered_map的简单介绍
1.1、unordered_set和unordered_map的基本概念
unordered_set 和 unordered_map 是 C++ 标准库中的两个关联容器,它们提供了基于哈希表的快速查找和插入操作。
unordered_set 是一个无序集合,它包含唯一的元素。换句话说,unordered_set 中的每个元素都是唯一的,并且没有重复。与 set 不同,unordered_set 不保证元素的排序,但通常提供了更快的插入和查找操作,因为它基于哈希表实现。
unordered_map 是一个无序关联数组(或哈希表),它存储键值对,并且每个键都是唯一的。与 map 不同,unordered_map 不保证元素的排序,但通常提供了更快的插入、删除和查找操作。
1.2、unordered_set和unordered_map的基本特性
unordered_set的基本特性:
1、unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。
2、在unordered_set中,元素的值同时也是唯一地标识它的key。
3、在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。
4、unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。
5、它的迭代器至少是前向迭代器。
unordered_map的基本特性:
1、unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
2、在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
3、在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4、unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5、unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
它的迭代器至少是前向迭代器。
2、unordered_set的基本操作
2.1、unordered_set的定义
void test_unordered_set1()
{
// 构造一个空壳的us1的unordered_set的容器
unordered_set<int> us1;
// 插入几个值
us1.insert(1);
us1.insert(2);
us1.insert(3);
us1.insert(4);
// 拷贝构造
unordered_set<int> us2(us1);
// 迭代器区间构造
unordered_set<int> us3(us2.begin(), us2.end());
// for循环打印一下
for (auto& e : us3)
{
cout << e << " ";
}
cout << endl;
}
2.2、unordered_set的基本接口使用
void test_unordered_set2()
{
// 先构造一个空的容器
unordered_set<int> us1;
// 插入元素(只有这一种插入法)
us1.insert(1);
us1.insert(2);
us1.insert(3);
us1.insert(1);
us1.insert(4);
us1.insert(5);
// 遍历容器第一种方法:迭代器遍历
unordered_set<int>::iterator it = us1.begin();
while (it != us1.end())
{
cout << *it << " ";
++it;
}
cout << endl; // 1 2 3 4 5
// 遍历容器第二种方法:for循环
for (auto& e : us1)
{
cout << e << " ";
}
cout << endl; // 1 2 3 4 5
// 删除元素的方式一:直接找到值进行删除
us1.erase(1);
// 删除元素的方法二:利用迭代器进行删除
unordered_set<int>::iterator pos = us1.find(2);
if (pos != us1.end())
{
us1.erase(pos);
}
// 打印一下
for (auto& e : us1)
{
cout << e << " ";
}
cout << endl; // 3 4 5
// 判断容器是否为空
cout << us1.empty() << endl; // 0
// 获取值为3的个数
cout << us1.count(3) << endl; // 1
// 查看当前容器的容量
cout << us1.size() << endl; // 3
// 交换数据
unordered_set<int> tmp{99, 88, 77, 66};
us1.swap(tmp);
// 打印一下
for (auto& e : us1)
{
cout << e << " ";
}
cout << endl; // 99 88 77 66
// 打印一下
for (auto& e : tmp)
{
cout << e << " ";
}
cout << endl; // 3 4 5
// 清空
us1.clear();
// 打印一下
for (auto& e : us1)
{
cout << e << " ";
}
cout << endl; //
}
2.3、unordered_multiset的基本介绍
大致实现的功能与unordered_map相同,但唯一不同的一点是在于这个多功能的set是允许值进行重复的!
这个多功能的set是相较于普通set来讲的count函数是返回的个数,而普通set的count函数是如果存在则返回1,不存在则返回0。
这个多功能set相较于普通set来讲的find函数是返回底层哈希表中第一个找到的键值为val的元素的迭代器,而普通set则是返回简单的key。
void test_unordered_set3()
{
unordered_multiset<int> ums1;
ums1.insert(1);
ums1.insert(2);
ums1.insert(4);
ums1.insert(3);
ums1.insert(1);
ums1.insert(5);
ums1.insert(2);
ums1.insert(7);
for (auto& e : ums1)
{
cout << e << " ";
}
cout << endl; // 1 1 2 2 3 4 5 7
}
3、unordered_map的基本操作
3.1、unordered_map的定义
void test_unordered_map1()
{
// 构造一个空的key为int,value为double的unordered_map
unordered_map<int, double> um1;
// 给um1赋上值
um1.insert(make_pair(1, 1.1));
um1.insert(make_pair(2, 2.2));
um1.insert(make_pair(3, 3.3));
um1.insert(make_pair(4, 4.4));
// 拷贝构造
unordered_map<int, double> um2(um1);
// 迭代器区间拷贝um2的一段
unordered_map<int, double> um3(um2.begin(), um2.end());
// for循环打印一下um3,um3没问题则um1和um2都没问题
for (auto& e : um3)
{
cout << e.first<< "=>" << e.second << " ";
}
cout << endl;
}
3.2、unordered_map的基本接口使用
void test_unordered_map2()
{
// 构造一个空的key为int,value为string的unordered_map
unordered_map<int, string> um1;
// 插入方法一:构造匿名对象插入
um1.insert(pair<int, string>(1, "111"));
um1.insert(pair<int, string>(2, "222"));
um1.insert(pair<int, string>(3, "333"));
// 插入方法二:调用make_pair插入
um1.insert(make_pair(4, "444"));
um1.insert(make_pair(5, "555"));
um1.insert(make_pair(6, "666"));
// 插入方法三:用operator[]
um1[7] = "777";
um1[8] = "888";
um1[9] = "999";
um1[10] = "000";
// 遍历方式一:利用迭代器进行遍历打印
//unordered_map<int, string>::iterator it = um1.begin();
auto it = um1.begin();
while (it != um1.end())
{
cout << (*it).first << "=>" << (*it).second << " ";
++it;
}
cout << endl; // 1=>111 2=>222 3=>333 4=>444 5=>555 6=>666 7=>777 8=>888 9=>999 10=>000
// 遍历方法二:利用for循环进行遍历打印
for (auto& e : um1)
{
cout << e.first<< "=>" << e.second << " ";
}
cout << endl; // 1=>111 2=>222 3=>333 4=>444 5=>555 6=>666 7=>777 8=>888 9=>999 10=>000
// 删除操作1:根据键值对key删除
um1.erase(5);
// 删除操作2:根据迭代器进行删除
unordered_map<int, string>::iterator rit = um1.find(7); // 顺带使用键值对key就可以用find函数了
if (rit != um1.end())
{
um1.erase(rit);
}
// 遍历打印一下,用for循环方便快捷一点
for (auto& e : um1)
{
cout << e.first << "=>" << e.second << " ";
}
cout << endl; // 1=>111 2=>222 3=>333 4=>444 6=>666 8=>888 9=>999 10=>000
// 修改键值对:通过find获得迭代器进行修改
auto pos = um1.find(1);
if (pos != um1.end())
{
pos->second = "11/11";
}
// 修改键值对:通过operator[]运算符重载进行修改
um1[2] = "22/22";
// 打印一下
for (auto& e : um1)
{
cout << e.first << "=>" << e.second << " ";
}
cout << endl; // 1=>11/11 2=>22/22 3=>333 4=>444 6=>666 8=>888 9=>999 10=>000
// 判空
cout << um1.empty() << endl; // 0 -- 不空
// 计算容器的大小
cout << um1.size() << endl; // 8个
// 计算容器中键值对的大小
cout << um1.count(3) << endl; // 1
// 交换两容器中的数据
unordered_map<int, string> tmp{{11, "123"}, { 22, "345" }};
um1.swap(tmp);
for (auto& e : tmp)
{
cout << "tmp=>" << " ";
cout << e.first << "=>" << e.second << " ";
}
cout << endl; // tmp=> 1=>11/11 2=>22/22 3=>333 4=>444 6=>666 8=>888 9=>999 10=>000
for (auto& e : um1)
{
cout << "um1=>" << " ";
cout << e.first << "=>" << e.second << " ";
}
cout << endl; // um1=> 11=>123 22=>345
// 清空
um1.clear();
for (auto& e : um1)
{
cout << e.first << "=>" << e.second << " ";
}
cout << endl;
}
重点讲一下[]:
1、若当前容器中已经存在着键值为key的键值对,则返回该键值对value的引用。
2、若当前容器中没有键值为key的键值对,则先插入键值对<key, value()>,然后再返回该键值对中value的引用。
3.3、unordered_multimap的基本介绍
这个容器与unordered_map基本一致,这两个的区别在于multimap允许键值对的冗余,也就是可以允许key和value有不同的值。
存在以下几个区别:find、count、operator[]接口功能
(1)find:
unordered_map 返回键值为key的键值对的迭代器
unordered_multimap 返回底层哈希表中第一个找到的键值为key的键值对的迭代器
(2)count:
unordered_map 键值为key的键值对存在则返回1,不存在则返回0(find成员函数可替代)
unordered_multimap 返回键值为key的键值对的个数(find成员函数不可替代)
(3)operator[]:
我们在unordered_multimap中是没有这个operator[]重载的,因为这个容器中是可以冗余的,所以我们不确定找的是哪一个,会导致很多的错误,所以我们的unordered_multimap是没有operator[]这个的!
void test_unordered_map3()
{
unordered_multimap<int, string> ummp1;
ummp1.insert(make_pair(2023, "yes"));
ummp1.insert(make_pair(2023, "no"));
ummp1.insert(make_pair(2023, "before"));
ummp1.insert(make_pair(2023, "now"));
for (auto& e : ummp1)
{
cout << e.first << "=>" << e.second << " ";
}
cout << endl;
}
4、map/set 和 unordered_map/unordered_set的区别
4.1、性能测试
#include <iostream>
#include <set>
#include <unordered_set>
#include <time.h>
using namespace std;
int main()
{
int N = 1000;
vector<int> v;
v.reserve(N);
srand((unsigned int)time(NULL));
//随机生成N个数字
for (int i = 0; i < N; i++)
{
v.push_back(rand());
}
//将这N个数插入set容器
set<int> s;
clock_t begin1 = clock();
for (auto e : v)
{
s.insert(e);
}
clock_t end1 = clock();
//将这N个数插入unordered_set容器
unordered_set<int> us;
clock_t begin2 = clock();
for (auto e : v)
{
us.insert(e);
}
clock_t end2 = clock();
//分别输出插入set容器和unordered_set容器所用的时间
cout << "set insert: " << end1 - begin1 << endl;
cout << "unordered_set insert: " << end2 - begin2 << endl;
//在set容器中查找这N个数
clock_t begin3 = clock();
for (auto e : v)
{
s.find(e);
}
clock_t end3 = clock();
//在unordered_set容器中查找这N个数
clock_t begin4 = clock();
for (auto e : v)
{
us.find(e);
}
clock_t end4 = clock();
//分别输出在set容器和unordered_set容器中查找这N个数所用的时间
cout << "set find: " << end3 - begin3 << endl;
cout << "unordered_set find: " << end4 - begin4 << endl;
//将这N个数从set容器中删除
clock_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
clock_t end5 = clock();
//将这N个数从unordered_set容器中删除
clock_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
clock_t end6 = clock();
//分别输出将这N个数从set容器和unordered_set容器中删除所用的时间
cout << "set erase: " << end5 - begin5 << endl;
cout << "unordered_set erase: " << end6 - begin6 << endl;
return 0;
}
4.2、总结
(1)、map 和 set:
- 优点
有序性:由于红黑树的特性,map和set中的元素都是有序的,这在很多应用中都会简化操作。
检索效率:红黑树的检索效率较高,通常可以达到O(log N)的时间复杂度。
- 缺点
空间占用:由于红黑树需要维护树的平衡和节点的额外信息(如父节点、子节点、颜色等),因此相对于其他数据结构(如哈希表),map和set的空间占用率较高。
(2)、unordered_map 和 unordered_set
- 优点
查找速度:由于哈希表的特性,unordered_map和unordered_set的查找速度非常快,平均时间复杂度可以达到O(1)。
动态增长:unordered_map和unordered_set可以动态增长,并且可以通过size属性获取元素个数。
- 缺点
遍历顺序:由于哈希表的特性,unordered_map和unordered_set中的元素是无序的。因此,遍历它们的顺序可能与插入顺序不同。
哈希表建立:虽然哈希表的查找速度很快,但是哈希表的建立过程(即哈希函数的计算和哈希表的初始化)可能会比较耗时。
(3)、性能总结
1.查找速度:对于查找操作,unordered_map/unordered_set 通常比 map/set 快,因为哈希表的查找时间复杂度通常为O(1),而红黑树的查找时间复杂度为O(log N)。
2.有序性:如果需要保持元素的顺序,map/set 是更好的选择,因为它们是基于红黑树实现的,可以提供有序的键值对或元素存储。
3.空间占用:由于红黑树需要维护额外的信息,因此 map/set 的空间占用率通常高于 unordered_map/unordered_set。
4.遍历顺序:如果遍历顺序很重要,且需要按照插入顺序遍历,那么应该选择 Set 或 Map,因为 unordered_map/unordered_set 的遍历顺序可能与插入顺序不同。