哈希表之闭散列的实现

news2024/11/19 15:35:15

在这里插入图片描述

闭散列实现哈希表

在闭散列实现哈希表中,我们选择线性探测法来解决哈希冲突。在哈希表的简介部分,我们已经介绍过线性探测法啦!

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

基本结构定义

在哈希表的简介部分,我们知道闭散列中删除一个元素是通过标记的方式删除的,因此在实现闭散列中我们需要定义一个变量用来标记哈希表中每一个位置的状态。这种情况可以使用枚举(enum)来表示哈希表中每个位置的状态。

enum STATE
{
	EXIST, //表示这个位置已经存放数据了
	EMPTY, //表示这个位置没有存放数据
	DELETE //表示这个位置的数据已经被删除
};

哈希表是 unordered_mapunordered_set 的底层数据结,HashTable 存储的数据依旧是 key-value 的形式,也就是 pair 啦!类比 mapset 就行啦!

template<class K, class V>
struct HashData
{
	pair<K, V> _kv; //存储数据
	STATE _state; //哈希表每一个位置的状态
};

哈希表就是一个数组,只不过具有特定的插入,删除数据的方式。比可以类比priority_queue(堆),他的底层也是一个数组,但是也具有插入删除数据的规则。

template<class K, class V>
class HashTable
{
public:

private:
	vector<HashData<K, V>> _table;
	size_t _n = 0; //哈希表中有效元素的个数
};

这里为什么需要一个 _n 来存储哈希表中有效元素的个数呢?在哈希表的简介部分我们已经知道,闭散列解决哈希冲突的方式可能会发生数据堆叠,降低哈希表的效率。因此,我们需要维护 _n 来管理哈希表的扩容,以此来提高效率。具体的做法会在哈希表的 Insert 函数中讲解。

构造函数

我们可以一开始就给哈希表开一段空间,原因后面再讲。

HashTable()
{
    _table.resize(10);
}

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

在哈希表的简介部分我们了解到很多哈希函数,我们选择比较常见的除留余数法作为实现哈希表的哈希函数。当我们要插入一个新的元素时,我们就拿他的 key 对数组的 size 取模,这样就能得到新元素对应的下标位置,如果这个下标对应的标识符不是 EMPTY 就找下一个位置,直到找到一个标识符为 DELETE 或者 EMPTY 的位置,然后插入元素就可以啦。

不可以对数组的 capacity 取模哦!

在这里插入图片描述

为了方便,哈希表中的数组一般都是 sizecapacity 一样大的,即使不一样大,对 size 取模都不会错。

为了确保 sizecapacity 一样大,在 HashTable 的构造函数调用 vectorresize 函数就可以!

bool Insert(const pair<K, V>& kv)
{
    size_t hashi = kv.first % _table.size();
    //找到一个空位置
    while (_table[hashi]._state != EXIST)
    {
        ++hashi;
        hashi %= _table.size();
    }

    //插入数据
    _table[hashi]._kv = kv;
    _table[hashi]._state = EXIST;
    ++_n;

    return true;
}

于是我们顺利写出了这样的代码,显然这是不够的,因为我们并没有考虑哈希表扩容的问题。那扩容逻辑应该怎么写呢?我们先不着急,来看看哈希表的载荷因子是个什么东东。

哈希表的载荷因子定义为: α \alpha α = 填入表中的元素个数 / 哈希表的长度

α \alpha α 是散列表装满程度的标志因子。由于表长是定值, α \alpha α 与 “填入表中的元素个数” 成正比,所以, α \alpha α 越大,表明填入表中的元素越多,产生冲突的可能性越大,但是空间利用率越大;反之, α \alpha α 越小,表明填入表中的元素越少,产生冲突的可能性就越小,但是空间利用率就变低了。实际上,哈希表的平均查找长度是载荷因子 α \alpha α 的函数,只是不同处理冲突的方法有不同的函数。

对于开放定址法,载荷因子是特别重要的因素,应严格限制在 0.7 - 0.8 以下。超多 0.8 查询时 CPU 缓存不明中按照指数曲线上升。因此,一些采用开放定址法的 hash 库,如 Java 的系统库限制了载荷因子为 0.75,超过这个值就要对哈希表进行扩容处理。

