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

C++ 哈希表原理与 STL 底层实现解析

哈希表通过哈希函数将键映射到存储位置,核心在于处理哈希冲突。常见冲突解决策略包括开放定址法与哈希桶。深入解析哈希值生成、多种哈希函数(如除留余数法)及 STL 中 unordered_map/set 的底层模板复用机制,涵盖自定义类型键的处理与 ExtractKey 仿函数的作用。

不知所云发布于 2026/3/25更新于 2026/8/1131 浏览
C++ 哈希表原理与 STL 底层实现解析

哈希表基础

理解哈希表,首先要区分'哈希'与'哈希表'。哈希是一种映射算法思想,而哈希表则是基于这种思想构建的数据结构。

所有哈希表的算法流程基本一致:获取一个 key,通过哈希函数计算 hash(key),得到存储位置存放 Value;或者从该位置取出查找的 Value。

理解哈希表

深入理解哈希表通常分为三层:

  1. 哈希值:这是最基础的整数映射。
  2. 哈希函数:将哈希值映射到具体空间位置。
  3. 哈希冲突:解决不同键映射到同一位置的问题。

STL 中的哈希表底层逻辑也是先获取哈希值(整形),再通过哈希函数处理。无论 Key 是 string 还是 vector,最终都需要转换为哈希值才能存入。

因此理解链条应为:哈希值 → 哈希函数 → 哈希冲突。

最基本的哈希表逻辑上可视为数组。数组有大小限制,当数据过多时必然发生哈希冲突。哈希是功能,冲突是可能结果,两者存在因果关系。

哈希值(整形)

为什么需要哈希值?因为大多数哈希函数(如除留余数法、平方取中法等)都是对整形进行运算。在 STL 中,若要对自定义类型进行哈希,必须提供该类型向哈希值的转换方法,即'转换策略'。

有了转换策略,自定义类型即可转为哈希值,再交给底层哈希函数处理。转化哈希值通常定义为仿函数,作为 unordered_map 的第三个模板参数传入。

设计转换策略时需注意速度快、离散度高。常见策略包括:

  • BKDR 哈希:适用于字符串。
  • 异或组合:适用于简单容器。
  • hash_combine:适用于复杂结构体。

BKDR 哈希

优点是实现简单、计算快、离散度高。选取种子 seed,遍历字符串每个字符进行处理。

size_t BKDRHash(const string &str) {
    size_t seed = 131; // 常用种子:31, 131, 1313...
    size_t hash = 1;
    for (auto e : str) {
        hash *= seed;
        hash += e;
    }
    return hash & 0x7FFFFFFF; // 确保返回正数
}

异或组合

实现简单,但冲突率相对较高。

struct PointHash {
    size_t operator()(const vector<int> &vec) {
         hash = ;
         ( e : vec) {
            hash ^= (e << );
        }
         hash;
    }
};
int
0
for
auto
1
return

hash_combine

冲突率低,推荐使用。常用于多成员结构体。

