【数据结构】16.哈希

一、哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。这样的话我们能不能不经过任何比较,一次直接从表中得到要搜索的元素?

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素

当向该结构中:

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

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

二、哈希函数

2.1 哈希函数设计原则

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

2.2 常见的哈希函数

  1. 直接定址法
    这种方法就是使用key的值来映射一个绝对或相对位置,计数排序的核心思想就基于此
  2. 除留余数法
    这种方法就是使用key的值来%N(表的大小)来映射到表中的一个位置

对于上述两种方法都存在着这样的情况,就是两个key映射的位置刚好相同……,这就是哈希冲突

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

三、哈希冲突及其解决

3.1 哈希冲突的概念

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞
那么发生哈希冲突该如何处理呢?

3.2 哈希冲突的解决

解决哈希冲突两种常见的方法是:闭散列(开放定址法)和开散列(哈希桶/拉链法)

3.2.1 闭散列——开放定址法

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

  1. 线性探测
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
    • 插入
      通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
    • 删除
      采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素
  2. 二次探测
    线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i = 1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
那么哈希表什么情况下进行扩容?如何扩容?
在这里插入图片描述
对闭散列的线性探测的实现:

#pragma once

#include<iostream>
#include<vector>
#include<cassert>
#include<algorithm>
using namespace std;

//哈希函数
//对于int 、double、size_t 、int* 等类型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};

//对于string 的特化处理
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t ret = 0;
		for (const auto& e : key)
			ret = ret * 31 + e;
		return ret;
	}
};

//状态
enum State
{
	Empty,
	Delete,
	Exit
};

//哈希表中的数据
template<class K,class V>
struct HashData
{
	pair<K, V> _kv;
	State _state;
};

//哈希表
template<class K,class V,class Hash=HashFunc<K>>
class HashTable
{
public:
	HashTable()
	{
		_ht.resize(10);
		_n = 0;
	}
	bool find(const K& key)
	{
		size_t hashi = Hash()(key) % _ht.size();//定位
		while (_ht[hashi]._state != Empty)
		{
			if (_ht[hashi]._kv.first == key && _ht[hashi]._state == Exit)
				return true;
			++hashi;
			hashi %= _ht.size();
		}
		return false;
	}
	bool insert(const pair<K,V>& kv)
	{
		if (find(kv.first))
			return false;
		
		//检查是否需要扩容
		if (_n * 10 / _ht.size() == 7)
		{
			//现代写法
			HashTable newht;
			newht._ht.resize(_ht.size() * 2);
			for (int i = 0; i < _ht.size(); i++)
				newht.insert(_ht[i]._kv);

			_ht.swap(newht._ht);//交换两个表
		}
		//定位
		size_t hashi = Hash()(kv.first) % _ht.size();
		while (_ht[hashi]._state == Exit)
		{
			hashi++;
			hashi %= _ht.size();
		}
		//插入
		_ht[hashi]._kv = kv;
		_ht[hashi]._state = Exit;
		_n++;
		return true;
	}

	bool erase(const K& key)
	{
		if (!find(key))
			return false;
		size_t  hashi = Hash()(key) % _ht.size();
		while (_ht[hashi]._state == Exit)
		{
			if (_ht[hashi]._kv.first == key)
			{
				_ht[hashi]._state = Delete;
				return true;
			}
			++hashi;
			hashi %= _ht.size();
		}
		return false;
	}
private:
	vector<HashData<K, V>> _ht;
	size_t _n;//有效元素个数
};

3.2.2 开散列——拉链法/哈希桶

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

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

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素
对开散列的实现:

#pragma once

#include<iostream>
#include<vector>
#include<cassert>
#include<algorithm>
using namespace std;
//对于int 、double、size_t 、int* 等类型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};

//对于string 的特化处理
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t ret = 0;
		for (const auto& e : key)
			ret = ret * 31 + e;
		return ret;
	}
};

//状态
enum State
{
	Empty,
	Delete,
	Exit
};

//哈希表中的数据
template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode* next;

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

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
	HashTable()
	{
		_ht.resize(10);
		_n = 0;
	}
	bool find(const K& key)
	{
		size_t hashi = Hash()(key) % _ht.size();//定位
		Node* cur = _ht[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
				return true;
			cur = cur->next;
		}
		return false;
	}
	bool insert(const pair<K, V>& kv)
	{
		if (find(kv.first))
			return false;
		//扩容
		if (_n == _ht.size())
		{
			vector<Node*> newht(_ht.size() * 2);
			for (int i = 0; i < _ht.size(); i++)
			{
				Node* cur = _ht[i];
				while (cur)
				{
					Node* next = cur->next;
					size_t hashi = Hash()(_ht[i]->_kv.first) % newht.size();
					if (newht[i] == nullptr)
						newht[i] = cur;
					else
					{
						cur->next = newht[i];
						newht[i] = cur;
					}
					_ht[i] = next;
				}
			}
			_ht.swap(newht);
		}

		size_t hashi = Hash()(kv.first) % _ht.size();//定位
		Node* newnode = new Node(kv);
		newnode->next = _ht[hashi];
		_ht[hashi] = newnode;
		_n++;
		return true;
	}

	bool erase(const K& key)
	{
		size_t hashi = Hash()(key) % _ht.size();//定位
		Node* prev = nullptr;
		Node* cur = _ht[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (prev == nullptr)
				{
					_ht[hashi] = nullptr;
				}
				else
				{
					prev->next = cur->next;
				}
				delete cur;
				cur = nullptr;
				_n--;
				return true;
			}
			prev = cur;
			cur = cur->next;
		}
		return false;
	}
private:
	vector<Node*> _ht;
	size_t _n;
};

3.3 开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值