【C++高阶(五)】哈希思想--哈希表哈希桶

💓博主CSDN主页:杭电码农-NEO💓

⏩专栏分类:C++从入门到精通⏪

🚚代码仓库:NEO的学习日记🚚

🌹关注我🫵带你学习C++
  🔝🔝


在这里插入图片描述

哈希结构

  • 1. 前言
  • 2. unordered系列容器
  • 3. 哈希概念以及哈希结构
  • 4. 哈希表详解(闭散列)
  • 5. 哈希表模拟实现
  • 6. 哈希桶详解(开散列)
  • 7. 哈希桶模拟实现
  • 8. 对于哈希结构的思考

1. 前言

相信大家一定听说过大名鼎鼎的
哈希结构吧,就算是没用过,也听说
过这句话:这道题无脑哈希就能做

哈希,哈希,到底什么是哈希?本篇文章
将带大家彻底搞懂这个问题!

本章重点:

本篇文章着重讲解关联式容器
unordered_map&set的底层结构
以及它们的模拟实现.并且将给大家
介绍unorder系列的接口函数!


2. unordered系列容器

不知道大家在刷题时有没有看见过
unordered_map和unordered_set
它们与map&set是什么关系?
什么时候可以用unordered系列?

带着这些疑问,进行今天的学习:
在这里插入图片描述

  1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
  3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。

可以发现,其实unordered_map和
map使用起来没什么区别,可以说
是一模一样,那么什么时候应该用
unordered系列呢?答案是你只关
心键值对的内容而不关心是否有序
时,选择unordered系列

同理,unordered_set和set的用法
也基本一致,这里就不多做介绍了
如果你不知道map和set的用法,请
先看这篇文章:

map和set的熟悉


3. 哈希概念以及哈希结构

unordered_map&set的底层
结构实际上是哈希桶,也就是
哈希结构,下面来了解一下哈希思想:

最简易的哈希思想,数组下标0到100
存储的值代表数字0到100存不存在

在这里插入图片描述

当然,实际情况下不可能最大值是几
就开辟多大的数组,因为会造成空间
的浪费,哈希的思路一般是根据某种
映射关系,把数据映射到数组中,查找
时也使用同样的映射关系来查找!

在这里插入图片描述
当然,当插入4后再插入14,此时会有问题
因为4这个位置已经被占用了,再次映射到
这个位置明显是行不通的,这个过程被称为
哈希冲突,具体内容会在后面讲解!

哈希结构又分为哈希表和哈希桶
下面就来一一讲解这两个的区别


4. 哈希表详解(闭散列)

引起哈希冲突的一个原因可能是:
哈希函数设计不够合理

在这里插入图片描述
然而不管哈希函数再怎么设计,都不能
完全保证不同的值映射到同一位置,所以
引申出了闭散列和开散列的解决方法

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

寻找下一个空位置的方法有很多,如
线性探测(挨个往后找)
二次探测(以2^i为单位向后找)

这里只讲解线性探测

在这里插入图片描述
插入44后,位置4被占用了就往后找空位

哈希表的删除以及查找操作:

哈希表中的元素如果只是原生数据类型,
那么我们将4删除后,再去查找4肯定是找
不到的,但是此时去查找44也会找不到,因
为44本来应该映射到4位置,但是由于哈希
冲突跑到了8位置,并且我们并不知道它在
哪个位置,所以查找时会找不到!

解决方案:

存储数据不单单存储原生类型
再给每一个位置加上一个状态枚举
分别代表此位置是空,被删除还是有数

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State {EMPTY, EXIST, DELETE};

查找元素时,若此位置是删除或存在
状态就继续向后找,若是空就代表此
元素并不在哈希表中!


5. 哈希表模拟实现

首先我们先将整个结构框架写出来:

enum State
{
	EMPTY,
	EXIST,
	DELETE
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state;
	HashData(const pair<K, V>& kv = make_pair(0, 0))
		:_kv(kv)
		,_state(EMPTY)
	{ }
};

template<class K, class V>
class HashTable
{
private:
	vector<HashData<K, V>> _table;//数组中存储HashData封装的数据
	size_t _size = 0; //有效数据的个数
};

再来探讨一下插入时的扩容规则:

