哈希及哈希表的实现

news2024/12/22 22:22:29

目录

一、哈希的引入 

二、概念

三、哈希冲突

四、哈希函数

常见的哈希函数

1、直接定址法

2、除留余数法

五、哈希冲突的解决

1、闭散列

2、开散列


一、哈希的引入 

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

这种思想就是我们接下来要讲的哈希了。


二、概念

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

当有这些数据时:1,4,5,6,7,9。我们可以通过下图的方式去插入数据。

 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。


三、哈希冲突

按照上面的方式,如果我们插入的是 1,2,32,222,7,9这几个数呢?

我们发现,如果按照哈希的思想去插入的话,2,32,22将会被放在同一个位置,这样就会引起一些麻烦。如果我去访问下标为2位置的数据,到底访问的哪一个呢?我们将这种现象称为哈希冲突。

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。


四、哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

常见的哈希函数

1、直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。
优点:简单、均匀。
缺点:需要事先知道关键字的分布情况。
使用场景:适合查找比较小且连续的情况。

2、除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p (p<=m),将关键码转换成哈希地址。


五、哈希冲突的解决

1、闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

那么这个下一个位置怎么确定呢?我们有两种方法来帮助我们寻找“下一个”位置。

~ 线性探测 

比如三中的情况,插入2之后,现在需要插入元素32,先通过哈希函数计算哈希地址为2,因此32理论上应该插在该位置,但是该位置已经放了值为2的元素,即发生哈希冲突。这时我们就需要去寻找该位置后面的空位置了。

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

如下图,32只能插入在3的位置了。 

 注:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素2,如果直接删除掉,32查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

在有限的空间内,随着我们插入的数据越来越多,冲突的概率也越来越大,查找效率越来越低,所以闭散列的冲突表不可能让它满了,所以我们就引入了负载因子:

负载因子(载荷因子):等于表中的有效数据个数/表的大小,衡量表的满程度,在闭散列中负载因子不可能超过1(1代表满了)。一般情况下,负载因子一般在0.7左右。负载因子越小,冲突概率也越小,但是消耗的空间越大,负载因子越大,冲突概率越大,空间的利用率越高。

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 val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}
		return val;
	}
};

namespace closehash
{
enum State
{
	EMPTY,
	DELETE,
	EXIST
};

template<class K,class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	bool insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
		{
			return false;
		}

		if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)
		{
			size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			HashTable<K, V, Hash> newHT;
			newHT._tables.resize(newsize);
			for (auto& e : _tables)
			{
				if (e._state == EXIST)
				{
					newHT.insert(e._kv);
				}
			}
			_tables.swap(newHT._tables);
		}

		Hash hash;
		size_t index = hash(kv.first) % _tables.size();
		//如果kv.first是string类型,那么就无法取模,因此我们要使用仿函数将其转换成整型
				//线性探测
				while (_tables[index]._state == EXIST)
				{
					index++;
					index %= _tables.size();
				}
				_tables[index]._kv = kv;
				_tables[index]._state = EXIST;
				_size++;

				return true;
			}

			HashData<K, V>* Find(const K& key)
			{
				if (_tables.size() == 0)
				{
					return nullptr;
				}
				Hash hash;
				size_t hashi = hash(key) % _tables.size();
				size_t start = hashi;
				while (_tables[hashi]._state != EMPTY)
				{
					if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
					{
						return &_tables[hashi];
					}
					hashi++;
					hashi %= _tables.size();

					if (hashi == start)
					{
						break;
					}
				}
				return nullptr;
			}

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

		private:
			vector<HashData<K, V>> _tables;
			size_t _size = 0; //存储了多少个有效数据
		};

2、开散列

概念:开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

 

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

那么是不是我们只需要开固定的空间,然后其他的数据就一直连接到对应的桶的后面,那样桶是不是太长了呢?

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容。

增容条件:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

