【C++进阶篇】哈希表的模拟实现(赋源码)

这里写目录标题

  • 前言
  • 一. 开放地址法实现哈希表
    • 1.1 闭散列结构定义
    • 1.2 构造函数
    • 1.3 插入(线性探测)
      • 1.3.1 传统写法
      • 1.3.2 现代写法
    • 1.4 查找
    • 1.5 删除
  • 二. 链地址法实现哈希表(哈希桶)
    • 2.1 开散列结构定义
    • 2.2 构造函数
    • 2.3 插入
    • 2.4 查找
    • 2.5 删除
  • 三. 最后

前言

哈希表的核心思想就是映射,通过将key值以某种算法映射成不大于哈希表长度的哈希值,从而实现存储数据。上篇提到解决哈希冲突有 闭散列 和 开散列,本文将用这两种理论思想实现哈希表。
代码位置:哈希模拟的实现源码

一. 开放地址法实现哈希表

1.1 闭散列结构定义

该闭散列哈希表使用vector数组,其中数组里面包含一个结构体,该结构存储pair类型数据及当前位置的状态,是否为空,不为空,或者有数据然后被删除了的三个状态,伪代码如下:

//节点可能的三种状态
enum State
{EXIST,EMPTY,DELETE
};//哈希表中存储的数据
template<class K, class V>
struct HashData
{pair<K, V> _kv;//存放数据类型State _state = EMPTY;//当前位置的状态,默认为空,即表示此位置没有数据
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
private:vector<HashData<K, V>> _tables;size_t _n = 0;//表中实际存储的数据个数
};
  • 仿函数作用:

因为里面是数组,通过key值映射成哈希值后,哈希值不能超过哈希表的长度,需要取模,所以同时也要传入仿函数,将任意类型数据不能取模转换成整数,本来就可以进行取模运算的,就无需借助额外的仿函数,所以就可以给一个默认的仿函数,该仿函数给本来是整数去使用的。

//默认的仿函数
template<class K>
class HashFunc
{
public:size_t operator()(const K& key){return (size_t)key;}
};

1.2 构造函数

给vector数组开一定的空间,为了防止2的幂次方的数,底层给了一定合理的素数,下面给代码即可。

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;
}HashTable()
{_tables.resize(__stl_next_prime(1));
}

1.3 插入(线性探测)

插入之前判断该key值是否在存在,存在直接返回即可。对插入的key值映射成哈希值,然后进行探测,探测规则:当前位置为空,继续向后探测,跳出循环后,将该位置插入数据并将该位置的状态设置为存在,_n++(表示存储的数据个数+1),这里有一个问题:扩容???

  • 问题:如何扩容

1.3.1 传统写法

创建一个数组(数组已经扩容后),将旧表中的vector数组中的数据重新进行映射成新开的数组(这里的一个优势在于:原来在旧表中冲突的数据可能在新表中就不冲突了),也需进行探测,过程基本与插入过程一致,将旧数据完成探测之后,在新表与旧表进行交换。伪代码如下:

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;//扩容if ((double)_n / (double)_tables.size() >= 0.7){// 获取素数表里面比当前表大的下一个素数size_t newSize = __stl_next_prime(_tables.size() + 1);vector<HashData<K, V>> newTables(newSize);//遍历旧表,将数据都映射到新表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){size_t hash0 = _tables[i].kv.first % newSize;size_t i = 1;size_t hashi = hash0;while (newTables[hashi]._state == EXIST){//冲突探测hashi = (hash0 + i) % newSize;i++;}newTables[hashi]._kv = kv;newTables[hashi]._state = EXIST;//_n++;需不需要写???思考一下}}_tables.swap(newTables);}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t i = 1;size_t hashi = hash0;while (_tables[hashi]._state == EXIST){//冲突探测hashi = (hash0 + i) % _tables.size();i++;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}
  • 细节1:

重新寻找哈希值,需要对新的哈希表的长度进行取余,不要依然对旧表中的哈希表长度进行取余,否则会导致新表中新增加的长度没有被使用,冲突概率未减少。

  • 细节2:

_n++需不需要写???不需要,因为是将旧表中的数据重新映射至新表,数据并没有增加。

注意:可以看出上述代码有点冗余,特别是插入过程与重新建立映射关系的代码很相似。
下面这种写法很巧妙,且代码量少。

1.3.2 现代写法

