【C++】哈希表模拟:闭散列技术与哈希冲突处理

在这里插入图片描述

C++语法相关知识点可以通过点击以下链接进行学习一起加油!
命名空间缺省参数与函数重载C++相关特性类和对象-上篇类和对象-中篇
类和对象-下篇日期类C/C++内存管理模板初阶String使用
String模拟实现Vector使用及其模拟实现List使用及其模拟实现容器适配器Stack与QueuePriority Queue与仿函数
模板进阶-模板特化面向对象三大特性-继承机制面向对象三大特性-多态机制STL 树形结构容器二叉搜索树
AVL树红黑树红黑树封装map/set哈希-开篇

在上一篇《哈希之路:序篇的知识启航》中,我们简要介绍了哈希方法及哈希表的基础概念。本篇将进一步探讨如何利用闭散列技术有效解决哈希冲突,并通过模拟实现哈希表的过程,深入解析这一关键技术。

请添加图片描述
Alt
🌈个人主页:是店小二呀
🌈C语言专栏:C语言
🌈C++专栏: C++
🌈初阶数据结构专栏: 初阶数据结构
🌈高阶数据结构专栏: 高阶数据结构
🌈Linux专栏: Linux

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅 请添加图片描述

文章目录

  • 前文
  • 一、闭散列
    • 1.1 线性探测(采用闭散列其中一种办法)
      • 1.1.1 操作方面
      • 1.1.2 状态标记
  • 二、实现哈希表
    • 2.1 哈希基本构架
    • 2.2 哈希表插入数据
    • 2.3 哈希表扩容逻辑
    • 2.4 哈希表扩容需要换表
    • 2.5 复用插入逻辑
    • 2.6 哈希表查找元素
    • 2.7 哈希表删除数据
  • 三、除留余数法出现类型问题
    • 3.1 类型问题分析
    • 3.2 简单类型做key
    • 3.3 string类型做key
      • 3.3.1 BKDR算法
      • 3.3.2 string模板特化
    • 3.4 复杂类型做key
  • 散列表头文件

前文

1.目前哈希使用方面存在【一些问题】:

  1. 如果数据很分散,容易导致空间浪费。
  2. 有些值不好映射:比如string,结构体对象。

2.采取哈希函数为【除留余数法】:

除留余数法去进行空间浪费的问题,除留余数法空间跟值得范围无关。

3.【除留余数法】 :

散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,在这里插入图片描述
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

一、闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去

在这里插入图片描述

注意:这种方式属于强盗逻辑,如果我的位置没有了,就需要去抢夺别人位置。

1.1 线性探测(采用闭散列其中一种办法)

场景:现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

1.1.1 操作方面

插入】:

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

删除】:

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素

1.1.2 状态标记

由于采用闭散列处理哈希冲突,如果直接删除元素会影响其他元素查找,同时在插入数据中我们需要一个状态标识判断该位置是否存在数据,是否可以在该位置进行插入逻辑,同时需要注意删除元素设置状态应该为删除,而不是为空,去满足实际的状态。

enum State{EMPTY,EXSIT,DELETE};

二、实现哈希表

2.1 哈希基本构架

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

    };
template<class K, class V>
    class HashTable
    {
        public:

        private:
        vector<HashData<K, V>> _tables;
        size_t _n = 0;
    };

哈希是通过哈希函数使得元素的存储位置与它的关键码之间能够建立一一映射的关系,需要使用pair<K,V>类型进行存储。采用vector作为底层逻辑,存储元素类型为哈希节点类型HashData<K, V>。

这里不采用size作为哈希表中有效元素个数,考虑到容器中结构的差异性,是由于_ size一般用于序列式容器中表示有效元素个数,在关联式容器中命名约定一般规定_n作为记录有效元素个数

2.2 哈希表插入数据

bool Insert(const pair<K, V>& kv)
{
    size_t hashi = kv.first % _tables.size();

    //如何判断是否删除,是否继续查找,通过标记
    while (_tables[hashi]._state == EXSIT)
    {
        hashi++;
        hashi %= _tables.size();
    }
    _tables[hashi]._kv = kv;
    _tables[hashi]._state = EXSIT;
    _n++;

    return true;
}

