哈希的概念及运用---闭散列

1、哈希概念

在常见的搜索方式中我们通常分为,静态的和动态的。

静态搜索方式:

给定一段序列数据 我们通用常采用:

顺序查找-----  for循环遍历  ——>  O(N)

二分查找---- 要求:带查找的数据排列必须是有序的

这些方式都是在一个固定的序列中查找

动态搜索方式 :底层结构发生改变,进行增 删等操作

二叉搜索树 ---- 有序或者接近有序 ----> 退化成单支树 ——>查找效率 O(N)

AVL树:二叉搜索树 + 平衡因子 ----->保证树的平衡性:任意节点的左右子树的高度差(平衡因子)绝对值不超过1  ——> 查找效率O(logN)

以上方式必须通过元素比较才能进行元素查找

在查找元素中,能否不用经过比较呢?

如果构造一种存储结构,通过某种函数(hashFunc)使

元素的存储位置与它的关键码之间能够建立一一映射的关系,

那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

插入元素

根据待插入的关键码,以此作为函数计算出该元素的存储位置,并按照此位置存放元素

搜索元素

对元素的关键码进行计算出,将所求出的函数值当作该元素的存储位置,将该存储位置上的元素与搜素元素进行比较,若关键码相同,则搜索成功

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

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述

2、哈希冲突

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

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

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

发生hash冲突该如何进行处理呢?

3、哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
如何设计一个比较合理的哈希函数(哈希函数设计原则):

哈希函数的值域必须在表格空间的范围之内

尽量保证哈希函数值域分布的均匀

哈希函数设计应该比较简单

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

2. 除留余数法–(常用) 尽可能模上素数
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

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

4、哈希冲突解决

闭散列:

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

如何寻找下一个空位置?
1、线性探测

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

#插入

通过哈希函数获取待插入元素在哈希表中的位置

如果该位置中没有元素则直接插入新元素,

如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

在这里插入图片描述#删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。

比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum STATE{EMPTY, EXIST, DELETE};

线性探测与二次探测

template<class T>
struct Elem
{
	Elem(const T& data = T())
	:_data(data)
	,_state(EMPTY)
	{
	
	}
	T _data;
	STATE _state;
};

// 自己约定:哈希表格中的元素必须唯一
// T: 表示元素的类型
// DF: 表示将T类型的对象转换为整数数据的方法的类型
// isLine: 选择用线性探测还是二次探测来解决哈希冲突
template<class T, class DF = DFDef<T>, bool isLine = true>
class HashTable
{
public:
	HashTable(size_t capacity = 10)
		: _size(0)
		, _table(0)
	{
		_table.resize(GetNextPrime(10));
	}

	bool Insert(const T& data)
	{
		CheckCapacity();

		// 1. 通过哈希函数,计算哈希地址
		size_t hashAddr = HashFunc(data);

		size_t i = 0;

		// 2. 找合适位置
		while (_table[hashAddr]._state != EMPTY)
		{
			// 元素已经存在
			if (_table[hashAddr]._state == EXIST && _table[hashAddr]._data == data)
				return false;

			// 位置状态:DELETE
			// 位置状态:EXIST,发生哈希冲突

			if (isLine)
			{
				// 线性探测
				hashAddr++;
				// 方式二:
				if (hashAddr == _table.capacity())
					hashAddr = 0;
			}
			else
			{
				i++;
				hashAddr = hashAddr + 2 * i + 1;
				hashAddr %= _table.capacity();
			}
		}

		// 找到了一个空位置,插入元素
		_table[hashAddr]._data = data;
		_table[hashAddr]._state = EXIST;
		++_size;
		_total++return true;
	}

