【C++ 第十七章】封装 unordered_map / unordered_set

news2024/11/17 11:00:28


声明:上一章讲解了 哈希结构的原理与实现,本章主要将上一章学过的拉链法的哈希结构封装进 unordered_map / unordered_set,所以需要先学过相关知识,才能更好吸收本章知识

上一章的链接:【C++ 第十六章】哈希

1. unordered_map / unordered_set 的 基本结构

之前封装 map 和 set 时,因为 map 的数据类型为 pair<key, value>,set 的数据类型为 key

则底层红黑树节点的数据类型为 T:T 可以是 pair<key, value>,也可以是 key,以此同时适配 map 和 set

在比较程序中,需要将节点数据的 key 和 其他节点的 key 比较,data < key

当 T 为 key 时,变量 T data,data 就是 key 类型数据:可以 data < key

当 T 为 pair<key, value> 时,变量 T data,data.first 才是 key 类型数据:不可以直接 data < key

因此需要各自在 map 和 set 结构中,设计仿函数 KeyOfT

(1) unordered_set 的 基本结构

namespace my
{
	template<class K>
	class unordered_set
	{
		// 仿函数:将 data 转换成 key
		struct set_KeyOfT {
			const K& operator()(const K& key) {
				return key;
			}
		};

	public:
	

	private:
		hash_bucket::HashTable<K, K, set_KeyOfT> _ht;   // 直接封装一个哈希结构
	};
}

(2) unordered_map 的 基本结构

namespace my
{
	template<class K, class V>
	class unordered_map
	{
		// 仿函数:将 data 转换成 key
		struct map_KeyOfT
		{
			const K& operator()(const pair<K, V>& kv) {
				return kv.first;
			}
		};
	public:

	private:
		// 注意要加上类域指定,否则编译器报奇怪的错误
		hash_bucket::HashTable<K, pair<K, V>, map_KeyOfT> _ht; m  // 直接封装一个哈希结构
	};
}

2. 设计哈希表迭代器

因为我们哈希表使用 拉链法的结构,因此迭代器实际上是封装链表节点指针

2.1 前置++ 功能

实现思路:哈希表迭代器 ++,即从当前有效数据节点到下一个有效数据节点,这有两种情况,一是当前链表节点不是尾节点,next 即为下一个有效数据节点;二是当前节点为尾节点,next为空,则需要往后遍历,跳到下一个单链表,继续寻找有效数据节点

如何从当前链表尾节点,到下一条链表?



我们这里采取的方法:将 “哈希表” 传入迭代器类中,通过 除留余数法 获取当前哈希桶位置 hashi(这就是哈希表数组下标),hashi ++ 就可以直接跳到下一个哈希桶中了,

伪代码:讲解思路

if (下一个节点不为空) {
	直接到下一个节点
}
else if (下一个节点为空) {
	除留余数法 获取当前哈希桶位置 hashi
	hashi++  跳到下一个桶

	while (hashi < 哈希表的size) {
		如果下一个桶是空的,就需要一直循环,直到找到非空桶
	}

	if (hashi >= 哈希表的size:说明到最后都没有非空桶) {
		指针直接指向 nullptr
	}
	else if(找到非空桶){
		指针指向新链表
	}
}

实际代码

// HT 是 哈希表指针
Self& operator++() {
	assert(_pNode);

	// 逻辑是 ++ 到下一个有效元素位置:因此需要判断下一个位置是否是空节点
	if (_pNode->_next != nullptr) {
		_pNode = _pNode->_next;
	}
	else {
		// 因为一个链表遍历完,需要重新到另一个哈希桶,因此还需要重新更新迭代器指向,需要获取哈希表信息,则传一个哈希表指针过来
		KeyOfT kot;
		Hash hash;
		size_t hashi = hash(kot(_pNode->_data)) % (HT->_table.size());
		hashi++;  // 这样就跳到下一个桶了

		// 如果下一个桶是空的,就继续跳
		while (hashi < (HT->_table.size())) {
			if (HT->_table[hashi]) break;
			hashi++;  // 这样就跳到下一个桶了
		}

		if (hashi >= (HT->_table.size())) {
			_pNode = nullptr;
		}
		else {
			_pNode = HT->_table[hashi];
		}
	}
	return *this;
}

2.2 迭代器 完整代码

一些基本的功能接口这里就不赘诉,主要是理解 前置++ 函数

