map / set 完全指南:红黑树与有序容器

std::map 是 C++ 里最常被用、也最容易被用错的容器。它的核心事实只有一句话:它是一棵自平衡的红黑树(red-black tree),所以 key 永远有序、查找永远是 O(log n) 。有序带来范围查询、中序遍历即排序、find 优于 std::find 这些能力;但也带来一个著名陷阱------operator[] 在找不到 key 时会默默插入一个默认值。这篇按「结构 → 只读查表 → 查找写法 → 插入写法 → 范围查询 → 完整示例」的顺序把 map / set 全家讲清楚。

1. 引子:查一个不存在的 key,容器却变大了

先看一段真实代码的实测结果:

text 复制代码
size before = 1
inventory["banana"] = 0 but size becomes 2

只是「读一下」banana 的数量,容器的 size 就从 1 变成了 2。std::map::operator[] 的语义不是「查」,而是「取引用,不存在就插入一个值初始化的元素 」。一个常见的 bug 是拿它当判断:if (m[key] == 0) { ... }------每次判断都在往 map 里塞垃圾。要理解为什么会这样,得先知道 map 底下是什么。

2. 红黑树:有序和 O(log n) 是同一个来源

std::map<Key, T> 不是哈希表,也不是普通二叉树,而是红黑树------一种每个节点多带一位颜色的自平衡二叉搜索树(binary search tree)。

text 复制代码
红黑树(red-black tree):一棵自平衡的二叉搜索树,每个节点多一位颜色

                     ┌───────────────┐
                     │  黑 50         │        根永远是黑
                     └──┬─────────┬──┘
                 ┌──────┘         └──────┐
          ┌──────┴──────┐          ┌──────┴──────┐
          │  红 30      │          │  黑 70      │
          └──┬───────┬──┘          └──┬───────┬──┘
        ┌────┘       └────┐     ┌─────┘       └─────┐
   ┌────┴───┐       ┌─────┴──┐  │ ...              │ ...
   │ 黑 20  │       │ 黑 40  │
   └────────┘       └────────┘

五条约束(教科书版本):
  ① 节点非红即黑              ② 根是黑
  ③ 叶子(NIL 空节点)都是黑   ④ 红节点的孩子必须是黑(不能连续两个红)
  ⑤ 从任一节点到它所有后代 NIL 的路径上,黑节点个数相同(黑高相等)

由 ④⑤ 推出:最长路径 <= 2 x 最短路径  =>  树高 O(log n)。
所以查找、插入、删除都是 O(log n),而且是「保证的」上界,
不是平均情况 ------ 这一点和下面要讲的 unordered_map 正好相反。

插入/删除破坏了 ①②④⑤ 时,用「旋转(rotation)+ 重新着色(recolor)」
修回来,每次只需常数次调整,代价已经算在 O(log n) 里。

中序遍历这棵树取出的是:20 30 40 50 70 ...
------ 天然有序。这就是 map/set 能提供 lower_bound/upper_bound 的原因。

注意最后一行:「有序」不是 map 额外维护的一个属性,而是树结构的副产品 。你插入 30、50、20、70、40,中序遍历永远是 20 30 40 50 70,和插入顺序无关。红黑树的具体实现和旋转细节不必背,记住两条就够用:key 有序 、树高被保证在 O(log n)。

map 家族有四个成员,区别只在「允不允许重复」:

容器 重复元素 有序性 查找 典型用途
std::map<K, V> 不允许重复 key 按 key 升序(可用比较器改) O(log n) 键值映射、需要有序遍历或范围查询
std::set<K> 不允许重复 升序 O(log n) 去重 + 有序遍历
std::multimap<K, V> 允许重复 key 有序 O(log n) 一对多(分数→姓名、索引倒排)
std::multiset<K> 允许重复 有序 O(log n) 有序袋、滑动窗口中位数
cpp 复制代码
// map_basic.cpp --- 编译: g++ -std=c++17 -Wall -O2 map_basic.cpp -o map_basic
#include <iostream>
#include <map>
#include <set>
#include <string>

