当前位置: 首页 > news >正文

C++ STL 有序关联容器高频面试题解析

目录

1. 什么是有序关联容器?

2. Q&A 高频面试题

Q1: std::map 和 std::set 有什么根本区别?

Q2: multi 版本 (multimap, multiset) 和非 multi 版本 (map, set) 有什么区别?

Q3: 有序关联容器的底层实现通常是什么?

Q4: 为什么选择红黑树作为底层实现? 它有什么优点?

Q5: std::map 和 std::unordered_map 有什么主要区别? 应如何选择?

Q6: map 插入元素有哪几种方式? 它们有什么区别和效率考量?

Q7: 在 map 中使用 operator[] 和 at() 访问元素有什么区别?

Q8: 如何在 map 或 set 中使用自定义类型作为键?

Q9: 如何安全地在遍历 map 或 set 的过程中删除元素?

Q10: map::lower_bound 和 map::upper_bound 的作用是什么?

Q11: map/set 中常用的查找 API 有哪些? 它们有什么区别?


1. 什么是有序关联容器?

在 C++ STL 中,关联容器 (Associative Containers) 允许根据键 (Key) 高效地存储和检索元素。有序 (Ordered) 关联容器会根据元素的键自动对元素进行排序。

C++ STL 提供了四种有序关联容器:

  1. std::map: 存储键值对 (pair<const Key, T>),键是唯一的,元素按键排序。

  2. std::set: 只存储键 (Key),键是唯一的,元素按键排序。

  3. std::multimap: 类似于 map,但允许键重复

  4. std::multiset: 类似于 set,但允许键重复

2. Q&A 高频面试题

Q1: std::mapstd::set 有什么根本区别?

答:

  • 存储内容不同:

    • std::map 存储的是键值对 (Key-Value Pair)。它的元素类型是 std::pair<const Key, T>

    • std::set 只存储键 (Key)。它的元素类型就是 Key

  • 使用场景不同:

    • map 用于需要根据一个键来存储和查找一个关联值(映射关系)的场景。

    • set 用于只需要快速检查某个元素(键)是否存在于一个集合中,或者需要一个自动排序的、不重复的元素集合的场景。

Q2: multi 版本 (multimap, multiset) 和非 multi 版本 (map, set) 有什么区别?

答:

唯一的区别在于是否允许键重复

  • mapset 要求键必须是唯一的。如果你尝试插入一个已经存在的键,insert 操作会失败。

  • multimapmultiset 允许插入具有相同键的多个元素。

Q3: 有序关联容器的底层实现通常是什么?

答:

C++ 标准并没有硬性规定必须使用哪种数据结构,但几乎所有主流的 STL 实现(如 GCC, Clang, MSVC)都使用红黑树 (Red-Black Tree)

红黑树是一种自平衡的二叉搜索树 (Self-Balancing Binary Search Tree)。

Q4: 为什么选择红黑树作为底层实现? 它有什么优点?

答:

选择红黑树主要是因为它在各种操作上提供了稳定且高效的性能保证:

  1. 自平衡: 红黑树通过一系列规则(节点颜色、旋转操作)确保树的高度大致保持在 $O(log N)$,其中 $N$ 是元素的数量。这防止了二叉搜索树退化成链表(最坏情况 $O(N)$)。

  2. 高效的操作复杂度: 由于树的高度平衡在 $O(log N)$,它所有关键操作的最坏情况时间复杂度都是 $O(log N)$,包括:

    • 插入 (Insert)

    • 删除 (Erase)

    • 查找 (Find / operator[] / at)

  3. 天然有序: 作为一种二叉搜索树,红黑树中的元素总是保持有序。这使得它能高效地支持:

    • 有序遍历: (中序遍历)

    • 范围查找: 如 lower_bound, upper_bound 操作,复杂度也是 $O(log N)$。

Q5: std::mapstd::unordered_map 有什么主要区别? 应如何选择?

答:

这是一个非常高频的问题,核心区别在于底层实现:

特性

std::map (有序)

std::unordered_map (无序)

底层实现

红黑树 (平衡二叉搜索树)

哈希表 (Hash Table)

元素顺序

有序 (按键排序)

无序 (按哈希值组织)

时间复杂度 (平均)

插入/删除/查找: $O(log N)$

插入/删除/查找: $O(1)$

时间复杂度 (最坏)

插入/删除/查找: $O(log N)$

插入/删除/查找: $O(N)$ (哈希碰撞导致)

对键的要求

需定义 operator< 或提供比较器

需定义哈希函数 (std::hash) 和 operator==

如何选择:

  • 使用 std::map:

    1. 当你需要按键顺序遍历元素时。

    2. 当你需要执行范围查找(例如,查找所有大于 $X$ 且小于 $Y$ 的键)时。

    3. 当键类型没有合适的哈希函数,或者哈希碰撞可能很严重时(map 的 $O(log N)$ 性能更稳定)。

  • 使用 std::unordered_map:

    1. 当你不需要元素有序时。

    2. 当你追求极致的平均查找、插入、删除性能($O(1)$)时。

    3. 对最坏情况 $O(N)$ 的性能不敏感(通常哈希函数设计良好时很少发生)。

