c++哈希表(原理、实现、开放寻址法)适合新手

news2024/12/1 10:39:32

c++系列哈希的原理及实现(上)

文章目录

  • c++系列哈希的原理及实现(上)
  • 前言
  • 一、哈希的概念
  • 二、哈希冲突
  • 三、哈希冲突解决
    • 3.1、开放寻址法
    • 3.2、删除操作
    • 3.3、负载因子
    • 四、代码实现
  • 总结


前言

红黑树平衡树和哈希有不同的用途。

红黑树、平衡树这类数据结构是有序的数据结构,它们可以高效地进行范围查询,比如查找一个区间内的值。在需要保持数据有序存储,并且频繁进行插入、删除和查找操作的场景下很有用,像数据库索引的实现就可能会用到。

而哈希主要用于快速的数据查找。它通过一个哈希函数把数据映射到一个特定的位置,理想情况下,查找操作可以在常数时间复杂度内完成,也就是时间复杂度为O(1)。在只需要快速判断某个元素是否存在的场景下,哈希就非常合适。所以学了红黑树等平衡树之后还需要学哈希,是因为它们解决的是不同类型的问题。


一、哈希的概念

首先我们要知道顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。
而我们想要的是可以不经过任何比较,一次直接从表中得到要搜索的元素。
下面我们看一个小问题:

有这样一个由小写字母组成的字符串“abcdef”我们需要查找某一个字符是否存在,这时我们就可以将他映射到一个数组中,要查找字符是否存在,我们只需要利用这给映射方法,判断它对应数组中的值是否为1即可。

string s = "abcdef";
int arr[26] = {0};//开辟空间初始化为0
for (auto ch : s)
{
    arr[ch - 'a'] = 1;
}

这就是利用哈希思想,哈希就是一种特殊的存储结构,通过特定的函数(方法),使得数据的存储位置与它的关键码之间建立一种一一映射的关系,这样在查找数据时就可以直接通过关键值来快速查找,这个函数也称为映射函数。
再来看一个例子:
对于这样一组数据{1,2,4,5,7,6},我们要将他映射到一个数组中就可以使用直接映射,即:
在这里插入图片描述
而如果数据的范围较大如:{1,2,3,4,7,6,9999999},这样一组数据,如果使用直接映射,就要开辟足够大的空间,这样开空间浪费的有点离谱了,这时我们就需要给它提供一个方法,将数据控制在一定的范围,所以就有了,除留余数法。我们将这个方法封装为一个函数,这个函数就称为映射函数
除留余数法:
我们将待存入数,对开辟空间大小,进行取余,得到的余数,作为下标,利用下标将带存入数存入到空间中。(待存入数据,我们称为关键字,余数我们称为,存储位置)

size_t hashFunc(size_t key)
{
    size_t i=key%capacity;
    return i;
}

在这里插入图片描述
下面我们再思考一个问题:
如果我们想再存储一个3,通过3进行哈希映射发现,家被偷了,该怎么办呢?
我们需要的位置已经存在值,这种问题称为,哈希冲突。我们先来对上面进行一下总结再来解决这个问题。

总结:

映射关系
1、直接定址法(直接映射):适合数据范围小,数据量小,没有重复值的数据。不存在哈希冲突。
2、除留余数法:适合数据范围大,数据量可以大。存在哈希冲突。

二、哈希冲突

哈希冲突指的是在使用哈希表进行数据存储和查找时,不同的关键字通过哈希函数计算得到了相同的哈希值(存储位置)。
哈希函数是将关键字映射到哈希表中的某个位置的函数。由于哈希表的存储空间是有限的,而可能的关键字数量是无限的,所以不同的关键字有可能被映射到相同的位置,这就产生了哈希冲突。
哈希冲突会影响哈希表的性能,比如增加查找、插入等操作的时间复杂度。

解决哈希冲突有两种常见的方法:

  • 闭散列:也叫开放寻址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
  • 开散列:也叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

这次我们先介绍闭散列法

三、哈希冲突解决

3.1、开放寻址法

开放定址法是解决哈希冲突的一种方法,其基本思想是当发生冲突时,通过某种系统的方法在哈希表中寻找下一个空槽位,并将冲突的关键码存储在这个槽位中。常用的方法有两种:

