C++——哈希(一)

1. unordered系列关联式容器

在了解哈希之前我们先简单了解一下unordered系列的关联式容器,因为其底层就是用哈希来实现的,其实也没啥好说的,C++11中,STL又提供了unordered系列的关联式容器(unordered_map和unordered_set),与红黑树结构的关联式容器(map和set)使用方式基本类似,就是其底层结构不一样而已

感兴趣的可以看一下链接

unordered_map:

https://cplusplus.com/reference/unordered_map/unordered_map/?kw=unordered_map

unordered_set:

https://cplusplus.com/reference/unordered_set/unordered_set/?kw=unordered_set

2.底层结构 

2.1哈希概念

哈希/散列其本质是存储的值和存储位置的映射

2.2哈希函数

哈希函数设计原则
  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
  • 域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见的哈希函数:

直接定值法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
面试题:字符串中第一个只出现一次字符
a7034e697c044b01a89debd8360cb1df.png

 

除留余数法 

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

2.3哈希冲突

引起哈希冲突的一个原因可能是: 哈希函数设计不够合理
对于两个数据元素的关键字$k_i$和 $k_j$(i != j),有$k_i$ != $k_j$,
但有:Hash($k_i$) == Hash($k_j$)
即: 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 。

注意:哈希冲突是无法避免的(在有限的空间中存无限的值,不论哈希函数多精妙,哈希冲突的结果是必然的),不过哈希函数设计得越精妙那么产生哈希冲突的可能性就越低 

2.4解决哈希冲突 

解决哈希冲突两种常见的方法是: 闭散列开散列

2.4.1闭散列解决

闭散列的开放定址法(本质是当前位置冲突了,后面找一个位置继续存储就得了)
常见两种
                1.线性探测                                  2.二次探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
以除留余数法的哈希冲突为例

8abd3012b0924664b0310775783aa843.png

 hash(key)可以看成下标,结合上图如果i位置已经有了,就线性往后查找到空位置放进去

查找

i=key%表的大小

如果i位置不是要找的key,就往后找直到找到或者遇到空,如果找到表示结尾的位置(还没遇到空),就要往头回绕了

插入

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

删除

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

 啥是伪删除法呢?在表中的节点(存数据的)增加一个表状态的成员变量(空、存在、删除)

enum State//状态
{EMPTY,//空EXIST, //存在DELETE//删除
};
template<class K, class V>
struct HashData
{pair<K, V> _val;State _state = EMPTY; // 标记
};

 

线性探测优点:实现非常简单,
线性探测缺点: 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降

 上文中说到将无限的数据插入到有限的空间中,随着空间中的元素越来越多,插入元素时产生哈希冲突的概率也就越来越大,既然冲突是必然发生的,那么多次冲突后查找的效率也就必然会降低

由此我们不得不考虑到这个问题:哈希表什么情况下进行扩容?如何扩容?

对于扩容,哈希表中增加了负载因子(载荷因子)

负载因子=表中数据的个数/空间大小

负载因子越大,表明表中填入的元素越多(空间利用率越高),产生冲突的可能性也就越大;反之,负载因子越小,表明表中填入的元素越少(空间利用率越低),产生冲突的可能性也就越小

对于开放定址法来说负载因子是一个特别重要的因素,最好控制在0.7~0.8以下,超过这个值应该对哈希表进行扩容

2.二次探测 

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位
置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题, 找下一个空位置的方法
为:
Hi = (H0+i^2)%m(i=1,2,3...)
Hi是冲突元素通过二次探测后得到的存放位置
H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

 

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任
何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在
搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出
必须考虑增容。

 因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

2.4.2开散列(哈希桶)

1. 开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
cdb535e09377404e980a2a98cd461e64.png

数组里面存冲突的指针然后像链表一样插入

再讲一下极端条件下的情况

所有元素的哈希值均相同,最终都放到同一个哈希桶中,此时这个哈希表的增删查改效率会降低

有的实现方法是当桶中元素个数超过一定长度,会把桶中的链表结构改成红黑树结构

d88b612a73f94f608cc29c2d9b3f6205.png

