C++数据结构——哈希表

前言:本篇文章将继续进行C++数据结构的讲解——哈希表。


目录

一.哈希表概念

二.哈希函数

1.除留取余法

三.哈希冲突

1.闭散列

线性探测

 (1)插入

(2)删除

 2. 开散列

开散列概念

四.闭散列哈希表

1.基本框架 

2.插入

3.寻找

4.删除

5.数据类型问题

五.开散列哈希表

1.基本框架

2.插入

3.寻找

4.删除

总结


一.哈希表概念

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

比如说:要统计一个全是小写英文字母的字符串中各个英文字母出现的个数,该怎么做???

很容易,因为小写英文字母有26个,所以我们直接创建一个大小为26的int数组并将值全部初始化为0让数组的每一位都代表一个小写英文字母该字母每出现一次,就让该怎么对于位置的值++,最终每个位置的值便是对应小写英文字母的个数

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


二.哈希函数

有时候我们可能不能像上边一样有多少种数据就创建多大的哈希表,比如说虽然我们只有1,12,103,1004,10005四个数要统计个数,难道要创建一个10005大小的数组吗??? 


1.除留取余法

实际上只需要创建大小为6的数组即可,此时我们可以使用哈希函数:除留取余法

找到一个合适的除数,能够让上述数字取余之后分别为不同的值,从而拉进各个数字之间的距离,创建更小的哈希表

比如说让上述数字均取余上10,得到的就会是1,2,3,4,5,刚好对应数组各个位置


三.哈希冲突

虽然上述函数可以解决大部分问题,但不妨有些时候会出现像1,11,111,1111这样的数字,它们%10之后得到的数字均为1,这样就会导致不同的值映射到相同的位置,从而导致哈希冲突

 那么为了解决哈希冲突,有两种常用的方法:闭散列和开散列


1.闭散列

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


线性探测

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

 (1)插入

通过哈希函数获取待插入元素在哈希表中的位置

如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

(2)删除

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

哈希表的每个空间都给上一个标记
EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除,通过枚举实现:
enum State{EMPTY, EXIST, DELETE};  


 2. 开散列

开散列概念

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

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。 由于其形状很像一个桶,所以开散列哈希表又叫哈希桶


四.闭散列哈希表

1.基本框架 

 下面来看代码,我们先给出哈希表的基本框架:

namespace close_address
{enum State{EMPTY,EXIST,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;};
}

这里的_n用来记录哈希表中数据的个数。 


2.插入

	bool Insert(const pair<K, V> kv){if (Find(kv.first))return false;size_t hashi = kv.first % _tables.size();//线性探测while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}

插入步骤还是容易实现的,值得注意的是,哈希表也不允许相同数据存在,所以需要提前判断,这里直接调用后边的Find()函数来判断

除此之外还有一个非常关键的点在于——扩容

 因为虽然我们的哈希表是用vector作为底层,但是实际上填入数据时并一定是挨着存放的,所以我们需要在插入之前,提前创造空间

 那么哈希表也要等数据存满的时候才扩容吗???并不是。

对于散列表,存在一个荷载因子的定义:

α = 填入表中的元素 / 散列表的长度

当表中的元素足够多但并未满时,此时如果继续插入数据,发生哈希冲突的可能性就会极大,导致插入时间变长。所以这里我们规定,当荷载因子的值 >= 0.7时就进行扩容

	HashTable(){_tables.resize(10);}

在扩容之前,应该给表一个初始的大小,这里通过构造函数使哈希表的初始长度为10

		//扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V> newHT;newHT.tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}

随后进行扩容判断,这里有一个细节, 因为荷载因子是一个浮点数,如果直接进行比较,我们还需进行类型转换,所以我们直接让哈希表数据个数乘10再来进行计算。

判断成立则进行扩容,注意,哈希表的扩容并非像顺序表那样进行复制粘贴,因为哈希表扩容,代表着哈希函数的分母发生变化,所以其对应的取余后的下标位置也会发生变化

这里我们通过新建一个二倍大小的哈希表遍历原表数据并在新表中调用插入函数进行插入,最后在通过交换即可。


3.寻找

	//寻找HashData<K, V>* Find(const K& key){size_t hashi = key % _tables.size();//线性探测while (_tables[hashi]._state == EXIST &&_tables[hashi]._state != EMPTY){if (_tables[hashi]._kv.first == key){return &_tables[hashi];}++hashi;hashi %= _tables.size();}return nullptr;}

寻找就比较简单了, 通过要寻找的值计算出其在哈希表中的下标判断是否存在且是否为空存在且不为空则进行判断,相等返回如果不相等,因为可能存在哈希冲突,所以循环往后遍历,直至遇到空位置仍然没有找到,说明其不在哈希表中,返回nullptr

这里有一个细节,返回值为哈希表中对应位置的地址,这是为后边的删除做伏笔。


4.删除

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

删除我们直接借用Find函数去找到对应位置,无需在通过哈希函数去寻找。

如果要删除的元素不存在,则返回失败,反之,将其位置的标记改为DELETE,即伪删除

下面来理解一下伪删除

因为我们在寻找和插入时的判断条件均为标记位EMPTY,所以删除时只需将该位置的标记改为DELETE,这样就不会影响该位置对应的冲突位的寻找以及新的插入了


5.数据类型问题

我们上述的哈希表实现能够采用哈希函数进行取余操作的前提是数据为int类型,那如果是其他类型的数据,不能进行取余操作,又该如何使用哈希函数呢???

这里的方法是,通过建立仿函数将其他类型转换成int类型

struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};

 此外,有一个特例,string类型不能直接转换为int类型,所以想要满足string类型,我们需要为其单独创造一个仿函数

