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

一.哈希初步

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!!!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值