【C++】Hash开散列,unordered_set(map) 的封装以及迭代器的实现

news2024/11/24 11:18:07

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

目录

一、开散列

1.1 开散列的概念 

1.2 开散列结构

1.3 Insert 插入

1.4 Find 查找

1.5 Insert 扩容

1.6 Erase 删除

1.7 析构函数

1.8 测试接口

1.9 性能测试

二、封装

2.1 封装内部结构

2.2 实现接口

三、迭代器

3.1 迭代器的定义

3.2 常用接口

3.3 迭代器++

3.4 begin()、end()

3.5 find的改动

3.6 下标访问[ ]重载

四、源代码与测试用例

4.1 底层HashTable

4.2 unordered_set/map

4.3 测试用例

一、开散列

1.1 开散列的概念 

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

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;
};

1.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函数。

1.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;
}

1.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;
}

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;
}

1.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;
	}
}

1.8 测试接口

接下来我们写一些hash表的测试接口,以便下面性能测试使用。

//表的长度
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;
}

1.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了。

二、封装

2.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.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值操作的,我们要进行使用两层仿函数进行取值。

接下来我们简单来测试一下代码:

三、迭代器

3.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;	   //指向当前表
};

3.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.3 迭代器++

STL中迭代器++的实现:

思路如下:

  • 判断_node的_next是否存在存在结点,如果存在直接让_node = _node->_next即可
  • 如果不存在结点,则当前桶遍历结束,要寻找下一个有数据的桶。
  • 根据_node中的data域求出映射位置,然后从映射位置向后遍历哈希表,直到talbe[i]处有数据,有数据则跳出循环
  • 当 i 等于哈希表的大小,则表示不存在下一个数据,则将_node赋值为nullptr
  • 返回*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的友元类(也要带上模板参数进行声明噢)。

3.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);
}

然后我们就可以在map、set添加其接口了。

注意,这里要使用typename,要指明iterator是HashTable中的一个类型,而不是变量。

3.5 find的改动

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

3.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;
	}

接下来就可以测试一下

以下这段代码即检测了 [ ]重载,也检测了迭代器,以及find、insert和两个仿函数。


注意,在源码中,其实还有一个仿函数模板参数。

这个仿函数的功能是,如果我们存储的是日期类的指针(Date*),按正常比较会根据指针中存放的地址大小进行比较,我们可以传入自定义传入这个仿函数,就可以做到即使传入的是Date*,也可以按照Date*指向的内容,根据日期进行排序。


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

set

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

unordered_set:

        要求K类型对象能转化为整形取模,或提供能装化为整形的仿函数

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

四、源代码与测试用例

4.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;
};

4.2 unordered_set/map

unordered_set:

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
	struct setKeyOfT;
	typedef typename Brant::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 Brant::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;
};

4.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/168924.html

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

相关文章

C 数据结构1 —— 线性表-顺序表\单链表\双链表

文章目录1. 线性表1.1 定义1.2 特点2. 顺序表(顺序存储结构)2.1 定义(存储结构代码描述)2.2 插入元素2.2.1 图形演示2.2.2 代码表示2.3 删除元素2.3.1 图形演示2.3.2 代码表示2.4 完整代码2.5 动态分配数组3. 单链表(链式存储结构)3.1 定义(存储结构代码描述)3.2 单链表的读取3…

COCO_04 展示COCO格式数据集 目标框与分割mask

文章目录1 前言2 绘制GT2.1 绘制目标框与类别2.2 绘制分割mask3 AppendixA. mask polygon格式转化为图片格式参考1 前言 上篇文章介绍了如何制作COCO个数数据集的Dataset与Dataloader&#xff0c;并绘制了dataloader->batch的返回的信息&#xff0c;https://blog.csdn.net/…

【打卡】医学搜索Query相关性判断学习赛

入坑传送门 赛事介绍 文本匹配拥有广泛的应用场景&#xff0c;可以用于去除重复问题和文本相似度中。在本次学习中我们将学习&#xff1a; 如何计算文本之间的统计距离如何训练词向量 & 无监督句子编码BERT模型搭建和训练 上述步骤都是一个NLP算法工程师必备的基础&…

【GD32F427开发板试用】02-ADC规则组连续采样

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;Stark_GS ADC 简介及特点 器件中集成了一个 12 位 2.6 MSPS 多通道 ADC。 一共有19个多路复用通道&#xff1a;16个外部通道&#xff0c;1个…

office365删除错误发送的邮件

微软喜欢变&#xff0c;office365删个邮件真是不容易。 --管理员登录 Connect-IPPSSession -UserPrincipalName adminmydomain.onmicrosoft.com --创建一个 "deleteemail"的搜索项目&#xff0c;项目名可以任意起&#xff0c;这个名称后面在office365安全合规门户里…

libcurl简介及其编程应用

本文为学习笔记&#xff0c;整合课程内容以及下列文章&#xff1a; 其中&#xff0c;libcurl函数库常用字段解读部分&#xff1a; 参考博文&#xff1a;原文地址 作者&#xff1a;冬冬他哥哥 目录 libcurl简介 libcurl的使用 学会开源包使用的一般步骤 包的解读 重点是看…

Spark 核心编程

文章目录Spark 核心编程一、RDD1、分布式计算模拟(1) 搭建基础的架子(2) 客户端向服务器发送计算任务Spark 核心编程 Spark 计算框架为了能够进行高并发和高吞吐的数据处理&#xff0c;封装了三大数据结构&#xff0c;用于处理不同的应用场景。三大数据结构分别是&#xff1a;…

