优先用 std::hash 并为自定义类型特化,避免手写常见错误;开链法更易调试,线性探测需谨慎处理删除与负载因子;rehash 前 reserve 预留空间,遍历时防止动态扩容导致迭代器失效。

哈希函数选 std::hash 还是手写?
直接用 std::hash 是最省事的,但只对内置类型和标准容器(如 std::string、std::pair<int,int>)有特化;自定义结构体必须显式提供特化,否则编译报错 error: use of deleted function。
手写哈希函数常见坑:用 % 取模前没处理负数,导致数组越界;或用 rand() 混入哈希值,破坏确定性——哈希表要求相同输入永远产出相同桶索引。
- 对
struct Point { int x, y; };,推荐这样特化:namespace std { template<> struct hash<Point> { size_t operator()(const Point& p) const { return hash<int>{}(p.x) ^ (hash<int>{}(p.y) << 16); } }; - 避免用乘法溢出做扰动,C++20 起
std::hash对整数默认用 FNV-1a,足够快且分布好,没必要重造轮子
开链法 vs 线性探测:内存和速度怎么取舍?
开链法(每个桶存 std::vector 或 std::forward_list)实现简单、无扩容压力,但指针跳转多、缓存不友好;线性探测查得快(连续内存),但删除操作必须留墓碑(DELETED 标记),否则会断链,而且负载因子超过 0.7 后性能陡降。
实际项目里,除非明确追求极致吞吐(如游戏引擎高频查找),否则优先开链法——调试时能直接打印每个桶内容,排查冲突一目了然。
立即学习“C++免费学习笔记(深入)”;
- 用
std::forward_list比std::vector更省内存,插入头结点 O(1),但不支持随机访问(本来也不需要) - 线性探测若用
std::vector<std::optional<T>>存值,std::optional构造/析构开销不可忽略,小对象建议用enum class State { EMPTY, OCCUPIED, DELETED };+ 原生数组
rehash 触发时机和拷贝代价怎么控?
负载因子 = 元素数 / 桶数。当它超过阈值(比如 0.75)就该 rehash——但别在每次 insert 后都检查,而是记录当前元素数,仅在 insert 成功后判断是否超限,避免冗余计算。
重新散列本质是申请新桶数组、遍历旧表、对每个元素调用哈希函数再插入新表。如果元素类型含深拷贝(如含 std::string 的结构体),rehash 会明显卡顿。解决办法:预留容量(reserve(n))或用移动语义转移元素。
-
reserve(1024)会让底层桶数组至少为 1024(通常向上取最近质数),避免初期频繁扩容 - 插入时用
emplace而非insert,绕过临时对象构造;rehash内部迁移尽量用std::move而非拷贝 - 别在循环里反复
insert后立刻size()判断——size()是 O(1),但判断逻辑本身增加分支预测失败概率
迭代器失效问题比你想的更隐蔽
开链法下,单次 insert 不会让已有迭代器失效(因为只改链表指针);但 rehash 会令所有迭代器、引用、指针全部失效——这点和 std::unordered_map 一致,但很多人写完自己的表就忘了文档里这句。
更麻烦的是:如果在遍历哈希表时触发了 rehash(比如边查边插),迭代器可能指向已释放内存,行为未定义。没有运行时检查,ASan 也未必能捕获这种跨桶跳转。
- 安全做法:遍历前先
reserve足够空间,确保过程中不rehash - 若必须边遍历边修改,改用
std::vector<std::pair<Key,Value>>+ 手动查找,牺牲 O(1) 换确定性 - 自测时加一个
assert(!rehashing_in_progress_)在迭代器解引用前,开发期快速暴露问题
哈希表看似简单,真正难的是边界状态下的行为一致性——比如两个不同键哈希值相同、删除后又插入同键、多线程读写没加锁却依赖“看起来没坏”。这些地方不打日志、不写单元测试,上线后只会以偶发崩溃或数据错乱出现。