当然不做这个处理也行,反正数据一多负载因子会增长,最后触发扩容条件

3.实现

3.1线性探测的实现

前面线性探测解决哈希冲突中我们提到不能直接删除哈希表中的元素,要采用伪删除

	enum State//枚举状态{EMPTY,//空EXIST, //存在DELETE//删除};template<class K, class V>struct HashData{pair<K, V> _val;State _state = EMPTY; // 标记};

哈希表的构造和框架

初始化的时候顺便把扩容的事情搞定

template<class K, class V>
class HashTable
{public:HashTable(size_t size = 10){_ht.resize(size);}
private:vector<HashData<K, V>> _ht;//hashtablesize_t _n;
};

查找和插入的思路在闭散列解决哈希冲突中已经说过我就不重复讲了

查找

// 查找
HashData<K, V>* Find(const K& key)
{size_t hashi = key % _ht.size();while (_ht[hashi]._state != EMPTY)//不等于空说明有存东西{if (_ht[hashi]._val.first == key){return &_ht[hashi];}hashi++;hashi %= _ht.size();}return nullptr;
}

插入

不过扩容的地方要注意,要讲旧表的数据一个个插入新表中,要重新计算哈希值(也就是下标)

bool Insert(const pair<K, V>& val)
{if (Find(val.first))return false;//扩容if (_n * 10 / _ht.size() >= 7){HashTable<K, V> newtable(_ht.size() * 2);for (auto& e : _ht){if (e._state == EXIST){newtable.Insert(e._val);}}_ht.swap(newtable._ht);}//线性探测size_t hashi = val.first % _ht.size();while (_ht[hashi]._state == EXIST)//如果位置被占了往后走{hashi++;hashi %= _ht.size();//中间走到后面,然后超过后面返回前面}//		找到空位放进去_ht[hashi]._val = val;_ht[hashi]._state = EXIST;_n++;return true;
}

删除

删除的步骤也是非常简单啊,find找一下有没有这个值,然后状态改为删除,哈希表中的有效个数-1就得了

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

源代码