template <typename T>
void hash_combine(std::size_t& seed, const T& val) {
    seed ^= std::hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

struct Person {
    std::string name;
    int age;
    bool operator==(const Person& p) const {
        return name == p.name && age == p.age;
    }
};

struct PersonHash {
    std::size_t operator()(const Person& p) const {
        std::size_t seed = 0;
        hash_combine(seed, p.name);
        hash_combine(seed, p.age);
        return seed;
    }
};

哈希函数

直接定址法

这是一种单调性函数,每个哈希值对应唯一的映射位置,理论上无冲突。但缺点是如果数据分散,会浪费大量空间。仅适用于数据集中的场景。

除留余数法

最常用的方法。对哈希表大小 m 取模,得到一个不大于 m 且接近 m 的质数 p 作为模数。例如表大小为 10,可选模数 7。键值 12, 13, 1 分别模 7 得到映射区域。

平方取中法

取关键字平方后的中间几位作为地址。例如键值 1234,平方为 1522756,取中间三位 227 作为地址。该方法能较好地打散连续键值,避免除留余数法因取值不当加剧冲突的问题。

基数转换法

将十进制数转换为其他进制(如十六进制),将结果视为十进制后再取模。若含字母则转为 ASCII 码。例如 255 转十六进制 FF,视为十进制 7070,再对表大小取模。

哈希冲突

上述方法(除直接定址法外)都可能产生冲突,即两个不同键值映射到同一地址。负载因子(已插入数据个数 / 表大小)越大,冲突概率越高,效率越低。

缓解冲突主要有两种方案:开放定址法、哈希桶。

开放定址法

当冲突发生时,键值对向后偏移寻找空位。探测规律包括线性探测(一格一格往后)和二次探测(按平方数跳跃)。

负载因子通常需小于 0.7,否则探测次数过多,时间复杂度可能逼近 O(n)。

以下是开放定址法的模拟实现,注意删除操作需标记为 DELETE 状态,以便查找时继续向后探测。

template <class K, class V>
class HashData {
public:
    enum State { EMPTY, EXIST, DELETE };
    pair<K, V> _kv;
    State _state;
};

template <class K, class V>
class HashTable {
public:
    bool insert(const pair<K, V> &kv) {
        if (_tables.size() == 0 || (_n * 10) / _tables.size() >= 7) {
            UpMemory();
        }
        if (Find(kv.first)) return false;

        int hashi = kv.first % _tables.size();
        while (_tables[hashi]._state == HashData<K, V>::State::EXIST) {
            hashi++;
            hashi %= _tables.size();
        }
        _tables[hashi]._kv = kv;
        _tables[hashi]._state = HashData<K, V>::State::EXIST;
        _n++;
        return true;
    }

    HashData<K, V> *Find(const K &key) {
        int hashi = key % _tables.size();
        int tmp = hashi;
        while (_tables[hashi]._state != HashData<K, V>::EMPTY) {
            if (_tables[hashi]._state == HashData<K, V>::EXIST && 
                _tables[hashi]._kv.first == key) {
                return &_tables[hashi];
            }
            hashi++;
            hashi %= _tables.size();
            if (hashi == tmp) return nullptr;
        }
        return nullptr;
    }

    bool Erase(const K &key) {
        HashData<K, V> *pdata = Find(key);
        if (pdata == nullptr) return false;
        pdata->_state = HashData<K, V>::DELETE;
        --_n;
        return true;
    }

private:
    void UpMemory() {
        int newsize = (_tables.size() == 0) ? 5 : 2 * _tables.size();
        HashTable<K, V> NewHT;
        NewHT._tables.resize(newsize);
        for (int i = 0; i < _tables.size(); i++) {
            if (_tables[i]._state == HashData<K, V>::EXIST) {
                NewHT.insert(_tables[i]._kv);
            }
        }
        _tables.swap(NewHT._tables);
    }

private:
    vector<HashData<K, V>> _tables;
    size_t _n = 0;
};

哈希桶

哈希桶本质上是一个挂有链表的数组。数组元素称为桶,冲突时将数据节点链入链表。相比开放定址法,哈希桶在负载因子为 1 时性能更优,查找通常为常数级时间复杂度。 C++ 标准库中的 unordered_map 和 unordered_set 底层均采用哈希桶。

unordered_map 和 unordered_set 如何共用哈希桶模板类

为了复用底层结构,设计者巧妙利用了模板参数:

  • unordered_set 的第二个模板参数传 Key。
  • unordered_map 的第二个模板参数传 pair<Key, T>。

这样,查找和删除只需关注 Key,而插入时根据实际类型自动适配。外层调用时,unordered_set 传入 Key,unordered_map 传入 pair,底层 Insert 接收 Value 类型数据。

STL 哈希桶中 Insert 如何得到键值

查找和删除依赖 Key,但插入时 Key 的提取方式不同。为此,哈希桶提供了第四个模板参数 ExtractKey(仿函数),用于从 Value 中提取 Key。

  • unordered_set 传入的 KeyOfValue 直接返回 Key。
  • unordered_map 传入的 KeyOfValue 返回 pair 的 first。
// 哈希桶定义示例
template<class Key, class Value, class Alloc, class ExtractKey, class Hash, class __Pred, .....>
class HashBucket {
    // ... Find, Erase, Insert 等接口
};

// unordered_set 使用
template<class Key, class Hash, class Pred.....>
class unordered_set {
    struct KeyOfValue {
        const K &operator()(const K &key) { return key; }
    };
    typedef HashBucket<Key, Key, Alloc, KeyOfValue, Hash, Pred> HT;
    // ...
};

// unordered_map 使用
template<class Key, class T, class Hash, class Pred.....>
class unordered_map {
    struct KeyOfValue {
        const K& operator()(const pair<K, V> &_kv) { return _kv.first; }
    };
    typedef HashBucket<Key, pair<Key, T>, Alloc, KeyOfValue, Hash, Pred> HT;
    // ...
};
键为自定义类型的处理

对于任意自定义类型作为键,前提是实现向整形转化的仿函数(Hash)以及相等比较仿函数(Pred)。

哈希桶底层插入时,先用 Hash 仿函数将自定义类型转为哈希值,再取模定位桶。由于哈希值有限,不同对象可能哈希值相同,因此查找时必须用原生比较(Pred)确认是否真正相等。

以上涵盖了哈希表的核心原理及 STL 底层实现细节。

目录

  1. 哈希表基础
  2. 理解哈希表
  3. 哈希值(整形)
  4. BKDR 哈希
  5. 异或组合
  6. hash_combine
  7. 哈希函数
  8. 直接定址法
  9. 除留余数法
  10. 平方取中法
  11. 基数转换法
  12. 哈希冲突
  13. 开放定址法
  14. 哈希桶
  15. unorderedmap 和 unorderedset 如何共用哈希桶模板类
  16. STL 哈希桶中 Insert 如何得到键值
  17. 键为自定义类型的处理
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 用 Prompt 进行数据清洗:缺失值与异常值自动标注
  • 智能家居插件管理工具技术指南:本地化网络优化方案
  • PyCharm 安装与基础配置指南
  • 若依 (RuoYi) 低代码框架深度剖析
  • 前端表格性能优化:虚拟滚动实现百万级数据流畅渲染
  • 使用 Java 客户端调用 Elasticsearch API 实战指南
  • 前端日期格式化:Intl.DateTimeFormat 详解与替代方案
  • BFS 实现拓扑排序:原理与 LeetCode 实战
  • 无人机视角山区泥石流与滑坡图像识别数据集
  • Trae 集成 Vizro:低代码构建专业数据可视化仪表板
  • 基于 SpringBoot 与 Vue3 的桂林旅游导游平台系统设计
  • STEP3-VL-10B WebUI 启用历史会话与上下文记忆教程
  • synchronized 底层原理详解:字节码、对象头与锁升级
  • GitHub 学生开发者包认证操作指南
  • Git 多 IDE 项目共用远程仓库及子模块问题解决方案
  • 前端状态管理方案对比:Redux、Zustand 与 Pinia
  • 华为 OD 机试:小朋友分组最少调整次数 (Java)
  • 人工智能基础概念解析:从图灵测试到深度学习
  • XGBoost Python 机器学习实战教程与参数详解
  • Ubuntu 系统下 Node.js 环境配置与常见问题排查
  • 相关免费在线工具

    • 加密/解密文本

      使用加密算法(如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