哈希——哈希表处理哈希冲突的方法

 处理哈希冲突

  • 实践中哈希表⼀般还是选择除法散列法作为哈希函数。

当然哈希表无论选择什么哈希函数也避免不了冲突(主要作用就是减少冲突),那么插入数据时,如何解决冲突呢?主要有两种两种方法,开放定址法链地址法
 

开放定址法

线性探测 

  • 从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。
  • h(key) = hash0 = key % M,  hash0位置冲突了,则线性探测公式为:hc(key, i) = hashi = (hash0 + i) % M,i = {1, 2, 3, ..., M - 1},因为负载因子小于1,则最多探测M-1次,⼀定能找到⼀个存储key的位置。
  • 线性探测的比较简单且容易实现,线性探测的问题假设,hash0位置连续冲突,hash0,hash1,hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做群集/堆积。下面的⼆次探测可以⼀定程度改善这个问题。

简单点说,就是线性探测会发生堆积问题,以下面的图来说,19占了(8)的位置,30经过哈希函数也是要插到(8)的位置,但(8)有值, 就往后插了;20要插(9)也往后插了,以此类推;这种就是堆积;    

也不是说堆积不好,但相同的哈希key(h(key))在一起,总感觉不太好

线性探测的插入
		//线性探测
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			//我们哈希表负载因⼦控制在0.7
			//if (_n * 10 / _tables.size() > 7)
			//{
			//	//.....
			//	//重复线性探测的过程
			//	vector<HashData<K, V>> newtable(_tables.size() * 2);
			//	size_t hhash0 = kv.first % newtable.size();
			//	size_t hhashi = hhash0;
			//	//为什么没有显示
			//	while (newtable[hhashi]._state == EMPTY)
			//	{
			//		//线性探测;
			//		hhashi++;
			//		hhashi = hhashi % newtable.size();
			//	}

			//	newtable[hhashi]._state = EXIST;
			//	newtable[hhashi]._kv = kv;
			//	++_n;

			//	_tables.swap(newtable);
			//}


			if (_n * 10 / _tables.size() >= 7)
			{
				
				//自我实现扩大的是两倍
				//HashTable<K, V> newt;
				//newt._tables.resize(_tables.size() * 2);

				//c++实现
				HashTable<K, V, Hash> newt;
				//lower_bound(first, last, n);  +1 正好能找到比他大的
				newt._tables.resize(__stl_next_prime(_tables.size() + 1));   //素数表

				for (auto e : _tables)
				{
					if (e._state == EXIST)
					{
						newt.Insert(e._kv);
					}
				}

				_tables.swap(newt._tables);
			}


			//线性探测
			//不是 capacity    size才是哈希表的容积
			Hash hash;
			size_t hsah0 = hash(kv.first) % _tables.size();
			size_t hashi = hsah0;


			//为什么没有显示
			while (_tables[hashi]._state != EMPTY)
			{
				//线性探测;
				hashi++;
				hashi = hashi % _tables.size();
			}

			_tables[hashi]._state = EXIST;
			_tables[hashi]._kv = kv;
			++_n;

			return true;
		}

二次探测 

  • 从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下⼀个没有存储数据的位置为止,如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表尾的位置;

如下图:

  • h(key) = hash0 = key % M, hash0位置冲突了,则⼆次探测公式为:

hc(key, i) = hashi = (hash0 ± i2) % M, i = {1, 2, 3, ..., }

  • 二次探测当 hashi = (hash0 - i2)%M 时,当hashi<0时,需要hashi += M

下面演⽰ {19,30,52,63,11,22} 等这一组值映射到M=11的表中。
 