在插入过程,元素需要通过除留余数法找到对应位置进行插入,期间可能会出现哈希冲突的问题,我们需要以该位置向后寻找状态标记为空的位置进行插入。

在向后寻找位置途中可能存在越界情况,这里采用处理循环队列方式进行取模操作,保证数据在合法范围进行查找。这里存在一个大前提,空间是充足的,不然找半天都找不到位置。

2.3 哈希表扩容逻辑

在这里插入图片描述

由于哈希表特殊的结构,在进行哈希表扩容操作时,并不采用空位置被填满才进行扩容,而是采用载荷因子的概念进行控制,否则用于空间过少,发生哈希冲突问题频繁,导致效率降低。

可以理解为:负载因子越小,冲突率越低,效率就越高,空间利用率就越低

扩容条件判断的问题】:if (_n / _tables.size() > = 0. 7)

用于这里涉及类型问题,可以采用类型装换或者乘个十

  1. if ((double)_n / _tables.size() > = 0. 7)
  2. if (_n * 10 / _tables.size() >= 7)

问题】:这里n是除以size还是capacity?
如果选择除以capacity空间容量,可能会导致越界访问。当然如果不知道除以size还是capacity,可以在构造函数先为_tables开辟空间,避免了这个问题的发生,同时防止了size出现等于零的风险。

2.4 哈希表扩容需要换表

当哈希表进行扩容逻辑时,导致了散列表长度发生了变换。这也意味着通过哈希函数(开发定址法)得到的位置需要重新安排插入。在哈希进行扩容操作时,整个过程中消耗最大的时候,需要开辟空间和插入数据,重新进行映射到新表中。

在这里插入图片描述

2.5 复用插入逻辑

在扩容操作需要插入数据,需要进行哈希函数的处理,但是在插入操作中已经存在哈希函数进行处理的逻辑,如果选择重新书写哈希函数显得有点冗余

在这里插入图片描述

bool Insert(const pair<K, V>& kv)
{

    //建立在空间充足及其目前不存在该数据基本上
    size_t hashi = kv.first % _tables.size();

    //扩容逻辑,这里涉及到负载因子拉
    if (_n * 10 / _tables.size() >= 7)
    {

        HashTable<K, V> NewHT;
        //插入逻辑,但是这里我们选择复用,不用我们去判断
        NewHT._tables.resize(_tables.size() * 2);

    }
    //如何判断是否删除,是否继续查找,通过标记
    while (_tables[hashi]._state == EXSIT)
    {
        hashi++;
        hashi %= _tables.size();
    }
    _tables[hashi]._kv = kv;
    _tables[hashi]._state = EXSIT;
    _n++;

    return true;
}

2.6 哈希表查找元素

HashData<K, V>* Find(const K& key)
{
    size_t hashi = key % _tables.size();
    //这里本身就是一个循环判断语句
    while (_tables[hashi]._state == EXSIT)
    {
        if (key == _tables[hashi]._kv.first && _tables[hashi]._state == EXSIT)
        {
            return &_tables[hashi];
        }
        hashi++;
        hashi %= _tables.size();
    }

    return nullptr;
}

关于哈希表进行查找逻辑是比较容易,其中我想分享一个在设计遇到的问题。在设计状态标记时,只考虑了存在EXSIT和空EMPTY两个状态,这导致了当删除某个元素时,查找过程中无法判断找到数据及其是否需要继续前进,需要DELET标记。

2.7 哈希表删除数据

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

哈希表删除数据是我们遇到最简单的删除逻辑,只需要改变位置状态就可以了。

三、除留余数法出现类型问题

除留余数法:size_t hashi = key % _tables.size();

3.1 类型问题分析

对于key进行取模查找,是建立在key属于无符号整型类型来考虑的,但是我们设计属于泛型编程,key需要去适应不同种类型:string类型,自定义类型,负数。

