首页 > 其他分享 >数据结构——哈希

数据结构——哈希

时间:2024-11-21 09:13:45浏览次数:3  
标签:tables cur index ht 哈希 数据结构 size

目录

一.哈希的相关概念

二.哈希函数

三.哈希冲突解决

1.闭散列

1. 线性探测

2.二次探测

2.开散列

1.开散列的增容

2.开散列的插入

3.开散列的查找

4.开散列的删除

四.整体代码

1.HashTable.h

2.Hash.cpp


一.哈希的相关概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O(log2N),搜索的效率取决于搜索过程中元素的比较次数

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素

当向该结构中:

  • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

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

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

44插入的位置和4冲突了,我们将这种情况称为哈希冲突

哈希冲突

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

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

发生哈希冲突该如何处理呢?

二.哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则:

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

常见哈希函数:

1. 直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

2. 除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数, 按照哈希函数:Hash(key) = key% p(p将关键码转换成哈希地址)

3. 平方取中法

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址

平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4. 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5. 随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。

通常应用于关键字长度不等时采用此法

6.数学分析法

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的 若干位分布较均匀的情况

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

三.哈希冲突解决

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

1.闭散列

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

1. 线性探测

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

//线性探测
//计算d中的key在表中映射的位置
size_t index = koft(d) % _tables.size();
while (_tables[index]._state == EXITS)
{
	if (koft(_tables[index]._data) == koft(d))
	{
		return false;
	}

	++index;
	if (index == _tables.size())
	{
		index = 0;
	}
}
_tables[index]._data = d;
_tables[index]._state = EXITS;
_num++;

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

enum State
{
	EMPTY,
	EXITS,
	DELETE,
};


bool Erase(const K& key)
{
	HashData* ret = Find(key);
	if (ret)
	{
		ret->_state = DELETE;
		--_num;
		return true;
	}
	else
	{
		return false;
	}
}

思考:哈希表什么情况下进行扩容?如何扩容?

我们通过负载因子来决定什么情况下增容,负载因子 = 表中数据/表的大小 -->衡量哈希表满的程度

表越接近满,插入数据越容易冲突,冲突越多,效率越低,哈希表并不是满了增容,在开放定址法中,一般负载因子到0.7左右就开始增容,负载因子越小,冲突概率越低,整体效率越高,但是浪费空间越大,所以负载因子一般取一个折中值

	KeyOfT koft;
	if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
	{
		//开2倍的新表
		//遍历旧表的数据,将旧表的数据在新表中重新映射
		//释放旧表
		vector<HashData> newtables;
		size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		newtables.resize(newsize);
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			if (_tables[i]._state == EXITS)
			{
				size_t index = koft(_tables[i]._data) % newtables.size();
				while (newtables[index]._state == EXITS)
				{
					++index;
					if (index == _tables.size())
					{
						index = 0;
					}
				}
				newtables[index] = _tables[i];
			}
		}
		_tables.swap(newtables);
	}

我们也可以通过复用insert来实现

	HashTable<K, T, KeyOfT> newht;
	size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
	newht._tables.resize(newsize);
	for (size_t i = 0; i < _tables.size(); ++i)
	{
		if (_tables[i]._state == EXITS)
		{
			newht.Insert(_tables[i]._data);
		}
	}
	_tables.swap(newht._tables);

线性探测优点:实现非常简单

线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?

2.二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法 为:H_i = (H_0 + i^2 )% m.其中:i = 1,2,3…, H_0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小

	//二次探测
	size_t start = koft(d) % _tables.size();
	size_t index = start;
	int i = 1;
	while (_tables[index]._state == EXITS)
	{
		if (koft(_tables[index]._data) == koft(d))
		{
			return false;
		}

		index = start + i * i;
		++i;
		index %= _tables.size();
	}
	_tables[index]._data = d;
	_tables[index]._state = EXITS;
	_num++;

	return true;
	void TestHashTables()
	{
		HashTable<int, int, SetKeyOfT<int>> ht;
		ht.Insert(2);
		ht.Insert(12);
		ht.Insert(4);
		ht.Insert(14);
		ht.Insert(24);
		ht.Insert(1);
		ht.Insert(5);
		ht.Insert(6);
	}

线性探测

二次探测

通过图片可以看出他们之间插入位置的不同

2.开散列

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

1.开散列的增容