二次探测的插入
//二次探测
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			if (_n * 10 / _tables.size() >= 7)
			{
				//自我实现扩大的是两倍
				//HashTable<K, V> newt;
				//newt._tables.resize(_tables.size() * 2);

				//c++实现
				HashTable<K, V> newt;
				//lower_bound(first, last, n);  +1 正好能找到比他大的
				newt._tables.resize(__stl_next_prime(_tables.size() + 1));
				for (auto e : _tables)
				{
					if (e._state == EXIST)
					{
						newt.Insert(e._kv);
					}
				}
				_tables.swap(newt._tables);
			}

			//不是 capacity    size才是哈希表的容积
			size_t hsah0 = kv.first % _tables.size();
			size_t hashi = hsah0;
			size_t i = 1;
			int flag = 1;
			//为什么没有显示
			while (_tables[hashi]._state != EMPTY)
			{
				//二次探测

				hashi = (hashi + i * i * flag) % _tables.size();

				if (flag > 0)
				{
					flag = -1;
				}
				else if(flag < 0)
				{
					flag = 1;
					i++;
				}
				//当然 既然插入的方法变了, 那么查找的方法也肯定有变化,但在这里就不演示了
			}

			_tables[hashi]._state = EXIST;
			_tables[hashi]._kv = kv;
			++_n;

			return true;
		}

 双重散列

  • 第一个哈希函数计算出的值发生冲突,使用第二个哈希函数计算出一个跟key相关的偏移量值,不断往后探测,直到寻找到下一个没有存储数据的位置为止
  • h1(key) = hash0 = key % M, hash0位置冲突了,则双重探测公式为:hc(key, i) = hashi = (hash0 + i ∗ h2(key)) % M, i = {1, 2, 3, ..., M}
  • 要求 h2(key) < M 且 h2(key) 和M互为质数,

有两种简单的取值方法:

  1. 当M为2整数冥时, 从[0,M-1]任选⼀个奇数;(其中要保证 每个key对应的  h2(key)是一致的)
  2. 当M为质数时, h2(key) = key % (M - 1) + 1
  • 保证 与M互质是因为根据固定的偏移量所寻址的所有位置将形成⼀个群,若最大公约数p = gcd(M, h1(key)) > 1,那么所能寻址的位置的个数为  M / P < M,使得对于⼀个关键字来说无法充分利⽤整个散列表。(简单来说就是充分利用整个散列表,尽量在减少堆积的同时也减少散列表的空位置)

举例来说,若初始探查位置为1,偏移量为3,整个散列表大小为12,那么所能寻址的位置为{1, 4, 7, 10},寻址个数为   12/ gcd(12, 3) = 4。(这个是反例)

(为什么要保证是互质总的来说就是,散列表的位置很多都用不到。还有就是这个公式可以最大的程度利用,其中具体情况 可以自行搜索了解其数学解法哦

下面演示 {19,30,52} 等这⼀组值映射到M=11的表中,设 h2(key) = key%10 + 1
 

二次探测
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			if (_n * 10 / _tables.size() >= 7)
			{
				//自我实现扩大的是两倍
				//HashTable<K, V> newt;
				//newt._tables.resize(_tables.size() * 2);

				//c++实现
				HashTable<K, V> newt;
				//lower_bound(first, last, n);  +1 正好能找到比他大的
				newt._tables.resize(__stl_next_prime(_tables.size() + 1));
				for (auto e : _tables)
				{
					if (e._state == EXIST)
					{
						newt.Insert(e._kv);
					}
				}
				_tables.swap(newt._tables);
			}

			//不是 capacity    size才是哈希表的容积
			size_t hsah0 = kv.first % _tables.size();
			size_t hashi = hsah0;
			size_t i = 1;
			int flag = 1;
			//为什么没有显示
			while (_tables[hashi]._state != EMPTY)
			{
				//二次探测

				hashi = (hashi + i * i * flag) % _tables.size();

				if (flag > 0)
				{
					flag = -1;
				}
				else if(flag < 0)
				{
					flag = 1;
					i++;
				}
				//当然 既然插入的方法变了, 那么查找的方法也肯定有变化,但在这里就不演示了
			}

			_tables[hashi]._state = EXIST;
			_tables[hashi]._kv = kv;
			++_n;

			return true;
		}

链地址法

解决冲突的思路:

开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,链地址法也叫做拉链法或者哈希桶
 

