【C++】通过哈希表实现map和set

news2025/9/15 10:22:44

前言 

        在前面,我们通过红黑树这一底层结构实现了map和set。它们是关联式容器。而现在,我们将通过哈希表这一数据结构重新实现map和set,即unordered系列的关联式容器。因为它们的遍历是无序的,和平衡二叉树不同,不能做到排序。

        既然不能做到排序,但是原本的map和set也能兼容这个功能,那么为什么要新增unordered_mapunordered_set呢?新增自然有其道理,因为是哈希思想,即映射关系,查找的效率非常高,让我们一起来看看吧~

红黑树实现map和set博客链接:

        【C++】map&set利用红黑树进行简单封装_柒海啦的博客-CSDN博客

目录

一、哈希概念

1.哈希函数

2.哈希冲突

1.闭散列解决哈希冲突

        1.1 线性探测

        1.2 二次探测

        1.3 扩容问题

2.开散列解决哈希冲突

二、闭散列实现哈希表

三、开散列实现哈希表

四、利用开散列哈希表模拟实现map和set


一、哈希概念

        理解一个哈希概念是重要前提。

        我们知道,在之前的顺序存储和红黑树中,我们插入和取出元素,都是通过比较去找的。即通过相同元素之间比较关系建立起的数据结构。顺序结构的查找时间复杂度为O(N),平衡搜索树结构的查找时间为树的高度O(log_2N)。

        但是查找效率还是不高。前面说了,哈希的核心就是映射关系,即哈希的插入和取出元素不再通过比较去实现,而是通过元素本身去映射一个位置。下次取的时候直接从这个映射位置找到即可。

        Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

        Hash算法是一个广义的算法,也可以认为是一种思想,使用Hash算法可以提高存储空间的利用率,可以提高数据的查询效率,也可以做数字签名来保障数据传递的安全性。所以Hash算法被广泛地应用在互联网应用中。(百度百科)

        那么重点就是如何去映射一个位置,已经关于此位置衍生出来的一系列问题。

1.哈希函数

        通过上面我们了解到,想要元素映射到一个位置,就需要哈希函数进行映射。

        下面我们通过一个例子引入哈希函数以及相关映射概念。

题目链接:

387. 字符串中的第一个唯一字符 - 力扣(Leetcode)

        看到这一题,你的第一想法是什么?总不会是遍历每一个字符,然后每一个字符和字符串中的其他字符进行比较吧,这样的话效率太低了,时间复杂度为O(N^2)。

        这个时候就可以借助哈希的思想,也是哈希函数-直接寻址法: 

        既然是不重复的字符,那么就和此字符的出现次数有关,我们可以将此字符映射到一个数组位置,此位置就代表此字符,然后当前位置就存储此字符的出现次数即可。题目要求我们找到第一个不重复的字符,只需要按顺序遍历每个字符,取出它映射下标的次数,为1就是第一个不重复的,直接返回对应字符即可。

        那么为什么是直接寻址法呢?因为是字符串,所以字母只有26个字母,我们的数组也只需要26个空间即可,每次就直接对应一个地址即可:

class Solution {
public:
    int firstUniqChar(string s) {
        int arr[26] = { 0 };
        for (auto e : s) arr[e - 'a']++;  //直接定址法
        for (int i = 0; i < s.size(); ++i)
        {
            if (arr[s[i] - 'a'] == 1) return i;
        }
        return -1;
    }
};

        这样实现的时间复杂度就是O(N),效率快了太多了。

        如上也就可以看到e - 'a'就是一个直接定址法,此法比较常用也是最基本的hash函数。

常见的哈希函数:

        1. 直接定制法

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

        2. 除留余数法

                设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

        3. 平方取中法(了解)

        4. 折叠法(了解)

        5. 随机数法(了解)

        6. 数学分析法(了解)

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

        可以看到,上面抛出了一个问题,那就是哈希冲突。什么是哈希冲突呢?

2.哈希冲突

        先给定义:

        对于两个数据元素的关键字和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

        比如上述的哈希函数中的除留余数法,假设除数为10,那么0和10留下的余数就是一致的,此时两者就映射同一位置上了。

        但是直接定址法就不会出现问题,因为是一个元素映射的一个唯一的位置。但是,如果我们给的不是一串字母,而是1 20 890呢?这样的话,如果元素数值差异较大,但是数量较少的话会严重浪费储存空间,这不就和我们的预想反着来了嘛。所以才会有除留余数法。这样的话就不受元素差异影响了,只需考虑开多少空间的问题。

        回到哈希冲突,如何解决这个问题呢?下面提供了两种解决方法:

1.闭散列解决哈希冲突

        我们使用数组对元素进行存储。

        从问题的根源出发,原因就是哈希函数产生的哈希地址冲突了。那么我们不妨每个哈希地址设置一种状态,没有插入就为空,第一次插入就设置为存在,第二次插入此位置,因为此位置的状态为存在,所以就往后找空位置。

        如何向后找空位置呢?下面提供了两种方法:

        1.1 线性探测

        线性探测说直白点就是每次遇到冲突了就地址+1,移动到下一个位置,如果还是冲突的话就继续+1遇到空位置为止。

        那么删除呢?我们是直接把元素对应的哈希地址置为空吗?显然不是。因为通过上面的哈希冲突我们知道,那就是在初次映射得到的哈希地址一致,那么重复的元素都是从当前位置开始往后进行线性探测,如果其中一个元素(后面还跟着元素)被置为空的话,那么就找不到后面的重复映射的元素了。所以我们还需要加入一种状态,那就是删除状态。所以删除的时候就置为删除状态就可以了。

        插入也改一下,在哈希冲突时,线性探测往后找,遇到空或者删除状态就插入。查找也非常简单,每次映射到对应的哈希地址,去找对应位置存储的元素,一致就返回,不一致往后一个一个去找,直到遇到空即可。

        1.2 二次探测

        首先可以看一下线性探测的缺点:由于每次发生哈希冲突后都是连续的往后找,那么数据就很容易的堆积到一起,此时的冲突概率就会越来越大,冲突越来越大的话就可以发现效率会越来越慢。所以为了缓解这个问题,提出了二次探测。

        实际上也就是在原来冲突的哈希地址的基础上,每次找下一个位置为(H0 + i ^ 2) % m的地址。其中H0位当前哈希地址,i为1234....,m为当前哈希表的总长。

        插入删除查找类似,只不过不是一个一个往后找吗,而是每次以上面的公式去找下一个为位置。

        1.3 扩容问题

        首先先谈谈容量问题。

        你认为哈希表的扩容能像以前那样满了直接扩吗?显然不能,因为存在冲突概率。哈希表自然是为了提高效率的,如果一个哈希表中存储的全是冲突元素,那哈希的作用不就微乎其微了吗,那还不如直接遍历一遍数组省去哈希函数的麻烦。

        所以我们需要降低冲突概率,冲突概率用负载因子进行计算,下面给出负载因子的计算公式:

        负载因子=插入元素个数 / 散列表的长度

        在大量实验证明下,一般闭散列(开放定址法)我们保持住0.7即可保证较高效率。即当哈希表中大于负载因子的时候就需要重新扩容。

        扩容也不是简简单单直接将值拷贝过去即可,而是需要重新进行映射。复用插入代码即可。

2.开散列解决哈希冲突

        闭散列我们可以明显的发现问题:一旦冲突的话就是一大片冲突。虽然二次探测稍微缓和了一点但是没有从本质上解决问题。

        那么我们可以这样想,如果发生了哈希冲突不是往后面找位置进行占用,而是挂在当前位置下呢?所以,这里我们就可以借助单链表,数组也就变成了存放指针的数组。

        开散列也称哈希桶,每个数组元素就相当于一个桶,桶里存放的就是存在哈希冲突的元素。插入的时候,就直接头插即可(尾插的话要往下也可以,但是效率不好)。寻找的话也是根据哈希地址找到对应的桶,然后遍历桶里的元素去寻找即可,删除就是找到对应元素的结点,链表的删除即可。

        因为是开散列,所以只要指针数组满了就扩容即可。那么每次扩多大呢?经过实验发现,每次扩成一个素数,似乎效率更高,所以我们不妨每次以素数的大小去扩。另外,需要注意此时的扩容就不能像上面直接复用了,因为是指针,所以浅拷贝不说,另外桶在当前容量下是冲突的,但是一旦变了容量的话那就不一定冲突了。所以需要老老实实的每个结点再次插入。

二、闭散列实现哈希表