1、线性探测:当发生哈希冲突时,从映射位置开始,向后按顺序查找,直到找到下一个空位。
2、二次探测:从映射位置开始,依次增加1、4、9…,探测距离是i^2的倍数,i为从零开始自增的整数。

线性探测例子:
如果我们想将11插入哈希表中,通过哈希函数得到的存储位置已经被占用,我们就从当前位置开始,依次向后查找空位置。
在这里插入图片描述
这种解决哈希冲突的方式,又给我们带来了一个麻烦。
大家思考一下,当我们要查找1这个元素是否存在哈希表中,我们该如何来查找呢?解铃还须系铃人,肯定第一时间就想到利用哈希映射,找到这个值的存储位置,然后比较哈希表中的值,是否等于要查找的值。这个问题是很简单的,那么我们又该如何查找11呢?显然简单的使用哈希函数映射得到到值是无法找到的,这时我们就需要判断它后面是否还有值,如果有,我们就将他与后面存储值继续比较,直到找到,或者遇到值为空的位置。

3.2、删除操作

接着上面的来讲,如果我们要将2删除,是否可以直接将它所在位置,制为空。如果我们这样做了那么上面查找11的操作,该怎么来完成呢?这时我们就需要一个方式将存储位置的状态进行标记,我们将删除位置标记为删除、已有数据位置,标记为存在,未存储数据位置设置为空。这样我们在查找元素时,跳过删除位置,直至空或找到终止程序。

3.3、负载因子

在我们向哈希表中不断映射数据时,发生哈希冲突的概率会随数据量的增大而提高,这会导致我们插入、查找效率大幅度下降,这时我们就需要对哈希表进行扩容操作。那么什么时候扩容呢,总不能等到哈希表存满,哈希冲突最多的时候再进行扩容吧!!!!这时就有了负载因子,来作为我们判断是否扩容的阈值,这个负载因子是使用已插入元素除以哈希表大小。那么当这个负载因子多大时我们对哈希表进行扩容操作呢?这个没有具体要求,但是我们要知道,如果设置太小,会产生空间浪费,设置太大就会发生较多的哈希冲突,所以我们也不能设置的太离谱,在接下来的代码实现中,我将他设置为0.7。

四、代码实现

具体操作及原理,上面已经讲解过了,由于这个结构实现起来还是很简单的,大家在看代码时结合注释及上文。

//使用枚举标识哈希表状态
enum status {
	EMPTY,
	EXIST,
	DELETE
};
template<class k, class v>
struct hashdate {
	pair<k, v>_kv;
	status _s;
};
//使用模板
template<class k>//仿函数将待存入数据统一处理为无符号整型
struct HashFunc {
	size_t operator ()(const k& key)
	{
		return (size_t)key;
	}
};
template<>//模板特化,应字符串存储问题
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t cout = 0;
		for (int i = 0; i < key.size(); i++)
		{
			cout += key[i];
		}
		return cout;
	}
};
//缺省值为哈希函数(仿函数),可跟据需要自己提供
template<class k, class v,class Hash=HashFunc<k>>
class hashTable {
	Hash hash;
public:
	hashTable()
	{
		_table.resize(10);
	}
	bool Insert(const pair<k, v>& kv)
	{
		if (Find(kv))
		{
			return false;
		}
		//负载因子设置为0.1因为隐式这里因为会发生隐式类型转换,特殊处理一下
		if (_n * 10 / _table.size() == 7)
		{
			size_t newsize = _table.size() * 2;
			hashTable<k, v> Newhs;
			Newhs._table.resize(newsize);
			for (int i = 0; i < _table.size(); i++)
			{
				if (_table[i]._s == EXIST)
				{
					Newhs.Insert(_table[i]._kv);
				}
			}
			_table.swap(Newhs._table);
		}
		size_t hashi = hash(kv.first) % _table.size();
		while (_table[hashi]._s == EXIST)
		{
			hashi++;
			hashi %= _table.size();
		}
		_table[hashi]._kv = kv;
		_table[hashi]._s = EXIST;
		_n++;
		return true;
	}
	//查找函数
	hashdate<k,v>* Find(const pair<k, v>& kv) {
		size_t hsi = hash(kv.first) % _table.size();
		while (_table[hsi]._s != EMPTY)
		{
			if (_table[hsi]._s == EXIST && _table[hsi]._kv.first == kv.first)
			{
				return &_table[hsi];
			}
			hsi++;
		}
		return NULL;
	}
	bool earse(const k& key)
	{
		size_t hasi = key % _table.size();
		while (_table[hasi]._s != EMPTY)
		{
			if (_table[hasi]._s == EXIST && _table[hasi]._kv.first == key)
			{
				_table[hasi]._s = DELETE;
				return true;
			}
			hasi++;
		}
		return false;
	}
	void print()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._s == EXIST)
			{
				cout << _table[i]._kv.first << ' ';
			}
		}
	}
