哈希(hash)——【C++实现】

news2024/11/23 20:12:59

在这里插入图片描述

本章gitee代码仓库:Hash

文章目录

    • 💐1. 哈希概念
    • 🌻2. 哈希冲突
    • 🌼3. 哈希函数
      • 🌸3.1 哈希函数设计原则
      • 🌸3.2 常见哈希函数
    • 🪴4. 哈希冲突解决方案
      • 🌱4.1 闭散列——开放定址法
        • 🌿4.11 负载因子
        • 🌿4.12 字符串哈希算法
        • 🌿4.13 代码实现
      • 🌱4.2 开散列——哈希桶
        • 🌿4.21 代码实现

💐1. 哈希概念

我们对元素进行搜索有几种方式:

  1. 暴力查找,直接遍历元素,时间复杂度为O(N)

    image-20230917101056152

  2. 二分查找,时间复杂度为O(logN)

    但二分查找有2个弊端:

    • 必须为有序
    • 增删查改不方便

    这两个弊端导致二分查找只是一个理想的查找方式,并不是很现实

    image-20230917101700713

  3. 平衡搜索树,增删查改的时间复杂度都是O(logN),总体性能都很不错

    image-20230917101710006

这些结构中的元素关键码和存储位置都没有对应的关系,而有一种方法名为哈希(也叫散列),它提供了一种与这些完全不同的存储和查找方式,即将存储的值和存储的位置建立出一个对应的函数关系。

有一种排序名为计数排序,将不同的值映射到对应的位置,这本质上就是哈希

不了解的可以看下这篇文章——非比较排序——计数排序

我们的通讯录,按照名字的首字母进行分类,本质也是哈希

image-20230917103230972

以数组{1,8,6,3}为例,假设hashi = key % 6 ,那则有如下对应关系

image-20230917104706750

这样就能通过取模直接定位到该元素的位置

但是如果进行插入元素,例如插入22%6=2,这就会导致和8的位置一样

🌻2. 哈希冲突

不同的关键字通过哈希函数计算出了相同的地址,值和位置出现了多对一的关系,这种线性称之为哈希冲突(哈希碰撞)。

解决方案:

  1. 选择合理的哈希函数
  2. 拟定冲突方案

🌼3. 哈希函数

出现哈希碰撞的原因之一可能就是哈希函数设计的不合理

🌸3.1 哈希函数设计原则

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须是[0,m-1]之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数较为简单,能在较短时间内计算出结构

🌸3.2 常见哈希函数

  1. 直接定址法(值的范围集中)

    取某个线性函数作为散列的地址:Hash(key) = A*key + B

  2. 除留余数法(值的范围分散)

    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m质数p作为除数

    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

🪴4. 哈希冲突解决方案

🌱4.1 闭散列——开放定址法

如果当前位置被占用,按照规则找到下一个位置(占用其他元素的位置)

image-20230917112017514

我们删除元素通常都不是直接删除,而采用覆盖的方式,而这里无法很好的覆盖

例如我们删除元素1,如果挪动后面的数据,这就导致映射关系全部乱了,如果不直接覆盖,那么之后又元素插入进来的时候,1这个位置还是有元素的,无法插入

所有这里采用状态的方式进行标记:

  • 存在——EXIST
  • 空——EMPTY
  • 删除——DELETE

🌿4.11 负载因子

哈希表定义了一个载荷因子α = 填入表中元素个数 / 哈希表的长度,这个是表示哈希表装满长度的标志因子

如果负载因子设计的大,那么哈希冲突的概率就越大(空间利用率高)

如果负载因子设计的小,那么哈希冲突的概率就越小(空间利用率低)

对于开放定址法,经过测算,负载因子应该控制在0.7 ~ 0.8,下面代码实现采用0.7

🌿4.12 字符串哈希算法

面对字符串的哈希函数,我们采用BKDRHash函数

有兴趣可查看此篇文章——各种字符串Hash函数

🌿4.13 代码实现

