yo!这里是STL::unordered系列简单模拟实现

目录

前言

相关概念介绍

哈希概念

哈希冲突与哈希函数

闭散列

框架

核心函数

开散列

框架

核心函数 

哈希表(开散列)的修改

迭代器实现

细节修改

 unordered系列封装

 后记


前言

        我们之前了解过map和set知道,map、set的底层结构是红黑树,插入查询等操作效率相对较高,但是当树中的节点非常多时,查询的效率也是很好,我们希望呢,最好进行较少的查询就能找到元素。因此,在c++11中,stl又提供了unordered_map和unordered_set等相关关联式容器,使用方法与map、set基本一样,重点是底层结构不同。从名字也可以看出,unordered系列容器是不做排序的,想想也是,很多查询情况下也是不需要排序的。所以下面让我们看看它们的神奇之处吧!

相关概念介绍

        对于undered_map与unordered_set的使用不多赘述,否则偏离本篇文章的标题,使用细节可参考cplusplus.com/reference/icon-default.png?t=N7T8https://cplusplus.com/reference/,也可以通过刷相关题来加深印象。在简单介绍完相关概念之后,我们重点介绍底层的实现并且手把手实现一番。

  • 哈希概念

        有无这样一种理想的搜索方法,可以不经过任何比较,一次直接从表中得到要搜索的元素? 如果构造一种存储结构,通过某种映射函数使元素的存储位置与元素之间能够建立一一对应的关系,那么在查找时通过该函数可以很快找到该元素。

        使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希(散列)表(Hash Table),比如说,有元素集{1,7,14,9,22,68},哈希函数是hashi=Hash(key)%capacity,映射关系如下图

        当我们在查找某个元素时,只需要也通过Hash函数计算找到对应位置就能查询到位,不需要进行多次比较。

        除以上概念,还一个较为重要的概念就是载荷因子,定义为α=填入到表中的元素个数/散列表的长度,这个概念在扩容机制中会使用到,要重点关注记忆。

  • 哈希冲突与哈希函数

        在上面的例子当中,当我们再次插入元素24时会发生什么?插入不进去,因为24%10=4的地方已经存放了元素14了。那我们称这种现象叫做哈希冲突(碰撞),即不同关键字通过相同哈希哈数计算出相同的哈希地址,同时把具有不同关键码而具有相同哈希地址的数据元素称为同义词

        引起哈希冲突的一个原因可能是哈希函数设计不够合理,根据数据集合的特点选择正确的哈希函数,哈希函数设计原则:

①哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;

②哈希函数计算出来的地址能均匀分布在整个空间中;

③哈希函数应该比较简单。

常见哈希函数:

        ①直接定址法:取关于关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B;优点在于简单、均匀;缺点是需要事先知道关键字的分布情况,适合查找比较小且连续的元素集合;

        ②除留余数法:设散列表的容量是m,取一个不大于m但最接近或者等于m的质数p作为除数,根据哈希函数Hash(key) = key%p,将key转换成哈希地址,

        除了以上两种常用的之外,还有许多不常用的,了解即可,如平方取中法、折叠法、随机数法、数学分析法。

        针对于哈希冲突我们得有解决的办法,常见的方法有闭散列和开散列,下面重点介绍解决这两种方法。在stl中实现哈希表使用的就是开散列,之后我们实现时也是用这种方式,但闭散列的方法也相当经典,我们也着重介绍一下。 

