【C++】哈希开散列 | unordered系列容器的封装

news2024/11/26 0:58:09

文章目录

  • 一.开散列
    • 1. 开散列的概念
    • 2. 开散列结构
    • 3. Insert 插入
    • 4. Find 查找
    • 5. Insert 扩容
    • 6. Erase 删除
    • 7. 析构函数
    • 8. 其它函数接口
    • 9. 性能测试
  • 二.封装
    • 1. 封装内部结构
    • 2. 实现接口
  • 三.代器器
    • 1. 迭代器的定义
    • 2. 常用接口
    • 3. 迭代器++
    • 4. begin()、end()
    • 5. find的改动
    • 6. 下标访问[ ]重载
  • 四.源码与测试用例
    • 1. 底层HashTable
    • 2. unordered_set/map
    • 3. 测试用例

前言: 上一篇博客我们使用闭散列的方式实现了Hash,其实在STL库unordered_set、unordered_map中底层是开散列的方式实现的Hash,所以,本篇博客就再使用开散列的方式实现Hash,并将unordered_set、unordered_map进行封装。

一.开散列

1. 开散列的概念

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

在这里插入图片描述

2. 开散列结构

首先我们要使用vector来存储每个链表的节点,然后每个节点中有数据域和指针next域。然后我们可以将HashNode的构造函数写一下,使用pair类型构造处一个HashNode。

template <class K, class V>
struct HashNode
{
	HashNode(const pair<K, V>& kv)
		:_kv(kv), _next(nullptr)
	{
	}
	pair<K, V> _kv;
	HashNode<K, V>* _next;
};
 
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	typedef HashNode<K, V> Node; 
private:
		vector<Node*> _table;
		size_t _size = 0;
};

3. Insert 插入

首先我们实现插入的主逻辑,然后对其进行逐步优化。

我们根据 kv 创建一个节点,然后根据仿函数进行取模求出映射位置,然后进行链表的头插。

bool Insert(const pair<K, V>& kv)
{        
    Hash hash;
	size_t hashi = hash(kv.first) % _table.size();
	//头插
	Node* newNode = new Node(kv);
	newNode->_next = _table[hashi];
	_table[hashi] = newNode;
	++_size;
	return true;
}

像哈希表中插入数据首先要保证数据的唯一性,所以我们要先进行去重处理,此时我们顺带实现Find函数。

4. Find 查找

根据key值求出映射位置,如果该位置不为空,则进行链表的遍历,如果找到key值,则返回cur节点,如果找不到则向后遍历,直到cur为空。

