【C++进阶】哈希思想之哈希表和哈希桶模拟实现unordered_map和unordered_set

哈希表和哈希桶

  • 一,什么是哈希
  • 二,关联式容器unordered_map/set
    • 1. unordered_map
    • 2. unordered_set
  • 三,哈希的结构
    • 1. 哈希函数
    • 2. 哈希冲突
  • 四,哈希表(闭散列)及其模拟实现
  • 五,哈希桶(开散列)及其模拟实现
  • 六,封装unordered_set和unordered_map

学习C++的过程就像是打怪升级,我们上一节讲完了红黑树,这一节我们来看哈希。

一,什么是哈希

哈希我们不陌生,在数据结构的排序部分我们实现过基数排序,这其实就是一种哈希思想的实现。
哈希(也叫散列),是一种映射的思想(关键值和值建立一种关联关系)
哈希表是哈希思想的一种实现。
下面我们先来看一组关联式容器unordered_map/set

二,关联式容器unordered_map/set

unordered_map/set和map和set的用法类似,只是底层的实现是由哈希实现的

1. unordered_map

在这里插入图片描述

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

下面是unordered_map的一些接口,和map的用法类似
在这里插入图片描述

2. unordered_set

unordered_set和unordered_map类似,可以参考下面的文档链接: unordered_set文档

三,哈希的结构

哈希结构就是构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时又通过该函数可以很快找到该元素的结构

1. 哈希函数

哈希函数就是一种将关键码和存储位置进行一一映射的函数

这里介绍两种常见的函数:
直接定址法
适用于关键值比较集中,数据量不大的场景下。关键值和存储位置一一对应,不存在哈希冲突,但是空间浪费大。

除留余数法
可以用于分散的,数据量可以很大的场景。是多对一的关系,存在哈希冲突。

2. 哈希冲突

哈希冲突(也叫哈希碰撞)就是不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

出现哈希冲突的原因也有可能是哈希函数设计的不合理

解决哈希冲突的两种常见的方法是:
开放定址法(闭散列)
当前位置被占用时,在开放空间中按照某种规则去找没有被存储的地方
开放定址法又有两种方式:
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是表
的大小。
在这里插入图片描述


开散列(哈希桶/拉链法)
开散列法又叫链地址法(开链法),对关键码用哈希函数计算地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
在这里插入图片描述
知道了这两种方式的区别后我们就来实现一下这两种方式

四,哈希表(闭散列)及其模拟实现

先来实现闭散列,开放地址法实现哈希表
开放定址法要判断这个位置是否有值,我们可以用一个标记位来标记:
这里用枚举类型来表示某个位置的三种状态(空位置,已经存在值,这个位置的值被删除)
每个数据节点中有两个关键值,键值对pair和标记位:

enum Status{//表中的每个位置都要有状态
	EMPTY,
	EXIST,
	DELETE
};

template<class K,class V>
struct HashDate {
	pair<K, V> _kv;
	Status _stat;
};

哈希表中既可以存储内置类型,也可以存储自定义类型,对于内置类型我们可以直接进行取模运算来进行哈希,但是自定义类型呢?

拿string为例,如果要将string映射在存储位上,无法直接进行取模运算,这里我们就要先将string映射为整型,再将这个整型经过哈希函数映射到存储的位置。
再这里我们借助仿函数来将内置类型和自定义类型转换为整型去进行哈希

//仿函数来将非整型来转换成整型去插入(线性探测要求用整型去求其所映射的位置)
template<class T>//这里只能将一般的类型强转成整型
struct Hashfunc {
	size_t operator()(const T& t) {
		return (size_t)t;
	}
};

//为了支持将string也转换成整型,这里做了特化
template<>
struct Hashfunc<string> {
	size_t operator()(const string& s) {//思路是将string的每个字符的ASCII相加来转换成整型
		//KBDR
		size_t hash = 0;
		for (auto e : s) {
			hash *= 31;//因为string对应的整型数量接近无限,而整型的数量只有2^32个,所以出现哈希冲突的可能性较高,为了降低冲突,这里做了特殊处理
			hash += e;
		}
		return hash;
	}
};