template<class K>
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//模板特化
template<>
struct DefaultHashFunc<string>
{
	size_t operator()(const string& str)
	{
		//BKDR hash
		size_t hash = 0;
		for (auto ch : str)
		{
			hash *= 131;
			hash += ch;
		}
		return (size_t)str[0];
	}
};


//开放定址法
namespace open_address
{
	enum STATE
	{
		EXIST,
		EMPTY,
		DELETE
	};

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

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);	//预先开好10个空间
		}

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

			if (Find(kv.first))
				return false;

			//扩容
			if (_n * 10 / _table.size() >= 7)	//设负载因子为0.7
			{
				size_t newSize = _table.size() * 2;
				//扩容之后关系改变,需要重新映射
				HashTable<K, V> newHT;
				newHT._table.resize(newSize);

				//旧表数据插入到新标
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state == EXIST)
					{
						newHT.Insert(_table[i]._kv);
					}
				}
				//新标和旧表交换
				_table.swap(newHT._table);
			}

			//不能取模capacity,虽然空间有,但访问还是要看size的大小,不然会发生越界
			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;
		}

		HashDate<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 (HashDate<const K, V>*) & _table[hashi];
				}
				++hashi;
				hashi %= _table.size();
			}
			return nullptr;
		}

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

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

线性探测会导致一片拥堵,为此还有一种方法为二次探测

例如线性探测是:

hashi = key % n;
//如果有值了 i>=0
hashi+=i;

而二次探测则是:

hashi = key % n;
//如果有值 i>=0
hashi + i^2;

这样就能在一定程度上减少拥堵

🌱4.2 开散列——哈希桶

开放定址法的缺陷就是冲突会相互影响。而哈希桶的做法是,设置一个指针数组,如果发现冲突,则内部消化

image-20230917141822713

这里桶的结构其实就是链式结构,对每个桶的管理就相当于对于链表的管理,下面的代码采用的是单链表

这里也是需要进行扩容,如果不扩容,就会导致在某种情况下,桶越来越长,这样查找数据就变成了对链表数据的查找,时间复杂度为O(N),所以还是需要进行扩容。

这里的负载因子可以适当放大一点,一般负载因子控制在1,平均下来每个桶都有数据

🌿4.21 代码实现

这里的桶因为是自定义的链式结构,所以需要我们自己写拷贝构造和析构函数

//哈希桶
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 = DefaultHashFunc<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;
			}
		}
		//拷贝构造
		HashTable(const HashTable& ht)
		{
			_table.resize(ht._table.size(), nullptr);
			HashFunc hf;
			for (size_t i = 0; i < ht._table.size(); i++)
			{
				Node* cur = ht._table[i];
				while (cur)
				{
					Node* newNode = new Node(cur->_kv);
					size_t hashi = hf(cur->_kv.first) % ht._table.size();

					//头插
					newNode->_next = _table[hashi];
					_table[hashi] = newNode;

					cur = cur->_next;
				}
			}
			_n = ht._n;
		}

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

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

			HashFunc hf;
			//扩容 -- 扩容的时候会稍微慢一点 ---^(扩容)-----^(扩容)----------^(扩容)-----.....
			//这里的扩容不能和开放定址法一样采用将旧表元素重新插入新表
			//因为这里涉及到开节点,新表开新节点,旧表释放旧节点,浪费
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newSize,nullptr);

				//遍历旧表,将节点牵过来
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						//头插到新表
						size_t newHashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[newHashi];
						newTable[newHashi] = cur;

						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newTable);
			}

			size_t hashi = hf(kv.first) % _table.size();
			//头插
			Node* newNode = new Node(kv);
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;

			++_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* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					//头删
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					--_n;
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}


	private:
		vector<Node*> _table;	//指针数组
		size_t _n = 0;	//有效元素
	};
}

本次参考了《数据结构(用面向对象方法与C++语言描述)》,详细的内容可以参考此书

那么本期的分享就到这里咯,我们下期再见,如果还有下期的话。

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

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

相关文章

Android存储权限完美适配(Android11及以上适配)

