【C++进阶篇】哈希表的封装(赋源码)

article/2025/8/14 15:13:53

C++哈希表终极封装指南:从线性探测到STL兼容的迭代器魔法

  • 一. 哈希表的封装
    • 1.1 基本结构
      • 1.1.1 插入
      • 1.1.2 查找
      • 1.1.3 删除
      • 1.1.4 Begin()
      • 1.1.5 End()
      • 1.1.6 构造函数
      • 1.1.7 析构函数
    • 1.2 迭代器设计(重点)
      • 1.2.1 重载operator*()
      • 1.2.2 重载operator->()
      • 1.2.3 重载operator++()
      • 1.2.4 重载operator==()
      • 1.2.5 重载operator==()
    • 1.3 封装 unordered_set
      • 1.3.1 基本结构
    • 1.4 封装unordered_map
      • 1.4.1 基本结构
  • 二. 最后

声明:最好在学习了红黑树的封装之后,再来学习哈希表的封装,这里涉及的模版(泛型编程思想)较多,综合较强,两个模块之间相互依赖,需要前置声明。

源码位置:哈希表的封装- Gitee

一. 哈希表的封装

1.1 基本结构

  • 使用哈希桶结构封装出哈希表
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{typedef HashNode<T> Node;
public:
private:vector<Node*> _tables;  // 指针数组size_t _n = 0;			// 表中存储数据个数
};

使用vector容器来存储节点指针,进行线性探测可以挂在后面。
在这里插入图片描述

  • KeyOfT的作用是返回key值,因为对于set而言是key值,而对于map而言是pair类型。
  • Hash作用是将不支持取模类型的数据转换成整数,如string等类型不支持。

1.1.1 插入

查找数据之前,查找该数据是否已经存在,存在则直接返回{该节点已经存在的迭代器,false},不存在则返回{新插入节点的迭代器,true},然后计算出该新插入数据节点的哈希值,然后直接头插即可(注:尾插也行,但麻烦,需要找尾),然后返回{当前新插入节点的迭代器,true}即可,将_n++,表示实际存储的数据个数增加了。

  • 伪代码如下:
inline unsigned long __stl_next_prime(unsigned long n)
{// Note: assumes long is at least 32 bits.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 = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}pair<Iterator, bool> Insert(const T& data)
{KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return  { it,false };Hash hs;// 扩容逻辑size_t hashi = hs(kot(data)) % _tables.size();Node* newnode = new Node(data);// 头插到桶里面newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return { Iterator(newnode,this),true };
}

问题1:如果插入的数据,当前空间已经满了???怎么办,需要扩容。

  • 扩容:
    将原哈希表的数据挪动至新表中,然后将旧表与新表交换即可,伪代码:
if (_n == _tables.size())
{size_t newSize = __stl_next_prime(_tables.size() + 1);vector<Node*> newtables(newSize, nullptr);// 遍历旧表,把旧表的节点挪动到新表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// cur头插到新表size_t hashi = hs(kot(cur->_data)) % newSize;cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);
}

1.1.2 查找

将要查找的数据转换成哈希值,然后在当前桶中遍历即可。找到返回当前节点的迭代器即可。未找到返回End()。

Iterator Find(const K& key)
{Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];KeyOfT kot;while (cur){if (kot(cur->_data) == key){return Iterator(cur,this);}cur = cur->_next;}return End();
}

1.1.3 删除

这里直接附上代码即可,在上篇文章已经说过细节:哈希表模拟实现 - CSDN详情看这篇文章。

bool Erase(const K& key)
{Hash hs;KeyOfT kot;size_t hashi = hs(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;return true;}prev = cur;cur = cur->_next;}return false;
}

1.1.4 Begin()

查找第一个不为空的桶,返回当前节点指针和表指针。都为空则返回nullptr即可。

Iterator Begin()
{for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return Iterator(cur, this);}}return End();
}

1.1.5 End()

返回使用{nullptr,表指针}即可。

Iterator End()
{return Iterator(nullptr, this);
}

const迭代器与上述类似。详情看源码即可。

1.1.6 构造函数

将当前容量默认设置为素数表第一个素数即可。