// 前置声明,迭代器类中使用哈希表,程序会向上查找是否有哈希表代码实现,否则识别不了,需要提前声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;


// 迭代器
template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash = HashFunc<K>>
class HashIterator
{
	typedef HashNode<T> Node;
	typedef HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;

public:
	Node* _pNode;   // 链表节点指针
	HashTable<K, T, KeyOfT, Hash>* HT; // 获取哈希表指针

	
	// 基本的功能接口
	Ref operator*() {
		return _pNode->_data;
	}
	Ptr operator->() {
		return &(_pNode->_data);
	}

	bool operator!=(const Self& it) {
		return _pNode != it._pNode;
	}
	bool operator==(const Self& it) {
		return _pNode == it._pNode;
	}


	// 前置++
	Self& operator++() {
		assert(_pNode);

		// 逻辑是 ++ 到下一个有效元素位置:因此需要判断下一个位置是否是空节点
		// 因为一个链表遍历完,需要重新到另一个哈希桶,因此还需要重新更新迭代器指向,需要获取哈希表信息,则传一个哈希表指针过来
		if (_pNode->_next != nullptr) {
			_pNode = _pNode->_next;
		}
		else {
			KeyOfT kot;
			Hash hash;
			size_t hashi = hash(kot(_pNode->_data)) % (HT->_table.size());
			hashi++;  // 这样就跳到下一个桶了

			// 如果下一个桶是空的,就继续跳
			while (hashi < (HT->_table.size())) {
				if (HT->_table[hashi]) break;
				hashi++;  // 这样就跳到下一个桶了
			}

			if (hashi >= (HT->_table.size())) {
				_pNode = nullptr;
			}
			else {
				_pNode = HT->_table[hashi];
			}
		}
		return *this;
	}

	HashIterator(Node* pNode, HashTable<K, T, KeyOfT, Hash>* pHT)
		:_pNode(pNode)
		, HT(pHT)
	{}
};

3. 在哈希表中封装与应用迭代器

template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
	// 将迭代器类设置成 友元,以便迭代器类可以访问该类的私有:迭代器类的 operator++ 函数实现中,需要访问哈希表类的 私有成员_table
	template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
	friend class HashIterator;


	typedef HashNode<T> Node;
public:

    // 定义迭代器类型 Iterator 和  Const_Iterator
	typedef HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
	typedef HashIterator<K, T, const T&, const T*, KeyOfT, Hash> Const_Iterator;



	// 迭代器
	Iterator begin() {
		// 先找到有效数据节点:不是每个哈希桶都有数据的
		for (size_t i = 0; i < _table.size(); ++i) {
			if (_table[i]) return Iterator(_table[i], this);
		}
	}

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

	Const_Iterator begin() const {
		// 先找到
		for (size_t i = 0; i < _table.size(); ++i) {
			if (_table[i]) return Const_Iterator(_table[i], this);
		}
	}

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

	// ..... 其他函数


private:
	//也可以使用 list:双向带头循环链表
	//vector<list<pair<K, V>>> _table;  
	vector<Node*> _table;
	size_t _n = 0; // 负载因子

	Hash hash;
	KeyOfT kot;
};

参考STL库的写法,以及为了后续实现 operator[] ,将 find 函数的返回值改成 Iterator,将 insert 函数的返回值改成 pair<Iterator, bool> ,并且两个函数内部一些程序对应稍微修改,这里不赘述

哈希表类完整代码

实现函数:封装迭代器 begin / end 、const_begin / const_end 、插入 insert、删除 erase、查询 find 

