哈希表/散列表(HashTable)c++实现

news2024/11/20 12:26:20

目录

哈希表实现的思想

除留余数法 

哈希冲突

第一种方法:探测法实现哈希表

探测法的思想 

结点类 

插入数据(insert)

冲突因子

数据扩容

哈希值 

插入的代码实现以及哈希类

查找数据(find)

删除数据(erase)

第二种方法:拉链法实现哈希表

结点类

哈希类的成员

插入(insert)

扩容

插入数据

插入代码总结:

查找(find)

删除数据(erase)

删除代码总结:


哈希表实现的思想

除留余数法 

哈希表的实现方法是通过每个值映射出一个下标位置,再存储到一个数组中的下标所对应的位置(有点类似计数排序)

这个值我们叫做哈希值

如图:我们有一个数组

假如我们此时插入一个14,那么直接映射到数组对应下标即可,此时哈希值也为14

 如图数组中通过这种直接映射的方法我们可以直接存储哈希值为0-15的数据,并且数据所对应的哈希值就是它本身

但如果我们此时要在数组中存储一个大于15的数据(例如50)怎么办呢?

解决方法也很简单,前文中哈希值等于数据本身,那么我们此时让数据除(%)数组大小,而余下来的数据就为它的哈希值

如图我们在数组中插入一个50

 这种方法我们叫做除留余数法

哈希冲突

如图数组

我们分别使用除留余数法插入两个数组(0和16)试试

可以看到,此时0和16的哈希值冲突了,这也就是所谓的哈希冲突

而以下说的两种方法都是为了解决哈希冲突 

第一种方法:探测法实现哈希表

探测法的思想 

 探测法的思想很简单,就是如果哈希值发生冲突,那么就从这个哈希值的后面进行探测

探测下一个位置是否为空/删除状态,如果不是,则继续往后探测,直到遇到为空/删除状态为止

 如图数组

我们利用探测法 分别插入0,8,16

 结点类 

哈希表中数据存储的是一个键值对(pair)

而除了键值对pair以外,用探测法实现的哈希表还需要有一个变量来表示结点此时处于删除/空/存在状态,这个状态我们采用的是用枚举实现

于是结点类的实现如下

  enum State
  {
    EMPTY,//空
    DELETE,//删除
    EXIST//存在
  };

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

插入数据(insert)

在插入数据之前我们需要知道的是,哈希表是什么扩容的?存储满再扩容吗?

答案是并不是,原因是当vector的存储数据个数已经快满了以后,哈希表再进行探测插入的话探测的次数就会变多,而此时效率也会变低,所以我们需要在哈希表快满的时候就进行扩容,那么什么时候算快满了呢?

冲突因子

冲突因子 = 哈希表中有的数据 / 哈希表的大小

而我们通过控制冲突因子来控制扩容,当冲突因子的大小>某个值时,我们就进行扩容

这里的某个值跟vector的扩容一样,是由用户自定义的

一般我们设置为0.7即可

数据扩容

哈希表的扩容不能跟vector一样直接拷贝数据到新表

因为哈希表需要根据数据的哈希值映射到不同的地方存储

哈希值会根据表大小的不同算出不同的哈希值

例如:

96在原大小为10的的表中哈希值为6,但如果表大小扩到20,那么此时96映射的位置为16

既然如此如何处理呢?那么我们就需要先创建一个新表,然后把数据一个一个的重新映射到新表当中

哈希值 

此时还面临一个问题,我们以上举例都为整形家族的,可以直接求出哈希值,但如果这个类型不是整形家族的呢?例如:string、日期类。。。

那么此时我们需要一个仿函数来求出这个类型的哈希值

需要注意的是:这个类型通过仿函数求出的哈希值必须是整形家族的,而这个哈希值我们也必须在插入以后能再次找到

插入的代码实现以及哈希类

 //hash为仿函数模板
  template <class K, class V, class hash>
  class HashTable
  {
    typedef HashData<K, V> Data;

  private:
    vector<Data> _table;//哈希表
    size_t _count = 0;//有效数据的个数

  public:
     bool insert(const pair<K, V> &val)
    {
      //1、判断数据是否存在
      if (Find(val.first))
      {
        return false;
      }
      //2、判断是否扩容
      //因为_count / _table.size() = 浮点数,所以要把_count * 10 ,这里的冲突因子>=0.7就扩容
      if (_table.size() == 0 || _count * 10 / _table.size() >= 7)
      {
        HashTable<K, V, hash> newtable;
        size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
        newtable._table.resize(newsize);
        for (int i = 0; i < _table.size(); ++i)
        {
          newtable.insert(_table[i]._kv);
        }
        newtable._table.swap(_table);
      }
      //3、求出哈希值
      hash hs;
      size_t hashi = hs(val.first);
      hashi %= _table.size();
      //4、找到插入的位置
      while (_table[hashi]._state == EXIST)
      {
        hashi++;
        hashi %= _table.size();
      }
      _table[hashi]._kv = val;
      _table[hashi]._state = EXIST;
      _count++;
      return true;
    }
  };

 查找数据(find)