Node* Find(const K& key)
{
	if (_table.size() == 0) return nullptr;
	Hash hash;
	size_t hashi = hash(key) % _table.size();
	//向桶中进行查找 
	Node* cur = _table[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return nullptr;
}

5. Insert 扩容

插入的主逻辑实现了,去重判断也实现了,接下来就是表的扩容。

如果哈希表的大小为0或达到了哈希的负载因子,则要进行扩容。

我们看一下STL库中负载因子控制的多少:

在这里插入图片描述

STL库中设计的负载因子为:当表中插入的元素个数>哈希表的大小,即负载因为为1的时候进行扩容,将表的大小扩容到 next_size.

扩容的挪动数据要注意,因为开散列的每个桶上的数据个数不同。进行扩容后,桶中每个元素都可能映射到不同的新位置处,所以我们不能像闭散列那样复用Insert,要重新将结点链接到新表中。

挪动时要让原表中的结点一个一个链接到新表中

//扩容  ---  如果插入的数据等于表的大小
if (_size == _table.size())
{
	size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
	vector<Node*> newTable;
	newTable.resize(newSize, nullptr);
	//将旧表中的节点移动映射到新表
	Hash hash;
	for (size_t i = 0; i < _table.size(); i++)
	{
		Node* cur = _table[i];
		while (cur)
		{
			Node* next = cur->_next;
			size_t hashi = hash(cur->_kv.first) % _table.size();
			cur->_next = newTable[hashi];
			newTable[hashi] = cur;
			cur = next;
		}
		//将旧表i位置处结点清空
		_table[i] = nullptr;
	}
	_table.swap(newTable);
}

在这里插入图片描述
在这里插入图片描述

发现源码中进行扩容时调用了next_size函数,扩容直接将size乘以2不就行了吗,为什么要特殊计算 size ?

因为hash表的大小最好是素数,如果是素数,映射的结果冲突几率就小,因为非素数因子多,进行映射后相同位置冲突大。将hash表的大小设计为素数后,其实就可以做到hash表中个别桶的冲突次数过多而过分的大。

详细可以看这篇文章:算法分析:哈希表的大小为何是素数

现在我们也添加这个功能:

库中使用lower_bound(返回第一个大于等于n的下标)/upper_bound(返回第一个大于n的下标),其实直接使用for循环遍历就行了.

inline size_t __stl_next_prime(size_t n)
{
	static const size_t __stl_num_primes = 28;
	static const size_t  __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 (size_t i = 0; i < __stl_next_prime; i++)
	{
		if (__stl_prime_list[i] > n)
			return __stl_prime_list[i];
	}
	return (size_t)-1;
}

6. Erase 删除

虽然我们实现了Find函数,但是单单使用Find是无法完成删除功能的。

例如下面这种情况,单链表删除中间结点我们还需要知道 prev 结点。

在这里插入图片描述

bool Erase(const K& key)
{
	if (_table.size() == 0) return false;
	Hash hash;
	size_t hashi = hash(key) % _table.size();
	Node* pre = nullptr;
	Node* cur = _table[hashi];
	while (cur)
	{
		if (cur->_kv.first == hash(key))
		{
			//如果删除的是链中第一个元素 --- 即头删
			if (pre == nullptr)
			{
				_table[hashi] = cur->_next;
			}
			//2.中间删除
			else
			{
				pre->_next = cur->_next;
			}
			delete cur;
			--_size;
			return true;
		}
		pre = cur;
		cur = cur->_next;
	}
	return false;
}

7. 析构函数

注意了,当哈希表生命周期结束后会调用析构函数,我们使用的vector会自动释放表中的内容,可是vector中存放的是链表,我们释放时还要对桶(链表)进行释放,所以我们要手动写一个析构函数。

~HashTable()
{
	for (size_t i = 0; i < _table.size(); ++i)
	{
		Node* cur = _table[i];
		while (cur)
		{
			Node* next = cur->_next;
			delete cur;
			cur = next;
		}
		_table[i] = nullptr;
	}
}

8. 其它函数接口

//表的长度
size_t BucketSize()
{
	return _table.size();
}
//数据个数
size_t Size()
{
	return _size;
}
//桶的数量
size_t BucketNum()
{
	size_t Num = 0;
	for (size_t i = 0; i < BucketSize(); i++)
	{
		if (_table[i]) Num++;
	}
	return Num;
}
//最长的桶
size_t MaxBucketLenth()
{
	size_t Max_len = 0;
	size_t temp = 0;
	for (size_t i = 0; i < BucketSize(); i++)
	{
		if (_table[i])
		{
			size_t len = 1;
			Node* cur = _table[i]->_next;
			while (cur)
			{
				len++;
				cur = cur->_next;
			}
			if (len > Max_len)
			{
				Max_len = len;
				temp = i;
			}
		}
	}
	printf("Max_len_i:[%u]\n", temp);
	return Max_len;
}

9. 性能测试

void TestHT()
{
	int n = 18000000;
	vector<int> v;
	v.reserve(n);
	srand((unsigned int)time(0));
	for (int i = 0; i < n; ++i)
	{
		v.push_back(rand()+i);  // 重复少
		//v.push_back(rand());  // 重复多
	}
	size_t begin1 = clock();
	HashTable<int, int> ht;
	for (auto e : v)
	{
		ht.Insert(make_pair(e, e));
	}
	size_t end1 = clock();
 
	cout << "数据个数:" << ht.Size() << endl;
	cout << "表的长度:" << ht.BucketSize() << endl;
	cout << "桶的个数:" << ht.BucketNum() << endl;
	cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
	cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
	cout << "负载因子:" << (double)ht.Size() / (double)ht.BucketSize() << endl;
}

在这里插入图片描述

发现,将哈希表的大小设置为素数后,即使负载因子到了0.9,最长的桶也不过才是 2。所以hash表的查找效率为O(1)。

接下来我们对比红黑树和hash表其查找效率(查找1千万个数据)

在这里插入图片描述

哈希表插入效率较低,是因为扩容挪动数据非常消耗时间。

接下来我们使用set、onordered_set(底层对应的就是红黑树和hash表),向其中插入1千万的随机数,对比其性能,并对onordered_set进行直接插入和提前扩容再进行插入的效率对比。

在这里插入图片描述

测试代码如下:

void test_op()
{
	int n = 10000000;   //1千万个数据
	vector<int> v;
	v.reserve(n);
	srand((unsigned int)time(0));
	for (int i = 0; i < n; ++i)
	{
		//v.push_back(i);
		v.push_back(rand()^ 1311 * 144+i);
	}
 
	size_t begin1 = clock();
	set<int> s;
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();
 
	size_t begin2 = clock();
 
	unordered_set<int> us;
	us.reserve(n);
 
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();
 
	cout << "有效数据个数:" << s.size() << endl;
	cout << "\nInsert 插入:" << endl;
	cout << "set : " << end1 - begin1 << endl;
	cout << "unordered_set : " << end2 - begin2 << endl;
 
	size_t begin3 = clock();
	for (auto e : v)
	{
		s.find(e);
	}
	size_t end3 = clock();
 
	size_t begin4 = clock();
	for (auto e : v)
	{
		us.find(e);
	}
	size_t end4 = clock();
 
	cout << "\nFind 查找:" << endl;
	cout << "set :" << end3 - begin3 << endl;
	cout << "unordered_set :" << end4 - begin4 << endl;
 
 
	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();
	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();
	cout << "\nErase 删除:" << endl;
 
	cout << "set erase:" << end5 - begin5 << endl;
	cout << "unordered_set erase:" << end6 - begin6 << endl;
}

以上就是我们hash开散列的基本实现了,实现了以上功能我们就可以封装unordered_map/unordered_set了。

二.封装

1. 封装内部结构

首先是改变HashTable中每个结点存储的数据类型,如unordered_set中存放的是key,unordered_map中存放的是pair类型,所以我们将结点中存储的类型改为T,如果是set,T对应就是key,如果是map,那T就对应pair结构

template <class T>
struct HashNode
{
	HashNode(const T& data)
		:_data(data), _next(nullptr)
	{}
 
	T _data;
	HashNode<T>* _next;
};

所以,Insert插入的类型也应改为T模板类型,在使用到类型中的值时,使用仿函数取出该比较的数据。

然后我们就来编写unordered_set(map)类

unordered中底层就是调用我们写的HashTable,所以直接使用HashTable定义成员变量,并传入模板参数。(以下简写的set、map都对应的Hash方法实现的unordered_set(map))

注意,因为set是Key模型,设置一个模板参数即可;而map是KV模型,需要设置两个模板参数对应pair的中的两个数据类型。所以,在底层我们统统传入HashTalbe两个模板参数,并以第二个模板参数为准决定底层存储什么类型,如果是set,就使用仿函数取出key,如果是map就使用仿函数取出pair.first。

所以,在传入参数前我们要先编写好仿函数set(map)KeyOfT,以便于底层取出数据。

//****   set   *********
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
private:
	struct setKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
    //两个模板参数都传入K
	HashTable<K, K, Hash, setKeyOfT> _ht;
};
//****   map   *********
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
private:
	//让HashTable取出pair中的K  --- 内部类
	struct mapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
	HashTable<K, pair<K, V>, Hash, mapKeyOfT> _ht;
};

