【数据结构】深入理解哈希及其底层数据结构

目录

一、unordered系列关联式容器

二、底层结构

2.1 哈希的概念

 2.2 哈希冲突(哈希碰撞)

2.3 哈希函数

2.4 哈希冲突处理

2.4.1 闭散列(开放定址法)

2.4.1.1 代码实现:

2.4.2 开散列(链地址法,较优)

2.4.2.1 扩容

 2.4.2.2 仿函数实现多类型储存

2.4.2.3 代码实现 

 2.4.3 开散列与闭散列比较

 三、哈希表的模拟实现(加迭代器)

1.unordered_set

 2.unordered_map.h

3.test.c


一、unordered系列关联式容器

        在C++11中一共添加了4个unordered系列关联式容器,它们提供了基于哈希表的实现,以平均常数时间复杂度进行元素的查找、插入和删除操作。  分别为

std::unordered_map       std::unordered_multimap
std::unordered_set       std::unordered_multiset

        这些unordered系列的容器与STL中的mapmultimapsetmultiset相对应,但后者是基于红黑树实现的,提供的是有序的元素集合,而前者的实现则基于哈希表,提供的是无序的元素集合,且在平均情况下,对元素的查找、插入和删除操作具有更好的性能。

二、底层结构

unordered系列的容器之所以效率高,归功于它的底层哈希结构。 

2.1 哈希的概念

        在顺序结构或者树中,元素的储存位置与其值没有什么对应关系,一般查找一个值时我们都需要去经过多次比较,时间复杂度为O(n),平衡树中为树的高度,效率与比较次数直接挂钩。

这时我们想有没有一个理想的方法可以不经过任何比较,一次直接从表中得到要搜索的元素呢?

        如果能够构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素

 在插入时:根据待插入元素的关键码,用哈希函数计算出该元素的存储位置并存放

 在查找时:同插入一样,对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

        大佬们将这种方法称为哈希(或者散列)方法,而哈希方法中计算存储位置的函数称为哈希函数,构造出的表叫做哈希表(散列表)。

例如:

 2.2 哈希冲突(哈希碰撞)

 在插入时有时多个值通过哈希函数的计算会计算处相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

2.3 哈希函数

哈希冲突是无法避免的,但只要我们设计出合理的哈希函数,就能极大的降低哈希冲突的概率 

哈希函数的设计有几个原则:

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

常见的哈希函数:   

1. 直接定址法(常用) :

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况,适合查找比较小且连续的情况

 2.除留余数法(常用):

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

常用的哈希函数还有数字分析法、平方取中法、折叠法、随机数法等,但上述两种方法最为常用。

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

2.4 哈希冲突处理

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

2.4.1 闭散列(开放定址法)

 闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

这里我们主要讲解线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

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

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

删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会              影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此            线性探测采用标记的伪删除法来删除一个元素

2.4.1.1 代码实现:
template<class K>
class HashFunc
{
public:
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<>
class HashFunc<string>
{
public:
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};

namespace Open_address
{
	//枚举状态
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K,class V>
	struct HashData
	{
		pair<K, V> _kv;//值
		State _state = EMPTY;//状态标记
	};

	template<class K,class V,class Hash=HashFunc<K>>
	class HashTable
	{
	public:
		HashTable(size_t size = 10)
		{
			_tables.resize(size);
		}
		HashData<K, V>* Find(const K& key)
		{
			Hash hs;
			// 线性探测
			size_t hashi = hs(key) % _tables.size();
			while (_tables[hashi]._state != EMPTY)
			{
				if (key == _tables[hashi]._kv.first
					&& _tables[hashi]._state == EXIST)
				{
					return &_tables[hashi];
				}

				++hashi;
				hashi %= _tables.size();
			}

			return nullptr;
		}
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			if (_n * 10 / _tables.size() >= 7)
			{
				HashTable<K, V, Hash> newHT(_tables.size() * 2);
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHT.Insert(e._kv);
					}
				}
				_tables.swap(newHT._tables);
			}
			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();
			while (_tables[hashi]._state==EXIST)
			{
				hashi++;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;

			return true;
		}

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

负载因子:表内元素/表的长度 

