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

C++ STL 哈希表原理与模拟实现

哈希表是 C++ STL 中 unordered_map 等容器的底层核心,通过哈希函数实现 O(1) 级别的查找效率。深入解析哈希函数设计原则、负载因子对性能的影响及冲突处理机制。重点对比了开放定址法(线性探测、二次探测、双重散列)与链地址法的优劣,并结合 C++ 模板代码展示了仿函数特化、质数扩容策略及节点状态管理的具体实现。内容涵盖理论推导与工程实践,帮助开发者理解并手写高性能哈希表。

疯疯癫癫发布于 2026/3/21更新于 2026/7/2330 浏览
C++ STL 哈希表原理与模拟实现

哈希表概述

哈希表(Hash Table)是 C++ STL 中 unordered_map 和 unordered_set 的底层核心数据结构。它通过哈希函数将键映射到固定长度的输出值,从而实现接近 O(1) 时间复杂度的查找、插入和删除操作。理解哈希表的内部机制,对于掌握高性能容器及编写自定义数据结构至关重要。

核心概念

哈希函数

哈希函数的作用是将任意长度的输入数据(键)映射为固定长度的哈希值。一个优秀的哈希函数应具备以下特点:

  • 确定性:相同的输入始终产生相同的输出。
  • 均匀分布:不同的键应尽可能均匀地分布在哈希表中,减少冲突。
  • 高效性:计算过程应快速,避免成为性能瓶颈。

常见的哈希函数构造方法包括:

直接定址法

直接使用关键字本身或其线性函数作为地址。公式为 H(key) = key 或 H(key) = a × key + b。适用于关键字范围较小且连续的场景,如存储年龄或月份。

除法散列法

最常用的方法,利用取余运算将关键字映射到 [0, m-1] 区间。公式为 H(key) = key % m。

关键点:模数 m 的选择直接影响性能。通常建议选取质数,以避免关键字分布不均导致的冲突。例如,若 m=10,偶数关键字都会映射到偶数位置;若 m=11,分布则更分散。

乘法散列法

先将关键字乘以一个常数 A (0 < A < 1),取小数部分后乘以表长 m 并向下取整。公式为 h(key) = floor(m * (key * A mod 1))。该方法对 m 的取值限制较少,通常使用黄金分割比作为 A。

全域散列法

从一组哈希函数族中随机选择一个函数。这能确保即使面对恶意构造的最坏情况输入,也能在平均意义上保持良好性能。

负载因子

负载因子(Load Factor)定义为 λ = n / m,其中 n 是元素数量,m 是桶的数量。它是衡量哈希表填充程度的指标。

  • λ 过小:空间浪费严重,但冲突少。
  • λ 过大:内存利用率高,但冲突概率激增,性能退化至 O(n)。

当负载因子超过阈值(通常为 0.7 或 1.0)时,需要触发扩容(Resize)。扩容流程包括:创建更大的新数组、重新计算所有元素的哈希值并迁移到新表、释放旧内存。

哈希冲突

由于输入空间远大于输出空间,不同键映射到同一位置的情况无法避免。处理冲突主要有两种策略:开放定址法和链地址法。

冲突处理策略

开放定址法

所有元素都存储在哈希表数组本身中。发生冲突时,按探测序列寻找下一个空闲位置。

线性探测

探测公式:h_i(key) = (h(key) + i) % m。

优点是实现简单,但容易产生'聚集'现象,即连续多个位置被占用,导致后续插入效率降低。

二次探测

探测公式:h_i(key) = (h(key) + c1 * i + c2 * i^2) % m。

相比线性探测,二次探测能减少聚集,但不能保证探测到所有位置,需满足特定条件(如表长为 4k+1 形式的质数)。

双重散列

探测公式:h_i(key) = (h1(key) + i * h2(key)) % m。

使用两个哈希函数,第二个函数提供偏移量。只要 h2(key) 与表长互质,就能覆盖整个表。这是开放定址法中性能较好的方案。

链地址法

哈希表底层是一个数组,每个元素指向一个链表。冲突的元素挂在同一个桶的链表上。

  • 优点:逻辑简单,支持动态扩容,无聚集问题。
  • 缺点:存在额外指针开销,链表过长时查找退化为 O(n)。

STL 中的 unordered_map 在 C++11 之后针对某些情况(如红黑树转换)做了优化,但基础结构仍基于链地址法思想。