2. 实现接口

接下来就是为我们封装的map、set 设计成员函数,其实我们只是封装了一层,本质还是调用HashTable中的Insert、Erase等函数。

// ******  set  ********
bool insert(const K& kv)
{
	return _ht.Insert(kv);
}
 
bool erase(const K& kv)
{
	return _ht.Erase(kv);
}
 
// ******  map  ********
bool insert(const pair<K, V>& kv)
{
	return _ht.Insert(kv);
}
bool erase(const K& k)
{
	return _ht.Erase(k);
}

注意,Insert、Erase的底层中,涉及到key值操作的,我们要进行使用两层仿函数进行取值。

三.代器器

1. 迭代器的定义

在HashTable中有迭代器的接口(begin()、end()),而迭代器中也会使用到HashTable的结构,所以,在实现迭代器之前我们要先进行HashTable的声明(注意:模板类的声明要加上模板参数一起声明)。

我们来看看源码中迭代器是如何定义的

在这里插入图片描述

接下来是我们的定义:

//前置声明
template <class K, class T, class Hash, class keyOfT>
class HashTable;
 
template<class K, class T, class Hash, class keyOfT>
class __Hash_Iteartor
{
public:
	typedef HashNode<T> Node;
	typedef HashTable<K, T, Hash, keyOfT> HT;
	typedef __Hash_Iteartor<K, T, Hash, keyOfT> Self;
    __Hash_Iteartor(Node* node, HT* pht)
  		:_node(node), _pht(pht)
	{}
    __Hash_Iteartor()
    {}
private:
	//成员变量
	Node* _node;   //指向结点
	HT* _pht;	   //指向当前表
};

