【 C++ 】闭散列哈希表的模拟实现

哈希节点状态

我们都很清楚数组里的每一个值无非三种状态:

  1. 如果某下标没有值,则代表空EMPTY。
  2. 如果有值在代表存在EXIST。
  3. 如果此位置的值被删掉了,则表示为DELETE。

而这三种状态我们可以借助enum枚举来帮助我们表示数组里每个位置的状态。这里我们专门封装一个类来记录每个位置的状态,以此汇报给后续的哈希表。

enum State
{EMPTY,EXIST,DELETE
};//哈希节点
template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;//记录每个位置的状态,默认给空
};//哈希表
template<class K, class V, class HashFunc = DefaultHash<K>>//添加仿函数便于把其他类型的数据转换为整型数据
class HashTable
{typedef HashData<K, V> Data;
public://相关功能的实现……
private:vector<Data> _tables;size_t _n = 0;//记录存放的有效数据的个数
};

实现好了哈希节点的类,就能够很好的帮助我们后续的查找,示例:

在这里插入图片描述

  • 查找50:

50%10=0,下标0的值不是50,继续++下标往后查找,直至下标3的下标为止。

  • 查找60:

60%10=0,下标0不是,往后++下标继续查找,找到下标4发现状态为EMPTY空,此时停止查询,因为往后就不可能出现了

  • 删除10,再查找50:

50%10=0,下标0的值不是,++下标到下标1,发现状态为DELETE删除,继续++下标直至下标3的值为50,找到了。

哈希表的扩容

  • 散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长度。
  • α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,表明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数。
  • 对于开放定址法(闭散列),载荷因子是特别重要因素,应严格限制在0.7 ~ 0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照质数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了载荷因子为0.75,超过此值将resize散列表。

综上,我们在后续的插入操作中,必然要考虑到扩容的情况,我们直接把负载因子控制在0.7,超过了就扩容。具体操作见下文哈希表的插入操作。

构建仿函数把所有数据类型转换为整型并特化

在我们后续的插入操作中,插入的数据类型如果是整数,那么可以直接建立映射关系,可若是字符串,就没那么容易了,因此,我们需要套一层仿函数,来帮助我们把字符串类型转换成整型的数据再建立映射关系。主要分为以下三类需要写仿函数的情况:

  1. key为整型,为默认仿函数的情况。
    此时的数据类型为整型,直接强转size_t随后返回。

  2. key为字符串,单独写个字符串转整型的仿函数。
    针对于字符串转整型,我们推出下面两种方法,不过都是会存在问题的:

    • 只用首字母的ascii码来映射,此法不合理,因为"abc"和"axy"本是俩不用字符串,经过转换,会引发冲突。
    • 字符串内所有字符ASCII码值之和,此法也会产生冲突,因为"abcd"和"bcad"在此情况就会冲突。

为了避免冲突,几位大佬推出多种算法思想,下面我取其中一种算法思想来讲解:

BKDR哈希算法:
hash = hash * 131 + ch;   // 也可以乘以31、131、1313、13131、131313..  

为了能够让我们的哈希表能够自动识别传入数据的类型,不用手动声明,这里我们可以借助特化来解决,仿函数+特化总代码如下:

//利用仿函数将数据类型转换为整型
template<class K>
struct DefaultHash
{size_t operator()(const K& key){return (size_t)key;}
};
//模板的特化
template<>
struct DefaultHash<string>
{size_t operator()(const string& key){//BKDR哈希算法size_t hash = 0;for (auto ch : key){hash = hash * 131 + ch;//把所有字符的ascii码值累计加起来}return hash;}
};

哈希表的插入

哈希表的插入主要是三大步骤:

  • 去除冗余
  • 扩容操作
  • 插入操作

下面分开来演示。

1、去除冗余:

  1. 复用Find查找函数,去帮助我们查找插入的值是否存在 。
  2. 若存在,直接返回false 。
  3. 不存在,再进行后续的插入操作。

2、扩容操作:

  1. 如果哈希表一开始就为空,则要扩容。
  2. 如果填入表中的元素个数*10 再 / 表的大小>=7,就扩容(*10是为了避免出现size_t的类型相除不会有小数的情况)。
  3. 扩容以后要重新建立映射关系。
  4. 创建一个新的哈希对象,扩容到先前旧表扩容的大小。
  5. 遍历旧表,把旧表每个存在的元素插入到新表,此步骤让新表自动完成映射关系,无序手动构建。
  6. 利用swap函数把新表交换到旧表那,此时的旧表就是已经扩好容且建立号映射关系的哈希表。

3、插入操作:

  1. 借助仿函数把插入的数据类型转为整型并定义变量保存插入键值对的key。
  2. 用此变量%=哈希表的size(),不能是capacity(),因为[ ]运算符会判断下标是否小于size,且对于哈希表,应该尽量控制size和capacity一样大。
  3. 遍历进行线性探测 / 二次探测,如果这个位置的状态为EXIST存在,说明还要往后遍历查找。
  4. 遍历结束,说明此位置的状态为空EMPTY或删除DELETE,可以放值。
  5. 把插入的值放进该位置,更新状态为EXIST,有效数据个数++。
//插入
bool Insert(const pair<K, V>& kv)
{//1、去除冗余if (Find(kv.first)){//说明此值已经有了,直接返回falsereturn false;}//2、扩容//负载因子超过0.7,就扩容if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7){size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;//扩容以后,需要重新建立映射关系HashTable<K, V, HashFunc> newHT;newHT._tables.resize(newSize);//遍历旧表,把旧表每个存在的元素插入newHTfor (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}newHT._tables.swap(_tables);//建立映射关系后交换}//3、插入HashFunc hf;size_t starti = hf(kv.first);//取出键值对的key,并且避免了负数的情况,借用仿函数确保是整型数据starti %= _tables.size();size_t hashi = starti;size_t i = 1;//线性探测/二次探测while (_tables[hashi]._state == EXIST){hashi = starti + i;//二次探测改为 +i^2++i;hashi %= _tables.size();//防止hashi超出数组}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}

哈希表的查找

查找的核心逻辑就是找到key相同,就返回此对象的地址,找到空就返回nullptr,具体细分规则如下:

  1. 先去判断表的大小是否为0,为0直接返回nullptr。
  2. 按照线性探测 / 二次探测的方式去遍历,遍历的条件是此位置的状态不为空EMPTY。
  3. 如果遍历到某哈希表中的对象的值等于要查找的值(前提是此位置的状态不为DELETE删除),返回此对象的地址。
  4. 当遍历结束后,说明此位置的状态为空EMPTY,哈希表没有我们要查找的值,返回nullptr。
//查找
Data* Find(const K& key)
{//判断表的size是否为0if (_tables.size() == 0){return nullptr;}HashFunc hf;size_t starti = hf(key);//通过仿函数把其它类型数据转为整型数据starti %= _tables.size();size_t hashi = starti;size_t i = 1;//线性探测/二次探测while (_tables[hashi]._state != EMPTY)//不为空就继续{if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key){return &_tables[hashi];//找到了就返回此对象的地址}hashi = starti + i;//二次探测改为 +i^2++i;hashi %= _tables.size();//防止hashi超出数组}return nullptr;
}

哈希表的删除

删除的逻辑很简单,遵循下面的规则:

  1. 复用Find函数去帮我们查找删除的位置是否存在。
  2. 若存在,把此位置的状态置为DELETE即可,此时表中的有效数据个数_n需要减减,最后返回true。
  3. 若不存在,直接返回false。
//删除
bool Erase(const K& key)
{//复用Find函数去帮助我们查找删除的值是否存在Data* ret = Find(key);if (ret){//若存在,直接把此位置的状态置为DELETE即可ret->_state = DELETE;return true;}else{return false;}
}

完整代码

#pragma once
#include<iostream>
#include<string>
#include<vector>
using namespace std;//利用仿函数将数据类型转换为整型
template<class K>
struct DefaultHash
{size_t operator()(const K& key){return (size_t)key;}
};
//模板的特化
template<>
struct DefaultHash<string>
{size_t operator()(const string& key){//BKDR哈希算法size_t hash = 0;for (auto ch : key){hash = hash * 131 + ch;//把所有字符的ascii码值累计加起来}return hash;}
};//闭散列哈希表的模拟实现
namespace CloseHash
{enum State{EMPTY,EXIST,DELETE};//哈希节点状态的类template<class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY;//记录每个位置的状态,默认给空};//哈希表的类template<class K, class V, class HashFunc = DefaultHash<K>>//添加仿函数便于把其他类型的数据转换为整型数据class HashTable{typedef HashData<K, V> Data;public://插入bool Insert(const pair<K, V>& kv){//1、去除冗余if (Find(kv.first)){//说明此值已经有了,直接返回falsereturn false;}//2、扩容//负载因子超过0.7,就扩容if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7){size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;//扩容以后,需要重新建立映射关系HashTable<K, V, HashFunc> newHT;newHT._tables.resize(newSize);//遍历旧表,把旧表每个存在的元素插入newHTfor (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}newHT._tables.swap(_tables);//建立映射关系后交换}//3、插入HashFunc hf;size_t starti = hf(kv.first);//取出键值对的key,并且避免了负数的情况,借用仿函数确保是整型数据starti %= _tables.size();size_t hashi = starti;size_t i = 1;//线性探测/二次探测while (_tables[hashi]._state == EXIST){hashi = starti + i;//二次探测改为 +i^2++i;hashi %= _tables.size();//防止hashi超出数组}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;}//查找Data* Find(const K& key){//判断表的size是否为0if (_tables.size() == 0){return nullptr;}HashFunc hf;size_t starti = hf(key);//通过仿函数把其它类型数据转为整型数据starti %= _tables.size();size_t hashi = starti;size_t i = 1;//线性探测/二次探测while (_tables[hashi]._state != EMPTY)//不为空就继续{if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key){return &_tables[hashi];//找到了就返回此对象的地址}hashi = starti + i;//二次探测改为 +i^2++i;hashi %= _tables.size();//防止hashi超出数组}return nullptr;}//删除bool Erase(const K& key){//复用Find函数去帮助我们查找删除的值是否存在Data* ret = Find(key);if (ret){//若存在,直接把此位置的状态置为DELETE即可ret->_state = DELETE;return true;}else{return false;}}private:vector<Data> _tables;size_t _n = 0;//记录存放的有效数据的个数};
}

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

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

相关文章

亿道推出重磅加固平板!为行业发展注入新动力

随着科技生产力的不断发展&#xff0c;各行各业都得到质的飞跃。产品的迭代速度也大大加快&#xff0c;作为全球领先的加固行移动终端一站式提供商&#xff0c;亿道信息跟紧时代潮流&#xff0c;推出EM-I10J、EM-I20J两款均衡型加固平板&#xff0c;为行业发展注入新动力。 接地…

【Python笔记-设计模式】命令模式

一、说明 命令模式是一种行为设计模式&#xff0c;旨在对命令的封装&#xff0c;根据不同的请求将方法参数化、延迟请求执行或将其放入队列中&#xff0c;且能实现可撤销操作。 (一) 解决问题 将请求发送者和接受者解耦&#xff0c;请求发送者只需知道如何发送请求&#xff…

LVGL 环境搭建-基于WSL

背景说明 小白刚开始接触LVGL&#xff0c;前些日子狠心花198元入手了一块堪称LVGL 入门利器~HMI-Board 开发板&#xff0c;虽然有RT-Thread 集成好的LVGL 环境&#xff0c;只需要几个步骤就能成功把lvgl 的示例运行起来&#xff0c;对于爱折腾的我来说&#xff0c;过于简单也并…

Sora专辑|AI视频制作新时代的曙光:OpenAI Sora 模型启示录

本文深入剖析 OpenAI 最新发布的人工智能视频生成模型 Sora 的工作原理,并探讨它对电影制作行业的深远影响。Sora 利用海量数据和强大的计算能力,学习视频的"语法规则"即物理定律,从而生成逼真的视频画面。Sora 将从根本上改变电影制作的方式,降低制作成本、赋能…

vue2、vue3各自的响应式原理

查看本专栏目录 关于作者 还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#x…

助力智能化农田作物除草,基于DETR(DEtection TRansformer)模型开发构建农田作物场景下玉米苗、杂草检测识别分析系统

在我们前面的系列博文中&#xff0c;关于田间作物场景下的作物、杂草检测已经有过相关的开发实践了&#xff0c;结合智能化的设备可以实现只能除草等操作&#xff0c;玉米作物场景下的杂草检测我们则少有涉及&#xff0c;这里本文的主要目的就是想要基于DETR模型来开发构建玉米…

【春运抢票攻略浅析】

参考 最全12306放票规则&#xff0c;抢票策略&#xff0c;候补作用2023年12306抢票攻略&#xff08;纯技巧&#xff09; 研究放票规则&#xff0c;候补的时候车次进行一下挑选&#xff0c;能够买长乘短的尽量买长&#xff0c;不要候补一些区间票吧&#xff0c;这是一开始放票…

LeetCode刷题---确认率

解题思路: 将Signups和Confirmations进行左连接&#xff0c;连接的条件为Signups.user_idConfirmations.user_id 根据题中要求进行查询&#xff0c;这里使用AVG聚合函数来求解确认率 AVG(c.action‘confirmed’)表示对action列进行求平均&#xff0c;如果action‘confirmed’&a…

【题解】—— LeetCode一周小结8

【题解】—— 每日一道题目栏 上接&#xff1a;【题解】—— LeetCode一周小结7 19.N 叉树的后序遍历 题目链接&#xff1a;590. N 叉树的后序遍历 给定一个 n 叉树的根节点 root &#xff0c;返回 其节点值的 后序遍历 。 n 叉树 在输入中按层序遍历进行序列化表示&#x…

深度学习目标检测】二十、基于深度学习的雾天行人车辆检测系统-含数据集、GUI和源码(python,yolov8)

雾天车辆行人检测在多种场景中扮演着至关重要的角色。以下是其作用的几个主要方面&#xff1a; 安全性提升&#xff1a;雾天能见度低&#xff0c;视线受阻&#xff0c;这使得驾驶者和行人在道路上的感知能力大大降低。通过车辆行人检测技术&#xff0c;可以在雾天条件下及时发现…

【Linux】head命令使用

head命令 head是一个在 Unix 和 Unix-like 操作系统中常用的命令行工具&#xff0c;用于输出文件的前 n 行。默认为 10&#xff0c;即显示 10 行的内容。 语法 head [options] [file(s)] head命令 -Linux手册页 选项及作用 执行令 &#xff1a; head --help 执行命令结果…

k8s二进制部署的搭建

1.1 常见k8s安装部署方式 ●Minikube Minikube是一个工具&#xff0c;可以在本地快速运行一个单节点微型K8S&#xff0c;仅用于学习、预览K8S的一些特性使用。 部署地址&#xff1a;Install Tools | Kubernetes ●Kubeadm Kubeadm也是一个工具&#xff0c;提供kubeadm init…

文心一言 VS 讯飞星火 VS chatgpt (203)-- 算法导论15.3 2题

二、对一个16个元素的数组&#xff0c;画出2.3.1节中MERGE-SORT过程运行的递归调用树。解释备忘技术为什么对MERGE-SORT这种分治算法无效。需要写代码的时候&#xff0c;请用go语言。 文心一言&#xff0c;代码不完整&#xff1a; 首先&#xff0c;让我们明确2.3.1节中的MERG…

docker创建mongodb数据库容器

介绍 本文将通过docker创建一个mongodb数据库容器 1. 拉取mongo镜像 docker pull mongo:3.63.6版本是一个稳定的版本&#xff0c;可以选择安装此版本。 2. 创建并启动主数据库 容器数据卷配置 /docker/mongodb/master/data # 数据库数据目录&#xff08;宿主机&am…

formality:set_constant应用

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 往期文章链接: formality:形式验证流程 scan mode func的功能检查需要把scan mode设置成0。

请求包的大小会影响Redis每秒处理请求数量

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容压测规划客户端长连接数量对性能的影响请求包大小的影响Pipleline模式对Redis的影响 &#x1f4e2;文章总结&#x1f4e5;博主目标 &#x1f50a;博主介绍 &#x1f31f;我是廖志伟&#xff0c;一名Java开发工程师、Java领…

第 2 章 微信小程序的构成 (代码导读)断更,后续继续更新

2.1 小程序项目的基本结构 Hello World&#xff01;程序.mp4 文泉云盘 -- 图书二维码资源管理系统兆泰源二维码管理系统https://www.wqyunpan.com/resourceDetail.html?id284928&openIdoUgl9wdyNYHu9EcAe-GEwbQdZilY&qrcodeId242916&signc2lnbm1PUmNxSndPWGFOck…

51.仿简道云公式函数实战-文本函数-JOIN

1. JOIN函数 JOIN 函数可通过连接符将数组的值连成文本。 2. 函数用法 JOIN(数组,"连接符") 3. 函数示例 如需将复选框中勾选的选项通过”-“组合在一起&#xff0c;则可设置公式为JOIN(复选框组,"-") 4. 代码实战 首先我们在function包下创建text包…

【数据结构】【双堆】【滑动窗口】3013. 将数组分成最小总代价的子数组 II

作者推荐 动态规划的时间复杂度优化 本文涉及的基础知识点 C算法&#xff1a;滑动窗口总结 数据结构 双堆 LeetCode3013. 将数组分成最小总代价的子数组 II 给你一个下标从 0 开始长度为 n 的整数数组 nums 和两个 正 整数 k 和 dist 。 一个数组的 代价 是数组中的 第一个…

JSON简介以及如何在Python中使用JSON

什么是JSON&#xff1f; JSON是"JavaScript Object Notation"的简称&#xff0c;是一种数据交换格式 JSON格式 假设我们有一个对象&#xff0c;这个对象有两个属性&#xff1a;“name”跟“age”。 在JSON中是这样表达的&#xff1a; { "name":"男孩…