C++【哈希表的完善及封装】

news2024/12/24 8:31:59

✨个人主页: 北 海
🎉所属专栏: C++修行之路
🎃操作环境: Visual Studio 2019 版本 16.11.17

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、哈希表的完善
      • 1.1、拷贝与赋值
      • 1.2、优化:哈希函数
      • 1.3、优化:素数大小
      • 1.4、新增:迭代器类
    • 2、封装实现 unordered_set 和 unordered_map
      • 2.1、解决 k/v 参数冲突问题
      • 2.2、解决 key 的获取问题
      • 2.3、解决 unordered_set 迭代器非法操作
      • 2.4、调整函数返回值
      • 2.5、unordered_map 新增 operator[ ]
    • 3、性能测试
    • 4、源码
  • 🌆总结


🌇前言

关于哈希表的两种实现方法:闭散列、开散列 已经在上一篇文章中学习过了,闭散列 存在 踩踏 问题,十分影响效率,因此在实践中往往会选择更加优秀的 开散列,哈希表(开散列)又叫做 哈希桶,作为被选中的结构,我们需要对其进行改造,完善哈希桶,使其最终能封装出 unordered_setunordered_map

图示


🏙️正文

1、哈希表的完善

1.1、拷贝与赋值

单链表 是我们自己写的,其中涉及到了 动态内存管理,这就意味着除了要自己释放内存外,还需要给出 深拷贝 版的 拷贝构造赋值重载 函数

//默认构造
HashTable()
	:_table()
	,_n(0)
{}

//拷贝构造
HashTable(const HashTable& ht)
	:_table()
	,_n(0)
{
	//开辟空间
	_table.resize(ht._table.size());

	//遍历插入节点
	for (auto node : ht._table)
	{
		//遍历桶中的元素
		Node* cur = node;
		while (cur)
		{
			Insert(cur->_kv);
			cur = cur->_next;
		}
	}
}

//赋值重载(现代写法)
HashTable& operator=(HashTable ht)
{
	//直接交换 _table 与 _n
	_table.swap(ht._table);
	_n = ht._n;

	return *this;
}

注意: 提供了 拷贝构造 之后,就得提供 默认构造函数

1.2、优化:哈希函数

在实际使用中,往往需要以 字符串 作为存储依据(键值),比如 姓名快递信息商品名称价格中文单词英文释义

总之,字符串是一种非常常见的数据类型

而在我们实现的哈希表中,只考虑 整型 的存储情况,即直接用 key % capacity 计算哈希值,如果把整型换成 字符串 是会出问题的

比如在下面这个场景中,程序无法编译

图示

图示

为了解决这个问题,我们可以将 获取 key 单独封装为一个 仿函数,再利用 模板特化,使其既能支持 整型 也能支持 字符串

//获取 key 值的仿函数
template<class K>
struct GetKey
{
	size_t operator()(const K& key)
	{
		//此时为整型,直接返回即可
		return key;
	}
};

//模板的特化
template<>
struct GetKey<string>
{
	size_t operator()(const string& key)
	{
		//根据字符串,计算出数值并返回
		size_t val = 0;
		for (auto e : key)
			val += static_cast<size_t>(e);

		return val;
	}
};

添加了这个仿函数之后,就需要对 哈希表 中所有需要获取 key 的地方进行修改

结果

此时 哈希表 中的键值可以正常存储 字符串

图示
三个字符串计算出的值分别为:140719561344

这是在 字符串长度不一且字符相差过大 的情况下计算出来的,假若 字符串过短或者字符串较为接近,可能会计算出 相同的值,这会导致 哈希冲突

因此,单纯的累加每个字符的 ASCII 码值显得不够专业

有人专门对 字符串 进行研究,搞出了各种各样重复率较低的 字符串哈希算法

字符串哈希算法

图示

在众多 字符串哈希算法 中,BKDRHash 一骑绝尘,各方面都非常优秀,因此这里我们选择 BKDRHash 算法作为 计算字符串值 的函数

图示

BKDRHash 的核心就是 在原来值的基础上 * 131,再加上字符的 ASCII 码值

//模板的特化
template<>
struct GetKey<string>
{
	size_t operator()(const string& key)
	{
		//根据字符串,计算出数值并返回
		size_t val = 0;

		//BKDRHash
		for (auto e : key)
			val = val + 131 + static_cast<size_t>(e);

		return val;
	}
};

修改之后,三个字符串计算出的值分别为:363451003702

显然此时的值更为分散,符合我们的需求