Q6: map 插入元素有哪几种方式? 它们有什么区别和效率考量?

答:

主要有三种方式:

  1. m[key] = value; (使用 operator[])

    • 行为: 如果 key 已存在,它会覆盖原有的 value。如果 key 不存在,它会自动插入一个新元素,键为 key,值会先进行默认构造,然后再被赋予 value

    • 效率: 效率可能较低。它涉及一次查找($O(log N)$),如果键不存在,还涉及一次默认构造和一次赋值操作。

    • 限制: 不能用于 const std::map

  2. m.insert(std::make_pair(key, value));m.insert({key, value});

    • 行为: 如果 key 已存在,插入会失败,不会覆盖原有值。

    • 效率: 高效。它只需要一次查找和插入操作($O(log N)$)。它返回 std::pair<iterator, bool>,其中 bool 值表示是否插入成功。

    • 优点: 可以明确知道是否插入了新元素。

  3. m.emplace(key, value); (C++11)

    • 行为: 和 insert 相同,如果 key 已存在,插入会失败。

    • 效率: 理论上最高效emplace 使用可变参数模板和完美转发,直接在容器内存中原地构造 (in-place construct) 元素(即 std::pair),避免了创建临时对象再进行拷贝或移动的开销。

    • 推荐: 在 C++11 及之后,优先使用 emplace

Q7: 在 map 中使用 operator[]at() 访问元素有什么区别?

答:

这个区别在面试中用以考察对 API 细节的掌握程度:

  • m[key] (operator[]):

    • 键存在: 返回对应 value 的引用。

    • 键不存在: 会自动插入一个新元素(键为 key,值为默认构造),并返回这个新值的引用。

    • 风险: 可能会无意中插入不想要的默认值元素。

    • 限制: 不能用于 const std::map(因为它可能会修改 map)。

  • m.at(key):

    • 键存在: 返回对应 value 的引用。

    • 键不存在: 抛出一个 std::out_of_range 异常

    • 优点: 语义明确,用于"只查不插"的场景。

    • 限制: 可以用于 const std::map

总结: 如果你确定键一定存在,或者你希望在键不存在时插入默认值,使用 []。如果你不确定键是否存在,且不希望插入新元素(而是想处理这个错误),使用 at() 并配合 try-catch,或者先用 find() 检查。

Q8: 如何在 mapset 中使用自定义类型作为键?

答:

mapset 依赖于键的比较。要使用自定义类型 MyType 作为键,必须满足以下二者之一

  1. 重载 operator<: 在类 MyType 内部或全局重载 operator<

    struct MyType {int id;std::string name;// 重载 operator<bool operator<(const MyType& other) const {// 必须提供一个“严格弱序”return this->id < other.id;}
    };std::set<MyType> mySet;
    std::map<MyType, int> myMap;
    
  2. 提供自定义比较器 (Comparator): 定义一个函数对象(Functor)或函数指针,并在模板参数中指定它。

    struct MyType {int id;std::string name;
    };// 自定义比较器(函数对象)
    struct MyCompare {bool operator()(const MyType& a, const MyType& b) const {return a.id < b.id;}
    };// 在模板参数中指定比较器
    std::set<MyType, MyCompare> mySet;
    std::map<MyType, int, MyCompare> myMap;
    

    注意: 比较器必须提供严格弱序 (Strict Weak Ordering),否则容器的行为是未定义的。

Q9: 如何安全地在遍历 mapset 的过程中删除元素?

答:

这是一个经典的迭代器失效问题。在 mapset(基于红黑树)中删除元素时,指向被删除元素的迭代器会立即失效,但其他迭代器保持有效。

错误的做法:

// 错误!erase后 it 失效,再执行 ++it 是未定义行为
for (auto it = m.begin(); it != m.end(); ++it) {if (/* 满足删除条件 */) {m.erase(it); }
}

正确的做法 (C++11 之前):

需要利用 erase 的后自增特性,或者在 erase 前先缓存下一个迭代器。