一、Bug简述 一个很普通的需求&#xff0c;需要下载图片到本地&#xff0c;我的三个测试机&#xff08;荣耀Android10&#xff0c;红米 11 和小米Android 13都没有问题&#xff09;。 然后&#xff0c;主角登场了&#xff0c;测试的三星Android 13 死活拉不起存储权限弹窗。 …

使用setInterval定时器实现文字一个一个的打印在页面中

前言&#xff1a; 项目需求实现请求回来的数据一个一个的打印在页面中&#xff0c;点击“停止生成”暂停打印&#xff1b;该功能需求类似于文心一言。 代码实现&#xff1a; view页面代码&#xff1a; script代码&#xff1a; 核心代码&#xff1a; let k 0; timer setInte…

【python】lightgbm 无法打开文件

问题&#xff1a;文件无法打开 在使用 lightgbm 读取模型文件时不能打开 报错&#xff1a; Traceback (most recent call last):File "detect_interpretability_.py", line 176, in <module>lightgbm_test(White_data, os.path.basename(data_path), model_f…

得帆信息联合创始人——王周健:大中型企业集成平台选型建议

在当今这个数字化时代&#xff0c;大中型企业面临着前所未有的挑战&#xff0c;企业需要快速响应外部市场变化&#xff0c;以保持竞争力。在这个过程中&#xff0c;企业需要快速、准确的决策力&#xff0c;需要大量有价值的数据作为支撑。所以跨企业、跨部门、跨系统的业务联通…

C语言字符函数和字符串函数(1)

大家好&#xff0c;我们又见面了&#xff0c;让大家久等了&#xff0c;我们今天就来学习字符函数和字符串函数。 在开启今天的学习之前呢&#xff0c;我来解决一下一些小伙伴平时找不到库函数使用的烦恼&#xff0c;因为我们cplusplus.com最新版本不能够查询函数&#xff0c;我…

C盘扩容(微PE工具箱)

C盘扩容&#xff08;微PE工具箱&#xff09; 1、关闭Bitlocker2、安装微PE3、以D盘分10G给C盘为例3.1安装后重启会出现两个选项电脑系统和PE系统&#xff0c;选择PE系统3.2调成分区空间3.3检查分盘情况并删除PE系统 1、关闭Bitlocker 2、安装微PE https://www.wepe.com.cn/dow…

2023年招标行业研究报告

第一章 行业概况 1.1 招标定义和分类 招标行业涉及政府、企事业单位通过公开、公平、公正的手段组织采购、工程建设等项目的过程。尽管中国的招标行业发展历程相对较短&#xff0c;但随着市场经济的深入发展和政府采购制度的持续改革&#xff0c;该行业已逐渐崭露头角&#x…

《遇见秋分》公众号排版模板,感受秋天的文艺唯美

秋分,是二十四节气之第十六个节气,秋季第四个节气。秋分这天太阳几乎直射地球赤道,全球各地昼夜等长。秋分&#xff0c;“分”即为“平分”、“半”的意思,除了指昼夜平分外,还有一层意思是平分了秋季。秋分日后,太阳光直射位置南移,北半球昼短夜长&#xff0c;昼夜温差加大,气…

ptmalloc源码分析 - Top chunk的扩容函数sysmalloc实现(09)

目录 一、sysmalloc函数基本分配逻辑 二、强制try_mmap分配方式 三、非主分配区分配的实现 1. 设置老的Top chunk的参数 2. 尝试使用grow_heap函数 3. 尝试使用new_heap函数 4. 尝试使用try_mmap方式 四、主分配区分配的实现 1. 设置Top扩容的size值 2. brk分配成功的…

(25)(25.1) 光学流量传感器的测试和设置

文章目录 25.1.1 测试传感器 25.1.2 校准传感器 25.1.3 测距传感器检查 25.1.4 预解锁检查 25.1.5 首次飞行 25.1.6 第二次飞行 25.1.7 正常操作设置 25.1.8 视频示例&#xff08;Copter-3.4&#xff09; 25.1.9 空中校准 25.1.1 测试传感器 将传感器连接至自动驾驶仪…

面试官:Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?