思想:建立一个新表,然后给新表中的哈希表开一定足够的空间,然后将旧表中的数据直接插入新表中即可,插入的过程同时也完成探测过程。很优雅的写法,建议采用它。

  • 伪代码:
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;//扩容if ((double)_n / (double)_tables.size() >= 0.7){size_t newSize = __stl_next_prime(_tables.size() + 1);HashTable<K, V, Hash> newHT;newHT._tables.resize(newSize);//遍历旧表,将数据都映射到新表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);//此行为已经将旧数据映射到了新表中,同时进行线性探测和冲突探测,可能原来在旧表中的数据冲突,在新表中它就不冲突了}}_tables.swap(newHT._tables);//将新表与旧表进行交换}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t i = 1;size_t hashi = hash0;while (_tables[hashi]._state == EXIST){//冲突探测hashi = (hash0 + i) % _tables.size();i++;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}

该实现是一个功能完整的线性探测哈希表,适合学习用途。

1.4 查找

查找过程很简单,首先对要查找的key值转化成映射后的哈希值,对应位置状态为!EXIST,进行查询当前位置状态不为空,且不为删除直接返回该指针即可,否则继续往后探测,当探测为空,说明不存在,直接返回nullptr即可。

  • 伪代码:
HashData<K, V>* Find(const K& key)
{Hash hs;size_t hash0 = hs(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;
}

1.5 删除

直接调用接口Find即可,接收返回值,判断返回值是否为空,为空直接返回false;不为空将该位置的状态设置为删除即可,然后返回true即可。

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

标记删除操作时间复杂度为 O(1),无数据迁移开销。
声明:上述做法了解一下即可,下面这个才是王炸,需重点关注及掌握。

二. 链地址法实现哈希表(哈希桶)

2.1 开散列结构定义

该开散列哈希表使用vector数组,其中数组里面包含一个哈希表节点,该节点存储pair类型数据及下一个哈希表数据的指针。伪代码如下:

template<class K, class V>
struct HashNode {pair<K, V> _kv;          // 键值对HashNode<K, V>* _next;   // 冲突链指针HashNode(const pair<K, V>& kv) : _kv(kv), _next(nullptr) {} // 构造函数
};template<class K, class V>
class HashTable {typedef HashNode<K, V> Node;vector<Node*> _tables;    // 哈希槽数组(存储链表头指针)size_t _n = 0;            // 当前元素总数
};

该代码提供了链地址法哈希表的核心骨架。

2.2 构造函数

构造函数与上述闭散列一致。

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;
}HashTable()
{_tables.resize(__stl_next_prime(1), nullptr);
}

2.3 插入

创建一个数组,扩容时,将旧表中的节点挪动至新表中,需重新建立映射关系,也需要进行探测,过程与插入过程一致,将旧数据完成探测之后,在新表与旧表进行交换。伪代码如下:
**插入数据时有个问题,**进行尾插还是头插,两者都可以,但头插很简单,尾插相对较复杂,需要找尾,当前桶挪动完毕,需要将当前桶只为nullptr,本文以头插为示例。

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;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;size_t hashi = cur->_kv.first % newSize;cur->_next = newtables[hashi];newtables[hashi] = cur;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = kv.first % _tables.size();Node* newnode = new Node(kv);//头插,尾插也行,找尾麻烦,需要找尾newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}

2.4 查找

查找过程很简单,将要查找的key值转换成哈希值,然后对该桶进行遍历,判断桶中的数据key值是否与要查找的key值是否相等,相等返回该节点指针即可,找到空还未找到,返回nullptr即可。

  • 伪代码如下:
Node* Find(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}

2.5 删除

删除需要前驱指针(默认为空)将删除后的节点连接起来,删除过程相对来说较复杂,也需要要删除的key值转换成哈希值,然后遍历该桶,判断桶中节点的数据key值与要查找的key值是否相等,相等进行删除,返回true即可,里面有细节 -> :

  • 细节一:

当删除头结点时,头结点没有前驱指针,会对空指针进1行解引用,所以需要分情况讨论。

  1. 前驱指针不为空,和为空两种状态。
  2. 不相等继续往后找,当前桶找完还未找到,返回false即可。
  • 伪代码:
		bool Erase(const K& key){size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == 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;}

代码正确处理了链表节点的删除,维护了计数器_n,但未处理重复键(若允许存在)。