由于哈希表采用的是向后探测的方法
来存放不同的数据,那么当数据的个数
和数组的大小很接近时,会有很多冲突,
所以在容量到0.7或0.8时就应该要扩容了!
并且在扩容后,数据要重新根据先有的规则
进行挪动,也就是将旧数据挪动到新表!

bool insert(const pair<K, V>& kv)
{
	if (_table.size() == 0 || 10 * _size / _table.size() >= 7) // 扩容
	{
		size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
		HashTable<K, V> newHT;
		newHT._table.resize(newSize);
		// 旧表的数据映射到新表
		for (auto e : _table)
		{
			if (e._state == EXIST)
			{
				newHT.insert(e._kv);
			}
		}
		_table.swap(newHT._table);
	}
	size_t index = kv.first % _table.size();//不能模capacity,如果模出来的数大于size了还插入进去了会报错
	//线性探测
	while (_table[index]._state == EXIST)
	{
		index++;
		index %= _table.size();//过大会重新回到起点
	}
	_table[index]._kv = kv;
	_table[index]._state = EXIST;
	_size++;
	return true;
}

HashData<K, V>* find(const K& key)
{
	if (_table.size() == 0)
		return nullptr;

	size_t index = key % _table.size();//负数会提升成无符号数,所以负数不影响结果,但是string类不能取模,需要加入一个仿函数
	size_t start = index;
	while (_table[index]._state != EMPTY)
	{
		if (_table[index]._kv.first == key && _table[index]._state == EXIST)
			return &_table[index];
		index++;
		index %= _table.size();
		if (index == start)//全是DELETE时,必要时会break
			break;
	}
	return nullptr;
}

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

6. 哈希桶详解(开散列)

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

哈希桶实际上是这样的结构:

在这里插入图片描述

看似是一格数据,其实是一个链表指针

并且开散列的扩容旧不需要像
闭散列一样到0.7旧扩容了

在这里插入图片描述

可以把数组的每一个位置想象成
一个抽屉,当你远观时它就是一个
单一的格子,当你仔细把玩时它就
是一个可以拉开的存储结构!


7. 哈希桶模拟实现

首先先把基础框架写出来:

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;
private:
	vector<Node*> _table;
	size_t _size = 0;//有效数据个数
};

下一步,将新来的元素头插到链表中
因为头插的效率是O(1),并且扩容后
的策略和哈希表一样,重新将数据映射
到新表中

bool insert(const pair<K, V>& kv)
{
	//去重+扩容
	if (find(kv.first))
		return false;
	//负载因子到1就扩容
	if (_size == _table.size())
	{
		vector<Node*> newT;
		size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
		newT.resize(newSize, nullptr);
		//将旧表中的节点移动到新表
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				size_t hashi = cur->_kv.first % newT.size();
				cur->_next = newT[hashi];
				newT[i] = cur;
				cur = next;
			}
			_table[i] == nullptr;
		}
		_table.swap(newT);
	}
	size_t hashi = kv.first % _table.size();
	//头插
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];
	_table[hashi] = newnode;
	++_size;
	return true;
}

Node* find(const K& key)
{
	if (_table.size() == 0)
		return nullptr;
	size_t hashi = key % _table.size();
	Node* cur = _table[hashi];
	while (cur)//走到空还没有就是没用此数据
	{
		if (cur->_kv.first == key)
			return cur;
		cur = cur->_next;
	}
	return nullptr;
}

bool erase(const K& key)
{
	Node* ret = find(key);
	if (ret == nullptr)
		return false;
	size_t hashi = key % _table.size();
	Node* cur = _table[hashi];
	Node* prev = nullptr;
	while (cur && cur->_kv.first != key)//找到要删除的节点
	{
		prev = cur;
		cur = cur->_next;
	}
	Node* next = cur->_next;
	if (cur == _table[hashi])//注意头删的情况
		_table[hashi] = next;
	else
		prev->_next = next;
	delete cur;
	cur = nullptr;
	_size--;
	return true;
}

对代码的解释都在注释中,还有问题欢迎私信!


8. 对于哈希结构的思考

我们会发现一个问题,不管是哈希
表还是哈希桶,都用到了cur.first模
上一个数,但是如果cur.first不是整型
不能取模怎么办?(如字符串)