做好了这些准备工作,我们就可以用线性探测实现插入了
在这里提出一个负载因子的概念,表示当前哈希表中被占用的位置多少
负载因子 = 数据个数 / 表的大小
线性探测法中负载因子是0.7时扩容,负载因子不能太大也不能太小,太大会出现哈希冲突,太小会造成空间浪费。

这里我们又提出扩容的问题,当负载因子达到0.7时进行扩容,扩容后在新表中重新建立映射关系
具体实现如下:

bool insert(const pair<K,V> &kv) {

	if (find(kv.first)) {//不可以有重复元素
		return false;
	}
	
	if (_n * 10 / _tables.size() == 7) {//_n和size都是无符号整型,这里给_n乘10可以防止出现小数
		size_t newSize = _tables.size() * 2;
		HashTable<K, V,Hash> newHT;//再开一个新表
		newHT._tables.resize(newSize);//提前开二倍的size
		//遍历旧表,将旧表的数据插入到新表,重新建立映射
		for (size_t i = 0; i < _tables.size(); i++) {
			if (_tables[i]._stat == EXIST) {
				newHT.insert(_tables[i]._kv);
			}
		}
		_tables.swap(newHT._tables);

	}

	Hash hf;//实例化仿函数
	//线性探测
	size_t hashi = hf(kv.first) % _tables.size();//这里除以size而不是capacity,如果模的是capacity的话,会插入在size之外
												//而插入时[]只能访问size之内的
	while (_tables[hashi]._stat == EXIST) {//当这个位置的状态表示有值时会往后找空闲位置
		hashi++;
		hashi %= _tables.size();//再环回回去继续找
	}
	_tables[hashi]._kv = kv;
	_tables[hashi]._stat = EXIST;
	++_n;

	return true;
}

完整的代码可以进入我的gitee仓库进行参考: 哈希表的实现

五,哈希桶(开散列)及其模拟实现

现在我们来实现开散列,开散列我们在上面进行了讲解,就是将具有相同特征的数据元素放到一个桶中,所以也就不需要再判断这个位置的状态了。

这里的每个节点中出了要保存的数据外,还要定义一个next指针来链接该桶中这个节点的下一个节点。


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)
	{}
};

哈希桶的结构是由一个vector中存储一个个的桶
所以插入的操作也比较简单,将关键值经过哈希映射后找到对应的位置,直接将这个节点头插进所在的这个桶即可
但是哈希桶的平衡因子可以达到1再扩容,因为哈希桶解决了哈希冲突,在有些情况下,当一个桶的长度太大时可以将其放入红黑树中。

bool insert(const pair<K, V>& kv) {

	if (find(kv.first)) {
		return false;
	}

	//扩容
	if (_n == _tables.size()) {//负载因子可以达到1再扩容
		size_t newSize = _tables.size() * 2;
		HashTable<K, V> newHT;//定义一个新的哈希表
		newHT._tables.resize(newSize);
		//遍历旧表,将旧表中的数据放进新表
		for (size_t i = 0; i < _tables.size(); i++) {
			Node* cur = _tables[i];
			while (cur) {
				newHT.insert(cur->_kv);
				cur = cur->_next;
			}
		}
		_tables.swap(newHT._tables);

	}

	size_t hashi = kv.first % _tables.size();
	Node* newnode = new Node(kv);

	//在映射的位置直接头插
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;//新节点成为这个位置的第一个
	++_n;

	return true;
}

想看完整代码点击这里跳转: 哈希桶的实现

六,封装unordered_set和unordered_map

在简单实现了哈希桶之后,我们就可以来封装unordered_set和unordered_map了。这个部分的封装比较复杂,用了很多的模板参数,我们需要捋清各个函数间的关系再来看
感兴趣的朋友可以在我的gitee仓库中参考一下: 封装unordered_set和unordered_map

