C++数据结构:哈希桶 -- 通过开散列的方法解决哈希冲突

news2024/12/24 2:30:08

目录

一. 什么是哈希桶

二. 哈希桶的实现

2.1 哈希表节点数据

2.2 特定Key值的查找find

2.3 哈希桶的扩容

2.4 数据插入操作insert

2.5 数据删除操作erase

2.6 哈希桶的析构函数 

附录:哈希桶的实现完整版代码


一. 什么是哈希桶

之前的博客中我提到过,可以采用闭散列(二次探测、线性探测)的方法来解决哈希冲突,但是,无论是线性探测还是二次探测,其解决哈希冲突的本质方法都是去“占位”,即:发生哈希冲突的Key会去占用其他Key值原本应当占用的位置,这样一个冲突可能引发一串冲突。

为了解决闭散列方法的上述弊端,可以使用哈希桶(开散列)的方法来解决哈希冲突。在hashTable的每个位置都挂一个哈希桶,哈希桶用单链表来表示。将所有发生冲突的数据都挂在一个桶上。

此时,hashTable存储的数据类型时单链表节点,hashTable[i]是每个哈希桶的头结点,发生冲突时的数据插入相当于单链表的头插操作。

图1.1 哈希桶结构图

二. 哈希桶的实现

2.1 哈希表节点数据

由于哈希桶的本质是挂在哈希表上的单链表,因此,哈希表节点的数据应当为单链表节点,包括:

  • 一个键值对pair<K, V> _kv,记录key值和与之配对的value值。
  • 一个节点指针_next,指向单链表的下一个节点。

在Hash类模板中,将hashDate<K, V>类型重定义为Node。

代码2.1:(哈希表节点)

template<class K, class V>
struct hashDate
{
	std::pair<K, V> _kv;
	hashDate<K, V>* _next;

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

2.2 特定Key值的查找find

函数Node* find(const K& key),找到了返回key所在的节点地址,找不到返回nullptr。

  1. 通过哈希函数,获取key所在的哈希桶应当挂在_hashTable哪个下标位置处(记为hashi)。
  2. 遍历以_hashTable[hashi]为头结点的单链表,查找key,找到了返回链表节点。
  3. 如果遍历到nullptr还没找到,则表示key不存在于哈希桶中,函数返回false。

代码2.2:(find函数的实现)

	Node* find(const K& key)
	{
		//哈希表中没有数据就直接返回false
		//这里是为了避免模0(%0)错误
		if (_hashTable.size() == 0)
		{
			return nullptr;
		}

		HashKey hashKey;
		//通过哈希函数计算key所在哈希桶
		size_t hashi = hashKey(key) % _hashTable.size();  
		//哈希桶单链表头结点
		Node* cur = _hashTable[hashi];

		//变量哈希桶(单链表),寻找key
		while (cur)
		{
			if (hashKey(cur->_kv.first) == hashKey(key))
			{
				return cur;
			}

			cur = cur->_next;
		}

		return nullptr;
	}

2.3 哈希桶的扩容

如果通过闭散列(线性探测或二次探测)的方法来解决哈希冲突,那么哈希表中存储的数据个数_size一定不能超过_table.size()。但是如果使用哈希桶,哈希表的每个位置挂的哈希桶可以有多个数据,因此负载因子是可以大于1的。

闭散列一般当负载因子大于0.7~0.8是扩容,而哈希桶则应适度提高。可以认为当插入数据后负载因子大于1就扩容。

哈希桶扩容同样需要改变原来的结构,因为不同的Key值对于的存储位置会发生改变。这里不再复用insert函数来实现数据位置的改变,而是依次遍历每个哈希桶的每个节点,创建一个新的vector作为hashTable,通过单链表头插,来使数据满足扩容后的哈希结构要求。

图2.3 扩容前后哈希桶结构的变化

代码2.3:(哈希表扩容) 

