【C++】哈希和unordered系列容器

news2024/11/20 6:35:41

目录

一、unordered系列关联式容器的引入

二、容器使用

2.1 unordered_map的文档说明

2.2 unordered_map的使用

2.3 unordered_set 

三、底层结构

3.1 哈希概念

3.2 哈希表

3.3 哈希冲突

3.4 哈希函数

3.5 哈希冲突解决

3.5.1 闭散列

3.5.2 开散列

3.5.3 思考

四、模拟实现

4.1 模板参数列表的改造

4.2 增加迭代器

4.3 模拟实现HashTable

4.4 模拟实现 unodered_set

4.5 模拟实现 unodered_map


一、unordered系列关联式容器的引入

在C++98中,STL(标准模板库)提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 log_2 N ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。

C++11引入了一组新的容器类型:unordered系列关联式容器,它们提供了一种不同于传统关联式容器(如map和set)的存储和访问方式。unordered系列容器基于哈希表实现,因此它们通常在查找、插入和删除操作上提供平均时间复杂度为O(1)的性能,但最坏情况下可能会退化到O(n)。

主要成员:

unordered_map:存储键值对(key-value pairs),其中每个键都是唯一的,它允许快速检索与给定的键相关联的值。
unordered_multimap:与unordered_map类似,但它允许键值对中的键不唯一,即多个值可以与同一个键关联。
unordered_set:存储唯一的元素,它允许快速判断一个元素是否存在于集合中。
unordered_multiset:与unordered_set类似,但它允许集合中有多个相同的元素。

二、容器使用

2.1 unordered_map的文档说明

在线文档说明

  • unordered_map是存储<key, value>键值对的关联式容器,其允许通过key快速的索引到与其对应的value。
  • 在unordered_map中,键值key通常用于惟一地标识元素,而映射值value是一个对象,其内容与此键关联。键和映射值的类型可能不同。
  • 在内部实现上,unordered_map没有按照key或value的任何特定顺序排列。为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  • unordered_map容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭代方面效率较低。
  • unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
  • 它的迭代器少是前向迭代器,可以使用迭代器从前向后遍历容器中的元素,但不能保证在双向或随机访问迭代器上提供相同的高效性能。

2.2 unordered_map的使用

unordered_map的使用与map的使用类似,在此不做过多说明。



2.3 unordered_set 

参考 unordered_set 在线文档说明 ,它的用法也和set无明显区别。

三、底层结构

unordered系列的关联式容器是基于哈希表实现的,因此效率比较高

3.1 哈希概念

通过某种函数将要查找的关键字key和另一个值建立一个关联关系,使得他们在一种存储结构中一 一映射。查找时通过函数可以很快地从存储结构中找到该元素。

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

这种方式叫做哈希方法。哈希方法中使用的函数叫做哈希函数(hashFunc)。构造的结构成为哈希表(hashTable)。

3.2 哈希表

哈希表通过一个哈希函数将key转换为数组索引。

index=hashFunc(key)

哈希表底层是一个数组,数组的每个位置或桶(bucket)用于存储一个或多个元素。这些元素可能是pair<key, value>或单个key。

这种方式查询速度极快,比如我们需要查询数组中是否有数字3,正常的做法就是从头遍历数组;当采用哈希函数存储的时候我们可以通过计算数字存储地址直接找到对应的位置,一次就能判断数组中是否有某个数以及数字的地址,这个特点在数据多的时候更为明显。

3.3 哈希冲突

哈希冲突(Hash Collision),也称为哈希碰撞,是指在使用哈希函数将键(key)映射到哈希表中的位置时,两个或多个不同的键产生了相同的哈希值的现象。由于哈希表的存储空间是有限的,而可能的键值是无限的,因此哈希冲突在理论上是不可能完全避免的。

3.4 哈希函数

哈希冲突的原因可能包括:

  • 哈希函数的设计不够合理:哈希函数可能不足以将不同的键均匀分布到哈希表中,导致多个键被映射到同一个位置。
  • 有限的哈希表大小:哈希表的大小是有限的,而键的数量可能远远超过哈希表的大小,因此必然会有多个键映射到同一个桶(bucket)。