三. 最后

本文详述了哈希表的两种实现方式:开放地址法(闭散列)与链地址法(开散列)。开放地址法通过线性探测解决冲突,采用标记删除优化性能,扩容时重建哈希表;链地址法以链表处理冲突,头插法简化操作,扩容时重新映射所有节点。两者均使用素数表优化哈希分布,核心操作包括插入、查找、删除,适用于不同场景,是高效键值存储的关键技术。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/907270.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

07-后端Web实战(部门管理)

5. 修改部门 对于任何业务的修改功能来说&#xff0c;一般都会分为两步进行&#xff1a;查询回显、修改数据。 5.1 查询回显 5.1.1 需求 当我们点击 "编辑" 的时候&#xff0c;需要根据ID查询部门数据&#xff0c;然后用于页面回显展示。 5.1.2 接口描述 参照参照…

深度解析项目集方向或目标根本性转变的应对策略 —— 项目集管理实战指南

在复杂多变的商业环境中&#xff0c;项目集管理面临着重重挑战&#xff0c;而项目集方向或目标的根本性转变无疑是其中最具冲击力的问题之一。本文将深度剖析这一难题&#xff0c;为项目集管理从业者提供实用、新颖且富有价值的应对策略&#xff0c;助力大家在项目集管理的复杂…

JAVA面试复习知识点

面试中遇到的题目&#xff0c;记录复习&#xff08;持续更新&#xff09; Java基础 1.String的最大长度 https://www.cnblogs.com/wupeixuan/p/12187756.html 2.集合 Collection接口的实现&#xff1a; List接口&#xff1a;ArraryList、LinkedList、Vector Set接口&#xff1a…

【烧脑算法】定长滑动窗口:算法题中的“窗口”智慧

目录 引言 定长滑动窗口习题剖析 3439. 重新安排会议得到最多空余时间 I 2134. 最少交换次数来组合所有的 1 II 1297. 子串的最大出现次数 2653. 滑动子数组的美丽值 567. 字符串的排列 438. 找到字符串中所有字母异位词 30. 串联所有单词的子串 220. 存在重复元素 II…

JWT安全:接收无签名令牌.【签名算法设置为none绕过验证】

JWT安全&#xff1a;假密钥【签名随便写实现越权绕过.】 JSON Web 令牌 (JWT)是一种在系统之间发送加密签名 JSON 数据的标准化格式。理论上&#xff0c;它们可以包含任何类型的数据&#xff0c;但最常用于在身份验证、会话处理和访问控制机制中发送有关用户的信息(“声明”)。…

XGBoost与SHAP深度解析:从算法原理到实战价值

在机器学习领域&#xff0c;XGBoost以其卓越的性能长期占据Kaggle竞赛和工业界的主流地位&#xff0c;而SHAP&#xff08;SHapley Additive exPlanations&#xff09;则成为模型可解释性的标杆工具。本文将深度解析两者的技术内核&#xff0c;并通过实战案例揭示其结合应用的实…

Java SE Cloneable接口和深/浅拷贝

Java为我们提供了各种各样功能的接口&#xff0c;Clonable接口就是其中之一。 它通常配合Object类的 clone方法使用。这个方法可以为我们创建一个对象的拷贝&#xff0c;即复制一个对象。在进入本文的主要内容之前&#xff0c;先来对访问限定符 protected进行一个解剖。 1.再…

Python学习(3) ----- Python的函数定义及其使用

Python 中函数是组织好的、可重复使用的代码块&#xff0c;用于实现单一或相关联的功能。下面是函数定义和使用的完整说明&#xff1a; &#x1f4cc; 一、函数定义语法 def 函数名(参数1, 参数2默认值, *args, **kwargs):"""函数说明文档"""函…

vue2使用el-tree实现两棵树间节点的拖拽复制

原文链接&#xff1a;两棵el-tree的节点跨树拖拽实现 参照这篇文章&#xff0c;把它做成组件&#xff0c;新增左侧树&#xff08;可拖出&#xff09;被拖节点变灰提示&#xff1b; 拖拽中&#xff1a; 拖拽后&#xff1a; TreeDragComponent.vue <template><!-- …

智变与重构:AI 赋能基础教育教学的范式转型研究报告

