跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 哈希表原理与 STL 容器实现详解

综述由AI生成哈希表作为 unordered_map 和 unordered_set 的底层结构,核心在于通过哈希函数建立键值映射。深入剖析了哈希冲突的解决策略,对比了闭散列(线性探测)与拉链法的优劣,重点阐述了开放定址法中“删除标记”的设计必要性及扩容时的重哈希逻辑。同时,针对非整型 Key 提供了仿函数特化方案,并详细拆解了迭代器的封装过程,包括如何支持 const 迭代器及类型转换,最终完成了 C++ 标准库风格的哈希容器实现。

未来可期发布于 2026/3/15更新于 2026/6/215 浏览
C++ 哈希表原理与 STL 容器实现详解

关联式容器初探

首先回顾一下 unordered_map 和 unordered_set,它们基于哈希表实现,用法与红黑树版本的 map/set 类似。主要区别在于:迭代器是单向的,且遍历时数据无序。

unordered_map 示例

它支持去重,也有 multi 版本。unordered_map 同样遵循这一特性。

哈希的核心思想

哈希的本质是在存储的值与位置之间建立映射关系。相比暴力查找的低效和二分查找对有序性的依赖,哈希提供了 O(1) 的平均查找复杂度。

例如计数排序中,最小值 15 映射到索引 0,最大值 30 映射到对应位置。这种直接定址法适用于数据分布集中的场景。若数据分散,可采用除留余数法:i = key % 空间大小。

除留余数法

此时会面临哈希冲突问题,即不同 Key 映射到同一位置。解决冲突主要有两种方式:闭散列(开放定址法)和拉链法(哈希桶)。

冲突示意图

闭散列——开放定址法

当发生冲突时,闭散列会在表中寻找下一个可用位置。常见策略包括线性探测和二次探测。

以线性探测为例,若当前位置被占,则向后查找空位。若遍历完未找到,则绕回开头继续查找。

线性探测

删除操作的陷阱

直接删除元素会导致后续查找中断(遇到空位就停止)。因此需引入状态标记:EXIST(存在)、EMPTY(空)、DELETE(已删除)。查找时跳过 DELETE,直到遇到 EMPTY 才停止。

enum STATE { EXIST, EMPTY, DELETE };
template<class K, class V>
struct HashData {
    pair<K, V> _kv;
    STATE _state = EMPTY;
};

负载因子与扩容

哈希表不能无限填充,否则效率下降。通常设定负载因子阈值(如 0.7),超过则扩容。扩容时需重新计算所有元素的哈希位置(Rehash)。

if (_n * 10 / _table.size() >= 7) {
    size_t newSize = _table.size() * 2;
    HashTable<K, V, HashFunc> newHT;
    newHT._table.resize(newSize);
    for (size_t i = 0; i < _table.size(); i++) {
        if (_table[i]._state == EXIST) {
            newHT.Insert(_table[i]._kv);
        }
    }
    _table.swap(newHT._table);
}

Key 类型的泛型处理

默认哈希函数假设 Key 可取模。对于 string 等非整型,需使用仿函数特化。简单的字符串哈希可将字符 ASCII 累加,但易冲突。更优方案是结合乘法因子(如 131)进行混合。

template<> struct DefaultHashFunc<string> {
    size_t operator()(const string& str) {
        size_t hash = 0;
        for (auto ch : str) {
            hash *= 131;
            hash += ch;
        }
        return hash;
    }
};

拉链法——哈希桶

为避免探测带来的性能抖动,拉链法将每个桶设计为链表。冲突时直接在链表头插入新节点。

拉链法

扩容优化

扩容时,直接将旧链表的节点迁移到新表,避免重复构造节点。

// 迁移节点逻辑
for (size_t i = 0; i < _table.size(); i++) {
    Node* cur = _table[i];
    while (cur) {
        Node* next = cur->_next;
        size_t hashi = hf(kot(cur->_data)) % newSize;
        cur->_next = newTable[hashi];
        newTable[hashi] = cur;
        cur = next;
    }
    _table[i] = nullptr;
}

迭代器封装

为了兼容 STL 风格,需要手动实现迭代器。关键在于 operator++ 的处理:当前桶遍历完后,需查找下一个非空桶。

Self& operator++() {
    if (_node->_next) {
        _node = _node->_next;
    } else {
        // 查找下一个非空桶
        size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
        ++hashi;
        while (hashi < _pht->_table.size()) {
            if (_pht->_table[hashi]) {
                _node = _pht->_table[hashi];
                return *this;
            }
            ++hashi;
        }
        _node = nullptr;
    }
    return *this;
}

Const 迭代器支持

需注意 const 对象调用成员函数时权限限制。通过重载构造函数或调整成员变量访问权限,确保 const_iterator 能正确返回只读引用。

总结

通过上述步骤,我们实现了基于哈希表的 unordered_map 和 unordered_set 基础框架。理解其底层机制有助于在开发中更好地利用标准库容器,并在特定场景下定制高性能数据结构。

目录

  1. 关联式容器初探
  2. 哈希的核心思想
  3. 闭散列——开放定址法
  4. 拉链法——哈希桶
  5. 迭代器封装
  6. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • JavaScript Proxy 代理机制与核心方法详解

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online