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

news2024/12/23 15:37:01

目录

前言

相关概念介绍

哈希概念

哈希冲突与哈希函数

闭散列

框架

核心函数

开散列

框架

核心函数 

哈希表(开散列)的修改

迭代器实现

细节修改

 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.coloradmin.cn/o/1185893.html

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

相关文章

亚信科技斩获“鼎新杯”多项大奖!AntDB数据库在信创赛道再创佳绩

近日&#xff0c;第二届“鼎新杯”数字化转型应用大赛全国总决赛在北京落下帷幕&#xff0c;亚信科技成功收获一等奖1项、二等奖1项、三等奖3项、行业标杆奖1项。 “两江协同创新区智慧园区项目&#xff08;二期&#xff09;”斩获两项殊荣 在“行业数字化融合方向-智慧园区”…

Java并发工具-4-并发框架(ExecutorForkJoin)

一 Executor 并发框架介绍 1 整体结构介绍 executor [ɪɡˈzekjətə(r)] 执行者 execute [ˈeksɪkjuːt] 执行 从 JDK 1.5 开始&#xff0c;java 中将工作单元和执行机制做了分离&#xff0c;于是 Executor 并行框架出现。 什么是工作单元&#xff08;或称为任务&#xff…

Leetcode 第 369 场周赛题解

Leetcode 第 369 场周赛题解 Leetcode 第 369 场周赛题解题目1&#xff1a;2917. 找出数组中的 K-or 值思路代码复杂度分析 题目2&#xff1a;2918. 数组的最小相等和思路代码复杂度分析 题目3&#xff1a;2919. 使数组变美的最小增量运算数思路代码复杂度分析 题目4&#xff1…

合并两个有序链表OJ

合并两个有序链表OJ 文章目录 合并两个有序链表OJ一、题目及要求二、思路分析三、代码实现 一、题目及要求 二、思路分析 其次&#xff0c;题目里说了新链表是通过拼接原来的结点形成的&#xff0c;所以说我们不需要开辟新的空间。 三、代码实现 if (list1 NULL) {return li…

在字节4年,一个27岁女软件测试工程师的心路历程

个人经验分享 简单的先说一下&#xff0c;坐标深圳&#xff0c;18届本科毕业&#xff0c;算上在字节的面试&#xff0c;一共有面试了5家公司&#xff08;不想请假&#xff0c;所以只是每个晚上去其他公司面试&#xff0c;面试的公司就比较少&#xff09; 其中面试成功的有3家&…

vue基础知识十八:说说你对keep-alive的理解是什么?

一、Keep-alive 是什么 keep-alive是vue中的内置组件&#xff0c;能在组件切换过程中将状态保留在内存中&#xff0c;防止重复渲染DOM keep-alive 包裹动态组件时&#xff0c;会缓存不活动的组件实例&#xff0c;而不是销毁它们 keep-alive可以设置以下props属性&#xff1a…

js各种简单事件处理(整理)

**## 获取当天昨天日期** // 当天日期 const today new Date();// 格式化当天日期为 YYYY-MM-DD 格式 const formattedToday today.toISOString().slice(0, 10);// 昨天日期 const yesterday new Date(); yesterday.setDate(yesterday.getDate() - 1);// 格式化昨天日期为 Y…

2023年11月在线IDE流行度最新排名

点击查看最新在线IDE流行度最新排名&#xff08;每月更新&#xff09; 2023年11月在线IDE流行度最新排名 TOP 在线IDE排名是通过分析在线ide名称在谷歌上被搜索的频率而创建的 在线IDE被搜索的次数越多&#xff0c;人们就会认为它越受欢迎。原始数据来自谷歌Trends 如果您相…

广和通5G模组FM650助力阿里云打造无影魔方Pro

随着云基础设施的完善及云电脑体验的不断优化&#xff0c;越来越多的个人和企业选择无影云电脑进行办公。基于云原生的云网端技术架构&#xff0c;无影云电脑相比传统PC&#xff0c;具有弹性、安全、保障个人数据等产品优势。 10月31日&#xff0c;阿里云在杭州云栖大会上宣布…

易货:一种古老而新颖的交易方式

在当今快速发展的经济环境中&#xff0c;易货模式正逐渐引起人们的关注。这种古老而新颖的交易方式&#xff0c;不仅为企业提供了新的商业机会&#xff0c;还为消费者带来了更多的选择。本文将详细介绍易货模式的概念、优势以及如何实现易货交易&#xff0c;并探讨这种模式未来…