下面演示 {19,30,5,36,13,20,21,12,24,96} 等这一组值映射到M=11的表中。

  • h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1,h(24) = 2,h(96) = 88


 

扩容

开放定址法负载因子必须小于1(这与开放性地址的扩容条件不太一样),链地址法的负载因子就没有限制了,可以大于1。

  • 负载因子越大,哈希冲突的概率越高,空间利用率越高;
  • 负载因子越小,哈希冲突的概率越低,空间利用率越低;

这里就在 负载因子 == 1 时扩容;

stl中unordered_xxx的最大负载因⼦基本控制在1,大于1就扩容,之后手动实现unordered_xxx也使用这个方式。

  • 如果极端场景下,某个桶特别长怎么办?其实我们可以考虑使用全域散列法,这样就不容易被针对了。但是假设不是被针对了,用了全域散列法,但是偶然情况下,某个桶很长,查找效率很低怎么办?

在Java8的HashMap中当桶的长度超过⼀定阀值(8)时就把链表转换成红黑树。这是一个解决极端场景的思路,大家可以了解一下。
其实一般也不会很长,毕竟会扩容;扩容的时候哈希表的数据会重新,经过哈希函数分到不同位置;
 

链地址法代码实现

namespace hash_bucket {

	template<class K, class V>
	struct HashData
	{

	public:
		pair<K, V> _kv;
		HashData* next;


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

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

	//如果不想每次实现都自己传,可以利用全特化,将其设置为默认模板
	//以string 为例子

	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t num = 0;
			for (auto ch : s)
			{
				num += ch;
				//为什么要乘 131 
				//这涉及到BKDR-哈希表算法
				//这可以最大程度避免冲突的情况,具体如何实现可以上网搜索
				num *= 131;

			}
			return num;
		}

	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		typedef HashData<K, V> Node;
		inline unsigned long __stl_next_prime(unsigned long n)
		{
			// Note: assumes long is at least 32 bits.
			static const int __stl_num_primes = 28;
			static const unsigned long __stl_prime_list[__stl_num_primes] = {
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};
			const unsigned long* first = __stl_prime_list;
			const unsigned long* last = __stl_prime_list + __stl_num_primes;
			const unsigned long* pos = lower_bound(first, last, n);
			return pos == last ? *(last - 1) : *pos;
		}

		HashTable()
			//:_tables(__stl_next_prime(0))
			:_tables(11)
			,_n(0)
		{}

		bool Insert(const pair<K, V>& kv)
		{
			
			// 负载因子 == 1时扩容
			// 
			// 这种没必要,有多少节点还有删多少节点,复制多少节点;效率很低;
			// 而且vector的默认析构函数还不能将 节点上的全部节点都删掉;还有自己实现
			//if (_n == _tables.size())
			//{
			//	HashTable newtb;
			//	newtb._tables.resize(__stl_next_prime(_tables.size() + 1));

			//	for (size_t i = 0; i < _tables.size(); i++)
			//	{
			//		Node* cur = _tables[i];
			//		while (cur)
			//		{
			//			newtb.Insert(cur->_kv);
			//			cur = cur->next;
			//		}
			//	}
			//	_tables.swap(newtb._tables);
			//}


			//负载因子 == 1时扩容
			if (_n == _tables.size())
			{
				vector<Node*> newtb;
				newtb.resize(__stl_next_prime(_tables.size() + 1));
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Hash hhash;
						Node* next = cur->next;
						size_t hhash0 = hhash(cur->_kv.first) % newtb.size();
						//头插
						cur->next = newtb[hhash0];
						newtb[hhash0] = cur;

						cur = next;
					}
				}
				_tables.swap(newtb);
			}
			
			Hash hash;
			size_t hash0 = hash(kv.first) % _tables.size();
			Node* newnode = new Node(kv);
			//头插
			newnode->next = _tables[hash0];
			_tables[hash0] = newnode;
			_n++;

			return true;
		}

		Node* Find(const K& key)
		{
			Hash hash;
			size_t hash0 = hash(key) % _tables.size();

			Node* cur = _tables[hash0];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				else
				{
					cur = cur->next;
				}
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			Hash hash;
			size_t hash0 = hash(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hash0];

			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hash0] = cur->next;
					}
					else
					{
						prev->next = cur->next;
					}
					delete cur;
					--_n;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->next;
				}

			}
			return false;
		}
	private:
		vector<Node*> _tables; // 指针数组
		size_t _n = 0;		    // 表中存储数据个数
	};
}

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

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