代码如下:(下面只以线性探测为例,二次探测类似)

// 闭散列
namespace CloseHash
{
	enum class State  // 设置状态,防止删除&插入&查找存在冲突
	{
		EMPTY,  // 空
		DELETE,  // 删除
		EXIST  // 存在
	};

	template<class K, class V>
	struct HashElem  // 元素类型
	{
		pair<K, V> _kv;
		State _state = State::EMPTY;  // 默认给空
	};

	template<class K, class V>
	ostream& operator<<(ostream& out, const HashElem<K, V>& he)
	{
		out << "[" << he._kv.first << ":" << he._kv.second << "]";
		return out;
	}

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		// 构造函数 - 可以一上来开空间
		HashTable(size_t capacity = 3)
			:_data(capacity), _size(0)
		{}

		// 插入
		bool Insert(const pair<K, V>& kv)
		{
			// 防止数据冗余,首先对进入的kv的frist进行一个查找
			if (Find(kv.first)) return false;
			// 首先考虑扩不扩容
			CheckCapacity();
			size_t hashi = Hash()(kv.first) % _data.size();  // 除留余数法 使用了匿名对象
			while (_data[hashi]._state == State::EXIST)  // 遇到标记删除和空均可插入
			{
				hashi++;  // 线性探测
				// 注意是一个循环进行的过程,超过边界了没有遇到空或者删除就到第一个去,循环数组
				hashi %= _data.size();
			}
			_data[hashi]._kv = kv;
			_data[hashi]._state = State::EXIST;
			++_size;
			return true;
		}

		// 删除
		bool Erase(const K& key)
		{
			HashElem<K, V>* tmp = Find(key);
			if (tmp == nullptr) return false;  // 没有找到
			tmp->_state = State::DELETE;
			--_size;
			return true;
		}

		HashElem<K, V>* Find(const K& key)
		{
			if (_size == 0) return nullptr;
			int hashi = Hash()(key) % _data.size();
			while (_data[hashi]._state != State::EMPTY)
			{
				if (_data[hashi]._state == State::EXIST && _data[hashi]._kv.first == key)
				{
					return &_data[hashi];
				}
				hashi++;
				hashi %= _data.size();  // 循环查找
			}
			return nullptr;  // 没有找到
		}

		void CheckCapacity()  // 检查是否扩容 - 根据负载因子:负载因子 = 当前数据个数 / 当前存储空间长度 - 闭散列负载因子建议控制在0.7左右
		{
			// 此时扩容就不再是以前单纯的扩展空间和迁移数据了,对于哈希表来说,由于是根据除留余数法进行的,所以一旦存储长度发生变化,就需要重新进行映射
			if (_data.size() == 0 || _size * 1.0 / _data.size() >= 0.7)
			{
				size_t newcapacity = _size == 0 ? 10 : _data.size() * 2;
				HashTable<K, V, Hash> newHash(newcapacity);  // 重新建立一个哈希表
				// 开完空间首先重新映射
				for (auto& e : _data)
				{
					if (e._state == State::EXIST) newHash.Insert(e._kv);
				}
				_data.swap(newHash._data);  // vector自带的swap交换函数
			}
		}

		void Print()
		{
			for (auto& e : _data)
			{
				if (e._state == State::EXIST) cout << "[" << e._kv.first << ":" << e._kv.second << "]" << " ";
			}
			cout << endl;
		}

		size_t Capacity()
		{
			return _data.size();
		}

		size_t Size()
		{
			return _size;
		}
	private:
		vector<HashElem<K, V>> _data;
		size_t _size;  // 实际有效数据个数
	};
}

哈希仿函数:

template<class K>
struct HashFunc
{
	size_t operator()(const K& k)
	{
		return (size_t)k;  // 针对于数而言
	}
};

 

三、开散列实现哈希表

 代码如下:

namespace OpenHash  // 哈希桶 - 开散列 - 存在哈希冲突,就挂结点在下面
{
	template<class K, class V>
	struct HashBucketNode
	{
		pair<K, V> _kv;
		HashBucketNode* _next;

		HashBucketNode(const pair<K, V>& kv)
			:_kv(kv), _next(nullptr)
		{}
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashBucket
	{
		typedef HashBucketNode<K, V> Node;
	public:
		HashBucket(size_t capacity = 3)
			:_size(0)
		{
			_data.resize(capacity, nullptr);  // 全部初始化为空
		}

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

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first)) return false;  // 防止数据冗余
			CheckCapacity(); // 扩容