		//扩容(负载因子大于1)
		if (_hashTable.size() == 0 || _size == _hashTable.size())
		{
			size_t newSize = _hashTable.size() == 0 ? 10 : 2 * _hashTable.size();

			//将原来哈希表数据的位置进行相应改变
			//这里使用一个新的vector来实现
			std::vector<Node*> newTable;
			newTable.resize(newSize, nullptr);

			//将原来table每个位置处挂的单链表,挪到新的table对应位置
			for (size_t i = 0; i < _hashTable.size(); ++i)
			{
				Node* cur = _hashTable[i];

				while (cur)
				{
					Node* next = cur->_next;
					size_t newHashi = hashKey(cur->_kv.first) % newSize;

					//执行头插操作
					cur->_next = newTable[newHashi];
					newTable[newHashi] = cur;

					cur = next;
				}

				_hashTable[i] = nullptr;
			}

			_hashTable.swap(newTable);
		}

2.4 数据插入操作insert

哈希桶是挂在hashTable指定位置处的单链表。进行数据插入时,只需在检查数据是否已经存在、是否需要扩容之后,要通过哈希函数Hash(Key)找到对应的位置,然后执行单链表头插操作即可。

图2.4 数据插入操作

2.5 数据删除操作erase

通过哈希函数获得哈希桶的位置后,遍历单链表(哈希桶)找到key所在的位置,执行单链表节点删除操作即可。

代码2.5:(数据删除)

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

		HashKey hashKey;

		size_t hashi = hashKey(key) % _hashTable.size();
		Node* prev = nullptr;
		Node* cur = _hashTable[hashi];

		while (cur)
		{
			//找到key了,删除
			if (hashKey(cur->_kv.first) == key)
			{
				//头删
				if (prev == nullptr)
				{
					_hashTable[hashi] = cur->_next;
					free(cur);
					cur = nullptr;
				}
				else  //中间删或尾删
				{
					prev->_next = cur->_next;
					free(cur);
					cur = nullptr;
				}

				--_size;
				return true;
			}

			prev = cur;
			cur = cur->_next;
		}

		return false;
	}

2.6 哈希桶的析构函数 

自己编写代码,依次对每个哈希桶进行释放,每个哈希桶的析构等同于单链表的析构。由于vector为自定义类型数据,编译的会主动调用vector的析构函数释放hashTable的空间。

代码2.6:(析构函数)

	~Hash()   //析构函数
	{
		//释放每个节点上挂的数据
		for (size_t i = 0; i < _hashTable.size(); ++i)
		{
			Node* cur = _hashTable[i];

			//释放每个非空节点
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}

			_hashTable[i] = nullptr;
		}
	}

附录:哈希桶的实现完整版代码

#include<vector>

template<class K, class V>
struct hashDate
{
	std::pair<K, V> _kv;
	hashDate<K, V>* _next;

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

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

template<class K, class V, class HashKey = HashKeyVal<K>>
class Hash
{
	typedef hashDate<K, V> Node;

public:
	~Hash()   //析构函数
	{
		//释放每个节点上挂的数据
		for (size_t i = 0; i < _hashTable.size(); ++i)
		{
			Node* cur = _hashTable[i];

			//释放每个非空节点
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}

			_hashTable[i] = nullptr;
		}
	}

	bool insert(const std::pair<K, V>& kv)
	{
		//查找 -- 确保不插入重复数据
		if (find(kv.first))
		{
			return false;
		}

		HashKey hashKey;

		//扩容(负载因子大于1)
		if (_hashTable.size() == 0 || _size == _hashTable.size())
		{
			size_t newSize = _hashTable.size() == 0 ? 10 : 2 * _hashTable.size();

			//将原来哈希表数据的位置进行相应改变
			//这里使用一个新的vector来实现
			std::vector<Node*> newTable;
			newTable.resize(newSize, nullptr);

			//将原来table每个位置处挂的单链表,挪到新的table对应位置
			for (size_t i = 0; i < _hashTable.size(); ++i)
			{
				Node* cur = _hashTable[i];

				while (cur)
				{
					Node* next = cur->_next;
					size_t newHashi = hashKey(cur->_kv.first) % newSize;

					//执行头插操作
					cur->_next = newTable[newHashi];
					newTable[newHashi] = cur;

					cur = next;
				}

				_hashTable[i] = nullptr;
			}

			_hashTable.swap(newTable);
		}

		//获取插入位置
		size_t hashi = hashKey(kv.first) % _hashTable.size();
		Node* node = new Node(kv);

		//插入数据
		node->_next = _hashTable[hashi];
		_hashTable[hashi] = node;

		++_size;
	}

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

		HashKey hashKey;

		size_t hashi = hashKey(key) % _hashTable.size();
		Node* prev = nullptr;
		Node* cur = _hashTable[hashi];