查找数据我们就把要查找的值的哈希值求出来,再直接通过哈希值映射到对应的下标后用探测法找数据即可

    Data *Find(const K key)
    {
      if (!_table.size())
      {
        return nullptr;
      }
      //求出哈希值并映射下标
      hash hs;
      size_t hashi = hs(key);
      hashi %= _table.size();
      //探测法找数据
      while (_table[hashi]._state == EXIST)
      {
        if (_table[hashi]._kv.first == key)
        {
          return &_table[hashi];
        }
        else
        {
          hashi++;
          hashi %= _table.size();
        }
      }
      return nullptr;
    }

删除数据(erase)

删除数据我们需要先找到这个数据,再把这个数据的状态设置为DELETE即可

    bool erase(const K& key)
    {
      Data* pNode = Find(key);
      if(pNode == nullptr)
      {
        //没找到
        return false;
      }
      else 
      {
        //找到了
        pNode->_state = DELETE;
        return true;
      }
    }

第二种方法:拉链法实现哈希表

第二种方法也是为了解决哈希冲突而实现的

我们知道探测法的解决办法是发生冲突的位置往后探测,直到遇到空/删除的位置插入即可

拉链法是把发生冲突的数据用一个链表串联起来

如图

 由上图可知,用拉链法实现的哈希表是由一个个链表组成的,所以此方法下的哈希表我们可以使用指针数组实现,并且这个链表我们用单链表实现即可

结点类

此方法下的结点类不需要再有状态因子了,只需要有数据+指针即可

template<class K,class V>
struct HashData
{
  std::pair<K,V> _kv;
  HashData<K,V>* _next;
  HashData(const std::pair<K,V> kv)
    :_kv(kv)
    ,_next(nullptr)
  {
  }
};

哈希类的成员

成员是由一个存储链表的数组实现的,并且还需要有一个值来存储有效数据的个数

template<class K,class V,class Hash = HashFunc<K>>
class HashTable
{
  typedef HashData<K,V> Data;
private:
  std::vector<Data*> _table;//表
  size_t _count = 0;//表内有效数据
public:
    //...
}

插入(insert)

插入需要分为两步:

第一步:考虑扩容

第二步:插入数据

扩容

扩容同样需要创建新表,但不需要把原表中的结点释放掉,因为原表中的每一个结点都是我们申请的,如果我们直接释放掉原表中的结点再重新开辟,显然就会导致效率变低

我们只需要把原表的结点重新映射到新表中即可

如下图

我们只需把表中的数据全部放到新表中,再把旧表里的结点全部置为空即可

插入数据

插入数据分为步:

第一步:求出哈希值

第二步:哈希值映射下标

第三步:单链表的插入

插入代码总结:

  bool insert(const std::pair<K,V> val)
  {
    //扩容
    if(find(val.first))
    {
      return false;
    }
    Hash hs;
    if(_count == _table.size())//冲突因子 = 1
    {
      size_t newCapacity = _table.size() == 0 ? 10 : 2 * _table.size();
      HashTable<K,V,Hash> newTable;
      newTable._table.resize(newCapacity);

      //拷贝一张表的数据
      for(size_t i = 0 ; i < _table.size() ; ++i)
      {
        Data* cur = _table[i];
        //拷贝一串链表的数据
        while(cur)
        {
          size_t hashi = hs(cur->_kv.first) % newTable._table.size();
          if(newTable._table[hashi])
          {
            cur->_next = newTable._table[hashi]->_next;
            newTable._table[hashi]->_next = cur;
          }
          else 
          {
            newTable._table[hashi] = cur;
          }
          cur = cur->_next;
        }
        _table[i] = nullptr;
      }
      *this = newTable;
    }

    //插入数据
    Data* node = new Data(val);
    size_t hashi = hs(node->_kv.first);
    hashi %= _table.size();
    if(_table[hashi])
    {
      node->_next = _table[hashi]->_next;
      _table[hashi]->_next = node;
    }
    else 
    {
      _table[hashi] = node;
    }
    _count++;
    return true;
  }
};

查找(find)

    Data* find(const K& key)
    {
        Hash hs;
        size_t hashi = hs(key);
        hashi %= _table.size();
        Data* cur = _table[hashi];
        while (cur)
        {
            if (cur->_kv.first == key)
            {
                return cur;
            }
            cur = cur->_next;
        }
        return nullptr;
    }

 删除数据(erase)