这时需要在哈希类中再传入一个模板
参数,此模板参数为仿函数,只需将写好
的仿函数传入即可进行取模,比如string
仿函数可以这样写:

template<>
struct HashFunc<string>
{
	//BKDR算法:将字符串转换为整数
	size_t operator()(const string& str)
	{
		size_t sum = 0;
		for (auto ch : str)
		{
			sum *= 131;
			sum += (size_t)ch;
		}

		return sum;//将字符的asc码全部加起来再返回
	}
};

🔎 下期预告:哈希思想的应用🔍

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

01.vue3大事件——项目初始化、技术介绍

后台数据管理系统 - 项目架构设计 在线演示&#xff1a;https://fe-bigevent-web.itheima.net/login 接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835 接口根路径&#xff1a; http://big-event-vue-api-t.itheima.net 本项…

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑氢储一体化协同的综合能源系统低碳优化》

这个标题涉及到考虑了多个方面的能源系统优化&#xff0c;其中关键的关键词包括"氢储一体化"、"协同"、"综合能源系统"和"低碳优化"。以下是对这些关键词的解读&#xff1a; 氢储一体化&#xff1a; 氢储存&#xff1a; 指的是氢气的存…

网络运维与网络安全 学习笔记2023.11.27

网络运维与网络安全 学习笔记 第二十八天 今日目标 OSPF基本原理、OSPF单区域配置、OSPF多区域配置 特殊区域之Stub、特殊区域之NSSA OSPF基本原理 项目背景 随着企业的发展&#xff0c;网络的规模越来越大&#xff0c;网段的数量越来越多&#xff0c;公司内部的路由器的…

JSP 条件动作标签之choose when otherwise组合标签详解

好 上文JSP 条件动作标签之if标签详解中 我们详细的说了说 if标签 但是 这个if是没有else的 多少对我们的编程习惯没有那么友好 所以 就出现了另外一种语法 由 choose when otherwise组成 和我们java中的switch语句 我们的基本语法就是 外面一个大的choose包裹起来 里面是很多…

VMware上pfsense开源防火墙的下载、安装、简单配置

文章目录 1. pfsense概述1.1. 官方描述1.2. 个人描述 2. pfsense下载2.1. 官网下载 3. pfsense安装3.1. 官网手册3.2. 安装步骤 4. pfsense配置4.1. 默认账号密码4.2. 初始化配置4.3. 切换语言 5. 简单测试5.1. 调整测试网络5.2. 测试结果 6. 虚拟机操作界面讲解7. 最后 1. pfs…

MySQL 高可用架构

MySQL 是实际生产中最常用的数据库&#xff0c;生产环境数据量极为庞大&#xff0c;对性能和安全要求很高&#xff0c;单机的 MySQL 是远远达不到的&#xff0c;所以必须搭建一个主从复制架构&#xff0c;同时可以基于一些工具实现高可用架构&#xff0c;在此基础上&#xff0c…

基于python协同过滤推荐算法的电影推荐与管理系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 电影推荐与管理系统是一个基于Python的协同过滤推荐算法的应用&#xff0c;它可以帮助用户根据他们的兴趣和偏好进行…

2016年五一杯数学建模C题二孩政策问题解题全过程文档及程序

2016年五一杯数学建模 C题 二孩政策问题 原题再现 多年来实施的严、紧计划生育政策对控制人口增长起到关键作用。在优生优育政策的指引下&#xff0c;我国人口质量显著提高&#xff0c;但也带来了不利影响&#xff0c;生育率偏低、男女比例失衡、人口老龄化情况严重等问题。2…

Linux 常用基本命令

文章目录 7.1 帮助命令7.1.1 man 获得帮助信息7.1.2 help 获得shell内置命令的帮助信息7.1.3 常用快捷键 7.2 文件目录类7.2.1 pwd 显示当前工作目录的绝对路径7.2.2 ls 列出目录的内容7.2.3 cd 切换目录7.2.4 mkdir 创建一个新的目录7.2.5 rmdir 删除一个空的目录7.2.6 touch …

【JavaEE】多线程 (2) --线程安全