		while (cur)
		{
			//找到key了,删除
			if (hashKey(cur->_kv.first) == key)
			{
				//头删
				if (prev == nullptr)
				{
					_hashTable[hashi] = cur->_next;
					free(cur);
					cur = nullptr;
				}
				else  //中间删或尾删
				{
					prev->_next = cur->_next;
					free(cur);
					cur = nullptr;
				}

				--_size;
				return true;
			}

			prev = cur;
			cur = cur->_next;
		}

		return false;
	}

	Node* find(const K& key)
	{
		//哈希表中没有数据就直接返回false
		//这里是为了避免模0(%0)错误
		if (_hashTable.size() == 0)
		{
			return nullptr;
		}

		HashKey hashKey;
		//通过哈希函数计算key所在哈希桶
		size_t hashi = hashKey(key) % _hashTable.size();  
		//哈希桶单链表头结点
		Node* cur = _hashTable[hashi];

		//变量哈希桶(单链表),寻找key
		while (cur)
		{
			if (hashKey(cur->_kv.first) == hashKey(key))
			{
				return cur;
			}

			cur = cur->_next;
		}

		return nullptr;
	}

private:
	size_t _size = 0;   //哈希表中数据个数
	std::vector<Node*> _hashTable;   //哈希表(存储桶节点)
};

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

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

相关文章

手机突然被停机了,那你可能‘摊上’这些问题了!

最近总是有小伙伴私信小编&#xff0c;手机明明有话费&#xff0c;但是不能用流量也不能打电话。小编一通分析&#xff0c;信号好&#xff0c;未欠费&#xff0c;八成是手机被停机了。结论一出&#xff0c;小伙伴更懵逼了&#xff0c;手机还可以被停机&#xff1f;今天这个视频…

一.手把手教你部署项目-VMware安装Centos

前言 市场内卷&#xff0c;只会写代码的程序员可没那么吃香了&#xff0c;在企业中很多时候会要求后端程序写前端&#xff0c;甚至做运维&#xff0c;因为小公司为了解决成本是不会请专门的运维人员的。所以对于后端程序原来说&#xff0c;会一些运维只是必不可少的&#xff0…

TCP协议/UDP协议(三次握手/四次挥手)

TCP协议 保证传输过程的三个关键的步骤&#xff0c;分别为三次握手、传输确认、四次挥手。 三次握手 三次握手是建立连接的过程&#xff0c;当客户端向服务端发起连接时&#xff0c;会先发一包连接请求数据&#xff0c;过去询问一下&#xff0c;能否与你建立连接&#xff0c;…

IPv4和IPv6协议

IPv4和IPv6报文比较 IPv4报文格式 IP Packet(IP数据包),其包头主要内容如下: Version:4 bit,4:表示为IPv4;6:表示为IPv6。 Header Length:4 bit,首部长度,如果不带Option字段,则为20,最长为60。 Type

找回 Windows 映射网络驱动器密码

随着越来越多的人使用 NAS&#xff08;网络云硬盘&#xff09;&#xff0c;各种网络映射驱动器的问题也随之出现。最近有个客户要换电脑。换电脑之后就无法访问 NAS 了&#xff0c;因为他记不得他 NAS 里边设置的用户名密码。还好他之前的电脑有保存这些密码记录。 第一次链接 …

【算法学习系列】02 - 你真的有好好使用过 Math.random() 函数吗?

文章目录 说明验证函数等概率返回功能验证 [0, 8)上也是等概率返回一个数的功能验证等概率返回[0, K - 1]中的一个整数实现&#xff1a;任意x&#xff0c;x属于[0, 1)&#xff0c;[0, x)范围上的数出现概率由原来的x调整成x平方 说明 获取随机数大家应该都有用到过 Math.random…

PMP课堂模拟题目及解析(第10期)

91. 在项目执行阶段&#xff0c;一名项目干系人要求项目经理加入一个新过程的优化。项目经理应该怎么做&#xff1f; A. 执行实施整体变更控制过程。 B. 与过程专家一起审查项目。 C. 将优化项目分配给团队。 D. 拒绝范围蔓延企图。 92. 项目经理要求团队提供对项目应急计…

【LeetCode】415. 字符串相加

415. 字符串相加&#xff08;简单&#xff09; 方法一 思路 这道题很简单&#xff0c;我们知道&#xff0c;如果对两个数相加&#xff0c;那么需要对位相加&#xff0c;为了方便对位&#xff0c;我们可以对长度较小的字符串前面补 0&#xff0c;使得 num1 和 num2 长度相等。…

美团Java开发一面凉经

