【C++】哈希表

在这里插入图片描述

个人主页:🍝在肯德基吃麻辣烫
我的gitee:C++仓库
个人专栏:C++专栏


文章目录

  • 前言
  • 一、什么是哈希?
  • 二、哈希表的插入及哈希冲突
    • 解决哈希冲突的方式
      • 1.闭散列的哈希表
        • 闭散列哈希表的删除
        • 实现(重点+细节处理)
        • 线性探测的缺陷
      • 2.开散列的哈希表
        • 开散列哈希表的插入
  • 总结


前言

这篇文章进入哈希表的学习,以及详细介绍如何用哈希表封装unordered_map和unordered_set。


一、什么是哈希?

在红黑树的查找效率中,平均查找时间复杂度为O(logN)。

但还是不够快,因为有大佬想出了更快的方法。即不像红黑
树那样遍历树进行节点之间的比较,而是直接知道查找的数据在结构中的位置。

怎么知道这个数据在不在呢?直接通过一个映射关系即可找到。

如何实现映射关系?通过函数实现,而这个函数就是哈希函数。

总结:哈希就是能让一个数据直接通过这个哈希函数直接查找是否在结构中,而不需要进行比较。

举个简单的例子:计数排序

计数排序就是一个经典的哈希式算法。

在计数排序中,
在这里插入图片描述

以该图为例,1这个数字出现了1次,3这个数字出现了1次,6这个数字出现了3次。

它的底层算法是这样的:比如接下来要统计的数字为x。

_a[x % _a.size()]++;

通过映射关系,直接找到x这个数字对应在_a顺序表中的下标,再+1即可。

而上面这个就是一个哈希函数

二、哈希表的插入及哈希冲突

在实际的底层场景,哈希表就是一个vector

来看下面的一段数据插入哈希表中。

int a[] = {1,7,6,4,5,9};

哈希函数设置为:hash(i) = key % hashtable.size();

此时hash(1) = 1 % 10, hash(7) = 7 % 10, hash(6) = 6 % 10,

hash(4) = 4 % 10, hash(5) = 5 % 10, hash(9) = 9 % 10;

在这里插入图片描述
此时如果再插入一个11,就会产生冲突
因为hash(11) = 11 % 10 = 1,11这个数据会通过哈希函数计算后映射到1这个位置,但是1这个位置已经存入了1,此时就产生了哈希冲突

解决哈希冲突的方式

1.闭散列的哈希表

闭散列也叫做开放定址法,当产生哈希冲突时,如果哈希表未被装满,说明哈希表中必然存在其他空位置,使用线性探测进行探测到空位置,然后在空位置进行插入。

比如在上面的案例中,
1这个位置已经存储数据了,11这个数据就往后找空位置,找到的第一个空位置进行插入即可。

这种直接通过映射的方式进行哈希表的插入时间复杂度为O(1)
在这里插入图片描述

闭散列哈希表的删除

在这种以线性探测来解决哈希冲突的操作中,不能真正地物理删除一个元素,因为你也不知道怎么删除,删除无非就是找一个数进行覆盖,找什么数都不行,所以,我们应该用一种状态表示一个位置是否存在元素,即
一个位置可以存在三种状态:

    1. 空状态(EMPTY)
    1. 删除状态(DELETE)
    1. 已存在状态(EXIST)

在这里可以使用枚举来表示三种状态

enum
{EXIST,EMPTY,DELETE
};

所以删除一个元素非常简单,只需要将该位置的状态设置为空即可。

实现(重点+细节处理)

下面来简单实现线性探测的闭散列哈希表。

注意到这里的一些问题:

  • 1.一个vector<int>是无法存储状态的,只能搞一个哈希节点存储数据和状态
template<class K,class V>
struct HashData
{pair<K, V> _kv;STATUS _status = EMPTY;
};
  • 2.在插入过程中,如果哈希表满了,即每个位置的状态都不是空了该怎么办呢?

    • 因为这种情况下,插入一个数据,会进行整个哈希表的搜索,时间复杂度又会下降到O(n)
    • 为了提高效率,引入一个负载因子
    • 负载因子通过判断当前哈希表的元素个数/哈希表的大小,来判断容量已达到多少,还剩余多少,当达到一定值时,会进行扩容并重新建立映射关系
    • 这样便可以让效率提升到平均O(1)的水平。
    • 在标准库的实现中,负载因子>=0.75时,会触发扩容。
    • 负载因子 = _n / hashtable.size();
    • 需要注意,两个都是size_t类型,÷出来之后会直接为0,所以可以这样:
    • 负载因子 = _n*100 / hashtable.size();
  • 3.在插入函数扩容逻辑中,由于需要重新建立映射关系,所以需要重新获取key,重新计算映射位置,而这些过程刚好是Insert这个函数的工作,所以在这段逻辑中可以复用Insert函数。