一、研究背景与核心价值 &#xff08;一&#xff09;技术驱动下的教育转型浪潮 在全球数字化转型加速的背景下&#xff0c;人工智能作为核心技术力量&#xff0c;正重塑基础教育生态。据《人工智能赋能未来教育研究报告》指出&#xff0c;我国教育数字化战略行动已推动超 70…

Go语言中Print、Printf和Println的区别及使用场景详解

在Go语言的fmt包中&#xff0c;Print、Printf和Println是三个基础但功能各异的输出函数。本文将从多个维度进行详细对比分析&#xff0c;并给出具体的使用建议。 1. 核心区别深度解析 1.1. 函数签名与基本行为 func Print(a ...interface{}) (n int, err error) func Printf…

高端制造行业 VMware 替代案例合集:10+ 头部新能源、汽车、半导体制造商以国产虚拟化支持 MES、PLM 等核心应用系统

在“中国制造 2025”政策的推动下&#xff0c;国内的新能源、汽车制造、半导体、高端装备等高端制造产业迎来了蓬勃发展&#xff0c;成为全球制造业版图中举足轻重的力量。订单数量的激增与国产化转型的趋势&#xff0c;也为高端制造企业的 IT 基础设施带来了新的挑战&#xff…

Spring Ai | 从零带你一起走进AI项目(中英)

目录 Thinking Study question pox.xml Maven Gradle Configure API Key Use the AI Client Question Thinking 让数据变得更加贴近用户的想法 Study question null pox.xml 添加依赖 Maven <dependencies><dependency><groupId>org.springfram…

LiveGBS作为下级平台GB28181国标级联2016|2022对接海康大华宇视华为政务公安内网等GB28181国标平台查看级联状态及会话

LiveGBS作为下级平台GB28181国标级联2016|2022对接海康大华宇视华为政务公安内网等GB28181国标平台查看级联状态及会话 1、GB/T28181级联概述2、搭建GB28181国标流媒体平台3、获取上级平台接入信息3.1、向下级提供信息3.2、上级国标平台添加下级域3.3、接入LiveGBS示例 4、配置…

卸载 Office PLUS

Office PLUS作为微软官方推出的智能办公提效工具&#xff0c;自2015年问世以来&#xff0c;凭借其丰富的模板资源和便捷的智能功能&#xff0c;迅速赢得了广大职场人士和学生的青睐。本文将全面介绍Office PLUS的发展历程、核心功能、可能带来的使用问题&#xff0c;以及如何彻…

影响沉金价格的因素如何体现在多层电路板制造上?

随着科技的不断发展&#xff0c;电子产品越来越普及&#xff0c;对电路板的需求也越来越大。多层电路板作为电子产品的核心部件&#xff0c;其性能和质量直接影响到整个产品的稳定性和可靠性。在多层电路板的生产过程中&#xff0c;沉金工艺是一种常用的表面处理方法&#xff0…

扩展摩尔投票法:找出出现次数超过 n/3 的元素

文章目录 问题描述关键洞察算法原理Java 实现算法演示投票阶段验证阶段 复杂度分析算法关键点通用化公式实际应用场景边界情况处理总结 标签&#xff1a;LeetCode 169, 摩尔投票法, 多数元素, 算法扩展, 数组处理 在解决多数元素问题时&#xff0c;我们学习了经典的摩尔投票法处…

Git:现代软件开发的基石——原理、实践与行业智慧·优雅草卓伊凡

Git&#xff1a;现代软件开发的基石——原理、实践与行业智慧优雅草卓伊凡 一、Git的本质与核心原理 1. 技术定义 Git是一个分布式版本控制系统&#xff08;DVCS&#xff09;&#xff0c;由Linus Torvalds在2005年为管理Linux内核开发而创建。其核心是通过快照&#xff08;Sna…

程序人生-hello’s P2P

计算机系统 大作业 题 目 程序人生-hello’s P2P 专 业 计算机与电子通信类 学   号 2023111990 班   级 23L0514 学 生 袁骋 指 导 教 师 史…

Java设计模式之设计原则

Java设计模式 Java设计模式主要原则是开闭原则&#xff0c;即对扩展开放&#xff0c;对修改关闭。由此衍生出5大原则&#xff1a;单一职责原则&#xff0c;里式替换原则&#xff0c;迪米特原则&#xff0c;接口隔离职责&#xff0c;依赖倒置原则。1、开闭原则 开闭原则&#x…