HashTable()
{_tables.resize(__stl_next_prime(1), nullptr);
}

1.1.7 析构函数

依次把当前桶桶中的节点指针析构即可,注意:析构当前节点时,需保存下一个节点指针的地址,未提前保存否则会找不到。

~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;}
}

1.2 迭代器设计(重点)

迭代器里面重要的成员:

  1. 节点指针:用于遍历当前桶的。
  2. 表指针:用于查找下一个不为空的表。
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){}
}	

问题1:这里表指针为啥要const修饰???
为了配合const的迭代器,const的迭代器修饰表指针,不用设计权限放大,这是不允许的。

1.2.1 重载operator*()

typedef __HTIterator<K, T, T&,T*,KeyOfT, Hash> Iterator;
typedef __HTIterator<K, T, const T&,const T*,KeyOfT, Hash> ConstIterator;
template<class K, class T,class Ref,class Ptr, class KeyOfT, class Hash>
struct __HTIterator ;

直接取节点中的数据即可。

Ref operator*()
{return _node->_data;
}

1.2.2 重载operator->()

返回节点中数据的地址即可。

Ptr operator->()
{return &_node->_data;
}

1.2.3 重载operator++()

  1. 当前桶中节点未走完,则需要继续往当前桶中继续往后走。
  2. 当前桶中的节点已经为空,表示已经遍历完了,则需要找下一个不为空的桶。
    问题1:如何找下一个不为空的桶???
  3. 将当前节点的数据转换成哈希值,然后将该哈希值++,继续判断,如果当前桶中的节点不为空,则将该节点赋值给_node,如果为空,继续将哈希值++,继续往后查找。
Self operator++()
{if (_node->_next){_node = _node->_next;}else{//当前桶中的链表数据已经遍历完,寻找下一个有数据的桶KeyOfT kot;Hash hs;size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();++hashi;//当前的下一个位置while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi]){_node = _ht->_tables[hashi];break;}else{++hashi;}}if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;
}

1.2.4 重载operator==()

直接判断节点相等即可。

bool operator==(const Self& s)
{return _node == s._node;
}

1.2.5 重载operator==()

直接判断节点是否相等即可。

bool operator!=(const Self& s)
{return _node != s._node;
}

1.3 封装 unordered_set

1.3.1 基本结构

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, const K, SetKeyOfT, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;
private:hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
};
  • 基本都是调用哈希桶中哈希表中的接口。
iterator begin()
{return _ht.Begin();
}
iterator end()
{return _ht.End();
}const_iterator begin()const
{return _ht.Begin();
}
const_iterator end()const
{return _ht.End();
}iterator Find(const K& key)
{return _ht.Find(key);
}
bool Erase(const K& key)
{return _ht.Erase(key);
}pair<iterator, bool> insert(const K& key)
{return _ht.Insert(key);
}

该代码实现了一个STL风格容器适配器,封装底层哈希表的核心功能:1) 提供const/non-const迭代器接口,支持范围遍历;2) insert方法返回插入状态对(迭代器+成功标识),兼容关联容器规范;3) Find/Erase实现键值查询与删除操作;4) 通过组合模式复用底层实现,提供类型安全的键值存储接口。整体设计遵循STL容器规范,支持迭代器遍历和标准操作接口,实现高效的键值对管理。

1.4 封装unordered_map

1.4.1 基本结构

template<class K, class V, class Hash>
class unoredred_map
{//仿函数struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};
public:typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;
private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
  • 基本都是调用哈希桶中哈希表中的接口。
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)
{return _ht.Insert(kv);
}iterator Find(const K& key)
{return _ht.Find(key);
}
bool Erase(const K& key)
{return _ht.Erase(key);
}V& operator[](const K& key)
{pair<iterator,bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;
}

该代码实现了一个容器适配器,封装了底层哈希表_ht的核心接口:1) 提供迭代器支持(begin/end),支持范围遍历;2) insert方法返回插入状态对(迭代器+成功标识);3) Find/Erase实现键值查找与删除;4) 重载operator[]实现类似map的便捷访问——若键不存在则自动插入默认值。整体设计遵循STL容器风格,通过组合模式复用底层哈希表功能,提供类型安全的键值存储与高效操作接口。