删除数据也并不复杂,首先我们需要找到数据,再用删除单链表结点的方式进行删除即可

我们需要求出哈希值找到对应结点所在的链表,再用cur逐个遍历这个链表,并且还需要一个prev结点来记录cur的前一个结点,方便单链表的中间删除

当找到数据后要分为两种情况:

第一种情况:结点是所在链表的头节点

如图

第二种情况:结点不是所在链表的头节点

如图

 删除代码总结:

    bool erase(const K& key)
    {
        Hash hs;
        size_t hashi = hs(key);
        hashi %= _table.size();
        Data* cur = _table[hashi];
        Data* prev = _table[hashi];//这个结点是用来记录cur的前一个结点,方便单链表不是头的删除
        while (cur)
        {
            if (cur->_kv.first == key)
            {
                //第一种情况,删除链表的头节点
                if (cur == _table[hashi])
                {
                    Data* next = cur->_next;
                    _table[hashi] = next;
                    delete cur;
                    return true;
                }
                //第二种情况:删除不是头的结点
                else
                {
                    prev->_next = cur->_next;
                    delete cur;
                    return true;
                }
            }
            prev = cur;
            cur = cur->_next;
        }
        //没找到
        return false;
    }
};

那么我们这期HashTable内容就到这了,感谢大家的支持

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

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

相关文章

Kotlin~迭代器模式

概念 提供一种遍历集合元素的方法&#xff0c;而不暴露集合内部的实现。 角色介绍 iterator 迭代器接口: 定义访问和遍历集合元素的接口&#xff0c;一般包含next和hasNext方法。concrete iterator 具体迭代器: 实现迭代器接口&#xff0c;迭代器的核心逻辑实现。aggregate …

极致呈现系列之:Echarts热力图的神奇光晕

目录 什么是热力图热力图的特性及应用场景热力图的特性热力图的应用场景 Echarts中热力图的常用属性vue3中创建热力图 什么是热力图 热力图&#xff08;Heatmap&#xff09;是一种基于颜色映射的数据可视化图表&#xff0c;用于展示数据点的密度和分布情况。它使用不同的颜色强…

RT-Thread-10-线程优先级翻转

线程优先级翻转 前面讲到信号量和互斥量&#xff0c;二者有些区别&#xff1a; 信号量&#xff0c;可以在任何线程&#xff08;以及中断&#xff09;释放&#xff0c;用于同步&#xff0c;线程只在获得许可时才可以运行&#xff0c;强调的是运行步骤&#xff1b; 互斥量&#…

科技项目验收测试规范有哪些?

随着科技的不断发展和进步&#xff0c;越来越多的科技项目被投入使用。为了保证这些科技项目的质量&#xff0c;需要进行验收测试。科技项目验收测试是一项非常重要的工作&#xff0c;其结果对项目的质量和功能正常使用有着直接的影响。本文将就科技项 目验收测试规范和第三方软…

基于51单片机设计的公交车LED屏

一、项目介绍 为了提高公交车站点信息的实时性和准确性,方便乘客及时了解公交车到站信息,从而提高公交出行的便利性和舒适度。传统的公交车到站信息是通过人工喊话或者静态的站牌来实现的,这种方式存在信息不及时、不准确、不方便等问题。当前设计基于STC89C52单片机和MAX7…

PyQt6中文手册

PyQt6中文手册 一、PyQt6 简介 最后更新于 2021.04.22 本教程是 PyQt6 的入门教程。本教程的目的是让您开始使用 PyQt6 库。 关于 PyQt6 PyQt6 Digia 公司的 Qt 程序的 Python 中间件。Qt库是最强大的GUI库之一。PyQt6的官网&#xff1a;www.riverbankcomputing.co.uk/new…

2023年企业应该关注的10种AI攻击类型

2023年&#xff0c;热度很高的一个话题莫不是生成式AI和chat GPT了。但是&#xff0c;人工智能&#xff08;AI&#xff09;技术的应用安全威胁都已经开始显现。安全研究人员表示&#xff0c;在AI技术快速应用发展过程中&#xff0c;其安全性也面临诸多挑战。为了防范AI技术大规…

【C++】哈希unordered系列容器的模拟实现

文章目录 一、哈希表的模拟实现&#xff08;开散列&#xff09;1. 开散列的概念2. 开散列的节点结构3. 开散列的插入删除与查找4. 开散列整体代码实现 二、unordered系列容器的封装实现(开散列)1. 迭代器2. unordered_set和unordered_map的封装实现3. 哈希表整体源码 一、哈希表…

