一、std::map 红黑树的物理布局
std::map<int, string> m;
m[5] = "five";
m[3] = "three";
m[7] = "seven";
内存中的红黑树结构:
堆地址 0x1000: 节点 3 [left=null|right=0x2000|parent=0x3000|color=RED |"three"]
堆地址 0x2000: 节点 7 [left=null|right=null |parent=0x3000|color=BLACK|"seven"]
堆地址 0x3000: 节点 5 [left=0x1000|right=0x2000|parent=null|color=BLACK|"five" ]
↑
| (根节点,平衡二叉树)
遍历时的缓存表现:每次通过指针跳到下一节点,和 list 一样——缓存命中率极低。
面试高频题:“为什么 map 的插入是 $O(\log N)$ 但实际可能比 unordered_map 的 $O(1)$ 更快?”
- 当 N 很小(< 1000)时,红黑树的 $\log N$ 大约 10 次比较,而哈希表需要计算哈希值(几十条指令)+ 取模 + 可能的冲突解决。
- 哈希表在元素少时,负载因子低,桶空间浪费大,缓存命中率比红黑树还差。
二、std::unordered_map 开链哈希表
unordered_map<int, string> um;
um.max_load_factor(1.0);
桶数组 (Bucket Array, 连续内存):
+───────+───────+───────+───────+───────+
| 桶 0 | 桶 1 | 桶 2 | 桶 3 | ... |
+───┬───+───┬───+───┬───+───┬───+───┬───+
│ │ │ │
▼ ▼ ▼ ▼
节点 节点 nullptr 节点
[5] [13] [7]
│ │ │
▼ ▼ ▼
nullptr nullptr nullptr
每个桶指向一个单向/双向链表的头节点
Rehash 的代价:
for (int i = 0; i < 100000; ++i) {
um[i] = "value"; // 可能触发多次 rehash
}
Rehash 内部流程:
- 分配新桶数组(通常是旧大小的 2 倍)。
- 遍历所有节点,重新计算
hash(key) % new_bucket_count,把节点挂到新桶上。 - 释放旧桶数组。
Rehash 的 3 个性能灾难:
- $O(N)$ 时间复杂度:100 万个元素 rehash 一次可能要几百毫秒。
- 所有迭代器失效:和 vector 扩容一样,桶数组指针变了。
- Cache 大清洗:新桶数组 + 重新链接节点,所有缓存行失效。
高性能教训:可以预先知道元素数量时,用 reserve(N) 提前分配桶空间。
三、容器选择速查
| 场景 | 推荐 | 原因 |
|---|---|---|
| 遍历为主、随机访问 | vector |
连续内存,缓存友好 |
| 两端插入、不需要随机访问 | deque |
两端 $O(1)$,无扩容搬移 |
| 中间插入/删除频繁 | list |
插入/删除 $O(1)$,但遍历极慢 |
| 有序键值查找 | map |
红黑树 $O(\log N)$ |
| 无序键值查找、海量数据 | unordered_map |
均摊 $O(1)$,小心 rehash |