当前位置: 首页 > news >正文

C++ 手写哈希表(开放定址法 + 链地址法)+ 封装 unordered_map/unordered_set,从原理到工程级实现

前言

哈希表(Hash Table)是计算机世界中平均 O (1) 查找、插入、删除的最强数据结构。C++ 标准库中的unordered_map/unordered_set底层正是基于哈希表实现。但绝大多数同学只会用,不懂原理;只会调库,不会手写。

本文带你从零搭建:

  1. 哈希表核心理论(哈希函数、冲突、负载因子、扩容)
  2. 开放定址法(线性探测、二次探测)完整实现
  3. 链地址法(哈希桶)完整实现(最接近 STL 标准)
  4. 迭代器封装、const 正确性、模板泛型
  5. 基于哈希桶封装unordered_map/unordered_set
  6. 性能对比、踩坑总结、面试高频考点

全文代码均在 VS2022 下测试通过,可直接复制使用。


一、哈希表基础概念(必须吃透)

1.1 什么是哈希表

哈希表通过哈希函数将关键字key映射到数组下标,从而实现直接寻址。映射关系:

下标 = hash(key) % 表大小

理想情况下:

  • 插入:O (1)
  • 查找:O (1)
  • 删除:O (1)

这就是哈希表 “无敌” 的原因。

1.2 哈希冲突(不可避免)

不同 key 可能算出相同下标,称为哈希冲突 / 哈希碰撞。任何哈希函数都无法彻底避免冲突,只能减少冲突。

冲突两大解决方案:

  1. 开放定址法(闭散列):数据存在表内,冲突向后找空位
  2. 链地址法(开散列 / 哈希桶):每个位置挂链表,冲突往链上放

1.3 负载因子(性能命脉)

负载因子α = 已存元素个数 / 哈希表总长度

  • α 越大 → 冲突越多 → 效率越低
  • α 越小 → 冲突越少 → 空间浪费越多

常规控制:

  • 开放定址法:α ≤ 0.7
  • 链地址法:α ≤ 1.0(STL 默认)

1.4 哈希函数设计(均匀散列是关键)

常见哈希函数:

  1. 直接定址法hash(key)=key
  2. 除留余数法hash(key) = key % capacity(最常用)
  3. 平方取中法折叠法随机数法
  4. 字符串哈希(BKDR、FNV、MurmurHash)

二、开放定址法(闭散列)实现

2.1 原理

所有数据存在同一张数组中。冲突时向后寻找空位:

  • 线性探测:hash_i = (hash0 + i) % cap
  • 二次探测:hash_i = (hash0 ± i²) % cap

必须给每个位置加状态标记

  • EMPTY 空
  • EXIST 存在
  • DELETE 已删除(解决删除后查找断链问题)

2.2 结构定义

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

2.3 素数表(扩容用)

哈希表容量必须用质数,能显著降低冲突率。

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 }; for (int i = 0; i < __stl_num_primes; ++i) { if (__stl_prime_list[i] > n) return __stl_prime_list[i]; } return __stl_prime_list[__stl_num_primes - 1]; }

2.4 哈希函数(支持 string)

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

2.5 开放定址法完整实现

namespace open_address { template<class K, class V, class Hash = HashFunc<K>> class HashTable { public: HashTable() { _tables.resize(_stl_next_prime(0)); } bool Insert(const pair<K, V>& kv) { if (Find(kv.first)) return false; if (_n * 10 / _tables.size() >= 7) { HashTable newHT; newHT._tables.resize(_stl_next_prime(_tables.size())); for (size_t i = 0; i < _tables.size(); ++i) { if (_tables[i]._state == EXIST) { newHT.Insert(_tables[i]._kv); } } _tables.swap(newHT._tables); } Hash hash; size_t hash0 = hash(kv.first) % _tables.size(); size_t hashi = hash0; size_t i = 1; while (_tables[hashi]._state == EXIST) { hashi = (hash0 + i) % _tables.size(); ++i; } _tables[hashi]._kv = kv; _tables[hashi]._state = EXIST; ++_n; return true; } HashData<K, V>* Find(const K& key) { Hash hash; size_t hash0 = hash(key) % _tables.size(); size_t hashi = hash0; size_t i = 1; while (_tables[hashi]._state != EMPTY) { if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key) { return &_tables[hashi]; } hashi = (hash0 + i) % _tables.size(); ++i; } return nullptr; } bool Erase(const K& key) { auto ret = Find(key); if (ret == nullptr) return false; ret->_state = DELETE; --_n; return true; } private: vector<HashData<K, V>> _tables; size_t _n = 0; }; }

2.6 优点与缺陷

✅ 优点:连续内存、缓存友好、无指针开销❌ 缺陷:冲突堆积明显、删除复杂、负载因子不能太高


三、链地址法(哈希桶)—— STL 标准实现

3.1 原理(最常用、最稳定)