struct HashStringFunc
{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash += e;}return hash;}
};

思想为,将string类型的每个字符的ascll码值加起来作为其int类型的数据

随后需要在模板中进行添加:

template<class K, class V,class Hash = HashFunc<K>>

这里采用了缺省参数默认情况下为其他类型,当为string类型时,在传入独有的模版。 


五.开散列哈希表

1.基本框架

namespace hash_bucket
{template<class K,class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}};template<class K,class V>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_tables.resize(10, nullptr);_n = 0;}private:vector<Node*> _tables;//指针数组size_t _n;};
}

不同于闭散列的哈希表vector中存放的是数据本身,哈希桶中vector存放的为节点指针


2.插入

因为哈希桶可能会在一个位置下面插入很多的数据如果采用尾插,就必须找到尾结点才能进行插入,效率会很低,所以我们采用头插的方式:

		//插入bool Insert(const pair<K, V>& kv){//判断是否存在if (Find(kv.first))return false;//扩容//......size_t hashi = kv.first % _tables.size();Node* newnode = new Node(kv);//头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;}

创造一个新节点,让新节点去指向原来的头结点,再让新节点成为头结点

下面我们来关注扩容问题,虽然哈希桶是通过链表的方式进行插入,原则上不用进行扩容就可以满足所有数据的存放。但是如果数据过大,会导致每个桶中的数据量过于庞大导致寻找操作的效率大大降低。所以规定,当数据个数等于哈希表的大小,即荷载因子α为1时,进行扩容

那么哈希桶的扩容又该如何进行呢???

因为是节点的缘故,如果像闭散列那样复用插入去扩容,那样就等于同一个节点又创建了一份,而原节点最后还需要进行销毁,这样未免过于麻烦和浪费时间。

所以更好一点的做法是,新建立一个哈希桶,遍历原桶的节点,让其按照哈希函数去直接指向新桶,最后再将两个桶进行交换

			//扩容if (_n == _tables.size()){vector<Node*> newtables; (_tables.size() * 2, nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = kv.first % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullpter;}_tables.swap(newtables);}

3.寻找

		//寻找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;}

寻找操作较为简单,就不在过多分享,注意返回值为节点类型


4.删除

链表的删除无非就三种情况,删除的是头结点,或者是中间节点,尾结点。其中尾结点可以和中间节点共用一种方式

那么如果删除中间节点,就必须提前记录该节点的前一个节点

