插入前必须检查桶是否为空,否则直接赋值会丢失原有链表;正确做法是头插法(new_node->next = table[hash]; table[hash] = new_node)或尾插法(先判空再遍历);哈希取模需处理负数,推荐转无符号后取模;重复键需遍历比对键而非仅哈希值;扩容应在插入前完成以避免异常导致状态不一致。

插入前必须检查桶是否为空
链式哈希表每个桶(bucket)本质是一个指针,指向该槽位的链表头节点。如果直接 table[hash] = new_node 而不判断原指针是否为 nullptr,会丢失已有链表——旧链表头被覆盖,整条链断裂。
正确做法是:先保存原链表头,再让新节点的 next 指向它,最后把桶指向新节点(头插法)或遍历到尾部插入(尾插法)。头插更简洁、常数时间,但不保序;尾插需遍历,适合需要稳定插入顺序的场景。
- 头插示例:
new_node->next = table[hash]; table[hash] = new_node; - 若用尾插,必须先判空:
if (table[hash] == nullptr) { table[hash] = new_node; },否则遍历到tail->next == nullptr再赋值 - 别忘了分配节点内存:
new Node{key, value, nullptr},漏掉nullptr初始化next会导致未定义行为
哈希函数和取模必须处理负数索引
C++ 中负数对正数取模结果仍为负(如 -5 % 8 == -5),直接作为数组下标会越界。哪怕键是 size_t,哈希计算过程(比如乘加扰动)也可能溢出产生负值。
安全写法是先转成无符号类型再取模,或用二次修正:
立即学习“C++免费学习笔记(深入)”;
- 推荐:
size_t hash = std::hash<key>{}(key) & (capacity - 1)</key>(仅当capacity是 2 的幂时可用,且要求哈希值足够分散) - 通用写法:
size_t hash = std::hash<key>{}(key) % capacity;</key>后接if (hash >= capacity) hash = 0;不够健壮;应改用hash = (std::hash<key>{}(key) % static_cast<long long>(capacity) + capacity) % capacity;</long></key> - 更稳妥:用
static_cast<size_t></size_t>强转哈希结果为无符号,再取模:size_t h = std::hash<key>{}(key); table[h % capacity]...</key>—— 因为size_t模运算天然非负
重复键插入需决定是否覆盖
标准 std::unordered_map::insert 遇到重复键直接返回 pair<iterator bool></iterator> 表示失败;而 operator[] 会覆盖。链式哈希表实现里,这个策略必须显式编码。
典型做法是插入前遍历链表比对键:
- 用
==或自定义比较器判断相等(注意:哈希相等 ≠ 键相等,必须逐个比对) - 若找到匹配节点,按需更新值(覆盖)或跳过(拒绝插入)
- 若遍历完无匹配,才执行插入逻辑
- 别在循环里只比对
hash值就认为键相同——这是常见错误,会导致误覆盖或漏查
扩容时机与重哈希不能在插入中途触发
插入后检查负载因子(size / capacity)是否超阈值(如 0.75),若超则扩容。但扩容本身要重建整个表:分配新桶数组、遍历所有旧节点、重新计算哈希并插入新表。
关键约束是:重哈希期间不能有其他线程或递归调用干扰,且旧表节点指针在迁移完成后才能释放。
- 扩容函数必须接收旧表和旧容量,不能依赖成员变量“当前状态”——否则在多线程或异常路径下易出错
- 新容量建议翻倍(如
new_capacity = capacity * 2),避免频繁扩容;但首次分配可设为 8 或 16 - 重哈希循环中,每个节点的
next指针在插入新表后仍有效,直到整个旧链表迁移完毕才可 delete——否则中间断链
最易被忽略的是:插入函数内部若调用了可能抛异常的操作(如 new 失败),而扩容又在插入末尾做,就会导致部分节点已插入、部分未处理、异常后状态不一致。稳妥做法是把扩容逻辑拆出来,在插入前预判并完成,再执行单次插入。


