闭散列

        闭散列,也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置,若找不到下一个空位置说明哈希表已经满了,则需要扩容。

        找下一个空位置的方法有两种,一种是线性探测法,即从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止;另一种是二次探测法,就是步长呈平方式的向后探测(或者左右探测),比如:

        对于线性探测法,当插入24时,计算hashi=4遇到14冲突了,则向后找到hashi=5的空位置存放元素24;对于二次探测法,当再次插入34时,计算hashi=4遇到14冲突了,则计算(hashi+1^2)%10=5遇到24再次冲突,计算(hashi+2^2)%10=8遇到68再次冲突,计算(hashi+3^2)%10=3不冲突,存放到下标为3的位置。插入是如此,查询也是如此,先计算哈希值,遇到冲突了根据解决冲突的方法查询下一个位置,直到遇到空位置说明未查询成功。

        思想如上,但实现起来彷佛有点麻烦,比如有两个同义词先后被插入,插入第二个同义词之后,删除第一个插入的同义词,之后查询第二个同义词,那是不是直接遇到空返回查询失败呢?很明显不对,第二个同义词是在的,因此我们在实现时引入一个标识元素状态的state,具体实现如下。

  • 框架

        我们知道,哈希表的数据结构是顺序表,所以这里使用vector,那里面的元素放什么呢?首先必然要放一个pair结构体来放对应的key和value,除此之外可以看到下方代码实现中还多了一个state属性,这个是为了标识当前下标元素的状态,其中包括,EMPTY表示该下标没有元素,EXIST表示该下标存在元素,DELETE表示该下标的元素已被删除,为什么要标识DELETE呢?

        举个例子,根据hash函数找到了对应下标,但是这个下标没有元素,那就一定代表所查找的关键字就不存在了嘛,不一定,有可能当前元素遇到了冲突被放到了其他位置,之后呢当前这个hash值的位置的元素被删除了,那就得标识DELETE,表示当前对应下标位置得元素不存在,但你要根据解决冲突得办法继续向后找,直到找到EMPTY为止。

        所以下方的HashData结构体就是哈希表的元素,状态属性用枚举实现;除此之外呢,可以看到HashFunc类模板,这个是解决不同类型关键字如何转化成下标的问题,比如说,一个int类型做关键字,很好理解,将其取模即可,但是遇到string做关键字如何应对呢?那我们就可以通过这个HashFunc类模板实现将string转化成整型的逻辑(比如字符相加或者相乘等等),重点在于要尽可能具有唯一性,如下实现的逻辑得到的整型更加合适(大佬研究所得)。

        将想要作为关键字转化成整型的逻辑实现在HashFunc之后,通过类模板参数传进哈希表的类中使用,如下代码中的class Hash=HashFunc(),这里是将此类模板作为了缺省值,之后在insert、find函数中实例化出对象即可使用。

代码:

enum State
{
	EXIST,
	DELETE,
	EMPTY
};

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

template<class T>
struct HashFunc
{
	size_t operator()(const T& t)
	{
		return (size_t)t;
	}
};
//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t sum = 0;
		for (auto i : s)
		{
			sum *= 131;
			sum += i;
		}
		return sum;
	}
};

template<class K, class V, class Hash=HashFunc<K>>
class HashTable
{
public:
    //...
private:
	vector<HashData<K, V>> _table;
	size_t _size = 0;   //存储有效数据的个数
};
  • 核心函数

        核心函数在这里我们介绍insert插入函数,find查找函数和erase删除函数,其他接口函数都较为简单,不再赘述。

        对于insert,首先是去重,即如果表中存在当前key,就直接插入失败,不再插入;其次就是扩容,当达到你想要的载荷因子(表元素/表容量)就选择扩容,这里我们选择使用复用的方法去扩容,即新实例化出一个哈希表对象,遍历旧表元素,复用insert函数将其插入到新表中,之后将旧表和新表的vector调换即可,新表就没用了,函数结束后自动析构掉;最后呢就是关键插入部分代码,通过hash函数映射出对应下标,如果冲突则使用线性探测法(后面介绍二次探测法)向后探测可以插入的位置,即遇到EXIST就继续找,遇到DELETE或EMPTY就插入,注意在向后找的过程中,记得将遍历下标取模,因为遍历到最后还需要回到开头继续找,找到之后设置好state及size即可。二次探测法与线性探测法相似,只不过线性探测法是顺次一个一个向后找,而二次探测法是平方着向后找(比如hashi+0²、hashi+1²、hashi+2²......,也比如hashi+0²、hashi+1²、hashi-1²、hashi+2²、hashi-2²......),实现逻辑很简单,下方代码可参考。

        对于find函数,实现逻辑也是很简单,通过hash函数映射出对应下标,遇到EMPTY就查找失败,遇到EXIST或DELETE就向后找,当遇到EXIST时判断是否与被查找key相等,注意查找成功时返回元素地址,失败时返回null。

        对于erase函数,相比之下更简单,使用find函数找到对应元素,将其状态改为DELETE及size-1即可,查找失败对应着删除失败。

