哈希表底层用vector+list实现拉链法,关键在于哈希函数设计、质数桶数、自定义类型需特化std::hash和重载==,扩容时需rehash且迭代器失效。
哈希表底层用 vector + list 实现拉链法
拉链法本质就是数组里每个槽位存一个链表,冲突的键值对全塞进同一个链表里。C++ 里最直接的做法是
存
,或者更灵活点用
存
(单向、轻量)。别一上来就手写链表——
已经够用,除非你明确要控制内存布局或迭代器稳定性。
关键不是“怎么造轮子”,而是“怎么让哈希函数和桶数量配合好”。比如桶数取质数(如 101、1009),能显著降低同余冲突;用
前必须确保
是正数,否则取模行为未定义。
桶数组大小建议初始化为质数,避免因 2 的幂次导致低位哈希位被忽略
对内置类型可靠,但自定义结构体必须显式特化,否则编译报错
插入时先算哈希再遍历链表查重,不查重会允许重复 key(除非业务允许)
自定义类型做 key 必须提供 hash 和 == 运算符
这是最容易卡住的地方:哪怕你只写了
,往
里塞,也会编译失败。因为
没定义,
也没定义(即使成员都可比,C++17 也不自动合成)。
解决方法只有两个:要么全写在类里(C++20
可简化比较,但 hash 还得自己写),要么在外面特化。推荐后者,更解耦:
立即学习
“
C++免费学习笔记(深入)
”;
C知道
CSDN推出的一款AI技术问答工具
下载
异或(
)不是好哈希组合方式,容易抵消;更稳妥用
特化
必须在
命名空间,且不能在头文件里多次定义(否则 ODR 违规)
如果 key 类型带指针或浮点字段,
和
必须语义一致,否则查找永远失败
resize 时机和 rehash 的开销很实在
拉链法不怕冲突多,怕的是某条链太长。当平均链长超过某个阈值(比如 8),查找退化成 O(n),就得扩容。C++ 标准库的
默认负载因子上限是 1.0,达到就自动
;但你自己实现时,这个逻辑得手动写——而且不能只扩 vector 大小,还得把所有旧节点重新哈希、插入新桶。
每次
都是 O(N) 时间,且会 invalidate 所有迭代器(包括
返回的)
扩容倍数建议用 2x 或 1.5x,别用 1.1x——小步慢跑反而触发频繁 rehash
如果已知元素总数,构造时用
预分配桶数,能省掉前几次扩容
迭代器失效只发生在 rehash 时,不是每次 insert
很多人误以为“拉链法 insert 就会让其他迭代器失效”,其实不会。只要没触发
,往某条链末尾 push_back,不影响其他链上的节点地址,原有迭代器照常可用。这点和
完全不同。
但要注意:如果你在遍历 map 的同时插入新元素,且恰好触发了 rehash,那当前迭代器立刻变成悬垂指针——这种“边遍历边改”逻辑本身就有风险,标准库也不保证安全。
安全做法是:先收集要插入的键值对,等遍历完再批量 insert
返回的
中的 iterator,在本次 insert 不触发 rehash 时有效;若触发,则返回新桶里的位置,旧迭代器全部失效
调试时遇到
且堆栈停在
或
,八成是迭代器用了已失效的
实际写的时候,真正麻烦的从来不是链表怎么连,而是哈希函数怎么写不撞车、扩容边界怎么控不抖动、自定义类型怎么塞进去不崩。这些地方错一点,表现出来就是查不到值、反复插入、甚至 core dump。
std::vectorstd::liststd::vectorstd::forward_liststd::listsize_t hash = key % bucket_countbucket_countstd::hasherror: call to implicitly-deleted default constructorstruct Person { std::string name; int id; };std::unordered_mapstd::hashoperator==operator<=>
namespace std {
template<> struct hash {
size_t operator()(const Person& p) const {
return hash{}(p.name) ^ (hash{}(p.id) << 1);
}
};
}
bool operator==(const Person& a, const Person& b) {
return a.name == b.name && a.id == b.id;
}
^hash{}(p.name) * 31 + p.id std::hashstd==hashstd::unordered_maprehashrehashbegin()reserve(n)rehashstd::vectorstd::unordered_map::insertpairSegmentation faultoperator*->