namespace open_addr //(开放定址法,闭散列法)
{template<class K,class V>struct HashData{pair<K, V> _kv;STATUS _status = EMPTY;};//为了解决一个string不能做key的问题,需要增加一个仿函数template<class K,class V,class HashFunc = DefaultHashFunc<K>>class HashTable{public:HashTable(){_table.resize(10);}//Insert不可能会出现插入失败的情况bool Insert(const pair<K,V>& kv){//扩容问题//当负载因子>=0.75时if (_n * 100 / _table.size() >= 75){size_t newsize = _table.size() * 2;HashTable<K,V,HashFunc> newtable;newtable._table.resize(newsize);//将旧数据重新映射到新表中//在开放定址法中复用Insert的原因是://插入过程并没有new的操作,复用更加方便for (auto& e : _table){newtable.Insert(e._kv);}//现代写法,最后newtable出了作用域会调析构,而此时newtabl的空间正好是_table想要的//这样写也刚好可以将_table的数据析构_table.swap(newtable._table);}HashFunc hf;//找到映射位置size_t hashi = hf(kv.first) % _table.size();  //注意必须是%size,如果%capacity//假如出现capacity扩容了,但是size不是等于capacity的情况,就可能出现越界的问题。while(_table[hashi]._status == EXIST){++hashi;hashi %= _table.size(); // 如果走到最后位置的下一个,就回到0位置}//走到这里可能为空/删除_table[hashi]._kv = kv; //如果在HashData将K设置成const K,连插入都不能插入了,最好的方法是封装成迭代器_table[hashi]._status = EXIST;++_n;return true;}HashData<const K,V>* Find(const K& key){HashFunc hf;size_t hashi = hf(key) % _table.size();while (_table[hashi]._status != EMPTY){if (_table[hashi]._status == EXIST && _table[hashi]._kv.first == key){return (HashData<const K, V>*)&_table[hashi];}++hashi;hashi %= _table.size();}return nullptr;}bool Erase(const K& key){HashData<const K, V>* ret = Find(key);if (ret){ret->_status = EMPTY;return true;}return false;}private:vector<HashData< K,V>> _table; // 哈希表size_t _n = 0; //存储有效数据个数};
}
线性探测的缺陷
  • 如果发生哈希冲突,空位置就会被占用,这样会产生数据堆积在一起,查找时数据的映射位置可能会被占用,还需要往后查找,使得效率下降。

所以为了减小哈希冲突而提高效率,需要多一点的空位置,也就是需要更多的空间来减小哈希冲突产生的概率,但这样就会降低空间利用率。

2.开散列的哈希表

开散列法,又叫哈希桶,不同于前面的用线性探测来解决哈西冲突问题,哈希桶,顾名思义,就是一个位置会挂着几个节点,这些节点被形象地称为

大致结构如下:

在这里插入图片描述

vector存储结构就应该修改一下了:

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

此时vector存储的每个位置的数据为:

vector<Node*> _hashtable;

vector的每个节点并不存储有效数据,而是一个数据通过哈希函数:key % _hashtable.size()找到。

找到映射位置后,此时就可以像链表头插一样,将该节点插入哈希表中挂起来。

核心代码如下:

size_t hashi = key % _hashtable.size();
Node* newnode = new Node(kv);
//头插
newnode->next = _hashtable[hashi];
//由于_hashtable[hashi]存储的是一个节点的地址
_hashtable[hashi] = newnode;
开散列哈希表的插入

插入的过程上面已经讲解了,需要注意的就是一个细节:

  • 在扩容过程中,重新建立映射关系时,最好不要复用Insert函数,因为原来的哈希表的节点还可以使用,复用Insert函数意味着先遍历原来的哈希表,然后对每个节点拷贝构造后再挂起到新的哈希表中,然后再析构旧的哈希表。

这样会降低效率,因为旧的节点仍然可以使用,何必多一层对每个节点拷贝构造呢。

所以可以在遍历旧表的时候,直接顺手牵羊将每个节点牵到新表上。

bool Insert(const pair<K, V>& kv)
{//扩容问题//负载因子为1时扩容,此时最坏的情况就是一个位置可能挂好几个节点//最好情况是每一个位置都只挂一个节点if (_n == _table.size()){size_t newsize = _table.size() * 2;HashTable<K, V, HashFunc> newht;newht._table.resize(newsize, nullptr);//遍历旧的哈希表,重新进行映射关系//在哈希桶写法中不复用Insert的原因是//Insert中有new Node的操作,复用时一个个申请新空间,挂上去,最后还得释放旧的节点//还不如直接把旧表的节点直接顺手牵羊拉下来for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];Node* next = nullptr;if (cur)next = cur->_next;while (cur){size_t newhashi = hf(cur->_kv.first) % newht._table.size();cur->next = newht._table[newhashi];newht._table[newhashi] = cur;cur = next;if(next)next = next->_next;}}//现代写法,交换资源_table.swap(newht._table);}HashFunc hf;//如果待插入节点重复了,那就不插入if (Find(kv.first)){assert(0);return false;}size_t hashi = hf(kv.first) % _table.size();//头插Node* newnode = new Node(kv);//头插 ,假如_table[hashi]不为空//那么新节点的next就指向当前位置挂着的第一个节点newnode->_next = _table[hashi];//更新当前位置挂着的第一个节点_table[hashi] = newnode;++_n;return true;
}

在这里插入图片描述

最后不要忘记,交换两个vector。