哈希函数设计原则:

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

常见的哈希函数 :

  1. 直接定址法--(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况
  2. 除留余数法--(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p (p<=m) ,将关键码转换成哈希地址。

    (可以不用取质数作为除数,stl中使用质数大小的哈希表,但VS中没有使用质数大小的哈希表)
  3. 平方取中法--(了解)
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

3.5 哈希冲突解决

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

3.5.1 闭散列

闭散列也叫开放地址法,当冲突发生时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,可以使用某种探测技术在散列表中形成一个探测序列。
沿此序列逐个位置地查找,直到找到冲突位置中的“下一个” 空位置,将key存放到该位置。

按照形成探查序列的方法不同,可将开放定址法区分为线性探测法、二次探测法等。

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

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

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

插入:

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

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

删除:

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

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


删除状态的意义:
1、再插入,这个位置可以覆盖值
2、防止后面冲突的值,出现找不到的情况。遇到删除状态,还是继续往后找。

查找一个值,如果找不到,则找到空位置结束。

线性探测法的实现:

//仿函数,求key对应的数字
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//模板特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t ret = 0;
		for (auto& e : str)
		{
			ret *= 31;	//降低str总和相同的概率
			ret += e;
		}
		return ret;
	}
};

namespace closed_hash	//闭散列、开放定址法、线性探测
{
	enum status {
		Empty,
		Exist,
		Delete
	};

	template<class K, class V>
	struct HashData
	{
		pair<K,V> _kv;
		status _status;
	};


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

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

			//设负载因子为0.7
			//负载因子太大,冲突可以会剧增,冲突增加,效率降低
			//负载因子太小,冲突降低,但是空间利用率就低了
			if (_count * 10 / _table.size() == 7)
			{

				HashTable<K, V> newhash;
				newhash._table.resize(_table.size() * 2);
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._status == Exist)
						newhash.Insert(_table[i]._kv);
				}
				_table.swap(newhash._table);
			}

			Hash hf;
			int hashi = hf(kv.first) % _table.size();
			while (_table[hashi]._status == Exist)
			{
				hashi++;
				hashi %= _table.size();
			}
			_table[hashi]._kv = kv;
			_table[hashi]._status = Exist;
			_count++;
			return true;
		}

		HashData<K,V>* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _table.size();
			while (_table[hashi]._status != Empty) //连续查找,伪删除的节点保证连续
			{
				if (_table[hashi]._status == Exist
					&& _table[hashi]._kv.first == key)
					return &_table[hashi];
				else hashi = (hashi + 1) % _table.size();
			}
			return nullptr;
		}

		//伪删除
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret == nullptr) return false;

			ret->_status = Delete;
			_count--;
			return true;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]._status == Exist)
				{
					//printf("[%d]:%d\n", i, _table[i]._kv.first);
					cout << "[" << i << "]:" << _table[i]._kv.first << "->" << _table[i]._kv.second << endl;
				}
				else if (_table[i]._status == Empty)
				{
					printf("[%d]:\n", i);
				}
				else
				{
					printf("[%d]:D\n", i);
				}
			}
		}

	private:
		vector<HashData<K,V>> _table;
		size_t _count = 0;	// 存储的关键字的个数
	};
}

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

在哈希表中,当元素的数量超过一定的阈值时,通常会进行扩容(resize)操作。这个阈值通常是由哈希表的负载因子(load factor)决定的。负载因子定义为填入表中的元素个数与散列表长度的比值,即 α=填入表中的元素个数 / 哈希表的长度

α是哈希表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,哈希表的平均查找长度是负载因子α的函数,只是不同处理冲突的方法有不同的函数。

对于开放定址法,负载因子是特别重要因素,应严格限制在0.7-0.8。负载因子太大,冲突的可能性增加,会导致查找效率下降。负载因子太小,冲突的可能性降低,但空间利用率也就降低了。因此,一些采用开放定址法的hash库,如Java的系统库限制了负载因子为0.75,超过此值将resize哈希表。


扩容的具体步骤如下:

1. 创建一个新的哈希表,其大小是原始哈希表大小的两倍或多倍(具体倍数取决于哈希表的实现)。

