1. 序列式容器与关联式容器
C++ STL 中的容器可以分为序列式容器和关联式容器。
常见的序列式容器包括:
string、vector、list、deque、array、forward_list
这些容器按照元素在容器中的位置保存和访问数据,数据之间通常是线性关系。
关联式容器则按照关键字保存和访问数据,常见的关联式容器包括:
set、multiset、map、multimap
set 和 map 的底层通常使用红黑树实现。红黑树是一棵近似平衡的二叉搜索树,因此它们的插入、删除和查找操作的时间复杂度通常为:
O(log N)
二者的主要使用场景如下:
-
set:保存关键字,适合搜索、排序和去重。 -
map:保存键值对,适合建立关键字和数据之间的映射关系。
2. set 系列容器
2.1 set 的特点
set 的基本声明如下:
template<
class T,
class Compare = less<T>,
class Alloc = allocator<T>
>
class set;
其中:
-
T是容器中元素的类型。 -
Compare是比较规则,默认使用less<T>。 -
Alloc是空间配置器,一般不需要手动设置。
set 具有以下特点:
-
元素不能重复。
-
元素会自动排序。
-
默认按照升序排列。
-
插入、删除和查找的时间复杂度通常为
O(log N)。 -
不能通过迭代器修改元素。
2.2 set 的构造
无参构造
set<int> values;
初始化列表构造
set<int> values = {5, 2, 8, 2, 1};
容器中的最终结果为:
1 2 5 8
set 会自动排序并删除重复元素。
迭代器区间构造
vector<int> numbers = {5, 2, 8, 2, 1};
set<int> values(
numbers.begin(),
numbers.end()
);
拷贝构造
set<int> first = {1, 2, 3};
set<int> second(first);
2.3 set 的迭代器
set 支持正向迭代器和反向迭代器:
values.begin();
values.end();
values.rbegin();
values.rend();
正向遍历:
for (auto iterator = values.begin();
iterator != values.end();
++iterator)
{
cout << *iterator << " ";
}
范围 for 遍历:
for (const auto& value : values)
{
cout << value << " ";
}
反向遍历:
for (auto iterator = values.rbegin();
iterator != values.rend();
++iterator)
{
cout << *iterator << " ";
}
由于 set 中的元素同时也是搜索关键字,修改元素可能会破坏底层搜索树的结构,因此不能通过迭代器修改元素。
set<int> values = {1, 2, 3};
auto iterator = values.begin();
// 不允许这样修改
// *iterator = 10;
如果需要修改,应先删除原来的元素,再插入新元素:
values.erase(1);
values.insert(10);
2.4 insert:插入元素
插入单个元素
set<int> values;
values.insert(5);
values.insert(2);
values.insert(7);
values.insert(5);
第二次插入 5 会失败,因为 set 不允许元素重复。
判断是否插入成功
auto result = values.insert(5);
if (result.second)
{
cout << "插入成功" << endl;
}
else
{
cout << "元素已经存在" << endl;
}
insert() 返回一个 pair<iterator, bool>:
-
first:指向对应元素的迭代器。 -
second:表示是否插入成功。
插入初始化列表
values.insert({2, 8, 3, 9});
已经存在的元素不会被重复插入。
插入一段迭代器区间
vector<int> numbers = {10, 20, 30};
values.insert(
numbers.begin(),
numbers.end()
);
2.5 find:查找元素
set<int> values = {2, 4, 5, 7, 8, 9};
int target = 7;
auto position = values.find(target);
if (position != values.end())
{
cout << "找到元素:" << *position << endl;
}
else
{
cout << "元素不存在" << endl;
}
如果找到元素,find() 返回对应位置的迭代器;如果没有找到,则返回 end()。
需要注意,算法库中的 find() 和 set 自身的 find() 效率不同:
// 时间复杂度为 O(N)
auto first = find(
values.begin(),
values.end(),
target
);
// 时间复杂度通常为 O(log N)
auto second = values.find(target);
使用 set 时,应优先调用容器自身的 find()。
2.6 count:统计元素数量
if (values.count(7))
{
cout << "7存在" << endl;
}
else
{
cout << "7不存在" << endl;
}
由于普通 set 不允许元素重复,因此 count() 的返回值只可能是:
-
0:元素不存在。 -
1:元素存在。
2.7 erase:删除元素
根据元素值删除
size_t count = values.erase(7);
if (count == 0)
{
cout << "元素不存在" << endl;
}
根据迭代器删除
auto position = values.find(8);
if (position != values.end())
{
values.erase(position);
}
删除最小元素
由于 set 默认按照升序排列,因此 begin() 指向最小元素:
if (!values.empty())
{
values.erase(values.begin());
}
删除迭代器区间
set<int> values = {
10, 20, 30, 40, 50,
60, 70, 80, 90
};
// 第一个大于或等于30的元素
auto first = values.lower_bound(30);
// 第一个严格大于60的元素
auto last = values.upper_bound(60);
// 删除[30, 60]
values.erase(first, last);
删除后剩余:
10 20 70 80 90
2.8 lower_bound 和 upper_bound
lower_bound
lower_bound(value) 返回第一个大于或等于 value 的元素。
set<int> values = {
10, 20, 30, 40, 50
};
auto position = values.lower_bound(25);
if (position != values.end())
{
cout << *position << endl;
}
输出:
30
upper_bound
upper_bound(value) 返回第一个严格大于 value 的元素。
auto position = values.upper_bound(30);
if (position != values.end())
{
cout << *position << endl;
}
输出:
40
2.9 set 设置降序排列
set 默认使用 less<T>,按照升序排列。
使用 greater<T> 可以设置为降序:
set<int, greater<int>> values = {
5, 2, 8, 1
};
for (int value : values)
{
cout << value << " ";
}
输出:
8 5 2 1
2.10 multiset 和 set 的区别
multiset 和 set 的使用方式基本相同,主要区别是 multiset 允许保存重复元素。
multiset<int> values = {
4, 2, 7, 2, 4,
8, 4, 5, 4, 9
};
遍历结果:
2 2 4 4 4 4 5 7 8 9
统计某个元素出现的次数:
cout << values.count(4) << endl;
输出:
4
删除指定值时,会删除所有相等的元素:
values.erase(4);
查找某个值对应的完整范围,可以使用:
auto range = values.equal_range(2);
for (auto iterator = range.first;
iterator != range.second;
++iterator)
{
cout << *iterator << " ";
}
set 与 multiset 的区别可以总结为:
| 容器 | 是否排序 | 是否允许重复 |
|---|---|---|
set |
是 | 否 |
multiset |
是 | 是 |
3. map 系列容器
3.1 map 的特点
map 用来保存键值对,其基本声明如下:
template<
class Key,
class T,
class Compare = less<Key>,
class Alloc = allocator<pair<const Key, T>>
>
class map;
其中:
-
Key是键的类型。 -
T是键所映射的数据类型。 -
Compare是键的比较规则。 -
Alloc是空间配置器。
map 具有以下特点:
-
每个键都是唯一的。
-
数据按照键自动排序。
-
键不能直接修改。
-
键对应的值可以修改。
-
插入、删除和查找的时间复杂度通常为
O(log N)。
3.2 pair 键值对
map 使用 pair 保存键值对。
pair<string, string> data(
"sort",
"排序"
);
pair 中包含两个成员:
data.first; // 第一个数据
data.second; // 第二个数据
对于 map 而言:
-
first表示键。 -
second表示键对应的值。
也可以使用 make_pair() 创建键值对:
auto data = make_pair(
"insert",
"插入"
);
map 中实际保存的元素类型可以表示为:
pair<const Key, T>
键使用 const 修饰,因此不能直接修改键。
3.3 map 的构造
无参构造
map<string, string> dictionary;
初始化列表构造
map<string, string> dictionary = {
{"left", "左边"},
{"right", "右边"},
{"insert", "插入"},
{"string", "字符串"}
};
迭代器区间构造
map<string, int> first = {
{"apple", 3},
{"banana", 2}
};
map<string, int> second(
first.begin(),
first.end()
);
拷贝构造
map<string, int> third(first);
3.4 map 的遍历
使用普通迭代器遍历:
for (auto iterator = dictionary.begin();
iterator != dictionary.end();
++iterator)
{
cout << iterator->first
<< ":"
<< iterator->second
<< endl;
}
使用范围 for 遍历:
for (const auto& data : dictionary)
{
cout << data.first
<< ":"
<< data.second
<< endl;
}
map 默认按照键的升序遍历,而不是按照值排序。
map 同样支持反向迭代器:
dictionary.rbegin();
dictionary.rend();
3.5 insert:插入键值对
方式一:先创建 pair 对象
pair<string, string> data(
"first",
"第一个"
);
dictionary.insert(data);
方式二:构造临时 pair
dictionary.insert(
pair<string, string>(
"second",
"第二个"
)
);
方式三:使用 make_pair
dictionary.insert(
make_pair("sort", "排序")
);
方式四:使用初始化列表
dictionary.insert({
"auto",
"自动的"
});
初始化列表的写法通常更加简洁。
判断插入是否成功
auto result = dictionary.insert({
"left",
"左边"
});
if (result.second)
{
cout << "插入成功" << endl;
}
else
{
cout << "键已经存在" << endl;
}
map 判断数据是否重复时,只比较键。
即使键对应的值不同,只要键已经存在,插入仍然会失败。
dictionary.insert({
"left",
"左边"
});
dictionary.insert({
"left",
"剩余"
});
第二次插入不会覆盖第一次保存的值。
3.6 find:根据键查找数据
string word;
cin >> word;
auto position = dictionary.find(word);
if (position != dictionary.end())
{
cout << position->second << endl;
}
else
{
cout << "没有找到对应单词" << endl;
}
如果查找成功,迭代器可以同时访问键和值:
position->first;
position->second;
3.7 使用迭代器修改值
map 不允许修改键,但是允许修改键所映射的值:
auto position = dictionary.find("left");
if (position != dictionary.end())
{
position->second = "左边、剩余";
}
下面的写法不允许:
// position->first = "right";
因为修改键会破坏底层搜索树的结构。
3.8 count:判断键是否存在
if (dictionary.count("left"))
{
cout << "left存在" << endl;
}
else
{
cout << "left不存在" << endl;
}
普通 map 中的键不能重复,所以 count() 的返回值只可能是 0 或 1。
3.9 erase:删除键值对
根据键删除
size_t count = dictionary.erase("left");
if (count == 0)
{
cout << "键不存在" << endl;
}
根据迭代器删除
auto position = dictionary.find("right");
if (position != dictionary.end())
{
dictionary.erase(position);
}
删除一段范围
map<int, string> values = {
{10, "A"},
{20, "B"},
{30, "C"},
{40, "D"},
{50, "E"}
};
auto first = values.lower_bound(20);
auto last = values.upper_bound(40);
values.erase(first, last);
这段代码会删除键位于 [20, 40] 范围内的键值对。
3.10 map 的 lower_bound 和 upper_bound
map 的范围查找比较的是键。
map<int, string> values = {
{10, "A"},
{20, "B"},
{30, "C"},
{40, "D"}
};
查找第一个大于或等于 25 的键:
auto position = values.lower_bound(25);
if (position != values.end())
{
cout << position->first << endl;
}
输出:
30
查找第一个严格大于 30 的键:
auto position = values.upper_bound(30);
if (position != values.end())
{
cout << position->first << endl;
}
输出:
40
3.11 operator\[\] 接口
operator[] 是 map 中非常重要的接口,它同时具有插入、查找和修改功能。
基本形式:
mapped_type& operator[](
const key_type& key
);
键不存在时
如果键不存在,operator[] 会插入一个新的键值对,并对值进行默认初始化。
map<string, string> dictionary;
dictionary["insert"];
插入的键值对相当于:
{"insert", string()}
插入并修改
dictionary["left"] = "左边";
当 "left" 不存在时,会先插入,再修改对应的值。
修改已有数据
dictionary["left"] = "左边、剩余";
当 "left" 已经存在时,会直接修改对应的值。
查找并访问
cout << dictionary["left"] << endl;
当键存在时,operator[] 会返回该键所对应值的引用。
因此,operator[] 可以总结为:
|---------|---------------|
| 键的状态 | 实际功能 |
| 键不存在 | 插入默认值,并返回值的引用 |
| 键不存在并赋值 | 插入并修改 |
| 键已经存在 | 查找并修改或访问 |
如果只想判断某个键是否存在,应使用 find() 或 count(),防止插入不需要的数据。
3.12 使用 map 统计次数
使用 find 和迭代器统计
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
string fruits[] = {
"苹果", "西瓜", "苹果",
"西瓜", "苹果", "苹果",
"西瓜", "苹果", "香蕉",
"苹果", "香蕉"
};
map<string, int> counts;
for (const auto& fruit : fruits)
{
auto position = counts.find(fruit);
if (position == counts.end())
{
counts.insert({fruit, 1});
}
else
{
++position->second;
}
}
for (const auto& data : counts)
{
cout << data.first
<< ":"
<< data.second
<< endl;
}
return 0;
}
使用 operator\[\] 统计
利用 operator[] 可以简化代码:
map<string, int> counts;
for (const auto& fruit : fruits)
{
++counts[fruit];
}
当水果第一次出现时,会先插入:
{fruit, 0}
然后执行自增,数量变成 1。再次出现时,直接将原来的数量加一。
3.13 multimap 和 map 的区别
multimap 和 map 的使用方式基本相同,主要区别是 multimap 允许键重复。
multimap<string, int> scores;
scores.insert({"Tom", 90});
scores.insert({"Tom", 95});
scores.insert({"Jack", 80});
统计某个键的数量:
cout << scores.count("Tom") << endl;
输出:
2
获取一个键对应的所有数据:
auto range = scores.equal_range("Tom");
for (auto iterator = range.first;
iterator != range.second;
++iterator)
{
cout << iterator->first
<< ":"
<< iterator->second
<< endl;
}
删除指定键时,会删除该键对应的所有键值对:
scores.erase("Tom");
multimap 不支持 operator[]。因为一个键可能对应多个值,无法通过一个键确定应该访问或修改哪一个值。
|------------|---------|-------------------|
| 容器 | 键是否允许重复 | 是否支持 operator[] |
| map | 否 | 是 |
| multimap | 是 | 否 |
4. set 和 map 的典型应用
4.1 set 的常见应用
set 适合解决:
-
数据去重。
-
数据自动排序。
-
快速判断元素是否出现。
-
求两个数组的交集。
-
记录链表节点,判断链表是否存在环。
-
查找某个范围内的数据。
例如,将数组放入 set,可以同时完成去重和排序:
vector<int> numbers = {
4, 2, 7, 2, 4, 8
};
set<int> values(
numbers.begin(),
numbers.end()
);
4.2 map 的常见应用
map 适合解决:
-
统计元素出现次数。
-
建立字典映射关系。
-
建立原对象和新对象之间的映射。
-
根据关键字快速查找数据。
-
统计高频单词。
-
保存编号和对象之间的对应关系。
例如:
map<string, int> counts;
counts["apple"]++;
counts["banana"]++;
counts["apple"]++;
最终结果为:
apple: 2
banana: 1
5. 常用接口总结
set 常用接口
|-----------------|-----------------|
| 接口 | 作用 |
| insert() | 插入元素 |
| find() | 查找元素 |
| count() | 统计元素数量 |
| erase() | 删除元素 |
| lower_bound() | 查找第一个大于或等于目标的元素 |
| upper_bound() | 查找第一个大于目标的元素 |
| begin() | 获取第一个元素的迭代器 |
| end() | 获取尾后迭代器 |
| rbegin() | 获取反向遍历的起始迭代器 |
| rend() | 获取反向遍历的尾后迭代器 |
| size() | 获取元素数量 |
| empty() | 判断容器是否为空 |
| clear() | 清空容器 |
map 常用接口
|-----------------|----------------|
| 接口 | 作用 |
| insert() | 插入键值对 |
| find() | 根据键查找数据 |
| count() | 统计键的数量 |
| erase() | 删除键值对 |
| lower_bound() | 查找第一个键不小于目标的位置 |
| upper_bound() | 查找第一个键大于目标的位置 |
| operator[] | 插入、访问或修改值 |
| begin() | 获取第一个键值对的迭代器 |
| end() | 获取尾后迭代器 |
| rbegin() | 获取反向遍历的起始迭代器 |
| rend() | 获取反向遍历的尾后迭代器 |
| size() | 获取键值对数量 |
| empty() | 判断容器是否为空 |
| clear() | 清空容器 |
6. 总结
set 和 map 都是基于关键字组织数据的关联式容器,底层通常使用红黑树实现。
set 的核心特点是:
-
元素不能重复。
-
自动排序。
-
适合搜索和去重。
-
不能直接修改元素。
map 的核心特点是:
-
保存键值对。
-
键不能重复。
-
按照键自动排序。
-
键不能修改,但对应的值可以修改。
-
operator[]同时具有插入、访问和修改功能。
set 和 map 的插入、删除和查找效率通常为 O(log N)。如果需要保存重复元素,可以分别使用 multiset 和 multimap。