#pragma once
#include<vector>namespace Close_Hash
{enum State//状态{EMPTY,//空EXIST, //存在DELETE//删除};template<class K, class V>struct HashData{pair<K, V> _val;State _state = EMPTY; // 标记};template<class K, class V>class HashTable{public:HashTable(size_t size = 10){_ht.resize(size);}// 查找HashData<K, V>* Find(const K& key){size_t hashi = key % _ht.size();while (_ht[hashi]._state != EMPTY)//不等于空说明有存东西{if (_ht[hashi]._val.first == key){return &_ht[hashi];}hashi++;hashi %= _ht.size();}return nullptr;}// 插入bool Insert(const pair<K, V>& val){if (Find(val.first))return false;//扩容if (_n * 10 / _ht.size() >= 7){HashTable<K, V> newtable(_ht.size() * 2);for (auto& e : _ht){if (e._state == EXIST){newtable.Insert(e._val);}}_ht.swap(newtable._ht);}//线性探测size_t hashi = val.first % _ht.size();while (_ht[hashi]._state == EXIST)//如果位置被占了往后走{hashi++;hashi %= _ht.size();//中间走到后面,然后超过后面返回前面}//		找到空位放进去_ht[hashi]._val = val;_ht[hashi]._state = EXIST;_n++;return true;}// 删除bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){_n--;ret->_state = DELETE;return true;}return false;}private:size_t HashFunc(const K& key){return key % _ht.capacity();}private:vector<HashData<K, V>> _ht;//hashtablesize_t _n;};void TestHT1(){int a[] = { 1,4,24,34,7,44,17,37 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_val.first << endl;}else{cout << ret->_val.first << ":N" << endl;}}cout << endl;ht.Erase(34);ht.Erase(4);for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_val.first << endl;}else{cout << e << ":N" << endl;}}cout << endl;}}

3.2哈希桶的实现

表由多个桶组成(表是数组),桶(链表)里面的节点

template <class K,class V>
struct HashNode//节点
{HashNode<K,V>* _next;pair<K, V> _kv;HashNode(const pair<K,V>& kv):_next(nullptr),_kv(kv){}
};

哈希表的框架

template<class K, class V>
class HashTable
{typedef HashNode<K,V> Node;
public://。。。
private:vector<Node*> _tables; // 指针数组size_t _n;
};

构造

HashTable()
{_tables.resize(10, nullptr);_n=0;
}

查找

对传来的值找下表,找不到返回空,找到了遍历这个桶(链表),如果找到了返回那个节点,找不到继续往下走直到走到空

		Node* Find(const K& key){size_t hashi = key % _tables.size();//找下标Node* cur = _tables[hashi];while (cur)//走链表{if (cur->_kv.first == key)//找到了{return cur;}cur = cur->_next;//往下走}//找不到return nullptr;}

 

插入

思路和线性探测的插入思路差不多

找那个值,如果已经有了那就不用插入了

如果没有那么就找到这个值对应的下标,然后头插入桶(链表),然后有效元素个数+1

判断负载因子看看要不要扩容,扩容还是创建新表,遍历旧表插入新表(节点挂到新表的下标可能不一样),再交换一下新旧表

		bool Insert(const pair<K, V>& kv){//插入失败(找不到)if (Find(kv.first))return false;//扩容if (_n == _tables.size())//负载因子极限{vector< Node*> newtable(_tables.size() * 2, nullptr);//把旧桶的节点挂到新表新桶中for (size_t i=0;i<_tables.size();i++){Node* cur = _tables[i];//遍历走一个个旧桶while (cur){Node* next = cur->_next;//存一下next,待会可以往下走size_t hashi = cur->_kv.first % newtable.size();//节点挂到新桶的下标可能不一样//挂上新桶cur->_next = newtable[hashi];newtable[hashi] = cur;//往下走cur = next;}//把旧桶被移走的节点置空_tables[i] = nullptr;}_tables.swap(newtable);}size_t hashi = kv.first % _tables.size();//找到下标Node* newnode = new Node(kv);//开一个节点的空间//头插(入桶)newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}

删除

删除的思路不一样(哈希桶可以直接删节点)

找下标,创建一个前驱节点(链表的删除嘛)

遍历对应下标的桶,如果找到的不是头节点,前驱直接指向遍历节点的下一个;如果找到的是头节点,头节点直接成遍历节点的下一个。最后delete删除遍历节点,减一下有效元素个数

bool Erase(const K& key)
{size_t hashi = key % _tables.size();Node* prev = nullptr;//保存遍历节点的上一个Node* cur = _tables[hashi];while (cur)	{if (cur->_kv.first == key)//找到{//删除if (prev)//节点有可能是头节点  prev非空,不是头节点{prev->_next = cur->_next;}else//要删除的节点是头节点{_tables[hashi] = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;
}

源码

#pragma once
#include <vector>
版本1
namespace HashBucket
{template <class K,class V>struct HashNode//表的节点(桶口){HashNode<K,V>* _next;pair<K, V> _kv;HashNode(const pair<K,V>& kv):_next(nullptr),_kv(kv){}};template<class K, class V>class HashTable{typedef HashNode<K,V> Node;public:HashTable(){_tables.resize(10, nullptr);_n=0;}Node* Find(const K& key){size_t hashi = key % _tables.size();//找下标Node* cur = _tables[hashi];while (cur)//走链表{if (cur->_kv.first == key)//找到了{return cur;}cur = cur->_next;//往下走}//找不到return nullptr;}bool Insert(const pair<K, V>& kv){//插入失败(找不到)if (Find(kv.first))return false;//扩容if (_n == _tables.size())//负载因子极限{vector< Node*> newtable(_tables.size() * 2, nullptr);//把旧桶的节点挂到新表新桶中for (size_t i=0;i<_tables.size();i++){Node* cur = _tables[i];//遍历走一个个旧桶while (cur){Node* next = cur->_next;//存一下next,待会可以往下走size_t hashi = cur->_kv.first % newtable.size();//节点挂到新桶的下标可能不一样//挂上新桶cur->_next = newtable[hashi];newtable[hashi] = cur;//往下走cur = next;}//把旧桶被移走的节点置空_tables[i] = nullptr;}_tables.swap(newtable);}size_t hashi = kv.first % _tables.size();//找到下标Node* newnode = new Node(kv);//开一个节点的空间//头插(入桶)newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}bool Erase(const K& key){size_t hashi = key % _tables.size();Node* prev = nullptr;//保存遍历节点的上一个Node* cur = _tables[hashi];while (cur)	{if (cur->_kv.first == key)//找到{//删除if (prev)//节点有可能是头节点  prev非空,不是头节点{prev->_next = cur->_next;}else//要删除的节点是头节点{_tables[hashi] = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;}private:vector<Node*> _tables; // 指针数组size_t _n;};void TestHB1(){int a[] = { 15,7,8,32,24,5,6,9,13,14,19,22,24,17};HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first <<":EXIST" << endl;}else{cout << ret->_kv.first << ":DELETE"<< endl;}}cout << endl;ht.Erase(24);ht.Erase(32);for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first << ":EXIST" << endl;}else{cout << e << ":DELETE" << endl;}}}
}

 

 

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

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

相关文章

python练习二

# Demo85def pai_xu(ls_test):#创建一个列表排序函数命名为pai_xu# 对创建的函数进行注释"""这是一个关于列表正序/倒序排列的函数:param ls_test: 需要排序的列表:return:"""ls1 [int(ls_test[i]) for i in range(len(ls_test))]#对input输入的…

Demis Hassabis,这位被封为爵士的AI领域的杰出研究者和神经科学家,是否能成为Google的救星?

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

一文教会女朋友学会日常Git使用!Git知识总结

文章目录 一文教会女朋友学会日常Git使用&#xff01;Git知识总结一、git基本知识了解1.git简介2.git区域了解3.git常用命令 二、常用工作场景1.克隆远程仓库&#xff0c;把仓库代码拉到本地2.推送代码到远程仓库&#xff08;1&#xff09;本地代码和远程仓库版本相同&#xff…

200个有趣的HTML前端游戏项目合集(持续更新中)

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

计算机组成原理1:计算机系统概述

此系列介绍计算机的组成原理&#xff0c;参考书&#xff1a;《计算机组成原理考研复习指导》(王道论坛组编)。 1.计算机发展史 1.1 计算机发展 计算机变化 第一代计算机 ( 1946 − 1957 ) (1946-1957) (1946−1957)&#xff1a;电子管时代。 逻辑元件采用电子管&#xff1b;使…

Java封装最佳实践:打造高内聚、低耦合的优雅代码~

​ 个人主页&#xff1a;秋风起&#xff0c;再归来~ 文章专栏&#xff1a;javaSE的修炼之路 个人格言&#xff1a;悟已往之不谏&#xff0c;知来者犹可追 克心守己&#xff0c;律己则安&#xff01; 1、封装 1.1 封装的概念 面向对象程序三大…

文献学习-24-用于少发罕见病诊断的动态特征拼接

Dynamic feature splicing for few-shot rare disease diagnosis Authors: Yuanyuan Chen, Xiaoqing Guo , Yongsheng Pan , Yong Xia , Yixuan Yuan Source: Medical Image Analysis 90 (2023) 102959 Keywords: 少样本学习 罕见病诊断 transformer 特征拼接 通道相似度 Ab…

FreeRTOS 任务挂起和恢复API函数

FreeRTOS 任务挂起和恢复API函数使用 挂起的作用就是当我们需要暂停某任务时候&#xff0c;等过一段时间在运行&#xff0c;这个时候要是使用删除和重建的方法就会当时任务进行时候的变量保存的值。当需要将这个任务停止运行一段时间的将这个任务挂起&#xff0c;当重新进行运…

华为云亮相KubeCon EU 2024,以持续开源创新开启智能时代

3月21日&#xff0c;在巴黎举办的云原生顶级峰会KubeCon EU 2024上 &#xff0c;华为云首席架构师顾炯炯在“Cloud Native x AI&#xff1a;以持续开源创新开启智能时代”的主题演讲中指出&#xff0c;云原生和AI技术的融合&#xff0c;是推动产业深刻变革的关键所在。华为云将…

进度管理与成本管理

1.裁剪考虑因素 2. 定义计划是把WBS分解的活动在分解成更小的活动。 3. 4.资源平衡会改变关键路径 5.资源平滑只能在自由和浮动时间延迟 6. 7. 8.成本管理&#xff0c;论文要写不足与解决过程 9.成本的类型 10. 11.规划XX管理的时候&#xff0c;输入一般有章程和项目…

C语言动态内存讲解+通讯录2.0

文章目录 前文malloc和freecallocrealloc枚举常量的简单说明及使用 通讯录2.0动态开辟通讯录,满了就扩容保存数据和载入数据 通讯录2.0演示推荐好用的软件 前文 本文主要介绍动态开辟的几个函数,以及改进之前的通讯录。 我们局部变量等是在栈区上开辟空间的,而我们动态开辟的空…

新model开发记录

模型使用 -- 用blender导出为 fbx &#xff0c;修改渲染方式&#xff08;点击模型->Materials->Extract Materials(将材质从fbx中 单独提取出来了)->Materials 选择 Shader -> SimpleURPToonLitExample 点开脸的材质&#xff0c;勾选第一条&#xff09; 解决角色…

力扣-python-故障键盘

题解&#xff1a; from collections import dequeclass Solution:def finalString(self, s: str) -> str:# 创建一个双端队列用于存储字符q deque()# 定义一个标志位&#xff0c;用于标记当前字符应该添加到队列的哪一端head False# 遍历输入的字符串s的每一个字符for ch…

C++刷题篇——05静态扫描

一、题目 二、解题思路 注意&#xff1a;注意理解题目&#xff0c;缓存的前提是先扫描一次 1、使用两个map&#xff0c;两个map的key相同&#xff0c;map1&#xff1a;key为文件标识&#xff0c;value为文件出现的次数&#xff1b;map2&#xff1a;key为文件标识&#xff0c;va…

CTF wed安全(攻防世界)练习题

一、Training-WWW-Robots 进入网站如图&#xff1a; 翻译&#xff1a;在这个小小的挑战训练中&#xff0c;你将学习Robots exclusion standard。网络爬虫使用robots.txt文件来检查它们是否被允许抓取和索引您的网站或只是其中的一部分。 有时这些文件会暴露目录结构&#xff0c…

【数据结构】优先级队列——堆

&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;个人主页&#x1f388;&#x1f388;&#x1f388;&#x1f388;&#x1f388; &#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;数据结构专栏&#x1f388;&#x1f388;&#x1f388;&…

C++11:右值引用

C11&#xff1a;右值引用 右值与左值右值引用语法右值引用底层移动语义引用折叠完美转发 传统的C语法中就有引用的语法&#xff0c;而C11中新增了的右值引用语法特性&#xff0c;所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用&#xff0c;都是给对…

VR全景赋能智慧农业,打造沉浸式种植体验平台

随着人口的增长&#xff0c;传统农业也正在面临着不一样的挑战&#xff0c;加上很多人对农业的固有印象&#xff0c;很少有年轻人愿意下到农田里&#xff0c;那么该如何提高产量、降低成本以及引导年轻人深刻感受现代农业成为了急需解决的问题。 随着城市化脚步的推进&#xff…

数码管时钟--LABVIEW编程

一、程序的前面板 1.获取系统时钟&#xff0c;年月日&#xff0c;时分秒&#xff0c;用14个数码管显示。 2.闹钟设定小时和分钟。 二、程序的后面板 三、程序运行图 四、程序源码 源程序可以在百度网盘自行下载&#xff0c;地址链接见下方。 链接&#xff1a;https://pan.b…

LeetCode-54. 螺旋矩阵【数组 矩阵 模拟】

LeetCode-54. 螺旋矩阵【数组 矩阵 模拟】 题目描述&#xff1a;解题思路一&#xff1a;定义上下左右四个边界&#xff0c;进行模拟。解题思路二&#xff1a;5行 Python zip函数图一乐解题思路三&#xff1a;0 题目描述&#xff1a; 给你一个 m 行 n 列的矩阵 matrix &#xf…