&#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 一、Object.defineProperty 为什么能实现响应式 小结 二、proxy 三、总结 一、Object.defineProperty 定义&am…

vue打印、vue-print-nb插件的基本使用

今天做项目碰到一个打印的需求&#xff0c;只打印一个表格&#xff0c;去网上找了些方法总结一下 打印的方法最常见的就是window.print(),这是浏览器自带的打印方法&#xff0c;方便快捷无需安装插件&#xff0c;但相应的自定义化也差无法打印页面局部&#xff0c;去网上查找资…

薄盒借周杰伦IP卖藏品 车翻在奈雪的茶

在瑞幸联名茅台、喜茶联名FENDI、茶百道联名米哈游后&#xff0c;奈雪的茶搭上了周杰伦。9月14日&#xff0c;在《范特西》专辑发行22周年之际&#xff0c;奈雪的茶推出“范特西音乐宇宙”主题的奶茶与周边。 周杰伦系IP加持&#xff0c;奈雪的茶卖爆了&#xff0c;范特西Styl…

淘宝天猫商品全网搜索接口,关键词搜索商品列表数据接口,淘宝API接口申请指南

淘宝搜索接口是一种提供更便捷的淘宝商品搜索服务的工具。通过该接口&#xff0c;用户可以更加快速地找到自己需要的商品&#xff0c;节省时间和精力。 淘宝关键字搜索接口主要用于以下几个方面的业务应用&#xff1a; 商品搜索。用户可以根据关键字搜索他们想要购买的商品。…

生信学院|09月20日《在线焊件建模——xFrame》

课程主题&#xff1a;在线焊件建模——xFrame 课程时间&#xff1a;2023年09月20日 14:00-14:30 主讲人&#xff1a;武旭 生信科技 售后服务工程师 1、3DEXPERIENCE设计平台介绍 2、xFrame设计工具使用 3、Q&A 请安装腾讯会议客户端或APP&#xff0c;微信扫描海报中的…

【深度学习】clip-interrogator clip docker 容器启动过程

文章目录 dockerfile备忘ENTRYPOINT ["bash", "/app/startProject.sh"]常用docker指令web服务脚本访问接口文件 给一张图片&#xff0c;输出图片描述。 dockerfile备忘 只有从dockerfile制作的镜像才有分层结构&#xff0c;加速传输&#xff0c;故第一步…

Linux内核源码分析 (B.2)深入理解 Linux 物理内存管理

Linux内核源码分析 (B.2)深入理解 Linux 物理内存管理 文章目录 Linux内核源码分析 (B.2)深入理解 Linux 物理内存管理[TOC] 1\. 前文回顾2\. 从 CPU 角度看物理内存模型2.1 FLATMEM 平坦内存模型2.2 DISCONTIGMEM 非连续内存模型2.3 SPARSEMEM 稀疏内存模型2.3.1 物理内存热插…

MySQL常见面试题(一)

&#x1f600;前言 在数据库管理系统中&#xff0c;存储引擎起着核心的角色&#xff0c;它决定了数据管理和存储的方式。MySQL作为一个领先的开源关系型数据库管理系统&#xff0c;提供了多种存储引擎来满足不同的需求和优化不同的应用。除了选择合适的存储引擎&#xff0c;数据…

拉格朗日乘子法思路来源

核心思路:由果索因 一. 直观理解 1. 问题描述 对于如"图1"式(等式约束优化问题, 可行域是边界), 转化成拉格朗日乘子法的思路来源: 图1: 拉格朗日乘子法问题描述图 如"图2",f为曲面.c为平面, 黑色加粗线是f和c的交线.(约束就是限制自变量的变化范围). …

Llama2-Chinese项目:2.1-Atom-7B预训练

虽然Llama2的预训练数据相对于第一代LLaMA扩大了一倍&#xff0c;但是中文预训练数据的比例依然非常少&#xff0c;仅占0.13%&#xff0c;这也导致了原始Llama2的中文能力较弱。为了能够提升模型的中文能力&#xff0c;可以采用微调和预训练两种路径&#xff0c;其中&#xff1…