首页 > 编程语言 >哈希算法(闭散列) - 线性探测 / 二次探测(缺支持string数据插入)

哈希算法(闭散列) - 线性探测 / 二次探测(缺支持string数据插入)

时间:2024-11-01 20:51:59浏览次数:3  
标签:tables string hashi 探测 Exist kv 哈希 ._ size

一.哈希初步

1.哈希的思想

哈希算法的思想是将要存储的顺序按照一定规律进行存储,查询时也依据此规律进行查询

相对于string字符串,会选择开辟一个大小为26的数组,将字母(仅小写)按照Ascall码表进行映射,统计其出现的次数
相对于没有规律的数据而言,常采用取模的方法( % 数组大小),做到一一映射的关系

哈希算法的思想是编程中极为重要的思想,可以避免长字符串的匹配,根据映射关系能够快速排除部分(布隆过滤器),要深刻理解哈希的思想和实现

二.哈希模拟实现

1.闭散列

储存大量数据时采用哈希的思想,此应用的方法是开辟一个数组,让不同的数据不断模数组的大小(nums.size()),对应的位置存储值,此方法也叫开放定值法(闭散列),此方法的弊端是会发生哈希冲突,所以我们采用线性探测和二次探测两种形式

实现方法:三种存在状态:Delet / Exist / Empty,若数据进入后查到此数组位为Empty / Delete,则直接插入数据,相反,若查到Exist,则会按照线性 / 二次向下寻找,若超过数组的size,则hashi % _nums.size()

重点:Delete的存在非常重要!!!,在查询数据时遇到Empty直接停止,遇到Delete就继续向后查询,若没有Delete这个标识位会导致删除数据后,被删除数据后面的数据全部丢失

2.状态表示 / 存储数据表示

状态:Empty / Delete / Exist

数据采用pair<K, V>,key为标识位,val为标识位对应的值,所以HashData<K, V> HTData,HTData有两个标识,一个为_kv,_s

// 枚举状态
	enum Status
	{
		Empty,
		Exist,
		Delete
	};
// 一个模板对应一个结构体
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		Status _s;
	};

3.class类中的成员变量

private成员变量有 vector<HashData<K, V>> _tables,开辟一个数组,数组中存储的类型是HashData,用HashTable<K, V>创建一个 HT,HT可以调用_tables,_tables[hashi]中对应的值又是HTData类型的,可以调用_kv,_s

_n:负载因子,当数组中数据达到70%时就进行扩容

4.线性探测

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

程序思想提升:1.if / else双条件判断 - while(单条件卡死) / 2.复用的思想:空间不够需要扩容时,先扩容,等到负载因子 < 0.7时,复用原程序插入的逻辑,大于0.7就扩容在复用小于0.7时的程序,小于0.7就直接插入

swap的思想,若扩容就创建一个新的数组,然后原数组的数据重新插入(剩余数据也全部插入)后进行交换,原数组变为新数组,新创的数组出了作用域自动调用析构函数

bool Insert_Linear(const pair<K, V>& kv)
{
	//size_t hashi = kv.first % _tables.size();
	//if (_tables[hashi]._s == Empty || _tables[hashi]._s == Delete)
	//{
	//	_tables[hashi]._kv = kv;
	//	_tables[hashi]._s = Exist;
	//		return true;
	//}

	//else
	//{
	//	while (_tables[hashi]._s == Exist)
	//	{
	//		++hashi;
	//		if (hashi >= _tables.size())
	//		{
	//			hash %= _tables.size();
	//		}
	//	}
	//	_tables[hashi]._kv = kv;
	//	_tables[hashi]._s = Exist;
	//	return true;
	//}
	
	// 优化
	//两种情况:1. 此位的状态是空 / 删除
	//			2. 此位状态是已被占

	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_Linear(_tables[i]._kv);
		}

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

	size_t hashi = kv.first % _tables.size();

	while (_tables[hashi]._s == Exist)
	{
		++hashi;
		//if (hashi >= _tables.size())
		//{
		//	hashi %= _tables.size();
		//}

		// 简化
		hashi %= _tables.size();
	}

	_tables[hashi]._kv = kv;
	_tables[hashi]._s = Exist;
	++_n;
	
	return true;
}

5.二次探测

当出现哈希冲突时,分别找寻1 / 4 / 9(pow(++i, 2))(i = 1)等位置,避免线性探测在某一个地点出现大量冲突,二次探测使冲突更加分散(思想较线性探测近乎相同)

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

	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_Twice(_tables[i]._kv);
		}
					
		newHt._tables.swap(_tables);
	}

	size_t hashi = kv.first % _tables.size();
	size_t hashtwice = 0;

	while (_tables[hashi]._s == Exist)
	{
		hashi = kv.first % _tables.size();
		//++hashtwice; // 优化
		// Warning: +=: 从double转换到size_t,可能丢失数据
		hashi += size_t(pow(++hashtwice, 2));
		hashi %= _tables.size();
	}
			
	_tables[hashi]._kv = kv;
	_tables[hashi]._s = Exist;
	++_n;

	return true;
}