int main() {
    std::map<std::string, int> ages;
    ages["alice"] = 30;
    ages["bob"] = 25;
    ages["carol"] = 35;

    std::cout << "map iteration (sorted by key):";
    for (const auto& [name, age] : ages) std::cout << ' ' << name << '=' << age;
    std::cout << '\n';

    // insert 返回 pair<iterator, bool>:bool 说明到底插进去了没有
    const auto [it1, inserted1] = ages.insert({"dave", 40});
    const auto [it2, inserted2] = ages.insert({"bob", 99});
    std::cout << "insert dave: inserted=" << inserted1 << " value=" << it1->second << '\n';
    std::cout << "insert bob : inserted=" << inserted2 << " value=" << it2->second << '\n';

    std::set<int> uniq{3, 1, 4, 1, 5, 9, 2, 6};
    std::cout << "set (dedup + sorted):";
    for (int v : uniq) std::cout << ' ' << v;
    std::cout << "\nset size = " << uniq.size() << '\n';
    std::cout << "count(4) = " << uniq.count(4) << " count(7) = " << uniq.count(7) << '\n';
    return 0;
}
text 复制代码
map iteration (sorted by key): alice=30 bob=25 carol=35
insert dave: inserted=1 value=40
insert bob : inserted=0 value=25
set (dedup + sorted): 1 2 3 4 5 6 9
set size = 7
count(4) = 1 count(7) = 0

三处细节都值得记住:遍历输出是 alice bob carol 而不是插入顺序(红黑树的中序即字典序);insert("bob", 99) 返回 inserted=0, value=25------没有覆盖 ,原来的 25 还在,这是 insert 与 operator[] 最本质的区别;set 把重复出现的 1 去掉了并排好序,count 就是「在不在」的判据。

官方文档:std::map --- cppreference

官方文档:std::set --- cppreference

官方文档:std::multimap --- cppreference

3. 陷阱一:operator\[\] 找不到就插入

operator[] 的完整语义是「返回 key 对应 value 的引用;key 不存在就先用默认构造塞一个进去,再返回它的引用 」。所以它有三个后果:容器会被改(size 变大);value 类型必须可默认构造(否则编译不过);const map 上根本不能用它。

operator[] 在写入 场景很好用(histogram[word]++ 是标准写法),但只读场景必须换 API:

cpp 复制代码
// map_subscript.cpp --- 编译: g++ -std=c++17 -Wall -O2 map_subscript.cpp -o map_subscript
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>

int main() {
    std::map<std::string, int> inventory{{"apple", 3}};
    std::cout << "size before = " << inventory.size() << '\n';

    const int missing = inventory["banana"];      // 反例,不要这么写:读一下就插进去了
    std::cout << "inventory[\"banana\"] = " << missing
              << " but size becomes " << inventory.size() << '\n';

    const auto found = inventory.find("cherry");  // 只读查询:找不到不会改容器
    std::cout << "find(\"cherry\") == end() ? " << (found == inventory.end())
              << " size = " << inventory.size() << '\n';

    try {
        std::cout << inventory.at("cherry") << '\n';
    } catch (const std::out_of_range& e) {         // 异常按引用捕,按值抛
        std::cout << "at(\"cherry\") threw std::out_of_range: " << e.what() << '\n';
    }

    const std::map<std::string, int>& frozen = inventory;
    std::cout << "size still = " << frozen.size() << '\n';
    return 0;
}
text 复制代码
size before = 1
inventory["banana"] = 0 but size becomes 2
find("cherry") == end() ? 1 size = 2
at("cherry") threw std::out_of_range: map::at
size still = 2

三种只读写法各有取舍:

写法 找不到时 能用在 const map 上 复杂度 什么时候用
m[key] 插入默认值(size +1) 不能 O(log n) 只在「写」的语义下用,如 m[k]++
m.at(key) 抛 std::out_of_range 能 O(log n) 逻辑上必须存在,不存在就是 bug
m.find(key) 返回 end() 能 O(log n) 常规只读查询,还要拿 value
m.count(key) 返回 0 能 O(log n) 只判断存在性
m.contains(key) 返回 false 能 O(log n) C++20 起可用,语义最直白