			size_t hashi = Hash()(kv.first) % _data.size();
			Node* cur = new Node(kv);
			cur->_next = _data[hashi];
			_data[hashi] = cur;
			++_size;
			return true;
		}

		bool Find(const K& key)
		{
			size_t hashi = Hash()(key) % _data.size();
			Node* cur = _data[hashi];
			while (cur)
			{
				if (cur->_kv.first == key) return true;
				cur = cur->_next;
			}
			return false;
		}

		bool Erase(const K& key)
		{
			if (_size == 0) return false;
			size_t hashi = Hash()(key) % _data.size();

			Node* pre = nullptr;
			Node* cur = _data[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					// 此时是头删
					if (pre == nullptr)
					{
						pre = cur->_next;
						_data[hashi] = pre;
					}
					else  // 中间删
					{
						pre->_next = cur->_next;
					}
					delete cur;
					--_size;

					return true;
				}
				pre = cur;
				cur = cur->_next;
			}
			// 没有找到
			return false;
		}

		size_t BucketsCount()  // 返回当前桶的个数 - 也就是哈希表的表长
		{
			return _data.size();
		}

		size_t BucketsSize()  // 当前数据个数
		{
			return _size;
		}

		void Print()
		{// 测试遍历用
			for (size_t i = 0; i < _data.size(); ++i)
			{
				Node* cur = _data[i];
				while (cur)
				{
					cout << "[" << cur->_kv.first << ":" << cur->_kv.second << "]";
					cur = cur->_next;
				}
			}
			cout << endl;
		}
	private:
		vector<Node*> _data;
		size_t _size;  // 存储的有效数据个数

		// 每次返回一个素数 - 这样每次摸上一个素数 - 扩容就按这个标准来
		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};

			for (size_t i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}

		void CheckCapacity()
		{
			// 扩展空间
			if (_data.size() == 0 || _size == _data.size())  // 哈希桶  控制负载因子不超过1 实际数据个数 / 哈希表长 
			{
				vector<Node*> newdata;
				newdata.resize(__stl_next_prime(_size), nullptr);  // 首先全部都是空的
				// 此时不可以像闭散列那样进行复用映射了,因为复用映射此时就是浅拷贝
				for (size_t i = 0; i < _data.size(); i++)
				{
					Node* cur = _data[i];
					while (cur)  // 依次重新进行映射,然后转移结点数据
					{
						Node* next = cur->_next;

						size_t hashi = Hash()(cur->_kv.first) % newdata.size();
						cur->_next = newdata[hashi];
						newdata[hashi] = cur;

						cur = next;
					}
					_data[i] = nullptr;
				}
				_data.swap(newdata);
			}

		}

	};
}

四、利用开散列哈希表模拟实现map和set

        和当初利用红黑树结构封装类似,同样的我们需要哈希表实现一个迭代器。由于其是无序的,所以只需要设计一个向前遍历的迭代器即可,其余操作类似,这里直接上代码了:

        哈希表实现:

namespace OpenHash  // 哈希桶 - 开散列 - 存在哈希冲突,就挂结点在下面
{
	template<class T>
	struct HashBucketNode
	{
		T _data;
		HashBucketNode<T>* _next;

		HashBucketNode(const T& data)
			:_data(data), _next(nullptr)
		{}
	};
	// 模板类声明
	template<class K, class T, class Hash, class KeyOfT>
	class HashBucket;

	template<class K, class T, class Hash, class KeyOfT>
	struct __HashBucketIterator
	{
		typedef HashBucketNode<T> Node;
		typedef HashBucket<K, T, Hash, KeyOfT> Hb;
		typedef __HashBucketIterator<K, T, Hash, KeyOfT> Self;

		Node* node;
		Hb* hb;

		__HashBucketIterator(Node* Node, Hb* Hb)
			:node(Node), hb(Hb)
		{}

		T& operator*()
		{
			return node->_data;
		}

		T* operator->()
		{
			return &node->_data;
		}

