C++容器详解:map、set与unordered_map
·
好的,我们来详细探讨一下 C++ 中的关联式容器 map, set 和 unordered_map。
一、关联式容器概述
关联式容器在 C++ 标准模板库(STL)中用于存储和管理元素集合。与序列式容器(如 vector, list)不同,关联式容器通过键(key)来高效地查找和访问元素。它们通常基于特定的数据结构实现,以保证特定的操作效率。
二、有序关联容器:map 和 set
这两种容器通常基于红黑树(一种自平衡二叉搜索树)实现。红黑树保证了元素按照键(map)或值本身(set)严格有序(通常是升序)。
1. std::map
- 概念:存储键值对(
key-value pairs)。每个键在容器中是唯一的,用于标识和访问其关联的值。 - 底层原理:红黑树。
- 每个节点存储一个键值对
std::pair<const Key, Value>。 - 树根据键(
Key)进行比较和排序。 - 插入、删除、查找操作的平均和最坏时间复杂度均为 $O(\log n)$。
- 保证元素按键的顺序迭代(升序)。
- 每个节点存储一个键值对
- 关键特性:
- 键唯一性:不能有重复的键。
- 有序性:元素按键排序。
- 主要操作:
- 插入:
insert或emplace。 - 访问/修改:
operator[](若键不存在则插入)或at()(键不存在时抛异常)。 - 查找:
find()(返回迭代器),count()(返回 0 或 1)。 - 删除:
erase()。 - 遍历:迭代器(
begin(),end()),得到的是有序序列。
- 插入:
- 应用场景:
- 需要按键排序存储键值对。
- 需要按键进行范围查询(如
lower_bound,upper_bound)。 - 需要按顺序遍历键值对。
- 需要稳定的对数时间查找、插入、删除。
- 例如:存储学生 ID(键)到姓名(值)的映射,需要按 ID 排序或查找特定 ID 的学生。
示例代码:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<int, std::string> studentMap;
// 插入
studentMap.insert({102, "Alice"});
studentMap[101] = "Bob"; // 使用 operator[]
studentMap.emplace(103, "Charlie");
// 访问
std::cout << "ID 101: " << studentMap[101] << std::endl;
try {
std::cout << "ID 104: " << studentMap.at(104) << std::endl; // 会抛出 std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "Key 104 not found." << std::endl;
}
// 查找
auto it = studentMap.find(102);
if (it != studentMap.end()) {
std::cout << "Found ID 102: " << it->second << std::endl;
}
// 遍历(有序)
for (const auto& pair : studentMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
2. std::set
- 概念:存储唯一的键(
Key)的集合。它本身既是键也是值。 - 底层原理:红黑树。
- 每个节点存储一个
Key。 - 树根据
Key进行比较和排序。 - 插入、删除、查找操作的平均和最坏时间复杂度均为 $O(\log n)$。
- 保证元素按值的顺序迭代(升序)。
- 每个节点存储一个
- 关键特性:
- 元素唯一性:集合中无重复元素。
- 有序性:元素按值排序。
- 主要操作:
- 插入:
insert或emplace。 - 查找:
find()(返回迭代器),count()(返回 0 或 1)。 - 删除:
erase()。 - 遍历:迭代器(
begin(),end()),得到的是有序序列。
- 插入:
- 应用场景:
- 需要存储唯一元素的集合。
- 需要集合元素有序。
- 需要按顺序遍历集合。
- 需要集合运算(并、交、差)或判断元素是否存在。
- 需要稳定的对数时间查找、插入、删除。
- 例如:存储一组唯一的单词,需要按字典序输出或检查某个单词是否存在。
示例代码:
#include <iostream>
#include <set>
int main() {
std::set<int> uniqueNumbers;
// 插入
uniqueNumbers.insert(5);
uniqueNumbers.insert(2);
uniqueNumbers.insert(8);
uniqueNumbers.insert(2); // 重复,不会插入
// 查找
if (uniqueNumbers.find(5) != uniqueNumbers.end()) {
std::cout << "5 is in the set." << std::endl;
}
std::cout << "Count of 3: " << uniqueNumbers.count(3) << std::endl; // 输出 0
// 遍历(有序)
for (int num : uniqueNumbers) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 2 5 8
return 0;
}
三、无序关联容器:std::unordered_map
这种容器基于哈希表(Hash Table)实现。哈希表通过哈希函数将键映射到存储位置(桶),提供平均常数时间的访问。
std::unordered_map
- 概念:存储键值对(
key-value pairs)。每个键在容器中是唯一的。 - 底层原理:哈希表(通常使用链地址法解决冲突)。
- 使用哈希函数
std::hash<Key>计算键的哈希值。 - 根据哈希值将键值对分配到不同的桶(
bucket)中。 - 桶内通常用链表(或红黑树,取决于实现和负载因子)存储具有相同哈希值的键值对。
- 插入、删除、查找操作的平均时间复杂度为 $O(1)$,最坏情况(如所有键哈希冲突)为 $O(n)$。
- 元素没有特定的顺序(迭代顺序不确定,可能随插入顺序、哈希函数、桶大小变化)。
- 使用哈希函数
- 关键特性:
- 键唯一性:不能有重复的键。
- 无序性:元素不按特定顺序存储。
- 主要操作:
- 插入:
insert或emplace。 - 访问/修改:
operator[](若键不存在则插入)或at()(键不存在时抛异常)。 - 查找:
find()(返回迭代器),count()(返回 0 或 1)。 - 删除:
erase()。 - 遍历:迭代器(
begin(),end()),但顺序是未定义的。
- 插入:
- 性能影响因素:
- 哈希函数质量:好的哈希函数应尽可能均匀分布键,减少冲突。
- 负载因子:已存储元素数量与桶数量的比值。负载因子过高会导致冲突增加,性能下降。可通过
load_factor()和max_load_factor()查看和设置,或通过rehash()和reserve()调整桶的数量。
- 应用场景:
- 需要非常快速的键值查找、插入、删除,且不关心元素顺序。
- 不需要按顺序遍历或范围查询。
- 例如:实现缓存(
Cache)、词频统计(不关心单词顺序)、快速查找配置项等。
示例代码:
#include <iostream>
#include <unordered_map>
#include <string>
int main() {
std::unordered_map<std::string, int> wordFrequency;
// 插入/更新 (operator[] 很方便)
wordFrequency["apple"] = 5;
wordFrequency["banana"]++;
wordFrequency["apple"] = 10; // 更新
// 访问
std::cout << "apple count: " << wordFrequency["apple"] << std::endl;
std::cout << "orange count: " << wordFrequency["orange"] << std::endl; // 不存在,会插入 orange:0
// 查找 (避免用 operator[] 检查存在性)
auto it = wordFrequency.find("banana");
if (it != wordFrequency.end()) {
std::cout << "Found banana: " << it->second << std::endl;
}
// 遍历 (顺序不确定)
for (const auto& pair : wordFrequency) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// 检查负载因子
std::cout << "Load factor: " << wordFrequency.load_factor() << std::endl;
std::cout << "Max load factor: " << wordFrequency.max_load_factor() << std::endl;
return 0;
}
四、总结与比较
| 特性 | std::map | std::set | std::unordered_map |
|---|---|---|---|
| 底层数据结构 | 红黑树 | 红黑树 | 哈希表 |
| 元素排序 | 按键排序 (有序) | 按值排序 (有序) | 无序 |
| 平均时间复杂度 | $O(\log n)$ (查找/插入/删除) | $O(\log n)$ (查找/插入/删除) | $O(1)$ (查找/插入/删除) |
| 最坏时间复杂度 | $O(\log n)$ | $O(\log n)$ | $O(n)$ (哈希冲突严重时) |
| 键唯一性 | 是 | 是 (元素唯一) | 是 |
| 是否需要哈希函数 | 否 (需比较运算符 <) | 否 (需比较运算符 <) | 是 (需 std::hash<Key>) |
| 内存开销 | 较高 (树节点指针) | 较高 (树节点指针) | 较低 (桶 + 链表/树节点) |
| 主要应用场景 | 需有序键值对/范围查询 | 需有序唯一元素集合 | 需极速查找/插入/删除,无序 |
选择建议:
- 如果需要元素有序存储或进行范围查询,选择
map或set。 - 如果只需要快速查找、插入、删除元素,且不关心顺序,选择
unordered_map。 - 对于
map和set,键/值类型需要支持严格弱序的比较(通常定义operator<)。 - 对于
unordered_map,键类型需要支持哈希函数(通常有std::hash特化)和相等比较(定义operator==)。
理解这些容器的底层原理和特性差异,有助于在 C++ 编程中选择最合适的工具来高效地解决实际问题。
更多推荐
所有评论(0)