哈希表 =指针数组每个位置是一条链表(桶)冲突 → 直接插入桶中

负载因子可以到 1.0 才扩容。

3.2 节点结构

namespace hash_bucket { template<class T> struct HashNode { T _data; HashNode<T>* _next; HashNode(const T& data) :_data(data) ,_next(nullptr) {} };

3.3 迭代器设计(核心难点)

3.3.1 为什么要用迭代器

  • 封装内部节点,不暴露实现
  • 统一 STL 风格接口
  • 支持范围 for、STL 算法
  • 支持 const 迭代器

3.3.2 迭代器结构

template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash> struct HTIterator { typedef HashNode<T> Node; typedef HashTable<K, T, KeyOfT, Hash> HT; typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self; Node* _node; const HT* _ht; HTIterator(Node* node, const HT* ht) :_node(node) ,_ht(ht) {} Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } bool operator!=(const Self& s) const { return _node != s._node; } Self& operator++() { if (_node->_next) { _node = _node->_next; } else { KeyOfT kot; Hash hash; size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size(); ++hashi; while (hashi < _ht->_tables.size() && _ht->_tables[hashi] == nullptr) { ++hashi; } if (hashi == _ht->_tables.size()) { _node = nullptr; } else { _node = _ht->_tables[hashi]; } } return *this; } };

3.3.3 为什么迭代器必须存this(哈希表指针)

因为++跨桶遍历,必须知道桶数组的位置。不传 this,迭代器无法实现!

3.4 哈希表完整实现

template<class K, class T, class KeyOfT, class Hash = HashFunc<K>> class HashTable { template<class K1, class T1, class Ref, class Ptr, class KeyOfT1, class Hash1> friend struct HTIterator; typedef HashNode<T> Node; public: typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator; typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator; Iterator Begin() { for (size_t i = 0; i < _tables.size(); ++i) { if (_tables[i]) return Iterator(_tables[i], this); } return End(); } Iterator End() { return Iterator(nullptr, this); } ConstIterator Begin() const { for (size_t i = 0; i < _tables.size(); ++i) { if (_tables[i]) return ConstIterator(_tables[i], this); } return End(); } ConstIterator End() const { return ConstIterator(nullptr, this); } HashTable() :_tables(_stl_next_prime(0)) ,_n(0) {} ~HashTable() { for (size_t i = 0; i < _tables.size(); ++i) { Node* cur = _tables[i]; while (cur) { Node* next = cur->_next; delete cur; cur = next; } _tables[i] = nullptr; } } bool Insert(const T& data) { KeyOfT kot; Hash hash; if (Find(kot(data))) return false; if (_n == _tables.size()) { vector<Node*> newTable(_stl_next_prime(_tables.size())); for (size_t i = 0; i < _tables.size(); ++i) { Node* cur = _tables[i]; while (cur) { Node* next = cur->_next; size_t hashi = hash(kot(cur->_data)) % newTable.size(); cur->_next = newTable[hashi]; newTable[hashi] = cur; cur = next; } _tables[i] = nullptr; } _tables.swap(newTable); } size_t hashi = hash(kot(data)) % _tables.size(); Node* newnode = new Node(data); newnode->_next = _tables[hashi]; _tables[hashi] = newnode; ++_n; return true; } Node* Find(const K& key) { KeyOfT kot; Hash hash; size_t hashi = hash(key) % _tables.size(); Node* cur = _tables[hashi]; while (cur) { if (kot(cur->_data) == key) return cur; cur = cur->_next; } return nullptr; } bool Erase(const K& key) { KeyOfT kot; Hash hash; size_t hashi = hash(key) % _tables.size(); Node* prev = nullptr; Node* cur = _tables[hashi]; while (cur) { if (kot(cur->_data) == key) { if (prev == nullptr) { _tables[hashi] = cur->_next; } else { prev->_next = cur->_next; } delete cur; --_n; return true; } prev = cur; cur = cur->_next; } return false; } private: vector<Node*> _tables; size_t _n = 0; }; }

四、封装 unordered_map(最接近 STL)

4.1 核心设计:KeyOfT 仿函数

用于从pair<K,V>中提取 key。

namespace hiro { template<class K, class V, class Hash = HashFunc<K>> class unordered_map { struct MapKeyOfT { const K& operator()(const pair<K, V>& kv) { return kv.first; } }; public: typedef typename hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT, Hash>::Iterator iterator; typedef typename hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator; iterator begin() { return _ht.Begin(); } iterator end() { return _ht.End(); } const_iterator begin() const { return _ht.Begin(); } const_iterator end() const { return _ht.End(); } pair<iterator, bool> insert(const pair<K, V>& kv) { bool ret = _ht.Insert(kv); return { iterator(_ht.Find(kv.first)), ret }; } private: hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT, Hash> _ht; }; }

4.2 为什么必须加typename