		Self& operator++()  // 前置++
		{
			if (node->_next)  // 存在就不用通过哈希表去找了
			{
				node = node->_next;
			}
			else
			{
				KeyOfT kot;
				size_t hashi = Hash()(kot(node->_data)) % hb->_data.size();
				Node* cur = hb->_data[++hashi];
				while (cur == nullptr)
				{
					hashi++;
					if (hashi >= hb->_data.size()) break;
					cur = hb->_data[hashi];
				}
				node = cur;
			}
			return *this;
		}

		bool operator==(const Self& s) const
		{
			return s.node == node;
		}

		bool operator!=(const Self& s) const 
		{
			return !(s == *this);
		}
	};
	template<class K, class T, class Hash, class KeyOfT>
	class HashBucket
	{
		typedef HashBucketNode<T> Node;

		template<class K, class T, class Hash, class KeyOfT>
		friend struct __HashBucketIterator;  // 友元类  注意模板类的声明

	public:
		typedef __HashBucketIterator<K, T, Hash, KeyOfT> iterator;

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

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

		HashBucket(size_t capacity = 3)
			:_size(0)
		{
			_data.resize(capacity, nullptr);  // 全部初始化为空
		}

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

		pair<iterator, bool> Insert(const T& data)
		{
			KeyOfT kot;
			iterator ret = Find(kot(data));
			if (ret != end()) return pair<iterator, bool>(ret, false);
			CheckCapacity(); // 扩容

			size_t hashi = Hash()(kot(data)) % _data.size();
			Node* cur = new Node(data);
			cur->_next = _data[hashi];
			_data[hashi] = cur;
			++_size;
			return make_pair(iterator(cur, this), true);
		}

		iterator Find(const K& key)
		{
			KeyOfT kot;
			size_t hashi = Hash()(key) % _data.size();
			Node* cur = _data[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key) return iterator(cur, this);
				cur = cur->_next;
			}
			return iterator(nullptr, this);
		}

		bool Erase(const K& key)
		{
			if (_size == 0) return false;
			KeyOfT kot;
			size_t hashi = Hash()(key) % _data.size();

			Node* pre = nullptr;
			Node* cur = _data[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					// 此时是头删
					if (pre == nullptr)
					{
						pre = cur->_next;
						_data[hashi] = pre;
					}
					else  // 中间删
					{
						pre->_next = cur->_next;
					}
					delete cur;
					--_size;

					return true;
				}
				pre = cur;
				cur = cur->_next;
			}
			// 没有找到
			return false;
		}

		size_t BucketsCount()  // 返回当前哈希表的表长
		{
			return _data.size();
		}

		size_t BucketsSize()  // 当前数据个数
		{
			return _size;
		}

	private:
		vector<Node*> _data;
		size_t _size;  // 存储的有效数据个数

		// 每次返回一个素数 - 这样每次摸上一个素数 - 扩容就按这个标准来
		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};

			for (size_t i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}

		void CheckCapacity()
		{
			// 扩展空间
			if (_data.size() == 0 || _size == _data.size())  // 哈希桶  控制负载因子不超过1 实际数据个数 / 哈希表长 
			{
				KeyOfT kot;
				vector<Node*> newdata;
				newdata.resize(__stl_next_prime(_size), nullptr);  // 首先全部都是空的
				// 此时不可以像闭散列那样进行复用映射了,因为复用映射此时就是浅拷贝
				for (size_t i = 0; i < _data.size(); i++)
				{
					Node* cur = _data[i];
					while (cur)  // 依次重新进行映射,然后转移结点数据
					{
						Node* next = cur->_next;

						size_t hashi = Hash()(kot(cur->_data)) % newdata.size();
						cur->_next = newdata[hashi];
						newdata[hashi] = cur;

						cur = next;
					}
					_data[i] = nullptr;
				}
				_data.swap(newdata);
			}

		}

	};
}

UnorderedMap:

#pragma once
#include "HashTable.h"

// 封装哈希表实现的map结构
namespace OpenHash
{
	template<class K, class V, class Hash = HashFunc<K>>
	class UnorderedMap
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename HashBucket<K, pair<K, V>, Hash, MapKeyOfT>::iterator iterator;

		UnorderedMap(size_t capacity = 3)
			:_hb(capacity)
		{}

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

		iterator end()
		{
			return _hb.end();
		}

		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _hb.Insert(kv);
		}

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

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

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

	private:
		HashBucket<K, pair<K, V>, Hash, MapKeyOfT> _hb;  // 底层封装哈希表
	};
}

 UnorderedSet:

#pragma once
#include "HashTable.h"

// 封装哈希实现的Set
namespace OpenHash
{
	template<class K, class Hash = HashFunc<K>>
	class UnorderedSet
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename HashBucket<K, K, Hash, SetKeyOfT>::iterator iterator;

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

		iterator end()
		{
			return _hb.end();
		}

		pair<iterator, bool> insert(const K& key)
		{
			return _hb.Insert(key);
		}

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

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


	private:
		HashBucket<K, K, Hash, SetKeyOfT> _hb;
	};
}

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

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

相关文章

APOLLO UDACITY自动驾驶课程笔记——规划、控制

1、路径规划使用三个输入&#xff0c;第一个输入为地图&#xff0c;Apollo提供的地图数据包括公路网和实时交通信息。第二个输入为我们当前在地图上的位置。第三个输入为我们的目的地&#xff0c;目的地取决于车辆中的乘客。 2、将地图转为图形 该图形由“节点”(node)和“边缘…

直流潮流计算matlab程序

一、直流潮流计算原理 直流潮流发的特点是用电力系统的交流潮流&#xff08;有功功率和无功功率&#xff09;等值的直流电流来代替。甚至只用直流电路的解析法来分析电力系统的有功潮流&#xff0c;而不考虑无功分布对有功的影响。这样一来计算速度加快&#xff0c;但计算的准确…

Rocket MQ : 拒绝神化零拷贝

注: 本文绝非对零拷贝机制的否定笔者能力有限&#xff0c;理解偏差请大家多多指正不可否认零拷贝对于Rocket MQ的高性能表现有着积极正面的作用&#xff0c;但是笔者认为只是锦上添花&#xff0c;并非决定性因素。Rocket MQ性能卓越的原因绝非零拷贝就可以一言以蔽之。 笔者企图…

第146篇 笔记-智能合约介绍

定义&#xff1a;当满足某些预定义条件时&#xff0c;智能合约是一种在区块链网络上运行的防篡改程序。 1.什么是智能合约 智能合约是在区块链网络上托管和执行的计算机程序。每个智能合约都包含指定预定条件的代码&#xff0c;这些条件在满足时会触发并产生结果。通过在去中…

IDEA热部署插件JRebel and XRebel

IDEA热部署插件JRebel and XRebel嘚吧嘚下载安装激活配置使用嘚吧嘚 刚开始用过一段时间的eclipse&#xff0c;其他方面没感觉&#xff0c;但是eclipse的热部署真的是深得我心啊&#x1f60a;。 后来换了IDEA&#xff0c;瞬间就心动了&#xff0c;各个方面真的很好用&#xf…

U3D VideoPlayer播放视频和坑点

最近做的游戏里,需要先播放一段几秒钟的工作室LOGO片头,拿到的视频是AVI格式,以前没在U3D里用到过视频,本以为很简单,没想到都2022年了,U3D播放视频还这么烂。。。 插件最好用的是AVPro,除非你有大量的视频要播放,否则没必要用插件,一个是贵,另一个插件很大。 首先…

Python爬虫从入门到进阶

前言 董伟明&#xff0c;国内某知名Python应用网站高级产品开发工程师&#xff0c;《 Python Web 开发实战》作者&#xff0c;本书目前已经售出 17k 余本&#xff0c;另外也已经在台湾地区上市。在 2012 和 2014 年分别通过 2 个爬虫免试获得 2 个业界知名公司 offer&#xff…

MyBatis缓存机制之一级缓存

MyBatis缓存机制之一级缓存 前言 MyBatis内部封装了JDBC&#xff0c;简化了加载驱动、创建连接、创建statement等繁杂的过程&#xff0c;是我们常见的持久性框架。缓存是在计算机内存中保存的临时数据&#xff0c;读取时无需再从磁盘中读取&#xff0c;从而减少数据库的查询次…

Node.js 入门教程 1 Node.js 简介

Node.js 入门教程 Node.js官方入门教程 Node.js中文网 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录Node.js 入门教程1 Node.js 简介1.1 大量的库1.2 Node.js 应用程序的示例1.3 Node.js框架和工具1 Node.js 简介 Node.js 是一个开源和跨平台…

子矩形计数(冬季每日一题 17)

