STL-关联容器(面试复习4)

目录

1)关联容器有哪些?各自底层是什么?

[2)为什么 map/set 查找是 O(logN)?红黑树有什么性质?](#2)为什么 map/set 查找是 O(logN)?红黑树有什么性质?)

[3)map vs unordered_map 选型:什么时候用哪个?](#3)map vs unordered_map 选型:什么时候用哪个?)

[4)map 的 operator[] 有什么副作用?](#4)map 的 operator[] 有什么副作用?)

[5)unordered_map 的 operator[] 也会插入吗?](#5)unordered_map 的 operator[] 也会插入吗?)

[6)count / find / equal_range 在 set/map 中怎么用?](#6)count / find / equal_range 在 set/map 中怎么用?)

[7)lower_bound / upper_bound 在 map/set 的意义?](#7)lower_bound / upper_bound 在 map/set 的意义?)

[8)multimap/multiset 和 map/set 区别?](#8)multimap/multiset 和 map/set 区别?)

[9)erase 的几种重载与迭代器失效规则](#9)erase 的几种重载与迭代器失效规则)

[10)unordered_map 的 rehash、load_factor、reserve:会问很频繁](#10)unordered_map 的 rehash、load_factor、reserve:会问很频繁)

[11)为什么 unordered_* 最坏 O(N)?如何缓解?](#11)为什么 unordered_* 最坏 O(N)?如何缓解?)

[12)自定义排序:map/set 如何按自定义规则排序?](#12)自定义排序:map/set 如何按自定义规则排序?)

[13)自定义 hash:unordered_map 如何支持自定义 key(如 pair/struct)?](#13)自定义 hash:unordered_map 如何支持自定义 key(如 pair/struct)?)

[14)map 的迭代器为什么是"稳定"的?能指向元素后长期使用吗?](#14)map 的迭代器为什么是“稳定”的?能指向元素后长期使用吗?)

[15)map 的元素是 const key,为什么?](#15)map 的元素是 const key,为什么?)

[16)如何高效"更新或插入"?try_emplace / emplace / insert_or_assign](#16)如何高效“更新或插入”?try_emplace / emplace / insert_or_assign)

[17)如何用 map 做"按 value 排序"?](#17)如何用 map 做“按 value 排序”?)

18)时间复杂度对比常考表

额外:面试官最喜欢追问的"实战题型"(关联容器常用套路)


1)关联容器有哪些?各自底层是什么?

Q:STL 的关联容器有哪些?map/setunordered_map/unordered_set 区别是什么?

A:

  • 有序关联容器:set / multiset / map / multimap

    • 底层:红黑树(平衡二叉搜索树)

    • 按 key 有序std::less<Key> 默认)

    • 典型复杂度:查找/插入/删除 O(log N)

  • 无序关联容器:unordered_set / unordered_multiset / unordered_map / unordered_multimap

    • 底层:哈希表(bucket + 链/开放寻址实现)

    • 不保证遍历顺序

    • 平均复杂度:查找/插入/删除 O(1) ,最坏 O(N)(哈希冲突退化)

常见坑:

  • 面试官常追问:*为什么 map 是 O(logN)?为什么 unordered_ 最坏会 O(N)?**(见后续题)

2)为什么 map/set 查找是 O(logN)?红黑树有什么性质?

Q:map/set 为什么查找插入删除是 O(logN)?红黑树核心性质是什么?

A:

因为红黑树是一种近似平衡 BST,树高是 O(logN) ,所以基于树高的操作都是 O(logN)

红黑树典型性质(说出 3~5 条就够):

  1. 节点是红或黑

  2. 根是黑

  3. 叶子 NIL 节点(空节点)是黑

  4. 红节点的孩子必须是黑(不能连红)

  5. 从任一节点到其所有后代 NIL 的路径,黑节点数相同(黑高一致)

常见坑:

  • 不需要背"证明",但要能说:通过限制红节点分布与黑高一致,让树不会退化成链表。

3)map vs unordered_map 选型:什么时候用哪个?

Q:什么时候用 map,什么时候用 unordered_map

A:

map 的场景:

  • 需要有序(比如按 key 范围遍历、找 lower_bound/upper_bound)

  • 需要稳定的 O(logN) 最坏保证

  • key 类型不适合/不好写 hash(或者想减少哈希攻击风险)

unordered_map 的场景:

  • 只关心 key->value 的快速访问,不需要排序

  • 平均性能优先(大量查询/插入)

  • key 哈希分布良好