如果大家有什么疑问也欢迎提出问题。

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

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

相关文章

练手项目层高阶3—《详解文件版本——通讯录管理系统》

文章目录 &#x1f6a9;前言&#x1f9e9;框架结构&#x1f4fa;效果展示&#x1f680;完整代码 &#x1f6a9;前言 我们前面写的两种方法&#xff08;静态和动态)&#xff0c;唯一缺点就是每次运行都要输入新的数据&#xff0c;很麻烦&#xff0c;也就是说写入的数据无法长久保…

树莓派5使用体验

原文地址&#xff1a;树莓派5使用体验 - Pleasure的博客 下面是正文内容&#xff1a; 前言 好久没有关于教程方面的博文了&#xff0c;由于最近打算入门嵌入式系统&#xff0c;所以就去购入了树莓派5开发板 树莓派5是2023年10月23日正式发售的&#xff0c;过去的时间不算太远吧…

XML文档节点导航与选择指南 | XPath基础知识

XPath&#xff08;XML Path Language&#xff09;是XSLT标准的主要组成部分。它用于在XML文档中浏览元素和属性&#xff0c;提供了一种强大的定位和选择节点的方式。 XPath的基本特点 代表XML路径语言&#xff1a; XPath是一种用于在XML文档中导航和选择节点的语言。 路径样式…

【CSS】新闻页面制作 案例一

&#xff08;大家好&#xff0c;今天我们将通过案例实战对之前学习过的CSS知识进行复习巩固&#xff0c;大家和我一起来吧&#xff0c;加油&#xff01;&#x1f495;&#xff09; 目录 一、前述 二、素材 案例文字素材 案例图片素材 三、案例分析 四、案例实施 五、应用…

Docker之镜像与容器的相关操作

目录 一、Docker镜像 搜索镜像 下载镜像 查看宿主机上的镜像 删除镜像 二、Docker容器 创建容器 查看容器 启停容器 删除容器 进入容器 创建/启动/进入容器 退出容器 查看容器内部信息 一、Docker镜像 Docker 运行容器前需要本地存在对应的镜像&#xff0c; 如…

安装包逆向2

import os import struct import lzma import hashlibDEBUG False # 调试标志 BASE_ADDRESS 0x00120200 # 基地址偏移量# Base类&#xff0c;用于存储基地址的数据 class Base:def __init__(self):self.startFilePos 0 # 基地址在文件中的起始位置self.data bytearray(0…

【蓝桥杯嵌入式】按键控制LED与LCD(必考三件套)

【蓝桥杯嵌入式】按键控制LED与LCD&#xff08;必考三件套&#xff09; 前言LED相关功能的实现LED基础功能函数&#xff08;点亮、全熄灭、翻转&#xff09;LED的闪烁与定时点亮熄灭流水灯的实现 按键的扫描及长短按、双击的实现按键的短按按键业务逻辑程序进程按键的长短按长短…

1995-2021年各省分品种能源产量和消费量数据

1995-2021年各省分品种能源产量和消费量数据 1、时间&#xff1a;1995-2021年 2、来源&#xff1a;能源统计年鉴、各省年鉴 3、指标&#xff1a;能源消费总量、煤炭消费量、焦炭消费量、原油消费量、汽油消费量、煤油消费量、柴油消费量、燃料油消费量、天然气消费量、电力消…

让智能体像孩子一样观察别人学习动作,跨视角技能学习数据集EgoExoLearn来了

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 在探索人工智能边界时&#xff0c;我们时常惊叹于人类孩童的学习能力 —— 可以轻易地将他人…

Unity学习笔记 - 第一个Hello World都算不上的项目

一、Unity安装 这里不细说安装了&#xff0c;首先需要Visual Studio&#xff0c;然后要安装Unity Hub&#xff0c;Unity Hub就像一个管理平台&#xff0c;安装完它之后&#xff0c;可以在它的界面上选择安装各个版本的编辑器。 开始您的创意项目并下载 Unity Hub | Unity通过 …

