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

news2024/10/6 22:23:19

哈希表和哈希桶

  • 一,什么是哈希
  • 二,关联式容器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

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

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

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

相关文章

Android JNI调试总结

1、确保NDK和CMake已经安装 新建能编译APK的工程&#xff0c;工程中添加相关ndk目录 2、添加C模块 添加完成后&#xff0c;工程目录自动更新&#xff0c;build.gradle导入了so编译器 修改build.gradle中添加相关gcc编译器如下 externalNativeBuild { cmake { abiFilters a…

零信任安全模型:构建未来数字世界的安全基石

在数字化转型的浪潮中&#xff0c;云原生技术已成为推动企业创新和灵活性的关键力量&#x1f4a1;。然而&#xff0c;随着技术的进步和应用的广泛&#xff0c;网络安全威胁也日益严峻&#x1f513;&#xff0c;传统的网络安全模型已经难以应对复杂多变的网络环境。在这样的背景…

webpack环境配置分类结合vue使用

文件目录结构 按照目录结构创建好文件 控制台执行: npm install /config/webpack.common.jsconst path require(path) const {merge} require(webpack-merge) const {CleanWebpackPlugin} require(clean-webpack-plugin) const { VueLoaderPlugin } require(vue-loader); c…

Spring Security——11,自定义权限校验方法

自定义权限校验方法 一键三连有没有捏~~ 我们也可以定义自己的权限校验方法&#xff0c;在PreAuthorize注解中使用我们的方法。 自定义一个权限检验方法&#xff1a; 在SPEL表达式中使用 ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的 hasAuthority方法&am…

中文地址分词器源码阅读(jiedi)

文章目录 structure.p文件pd.read_excelenumerate思维导图核心源码讲解jiedi.pytrain.py 总结 structure 点击左边的Structure按钮就如Structure界面。从Structure我们可以看出当前代码文件中有多少个全局变量、函数、类以及类中有多少个成员变量和成员函数。 其中V图标表示全…

mid转MP3怎么转?一分钟搞定~

MIDI&#xff08;Musical Instrument Digital Interface&#xff09;文件格式的诞生可以追溯到上世纪80年代&#xff0c;音频技术迅速崛起。为了让不同音乐设备之间能够互相通信&#xff0c;MIDI格式成为了音乐的标准。它不同于常见的音频文件格式&#xff0c;不包含实际的声音…

基于java web的超市管理系统

摘要 随着社会经济的不断发展&#xff0c;人们的生活水平不断提高。越来越多的零售行业得到了快速的发展&#xff0c;以最常见的超市最为明显。零售行业繁荣的背后也随之带来了许多行业隐患&#xff0c;越来越激烈的行业竞争不断的要求经营者更加高要求的管理超市内部的整个供…

CleanMyMac有必要购买吗?有哪些功能

作为一位产品营销专家&#xff0c;对各类软件产品的功能和特点都有深入的研究&#xff0c;对于CleanMyMac这款产品也有深入了解。CleanMyMac是一款专为Mac用户设计的系统清理与优化软件&#xff0c;旨在帮助用户解决Mac电脑使用过程中的各种问题&#xff0c;让电脑恢复如新的状…

Linux系统中网络协议栈优化

在现代计算机网络中&#xff0c;网络协议栈是实现网络通信的核心组件之一。在Linux系统中&#xff0c;网络协议栈的优化对于提高网络性能、降低延迟、增强安全性等方面至关重要。本文将深入探讨Linux系统中网络协议栈的优化方法和技术&#xff0c;包括使用更快的网络协议栈和禁…

基于Arduino nano配置银燕电调

1 目的 配置电调&#xff0c;设置电机转动方向&#xff0c;使得CW电机朝顺时针方向转动&#xff0c;CCW电机朝逆时针转动。 2 步骤 硬件 Arduino nano板子及USB线变阻器银燕电调EMAX Bullet 20A朗宇电机 2205 2300KV格氏电池3S杜邦线若干接线端子 软件 BLHeliSuite 注意…

