【C++进阶】hash表的封装

文章目录

  • hash表
    • 哈希表的关键组成部分
    • 哈希表的优缺点
      • 优点:
      • 缺点:
    • 常见应用场景
  • 开放定址法实现hash表
    • 负载因子 (Load Factor)
      • 负载因子的意义
      • 负载因子的影响
      • 再散列 (Rehashing)
      • 示例
    • 整体框架
    • insert
    • Find
    • erase
    • hash桶封装
    • 框架
    • insert
    • find
    • erase
    • ~HashTable()
  • 总结

hash表

哈希表是一种数据结构,它通过将键映射到存储桶或槽来快速查找数据。它的核心思想是通过一个哈希函数(Hash Function)将输入数据(键)转换为数组中的索引,以便在常数时间内进行查找、插入和删除操作。

哈希表的关键组成部分

  1. 哈希函数 (Hash Function):将输入的键(key)映射为哈希表的索引。理想的哈希函数应该均匀分布键,避免过多冲突。
  2. 存储桶 (Bucket):每个哈希表的槽位。如果两个不同的键通过哈希函数得到了相同的索引(称为哈希冲突),多个键可以通过链表或其他方式存储在同一个槽中。
  3. 哈希冲突 (Hash Collision):当不同的键映射到同一个存储桶时,发生冲突。常见的解决方法有:
    • 链地址法 (Separate Chaining):在每个槽中存储一个链表,冲突的键会被添加到链表中。
    • 开放地址法 (Open Addressing):通过重新计算索引(如线性探测、二次探测等),将冲突的键存储在下一个可用的槽位中。

哈希表的优缺点

优点:

  • 平均情况下,哈希表的查找、插入和删除操作都能在 O(1) 时间复杂度内完成。

缺点:

  • 当发生大量冲突时,查找和插入的性能可能退化到 O(n)
  • 哈希表的空间利用率不如树结构等其他数据结构高。

常见应用场景

哈希表常用于需要快速查找的场景,如数据库索引、缓存、字典等。

接下来我们将分别用开放定址法和哈希桶的方法来实现hash表

开放定址法实现hash表

首先我们在封装hash表之前要了解什么是负载因子。

负载因子 (Load Factor)

负载因子是指哈希表中已存储元素的数量与哈希表大小(存储桶数量)的比值,公式为:

α = n m \alpha = \frac{n}{m} α=mn

  • n:哈希表中已存储的元素数量。
  • m:哈希表的总存储桶(bucket)数量。

例如,如果哈希表有 100 个存储桶,已存储了 60 个元素,那么负载因子为:

α = 60 100 = 0.6 \alpha = \frac{60}{100} = 0.6 α=10060=0.6

负载因子的意义

  1. 负载因子越小:意味着哈希表中空槽越多,冲突(collisions)的概率越低,查找和插入操作的性能更好。
  2. 负载因子越大:意味着哈希表中存储的元素越来越多,冲突的概率增加,查找和插入操作的效率下降。

负载因子的影响

  • 如果负载因子过高(接近 1 或大于 1),冲突会变得频繁,导致性能下降。这时通常会进行再散列 (rehashing),即扩展哈希表的大小,并重新计算所有元素的哈希值。
  • 在一些常见的哈希表实现中,通常当负载因子超过一定的阈值(如 0.75)时,会触发再散列操作,以保证哈希表的操作性能。

再散列 (Rehashing)

当负载因子达到阈值时,哈希表会增大存储桶的数量(通常是倍增),并重新计算所有已存储元素的哈希值,将它们放入新的存储桶中。这一操作虽然代价较高,但可以避免长期的性能退化。

示例

假设一个哈希表的存储桶数量为 10:

  • 当插入 5 个元素时,负载因子为:

α = 5 10 = 0.5 \alpha = \frac{5}{10} = 0.5 α=105=0.5

  • 当插入 9 个元素时,负载因子为:

