C++ 中 set 与 map 的区别详解

一、先给结论:一句话区分 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。

七、常见误区与注意事项

  1. 不要用 m[key] 做只读查询:会意外插入默认值,导致 map 变大甚至逻辑错误。
  2. set 的元素不可修改 :迭代器是 const 的,想改值只能先删除再插入。
  3. 自定义类型作为 key 需要比较器 :要么重载 operator<,要么传入自定义 Compare;哈希容器则需要提供 std::hash 特化和 operator==。
  4. 比较器必须满足严格弱序:否则行为未定义。
  5. 允许重复时用 multiset/multimap :multimap 没有 operator[],因为一个 key 可能对应多个 value。
  6. 遍历时删除元素 :使用 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。

理解二者的元素类型差异,就能自然推导出接口差异和使用场景差异,这也是面试中回答这道题的最佳切入点。

相关推荐
我叫洋洋3 小时前
Allegro 画布“垃圾显示“清理指南:把板子调成干净工作台(实战总结)
c语言·c++·单片机·嵌入式硬件·eda·嘉立创
lisw053 小时前
算法与软件的本质区别
驱动开发·算法·数学建模
我不会插花弄玉3 小时前
5.Hash 【由浅入深-redis】
redis·算法·哈希算法
Nuanyt3 小时前
LeetCode Hot 100 · 双指针 3 题 Java
java·笔记·算法·leetcode·双指针
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 圈子互动
java·开发语言·交友
jimy13 小时前
构造函数表达式的值类别是:prvalue
开发语言·c++
小嘉丶学长3 小时前
Day06 · 数据类型与类型检测
开发语言·python·学习·1024程序员节
wuminyu3 小时前
JVM规范学习总结-Class文件结构
java·linux·c语言·jvm·c++
多弗朗皮卡丘4 小时前
C++特殊类设计
开发语言·c++·设计模式