关于 static_cast<>

  • 这是 C++ 中提供的类型转换函数,static_cast<> 相当于 C语言 中的 隐式类型转换,这样写的话更加规范,让别人一眼就能看出这里发生了 隐式类型转换

1.3、优化:素数大小

使用除留余数法时,哈希表的大小最好是素数,这样能够减少哈希冲突产生的次数

SGISTL 中,哈希表 在扩容时就使用了这一技巧

图示

简单来说,就是当我们扩容后,按照 下一个素数值大小 进行扩容

这些素数都是近似 2 倍的大小关系,在确保不会频繁扩容的同时,尽可能减少哈希冲突

所以需要这样一个函数

//获取素数
size_t GetNextPrime(size_t prime)
{
	//返回当前位置的下一个素数值,作为扩容后的空间大小
	// SGI版
	static const int __stl_num_primes = 28;
	static const unsigned long __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 (int i = 0; i < __stl_num_primes; i++)
		if (__stl_prime_list[i] > prime)
			return __stl_prime_list[i];
	
	return __stl_prime_list[__stl_num_primes - 1];	//返回最后一个值
}

同样的,需要对 扩容 的地方进行改造

图示

在改造之后,哈希表 的初始大小变为 53

1.4、新增:迭代器类

哈希表 中理应提供一个 迭代器 对其中的值进行判断,因为 是一个 单链表,只能向前走,不能回头,因此我们的 迭代器 要设计为 单向迭代器(只支持 ++

关于多模板参数 template<class K, class V, class Ref, class Ptr> 的设计原理这里不再阐述,感兴趣的可以看看这篇文章:《C++ STL学习之【list的模拟实现】》

图示

//迭代器类
template<class K, class V, class Ref, class Ptr>
struct HashTableIterator
{
	typedef HashNode<K, V> Node;	//迭代器中元素
	typedef HashTableIterator<K, V, Ref, Ptr> Self;	//迭代器自己

	HashTableIterator(Node* node)
		:_node(node)
	{}

	//基本功能
	//获取值
	Ref operator*()
	{
		return _node->_kv;
	}

	//获取指针
	Ptr operator->()
	{
		return &(operator*());
	}

	//判断逻辑
	bool operator==(const Self& it)
	{
		return _node == it._node;
	}
	bool operator!=(const Self& it)
	{
		return !(*this == it);
	}

	//++ 的实现

	Node* _node;	//迭代器
};

关于 迭代器类 比较麻烦的就是 operator++()

先来说说移动逻辑:

  • 如果当前所在桶中还有数据,简单,直接移动至 _next 即可
  • 如果没有数据(为空),就比较麻烦了,需要移动至当前哈希表中,下一个有数据的桶

显然,需要用到 哈希表,并且是 同一个哈希表
解决办法:构造迭代器时,传递当前哈希表的地址,构造一个指针指向哈希表

如何在 哈希表 中进行移动?
解决办法:首要问题是知道当前位于哈希表中的哪个位置。这个可以通过自己的 值 % 哈希表的大小 求出,清楚位置后,就向后移动,直到移动至一个不为空的位置,返回即可

图示

因为要获取使用 哈希表,所以需要对 迭代器类 做出一些调整

//对哈希表的前置声明
template<class K, class V>
class HashTable;
	
//迭代器类
template<class K, class V, class Ref, class Ptr>
struct HashTableIterator
{
	typedef HashNode<K, V> Node;	//迭代器中元素
	typedef HashTableIterator<K, V, Ref, Ptr> Self;	//迭代器自己
	typedef HashTable<K, V> HT;	//哈希表·新增

	//改造构造函数
	HashTableIterator(Node* node, const HT* ht)	
		:_node(node)
		, _pht(ht)
	{}
	
	//基本功能
	//……

	Node* _node;	//迭代器
	const HT* _pht;	//指向哈希表的指针·新增
};

现在能通过 _pht 访问同一个哈希表

细节:

  • 需要对哈希表进行前置声明才能正常使用
  • 指向哈希表的指针为 const 指针,否则 const 哈希表对象调不动迭代器

现在对 operator++() 进行实现

//++ 的实现
Self operator++()
{
	//计算当前所处的位置
	size_t HashI = GetKey<K>()(_node->_kv.first) % _pht->_table.size();

	//如果下一个位置不为空,则直接向下走即可
	if (_node->_next)
		_node = _node->_next;
	else
	{
		//比较麻烦,实现桶之间的移动
		while (++HashI < _pht->_table.size())
		{
			//判断当前是否为空桶
			if (_pht->_table[HashI] != nullptr)
			{
				_node = _pht->_table[HashI];
				break;
			}
		}

		//看看是不是走到最后了
		if (HashI == _pht->_table.size())
			_node = nullptr;
	}

	return *this;	//返回的是迭代器对象
}

在这个函数中,访问了 哈希表类 中的私有成员 _table,这是不行的,为了让其能成功访问,我们可以把 迭代器类 设为 哈希表类友元类

同时,在 哈希表类 中增加 迭代器操作 的相关函数

template<class K, class V, class Ref, class Ptr>
friend struct HashTableIterator;	//友元声明

//迭代器相关
typedef HashTableIterator<K, V, pair<K, V>&, pair<K, V>*> iterator;
typedef HashTableIterator<K, V, const pair<K, V>&, const pair<K, V>*> const_iterator;

iterator begin()
{
	//起始位置是第一个有数据的桶
	Node* _node = nullptr;
	for (auto e : _table)
	{
		if (e != nullptr)
		{
			_node = e;
			break;
		}
	}

	return iterator(_node, this);	//构造迭代器对象
}

iterator end()
{
	//最后一个位置为空
	return iterator(nullptr, this);
}

const_iterator begin() const
{
	Node* _node = nullptr;
	for (auto e : _table)
	{
		if (e != nullptr)
		{
			_node = e;
			break;
		}
	}

	return const_iterator(_node, this);	//构造迭代器对象
}

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

现在可以测试 迭代器

直接用 范围 for,分别测试 普通迭代器const 迭代器

void func(const HashTable<string, int> hash)
{
	//这里面的是 const 对象
	cout << "const 对象" << endl;
	for (auto e : hash)
		cout << e.first << " " << e.second << endl;

	cout << endl << "====================================" << endl << endl;
}

void TestOpenHash2()
{
	vector<pair<string, int>> goods{ make_pair("iPhone 14 Pro Max", 9399),
							make_pair("SAMSUNG Galaxy S23 Ultra", 6509),
							make_pair("HUAWEI Mate 50 Pro", 5999) };
	HashTable<string, int> hash;
	for (auto& e : goods)
		hash.Insert(e);


	func(hash);

	cout << "普通对象" << endl;
	for (auto e : hash)
		cout << e.first << " " << e.second << endl;
}

图示

范围 for 没问题,迭代器也就没问题了

注意:

  • const 迭代器是为 const 对象提供的,所以可以选择重载 begin()end(),也可以选择重新编写 cbegin()cend(),二者除了函数名外,其他都是一样的
  • 单向迭代器是不能向后走的,所以哈希表中没有反向迭代器

2、封装实现 unordered_set 和 unordered_map

如同使用 一棵红黑树同时封装 set/map

同样可以使用 一张哈希表同时封装 unordered_set/unordered_map

就连封装时遇到的问题都差不多

2.1、解决 k/v 参数冲突问题

unordered_set 需要 k 的模型,而 unordered_map 需要 k/v 的模型

为了满足 不同 的需求,需要对 哈希表 的模板进行调整,让其既能适应 unordered_set,也能适应 unordered_map

至于如何调整,可以看看 红黑树 封装时的图示(类似的原理)

图示

//节点类
template<class T>
struct HashNode
{
	T _data;
	HashNode<T>* _next;	//指向下一个节点
};

//对哈希表的前置声明
template<class K, class T>
class HashTable;

//迭代器类
template<class K, class T, class Ref, class Ptr>
struct HashTableIterator
{
	typedef HashNode<T> Node;	//迭代器中元素
	typedef HashTableIterator<K, T, Ref, Ptr> Self;	//迭代器自己
	typedef HashTable<K, T> HT;	//哈希表
	
	Node* _node;	//迭代器
	const HT* _pht;	//指向哈希表的指针
};

//哈希表
template<class K, class T>
class HashTable
{
	typedef HashNode<T> Node;

	template<class K, class T, class Ref, class Ptr>
	friend struct HashTableIterator;	//友元声明
public:

	//迭代器相关
	typedef HashTableIterator<K, T, T&, T*> iterator;
	typedef HashTableIterator<K, T, const T&, const T*> const_iterator;

private:
	//哈希桶中不需要平衡因子,节点存储满后,进行扩容就行了
	vector<Node*> _table;
	size_t _n = 0;	//有效数据量
};

还有很多细节都是需要改动的,但把接口简单修改后,unordered_setunordered_map 都可以传递各自需要的参数了

unordered_set

#pragma once
#include "HashTable.hpp"

using namespace OpenHash;

namespace US
{
	template<class K>
	class unordered_set
	{
		typedef K data;
		typedef HashTable<K, data> HT;
	private:
		HT _t;	//这是一张哈希表
	};
}

unordered_map

#pragma once
#include "HashTable.hpp"

using namespace OpenHash;

namespace UM
{
	template<class K, class V>
	class unordered_map
	{
		typedef pair<K, V> data;
		typedef HashTable<K, data> HT;
	private:
		HT _t;	//这也是一张哈希表
	};
}

2.2、解决 key 的获取问题

现在面临一个尴尬的问题:两个参数不同的类型,如何同时使用一种获取 key 的方法?

答案是:传递仿函数,根据自己的需求,创建仿函数,然后传给 哈希表,让 哈希表 在计算 key 时使用即可,当然 哈希表 中涉及获取 key 的地方都要改

HashTable.hpp

//对哈希表的前置声明
template<class K, class T, class KeyOfT>
class HashTable;

//迭代器类
template<class K, class T, class Ref, class Ptr, class KeyOfT>
struct HashTableIterator
{
	typedef HashNode<T> Node;	//迭代器中元素
	typedef HashTableIterator<K, T, Ref, Ptr, KeyOfT> Self;	//迭代器自己
	typedef HashTable<K, T, KeyOfT> HT;	//哈希表
	
	//……

	//++ 的实现
	Self operator++()
	{
		//计算当前所处的位置
		size_t HashI = GetKey<K>()(KeyOfT()(_node->_data)) % _pht->_table.size();
		
		//……
	}
	
	//……
};

//哈希表
template<class K, class T, class KeyOfT>
class HashTable
{
	typedef HashNode<T> Node;

	template<class K, class T, class Ref, class Ptr, class KeyOfT>
	friend struct HashTableIterator;	//友元声明
public:

	//……

	//迭代器相关
	typedef HashTableIterator<K, T, T&, T*, KeyOfT> iterator;
	typedef HashTableIterator<K, T, const T&, const T*, KeyOfT> const_iterator;
	
	//……

	//查找
	iterator Find(const K& key)
	{
		//……
		
		while (cur)
		{
			if (KeyOfT()(cur->_data) == key)
				//……
		}

		//……
	}

	//插入
	bool Insert(const T& data)
	{
		if (Find(KeyOfT()(data)) != end())
			return false;	//冗余

		//判断扩容
		if (_n == _table.size())
		{
			//传统写法
			size_t newSize = GetNextPrime(_table.size());
			vector<Node*> newTable(newSize);	//新的表

			for (auto& cur : _table)
			{
				while (cur)
				{
					size_t HashI = GetKey<K>()(KeyOfT()(cur->_data)) % newSize;	//计算新的哈希值
					
					//……
				}
			}
		}

		//插入
		size_t HashI = GetKey<K>()(KeyOfT()data) % _table.size();	//计算哈希值
		
		//……
	}

	bool Erase(const K& key)
	{
		//这里直接查找,因为需要保存上一个节点信息(单链表的删除)

		size_t HashI = GetKey<K>()(key) % _table.size();
		Node* prev = nullptr;
		Node* cur = _table[HashI];

		//单链表的删除
		while (cur)
		{
			if (KeyOfT()(cur->_data) == key)
			{
				//……
			}
			
			//……
		}

	}
};

注意: 新增了迭代器之后,Find 的返回值变成了 iterator

对于 哈希表 类来说,主要改动其实就两个:模板参数的改变、获取哈希表对象 key

如此一来,unordered_setunordered_map 只需要提供符合自己特色的 key 获取仿函数即可,增加部分基础功能(具体函数的功能实现位于 HashTable.hpp 中)

unordered_set

#pragma once
#include "HashTable.hpp"

using namespace OpenHash;

namespace US
{
	template<class K>
	class unordered_set
	{
		struct KOfKey
		{
			K operator()(const K& key) const
			{
				return key;
			}
		};

		typedef K data;
		typedef HashTable<K, data, KOfKey> HT;
	public:
		//默认构造
		unordered_set()
			:_t()
		{}

		//迭代器区间构造
		template<class InputIterator>
		unordered_set(InputIterator first, InputIterator last)
			: _t()
		{
			//遍历迭代器,插入即可
			while (first != last)
			{
				insert(*first);
				++first;
			}
		}

		//迭代器
		typedef typename HT::iterator iterator;
		typedef typename HT::const_iterator const_iterator;

		iterator begin() { return _t.begin(); };
		iterator end() { return _t.end(); };

		const_iterator begin() const { return _t.begin(); };
		const_iterator end() const { return _t.end(); };

		//判空、求大小
		bool empty() const { return _t.Empty(); };
		size_t size() const { return _t.Size(); };

		//插入、删除元素
		bool insert(const data& val) { return _t.Insert(val); };
		bool erase(const K& key) { return _t.Erase(key); };

		//交换、查找、清理
		void swap(unordered_set& us) { _t.Swap(us._t); };
		iterator find(const K& key) { return _t.Find(key); };
		void clear() { _t.Clear(); };

	private: 
		HT _t;	//这是一张哈希表
	};
}

unordered_map

#pragma once
#include "HashTable.hpp"

using namespace OpenHash;

namespace UM
{
	template<class K, class V>
	class unordered_map
	{
		struct KVOfKey
		{
			K operator()(const pair<K, V>& kv) const
			{
				return kv.first;
			}
		};

		typedef pair<K, V> data;
		typedef HashTable<K, data, KVOfKey> HT;
	public:
		//默认构造
		unordered_map()
			:_t()
		{}

		//迭代器区间构造
		template<class InputIterator>
		unordered_map(InputIterator first, InputIterator last)
			: _t()
		{
			//遍历迭代器,插入即可
			while (first != last)
			{
				insert(*first);
				++first;
			}
		}

		//迭代器
		typedef typename HT::iterator iterator;
		typedef typename HT::const_iterator const_iterator;

		iterator begin() { return _t.begin(); };
		iterator end() { return _t.end(); };

		const_iterator begin() const { return _t.begin(); };
		const_iterator end() const { return _t.end(); };

		//判空、求大小
		bool empty() const { return _t.Empty(); };
		size_t size() const { return _t.Size(); };

		//插入、删除元素
		bool insert(const data& val) { return _t.Insert(val); };
		bool erase(const K& key) { return _t.Erase(key); };

		//交换、查找、清理
		void swap(unordered_map& um) { _t.Swap(um._t); };
		iterator find(const K& key) { return _t.Find(key); };
		void clear() { _t.Clear(); };
	private:
		HT _t;	//这也是一张哈希表
	};
}

进行功能测试:

结果

可以正常使用,现在来进行 优化

2.3、解决 unordered_set 迭代器非法操作

unordered_set 中只有 键值,而 键值 是不能被随意修改的(通过迭代器的方式)

void TestUS2()
{
	vector<int> arr = { 7,3,6,9,3,1,6,2 };
	unordered_set<int> s1(arr.begin(), arr.end());

	auto it = s1.begin();
	*it = 668;

	for (auto e : s1)
		cout << e << " ";
	cout << endl;
}

图示

结果为 668,这很正常,因为已经把迭代器中的键值改了,这就导致迭代器在移动时,是根据更改后的键值计算哈希值 = 668 % 53 = 32,而我们这组数中,32 位置及其后面都没有值,所以也就只打印了一个 668

当然,这不是重点,重点在于 我们把 unordered_set 中的键值修改了!

库中的解决方法:不管你 unordered_set 申请的是什么迭代器,我都给你 const 迭代器

//迭代器
typedef typename HT::const_iterator iterator;
typedef typename HT::const_iterator const_iterator;

再次测试(此时需要把赋值语句屏蔽,否则影响后面的结果)

为什么要屏蔽赋值语句?

  • 因为接下来要展示的是一个编译时错误
  • 而给常量赋值这个错误优先级更高,在编译前就报错了,也就是说,不能让赋值语句报的错影响我们的操作

虽然最终都是报了不能随便赋值 的错误,但如果我们不借此根治问题,后续没有出现赋值语句时,一样会报错

结果

此时出现了一个非常经典的 类型转换 错误

为什么?
这是因为 unordered_set 中 普通对象版的 begin()end() 使用的是 哈希表中 const 迭代器,但哈希表中的迭代器相关函数返回的是 普通迭代器 啊,也就是说,存在一个 普通迭代器 转为 const 迭代器 的问题,两者差别很大,编译器无法自行转换

库中的解决方案:
在迭代器类中提供一个十分巧妙的函数,它对于 普通迭代器对象 来说,当传入的是 普通迭代器时,相当于 拷贝构造;当传入的是 const 迭代器时,相当于一个特殊的迭代器构造,即把 普通迭代器对象构造为 const 迭代器;当然,这个函数对于 const 迭代器对象 没有影响,毕竟这玩意不能被修改

//迭代器类
template<class K, class T, class Ref, class Ptr, class KeyOfT>
struct HashTableIterator
{
	//……
	
	typedef HashTableIterator<K, T, T&, T*, KeyOfT> iterator;	//普通版的迭代器·关键

	//一个特殊的构造,既能充当拷贝构造,也能充当特殊构造
	HashTableIterator(const iterator& it)
		:_node(it._node)
		, _pht(it._pht)
	{}

	//……
};

加上之后,代码能正常编过,当然不能给常量赋值的错误也能正常显现

图示

这是一个非常牛X的解决方案

2.4、调整函数返回值

unordered_setunordered_map 中的 insert() 返回值比较特殊,它不仅要返回 迭代器,也要表示本次插入操作 是否成功

改造起来也是十分的简单

HashTable.hpp

//插入
pair<iterator, bool> Insert(const T& data)
{
	auto ret = Find(KeyOfT()(data));
	if (ret != end())
		return make_pair(ret, false);	//冗余

	//判断扩容
	if (_n == _table.size())
	{
		//传统写法
		size_t newSize = GetNextPrime(_table.size());
		vector<Node*> newTable(newSize);	//新的表

		for (auto& cur : _table)
		{
			while (cur)
			{
				size_t HashI = GetKey<K>()(KeyOfT()(cur->_data)) % newSize;	//计算新的哈希值
				Node* next = cur->_next;

				//单链表头插至新表
				cur->_next = newTable[HashI];
				newTable[HashI] = cur;


				cur = next;
			}
		}

		_table.swap(newTable);
	}

	//插入
	size_t HashI = GetKey<K>()(KeyOfT()(data)) % _table.size();	//计算哈希值

	//单链表头插
	Node* cur = _table[HashI];	//原来的头节点
	_table[HashI] = new Node(data);	//创建新的头
	_table[HashI]->_next = cur;	//连接
	_n++;

	return make_pair(iterator(_table[HashI], this), true);
}

进行简单测试

unordered_set

void TestUS3()
{
	unordered_set<int> s1;
	auto ret = s1.insert(1);
	cout << "<" << *ret.first << ">" << " | " << ret.second << endl;

	ret = s1.insert(1);
	cout << "<" << *ret.first << ">" << " | " << ret.second << endl;
}

unordered_map

void TestUS3()
{
	unordered_set<int> s1;
	auto ret = s1.insert(1);
	cout << "<" << *ret.first << ">" << " | " << ret.second << endl;

	ret = s1.insert(1);
	cout << "<" << *ret.first << ">" << " | " << ret.second << endl;
}

测试结果:

结果

显然,第二次插入时均失败(因为冗余了)

2.5、unordered_map 新增 operator[ ]

作为同时用于 键值实值 的容器,unordered_map 需要一个能快速访问 实值 的函数,即 operator[]()

这个函数功能十分强大,具备:插入、修改、插入+修改、查找 等诸多功能,是 unordered_map 中的真香函数

实现逻辑:

  • 判断 key 存不存在,如果存在,返回 value
  • 如果不存在,就插入,并返回新的 value

可以分为几个判断写,也可以直接使用 insert(),毕竟这玩意的返回值也是 重量级

//unordered_map 中独有的功能
V& operator[](const K& key)
{
	auto ret = insert(make_pair(key, V()));	//获取 <迭代器, bool> 的键值对
	auto it = ret.first;	//获取迭代器
	return it->second;	//返回实值
}

简单测试一下

void TestUM3()
{
	vector<pair<string, int>> goods{ make_pair("iPhone 14 Pro Max", 9399),
					make_pair("SAMSUNG Galaxy S23 Ultra", 6509),
					make_pair("HUAWEI Mate 50 Pro", 5999) };

	unordered_map<string, int> m1;
	for (auto& e : goods)
		m1[e.first] = e.second;

	for (auto& e : m1)
		cout << e.first << " " << e.second << endl;
}

结果
没有问题,至此,使用一张哈希表同时封装出 unordered_setunordered_map 就算是完成了


3、性能测试

将自己封装的 unordered_set 与库中的 unordered_set 进行性能对比(Release 模式下)

void TestPerformance()
{
	US::unordered_set<int> myUSet;
	std::unordered_set<int> stdUSet;

	srand((size_t)time(NULL));

	int mySetTime = 0;
	int stdSetTime = 0;

	clock_t begin, end;

	int sum = 0;
	int n = 5000000;
	for (int i = 0; i < n; i++)
	{
		int val = rand() % n + i;

		begin = end = 0;
		begin = clock();
		auto ret1 = myUSet.insert(val);
		end = clock();
		mySetTime += (end - begin);

		begin = end = 0;
		begin = clock();
		auto ret2 = stdUSet.insert(val);
		end = clock();
		stdSetTime += (end - begin);

		if (ret1.second && ret2.second)
			sum++;	//成功插入的数据量
	}

	cout << "成功插入 " << sum << " 个数据" << endl;
	cout << "myUSet 耗时: " << mySetTime << " ms" << endl;
	cout << "stdUSet 耗时: " << stdSetTime << " ms" << endl;
}

插入约 300w 个数据

结果

在经过 Release 模式的优化后,我们自己封装实现的 unordered_set 异常生猛,遥遥领先


4、源码

源码在下面的仓库里

注:HashTable.hpp 是封装 unordered_setunordered_map 后的成品;HashTable-副本.hpp 是纯净版的哈希表

《哈希表的完善及封装》

图示


🌆总结

以上就是本次关于 C++【哈希表的完善及封装】的全部内容了,在本文中,我们首先将 哈希表 进行了完善,解决了一些深拷贝问题,新增了迭代器;当 哈希表 完善后,我们用一张 哈希表同时封装实现了 unordered_setunordered_map,其中涉及大量 泛型编程思想,值得仔细推敲


星辰大海

相关文章推荐

C++ 进阶知识

C++【哈希表的模拟实现】

C++【初识哈希】

C++【一棵红黑树封装 set 和 map】

C++【红黑树】

C++【AVL树】

C++【set 和 map 学习及使用】

C++【二叉搜索树】

C++【多态】

C++【继承】

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

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

相关文章

带你快速了解字符(串)函数

​ ⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f496; 持续更文&#xff0c;谢谢大家支持 &#x1f496; 文章目录 本文重点1. strlen函数1.1 模拟实现 2. strcpy函数2.1 模拟实现 3. strcat函数3.1 模拟实现 4. strcmp函…

基于linux下的高并发服务器开发(第一章)- 目录遍历函数

10 / 目录遍历函数 // 打开一个目录 #include <sys/types.h> #include <dirent.h>DIR *opendir(const char *name); 参数&#xff1a; - name: 需要打开的目录的名称 返回值&#xff1a; DIR * 类型&#xff0c;理解为目录流 错误…

Hcip第五次作业----BGP联邦综合实验

配置IP地址 r1 [r1]int g0/0/0 [r1-GigabitEthernet0/0/0]ip add 12.0.0.1 24 [r1-GigabitEthernet0/0/0]int lo0 [r1-LoopBack0]ip add 192.168.1.1 24 [r1-LoopBack0]int lo1 [r1-LoopBack1]ip add 10.0.0.1 24 r2 [r2]int g0/0/0 [r2-GigabitEthernet0/0/0]ip add 12.0.0.2…

Orangepi Zero2 基于官方外设开发(二)

一、OLED屏显示-IIC协议 1、相关介绍 IIC及OLED相关内容请参考以下文章&#xff1a; IIC协议_单行梦想家的博客-CSDN博客 OLED显示屏_单行梦想家的博客-CSDN博客 2、OrangePi的IIC接口 由原理图可知&#xff0c;Orange Pi Zero 2 可用的 i2c 为 i2c3 Linux系统启动后&…

针对我国水资源量设计的农田灌溉收费管理平台

安科瑞虞佳豪 降水量 2013年&#xff0c;全国平均降水量661.9mm&#xff0c;折合降水总量62674.4亿立方米&#xff0c;比常年值偏多3.0%。从水资源分区看&#xff0c;松花江、辽河、海河、黄河、淮河、西北诸河6个水资源一级区&#xff08;以下简称北方6区&#xff09;平均降水…

Ubuntu22.04密码忘记怎么办 Ubuntu重置root密码方法

在Ubuntu 22.04 或其他更高版本上不小心忘记root或其他账户的密码怎么办&#xff1f; 首先uname -r查看当前系统正在使用的内核版本&#xff0c;记下来 前提&#xff1a;是你的本地电脑&#xff0c;有物理访问权限。其他如远程登录的不适用这套改密方法。 通过以下步骤&#…

基于GIS的生态敏感性评价与产业路径选择研究:以江西省吉安市为例

导读: 确立绿水青山就是金山银山的理念,建立生态经济体系,是新时代生态环境保护与经济发展的协调之道。对产业规划而言,与生态同行,构建绿色产业体系,是推动地区高质量发展的根本要求。鉴于此,文章从实证角度出发,以江西省吉安市为研究对象,采用生态敏感性评价方法,选…

rv1126板子挂载nfs、拉流测试、固件烧写

目录 一、Ubuntu NFS服务器设置设置有线网卡桥接模式安装NFS并启动NFS二、烧录固件三、配置ipwindow上安装adb修改板子的ip,与ubuntu的桥接网卡同网段四、通过ssh登录开发板五、板子上挂载nfs六、测试拉流七、遇见的问题问题一问题二一、Ubuntu NFS服务器设置 设置有线网卡桥…

openwrt上ipv6 ddns 解析

之前写过一个教程如何在openwrt上使用docker版本的ddns解析工具&#xff0c;使用docker的好处是部署简单&#xff0c;支持的域名种类多&#xff1b;openwrt的docker环境安装起来也很方便&#xff0c;尤其有不少编译好的&#xff0c;带docker环境的镜像可以用&#xff0c;例如笔…

什么是向量数据库?

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

via24种人格力量之学习的力量,爱学习的特征和益处

人格力量是一种可支配的价值观&#xff0c;通常是正向的&#xff0c;有益于学习工作和日常生活的&#xff0c;在via 24种人格力量测试中&#xff0c;爱学习是其中的维度之一&#xff0c;爱学习的人格特征会让人终生受益&#xff0c;但是凡事都适度&#xff0c;如果过度的痴迷于…

检测到会话cookie中缺少HttpOnly属性

绿盟科技"远程安全评估系统"安全评估报告,这里记录一下处理过程。 检测到会话cookie中缺少HttpOnly属性 详细描述 会话cookie中缺少HttpOnly属性会导致攻击者可以通过程序(JS脚本、Applet等)获取到用户的cookie信息&#xff0c;造成用户cookie信息泄露&#xff0c…

Java 中 注解是什么?如何使用

当谈到 Java 中的注解时&#xff0c;我们指的是 Java 5 中引入的一种元数据机制&#xff0c;它允许我们在代码中添加元数据信息并在运行时读取它们。在本文中&#xff0c;我们将深入探讨 Java 中的注解&#xff0c;包括它们是什么、如何使用它们以及一些示例代码。 注解是什么&…

浅读《商用密码应用性评估白皮书》

浅读《商用密码应用性评估白皮书》 密码的重要性商用密码概念商用密码典型应用场景&#xff08;一&#xff09;电信和互联网领域&#xff08;二&#xff09;工业互联网领域&#xff08;三&#xff09;车联网领域&#xff08;四&#xff09;物联网领域&#xff08;五&#xff09…

路径规划算法:基于驾驶训练优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于驾驶训练优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于驾驶训练优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化…

Redis实战篇(五)

8、达人探店 8.1 达人探店-发布探店笔记 探店笔记类似点评网站的评价&#xff0c;往往是图文结合。对应的表有两个&#xff1a; tb_blog&#xff1a;探店笔记表&#xff0c;包含笔记中的标题、文字、图片等tb_blog_comments&#xff1a;其他用户对探店笔记的评价 具体发布流…

使用SiO2和高介电常数介质的SiC功率MOSFET的栅极阻抗分析

Impedance n.阻抗 dielectric n.电解质 propagation n.传播 标题&#xff1a;Gate Impedance Analysis of SiC power MOSFETs with SiO2 and High-κ Dielectric 阅读日期&#xff1a;2023.7.13 研究了什么 这篇论文研究了SiC功率MOSFET的门电阻Zgg特性&#xff0c;包括SiO2…

大语言模型举例和相关论文推荐

大语言模型如火如荼。甚至已经爆发了“百模大战” 2023年&#xff0c;“百模大战”&#xff0c;一触即发。 因为工作需要&#xff0c;我除了参加行业、企业、研究机构的发布会和闭门会&#xff0c;还需要基于自身的业务&#xff0c;不断了解最新的AI大模型和AIGC应用。 2024…

Java线程状态

Java线程状态 有哪几种 在Java中&#xff0c;线程可以存在多种状态。以下是常见的几种线程状态&#xff1a; 新建状态&#xff08;New&#xff09;&#xff1a;当创建一个线程对象时&#xff0c;线程即处于新建状态。此时尚未调用线程的start()方法。 可运行状态&#xff08;…

26-分布式锁

1、JMeter下载安装 Index of /dist/jmeter/binaries 1.1、JMeter配置 1.1.1、JMeter页面显示中文 将jmeter.properties文件中的language=zh_CN。 1.1.2、JMeter发送请求返回的Response显示中文 将jmeter.properties文件中的sampleresult.default.encoding=UTF-8 1.2、启…