代码:

	bool insert(const pair<K, V>& kv)
	{
		//去重
		if (find(kv.first))
			return false;

		//载荷因子大于0.7需要扩容
		if (_table.size() == 0 || 10 * _size / _table.size() >= 7)
		{
			size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
			HashTable<K,V,Hash> newHT;
			newHT._table.resize(newsize);
			for (auto& i : _table)
			{
				if (i._state == EXIST)
				{
					newHT.insert(i._kv);
				}
			}
			_table.swap(newHT._table);
		}
		Hash hash;
		size_t Hashi = hash(kv.first) % _table.size();
		//线性探测
	    while (_table[Hashi]._state == EXIST)   //元素存在就++,遇到删除或空就插入
		{
			Hashi++;
			Hashi %= _table.size();
		}
		_table[Hashi]._kv = kv;
		_table[Hashi]._state = EXIST;
		_size++;

		//二次探测
		//size_t i = 0;
		//while (_table[Hashi + i * i]._state == EXIST)
		//{
		//	 i++;
		//	 Hashi %= _table.size();
		//}
		//_table[Hashi + i * i]._kv = kv;
		//_table[Hashi + i * i]._state = EXIST;
		//_size++;

		return true;
	}

	HashData<K, V>* find(const K& key)
	{
		if (_size == 0 || _table.size() == 0)
			return nullptr;

		Hash hash;
		size_t Hashi = hash(key) % _table.size();
		while (_table[Hashi]._state != EMPTY)
		{
			if (_table[Hashi]._state == EXIST && _table[Hashi]._kv.first == key)
			{
				return &_table[Hashi];
			}
			Hashi++;
			Hashi %= _table.size();
		}
		return nullptr;
	}

	bool erase(const K& key)
	{
		HashData<K, V>* ret = find(key);
		if (!ret)
			return false;
		ret->_state = DELETE;
		--_size;   //注意控制哈希表属性size
		return true;
	}

开散列

        开散列,又叫做链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个,各个桶中的元素通过单链表链接起来,各链表的头结点存储在哈希表中,因此每一个桶中放的都是发生哈希冲突的元素,比如有集合{1,34,22,65,77,24,71,69,9,0},哈希函数是hashi=Hash(key)%capacity,映射关系如下图:

        思路如上,很好理解,并且实现起来不是很麻烦,个人认为开散列的性价比比闭散列要高,至少比它节省空间,下面看看具体实现。

  • 框架

        首先,哈希桶也是使用vector实现,因为放入的元素是由链表组成,所以vector的元素应当是一个指针变量,这里我们封装一个HBnode结构体,存储key+value组成的pair结构以及指向下一节点的next指针,除了属性之外,还包括节点的构造函数,用来创建节点以及初始化属性,因此,vector中存储的就是HBnode类型的指针。

        其次依旧是在哈希表中提到的HashFunc类模板,用以解决不同类型关键字如何转化成下标的问题,介绍如上。

代码:

template<class K, class V>
struct HBnode
{
	pair<K, V> _kv;
	HBnode<K, V>* _next;

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

	}
};

template<class T>
struct HashFunc
{
	size_t operator()(const T& t)
	{
		return (size_t)t;
	}
};
//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t sum = 0;
		for (auto i : s)
		{
			sum *= 131;
			sum += i;
		}
		return sum;
	}
};

