C++实现哈希表

news2025/1/9 16:38:43

文章目录

  • 前言
  • 1.哈希表的相关介绍
  • 2.哈希表的实现
    • 1.开放定址法实现哈希表
      • 1.插入
      • 2.查找
      • 3.删除
    • 2.链地址法(开链法)实现哈希表
      • 1.插入节点
      • 2.查找
      • 3.删除
      • 4.相关的一些补充
  • 3.封装unordered_map与unordered_set
    • 1.封装前的改造
    • 2.迭代器的实现
    • 3.unordered_map和unordered_set复用

前言

本文主要是对哈希表这种数据结构进行介绍。含义是根据存储的值和存储的位置建立映射关系从而快速检索查找存储的数据,哈希表是一种非常高效的数据结构。本文会用C++介绍哈希表的相关实现。


1.哈希表的相关介绍

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立映射的关系,那么在查找时通过该函数可以很快找到该元素。基于这种思想,哈希表便应运而生了。

哈希表的核心就是通过存储位置和存储的数据之间建立映射关系,来达到快速检索的目标。通常数据与其存储的位置是通过哈希函数来表示的。通过哈希函数构造出来的结构称为哈希表(Hash Table)(或者称散列表)常见的哈希函数有直接定址法,除留余数法。

直接定址法
在这里插入图片描述

Hash(Key)= A*Key + B,优点:简单、均匀,缺点:需要事先知道关键字的分布情况,使用场景:适合查找比较小且连续的情况。这种方式是有局限性的,只有在特定的处境中才比较合适。

除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。这是一种比较常用的哈希函数。通常是用存储的数据来对哈希表的空间大小进行取模,所得的数值即为哈希地址,也是就是映射后的地址。

在这里插入图片描述

这就是一种除留余数法是表示方法,我们由此可以建立起存储数据和起存储位置的映射关系。但是这种方法也会出现哈希冲突的问题。:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

在这里插入图片描述

哈希函数在计算哈希地址会可能出现冲突,由此我们需要解决哈希冲突。解决哈希冲突有两种方式:闭散列和开散列。

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?采用 线性探测的方式。线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

在这里插入图片描述

这种解决哈希冲突的方式其实通过抢占其他位置来解决的,当随着哈希冲突越来越多,被抢占的位置也可能越来越多,导致后续的其他的元素也要继续抢占位置。由此出现了开散列这种方式来解决哈希冲突。

开散列

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

在这里插入图片描述

这种方法相比之前的开放定址法更优雅一点。关于哈希表的相关概念我们介绍完了,接着就是来实现这两种不同形式的哈希表了。

2.哈希表的实现

这里的哈希函数我们选用除留余数法。哈希表的底层一般都是顺序表,这里我们用vector作为底层容器,这样方便存储数据时的后续扩容。而且既然有现成的容器,也不用在去重复造个顺序表了。毕竟我们主要的研究对象是哈希表。

1.开放定址法实现哈希表

在使用开放定址法之前,我们有几个的小细节,提前处理一下。1.我们的选用的哈希函数是除留余数,这里我们选取vector的空间数据的size作为模除对象,为啥不选容量呢?我们知道随着存储数据的增多,哈希冲突的概率会越来大,所需要空位越多。为了保证哈希的效率和合理的使用空间,我们将哈希表和大小和存储的数据控制在一定的比例,在适合的时机对vector进行扩容。这样为方便后续的扩容我们选用vector的size作为模对象。2.删除数据,如果将数据从哈希表中真实的删除,这无疑的会涉及数据的挪动的,这就会影响到其他数据,因此我们采用伪删除方式,将每个位置会有一个状态,来表示这这个位置是否为空位置,这个位置的数据是否已经被删除。这样的话就解决的删除的问题。3.这里哈希表中存储的真实数据我们用pair来表示,这样方便我们后续将其改造成unordered_map和unordered_set。

每个位置的状态表示

enum State
	{
		EMPTY,//空状态
		EXIST,//删除状态
		DELETE//删除状态
	};

存储数据的hash节点

//存储的hash节点
template<class K, class V>
	struct HashData
	{
		pair<K, V>_kv;
		State _state = EMPTY;
	};
//哈希表
template<class K, class V>
	class HashTable
	{
	  private:
		vector<HashData<K,V>> _table;
		size_t _n = 0;//记录存储数据的个数
	};

每个vector中存储的都是hash节点。节点包括了存储数据和该存储的位置状态。

1.插入