二. 最后

本文详细阐述了C++哈希表容器的封装实现,采用vector存储指针数组处理冲突,通过线性探测法解决哈希碰撞,并设计素数表实现动态扩容。核心模块包括:1) 哈希表基类:实现插入(含自动扩容)、查找、删除等操作,提供迭代器接口;2) 迭代器设计:支持跨桶遍历,通过哈希值跳跃实现高效遍历;3) 无序容器适配:通过组合模式封装哈希表,实现unordered_set/unordered_map容器,提供STL标准接口(begin/end/insert/find/erase),其中unordered_map重载operator[]实现自动值初始化。整体设计遵循RAII原则,内存管理安全,完美兼容STL生态体系。


http://www.hkcw.cn/article/BqtKmnXouc.shtml

相关文章

238除自身以外数组的乘积

题目链接: https://leetcode.cn/problems/product-of-array-except-self/description/解法一&#xff1a;暴力解法 直接遍历一遍数组&#xff0c;求该数组的除该数之外的乘积&#xff0c;但是超时时间复杂度为n方。 vector<int> productExceptSelf(vector<int>&a…

主数据编码体系全景解析:从基础到高级的编码策略全指南

在数字化转型的浪潮中&#xff0c;主数据管理&#xff08;MDM&#xff09;已成为企业数字化转型的基石。而主数据编码作为MDM的核心环节&#xff0c;其设计质量直接关系到数据管理的效率、系统的可扩展性以及业务决策的准确性。本文将系统性地探讨主数据编码的七大核心策略&…

C# 类和继承(构造函数的执行)

构造函数的执行 在前一章中&#xff0c;我们看到了构造函数执行代码来准备一个即将使用的类。这包括初始化类的静 态成员和实例成员。在这一章&#xff0c;你会看到派生类对象有一部分就是基类对象。 要创建对象的基类部分&#xff0c;需要隐式调用基类的某个构造函数。继承层…

79. Word Search

题目描述 79. Word Search 回溯 代码一&#xff0c;使用used数组 class Solution {vector<pair<int,int>> directions{{0,1},{0,-1},{1,0},{-1,0}};vector<vector<bool>> used; public:bool exist(vector<vector<char>>& board, st…

大模型备案中语料安全详细说明

《AIGC安全要求》针对语料安全&#xff0c;在语料来源授权合法、安全评估核验、不良语料类型三个方面提出了重点要求&#xff0c;具体要求包括&#xff1a; 1、授权合法 语料的来源需要有合法的、明确的授权&#xff0c;确保其符合“授权、同意、告知”的合法性原则。根据语料…

汽车安全:功能安全FuSa、预期功能安全SOTIF与网络安全Cybersecurity 解析

汽车安全的三重防线&#xff1a;深入解析FuSa、SOTIF与网络安全技术 现代汽车已成为装有数千个传感器的移动计算机&#xff0c;安全挑战比传统车辆复杂百倍。 随着汽车智能化、网联化飞速发展&#xff0c;汽车电子电气架构已从简单的分布式控制系统演变为复杂的移动计算平台。现…

【云安全】以Aliyun为例聊云厂商服务常见利用手段

目录 OSS-bucket_policy_readable OSS-object_public_access OSS-bucket_object_traversal OSS-Special Bucket Policy OSS-unrestricted_file_upload OSS-object_acl_writable ECS-SSRF 云攻防场景下对云厂商服务的利用大同小异&#xff0c;下面以阿里云为例 其他如腾…

[MongoDB] 认识MongoDB以及在Windows和Linux上安装MongoDB

初次学习&#xff0c;如有错误还请指正 目录 MongoDB简介 体系结构 数据模型 MongoDB的特点 Windows中的安装 Linux系统中的安装启动和连接 MongoDB简介 MongoDB是一个开源、高性能、无模式的文档型数据库&#xff0c;当初的设计就是用于简化开发和方便扩展&#xff0c;…

iOS —— UI 初探

简介 第一次新建时&#xff0c;你可能会好奇。为什么有这么多文件&#xff0c;他们都有什么用&#xff1f; App 启动与生命周期管理相关 文件名 类型 作用 main.m m 程序入口&#xff0c;main() 函数定义在这里 AppDelegate.h/.m h/m App 启动/进入后台/退出等全局事…