【数据结构与算法理论知识点】1.1基本概念

1.1基本概念 为什么要学习数据结构与算法&#xff1f; AlgorithmsData StructuresPrograms---- Niklaus Wirth ( Pascal程序设计语言之父、结构化程序设计首创者、图灵奖获得者) 计算机程序&#xff1a;使用计算机求解问题算法是求解问题的步骤的描述&#xff1a;从蛮力到策…

套接字编程(二)UDP服务端与客户端的通信模拟实现

目录 一、前言 二、UDP客户端流程信息 1、创建套接字 2、为套接字绑定地址信息&#xff08;不推荐&#xff09; 3、发送数据&#xff08;将数据放入发送缓冲区中&#xff09; 4、接收数据&#xff08;从socket结构体接收缓冲区中取出数据&#xff09; 5、关闭套接字 三…

机器学习基本概念及问题梳理

前言&#xff1a;整理西瓜书第一、二章中的基本概念 待办&#xff1a;第二章评估方法、性能度量及后续内容未整理 下图梳理机器学习中部分概念 模型评估与选择相关知识点&#xff1a; 错误率&#xff08;error rate, E&#xff09;&#xff1a;如果在m个样本中有a个样本分类…

WordPress安全指南:19个步骤让您的WordPress安全防线坚如磐石

谈到WordPress安全性&#xff0c;您可以采取很多措施来锁定您的网站&#xff0c;以防止黑客和漏洞影响您的电子商务网站或博客。您最不想发生的事情是一天早上醒来发现您的网站一团糟。因此&#xff0c;今天我们将分享许多技巧、策略和技术&#xff0c;您可以使用这些技巧、策略…

WEBSHELL管理工具流量特征——基础篇

前言 前一阵子帮别人做取证题目&#xff0c;有很多关于WEBSHELL的流量要分析&#xff0c;想起来还有没好好分析过于是准备写篇文章总结一下帮助大家能够快速的辨别WEBSHELL流量&#xff0c;下面我们展开文章来讲。 中国菜刀 这个应该是大家最熟悉的WEBSHELL管理工具&#xf…

NeuRay学习笔记

Neural Rays for Occlusion-aware Image-based Rendering 主页&#xff1a;https://liuyuan-pal.github.io/NeuRay/ 论文&#xff1a;https://arxiv.org/abs/2107.13421 Code&#xff1a;https://github.com/liuyuan-pal/NeuRay 效果&#xff1a; desktop摘要 We present a ne…

一文读懂 UniProt 数据库(2023 最新版)

一、UniProt 数据库介绍 Uniprot &#xff08;Universal Protein &#xff09;是包含蛋白质序列&#xff0c;功能信息&#xff0c;研究论文索引的蛋白质数据库&#xff0c;整合了包括EBI&#xff08; European Bioinformatics Institute&#xff09;&#xff0c;SIB&#xff0…

【面试题】前端最新面试题-浏览器 dom、bom篇

原文见&#xff1a;语雀&#xff08;https://www.yuque.com/deepstates/interview/fsitlt&#xff09; ● BOM ● window对象 ○ frames ■ iframe ■ 跨窗口通信 ■ 同源策略/跨域 ○ navigator ● DOM ○ DOM结构 ○ DOM操作 ○ DOM事件 ■ 表单事件 ● 浏览器渲染 ○ 进程、…

Vue组件化编程的组件通信

对于组件化编程&#xff0c;组件之间的通信技术无疑是非常重要的内容&#xff0c;需要将细节牢牢把握。 组件通信&#xff0c;就是子组件放置在父组件内之后&#xff0c;父组件如何向子组件传递参数以及子组件如何与外部组件进行互动。 这部分的知识很重要&#xff0c;需要展开…

基于Ubuntu20.04搭建OpenHarmony v3.0.6的qemu仿真环境

基于Ubuntu20.04搭建OpenHarmony v3.0.6的qemu仿真环境0. 前言1. 安装Ubuntu1.1 更换华为源1.2 安装必要工具2. 下载代码2.1 解压与目录设置3. 配置环境3.1 安装库和工具3.2 设置python版本3.3 安装编译工具hb3.4 切换dash为bash4. 编译4.1 hb构建4.2 启动qemu5. 第二种环境配置…

Java之日期与时间、JDK8新增日期类、包装类、正则表达式、Arrays类、常见算法和Lambda表达式

目录日期与时间DateSimpleDateFormatCalendar概述JDK8新增日期类概述、LocalTime /LocalDate / LocalDateTimeInstantDateTimeFormatterDuration/PeriodchronoUnit包装类正则表达式Arrays类Arrays类概述&#xff0c;常用功能演示Arrays类对于Comparator比较器的支持常见算法选择…

[论文翻译] GIKT: A Graph-based Interaction Model forKnowledge Tracing

摘要随着在线教育的快速发展&#xff0c;知识追踪&#xff08;KT&#xff09;已成为追踪学生知识状态并预测他们在新问题上的表现的基本问题。在线教育系统中的问题通常很多&#xff0c;并且总是与更少的技能相关联。然而&#xff0c;以往的文献未能将问题信息与高阶问题-技能相…

计网必会:电路交换和分组交换

电路交换和分组交换的概念和区别&#xff0c;为什么分组交换更有效&#xff1f; 电路交换&#xff1a;由于电路交换在通信之前要在通信双方之间建立一条被双方独占的物理通路&#xff08;由通信双方之间的交换设备和链路逐段连接而成&#xff09; 特点是源和目标点建立起名副其…