我们通过负载因子来判断什么时候增容,当负载因子为1的时候增容

	//如果负载因子等于1就增容,避免大量的哈希冲突
	if ( _tables.size() == _num)
	{
		vector<Node*> newtables;
		size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		newtables.resize(newsize);
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				size_t index = HashFunc(koft(cur->_data)) % newtables.size();
				cur->_next = newtables[index];
				newtables[index] = cur;

				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newtables);
	}

因为我们计算映射的位置时是用数据%表的大小,但是有时候我们会传入string类似的类型,他们不可以直接%,所以我们需要一个仿函数来处理这些不能直接%的类型

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

template<>
struct _Hash<string>
{
	size_t operator()(const string& key)
	{
		//BKDR Hash
		size_t hash = 0;
		for (size_t i = 0; i < key.size(); ++i)
		{
			hash *= 131;
			hash += key[i];
		}
		return hash;
	}
};

size_t HashFunc(const K& key)
{
	Hash hash;
	return hash(key);
}

我们特化了一个处理string类型的模板,当传入string的时候就会调用特化的模板,其余情况使用默认的

2.开散列的插入

	size_t index = HashFunc(koft(data)) % _tables.size();
	//查找值在不在表中
	Node* cur = _tables[index];
	while (cur)
	{
		if (koft(cur->_data) == koft(data))
		{
			return false;
		}
		else
		{
			cur = cur->_next;
		}
	}

	//头插(尾插也可以)
	Node* newnode = new Node(data);
	newnode->_next = _tables[index];
	_tables[index] = newnode;

	++_num;
	return true;

3.开散列的查找

Node* Find(const K& key)
{
	KeyOfT koft;
	size_t index = HashFunc(key) % _tables.size();
	Node* cur = _tables[index];
	while (cur)
	{
		if (koft(cur->_data) == key)
		{
			return cur;
		}
		else
		{
			cur = cur->_next;
		}
	}
	return nullptr;
}

4.开散列的删除

