【C++】哈希表的底层逻辑

目录

一、哈希概念

1、哈希冲突

2、哈希冲突的解决

a、闭散列

🟢插入

 🟢查找

🟢删除

🟢其他类型的数据

🟢实现 

b、 开散列

 🟢插入

🟢查找

 🟢删除

🟢析构

🟢其他类型的数据

🟢实现


一、哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素

      根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

  • 搜索元素

       对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称 为哈希表(Hash Table)(或者称散列表)

1、哈希冲突

不同关键字通过相同哈希哈数计算出相同哈希地址,该种现象称为哈希冲突或哈希碰撞 

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

2、哈希冲突的解决

解决哈希冲突两种常见的方法是:闭散列开散列

a、闭散列

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

会使用线性探测的方法。

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

🟢插入

通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

插入就会面临扩容的问题,如果我们按照之前的思路在原来空间上直接扩容,这样size就会改变,每个元素对应的地址位置会发生变化。所以我们直接新建一个哈希表,将旧表的数据遍历插入到新的表中,然后交换新表和旧表。 

插入的数值先求出哈希值,如果对应的哈希值的状态是存在,哈希++,当遇到没有放入值的地方时,将插入的值放入,将状态改为存在,关键字++。

 🟢查找

当状态存在并且 kv 相等时,返回相等的值,如果没有找到,返回空。为了避免删除之后的值被查到到,查找时对数组的状态进行限制。

🟢删除

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

所以我们记录每一个位置的状态:哈希表每个空间给个标记,EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除

enum State{EMPTY, EXIST, DELETE};

找到值将要删除的值的状态设置为DELETE,关键字的个数--,如果没有找到返回 false。

🟢其他类型的数据

当我们想统计字符串或者其他类型的数据时,我们需要找到其对应的地址。之前 int 类型可以直接用key值%capacity 的值 。当处理其他的值时,我们可以使用仿函数将其强制转换为int类型。string不能直接转换为int,我们刻画一个仿函数处理string类型。

🟢实现 
namespace open_address
{
	enum Status
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		Status _s;          //状态
	};

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

			cout << key << ":" << hash << endl;
			return hash;
		}
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);
		}

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

			//负载因子是空间占有率=存储数据的个数/表的大小
			// 负载因子0.7就扩容
			if (_n * 10 / _tables.size() == 7)
			{
				size_t newSize = _tables.size() * 2;
				HashTable<K, V> newHT;
				newHT._tables.resize(newSize);
				// 遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._s == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}

				_tables.swap(newHT._tables);
			}

			Hash hf;
			size_t hashi =hf(kv.first) % _tables.size();
			while (_tables[hashi]._s == EXIST)
			{
				hashi++;

				hashi %= _tables.size();
			}

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

			return true;
		}

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

				hashi++;
				hashi %= _tables.size();
			}

			return NULL;
		}

		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._s == EXIST)
				{
					//printf("[%d]->%d\n", i, _tables[i]._kv.first);
					cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
				}
				else if (_tables[i]._s == EMPTY)
				{
					printf("[%d]->\n", i);
				}
				else
				{
					printf("[%d]->D\n", i);
				}
			}

			cout << endl;
		}

		// 伪删除法
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_s = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}

	private:
		vector<HashData<K,V>> _tables;
		size_t _n = 0; // 存储的关键字的个数
	};

	void TestHT1()
	{
		string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		//HashTable<string, int, HashFuncString> ht;
		HashTable<string, int> ht;
		for (auto& e : arr)
		{
			//auto ret = ht.Find(e);
			HashData<string, int>* ret = ht.Find(e);
			if (ret)
			{
				ret->_kv.second++;
			}
			else
			{
				ht.Insert(make_pair(e, 1));
			}
		}

		ht.Print();

		ht.Insert(make_pair("apple", 1));
		ht.Insert(make_pair("sort", 1));

		ht.Insert(make_pair("abc", 1));
		ht.Insert(make_pair("acb", 1));
		ht.Insert(make_pair("aad", 1));

		ht.Print();
	}
}

b、 开散列

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

 🟢插入

