【C++】哈希表

文章目录

  • 哈希概念
  • 哈希冲突
  • 哈希函数
  • 哈希表
    • 闭散列
    • 开散列
  • 开散列与闭散列比较

正文开始前给大家推荐个网站,前些天发现了一个巨牛的 人工智能学习网站, 通俗易懂,风趣幽默,忍不住分享一下给大家。 点击跳转到网站。

哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

哈希函数

哈希函数就是把关键字转化为对应哈希地址。引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:

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

常用的哈希函数:

  1. 直接定址法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况

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

哈希表

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

闭散列

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

怎么找下个空位置呢?

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

线性探测和二次探测实现方法类似,只有找空位置的地方有点区别,我们以实现线性探测为例。我们使用的是除留余数法。每个Key对表的大小取余,确定对应的哈希地址。

一个位置的状态可能有三种,空、存在、删除,有很多人有一个误区,觉得空状态和删除状态是一样的,其实他们差别挺大的,为什么这么说呢?我们看下面场景

在这里插入图片描述
我们把6删除了。
在这里插入图片描述

此时如果找44,遇到空就会停止,那44我们就无法找到,所以删除状态是必须的,我们找查找时是遇到空就停止,并且删除44也是无法成功的,与查找同理。

结构
我们需要枚举,来表示三个状态,哈希表本质就是用数组实现的,我们这里可以直接使用vector,我们把状态和需要存储的值用一个结构体封起来,存到vector中就可以了。

enum Status
{EMP,//空DEL,//删除EXI//存在
};template <class K, class V>
struct HashNode
{//表示状态Status _s;pair<K, V> _kv;
};template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
private:vector<HashNode<K, V>> _v;//表示存储了多少个数据size_t _n = 0;//为了解决不能被取余的值Hash hs;
};

插入

首先需要通过哈希映射,所以要对表的大小取余,然后在这个对应的位置去把值放进去,并且把状态改为存在。但是有一个问题,这个位置有可能已经被有的值映射过了,这是就需要使用线性探测,我们要从这个位置开始找到一个状态为空或者删除的位置,然后才能把值放进去。还有一个问题就是,我们什么时候扩容呢?

有一个东西叫做负载因子,负载因子 = 数据的个数 / 表的大小,我们通过控制负载因子来控制扩容的时机,如果负载因子控制的太大,那么出现哈希冲突的概率就太大了,但是如果负载因子太小了,那么对于空间的浪费太多了,所以负载因子的选择对哈希表的影响还是挺大的。闭散列我们一般把负载因子控制在0.7.

那么怎么扩容呢?
我们不能再原表直接扩容,因为扩容会打乱映射关系,所以我们需要建立一个新表,然后把新表的大小初始化成旧表的二倍,然后遍历旧表,把旧表的数据重新插入映射到新表,新表的内容就是我们需要的,所以我们此时可以考虑直接swap两个新旧表中的vector,这样就可以完美的符合我们的需求。

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first)){return false;}if (_n * 10 / _v.size() == 7){int newsize = _v.size() * 2;HashTable<K, V> newTable;newTable._v.resize(newsize);for (size_t i = 0; i < _v.size(); i++){if (_v[i]._s == EXI){newTable.Insert(_v[i]._kv);}}_v.swap(newTable._v);}size_t hashi = hs(kv.first) % _v.size();//寻找下个是删除或者空的位置while (_v[hashi]._s == EXI){hashi++;//防止越界hashi %= _v.size();}_v[hashi]._kv = kv;_v[hashi]._s = EXI;_n++;return true;
}

删除和查找

查找同样需要映射,然后从当前位置开始查找,只要不为空就一直找,如果找到了Key并且状态为存在,我们就可以直接返回当且的节点,如果到空都没找到,就返回nullptr。
删除需要先查找,查找到直接改状态就可以,不然就返回false。

HashNode<K,V>* Find(const K& k)
{size_t  hashi = hs(k) % _v.size();while (_v[hashi]._s != EMP){if (_v[hashi]._s == EXI && _v[hashi]._kv.first == k){return &_v[hashi];}hashi++;hashi %= _v.size();}return nullptr;
}bool Erase(const K& k)
{HashNode<K, V>* f = Find(k);if (f){f->_s = DEL;_n--;return true;}else{return false;}
}

以上情况如果我们存储整型的话当然是没问题的,但是如果我们存储的是string等不能直接被取余的类型怎么办?

这是我们就可以传一个仿函数,来把不能不能直接被取余的类型转化为整型。

template<class K>
struct HashFunc
{size_t operator()(const K& k){return (size_t)k;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t sum = 0;for (auto e : s){sum = sum * 31 + e;}return sum;}
};

一般我们会特化一个string的,因为字符串是最常见的。我们这里字符串转化为整型时BKDR法。

开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。开散列中每个桶中放的都是发生哈希冲突的元素。
这里实现我们直接使用vector就可以,因为每个桶都是哈希冲突的元素,所以结构需要一个链接下一个节点的指针。

