【C++】哈希冲突的解决办法:闭散列 与 开散列

news2024/11/28 20:40:38

哈希冲突解决

上一篇博客提到了,哈希函数的优化可以减小哈希冲突发生的可能性,但无法完全避免。本文就来探讨一下解决哈希冲突的两种常见方法:闭散列开散列

1.闭散列

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

1.1线性探测

插入的情况

还是上述这种情况,当我想插入元素13时,通过哈希函数计算出哈希地址为3,但此时该位置已经存放了元素3,发生了哈希冲突。

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

由于下标4、5都有元素存放,此时找到的空位置下标为6,于是在下标6处存放元素13。

但是哈希表中的空间终究是有限的,空间不够时我们需要进行扩容。但如果等到表被填满再进行扩容的话,这样一来搜索的时间复杂度更趋向于O(N)了,因为表中数据存放越满,理论哈希地址与实际哈希地址相隔越远,最坏情况就近乎O(N)。所以我们需要更早地进行扩容。我们规定在什么情况下进行扩容呢?这里要引入一个载荷因子的概念。

散列表的载荷因子: α = 表中的元素个数 / 散列表的长度

载荷因子标记了需要进行扩容的情况。我们可以得知,载荷因子α越大,散列表越满,而散列表越满,产生冲突的可能性越大;反之α越小,产生冲突的可能性越小。但是不是载荷因子越小越好呢,并不是,载荷因子太小,会造成空间的极大浪费,因此对于开放定址法,载荷因子应限制在0.7-0.8 之间。也就是散列表的空间被利用了70%-80%时,就可以进行扩容了。

扩容之后,由于地址数增加了,关键码通过哈希函数求得的哈希地址也随之改变了,所以需要改变映射关系,改变映射关系之后,原先冲突的元素可能就不再冲突了:

原先元素13,元素17分别和元素3,元素7发生冲突,但是扩容之后,映射关系重新调整,它们就不再冲突了,这就是为什么即使哈希结构不能完全保证搜索的时间复杂度为O(1),但也可以近似于O(1)

搜索的情况

搜索元素时,同样将关键码通过哈希函数计算出理论上的哈希地址,但是该地址处可能发生哈希冲突,若发生哈希冲突,则需要继续向后探测,当我们遇到空时,探测结束,说明搜索失败。

但是碰到下面这种情况呢:

我们搜索的目标元素是:13,由于13前一个位置的元素25被删除,该位置为空,所以会导致探测失败,但实际上13是存在的。所以我们在采用闭散列处理哈希冲突时,不能随意对已有元素进行物理删除,若直接删除,会影响其他元素的搜索。因此线性探测采用标记的的伪删除法来删除元素

对哈希表每个空间给个标记:
EMPTY(此位置空), EXIST(此位置已经有元素), DELETE(元素已经删除)

enum State{EMPTY, EXIST, DELETE}; 

在删除元素时,只需要把该哈希位置的标记从 EXIST 改成 DELETE 即可;

在搜索元素时,探测到标记为 EMPTY 位置时停止,表示探测失败。

这样一来就完美解决了上述问题。

线性探测的代码实现:

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 hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}

		return hash;
	}
};

// 以下采用开放定址法,即线性探测解决冲突
namespace open_address
{
	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 HashFunc = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			// 根据装载因子决定是否扩容
			if (_n * 10 / _table.size() >= 7)
			{
				// 平衡因子>=0.7,需要扩容,扩容后需要重新插入
				size_t newSz = _table.size() * 2;
				HashTable<K, V, HashFunc> newHT;
				newHT._table.resize(newSz);
				for (size_t 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 (EXIST == _table[hashi]._state)
			{
				++hashi;
				hashi %= _table.size();
			}
			_table[hashi]._kv = kv;
			_table[hashi]._state = EXIST;
			++_n;
			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			while (_table[hashi]._state != EMPTY)
			{
				if (_table[hashi]._kv.first == key)
					return &_table[hashi];
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			if (Find(key))
			{
				Find(key)->_state = DELETE;
				return true;
			}
			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]._state == EXIST)
					cout << i << " -> " << _table[i]._kv.first << "," << _table[i]._kv.second << endl;
				else
					cout << i << " -> NULL" << endl;
			}
		}

	private:
		vector<HashData<K, V>> _table;
		size_t _n = 0;  // 表中存储数据个数
	};
}
1.2二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式是挨个往后去找。二次探测就避免了这个问题,在二次探测中,若初始哈希位置 Hash(Key) = h 已被占用,则探测下一个位置的公式为:h(i) = (h + i^2) % m(其中i = 1,2,3,...)。