 对于开放寻址法来说,由于所有元素都存储在哈希表的数组中,并且不使用额外的数据结构(如链表)来处理冲突,因此负载因子的控制尤为重要。一旦负载因子过高,就可能导致哈希表性能急剧下降,因为插入和查找操作可能需要遍历更多的槽位才能找到所需元素或空槽位。

一般控制在0.7~0.8之间 

闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

2.4.2 开散列(链地址法,较优)

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

开散列中每个桶中放的都是发生哈希冲突的元素

2.4.2.1 扩容

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

//负载因子到1就扩容
if (_n == _tables.size())
{
	vector<Node*> newTables(_tables.size() * 2, nullptr);
	for (size_t i = 0; i < _tables.size(); i++)
	{
		// 取出旧表中节点,重新计算挂到新表桶中
		Node* cur = _tables[i];
		while (cur)
		{
			Node* next = cur->_next;

			// 头插到新表
			size_t hashi = hs(cur->_kv.first) % newTables.size();
			cur->_next = newTables[hashi];
			newTables[hashi] = cur;

			cur = next;
		}
		_tables[i] = nullptr;
	}

	_tables.swap(newTables);
}
 2.4.2.2 仿函数实现多类型储存

如果想要存储各种类型的数据,我们可以通过传仿函数来实现

template<class K>
class HashFunc
{
public:
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};


//特化
template<>
class HashFunc<string>
{
public:
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};

template<class K,class V,class Hash=HashFunc<K>>
class Hashtable;
2.4.2.3 代码实现 

代码实现:

template<class K>
class HashFunc
{
public:
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<>
class HashFunc<string>
{
public:
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};

namespace hash_bucket
{
	template<class K,class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _kv;

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

	template<class K,class V,class Hash=HashFunc<K>>
	class Hashtable
	{
		typedef HashNode<K, V> Node;
	public:
		Hashtable()
		{
			_tables.resize(10, nullptr);
			_n = 10;
		}
		~Hashtable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;

					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		Node* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
					cur = cur->_next;
			}
			return nullptr;
		}
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			Hash hs;
			//负载因子到1就扩容
			if (_n == _tables.size())
			{
				vector<Node*> newTables(_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					// 取出旧表中节点,重新计算挂到新表桶中
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插到新表
						size_t hashi = hs(cur->_kv.first) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}
			size_t hashi = hs(kv.first) % _tables.size();
			Node* newNode = new Node(kv);

			//头插
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;

			_n++;
			return true;
		}
		bool Erase(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					// 删除
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						_tables[hashi] = cur->_next;
					}

					delete cur;

					--_n;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		vector<Node*> _tables;
		size_t _n;
	};

}

 2.4.3 开散列与闭散列比较

        应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。

        事实上, 由于开地址法必须保持大量的空闲空间以确保搜索效率,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

 三、哈希表的模拟实现(加迭代器)

1.unordered_set

unordered_set.h

#pragma once
#include"Hashtable.h"

namespace L
{
	template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename hash_bucket::Hashtable<K, const K, SetKeyOfT, Hash>::iterator iterator;

		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		bool insert(const K& key)
		{
			return _ht.Insert(key);
		}
		bool find(const K& key)
		{
			return _ht.Find(key);
		}
		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		hash_bucket::Hashtable<K, const K, SetKeyOfT, Hash> _ht;
	};


	
}

Hashtable.h 

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<string>

template<class K>
class HashFunc
{
public:
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<>
class HashFunc<string>
{
public:
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};


namespace hash_bucket
{
	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 KeyOfT,class Hash>
	struct __HTIterator
	{
		typedef HashNode<T> Node;
		typedef Hashtable<K, T, KeyOfT, Hash> HT;
		typedef __HTIterator<K, T, KeyOfT, Hash> Self;

		Node* _node;
		HT* _ht;

		__HTIterator(Node* node,HT* ht)
			:_node(node)
			,_ht(ht)
		{}
		T& operator*()
		{
			return _node->_data;
		}
		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				KeyOfT kot;
				Hash hs;
				size_t hashi = hs(kot(_node->_data)) %_ht->_tables.size();
				hashi++;

				while (hashi < _ht->_tables.size())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;
					}