bool Erase(const K& key)
{
	KeyOfT koft;
	size_t index = HashFunc(key) % _tables.size();
	Node* prev = nullptr;
	Node* cur = _tables[index];
	while (cur)
	{
		if (koft(cur->_data) == key)
		{
			if (prev == nullptr)
			{
				_tables[index] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;

			return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
		}
	}
	return false;
}
void TestHashTables1()
{
	HashTable<int, int, SetKeyOfT<int>> ht;
	ht.Insert(2);
	ht.Insert(12);
	ht.Insert(4);
	ht.Insert(14);
	ht.Insert(24);
	ht.Insert(1);
	ht.Insert(5);
	ht.Insert(6);
	ht.Insert(16);
	ht.Insert(26);
	ht.Insert(36);
}

void TestHashTables2()
{
	HashTable<string, string, SetKeyOfT<string>> ht;
	ht.Insert("sort");
	ht.Insert("vector");
	ht.Insert("string");
}

我们可以看到冲突的数据排列在一个链表中

即便传的是string也能够正常处理

四.整体代码

1.HashTable.h

#pragma once
#include<vector>

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

template<class K, class V>
struct MapKeyOfT
{
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
};

namespace close_hash
{
	enum State
	{
		EMPTY,
		EXITS,
		DELETE,
	};

	template<class T>
	struct HashData
	{
		T _data;
		State _state;
	};

	//unordered_set --> HashTable<K, K>
	//unordered_map --> HashTable<K, pair<K, V>>
	template<class K, class T, class KeyOfT>
	class HashTable
	{
		typedef HashData<T> HashData;
	public:
		bool Insert(const T& d)
		{
			//负载因子 = 表中数据/表的大小 -->衡量哈希表满的程度
			//表越接近满,插入数据越容易冲突,冲突越多,效率越低
			//哈希表并不是满了增容,在开放定址法中,一般负载因子到0.7左右就开始增容
			//负载因子越小,冲突概率越低,整体效率越高,但是浪费空间越大
			//所以负载因子一般取一个折中值
			KeyOfT koft;
			if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
			{
				//开2倍的新表
				//遍历旧表的数据,将旧表的数据在新表中重新映射
				//释放旧表
				/*vector<HashData> newtables;
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				newtables.resize(newsize);
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					if (_tables[i]._state == EXITS)
					{
						size_t index = koft(_tables[i]._data) % newtables.size();
						while (newtables[index]._state == EXITS)
						{
							++index;
							if (index == _tables.size())
							{
								index = 0;
							}
						}
						newtables[index] = _tables[i];
					}
				}
				_tables.swap(newtables);*/
				HashTable<K, T, KeyOfT> newht;
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				newht._tables.resize(newsize);
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					if (_tables[i]._state == EXITS)
					{
						newht.Insert(_tables[i]._data);
					}
				}
				_tables.swap(newht._tables);
			}

			//线性探测
			//计算d中的key在表中映射的位置
			/*size_t index = koft(d) % _tables.size();
			while (_tables[index]._state == EXITS)
			{
				if (koft(_tables[index]._data) == koft(d))
				{
					return false;
				}

				++index;
				if (index == _tables.size())
				{
					index = 0;
				}
			}
			_tables[index]._data = d;
			_tables[index]._state = EXITS;
			_num++;*/

			//二次探测
			size_t start = koft(d) % _tables.size();
			size_t index = start;
			int i = 1;
			while (_tables[index]._state == EXITS)
			{
				if (koft(_tables[index]._data) == koft(d))
				{
					return false;
				}

				index = start + i * i;
				++i;
				index %= _tables.size();
			}
			_tables[index]._data = d;
			_tables[index]._state = EXITS;
			_num++;

			return true;
		}

		HashData* Find(const K& key)
		{
			KeyOfT koft;
			//计算key在表中映射的位置
			size_t index = koft(key) % _tables.size();
			while (_tables[index]._state != EMPTY)
			{
				if (koft(_tables[index]._data) == key)
				{
					if (_tables[index]._state == EXITS)
					{
						return &_tables[index];
					}
					else if (_tables[index]._state == DELETE)
					{
						return nullptr;
					}
				}

				++index;
				if (index == _tables.size())
				{
					index = 0;
				}

				return nullptr;
			}
		}

		bool Erase(const K& key)
		{
			HashData* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_num;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		vector<HashData> _tables;
		size_t _num = 0;//存了几个有效数据
	};

	void TestHashTables()
	{
		HashTable<int, int, SetKeyOfT<int>> ht;
		ht.Insert(2);
		ht.Insert(12);
		ht.Insert(4);
		ht.Insert(14);
		ht.Insert(24);
		ht.Insert(1);
		ht.Insert(5);
		ht.Insert(6);
	}
}

namespace open_hash
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		HashNode(const T& data) :_data(data), _next(nullptr) { }
	};

	template<class K,class T,class KeyOfT>
	struct __HashTableIterator
	{
		typedef HashNode<T> Node;
		Node* _node;
	};

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

	template<>
	struct _Hash<string>
	{
		size_t operator()(const string& key)
		{
			//BKDR Hash
			size_t hash = 0;
			for (size_t i = 0; i < key.size(); ++i)
			{
				hash *= 131;
				hash += key[i];
			}
			return hash;
		}
	};

	template<class K, class T, class KeyOfT, class Hash=_Hash<K>>
	class HashTable
	{
		typedef HashNode<T> Node;
	public:
		~HashTable()
		{
			Clear();
		}

		void Clear()
		{
			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;
			}
		}

		size_t HashFunc(const K& key)
		{
			Hash hash;
			return hash(key);
		}

		bool Insert(const T& data)
		{
			KeyOfT koft;
			//如果负载因子等于1就增容,避免大量的哈希冲突
			if ( _tables.size() == _num)
			{
				vector<Node*> newtables;
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				newtables.resize(newsize);
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t index = HashFunc(koft(cur->_data)) % newtables.size();
						cur->_next = newtables[index];
						newtables[index] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newtables);
			}

			size_t index = HashFunc(koft(data)) % _tables.size();
			//查找值在不在表中
			Node* cur = _tables[index];
			while (cur)
			{
				if (koft(cur->_data) == koft(data))
				{
					return false;
				}
				else
				{
					cur = cur->_next;
				}
			}

			//头插(尾插也可以)
			Node* newnode = new Node(data);
			newnode->_next = _tables[index];
			_tables[index] = newnode;

			++_num;
			return true;
		}

		Node* Find(const K& key)
		{
			KeyOfT koft;
			size_t index = HashFunc(key) % _tables.size();
			Node* cur = _tables[index];
			while (cur)
			{
				if (koft(cur->_data) == key)
				{
					return cur;
				}
				else
				{
					cur = cur->_next;
				}
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			KeyOfT koft;
			size_t index = HashFunc(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[index];
			while (cur)
			{
				if (koft(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						_tables[index] = 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 _num = 0;
	};

	void TestHashTables1()
	{
		HashTable<int, int, SetKeyOfT<int>> ht;
		ht.Insert(2);
		ht.Insert(12);
		ht.Insert(4);
		ht.Insert(14);
		ht.Insert(24);
		ht.Insert(1);
		ht.Insert(5);
		ht.Insert(6);
		ht.Insert(16);
		ht.Insert(26);
		ht.Insert(36);
	}

	void TestHashTables2()
	{
		HashTable<string, string, SetKeyOfT<string>> ht;
		ht.Insert("sort");
		ht.Insert("vector");
		ht.Insert("string");
	}
}

2.Hash.cpp

#include<iostream>
using namespace std;
#include"HashTable.h"
int main()
{
	close_hash::TestHashTables();
	open_hash::TestHashTables1();
	open_hash::TestHashTables2();
	return 0;
}

标签:tables,cur,index,ht,哈希,数据结构,size
From: https://blog.csdn.net/w200514/article/details/143927018

相关文章

  • NFLS贪心与数据结构题单笔记(未完结)
    A.奶牛飞车贪心,把最慢的放前面#include<bits/stdc++.h>usingnamespacestd;constexprintmaxn=1e6+10;intn,m,d,L;ints[maxn];intans=0;inlineboolcmp(intx,inty){returnx>y;}intmain(){cin>>n>>m>>d>......
  • 数据结构在二叉树中用子问题思路来解决问题
    二叉树Oj题获取二叉树的节点数获取二叉树的终端节点个数获取k层节点的个数获取二叉树的高度检测为value的元素是否存在判断两颗树是否相同判断是否是另一棵的子树反转二叉树判断一颗二叉树是否是平衡二叉树时间复杂度O(n*n)复杂度O(N)二叉树的遍历判断是否是对称的二叉......
  • 【数据结构】栈和队列的定义与实现
    主页:HABUO......
  • 【数据结构OJ】【图论】货币套汇(图路径)
    题目描述套汇是指利用货币汇兑率的差异将一个单位的某种货币转换为大于一个单位的同种货币。例如,假定1美元可以买0.7英镑,1英镑可以买9.5法郎,1法郎可以买到0.16美元。通过货币兑换,一个商人可以从1美元开始买入,得到0.7×9.5×0.16=1.064美元,从而获得6.4%的利润。给定n种......
  • 哈希表、map、unordered_map
    目录哈希函数哈希冲突解决哈希冲突的办法1.线性探测再散列2.再哈希法3.链地址法4.建立一个公共溢出区map与unordered_map的区别底层实现原理元素查找效率插入和删除操作效率内存占用情况元素遍历顺序unordered_map:​编辑使用场景哈希表(HashTable,也叫散列表......
  • 数据结构/第五章 树与二叉树/数据结构习题/树与二叉树的习题/考研/期末复习
    一、选择题1.一棵树中,所有结点的度数之和为n,则该树共有(    )个结点。A.n-1   B.n   C.n+1   D.无法确定2.高度为4的3叉树至多有(    )个结点。A.6   B.27   C.40   D.803.度为m的树中第6层至多有(    ......
  • 【数据结构】`unordered_map` 和 `unordered_set` 的底层原理
    unordered_map和unordered_set是C++标准库中的两个容器,它们被广泛应用于需要快速查找的场景中。它们的查找、插入和删除的平均时间复杂度都是O(1),这也是它们的一个重要特性。本文将详细介绍unordered_map和unordered_set的底层原理,帮助计算机专业的小白理解什么是......
  • 数据结构复习 ---- 顺序表(数组)--定长版本+不定长版本
    //我的思考************////1.顺序表是一种线性结构(一对一关系),每个数据都是有一个前驱(除了第一个元素)和一个后继(除了最后一个元素)//2.顺序表分为定长顺序表(指针存储固定数量的元素)和不定长顺序表(顾名思义。。。使用较多)----类似于动态数组,就像Go语言中的切片,Pytho......
  • 2024/11/19日 日志 数据结构实验(2)---栈实现表达式求值、队列应用(蓝桥杯)
    栈实现表达式求值问题:https://pintia.cn/problem-sets/1858366427985383424/exam/problems/type/7?problemSetProblemId=1858366732315615232解答:点击查看代码#include<bits/stdc++.h>usingnamespacestd;//运算符优先级intprecedence(charop){switch(op){......
  • 2024/11/18日 日志 数据结构实验(1)---链表逆置、线性表A,B顺序存储合并、双向循环链表应
    链表逆置题目:https://pintia.cn/problem-sets/1855808612225744896/exam/problems/type/6?problemSetProblemId=1855808768018968576解答:点击查看代码structListNode*reverse(structListNode*head){structListNode*prev=NULL;structListNode*current=head;......