在插入节点时,我们知道使用线性探索的时候,解决哈希冲突的方法是占据其他的空位置,当哈希的表的容量越大,这种抢占空位置的方式对后续的节点影响就越小,哈希查找效率也越高。但是哈希表的空间越大所浪费的空间也就可能越多。为了平衡这这种两种优缺点,有一种比较折中的方式就控制这个哈希表的负载因子。负载因子就是哈希表中存储的数据个数和哈希表长度之比。这个比例控制在0.7以下较为合适,这是通过相关数据证明出来的。因此在负载因子超过0.7的时候就需要进行扩容操作。这里因为size都是整形我们先扩大10倍在进行除法。

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

			if (_table.size() == 0 || _n * 10 > _table.size() >= 7)
			{
				size_t newsize = _table.size() == 0 ? 10 : 2 * _table.size();
				HashTable<K, V>newtable;
				newtable._table.resize(newsize);
				for (auto data : _table)
				{
					if (data._state == EXIST)
					{
						newtable.Insert(data._kv);
					}
				}
				_table.swap(newtable._table);
			}
			size_t hashi = kv.first % _table.size();

			//线性探测
			size_t index = hashi;
			size_t i = 1;
			while (_table[index]._state == EXIST)
			{
				index = hashi + i;
				index %= _table.size();
				++i;
			}
			_table[index]._kv = kv;
			_table[index]._state = EXIST;
			_n++;
			return true;
		}

这里在扩容之前还是有个问题,就是扩容之后,因为哈希表的长度发生改变,之前原来哈希表中的数据是通过之前的哈希表的长度进行映射的,这里就需要把之前的数据重新进行一遍映射,在插入数据。为了解决这个问题,上述示例中提供一种较为优雅的书写方式。先定义一个哈希表对象,将这个哈希表对象的size设置为扩容后的大小,这个对象在调用insert接口将之前的哈希表的数据进行转移,之后在交换这个对象的_table即可。这个线性探索就是一步步往后试探的过程。注意这里往后试探也是取模运算,往后试探也是重新计算哈希地址的过程,这里的哈希函数要保持一致。

2.查找

这里查找也是根据要查找的数据计算出对应的哈希地址,从得到的哈希地址开始一步一步往后逐步试探。如果该哈希地址不为空状态也不为删除状态就可以和查找的数据进行比对。这里我们要注意一种特殊的场景就是大量的空状态或者是删除状态,这样就会陷入死循环.因此这里加上判断,如果已经走了一圈了就跳出循环即可。

HashData<K, V>* Find(const K& key)
		{
			if (_table.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = key % _table.size();
			size_t index = hashi;
			size_t i = 1;
	  while (_table[index]._state == EMPTY)
		  {
			if (_table[index]._state == EXIST && _table[index]._kv.first == key)
				{
					return &_table[index];
				}
				index = hashi + i;
				index = index % _table.size();
				++i;
				//避免死循环
				if (index == hashi)
				{
					break;
				}
			}
			return nullptr;
		}

3.删除

删除节点是伪删除就是把存储节点位置的状态设置为了删除即可。


		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}

到了这里开放地址法的哈希表就实现完了。相对红黑树来说总体实现过程不算太难,注意一些细节即可。

2.链地址法(开链法)实现哈希表

链地址法解决哈希冲突是将映射到同一个哈希地址上的数据链接起来形成单链表的结构。既然链表那么哈希表中存储就是节点指针,这里我们是用通过new节点进行数据插入,所以这里就必须实现析构函数了将new的节点给释放掉。

哈希节点

template<class K,class V>
	struct HashNode
	{
		HashNode(const pair<K, V>& kv)
			:_next(nullptr)
			, _kv(kv)
		{}
		HashNode<K, V>* _next;
		pair<K, V> _kv;

	};
template<class K,class V,class Hash= HashFunc<K>>
	class HashTable
	{
		
	public:
		typedef HashNode<K, V> Node;
		 private:
		vector<Node*>_table;
		size_t _n = 0;//记录存储有效数据个数

	};