private:
	size_t _n = 0;
	vector<hashdate<k, v>> _table;//使用vector充当哈希表
};

总结

线性探测优点:实现非常简单
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?这就需要用到开放定址法了

下篇也完成了,链接放下面了

c++哈希开散列讲解

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

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

相关文章

服务器数据恢复—raid6阵列硬盘被误重组为raid5阵列的数据恢复案例

服务器存储数据恢复环境&#xff1a; 存储中有一组由12块硬盘组建的RAID6阵列&#xff0c;上层linux操作系统EXT3文件系统&#xff0c;该存储划分3个LUN。 服务器存储故障&分析&#xff1a; 存储中RAID6阵列不可用。为了抢救数据&#xff0c;运维人员使用原始RAID中的部分…

Python酷库之旅-第三方库Pandas(250)

目录 一、用法精讲 1181、pandas.tseries.offsets.BusinessMonthEnd.is_on_offset方法 1181-1、语法 1181-2、参数 1181-3、功能 1181-4、返回值 1181-5、说明 1181-6、用法 1181-6-1、数据准备 1181-6-2、代码示例 1181-6-3、结果输出 1182、pandas.tseries.offse…

layui table 纵向滚动条导致单元格表头表体错位问题

我用的时layui2.6.8版本 历史项目维护&#xff0c;bug给我让我做了&#xff0c;本来利用前端手段强解决&#xff0c;后来发现很多table 找了解决办法 打开layui-v2.6.8/lay/modules/table.js 如果打开后时压缩的代码 直接搜索 e.find(".layui-table-patch") …

BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测,含优化前后对比

BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测&#xff0c;含优化前后对比 目录 BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测&#xff0c;含优化前后对比预测效果基本介绍模型描述程序设计…

vue.js学习(day 13)

.sync修饰符 App.vue <template><div class"app"><buttonclick"isShow true">退出按钮</button><!-- :visible.sync > :visible update:visible--><BaseDialog :visible.sync"isShow"></BaseDia…

Android复习简答题

一、基础入门 Android程序架构 &#xff08;1&#xff09;app:用于存放程序的代码和资源等内容。包含很多子目录 libs:存放第三方jar包 src/androidTest&#xff1a;存放调试的代码文件 src/main/androidMainfest.xml 整个程序的配置文件&#xff0c;可配置程序所需要的权…

【娱乐项目】竖式算术器

Demo介绍 一个加减法随机数生成器&#xff0c;它能够生成随机的加减法题目&#xff0c;并且支持用户输入答案。系统会根据用户输入的答案判断是否正确&#xff0c;统计正确和错误的次数&#xff0c;并显示历史记录和错题记录。该工具适合用于数学练习&#xff0c;尤其适合练习基…

架构-微服务-服务配置

文章目录 前言一、配置中心介绍1. 什么是配置中心2. 解决方案 二、Nacos Config入门三、Nacos Config深入1. 配置动态刷新2. 配置共享 四、nacos服务配置的核心概念 前言 服务配置--Nacos Config‌ 微服务架构下关于配置文件的一些问题&#xff1a; 配置文件相对分散。在一个…

基础(函数、枚举)错题汇总

枚举默认从0开始&#xff0c;指定后会按顺序赋值 而这个枚举变量X&#xff0c;如果在全局&#xff08;函数外部&#xff09;定义&#xff0c;那默认为0&#xff0c;如果在函数内部&#xff08;局部变量&#xff09;&#xff0c;那就是随机值&#xff0c;必须初始化。 枚举变量…

flink学习(7)——window