给定一个长度为 nnn 的数组 aaa 和一个长度为 mmm 的数组 bbb。 两个数组均只包含 000 和 111。 利用两个给定数组生成一个 nmnmnm 的矩阵 ccc&#xff0c;其中 cijaibjc_{ij}a_ib_jcij​ai​bj​。 显然&#xff0c;矩阵 ccc 中也只包含 000 和 111。 请问&#xff0c;矩阵…

期末复习 c

作者&#xff1a;小萌新 专栏&#xff1a;C语言复习 作者简介&#xff1a; 大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;回顾之前的分支循环以及一些题目博客 [TOC](这里写目录标题分支循环选择switch casegetchar putchar 以及EOF三个C语言练习题总结…

C++智能指针之unique_ptr

C智能指针之unique_ptr前言一、unique_ptr1.1 unique_ptr类的初始化1.2 unique_ptr禁止拷贝和赋值1.3 release、reset函数1.4 向unique_ptr传递删除器1.5 unique_ptr与动态数组的使用总结前言 在C中&#xff0c;动态内存的申请和释放是通过运算符&#xff1a;new 和 delete 进行…

【无线传感器】基于Matlab实现WSN 查找两个节点之间的最短路径并发送数据

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Linux基础内容(13)—— 进程控制

目录 1.fork函数的进程创建 1.fork返回值 2.fork返回值 3.fork调用失败 2.写时拷贝 3.退出码的知识 4.进程退出 1.退出的情况 2.正常退出 5.进程等待 1.调用系统等待函数杀死僵尸进程 2.僵尸状态与PCB的关系 3.进程阻塞等待与非阻塞等待方式 6.进程程序替换 1.替…

【网络篇】第十八篇——IP协议相关技术

目录 DNS DNS背景 域名的层级关系 域名解析过程 使用dig工具分析DNS过程 ARP DHCP NAT NAT IP转换过程 NAPT NAT技术的缺陷 如何解决NAT潜在问题 ICMP ICMP功能 ICMP协议格式 ping命令 traceroute命令 IGMP 跟IP 协议相关的技术也不少&#xff0c;接下来说说与IP 协议相关的重…

Docker——Prometheus监控服务治理

摘要 Prometheus是继Kubernetes之后&#xff0c;第二个从云原生计算基金会&#xff08;CNCF&#xff09;毕业的项目。Prometheus是Google监控系统BorgMon类似实现的开源版&#xff0c;整套系统由监控服务、告警服务、时序数据库等几个部分&#xff0c;及周边生态的各种指标收集…

uniapp vuex正确的打开方式

uniapp vuex正确的打开方式一、vuex与全局变量globalData的区别二、uniapp vuex使用目录结构如下1. 根目录创建vuex目录&#xff0c;创建index.js文件2. 模块化代码3. 在 main.js 中导入store文件4. 调用一、vuex与全局变量globalData的区别 uni-app像小程序一样有globalData&…

项目开发——【流程图】软件工程程序流程图详解《如何正确绘制项目开发流程图》

程序流程图详解 介绍&#xff1a;通过图形符号形象的表示解决问题的步骤和程序。好的流程图&#xff0c;不仅能对我们的程序设计起到作用&#xff1b;在帮助理解时&#xff0c;往往能起到"一张图胜过千言万语"的效果。 一、程序流程图基本控制结构 顺序型&#xf…

如何实现RTS游戏中鼠标在屏幕边缘时移动视角

文章目录&#x1f9e8; Preface&#x1f38f; 判断鼠标是否处于屏幕边缘⚽ 获取鼠标处于屏幕边缘时的移动方向&#x1f3a8; 控制相机在x、z轴形成的平面上移动&#x1f3d3; 完整示例代码&#x1f9e8; Preface 本文简单介绍如何在Unity中实现即时战略游戏中鼠标在屏幕边缘的…

创新赋能合作伙伴,亚马逊云科技re:Invent科技盛宴

北京时间11月29号&#xff0c;亚马逊云科技年度峰会re:Invent 2022将在拉斯维加斯开幕。这场年度最重磅的云计算技术大会不仅是科技盛宴&#xff0c;也是亚马逊云科技与诸多客户交流互鉴的绝佳平台&#xff0c;今天带大家认识一下几位资深云计算用户&#xff0c;以及他们和re:I…