【LeetCode: 21. 合并两个有序链表 + 链表】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

Redis实现高可用持久化与性能管理

前言 在生产环境中&#xff0c;为了实现Redis的高可用性&#xff0c;可以采用持久化、主从复制、哨兵模式和 Cluster集群的方法确保数据的持久性和可靠性。这里首先介绍一下使用持久化实现服务器的高可用。主从复制、哨兵模式和集群介绍请参考&#xff1a;Redis主从复制、哨兵…

STL库 —— vector 的编写

一、成员变量 二、容量成员 2.1 size 函数 我们在定义私有成员时就会发现&#xff0c;其中 _finish 相当于 string 中的 size 的地址&#xff0c; _endofstorage 相当于 string 中的 capacity 的地址&#xff0c;所以 size 函数和 capacity 函数其实基本没有改变。 size_t s…

MIT6.828 Lab1 Xv6 and Unix utilities

2023MIT6.828 lab-1 官方地址 一、sleep 实验内容 调用sleep&#xff08;系统调用&#xff09;编写用户级别程序能暂停特定时常的系统滴答程序保存在user/sleep.c 实验过程 xv6的参数传递 查看官方文档提示的文件中&#xff0c;多采用如下定义&#xff1a; int main(in…

如何在nuxt中优雅使用swiper,实现过渡反向+贴合无缝+循环播放【核心代码分析】

视频效果 20240402-1723 图片效果 技术栈 Nuxt3 + Swiper11 Nuxt3 Nuxt: The Intuitive Vue Framework Nuxt Swiper11 Swiper - The Most Modern Mobile Touch Slider (swiperjs.com) 当然你也可以是使用nuxt-swiper Nuxt-Swiper GitHub - cpreston321/nuxt-swiper: Swi…

CPU+GPU+NPU三位一体AI边缘控制器,三屏异显,搭载RK3588处理器

XMS-201采用了Rockchip RK3588八核64位处理器&#xff0c;集成ARM Mali-G610 MP4四核GPU&#xff0c;内置AI加速器NPU&#xff0c;可提供6Tops算力&#xff0c;支持主流的深度学习框架&#xff1b;性能强劲的RK3588可为各类AI应用场景带来更强大的性能表现&#xff0c;适用于机…

【40分钟速成智能风控1】互联网金融风险管理简介

目录 瓦联网金融的发展和现状 风险管理类型划分 欺诈风险 第一方和第三方 账户级和交易级 个人和团伙 互联网金融是传统金融业务与新兴互联网技术结合的一个交叉领域&#xff0c;例如互联网公司开展的金融业务&#xff0c;或者金融机构的线上化服务&#xff0c;都属于互联…

Brain.js 的力量:构建多样化的人工智能应用程序

机器学习&#xff08;ML&#xff09;是人工智能 (AI) 的一种形式&#xff0c;旨在构建可以从处理的数据中学习或使用数据更好地执行的系统。人工智能是模仿人类智能的系统或机器的总称。 机器学习&#xff08;ML&#xff09;与复杂的数学纠缠在一起&#xff0c;让大多数初学者…

08 | Swoole 源码分析之 Timer 定时器模块

原文首发链接&#xff1a;Swoole 源码分析之 Timer 定时器模块 大家好&#xff0c;我是码农先森。 引言 Swoole 中的毫秒精度的定时器。底层基于 epoll_wait 和 setitimer 实现&#xff0c;数据结构使用最小堆&#xff0c;可支持添加大量定时器。 在同步 IO 进程中使用 seti…

three.js能实现啥效果?看过来,这里都是它的菜(01)

经常有老铁问我&#xff0c;这里炫酷效果是如何实现的&#xff0c;还有问我想实现什么效果怎么办&#xff0c;甚至还有想实现动态效果&#xff0c;但是描述不出来的。 好吧&#xff0c;统统满足老铁们呢&#xff0c;本期开始分享three.js效果图&#xff0c;并附带简要简介&…