					hashi++;
				}
				if (hashi == _ht->_tables.size())
				{
					_node = nullptr;
				}
			}
			return *this;
		}
		bool operator!=(const Self& s)
		{
			return _node != s._node;
		}
	};

	template<class K, class T, class KeyOfT,class Hash>
	class Hashtable
	{
		typedef HashNode<T> Node;
		//友元 
		template<class K, class T, class KeyOfT, class Hash>
		friend struct __HTIterator;
	public:
		typedef __HTIterator<K, T, KeyOfT, Hash> iterator;
		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return iterator(_tables[i], this);
				}
			}
			return end();
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}
		Hashtable()
		{
			_tables.resize(10, nullptr);
			_n = 10;
		}
		~Hashtable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;

					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		Node* Find(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}
		bool Insert(const T& data)
		{
			KeyOfT kot;
			if(Find(kot(data)))
				return false;
			Hash hs;
			//负载因子到1就扩容
			if (_n == _tables.size())
			{
				vector<Node*> newTables(_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					// 取出旧表中节点,重新计算挂到新表桶中
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插到新表
						size_t hashi = hs(kot(cur->_data)) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}
			size_t hashi = hs(kot(data)) % _tables.size();
			Node* newNode = new Node(data);

			//头插
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;

			_n++;
			return true;
		}
		bool Erase(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->kot(cur->_data) == key)
				{
					// 删除
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						_tables[hashi] = cur->_next;
					}

					delete cur;

					--_n;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		vector<Node*> _tables;
		size_t _n;
	};
}

 2.unordered_map.h

#pragma once
#include"Hashtable.h"
namespace L
{
	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 hash_bucket::Hashtable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}

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

		bool insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}
	private:
		hash_bucket::Hashtable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};
	
}

Hashtable.h

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<string>

template<class K>
class HashFunc
{
public:
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<>
class HashFunc<string>
{
public:
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};


namespace hash_bucket
{
	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 KeyOfT,class Hash>
	struct __HTIterator
	{
		typedef HashNode<T> Node;
		typedef Hashtable<K, T, KeyOfT, Hash> HT;
		typedef __HTIterator<K, T, KeyOfT, Hash> Self;

		Node* _node;
		HT* _ht;

		__HTIterator(Node* node,HT* ht)
			:_node(node)
			,_ht(ht)
		{}
		T& operator*()
		{
			return _node->_data;
		}
		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				KeyOfT kot;
				Hash hs;
				size_t hashi = hs(kot(_node->_data)) %_ht->_tables.size();
				hashi++;

				while (hashi < _ht->_tables.size())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;
					}

					hashi++;
				}
				if (hashi == _ht->_tables.size())
				{
					_node = nullptr;
				}
			}
			return *this;
		}
		bool operator!=(const Self& s)
		{
			return _node != s._node;
		}
	};

	template<class K, class T, class KeyOfT,class Hash>
	class Hashtable
	{
		typedef HashNode<T> Node;
		//友元 
		template<class K, class T, class KeyOfT, class Hash>
		friend struct __HTIterator;
	public:
		typedef __HTIterator<K, T, KeyOfT, Hash> iterator;
		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return iterator(_tables[i], this);
				}
			}
			return end();
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}
		Hashtable()
		{
			_tables.resize(10, nullptr);
			_n = 10;
		}
		~Hashtable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;

					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		Node* Find(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}
		bool Insert(const T& data)
		{
			KeyOfT kot;
			if(Find(kot(data)))
				return false;
			Hash hs;
			//负载因子到1就扩容
			if (_n == _tables.size())
			{
				vector<Node*> newTables(_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					// 取出旧表中节点,重新计算挂到新表桶中
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插到新表
						size_t hashi = hs(kot(cur->_data)) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}
			size_t hashi = hs(kot(data)) % _tables.size();
			Node* newNode = new Node(data);

			//头插
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;

			_n++;
			return true;
		}
		bool Erase(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->kot(cur->_data) == key)
				{
					// 删除
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						_tables[hashi] = cur->_next;
					}

					delete cur;

					--_n;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		vector<Node*> _tables;
		size_t _n;
	};
}

3.test.c

