数据结构 5分钟带你搞定哈希表(建议收藏)!!!

对比之前博客讨论的二叉排序树 二叉平衡树 红黑树,它们的查找都是先从根节点进行查找,从节点取出数据或索引与查找值进行比较。那么,有没有一种函数H,根据这个函数和查找关键字key,可以直接确定查找值所在位置,而不需要一个个比较。这样就预先知道key所在的位置,直接找到数据,提升效率

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表

常见的哈希函数

一个哈希函数的好不好,取决于以下三点

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

除留余数法(最常用)
函数:Hash(key)=key MOD p (p<=m m为表长),求出来的hash(key)就为存储该key的下标

例如有一下数据{2, 4, 6, 8, 9}
表长为10,也就是数组容量为10
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第1张图片

直接定制法(常用)
取关键字的某个线性函数为散列地址(A、B为常数):Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
适用场景:适合查找较小数据范围且连续的情况

平方取中法(少)
如果关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数位作为最终存储地址。
使用举例
比如key=1234 1234^2=1522756 取227作hash地址
比如key=4321 4321^2=18671041 取671作hash地址
适用场景:事先不知道数据并且数据长度较小的情况

哈希冲突

即不同的key通过同一哈希函数产生了相同的哈希位置,H(key1)=H(key2),例如我们在除留余数法中的例子,如果此时插入一个12,其hash(12)为2,此时下标为2的位置已经有元素,此时就会产生哈希冲突

处理哈希冲突

解决哈希冲突主要有两个方案:闭散列开散列

闭散列

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

闭散列中主要处理方法有 线性探测二次探测

线性探测

思想:从计算的哈希位置开始,往后找到第一个空闲的位置存放数据
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第2张图片
插入:插入就是计算哈希地址,将数据存放在计算出来的哈希位置上,如果该位置有数据则往后查找第一个空闲位置插入。但是当我们的元素越多时,我们产生的哈希冲突的次数就会越多,

删除:当我们要删除一个元素时,不能物理上直接删除,例如我们把15删除了,此时下标为8的位置为空,当我们要查找25这个元素时,也是会从下标为5这个位置开始查找,当5这个位置不是25时,说明产生了哈希冲突,且该插入是使用的是线性探测,也就是第一个空位置插入。我们往后查找时,如果该数据存在,则在空位置之前一定存在该数。但是此时我们物理上把15删除了。查找会查找到下标为8的位置就结束查找,此时也就不会找到25这个数据了。
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第3张图片
所以使用线性探测方法,删除并不是实际意义上的删除,而是一种伪删除,我们可以定义三种状态,分别是:EMPTYEXISTDELETE。EMPTY表示该位置从来没存放过数据,是一个空位置;EXIST表示该位置存在数据;DELETE表示该位置之前存放过数据,只是已经删除了而已

此时我们想要删除8这个位置上的数据时,就将该位置的状态置为DELETE,我们再次查找25这个数字时,遇到8位置就不会停止搜索,会继续往后搜索,直至遇到状态为EMPTY的位置为止。但是次方法会造成一个问题,就是有可能数据满了,如果此时还一直搜索,就不会找到空的位置,会一直搜索下去。而且如果数据比较极端且数据越来越多,产生的哈希冲突会越来越多。这就不符合我们的哈希要求的高效率的插入与查找。解决办法就是进行扩容

扩容:扩容并不是一定要等到数据满了才扩容。我们知道当数据越来越多,产生哈希冲突的次数就越多,所以我们要设定一个阈值,也就是当数据达到一定的数量时,就有必要进行扩容。而这决定这个阈值的高低的是一个叫负载因子。负载因子 = 实际存放元素 / 数组容量,范围在0~1之间,我们通常将负载因子置为[0.6, 0.8]之间。例如我们数组大小有10个,负载因为为0.7,则当插入第8个元素的时候就需要进行扩容,因为8/10=0.8>.07,也就是大于我们的负载因子就需要进行扩容。扩容的时候要注意,我们需要将原来的数据移动到新的表中,但是如果是单纯的赋值获取,那哈希冲突并没有解决,而此时我们应该将旧表中的数据重新以插入的方式插入到新的表中,从而减少哈希冲突的次数
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第4张图片
查找:哈希的优势之一也就是在于查找,查找的效率是非常高,只要通过key来计算哈希地址,如果计算的哈希位置的值与要查找的key相同,则表示已经找到,如果遇到空之后还没找到表示不存在这个要查找的数。并且要注意的是,如果查找到末尾就需要将查找的索引置为0,从头开始查找

