一、先给结论:一句话区分 set 和 map
set 存的是"值本身",map 存的是"键值对" 。两者都是 C++ 标准库中的关联容器,底层通常都由红黑树实现,元素按 key 有序排列,查找、插入、删除的平均复杂度都是 O(log n)。区别在于:std::set 只关心元素是否存在,std::map 关心的是"通过一个键找到对应的值"。
记忆口诀:set 是集合,map 是字典。集合里只有元素,字典里是"词条 → 释义"。
二、本质区别:元素类型不同
从模板声明就能看出最根本的差异:
cpp
template<class Key, class Compare = std::less<Key>,
class Allocator = std::allocator<Key>>
class set;
template<class Key, class T, class Compare = std::less<Key>,
class Allocator = std::pair<const Key, T>>
class map;
set<Key>的元素类型就是Key,一个元素只占一份数据。map<Key, T>的元素类型是std::pair<const Key, T>,每个元素由 key 和 value 两部分组成,其中 key 是const,不可修改。
因此 set 的迭代器解引用得到的是 const Key&,而 map 的迭代器解引用得到的是 pair<const Key, T>&,你可以修改 second,但不能修改 first。
三、核心差异对比表
| 对比维度 | std::set | std::map |
|---|---|---|
| 存储内容 | 单一元素(key) | 键值对(key → value) |
| 元素类型 | Key |
std::pair<const Key, T> |
| 是否允许重复 | 不允许(multiset 允许) |
key 不允许重复(multimap 允许) |
| 是否有序 | 有序,按 key 升序(默认 less) |
有序,按 key 升序(默认 less) |
| 底层结构 | 红黑树 | 红黑树 |
| 查找接口 | find / count / contains |
find / count / contains / operator[] / at |
| 下标访问 | 不支持 | 支持 m[key],但会插入默认值 |
| 典型用途 | 去重、判重、有序集合运算 | 映射查找、计数、缓存、字典 |
四、接口差异:map 独有的 operator\[\] 与 at
这是实际编码中最容易踩坑的地方。map 提供了下标运算符,而 set 没有:
cpp
#include <iostream>
#include <map>
#include <set>
#include <string>
int main() {
std::map<std::string, int> score;
score["Alice"] = 90; // 插入新键
score["Bob"] = 85;
std::cout << score["Alice"] << "\n"; // 90
std::cout << score["Tom"] << "\n"; // 0,注意:Tom 被插入了!
// 只想查询、不想插入,用 find 或 at
auto it = score.find("Jerry");
if (it == score.end()) {
std::cout << "Jerry not found\n";
}
try {
std::cout << score.at("Jerry") << "\n"; // 抛 std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "at() 抛出异常: " << e.what() << "\n";
}
std::set<std::string> names{"Alice", "Bob"};
// names["Alice"]; // 编译错误:set 没有 operator[]
if (names.count("Alice")) {
std::cout << "Alice 在集合中\n";
}
return 0;
}
关键提醒 :m[key] 在 key 不存在时会默认构造 value 并插入 ,因此它不能用于 const map,也不适合"只查不改"的场景。查询请优先用 find 或 at。
五、典型使用场景
5.1 set 的典型场景
- 去重 :把一批数据插入
set,自动去重且保持有序。 - 判重 :
if (s.count(x))或 C++20 的s.contains(x)。 - 集合运算 :配合
std::set_union、std::set_intersection、std::set_difference求并、交、差。 - 维护有序序列 :需要频繁插入删除并保持有序时,比
vector+sort更合适。
cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <set>
int main() {
std::set<int> a{1, 2, 3, 4, 5};
std::set<int> b{4, 5, 6, 7};
std::set<int> inter;
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
std::inserter(inter, inter.begin()));
for (int x : inter) std::cout << x << " "; // 4 5
std::cout << "\n";
return 0;
}
5.2 map 的典型场景
- 计数统计:统计单词出现次数、字符频率等。
- 映射查找:ID → 对象、名字 → 属性、配置项 → 值。
- 稀疏数据:key 分布稀疏、需要按 key 有序遍历时。
- 区间查询 :利用
lower_bound/upper_bound做范围检索。
cpp
#include <iostream>
#include <map>
#include <string>
int main() {
std::string text = "hello world hello cpp";
std::map<std::string, int> freq;
std::string word;
for (char c : text) {
if (c == ' ') {
if (!word.empty()) { ++freq[word]; word.clear(); }
} else {
word += c;
}
}
if (!word.empty()) ++freq[word];
for (const auto& [w, cnt] : freq) {
std::cout << w << ": " << cnt << "\n";
}
return 0;
}
六、性能与底层实现
两者底层都是红黑树(一种自平衡二叉搜索树),因此:
- 插入、删除、查找均为
O(log n)。 - 中序遍历天然有序,迭代器按 key 升序输出。
- 节点分散在堆上,缓存局部性较差,常数因子比
vector大。 - 插入删除不会使其他元素的迭代器失效(被删除元素本身除外)。
如果不需要有序性,只追求平均 O(1) 查找,应优先考虑哈希容器:
| 有序容器 | 无序容器 | 查找复杂度 |
|---|---|---|
std::set |
std::unordered_set |
O(log n) vs 平均 O(1) |
std::map |
std::unordered_map |
O(log n) vs 平均 O(1) |
选择原则:需要有序遍历或范围查询 → 用 set/map;只需要快速查找 → 用 unordered_set/unordered_map。
七、常见误区与注意事项
- 不要用
m[key]做只读查询:会意外插入默认值,导致 map 变大甚至逻辑错误。 - set 的元素不可修改 :迭代器是
const的,想改值只能先删除再插入。 - 自定义类型作为 key 需要比较器 :要么重载
operator<,要么传入自定义Compare;哈希容器则需要提供std::hash特化和operator==。 - 比较器必须满足严格弱序:否则行为未定义。
- 允许重复时用 multiset/multimap :
multimap没有operator[],因为一个 key 可能对应多个 value。 - 遍历时删除元素 :使用
it = container.erase(it),不要写erase(it++)之外的错误形式。
cpp
#include <iostream>
#include <map>
int main() {
std::map<int, int> m{{1, 10}, {2, 20}, {3, 30}, {4, 40}};
for (auto it = m.begin(); it != m.end(); ) {
if (it->second > 20) {
it = m.erase(it); // 正确:用返回值更新迭代器
} else {
++it;
}
}
for (const auto& p : m) std::cout << p.first << ":" << p.second << " ";
std::cout << "\n"; // 1:10 2:20
return 0;
}
八、总结
set 和 map 同源同构,都是基于红黑树的有序关联容器,复杂度一致,差别集中在**"存什么"** 和**"怎么用"**上:
- 只需要判断元素是否存在、去重、做集合运算 → 选
set。 - 需要通过键取值、计数、建立映射关系 → 选
map。 - 不需要有序性且追求极致查找性能 → 选
unordered_set/unordered_map。
理解二者的元素类型差异,就能自然推导出接口差异和使用场景差异,这也是面试中回答这道题的最佳切入点。