C++效率掌握之STL库:map && set底层剖析及迭代器

C++ 效率掌握之 STL 库:map && set 底层剖析及迭代器详解

std::mapstd::set 是 C++ STL 中最常用的关联式有序容器,掌握它们的底层实现和迭代器特性,能让你在性能敏感场景(如查找、去重、区间查询、缓存等)做出正确选择。

1. map vs set 一目了然

特性std::set<T>std::map<Key, Value>std::multiset / std::multimap
存储内容仅 key(元素本身)key-value 对允许 key 重复
元素唯一性是(自动去重)key 唯一允许重复
访问方式只能通过 key 本身通过 key 访问 value同左
底层结构红黑树红黑树(节点存 pair)红黑树
迭代器顺序按 key 升序(中序遍历)按 key 升序同左
典型场景去重、排序、集合运算字典、缓存、配置表允许重复的统计、多值映射

核心区别set 的元素既是 key 也是 value;map 的 key 是 const,不能修改(修改 key 会破坏树结构)。

2. 底层实现:红黑树(Red-Black Tree)

在 GCC(libstdc++)、Clang(libc++)等主流实现中,mapset 都基于 _Rb_tree(红黑树)实现。

为什么是红黑树而不是普通二叉搜索树(BST)?

  • 普通 BST 最坏情况(有序插入)会退化成链表 → O(N) 操作。
  • 红黑树是近似平衡的 BST,保证任意操作 O(log N)

红黑树 5 大性质(必须记住)

  1. 每个节点是红色或黑色。
  2. 根节点是黑色
  3. 所有叶子(NIL 节点)是黑色。
  4. 红色节点的子节点必须是黑色(无连续红节点)。
  5. 从任一节点到其每个叶子的所有路径都包含相同数量的黑色节点(黑高相等)。

这些性质保证树的高度差不超过 2 倍,最坏高度 ≈ 2×log₂(N)。

节点结构(简化版,实际在 STL 源码中)

struct _Rb_tree_node {
    bool color;                    // red = 0, black = 1
    _Rb_tree_node* parent;
    _Rb_tree_node* left;
    _Rb_tree_node* right;
    // value_type(set 是 T,map 是 pair<const Key, T>)
    value_type value;
};

操作复杂度(最坏情况)

  • 插入(insert / emplace):O(log N)
  • 删除(erase):O(log N)
  • 查找(find / count / lower_bound):O(log N)
  • 遍历(begin → end):O(N)

3. 迭代器详解(最容易被问到的点)

mapset 的迭代器是 Bidirectional Iterator(双向迭代器)

特性

  • 支持 ++it--it(正向、反向遍历)。
  • 不支持随机访问(不能 it + 5)。
  • 遍历顺序 = 中序遍历 → 天然有序。
  • *it 返回 const value_type&(set 返回 const T&,map 返回 const pair&)。

迭代器失效规则(核心区别于 vector)

操作是否使迭代器失效说明
insert / emplace完全不失效(所有已有迭代器仍有效)红黑树插入不移动现有节点
erase(it)仅当前 it 失效,其他迭代器全部有效最友好!
clear()所有迭代器失效显然
容器销毁所有迭代器失效显然

对比记忆

  • vector:插入/删除可能导致所有后续迭代器失效(甚至扩容全失效)。
  • list:删除仅当前失效。
  • map/set:删除仅当前失效,插入零失效 → 最安全的关联容器迭代器

推荐写法(避免失效问题):

for (auto it = s.begin(); it != s.end(); ) {
    if (需要删除) {
        it = s.erase(it);   // erase 返回下一个有效迭代器
    } else {
        ++it;
    }
}

4. 性能与使用对比:map/set vs unordered_map/unordered_set

场景推荐容器原因
需要有序 + 区间查询map / setlower_boundupper_bound 神器
纯查重 / 缓存unordered_*平均 O(1)
key 是字符串先试 unordered,再考虑 map哈希冲突风险
需要频繁遍历有序结果map / set遍历本身就有序

5. 实战代码示例

#include <iostream>
#include <map>
#include <set>
#include <string>

int main() {
    std::map<std::string, int> scores;
    scores["Alice"] = 95;           // operator[]:不存在则插入默认值
    scores.insert({"Bob", 88});
    scores.emplace("Charlie", 92);

    // 查找
    auto it = scores.find("Bob");
    if (it != scores.end()) {
        std::cout << it->first << ": " << it->second << '\n';
    }

    // 区间查询:所有分数 >= 90 的人
    auto low = scores.lower_bound("A");   // >= "A"
    auto up  = scores.upper_bound("C");   // > "C"
    for (auto i = low; i != up; ++i) {
        std::cout << i->first << '\n';
    }

    std::set<int> s{3, 1, 4, 1, 5};   // 自动去重排序 → 1,3,4,5
    for (int x : s) std::cout << x << ' ';
}

常见陷阱

  • map[key]插入默认构造的 value(即使你只是想读)。
  • 正确做法:用 findcount 先判断。
  • map 的 key 不能修改(const)。

6. 效率掌握口诀

  • 要有序、要区间 → map/set(红黑树)
  • 纯极致查重 → unordered(哈希)
  • 插入删除频繁 → 放心用 map/set(迭代器极稳)
  • 遍历必须有序 → 用 map/set + 范围 for
  • 自定义排序 → 传比较器set<T, std::greater<T>> 或 lambda

掌握了红黑树 + 迭代器失效规则,你就真正把 mapset 从“会用”提升到了“懂为什么高效”。

想继续深入哪一块?

  • 红黑树插入/删除旋转细节 + 手画图
  • multimap / multiset 实际应用
  • 与 unordered 的哈希冲突处理
  • 自定义比较器 + 结构体作为 key
  • 还是直接来练习题?

随时说,我们继续!

文章已创建 4549

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部