结构

template <class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;HashNode(const pair<K,V>& kv): _kv(kv), _next(nullptr){}
};template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{typedef HashNode<K, V> Node;
private:vector<Node*> _tables;size_t _n;Hash hs;
};

这里的插入删除查找都是直接映射然后都是单链表的操作,插入因为哈希本来就是无序的所以我们直接头插就行了,只有扩容时需要我们重点说一下,我们可以跟上面闭散列一样,建立一个新表,然后把节点重新插入一下,但是这里会产生很多的浪费,因为原来表的节点重现插入以后,就会被释放了,有没有一种办法把原来的节点直接拿到新表呢?
我们遍历原表时,再拿到一个节点后直接通过映射把他的链接关系连接到新表中去就可以实现这样的效果,只不过需要我们提前记录一下下一个节点,不然会找不到下一个节点。

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?
开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

开散列

template<class K>
struct HashFunc
{size_t operator()(const K& k){return (size_t)k;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t sum = 0;for (auto e : s){sum = sum * 31 + e;}return sum;}
};
template <class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;HashNode(const pair<K,V>& kv): _kv(kv), _next(nullptr){}
};template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{typedef HashNode<K, V> Node;
public:HashTable(){_tables.resize(10);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}bool Insert(const pair<K,V>& kv){if (Find(kv.first)){return false;}if (_n == _tables.size()){//需要扩容vector<Node*> newtables;newtables.resize(2 * _tables.size());for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = cur->_kv.first % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = hs(kv.first) % _tables.size();Node* cur = new Node(kv);cur->_next = _tables[hashi];_tables[hashi] = cur;_n++;return true;}Node* Find(const K& k){size_t hashi = hs(k) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == k){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& k){size_t hashi = hs(k) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == k){if (prev==nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}cur = cur->_next;}return false;}
private:vector<Node*> _tables;size_t _n;Hash hs;};

开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

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

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

相关文章

CPython(将Python编译为so)

环境 先配一下环境&#xff0c;我使用的是python3.8.5 pip install Cython 编译过程 我们准备一个要编译的文件 test.py def xor(input_string): output_string "" for char in input_string: output_string chr(ord(char) ^ 0x66) return output_string…

Redis - 事务隔离机制

Redis 的事务的本质是 一组命令的批处理 。这组命令在执行过程中会被顺序地、一次性 全部执行完毕&#xff0c;只要没有出现语法错误&#xff0c;这组命令在执行期间是不会被中断。 当事务中的命令出现语法错误时&#xff0c;整个事务在 exec 执行时会被取消。 如果事务中的…

PyTorch深度学习实战(25)——自编码器

PyTorch深度学习实战&#xff08;25&#xff09;——自编码器 0. 前言1. 自编码器2. 使用 PyTorch 实现自编码器小结系列链接 0. 前言 自编码器 (Autoencoder) 是一种无监督学习的神经网络模型&#xff0c;用于数据的特征提取和降维&#xff0c;它由一个编码器 (Encoder) 和一…

20分钟部署ChatGLM3-6B

准备工作 1.下载源代码&#xff1a; https://github.com/THUDM/ChatGLM3 2.下载预训练模型&#xff1a; https://modelscope.cn/models/ZhipuAI/chatglm3-6b/files 可以创建一个py文件&#xff0c;直接使用如下代码下载到本地&#xff1a; from modelscope.hub.snapshot_dow…

python实现形态学建筑物指数MBI提取建筑物及数据获取

前言 形态学建筑物指数MBI通过建立建筑物的隐式特征和形态学算子之间的关系进行建筑物的提取[1]。 原理 上图源自[2]。 实验数据 简单找了一张小图片&#xff1a; test.jpg 代码 为了支持遥感图像&#xff0c;读写数据函数都是利用GDAL写的。 import numpy as np import …

LNMP网站架构分布式搭建部署

1. 数据库的编译安装 1. 安装软件包 2. 安装所需要环境依赖包 3. 解压缩到软件解压缩目录&#xff0c;使用cmake进行编译安装以及模块选项配置&#xff08;预计等待20分钟左右&#xff09;&#xff0c;再编译及安装 4. 创建mysql用户 5. 修改mysql配置文件&#xff0c;删除…

时间序列预测 — BiLSTM实现多变量多步光伏预测(Tensorflow)

目录 1 数据处理 1.1 导入库文件 1.2 导入数据集 1.3 缺失值分析 2 构造训练数据 3 模型训练 3.1 BiLSTM网络 3.2 模型训练 4 模型预测 1 数据处理 1.1 导入库文件 import time import datetime import pandas as pd import numpy as np import matplotlib.pyplot…

触发器和函数:让代码更接近数据

来源&#xff1a;艾特保IT 虹科干货丨触发器和函数&#xff1a;让代码更接近数据 原文链接&#xff1a;虹科干货 | 触发器和函数&#xff1a;让代码更接近数据 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; 文章速览&#xff1a; 触发器和函数的基础知识 编写语言…

AI创新之美:AIGC探讨2024年春晚吉祥物龙辰辰的AI绘画之独特观点

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《粉丝福利》 《linux深造日志》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 引言一、龙辰辰事件概述二、为什么龙辰辰会被质疑AI创作&#xff1f;1.1 AI 作画的特点2.2 关于建行的合作宣传图…

都是星光赶路人

不知不觉已经快工作五年了&#xff0c;工作以后就感觉时间一年比一年快&#xff0c;仿佛昨天才刚毕业&#xff0c;就像陈鸿宇歌中的那样&#xff0c;多少遗憾自负存念想&#xff0c;唯有时间不可挡。五年&#xff0c;思考了很多&#xff0c;也想明白了许多。正好借着年末&#…

Angular+Nginx区域HIS医院信息管理系统源码

医院管理信息系统&#xff08;HIS&#xff09;是医院基本、重要的管理系统&#xff0c;是医院大数据的基础。“云”指系统采用云计算的技术和建设模式&#xff0c;具有可扩展、易共享、区域化、易协同、低成本、易维护、体验好的优势。“H”是医疗卫生&#xff0c;由原来医院 (…

利用transition-group标签包裹li标签,实现输入数据后按Enter键将数据添加到列表中

1.效果图 2.代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title></title><script src"https://cdn.bootcdn.net/ajax/libs/vue/2.3.0/vue.js"></script><div id&quo…

CLEAR MOT评估指标

错误正样本&#xff08;False Positive&#xff0c;FP&#xff09;&#xff1a;整个视频中被预测为正的负样本数。 错误负样本&#xff08;False Negatives&#xff0c;FN&#xff09;&#xff1a;整个视频中被预测为负的正样本数。 IDs&#xff1a;跟踪过程中目标ID切换总数。…

QT----第三天,Visio stdio自定义封装控件

目录 第三天1 自定义控件封装 源码&#xff1a;CPP学习代码 第三天 1 自定义控件封装 新建一个QT widgetclass&#xff0c;同时生成ui,h,cpp文件 在smallWidget.ui里添加上你想要的控件并调试大小 回到mainwidget.ui&#xff0c;拖入一个widget&#xff08;因为我们封装的也…

【送书活动】探究AIGC、AGI、GPT和人工智能大模型

文章目录 前言01 《ChatGPT 驱动软件开发》推荐语 02 《ChatGPT原理与实战》推荐语 03 《神经网络与深度学习》推荐语 04 《AIGC重塑教育》推荐语 05 《通用人工智能》推荐语 后记赠书活动 前言 人工智能技术在过去几年中发展迅猛&#xff0c;得益于大数据、云计算、深度学习等…

C++1114新标准——统一初始化(Uniform Initialization)、Initializer_list(初始化列表)、explicit

系列文章目录 C11&14新标准——Variadic templates&#xff08;数量不定的模板参数&#xff09; C11&14新标准——Uniform Initialization&#xff08;统一初始化&#xff09;、Initializer_list&#xff08;初始化列表&#xff09;、explicit 文章目录 系列文章目录1…

TiDB 7.5 LTS 发版丨提升规模化场景下关键应用的稳定性和成本的灵活性

作者&#xff1a; TiDB社区小助手 原文来源&#xff1a; https://tidb.net/blog/1cffec89 互联网时代&#xff0c;数据的迅猛增长给数据库带来了可扩展性的挑战&#xff0c;Gen AI 带来的数据暴增更加剧了这种挑战。传统的数据分片已经不能承载新时代数据暴增的需求&#xf…

UE4 Niagara学习笔记

需要在其他发射器的同一个粒子位置发射其他粒子就用Spawn Particles from other Emitter 把发射器名字填上去即可 这里Move to Nearest Distance Field Subface GPU&#xff0c;可以将生成的Niagara附着到最近的物体上 使用场景就是做的火苗附着到物体上

【每日一题】2697. 字典序最小回文串-2023.12.13

题目&#xff1a; 2697. 字典序最小回文串 给你一个由 小写英文字母 组成的字符串 s &#xff0c;你可以对其执行一些操作。在一步操作中&#xff0c;你可以用其他小写英文字母 替换 s 中的一个字符。 请你执行 尽可能少的操作 &#xff0c;使 s 变成一个 回文串 。如果执行…

Python和Beautiful Soup爬虫助力提取文本内容

大家好&#xff0c;网络爬虫是一项非常抢手的技能&#xff0c;收集、分析和清洗数据是数据科学项目中最重要的部分。今天介绍如何从链接中爬取高质量文本内容&#xff0c;我们使用迭代&#xff0c;从大约700个链接中进行网络爬取。如果想直接跳转到代码部分&#xff0c;可以在下…