关键实现细节

非整数 Key 的处理

哈希函数通常要求输入为整数。对于字符串或其他类型,需要定义仿函数(Functor)将其转换为 size_t 类型的哈希值。例如,BKDR 算法常用于字符串哈希,通过累加字符 ASCII 值并乘以质数(如 131)来减少冲突。

template<class K>
struct HashFunc {
    size_t operator()(const K& key) {
        return (size_t)key;
    }
};

template<>
struct HashFunc<std::string> {
    size_t operator()(const std::string& s) {
        size_t hash = 0;
        for (auto it : s) {
            hash += it;
            hash *= 131;
        }
        return hash;
    }
};

开放定址法的删除操作

在开放定址法中,不能简单地清空位置,否则会导致后续查找中断。因此需要引入状态标记:EXIST(存在)、EMPTY(空)、DELETE(已删除)。查找时遇到 DELETE 继续探测,直到遇到 EMPTY 停止;插入时可将 DELETE 视为可用位置。

扩容与质数表

为了保证哈希分布均匀,哈希表容量通常选择质数。SGI 版本的实现维护了一个预定义的质数表,扩容时通过二分查找找到大于当前容量的最小质数。

inline unsigned long _stl_next_prime(unsigned long n) {
    static const int __stl_num_primes = 28;
    static const unsigned long _stl_prime_list[__stl_num_primes] = {
        53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
        49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
        6291469, 12582917, 25165843, 50331653, 100663319, 201326611,
        402653189, 805306457, 1610612741, 3221225473, 4294967291
    };
    const unsigned long* first = _stl_prime_list;
    const unsigned long* last = _stl_prime_list + __stl_num_primes;
    const unsigned long* pos = std::lower_bound(first, last, n);
    return pos == last ? *(last - 1) : *pos;
}

代码实战

开放定址法实现

enum State { EXIST, EMPTY, DELETE };

template<class K, class V>
struct HashData {
    std::pair<K, V> _kv;
    State _state = EMPTY;
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTableOpenAddress {
private:
    std::vector<HashData<K, V>> _tables;
    size_t _n;
public:
    HashTableOpenAddress() : _tables(_stl_next_prime(0)), _n(0) {}

    bool Insert(const std::pair<K, V>& kv) {
        if (Find(kv.first)) return false;
        if (_n * 10 / _tables.size() >= 7) {
            // 扩容逻辑
            HashTableOpenAddress newHt;
            newHt._tables.resize(_stl_next_prime(_tables.size() + 1));
            for (auto& htData : _tables) {
                if (htData._state == EXIST)
                    newHt.Insert(htData._kv);
            }
            _tables.swap(newHt._tables);
        }
        Hash hash;
        size_t hash_0 = hash(kv.first) % _tables.size();
        size_t hash_i = hash_0;
        size_t i = 1;
        while (_tables[hash_i]._state == EXIST) {
            hash_i = (hash_0 + i) % _tables.size();
            ++i;
        }
        _tables[hash_i]._kv = kv;
        _tables[hash_i]._state = EXIST;
        ++_n;
        return true;
    }

    HashData<K, V>* Find(const K& key) {
        Hash hash;
        size_t hash_0 = hash(key) % _tables.size();
        size_t hash_i = hash_0;
        size_t i = 1;
        while (_tables[hash_i]._state != EMPTY) {
            if (_tables[hash_i]._state == EXIST && _tables[hash_i]._kv.first == key)
                return &_tables[hash_i];
            hash_i = (hash_0 + i) % _tables.size();
            ++i;
        }
        return nullptr;
    }

    bool Erase(const K& key) {
        auto ret = Find(key);
        if (ret) {
            ret->_state = DELETE;
            --_n;
            return true;
        }
        return false;
    }
};

链地址法实现

template<class K, class V>
struct HashNode {
    std::pair<K, V> _kv;
    HashNode* _next;
    HashNode(const std::pair<K, V>& kv) : _kv(kv), _next(nullptr) {}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTableChaining {
private:
    std::vector<HashNode<K, V>*> _tables;
    size_t _n;
public:
    HashTableChaining() : _tables(_stl_next_prime(0)), _n(0) {}

    ~HashTableChaining() {
        for (size_t i = 0; i < _tables.size(); ++i) {
            HashNode<K, V>* current = _tables[i];
            while (current) {
                HashNode<K, V>* next = current->_next;
                delete current;
                current = next;
            }
            _tables[i] = nullptr;
        }
    }

    bool Insert(const std::pair<K, V>& kv) {
        if (Find(kv.first)) return false;
        if (_n == _tables.size()) {
            // 扩容逻辑
            std::vector<HashNode<K, V>*> newVector(_tables.size() * 2);
            for (size_t i = 0; i < _tables.size(); ++i) {
                HashNode<K, V>* current = _tables[i];
                while (current) {
                    HashNode<K, V>* next = current->_next;
                    size_t hash_i = Hash()(current->_kv.first) % newVector.size();
                    current->_next = newVector[hash_i];
                    newVector[hash_i] = current;
                    current = next;
                }
            }
            _tables.swap(newVector);
        }
        Hash hashFunc;
        size_t hash_i = hashFunc(kv.first) % _tables.size();
        HashNode<K, V>* newNode = new HashNode(kv);
        newNode->_next = _tables[hash_i];
        _tables[hash_i] = newNode;
        ++_n;
        return true;
    }

    HashNode<K, V>* Find(const K& key) {
        Hash hashFunc;
        size_t hash_i = hashFunc(key) % _tables.size();
        HashNode<K, V>* current = _tables[hash_i];
        while (current) {
            if (current->_kv.first == key)
                return current;
            current = current->_next;
        }
        return nullptr;
    }

    bool Erase(const K& key) {
        Hash hashFunc;
        size_t hash_i = hashFunc(key) % _tables.size();
        HashNode<K, V>* curr = _tables[hash_i];
        HashNode<K, V>* prev = nullptr;
        while (curr) {
            if (curr->_kv.first == key) {
                if (!prev) _tables[hash_i] = curr->_next;
                else prev->_next = curr->_next;
                delete curr;
                --_n;
                return true;
            }
            prev = curr;
            curr = curr->_next;
        }
        return false;
    }
};

测试验证

在实际开发中,建议编写单元测试覆盖插入、查找、删除及扩容场景。特别是针对负数 Key、字符串 Key 以及自定义结构体 Key 的哈希行为进行测试,确保仿函数正确实现了转换逻辑。

通过上述实现,我们可以清晰地看到哈希表如何在内存中组织数据,以及在不同冲突策略下如何维持性能。掌握这些底层原理,有助于在面对复杂业务需求时做出更合理的数据结构选型。

目录