我们实现,选择载荷因子大于等于 0.7 之后扩容哈!假设我们一开始没有给哈希表初始化空间,在扩容判断的时候就比较麻烦,因为一开始哈希表的 size 为 0,就需要特殊判断了! α = n / t a b l e . s i z e ( ) \alpha = n / table.size() α=n/table.size(),n 为哈希表中有效元素的个数,显然 table.size() 作为除数是不能为 0 的,因此需要特殊判断。

在扩容之后,因为 HashTable 的大小发生改变了,原来哈希表中的元素都需要重新映射。

不可以直接 resize(newSize) 就完事儿了,必须重新映射!

在计算载荷因子时有如下计算方法:

1 : n ∗ 10 / t a b l e . s i z e ( ) ≥ 7 1:n * 10 / table.size() \geq 7 1n10/table.size()7

2 : ( d o u b l e ) n / t a b l e . s i z e ( ) ≥ 0.7 2:(double)n / table.size() \geq 0.7 2(double)n/table.size()0.7

看你喜欢哪一种了!

bool Insert(const pair<K, V>& kv)
{
	if (_n * 10 / _table.size() >= 7)
	{
		size_t newSize = _table.size() * 2;
		//遍历旧表,重新映射到新表
		HashTable<K, V> newHT;
		newHT._table.resize(newSize);

		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._state == EXIST)
			{
				newHT.Insert(_table[i]._kv);
			}
		}

		_table.swap(newHT._table);
	}

	size_t hashi = kv.first % _table.size();
	//找到一个空位置
	while (_table[hashi]._state != EXIST)
	{
		++hashi;
		hashi %= _table.size();
	}

	//插入数据
	_table[hashi]._kv = kv;
	_table[hashi]._state = EXIST;
	++_n;

	return true;
}

HashData<const K, V>* Find(const K& key)

查找比较简单,根据 key 算出下标,然后向后查找,如果找到和 key 值相等的元素并且该元素的标志位不是 DELETE,返回即可;如果直到找到标志位为空的元素还没找到,那么返回 nullptr 就可以。


HashData<const K, V>* Find(const K& key)
{
	size_t hashi = key % _table.size();
	while (_table[hashi]._state != EMPTY)
	{
		if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
		{
			return (HashData<const K, V>*)&_table[hashi];
		}
		++hashi; //继续到下一个位置寻找
		hashi %= _table.size();
	}
	return nullptr; //哈希表中找不到这个元素
}

bool Erase(const K& key)

这个函数就更简单了,如果说要删除的元素在哈希表中存在的话,将对应的标志位改为 DELETE 就行啦!

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

解决 string 作为 key 的情况

像那些整形,指针啥的都可以直接取模,然后计算他在哈希表中的位置。但是对于字符串呢,他是无法取模的。因此,怎么解决这个问题呢?又得靠我们的仿函数了,当 key 值为 string 类型的时候,通过模板的特化,调用处理 string 的那个 operator() 就可以啦!

template<class K>
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

这是一个对于可以取模的 key 准备的 operator() 我们啥也不需要做,返回传入的 key 就行啦。返回值强转为 size_t 能够解决负数作为 key 值的问题。

模板特化的知识点已经讲过了,忘记了可以复习复习:21天学会C++:Day14----模板_姬如祎的博客-CSDN博客

这里还有一个问题就是,如何让字符串能够对整数取模呢?这里就涉及到字符串的哈希了!字符串的哈希算法有很多,你可以在网上搜索。我们这里选择一个比较常见的字符串哈希算法:BKDR Hash算法。

在这里插入图片描述

模板特化:

template<>
struct DefaultHashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t hash = 0;
		for (auto e : str)
		{
			hash *= 131;
			hash += e;
		}
		return hash;
	}
};

最终代码:

template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
	HashTable()
	{
		_table.resize(10);
	}

	bool Insert(const pair<K, V>& kv)
	{
		if (_n * 10 / _table.size() >= 7)
		{
			size_t newSize = _table.size() * 2;
			//遍历旧表,重新映射到新表
			HashTable<K, V> newHT;
			newHT._table.resize(newSize);

			for (int i = 0; i < _table.size(); i++)
			{
				if (_table[i]._state == EXIST)
				{
					newHT.Insert(_table[i]._kv);
				}
			}

			_table.swap(newHT._table);
		}
		HashFunc hf;

		size_t hashi = hf(kv.first) % _table.size();
		//找到一个空位置
		while (_table[hashi]._state != EXIST)
		{
			++hashi;
			hashi %= _table.size();
		}

		//插入数据
		_table[hashi]._kv = kv;
		_table[hashi]._state = EXIST;
		++_n;

		return true;
	}

	HashData<const K, V>* Find(const K& key)
	{
		HashFunc hf;
		size_t hashi = hf(key) % _table.size();
		while (_table[hashi]._state != EMPTY)
		{
			if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
			{
				return (HashData<const K, V>*)&_table[hashi];
			}
			++hashi; //继续到下一个位置寻找
			hashi %= _table.size();
		}
		return nullptr; //哈希表中找不到这个元素
	}

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


private:
	vector<HashData<K, V>> _table;
	size_t _n = 0; //哈希表中有效元素的个数
};

好啦,这就是开散列实现哈希表的全部过程啦!下一讲我们会探索闭散列实现哈希表的方式。

在这里插入图片描述

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

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

相关文章

【Springboot】基于注解式开发Springboot-Vue3整合Mybatis-plus实现分页查询(二)——前端el-pagination实现

系列文章 【Springboot】基于注解式开发Springboot-Vue3整合Mybatis-plus实现分页查询—后端实现 文章目录 系列文章系统版本实现功能实现思路后端传入的数据格式前端el-table封装axois接口引入Element-plus的el-pagination分页组件Axois 获取后台数据 系统版本 后端&#xf…

Day58_《MySQL索引与性能优化》

文章目录 一、SQL执行顺序二、索引简介1、关于索引2、索引的类型Btree 索引Btree 索引 三、Explain简介四、Explain 详解1、id2、select_type3、table4、type5、possible_keys6、key7、key_len8、ref9、rows10、Extra11、小案例 五、索引优化1、单表索引优化2、两表索引优化3、…

Spring Boot 整合xxl-job实现分布式定时任务

xxl-job介绍 XXL-JOB是一个分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c;开箱即用。 xxl是xxl-job的开发者大众点评的许雪里名称的拼音开头。 设计思想 将调度行为抽象形成“调度…

multilinear多项式承诺方案benchmark对比

1. 引言 前序博客有&#xff1a; Lasso、Jolt 以及 Lookup Singularity——Part 1Lasso、Jolt 以及 Lookup Singularity——Part 2深入了解LassoJolt Lasso lookup中&#xff0c;multilinear多项式承诺方案的高效性至关重要。 本文重点关注4种multilinear多项式承诺方案的实…

linux 不同用户不同jdk

0、 解压一个新版本的jdk 1、 检查root用户下的环境变量&#xff0c;是否配置了JAVA_HOME&#xff0c;基于这个变量再配置的PATH变量是实现切换的前提。 2、 创建新用户 adduser jdk11 passwd jfjfjfjfjfjfj123 3、 编辑改用下的 .bashrc 文件 执行命令进行编辑&#xff0…

【Nginx】深入浅出搞懂Nginx

Nginx是一款轻量级的Web服务器、反向代理服务器&#xff0c;由于它的内存占用少&#xff0c;启动极快&#xff0c;高并发能力强&#xff0c;在互联网项目中广泛应用。 反向代理服务器&#xff1f; 经常听人说到一些术语&#xff0c;如反向代理&#xff0c;那么什么是反向代理&a…

BGP基本配置实验

目录 一、实验拓扑 二、实验需求 三、实验步骤 1、IP地址配置 2、内部OSPF互通&#xff0c;配置OSPF协议 3、BGP建立邻居关系 4、R1和R5上把业务网段宣告进BGP 5、消除路由黑洞&#xff0c;在R2、R4上做路由引入 6、业务网段互通 一、实验拓扑 二、实验需求 1、按照图…

开发者测试2023省赛--UnrolledLinkedList测试用例