6.Find查询函数

Delete状态存在的意义:如果没有Delete状态,会导致Empty后面的数据均无法访问

易错点:查询的if判断条件中要判断状态是否存在,因为程序中的删除用的是尾删除法,仅改变了此位的状态为Delete,不判断Exist的状态会将Delete误判成存在

HashData<K, V>* Find(const K& k)
{
	size_t hashi = k % _tables.size();

	while (_tables[hashi]._s != Empty)
	{
		// 易错点:如果此值是已经删除的值,那么也会被当作是存在的值,则被删除的值也会被Find,无法重新插入
		// 但插入值是只要Find不到就可以插入,所以会无法插入目标值,或把目标值覆盖
		// if (_tables[hashi]._kv.first == k)
		if(_tables[hashi]._s == Exist
		&& _tables[hashi]._kv.first == k)
		{
			return &_tables[hashi];
		}

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

	return nullptr;
}

7.删除Erase(伪删除法)

伪删除法,不删除位上的值,仅改变相应位的状态

bool Erase(const K& k)
{
	HashData<K, V>* ret = Find(k);
			
	if (ret)
	{
		ret->_s = Delete;
		--_n; // 负载因子
		return true;
	}

	return false;
}

三.哈希模拟实现完整代码展示

闭散列 :线性探测 / 二次探测(open_adress是开放定址法(闭散列的别名))

不足:1.无法支持数据为string(需要加仿函数(结构体 + 函数重载))
2.闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷(链式结构(开散列))

重要思想:1.代码优化 / 2.函数自身复用 / 3.Swap的思想

namespace open_adress
{
	enum Status
	{
		Empty,
		Exist,
		Delete
	};

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

	//一个结构体 / 一个类对应一个模板
	template<class K, class V>
	class HashTable
	{
	public:
		// 构造函数
		HashTable()
		{
			_tables.resize(10);
		}

		// 线性探测
		bool Insert_Linear(const pair<K, V>& kv)
		{
			//size_t hashi = kv.first % _tables.size();
			//if (_tables[hashi]._s == Empty || _tables[hashi]._s == Delete)
			//{
			//	_tables[hashi]._kv = kv;
			//	_tables[hashi]._s = Exist;
			//		return true;
			//}

			//else
			//{
			//	while (_tables[hashi]._s == Exist)
			//	{
			//		++hashi;
			//		if (hashi >= _tables.size())
			//		{
			//			hash %= _tables.size();
			//		}
			//	}
			//	_tables[hashi]._kv = kv;
			//	_tables[hashi]._s = Exist;
			//	return true;
			//}

			// 优化
			//两种情况:1. 此位的状态是空 / 删除
			//			2. 此位状态是已被占

			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_Linear(_tables[i]._kv);
				}

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

			size_t hashi = kv.first % _tables.size();

			while (_tables[hashi]._s == Exist)
			{
				++hashi;
				//if (hashi >= _tables.size())
				//{
				//	hashi %= _tables.size();
				//}

				// 简化
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._s = Exist;
			++_n;
			return true;
		}

		// 二次探测
		bool Insert_Twice(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			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_Twice(_tables[i]._kv);
				}
					
				newHt._tables.swap(_tables);
			}

			size_t hashi = kv.first % _tables.size();
			size_t hashtwice = 0;

			while (_tables[hashi]._s == Exist)
			{
				hashi = kv.first % _tables.size();
				//++hashtwice; // 优化
				// Warning: +=: 从double转换到size_t,可能丢失数据
				hashi += size_t(pow(++hashtwice, 2));
				hashi %= _tables.size();
			}
			
			_tables[hashi]._kv = kv;
			_tables[hashi]._s = Exist;
			++_n;

			return true;
		}

		// Delete状态存在的意义:如果没有Delete状态,会导致Empty后面的数据均无法访问
		HashData<K, V>* Find(const K& k)
		{
			size_t hashi = k % _tables.size();

			while (_tables[hashi]._s != Empty)
			{
				// 易错点:如果此值是已经删除的值,那么也会被当作是存在的值,则被删除的值也会被Find,无法重新插入
				// 但插入值是只要Find不到就可以插入,所以会无法插入目标值,或把目标值覆盖
				// if (_tables[hashi]._kv.first == k)
				if(_tables[hashi]._s == Exist
				&& _tables[hashi]._kv.first == k)
				{
					return &_tables[hashi];
				}

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

			return nullptr;
		}

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

			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i]._s == Exist)
				{
					cout << "[" << i << "]-> " << _tables[i]._kv.first << endl;
				}

				else if (_tables[i]._s == Empty)
				{
					cout << "[" << i << "]-> " << "Empty" << endl;
				}

				else
				{
					cout << "[" << i << "]-> " << "Delete" << endl;
				}
			}

			cout << endl;
		}


	private:
		vector<HashData<K, V>> _tables;
		size_t _n;
	};