相关文章

海外云手机是什么?对外贸电商有什么帮助?

在外贸电商领域&#xff0c;流量引流已成为卖家们关注的核心问题。越来越多的卖家开始利用海外云手机&#xff0c;通过TikTok等社交平台吸引流量&#xff0c;以推动商品在海外市场的销售。那么&#xff0c;海外云手机到底是什么&#xff1f;它又能为外贸电商卖家提供哪些支持呢…

Hadoop-001-本地虚拟机环境搭建

一、安装VMware 官方下载VMware&#xff1a; https://vmware.mdsoft.top/?bd_vid5754305114651491003 二、下载镜像文件 阿里云镜像仓库&#xff1a; https://mirrors.aliyun.com/centos/ 本文档使用 CentOS-7-x86_64-DVD-1810-7.6.iso 搭建虚拟机 三、搭建虚拟机 1、编辑…

Oracle视频基础1.1.2练习

1.1.2 需求&#xff1a; 查询oracle组件和粒度大小&#xff0c; select component,granule_size from v$sga_dynamic_components;Oracle SGA 中组件和粒度大小查询详解 在 Oracle 数据库的内存结构中&#xff0c;SGA&#xff08;System Global Area&#xff0c;系统全局区&am…

动态上下文信念(DCB)

DCB&#xff08;动态上下文信念&#xff09;是一个用于累积通过注视获得信息的状态表示组件。它由三个部分组成&#xff1a; Fovea&#xff08;中央凹&#xff09;&#xff1a;接收来自注视位置周围区域的高分辨率视觉输入。Contextual beliefs&#xff08;上下文信念&#xf…

双月生日会:温暖相聚,共庆美好时刻

亲爱的华清远见西安中心的家人们&#xff1a; &#x1f389;&#x1f382; 在这金风送爽的秋日里&#xff0c;我们迎来了9、10月的生日会。在这个特别的日子里&#xff0c;我们聚集一堂&#xff0c;共同庆祝那些在这两个月份里出生的小伙伴们的生日。&#x1f382; 活动现场布…

Junit + Mockito保姆级集成测试实践

一、做好单测&#xff0c;慢即是快 对于单元测试的看法&#xff0c;业界同仁理解多有不同&#xff0c;尤其是在业务变化快速的互联网行业&#xff0c;通常的问题主要有&#xff0c;必须要做吗&#xff1f;做到多少合适&#xff1f;现在没做不也挺好的吗&#xff1f;甚至一些大…

【经典论文阅读11】ESMM模型——基于贝叶斯公式的CVR预估

传统的CVR模型&#xff08;也就是直接对conversion rate建模的模型&#xff09;在实际应用中面临两个问题&#xff08;样本选择偏差与数据稀疏性问题&#xff09;。为了解决这两个问题&#xff0c;本文提出ESMM模型。该模型巧妙地利用用户行为序列去建模这个问题&#xff0c;从…

使用Git进行版本控制的最佳实践

文章目录 Git简介基本概念仓库&#xff08;Repository&#xff09;提交&#xff08;Commit&#xff09;分支&#xff08;Branching&#xff09; 常用命令初始化仓库添加文件提交修改查看状态克隆仓库分支操作合并分支推送更改 最佳实践使用有意义的提交信息定期推送至远程仓库使…

Vision-Language Models for Vision Tasks: A Survey阅读笔记

虽然LLM的文章还没都看完&#xff0c;但是终究是开始看起来了VLM&#xff0c;首当其冲&#xff0c;当然是做一片文献综述啦。这篇文章比较早了&#xff0c;2024年2月份出的last version。 文章链接&#xff1a;https://arxiv.org/abs/2304.00685 GitHub链接&#xff1a;GitHu…

