哈希表的底层实现(1)---C++版
目录
哈希表的基本原理
哈希表的优点
哈希表的缺点
应用场景
闭散列法
开散列法
开放定值法Open Addressing——线性探测的模拟实现
超大重点部分评析
链地址法Separate Chaining——哈希桶的模拟实现
哈希表(Hash Table)是一种数据结构,它通过将键(Key)映射到值(Value)的方式来实现快速的数据存储与查找。哈希表的核心概念是哈希函数,该函数用于将输入的键转换为哈希值(通常是一个整数),并根据这个哈希值将数据存储在数组中的特定位置。
哈希表的基本原理
-  
哈希函数:这是哈希表的关键部分,它接收一个键并返回一个整数(哈希值)。理想情况下,哈希函数会将不同的键均匀地分布到数组的不同位置上。
 -  
数组(桶,Bucket):哈希表通常是基于数组实现的,哈希函数的输出决定了键值对应该存储在数组中的哪个位置。
 -  
处理冲突:由于不同的键可能会生成相同的哈希值(称为哈希冲突),需要有机制来处理这些冲突。常见的处理方式包括:
- 链地址法(Separate Chaining):每个数组位置存储一个链表,所有哈希值相同的键值对都存储在同一个链表中。
 - 开放地址法(Open Addressing):当冲突发生时,查找数组中的下一个空闲位置存储键值对,常见的方式包括线性探测、二次探测和双重散列。
 
 -  
查找和插入:哈希表允许以 O(1) 的时间复杂度进行查找和插入操作(在理想情况下),即只需一次哈希函数计算和一次数组访问即可找到或插入数据。
 
哈希表的优点
- 快速的查找、插入和删除:在平均情况下,哈希表的查找、插入和删除操作都是常数时间(O(1)),这使其非常高效。
 - 简单实现:哈希表相对容易实现,且不需要复杂的数据结构操作。
 
哈希表的缺点
- 空间浪费:如果哈希表的负载因子(存储的元素数量与数组大小的比值)较高,容易导致冲突增加,从而降低性能。为避免冲突,通常会使用更大的数组,这会导致空间浪费。
 - 无法有序遍历:哈希表中的数据是无序的,如果需要顺序访问数据,需要额外的处理。
 - 哈希函数质量:哈希表的性能高度依赖于哈希函数的质量,差劲的哈希函数可能导致较多的冲突。
 
应用场景
哈希表在许多场景中广泛使用,包括但不限于:
- 字典实现:例如,Python 中的字典(dict)就是通过哈希表实现的。
 - 缓存:哈希表常用于实现缓存机制,如 LRU 缓存。
 - 集合操作:例如,在查找某个元素是否在集合中时,哈希表可以显著提高效率。
 - 位图:略
 - 布隆过滤器:略
 
通过哈希表,可以在大型数据集上快速执行查找、插入和删除操作,这使得它在实际应用中非常实用。
所以依照哈希表的基本原理可以看出哈希表的底层结构可以分为以下两种,这两种方法的本质都是除留余数法:
闭散列法
闭散列法也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置 呢? 比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入: 通过哈希函数获取待插入元素在哈希表中的位置 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素。
删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影 响。因此线性探测采用标记的伪删除法来删除一个元素。

上面有一个名词为线性探测,就是当待插入元素出现哈希冲突时,一个一个往下寻找空位置称为线性探测,每次n的固定次方倍往下找称为二次探测,两种探测的本质解决的问题完全一致,所有我们就只使用线性探测进行实现。
开散列法
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。代码总体实现的功能和上面的闭散列法差不多。

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开放定值法Open Addressing——线性探测的模拟实现
#include<iostream>
 #include<vector>
 using namespace std;