四.哈希模拟实现测试代码

闭散列 - 线性探测 / 二次探测插入数据测试(是否能正常对应位 / 是否能正常扩容等)- Print用于测试

// 线性探测测试
void TestHashLinear()
{
	HashTable<int, int> ht;

	int a[] = { 4,14,24,34,5,7,1,16,28,39,3,7 };
		
	for (auto e : a)
	{
		ht.Insert_Linear(make_pair(e, e));
	}

	ht.Erase(4);
	//ht.Print();
	ht.Insert_Linear(make_pair(4, 4));
	// 插入数据的方式:make_pair(key, value)
	ht.Insert_Linear(make_pair(44, 44));
	ht.Print();
}
// 二次探测测试
void TestHashTwice()
{
	HashTable<int, int> ht;

	// 分别占4号位 / 5号位 / 8号位
	//ht.Insert_Twice(make_pair(4, 4));
	//ht.Insert_Twice(make_pair(14, 14));
	//ht.Insert_Twice(make_pair(24, 24));

	int a[] = { 4,14,24,34,5,7,1,16,28,39,3,7 };

	for (auto e : a)
	{
		ht.Insert_Twice(make_pair(e, e));
	}

	ht.Erase(4);
	//ht.Print();
	ht.Insert_Twice(make_pair(4, 4));
	ht.Insert_Twice(make_pair(44, 44));
	ht.Print();
}

End!!!

标签:tables,string,hashi,探测,Exist,kv,哈希,._,size
From: https://blog.csdn.net/ZCDL1314/article/details/143439587

相关文章

  • 【C++】string 类模拟实现:深入探索字符串操作原理
     快来参与讨论......
  • Java 传参时,如何做到两个 String 实参的实际值交换_3
    ###Java传参时,如何做到两个String实参的实际值交换在Java中,所有的参数传递都是值传递,这意味着方法接收的是实参值的一个副本。对于基本数据类型,这个副本是实际值;对于对象,副本是引用的一个拷贝。因此,直接在方法内部交换两个`String`实参的实际值是不可能的。然而,可以通过一......
  • C语言数据结构之哈希表(HASHTABLE)的实现
    C语言数据结构之哈希表(HASHTABLE)的实现哈希表的每个节点保存的数据格式为key:value,其中key为字符串,根据字符串内容采用不同方法(哈希函数)生成一个无符号整型哈希码,根据表的长度,采用取余法,将数据存入表单元,如果此表单元中已存在数据,则以此表单元为链表头,向链表追加数据,这......
  • String、StringBuffer和StringBuilder的区别
    String、StringBuffer和StringBuilder的区别  下面从可变性、是否线程安全等方面来对String、StringBuffer、StringBuilder进行比较。  一、可变性  1.String  String类中使用final关键字修饰字符数组来保存字符串。publicfinalclassStringimplementsja......
  • 哈希表
    #include<stdio.h>#include<iostream>#include<algorithm>#include<vector>#include<random>#include<time.h>#include<ext/pb_ds/assoc_container.hpp>#include<ext/pb_ds/hash_policy.hpp>#include<unordered_m......
  • 用哈希表封装myunordered_map和myunordered_set
    在学习这个之前,已经学习过,myunordered_map和myunordered_set的基本用法和哈希表怎么用哈希思想模拟实现;因此为了更深入的了解myunordered_map和myunordered_set与哈希表的内容,我们来自己用哈希表模拟实现myunordered_map和myunordered_set;这种模拟实现和之前模拟实现map与set......
  • string类的深度剖析1
    文章目录1.前置语法知识——auto和范围for1.1auto关键字1.2范围for2.string类2.1为什么要学string类2.2认识string类2.3string类的常用接口说明2.3.1常见构造2.3.2容量操作2.3.3访问及遍历操作2.3.4修改操作2.3.5非成员函数3.结尾1.前置语法知识——au......
  • C++(std::to_string())
    目录1.函数定义2.示例代码3.内部实现机制4.注意事项5.应用场景6.使用std::ostringstream控制精度的示例7.总结std::to_string()是C++11引入的一个标准库函数,用于将基本数据类型(如整数、浮点数等)转换为对应的字符串格式。这个函数属于<string>头文件,因此使用时需......
  • 两数之和到哈希表
    两数之和给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。你可以按任意顺序返回答案。示例1:输入:nums=[2,7,11,......
  • c++ string 识别标志位并解析标志位后面的字符
    解析字符串中的固定标志位正则表达式和iterator的配合应用#include<string>#include<map>#include<regex>#include<iostream>//替换\\M+后面的字符//\\M+195B6替换为文std::regexpattern(R"(\\M+[^\\M]*)");//匹配\\M+后跟任意非\\M的字符(0次或多次)......