首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,哈希表是一个指针数组,指向第一个关键码。

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

扩容的逻辑:

①当负载因子到达1时就进行扩容
②按照二倍扩容开空间,开辟一个新的哈希表。
③遍历旧表,将旧表上的结点全部拿下来,按照插入逻辑迁到新表上。
④最后遍历完后,将新表和旧表交换一下,这样数据就到旧表上了。如果我们采用相同的方法,要拷贝每一个哈希桶,但是哈希桶的数据都是一样的,这样会造成浪费。

插入的逻辑:

①对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,

②为插入的元素新建一个节点,将元素头插到哈希桶里。

为什么要头插而不是尾插呢?尾插也可以,但是尾插还需要找尾,头插直接插入到哈希表里即可,让新结点的尾部指向原来的新结点,而原来新结点的位置就变成了新结点。

③关键个数n++;

🟢查找

计算数值对应的哈希地址,看该哈希地址上是否有该值。不过由于哈希冲突,可能查找的值在哈希桶头结点的后面。所以需要遍历一下哈希桶。

 🟢删除

删除逻辑:

①首先根据除留余数法计算数值的哈希地址。
②遍历所在的哈希桶,在遍历时,需要记录前面结点的位置。
③找到要删除结点后,删除(让前一个节点的next指向删除节点的next),没有找到就继续往后找,记录前面位置。
③注意要删除结点可能是头结点。

🟢析构

vector不是会自动调用析构函数吗?为什么我们还需要自己写析构函数呢?
vector存储的数据指针类型,是自定义类型,没有默认构造,所以我们开辟的结点的那些空间,需要我们自己释放,不然无法释放。析构需要遍历哈希表,都释放即可。

🟢其他类型的数据

 现在只能存储key为整型的元素,如果遇到其他类型需要怎么处理呢?

这里就需要使用仿函数

对于数值类型我们可以直接强制类型转换成 size_t 类型,这样做的好处就是对于负数我们也可以进行取模了。如果对于string类型,那我们可以将string类型的字符全部相加,这样就会得到一个数值。不过这样可能会存在大量的冲突,可能两个不同的字符串,最后得到的数值是一样大的。所以为了减少冲突。又大佬依据大量的数据处理,得出,每个字符都乘数131后再相加,这样就可以大大减少冲突概率。

我们可以使用一个模板的特化,根据传的模板类型来调用不同的仿函数了。模板的特化,当数据类型是 int 的时候就默认使用 HashFunc<int>,当数据类型是string类型时,就默认使用HashFunc<string>

🟢实现
namespace hash_bucket
{
	template<class K, class V>
	struct HashNode
	{
		HashNode* _next;
		pair<K, V> _kv;

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

	template <class K>
		struct HashFunc//默认的仿函数可以让数据模
		{
			size_t operator()(const K& key)
			{
				return (size_t)key;
				//将key强行类型转化
				//这样作的意义:可以负数也可以进行模了
			}
		};

