【C++】mapset使用与实战 OJ题
📚 博主的专栏
🐧 Linux | 🖥️ C++ | 📊 数据结构
关联文章:【C++】搜索二叉树的实现以及应用-CSDN博客
目录
🌟 核心知识点概览
set的介绍
set的使用
set的模板参数列表
1. 插入元素
2. 遍历与访问
3. 创建与初始化
4. 查找与统计
oj:
349. 两个数组的交集 - 力扣(LeetCode)
multiset
初始化与插入
批量插入验证
map的使用:
1. 构造初始化与插入
pair编辑
make_pair
编辑
initializer_list
使用迭代器遍历:
范围for遍历:
2. 安全查找
3. 统计次数:
4.operator[]的简单实现与使用
oj
138. 随机链表的复制 - 力扣(LeetCode)
5.count --- 计数,可判断是否存在,返回值是存在的个数
multimap:
equal_range
功能和用途
返回值
输出
进阶
1. 自定义排序规则
2. 范围查询
3. 高效删除
OJ练习
692. 前K个高频单词 - 力扣(LeetCode)
解决办法1:使用一个稳定的排序就能解决
实现原理
stable_sort与 sort 的区别
解决办法2:仿函数的返回值,次数大的在前面,次数相等的,字典序小的在前面
🌟 核心知识点概览
关联式容器 vs 序列式容器
序列式容器:
vector
、list
等,存储元素本身,无关联性。关联式容器:
set
、map
等,存储<key, value>
键值对,数据关联性强,查询效率高(O(log n))。
🔑 核心区别:底层数据结构(红黑树 vs 线性结构)。键值对(Key-Value)
表示一一对应关系,如字典中的英文单词与释义。
set
是key
模型,map
是key-value
模型,底层均为红黑树。🌲 树形结构的关联式容器
1. Set
特性:
元素唯一、有序、不可修改。
插入自动去重,遍历输出有序序列。
底层红黑树实现,查找时间复杂度为
O(log n)
。操作示例:
2. Map
特性:
存储
<key, value>
键值对,key
唯一。支持
operator[]
快速访问(插入或修改值)。操作示例:
3. Multiset & Multimap
特性:
允许重复键值,元素按排序规则存储。
multimap
无operator[]
,需用equal_range
遍历相同键值。示例:
multimap<string, int> mm; mm.insert({"apple", 1}); mm.insert({"apple", 2}); // 允许重复
⚙️ 进阶操作与技巧
自定义排序规则
范围查询与删除
统计高频元素(LeetCode 692)
结合
map
统计词频,priority_queue
取Top K。仿函数优化:
🚀 实战应用场景
数组交集(LeetCode 349)
双指针遍历有序集合,比较元素大小。
随机链表复制(LeetCode 138)
使用
map
记录新旧节点映射关系。
1. 关联式容器
在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。存储的数据与数据之间没什么关联
那什么是关联式容器?它与序列式容器有什么区别?
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。不仅仅是数据存储,一般还可以查找数据,存储的数据和数据之间有很强的关联性。
2. 键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
实际上set就是key模型的搜索树,map就是key-value的搜索树,但是底层不是直接的搜索树,底层使用的是红黑树,时间复杂度是O(logn),10亿个值,找30次
对于搜索树的实现与应用可以看以下这篇博客:
3. 树形结构的关联式容器
根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
set的介绍
cplusplus.com/reference/set/set/
翻译:
1. set是按照一定次序存储元素的容器
2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。
set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对
子集进行直接迭代。
5. set在底层是用二叉搜索树(红黑树)实现的。
注意:
1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放
value,但在底层实际存放的是由<value, value>构成的键值对。
2. set中插入元素时,只需要插入value即可,不需要构造键值对。
3. set中的元素不可以重复(因此可以使用set进行去重)。
4. 使用set的迭代器遍历set中的元素,可以得到有序序列
5. set中的元素默认按照小于来比较
6. set中查找某个元素,时间复杂度为:$log_2 n$
7. set中的元素不允许修改(为什么?)
8. set中的底层使用二叉搜索树(红黑树)来实现
set的使用
set的模板参数列表
T: set中存放元素的类型,实际在底层存储<value, value>的键值对。
Compare:set中元素默认按照小于来比较
Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
1. 插入元素
set的主业是搜索副业是排序(中序排序),并且去重
#include<iostream>
#include<set>
using namespace std;
void test_set1()
{
set<int> s1;
s1.insert(1);
s1.insert(5);
s1.insert(0);
s1.insert(11);
s1.insert(6);
s1.insert(9);
s1.insert(3);
s1.insert(20);
s1.insert(1);
s1.insert(8);
//遍历:
set<int>::iterator it = s1.begin();
while(it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
test_set1();
return 0;
}
输出结果:
set的普通迭代器不允许修改:
2. 遍历与访问
vector<int> v = { 1,5,4,5456,45,885 };
set<int> s2(v.begin(), v.end());
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
运行结果:
1 4 5 45 885 5456
3. 创建与初始化
set支持{}初始化:
set<int> s1 = {3, 1, 4, 1, 5}; // 实际存储 {1, 3, 4, 5}
set<string> s2{"apple", "banana"};
4. 查找与统计
if (s1.find(3) != s1.end()) {
cout << "Found 3" << endl;
}
cout << "Count of 3: " << s1.count(3) << endl; // 输出1或0
oj:
349. 两个数组的交集 - 力扣(LeetCode)
在set中两个数组已经排好了,使用迭代器比较
1.相等就是交集,it1++,it2++
2.不相等,小的++
3.有一个到尾,就结束
如果想要找差集?比对(同步)算法(几个设备之间的数据同步,我有你没有,你就同步)
1.相等就是交集,it1++,it2++
2.不相等,小的就是差集(后面不可能有比我小的),小的++
multiset
允许存储重复元素
相同元素按插入顺序相邻存储(排序规则依然有效)
时间复杂度与
set
相同:插入/查找 O(log n)
初始化与插入
multiset<int> ms = {3, 1, 4, 1, 5}; // 存储 {1, 1, 3, 4, 5}
ms.insert(1); // 插入第四个"1"
批量插入验证
vector<int> batch{2, 2, 2};
ms.insert(batch.begin(), batch.end()); // 插入三个"2"
multiset中,相等值可以在左边,也可以在右边,看具体实现,左边树默认,插入在右边,find默认查找的是中序的第一个(我找到这个x了,并且左树再没有x,那就是现在找到的这个x)
map的使用:
1. 构造初始化与插入
map<string, int> m1;
m1["apple"] = 5; // 插入或修改
m1.insert({"banana", 3}); // 插入(若存在则不修改)
m1.emplace("orange", 8); // 原地构造
pair
map<string, string> dict;
pair<string, string> kv1("sort", "排序");
dict.insert(pair<string, string>("left", "左"));
//pair<string, string> kv2{"sort", "排序"};
dict.insert({ "hello", "c++" });//隐式类型转换,转pair
dict.insert(kv1);
dict.insert(kv2);
make_pair
只会比较key,如果key值相同就不会再插入
dict.insert(make_pair("left", "左"));
initializer_list
map<string, string> dict2 = { {"sort", "排序"},{"left", "左"},{"right", "右"} };
使用迭代器遍历:
//遍历
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
//pair不支持流插入
//cout << *it << " ";
//cout << (*it).first << ":" << (*it).second << " ";
cout << it->first << ":" << it->second << " ";//这里省略了一个->
++it;
}
cout << endl;
first(key不能修改,修改了改变了搜索树大小关系)不能修改,second(value)可以修改
这里所使用的it -> first it->second的原理,可以看这篇博客:List
查找目录 :
范围for遍历:
相当于把*it给了kv 最好加&,这样就没有拷贝,
不然全去走深拷贝,代价大,如果不需要修改,加const
for (auto& kv : dict) //相当于把*it给了kv 最好加&,不然全去走深拷贝,代价大
{
cout << kv.first << ":" << kv.second << " ";
}
cout << endl;
2. 安全查找
if (auto it = m1.find("apple"); it != m1.end()) {
cout << it->second << endl; // 使用迭代器访问
}
3. 统计次数:
void test_map2()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
map<string, int> countMap;
for (auto& e : arr)
{
auto it = countMap.find(e);
if (it != countMap.end())
{
it->second++;//找到了,就计数++
}
else
{
//const pair<string, int>& val = { e, 1 }; //等价
countMap.insert({ e, 1 });//首次出现,插入到countMap
}
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
按照Key(这里就是字符)来排序的:而不是按照val
4.operator[]的简单实现与使用
operator[]的本质就是,给一个key,就返回key对应的value的引用
需要注意的是,如果已经存在key了,直接返回这个key的value的引用,如果没有这个key,就先插入这个key,value相当于一个缺省值。
void test_map2() { string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" }; map<string, int> countMap; for (auto& e : arr) { countMap[e]++; //map最常用的就是[] } for (auto& kv : countMap) { cout << kv.first << ":" << kv.second << endl; } cout << endl; }
运行结果:
常规的operator[]是通过下标的来随机访问,看这里与vector的对比
类似于这样写的:
示例:插入,插入 + 修改
这种情况,就充当查找
修改:
operator[]能够当vector来使用吗?
这两个的意义相同吗?
vector的v[5],返回的是下标为5的数据。
而map的m[5],1.如果没有key = 5,就会新插入一个,如果有,假设从key = 0,1,2,3,4,5都和左边是对应的,那么差不多,但是也有可能只有m[5],就直接返回key = 5所对应的val。
因此,Countmap[]++的能直接计数原理在于:
如果苹果不存在,遇到苹果的时候,充当的就是插入 + 修改,key就是苹果,而val++。
oj
138. 随机链表的复制 - 力扣(LeetCode)
解答:
5.count --- 计数,可判断是否存在,返回值是存在的个数
string str;
cin >> str;
if (dict.count(str))
{
cout << "在" << endl;
}
else
{
cout << "不在" << endl;
}
multimap:
multimap允许键值冗余,就不会存在插入失败的问题,返回新插入节点的迭代器,没有operator[],因为key存在多个。
想让上面的水果按出现的次数排序:
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
//想让上面的水果按出现的次数排序:
map<int, string> sortMap;
for (auto& kv : countMap)
{
sortMap[kv.second] = kv.first;
}
cout << endl;
for (auto& kv : sortMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
这种方法是不正确的,这里使用的是operator[],因为草莓和香蕉出现的次数一样key值同样,插入香蕉的时候,草莓被修改成香蕉,如果直接使用插入insert会导致插入失败,因为存在相同的key值。
因此得使用mutimap: 并且插入sortMap.insert({ kv.second, kv.first });
equal_range
想删除某个范围,想找出某个范围:
功能和用途
std::multimap::equal_range
会返回一个std::pair
,其中包含两个迭代器:
第一个迭代器指向第一个键等于指定键的元素。
第二个迭代器指向最后一个键等于指定键的元素的下一个位置。
通过这两个迭代器,可以遍历所有具有相同键的元素。
返回值
std::multimap::equal_range
返回一个std::pair
,其中:
pair.first
是一个迭代器,指向第一个键等于指定键的元素。
pair.second
是一个迭代器,指向最后一个键等于指定键的元素的下一个位置。如果找不到任何键等于指定键的元素,
pair.first
和pair.second
都会等于multimap.end()
。
以下是一个使用 std::multimap::equal_range
的示例:
#include <iostream>
#include <map>
#include <string>
int main() {
// 创建一个 std::multimap
std::multimap<std::string, int> studentGrades = {
{"Alice", 85},
{"Bob", 90},
{"Alice", 92},
{"Charlie", 78},
{"Bob", 88}
};
// 查找所有键为 "Alice" 的元素
auto range = studentGrades.equal_range("Alice");
// 遍历范围内的所有元素
for (auto it = range.first; it != range.second; ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
return 0;
}
输出
Alice: 85
Alice: 92
进阶
1. 自定义排序规则
struct CaseInsensitiveCompare {
bool operator()(const string& a, const string& b) const {
return strcasecmp(a.c_str(), b.c_str()) < 0;
}
};
set<string, CaseInsensitiveCompare> caseInsensitiveSet;
2. 范围查询
auto low = s.lower_bound(3); // 第一个>=3的元素
auto high = s.upper_bound(8); // 第一个>8的元素
myset.erase(low, high)//[)左闭右开
3. 高效删除
// 删除所有偶数
for (auto it = s.begin(); it != s.end(); ) {
if (*it % 2 == 0) {
it = s.erase(it); // C++11起返回下一元素的迭代器
} else {
++it;
}
}
OJ练习
692. 前K个高频单词 - 力扣(LeetCode)
统计次数+Top K(优先级队列)
注意:此时的代码不能解决当有两个单词出现相同的次数,顺序会出错。
class Solution {
public:
//写一个仿函数
struct kvCom
{
bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2)
{
return kv1.second > kv2.second;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
//统计次数,此时单词已经按字典序排序
map<string, int> countMap;
for(auto& e : words)
{
countMap[e]++;
}
vector<pair<string, int>> v(countMap.begin(), countMap.end());
//pair支持比较大小
sort(v.begin(), v.end(), kvCom());
//取前k个
vector<string> vRet;
for(size_t i = 0; i < k; ++i)
{
vRet.push_back(v[i].first);
}
return vRet;
}
};
失败用例:
解决办法1:使用一个稳定的排序就能解决
如何理解稳定的排序?这是打印出的最后sort完的排序
1.首先map排完之后会得到一个按照原数组单词的比较后排序的一个序列:类似于这样
2.如果是稳定的,那么就会保证相同的值,相对顺序依旧不变
3.稳定的排序
stable_sort
是 C++ 标准库中提供的一种稳定排序算法,其核心特点是保持相等元素的原有相对顺序。
实现原理
归并排序:多数实现(如 GCC 的 libstdc++)在内存充足时使用归并排序。
自适应策略:小数据量可能使用插入排序(稳定且对缓存友好),大数据量切换为归并排序。
内存不足时:可能退化为原地归并排序或其他稳定算法,但时间复杂度增加。
stable_sort
与 sort
的区别
特性 | stable_sort | sort |
---|---|---|
稳定性 | 稳定(保持相等元素顺序) | 不稳定(可能改变顺序) |
时间复杂度 | O(n log n) 或 O(n log² n) | O(n log n) |
空间复杂度 | O(n) | O(log n)(递归栈) |
适用迭代器 | 仅随机访问迭代器 | 仅随机访问迭代器 |
4.将原来的sort --> stable_sort
stable_sort(v.begin(), v.end(), kvCom());
解决办法2:仿函数的返回值,次数大的在前面,次数相等的,字典序小的在前面
struct kvCom
{
bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2)
{
return kv1.second > kv2.second;
}
};
原来是次数大的在前面,现在添加 次数相等的,字典序小的在前面
//写一个仿函数
struct kvCom
{
bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2)
{
//次数大的在前面,次数相等的,字典序小的在前面
return kv1.second > kv2.second
|| (kv1.second == kv2.second && kv1.first < kv2.first);
}
};
结语:
随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。