
一、基础概念
unordered_set、unordered_map 属于 C++ STL 关联式容器。
unordered_set:存储 key,key 唯一;unordered_map:存储pair<const Key,T>键值对,key 唯一;
与之对应的红黑树容器:set、map。 二者对外 API 接口高度趋同,都支持insert、find、erase;但是底层数据结构完全不同,带来有序性、时间复杂度、迭代器行为上巨大差异。
核心区分:
set/map底层:红黑树(平衡二叉搜索树),遍历输出 key 有序;增删查稳定 \(O(logN)\)。unordered_set/unordered_map底层:开链法哈希桶 ,遍历输出无序;增删查平均 \(O(1)\),最坏 \(O(N)\)。
头文件:
cpp
#include <unordered_set>
#include <unordered_map>
表格
| 容器 | 底层 | 是否有序 | key 是否唯一 | 迭代器类别 | 时间复杂度 | key 要求 |
|---|---|---|---|---|---|---|
| set | 红黑树 | ✅有序 | 唯一 | 双向迭代器 | \(O(logN)\) | 支持operator< |
| unordered_set | 开链哈希桶 | ❌无序 | 唯一 | 单向迭代器 | 平均\(O(1)\),最坏\(O(N)\) | 可哈希 + 支持operator== |
| map | 红黑树 | ✅有序 | 唯一 | 双向迭代器 | \(O(logN)\) | 支持operator< |
| unordered_map | 开链哈希桶 | ❌无序 | 唯一 | 单向迭代器 | 平均\(O(1)\),最坏\(O(N)\) | 可哈希 + 支持operator== |
| multiset | 红黑树 | ✅有序 | 允许重复 | 双向迭代器 | \(O(logN)\) | 支持operator< |
| unordered_multiset | 开链哈希桶 | ❌无序 | 允许重复 | 单向迭代器 | 平均\(O(1)\),最坏\(O(N)\) | 可哈希 + 支持operator== |
二、模板参数完整拆解
unordered_set 模板原型
cpp
template <class Key,
class Hash = hash<Key>, //哈希仿函数
class Pred = equal_to<Key>, //相等比较仿函数
class Alloc = allocator<Key> //内存分配器
>
class unordered_set;
- Key:存储的关键字类型。
- Hash = hash<Key> :哈希仿函数,接收 key,返回
size_t无符号整数哈希值;内置类型int、std::string标准库已经提供hash特化版本,直接可用。自定义类型作为 key,必须自己提供哈希仿函数。 - Pred = equal_to<Key> :相等比较仿函数,用来判断两个 key 是否完全等价,底层调用
operator==。自定义类型做 key,需要重载==运算符。 - Alloc:空间配置器,管理内存申请释放,业务开发几乎不需要手动传入。
unordered_map 模板原型
cpp
template<class Key, class T,
class Hash = hash<Key>,
class Pred = equal_to<Key>,
class Alloc = allocator<pair<const Key, T>>
>
class unordered_map;
- 存储元素类型:
pair<const Key, T>,key是const,不允许修改,避免哈希定位错乱;value 可以正常读写。 - 和
map一样支持operator[];unordered_multimap 依然不支持 operator \[\]。
对外常用接口,和 map/set 保持一致:
cpp
//插入,返回pair<迭代器,bool>;bool标记插入是否成功(key是否重复)
pair<iterator,bool> insert(const value_type& val);
//按key删除
size_type erase(const key_type& k);
//查找key,找不到返回end()
iterator find(const key_type& k);
//unordered_map独有
mapped_type& operator[](const key_type& k);
三、底层:开链哈希桶(拉链法)
unordered 系列底层是数组 + 单向链表 的组合,数组每一个下标位置称为一个
bucket哈希桶。
- 插入流程 ① 使用哈希仿函数对 key 计算,得到
size_t哈希值; ② 哈希值对桶数组的总容量取模,得到桶数组下标; ③ 将结点挂到该下标对应的单向链表尾部。 - 查找流程 ① 计算 key 哈希值,取模定位对应桶; ② 遍历这个桶内单向链表,使用
==逐个对比 key; ③ 找到匹配 key 返回迭代器;遍历完链表没找到返回end()。 - 删除流程 ① 哈希定位桶; ② 遍历桶内链表找到目标结点; ③ 链表删除该结点,释放内存。
哈希冲突
哈希冲突:不同的 key,经过哈希计算取模之后,落到同一个哈希桶。 哈希值不一样,取模之后下标相同,同样会落到同一个桶。 ⚠重点:哈希值相等,不代表 key 相等;哈希值不等,key 一定不相等。 所以定位桶依靠哈希;桶内部区分真正相等 key,必须依靠
==相等判断,两套逻辑缺一不可。
开链法解决冲突:冲突的结点全部挂载同一个桶下的单向链表,不会覆盖原有数据。 缺点:如果大量元素集中落在少数桶,链表越来越长,查找就要遍历长链表,性能退化。
负载因子 load_factor
\(load\_factor = \frac{容器有效元素数量size}{哈希桶总数量bucket\_count}\)
max_load_factor:负载因子阈值,STL 默认值为1.0。- 当
load_factor > max_load_factor,触发rehash 重哈希。
rehash 重哈希机制【面试高频】
- rehash 会开辟一块更大的哈希桶数组(通常扩大到原来倍数);
- 遍历旧哈希表全部结点,每一个 key 重新计算哈希,重新取模,挂载到新桶数组对应链表;
- 全部迁移完成后释放旧桶数组内存。
重大后果:rehash 之后,全部旧迭代器全部失效! 因为结点迁移到新内存,旧迭代器指向已经释放的旧数组,解引用属于未定义行为。
优化手段:如果预先知道要插入多少数据,可以调用reserve(n),提前预分配足够桶,减少运行过程中频繁 rehash 带来性能抖动。
cpp
unordered_set<int> us;
us.reserve(100000); //预留足够桶,降低rehash触发次数
哈希相关专属接口(业务写代码很少用,底层调试、面试会考)
cpp
bucket_count() // 当前桶总数量
load_factor() // 当前负载因子
max_load_factor() // 获取/设置最大负载因子阈值
rehash(n) // 手动指定至少n个桶,强制重哈希
reserve(n) // 根据元素数量n,自动计算合适桶数预分配
四、迭代器核心特性
- unordered_xxx 是单向迭代器 :仅支持前置 / 后置
it++;不支持--it反向迭代 ,没有rbegin()、rend()。
对比 map/set 双向迭代器:
++--都支持,底层红黑树结点保存 parent 父指针,可以向上回溯。 unordered 底层是单向链表,结点没有向前指针,无法后退。
- 迭代器失效规则:
- rehash 重哈希:全部迭代器直接失效。
- insert 插入:只要不触发 rehash,现有迭代器保持有效;一旦 rehash,全部失效。
- erase 删除:只有被删除结点迭代器失效,其余迭代器仍然有效。
- 遍历无序:
unordered 容器遍历输出顺序,和插入顺序无关;顺序由哈希值、桶下标、链表顺序共同决定。即使同样输入,rehash 之后遍历顺序会改变。 ⚠禁止业务代码依赖 unordered 的遍历顺序,测试偶然有序只是巧合。
五、key 的约束条件(面试高频)
map /set(红黑树)
只需要 key 支持operator<小于比较;内部依靠小于完成排序搜索。
unordered_set /unordered_map(哈希)
两个条件必须同时满足
- key 可以被哈希:
hash<Key>能够得到 size_t 哈希值; - key 支持相等比较:重载
operator==。
面试题:自定义结构体,作为 unordered_set 的 key 需要做什么?
- 编写自定义哈希仿函数,把结构体成员混合计算,返回
size_t哈希值; - 对结构体重载
operator==,判断两个对象 key 语义等价。
对比:自定义结构体作为 set/map 的 key,只需要重载 operator<。
示例伪代码:
cpp
struct Student
{
int id;
string name;
bool operator==(const Student& other) const
{
return id == other.id;
}
};
//自定义哈希仿函数
struct HashStudent
{
size_t operator()(const Student& s) const
{
return hash<int>()(s.id);
}
};
//使用:传入自定义哈希
unordered_set<Student, HashStudent> st;
六、unordered_multiset /unordered_multimap
- 特性:允许 key 重复,底层依然是开链哈希桶;无序,单向迭代器。
insert:允许插入重复 key;返回的 pair 中 bool 无实际意义。find(key):返回桶链表中第一个匹配 key 的迭代器。erase(key)按值删除:会删除容器中全部等于 key 的元素 ;只想删除其中一个,必须传入迭代器erase(it)。- unordered_multimap不支持 operator \[\],和 multimap 保持一致,重复 key 无法确定取哪一个 value。
七、map/set 和 unordered_map/unordered_set 选型策略
✅优先选择 unordered(哈希)
- 业务只做单点增、删、查,不需要 key 有序输出;
- 数据量大,追求平均性能;
缺点:存在 rehash 性能抖动;最坏退化 O (N);内存开销更大;单向迭代器;无序。
✅优先选择 map /set(红黑树)
- 需要 key 有序输出;
- 需要区间操作:使用
lower_bound、upper_bound做范围查询;哈希表无法高效区间查找; - 业务对最坏时间复杂度有硬性要求,需要稳定\(O(logN)\),不能接受退化到 O (N);
- 需要反向遍历,依赖双向迭代器
--it。
补充:小数据规模场景,\(O(logN)\)红黑树与哈希\(O(1)\)差距很小;哈希还要计算哈希值、处理 rehash,甚至性能不如红黑树。
八、完整性能测试参考代码
对比 set 与 unordered_set 插入、查找、删除耗时,直观体现性能差异。
cpp
#include <unordered_set>
#include <set>
#include <iostream>
#include <vector>
#include <ctime>
using namespace std;
int main()
{
const size_t N = 1000000;
vector<int> v;
v.reserve(N);
srand((unsigned int)time(nullptr));
for(size_t i = 0; i < N; ++i)
{
v.push_back(rand() + i);
}
set<int> s;
unordered_set<int> us;
//插入计时
size_t begin1 = clock();
for(auto e : v)
s.insert(e);
size_t end1 = clock();
cout << "set insert time:" << end1 - begin1 << endl;
size_t begin2 = clock();
us.reserve(N);
for(auto e : v)
us.insert(e);
size_t end2 = clock();
cout << "unordered_set insert time:" << end2 - begin2 << endl;
//查找计时
size_t begin3 = clock();
for(auto e : v)
s.find(e);
size_t end3 = clock();
cout << "set find time:" << end3 - begin3 << endl;
size_t begin4 = clock();
for(auto e : v)
us.find(e);
size_t end4 = clock();
cout << "unordered_set find time:" << end4 - begin4 << endl;
return 0;
}
现象:大数据量,哈希容器平均速度更快;如果哈希函数设计差,大量冲突,性能会大幅下跌。
九、大厂面试高频问题总结
Q1:map 与 unordered_map 底层,时间复杂度?
map 底层红黑树平衡二叉搜索树;增删查稳定\(O(logN)\),key 有序,双向迭代器,支持
lower_bound区间查找。 unordered_map 底层开链哈希桶;平均\(O(1)\),哈希冲突严重最坏退化\(O(N)\);遍历无序,单向迭代器;rehash 会全部迭代器失效。
Q2:unordered 为什么既需要 hash 函数,又需要 == 运算符?
hash 函数只用来计算哈希值定位哈希桶;不同 key 可以算出相同哈希值,发生哈希冲突 ;同一个桶链表内,必须用
==来真正判断两个 key 是否等价;哈希值相等不等于 key 相等。
Q3:rehash 重哈希做了什么,会带来什么问题?
负载因子超过阈值 max_load_factor,开辟更大哈希桶数组;把旧容器中全部结点重新计算哈希,迁移挂载到新桶数组,释放旧内存。rehash 发生之后所有旧迭代器全部失效 。可以使用
reserve()提前预分配,减少 rehash。
Q4:unordered 的迭代器为什么不支持 -- 反向迭代?
底层桶内是单向链表,结点只有后继指针,没有前驱指针,只能向后遍历,属于单向迭代器;红黑树结点保存 parent 父指针,可以向上回溯,支持双向迭代。
Q5:multimap 和 unordered_multimap 的区别?
multimap 底层红黑树,key 有序,双向迭代器,允许重复 key; unordered_multimap 底层开链哈希桶,无序,单向迭代器,允许重复 key;二者都不支持
operator[];按 key 调用 erase,会删除全部匹配 key。
Q6:什么时候用 map,什么时候用 unordered_map?
需要 key 有序、区间查找、要求最坏时间复杂度稳定\(O(logN)\) → map; 大数据量,只做单点增删查,不需要有序,追求平均性能 → unordered_map; 小数据量两者性能差距不大。
Q7:自定义结构体作为 unordered_map 的 key,需要实现什么?
- 自定义哈希仿函数,返回
size_t哈希值; - 重载
operator==,实现 key 相等语义;
如果作为 map 的 key,仅需要重载
operator<。
十、开发易错坑清单
- ❌不要依赖 unordered 容器的遍历顺序,环境、rehash 都会改变输出顺序;
- ❌rehash 之后继续使用旧迭代器,解引用直接未定义行为;
- ❌自定义结构体当 key 忘记提供哈希仿函数,直接编译报错;
- ❌
unordered_multimap使用operator[],编译报错,不支持; - ✅已知插入数据规模,优先调用
reserve(n),减少 rehash 带来性能抖动。
