【阿咘的 C++ 学习笔记】吃透 unordered 系列容器:unordered_map/unordered_set 实战指南
大家好,我是阿咘~上一篇咱们讲了 vector、map、string 这三种基础容器,有朋友在评论区问 “为什么用 map 查找时,数据总是按 key 排序?我不需要排序,只想快点查到结果怎么办?” 其实 C++ 的 STL 里早就为这种场景准备了 “unordered 系列容器”—— 比如 unordered_map、unordered_set,它们底层用哈希表实现,查找效率比 map、set 快得多,还不用自动排序。今天咱们就专门讲 unordered 系列的两个核心容器,从用法到原理,再到和 map/set 的区别,帮大家搞定 “快速查找且无需排序” 的需求~
一、先搞懂:什么是 unordered 系列容器?和 map/set 有啥区别?
很多新手会把 unordered_map 和 map 搞混,其实它们的核心区别就在 “底层结构” 和 “是否有序” 上:
- map/set:底层是 “红黑树”(一种平衡二叉搜索树),插入元素后会自动按 key 排序,查找时间复杂度是 O (log n);
- unordered_map/unordered_set:底层是 “哈希表”(也叫散列表),插入元素后无序,查找时间复杂度是 O (1)(理想情况下,比红黑树快很多)。
简单说,如果你需要 “有序 + 查找”,用 map/set;如果只需要 “快速查找,不在乎顺序”,用 unordered_map/unordered_set—— 比如缓存用户信息(用用户 ID 当 key,只需要快速查信息,不需要按 ID 排序),用 unordered_map 就比 map 更合适。
今天咱们重点讲两个最常用的 unordered 容器:
- unordered_map:键值对容器(key-value),和 map 用法类似,但无序、查找快;
- unordered_set:单值容器(只存 key,不存 value),用于 “去重” 和 “快速判断元素是否存在”。
二、unordered_map:无序的 “键值对” 神器
unordered_map 的用法和 map 几乎一样,比如创建、增删改查的接口都差不多,但底层是哈希表,所以有一些特殊的注意事项(比如哈希冲突)。咱们用代码实战来看具体用法。
1. 核心用法:创建 + 增删改查(附代码)
#include <iostream>
#include <unordered_map> // 使用unordered_map必须包含这个头文件
#include <string>
using namespace std;
int main() {
// 1. 创建unordered_map:key是int(用户ID),value是string(用户名)
unordered_map<int, string> userMap;
// 2. 新增键值对(三种常用方式,和map一样)
userMap[1001] = "阿咘"; // 方式1:下标赋值(最常用)
userMap.insert({1002, "张三"}); // 方式2:insert+初始化列表(C++11及以上)
userMap.emplace(1003, "李四"); // 方式3:emplace(效率高,推荐)
// 3. 访问元素(两种方式)
cout << "方式1:下标访问(注意:不存在的key会自动创建空值)" << endl;
cout << "用户1001:" << userMap[1001] << endl; // 输出:阿咘
cout << "用户1004:" << userMap[1004] << endl; // 注意:1004不存在,会创建key=1004、value为空的键值对
cout << "\n方式2:find查找(推荐,避免创建无效键值对)" << endl;
auto findIt = userMap.find(1003); // 查找key=1003
if (findIt != userMap.end()) { // 找到:返回迭代器
cout << "找到用户1003:" << findIt->second << endl; // 输出:李四
} else { // 没找到:返回end()
cout << "未找到用户1003" << endl;
}
// 4. 遍历元素(无序!和插入顺序无关)
cout << "\n遍历unordered_map(无序):" << endl;
// 范围for循环(最简洁)
for (auto& pair : userMap) {
cout << "用户ID:" << pair.first << ",用户名:" << pair.second << endl;
}
// 迭代器遍历(和map一样,it->first是key,it->second是value)
for (unordered_map<int, string>::iterator it = userMap.begin(); it != userMap.end(); it++) {
cout << "用户ID:" << it->first << ",用户名:" << it->second << endl;
}
// 5. 修改元素(和map一样,通过key修改)
userMap[1002] = "张三三"; // 把1002的用户名从“张三”改成“张三三”
cout << "\n修改后用户1002:" << userMap[1002] << endl;
// 6. 删除元素(三种方式)
userMap.erase(1004); // 方式1:通过key删除(删除刚才自动创建的1004)
auto delIt = userMap.find(1003);
if (delIt != userMap.end()) {
userMap.erase(delIt); // 方式2:通过迭代器删除
}
// userMap.erase(userMap.begin(), userMap.end()); // 方式3:删除所有元素(清空)
cout << "\n删除后容器大小:" << userMap.size() << endl;
return 0;
}
2. 关键知识点:避免踩坑
- 无序性是核心特点:运行上面的代码,你会发现遍历输出的顺序和插入顺序(1001→1002→1003→1004)完全不一样 —— 这是因为哈希表会根据 key 的哈希值分配存储位置,所以无序是正常的,不要试图用 unordered_map 实现 “有序存储”;
- 下标访问的坑:和 map 一样,用userMap[key]访问不存在的 key 时,会自动创建一个 “key = 目标 key、value = 默认值” 的键值对(比如 string 的默认值是空字符串),这会导致容器里出现无效数据。所以查找元素时一定要用 find 方法,不要用下标;
- 哈希冲突的影响:哈希表的核心是 “把 key 通过哈希函数转换成索引”,但不同的 key 可能算出相同的索引(这就是 “哈希冲突”)。STL 会用 “链地址法” 解决冲突(把冲突的元素存在链表上),如果冲突太多,查找效率会从 O (1) 降到 O (n)。解决办法是:尽量用 “分布均匀的 key”(比如 int 类型比 string 类型冲突少),或者自定义哈希函数;
- 自定义类型当 key 的注意事项:如果想用自定义类(比如 User 类)当 unordered_map 的 key,必须手动实现两个东西:
- 哈希函数:告诉 STL 怎么计算这个类的哈希值;
- == 运算符重载:告诉 STL 怎么判断两个类对象是否相等(解决哈希冲突时需要)。
3. 适用场景:什么时候用 unordered_map?
- 不需要排序,只需要 “快速查找” 的场景,比如:
-
- 缓存系统(用户 ID→用户信息,查缓存时只需要快,不在乎顺序);
-
- 统计单词出现次数(单词→次数,只需要统计结果,不需要按单词排序);
- 数据量较大的场景(比如 10 万条数据):unordered_map 的 O (1) 查找比 map 的 O (log n) 快得多,数据量越大,优势越明显。
三、unordered_set:无序的 “去重 + 快速判断” 工具
unordered_set 是 “单值容器”,意思是它只存 key,不存 value—— 而且 key 不能重复(自动去重)。它的核心作用有两个:去重和快速判断元素是否存在,比如判断一个单词是否在词典里、判断一个用户 ID 是否已存在。
1. 核心用法:创建 + 去重 + 查找(附代码)
#include <iostream>
#include <unordered_set> // 使用unordered_set必须包含这个头文件
#include <vector>
using namespace std;
int main() {
// 1. 创建unordered_set:存int类型的元素(比如用户ID)
unordered_set<int> userIdSet;
// 2. 新增元素(三种方式)
userIdSet.insert(1001); // 方式1:insert
userIdSet.emplace(1002); // 方式2:emplace(效率高,推荐)
vector<int> ids = {1003, 1004, 1001}; // 方式3:插入另一个容器的元素(去重!)
userIdSet.insert(ids.begin(), ids.end());
// 3. 自动去重的特点:插入重复元素会被忽略
cout << "插入1001、1002、1003、1004、1001后,容器大小:" << userIdSet.size() << endl; // 输出4(1001只存一次)
// 4. 查找元素(核心功能,快速判断是否存在)
int targetId = 1003;
auto findIt = userIdSet.find(targetId);
if (findIt != userIdSet.end()) {
cout << "\n找到用户ID:" << *findIt << endl; // unordered_set的迭代器解引用就是元素值
} else {
cout << "\n未找到用户ID:" << targetId << endl;
}
// 5. 遍历元素(无序,和插入顺序无关)
cout << "\n遍历unordered_set(无序):" << endl;
for (auto id : userIdSet) {
cout << id << " ";
}
cout << endl;
// 6. 删除元素(三种方式)
userIdSet.erase(1004); // 方式1:通过元素值删除
auto delIt = userIdSet.find(1002);
if (delIt != userIdSet.end()) {
userIdSet.erase(delIt); // 方式2:通过迭代器删除
}
// userIdSet.clear(); // 方式3:清空所有元素
cout << "\n删除后容器大小:" << userIdSet.size() << endl; // 输出2(剩下1001、1003)
return 0;
}
2. 关键知识点:避免踩坑
- 元素不能重复:unordered_set 的 key 是唯一的,插入重复元素会被自动忽略(insert 方法会返回一个 pair,first 是迭代器,second 是 bool 值,表示是否插入成功)。如果需要 “允许重复元素”,用unordered_multiset(和 unordered_set 用法类似,支持重复 key);
- 不能修改元素:unordered_set 的元素是 “const” 的,不能直接修改(比如*it = 2000会编译报错)—— 因为元素值是哈希表的索引依据,修改后哈希值会变,导致元素位置错乱。如果需要修改,只能先删除旧元素,再插入新元素;
- 和 vector 的去重对比:用 vector 去重需要先排序(sort)再去重(unique),时间复杂度是 O (n log n);而 unordered_set 插入时自动去重,时间复杂度是 O (n),数据量越大,unordered_set 的去重效率越高。
3. 适用场景:什么时候用 unordered_set?
- 去重:比如统计一个数组中不重复的元素、过滤重复的用户 ID;
- 快速判断元素是否存在:比如判断一个单词是否在词典中、判断一个 IP 是否在黑名单中;
- 不需要排序的场景:如果需要排序,用set(底层红黑树,自动排序),否则优先用 unordered_set。
四、阿咘的总结:unordered 系列 vs 普通系列容器怎么选?
最后用一张表总结 unordered_map/unordered_set 和 map/set 的核心区别,帮大家快速选型:
|
容器对比 |
底层结构 |
是否有序 |
查找时间复杂度 |
适用场景 |
|
unordered_map |
哈希表 |
无序 |
O (1)(理想) |
快速查找键值对,无需排序(缓存、统计次数) |
|
map |
红黑树 |
有序 |
O(log n) |
需要有序的键值对(按 key 排序的排行榜) |
|
unordered_set |
哈希表 |
无序 |
O (1)(理想) |
快速去重、判断元素存在,无需排序 |
|
set |
红黑树 |
有序 |
O(log n) |
需要有序的去重(按元素值排序的不重复列表) |
给新手的小建议:日常开发中,如果不需要排序,优先用 unordered 系列容器—— 因为 O (1) 的查找效率比 O (log n) 快得多,尤其是数据量大的时候。只有当需要 “自动排序” 时,再用 map/set。
今天的 unordered 系列容器就讲到这里啦~其实它们的用法和 map/set 很像,核心区别就在 “无序” 和 “查找效率” 上。如果大家在使用时遇到 “自定义类型当 key 报错”“哈希冲突导致效率低” 这类问题,欢迎在评论区留言,咱们一起解决~感兴趣的朋友可以关注一下!
更多推荐



所有评论(0)