template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
	// 将迭代器类设置成 友元,以便迭代器类可以访问该类的私有:迭代器类的 operator++ 函数实现中,需要访问哈希表类的 私有成员_table
	template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
	friend class 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() {
		// 一开始vector里面是随机值,不是 nullptr!! 要自己初始化
		_table.resize(10, nullptr);
	}

	// 本哈希桶需要显式写析构:因为 Node* 是内置类型,只会默认析构成 nullptr,链表节点不会被处理
	~HashTable() {
		Node* cur = nullptr;
		Node* next = nullptr;
		for (size_t i = 0; i < _table.size(); ++i) {
			cur = _table[i];
			while (cur) {
				next = cur->_next;
				delete cur;
				cur = next;
			}
		}
	}

	// 迭代器
	Iterator begin() {
		// 先找到
		for (size_t i = 0; i < _table.size(); ++i) {
			if (_table[i]) return Iterator(_table[i], this);
		}
	}
	Iterator end() {
		return Iterator(nullptr, this);
	}
	Const_Iterator begin() const {
		// 先找到
		for (size_t i = 0; i < _table.size(); ++i) {
			if (_table[i]) return Const_Iterator(_table[i], this);
		}
	}
	Const_Iterator end() const {
		return Const_Iterator(nullptr, this);
	}

	Iterator find(const K& key) {
		size_t hashi = hash(key) % _table.size();

		Node* cur = _table[hashi];
		while (cur) {
			if (kot(cur->_data) == key) return Iterator(cur, this);
			cur = cur->_next;
		}
		return end();
	}

	pair<Iterator, bool> insert(const T& data) {
		// 考虑冗余
		Iterator ret = find(kot(data));
		if (ret != end()) {
			cout << "该数据已存在" << '\n';
			return make_pair(ret, false);
		}


		// 考虑扩容:当负载比率为 1 时,扩容(即 n == size)
		// 甚至可以大于 1,即理想情况下,就是平均每个桶有 1个以上 
		if (_n == _table.size()) {
			HashTable<K, T, KeyOfT, Hash> newTable;
			newTable._table.resize(2 * _table.size());


			// 若直接遍历每个节点,取数值 data insert 插入新表,会导致频繁的 new 节点,造成一定消耗
			// 我们可以直接将旧表的节点 直接转接到 新表,省去new节点的消耗
			for (size_t i = 0; i < _table.size(); ++i) {
				Node* cur = _table[i];
				/*while (cur) {
					newTable.insert(cur->_data);
					cur = cur->_next;
				}*/
				while (cur) {
					Node* Next = cur->_next;
					size_t hashi = hash(kot(cur->_data)) % newTable._table.size();
					cur->_next = newTable._table[hashi];
					newTable._table[hashi] = cur;
					cur = Next;
				}
				_table[i] = nullptr;
			}
			_table.swap(newTable._table);
		}


		size_t hashi = hash(kot(data)) % _table.size();

		// 头插和尾插都行:头插最方便
		Node* newNode = new Node(data);

		newNode->_next = _table[hashi];
		_table[hashi] = newNode;

		_n++;

		return make_pair(Iterator(newNode, this), true);
	}

	bool erase(const K& key) {
		Iterator ret = find(kot(data));
		if (ret._pNode == nullptr) {
			cout << "该节点不存在" << '\n';
			return false;
		}

		// 删除函数需要自己找目标节点,因为底层是单链表,没有prev,删除不好搞
		size_t hashi = hash(key) % _table.size();
		Node* cur = _table[hashi];
		Node* prev = nullptr;
		while (cur) {
			if (kot(cur->_data) == key) {
				if (prev == nullptr) _table[hashi] = cur->_next;
				else prev->_next = cur->_next;
				delete cur;
				cur = nullptr;
				--_n;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}

		return false;
	}

private:
	//也可以使用 list:双向带头循环链表
	//vector<list<pair<K, V>>> _table;  
	vector<Node*> _table;
	size_t _n = 0; // 负载因子

	Hash hash;
	KeyOfT kot;
};

4. 完善 unordered_map / unordered_set 结构

前面实现了 哈希表迭代器 与 哈希表

因此,在 unordered_map / unordered_set 类中添加这些相关操作以完善。  【迭代器 begin/end、const_begin/const_end、插入insert、删除erase、查询find  】

4.1 u_map 的 operator[] 功能

前面实现了 insert 函数的返回值为 pair<iterator, bool>

(1)若插入成功,则返回的 pair<iterator, bool> 中的 iterator 迭代器指向 新元素,bool == true

(2)若插入失败,则返回的 pair<iterator, bool> 中的 iterator 迭代器指向 已存在且数值相同的那个元素,bool == false

operator[] 返回迭代器 iterator 指向节点中数据 data.second == value

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

4.2 关于处理 u_map / u_set 的 key 值不能修改

因为 unordered_map / unordered_set 底层使用 拉链法的哈希结构,需要通过 除留余数法 计算 key 值映射的下标位置,确定该数据的存放位置

如果修改了 u_map / u_set 的 key,则 删除或查询操作时,通过 除留余数法 计算 key 值映射的下标位置 就会出现严重错误!!比如本来 key == 19,映射的位置 hashi == 19/10 == 9,将key修改,key == 23,映射的位置就变化 hashi == 23/10 == 3,位置就变了!!