α = 9 10 = 0.9 \alpha = \frac{9}{10} = 0.9 α=109=0.9 此时可能需要进行再散列操作来扩展哈希表的大小。

整体框架

	//三种状态
	enum State
	{
		EXIST,//存在
		EMPTY,//空
		DELETE//删除
	};
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		//初始状态是空
		State _state = EMPTY;
	};
	template<class K, class V>
	class HashTable
	{
	public:
	private:
		//用一个Hash表来存储
		vector<HashData<K, V>> _tables;
		//表中插入的数据个数
		size_t _n = 0;
	};

在开放定址法中,我们需要用一个状态来表示hash表中每个位置的状态,由于每个位置的状态不止两种,所以我们不能使用布尔值来表示每个位置的状态,应该使用enum来定义多种状态,我们定义三种状态(EMPTY,EXIST,DELETE)分别表示空,存在,删除。
在HashTables中_n表示hash表中插入了多少个数。
接下来我们来封装insert

insert

bool Insert(const pair<K, V>& kv)
{
	//去冗余
	if (Find(kv.first))
	{
		return false;
	}
	//扩容,通过负载因子来进行扩容
	if (_n * 10 / _tables.size() == 7)//负载因子为0.7的时候进行扩容
	{
		//不能直接进行resize,因为扩容之后空间变化了,映射也要跟着变化
		//创建一个新表是以前表的二倍
		HashTable<K, V>  newHT;
		newHT._tables.resize(_tables.size() * 2);
		for (size_t i = 0;i < _tables.size();i++)
		{
			if (_tables[i]._state == EXIST)
			{
				//直接复用insert
				//这里不是自己调用自己,因为这里是两个对象
				newHT.Insert(_tables[i]._kv);
			}
		}
		_tables.swap(newHT._tables);
	}
	Hash hs;
	//算起始位置
	size_t hashi = hs(kv.first) % _tables.size();
	//状态存在则继续
	while (_tables[hashi]._state == EXIST)
	{
		hashi++;
		//防止hashi越界
		hashi %= _tables.size();
	}
	//添加值并且状态改为存在
	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	_n++;
	return true;
}

在这里插入图片描述
要进行插入我们首先需要找到插入位置,假如当我们找到位置的时候,当前位置已经被占据了,所以我们只能向后移动。
在这里插入图片描述
插入的及基本逻辑经济就是如此,既然有插入那肯定有插入位置满的时候,所以插入逻辑中应该涉及到扩容,但是我们不能直接进行扩容,因为当我们扩容的时候,每个data的映射的位置也会随之而变化,这就涉及到我们应该找到新的映射位置,但是对于开放定址法来说,我们不是在插入位置满的时候扩容,而是在负载因子到达0.7的时候进行扩容。
在这里插入图片描述
在扩容的时候,我们可以直接创建一个新表,然后在新表中重新映射数据,这里我们可以直接复用insert。(注意:这里调用的不是同一个insert,因为是两个不同的对象)

Find

HashData<K, V>* Find(const K& key)
{
	Hash hs;
	//算起始位置
	size_t hashi = hs(key) % _tables.size();
	//状态存在则继续
	while (_tables[hashi]._state != EMPTY)
	{
		//这个状态不是DELETE才能进行查找
		if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];
		}
		hashi++;
		//防止hashi越界
		hashi %= _tables.size();
	}
	return nullptr;
}

对于查找来说我们只需要找到当前需要查找的数据的所在的位置,然后从查找的位置对这个表进行遍历,如果遇到和当前值相等的则直接返回当前位置的地址,需要注意的是:我们还要考虑状态,当状态是DELETE的时候,但是当前值又等于查找的值,这个值是不能返回的,因为当前值已经删除了,所以这种情况需要排除,所以前面需要添加一个判断条件。

erase

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

对于删除某个值,我们首先需要查找,所以这里我们可以复用查找,用HashData<K,V>接收一下,如果当前指针是空指针就直接返回false,说明需要删除的值没在表中,如果当前指针不是空指针,则将状态设置为DELETE状态。