template<class K, class V, class Hash=HashFunc<K>>
class HashBucket
{
	typedef HBnode<K, V> node;
public:
    //...
private:
	vector<node*> _buckets;
	size_t _size = 0;
};
  • 核心函数 

        对于insert函数,思路与哈希表的insert函数实现大体一样。扩容方面,不同于哈希表中存储了多少值,就会占用多少下标位置,哈希桶存储了多少值,与占用下标位置数没有关系,因为元素可以连接在同一个下标位置上,也就是形成一个桶,所以这里我们设置触发扩容的条件是元素个数达到vector容量;在哈希表中的扩容部分我们是使用了复用的方式,但是对于这种节点链表也适合吗?不太行,因为旧表上的节点可以重复利用,也就将其拆卸下来装到新表上,所以我们初始化一个新表HashBucket,遍历旧表,将旧表的节点拆卸下来再通过hash函数映射到新表上,之后交换旧表与新表的vector即可;插入方面就是创建新的节点,主要难点还是在于将其插入到链表中,但因其是旧知识,这里也不多赘述,实现可参考下方代码。

        对于find函数,相比之下比较简单,遍历表中的各个链表,找到对应节点,将其pair结构体的地址返回,找不到则返回null。

        对于erase函数,不能像哈希表中的erase实现一样,先调用find函数找到对应关键字再删除,注意这里删除是删除一个链表节点,我们知道删除链表节点必须找到当前节点的前一个节点,因此我们需要像find函数一样去遍历vector中的每一个链表,找到所要删除的节点,同时记录当前节点的前一个节点,下面就是链表节点的删除操作,只要着重注意一下头删和中间删的操作的区别,其他都是基操,代码参考下方。

代码:

bool insert(const pair<K, V>& kv)
	{
		Hash hash;
		if (find(kv.first))
			return false;

		if (_size == _buckets.size())
		{
			size_t newsize = _buckets.size() == 0 ? 10 : _buckets.size() * 2;
			HashBucket newHB;
			newHB._buckets.resize(newsize);
			for (size_t i = 0; i < _buckets.size(); i++)
			{
				node* cur = _buckets[i];
				while (cur)
				{
					node* curnext = cur->_next;
					size_t newHashi = hash(cur->_kv.first) % newHB._buckets.size();
					if (newHB._buckets[newHashi])
					{
						cur->_next = newHB._buckets[newHashi];
						newHB._buckets[newHashi] = cur;
					}
					else
					{
						newHB._buckets[newHashi] = cur;
					}
					cur = curnext;
				}
			}
			_buckets.swap(newHB._buckets);
		}
		size_t Hashi = hash(kv.first) % _buckets.size();
		node* Node = new node(kv);
		if (_buckets[Hashi])
		{
			Node->_next = _buckets[Hashi];
			_buckets[Hashi] = Node;
		}
		else
		{
			_buckets[Hashi] = Node;
		}
		_size++;
		return true;
	}

	pair<K, V>* find(const K& key)
	{
		if (_size == 0 || _buckets.size() == 0)
			return nullptr;
		Hash hash;
		size_t Hashi = hash(key) % _buckets.size();
		node* cur = _buckets[Hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
				return &cur->_kv;
			cur = cur->_next;
		}
		return nullptr;
	}

	bool erase(const K& key)
	{
		if (_size == 0 || _buckets.size() == 0)
			return false;
		Hash hash;
		size_t Hashi = hash(key) % _buckets.size();
		if (!_buckets[Hashi])
			return false;
		node* prev = _buckets[Hashi];
		if (prev->_kv.first == key)   //头删
		{
			_buckets[Hashi] = prev->_next;
			delete prev;
			_size--;
			return true;
		}
		node* cur = prev->_next;
		while (cur)
		{
			if (cur->_kv.first == key)   //中间删
			{
				prev->_next = cur->_next;
				delete cur;
				_size--;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}
		return false;
	}

哈希表(开散列)的修改

  • 迭代器实现

         首先考虑其成员变量需要包括节点指针以及指向此哈希表的指针,因为在实现迭代器++时,需要找到下一个元素,如果没有当前哈希表的信息,就无法找到下一个元素,所以需要一个指向哈希表类的指针,也是正因为如此,迭代器的实现需要哈希表,但是哈希表的实现也需要迭代器,造成了一个“你中有我,我中有你”的局面,需要使用前置声明的方法打破,因此在迭代器的实现之前前置声明了哈希表。

        对于构造函数,定义时需传入一个节点指针和当前哈希表指针用以初始化;对于解引用,得到节点中的数据;对于->,得到节点数据的地址;对于关系运算符,重点是比较迭代器的节点;对于迭代器++操作(这里仅强调前置++,后置++类似,并且迭代器只有++操作,无--操作,因为哈希表的迭代器是单向迭代器),根据当前迭代器节点的下一节点的存在情况进行讨论,若下一节点存在则直接将下一节点赋值给迭代器,若下一节点不存在,则通过哈希表指针遍历到当前桶的下一个桶,将下一个桶的第一个节点赋值给迭代器,若后面没有桶了,则给迭代器节点赋值空,代码参考下方。

代码:

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

template<class K, class T, class Hash, class KeyOfT>
struct __HashIterator
{
	typedef HBnode<T> node;
	typedef HashBucket<K, T, Hash, KeyOfT> HB;
	typedef __HashIterator<K, T, Hash, KeyOfT> Self;

	node* _node;
	HB* _hb;   //向上找不到HB的声明定义,需要HB前置声明一下
			   //那为什么不把迭代器定义在HB下面,因为HB中也有使用迭代器,是一个“你中有我,我中有你”的关系,需要前置声明打破一下

	__HashIterator(node* pnode, HB* phb)
		:_node(pnode)
		, _hb(phb)
	{

	}

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

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

	bool operator==(const Self& iterator) const
	{
		return _node == iterator._node;
	}

	bool operator!=(const Self& iterator) const
	{
		return _node != iterator._node;
	}

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

		return *this;
	}
};
  • 细节修改

        首先我们先加上对于迭代器最基本的begin()、end()。其中begin()是返回哈希桶中的第一个桶的第一个节点所构造的迭代器,实现逻辑很简单,即从头遍历表,找到第一个桶(很多地方需要用到哈希表类的成员属性_buckets,所以将迭代器类设置成哈希表类的友元,用以访问哈希表类的_buckets),end()是返回空迭代器,值得注意的是,构造迭代器时传入的哈希表指针可以使用this指针。

        其次就是加上迭代器后部分核心函数的修改,

①insert函数返回值变成了pair<iterator,bool>;

②find函数返回值变成了iterator;

③加入了哈希表的析构函数,即将所有桶的节点给释放掉,因为自带的析构函数只会析构掉vector,不会释放内部元素指向的节点。

代码:

template<class K, class T, class Hash, class KeyOfT>
class HashBucket
{
	typedef HBnode<T> node;

	template<class K, class T, class Hash, class KeyOfT>
	friend struct __HashIterator;
public:
	typedef __HashIterator<K, T, Hash, KeyOfT> iterator;
	iterator begin()
	{
		for (size_t i = 0; i < _buckets.size(); i++)
		{
			if (_buckets[i])
				return iterator(_buckets[i], this);
		}
		return end();
	}

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

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