二次探测

二次探测和线性探测都属于闭散列,其原理都一样,两者的主要区别就是探测的方式不同,线性探测是如果要插入的位置已有元素,会一个一个往后查找到新的空位置。而二次探测是通过该位置的哈希冲突次数的平方来向后查找新的位置
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第5张图片
将产生哈希冲突的数据分散,使不堆积在一起。但是这两种方法都有很大的缺陷,就是空间利用率低。在这个基础上,引进一种新的技术来解决哈希冲突----开散列

开散列

开散列方法又叫链地址法,哈希表中存储的是链表的头结点。具有相同的哈希地址会存放在同一链表中,每个链表中的元素都具有相同的哈希地址。
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第6张图片
该哈希表示由指针数组来组成的,每个数组中的元素都是一个链表的头指针。从该表中我们可以看出,产生哈希冲突的元素并不会占用其他元素的位置,每个链表中的元素都是哈希冲突的元素

插入:当我们插入时,计算出哈希地址,就可以直接定位到该key对应的链表的头结点,但是由于不能存放相同的key,我们需要遍历该链表中是否存在相同元素,如果不存在才进行插入。插入时我们可以进行头插或者尾插,这里头插会更简单些,创建key的一个新的结点cur,让该结点指向该链表的头结点,并将该链表的头结点更新为新创建的新结点cur
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第7张图片
增容:开散列的增容并不像闭散列一样要求那么严格,虽然也是有个负载因子,但是这个负载因子可以为1,也就是当有10个元素,负载因子为1时。此时10个元素都没有产生哈希冲突,这效率才是最高的,所以我们没必要限制它的上限。也就是当元素比哈希表的元素大时才需要进行扩容,来减少产生哈希冲突。哈希表中每个元素都是一个链表的头结点,我们可以创建新的一个指针数组,遍历旧的哈希表,只要遍历的链表头结点不为空,就定义一个cur,用来遍历这个单链表,遍历单链表的也需要记录该当前节点的下一个结点,否则会下一个结点会被丢弃。我们在当前节点cur中计算它的哈希地址,然后在新的指针数组中的指定位置进行头插。每遍历完一个单链表,都需要将旧的链表的头结点置为空,因为后面我们需要交换这两个指针数组,然后释放掉旧的指针数组。
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第8张图片
查找:查找一个元素,我们可以先计算出要查找元素的哈希地址,直接定位到指定的链表的头结点,然后遍历该条链表,如果当前节点的值和要查找的元素的值相同,则表示查找,返回所找到的结点,如果没有找到则返回空
删除:这里的删除是实际上的删除,我们可以先通过查找,查看要删除的值是否存在哈希表中,如果不存直接返回false,存在则需要先计算当前删除的结点的所在链表的哈希地址,找到后遍历该链表,并用一个prev结点记录要删除的前一个结点,当遍历找到我们删除的结点时,要先判断该节点是否为链表的头结点,如果为头结点,则将要删除的结点的下一个结点置为头结点,如果不是头结点则将记录的前结点prev的下一个结点的next置为要删除结点的下一个结点。最后将有效元素-1并删除要删除的结点
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第9张图片

代码

代码:闭散列----线性探测

enum STATE
{
     
	EXIST,
	DELETE,
	EMPTY
};
//哈希表:线性探测解决哈希冲突
template<class K, class V>
struct HashNode
{
     
	pair<K, V> _kv;//数据
	STATE _state = EMPTY;//状态
};

//顺序表实现哈希
template<class K, class V>
class HashTable
{
     
public:
	typedef HashNode<K, V> Node;

	HashTable(size_t n = 10)
		:_hTable(n)
		,_size(0)
	{
     }

	bool insert(const pair<K, V>& kv)
	{
     
		//0.检查容量
		checkCapacity();

		//1.当前元素的哈希位置
		int idx = kv.first % _hTable.size();

		//2.判断key是否存在
		while (_hTable[idx]._state != EMPTY)//当前位置已有数据或者为删除位置,都不能存放
		{
     
			//当前位置存在数据且key相同,则不能插入
			if (_hTable[idx]._state == EXIST
				&& _hTable[idx]._kv.first == kv.first)
			{
     
				return false;
			}
			//继续往后搜索空位置
			++idx;
			//走到末尾,需要从头开始找
			if (idx == _hTable.size())
				idx = 0;
		}

		_hTable[idx]._kv = kv;
		_hTable[idx]._state = EXIST;
		++_size;
		
		return true;
	}