测试结果 官方提交结果 EclEmma PITest 被测文件UnrolledLinkedList.java /** This source code is placed in the public domain. This means you can use it* without any restrictions.*/package net.mooctest;import java.util.AbstractList; import java.util.Collectio…

担忧CentOS停服?KeyarchOS系统来支撑

担忧CentOS停服&#xff1f;KeyarchOS系统来支撑 近年发生的“微软黑屏门”、“微软操作系统停更”、“棱镜门”、“中兴华为”等安全事件&#xff0c;敲响了我国 IT 产业的警钟&#xff0c;建立由我国主导的 IT 产业生态尤为迫切。对此&#xff0c;我国信息技术应用创新行业乘…

数字媒体技术基础之:分辨率

分辨率 Resolution&#xff0c;中国大陆译为“分辨率”&#xff0c;中国香港地区、中国台湾地区分别译为“解像度”和“解析度”&#xff0c;泛指测量设备对细节的分辨能力。 ◆ ◆ ◆ 图像尺寸 在数字图像处理中&#xff0c;像素 Pixel是一个无具体物理尺寸的抽象单位。 一张…

序列化模块-json和pickle

一、json json是所有语言都通用的一种序列化格式 &#xff0c;只支持 列表、 字典、 字符串、 数字 &#xff0c; 字典的key必须是字符串 1、dumps、loods # 在内存中做数据转换 : # durps 数据类型 转成 字符串 序列化 # loods 字符串 转成 数据类型 反序…

基于STC12C5A60S2系列1T 8051单片机定时器/计数器应用

基于STC12C5A60S2系列1T 8051单片机定时器/计数器应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍STC12C5A60S2系列1T 8051单片机定时器/计数器介绍STC12C5A60S2系…

No179.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

C++算法:完美矩形

题目 给你一个数组 rectangles &#xff0c;其中 rectangles[i] [xi, yi, ai, bi] 表示一个坐标轴平行的矩形。这个矩形的左下顶点是 (xi, yi) &#xff0c;右上顶点是 (ai, bi) 。 如果所有矩形一起精确覆盖了某个矩形区域&#xff0c;则返回 true &#xff1b;否则&#xf…

C语言基础篇3:函数

1 函数简介 C源程序是由函数组成的&#xff0c;一个程序往往由多个函数组成&#xff0c;函数是程序实现模块化变成的基本的单元&#xff0c;一般是为了完成某一个特定的功能&#xff0c;相当于其他语言中的子程序。一个较大程序的各项功能都是由各个子程序共同完成的&#xff0…

【Seata源码学习 】 AT模式 第一阶段 @GlobalTransaction的扫描

1. SeataAutoConfiguration 自动配置类的加载 基于SpringBoot的starter机制&#xff0c;在应用上下文启动时&#xff0c;会加载SeataAutoConfiguration自动配置类 # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfigurationio.seata.spring.boot.aut…

Zabbix SNMPv3

一、Snmpv3简述 SNMPv3是Simple Network Management Protocol version 3&#xff08;简单网络管理协议第三版&#xff09;的缩写。它是一种网络管理协议&#xff0c;用于监控和管理网络中的设备、系统和应用程序。 相对于之前的版本&#xff0c;SNMPv3具有更强的安全性和扩展…

NGINX三种虚拟主机的配置

基于IP的配置 首先在原本基础上增加两个IP地址 [rootlocalhost conf.d]# nmcli connection modify ens33 ipv4.addresses 192.168.38.140 [rootlocalhost conf.d]# nmcli connection modify ens33 ipv4.addresses 192.168.38.150 [rootlocalhost conf.d]# nmcli connection u…

SharePoint 是什么

SharePoint 平台使您能够以在线方式和本地方式轻松地管理和协调业务数据。因为其灵活性和易使用性&#xff0c;公司可以快速采用SharePoint来管理其业务数据。 SharePoint Microsoft 365 一种基于云的服务&#xff0c;由 Microsoft 托管&#xff0c;适用于各种规模的企业。 任何…

vue2 数字软键盘 封装 可拖动 使用简单

1、效果图 2、使用方式 <Keyboard v-if"show" close"show false" :inputDom"$refs.input" /> 封装的数字键盘 Keyboard.vue 组件代码 <template><divclass"keyboard"ref"keyboard":style"{ left: …