at 抛出的异常信息里那句 map::at 是 libstdc++ 的实现细节(标准只要求抛 std::out_of_range,没规定 what() 内容),换编译器可能不同,不要拿去匹配字符串。

官方文档:std::map::operator\[\] --- cppreference:官方第一句话就写着「若 key 不存在则插入 value_type(key, T())」。

官方文档:std::map::at --- cppreference

官方文档:std::map::contains(C++20)--- cppreference

C++20 才有 contains,在 C++17 里用 count 代替:

cpp 复制代码
// verify: std=c++20
#include <map>

bool has_key(const std::map<int, int>& table, int key) {
    return table.contains(key);      // 需要 C++20;C++17 写 table.count(key) != 0
}

4. 陷阱二:拿 std::find 去查 map

「map 的查找快」不是修辞。std::map::find 走的是红黑树的搜索路径,一次比较就把候选范围砍一半;而 std::find / std::find_if 是从 begin() 开始逐个线性扫描 ,完全无视树结构。给 map 的 key 装一个计数比较器、再给 find_if 的谓词装一个计数器,就能量出差距:

cpp 复制代码
// map_find_vs_std_find.cpp --- 编译: g++ -std=c++17 -Wall -O2 map_find_vs_std_find.cpp -o map_find_vs_std_find
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <map>

struct Stats {
    inline static std::size_t comparisons = 0;      // map 内部比较次数
    inline static std::size_t predicate_calls = 0;  // find_if 谓词调用次数
};

struct Key {
    int id = 0;
};

struct CountingLess {
    bool operator()(const Key& a, const Key& b) const {
        ++Stats::comparisons;
        return a.id < b.id;
    }
};

constexpr int N = 1000;

int main() {
    std::map<Key, int, CountingLess> table;
    for (int i = 0; i < N; ++i) table.emplace(Key{i}, i * 10);

    const Key target{500};                          // 正好在中间

    Stats::comparisons = 0;
    const auto by_tree = table.find(target);
    const std::size_t tree_cmp = Stats::comparisons;

    Stats::predicate_calls = 0;
    const auto by_scan = std::find_if(table.begin(), table.end(),
                                      [&target](const auto& kv) {
                                          ++Stats::predicate_calls;
                                          return kv.first.id == target.id;
                                      });

    std::cout << "keys = " << N << '\n';
    std::cout << "map::find    comparisons = " << tree_cmp
              << "  found=" << (by_tree != table.end()) << '\n';
    std::cout << "std::find_if calls       = " << Stats::predicate_calls
              << "  found=" << (by_scan != table.end()) << '\n';
    return 0;
}
text 复制代码
keys = 1000
map::find    comparisons = 11  found=1
std::find_if calls       = 501  found=1

同样是「查一个存在的 key」:

写法 依据 复杂度 1000 个 key 的实测
table.find(k) 红黑树搜索路径,每次比较砍一半 O(log n) 11 次比较
std::find_if(m.begin(), m.end(), ...) 从头部线性扫描 O(n) 501 次谓词调用

11 次比较正好是 log₂(1000) ≈ 10 的量级(红黑树最长路径不超过 2log₂(n+1));501 次是「目标在中间」的必然结果。规模再大十倍,前者只多 3~4 次,后者多十倍。用了 map 却拿 std::find 去查,等于把 O(log n) 亲手降级成 O(n)。

官方文档:std::map::find --- cppreference:标注的复杂度就是「与容器大小的对数成正比」。

官方文档:std::find_if --- cppreference:线性扫描的泛型版本。

5. 插入 API 选型:五种写法,构造次数差一倍

map 一次性提供了五种「插入或更新」的写法,它们的区别全在构造/拷贝/移动的次数 以及key 已存在时的行为。用一个自带计数器的 value 类型实测一遍:

cpp 复制代码
// map_emplace.cpp --- 编译: g++ -std=c++17 -Wall -O2 map_emplace.cpp -o map_emplace
#include <cstddef>
#include <iostream>
#include <map>
#include <string>

struct WidgetStats {
    inline static std::size_t default_ctors = 0;
    inline static std::size_t value_ctors = 0;
    inline static std::size_t copies = 0;
    inline static std::size_t moves = 0;
    static void reset() { default_ctors = 0; value_ctors = 0; copies = 0; moves = 0; }
    static std::size_t ctors() { return default_ctors + value_ctors; }
};

struct Widget {
    int id = 0;

    Widget() { ++WidgetStats::default_ctors; }
    explicit Widget(int v) : id(v) { ++WidgetStats::value_ctors; }
    Widget(const Widget& other) : id(other.id) { ++WidgetStats::copies; }
    Widget(Widget&& other) noexcept : id(other.id) { ++WidgetStats::moves; }
    Widget& operator=(const Widget& other) { id = other.id; ++WidgetStats::copies; return *this; }
    Widget& operator=(Widget&& other) noexcept { id = other.id; ++WidgetStats::moves; return *this; }
};

void report(const char* label) {
    std::cout << label << " ctors=" << WidgetStats::ctors()
              << " copies=" << WidgetStats::copies
              << " moves=" << WidgetStats::moves << '\n';
}

int main() {
    {
        std::map<std::string, Widget> m;
        WidgetStats::reset();
        m["a"] = Widget{1};                 // 默认构造一个 + 移动赋值
        report("operator[]      ");
    }
    {
        std::map<std::string, Widget> m;
        WidgetStats::reset();
        m.insert({"b", Widget{2}});         // 构造临时 pair 再移动进节点
        report("insert          ");
    }
    {
        std::map<std::string, Widget> m;
        WidgetStats::reset();
        m.emplace("c", 3);                  // 就地在节点里构造,零临时对象
        report("emplace         ");
    }
    {
        std::map<std::string, Widget> m;
        WidgetStats::reset();
        m.try_emplace("d", 4);              // key 不存在才构造(C++17)
        report("try_emplace     ");
    }
    {
        std::map<std::string, Widget> m;
        m.emplace("e", 5);
        WidgetStats::reset();
        m.emplace("e", 9);                  // key 已存在:白构造一个再丢掉
        report("emplace dup     ");
        std::cout << "  kept = " << m.at("e").id << '\n';
    }
    {
        std::map<std::string, Widget> m;
        m.emplace("f", 5);
        WidgetStats::reset();
        m.try_emplace("f", 9);              // key 已存在:一个对象都不构造
        report("try_emplace dup ");
        std::cout << "  kept = " << m.at("f").id << '\n';
    }
    {
        std::map<std::string, Widget> m;
        m.emplace("g", 5);
        WidgetStats::reset();
        m.insert_or_assign("g", Widget{9}); // C++17:明确要覆盖
        report("insert_or_assign");
        std::cout << "  kept = " << m.at("g").id << '\n';
    }
    return 0;
}
text 复制代码
operator[]       ctors=2 copies=0 moves=1
insert           ctors=1 copies=0 moves=2
emplace          ctors=1 copies=0 moves=0
try_emplace      ctors=1 copies=0 moves=0
emplace dup      ctors=1 copies=0 moves=0
  kept = 5
try_emplace dup  ctors=0 copies=0 moves=0
  kept = 5
insert_or_assign ctors=1 copies=0 moves=1
  kept = 9

把这些数字翻译成人话:

写法 key 已存在时 构造次数 拷贝/移动 实测(ctors/copies/moves) 什么时候用
m[k] = v 覆盖 2(默认构造 + 赋值) 1 次移动赋值 2 / 0 / 1 value 可默认构造,且你就是要覆盖
m.insert({k, v}) 忽略,bool=false 1 2 次移动 1 / 0 / 2 需要「到底插进去了没有」的返回值
m.emplace(k, args...) 忽略,但白构造一个 1(命中时也构造) 0 1 / 0 / 0(dup 也是 1) 新 key 就地构造;别对已存在的 key 用
m.try_emplace(k, args...) 忽略,且不构造 1(命中时 0) 0 1 / 0 / 0(dup 是 0) C++17 起的新代码首选
m.insert_or_assign(k, v) 覆盖 1 1 次移动赋值 1 / 0 / 1 C++17,语义上明确要覆盖