2. 常用接口

接下来实现一些常用的接口:

T& operator*()
{
	return _node->_data;
}
 
T* operator->()
{
	return &_node->_data;
}
bool operator!=(const Self& self)
{
	return _node != self._node;
}
bool operator==(const Self& self)
{
	return _node == self._node;
}

3. 迭代器++

STL中迭代器++的实现:

在这里插入图片描述

思路如下:

  1. 判断_node的_next是否存在存在结点,如果存在直接让_node = _node->_next即可
  2. 如果不存在结点,则当前桶遍历结束,要寻找下一个有数据的桶。
  3. 根据_node中的data域求出映射位置,然后从映射位置向后遍历哈希表,直到talbe[i]处有数据,有数据则跳出循环
  4. 当 i 等于哈希表的大小,则表示不存在下一个数据,则将_node赋值为nullptr
  5. 返回*this,即返回当前对象。
Self& operator++()
{
	//在当前桶中进行++
	if (_node->_next)
	{
		_node = _node->_next;
	}
	else //找下一个有效的桶
	{
		Hash hash;
		keyOfT kft;
		size_t i = hash(kft(_node->_data)) % _pht->_table.size();
		for (i += 1; i < _pht->_table.size(); i++)
		{
			if (_pht->_table[i])
			{
				_node = _pht->_table[i];
				break;
			}
		}
		//如果不存在有数据的桶
		if (i == _pht->_table.size())
			_node = nullptr;
	}
	return *this;
}

注意,此时我们使用了哈希表,具体访问了其中的元素,所以我们要让迭代器作为HashTable的友元类(也要带上模板参数进行声明噢)。

在这里插入图片描述

4. begin()、end()

begin就是返回HashTable中第一个存储了数据的桶。如果表中没有存储数据,直接返回end(),而end()迭代器中的_node为nullptr构造的。

typedef __Hash_Iteartor<K, T, Hash, keyOfT> iterator;
 
iteratorbegin()
{
	for (size_t i = 0; i < _table.size(); i++)
	{
		if (_table[i])
			return iterator(_table[i], this);
	}
	return end();
}
iterator end()
{
	return iterator(nullptr, this);
}

5. find的改动

find中,我们返回是直接返回迭代器,在return的地方使用匿名对象进行返回即可。

在这里插入图片描述

6. 下标访问[ ]重载

如果要实现map中的下标访问操作符重载,我们要对Insert进行改造,让其返回值为pair结构,其中first为迭代器,second为bool类型,表示插入成功与否(虽然不改变也能实现)。

在这里插入图片描述
在这里插入图片描述

Insert的改动完成后,接下来就可以在map中添加 [] 下标访问操作符重载了。

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

面试题:
一个类型K去做 set 和 unordered_set 的模板参数有什么要求?

  1. set :
    set要求支持能进行小于号比较,或者显示提供比较的仿函数

  2. unordered_set:

    • 要求K类型对象能转化为整形取模,或提供能装化为整形的仿函数
    • K类型对象要支持等于比较,或提供等于比较的仿函数 (set有小于,就可以通过左小右大的方式找到数据;而unordered_set会出现冲突,使用key值只能找到映射的桶,遍历桶的时候,就需要进行等于比较了)

四.源码与测试用例

1. 底层HashTable

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
 
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
			val = val * 131 + ch;
		return val;
	}
};
 
template <class T>
struct HashNode
{
	HashNode(const T& data)
		:_data(data), _next(nullptr)
	{}
 
	T _data;
	HashNode<T>* _next;
};
 
 
// 对哈希表进行前置声明
template <class K, class T, class Hash, class keyOfT>
class HashTable;
 