1.插入节点

		bool Insert(const pair<K, V>& kv)
		{    
			Hash hash;
			if (Find(kv.first))
			{
				return false;
			}
			if (_n ==_table.size())
			{
				size_t newsize = _table.size() == 0 ? 10 : 2 * _table.size();
				vector<Node*> newtable(newsize, nullptr);
					for (Node* & cur :_table)
					{
						while (cur)
						{
							Node* tem = cur->_next;
							size_t hashi = hash(cur->_kv.first )% newtable.size();
							cur->_next = newtable[hashi];
							newtable[hashi] = cur;
							cur = tem;
						}
				    }
					_table.swap(newtable);
			}
			size_t hashi = hash(kv.first) % _table.size();
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

这里插入节点是采用头插法,这样来一个节点就可以插入一个节点,如果采用尾插节点还需要去遍历每个位置上的链接找到尾节点,在进行插入。每个存储的哈希节点都可以是一条链表的头节点,这里在扩容转移之前的数据时需要注意一下。

2.查找

这里查找先根据哈希函数计算出要查找的数据所在的哈希地址,在去遍历这个哈希地址上的单链表进行比对。当size为0的时候需要单独判断处理一下。

Node* Find(const K& key)
		{
			Hash hash;
			if (_table.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = hash(key )% _table.size();
			Node* ret = _table[hashi];
			while (ret)
			{
				
				if (ret->_kv.first == key)
				{
					return ret;
				}
				ret = ret->_next;
			}
			return nullptr;
	    }

3.删除

因为这个链地址法的存储特性,我们在删除节点的时候是在某天单链表上进行操作。就是本质就是删除单链表上的某个节点,我们需要一个变量保存要删除节点的前驱节点,这样我们才你能保证删除节点后,这个链表节点的指向关系是正确的。这里删除节点就是尾删法,因此删除头结点的时候需要单独判断一下。

bool Erase(const K& key)
		{
			Hash hash;
			size_t hashi = hash(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;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

4.相关的一些补充

实现这里哈希表是有3个模板参数的,前面两个参数一眼看得出来是和存储数据相关的。第三个参数是用来将数据转成可运算的整形的,试想一下我们存储string的话还需要将其转成整形来计算,不然无法通过哈希函数来计算哈希地址。这里转化方式就还是比较有讲究的,我们要尽量避免相近的字符串转化的整形值是一样的。下面提供C语言之父提出的一种转化方式。


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

			return hash;
		}
	};

对于一些内置类型比如整形来说,就可以直接进行哈希地址的计算,但是string就需要转化,因此这里提供了模板的特化处理。对于string来说会走专门的特殊处理进行转化。这里转化也是重载了括号,类似于仿函数的处理方式。

之前提到了,我们每个哈希位置存储相当于一条链表,对于每个new的节点最后需要释放空间,因此需要编写对应的析构函数。

析构函数

~HashTable()
		{
			for (Node*& cur : _table)
			{
				while (cur)
				{
					Node* tem = cur->_next;
					delete cur;
					cur = tem;
				}
				cur = nullptr;
			}
	
		}

其实虽然说每个存储的位置相当于都有一条单链表,但是实际上这个单链表的长度非常短,大多数都是只有一个节点,我们可以通过下面函数查找哈希表中最长的链表的节点个数,进行验证。

size_t MaxBucketSize()
		{
			size_t max = 0;
			int cnt = 1;
			for (auto cur : _table)
			{
				size_t i = 0;
				while (cur)
				{
					++i;
					cur = cur->_next;
				}
				if (max < i)
				{
					max = i;
				}
			}
			return max;
		}

这里我们采用这种链地址法的时候这个负载因子控制在1,也是可以的,就是size慢了在进行扩容,这样空间利用率就比较高了。这也是常常采用这种方式来解决哈希冲突的原因。

3.封装unordered_map与unordered_set

1.封装前的改造

和之前用红黑树封装map和set一样,这里也是用哈希表封装unordered_map和unordered_set,这两个容器都会复用哈希表的代码。哈希表在之前的基础上稍加改动之后,这两个容器就可以直接复用其接口来实现各自相关的功能了。因为有之前用红黑树封装map和set的基础,这里如果用哈希表来封装,我们就需要4个参数,一个参数确定k值的类型,一个参数确定存储数据的类型,是键值对还是k值,一个参数用来将拿到k值,另一个函数用来进行k值的转化,因为字符串这种复杂的自定义类型需要用到这个函数。


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

	size_t operator()( const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 31;
		}

		return hash;
	}
};

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

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