三条结论:

  1. 要「插入,已存在就别动」→ 用 try_emplace。 它是唯一在 key 已存在时连一个对象都不构造 的写法(实测 ctors=0),而 emplace 在同样情况下白构造一个 Widget 再销毁(实测 ctors=1)。value 很大或是持有资源(文件句柄、unique_ptr)时,这个差别就是实打实的开销。
  2. operator[] 的代价是「默认构造 + 赋值」两次操作 (实测 ctors=2 moves=1),而且要求 value 可默认构造。它在明确的写入场景(m[k] = v、m[k]++)够用,但不要用它做「不存在才插入」的逻辑。
  3. 要「覆盖」→ 用 insert_or_assign ,它的意图写在函数名里,比 m[k] = v 更明确;不过它要求 value 可赋值 ------注意上面代码里写的是 Widget{9},直接写 9 会编译失败,因为 Widget 的单参构造函数是 explicit,不存在从 int 到 Widget 的隐式赋值转换。

insert 的返回值 std::pair<iterator, bool> 值得单独记一笔:bool 告诉你「到底有没有真的插入」,iterator 指向「现在那个 key 所在的位置」(不管是新插的还是本来就有的)。上面第 3 节的 insert("bob", 99) 输出 inserted=0 就是这个 boole 在起作用。

官方文档:std::map::emplace --- cppreference

官方文档:std::map::try_emplace --- cppreference

官方文档:std::map::insert_or_assign --- cppreference

还有一条必须记住的:map 的 key 是 const 。value_type 是 std::pair<const Key, T>,所以 it->second 可以改,it->first 改不了:

cpp 复制代码
#include <map>
#include <string>

void keys_are_const(std::map<int, std::string>& m) {
    auto it = m.begin();
    it->second = "value is mutable";     // 可以:value 非 const
    // 反例,不要这么写:编译失败 ------ key_type 是 const int
    // it->first = 5;                    // error: assignment of read-only member
}

想改 key,只能 erase 再 insert (这正好也说明为什么 key 不能随便改:改了树的有序性就崩了)。上面的片段故意不写 main(),因为它是用来展示编译错误的。

6. 范围查询与自定义比较器

有序容器真正让人舍不得放弃的能力是范围查询 :lower_bound(第一个 ≥ key)、upper_bound(第一个 > key)、equal_range(一次拿到两者,也就是 [lower_bound, upper_bound))。

cpp 复制代码
// map_range.cpp --- 编译: g++ -std=c++17 -Wall -O2 map_range.cpp -o map_range
#include <functional>
#include <iostream>
#include <iterator>
#include <map>
#include <string>

// 踩坑用的比较器:只按字符串长度比较
struct ByLength {
    bool operator()(const std::string& a, const std::string& b) const {
        return a.size() < b.size();
    }
};