  1. 哈希表概述
  2. 核心概念
  3. 哈希函数
  4. 直接定址法
  5. 除法散列法
  6. 乘法散列法
  7. 全域散列法
  8. 负载因子
  9. 哈希冲突
  10. 冲突处理策略
  11. 开放定址法
  12. 线性探测
  13. 二次探测
  14. 双重散列
  15. 链地址法
  16. 关键实现细节
  17. 非整数 Key 的处理
  18. 开放定址法的删除操作
  19. 扩容与质数表
  20. 代码实战
  21. 开放定址法实现
  22. 链地址法实现
  23. 测试验证
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Vue 3 开发实战:10 个提升效率的核心技巧
  • Stable Diffusion XL 文生图模型结构与部署解析
  • LLaMA 大模型本地部署与调用指南
  • 三维人体姿态估计前沿算法与论文案例
  • OpenClaw 在 macOS 和 Windows 上的安装与配置
  • CleanShot X Mac 截图录屏及 GIF 录制完整指南
  • RTX 4090 本地部署腾讯混元与阿里通义万相视频模型
  • Flutter WalletConnect 鸿蒙化适配:Web3 钱包连接与 DApp 授权
  • Python 环境管理对比:uv 与 conda 的核心差异与选型指南
  • 基于无人机遥感的植被覆盖度测量实践
  • AI 时代如何脱颖而出:商业认知与行动指南
  • 【异常】飞书OpenClaw机器人 HTTP 401: Invalid Authentication 报错排查与解决方案
  • Dart 设计模式:单例模式
  • 10 款开源 AI 视频工具:免配置开箱即用
  • C++ 线程库与多线程编程核心机制解析
  • Python heapq 入门与实战:最小堆、Top-K 和优先队列
  • C++入门基础:逐步剖析核心语法
  • 大模型浪潮:是泡沫还是技术革命?
  • Claude Code 安装配置与实战教程
  • 降低论文 AIGC 检测率的实战经验与工具分析

相关免费在线工具

  • 加密/解密文本

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