这种场景需要使用仿函数进行解决,key不支持强转整型取模,那么就要自己提供换成整型仿函数

3.2 简单类型做key

在这里插入图片描述

3.3 string类型做key

关于string类型转化为整型类型,这里不推荐使用stoi函数,从编码角度上,对于中文字符底层是通过多个字符对应一个编码表里面的汉字,一旦不采用这张表会出现乱码的情况。

虽然拿string首字母转化伪ASCII码值可行,但是很容易发生哈希冲突。

3.3.1 BKDR算法

最初考虑将字符串中所有字符的ASCII码值相加,以优于仅考虑首字母的效果。然而,这种方法存在缺陷:不同字符组合可能具有相同的ASCII码总和。因此,我们将采用BKDR算法进行优化。

在这里插入图片描述

3.3.2 string模板特化

由于string作key是很常见的情况,</这里我们可以对string模板特殊化。(模板特化建立在存在模板的情况下

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

3.4 复杂类型做key

如果出现复杂类型做key,这种情况一般是自定义类型,比如容器类、学生信息,我们都可以考虑将类中成员相加起来或者独特项,出来的数据不太唯一

struct Person
{
	string _name;
    int _age;
};

散列表头文件

#pragma once
#include <iostream>
using namespace std;
#include <vector>

    template<class K>
        struct HashFunc
        {
            size_t operator()(const K& key)
            {
                return (size_t)key;
            }
        };

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

namespace HashTable
{
    enum State
    {
        EMPTY,
        EXSIT,
        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:
            //这里只能构造成空的容器,等待数据插入。我们需要进入的元素是pair类型,K和V是明面上的
            HashTable()
            {
                //避免size和capacity问题
                _tables.resize(10);
            }


            bool Insert(const pair<K, V>& kv)
            {
                if (Find(kv.first) == nullptr)
                {
                    return false;
                }
                Hash hs;
                //建立在空间充足及其目前不存在该数据基本上

                size_t hashi = hs(kv.first) % _tables.size();

                //扩容逻辑,这里涉及到负载因子拉
                if (_n * 10 / _tables.size() >= 7)
                {

                    HashTable<K, V> NewHT;
                    //插入逻辑,但是这里我们选择复用,不用我们去判断
                    NewHT._tables.resize(_tables.size() * 2);

                }
                //如何判断是否删除,是否继续查找,通过标记
                while (_tables[hashi]._state == EXSIT)
                {
                    hashi++;
                    hashi %= _tables.size();
                }
                _tables[hashi]._kv = kv;
                _tables[hashi]._state = EXSIT;
                _n++;

                return true;
            }

            HashData<K, V>* Find(const K& key)
            {
                Hash hs;
                size_t hashi = hs(key) % _tables.size();
                //这里本身就是一个循环判断语句
                while (_tables[hashi]._state == EXSIT)
                {
                    if (key == _tables[hashi]._kv.first && _tables[hashi]._state == EXSIT)
                    {
                        return &_tables[hashi];
                    }
                    hashi++;
                    hashi %= _tables.size();
                }

                return nullptr;
            }

            bool Erase(const K& key)
            {
                HashData<K, V>* ret = Find(key);
                if (ret)
                {
                    ret->_state = DELETE;
                    _n--;
                    return true;
                }
                else
                {
                    return false;
                }
            }
            private:
            vector<HashData<K, V>> _tables;
            size_t _n = 0;
        };

    void TestHT1()
    {
        int a[] = { 10001,11,55,24,19,12,31 };
        HashTable<int, int> ht;
        for (auto e : a)
        {
            ht.Insert(make_pair(e, e));
        }

        cout << ht.Find(55) << endl;
        cout << ht.Find(31) << endl;

        ht.Erase(55);
        cout << ht.Find(55) << endl;
        cout << ht.Find(31) << endl;
    }
}

在这里插入图片描述

以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SketchUp 云渲染—助力您的渲染

目前市面上的渲染平台有很多&#xff0c;但是能支持SketchUp云渲染的特别少&#xff0c;大部分云渲染是还是不支持的&#xff0c;今天就给大家介绍国内支持Sketchup渲染的云渲染——【渲染101】云渲染的使用方法。 1、官网下载最新的客户端并且安装。 2、登录客户端配置好对应…

栈和队列(2)——队列

队列的基本概念 1. 队列定义&#xff1a;只允许在一端进行插入&#xff0c;在另一端进行删除的线性表。 2. 队列特点&#xff1a;先进先出&#xff08;FIFO&#xff09;。 3. 队列基本操作&#xff1a;初始化队列、销毁队列、入队、出队、读队头元素、判队列空等。 InitQueue…

凭什么你说不是就不是-zzj杯·UMLChina建模答题赛第6赛季第2轮

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 参考潘加宇在《软件方法》和UMLChina公众号文章中发表的内容作答。在本文下留言回答。 只要最先答对前3题&#xff0c;即可获得本轮优胜。 如果有第4题&#xff0c;第4题为附加题&am…

【hacker送书第14期】AI训练师算法与模型训练从入门到精通

全面精通人工智能训练&#xff0c;成为行业领先、更懂AI的人&#xff01; 前言内容简介总结参与方式 前言 在人工智能&#xff08;AI&#xff09;技术日益成熟的今天&#xff0c;AI训练师成为了一个新兴且重要的职业。他们不仅需要掌握AI的核心技术&#xff0c;还要能够将这些…

一文详细讲解进销存系统(附架构图、流程、功能介绍)

企业经营的七大要素是“人、财、物、产、供、销、存”&#xff0c;进销存管理就占到了其中的多项。然而&#xff0c;许多企业在进销存管理方面面临着诸多痛点问题&#xff0c;例如库存管理混乱、采购销售流程不清晰、数据不准确等。这些问题不仅影响企业的运营效率&#xff0c;…

如何在Python爬虫等程序中设置和调用http代理

在Python爬虫中为了更好地绕过反爬机制&#xff0c;获取网页信息&#xff0c;有时可能需要在Python中应用代理服务&#xff0c;这样做的目的就是防止自己的ip被服务器封禁&#xff0c;造成程序运行时中断连接&#xff0c;那么如何在python中设置代理呢&#xff1f; 我们通过几个…

2024年【浙江省安全员-C证】试题及解析及浙江省安全员-C证复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【浙江省安全员-C证】试题及解析及浙江省安全员-C证复审考试&#xff0c;包含浙江省安全员-C证试题及解析答案和解析及浙江省安全员-C证复审考试练习。安全生产模拟考试一点通结合国家浙江省安全员-C证考试最新…

8、Node.js Express框架

五、Express框架 5.1概念 Express框架是一个基于Node.js平台的极简、灵活的WEB开发框架:www.express.com.cn 简单来说,Express是一个封装好的工具包,封装了很多功能,便于我们开发WEB应用 5.2安装 npm i express5.3 Express初体验 //01-express初体验.js //1.导入exrp…

Python(包和模块)

包 定义 包是将模块以文件夹的组织形式进行分组管理的方法&#xff0c;以便更好地组织和管理相关模块。 包是一个包含一个特殊的__init__.py文件的目录&#xff0c;这个文件可以为空&#xff0c;但必须存在&#xff0c;以标识目录为Python包。 包可以包含子包&#xff08;子…

万方数据库功能亮点介绍及个人下载万方论文的方法

一、万方数据库介绍 万方数据知识服务平台是北京万方数据股份有限公司主要产品之一。该平台整合数亿条全球优质学术资源&#xff0c;集成期刊、学位、会议、标准、专利等十余种资源类型、品质知识资源、先进的发现技术、人性化设计于一身&#xff0c;是国内一流的品质知识资源…

18 实战:基于Tkinter和OpenCV的视频编码器:实现MPEG4矩形帧编码器

引言 在视频处理领域,视频编码器的设计与实现一直是研究的热点。本文将深入解析一段基于Python的代码,该代码利用Tkinter、OpenCV和NumPy库构建了一个MPEG4矩形帧编码器的图形用户界面(GUI)。通过详尽的代码讲解,帮助读者全面理解视频编码的基本原理及其在实际应用中的实…

12-Docker发布微服务

12-Docker发布微服务 Docker发布微服务 搭建SpringBoot项目 新建一个SpringBoot项目 选择依赖项Spring Web和Spring Boot Actuator 在com.qi.docker_boot下创建controller目录&#xff0c;并在该目录下创建OrderController的java类 OrderControllerjava类的内容如下&#xf…

【IEEE出版|:IEEE Xplore,EI Compendex,Scopus检索|征稿正在进行中!】

第七届机械工程与智能制造国际会议&#xff08;WCMEIM 2024&#xff09; 2024 7th World Conference on Mechanical Engineering and Intelligent Manufacturing 【会议信息】 会议日期&#xff1a;2024年11月15-17日 会议地点&#xff1a;中国武汉&#xff08;武汉纺织大学…

如何成为开源代码库Dify的contributor:解决issue并提交PR

前言 Dify 是一个开源的大语言模型&#xff08;LLM&#xff09;应用开发平台&#xff0c;它融合了后端即服务&#xff08;Backend as Service&#xff09;和LLMOps的理念&#xff0c;旨在简化和加速生成式AI应用的创建和部署。Dify提供了一个用户友好的界面和一系列强大的工具…

前端如何安全存储密钥,防止信息泄露

场景 把公钥硬编码在前端代码文件里&#xff0c;被公司安全检测到了要整改&#xff0c;于是整理几种常见的前端密钥存储方案。 1. 设置环境变量再读取 在打包或部署前端应用时&#xff0c;可以将密钥配置为环境变量&#xff0c;在应用运行时通过环境变量读取密钥。这样可以将密…

深入了解 Three.js 中的材质与光照

开发领域&#xff1a;前端开发 | AI 应用 | Web3D | 元宇宙 技术栈&#xff1a;JavaScript、React、ThreeJs、WebGL、Go 经验经验&#xff1a;6年 前端开发经验&#xff0c;专注于图形渲染和AI技术 开源项目&#xff1a;github 晓智元宇宙、数字孪生引擎、前端面试题 大家好&am…

【Linux】网络基础常识{OSI七层模型 TCPIP 端口号 各种协议}_哪种nat类型适用于多个内部设备共享有限的公共ip地址

文章目录 1.网络常识 1.0DHCP协议1. 1IP地址/MAC地址/ARP协议是什么&#xff1f; IP/MACARP&#xff1a;IP ⇒ MAC 1.2手机连接wifi的原理 SSID与BSSID 手机连接wifiSSID与BSSID 1.3手机如何通过“数据/流量”上网&#xff1f;1.4电脑连接wifi的原理&#xff1f;电脑通过热点…

uniapp使用uni-push模拟推送

uniapp使用uni-push模拟推送 第一步先去uniapp开发者中心添加开通uni-push功能 这里的Android 应用签名可以先用测试的官网有,可以先用这个测试 官方测试链接文档地址 在项目中的配置文件勾选 组件中使用 如果要实时可以去做全局ws //消息推送模版uni.createPushMessage(…

ai画质修复工具有哪些?这4款AI照片修复神器建议收藏!

在当今这个科技迅猛发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;正以前所未有的速度重塑我们的日常生活&#xff0c;而照片修复领域正是AI技术大放异彩的舞台。从年代久远、泛黄的老照片到追求极致细节的现代摄影佳作&#xff0c;AI以其非凡的能力&#xff0c;成…

MES管理系统在工艺管理中具备哪些作用

在现代制造业的洪流中&#xff0c;MES管理系统正逐步成为工艺管理领域的一股强大力量&#xff0c;它不仅革新了传统的管理方式&#xff0c;还为企业带来了前所未有的效率提升与成本控制优势。尽管许多企业尚未全面拥抱这一数字化变革&#xff0c;但MES管理系统在工艺管理中的潜…