int main() {
    std::map<int, std::string> m{{10, "ten"}, {20, "twenty"}, {30, "thirty"}, {40, "forty"}};

    const auto lo = m.lower_bound(20);      // 第一个 >= 20
    const auto hi = m.upper_bound(30);      // 第一个 > 30
    std::cout << "range [20, 30] :";
    for (auto it = lo; it != hi; ++it) std::cout << ' ' << it->first << '=' << it->second;
    std::cout << '\n';

    const auto [first, last] = m.equal_range(30);   // 一次拿到上下界
    std::cout << "equal_range(30) count = " << std::distance(first, last) << '\n';

    std::map<int, std::string, std::greater<int>> desc{{10, "ten"}, {40, "forty"}, {20, "twenty"}};
    std::cout << "descending keys:";
    for (const auto& [key, value] : desc) std::cout << ' ' << key;
    std::cout << '\n';

    std::multimap<std::string, int> scores{{"alice", 80}, {"bob", 90}, {"alice", 95}};
    const auto [af, al] = scores.equal_range("alice");
    std::cout << "alice scores:";
    for (auto it = af; it != al; ++it) std::cout << ' ' << it->second;
    std::cout << '\n';

    std::map<std::string, int, ByLength> by_len;
    by_len["ab"] = 1;
    by_len["cd"] = 2;                       // 与 "ab" 等价(长度相同)→ 不新增条目
    std::cout << "by_len size = " << by_len.size()
              << " key = " << by_len.begin()->first
              << " value = " << by_len.begin()->second << '\n';
    return 0;
}
text 复制代码
range [20, 30] : 20=twenty 30=thirty
equal_range(30) count = 1
descending keys: 40 20 10
alice scores: 80 95
by_len size = 1 key = ab value = 2

逐个看:

  • [lower_bound(20), upper_bound(30)) 恰好是 [20, 30] :下半界是闭、上半界是开,所以想要「含 30」就必须用 upper_bound(30) 而不是 lower_bound(30)。这个半开区间约定和 STL 其他算法一致。
  • equal_range 一次返回上下界 ,用 std::distance 数一下就是「这个 key 有几个元素」。map 里最多 1 个,multimap 里可以多个------所以 equal_range 配 multimap 最合适(示例里 alice 的 80 和 95 都拿到了)。
  • 换比较器就换了遍历顺序 :std::greater<int> 让 key 降序,这是「有序」的另一个用法。
  • 最后一段是踩坑示范 :ByLength 只比字符串长度,于是 "ab" 和 "cd" 在它眼里等价 (谁都不小于谁),map 认为它们是同一个 key------by_len["cd"] = 2 根本没新增条目,只是把 "ab" 那格的值改成了 2。输出 by_len size = 1 key = ab value = 2 证实了这一点。

规则 :自定义比较器必须是一个严格弱序(strict weak ordering) ,其中最容易被忽略的一条是「等价必须可传递」------comp(a,b)==false && comp(b,a)==false 的两个 key 会被当作同一个 key。只比一部分字段(长度、大小写忽略后的样子、float 的近似相等)是很危险的比较器。默认的 std::less<Key>(即 operator<)在绝大多数场景都是对的。

官方文档:std::map::lower_bound --- cppreference

官方文档:std::lower_bound(泛型算法)--- cppreference

官方文档:std::less --- cppreference:std::map 的默认比较器,语义就是 operator<。

7. 完整示例:一份有序的成绩单

下面这个例子把前面所有要点串起来:insert 的 bool 判重、multimap 按分数排序取 Top-N、lower_bound/upper_bound 做分数区间查询。Key(姓名)用 map 保证不重复且有序;分数是「一对多」的,所以排序输出用 multimap。

cpp 复制代码
// score_board.cpp --- 编译: g++ -std=c++17 -Wall -O2 score_board.cpp -o score_board
#include <cstddef>
#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>

class ScoreBoard {
public:
    void add(const std::string& name, int score) {
        const auto [it, inserted] = scores_.insert({name, score});
        if (!inserted) {
            it->second = score;              // 已存在就更新分数(key 不可改,value 可改)
        }
    }

    std::vector<std::string> top(std::size_t count) const {
        std::multimap<int, std::string, std::greater<int>> ranked;   // 分数从高到低
        for (const auto& [name, score] : scores_) ranked.insert({score, name});
        std::vector<std::string> result;
        for (const auto& [score, name] : ranked) {
            if (result.size() == count) break;
            result.push_back(name);
        }
        return result;
    }

    std::vector<std::string> between(int low, int high) const {
        std::multimap<int, std::string> sorted;      // 默认 less:升序,才能做区间查询
        for (const auto& [name, score] : scores_) sorted.insert({score, name});
        std::vector<std::string> result;
        const auto last = sorted.upper_bound(high);  // 上半界开,才包含 high
        for (auto it = sorted.lower_bound(low); it != last; ++it) {
            result.push_back(it->second);
        }
        return result;
    }