目录 1. 观察线程不安全 2. 线程安全的概念 3. 线程不安全的原因 4. 解决之前的线程不安全问题 5. synchronized 关键字 - 监视器锁 monitor lock 5.1 synchronized 的特性 5.2 synchronized 使⽤⽰例 1. 观察线程不安全 package thread; public class ThreadDemo19 {p…

ICMPv6报文与邻居状态跟踪

ICMPv6报文 ICMPv6(Internet Control Message Protocol for the IPv6)是IPv6的基础协议之一。 在IPv4中,Internet控制报文协议ICMP(Internet Control Message Protocol)向源节点报告关于向目的地传输IP数据包过程中的错误和信息。它为诊断、信息和管理目的定义了一些消息…

计算一个Series序列的n阶滞后相关系数Series.autocorr()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 计算一个时间序列的 n阶滞后自相关系数 Series.autocorr(n) [太阳]选择题 以下代码的说法中正确的是? import pandas as pd s1 pd.Series([1,2,3,4,5,6]) print("【显示】s1:\n",…

【JAVA学习笔记】71 - JDBC入门

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter25/src/com/yinhai/dao_ 一、JDBC概述 1.基本介绍 1. JDBC为访问不同的数据库提供了统一的接口&#xff0c;为使用者屏蔽了细节问题。 2. Java程序员使用JDBC,可以连接任何提供了JDBC驱动…

opencv-利用DeepLabV3+模型进行图像分割去除输入图像的背景

分离图像中的人物和背景通常需要一些先进的图像分割技术。GrabCut是一种常见的方法&#xff0c;但是对于更复杂的场景&#xff0c;可能需要使用深度学习模型。以下是使用深度学习模型&#xff08;如人像分割模型&#xff09;的示例代码&#xff1a; #导入相关的库 import cv2 …

VMware上面安装部署centos7镜像系统【详细含镜像】

VMware上面安装部署centos7镜像系统【详细含镜像】 废话不多说直接开始 下载centos7镜像 网上有好多&#xff0c;但是我相信来看小编文章的基本上应该都有centos7的镜像了吧&#xff0c;毕竟咱们都是同一类人&#xff0c;哈哈不卖关子了&#xff0c;小编直接给大家一个百度云盘…

OpenCV项目开发实战--基本图像分割图生成器

欢迎回到我们有关 OpenCV 的系列文章以及我们如何利用其强大的图像预处理功能。在我们之前的文章的基础上,今天我们向您展示如何创建基本的图像分割图生成器。 具体来说,我们的图像掩模应该帮助识别每个像素是否: 背景的一部分(指定值为0)在感兴趣的对象的边缘(指定值 …

答题活动小程序竞品分析

答题小程序竞品分析 答题活动小程序竞品分析 知识竞赛小程序竞品分析 ~ 从2020年开始&#xff0c;机缘巧合&#xff0c;我开始涉及答题小程序的开发&#xff0c;从最初的刷题场景到答题活动场景&#xff0c;已经走过了三个年头&#xff0c;这期间我开发的答题小程序产品也逐…

laravel8中常用路由使用(笔记四)

目录 1、框架路由目录统一放该目录 2、基本路由,路由都调用Route方法 3、控制器使用路由 4、路由参数 5、路由组 6、命名路由 7、命令查看当前路由列表 8、路由缓存 在Laravel 8中&#xff0c;路由定义了应用程序中接受请求的方式。它们定义了URL和相应的控制器方法之间的…

ros2智能小车中STM32地盘需要用到PWM的模块

我做的地盘比较简单&#xff0c;使用了一下模块&#xff1a; 4个直流减速电机&#xff0c;&#xff08;每个模块用到了一个PWM&#xff09;---这会通过L298N的ENA,ENB来实现控制 光电对射测速模块&#xff08;不用PWM) 超声波测距模块&#xff08;不用PWM&#xff0c;只需要…

Ceph----CephFS文件系统的使用:详细实践过程实战版

CephFS 介绍 是一个基于 ceph 集群 且兼容 POSIX 标准的文件系统。 创建 cephfs 文件系统时 需要在 ceph 集群中添加 mds 服务&#xff0c;该服务 负责处理 POSIX 文件系统中的 metadata 部分&#xff0c; 实际的数据部分交由 ceph 集群中的 OSD 处理。 cephfs 支持以内核模块…
最新文章