hash桶封装

框架

template<class K,class V>
struct HashNode
{
	pair<K,V> _kv;
	HashNode<T>* _next;
	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}
};
template<class K, class V>
class HashTable
{
public:
	typedef HashNode<K,V> Node;
private:
	//hash桶
	//hash桶当中的Node节点是new出来的,需要写析构函数
	vector<Node*> _tables;
	//表中存储的数据个数
	size_t _n = 0;
};

用hash桶来封装hash表我们需要一个结构体,结构体中包含指向下一个结构体的指针,还有一个存放数据的kv。
在HashTable中我们需要一个vector,这个vector的类型是Node*,相当于存放的是指针。
在这里插入图片描述
上述代码描述的结构就是上面这种结构。

insert

bool Insert(const pair<K,V>& kv)
{
	if (Find()) {
		return false;
	}
	//找到插入的位置

	size_t hashi = kv.first % _tables.size();
	//需要控制负载因子,hash桶的负载因子需要控制在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 = cur->_kv.first % newtables.size();
				cur->_next = newtables[hashi];//先指向第一个
				newtables[hashi] = cur;//然后再成为第一个
				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newtables);
	}
	//头插,尾插也可以,但是尾插需要找到尾,所以这里我们选择头插不用找尾
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	_n++;
	return true;
}

对于插入来说,我们可以直接算出插入位置,然后在当前桶中进行头插,为什么进行头插而不进行尾插呢?
因为头插可以直接插,而尾插则需要找到尾之后才能插入,大大影响了我们的插入效率,所以我们进行头插。
插入的逻辑说完之后,,我们也要扩容,对于hash桶实现hash表来说,我们的扩容的前提是当每个_n== 1的时候,意思就是每个桶都有一个节点的时候,_n==1的时候最好的情况是每个桶有一个节点,,这个情况的前提就是保证每个桶都有节点。

如果我们直接扩容的话也不是不行,但是会很浪费我们的空间,所以我们可以不释放当前节点,直接把旧表的节点插入到新表映射的位置上去,就不用浪费空间了。

find

Node* Find(const K& key)
{
	size_t hashi = key % _tables.size();
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_data == key)
		{
			return cur
		}
		cur = cur->_next;
	}
	return nullptr;
}

对于查找来说直接找到映射位置遍历hash桶即可。

erase