【Qt 学习笔记】Qt 中出现乱码的解释及讨论

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt 中出现乱码的解释及讨论 文章编号&#xff1a;Qt 学习笔记 / 06 文…

Nginx配置之localhost和反向代理

文章目录 第一步、查看安装位置和配置文件第二步、web服务器设置第三步、localhost 指令第四步、设置反向代理 清明假期&#xff0c;在家练习Nginx配置&#xff0c;在前期【 linux环境下安装配置nginx代理服务器】已经完成nginx环境搭建&#xff0c;本期主要实践web服务器&…

副业选择攻略:如何找到最适合自己的那一个?

大家好&#xff0c;我是木薯。今天有个新人伙伴来咨询客服&#xff1a;新手适不适合在水牛社上做副业&#xff1f;什么样的副业适合自己&#xff1f; 这种问题其实对我们来说已经见得太多太多了&#xff0c;归其原因是因为自己对副业没有一个清晰的自我认知&#xff0c;从而感觉…

阿里千问大模型 Qwen1.5 开源 32B 模型,将开源进行到底!!!

阿里开源的千问系列模型&#xff0c;一直受到业界好评&#xff0c;之前版本有0.5B、1.8B、7B、14B、72B&#xff0c;但一直缺少的30B级别开源模型&#xff0c;这也一直是一个遗憾。 怎么说呢&#xff1f;72B模型太大&#xff0c;很多人用不起来&#xff0c;无论是微调&#xf…

基于JAVA+SSM+微信小程序+MySql+前后端分离的图书捐赠管理系统设计与实现

一、项目背景介绍&#xff1a; 在当今社会&#xff0c;图书捐赠是一种普遍而有益的行为&#xff0c;旨在促进阅读、教育和知识传播。图书捐赠可以帮助改善教育资源不足的地区、学校和社区的阅读环境&#xff0c;提供更多的学习机会和知识获取途径。随着互联网和移动技术的发展&…

pytorch交叉熵

目录 1. Entropy2. 交叉熵3. 二分类4. 为什么分类问题使用交叉熵5. 代码示例 1. Entropy Entropy中文是熵的意思&#xff0c;它代表一种不确定性&#xff0c;不确定性越高惊喜度也就越高。 如上图&#xff0c;假设熵代表中奖概率&#xff0c;当熵为2 的中奖概率为1/4没什么惊…

sharding‐jdbc之分库分表(mysql主从同步的数据库安装和使用)

水平分表 创建基础工程.. 引入sharding‐jdbc的maven依赖包 注意需要数据库连接池等依赖 <dependency><groupId>org.apache.shardingsphere</groupId><artifactId>sharding-jdbc-spring-boot-starter</artifactId><version>4.0.0-RC1&l…

1.0-spring入门

文章目录 前言一、版本要求二、第一个spring程序1.引入pom2.代码部分2.1 spring bean2.2 springContext.xml2.3 测试2.4 执行结果 总结 前言 最近想要系统的学习下spring相关的框架,于是乎,来到了B站(真是个好地方),spring会专门开一个专栏出来,记录学习心得,与大家共勉。 Spri…

51-37 由浅入深理解 Stable Diffusion 3

2024年3月5日&#xff0c;Stability AI公开Stable Diffusion 3论文&#xff0c;Scaling Rectified Flow Transformers for High-Resolution Image Synthesis。公司像往常一样承诺后续将开源代码&#xff0c;开源之光&#xff01;&#xff01;&#xff01; 在LDW潜在扩散模型论文…

缓存击穿以及解决方案

1.定义 缓存击穿问题也叫热点Key问题&#xff0c;就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了&#xff0c;无数的请求访问会在瞬间给数据库带来巨大的冲击。 问题描述&#xff1a;假设线程1在查询缓存之后&#xff0c;本来应该去查询数据库&#xff0c;然后把…