插入元素13时,与元素3产生冲突,通过二次探测,找到新的哈希位置7进行插入。 

2. 开散列

概念

开散列法又叫链地址法(开链法),它解决哈希冲突的办法是:将具有相同哈希地址的元素通过单链表链接,而哈希表中存放的是各链表的头节点

插入的情况:

初始哈希表中存放都是空指针。需要进行元素插入时,首先根据哈希函数计算出对应的哈希地址,然后为该元素开辟一块地址空间(存放元素数据_data 以及_next指针用于链接冲突的元素)

如果该哈希地址为空,则存放元素节点指针:

如果当前地址发生哈希冲突,则使用头插的方法,即新节点的_next指针指向旧节点,哈希表中更新头结点指针:

与开散列不同的是,由于插入的元素通过单链表的方式进行链接,哈希表中只需要存放各链表头结点指针,所以哈希表不需要扩容就可以实现任意元素的插入。但是不要忘记我们的初衷,哈希表是为了提高数据的搜索效率的,因此我们需要控制链表的长度(链表越长,搜索效率越低下),原理和闭散列一样,也是通过对哈希表扩容,实现元素的分散存储

在开散列中,我们通常将载荷因子定为1 ,即表中元素个数等于散列表长度时,进行扩容。扩容之后需要更新哈希地址,重新进行存储。

下面是扩容后的情况(仅演示地址更新情况,其实该散列还不需要扩容):

删除的情况:

删除指定元素时,我们需要对该元素存放节点进行空间释放,释放后要注意更新链表的链接情况 

代码实现
#pragma once
#include<iostream>
using namespace std;
#include<string>
#include<vector>

// 哈希函数采用除留余数法
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 hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}

		return hash;
	}
};

namespace hash_bucket
{
	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 HashFunc = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_table.resize(10, nullptr); // 显式初始化
		}

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

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			HashFunc hf;
			// 根据装载因子决定是否扩容
			if (_n == _table.size())
			{
				size_t newSz = _table.size() * 2;
				vector<Node*> newTB ;
				newTB.resize(newSz, nullptr);
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						// 头插到新表
						size_t newi = hf(cur->_kv.first) % newSz;
						cur->_next = newTB[newi];
						newTB[newi] = cur;

						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newTB);
			}
			// 插入操作
			size_t hashi = hf(kv.first) % _table.size();
			Node* newNd = new Node(kv);
			newNd->_next = _table[hashi];
			_table[hashi] = newNd;
			++_n;
			return true;
		}

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

		bool Erase(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			if (!cur)
				return false;
			if ( cur->_kv.first == key)
			{
				_table[hashi] = cur->_next;
				return true;
			}
			Node* prev = _table[hashi];
			cur = cur->_next;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					prev->_next = cur->_next;
					delete cur;
					cur = nullptr;
					return true;
				}
				cur = cur->_next;
				prev = prev->_next;
			}
			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i])
				{
					cout << i << ":";
					Node* cur = _table[i];
					while (cur)
					{
						cout << "-->(" << cur->_kv.first << "," << cur->_kv.second << ")";
						cur = cur->_next;
					}
				}
				else
					cout << i << ":-->NULL";
				cout << endl;
			}
		}

	private:
		vector<Node*> _table; // 指针数组
		size_t _n = 0; // 存储了多少个有效数据
	};
}