template<class K, class T, class Hash, class keyOfT>
class __Hash_Iteartor
{
public:
	typedef HashNode<T> Node;
	typedef HashTable<K, T, Hash, keyOfT> HT;
	typedef __Hash_Iteartor<K, T, Hash, keyOfT> Self;
	__Hash_Iteartor(Node* node, HT* pht)
		:_node(node), _pht(pht)
	{}
	__Hash_Iteartor()
		:_node(nullptr), _pht(nullptr)
	{}
 
	T& operator*()
	{
		return _node->_data;
	}
 
	T* operator->()
	{
		return &_node->_data;
	}
	Self& operator++()
	{
		//在当前桶中进行++
		if (_node->_next)
		{
			_node = _node->_next;
		}
		else //找下一个有效的桶
		{
			Hash hash;
			keyOfT kft;
			size_t i = hash(kft(_node->_data)) % _pht->_table.size();
			for (i += 1; i < _pht->_table.size(); i++)
			{
				if (_pht->_table[i])
				{
					_node = _pht->_table[i];
					break;
				}
			}
			//如果不存在有数据的桶
			if (i == _pht->_table.size())
				_node = nullptr;
		}
		return *this;
	}
 
	bool operator!=(const Self& self)
	{
		return _node != self._node;
	}
	bool operator==(const Self& self)
	{
		return _node == self._node;
	}
 
private:
	//成员
	Node* _node;   //指向结点
	HT* _pht;	   //指向当前表
 
};
 
template <class K, class T, class Hash, class keyOfT>
class HashTable
{
public:
	typedef HashNode<T> Node;
	//将迭代器设为友元
	template<class K, class T, class Hash, class keyOfT>
	friend class  __Hash_Iteartor;
 
	typedef __Hash_Iteartor<K, T, Hash, keyOfT> iterator;
 
	iterator begin()
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			if (_table[i])
				return iterator(_table[i], this);
		}
		return end();
	}
	iterator end()
	{
		return iterator(nullptr, this);
	}
 
	//析构要进行特殊处理,遍历整个表,再删除桶中的数据。
	~HashTable()
	{
		for (size_t i = 0; i < _table.size(); ++i)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
	}
 
	inline size_t __stl_next_prime(size_t n)
	{
		static const size_t __stl_num_primes = 28;
		static const size_t  __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 (size_t i = 0; i < __stl_num_primes; i++)
		{
			if (__stl_prime_list[i] > n)
				return __stl_prime_list[i];
		}
		return (size_t)-1;
	}
	pair<iterator, bool> Insert(const T& data)
	{
		Hash hash;
		keyOfT koft;
		//去重
		iterator ret = Find(koft(data));
		if (ret != end())
		{
			return make_pair(ret, false);
		}
		//扩容  ---  如果插入的数据等于表的大小
		if (_size == _table.size())
		{
			//size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
			vector<Node*> newTable;
			size_t newSize = __stl_next_prime(_table.size());
			newTable.resize(newSize, nullptr);
			//将旧表中的节点移动映射到新表
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					size_t hashi = hash(koft(cur->_data)) % newSize;
					cur->_next = newTable[hashi];
					newTable[hashi] = cur;
					cur = next;
				}
				//将旧表i位置处结点清空
				_table[i] = nullptr;
			}
			_table.swap(newTable);
		}
		size_t hashi = hash(koft(data)) % _table.size();
		//头插
		Node* newNode = new Node(data);
		newNode->_next = _table[hashi];
		_table[hashi] = newNode;
		++_size;
 
		return make_pair(iterator(newNode, this), true);
	}
 
	iterator Find(const K& key)
	{
		if (_table.size() == 0) return end();
		Hash hash;
		keyOfT koft;
		size_t hashi = hash(key) % _table.size();
		//向桶中进行查找 
		Node* cur = _table[hashi];
		while (cur)
		{
			if (koft(cur->_data) == key)
			{
				return iterator(cur, this);
			}
			cur = cur->_next;
		}
		return end();
	}
 
	//单链表不能直接找到该节点并删除
	bool Erase(const K& key)
	{
		if (_table.size() == 0) return false;
		Hash hash;
		keyOfT koft;
		size_t hashi = hash(key) % _table.size();
		Node* pre = nullptr;
		Node* cur = _table[hashi];
		while (cur)
		{
			if (koft(cur->_data) == hash(key))
			{
				//如果删除的是链中第一个元素 --- 即头删
				if (pre == nullptr)
				{
					_table[hashi] = cur->_next;
				}
				//2.中间删除
				else
				{
					pre->_next = cur->_next;
				}
				delete cur;
				--_size;
				return true;
			}
			pre = cur;
			cur = cur->_next;
		}
		return false;
	}
 
 
	//表的长度
	size_t BucketSize()
	{
		return _table.size();
	}
	//数据个数
	size_t Size()
	{
		return _size;
	}
	//桶的数量
	size_t BucketNum()
	{
		size_t Num = 0;
		for (size_t i = 0; i < BucketSize(); i++)
		{
			if (_table[i]) Num++;
		}
		return Num;
	}
	//最长的桶
	size_t MaxBucketLenth()
	{
		size_t Max_len = 0;
		size_t temp = 0;
		for (size_t i = 0; i < BucketSize(); i++)
		{
			if (_table[i])
			{
				size_t len = 1;
				Node* cur = _table[i]->_next;
				while (cur)
				{
					len++;
					cur = cur->_next;
				}
				if (len > Max_len)
				{
					Max_len = len;
					temp = i;
				}
			}
		}
		printf("Max_len_i:[%u]\n", temp);
		return Max_len;
	}
	void Print_map()
	{
		cout << "Print_map:" << endl;
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				cout << "i:" << i << " [" << cur->_data.first << " " << cur->_data.second << "] " << endl;
				cur = cur->_next;
			}
		}
	}
	void Print_set()
	{
		cout << "Print_set:" << endl;
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				cout << "i:" << i << " [" << cur->_data << "] " << endl;
				cur = cur->_next;
			}
		}
	}
 
 
 