目录 1.HashMap底层数据结构2.列举几个常见的线程安全容器3.HashMap线程问题4.concurrentHashMap5.ConcurrentModificationException6.Spring AOP、IOC、DI介绍下7.不使用依赖注入&#xff0c;使用传统方式的声明会有什么问题8.最左前缀原则9.TCP三次握手、四次挥手 1.HashMap底…

EasyExcel读取EXcel文件内容

目录 一 官方文档介绍 二 读取文件内容 1.根据文档内容建立读对象 2.创建读监听器 3.测试类代码 一 官方文档介绍 Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存&#xff0c;poi有一套SAX模式的API可以一定程度的解决…

(二十三)数据结构-哈希表

1 哈希表的基本介绍 1.1 用于存储的数据结构 在计算机中&#xff0c;数组和链表都可以用于数据的存储&#xff0c;既然有数据存储&#xff0c;那么必然要有数据的查询&#xff0c;因此我们在将数据存储进数组和链表中之后&#xff0c;必然要对它们进行查询操作。一个链表的查…

java+springboot+jsp农产品商城农场信息化系统多用户

系统功能包括前台&#xff1a;首页、商品信息、新闻资讯、我的、跳转到后台、购物车&#xff0c;管理员&#xff1a;个人中心、用户管理、员工管理、技术专家管理、部门信息管理、资金统计管理、农资信息管理、商品分类管理、商品信息管理、入库记录管理、出库记录管理、销售统…

如何调用api接口获取其中的数据

part1.API接口可以运用到的场景&#xff0c;主要包括以下几个方面&#xff1a; 1. 应用程序集成&#xff1a;API可以使不同的应用程序相互之间进行集成&#xff0c;比如将某个应用程序的数据传递给另一个应用程序&#xff0c;或者调用另一个应用程序的功能。 2. 数据共享&#…

cocos2dx游戏项目,集成到其他安卓项目工程之中!

背景 公司&#xff0c;想优化掉&#xff0c;在app中&#xff0c;以webview方式&#xff0c;加载游戏的方式。以安卓项目为例&#xff0c;改成&#xff1a;游戏项目导出安卓工程&#xff0c;可直接使用的aar资源。 第一步&#xff1a;cocos项目&#xff0c;构建安卓工程 安装…

icmp协议

1、icmp协议 2、工具之ping -c (设置ping的次数&#xff0c;默认无限次&#xff0c;可选) -i (设置ping的时间间隔&#xff0c;默认1秒&#xff0c;可选) -W (设置ping的超时时间&#xff0c;单位秒&#xff0c;可选) ping -c 3 -i 0.1 -W 0.1 www.baidu.com-i 自定义时间间…

接口自动化框架对比 | 质量工程

一、前言 自动化测试是把将手工驱动的测试行为转化为机器自动执行&#xff0c;通常操作是在某一框架下进行代码编写&#xff0c;实现用例自动发现与执行&#xff0c;托管在CI/CD平台上&#xff0c;通过条件触发或手工触发&#xff0c;进行回归测试&线上监控&#xff0c;代替…

opencv-python相机标定详解

文章目录 角点检测查看角点标定 opencv中内置了张正友的棋盘格标定法&#xff0c;通过一些姿态各异的棋盘格图像&#xff0c;就能标定相机的内外参数。 角点检测 第一步是角点检测&#xff0c;首先需要读取棋盘格图像 import numpy as np import cv2 import ospath imgs #…

一种Android应用的桌面图标隐藏方法

在Android10之前&#xff0c;应用程序通过调用PackageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)函数来实现图标隐藏。 但是在android10之后&#xff0c;所有带四大组件&#xff08…

RabbitMQ养成记 (6. spingboot 集成 rabbitMQ,生产者/消费者)

Springboot集成 搞springboot的那群人 不喜欢造轮子&#xff0c;就喜欢搞各种集成。 首先创建一个springboot项目&#xff1a; 之前我们在方法中 创建工厂类配置&#xff0c; 现在直接在application.yml 中配置即可&#xff1a; spring:rabbitmq:host: **********username:…

赢得浮生半日闲,内卷时代,我们需要怎样的智能科技?

哲学家罗泰戈拉说&#xff1a;“人是万物的尺度&#xff0c;是存在的事物存在的尺度。” 智能电器&#xff0c;究竟能为用户和市场提供什么样的价值&#xff1f;凭什么让消费者买单&#xff1f;毫无疑问&#xff0c;也应该以“人”为标尺。 今天&#xff0c;忙碌似乎成了现代人…