#include"My_unordered_map.h"
#include"My_unordered_set.h"
//void test_set1()
//{
//	L::unordered_set<int> us;
//	us.insert(3);
//	us.insert(1);
//	us.insert(5);
//	us.insert(15);
//	us.insert(45);
//	us.insert(7);
//
//	L::unordered_set<int>::iterator it = us.begin();
//	while (it != us.end())
//	{
//		//*it += 100;
//		cout << *it << " ";
//		++it;
//	}
//	cout << endl;
//
//	for (auto e : us)
//	{
//		cout << e << " ";
//	}
//	cout << endl;
//}
//
//void test_map1()
//{
//	L::unordered_map<string, string> dict;
//	dict.insert(make_pair("sort", "1"));
//	dict.insert(make_pair("left", "2"));
//	dict.insert(make_pair("right", "3"));
//
//
//	L::unordered_map<string, string>::iterator it = dict.begin();
//	while (it != dict.end())
//	{
//		cout << (*it).first << " " << (*it).second << endl;
//		//cout << *it.first << " " << *it.second << endl;
//		/*pair<string, string> t=*it;
//		cout << t.first << " " << t.second<<endl;*/
//		++it;
//	}
//	cout << endl;
//	//for (auto& kv : dict)
//	//{
//	//	//kv.first += 'x';
//	//	kv.second += 'y';
//
//	//	cout << kv.first << ":" << kv.second << endl;
//	//}
//}

int main()
{
//	L::test_set1();
//	L::test_map1();
	return 0;
}

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

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

相关文章

高职计算机网络实训室

一、高职计算机网络实训室建设的背景 如今&#xff0c;数字化发展已成为国家发展的战略方向&#xff0c;是推动社会进步和经济发展的重要动力。在这一时代背景下&#xff0c;计算机网络技术作为数字化发展的基础设施&#xff0c;其地位和作用愈发凸显。因此&#xff0c;高职院…

Python数据分析-乳腺癌诊断分析预测

一、研究背景 乳腺癌是全球女性中最常见的癌症之一&#xff0c;发病率和死亡率都处于较高水平。据世界卫生组织&#xff08;WHO&#xff09;统计&#xff0c;乳腺癌每年造成数百万女性的死亡&#xff0c;并且其发病率在许多国家呈上升趋势。乳腺癌的早期诊断对于提高患者的生存…

帕金森老人的锻炼建议

对于帕金森病老人来说&#xff0c;适当的锻炼可以帮助改善症状、增强肌肉力量、提高关节灵活性&#xff0c;并预防长期并发症。以下是一些基于最新信息的锻炼建议&#xff1a; 选择合适的运动类型&#xff1a;包括有氧运动、抗阻运动和牵伸运动。有氧运动如快走、慢跑、游泳和舞…

旅游景区度假村展示型网站如何建设渠道品牌

景区、度假村、境外旅游几乎每天的人流量都非常高&#xff0c;还包括本地附近游等&#xff0c;对景区及度假村等固定高流量场所&#xff0c;品牌和客户赋能都是需要完善的&#xff0c;尤其是信息展示方面&#xff0c;旅游客户了解前往及查看信息等。 通过雨科平台建设景区度假…

收银系统源码-视频介绍

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物等连锁店使用。 详细介绍请…

目标检测基本标注工具-labelImg安装与使用

&#x1f349;一、安装 1.1 打开conda创建虚拟环境&#x1f388; conda create -n labelImg python3.8 -y 1.2 激活labelImg虚拟环境&#x1f388; activate labelImg1.3 安装labelImg&#x1f388; pip install -i https://pypi.tuna.tsinghua.edu.cn/simple lab…

LayoutLMv1

近年来&#xff0c;预训练技术在各种NLP任务中得到了成功的验证。尽管NLP应用程序广泛使用预训练模型&#xff0c;但它们几乎只关注文本级操作&#xff0c;而忽略了对文档图像理解至关重要的布局和样式信息。在本文中&#xff0c;我们提出了LayoutLM来联合建模文本和布局信息在…

【走出阴霾,拥抱阳光】当心情陷入抑郁,我们该如何自救?

在这个快节奏的时代&#xff0c;我们时常会感受到生活的压力和种种不如意。当心情长时间处于低落状态&#xff0c;甚至影响到日常生活时&#xff0c;我们或许已经步入了抑郁的阴影。面对这种情况&#xff0c;我们不必过于恐慌&#xff0c;更不能自暴自弃。接下来&#xff0c;就…

简单分享下利用python做测试的学习方向

做为一名转行过来的工程师&#xff0c;我想分享一下这些年来&#xff0c;我对于技术是怎样晋升的&#xff0c;我是在职&#xff0c;边上班边利用时间学习起来的&#xff0c;也听过很多业内人的分享&#xff08;简单可以总结以下几点&#xff0c;分享给大家碎片的式学习方式&…