private:
	vector<Node*> _table;
	size_t _size = 0;
};

2. unordered_set/map

unordered_set:

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
	struct setKeyOfT;
	typedef typename dianxia::HashTable<K, K, Hash, setKeyOfT>::iterator iterator;
 
	iterator begin()
	{
		return _ht.begin();
	}
	iterator end()
	{
		return _ht.end();
	}
 
	pair<iterator, bool> insert(const K& kv)
	{
		return _ht.Insert(kv);
	}
 
	bool erase(const K& kv)
	{
		return _ht.Erase(kv);
	}
	void print()
	{
		_ht.Print_set();
	}
 
private:
	struct setKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
	HashTable<K, K, Hash, setKeyOfT> _ht;
};

unordered_map:

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
	struct mapKeyOfT;
	typedef typename dianxia::HashTable<K, pair<K, V>, Hash, mapKeyOfT>::iterator iterator;
 
	iterator begin()
	{
		return _ht.begin();
	}
	iterator end()
	{
		return _ht.end();
	}
 
	pair<iterator, bool> insert(const pair<K, V>& kv)
	{
		return _ht.Insert(kv);
	}
	bool erase(const K& k)
	{
		return _ht.Erase(k);
	}
 
	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
		return ret.first->second;
	}
	void print()
	{
		_ht.Print_map();
	}
 
private:
	//取出pair中的K值  --- 内部类
	struct mapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
	HashTable<K, pair<K, V>, Hash, mapKeyOfT> _ht;
};

3. 测试用例

封装测试:

void test_unordered01()
{
	Brant::unordered_map<int, int> mp1;
	mp1.insert({ 1,1 });
	mp1.insert({ 54,54 });
	mp1.insert({ 2,2 });
	mp1.insert({ 3,3 });
	mp1.insert({ 4,4 });
	mp1.insert({ 6,6 });
	mp1.insert({ 6,6 });
	mp1.print();
	cout << "Erase:---------------" << endl;
	mp1.erase(1);
	mp1.erase(54);
	mp1.print();
 
	cout << endl << "--------------------------------------" << endl;
	Brant::unordered_set<int> st1;
	st1.insert(1);
	st1.insert(54);
	st1.insert(2);
	st1.insert(3);
	st1.insert(4);
	st1.insert(6);
	st1.insert(6);
	st1.print();
	cout << "Erase:---------------" << endl;
	st1.erase(1);
	st1.erase(54);
	st1.print();
}

迭代器测试:

void test_iterator01()
{
	Brant::unordered_map<string, string> dict;
	dict.insert({ "sort","排序" });
	dict.insert({ "left","左边" });
	dict.insert({ "right","右边" });
	dict.insert({ "string","字符串" });
	Brant::unordered_map<string, string>::iterator it = dict.begin();
	while (it != dict.end())
	{
		cout << it->first << " : " << it->second << endl;
		++it;
	}
	cout << endl;
}
 