【设计模式-3.4】结构型——代理模式

说明&#xff1a;说明&#xff1a;本文介绍结构型设计模式之一的代理模式 定义 代理模式&#xff08;Proxy Pattern&#xff09;指为其他对象提供一种代理&#xff0c;以控制对这个对象的访问&#xff0c;属于结构型设计模式。&#xff08;引自《设计模式就该这样学》P158&am…

C++: STL简介与string类核心技术解析及其模拟实现

目录: 一.STL二.string类一、创建对象的6种构造方式二、常用接口解析1. 容量操作2. 元素访问3. 修改操作4. 字符串操作 三.string模拟实现一、设计基础&#xff1a;类结构与资源管理二、拷贝控制&#xff1a;深拷贝的三种实现1. 传统深拷贝2. 现代写法&#xff08;推荐&#xf…

【复杂网络分析】什么是modularity?

在复杂网络研究中&#xff0c;modularity&#xff08;模块化程度或模块度&#xff09; 是衡量网络社区结构&#xff08;即节点分组为紧密连接的社区&#xff0c;而社区间连接稀疏&#xff09;的重要指标。它由Mark Newman和Michelle Girvan于2004年提出&#xff0c;广泛用于评估…

模型训练相关的问题

与模型训练相关问题 损失函数Cross entropy loss的含义训练数据有脏数据,怎么处理?loss一直不收敛,怎么排查?连续值的特征怎么处理后输入到机器学习模型当中损失函数Cross entropy loss的含义 在深度学习中,可以看作通过概率分布q ( x )(预测概率)表示概率分布p ( x ) …

【项目记录】登录认证(下)

1 过滤器 Filter 刚才通过浏览器的开发者工具&#xff0c;可以看到在后续的请求当中&#xff0c;都会在请求头中携带JWT令牌到服务端&#xff0c;而服务端需要统一拦截所有的请求&#xff0c;从而判断是否携带的有合法的JWT令牌。 那怎么样来统一拦截到所有的请求校验令牌的有…

Portainer安装指南:多节点监控的docker管理面板-家庭云计算专家

背景 Portainer 是一个轻量级且功能强大的容器管理面板&#xff0c;专为 Docker 和 Kubernetes 环境设计。它通过直观的 Web 界面简化了容器的部署、管理和监控&#xff0c;即使是非技术用户也能轻松上手。Portainer 支持多节点管理&#xff0c;允许用户从一个中央控制台管理多…

基于微信小程序的垃圾分类系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

【前端面经】百度一面

写在前面&#xff1a;面经只是记录博主遇到的题目。每题的答案在编写文档的时候已经有问过deepseek&#xff0c;它只是一种比较普世的答案&#xff0c;要学得深入还是靠自己 Q&#xff1a; <html><style>.a {background-color: red;width: 200px;height: 100px;}…

智能体觉醒:AI开始自己“动手”了-自主进化开启任务革命时代

1. 智能体&#xff1a;AI从“工具”到“伙伴”的关键跃迁 1.1 什么是智能体&#xff1f; 智能体&#xff08;Agent&#xff09;是AI的“进化版”——它不再局限于生成文字或图像&#xff0c;而是能像人类一样“规划任务”“调用工具”甚至“协同合作”。例如&#xff0c;一个…

STM32软件spi和硬件spi

核心观点 本文主要介绍了SPI通信的两种实现方式&#xff1a;软件SPI和硬件SPI。详细阐述了SPI通信协议的基本概念、硬件电路连接方式、移位示意图、时序基本单元以及四种工作模式。同时&#xff0c;对W25Q64模块进行了详细介绍&#xff0c;包括其硬件电路、框图以及操作注意事…

MongoDB数据库命令

目录 一、数据库操作 二、集合&#xff08;表&#xff09;操作 三、文档&#xff08;记录&#xff09;CRUD 操作 1、插入文档 2、查询文档 3、更新文档 4、删除文档 四、聚合操作 1、单目的聚合操作 2、聚合管道 3、MapReduce编程 五、索引管理操作 六、用户权限管…