	pair<iterator,bool> insert(const T& data)
	{
		Hash hash;
		KeyOfT kot;
		iterator ret = find(kot(data));
		if (ret != end())
			return make_pair(ret, false);

		if (_size == _buckets.size())
		{
			size_t newsize = _buckets.size() == 0 ? 10 : _buckets.size() * 2;
			HashBucket newHB;
			newHB._buckets.resize(newsize);
			for (size_t i = 0; i < _buckets.size(); i++)
			{
				node* cur = _buckets[i];
				while (cur)
				{
					node* curnext = cur->_next;
					size_t newHashi = hash(kot(cur->_data)) % newHB._buckets.size();
					if (newHB._buckets[newHashi])
					{
						cur->_next = newHB._buckets[newHashi];
						newHB._buckets[newHashi] = cur;
					}
					else
					{
						newHB._buckets[newHashi] = cur;
					}
					cur = curnext;
				}
			}
			_buckets.swap(newHB._buckets);
		}
		size_t Hashi = hash(kot(data)) % _buckets.size();
		node* Node = new node(data);
		if (_buckets[Hashi])
		{
			Node->_next = _buckets[Hashi];
			_buckets[Hashi] = Node;
		}
		else
		{
			_buckets[Hashi] = Node;
		}
		_size++;
		return make_pair(iterator(Node, this), true);
	}

	iterator find(const K& key)
	{
		if (_size == 0 || _buckets.size() == 0)
			return end();
		Hash hash;
		KeyOfT kot;
		size_t Hashi = hash(key) % _buckets.size();
		node* cur = _buckets[Hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
				return iterator(cur, this);
			cur = cur->_next;
		}
		return end();
	}

	bool erase(const K& key)
	{
		if (_size == 0 || _buckets.size() == 0)
			return false;
		Hash hash;
		size_t Hashi = hash(key) % _buckets.size();
		if (!_buckets[Hashi])
			return false;
		node* prev = _buckets[Hashi];
		if (kot(prev->_data) == key)   //头删
		{
			_buckets[Hashi] = prev->_next;
			delete prev;
			_size--;
			return true;
		}
		node* cur = prev->_next;
		while (cur)
		{
			if (kot(cur->_data) == key)   //中间删
			{
				prev->_next = cur->_next;
				delete cur;
				_size--;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}
		return false;
	}
private:
	vector<node*> _buckets;
	size_t _size = 0;
};

 unordered系列封装

        这里介绍unordered_map封装,unordered_set封装情况一致,代码参考如下。

        首先考虑成员属性包括一个哈希表实例化的一个对象,注意key依旧是key,但value却是一个pair结构体,unordered_set也是一样,key依旧是key,但value确实一个key,这是因为统一成使用kv模型中的v来存储值,而k仅用来索引,为什么要把key单独拿出来作为一个类模板的参数呢,因为一些地方还是需要用到key的(比如[]操作符需要传进一个key类型的一个对象,是需要用到key类型的);进而也需要一个KeyOfT的仿函数来返回key,unordered_map是传出pair结构体的first,而unordered_set仅是为了保持一致,传出key即可。

        其次,对于begin()、end()、insert()是调用了成员属性哈希表的成员函数,无需过多封装;[]操作符功能是传进key返回value,若表中不存在此key,则插入,对应value使用匿名对象进行默认构造,若存在则直接返回对象value,代码参考如下。

代码(unordered_map):

template<class K, class V, class Hash = HashFunc<K>>
class Unordered_map
{
	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;
	
	iterator begin()
	{
		return _HB.begin();
	}
	iterator end()
	{
		return _HB.end();
	}

	pair<iterator,bool> insert(const pair<K,V>& data)
	{
		return _HB.insert(data);
	}

	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = _HB.insert(make_pair(key, V()));
		return ret.first->second;
	}
private:
	HashBucket<K, pair<K,V>, Hash, mapKeyOfT> _HB;
};

 代码(unordered_set):

template<class K, class Hash = HashFunc<K>>
class Unordered_set
{
	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& data)
	{
		return _HB.insert(data);
	}
private:
	HashBucket<K, K, Hash, setKeyOfT> _HB;
};

 后记

        unordered系列容器是c++11提出的,完美地弥补了map与set在多元素情况下查询效率慢的缺点,其使用与它们并无太大的差别,但实现难度上个人认为比set和map底层的红黑树实现要容易许多。本篇重点讲解了底层实现,其使用方法也不可小视,在一些笔试题、oj题上都需要对这些容器的熟练使用,同时两手抓才能将知识点学的扎实,好了,unordered系列容器介绍就是这样,拜拜!


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