template<class K,class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;

		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{}
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					free(cur);
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		bool insert(const pair<K, V>& kv)
		{
			//去重
			if (Find(kv.first))
			{
				return false;
			}

			Hash hash;
			//扩容
			if (_size == _tables.size())
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 10;
				vector<Node*> newTables;
				newTables.resize(newsize, nullptr);
				//将旧表中结点移动映射到新表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hash(cur->_kv.first) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}

			size_t hashi = hash(kv.first) % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			_size++;

			return true;
		}

		Node* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
					return cur;
				else
					cur = cur->_next;
			}
			return nullptr;
		}

		bool erase(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					// 1、头删
					// 2、中间删
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					_size--;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		vector<Node*> _tables;
		size_t _size = 0;
	};

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

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

相关文章

【Java 基础篇】Java标准输入流详解:读取用户输入的完整指南

Java是一门流行的编程语言&#xff0c;常用于开发各种类型的应用程序&#xff0c;包括控制台应用、桌面应用、Web应用等。在这些应用中&#xff0c;与用户进行交互是一项重要的任务。本文将重点介绍Java标准输入流&#xff0c;它是Java程序中用于从用户获取输入的关键组成部分。…

【unity小技巧】Unity 存储存档保存——PlayerPrefs、JsonUtility和MySQL数据库的使用

文章目录 前言PlayerPrefs一、基本介绍二、Demo三、优缺点 JsonUtility一、基本使用二、Demo三、优缺点 Mysql&#xff08;扩展&#xff09;完结 前言 游戏存档不言而喻&#xff0c;是游戏设计中的重要元素&#xff0c;可以提高游戏的可玩性&#xff0c;为玩家提供更多的自由和…

【JavaScript】video标签配置及相关事件:

文章目录 一、标签配置&#xff1a;二、事件&#xff1a;三、案例&#xff1a; 一、标签配置&#xff1a; 标签名描述src要播放的路径地址autoplay是否自动播放&#xff0c;默认值是false,&#xff08;Boolean&#xff09;loop是否循环播放&#xff0c;默认值是false,&#xf…

Hbase工作原理

Hbase&#xff1a;HBase 底层原理详解&#xff08;深度好文&#xff0c;建议收藏&#xff09; - 腾讯云开发者社区-腾讯云 Hbase架构图 同一个列族如果有多个store&#xff0c;那么这些store在不同的region Hbase写流程&#xff08;读比写慢&#xff09; MemStore Flush Hbas…

arm day2(9.15)数据操作指令,跳转指令,特殊功能寄存器指令,+XMind

作业 1.求最大公约数&#xff1a; .text .global _start _start:mov r0,#0x9mov r1,#0x15bl Loop Loop:cmp r0,r1 比较r0寄存器和r1寄存器的中的值beq stop 当两数相同时,退出程序subhi r0,r0,r1 r0>r1 r0 r0 - r1subcc r1,r1,r0 r0<r1 r1 r1 - r0mov pc,lr 恢复现…

自动驾驶行业观察之2023上海车展-----整体发展趋势

1.行业趋势 新能源势不可挡。 本次车展上首发了150多款新车&#xff0c;约有100款是新能源车;跨国车企全面电动化&#xff0c;但日韩系布局相对缓慢&#xff1b; 2.自主品牌 品牌持续向上 本届车展自主品牌开始疯狂向高端内卷&#xff0c;高端化态度坚决 &#xff08;包括仰…

Modbus RTU(Remote Terminal Unit)与RS-485协议介绍(主站设备(Master)、从站设备(Slave))

文章目录 Modbus RTU与RS-485协议介绍一、引言二、Modbus RTU 协议介绍2.1 Modbus RTU 协议简介2.2 Modbus RTU 协议帧结构主站设备、从站设备与从站设备地址2.3 Modbus RTU 协议举例 三、RS-485 协议介绍3.1 RS-485 协议简介3.2 RS-485 物理连接方式3.3 RS-485 与 Modbus RTU …

代码片段的理解

1.后面的error直接走的是失败的回调 例如:权限不足,可以理解为服务器的一种形式 2.前面走的是成功的回调 但是也可能不对,例如在传过去的参数,在数据库查询不了这个值,传递过来的值不一样&#xff0c;这样它也是走的成功回调。

提升前端开发效率:基于vue的van-radio-group组件封装指南