Jacoco代码覆盖率测试

​欢迎光临我的博客查看最新文章: https://river106.cn 1、简介 JaCoCo(Java Code Coverage)是一个开源的覆盖率工具&#xff0c;它针对的开发语言是java&#xff0c;其使用方法很灵活&#xff0c;可以嵌入到Ant、Maven中。 很多第三方的工具提供了对JaCoCo的集成&#xff0c;…

Java设计模式之结构型-装饰器模式

目录 一、基本概念 二、角色设计 三、代码实现 四、总结 一、基本概念 装饰器模式是指不必在改变原有的类和不使用继承的情况下&#xff0c;动态扩展一个对象的功能。 二、角色设计 角色描述抽象构件是一个接口或者抽象类&#xff0c;定义我们最核心的对象基础构件抽象构…

GD32 SPI 查询方式和DMA方式在全双模式下效率区别

最近在使用SPI的时候&#xff0c;遇到了一些数据传输效率问题&#xff0c;在此记录自己学习过程。SPI的基础知识这里就不在讲述了&#xff0c;直接分析SPI查询方式和DMA方式的效率问题。这里使用的芯片是GD32F303CC。 SPI以查询方式进行全双工通信 1.查询手册&#xff0c;SPI…

java——网络编程

文章目录 网络通信协议1. TCP/IP协议2. HTTP协议 Socket编程1.创建Socket对象2.获取输入输出流3.发送数据4.接收数据5.关闭Socket连接 NIO编程1.创建Channel2.创建Buffer3.从Channel中读取数据4.写入数据到Channel中5.关闭Channel和Stream Java网络编程是使用Java语言实现计算机…

Spark7-9

7. Spark中的一些重要概念 7.1 Application 使用SparkSubmit提交的个计算应用&#xff0c;一个Application中可以触发多次Action&#xff0c;触发一次Action产生一个Job&#xff0c;一个Application中可以有一到多个Job 7.2 Job Driver向Executor提交的作业&#xff0c;触发…

没想到,老刘是逃离北上广的那波人

我今天跟老刘调试的时候&#xff0c;我问了老刘一个问题——我问你工作这么久了&#xff0c;有没有遇到什么可以让你财富自由的机会。 老刘那个时候正在焊板子&#xff0c;背着我&#xff0c;他抬起头又低了下去&#xff0c;然后说「我是有一次机会了&#xff0c;但是没有抓住&…

MySQL-SQL存储函数以及触发器详解

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

PyTorch翻译官网教程3-DATASETS DATALOADERS

官网链接 Datasets & DataLoaders — PyTorch Tutorials 2.0.1cu117 documentation 数据集和数据加载器 处理样本数据的代码可能会变得混乱并且难以维护。理想情况下&#xff0c;我们希望我们的数据集代码与模型训练代码解耦&#xff0c;以获得更好的可读性和模块化。PyT…

轻松了解工作与学习必备的版本控制+Git,全程舒适~

目录 一、版本控制 二、版本控制器 三、Git 四、项目实操 第一步 在github上创建一个新的远程仓库 第二步 克隆到本地文件夹 第三步 IDEA&#xff08;PyCharm为例&#xff09;集成Git 一、版本控制 概念&#xff1a;版本控制是指对软件开发过程中各种程序代码、配置文件…

【spring cloud学习】4、创建服务提供者

注册中心Eureka Server创建并启动之后&#xff0c;接下来介绍如何创建一个Provider并且注册到Eureka Server中&#xff0c;再提供一个REST接口给其他服务调用。 首先一个Provider至少需要两个组件包依赖&#xff1a;Spring Boot Web服务组件和Eureka Client组件。如下所示&…

ADRC自抗扰控制(CODESYS平台完整源代码)

博途PLC ADRC完整源代码请参考下面文章链接: 博途PLC ADRC自抗扰控制完整SCL源代码_adrc控制算法代码_RXXW_Dor的博客-CSDN博客关于自抗扰控制框图可以参看专栏的其它文章,这里不再讲解具体算法过程,详细了解也可以参看韩京清研究员写的 《ADRC自抗扰》一书。_adrc控制算法…

基于混合策略的改进哈里斯鹰优化算法-附代码

基于混合策略的改进哈里斯鹰优化算法 文章目录 基于混合策略的改进哈里斯鹰优化算法1.哈里斯鹰优化算法2.改进哈里斯鹰优化算法2.1 Sobol 序列初始化种群2.2 limit 阈值执行全局搜索阶段2.4 动态反向学习 3.实验结果4.参考文献5.Matlab代码6.python代码 摘要&#xff1a;针对原…