设计程序:将参数 key 使用 const 修饰,从源头上限制该类型的参数不能被修改

对于 unordered_set,需要将 哈希表 模板类型参数修改成 const K

HashTable<K, const K, set_KeyOfT, Hash> _ht;

对于 unordered_map,需要将 哈希表 模板类型参数修改成 pair<const K, V>  

HashTable<K, pair<const K, V>, map_KeyOfT, Hash> _ht;

其他某些部分也要对应修改,自行调整(通常会报错来提示你,如果不改 doge)

4.3 unordered_map 的完整代码

#pragma once
#include"HashTable.h"


namespace my
{
	template<class K, class V, class Hash = hash_bucket::HashFunc<K>>
	class unordered_map
	{
		struct map_KeyOfT
		{
			const K& operator()(const pair<K, V>& kv) {
				return kv.first;
			}
		};
	public:
		// 重命名哈希表的迭代器
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, map_KeyOfT, Hash>::Iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, map_KeyOfT, 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);
		}
		bool erase() {
			return _ht.erase();
		}
		iterator find(const K& key) {
			return _ht.find();
		}

		// operator[]
		V& operator[](const K& key) {
			pair<iterator, bool> ret = _ht.insert({ key, V() });
			return ret.first->second;
		}
	private:
		// 注意要加上类域指定,否则编译器报奇怪的错误
		hash_bucket::HashTable<K, pair<const K, V>, map_KeyOfT, Hash> _ht;
	};
}

4.4 unordered_set 的完整代码

#pragma once
#include"HashTable.h"


namespace my
{
	template<class K, class Hash = hash_bucket::HashFunc<K>>
	class unordered_set
	{
		struct set_KeyOfT {
			const K& operator()(const K& key) {
				return key;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, const K, set_KeyOfT, Hash>::Iterator iterator;
		typedef typename hash_bucket::HashTable<K, const K, set_KeyOfT, 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);
		}
		bool erase() {
			return _ht.erase();
		}
		iterator find(const K& key) {
			return _ht.find();
		}


	private:
		hash_bucket::HashTable<K, const K, set_KeyOfT, Hash> _ht;
	};
}

5. 哈希表结构 完整代码:节点+迭代器+哈希函数+哈希表