2. 将原始哈希表中的所有元素重新哈希(rehash)到新的哈希表中。这通常涉及到计算每个元素的新的哈希值,并将其存储在新的哈希表的相应位置。

3. 释放原始哈希表的内存。

4. 将新哈希表设置为当前的哈希表。

注:拷贝时直接用新哈希表调用Insert函数,此时可以减少代码量。但该方法不是递归,因为使用者不是同一个对象。

扩容是一个相对耗时的操作,因为它涉及到重新哈希所有元素。因此,通常在哈希表的大小增长到一定程度时才会触发扩容,以避免频繁的扩容操作。
在实际应用中,不同的哈希表实现可能会有不同的扩容策略和负载因子阈值。例如,Java 中的哈希表默认负载因子是 0.75,超过这个值就会进行扩容。而C++中的'unordered_map'和 'unordered_set'的默认负载因子是 1,这意味着它们会在哈希表大小达到两倍时进行扩容。用户可以通过构造函数或成员函数 'reserve' 来调整负载因子,以控制扩容的时机。
 

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

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

  • 二次探测

二次探测的步骤如下:

  1. 计算初始位置:首先,使用哈希函数计算元素的初始位置。
  2. 检查初始位置:如果初始位置是空的,或者可以安全地插入元素(例如,对于只存储键的集合,新元素与初始位置的元素具有相同的哈希值),则将元素插入到该位置。
  3. 二次探测:如果初始位置被占用,则计算下一个位置。这个位置是通过以下公式计算的:下一个位置 = (初始位置 + i^2) % 哈希表大小
    其中,i 是一个整数,通常从 1 开始,每次增加 1,直到找到一个空闲的位置或者达到哈希表的大小。
  4. 重复步骤:重复步骤 2 和 3,直到找到一个空闲的位置或者达到哈希表的大小。
  5. 插入元素:将元素插入到找到的空闲位置

二次探测法也会导致数据“堆积”现象,导致搜索效率降低。

3.5.2 开散列

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

从上图可以看出,开散列把发生哈希冲突的元素都同一个桶中。

开散列的实现:

namespace Hash_Bucket
{
	enum status {
		Empty,
		Exist,
		Delete
	};

	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()
		{
			_table.resize(10);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

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

			Hash hf;
			//设平衡因子为1
			if (_count == _table.size())
			{
				vector<Node*> newtable;
				newtable.resize(_table.size() * 2);
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					Node* next;
					while (cur)
					{
						next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newtable.size();
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				_table.swap(newtable);
			}

			//新节点直接头插
			int hashi = hf(kv.first) % _table.size();
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			_count++;
			return true;
		}

		Node* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];

			while (cur) 
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				else
				{
					cur = cur->_next;
				}
			}
			return nullptr;
		}

		//伪删除
		bool Erase(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					_count--;
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

	private:
		vector<Node*> _table;
		size_t _count = 0;	// 存储的关键字的个数
	};

}

发生哈希冲突时,向桶中插入元素,可以选择头插或尾插,或者其它方式,在此我选择的是头插。

析构哈希桶时,要将桶内的每一个元素都清除掉。

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

如果像之前一样,直接将原来的元素拷贝到新的哈希桶中,则会增加拷贝和删除的时间,为了解决这种问题,采用的方式是:将原来的节点根据新哈希桶的映射关系,链入到新的哈希桶

			//设平衡因子为1
			if (_count == _table.size())
			{
				vector<Node*> newtable;
				newtable.resize(_table.size() * 2);
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					Node* next;
					while (cur)
					{
						next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newtable.size();
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				

3.5.3 思考

1. 哈希函数采用处留余数法,被模的key必须要为整形才可以处理,下面的方法将key转化为
整形。

//仿函数,求key对应的数字
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//模板特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t ret = 0;
		for (auto& e : str)
		{
			ret *= 31;	//降低str总和相同的概率
			ret += e;
		}
		return ret;
	}
};