void test_iterator02()
{
	Brant::unordered_map<string, int> countMap;
	string arr[] = { "苹果","西瓜","菠萝","草莓","菠萝","草莓" ,"菠萝","草莓"
			, "西瓜", "菠萝", "草莓", "西瓜", "菠萝", "草莓","苹果" };
	for (auto e : arr)
	{
		countMap[e]++;
	}
	for (auto kv : countMap)
	{
		cout << kv.first << " " << kv.second << endl;
	}
}

本文到此结束,码文不易,还请多多支持哦!!!

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

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

相关文章

如果您需要高质量的电源模块,不要犹豫,选择YB5011 非隔离AC-DC!

您是否正在寻找高质量的电源模块&#xff1f;我们昱灿电子推荐YB5011 非隔离AC-DC。它具有广泛的输入电压范围和高达90%的高效率。这款电源模块还配备了多种保护功能&#xff0c;如过载和短路保护&#xff0c;确保您的设备始终处于安全状态。不仅如此&#xff0c;YB5011还采用了…

Linux命令200例:whereis用于搜索以及定位二进制文件

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;本文已收录于专栏&#xff1a;Linux命令大全。 &#x1f3c6;本专栏我们会通过具体的系统的命令讲解加上鲜…

【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制

目录 前言&#xff1a; 引入&#xff1a; 锁机制&#xff1a; CAS算法&#xff1a; 乐观锁与悲观锁&#xff1a; 总结&#xff1a; 前言&#xff1a; 在多线程编程中&#xff0c;线程之间的协作和资源共享是一个重要的话题。当多个线程同时操作共享数…

windows(iis)服务器部署安装wordpress(php)网站教程

该教程包含iis安装,php安装,mysql安装,php网站部署上线,windows服务部署php网站,只需要这一篇文章就够了。 该教程为iis服务器部署安装wordpress(php)网站教程,同样适用wordpress网站迁移。 配置要求 1、windows服务器安装iis windows服务器安装iis管理器 打开控制面…

最新AI创作系统ChatGPT源码V2.5.8/支持GPT4.0+GPT联网提问/支持ai绘画Midjourney+Prompt+MJ以图生图+思维导图生成!

使用Nestjs和Vue3框架技术&#xff0c;持续集成AI能力到系统&#xff01; 最新版【V2.5.8】更新&#xff1a; 新增 MJ 官方图片重新生成指令功能同步官方 Vary 指令 单张图片对比加强 Vary(Strong) | Vary(Subtle)同步官方 Zoom 指令 单张图片无限缩放 Zoom out 2x | Zoom ou…

2020年09月 Python(一级)真题解析#中国电子学会#全国青少年软件编程等级考试

一、单选题 第1题 Python自带的编程环境是&#xff1f; A&#xff1a;PyScripter B&#xff1a;Spyder C&#xff1a;Notepad D&#xff1a;IDLE 正确的答案是&#xff1a;D Python自带的编程环境是IDLE&#xff08;Integrated Development and Learning Environment&a…

LVS工作环境配置

一、LVS-DR工作模式配置 模拟环境如下&#xff1a; 1台客户机 1台LVS负载调度器 2台web服务器 1、环境部署 &#xff08;1&#xff09;LVS负载调度器 yum install -y ipvsadm # 在LVS负载调度器上进行环境安装 ifconfig ens33:200 192.168.134.200/24 # 配置LVS的VIP…

Synchronized同步锁的优化方法 待完工

Synchronized 和后来出的这个lock锁的区别 在并发编程中&#xff0c;多个线程访问同一个共享资源时&#xff0c;我们必须考虑如何维护数据的原子性。在 JDK1.5 之前&#xff0c;Java 是依靠 Synchronized 关键字实现锁功能来做到这点的。Synchronized 是 JVM 实现的一种内置锁…

【枚举】CF1706 C

有人一道1400写了一个小时 Problem - C - Codeforces 题意&#xff1a; 思路&#xff1a; 首先先去观察样例&#xff1a; 很显然&#xff0c;对于n是奇数的情况&#xff0c;只有一种情况&#xff0c;直接操作偶数位就好了 主要是没搞清楚n是偶数的情况 其实有个小技巧&…