常见坑:

  • unordered_map 遍历顺序不稳定,rehash 后顺序也可能变。

  • 最坏 O(N) 在安全场景/对抗输入(哈希冲突)可能是问题。


4)map 的 operator[] 有什么副作用?

Q:map[key] 做了什么?与 find 区别?

A:

  • operator[]:若 key 不存在,会插入一个新元素,value 用默认构造生成,然后返回 value 引用。

  • find:不插入,只查找。

示例:

复制代码
std::map<std::string,int> m;
m["a"]++;        // 插入 {"a",0} 再自增 => 1
auto it = m.find("b"); // 不会插入

常见坑:

  • 面试常考:统计频次时 m[x]++ 可以;但"只想判断存在"就别用 [],会污染容器。

5)unordered_map 的 operator[] 也会插入吗?

Q:unordered_map[key] 会插入吗?

A:

会。规则与 map 一样:key 不存在就插入默认值。

坑:

  • 频繁 [] 可能导致不断增长、触发 rehash,性能抖动。

6)count / find / equal_range 在 set/map 中怎么用?

Q:countfindequal_range 分别适合什么?

A:

  • find(k):找一个 key 的迭代器(不存在返回 end)

  • count(k)

    • set/map 结果只可能是 0 或 1

    • multiset/multimap 可能大于 1

  • equal_range(k)

    • 返回 [lower_bound(k), upper_bound(k))

    • 对 multi 容器尤其好用(一次拿到某个 key 的所有元素范围)


7)lower_bound / upper_bound 在 map/set 的意义?

Q:lower_bound / upper_bound 返回什么?复杂度?

A:

  • lower_bound(k):第一个 >= k 的位置

  • upper_bound(k):第一个 > k 的位置

  • map/set 上复杂度 O(logN)(树查找)

常见用法:

  • 范围查询、区间删除、统计区间元素数量(结合 distance/迭代)

8)multimap/multiset 和 map/set 区别?

Q:multi 版本与非 multi 版本区别?如何删除某个 key 的一个元素 vs 全部?

A:

  • map/set:key 唯一

  • multimap/multiset:key 可重复

删除:

  • 删除全部:ms.erase(key)(返回删掉的数量)

  • 删除一个:先 auto it = ms.find(key); if(it!=end) ms.erase(it);

坑:

  • erase(key) 在 multi 容器会删一堆;面试经常考你是否知道差异。

9)erase 的几种重载与迭代器失效规则

Q:erase 有哪些用法?迭代器会不会失效?

A:

  • erase(it) 删除迭代器所指元素

  • erase(key) 删除 key 匹配的元素

  • erase(first, last) 删除区间

迭代器失效:

  • map/set:删除某个元素只会使被删元素的迭代器失效,其他迭代器仍有效。

  • unordered_*

    • erase 会让被删元素迭代器失效;

    • rehash 会让所有迭代器可能失效(这是大坑)

安全遍历删除写法(通用):

复制代码
for(auto it = m.begin(); it != m.end(); ){
    if(cond) it = m.erase(it);
    else ++it;
}

10)unordered_map 的 rehash、load_factor、reserve:会问很频繁

Q:rehash 什么时候发生?load_factormax_load_factorreserve 有什么用?

A:

  • 哈希表维护:load_factor = size / bucket_count

  • load_factor > max_load_factor 时,触发 rehash(扩 bucket 并重新分布元素)

  • reserve(n):让容器至少能容纳 n 个元素而不频繁 rehash(典型优化手段)

  • rehash(b):直接设置 bucket 数下限为 b

面试加分点:

  • 大量插入前 reserve 能减少 rehash 次数,显著优化性能稳定性。

11)为什么 unordered_* 最坏 O(N)?如何缓解?

Q:unordered_map 为什么可能退化到 O(N)?怎么解决?

A:

原因:大量 key 哈希到同一个桶,桶内查找退化成线性链(或冲突处理退化)。

缓解:

  • 选择更好的 hash(自定义 hash

  • 提前 reserve

  • 控制 max_load_factor

  • 对抗场景下选 map(避免哈希攻击导致退化)


12)自定义排序:map/set 如何按自定义规则排序?

Q:如何让 set 按自定义规则排序?比较器要满足什么?

A:

  • 传入比较器类型:std::set<T, Cmp>

  • 比较器必须满足严格弱序(strict weak ordering)

    • 不能出现自相矛盾:cmp(a,b)cmp(b,a) 不能同时为 true

    • 等价关系要稳定,否则会导致容器行为未定义/丢元素