		//template <class string>
		//struct HashFunc
		//{
		//	size_t operator()(const string& str)
		//	{
		//		//为了减少冲突,我们将字符串的每个字符的值相加
		//		size_t hash = 0;
		//		for (auto& it : str)
		//		{
		//			hash *= 131;
		//			hash += it;
		//		}
		//		return hash;
		//	}
		//};

	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& key)
		{
			// BKDR
			size_t hash = 0;
			for (auto e : key)
			{
				//字母相同,顺序不同
				hash *= 31;
				hash += e;
			}

			cout << key << ":" << hash << endl;
			return hash;
		}
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_tables.resize(10);
		}

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

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

			// 负载因子最大到1时,扩容
			if (_n == _tables.size())
			{
				vector<Node*> newTables;
				newTables.resize(_tables.size() * 2, nullptr);
				// 遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					//扩容后,空间size变大了,有的数据就可能会存到不同的桶里了
				    //拿下来的结点要重新计算放进哪个位置
					Node* cur = _tables[i];
					while (cur)
					{
						//记录cur的下一个节点
						Node* next = cur->_next;
						Hash hf;
						// 挪动到映射的新表
						size_t hashi = hf(kot(cur->data)) % newTables.size();
						cur->_next = newTables[i];
						//头插 这个结点的 接到插入结点的前面对吧
						//那么next就接到newtavle[i]
						newTables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}

			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);

			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return true;
		}

		Node* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(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)
		{
			Hash hf;
			size_t hashi =hf(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					//如果删除的是头结点,指向头结点的next
					if (prev = nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

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

	
}

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

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

相关文章

《霍格沃茨之遗》找不到emp.dll如何修复?分享5种亲测有效的方法

在我们享受电脑游戏带来的乐趣时&#xff0c;偶尔会遇到一些技术上问题&#xff0c;具体来说&#xff0c;当你启动一款游戏&#xff0c;系统却弹出一个提示“由于找不到emp.dll文件&#xff0c;因此无法继续执行代码”&#xff0c;这样的情况确实让人感到扫兴。这究竟是什么原因…

.net core ef 连表查询

Information和TypeInfo连表查询 类似&#xff1a; select st.Title1,si.* from [Star_Information] si left join Star_TypeInfo st on si.typeId2st.id 先在EfCoreDbContext.cs配置 protected override void OnModelCreating(ModelBuilder builder){base.OnModelCreating(b…

Jupyter Notebook 中使用虚拟环境的Python解释器

问题&#xff1a;创建虚拟环境&#xff0c;在pycharm中配置虚拟环境的Python解释器&#xff0c;然后在pycharm中打开ipynb&#xff0c;执行发现缺少包&#xff0c;但是虚拟环境中已经安装了 解决方式&#xff1a; 配置Jupyter Notebook 使用虚拟环境的Python解释器 1&#x…

ElasticSearch总结2

一、创建索引库&#xff1a;PUT ES中通过Restful请求操作索引库、文档。请求内容用DSL语句来表示。创建索引库和mapping的DSL语法如下&#xff1a; 整个jason 里边&#xff0c;它有一个叫mapping的属性&#xff0c;代表的是映射。映射里边有properties代表就是字段。可以看到这…

C++入门系列-缺省参数

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 缺省参数的概念 缺省参数是生命或者定义函数时为函数的参数指定一个缺省值&#xff0c;在调用该函数时&#xff0c;如果没有指定实参则采用该形参的缺省值&#xff0c;否则使用…

一单利润100+,不起眼的小生意,却能闷声发财!

今天&#xff0c;我想向大家介绍一个看似不太热门&#xff0c;但实际上需求很高的项目——酒店代订。这个项目其实很早以前就已经有人开始尝试了&#xff0c;但可能并没有被大众所熟知。简而言之&#xff0c;酒店代订就是帮助他人通过我们来预订他们想要入住的酒店。 当客户将…

ThinkPHP5 SQL注入漏洞敏感信息泄露漏洞

1 漏洞介绍 ThinkPHP是在中国使用极为广泛的PHP开发框架。在其版本5.0&#xff08;<5.1.23&#xff09;中,开启debug模式&#xff0c;传入的某参数在绑定编译指令的时候又没有安全处理&#xff0c;预编译的时候导致SQL异常报错。然而thinkphp5默认开启debug模式&#xff0c…

Allegro如何将铜皮复制到其他层

如何将铜皮复制到其他层 方法一&#xff1a; 第一步&#xff1a;选择需要复制的铜皮&#xff0c;然后按下CtrlC 第二步&#xff1a;选择要粘贴到的层面&#xff0c;然后按Ctrl V 将在所在层面创建一个新的铜皮&#xff0c;几何形状与原铜皮完全一样 方法二&#xff1a; 第一…

基于SSM的个人博客系统(二)

目录 第四章 系统设计 4.1 系统总流程 4.2 博主用例 4.3 游客用例 4.4 系统类 一、博客类 二、博客类型类 三&#xff0c;评论类&#xff1a; 四&#xff0e;友情链接类 4.5 E-R图 4.6 系统表设计 前面内容请移步 基于SSM的个人博客系统&#xff08;一&#xff09;…

docker mysql更新升级版本

一、环境说明 操作系统&#xff1a;Centos7 数据库版本&#xff1a;MySql 8.0.22 数据库中数据量不大&#xff0c;处于开发/测试环境&#xff0c;风险较低 二、升级原因 升级是因为测评漏洞&#xff0c;在进行国家三级等级保护测评过程中&#xff0c;漏扫发现多个MySql漏洞…

(十五)Servlet教程——Servlet文件上传

JSP和HTML标签一起使用&#xff0c;来允许用户把文件上传到服务器。 首先我们需要创建一个前端界面&#xff0c;创建上传文件表单时&#xff0c;需要注意以下几点&#xff1a; (1) 表单的method属性必须设置为POST方法&#xff0c; 不能使用GET方法。 (2) 表单enctype属性应该…

【Unity面试篇】Unity 面试题总结甄选 |Unity基础篇 | ❤️持续更新❤️

2.2 前言 关于Unity面试题相关的所有知识点&#xff1a;&#x1f431;‍&#x1f3cd;2023年Unity面试题大全&#xff0c;共十万字面试题总结【收藏一篇足够面试&#xff0c;持续更新】为了方便大家可以重点复习某个模块&#xff0c;所以将各方面的知识点进行了拆分并更新整理…

19 做好微服务间依赖的治理和分布式事务

在前两讲里&#xff0c;分别从微服务的对外接口、消息消费以及微服务自身的相关编码规范上阐述了“防备上游、做好自己”这两个准则如何落地。 在本讲里&#xff0c;将会讲解为什么要“怀疑下游”&#xff0c;以及有哪些手段可以落地此条准则。此外&#xff0c;还会介绍在进行…

基于SSM的个人博客系统(三)

目录 第五章 系统实现 5.1 登录模块 5.1.1 博主登录 5.2 博客管理模块&#xff1a; 5.2.1 博客查询 5.2.2 博客新建 5.2.3 博客修改 5.2.4 博客删除 5.3 博客类别管理模块 前面内容请移步 基于SSM的个人博客系统&#xff08;二&#xff09; 个人博客系统的设计…

Qt+Ubuntu20.04:打包qt

打包程序 参考 qt项目在Linux平台上面发布成可执行程序.run_qt.run不是虚拟机的配置文件-CSDN博客 Linux下Qt程序的打包发布(1)-不使用第三方工具 - 知乎 (zhihu.com) 过程 1、Release编译 先将你的程序在release下编译通过&#xff0c;保证下面打包的程序是你最新的。 2…

沐风老师3DMAX一键生成桌子插件TableMaker使用方法

3DMAX一键生成桌子插件TableMaker使用教程 3DMAX一键生成桌子插件TableMaker&#xff0c;参数化方式快速创建各种样式桌子模型。 【适用版本】 3dMax2011-2025&#xff08;不仅限于此范围&#xff09; 【安装方法】 3DMAX一键生成桌子插件无需安装&#xff0c;使用时直接拖动…

GCB | 陆地生态系统C:N:P化学计量对降水变化的响应

西北农林科技大学水保学院上官周平研究员团队在陆地生态系统C:N:P化学计量对降水变化的响应方面取得新进展&#xff0c;并以“C:N:P stoichiometry of plants, soils, and microorganisms: Response to altered precipitation”为题发表在国际生态环境领域著名期刊Global Chang…

SpringBoot之自定义注解参数校验

SpringBoot之自定义注解参数校验 为什么要自定义注解 我这里先引入一个例子&#xff0c;就比如我现在要写文章&#xff0c;文章也许写完正要发布&#xff0c;也可以是还没写完正要存草稿&#xff0c;前端往后端发送数据&#xff0c;如果前端的state不是草稿或者已发布状态&…

C语言中的趣味代码(五)

我想以此篇结束关于C语言的博客&#xff0c;因为在C语言拖得越久越不能给大家带来新的创作&#xff0c;在此我也相信大家对C语言已经有了一个新的认知。进入正题&#xff0c;在这一篇中我主要编一个“英语单词练习小程序”来给大家展开介绍&#xff0c;从测试版逐步改良&#x…

C语言(操作符)1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…