template<class K, class T, class KeyOfT, class Hash>
		class HashTable
		{
			template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
			friend struct __HashIterator;
			typedef HashNode<T> Node;
		public:
			typedef __HashIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
			typedef __HashIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;

		
			~HashTable()
			{
				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;
						delete cur;
						cur = next;
					}

					cur = nullptr;
				}
			}

			iterator Find(const K& key)
			{
				if (_tables.size() == 0)
					return end();

				KeyOfT kot;
				Hash hash;
				size_t hashi = hash(key) % _tables.size();
				Node* cur = _tables[hashi];
				while (cur)
				{
					if (kot(cur->_data) == key)
					{
						return iterator(cur, this);
					}

					cur = cur->_next;
				}

				return end();
			}

			bool Erase(const K& key)
			{
				Hash hash;
				KeyOfT kot;
				size_t hashi = hash(key) % _tables.size();
				Node* prev = nullptr;
				Node* cur = _tables[hashi];
				while (cur)
				{
					if (kot(cur->_data) == key)
					{
						if (prev == nullptr)
						{
							_tables[hashi] = cur->_next;
						}
						else
						{
							prev->_next = cur->_next;
						}
						delete cur;

						return true;
					}
					else
					{
						prev = cur;
						cur = cur->_next;
					}
				}

				return false;
			}

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

				Hash hash;

				// 负载因因子==1时扩容
				if (_n == _tables.size())
				{
					size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
					vector<Node*> newtable(newsize, nullptr);
					for (Node*& cur : _tables)
					{
						while (cur)
						{
							Node* tem = cur->_next;
							size_t hashi = hash(cur->_data) % newtable.size();
							cur->_next = newtable[hashi];
							newtable[hashi] = cur;
							cur = tem;
						}
					}
					_tables.swap(newtable);
				}


				size_t hashi = hash(kot(data)) % _tables.size();
				// 头插
				Node* newnode = new Node(data);
				newnode->_next = _tables[hashi];
				_tables[hashi] = newnode;

				++_n;
				return make_pair(iterator(newnode, this), true);
			}

			size_t MaxBucketSize()
			{
				size_t max = 0;
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					auto cur = _tables[i];
					size_t size = 0;
					while (cur)
					{
						++size;
						cur = cur->_next;
					}
					if (size > max)
					{
						max = size;
					}
				}

				return max;
			}
		private:
			vector<Node*> _tables; // 指针数组
			size_t _n = 0; // 存储有效数据个数
		};
	}

其实这里没有太多的改动,主要就是加上了对应的模板参数和之前实现红黑树类似,这里比较重要点就是这个迭代器的实现。

2.迭代器的实现

和之前实现迭代器一样,这里迭代器也是采用写一个对应的迭代器类。但是这里的迭代器类的模板参数会变多。首先我们根据之前的经验知道首先必须得有3个模板参数,这是为了重载->和重载&以及const的迭代器的复用。我实试想一下如果要实现迭代器++那么我们我们需要依次遍历每个位置上的哈希链表,如果当前位置是链表的空或者遍历到尾节点了,下次就要移动到新的位置上进行遍历,也就说需要计算哈希位置。因此我们需要哈希表,一来是需要计算哈希地址,二来还需要通过哈希表的节点进行遍历移动。那么哈希表中的模板参数,这个迭代器也需要一份。这就造成了迭代器的模板的参数比较多。

     // 前置声明
		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 __HashIterator
		{
			typedef HashNode<T> Node;
			typedef HashTable<K, T, KeyOfT, Hash> HT;
			typedef __HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;

			typedef __HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;

			Node* _node;
			const HT* _ht;

			__HashIterator(Node* node, const HT* ht)
				:_node(node)
				, _ht(ht)
			{}

			__HashIterator(const Iterator& it)
				:_node(it._node)
				, _ht(it._ht)
			{}

			
		};

这里需要前置申明,因为前我们迭代器中需要哈希表,哈希表中需要迭代器,就必须要前置申明。这里为了方便,我们直接定义一个哈希表指针和节点指针作为成员变量。因为我们成员变量是哈希表指针,我们在哈希表中实现begin接口的时候,可以直接使用this指针进行构造。

	Ref operator*()
	  {
		 return _node->_data;
	  }
	
    Ptr operator->()
        {
		  return &_node->_data;
	    }
	
	bool operator!=(const Self& s)
			 {
				return _node != s._node;
			 }

这里我们先把简单的迭代器接口先实现了再说,这3个接口相对来说比较简单,我们再来看看++重载操作。


			Self& operator++()
			{
				if (_node->_next != nullptr)
				{
					_node = _node->_next;
				}
				else
				{
					KeyOfT kot;
					Hash hash;
					size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
					++hashi;
					while (hashi < _ht->_tables.size())
					{
						if (_ht->_tables[hashi])
						{
							_node = _ht->_tables[hashi];
							break;
						}
						else
						{
							++hashi;
						}
					}
					if (hashi == _ht->_tables.size())
					{
						_node = nullptr;
					}
				}

				return *this;
			}
		};

