C++ STL 映射容器家族:std::map, std::unordered_map, std::multimap 详解
STL提供 三种主要的关联容器,用于存储 键值对 (key-value pairs) : std::map 、 std::unordered_map 和 std::multimap 。 它们的核心概念都是通过一个唯一的键来快速查找对应的值,但它们在 内部实现、键的唯一性要求和元素顺序 上有着显著区别。 此外,STL 也提供了一组 集合容器 ,用于存储 唯一元素 或 允许重复元素 的集合。 其中, std::set 和 std::unordered_set 类似于映射容器,但它们只存储键,不存储值。
共同核心知识
- 头文件:
- std::map 和 std::multimap ->
#include <map>
。 - std::unordered_map 和 std::unordered_multimap ->
#include <unordered_map>
。 - std::set 和 std::multiset ->
#include <set>
。 - std::unordered_set ->
#include <unordered_set>
。
- std::map 和 std::multimap ->
- 定义:
- 映射容器 (存储键值对) :
ContainerType<KeyType, ValueType> myContainer
;- 例如:
std::map<int, std::string> myMap
; - std::unordered_map<std::string, double> myUnorderedMap;
- std::multimap<char, int> myMultimap;
- 例如:
- 集合容器 (只存储键/元素) :
ContainerType<ElementType> myContainer
;- 例如:
std::set<int> mySet
; std::unordered_set<std::string> myUnorderedSet
;
- 例如:
- 映射容器 (存储键值对) :
- 基本概念:
- 键值对 (映射容器): 存储 (键, 值) 形式的数据。
- 元素 (集合容器): 只存储 元素本身 (作为键)。
- 关联容器: 元素通过键来访问,而不是通过位置 (如 std::vector)。
- 动态大小: 容器的大小会根据元素的增删动态调整。
- 迭代器: 都支持迭代器遍历,但迭代顺序因容器类型而异。
- 常用成员函数 (共通部分):
- insert() / emplace(): 插入元素。
- erase(): 删除元素。
- find(): 查找元素。
- empty(): 检查容器是否为空。
- size(): 返回元素数量。
- begin() / end(): 获取迭代器。
核心区别与特性对比 (映射容器)
特性 / 容器 | std::map | std::unordered_map | std::multimap |
---|---|---|---|
键的唯一性 | 唯一键 (Unique Keys) | 唯一键 (Unique Keys) | 允许重复键 (Multiple Keys) |
元素顺序 | 按键排序 (有序) :根据键的比较函数 (默认 operator<) 自动排序。 | 无序: 元素存储顺序依赖于哈希函数和内部实现,不保证任何特定顺序。 | 按键排序 (有序) :与 std::map 相同,根据键的比较函数自动排序。对于重复键,其相对顺序不保证。 |
底层实现 | 红黑树 (通常是自平衡二叉搜索树)。 | 哈希表 (通常是开放寻址或链式拉链法)。 | 红黑树 (通常是自平衡二叉搜索树)。 |
时间复杂度 | O(log N) (插入、删除、查找)。 | 平均 O(1) (插入、删除、查找); 最坏 O(N) (哈希冲突严重)。 | O(log N) (插入、删除、查找)。对于 count 和 equal_range 涉及重复键的操作,可能是 O(log N + M),M 为重复键的数量。 |
键类型要求 | 键类型必须支持 严格弱序 (Strict Weak Ordering) (默认 operator<)。 | 键类型必须是 可哈希的 (Hashable) (提供 std::hash 特化或自定义哈希函数) 且支持 相等比较 (operator==) 。 | 键类型必须支持 严格弱序 (Strict Weak Ordering) (默认 operator<)。 |
特定成员函数 | operator[], at() | operator[], at() | count(), equal_range(), lower_bound(), upper_bound() |
访问重复键 | 无概念 (键唯一) | 无概念 (键唯一) | 使用 count() 获取数量;使用 equal_range() 、 lower_bound() 和 upper_bound() 迭代所有重复键。 |
集合容器 (std::set, std::unordered_set) 简述
集合容器不存储键值对,只存储元素本身。其内部实现和性能特性与对应的映射容器非常相似:
std::set<T>
:- 特点: 有序,元素唯一。底层实现为红黑树。
- 时间复杂度: O(log N)。
- 应用场景: 需要存储一组唯一且有序的元素,例如:==排序后的不重复数字列表==。
std::unordered_set<T>
:- 特点: 无序,元素唯一。底层实现为哈希表。
- 时间复杂度: 平均 O(1),最坏 O(N)。
- 应用场景: ==需要快速检查某个元素是否存在于集合中,且不关心元素的顺序。==例如:记录已访问过的节点、去重。
详细讲解与代码示例
1. std::map (有序,唯一键)
- 特点: 像一本字典,每个单词 (键) 都有唯一的解释 (值),并且所有单词都按照字母顺序排列。
- 优点: 键自动排序 ,遍历时元素有序; 性能稳定 。
- 缺点: 查找速度不如 unordered_map (平均);额外内存开销。
- 代码示例:cpp
#include <iostream> #include <map> #include <string> void demo_map() { std::map<int, std::string> students; // 插入元素:operator[] 更简洁,insert 返回插入结果 students[101] = "Alice"; students.insert({103, "Charlie"}); students[101] = "Alicia"; // 键101已存在,修改其值 std::cout << "--- std::map 示例 ---" << std::endl; std::cout << "Map size: " << students.size() << std::endl; // 遍历 (按键有序) for (const auto& pair : students) { std::cout << "ID: " << pair.first << ", Name: " << pair.second << std::endl; } // 查找元素 auto it = students.find(103); if (it != students.end()) { std::cout << "Found 103: " << it->second << std::endl; } // 使用 at() 访问 (键不存在会抛异常) try { std::cout << "Accessing 101 with at(): " << students.at(101) << std::endl; // std::cout << "Accessing 999 with at(): " << students.at(999) << std::endl; // 这行会抛出异常 } catch (const std::out_of_range& e) { std::cerr << "Error: " << e.what() << std::endl; } // 删除元素 students.erase(101); std::cout << "After erasing 101, map size: " << students.size() << std::endl; }
2. std::unordered_map (无序,唯一键)
- 特点: 像一个散装的杂物抽屉,东西放进去时不会按顺序整理,但你知道大概放在哪个分区 (桶) 里,所以查找起来通常非常快。
- 优点: 极速查找 (平均 O(1))。
- 缺点: 元素无序 ;最坏情况性能可能退化;可能需要自定义哈希函数。
- 代码示例:cpp
#include <iostream> #include <unordered_map> #include <string> void demo_unordered_map() { std::unordered_map<std::string, double> grades; // 插入元素 grades["Math"] = 95.5; grades["Physics"] = 88.0; grades["Chemistry"] = 92.0; grades["Math"] = 98.0; // 键"Math"已存在,更新其值 std::cout << "\n--- std::unordered_map 示例 ---" << std::endl; std::cout << "Unordered map size: " << grades.size() << std::endl; // 遍历 (顺序不确定) for (const auto& pair : grades) { std::cout << "Subject: " << pair.first << ", Grade: " << pair.second << std::endl; } // 查找元素 auto it = grades.find("Physics"); if (it != grades.end()) { std::cout << "Found Physics grade: " << it->second << std::endl; } // 使用 at() 访问 try { std::cout << "Accessing Chemistry with at(): " << grades.at("Chemistry") << std::endl; } catch (const std::out_of_range& e) { std::cerr << "Error: " << e.what() << std::endl; } // 删除元素 grades.erase("Math"); std::cout << "After erasing Math, unordered map size: " << grades.size() << std::endl; }
3. std::multimap (有序,允许重复键)
- 特点: 像一个学生成绩单,一个学生 (键) 可能有多门课程的成绩 (值),所有学生仍按姓名排序,但一个学生名下可能有多条成绩记录。
- 优点: 允许重复键 ; 键自动排序 。
- 缺点: 没有 operator[] 和 at() ;查找所有重复键的值相对复杂。
- 代码示例:cpp
#include <iostream> #include <map> // multimap也在<map>头文件中 #include <string> void demo_multimap() { std::multimap<std::string, int> studentScores; // 插入元素 (允许重复键) studentScores.insert({"Alice", 90}); studentScores.insert({"Bob", 85}); studentScores.insert({"Alice", 95}); // Alice 的第二门成绩 studentScores.emplace("Charlie", 78); studentScores.insert({"Bob", 70}); // Bob 的第二门成绩 std::cout << "\n--- std::multimap 示例 ---" << std::endl; std::cout << "Multimap size: " << studentScores.size() << std::endl; // 遍历 (按键有序) for (const auto& pair : studentScores) { std::cout << "Name: " << pair.first << ", Score: " << pair.second << std::endl; } // 统计某个键的数量 std::cout << "Count of Alice: " << studentScores.count("Alice") << std::endl; // 查找某个键的所有值 std::string searchName = "Bob"; auto range = studentScores.equal_range(searchName); std::cout << "Scores for " << searchName << ":" << std::endl; for (auto it = range.first; it != range.second; ++it) { std::cout << " - " << it->second << std::endl; } // 删除某个键的所有元素 studentScores.erase("Bob"); std::cout << "After erasing Bob, multimap size: " << studentScores.size() << std::endl; }
4. std::unordered_set<int>
(无序,唯一元素)
- 特点: 一个高效的篮子,用于存放独一无二的物品。当你想知道某个物品是否已经在篮子里时,它能非常快速地告诉你。
- 优点: 极速检查元素是否存在 (平均 O(1)); 自动去重 。
- 缺点: 元素无序 ;最坏情况性能可能退化。
- 代码示例:cpp
#include <iostream> #include <unordered_set> // 包含unordered_set头文件 void demo_unordered_set() { // 定义一个存储整数的无序集合,用于记录已访问过的金额 std::unordered_set<int> visited_amounts; std::cout << "\n--- std::unordered_set 示例 ---" << std::endl; // 插入元素 visited_amounts.insert(100); visited_amounts.insert(25); visited_amounts.insert(75); visited_amounts.insert(25); // 25已存在,不会被重复插入 std::cout << "Set size: " << visited_amounts.size() << std::endl; // Output: 3 (100, 25, 75) // 检查元素是否存在 (O(1) 平均时间复杂度) if (visited_amounts.count(100)) { // count返回0或1 std::cout << "100 has been visited." << std::endl; } if (visited_amounts.find(50) == visited_amounts.end()) { // find返回迭代器 std::cout << "50 has not been visited." << std::endl; } // 遍历 (顺序不确定) std::cout << "Visited amounts: "; for (int amount : visited_amounts) { std::cout << amount << " "; } std::cout << std::endl; // 删除元素 visited_amounts.erase(75); std::cout << "After erasing 75, set size: " << visited_amounts.size() << std::endl; // Output: 2 } // 主函数,调用所有示例 int main() { demo_map(); demo_unordered_map(); demo_multimap(); demo_unordered_set(); return 0; }
选择指南
- 需要保持键的排序且键唯一? -> 选择 std::map 。
- 不关心键的排序,但需要最快的平均查找/插入/删除速度且键唯一? -> 选择 std::unordered_map 。
- 需要保持键的排序,且允许键重复? -> 选择 std::multimap 。
- 需要存储一组唯一且有序的元素? -> 选择 std::set 。
- 需要快速判断某个元素是否存在于一个唯一集合中,不关心顺序? -> 选择 std::unordered_set 。
- 不关心键的排序,且允许键重复? -> 考虑 std::unordered_multimap (与 unordered_map 类似,但允许重复键)。
- 需要存储一组允许重复且有序的元素? -> 考虑 std::multiset 。