	void checkCapacity()
	{
     
		//负载因子[0, 1],这里定负载因子为0.7
		if (_hTable.size() == 0 || _size * 10 / _hTable.size() >= 7)
		{
     
			//创建新表
			int newC = _hTable.size() == 0 ? 10 : 2 * _hTable.size();
			HashTable<K, V> newHt(newC);

			for (size_t i = 0; i < _hTable.size(); ++i)
			{
     
				//将原先的表的数据插入到新的表中,
				if (_hTable[i]._state == EXIST)
				{
     
					newHt.insert(_hTable[i]._kv);
				}
			}
			//交换两个表的内容
			Swap(newHt);
		}
	}

	void Swap(HashTable<K, V>& Ht)
	{
     
		swap(_hTable, Ht._hTable);
		swap(_size, Ht._size);
	}

	Node* find(const K& key)
	{
     
		//计算位置
		int idx = key % _hTable.size();
		while (_hTable[idx]._state != EMPTY)
		{
     
			//找到
			if (_hTable[idx]._state == EXIST
				&& key == _hTable[idx]._kv.first)
			{
     
				return &_hTable[idx];
			}
			++idx;
			if (idx == _hTable.size())
				idx = 0;
		}
		//如果遇到空格则表示没找到,返回空
		return nullptr;
	}

	bool erase(const K& key)
	{
     
		Node* node = find(key);
		if (node)
		{
     
			//伪删除
			--_size;
			node->_state = DELETE;
			return true;
		}
		return false;
	}
private:
	vector<Node> _hTable;//表
	size_t _size;//有效元素个数
};

测试:
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第10张图片
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第11张图片

代码:开散列法

#include 
#include 
using namespace std;

template<class K>
struct HashNode
{
     
	typedef HashNode<K> Node;
	K _val;
	Node* _next;

	HashNode(const K& val)
		:_val(val)
		, _next(nullptr)
	{
     }
};

template<class K>
class HTable
{
     
public:
	typedef HashNode<K> Node;

	HTable(int n = 10)
		:_ht(n)
		, _size(0)
	{
     }

	bool insert(const K& val)
	{
     
		//0.检查容量
		checkCapacity();

		//1.计算hash位置
		int idx = val % _ht.size();

		//2.查找
		Node* cur = _ht[idx];
		while (cur)
		{
     
			if (cur->_val == val)
				return false;
			cur = cur->_next;
		}

		//3.插入--头插
		cur = new Node(val);
		cur->_next = _ht[idx];
		_ht[idx] = cur;
		++_size;
		return true;
	}

	void checkCapacity()
	{
     
		if (_size == _ht.size())
		{
     
			int newC = _size == 0 ? 10 : 2 * _size;

			//创建新的指针数组
			vector<Node*> newHt(newC);

			//遍历旧表
			for (size_t i = 0; i < _ht.size(); ++i)
			{
     
				Node* cur = _ht[i];
				//遍历单链表
				while (cur)
				{
     
					Node* next = cur->_next;
					//计算新的位置
					int idx = cur->_val % newHt.size();
					//头插
					cur->_next = newHt[idx];
					newHt[idx] = cur;

					cur = next;
				}
				//旧表指针置空
				_ht[i] = nullptr;
			}

			//交换新表和旧表
			swap(_ht, newHt);
		}
	}

	Node* find(const K& val)
	{
     
		int idx = val % _ht.size();
		Node* cur = _ht[idx];
		while (cur)
		{
     
			if (cur->_val == val)
				return cur;
			cur = cur->_next;
		}
		return nullptr;
	}

	bool erase(const K& val)
	{
     
		Node* node = find(val);
		if (node)
		{
     
			int idx = val % _ht.size();
			Node* cur = _ht[idx];
			Node* prev = nullptr;
			while (cur != node)
			{
     
				prev = cur;
				cur = cur->_next;
			}
			Node* next = cur->_next;
			if (prev)
				prev->_next = next;
			else
				_ht[idx] = next;
			--_size;
			delete node;
			return true;
		}
		return false;
	}
private:
	//指针数组
	vector<Node*> _ht;
	int _size;
};

测试:
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第12张图片
数据结构 5分钟带你搞定哈希表(建议收藏)!!!_第13张图片

你可能感兴趣的:(数据结构,链表,指针,数据结构,新星计划)