1. 序列式容器和关联式容器
前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间一般没有紧密的关联关系,比如交换一下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置有紧密的关联关系,交换一下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。
本章节讲解的map和set底层是红黑树,红黑树是一棵平衡二叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
2. set系列的使用
2.1 set和multiset参考文档
https://legacy.cplusplus.com/reference/set/
2.2 set类的介绍
• set的声明如下,T就是set底层关键字的类型
• set默认要求T支持小于比较,如果不支持或者想按自己的需求可以自行实现仿函数传给第二个模版参数
• set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
• 一般情况下,我们都不需要传后两个模版参数。
• set底层是用红黑树实现,增删查效率是O(logN) ,迭代器遍历是走的搜索树的中序,所以是有序的。
• 前面部分我们已经学习了vector/list等容器的使用,STL容器接口设计,高度相似,所以这里我们就不再一个接口一个接口的介绍,而是直接带着大家看文档,挑比较重要的接口进行介绍。
template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type
> class set;
2.3 set的构造
#include <set>
#include <vector>
std::set<int> s1; // 默认构造(空集合)
std::set<int> s2 = {3, 1, 4, 1, 5}; // 初始化列表构造(元素去重后为 {1, 3, 4, 5})
std::vector<int> vec = {5, 2, 8};
std::set<int> s3(vec.begin(), vec.end()); // 迭代器范围构造(元素为 {2, 5, 8})
std::set<int> s4(s3); // 拷贝构造
2.3 set的增删查
set的增删查关注以下几个接口即可:
Member types
key_type -> The first template parameter (T)
value_type -> The first template parameter (T)
// 单个数据插入,如果已经存在则插入失败
pair<iterator,bool> insert (const value_type& val);
// 列表插入,已经在容器中存在的值不会插入
void insert (initializer_list<value_type> il);
// 迭代器区间插入,已经在容器中存在的值不会插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
// 查找val,返回val所在的迭代器,没有找到返回end()
iterator find (const value_type& val);
// 查找val,返回Val的个数
size_type count (const value_type& val) const;
// 删除一个迭代器位置的值
iterator erase (const_iterator position);
// 删除val,val不存在返回0,存在返回1
size_type erase (const value_type& val);
// 删除一段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
// 返回大于等val位置的迭代器
iterator lower_bound (const value_type& val) const;
// 返回大于val位置的迭代器
iterator upper_bound (const value_type& val) const;
2.5 insert
#include <iostream>
#include<set>
using namespace std;
int main()
{
// 去重+升序排序
set<int> s;
s.insert(5);
s.insert(2);
s.insert(7);
s.insert(5);
//set<int>::iterator it = s.begin();
auto it = s.begin();
while (it != s.end())
{
// error C3892: “it”: 不能给常量赋值
// *it = 1;
cout << *it << " ";
++it;
}
cout << endl;
// 插入一段initializer_list列表值,已经存在的值插入失败
s.insert({ 2,8,3,9 });
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
set<string> strset = { "sort", "insert", "add" };
// 遍历string比较ascll码大小顺序遍历的
for (auto& e : strset)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
返回值为pair键值对的我们下文再讲
2.6 find和erase
set<int> s = { 4,2,7,2,8,5,9 };
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 删除最小值
s.erase(s.begin());
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:
size_type erase (const value_type& val);
删除val,val不存在返回0,存在返回1
// 直接删除x
int x;
cin >> x;
int num = s.erase(x);
if (num == 0)
{
cout << x << "不存在!" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:
查找val,返回val所在的迭代器,没有找到返回end()
iterator find (const value_type& val);删除一个迭代器位置的值
iterator erase (const_iterator position);
// 直接查找在利用迭代器删除x
int x;
cin >> x;
auto pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);
}
else
{
cout << x << "不存在!" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:
// 算法库的查找 O(N)
int x;
auto pos1 = find(s.begin(), s.end(), x);
// set自身实现的查找 O(logN)
auto pos2 = s.find(x);
查找val,返回Val的个数
size_type count (const value_type& val) const;
// 利用count间接实现快速查找
int x;
cin >> x;
if (s.count(x))
{
cout << x << "在!" << endl;
}
else
{
cout << x << "不存在!" << endl;
}
运行结果:
2.7 lower_bound和upper_bound
set<int> myset;
for (int i = 1; i < 10; i++)
myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
// 实现查找到的[itlow,itup)包含[30, 60]区间
// 返回 >= 30
auto itlow = myset.lower_bound(30);
// 返回 > 60
auto itup = myset.upper_bound(60);
// 删除这段区间的值
myset.erase(itlow, itup);
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
运行结果:
2.8 multiset和set的差异
multiset和set的使用基本完全类似,主要区别点在于multiset支持值冗余,那么 insert/find/count/erase都围绕着支持值冗余有所差异。
特性 | std::set | std::multiset |
---|---|---|
元素唯一性 | 元素值必须唯一 | 允许重复元素 |
插入行为 | 插入重复元素会被忽略 | 允许插入重复元素 |
erase(val) 返回值 | 返回删除的元素数量(0 或 1) | 返回删除的元素数量(可能 >1) |
count(val) 返回值 | 0 或 1 | 返回元素出现的次数 |
容器 | 适用场景 |
---|---|
std::set | 需要唯一且有序的元素(如用户 ID 集合、关键字去重)。 |
multiset | 允许重复的有序数据(如记录成绩分布、统计词频、日志时间戳允许重复的场景)。 |
// 相比set不同的是,multiset是排序,但是不去重
multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 相比set不同的是,x可能会存在多个,find查找中序的第一个
int x;
cin >> x;
auto pos = s.find(x);
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
++pos;
}
cout << endl;
// 相比set不同的是,count会返回x的实际个数
cout << s.count(x) << endl;
// 相比set不同的是,erase给值时会删除所有的x
s.erase(x);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:
3. map系列的使用
3.1 map和multimap参考文档
https://legacy.cplusplus.com/reference/map/
3.2 map类的介绍
map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key支持小于比较,如果不支持或者需要的话可以自行实现仿函数传给第二个模版参数,map底层存储数据的内存是从空间配置器申请的。一般情况下,我们都不需要传后两个模版参数。map底层是用红黑树实现,增删查改效率是 O(logN) ,迭代器遍历是走的中序,所以是按key有序顺序遍历的。
一. 核心特性
-
键唯一性
每个键(key)唯一,重复插入会覆盖原有值(若允许重复键,需使用multimap
)。 -
自动排序
元素默认按 键升序 排列(基于std::less<Key>
),可自定义排序规则。 -
高效操作
插入、删除、查找操作的时间复杂度为 O(logn)O(logn),得益于红黑树的平衡性。 -
键不可修改
键值在插入后不可直接修改(需删除后重新插入),但关联的值(value)可修改。
二. 头文件与模板声明
#include <map>
template < class Key, // map::key_type
class T, // map::mapped_type
class Compare = less<Key>, // map::key_compare
class Alloc = allocator<pair<const Key, T> > //map::allocator_type
> class map;
模板参数:
-
Key
:键类型(需支持<
运算符或自定义比较规则)。 -
T
:值类型(任意类型)。 -
Compare
:比较器类型(默认std::less<Key>
)。 -
Alloc
:内存分配器(通常无需修改)。
3.3 pair类型介绍
map底层的红黑树节点中的数据,使用pair<Key, T>存储键值对数据。
I. 基本定义
value_type是什么呢?在接口中,我们经常会看到类如value_type这样的类型,这些类型都是经过typedef来的,我们来看看它到底是什么
typedef pair<const Key, T> value_type;
或者在文档中可以看到
我们再来看看它的底层代码
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)
{}
template<class U, class V>
pair(const pair<U, V>& pr) : first(pr.first), second(pr.second)
{}
};
template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y)
{
return (pair<T1, T2>(x, y));
}
其实就是我们提到的键值对
II. 创建与初始化
(1) 直接初始化
std::pair<int, std::string> p1(10, "hello"); // first=10, second="hello"
(2) 使用 make_pair
(自动推导类型)
auto p2 = std::make_pair(3.14, true); // pair<double, bool>
(3) 列表初始化(C++11+)
std::pair<int, float> p3 = {42, 9.8f};
III. 访问成员
直接通过 .first
和 .second
访问:
std::cout << p1.first << ", " << p1.second << std::endl; // 输出: 10, hello
IV. 常用操作
(1) 比较运算符
pair
支持 ==
, !=
, <
, >
等比较操作,规则为:
-
先比较
first
,若相等再比较second
。
std::pair<int, int> a(1, 2), b(1, 3);
if (a < b) { /* true,因为 a.first == b.first,但 a.second < b.second */ }
(2) 赋值与拷贝
std::pair<int, std::string> p4 = p1; // 拷贝构造
p4.second = "world"; // 修改 second
(3) 结构化绑定(C++17+)
auto [num, str] = p1; // num=10, str="hello"
3.4 构造函数与初始化
// 默认构造(空容器)
std::map<int, std::string> m1;
// 列表初始化
std::map<int, std::string> m2 = {
{1, "Alice"},
{2, "Bob"},
{3, "Charlie"}
};
// 迭代器范围构造
std::vector<std::pair<int, std::string>> vec = {{4, "David"}, {5, "Eve"}};
std::map<int, std::string> m3(vec.begin(), vec.end());
// 拷贝构造
std::map<int, std::string> m4(m3);
3.5 map的增删查
map的增删查关注以下几个接口即可:
map增接口,插入的pair键值对数据,跟set所有不同,但是查和删的接口只用关键字key跟set是完全类似的,不过find返回iterator,不仅仅可以确认key在不在,还找到key映射的value,同时通过迭代还可以修改value
// 单个数据插入,如果已经key存在则插入失败,key存在相等value不相等也会插入失败
pair<iterator,bool> insert (const value_type& val);
// 列表插入,已经在容器中存在的值不会插入
void insert (initializer_list<value_type> il);
// 迭代器区间插入,已经在容器中存在的值不会插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
// 查找k,返回k所在的迭代器,没有找到返回end()
iterator find (const key_type& k);
// 查找k,返回k的个数
size_type count (const key_type& k) const;
// 删除一个迭代器位置的值
iterator erase (const_iterator position);
// 删除k,k不存在返回0,存在返回1
size_type erase (const key_type& k);
// 删除一段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
// 返回大于k位置的迭代器
iterator lower_bound (const key_type& k);
// 返回大于k位置的迭代器
const_iterator lower_bound (const key_type& k) const;
这些接口的用法和set是一样的,只不过插入的数据是键值对,这里就不再举例介绍
3.6 map的数据修改
前面我提到map支持修改mapped_type 数据,不支持修改key数据,修改关键字数据,破坏了底层搜索树的结构。
map第一个支持修改的方式是通过迭代器,迭代器遍历时或者find返回key所在的iterator修改,map还有一个非常重要的修改接口operator[],但是operator[]不仅仅支持修改,还支持插入数据和查找数据,所以他是一个多功能复合接口
需要注意从内部实现角度,map这里把我们传统说的value值,给的是T类型,typedef为mapped_type。而value_type是红黑树结点中存储的pair键值对值。日常使用我们还是习惯将这里的T映射值叫做value。
那接下来我们就来介绍一下返回值为pair类型的插入和operator[]
insert
// 单个数据插入,如果已经key存在则插入失败,key存在相等value不相等也会插入失败
pair<iterator,bool> insert (const value_type& val);
来看看文档中是怎么介绍这个返回值的
insert插入一个pair<key, T>对象:
1、如果key已经在map中,插入失败,则返回一个pair<iterator,bool>对象,返回pair对象first是key所在结点的迭代器,second是false
2、如果key不在在map中,插入成功,则返回一个pair<iterator,bool>对象,返回pair对象first是新插入key所在结点的迭代器,second是true
也就是说无论插入成功还是失败,返回pair<iterator,bool>对象的first都会指向key所在的迭代器,那么也就意味着insert插入失败时充当了查找的功能,正是因为这一点,insert可以用来实operator[]需要注意的是这里有两个pair,不要混淆了,一个是map底层红黑树节点中存的pair<key, T>,另一个是insert返回值pair<iterator, bool>
代码示例:
// 利用find和iterator修改功能,统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (const auto& str : arr)
{
// 先查找水果在不在map中
// 1、不在,说明水果第一次出现,则插入{水果, 1}
// 2、在,则查找到的节点中水果对应的次数++
auto ret = countMap.find(str);
if (ret == countMap.end())
{
countMap.insert({ str, 1 });
}
else
{
ret->second++;
}
}
for (const auto& e : countMap)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
先查找水果在不在map中:
1、不在,说明水果第一次出现,则插入{水果, 1}
2、在,则查找到的节点中水果对应的次数++
运行结果:
operator[]
我们可以看一下operator[]的底层实现
// operator的内部实现
mapped_type& operator[] (const key_type& k)
{
// 1、如果k不在map中,insert会插入k和mapped_type默认值,同时[]返回结点中存储mapped_type值的引用,那么我们可以通过引用修改返回的映射值。所以[]具备了插入 + 修改功能
// 2、如果k在map中,insert会插入失败,但是insert返回pair对象的first是指向key结点的迭代器,返回值同时[]返回结点中存储mapped_type值的引用,所以[]具备了查找 + 修改的功能
pair<iterator, bool> ret = insert({ k, mapped_type() });
iterator it = ret.first;
return it->second;
}
1、如果k不在map中,insert会插入k和mapped_type默认值,同时[]返回结点中存储mapped_type值的引用,那么我们可以通过引用修改返回的映射值。所以[]具备了插入 + 修改功能
2、如果k在map中,insert会插入失败,但是insert返回pair对象的first是指向key结点的迭代器,返回值同时[]返回结点中存储mapped_type值的引用,所以[]具备了查找 + 修改的功能
代码示例1:
// 利用[]插⼊+修改功能,巧妙实现统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (const auto& str : arr)
{
// []先查找水果在不在map中
// 1、不在,说明水果第一次出现,则插入{水果, 0},同时返回次数的引用,++一下就变成1次了
// 2、在,则返回水果对应的次数++
countMap[str]++;
}
for (const auto& e : countMap)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
[]先查找水果在不在map中
1、不在,说明水果第一次出现,则插入{水果, 0},同时返回次数的引用,++一下就变成1次了
2、在,则返回水果对应的次数++
运行结果:
代码示例2:
map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
// key不存在->插入 {"insert", string()}
dict["insert"];
// 插入+修改
dict["left"] = "左边";
// 修改
dict["left"] = "左边、剩余";
// key存在->查找
cout << dict["sort"] << endl;
cout << dict["insert"] << endl;
cout << dict["left"] << endl;
使用[]访问键"insert"时,key不存在,插入{"insert", string()},注意key对应的值value是值value类型的构造
运行结果:
3.7 multimap和map的差异
multimap和map的使用基本完全类似,主要区别点在于multimap支持关键值key冗余,那么 insert/find/count/erase都围绕着支持关键值key冗余有所差异,这里跟set和multiset完全一样,比如find时,有多个key,返回中序第一个。其次就是multimap不支持[],因为支持key冗余,[]就只能支持插入了,不能支持修改。
一、核心差异
特性 | std::map | std::multimap |
---|---|---|
键唯一性 | 键必须唯一 | 允许重复键 |
插入重复键 | 覆盖原有值(若存在) | 允许插入重复键 |
operator[] | 支持(通过键访问或插入值) | 不支持(键不唯一) |
erase(key) 返回值 | 返回删除的元素数量(0 或 1) | 返回删除的元素数量(可能 >1) |
count(key) 返回值 | 0 或 1 | 返回键的出现次数 |
注意这里map在插入重复键,只有一种情况才能覆盖,下文会介绍
二、接口差异与行为对比
1. 插入操作
-
map
插入重复键时,会覆盖原有值,返回pair<iterator, bool>
,其中bool
表示是否插入成功。std::map<int, std::string> m; auto [it1, success1] = m.insert({1, "Apple"}); // success1 = true auto [it2, success2] = m.insert({1, "Ant"}); // success2 = false,值仍为 "Apple" m[1] = "Ant"; // 直接覆盖值
注意这里只有第三中情况才能覆盖
-
multimap
允许重复键插入,返回指向新元素的迭代器。std::multimap<int, std::string> mm; auto it1 = mm.insert({1, "Apple"}); // 插入成功 auto it2 = mm.insert({1, "Ant"}); // 插入成功,允许重复键
2. 删除操作
-
map
按键删除返回0
或1
(表示是否删除了元素)。std::map<int, std::string> m = {{1, "A"}, {2, "B"}}; size_t count = m.erase(1); // count = 1
-
multimap
按键删除返回实际删除的元素数量。std::multimap<int, std::string> mm = {{1, "A"}, {1, "B"}}; size_t count = mm.erase(1); // count = 2
3. 查找与访问
-
map
使用operator[]
直接访问或修改值(键不存在时插入默认值)。std::map<int, std::string> m; m[1] = "Apple"; // 插入键 1 std::cout << m[2]; // 插入键 2(值为空字符串)
-
multimap
无operator[]
,需通过迭代器或equal_range
处理重复键。std::multimap<int, std::string> mm = {{1, "A"}, {1, "B"}}; auto range = mm.equal_range(1); // 获取所有键为 1 的键值对 for (auto it = range.first; it != range.second; ++it) { std::cout << it->second << " "; // 输出 "A B" }
三、应用场景
容器 | 典型场景 |
---|---|
std::map | 键值唯一映射(如字典、用户ID到信息的映射)。 |
std::multimap | 允许重复键的有序映射(如日志按时间戳排序、一人多个电话号码)。 |
四、代码示例对比
1. 插入重复键
// map:重复键插入失败,需用 operator[] 覆盖
std::map<int, std::string> m;
m.insert({1, "Apple"});
m.insert({1, "Ant"}); // 无效,值仍为 "Apple"
m[1] = "Ant"; // 直接覆盖
// multimap:允许重复键
std::multimap<int, std::string> mm;
mm.insert({1, "Apple"});
mm.insert({1, "Ant"}); // 存储两个键值对 {1:"Apple"}, {1:"Ant"}
2. 遍历所有元素
// map:键唯一,遍历时无重复
std::map<int, std::string> m = {{2, "B"}, {1, "A"}};
for (const auto& [k, v] : m) {
std::cout << k << ":" << v << " "; // 输出 "1:A 2:B"(自动排序)
}
// multimap:允许重复键,遍历时可能连续出现相同键
std::multimap<int, std::string> mm = {{1, "A"}, {1, "B"}, {2, "C"}};
for (const auto& [k, v] : mm) {
std::cout << k << ":" << v << " "; // 输出 "1:A 1:B 2:C"
}
3. 删除操作
// map:返回 0 或 1
std::map<int, std::string> m = {{1, "A"}, {2, "B"}};
size_t count1 = m.erase(1); // count1 = 1
size_t count2 = m.erase(3); // count2 = 0
// multimap:返回实际删除数量
std::multimap<int, std::string> mm = {{1, "A"}, {1, "B"}};
size_t count = mm.erase(1); // count = 2
五、总结
选择依据 | std::map | std::multimap |
---|---|---|
键是否唯一 | 是 | 否 |
插入重复键 | 覆盖原有值 | 保留所有副本 |
访问方式 | 支持 operator[] 快速访问 | 需通过 equal_range 处理重复键 |
适用场景 | 配置管理、唯一键映射 | 日志记录、多值关联数据 |
根据键的唯一性需求选择容器,若需高效处理唯一键映射,优先使用 map
;若允许重复键且需有序性,选择 multimap
。