bool Erase(const K& key)
{
	size_t hashi = key % _tables.size();
	Node* cur = _tables[hashi];
	Node* prev = nullptr;
	while (cur)
	{
		if (cur->_data == key)
		{
			if (prev == nullptr)
			{
				_tables[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;
			_n--;
			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

对于删除的逻辑我们先找到映射位置,然后先定义一个prev标记前一个指针,然后遍历当前桶,如果当前位置的值和需要删除的值相同,则可以分出两种情况,第一种情况是头删,头删就说明prev是nullptr,则可以直接令头指针等于下一个节点,如果是删除中间的,则可以令prev指向cur的下一个节点,然后释放当前节点,注意:释放当前节点的时候需要_n—。

~HashTable()

因为每个节点都是我们new出来的,所以需要我们手动释放,所以写一个析构函数,释放每个节点,遍历每个桶逐个释放

~HashTable()
{
	for (size_t i = 0;i < _tables.size();i++)
	{
		Node* cur = _tables[i];
		while(cur)
		{
			Node* next = cur->_next;
			delete cur;
			cur = next;
		}
		_tables[i] = nullptr;
	}
}

总结

通过对哈希表的封装,我们不仅提升了代码的可读性和复用性,还通过合理设计哈希函数和处理冲突机制,优化了哈希表的性能。封装让我们可以将底层的细节隐藏起来,提供一个简洁高效的接口,便于后续在项目中的使用。在实际开发中,理解负载因子、再散列等概念,并针对具体场景合理调整这些参数,能够确保哈希表在性能和内存占用上的平衡。掌握了这些知识后,相信你能够更加自信地在复杂应用中使用哈希表。

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

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

相关文章

Apache ShardingSphere数据分片弹性伸缩加解密中间件

Apache ShardingSphere Apache ShardingSphere 是一款分布式 SQL 事务和查询引擎,可通过数据分片、弹性伸缩、加密等能力对任意数据库进行增强。 软件背景 ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding…

git 提交自动带上storyid

公司里的运维团队的产品经理&#xff0c;那老六提出说要在每个提交带上的jira storyid或者bugid&#xff0c;不用他自己弄不顾他人麻烦&#xff0c;真想问候他的xx。不过既然已经成为定局&#xff0c;还是想想有没有其他办法。经一番调研&#xff0c;网上有比较零碎的信息&…

Nginx 负载均衡+高可用 集群部署(Keepalived+LVS DR模式)

一、LVS负载均衡简介 1.1 LVS基本介绍 LVS&#xff08;Linux Virtual Server&#xff09;即Linux虚拟服务器&#xff0c;是由章文嵩博士主导开发的开源负载均衡项目&#xff0c;目前LVS已经被集成在Linux内核中。该项目在Linux内核中实现了基于IP地址的请求数据负载均衡调度方…

以太网交换机工作原理学习笔记

在网络中传输数据时需要遵循一些标准&#xff0c;以太网协议定义了数据帧在以太网上的传输标准&#xff0c;了解以太网协议是充分理解数据链路层通信的基础。以太网交换机是实现数据链路层通信的主要设备&#xff0c;了解以太网交换机的工作原理也是十分必要的。 1、以太网协议…

CTFHub技能树-Git泄漏-Stash

目录 一、前提知识 1.什么是git stash 2.git文件目录结构 3.git中对象指向 二、解题过程 方法一&#xff1a;使用GitHack 方法二&#xff1a;使用Git_Extract工具&#xff0c;这个是自动解析不用git stash等操作&#xff0c;直接得到flag 当前大量开发人员使用git进行版本…

SQL进阶技巧:截止当前批次前的批次量与订单量 | 移动窗口问题

目录 0 场景描述 1 数据准备 2 问题分析 3 小结 0 场景描述 表A有如下字段,user id(用户ID),batch id(批次ID),order id(订单ID),create time(创建时间),同一个用户ID下有多个批次,同一个批次下有多个订单ID,相同批次ID的创建时间是相同的,创建时间精确到了秒。 统计,截…

1-10 图像增强对比度 opencv树莓派4B 入门系列笔记

目录 一、提前准备 二、代码详解 enhanced_image cv2.convertScaleAbs(image, alpha1.5, beta0) 三、运行现象 四、完整工程贴出 一、提前准备 1、树莓派4B 及 64位系统 2、提前安装opencv库 以及 numpy库 3、保存一张图片 二、代码详解 import cv2 # 增强图像的对比度 …

【音视频】播放音视频时发生了什么? 视频的编解码 H264是什么? MP4是什么?

目录 ✨播放一个视频的流程✨为什么要编码&#xff08;压缩&#xff09;视频数据&#xff1f;✨如何编码&#xff08;压缩&#xff09;数据&#x1f384;简单的例子&#x1f384;音视频编码方式&#x1f384;视频编码格式H264编码是什么&#xff1f;发展历程&#xff1f;H.264基…

【Python游戏开发】拼图小游戏demo

使用mu编辑器 pgzero编写拼图小游戏 import randomSIZE 96 # 设置每张图块的大小 WIDTH SIZE * 3 # 根据土块大小设置窗口 HEIGHT SIZE * 3 pics [] # 存放图块 finished False # 游戏结束标识# 将前八张图块存放在pics列表中 for i in range…

009.Python爬虫系列_urllib模块案例

我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈 入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈 虚 拟 环 境 搭 建 :👉👉 Python项目虚拟环境(超详细讲解) 👈👈 PyQt5 系 列 教 程:👉👉 Python GUI(PyQt5)文章合集 👈👈 Oracle数…

传统CV算法——基于Opencv的多目标追踪算法

基于 OpenCV 的跟踪算法有多种&#xff0c;每种算法都有其特定的应用场景和优缺点。以下是一些常见的基于 OpenCV 的目标跟踪算法&#xff1a; 1. BOOSTING 跟踪器 描述&#xff1a;基于 AdaBoost 算法的跟踪器。它是一种早期的跟踪算法&#xff0c;使用的是基于弱分类器的强…

php转职golang第二期

以下是一份简单的 Go 基本语法笔记&#xff1a; 变量与常量&#xff1a; • var 声明变量。• const 声明常量。数据类型&#xff1a; • 整型、浮点型、布尔型、字符串型等。流程控制&#xff1a; • if-else 语句。• for 循环。函数&#xff1a; • 定义和调用函数。数…

Linux-【组管理、权限管理、定时任务调度】

目录 前言 Linux组基本介绍 文件/目录 所有者 查看文件 所有者 修改文件所有者 文件/目录 所在组 修改文件/目录 所在组 其它组 改变用户所在组 权限的基本介绍 rwx权限 rwx作用到文件 rwx作用到目录 修改权限 第一种方式&#xff1a;、-、变更权限 第二种方式…

系统编程--线程

这里写目录标题 线程概念什么是线程简介图解 内核原理图解 线程共享资源与非共享资源共享资源非共享资源 线程优缺点 线程控制原语pthread_self、pthread_create简介代码总结 循环创建多个子线程错误代码 线程间全局变量共享pthread_exit简介代码 一级目录二级目录二级目录二级…

可持久化Trie详解,最大异或和,k大异或和

零、碎碎念 打比赛没遇上可持久化Trie&#xff0c;做个CMU 15-445的project0&#xff0c;上来就碰上了…… 关于Trie详见&#xff1a;[Trie树/字典树的原理及实现C/C]_trie字典树原理-CSDN博客 一、可持久化Trie 1.1 基本思想 可持久化Trie和可持久化线段树类似&#xff0c…

白小白为波司登新品创作歌曲《登峰之路》,穿越风雨守护前行者

随着天气渐凉&#xff0c;波司登品牌推出全新新品——轻薄羽绒叠变系列&#xff0c;作为波司登品牌的新品推荐官&#xff0c;歌手白小白为波司登创作并演唱《轻薄羽绒叠变》系列主题曲《登峰之路》。歌曲中&#xff0c;白小白以激昂澎湃&#xff0c;明快有力的旋律以及深情又充…

[数据集][目标检测]西红柿缺陷检测数据集VOC+YOLO格式17318张3类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;17318 标注数量(xml文件个数)&#xff1a;17318 标注数量(txt文件个数)&#xff1a;17318 标…

【第0006页 · 数组】寻找重复数

【前言】本文以及之后的一些题解都会陆续整理到目录中&#xff0c;若想了解全部题解整理&#xff0c;请看这里&#xff1a; 第0006页 寻找重复数 今天想讨论的一道题在 LeetCode 上评论也是颇为“不错”。有一说一&#xff0c;是道好题&#xff0c;不过我们还是得先理解了它才…

【Unity小技巧】URP管线遮挡高亮效果

前言 在URP渲染管线环境下实现物体遮挡高亮显示效果&#xff0c;效果如下&#xff1a;Unity URP遮挡高亮 实现步骤 创建层级&#xff0c;为需要显示高亮效果的物体添加层级&#xff0c;比如Player 创建一个材质球&#xff0c;也就是高亮效果显示的材质球找到Universal Render…

react项目搭建、基础知识

前言 教学内容来源于黑马 黑马程序员前端React18入门到实战视频教程&#xff0c;从reacthooks核心基础到企业级项目开发实战 项目搭建 创建项目 pnpm create vite选择框架 选择语言和构建 安装依赖并运行 pnpm install pnpm run dev运行成功 基础知识 文件 main…