前沿分享-鱼形机器人

可能并不太前沿了&#xff0c;是21年底的新闻了&#xff0c;但是看见了就顺便发一下吧。 大概就是&#xff0c;通过在pH响应型水凝胶中编码不同的膨胀速率而构建了一种环境适应型变形微机器人,让微型机器人直接向癌细胞输送药物从而减轻药物带来副作用。 技术原理是&#xff0c…

第五章 树与二叉树

一、数据结构定义 二叉树的顺序存储结构二叉树的链式存储结构&#xff08;即二叉链表&#xff09;&#xff08;因为有两个指针所以是二叉链表&#xff0c;如果再加一个指向父节点的指针就是三叉链表&#xff09;typedef struct BTNode{ // BT即Binary Tree&#xff0c;二叉树i…

Reinforcement Learning with Code 【Code 4. DQN】

Reinforcement Learning with Code 【Code 4. DQN】 This note records how the author begin to learn RL. Both theoretical understanding and code practice are presented. Many material are referenced such as ZhaoShiyu’s Mathematical Foundation of Reinforcement…

C++笔记之从数组指针到函数数组指针(使用using name和std::function)

C笔记之从数组指针到函数数组指针(使用using name和std::function) 参考笔记&#xff1a; C之指针探究(三)&#xff1a;指针数组和数组指针 C之指针探究(十三)&#xff1a;函数指针数组 C之指针探究(二)&#xff1a;一级指针和一维数组 C之指针探究(十一)&#xff1a;函数名的…

HCIP VLAN--Hybrid接口

一、VLAN的特点 1、一个VLAN就是一个广播域&#xff0c;所以在同一个VLAN内部&#xff0c;计算机可以直接进行二层通信&#xff1b;而不同VLAN内的计算机&#xff0c;无法直接进行二层通信&#xff0c;只能进行三层通信来传递信息&#xff0c;即广播报文被限制在一个VLAN内。 …

数字人现身大运会,怎么以动作捕捉技术助推运动与文博相结合

中国移动动感地带数字人橙络络&#xff0c;作为数智体验官以元宇宙的视角&#xff0c;带领观众沉浸式体验大运会&#xff0c;以极具科技和未来的数字人&#xff0c;对外传递大运青春风采&#xff0c;并且数字人橙络络还对大运会的赛事、活动进行了科普、讲解以及表演当地特色才…

Cpp学习——string(2)

目录 ​编辑 容器string中的一些函数 1.capacity() 2.reserve() 3.resize() 4.push_back()与append() 5.find系列函数 容器string中的一些函数 1.capacity() capacity是string当中表示容量大小的函数。但是string开空间时是如何开的呢&#xff1f;现在就来看一下。先写…

Postgresql15安装插件madlib2.0

madlib2.0简介 Apache MADlib madlib使用指南 MADlib: Main Page madlib安装 Installation Guide - Apache MADlib - Apache Software Foundation 准备 cmake 3.5.2及以上版本python3.9postgresql15&#xff0c;源码编译时必须指定 –with-pythonpostgresql插件plpython3u 源…

【Autolayout自动布局介绍2 Objective-C语言】

一、好,我们继续来介绍Autolayout 1.好,那么,我们接下来,开始上课了, 刚才,给大家介绍了一下,我们就是屏幕布局里面,用到的,主要就是这四个按钮: 1)第一个:是用来做对齐的 2)第二个:是用来做固定的 3)第三个:是用来解决自动布局中出现的一些问题的 4)第四…

元组是什么,python怎么使用元组

目录 引言 元组的概念 作用和优势 元组的应用 Python中如何使用元组 注意事项 总结 引言 在Python编程语言中&#xff0c;元组&#xff08;Tuple&#xff09;是一种不可变的数据结构&#xff0c;它允许我们以有序且不可修改的方式存储多个元素。与列表不同&#xff0c;…

简单游戏截图_可控截取内容2

一个需求 我需要在场景中截取不同层级的截图(如只截模型或只截UI或只截外部相加看到的画面 或全都截或和Shader配合呈现人眼夜视仪热成像的画面切换) 将截图排到列表中&#xff0c;在场景UI中展示出来 如何做 相机要能够看到不同的画面 将当前帧画面存储下来 将存储的画面展示出…