Java小白入门到实战应用教程-开发环境搭建-JDK安装详细教程

Java小白入门到实战应用教程-JDK安装详细教程 writer:eleven 开发环境搭建 上节内容补充 在带领大家搭建开发环境前&#xff0c;先来了解一些java领域的名词。 Java根据应用领域区别可分为三个版本&#xff1a; JavaSE&#xff1a;是Java的标准版&#xff0c;提供了Java的…

Java 常用的参数校验,简化参数校验,赶紧学起来!!

Java 常用的参数校验&#xff0c;简化参数校验&#xff0c;赶紧学起来&#xff01;&#xff01;Java中的参数校验注解主要用于简化数据验证的过程&#xff0c;它们允许开发者以声明式的方式指定参数的验证规则&#xff0c;而无需在业https://mp.weixin.qq.com/s?__bizMzkzMTY0…

289个地级市-资源型城市划分数据

资源型城市&#xff1a;经济地理的独特现象与可持续发展的挑战 资源型城市是指那些以丰富的自然资源为基础&#xff0c;对国家经济和工业化进程有着重要影响的城市。这些城市在国家现代化建设中扮演着关键角色&#xff0c;其发展状况直接关系到区域经济的繁荣与社会的稳定。 资…

使用ffmpeg将一个目录下的mkv格式的视频文件转换成mp4格式

最近学剪辑&#xff0c;从BT种子下载的素材资源都是mkv格式的&#xff0c;不能直接导入到视频剪辑软件中。这种情况下需要用一些格式转换工具进行转换&#xff0c;也可以使用ffmpeg进行编辑。 ffmpeg是一个命令行工具&#xff0c;用来对本地的音频视频软件进行编辑。ffmpeg我也…

无人机之电池保养

一、充电时 1、推荐使用官方充电器和充电管家 2、充电时确保电池处于关闭状态 3、冷却后再充电理想充电温度22-28度 二、使用时 1、首次使用需要充电唤醒电池 2、切勿将电池耗尽过放容易造成电池鼓包 三、储存时 1、存放在环境干燥通风的地方 2、不使用时每两个月充一…

【鸿蒙学习笔记】文件管理

官方文档&#xff1a;Core File Kit简介 目录标题 文件分类什么是应用沙箱&#xff1f; 文件分类 应用文件&#xff0c;比如应用的安装包&#xff0c;自己的资源文件等。用户文件&#xff0c;比如用户自己的照片&#xff0c;录制的音视频等。 什么是应用沙箱&#xff1f; 应…

Tomcat下载安装配置教程(零基础超详细)

「作者简介」&#xff1a;冬奥会网络安全中国代表队&#xff0c;CSDN Top100&#xff0c;就职奇安信多年&#xff0c;以实战工作为基础著作 《网络安全自学教程》&#xff0c;适合基础薄弱的同学系统化的学习网络安全&#xff0c;用最短的时间掌握最核心的技术。 Tomcat 1、下载…

前端进阶全栈计划:Java基础语法

前言 本教程旨在帮助初学者系统地掌握Java的基础知识。我们将从Java的基本语法开始&#xff0c;逐步深入到面向对象编程、异常处理、多线程编程等核心概念。无论你是编程新手&#xff0c;还是希望夯实基础的开发者&#xff0c;这份指南都将带你走进Java的世界&#xff0c;打下坚…

RISC-V 指令系统

指令系统 指令集 指令集从本质上可以分为复杂指令集&#xff08;Complex Instruction Set Computing&#xff0c;CISC&#xff09;和精简指令集&#xff08;Reduced Instruction Set Computing&#xff0c;RISC&#xff09;两种。复杂指令集的特点是能够在一条指令内完成很多…

STM32智能电网监控系统教程

目录 引言环境准备智能电网监控系统基础代码实现&#xff1a;实现智能电网监控系统 4.1 数据采集模块 4.2 数据处理与分析 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;电网监控与优化问题解决方案与优化收尾与总结 1. 引言 智能电网监控系统通过S…

5G与未来通信技术

随着科技的迅猛发展&#xff0c;通信技术也在不断演进。5G技术作为第五代移动通信技术&#xff0c;已成为现代通信技术的一个重要里程碑。本文将详细介绍5G及其对未来通信技术的影响&#xff0c;重点探讨超高速互联网和边缘网络的应用。 一、超高速互联网 1. 低延迟 5G技术最显…