第一个模板特化用于任何类型的 K,它简单地将键值直接转换为其整数值。这意味着如果 K 是一个整数,那么这个哈希函数将直接返回该整数作为哈希值。
第二个模板特化专门用于字符串类型,它计算字符串的哈希值。这个哈希函数使用了一个简单的哈希算法,它将字符串中的每个字符转换为其整数值,并将这些值相加,然后乘以一个质数(在这个例子中是 31),最后返回结果。

如果key是abc或acb或aad,它们对应ASCII码总和相同,为了区分,将ret的每一次结果乘以31,来降低它们冲突的概率。

要注意的是,这种冲突无法避免,例如仅仅10个字符,对应的情况就有26^10,而整数的范围是2^32,即相对而言,整数是有限的,而字符串情况是无限的,所以冲突是无法避免的。

2. 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

前面讲到,stl中使用的哈希表是素数大小,它是将一组素数存储在一个数组中,要扩容时,就在找数组中找到比哈希表size 大的下一个素数。

3. 闭散列和开散列的比较
开散列:

  • 开散列是指在哈希表中插入元素时,如果发生冲突,直接在表中查找下一个空闲位置,并将元素存储在那里。
  • 开散列不需要额外的存储空间来存储链表,因为它直接在表中处理冲突。
  • 开散列通常要求保持较高的空闲空间,以避免冲突导致的性能下降。需要使用负载因子进行控制。
  • 开散列的优点是实现简单,不需要额外的内存开销,但在最坏情况下,查找、插入和删除操作的时间复杂度可能会退化到 O(n)。

闭散列:

  • 闭散列是指在哈希表中插入元素时,如果发生冲突,使用链表将所有冲突的元素存储在一起。
  • 闭散列需要额外的存储空间来存储链表结构,因为每个冲突的元素都需要一个指针指向链表中的下一个元素。
  • 闭散列不需要保持大量的空闲空间,因为它通过链表解决了冲突,即使哈希表中有很多元素,只要链表长度合理,查找效率也不会下降。
  • 闭散列的优点是在最坏情况下,查找、插入和删除操作的时间复杂度仍然是 O(1),因为它避免了在表中查找空闲位置的开销。

由于开放地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 
0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

四、模拟实现

在C++中,std::unordered_map 和 std::unordered_set 默认使用开散列方法来解决哈希冲突。

4.1 模板参数列表的改造

unodered_set和unodered_map的元素为K和pair<K,V>,为了确定是哪种类型,直接将HashNode的类型改为:

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

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

为了确定T的关键字,HashTable需要添加一个模板参数 KeyOfT。

template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
    //...
};

HashFunc来将关键字转换成整数,通过HashFunc和KeyOfT 结合使用,可以查找到元素在HashTable中的索引(下标)。

4.2 增加迭代器

为了实现简单,在哈希桶的迭代器类中需要用到HashBucket本身。

迭代器的模板参数要有Ref 和 Ptr ,用来实现普通迭代器和 const 迭代器。
        typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
        typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;

