【C++】map和set的使用
🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉关联式容器👈
- 👉键值对👈
- 👉树形结构的关联式容器👈
- set
- 1. set 的介绍
- 2. set 的使用
- multiset
- 1. multiset 的介绍
- 2. multiset 的使用
- map
- 1. map 的介绍
- 2. map 的使用
- multimap
- 1. multimap 的介绍
- 2. multimap 的使用
- 👉前K个高频单词👈
- 👉两个数组的交集👈
- 👉总结👈
👉关联式容器👈
我们已经接触过 STL 中的部分容器,比如:vector、list、deque、forward_list(C++11) 等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面
存储的是元素本身。序列式容器中存储的数据通常没有什么关系。那什么是关联式容器呢?
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是 <key, value> 结构的键值对,在数据检索时比序列式容器效率更高。
👉键值对👈
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量 key 和 value,key 代表键值,value 表示与 key 对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
SCI - STL 中关于键值对的定义
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair() : first(T1()), second(T2())
{}
pair(const T1& a, const T2& b) : first(a), second(b)
{}
};
👉树形结构的关联式容器👈
根据应用场景的不同,STL 总共实现了两种不同结构的关联式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
set
1. set 的介绍
- set 是按照一定次序存储元素的容器。
- 在 set 中,元素的 value 也标识它(value 就是 key,类型为 T),并且每个 value 必须是唯一的。 set 中的元素不能在容器中修改(元素总是 const),但是可以从容器中插入或删除它们。
- 在内部,set 中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
- set 容器通过 key 访问单个元素的速度通常比unordered_set 容器慢,但它们允许根据顺序对子集进行直接迭代。
- set 在底层是用二叉搜索树(红黑树)实现的。
注意:
- 与 map / multimap 不同,map / multimap 中存储的是真正的键值对 <key, value>,set 中只 value,但在底层实际存放的是由 <value, value> 构成的键值对。
- set 中插入元素时,只需要插入 value 即可,不需要构造键值对。
- set 中的元素不可以重复(因此可以使用 set 进行去重)。
- 使用 set 的迭代器遍历 set 中的元素,可以得到有序序列。
- set 中的元素默认按照小于来比较。
- set 中查找某个元素,时间复杂度为 l o g 2 N log_2 N log2N。
2. set 的使用
- set 的模板参数列表
T:set 中存放元素的类型,实际在底层存储 <value, value> 的键值对。
Compare:set 中元素默认按照小于来比较,即中序遍历的结果是升序(注:Compare 可以自己写一个仿函数来控制比较的方式)。
Alloc:set 中元素空间的管理方式,使用 STL 提供的空间配置器管理。
- set 的构造和赋值运算符重载
函数声明 | 功能介绍 |
---|---|
set (const Compare& comp = Compare(), const Allocator = Allocator() ); | 构造空的 set |
set (InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& = Allocator() ); | 用 [first, last) 区间中的元素构造 |
set (const set& x); | set 的拷贝构造 |
set& operator= (const set& x); | set 的运算符重载 |
注:set 的拷贝构造和赋值运算符重载,代价是比较大的!
- set 的修改操作
函数声明 | 功能介绍 |
---|---|
pair<iterator,bool> insert (const value_type& x ) | 在 set 中插入元素 x,实际插入的是 <x, x> 构成的键值对,如果插入成功,返回 <该元素在set中的位置,true>;如果插入失败,说明 x 在 set 中已经存在,返回 <x在set中的位置,false> |
void erase ( iterator position ) | 删除 set 中 position 位置上的元素 |
size_type erase ( const key_type& x ) | 删除 set 中值为 x 的元素,返回删除的元素的个数 |
void erase ( iterator first, iterator last ) | 删除 set 中 [first, last) 区间中的元素 |
void swap (set<Key,Compare,Allocator>&st ); | 交换 set 中的元素,通过交换根节点就可以实现了。 |
void clear ( ) | 将 set 中的元素清空 |
iterator find ( const key_type& x ) const | 返回 set 中值为 x 的元素的位置 |
size_type count ( constkey_type& x ) const | 返回 set 中值为 x 的元素的个数,可以通过该接口判断 x 在不在 |
注:set 中的元素不允许修改,因为修改元素可能会破坏二叉搜索树的结构。
- set 的使用举例
void SetTest1()
{
// 排序+去重
set<int> s = { 1, 2,1,6,3, 8,5,9 }; // C++11的列表初始化
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
void SetTest2()
{
// greater的该文件是functional
set<int, greater<int>> s = { 1, 2,1,6,3, 8,5,9 }; // C++11的列表初始化
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
void SetTest3()
{
int arr[] = { 1,2,1,6,3,8,5,9 };
// 迭代器区间初始化
set<int, greater<int>> s(arr, arr + sizeof(arr) / sizeof(arr[0]));
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
s.erase(3);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
auto it = s.find(2);
if (it != s.end())
{
s.erase(it);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
}
注:find 函数如果没有找到指定的 key 值,会返回 end();如果找到了,会返回该 key 值的迭代器位置。所以如果要通过迭代器位置来删除元素,需要判断该迭代器位置是否为 end()。而通过 key 值来删除的话,容器中没有指定 key 值的话,就返回 0;如果有的话,就返回 1。(注:这里的 0 和 1 是 key 值在容器 set 中的个数)
void SetTest4()
{
set<int> myset;
set<int>::iterator itlow, itup;
for (int i = 1; i < 10; i++)
myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
itlow = myset.lower_bound(30); // 返回大于等于val的第一个元素的迭代器位置
itup = myset.upper_bound(60); // 返回大于val的第一个元素的迭代器位置
myset.erase(itlow, itup); // 10 20 70 80 90
std::cout << "myset contains:";
for (auto it = myset.begin(); it != myset.end(); ++it)
std::cout << ' ' << *it;
cout << endl;
}
void SetTest5()
{
std::set<int> myset;
for (int i = 1; i <= 5; i++) myset.insert(i * 10); // myset: 10 20 30 40 50
std::pair<std::set<int>::const_iterator, std::set<int>::const_iterator> ret;
// equal_range的返回值是键值对pair<iterator,iterator>
ret = myset.equal_range(30); // x <= val < y
std::cout << "the lower bound points to: " << *ret.first << '\n';
std::cout << "the upper bound points to: " << *ret.second << '\n';
}
multiset
1. multiset 的介绍
- multiset 是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 在 multiset 中,元素的 value 也会识别它(因为multiset 中本身存储的就是 <value, value> 组成的键值对,因此 value 本身就是 key,key 就是 value,类型为 T)。multiset 元素的值不能在容器中进行修改(因为元素总是 const 的),但可以从容器中插入或删除。
- 在内部,multiset 中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排序。
- multiset 容器通过 key 访问单个元素的速度通常比unordered_multiset 容器慢,但当使用迭 代器遍历时会得到一个有序序列。
- multiset 的底层结构为二叉搜索树(红黑树)。
注意:
- multiset 中在底层中存储的是 <value, value> 的键值对。
- mtltiset的插入接口中只需要插入即可。
- 与 set 的区别是 multiset 中的元素可以重复,set 是中value 是唯一的。
- 使用迭代器对 multiset 中的元素进行遍历,可以得到有序的序列。
- multiset 中的元素不能修改。
- 在 multiset 中找某个元素,时间复杂度为 O ( l o g 2 N ) O(log_2 N) O(log2N)。
- multiset 的作用是可以对元素进行排序。
2. multiset 的使用
void MultisetTest1()
{
int arr[] = { 1, 2,1,8,5,7,6,2,9 };
multiset<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
cout << s.count(1) << endl;
auto pos = s.find(2);
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
s.erase(2);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
注:find 返回的是中序遍历中第一个 key 值的迭代器位置,通过 key 值来调用 erase 函数是会删除所有的 key 值的。
map
1. map 的介绍
- map 是关联容器,它按照特定的次序(按照 key 来比较)存储由键值 key 和值 value 组合而成的元素。
- 在 map 中,键值 key 通常用于排序和唯一地标识元素,而值 value 中存储与此键值 key 关联的内容。键值 key 和值 value 的类型可能不同。在 map 的内部,key 与value 通过成员类型 value_type 绑定在一起,为其取别名称为 pair:
typedef pair<const key, T> value_type;
- 在内部,map 中的元素总是按照键值 key 进行比较排序的。
- map 中通过键值访问单个元素的速度通常比unordered_map 容器慢,但 map 允许根据顺序对元素进行直接迭代(即对 map 中的元素进行迭代时,可以得到一个有序的序列)。
- map 支持下标访问符,即在
[]
中放入key,就可以找到与 key 对应的 value。- map 通常被实现为二叉搜索树(更准确地说,是平衡二叉搜索树(红黑树))。
2. map 的使用
- map 的模板参数说明
key:键值对中 key 的类型
T:键值对中 value 的类型
Compare:比较器的类型,map 中的元素是按照 key 来比较的,缺省情况下按照小于来比较。一般情况下,该参数(内置类型)不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)。
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器。
注意:在使用 map 时,需要包含头文件。
map 的修改操作和 set 的修改操作基本是相同的,就是 map 比 set 多了一个 value 值。
注:make_pair 是一个函数模板,其通常被定义成内联函数,其头文件是 utility,通过会被间接包含。
void MapTest1()
{
map<string, string> dict;
// 有名对象
pair<string, string> kv1("sort", "排序");
dict.insert(kv1);
// 匿名对象
dict.insert(pair<string, string>("test", "测试"));
dict.insert(pair<string, string>("sort", "排序"));
dict.insert(pair<string, string>("string", "字符串"));
typedef pair<string, string> DictKV;
dict.insert(DictKV("left", "左边"));
dict.insert(make_pair("left", "剩下")); // make_pair是个函数模板
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
//cout << (*it).first << ":" << (*it).second << endl;
// operator->返回的是结构的指针,再加上一个->就是结构中的数据
// 为了可读性,编译器省略了一个->
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
for (auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
注:map 的 operator*
返回的是结构体 pair。
注:map 和 set 的迭代器都是双向迭代器!!!
统计出现次数
void MapTest2()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto& str : arr)
{
map<string, int>::iterator it = countMap.find(str);
if (it != countMap.end())
++it->second;
else
countMap.insert(make_pair(str, 1));
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
void MapTest3()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto& str : arr)
{
// 1. str不在countMap中,插入pair(str, int()),再对value的引用进行++
// 2. str在countMap中,对value的引用进行++
++countMap[str];
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
multimap
1. multimap 的介绍
- multimap 是关联式容器,它按照特定的顺序,存储由 key 和 value 映射成的键值对 <key, value>,其中多个键值对之间的 key 是可以重复的。
- 在 multimap 中,通常按照 key 排序和唯一地标识元素,而映射的 value 存储与 key 关联的内容。key 和 value 的类型可能不同,通过 multimap 内部的成员类型 value_type 组合在一起,value_type 是组合 key 和 value 的键值对:
typedef pair<const Key, T> value_type;
- 在内部,multimap 中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对 key 进行排序的。
- multimap 通过 key 访问单个元素的速度通常比unordered_multimap 容器慢,但是使用迭代器直接遍历 multimap 中的元素可以得到关于 key 有序的序列。
- multimap 在底层用二叉搜索树(红黑树)来实现。
注意:multimap 和 map 的唯一不同就是:map中的key 是唯一的,而 multimap 中 key 是可以重复的。
2. multimap 的使用
multimap 中没有重载[]
,原因是 multimap 允许键值冗余。当有多个相同的键值时,不知道返回哪一个。
void MultimapTest()
{
multimap<string, string> mdict;
mdict.insert(make_pair("sort", "排序"));
mdict.insert(make_pair("left", "左边"));
mdict.insert(make_pair("left", "剩下"));
mdict.insert(make_pair("string", "字符串"));
for (auto& kv : mdict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
👉前K个高频单词👈
给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序排序。
思路一:可以通过优先级队列来做,认为出现次数多的单词优先级高。如果出现次数相同,则在字典顺序靠前的优先级高。根据优先级比较的规则,我们自己实现一个仿函数即可。
class Solution
{
struct Less
{
bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2) const
{
if(kv1.second < kv2.second)
return true;
else if(kv1.second == kv2.second && kv1.first > kv2.first)
return true;
else
return false;
}
};
public:
vector<string> topKFrequent(vector<string>& words, int k)
{
// 统计出现的次数
map<string, int> countMap;
for(auto& str : words)
{
++countMap[str];
}
// TOPK问题
priority_queue<pair<string, int>, vector<pair<string, int>>, Less> mh(countMap.begin(), countMap.end());
vector<string> v;
while(k--)
{
v.push_back(mh.top().first);
mh.pop();
}
return v;
}
};
思路二:因为countMap
中已经按照字典顺序排序了,所以我们可以采用稳定的排序对countMap
中的元素按照出现次数来排序就行了。sort 底层是一个快排,快排是不稳定排序,那么我们可以给 sort 传一个比较方式,就可以保证稳定性了。注:sort 要求传入的迭代器是随机迭代器,而 map 的迭代器是双向迭代器。那我们可以先将 map 的元素存储 vector 中。
class Solution
{
struct Greater
{
bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2) const
{
if(kv1.second > kv2.second)
return true;
else if(kv1.second == kv2.second && kv1.first < kv2.first)
return true;
else
return false;
}
};
public:
vector<string> topKFrequent(vector<string>& words, int k)
{
// 统计出现的次数
map<string, int> countMap;
for(auto& str : words)
{
++countMap[str];
}
vector<pair<string, int>> sortV(countMap.begin(), countMap.end());
sort(sortV.begin(), sortV.end(), Greater());
vector<string> v;
for(size_t i = 0; i < k; ++i)
{
v.push_back(sortV[i].first);
}
return v;
}
};
上面的解法是通过比较方式来控制稳定性的。那么我们也可以使用库里提供的稳定排序stable_sort
来控制稳定性。
class Solution
{
struct Greater
{
bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2) const
{
if(kv1.second > kv2.second)
return true;
else
return false;
}
};
public:
vector<string> topKFrequent(vector<string>& words, int k)
{
// 统计出现的次数
map<string, int> countMap;
for(auto& str : words)
{
++countMap[str];
}
vector<pair<string, int>> sortV(countMap.begin(), countMap.end());
stable_sort(sortV.begin(), sortV.end(), Greater());
vector<string> v;
for(size_t i = 0; i < k; ++i)
{
v.push_back(sortV[i].first);
}
return v;
}
};
思路三:因为countMap
已经按照字典顺序排序了,那可以将其中的元素依次插入到以整型为 key 值的multimap
中即可。
class Solution
{
public:
vector<string> topKFrequent(vector<string>& words, int k)
{
// 统计出现的次数
map<string, int> countMap;
for(auto& str : words)
{
++countMap[str];
}
// 传greater<int>也是为了保证稳定性
multimap<int, string, greater<int>> sortMap;
for(auto& str : countMap)
{
sortMap.insert(make_pair(str.second, str.first));
}
vector<string> v;
auto it = sortMap.begin();
for(size_t i = 0; i < k; ++i)
{
v.push_back(it->second);
++it;
}
return v;
}
};
👉两个数组的交集👈
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序 。
思路:因为set
容器是对元素进行排序加上去重的,所以我们可以用数组中的元素来构造set
,然后再来求交集。求交集的思路:谁小谁往后走,相等就添加到vector
中并同时往后走。
class Solution
{
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2)
{
set<int> s1(nums1.begin(), nums1.end());
set<int> s2(nums2.begin(), nums2.end());
auto it1 = s1.begin();
auto it2 = s2.begin();
vector<int> v;
while(it1 != s1.end() && it2 != s2.end())
{
if(*it1 < *it2)
++it1;
else if(*it1 > *it2)
++it2;
else
{
v.push_back(*it1);
++it1;
++it2;
}
}
return v;
}
};
补充:求差集的思路:先用两个数组的元素构造出两个et
,然后遍历set
。谁小就将谁添加到vector
中并且往后走,相等时就同时加加。求并集只需要将两个数组的元素插入到set
中就能得到两个数组的并集了。
class Solution
{
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2)
{
set<int> s1(nums1.begin(), nums1.end());
set<int> s2(nums2.begin(), nums2.end());
auto it1 = s1.begin();
auto it2 = s2.begin();
vector<int> v;
while(it1 != s1.end() && it2 != s2.end())
{
if(*it1 < *it2)
{
v.push_back(*it1);
++it1;
}
else if(*it1 > *it2)
{
v.push_back(*it2);
++it2;
}
else
{
++it1;
++it2;
}
}
// 后面的元素都是相差的元素
while(it1 != s1.end())
{
v.push_back(*it1);
++it1;
}
while(it2 != s2.end())
{
v.push_back(*it2);
++it2;
}
return v;
}
};
👉总结👈
本篇博客主要讲解了键值对、关联式容器 set、multiset、map 和 multiset 以及两道 OJ 题前 K 个高频单词和两个数组的交集等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️