常见坑:

  • 用"<="写比较器会炸(必须是严格 < 语义)

  • 比较器依赖可变外部状态也容易出事故


13)自定义 hash:unordered_map 如何支持自定义 key(如 pair/struct)?

Q:unordered_map<pair<int,int>, int> 怎么写?

A:

需要提供 hash(以及必要时 ==):

复制代码
struct PairHash {
    size_t operator()(const std::pair<int,int>& p) const noexcept {
        return std::hash<int>{}(p.first) ^ (std::hash<int>{}(p.second) << 1);
    }
};

std::unordered_map<std::pair<int,int>, int, PairHash> mp;

加分点:

  • 提到 noexcept、hash 混合方式、以及 pair/struct 要保证 == 与 hash 一致。

14)map 的迭代器为什么是"稳定"的?能指向元素后长期使用吗?

Q:map 插入/删除后,已有迭代器会失效吗?

A:

  • map/set 插入不会让已有迭代器失效(节点式结构)

  • 删除只让被删元素迭代器失效

    因此很多场景下迭代器更"稳定"。

对比:

  • vector 插入/扩容会让大量迭代器失效

  • unordered_* rehash 会让迭代器大面积失效


15)map 的元素是 const key,为什么?

Q:为什么 mapvalue_typepair<const Key, T>

A:

因为 key 决定在红黑树中的位置。

如果允许修改 key,会破坏有序性和树结构不变量,所以 key 必须是 const。

坑:

  • 不能 it->first = newKey;(编译失败),要改只能 erase + insert。

16)如何高效"更新或插入"?try_emplace / emplace / insert_or_assign

Q:C++17 里 map/unordered_map 如何避免不必要构造?

A:

  • try_emplace(k, args...):key 不存在才构造 value(更省)

  • insert_or_assign(k, v):存在就赋值,不存在就插入

  • emplace:原地构造(但如果你传了 value 对象,仍可能先构造再移动)

常见坑:

  • 面试官会让你解释:emplace 不是"永远零拷贝",取决于参数传递方式。

17)如何用 map 做"按 value 排序"?

Q:map 只能按 key 排序,如果要按 value 排序怎么办?

A:

  • 常规做法:把 map 拷贝到 vector<pair<...>>,然后 sort 按 value 排序

  • 或者使用 multimap<value, key> 反转存储(注意 value 重复)

坑:

  • 直接写比较器按 value 排序会破坏 key 唯一/查找语义(不建议)。

18)时间复杂度对比常考表

Q:map/set、unordered_map/unordered_set 的复杂度表?

A(口述版即可):

  • map/set:查找/插入/删除 O(logN)(最坏也保证)

  • unordered_*:平均 O(1) ,最坏 O(N);rehash 会导致阶段性抖动


额外:面试官最喜欢追问的"实战题型"(关联容器常用套路)

  1. 词频统计/TopKunordered_map 计数 + priority_queue 或 sort

  2. 去重set / unordered_set

  3. 区间查询 :必须用 map/setlower_bound/upper_bound

  4. LRU/缓存unordered_map + list(关联容器当索引用)

  5. 自定义 key:struct/pair 的 hash 与比较器

相关推荐
bybitq2 小时前
string,byte,rune,character?详解Golang编码-UTF-8
开发语言·后端·golang
wjs20242 小时前
HTML 框架:构建网页结构的基础
开发语言
无限进步_2 小时前
【C语言】栈(Stack)数据结构的实现与应用
c语言·开发语言·数据结构·c++·后端·visual studio
闻缺陷则喜何志丹2 小时前
【计算几何 SAT轴】P6732 「Wdsr-2」方分|普及+
c++·数学·计算几何·sat轴·凸多边形分离
embrace992 小时前
【C语言学习】预处理详解
java·c语言·开发语言·数据结构·c++·学习·算法
拼好饭和她皆失2 小时前
《二分答案算法精讲:从原理到实战(上篇)》
c++·算法
helloworddm2 小时前
C++与C#交互 回调封装为await
c++·c#·交互
应用市场2 小时前
TCP网络连接断开检测机制详解——C++实现网络连通性判断与断线类型识别
网络·c++·tcp/ip
浅尝辄止;2 小时前
C# 优雅实现 HttpClient 封装(可直接复用的工具类)
开发语言·c#