    std::size_t size() const { return scores_.size(); }

private:
    std::map<std::string, int> scores_;              // 按人名有序,便于查找
};

int main() {
    ScoreBoard board;
    board.add("alice", 80);
    board.add("bob", 95);
    board.add("carol", 70);
    board.add("dave", 88);
    board.add("alice", 92);      // 重名 -> 更新分数,不新增条目

    std::cout << "players = " << board.size() << '\n';
    std::cout << "top 3:";
    for (const std::string& name : board.top(3)) std::cout << ' ' << name;
    std::cout << '\n';
    std::cout << "score in [80, 95]:";
    for (const std::string& name : board.between(80, 95)) std::cout << ' ' << name;
    std::cout << '\n';
    return 0;
}
text 复制代码
players = 4
top 3: bob alice dave
score in [80, 95]: dave alice bob

players = 4 而不是 5,说明重名的 alice 走了「更新」分支;top 3 按分数降序给出 bob(95)、alice(92)、dave(88);区间查询按分数升序给出 dave(88)、alice(92)、bob(95)。四个容器(map / multimap × 两种比较器)各司其职。

8. 选型速查

需求 选谁 理由
键值映射 + 需要有序遍历 std::map 红黑树,O(log n) 且 key 有序
去重 + 有序 std::set 同上,只存 key
一对多且要有序 std::multimap / std::multiset 允许等价 key,equal_range 拿到全部
只判断存在性 find / count(C++20 用 contains) 别用 operator[],它会插入
「不存在才插入」 try_emplace(C++17) 命中时不构造任何对象
「存在就覆盖」 insert_or_assign(C++17) 语义明确,且返回是否为新插入
只需要 O(1) 平均查找、不在乎顺序 std::unordered_map 哈希表,见《unordered_map / unordered_set 完全指南》
元素很少(十几个) std::vector + 排序 小 N 时线性查找的常数因子比树更低

9. 延伸阅读

10. 一句话总结

std::map / std::set 是一棵红黑树,有序和 O(log n) 是同一套结构的两个结果 :中序遍历即有序,所以有 lower_bound/upper_bound/equal_range;树高被保证在 O(log n),所以 map::find 查 1000 个 key 只要 11 次比较,而 std::find_if 要扫 501 次。用之前记住三条:operator[] 找不到会插入默认值 ,只读查询用 find(要抛异常用 at,C++20 可 contains);插入用 try_emplace ,它是唯一在 key 已存在时不构造任何对象的写法(emplace 会白构造一个);key 是 const,且自定义比较器必须满足严格弱序,否则「等价」的 key 会被悄悄吞掉。

相关推荐
朝朝辞暮i1 小时前
C++ 第 28 课:Lambda 表达式
开发语言·c++·算法
All for pursuit.1 小时前
【回溯-3】39.组合总和
数据结构·c++·算法·leetcode
见叶之秋1 小时前
【C++】一条双向链的诞生:手写 list 模拟实现
c++
沫璃染墨1 小时前
《从零入门Linux系统篇(五十六):线程篇·九——生产者消费者模型:从条件变量到BlockingQueue实现》
linux·服务器·c++·驱动开发·设计模式·架构·系统架构
张小姐的猫1 小时前
【Linux】网络编程 —— 五种IO模型
linux·运维·服务器·网络·c++·人工智能·php
不会就选b2 小时前
算法日常・每日刷题--<动态规划>6
数据结构·算法·leetcode
漂流瓶jz2 小时前
UVA-12545 比特变换器 题解答案代码 算法竞赛入门经典第二版
数据结构·算法·题解·aoapc·算法竞赛入门经典·uva·12545
hhzz2 小时前
【YOLO 入门到精通 08】推理预测完全指南:多数据源、流式推理与性能优化
人工智能·python·yolo·计算机视觉·性能优化
重生之小比特2 小时前
【C++进阶】02 Linux基本指令
java·数据库·c++