以上就是对哈希冲突的两种常见解决办法的介绍,欢迎在评论区留言,码文不易,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

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

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

相关文章

线程的理解及基本操作

目录 一、线程的理解 &#xff08;1&#xff09;什么是线程呢&#xff1f; &#xff08;2&#xff09;线程的优缺点及异常 二、线程的基本操作 &#xff08;1&#xff09;创建一个新的进程 &#xff08;2&#xff09;获取线程id &#xff08;3&#xff09;线程终止 &…

H3C OSPF配置

OSPF配置实验 实验拓扑图 实验需求 1.配置IP地址 2.分区域配置OSPF&#xff0c;实现全网互通 3.为了路由结构稳定&#xff0c;要求路由器使用环回口作为Router-id&#xff0c;ABR的环回口宣告进骨干区域 实验配置 1.配置IP地址 R1&#xff1a; <H3C>system-view …

apt的编译安装(古老通讯)

Ubuntu系统的防火墙关闭&#xff1a; ufw disable 第一步&#xff1a;Ubuntu 安装依赖环境 apt -y install libpcre3-dev zlib1g-dev libssl-dev build-essential 如果出现无法下载则在末尾处假如 --fix missing如下图所示 出现下图则为安装成功 第二步&#xff1a; useradd…

Vue.js(2) 入门指南:从基础知识到核心功能

我相信一万小时定律&#xff0c;不相信天上掉馅饼的灵感和坐等的成就。做一个自由而自律的人&#xff0c;势必靠决心认真地活着 文章目录 前言vue是什么?vue做什么?vue的核心功能安装vuevue初体验vue配置选项插值表达式指令vue阻止默认行为总结 前言 Vue.js 是一个用于构建用…

Spring 启动流程分析

Spring 的设计 Bean: Spring作为一个IoC容器&#xff0c;最重要的当然是Bean咯 BeanFactory: 生产与管理Bean的工厂 BeanDefinition: Bean的定义&#xff0c;也就是我们方案中的Class&#xff0c;Spring对它进行了封装 BeanDefinitionRegistry: 类似于Bean与BeanFactory的关…

智能名片小程序源码

智能名片小程序&#xff0c;是一款集在线介绍公司和个人名片、高效获取客户信息以及全面展示公司产品于一体的数字化工具。它通过数字化的方式&#xff0c;让名片信息的传递更加高效、便捷&#xff0c;极大地提升了商务交流的效率和效果。 在功能性方面&#xff0c;智能名片小…

LabVIEW汽车状态监测系统

LabVIEW汽车状态监测系统通过模拟车辆运行状态&#xff0c;有效地辅助工程师进行故障预测和维护计划优化&#xff0c;从而提高汽车的可靠性和安全性。 项目背景&#xff1a; 现代汽车工业面临着日益增长的安全要求和客户对于车辆性能的高期望。汽车状态监测系统旨在实时监控汽…

Golang的Web应用架构设计

# Golang的Web应用架构设计 介绍 是一种快速、高效、可靠的编程语言&#xff0c;它在Web应用开发中越来越受欢迎。Golang的Web应用架构设计通常包括前端、后端和数据库三个部分。在本篇文章中&#xff0c;我们将详细介绍Golang的Web应用架构设计及其组成部分。 前端 在Golang的…

SIP 业务举例之 三方通话:邀请第三方加入的信令流程

目录 1. 3-Way Conference - Third Party Is Added 简介 2. RFC5359 的 3-Way Conference - Third Party Is Added 信令流程 3. 3-Way Conference - Third Party Is Added 总结 博主wx:yuanlai45_csdn 博主qq:2777137742 想要 深入学习 5GC IMS 等通信知识(加入 51学通信)…

GNN+强化学习:双霸主强强联合,10种创新思路刷爆顶会!

图神经网络&#xff08;GNN&#xff09;强化学习&#xff08;RL&#xff09;&#xff0c;融合了GNN在图数据表示上的深度学习能力和RL在决策过程中的策略优化能力。这种结合为处理具有复杂图结构的数据问题提供了强大的工具。 GNN与强化学习的结合不仅推动了图机器学习的研究进…