精解括号匹配问题与极致栈设计:揭开最大栈和最小栈的奥秘

目录 括号匹配问题最小栈最大栈 最大栈和最小栈是极致栈的两个重要变种。最大栈用于存储当前匹配的最大值&#xff0c;而最小栈用于存储当前匹配的最小值。 括号匹配问题 这个问题我们来看力扣20题的描述&#xff1a; 给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’…

如何修改文件的修改日期?

如何修改文件的修改日期&#xff1f;文件的修改日期指的是文件最近一次被修改的日期和时间。当文件内容被修改、编辑或更新时&#xff0c;系统会自动更新文件的修改日期。这个日期记录了文件内容的实际修改时间&#xff0c;可以帮助用户了解文件的更新情况以及文件版本的管理。…

[云原生案例2.2 ] Kubernetes的部署安装 【单master集群架构 ---- (二进制安装部署)】网络插件部分

文章目录 1. Kubernetes的网络类别2. Kubernetes的接口类型3. CNI网络插件 ---- Flannel的介绍及部署3.1 简介3.2 flannel的三种模式3.3 flannel的UDP模式工作原理3.4 flannel的VXLAN模式工作原理3.5 Flannel CNI 网络插件部署3.5.1 上传flannel镜像文件和插件包到node节点3.5.…

2023年度API安全状况详解

随着云计算和移动应用的快速发展&#xff0c;API&#xff08;应用程序接口&#xff09;已成为不可或缺的技术组成部分。然而&#xff0c;API的广泛使用也带来了安全风险。本文将探讨2023年的API安全状况&#xff0c;并介绍了一些应对这些安全挑战的最佳实践。 引言 随着全球互联…

改进YOLOv5:结合ICCV2023|动态蛇形卷积,构建不规则目标识别网络

🔥🔥🔥 提升多尺度、不规则目标检测,创新提升 🔥🔥🔥 🔥🔥🔥 捕捉图像特征和处理复杂图像特征 🔥🔥🔥 👉👉👉: 本专栏包含大量的新设计的创新想法,包含详细的代码和说明,具备有效的创新组合,可以有效应用到改进创新当中 👉👉👉: �…

安装ubuntu-20.04.6-desktop版本、根据ISO文件制作U盘启动盘

前言 本文简述&#xff0c;安装Ubuntu20.04.6的过程&#xff0c;包括制作U盘启动盘、安装。 下载Ubuntu镜像 去官网下载桌面版ubuntu-20.04.6镜像&#xff0c;下载完后文件名是ubuntu-20.04.6-desktop-amd64.iso&#xff0c;这里有个问题amd64.iso能安装在intel处理器的电脑…

计算机网络学习笔记(五):运输层(待更新)

5.1 概述 5.1.1 TCP协议的应用场景 TCP为应用层协议提供可靠传输&#xff0c;发送端按顺序发送&#xff0c;接收端按顺序接收&#xff0c;其间发送丢包、乱序&#xff0c;TCP负责重传和排序。下面是TCP的应用场景。 多次交互&#xff1a;客户端程序和服务端程序需要多次交互才…

ai实景直播矩阵式引流---技术开发搭建(剪辑、矩阵、直播)

目前我们的短视频矩阵剪辑分发系统更新&#xff1a; 无人直播更新&#xff1a; 1、新增文案引流&#xff1a;已接入混元数据大模型&#xff0c;千帆数据大模型&#xff0c;星火数据大模型&#xff0c;盘古数据大模型&#xff0c;通义数据大模型&#xff0c;ChatGPT数据大模型…

muduo源码剖析之TimerQueue类

简介 TimerQueue ​ 通过timerfd实现的定时器功能&#xff0c;为EventLoop扩展了一系列runAt&#xff0c;runEvery&#xff0c;runEvery等函数TimerQueue中通过std::set维护所有的Timer&#xff0c;也可以使用优先队列实现 muduo的TimerQueue是基于timerfd_create实现&#…

Jupyter Notebook 内核似乎挂掉了,它很快将自动重启

报错原因&#xff1a; OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized. OMP: Hint This means that multiple copies of the OpenMP runtime have been linked into the program. That is dangerous, since it can degrade perfo…