哈希桶的删除和查找与前面的线性探测几乎一致,这里不再赘述。


总结

实现哈希表并不难,哈希表的结构也比较清晰,难点就难在对哈希表进行封装,这个才是重点。
下集预告:

    1. 涉及到string不能做key的问题应该用仿函数处理。
    1. 不再固定地使用pair,而是使用T来替代,因为要适应map和set。
    1. 涉及普通迭代器的问题。
    1. 涉及const迭代器问题,因为set的特性是Key不允许修改,那么如何解决呢?
  • 5.inert返回值处理,在库的实现中,insert返回值是
  • pair<iterator,bool>的问题,这也是为了适配map的operator[]而产生的。
  • 6.key不能修改的问题,set比较难解决,map比较轻松可以解决。

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

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

相关文章

LNMP和数据库的安装

LNMP是什么 L&#xff1a;Linux平台&#xff0c;操作系统&#xff0c;另外桑组件的运行平台 N&#xff1a;nginx 提供前端页面 M&#xff1a;MySQL&#xff0c;开源关系的数据库&#xff0c;主要是用来保存用户账号信息。 P&#xff1a;PHP&#xff0c;开发一种动态页面的编程语…

基于 ACK Fluid 的混合云优化数据访问(一):场景与架构

作者&#xff1a;车漾&#xff08;必嘫&#xff09; 本系列文章将介绍如何基于 ACK Fluid 支持和优化混合云的数据访问场景。 概述 在 AI 和大数据时代&#xff0c;算力即正义&#xff0c;强大的算力推动了源源不断的创新。然而&#xff0c;企业自建的算力集群存在资源容量和…

C#中的Dispatcher:Invoke与BeginInvoke的使用

Dispatcher是.NET框架中的一个重要概念&#xff0c;用于处理异步消息传递。在C#中&#xff0c;Dispatcher提供了两种方法&#xff1a;Invoke和BeginInvoke&#xff0c;用于控制线程上消息的顺序和执行方式。 目录 一、Dispatcher.Invoke二、Dispatcher.BeginInvoke三、使用场景…

安卓三防平板在行业应用中有哪些优势

在工业维修和检测中&#xff0c;安卓三防平板的应用也十分广泛。它可以搭载各种专业软件和工具&#xff0c;帮助工人们进行设备故障排查和维护&#xff0c;降低了维修成本和停机时间。 一、产品卖点&#xff1a; 1. 防水性能&#xff1a;该手持平板采用了防水设计&#xff0c;…

基于FPGA的数字时钟系统设计

在FPGA的学习中&#xff0c;数字时钟是一个比较基础的实验案例&#xff0c;通过该实验可以更好的锻炼初学者的框架设计能力以及逻辑思维能力&#xff0c;从而打好坚实的基本功&#xff0c;接下来就开始我们的学习吧&#xff01; 1.数码管介绍 数码管通俗理解就是将8个LED(包含…

什么是UI自动化测试工具?

UI自动化测试工具有着AI技术驱动&#xff0c;零代码开启自动化测试&#xff0c;集设备管理与自动化能力于一身的组织级自动化测试管理平台。基于计算机视觉技术&#xff0c;可跨平台、跨载体执行脚本&#xff0c;脚本开发和维护效率提升至少50%;多端融合统一用户使用体验&#…

C/C++ 线程超详细讲解(系统性学习day10)

目录 前言 一、线程基础 1.概念 2.一个进程中多个线程特征 2.1 线程共有资源 2.2 线程私有资源 3.线程相关的api函数 3.1 创建线程 创建线程实例代码如下&#xff1a; 需要特别注意的是&#xff1a; -lpthread和-pthread的区别 3.2 给线程函数传参 传参实例代码如…

springboot项目Html页面引入css文件不生效

我的出错原因&#xff1a; 在调用css文件时&#xff1a; <link rel"stylesheet" type"text/css" href"/static/css/style.css" /> 这里我多加了一个/static,而使得css样式不生效 因为在springboot项目中&#xff0c;静态资源是默认存…

3D机器视觉:解锁未来的立体视野

原创 | 文 BFT机器人 机器视觉领域一直在不断演进&#xff0c;从最初的二维图像处理&#xff0c;逐渐扩展到了更复杂的三维领域&#xff0c;形成了3D机器视觉。3D机器视觉技术的涌现为计算机系统带来了全新的感知和理解能力&#xff0c;这一领域的发展正日益受到广泛关注。本文…

上海亚商投顾:沪指探底回升 华为汽车概念股集体大涨

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数昨日探底回升&#xff0c;早盘一度集体跌超1%&#xff0c;随后震荡回暖&#xff0c;深成指、创业板指…

043:mapboxGL鼠标点击提示source属性信息

第043个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中通过鼠标点击提示source属性信息。这里用到了popup弹窗,用到了click事件,用到了鼠标样式的变化等功能。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源…

Unity设计模式——建造者模式

Product类——产品类&#xff0c;由多个部件组成。 class Product {IList<string> parts new List<string>();//添加产品部件public void Add(string part){parts.Add(part);}public void Show(){foreach (string part in parts){Debug.Log("产品:"pa…