namespace hash_bucket
{
	template<class T>
	struct HashNode {
		typedef HashNode<T> Node;
		T _data;
		Node* _next;

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

	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 n = 0;
			for (auto& ch : s) {
				n += ch;  // 将字符串的每个字符的ASCLII码值相加起来,但这样还是不可完全避免冲突,如 abc 和 cba 的 ASCII码值总和是相等的,则 n 相等,取模之后也就冲突
				// 缓解冲突的一个方法:每一个字符都相乘一个 31
				n *= 31;
			}
			return n;
		}
	};


	// 前置声明,迭代器类中使用哈希表,会向上查找是否有哈希表,否则不匹配,需要提前声明
	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;


	// 迭代器
	template <class K, class T, class Ref, class Ptr, class KeyOfT, class Hash = HashFunc<K>>
	class HashIterator
	{
		typedef HashNode<T> Node;
		typedef HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;

	public:
		Node* _pNode;
		HashTable<K, T, KeyOfT, Hash>* HT; // 获取哈希表指针

		Ref operator*() {
			return _pNode->_data;
		}
		Ptr operator->() {
			return &(_pNode->_data);
		}

		bool operator!=(const Self& it) {
			return _pNode != it._pNode;
		}
		bool operator==(const Self& it) {
			return _pNode == it._pNode;
		}

		Self& operator++() {
			assert(_pNode);

			// 逻辑是 ++ 到下一个有效元素位置:因此需要判断下一个位置是否是空节点
			// 因为一个链表遍历完,需要重新到另一个哈希桶,因此还需要重新更新迭代器指向,需要获取哈希表信息,则传一个哈希表指针过来
			if (_pNode->_next != nullptr) {
				_pNode = _pNode->_next;
			}
			else {
				KeyOfT kot;
				Hash hash;
				size_t hashi = hash(kot(_pNode->_data)) % (HT->_table.size());
				hashi++;  // 这样就跳到下一个桶了
				
				// 如果下一个桶是空的,就继续跳
				while (hashi < (HT->_table.size())) {
					if (HT->_table[hashi]) break;
					hashi++;  // 这样就跳到下一个桶了
				}

				if (hashi >= (HT->_table.size())) {
					_pNode = nullptr;
				}
				else {
					_pNode = HT->_table[hashi];
				}
			}
			return *this;
		}

		HashIterator(Node* pNode, HashTable<K, T, KeyOfT, Hash>* pHT)
			:_pNode(pNode)
			, HT(pHT)
		{}
	};

	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 class 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() {
			// 一开始vector里面是随机值,不是 nullptr!! 要自己初始化
			_table.resize(10, nullptr);
		}

		// 本哈希桶需要显式写析构:因为 Node* 是内置类型,只会默认析构成 nullptr,链表节点不会被处理
		~HashTable() {
			Node* cur = nullptr;
			Node* next = nullptr;
			for (size_t i = 0; i < _table.size(); ++i) {
				cur = _table[i];
				while (cur) {
					next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}

		// 迭代器
		Iterator begin() {
			// 先找到
			for (size_t i = 0; i < _table.size(); ++i) {
				if (_table[i]) return Iterator(_table[i], this);
			}
		}
		Iterator end() {
			return Iterator(nullptr, this);
		}
		Const_Iterator begin() const {
			// 先找到
			for (size_t i = 0; i < _table.size(); ++i) {
				if (_table[i]) return Const_Iterator(_table[i], this);
			}
		}
		Const_Iterator end() const {
			return Const_Iterator(nullptr, this);
		}

		Iterator find(const K& key) {
			size_t hashi = hash(key) % _table.size();
			 
			Node* cur = _table[hashi]; 
			while (cur) {
				if (kot(cur->_data) == key) return Iterator(cur, this);
				cur = cur->_next;
			}
			return end();
		}

		pair<Iterator, bool> insert(const T& data) {
			// 考虑冗余
			Iterator ret = find(kot(data));
			if (ret != end()) {
				cout << "该数据已存在" << '\n';
				return make_pair(ret, false);
			}


			// 考虑扩容:当负载比率为 1 时,扩容(即 n == size)
			// 甚至可以大于 1,即理想情况下,就是平均每个桶有 1个以上 
			if (_n == _table.size()) {
				HashTable<K, T, KeyOfT, Hash> newTable;
				newTable._table.resize(2 * _table.size());


				// 若直接遍历每个节点,取数值 data insert 插入新表,会导致频繁的 new 节点,造成一定消耗
				// 我们可以直接将旧表的节点 直接转接到 新表,省去new节点的消耗
				for (size_t i = 0; i < _table.size(); ++i) {
					Node* cur = _table[i];
					/*while (cur) {
						newTable.insert(cur->_data);
						cur = cur->_next;
					}*/
					while (cur) {
						Node* Next = cur->_next;
						size_t hashi = hash(kot(cur->_data)) % newTable._table.size();
						cur->_next = newTable._table[hashi];
						newTable._table[hashi] = cur;
						cur = Next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newTable._table);
			}


			size_t hashi = hash(kot(data)) % _table.size();

			// 头插和尾插都行:头插最方便
			Node* newNode = new Node(data);

			newNode->_next = _table[hashi];
			_table[hashi] = newNode;

			_n++;

			return make_pair(Iterator(newNode, this), true);
		}

		bool erase(const K& key) {
			Iterator ret = find(kot(data));
			if (ret._pNode == nullptr) {
				cout << "该节点不存在" << '\n';
				return false;
			}

			// 删除函数需要自己找目标节点,因为底层是单链表,没有prev,删除不好搞
			size_t hashi = hash(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur) {
				if (kot(cur->_data) == key) {
					if (prev == nullptr) _table[hashi] = cur->_next;
					else prev->_next = cur->_next;
					delete cur;
					cur = nullptr;
					--_n;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		//也可以使用 list:双向带头循环链表
		//vector<list<pair<K, V>>> _table;  
		vector<Node*> _table;
		size_t _n = 0; // 负载因子

		Hash hash;
		KeyOfT kot;
	};


}

 unordered_map / unordered_set 的 测试代码

void test_map() {
	my::unordered_map<string, string> dict;
	dict.insert({ "sort", "排序" });
	dict.insert({ "left", "左边" });
	dict.insert({ "right", "右边" });

	//dict["left"] = "左边,剩余";
	dict["insert"] = "插入";
	dict["string"];

	my::unordered_map<string, string>::iterator it = dict.begin();
	while (it != dict.end()) 
	{
		// 不能修改first,可以修改second
		//it->first += 'x';
		it->second += 'x';

		cout << it->first << ":" << it->second << endl;
		++it;
	}
	cout << endl;
}


void test_set() {
	my::unordered_set<string> st;
	st.insert({ "sort" });
	st.insert({ "left" });
	st.insert({ "right" });

	/*dict["left"] = "左边,剩余";
	dict["insert"] = "插入";
	dict["string"];*/

	my::unordered_set<string>::iterator it = st.begin();
	while (it != st.end())
	{
		// 不能修改*it
		//*it += 'x';

		cout << *it <<  endl;
		++it;
	}
	cout << endl;
}

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

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

相关文章

Ubuntu24.04 安装向日葵远程访问工具

目录 安装向日葵远程访问工具 解决方案&#xff1a; 1.下载软件包 2.远程Ubuntu桌面控制卡住 卸载向日葵远程访问工具 安装向日葵远程访问工具 安装命令&#xff1a;sudo dpkg -i 文件名.deb sudo dpkg -i SunloginClient_15.2.0.63064_amd64.deb 提示错误如下&#xf…

后端修改资源后重新运行项目了,浏览器刷新资源没更新问题

修改后重启项目&#xff0c;去浏览器刷新&#xff1a; 没有改变&#xff1f; 解决办法&#xff1a; F12去调试器里“网络”工具栏下找到“禁用缓存”按钮即可解决

视频转换成文字的5种方法,看一遍就能学会

视频已成为我们获取信息的重要渠道之一。然而&#xff0c;有时我们更需要将视频中的精华内容以文字形式提取出来&#xff0c;以便进行编辑、整理或分享。今天&#xff0c;就为大家介绍五种视频转换成文字的高效方法&#xff0c;一起来了解下吧。 方法一&#xff1a;口袋视频转换…

如何抠图把背景换成透明怎么做?

要抠图并将背景变为透明&#xff0c;这样做的好处是可以方便地将所选物体从原始图像中分离出来&#xff0c;并在其他背景上自由组合。怎么把图片变成透明底&#xff0c;抠图攻略分享&#xff1a;让你轻松上手抠图&#xff01; 通过抠图和背景透明化&#xff0c;您可以创建更多种…

深入理解微服务中的负载均衡算法与配置策略

负载均衡算法 我们首先来探讨一下默认情况下Ribbon使用的负载均衡算法。有些人可能会说它使用轮询算法&#xff0c;因为在本地测试时&#xff0c;我们经常会看到轮询的效果。然而&#xff0c;简单地依赖这种表面的观察来回答面试题是有风险的。实际上&#xff0c;忽略了深入理解…

Superset 无需登录访问分享的图表

1&#xff0c;进入Superset安装目录找到config.py文件 2&#xff0c;修改config.py中的配置项 添加 PUBLIC_ROLE_LIKE: Optional[str] "Gamma" # Grant public role the same set of permissions as for a selected builtin role. # This is useful if one wants to…

Large Bin Attack 源码调试

Large Bin Attack 分配跟 large bin 有关的 chunk&#xff0c;要经过 fastbin&#xff0c;unsorted bin&#xff0c;small bin 的分配&#xff0c;建议在学习 large bin attack 之前搞清楚 fastbin&#xff0c;unsorted bin 分配的流程。 large bin中双向链表的连接方法&…

五款伪原创文章生成器软件,为创作者快速生成高质量内容

在内容为王的时代&#xff0c;创作者们面临着巨大的压力&#xff0c;需要不断地产出高质量、有深度的文章。在这个过程中&#xff0c;伪原创文章生成器软件成为了许多创作者的得力助手。本文将为你详细介绍5款伪原创文章生成器软件&#xff0c;帮助你快速生成高质量内容。 一、…

vue part4

收集表单数据 v-model label直接包住input不用关联也可以获取焦点 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>收集表单数据</title><script type"text/javascript" src&quo…

SpringBoot校园万能墙系统的设计与实现97395

目 录 1 绪论 1.1研究背景与意义 1.2研究现状 1.3论文结构与章节安排 2 相关技术介绍 2.1 springboot框架介绍 2.2 JavaScript 2.3 Mysql数据库 2.4 Vue.js 主要功能 3 系统分析 3.1 可行性分析 3.1.1 技术可行性分析 3.1.2 经济可行性分析 3.1.3 法律可行性分析…

“领导让我帮忙买30杯奶茶,实际花费535元,但领导却只转了500元,我该如何提醒领导转我35元的差额?”

在职场中&#xff0c;我们时常会遇到一些让人哭笑不得的小事&#xff0c;它们虽小&#xff0c;却足以反映出职场中的微妙关系和处事哲学。 一位行政朋友曾发帖称&#xff1a;“我是一名5年工作经验的企业行政助理&#xff0c;也不是小白。但有一次&#xff0c;业务部门领导让我…

【机器学习】嘿马机器学习(科学计算库)第4篇:Matplotlib,学习目标【附代码文档】

本教程的知识点为&#xff1a;机器学习&#xff08;常用科学计算库的使用&#xff09;基础定位 机器学习概述 机器学习概述 1.5 机器学习算法分类 1 监督学习 机器学习概述 1.7 Azure机器学习模型搭建实验 Azure平台简介 Matplotlib 3.2 基础绘图功能 — 以折线图为例 1 完善原…

为什么每个Java开发者都应该掌握CompletableFuture?深入探索这一强大的并发工具!

文章目录 1 如何正确处理异步任务的异常情况&#xff1f;2 如何设置 CompletableFuture 的超时时间&#xff1f;3 如何取消正在执行的CompletableFuture任务&#xff1f; 1 如何正确处理异步任务的异常情况&#xff1f; 想象一下&#xff0c;用餐厅的例子来比喻 CompletableFut…

计算机组成与设计 - 1.7 功耗墙 - 1.8 单处理器向多处理器的转变 - 1.9 基准

1.7 功耗墙 25 年间 1ntel x86 八代微处理器的时钟频率和功耗 。 奔腾 4 处理器时钟频率和功耗提高很大 &#xff0c;但是性能提升不大 。 Prescott 发热问题导致奔腾 4 处理器的生产线被放弃 。 Core 2 生产线恢复使用低时钟频率的简单流水线和片上多处理器 。 Core i5 采用同…

前端常见问题

一、<!DOCTYPE html>作用 <!DOCTYPE>是html文档的第一行&#xff0c;用于声明文档的类型&#xff0c;它会告诉浏览器要用哪一种HTML规范来渲染文档&#xff0c;确保正确渲染页面。 二、src与 href 的区别 &#xff08;一&#xff09;、请求资源类型的不同 在请…

研究生如何利用 ChatGPT 帮助开展日常科研工作?

研究生可以通过以下几种方式利用 ChatGPT 来帮助开展日常科研工作&#xff1a; 文献综述和研究方向&#xff1a;ChatGPT 可以帮助研究生快速了解某一领域的研究现状和热点问题。通过提供相关的文献综述、研究趋势和技术细节&#xff0c;ChatGPT 可以协助确定研究方向和课题。 …

来抄作业!企业财务报表还可以做出这样的可视化效果

从繁琐的手工记账到智能化的数据分析&#xff0c;每一步都标志着企业向更加高效、精准的管理模式迈进。大家可以想象一下&#xff0c;将复杂多变的财务数据以直观、动态的形式展现在眼前的大屏之上&#xff0c;会是一种怎样的体验&#xff1f; 这里就要说到使用山海鲸报表工具搭…

解读2024年国自然资助情况:史上最卷,三无人员也能中

2024年国自然资助情况 面上史上最难&#xff01; 面上项目申请近18万&#xff0c;资助20758项&#xff0c;资助率为11.66%&#xff0c;较2023年下降5.33个百分点。这与今年取消了"申二停一"限制有关&#xff0c;导致资历老的教授、各类人才帽子大量涌入&#xff0c…

昇腾 AscendCL C++应用开发 获取模型的信息

昇腾 AscendCL C应用开发 获取模型的信息 flyfish 举例说明 程序在香橙派 AIpro 下获取到的模型信息 从ACLLite代码里看一些类型&#xff0c;因为获取模型信息的时候&#xff0c;会获取到数字&#xff0c;所以要看数字代表的意思&#xff0c;所有要看常量的名字 数据类型 # …

ch32v307vct6从头移植FreeRTOS

使用官方的ide可以直接创建带FreeRTOS的工程,但是不利于我们学习移植,所以特此记录怎么从头开始移植FreeRTOS到CH32V307VCT6芯片使用。 下载FreeRTOS源码 首先进入https://www.freertos.org/官网,然后找到如下Download字样,进入下载即可 下载完成后我们解压使用。移植开始…