//前置声明
	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;

	template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
	struct __HTIterator
	{
		typedef HashNode<T> Node;
		typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
		
		Node* _pnode;
		const HashTable<K, T, KeyOfT, Hash>* _pht;
		size_t _hashi; //定位哈希表中的位置,使得++可以到下一个桶

		__HTIterator(Node* pnode, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
			:_pnode(pnode)
			, _pht(pht)
			, _hashi(hashi)
		{}
        
		Self& operator++() 
		{
			if (_pnode->_next)
			{
				_pnode = _pnode->_next;
			}
			else
			{
				_hashi++;
				while (_hashi < _pht->_table.size())
				{
					if (_pht->_table[_hashi]) break;
					else _hashi++;
				}
				if (_hashi == _pht->_table.size()) _pnode = nullptr;
				else _pnode = _pht->_table[_hashi];
			}
			return *this;
		}

		Ref operator*()
		{
			return _pnode->_data;
		}
		
		Ptr operator->()
		{
			return &_pnode->_data;
		}

		bool operator!=(const Self& other)
		{
			return _pnode != other._pnode;
		}

		bool operator ==(const Self& other)
		{
			return _pnode == other._pnode;
		}
};

实现operator++时,当前迭代器所指节点后还有节点时直接取其下一个节点,如果该节点是当前桶的最后一个节点,需要找到下一个不空的桶,返回该桶中第一个节点。

4.3 模拟实现HashTable

	//unordered_set -> HsahTable<K, K,...>
	//unordered_set -> HsahTable<K, pair<const K,V>,...>
	template<class K, class T, class KeyOfT, class Hash>
	class HashTable
	{
		typedef HashNode<T> Node;

		template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
		friend struct __HTIterator;

	public:
		typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
		typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;

		//构造函数格式
		//__HTIterator(Node* pnode, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)

		iterator begin()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]) return iterator(_table[i], this, i);
			}
			return end();
		}
		const_iterator begin()const
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]) return const_iterator(_table[i], this, i);
			}
			return end();
		}
		
		iterator end()
		{
			return iterator(nullptr, this, -1);
		}
		const_iterator end()const
		{
			return const_iterator(nullptr, this, -1);
		}

		HashTable()
		{
			_table.resize(10);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

		pair<iterator, bool> Insert(const T& data)
		{
			KeyOfT kot;
			iterator it = Find(kot(data));
			if (it != end()) return make_pair(it,false);

			Hash hf;
			//设平衡因子为1
			if (_count == _table.size())
			{
				vector<Node*> newtable;
				newtable.resize(_table.size() * 2);
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					Node* next;
					while (cur)
					{
						next = cur->_next;
						size_t hashi = hf(kot(cur->_data)) % newtable.size();
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				_table.swap(newtable);
			}

			//新节点直接头插
			int hashi = hf(kot(data)) % _table.size();
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			_count++;
			return make_pair(iterator(newnode, this, hashi), true);
		}

		iterator Find(const K& key)
		{
			Hash hf;
			KeyOfT kot;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];

			while (cur) 
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur,this,hashi);
				}
				else
				{
					cur = cur->_next;
				}
			}
			return end();
		}

		//伪删除
		bool Erase(const T& data)
		{
			KeyOfT kot;
			Hash hf;
			size_t hashi = hf(kot(data)) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_data == data)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					_count--;
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				cout << "[" << i << "]: ";
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_data << "->" << cur->_data << "    ";
					cur = cur->_next;
				}
				cout << endl;
			}
		}

	private:
		vector<Node*> _table;
		size_t _count = 0;	// 存储的关键字的个数
	};

4.4 模拟实现 unodered_set

要注意的是,之前在模拟实现实现map和set时,可以直接将成员调用Insert的返回值,传给map、set的insert返回值进行拷贝构造。但是此处不行,因为上面的返回值虽然传递的pair<iterator, bool>,但是它的iterator类型是Node*,可以直接构造成iterator。

此处的iterator包含三个成员:
        __HTIterator(Node* pnode, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
            :_pnode(pnode)
            , _pht(pht)
            , _hashi(hashi)
        {}

不能再简单的拷贝构造,需要自己写一个构造函数。

	template<class K, class Hash = HashFunc<K>>
	class myUnorderedSet
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

	public:
		typedef typename Hash_Bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
		typedef typename Hash_Bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;
		//iterator begin()
		//{
		//	return _ht.begin();
		//}

		//iterator end()
		//{
		//	return _ht.end();
		//}
		const_iterator begin()const 
		{
			return _ht.begin();
		}

		const_iterator end()const 
		{
			return _ht.end();
		}

		pair<const_iterator, bool> insert(const K& key)
		{
			auto ret = _ht.Insert(key);
			return pair<const_iterator, bool>(const_iterator(ret.first._pnode, ret.first._pht, ret.first._hashi), ret.second);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		void Print()
		{
			_ht.Print();
		}

	private:
		Hash_Bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
	};

4.5 模拟实现 unodered_map

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

	public:
		typedef typename Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
		typedef typename Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::const_iterator const_iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}
		
		const_iterator begin()const
		{
			return _ht.begin();
		}

		const_iterator end()const
		{
			return _ht.end();
		}
		
		pair<iterator, bool> insert(const pair<const K, V>& data)
		{
			auto ret = _ht.Insert(data);
			return pair<iterator, bool>(iterator(ret.first._pnode, ret.first._pht, ret.first._hashi), ret.second);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}
		const V& operator[](const K& key)const
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1691865.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ RBTree