在模板中,编译器无法识别HashTable<...>::Iterator是类型还是变量。typename明确告诉编译器:这是一个类型


五、封装 unordered_set(与 map 同理)

namespace hiro { template<class K, class Hash = HashFunc<K>> class unordered_set { struct SetKeyOfT { const K& operator()(const K& key) { return key; } }; public: typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::Iterator iterator; typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::ConstIterator const_iterator; iterator begin() { return _ht.Begin(); } iterator end() { return _ht.End(); } const_iterator begin() const { return _ht.Begin(); } const_iterator end() const { return _ht.End(); } pair<iterator, bool> insert(const K& key) { bool ret = _ht.Insert(key); return { iterator(_ht.Find(key)), ret }; } private: hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht; }; }

六、测试代码

#include <iostream> #include <string> #include "unordered_map.h" #include "unordered_set.h" using namespace std; using namespace hiro; int main() { // unordered_map 测试 unordered_map<string, string> dict; dict.insert({ "apple", "苹果" }); dict.insert({ "hash", "哈希" }); dict.insert({ "map", "映射" }); for (auto& kv : dict) { cout << kv.first << " : " << kv.second << endl; } // unordered_set 测试 unordered_set<int> s; s.insert(1); s.insert(2); s.insert(3); for (auto x : s) { cout << x << " "; } cout << endl; return 0; }

七、高频面试题

1. 哈希冲突如何解决?

  • 开放定址:线性探测、二次探测
  • 链地址:链表挂桶(STL 方案)
  • 再哈希、公共溢出区

2. 为什么哈希表容量要用质数?

质数能让余数分布更均匀,显著降低冲突率。

3. 负载因子是什么?为什么要控制?

已存元素 / 总容量。太高冲突爆炸,太低浪费空间。

4. unordered_map 与 map 区别?

  • map:红黑树,O (logN),有序
  • unordered_map:哈希表,O (1),无序

5. 迭代器为什么要存哈希表指针?

为了支持++ 跨桶遍历

6. 为什么要实现 const_iterator?

保证 const 对象可以遍历,同时不被修改。


八、踩坑总结

  1. 迭代器 ++ 必须实现跨桶遍历
  2. 友元声明模板参数不能重名(必须用 K1、T1)
  3. string 哈希必须用 BKDR 等高质量哈希
  4. typename 必须加在嵌套类型前
  5. 扩容必须重新哈希,不能简单拷贝
  6. 哈希表容量必须用质数
http://www.jsqmd.com/news/707465/

相关文章:

  • ARM嵌入式C/C++库架构与优化实践
  • 开源光标主题合集:从原理到实战,打造个性化桌面交互体验
  • Xinference-v1.17.1与Latex集成:AI辅助的学术论文写作系统
  • 多模态AI应用开发实战:从开源工具箱到生产部署全解析
  • 冥想第一千八百六十一天(1861)
  • 快速体验Fairseq-Dense-13B-Janeway:科幻奇幻写作AI助手入门教程
  • MCP低代码集成调试成功率从41%→98.6%:基于137个真实产线案例提炼的7阶渐进式验证模型
  • 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC)
  • 【项目实训】——管理员前端页面开发
  • Canvas Quest与3D建模工作流结合:生成贴图与概念设计
  • 世界及中国地震相关数据(2012-2024年)
  • Python单变量函数优化算法与应用实践
  • 虚拟级联技术:运营商网络的带宽优化方案
  • 终极抖音下载指南:免费开源工具让你的视频获取效率飙升300%
  • 关于Navicat Premium 17破解方法
  • cv_unet_image-matting WebUI二次开发指南:从改颜色到加功能的完整教程
  • 机器学习核心原理与实践指南:从数据到智能应用
  • 智能体“自我纠错”循环的设计模式:何时重试、何时求助、何时报错?
  • Clink 在 VS 2022 Developer Command Prompt 中的配置与路径精简调校
  • 【CLAUDE】CLAUDE.md 完全实战指南:用好Claude Code的核心记忆体系
  • Rust的#[non_exhaustive]:防止模式匹配穷尽的可扩展枚举
  • 《B4447 [GESP202512 二级] 环保能量球》
  • Flux2-Klein-9B-True-V2效果集:Proteus电路仿真与AI概念艺术设计的碰撞
  • 原创文档:智慧地下管廊知识图谱设计与实现
  • 2026年最新实测:5个降AI工具助我把知网AIGC率从79%降至6.2%(附免费反向优化法) - 降AI实验室
  • 别再用namespace硬隔离了!MCP 2026正式启用硬件辅助隔离(Intel AMX+AMD SVM-V),性能损耗<0.7%?
  • 2026插座选哪个牌子性价比高?实用推荐指南 - 品牌排行榜
  • 登山包/电脑包/军用背包用TPU牛津布厂家推荐:轻便+防水+耐刮
  • 立知多模态重排序模型体验:图片搜索排序新利器
  • Day56基本包装类型