【C++ —— 认识哈希和unordered_set、unordered_map的介绍及模拟】

哈希表基础

哈希的概念

哈希表(Hash Table),也称为散列表,是一种通过哈希函数将键值(Key) 映射到对应位置(Bucket)数据结构。哈希表使用一个数组来存储数据,通过哈希函数计算出键值的索引,插入、查找和删除操作的时间复杂度可以接近 O ( 1 ) O(1) O(1)

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

哈希表的基本操作

当向哈希表的结构中:

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

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


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

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

哈希冲突

哈希冲突的定义

哈希冲突(Hash Collision) 是指两个或多个不同的键通过哈希函数映射到同一个位置。当哈希表发生冲突时,需要使用某种机制来解决冲突,确保所有键值对能够正确存储和查找。

哈希冲突的影响

哈希冲突会导致数据存储位置重叠,增加查找时间,降低哈希表的整体性能。频繁的冲突会使哈希表的操作复杂度从 O ( 1 ) O(1) O(1)恶化为 O ( n ) O(n) O(n),影响系统性能。

常见的哈希冲突的解决方法

1. 开放地址法
开放地址法通过探测空闲位置来解决冲突。常见的探测方法有:

下面讲重点讲解线性探测二次探测分离链接法

线性探测(Linear Probing)

  • 发生冲突时,从当前位置开始,逐个检查后续位置,直到找到空位为止。
  • 优点:实现简单。
  • 缺点:可能导致“堆积”现象,即冲突连续发生。

比如上述中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

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

  • 插入

    • 通过哈希函数获取待插入元素在哈希表中的位置
    • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
      在这里插入图片描述
  • 删除

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

二次探测(Quadratic Probing)

  • 发生冲突时,从当前位置开始,以二次方为步长逐个检查后续位置(例如,1^2, 2^2, 3^2,…),直到找到空位为止。
  • 优点:减少线性探测中的堆积现象。
  • 缺点:需要处理哈希表大小和步长选择的问题。

二次探测

 线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi =( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H O H_O HO - i 2 i^2 i2 )% m。其中: i =1,2,3., H o H_o Ho是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小。对于上述中如果要插入44,产生冲突,使用解决后的情况为:

在这里插入图片描述

  研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜素时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。


双重哈希(Double Hashing)

  • 发生冲突时,使用第二个哈希函数计算新的步长,从当前位置开始探测空闲位置。
  • 优点:进一步减少冲突聚集。
  • 缺点:需要设计两个有效的哈希函数。

2. 链地址法
链地址法通过在每个哈希表位置存储一个链表来解决冲突。

分离链接(Separate Chaining)

  • 分离链接法通过将每个哈希表槽位维护一个链表来处理冲突。
  • 每个哈希表槽位存储一个链表,所有冲突的元素都插入该链表中。
  • 优点:简单易实现,动态调整链表长度。
  • 缺点:在最坏情况下(所有元素都落在同一个桶中),查找时间复杂度退化为 O ( n ) O(n) O(n)

在这里插入图片描述
在这里插入图片描述

自适应链表(Adaptive Chaining)

  • 根据链表长度和冲突情况,动态调整存储结构,例如在链表长度较长时将其转换为红黑树。
  • 优点:在链表较短时保持简单性,在链表较长时提高查找效率。
  • 缺点:实现较为复杂,需要维护多种数据结构。

哈希函数

哈希函数的定义

哈希函数的定义
 哈希函数(Hash Function)是将键值转换为哈希值的函数。哈希值用于确定键值在哈希表中的存储位置。一个好的哈希函数能均匀分布键,减少冲突,提高哈希表的性能。

哈希函数的设计原则

  • 均匀分布:哈希值应均匀分布在整个哈希表,避免冲突集中在某些位置。
  • 快速计算:哈希函数应高效计算,减少额外开销,提高性能。
  • 最小冲突:尽量避免相同哈希值的情况,使不同的键尽量映射到不同的位置。

常见的哈希函数

1. 除留余数法 (Modulo Division Method)
 解释:除留余数法是最简单和常用的哈希函数之一。它通过将键除以哈希表的大小并取余数来生成哈希值。这样生成的哈希值会在0到表大小-1的范围内,适合直接作为哈希表的索引。

优点:
 实现简单,计算快速。
缺点:
 如果键的分布不均匀,可能导致冲突较多。
C++代码

int modHash(int key, int table_size) 
{
    return key % table_size;
}

2. 平方取中法 (Mid-Square Method)
 解释:平方取中法是将键值平方,然后取中间几位作为哈希值。这样做的优点是可以通过平方操作增加哈希值的复杂性,从而使得哈希值更均匀地分布。

优点:
 中间位数的选择可以减少键值的偏差,减少冲突。
缺点:
 适用于键值长度较短的情况。
C++代码:

int midSquareHash(int key, int table_size) 
{
    int squared = key * key;
    // 假设key是一个4位数,取中间2位
    int mid_digits = (squared / 100) % 100;
    return mid_digits % table_size;
}

3. 直接定址法 (Direct Addressing Method)
 解释:直接定址法将键值直接映射到哈希表的索引上,适用于键值范围较小且较为密集的情况。键值直接作为索引使用,不需要额外计算。

优点:
 查找、插入和删除操作的时间复杂度都是O(1)。
 实现非常简单。
缺点:
 需要保证键值的范围在哈希表大小之内,否则会导致索引越界。
 键值范围大且稀疏时会浪费内存。
C++代码:

int multiplicativeHash(int key, int table_size) 
{
    double A = 0.618033;  // (sqrt(5) - 1) / 2 的值
    double frac = key * A - int(key * A);
    return int(table_size * frac);
}

unordered系列关联式容器

unordered系列关联式容器 包含 unordered_mapunordered_set,在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同。

unordered_map/unordered_set 和 map/set 的区别

特性map/setunordered_map/unordered_set
内部实现红黑树(自平衡二叉搜索树)哈希表
元素顺序有序(按键值排序)无序(按哈希值存储)
时间复杂度O(log n)平均 O(1),最坏 O(n)
迭代顺序按键值顺序无固定顺序
内存占用较高(存储节点信息)较低(可能有冲突链表开销)
使用场景需要有序存储和范围查询需要快速查找、插入和删除

简要总结

  • map 和 set:适用于需要保持元素有序的场景,例如有序字典、按键值范围查询。
  • unordered_map 和 unordered_set:适用于需要快速查找、插入和删除的场景,且对元素顺序没有要求。

unordered_map/unordered_set 的接口可以查询cplusplus.com使用用法与之前介绍的相似。

hash模拟

开放地址法hash

#pragma once
#include <vector>

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

//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

namespace open_address
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	struct StringHashFunc
	{
		size_t operator()(const string& k)
		{
			size_t hash = 0;
			for (auto& e : k)
			{
				hash *= 131;
				hash += e;
			}
			return hash;
		}
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			//扩容
			if (_n * 10 / _tables.size() >= 7)
			{
				HashTable<K, V, Hash> newHT;
				newHT._tables.reserve(_tables.size() * 2);

				//旧表插入到新表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}

				_tables.swap(newHT._tables);
			}

			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();

			//线性探测
			while (_tables[hashi]._state == EXIST)
			{
				hashi++;
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			_n++;

			return true;
		}

		HashData<K, V>* Find(const K& k)
		{
			Hash hs;
			size_t hashi = hs(k) % _tables.size();

			//线性探测
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state == 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 == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;
				_n--;
				return true;

			}
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;		//有效数据个数

	};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值