概述 窗口的长度(大小): 决定了要计算最近多长时间的数据 窗口的间隔: 决定了每隔多久计算一次 举例&#xff1a;每隔10min,计算最近24h的热搜词&#xff0c;24小时是长度&#xff0c;每隔10分钟是间隔。 窗口的分类 1、根据window前是否调用keyBy分为键控窗口和非键控窗口…

Spring Boot 的 WebClient 实践教程

什么是 WebClient&#xff1f; 在 Spring Boot 中&#xff0c;WebClient 是 Spring WebFlux 提供的一个非阻塞、响应式的 HTTP 客户端&#xff0c;用于与 RESTful 服务或其他 HTTP 服务交互。相比于传统的 RestTemplate&#xff0c;WebClient 更加现代化&#xff0c;具有异步和…

二叉搜索树讲解

二叉搜索树概念和定义 二叉搜索树是一个二叉树&#xff0c;其中每个节点的值都满足以下条件&#xff1a; 节点的左子树只包含小于当前节点值的节点。节点的右子树只包含大于当前节点值的节点。左右子树也必须是二叉搜索树。 二叉树搜索树性质 从上面的二叉搜索树定义中可以了…

FinalShell工具数据备份升级、密码解密方法

前言 FinalShell 作为国产的服务器管理工具和远程终端软件。一个一体化的运维工具&#xff0c;在国内运维人员中还是比较受欢迎。它整合了多个常用功能&#xff0c;界面友好&#xff0c;使用方便。不过它是一个闭源的商业软件&#xff0c;虽然提供免费版本&#xff0c;但部分高…

241130_昇思MindSpore函数式自动微分

241130_昇思MindSpore函数式自动微分 函数式自动微分是Mindspore学习框架所特有的&#xff0c;更偏向于数学计算的习惯。这里也是和pytorch差距最大的部分&#xff0c;具体体现在训练部分的代码&#xff0c;MindSpore是把各个梯度计算、损失函数计算 在这幅图中&#xff0c;右…

菱形打印(Python)

“以块组合块”&#xff0c;以行凝结循环打印。 (笔记模板由python脚本于2024年11月30日 19:55:22创建&#xff0c;本篇笔记适合正在学习python循环的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”…

【QT入门到晋级】QT项目打生产环境包--(Linux和window)

前言 使用QTcreator完成正常编译后&#xff0c;在构建目录中有可执行程序生成&#xff0c;如果直接把可执行程序拷贝到干净的生产环境上是无法运行成功的&#xff0c;使用ldd&#xff08;查看程序依赖包&#xff09;会发现缺失很多QT的特性包&#xff0c;以及将介绍国产Linux桌…

Super Vlan与Mux Vlan

SuperVlan VLAN Aggregation&#xff0c; 也称 Super-VLAN : 指 在一个物理网络内&#xff0c;用多个 VLAN &#xff08;称为 Sub-VLAN &#xff09;隔离 广播域&#xff0c;并将这些 Sub-VLAN 聚合成一个逻辑的 VLAN &#xff08;称为 Super-VLAN &#xff09;&#xff0c;这…

蓝牙定位的MATLAB程序,四个锚点、三维空间

这段代码通过RSSI信号强度实现了在三维空间中的蓝牙定位&#xff0c;展示了如何使用锚点位置和测量的信号强度来估计未知点的位置。代码涉及信号衰减模型、距离计算和最小二乘法估计等基本概念&#xff0c;并通过三维可视化展示了真实位置与估计位置的关系。 目录 程序描述 运…

Hutool 秒速实现 2FA 两步验证

前言 随着网络安全威胁的日益复杂&#xff0c;传统的用户名和密码认证方式已不足以提供足够的安全保障。为了增强用户账户的安全性&#xff0c;越来越多的应用和服务开始采用多因素认证&#xff08;MFA&#xff09;。基于时间的一次性密码&#xff08;TOTP, Time-based One-Ti…

【继承】—— 我与C++的不解之缘(十九)

前言&#xff1a; 面向对象编程语言的三大特性&#xff1a;封装、继承和多态 本篇博客来学习C中的继承&#xff0c;加油&#xff01; 一、什么是继承&#xff1f; ​ 继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段&#xff0c;它允许我们在保持原有类…