此外就是头结点,提前定义一个prev,并置空如果cur不为头结点,就让prev继承cur,cur在往后,所以如果首次循环prev就为空,说明要删除的节点即为头结点。 

		bool Erase(const K& key){size_t hashi = key % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){//删除的是第一个节点if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;}else{prev = cur;cur = cur->_next;}}return false;}

最后我们仍需使用仿函数来解决数据类型的问题,因方法与闭散列完全相同,这里不再重复


总结

关于哈希表就分享这么多,喜欢本篇文章记得一键三连,我们下期再见!

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

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

相关文章

场内期权怎么开户?佣金手续费最低是多少?

今天期权懂带你了解场内期权怎么开户&#xff1f;佣金手续费最低是多少&#xff1f;我国的首个场内期权是50ETF期权&#xff0c;随着投资者对期权产品日渐熟悉&#xff0c;投资者参与数量与交易量稳步增长。 场内期权怎么开户&#xff1f; 满足资金要求&#xff1a;根据监管要…

MyBatis:Parameter Maps collection does not contain value for 报错解决收录

MyBatis&#xff1a;Parameter Maps collection does not contain value for 报错问题解决收录 1.报错收录 后端测试时偶然遇到的用mybatis生成好的mapper文件&#xff0c;报Result Maps collection does not contain value…的错误 2.报错分析 java.lang.ILledalAraumentEx…

必应bing国内广告开户首充和开户费是多少?

微软必应Bing作为国内领先的搜索引擎之一&#xff0c;其广告平台凭借其精准的投放、高效的数据分析和广泛的用户覆盖&#xff0c;已成为众多企业的首选。 根据最新政策&#xff0c;2024年必应Bing国内广告开户预充值金额设定为1万元人民币起。这一调整旨在确保广告主在账户初始…

论文阅读--GLIP

把detection和phrase ground(对于给定的sentence&#xff0c;要定位其中提到的全部物体)这两个任务合起来变成统一框架&#xff0c;从而扩展数据来源&#xff0c;因为文本图像对的数据还是很好收集的 目标检测的loss是分类loss定位loss&#xff0c;它与phrase ground的定位los…

爬虫学习--11.MySQL数据库的基本操作(上)

MySQL数据库的基本操作 创建数据库 我们可以在登陆 MySQL 服务后&#xff0c;使用命令创建数据库&#xff0c;语法如下: CREATE DATABASE 数据库名; 显示所有的数据库 show databases; 删除数据库 使用普通用户登陆 MySQL 服务器&#xff0c;你可能需要特定的权限来创建或者删…

内脏油脂是什么?如何减掉?

真想减的人&#xff0c;减胖是很容易的&#xff0c;但想要形体美又健康&#xff0c;还是得从减内脏油脂开始&#xff0c;那么&#xff0c;问题来了&#xff0c;什么是内脏油脂&#xff1f; 油脂它分部于身体的各个角落&#xff0c;四肢、腹部、腰、臀部、脸、脖子...等&#xf…

VUE3+TS+elementplus创建table,纯前端的table

一、前言 开始学习前端&#xff0c;直接从VUE3开始&#xff0c;从简单的创建表格开始。因为自己不是专业的程序员&#xff0c;编程主要是为了辅助自己的工作&#xff0c;提高工作效率&#xff0c;VUE的基础知识并不牢固&#xff0c;主要是为了快速上手&#xff0c;能够做出一些…

Kubernetes中 Requests 和 Limits 的初步理解

1 灵魂拷问 我们在使用 Kubernetes 时是否遇到以下情况&#xff1a; 你会不会部署负载的时候将 CPU requests/limits 设置得过低或过高&#xff1f;你会不会部署负载的时候将 内存 requests/limits 设置得过低或过高&#xff1f;又或者你根本不设置 requests/limits&#xff…

SVN创建项目分支

目录 背景调整目录结构常规目录结构当前现状目标 调整SVN目录调整目录结构创建项目分支 效果展示 背景 当前自己本地做项目的时候发现对SVN创建项目不规范&#xff0c;没有什么目录结构&#xff0c;趁着创建目录分支的契机&#xff0c;顺便调整下SVN服务器上的目录结构 调整目…

【Vue】Vue2使用ElementUI

目录 Element UI介绍特点Vue2使用Element安装引入ElementUI组件库 使用ElementUI用户注册列表展示其他 mint-ui介绍特点安装组件引入组件Mint-ui相关组件 Element UI 介绍 官网(基于 Vue 2.x ):https://element.eleme.cn/#/zh-CN ElementUI 是一个基于 Vue.js 的桌面端组件库…

Vue文本溢出如何自动换行

css新增 word-break: break-all; word-wrap: break-word;

【Linux系统】文件与基础IO

本篇博客整理了文件与文件系统、文件与IO的相关知识&#xff0c;借由库函数、系统调用、硬件之间的交互、操作系统管理文件的手段等&#xff0c;旨在让读者更深刻地理解“Linux下一切皆文件”。 【Tips】文件的基本认识 文件 内容 属性。文件在创建时就有基本属性&#xff0…

网易:一季度营收269亿元,连续7季研发强度超15%领跑行业

5月23日&#xff0c;网易发布2024年第一季度财报。财报显示&#xff0c;网易Q1营收269亿元&#xff0c;归属于公司股东的净利润85亿元&#xff08;Non-GAAP&#xff09;&#xff0c;以连续7个季度超15%的研发投入强度领跑行业&#xff0c;首季业绩稳健启航。 一季度&#xff0…

JVM学习-动态链接和方法返回地址

动态链接–指向运行时常量池的方法引用 每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用&#xff0c;包含这个引用的目的为了支持当前方法的代码能够实现动态链接(Dynamic Linking)&#xff0c;如invokednamic指令。在Java源文件被编译到字节码文件中时&#x…

JAVA:浅谈JSON与JSON转换

可能有很多人&#xff0c;无论是前端还是后端&#xff0c;无论是JAVA还是Python还是C&#xff0c;都应该跟JSON这种数据格式打过交道&#xff0c;那么有没有仔细的想过&#xff0c;什么叫JSON&#xff1f; JSON是一种轻量级的数据交换格式。它基于JavaScript语言的对象表示法&a…

初识java——javaSE(6)抽象类与接口【求个关注!】

文章目录 前言一 抽象类1.1 抽象类的概念1.2 抽象类的语法&#xff1a;1.3 抽象类与普通类的区别&#xff1a; 二 接口2.1 接口的概念2.2 接口的语法2.2.1 接口的各个组成2.2.2 接口之间的继承 2.3 接口的实现接口不可以实例化对象 2.4 接口实现多态 三 Object类3.1 Object类是…

第17讲:C语言内存函数

目录 1.memcpy使用和模拟实现2.memmove使用和模拟实现3.memset函数的使用4.memcmp函数的使用 1.memcpy使用和模拟实现 void * memcpy (void * destination, const void * source, size_t num);• 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存…

网络安全技术与应用:远程控制与数据库安全

实验准备 软件&#xff1a;VMware Workstation Pro 虚拟机&#xff1a;Red Hat Enterprise Linux 7 服务器&#xff0c;Red Hat Enterprise Linux 7 客户端 网络模式&#xff1a;NAT模式 1、配置服务器及客户端网络 服务器IP 客户端IP 测试相互通信 在客户机上设置镜像&#…

车载网络测试实操源码_使用CAPL脚本模拟发送符合协议要求(Counter和CRC)的CAN报文

系列文章目录 车载网络测试实操源码_使用CAPL脚本解析hex、S19、vbf文件 车载网络测试实操源码_使用CAPL脚本对CAN报文的Counter和CRC进行实时监控 车载网络测试实操源码_使用CAPL脚本模拟发送符合协议要求(Counter和CRC)的CAN报文 车载网络测试实操源码_使用CAPL脚本实现安全…

利用神经网络学习语言(四)——深度循环神经网络

相关说明 这篇文章的大部分内容参考自我的新书《解构大语言模型&#xff1a;从线性回归到通用人工智能》&#xff0c;欢迎有兴趣的读者多多支持。 本文涉及到的代码链接如下&#xff1a;regression2chatgpt/ch10_rnn/char_rnn_batch.ipynb 《循环神经网络&#xff08;RNN&…