目录 概念 性质 节点的定义 树的结构 Insert 1. pparent->_left parent 1.1 uncle && uncle->_col RED 1.2 !(uncle && uncle->_col RED) 1.2.1 parent->_left cur 1.2.2 parent->_right cur 2. pparent->_right parent …

hive3从入门到精通(一)

Hive3入门至精通(基础、部署、理论、SQL、函数、运算以及性能优化)1-14章 第1章:数据仓库基础理论 1-1.数据仓库概念 数据仓库&#xff08;英语&#xff1a;Data Warehouse&#xff0c;简称数仓、DW&#xff09;,是一个用于存储、分析、报告的数据系统。 数据仓库的目的是构…

第十六讲:数据在内存中的存储

第十六讲&#xff1a;数据在内存中的存储 1.整数在内存中的存储1.1存储方式1.2大小端字节序1.3大小端字节序排序规则1.4为什么要有大小端1.5练习1.5.1练习11.5.2练习21.5.3练习31.5.4练习41.5.5练习51.5.6练习61.5.7练习7 2.浮点数在内存中的存储2.1练习2.2浮点数的存储2.3浮点…

常见的几种数据库通过SQL对表信息进行查询

一、前言 我们查询数据库表的信息&#xff0c;一般都使用界面化的连接工具查看&#xff0c;很少使用SQL语句去查&#xff0c;而且不同的数据库SQL语句又各自有差异。但如果通过代码去获取数据库表的信息&#xff0c;这时就需要通过SQL语句去查了&#xff0c;这个在逆向代码生成…

【案例分享】医疗布草数字化管理系统:聚通宝赋能仟溪信息科技

内容概要 本文介绍了北京聚通宝科技有限公司与河南仟溪信息科技有限公司合作开发的医疗布草数字化管理系统。该系统利用物联网技术实现了医疗布草生产过程的实时监控和数据分析&#xff0c;解决了医疗布草洗涤厂面临的诸多挑战&#xff0c;包括人工记录、生产低效率和缺乏实时…

打造专业级网页排版:全方位解析专业字体家族font-family实践与全球知名字体库导览

CSS中的字体家族&#xff08;font-family&#xff09;属性用于指定文本所使用的字体系列。它允许开发者选择一种或多种字体作为备选&#xff0c;确保在浏览器中以最佳可用字体显示文本。本文将深度解析专业级网页排版中字体家族&#xff08;font-family&#xff09;设置的实践技…

掌握Python基本语法的终极指南【基本语法部分】

一、基本语法部分 1.简单数据类型 1.1字符串类型及操作 字符串访问&#xff1a; 1.索引访问 mystr"Hello world" #索引访问 print(mystr[0]) #H print(mystr[-1]) #d print(mystr[-7]) #o print(mystr[6]) #w 2.切片访问 [头下标&#xff1a;尾下标] &#x…

车灯合面合壳密封使用UV胶的优缺点是什么呢?汽车车灯的灯罩如果破损破裂破洞了要怎么修复?

车灯合面合壳密封使用UV胶的优缺点是什么呢? 车灯合壳密封使用UV胶的优缺点如下&#xff1a; 优点&#xff1a; 快速固化&#xff1a;UV胶通过紫外线照射可以在短时间内迅速固化&#xff0c;大大缩短了车灯制造的工艺流程时间&#xff0c;提高了生产效率。高度透明&#xff…

SVG批量转为pdf超有效的方式!

最近在整理工作&#xff0c;发现ppt里面画的图智能导出svg格式无法导出pdf格式&#xff0c;由于在线的网站会把我的图片搞乱而且不想下载visio&#xff08;会把本地的word搞坏&#xff09;&#xff0c;因此琢磨出这种批量转换的方式。 1. 下载并安装Inkscape 下载链接&#xf…

基于Matlab完整版孤立词识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 孤立词识别是语音识别领域的一个重要分支&#xff0c;其目标是将输入的语音信号转换为计算机可…