for (auto it = m.begin(); it != m.end(); /* 不在这里自增 */) {if (/* 满足删除条件 */) {// C++98/03 写法:m.erase(it++); // it++ 返回 it 的旧值供 erase 删除,同时 it 已经指向下一个} else {++it;}
}

正确的做法 (C++11 及之后):

erase(it) 会返回指向被删除元素之后元素的迭代器

for (auto it = m.begin(); it != m.end(); /* 不在这里自增 */) {if (/* 满足删除条件 */) {it = m.erase(it); // erase 返回下一个有效的迭代器} else {++it;}
}

Q10: map::lower_boundmap::upper_bound 的作用是什么?

答:

这两个函数利用了 map 的有序性,提供了 $O(log N)$ 复杂度的范围查找能力。

  • m.lower_bound(key): 返回一个迭代器,指向第一个键不小于 (即大于或等于) key 的元素。

    • 如果 key 存在,返回指向 key 的迭代器。

    • 如果 key 不存在,返回指向第一个比 key 大的元素的迭代器。

    • 如果所有元素都比 key 小,返回 m.end()

  • m.upper_bound(key): 返回一个迭代器,指向第一个键大于 key 的元素。

    • 无论 key 是否存在,都返回第一个比 key 大的元素的迭代器。

    • 如果所有元素都小于或等于 key,返回 m.end()

应用: 它们经常组合使用来获取一个键范围 [key_a, key_b]

// 遍历所有键在 [key_a, key_b] 范围内的元素
auto start = m.lower_bound(key_a);
auto end = m.upper_bound(key_b); // 查找第一个大于 key_b 的for (auto it = start; it != end; ++it) {// it->first 在 [key_a, key_b] 范围内
}

Q11: map/set 中常用的查找 API 有哪些? 它们有什么区别?

答:

有序关联容器提供了多种查找 API,它们的时间复杂度通常都是 $O(log N)$。

  1. find(key):

    • 作用: 最常用的查找函数。

    • 行为: 查找键为 key 的元素。

    • 返回值: 如果找到,返回指向该元素的迭代器;如果未找到,返回 container.end()

    • 用法: if (m.find(key) != m.end()) { /* 找到了 */ }

  2. count(key):

    • 作用: 统计键为 key 的元素数量。

    • 行为:

      • 对于 mapset (键唯一): 返回 1 (找到) 或 0 (未找到)。

      • 对于 multimapmultiset (键可重复): 返回键为 key 的元素总数 (可能 $\ge 1$)。

    • 复杂度: $O(k + log N)$,$k$ 为返回的元素数量。对于 map/set,就是 $O(log N)$。

  3. lower_bound(key)upper_bound(key):

    • 作用: 范围查找 (详见 Q10)。

    • lower_bound(key): 查找 $\ge key$ 的第一个元素。

    • upper_bound(key): 查找 $> key$ 的第一个元素。

  4. equal_range(key):

    • 作用: 获取一个键对应的所有元素范围。

    • 行为: 返回一个 std::pair<iterator, iterator>,该 pair 标记了所有键等于 key 的元素范围。

      • pair.first 等同于 lower_bound(key)

      • pair.second 等同于 upper_bound(key)

    • 用法:

      • multimapmultiset 中遍历所有特定键的元素时非常有用。

      • auto range = m.equal_range(key); for (auto it = range.first; it != range.second; ++it) { /* 处理所有 key 相同的元素 */ }

  5. at(key) (仅 map):

    • 作用: 访问 key 对应的 value (详见 Q7)。

    • 行为: 如果 key 存在,返回 value 的引用。如果 key 不存在,抛出 std::out_of_range 异常。

  6. operator[] (仅 map):

    • 作用: 访问或插入 (详见 Q7)。

    • 行为: 如果 key 存在,返回 value 的引用。如果 key 不存在,插入一个新元素(值-默认构造)并返回其引用。

http://www.dtcms.com/a/553703.html

相关文章:

  • 腾讯风铃怎么做网站成都哪家做网站
  • 二叉树核心算法分类精讲:选择、遍历与结构关系
  • 【人工智能系列:走近人工智能05】基于 PyTorch 的机器学习开发与部署实战
  • Arbess零基础学习,创建第一条流水线
  • Linux DNS 深度解析与最佳实践
  • RGB转换为NV12,查表式算法
  • PostIn零基础学习,创建第一个项目
  • 百度网站优化排行做响应式网站应该注意什么
  • 女生化妆品网站建设规划书该网站的域名为
  • 基于NvVideoEncoder的H265视频编码器
  • 淄博网站快照优化公司html5开发网站
  • 厦门门户网站制作服务商保健品商城网站模板
  • day61-devops
  • 【代码随想录算法训练营——Day54】并查集——107.寻找存在的路线
  • 用「费曼-神经耦合学习法」21天攻克算法
  • VScode C/C++环境配置
  • 禁用vscode的任务结束提示
  • 做网站接单的网站公司做网站哪个好
  • 轻量实用的 XML 与 JSON / 对象互转工具类(Jackson 实现)
  • Go Web 编程快速入门 19 - 附录C:事务与 CRUD(含最佳实践)
  • SQL Server从Enterprise CAL到Core版升级全记录:解锁160核心性能的完整复盘与深刻反思
  • 网站建设和管理培训自建服务器做网站要备案
  • ArkTS 第一课:从零开始学鸿蒙应用开发
  • 做门户网站代码质量方面具体需要注意什么厦门网页制作设计营销
  • LightGBM(Light Gradient Boosting Machine)模型详解
  • LeetCode 每日一题 1526. 形成目标数组的子数组最少增加次数
  • Linux中伙伴系统页面回收free_pages_bulk和分配函数__rmqueue的实现
  • 33.点赞功能
  • 网站怎么快速做排名个人在线免费公司注册
  • 微信官网网站模板百度站长平台网页版