集合贴4——QA机器人设计与优化

基础课21——知识库管理-CSDN博客文章浏览阅读342次&#xff0c;点赞6次&#xff0c;收藏2次。知识库中有什么信息内容&#xff0c;决定了智能客服机器人在回答时可以调用哪些信息内容&#xff0c;甚至可以更简单地理解为这是智能客服机器人的话术库。https://blog.csdn.net/22…

Leetcode-2 两数相加

不知道为什么有些测试用例通不过&#xff0c;思路很明晰&#xff0c;改不明白了&#xff0c;求大佬指点&#xff01;&#xff01;&#xff01;&#xff01; /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNo…

写一下关于部署项目到服务器的心得(以及遇到的难处)

首先要买个服务器(本人的是以下这个) 这里我买的是宝塔面板的,没有宝塔面板的也可以自行安装 点击登录会去到以下页面 在这个界面依次执行下面命令会看到账号和密码和宝塔面板内外网地址 sudo -s bt 14点击地址就可以跳转宝塔对应的内外网页面 然后使用上述命令提供的账号密…

《006.Springboot+vue之旅游信息推荐系统》【有文档】

《006.Springbootvue之旅游信息推荐系统》【有文档】 项目简介 [1]本系统涉及到的技术主要如下&#xff1a; 推荐环境配置&#xff1a;DEA jdk1.8 Maven MySQL 前后端分离; 后台&#xff1a;SpringBootMybatis; 前台&#xff1a;vueElementUI; [2]功能模块展示&#xff1a; …

计算机是如何进行工作的+进程和线程

一)计算机是如何工作的? 指令是如何执行的?CPU基本工作过程&#xff1f; 假设上面有一些指令表&#xff0c;假设CPU上面有两个寄存器A的编号是00&#xff0c;B的编号是01 1)第一个指令0010 1010&#xff0c;这个指令的意思就是说把1010地址上面的数据给他读取到A寄存器里面 2…

数据结构-图的应用

最小生成树&#xff08;最小代价树&#xff09; 对于一个带权连通无向图G(V,E)&#xff0c;生成树不同&#xff0c;每棵树的权&#xff08;即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合&#xff0c;若T为R中边的权值之和最小的生成树&#xff0c;则T称为G的…

商城免费搭建之java商城 java电子商务Spring Cloud+Spring Boot+mybatis+MQ+VR全景+b2b2c

1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、Redis 3. 前端框架…

工业级环网交换机:高效过滤和转发数据包的网络设备

环形网络是一种网络拓扑结构&#xff0c;其特点是将每个设备连成一个连续的环形。它可以确保一台设备发出的信号可以被环上所有其他设备接收到。环网冗余是指工业级环网交换机是否能够应对网络线缆连接中断的情况。当出现连接中断时&#xff0c;工业级环网交换机会接收到此消息…

常见排序算法之快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。 基本思想为∶任取待排序元素序列中的某元素作为基准值&#xff0c;按照该排序码将待排序集合分割成两子序列&#xff0c;左子序列中所有元素均小于基准值&#xff0c;右子序列中所有元素均大于基准值&#xff0c;…

【Pytest】跳过执行之@pytest.mark.skip()详解

一、skip介绍及运用 在我们自动化测试过程中&#xff0c;经常会遇到功能阻塞、功能未实现、环境等一系列外部因素问题导致的一些用例执行不了&#xff0c;这时我们就可以用到跳过skip用例&#xff0c;如果我们注释掉或删除掉&#xff0c;后面还要进行恢复操作。 1、skip跳过成…

nodejs express multer 保存文件名为中文时乱码,问题解决 originalname