R语言机器学习算法实战系列(十三)随机森林生存分析构建预后模型 (Random Survival Forest)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍教程加载R包案例数据数据预处理数据描述构建randomForestSRC模型评估模型C-indexBrier score特征重要性构建新的随机森林生存模型风险打分高低风险分组的生存分析时间依赖的ROC(Ti…

Http 状态码 301 Permanent Rediret 302 Temporary Redirect、 重定向 重写

HTTP状态码301和302是什么&#xff1f; 1、HTTP状态码301 HTTP状态码301表示永久性转移&#xff08;Permanent Redirect&#xff09;&#xff0c;这意味着请求的资源已经被分配了一个新的URI&#xff0c;以后的引用应该使用资源现在所指的URI。 HTTP 301状态码表示请求的资源…

Segugio:一款针对恶意软件的进程执行跟踪与安全分析工具

关于Segugio Segugio是一款功能强大的恶意软件安全分析工具&#xff0c;该工具允许我们轻松分析恶意软件执行的关键步骤&#xff0c;并对其进行跟踪分析和安全审计。 Segugio允许执行和跟踪恶意软件感染过程中的关键步骤&#xff0c;其中包括从点击第一阶段到提取恶意软件的最…

CSS.导入方式

1.内部样式 在head的style里面定义如 <style>p1{color: brown;}</style> 2.内联样式 直接在标签的里面定义如 <p2 style"color: blue;">这是用了内联样式&#xff0c;蓝色</p2><br> 3.外部样式表 在css文件夹里面构建一个css文件…

Java Lock CyclicBarrier 总结

前言 相关系列 《Java & Lock & 目录》&#xff08;持续更新&#xff09;《Java & Lock & CyclicBarrier & 源码》&#xff08;学习过程/多有漏误/仅作参考/不再更新&#xff09;《Java & Lock & CyclicBarrier & 总结》&#xff08;学习总结…

【现代C++】常量求值

现代C&#xff08;特别是C11及以后的版本&#xff09;增强了对编译时常量求值的支持&#xff0c;包括constexpr函数、constinit和consteval关键字。这些特性允许在编译时进行更多的计算&#xff0c;有助于优化运行时性能并确保编译时的数据不变性。 1. constexpr - 编译时常量…

[含文档+PPT+源码等]精品基于PHP实现的培训机构信息管理系统的设计与实现

基于PHP实现的培训机构信息管理系统的设计与实现背景&#xff0c;可以从以下几个方面进行阐述&#xff1a; 一、社会发展与教育需求 随着经济的不断发展和人口数量的增加&#xff0c;教育培训行业迎来了前所未有的发展机遇。家长对子女教育的重视程度日益提高&#xff0c;课外…

雷池社区版compose配置文件解析-mgt

在现代网络安全中&#xff0c;选择合适的 Web 应用防火墙至关重要。雷池&#xff08;SafeLine&#xff09;社区版免费切好用。为网站提供全面的保护&#xff0c;帮助网站抵御各种网络攻击。 compose.yml 文件是 Docker Compose 的核心文件&#xff0c;用于定义和管理多个 Dock…

LeetCode题(二分查找,C++实现)

LeetCode题&#xff08;二分查找&#xff0c;C实现&#xff09; 记录一下做题过程&#xff0c;肯定会有比我的更好的实现办法&#xff0c;这里只是一个参考&#xff0c;能帮到大家就再好不过了。 目录 LeetCode题&#xff08;二分查找&#xff0c;C实现&#xff09; 一、搜…

Rust编程与项目实战-元组

【图书介绍】《Rust编程与项目实战》-CSDN博客 《Rust编程与项目实战》(朱文伟&#xff0c;李建英)【摘要 书评 试读】- 京东图书 (jd.com) Rust编程与项目实战_夏天又到了的博客-CSDN博客 8.2.1 元组的定义 元组是Rust的内置复合数据类型。Rust支持元组&#xff0c;而且元…