成都爱尔眼科医院《中、欧国际近视手术大数据白皮书2.0》解读会圆满举行

2024年5月12日&#xff0c;爱尔眼科联合中国健康促进基金会健康传播与促进专项基金、新华社新媒体中心与中南大学爱尔眼科研究院、爱尔数字眼科研究所重磅发布《中、欧国际近视手术大数据白皮书2.0》。这是继2021、2022年在国内相继发布《国人近视手术白皮书》、《2022中、欧近…

C++笔试强训day32

目录 1.素数回文 2.活动安排 3.合唱团 1.素数回文 链接https://www.nowcoder.com/practice/d638855898fb4d22bc0ae9314fed956f?tpId290&tqId39945&ru/exam/oj 现将其转化为回文数&#xff08;这里用字符串存储比较方便转化&#xff09;&#xff0c;然后判断是否为…

无线网卡有几种接口?怎么给电脑选择一款合适的无线网卡?

前言 这篇文章一共有两个问题&#xff1a; 无线网卡有几种接口 怎么给电脑选择一款合适的无线网卡 目测这一期的文章很长很长&#xff0c;但不水。想要给笔记本或台式机升级无线网卡的小伙伴看过来了&#xff01; 最近有小伙伴问&#xff1a;华硕r555笔记本能不能升级无线…

MySql的环境配置与安装

MySQL 数据库 MySQL是一款关系型数据库 关系型数据库 ​ 基本单位是表,一个表中存储一类信息,表与表之间存在关联关系 sql语言(Structured Query Language) 数据库操作语言也属于一种编程语言,专门用作数据库操作分为三种语言 如下 sql安装使用流程 官网 href https://…

【设计模式】JAVA Design Patterns——Converter(转换器模式)

&#x1f50d;目的 转换器模式的目的是提供相应类型之间双向转换的通用方法&#xff0c;允许进行干净的实现&#xff0c;而类型之间无需相互了解。此外&#xff0c;Converter模式引入了双向集合映射&#xff0c;从而将样板代码减少到最少 &#x1f50d;解释 真实世界例子 在真实…

大众汽车集团CARIAD中国领导团队莅临知迪科技考察交流

5月23日&#xff0c;大众汽车集团旗下软件子公司CARIAD中国领导团队莅临知迪科技参观考察&#xff0c;知迪科技COO尹晓航先生率公司技术代表热情接待。 CARIAD中国一行来宾首先参观了知迪科技数采项目改制车。知迪科技软硬件工程师为考察团领导专家们讲解了知迪智驾数采系统&am…

【vs2022】安装copilot和reshaper

直接安装新版vs 17.10 自带集成的copilot支持安装resharper 可以跳过市场里的reshper安装好后依然可以直接使用vs。 resharper 2024.1.2 市场里还是i老版本&#xff1a; copilot 不兼容,这个是之前市场安装的版本 官方建议用vs intall 安装 安裝 GitHub Copilot GitHub.Co…

MySQL之Schema与数据类型优化(五)

Schema与数据类型优化 特殊类型数据 某些类型的数据并不直接与内置类型一致。低于秒级精度的时间戳就是一个例子。另外一个例子是一个IPv4地址。人们经常使用VARCHAR(15)列存储IP地址。然而&#xff0c;它们实际上是32位无符号整数。不是字符串。用小数点将地址分成四段的表示…

ThreadLocal为什么会导致内存泄漏?

问题引出&#xff1a; ThreadLocal是为了解决什么问题而产生的&#xff1f; ThreadLocal发生内存泄漏的根本原因是什么&#xff1f; 如何避免内存泄漏的发生&#xff1f;定义 为了解决多个线程同时操作程序中的同一个变量而导致的数据不一致性的问题。   假设现在有两个线程A…

ASP+ACCESS教师档案管理系统

3.1 系统功能模块图 3.2 E&#xff0d;R模型图 3.3 系统使用流程图 3.4 各个模块功能简介&#xff1a; 本系统分为五个功能模块&#xff0c;它们分别是教师信息录入模块、教师信息修改模块、教师信息查询模块、教师信息打印模块。 下面分别介绍各个模块的功能用途&#x…