如果节点指针的next不为空的话,我们直接就向前遍历,如果为空的话我们重新计算哈希地址向前遍历。这里遍历的时候需要判断一下,如果遍历的位置和哈希表的长度相等的话,就说明已经遍历到尾了,_node需要置为空。这里需要注意一下的就是,在哈希表中我们将vector设置为私有的成员变量,但是我们在迭代器类的时候需要访问vector的size,这样的话为了便于访问,我们在哈希表中将其设置友元类。这样就可以正常访问了。

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

			iterator begin()
			{
				Node* cur = nullptr;
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					cur = _tables[i];
					if (cur)
					{
						break;
					}
				}

				return iterator(cur, this);
			}

			iterator end()
			{
				return iterator(nullptr, this);
			}

			const_iterator begin() const
			{
				Node* cur = nullptr;
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					cur = _tables[i];
					if (cur)
					{
						break;
					}
				}

				return const_iterator(cur, this);
			}

			const_iterator end() const
			{
				return const_iterator(nullptr, this);
			}

这里begin的接口只用按次序遍历这个vector即可,我们只用找到每个位置的头节点即可。之前就说过这里直接传入this指针进行构造即可。

3.unordered_map和unordered_set复用

这里复用和之前封装set于map一样,直接调用对应的哈希表的接口即可

unorder_map

template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
	public:
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;

		typedef typename HashBucket::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<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = 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:
		HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};

}

unordered_set

template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
	public:
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename HashBucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
		typedef typename HashBucket::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<iterator, bool> insert(const K& key)
		{
			return _ht.Insert(key);
		}

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

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

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

这里就实现了对应的容器的封装,以上内容如有问题,欢迎指正!

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

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

相关文章

60题学会动态规划系列:动态规划算法第一讲

坚持就是胜利 - - 文章目录 1.第N个泰波那切数 2.三步问题 3.使用最小花费爬楼梯 4.解码方法 1.第N个泰波那切数 力扣链接&#xff1a;力扣 泰波那契序列 Tn 定义如下&#xff1a; T0 0, T1 1, T2 1, 且在 n > 0 的条件下 Tn3 Tn Tn1 Tn2 给你整数 n&#xff0c…

多线程 -- 线程安全问题(3)

本篇重点: 总结线程安全问题的原因以及解决办法 目录 synchronized 加锁关键字join 和 synchronized 的区别volatile 关键字 在上一篇中我们介绍了Thread类的基本使用方法, 本篇将会介绍有关于线程的安全问题 线程不安全的原因: 抢占式执行(罪魁祸首, 万恶之源) 多个线程修改同…

搜索推荐系统[10]项目实战系列Z5:汽车说明书跨模态智能问答系统,针对汽车说明书(可自定义文档)进行自动问答,采用了OCR、RocketQA等技术

搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术细节以及项目实战(含码源) 专栏详细介绍:搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术…

小程序之页面通信派发通知

文章目录 1. 介绍小程序页面通信的概念解释小程序页面通信的意义和必要性介绍小程序页面通信的方法 2. 小程序页面通信的实现示例通过事件传递数据实现页面之间通信通过全局变量实现页面之间通信 3. 实现小程序页面之间的消息通知介绍小程序发布订阅模式的概念使用事件订阅-发布…

网络通信IO模型-BIO

承接上文网络通信IO模型上 BIO的Java代码 服务端创建一个ServerSocket&#xff0c;绑定了端口号8090&#xff0c;目的是让客户端和服务端建立连接后进行通信&#xff0c;然后进入死循环&#xff0c;死循环里面会调用server.accept得到一个socket客户端&#xff0c;打印客户端的…

【PyQt5】指示灯显示

【PyQt5】指示灯显示 1、背景2、代码示例3、QtDesigner绘制 1、背景 利用Qt5写工业控制软件交互界面的时候&#xff0c;经常需要在界面上有指示灯功能。 例如下面的明暗表示串行端口的连接和断开。 我们本质是用Qt5的label文本标签来实现的&#xff0c;即通过设置标签的样式表…

115.删除有序数组中的重复项 removeDuplicatesFromSortedArray

文章目录 题目描述解题思路代码详解运行截图 题目描述 题目链接 给你一个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元…

C# | 凸包算法之Jarvis,寻找一组点的边界/轮廓