nodejs express multer 保存文件名为中文时乱码&#xff0c;问题解决 originalname 一、问题描述 用 express 写了个后台&#xff0c;在接收文件并保存的时候 multer 接收到的文件名为乱码。 二、解决 找了下解决方法&#xff0c;在 github 的 multer issue 中找到了答案 参…

【MySQL进阶之路丨第十七篇(完结)】一文带你精通MySQL运算符

引言 在上一篇中我们介绍了MySQL函数&#xff1b;在开发中&#xff0c;对MySQL运算符的运用是十分重要的。这一篇我们使用命令行方式来帮助读者掌握MySQL中运算符的操作。 上一篇链接&#xff1a;【MySQL进阶之路丨第十六篇】一文带你精通MySQL函数 MySQL运算符 MySQL中的运…

node插件MongoDB(四)—— 库mongoose 的个性话读取(字段筛选、数据排序、数据截取)(四)

文章目录 一、字段筛选二、数据排序三、数据截取1. skip 跳过2. limit 限定![在这里插入图片描述](https://img-blog.csdnimg.cn/c7067b1984ee4c6686f8bbe07cae9176.png) 一、字段筛选 字段筛选&#xff1a;只读取指定的数据&#xff0c;比如集合&#xff08;表&#xff09;中有…

JMeter 相关的面试题

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;加入1000人软件测试技术学习交流群&#x1f4e2;资源分享&#xff1a;进了字节跳动之后&#xff0c;才…

【已验证】php配置连接sql server中文乱码(解决方法)更改utf-8格式

解决数据库中的中文数据在页面显示乱码的问题 在连接的$connectionInfo中设置"CharacterSet" > "UTF-8"&#xff0c;指定编码方式即可 $connectionInfo array("UID">$uid, "PWD">$pwd, "Database">$database…

提升采购订单管理效率的五个最佳实践

企业每年都要订购成千上万的商品和服务。公司运营和发展所需的一切都来自庞大的供应商网络&#xff0c;而沟通这些需求的主要方式是通过采购订单。 由于所有订单都会流经系统&#xff0c;而且每个月都会发生数千元不等的供应支出&#xff0c;因此掌握采购订单流程成为重中之重…

[autojs]逍遥模拟器和vscode对接

第一步&#xff1a;启动autojs服务 第二步&#xff1a;去cmd查看ip地址&#xff0c;输入ipconfig 第三步&#xff1a;打开逍遥模拟器中的sutojs-左上角- 连接电脑&#xff0c;然后输入WLAN或者其他ip也行&#xff0c;根据自己电脑实际情况确认 此时vscode显示连接成功。我们写…

selenium 自动化测试 1-如何搭建自动化测试环境,搭建环境过程应该注意的问题

最近也有很多人私下问我&#xff0c;selenium学习难吗&#xff0c;基础入门的学习内容很多是3以前的版本资料&#xff0c;对于有基础的人来说&#xff0c;3到4的差别虽然有&#xff0c;但是不足以影响自己&#xff0c;但是对于没有学过的人来说&#xff0c;通过资料再到自己写的…

Pinia 是什么?Redux、Vuex、Pinia 的区别?

结论先行&#xff1a; Pinia 是 Vue 官方团队开发的一个全新状态管理库。核心都是解决组件间的通信和数据的共享问题。 它和 Vuex 的区别呢&#xff0c;一个是虽然它和 Vuex 类似&#xff0c;但 Pinia 使用起来更加简单和直观。因为 Pinia 基于 Vue3 的组合式 API 风格&…

【Unity】零基础实现塔防游戏中敌人沿固定路径移动的功能

目录 场景搭建 烘焙(Bake) 敌人动作控制 脚本实现 我们知道&#xff0c;在一些塔防小游戏中&#xff0c;敌人往往会沿着给定的一条路径移动&#xff0c;我们在条路的路边会布置防御设施&#xff0c;攻击消灭敌人&#xff0c;阻止敌人到达终点。 场景搭建 我们首先新建一个…