	int Find(const T& data)
	{
		// 1. 通过哈希函数,计算元素在表格中的位置
		size_t hashAddr = HashFunc(data);
		size_t i = 0;
		// 2. 查找
		while (_table[hashAddr]._state != EMPTY)
		{
			if (_table[hashAddr]._state == EXIST && _table[hashAddr]._data == data)
				return hashAddr;

			// 需要继续往后探测:线性探测
			if (isLine)
			{
				// 线性探测
				hashAddr++;
				// 方式二:
				if (hashAddr == _table.capacity())
					hashAddr = 0;
			}
			else
			{
				i++;
				hashAddr = hashAddr + 2 * i + 1;
				hashAddr %= _table.capacity();
			}
		}

		return -1;
	}

	bool Erase(const T& data)
	{
		int pos = Find(data);
		if (pos != -1)
		{
			_table[pos]._state = DELETE;
			_size--;
			return true;
		}

		return false;
	}

	size_t Size()const
	{
		return _size;
	}

	void Swap(HashTable<T, DF, isLine>& ht)
	{
		_table.swap(ht._table);
		swap(_size, ht._size);
	}

private:
	
	size_t HashFunc(const T& data)
	{
		//DF df;
		//return df(data) % 10;
		return DF()(data) % _table.capacity();
	}

private:
	vector<Elem<T>> _table;
	size_t _size;  // 表示哈希表中存储的有效元素个数
	size_t _total;  // 表示哈希表格中已经存储元素个数:有效元素+删除元素之和
};

线性探测优点:

实现非常简单。

线性探测缺点:

一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”。

即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

如何缓解呢?
不要一次挨着往后找,就不会产生堆积。

二次探测

不是探测两次,而是相比于线性探测,不会挨着往后查找。

优点:

可以解决线性探测的堆积问题

缺点:

当表格中空位置较少时,找下一个空位置,可能需要多次探测

注意: 二次探测中,如果哈希地址越界,必须要用%的方式将哈希地址回到哈希表中,一定不能将哈希地址置零。

在往哈希表存储时,我们不一定存整型,这个问题如何解决?

为了避免只能存储为整形的元素,提供不同的将T类型的对象转换为整数数据的方法的类型


// 如果T是整型
template <class T>
class DFDef
{

public:
	T operator()(const T& data)
	{
		return data;
	}
};

// 比如T 是string 类型得对象,
class DFStr
{
public:
	size_t operator()(const string& s)
	{
		// 如何将string类型字符串转化为整型?

		return BKDRHash(s.c_str());

	}

private:
	size_t BKDRHash(const char *str) // 提供好的字符串hash算法
	{
		register size_t hash = 0;
		while (size_t ch = (size_t)*str++)
		{
			hash = hash + 131 + ch; //也可以乘以31、131、1313、13131
		}
		return hash;
	}

};

上述还未对哈希表扩容进行实现,那哈希表什么时机增容?如何增容?能否按照vector的方式进行扩容呢?

哈希负载因子(散列表的载荷因子) = 哈希表元素个数 / 表格容量

载荷因子必须严格控制在0.7–0.8以下。
超过0.8,查表时的cpu缓冲不命中(cache missing)按照指数曲线上升。

vector的扩容方式:申请新空间、拷贝元素、释放旧空间

一旦扩容 ,容量就会发生变化,根据哈希函数所求的哈希地址也随之发生变化,
因此并不能使用vector的扩容方式。

对于哈希表的扩容方式我们采取:

  1. 新创建一个哈希表
  2. 将旧哈希表中状态为存在的元素向新哈希表格中插入
void CheckCapacity()
	{
		//if (_size*10 / _table.capacity() >= 7)    // 如果删除位置可以插入元素
		if (_total * 10 / _table.capacity() >= 7)   // 如果删除位置不可以插入元素
		{
			// 1. 新创建一个哈希表
			HashTable<T, DF, isLine> newHT(GetNextPrime(_table.capacity()));
			
			// 2. 将旧哈希表中状态为存在的元素向新哈希表格中插入
			for (auto e : _table)
			{
				if (e._state == EXIST)
					newHT.Insert(e);
			}

			Swap(newHT);
		}
	}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值