C#实现凸包算法之Jarvis 文章目录 C#实现凸包算法之Jarvis前言示例代码实现思路测试结果结束语 前言 这篇关于凸包算法的文章&#xff0c;本文使用C#和Jarvis算法来实现凸包算法。 首先消除两个最基本的问题&#xff1a; 什么是凸包呢&#xff1f; 凸包是一个包围一组点的凸多…

驱动LSM6DS3TR-C实现高效运动检测与数据采集(1)----获取ID

概述 本文将介绍如何驱动和利用LSM6DS3TR-C传感器&#xff0c;实现精确的运动感应功能。LSM6DS3TR-C是一款先进的6轴惯性测量单元&#xff08;IMU&#xff09;&#xff0c;集成了三轴加速度计和三轴陀螺仪&#xff0c;可用于测量和检测设备的加速度、姿态和运动。 本文将提供L…

车载软件架构 —— 闲聊几句AUTOSAR OS(二)

我是穿拖鞋的汉子,魔都中坚持长期主义的工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 在最艰难的时候,自己就别去幻想太远的将来,只要鼓励自己过好今天就行了! 这世间有太多的猝不及防,有些东西根本不配占有自己的情绪,人生就是一场体验,…

牛客HJ43迷宫问题 - 创建智能体通过策略自己找路

文章目录 问题描述思路代码C 问题描述 描述 定义一个二维数组 N*M &#xff0c;如 5 5 数组下所示&#xff1a; int maze[5][5] { 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; 它表示一个迷宫&#xff0c;其中的1表示墙壁&#xff0…

SPA首屏加载速度慢的怎么解决?

SPA首屏加载速度慢的怎么解决&#xff1f; 加载慢的原因 网络延时问题资源文件体积是否过大资源是否重复发送请求去加载了加载脚本的时候&#xff0c;渲染内容堵塞了 解决方案 1.减小入口文件体积 常用的手段是路由懒加载&#xff0c;把不同路由对应的组件分割成不同的代码…

如何在华为OD机试中获得满分?Java实现【水仙花数】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述4. Java算法源码5. 测试6.解题思路1. 题目描述 所谓水仙花数,是指一个…

LeetCode高频算法刷题记录10

文章目录 1. 旋转图像【中等】1.1 题目描述1.2 解题思路1.3 代码实现 2. 组合总和【中等】2.1 题目描述2.2 解题思路2.3 代码实现 3. 回文链表【简单】3.1 题目描述3.2 解题思路3.3 代码实现 4. 字符串解码【中等】4.1 题目描述4.2 解题思路4.3 代码实现 5. 多数元素【简单】5.…

高压功率放大器ATA4014VS高压功率放大器HSA42014

高压功率放大器ATA4014VS高压功率放大器HSA42014 一、企业背景&#xff1a; Aigtek是一家来自中国的专业从事测量仪器研发、生产和销售的高科技企业。公司主要研发和生产功率放大器、功率放大器模块、功率信号源、计量校准源等产品。核心团队主要是来自西安交通大学及西北工业大…

ERP系统介绍

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、ERP系统概述&#xff1f;1.什么是ERP2.主流ERP系统介绍3.用友ERP4.部署用友ERP畅捷通T6软件系统环境要求4.用友ERP畅捷通T6软件用户管理4.用友ERP畅捷通T6软…

简单的UDP网络程序

目录 准备工作 makefile udpServer.hpp udpServer.cc 细节1 服务端部署 创建套接字 接口认识1 socket 协议家族 绑定套接字 认识接口2 bind sockaddr_in结构体类型 细节2 bzero inet_addr 服务器启动(初启动) udpServer.hpp udpServer.cc 细节3 本地回环通…

跑通NeRF-SLAM代码记录

前言 Install 原文章github链接 下载代码 git clone https://github.com/ToniRV/NeRF-SLAM.git --recurse-submodules git submodule update --init --recursive因为有相关依赖&#xff0c;所以尽量使用命令下载代码。 2. 新建nerf-slam环境&#xff0c;github上也没提到p…

从sftp下载大文件到浏览器

从sftp下载大文件到浏览器 问题方案相关依赖包相关代码片段&#xff08;后端&#xff09;文件信息缓存工具类-FileChunkCache文件信息对象-FileDetailsftp传输进度监控-FileProgressMonitor切片工具类-ChunkService文件下载服务-AsyncDownloadService 问题 近期遇到直接使用sf…

Dubbo与SpringBoot整合

1.注意starter版本适配 2.服务提供者 创建Maven项目 boot-user-service-provider 服务提供者 2.1.通用模块依旧照用 2.2.POM <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</a…