云计算百科
云计算领域专业知识百科平台

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

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

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

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

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)。

红黑树(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) 有序袋、滑动窗口中位数

// 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;
}

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:

// 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;
}

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 代替:

// 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 的谓词装一个计数器,就能量出差距:

// 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;
}

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 类型实测一遍:

// 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;
}

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,语义上明确要覆盖

三条结论:

  • 要「插入,已存在就别动」→ 用 try_emplace。 它是唯一在 key 已存在时连一个对象都不构造的写法(实测 ctors=0),而 emplace 在同样情况下白构造一个 Widget 再销毁(实测 ctors=1)。value 很大或是持有资源(文件句柄、unique_ptr)时,这个差别就是实打实的开销。
  • operator[] 的代价是「默认构造 + 赋值」两次操作(实测 ctors=2 moves=1),而且要求 value 可默认构造。它在明确的写入场景(m[k] = v、m[k]++)够用,但不要用它做「不存在才插入」的逻辑。
  • 要「覆盖」→ 用 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 改不了:

    #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))。

    // 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;
    }

    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。

    // 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;
    }

    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. 延伸阅读

    • std::map — cppreference:接口全貌;注意 value_type 是 pair<const Key, T>,这就是 key 不可改的来源。
    • std::map::operator[] — cppreference:官方明确写了「key 不存在时插入 value-initialized 的 value」,是第 3 节的依据。
    • std::map::try_emplace — cppreference:注意「若 key 已存在则不做任何事」这句,是它比 emplace 省的根源。
    • std::map::lower_bound — cppreference 与 std::map::upper_bound:半开区间的官方定义,配合 equal_range 一起看。
    • std::multimap — cppreference:等价 key 的相邻性保证(同一个 key 的元素在遍历中连续出现)。
    • C++ Core Guidelines — isocpp.github.io:容器选型与「别用 operator[] 做查询」这类接口设计思路的来源。

    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 会被悄悄吞掉。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » map / set 完全指南:红黑树与有序容器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!