//namespace hashbucket//包一个命名空间就可以调用了
 //{
    template<class K>
     struct Hashfunc
     {
         size_t operator()(const K& key)
         {
             return (size_t)key;//由于像当K为string时不可以直接强制转换成整型,所以如果哈希表需要插入string类型的数据就需要写string的特化版本,如下
        }
     };
     //struct StringHashFunc
     //{
     //    size_t operator()(const string& s)
     //    {
     //        size_t hash = 0;
     //        for (auto e : s)
     //        {
     //            hash *= 31;
     //            hash += e;
     //        }
     //
     //        return hash;
     //    }
     //};
    template<>
     struct Hashfunc<string>//可以直接在这里写,模板的特化前提是前面有写过了,且名称一样,遇到和特化吻合的会优先匹配特化版本
    {
         size_t operator()(const string& key)
         {
             size_t hash = 0;
             for (auto e : key)
             {
                 hash += e;
                 hash *= 31;//根据某篇文章可得多乘31可以有效避免很多重复
            }
             return hash;
         }
     };
     enum status
     {
         EXIST,
         EMPTY,
         DELETE//三个状态,这里定义了哈希表中的数据的多个状态,在红黑树的模拟实现中有用过,这样方便判断此位置的数据状态,方便以后的线性探测和删除数据等
    };
    template<class K, class V>
     struct Hashdata
     {
         pair<K, V> _kv;
         status s = EMPTY;//刚开始时所有空位的状态都是EMPTY
    };
    template<class K, class V, class Hash = Hashfunc<K>>
     class Hashtable
     {
     public:
         Hashtable()
         {
             _table.resize(10);//直接在初始化时开10个空间
        }
         Hash ha;//仿函数
        bool insert(const pair<K, V>& kv)
         {
             if (Find(kv.first))
             {
                 return false;
             }
             if (10 * n / _table.size() >= 7)
             {
                 //vector<HashData<K, V>> newTables(_tables.size() * 2);
                  遍历旧表, 将所有数据映射到新表
                  ...
                 //_tables.swap(newTables);
                 //以上方法无法复用insert
                Hashtable<K, V, Hash> newht;//是KV
                newht._table.resize(2 * _table.size());//_table肯定是类里的,不然要指定的
                for (int i = 0; i < _table.size(); i++)
                 {
                     newht.insert(_table[i]._kv);//复用
                }
                 _table.swap(newht._table);//交换
            }
             size_t hashi = ha(kv.first) % _table.size();//非整型不能取%,所以使用仿函树转换一下
            while (_table[hashi].s == EXIST)
             {
                 hashi++;
                 hashi %= _table.size();//hashi不断的++的时候有可能会超出整个vector的数据的长度,所以需要再%_table.size()使其处在最后时下一步回到数据的开头,所以可以看出hashi是不断循环的。
            }
             _table[hashi]._kv = kv;
             _table[hashi].s = EXIST;
             n++;//每成功插入一个数据,其负载因子就加1
            return true;
         }
         vector<Hashdata<K, V>>* begin() const
         {
             return &_table[0];
         }
        vector<Hashdata<K, V>>* end() const
         {
             return &_table[_table.size()];
         }
         Hashdata<K, V>* Find(const K& key)
         {
             size_t hashi = ha(key) % _table.size();
             size_t n = hashi;
             while (_table[hashi].s != DELETE)
             {
                 if (_table[hashi]._kv.first == key && _table[hashi].s == EXIST)//由于下面的删除逻辑为不删除数据只改名数据的状态所以当找到相等时,这个数据有可能会有两种状态:EXIST和DELETE
                {
                     return &_table[hashi];
                 }
                 hashi++;
                 hashi = hashi % _table.size();
                 if (hashi == n)
                 {
                     return nullptr;
                 }
             }
             return nullptr;
         }
        bool Erase(const K& key)//只要传一个K就可以了
        {
             Hashdata<K, V>* ret = Find(key);
             if (ret)
             {
                 ret->s == DELETE;
                 return true;
             }
             return false;
         }
     private:
         vector<Hashdata<K, V>> _table;
         size_t n = 0;//表中存储数据个数, n为负载因子
    };
    
//}
超大重点部分评析

这边说一下扩容的逻辑,由于插入逻辑里面hashi是不断循环的,所以当哈希表里面数据都占满时,hashi会陷入死循环,但是就算没有全部占满,如果已占数据过多就会使得下一个数据在插入时哈希冲突过多使时间复杂度变大,所以为了避免这种情况,引入负载因子,当插入数据占比>=百分之70时,也就是负载因子/空间数据容纳最大个数 == 0.7时,将容器的容量扩大成原来的双倍。
再观察上面的扩容代码,为什么不能直接再原空间之间扩容呢,需要另开一个空间再交换回去,因为如果之间在原空间扩容,会使得size(数据最大个数)变大使得原本的数据的映射乱了。像现在这种扩容写法交换swap之后,由于新创建的临时变量newht出函数体就会销毁了,也就会顺带释放掉swap交给它的原空间,这样扩容之后的空间就保留下来了,所以不需要考虑旧表没有被释放的问题。