前言 vant 作为一款流行的 ui 框架&#xff0c;其中&#xff0c;van-radio-group 组件是一个常用的单选框组件&#xff0c;但有时我们需要根据项目需求进行定制化封装。本文将介绍如何基于 vue 框架封装 van-radio-group 组件&#xff0c;让我们一起来探索吧&#xff01; 封装文…

Python网络编程:构建网络应用与通信

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 Python是一门强大的编程…

node查询七牛云上的文件信息

const qiniu require(qiniu) const {getQiNiuKey, } require(./tools)//#region 七牛云 const { accessKey, secretKey } getQiNiuKey() const mac new qiniu.auth.digest.Mac(accessKey, secretKey) let config new qiniu.conf.Config() // 空间对应的机房 config.zone …

目标跟踪方向开源数据集资源汇总

Temple Color 128 数据集下载链接&#xff1a;http://suo.nz/2dKEEL 本数据集包含一大组 128 种颜色序列&#xff0c;带有基本事实和挑战因素注释&#xff08;例如&#xff0c;遮挡&#xff09; NfS高帧率视频数据集 数据集下载链接&#xff1a;http://suo.nz/34o8df 第一个…

旺店通·企业奇门与金蝶云星空对接集成订单查询连通销售订单新增(旺店通销售-金蝶销售订单-小红书)

旺店通企业奇门与金蝶云星空对接集成订单查询连通销售订单新增(旺店通销售-金蝶销售订单-小红书) 接通系统&#xff1a;旺店通企业奇门 慧策最先以旺店通ERP切入商家核心管理痛点——订单管理&#xff0c;之后围绕电商经营管理中的核心管理诉求&#xff0c;先后布局流量获取、会…

react-route的路由

React-Router是一个基于React的强大路由库&#xff0c;它可以帮助我们在React应用中实现页面之间的跳转和路由管理。本文将详细介绍React-Router的路由功能、常用功能模块、路由传参和路由嵌套&#xff0c;并提供相关代码和解释。 路由功能 React-Router通过管理URL和组件的映…

Vue3 ~

变动 实例 const app new Vue({}) Vue.use() Vue.mixin() Vue.component() Vue.directive()const app Vue.createApp({}) app.use() app.mixin() app.component() app.directive()createApp 代替 new Vue 允许多个根标签 createStore 代替 Vue.use(Vuex) createRouter 代替…

保障网络安全:IP代理识别API的作用与应用

引言 随着互联网的不断发展&#xff0c;网络安全问题已经变得愈发重要。在网络上&#xff0c;恶意用户可以利用IP代理隐藏其真实身份&#xff0c;从而发动各种网络攻击或欺诈行为。为了保障网络安全&#xff0c;IP代理识别API成为了一种不可或缺的工具&#xff0c;本文将深入探…

计算机竞赛 深度学习 opencv python 公式识别(图像识别 机器视觉)

文章目录 0 前言1 课题说明2 效果展示3 具体实现4 关键代码实现5 算法综合效果6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于深度学习的数学公式识别算法实现 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学…

Apollo配置更新通知

文章目录 启用方式hook编写服务部署本地部署容器化部署构建镜像 使用 ⚡️: 应领导要求想要把 Apollo 配置变更信息更新到企业微信群中&#xff0c;线上出现异常可根据变更时间&#xff0c;快速反应是否是配置变更导致异常 启用方式 &#x1f31b;: 前提有一个可正常使用的Apo…

微服务保护-Sentinel

初识Sentinel 雪崩问题及解决方案 雪崩问题 微服务中&#xff0c;服务间调用关系错综复杂&#xff0c;一个微服务往往依赖于多个其它微服务。 如图&#xff0c;如果服务提供者I发生了故障&#xff0c;当前的应用的部分业务因为依赖于服务I&#xff0c;因此也会被阻塞。此时&a…

深度学习pytorch之tensorboard和transform的使用

这样操作是引入tensorboard&#xff0c;申明一个类&#xff0c;logs是生成日志的文件夹&#xff0c;事件就在这里产生。 writer为申明的实例&#xff0c;这里做的画线操作 第一个是tags是图片的标签&#xff0c;第二个参数是y值&#xff0c;第三个是步长&#xff0c;x值 关闭…