Oracle OCP认证考试考点详解082系列07

题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 31. 第31题&#xff1a; 题目 解析及答案&#xff1a; 关于 “SET VERIFY ON” 命令&#xff0c;以下哪两个陈述是正确的&#xff1f; A…

网络搜索引擎Shodan(7)完结

声明&#xff1a;学习视频来自b站up主 泷羽sec&#xff0c;如涉及侵权马上删除文章 声明&#xff1a;本文主要用作技术分享&#xff0c;所有内容仅供参考。任何使用或依赖于本文信息所造成的法律后果均与本人无关。请读者自行判断风险&#xff0c;并遵循相关法律法规。 感谢泷…

【C++ 算法进阶】算法提升八

复杂计算 &#xff08;括号问题相关递归套路 重要&#xff09; 题目 给定一个字符串str str表示一个公式 公式里面可能有整数 - * / 符号以及左右括号 返回最终计算的结果 题目分析 本题的难点主要在于可能会有很多的括号 而我们直接模拟现实中的算法的话code会难写 要考虑…

​IOT NTN 与 NR NTN​

NTN&#xff08;Non-Terrestrial Network)&#xff09;&#xff0c;即非地面网络通信&#xff0c;通过不同轨道高度的卫星对地面上的终端提供网络连接的服务。利用卫星通信网络与地面蜂窝网络的融合&#xff0c;可以在不受地形地貌的限制和影响下&#xff0c;连通空、天、地、海…

44-RK3588s调试 camera-engine-rkaiq(rkaiq_3A_server)

在RK3588s平台上调试imx415 camera sensor 过程中&#xff0c;已经识别到了camera sensor ID&#xff0c;并且可以拿到raw图和isp处理后的图像&#xff0c;但是isp处理后的图像偏绿&#xff0c;来看查看后台服务发现rkaiq_3A_server没有运行&#xff0c;然后单独运行rkaiq_3A_s…

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper+代码——交叉注意力(Cross-Attention)

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 文章目录 【…

springboot响应文件流文件给浏览器+前端下载

springboot响应文件流文件给浏览器前端下载 1.controller: Api(tags {"【样本提取系统】-api"}) RestController("YbtqYstbtqController") RequiredArgsConstructor RequestMapping("/ybtq-ystbtq") Slf4j public class YbtqYstbtqController …

DAY67WEB 攻防-Java 安全JNDIRMILDAP五大不安全组件RCE 执行不出网

知识点&#xff1a; 1、Java安全-RCE执行-5大类函数调用 2、Java安全-JNDI注入-RMI&LDAP&高版本 3、Java安全-不安全组件-Shiro&FastJson&JackJson&XStream&Log4j Java安全-RCE执行-5大类函数调用 Java中代码执行的类&#xff1a; Groovy Runti…

vue下载安装

目录 vue工具前置要求&#xff1a;安装node.js并配置好国内镜像源下载安装 vue 工具 系统&#xff1a;Windows 11 前置要求&#xff1a;安装node.js并配置好国内镜像源 参考&#xff1a;本人写的《node.js下载、安装、设置国内镜像源&#xff08;永久&#xff09;&#xff…

书生实战营第四期-第四关 玩转HF/魔搭/魔乐社区

一、任务1&#xff1a;模型下载 使用魔搭社区平台下载文档中提到的模型 1.创建开发机 2.环境配置 # 激活环境 conda activate /root/share/pre_envs/pytorch2.1.2cu12.1# 安装 modelscope pip install modelscope -t /root/env/maas pip install numpy1.26.0 -t /root/env/m…

【Blender】 学习笔记(一)

文章目录 参考概念原点 Origin游标 轴心点坐标操作默认快捷键两个比较好用的功能渲染器元素不可选&#xff08;防止误选&#xff09;关联材质 参考 参考b站视频&#xff1a;【Kurt】Blender零基础入门教程 | Blender中文区新手必刷教程(已完结) 概念 模型、灯光、摄像机 原点…