最后注意一下这个Erase的写法,很多人可能会认为删除某个元素需要像之前顺序表的那种写法(毕竟每个数据就在顺序表里面),删除了这个数据,让这个数据后面的数据依次往前移,时间复杂度就是O(n),这里不是不可以,但是如果你这么写就麻烦了,因为每个数据都外带了一个数据状态,按顺序表那么写的话就忽略的状态这个东西。
链地址法Separate Chaining——哈希桶的模拟实现
预知后事如何,请持续关注本系列内容!!!
相关文章:
哈希表的底层实现(1)---C++版
目录 哈希表的基本原理 哈希表的优点 哈希表的缺点 应用场景 闭散列法 开散列法 开放定值法Open Addressing——线性探测的模拟实现 超大重点部分评析 链地址法Separate Chaining——哈希桶的模拟实现 哈希表(Hash Table)是一种数据结构&#x…...
如何使用PTK一键安装opengaussdb 5.0
1、关于PTK工具 MogDB数据库是云和恩墨基于openGauss开源数据库打造,安稳易用的企业级关系型数据库。 PTK是云和恩墨出品的一款工具,帮助用户更便捷地部署管理MogDB数据库。 1.1 使用场景 开发人员快速启动多个本地 MogDB 环境用户通过 PTK 快速安装…...
跟李沐学AI:长短期记忆网络LSTM
输入们、遗忘门和输出门 LSTM引入输入门、忘记门和输出门 输入门计算公式为:。 遗忘门计算公式为:。 输出门计算公式为:。 它们由三个具有sigmoid激活函数的全连接层处理, 以计算输入门、遗忘门和输出门的值。 因此,…...
【BIM模型数据】BIM模型的数据如何存储,BIM大模型数据云端存储,需要考虑哪些因素,BIM模型数据存储和获取
【BIM模型数据】BIM模型的数据如何存储,BIM大模型数据云端存储,需要考虑哪些因素,BIM模型数据存储和获取 BIM文件的结构化数据和非结构化数据的存储方式,需要根据数据的特性和使用需求来选择。以下是一些推荐的存储策略࿱…...
【LLM大模型】大模型架构:layer\_normalization
2.layer_normalization 1.Normalization 1.1 Batch Norm 为什么要进行BN呢? 在深度神经网络训练的过程中,通常以输入网络的每一个mini-batch进行训练,这样每个batch具有不同的分布,使模型训练起来特别困难。Internal Covariat…...
PON光模块的独特类型和特性
在当前互联网需求快速增长的背景下,PON光模块已成为实现光纤网络高速数据传输的重要组成部分。从住宅宽带到各种企业应用程序解决方案,PON光模块始终致力于实现高质量的数据传输与无缝通信。了解PON光模块的类型和特性对于深入理解现代网络基础设施至关重…...
架构与业务的一致性应用:实现企业战略目标和合规管理的全面指南
在当今快速变化的数字经济中,信息架构已成为企业实现其业务目标、优化运营效率和确保数据安全的关键工具。 一个成功的信息架构不仅要与企业的战略目标紧密对齐,还必须遵循日益严格的合规性要求,以保护敏感数据并满足法规规定。《信息架构&a…...
时尚穿搭想换就换,各种风格一键完美搭配!亲测在线虚拟试衣换装平台效果超赞!
随着科技的发展,时尚领域也迎来了新的革命。传统的试衣方式逐渐被现代科技所取代,虚拟试衣间的出现使得用户可以在舒适的家中轻松体验不同的服装风格。 先前给大家也介绍过一些虚拟试衣的技术,例如AnyFit或者OutfitAnyone等,今天…...
【C++】C++ 标准库string类介绍(超详细解析,小白必看系列)
C 标准库中的 std::string 类是一个非常强大的工具,用于处理和操作字符串。它属于 <string> 头文件,并提供了一套丰富的功能和方法。以下是 std::string 类的一些主要特性和常用操作: 1 string简介 字符串是表示字符序列的类 标准的字…...
若依RuoYi项目环境搭建教程(RuoYi-Vue + RuoYi-Vue3版本)
文章目录 一、开发脚手架选择二、RuoYi框架1、介绍2、版本发展3、为什么选择若依4、优缺点5、项目内置功能 三、后端项目部署1、拉取源码2、环境要求3、Maven构建4、MySQL相关(1)导入SQL脚本(2)配置信息 5、Redis相关(…...
Java 后端接口入参 - 联合前端VUE 使用AES完成入参出参加密解密
加密效果: 解密后的数据就是正常数据: 后端:使用的是spring-cloud框架,在gateway模块进行操作 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30…...
HarmonyOS开发之使用PhotoViewPicker(图库选择器)保存图片
一:效果图 二:添加依赖 import fs from ohos.file.fs;//文件管理 import picker from ohos.file.picker//选择器 三:下载,保存图片的实现 // 下载图片imgUrldownloadAndSaveImage(imgUrl: string) {http.createHttp().request(…...
跨境独立站支付收款常见问题排雷篇1.0丨出海笔记
最近小伙伴们在社群讨论挺多关于独立站支付问题的,鉴于不少朋友刚接触独立站,我整理了一些独立站支付相关的问题和解决方案,供大家参考,百度网上一堆媒体的那些软文大家就别看了,都是软广或者抄来抄去,让大…...
uni-app实现web-view和App之间的相互通信
双向实时 如果app端部署成网站,则web-view就是iframe,使用也可以双向通讯 https://uniapp.dcloud.net.cn/component/web-view.html APP端代码 index.vue: <template><web-viewid"m-webview":fullscreen"true":src"…...
HTB-Vaccine(suid提权、sqlmap、john2zip)
前言 各位师傅大家好,我是qmx_07,今天来为大家讲解Vaccine靶机 渗透过程 信息搜集 服务器开放了 21FTP服务、22SSH服务、80HTTP服务 通过匿名登录FTP服务器 通过匿名登录到服务器,发现backup.zip文件,可能存在账号密码 发现b…...
【达梦数据库】异构数据库迁移到达梦
目录 1、迁移准备2、正式迁移3、问题处理3.1、return附近出现错误3.1.1、排查过程3.1.2、问题原因3.1.2、解决方法 3.2、对象[XXX]处于无效状态-类型13.2.1、排查过程3.2.2、问题原因3.2.3、解决方法 3.3、对象[XXX]处于无效状态-类型23.3.1、排查过程3.3.2、问题原因3.3.3、解…...
抽象类和接口(1)
抽象类: 什么是抽象类: 听着就很抽象,确实挺抽象,先来写一个抽象类感觉一下: 这就是抽象类! 在 Java 中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法…...
epoll内核原理与实现详解
目录 1 epoll相关理论基础 1.1 I/O多路复用技术 1.2 事件驱动模型 1.2.1 基本概念 1.2.2 优缺点分析 1.2.3 与epoll的关联 1.3 epoll机制简介 1.3.1 核心原理 1.3.2 优点 2 epoll内核原理 2.1 epoll数据结构 2.1.1 主要数据结构 2.1.2 数据结构关系 2.2 epoll工作…...
被低估的SQL
SQL是现代数据库管理系统中不可或缺的一部分。尽管它的使用已十分普遍,但在数据处理领域,SQL的某些功能和潜力仍然被许多人低估。接下来,小编将与您一起,探讨SQL的一些被忽视的特性,揭示它在数据管理中的真正实力。 1.…...
数字证书、数字签名及其关系
一.数字证书与数字签名 1.数字证书是一个经证书授权中心数字签名的包含公开密钥拥有者信息以及公开密钥的文件。简单地说,数字证书是一段包含用户身份信息、用户公钥信息以及份验证机构数字签名的数据。 通俗理解:数字证书相当于【身份证】 —— 确认你…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
R语言速释制剂QBD解决方案之三
本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...
MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...
消息队列系统设计与实践全解析
文章目录 🚀 消息队列系统设计与实践全解析🔍 一、消息队列选型1.1 业务场景匹配矩阵1.2 吞吐量/延迟/可靠性权衡💡 权衡决策框架 1.3 运维复杂度评估🔧 运维成本降低策略 🏗️ 二、典型架构设计2.1 分布式事务最终一致…...
k8s从入门到放弃之HPA控制器
k8s从入门到放弃之HPA控制器 Kubernetes中的Horizontal Pod Autoscaler (HPA)控制器是一种用于自动扩展部署、副本集或复制控制器中Pod数量的机制。它可以根据观察到的CPU利用率(或其他自定义指标)来调整这些对象的规模,从而帮助应用程序在负…...
