当前位置: 首页 > news >正文

哈希表的完善及unordered_map/unordered_set封装

1.哈希表的完善

1.1 优化:哈希函数

在实际使用中,往往需要以字符串作为存储依据(键值),比如姓名与快递信息、商品名称与价格、中文单词与英文释义等。
而在上一篇文章中,我们实现的哈希表只考虑了整型的存储情况,即直接用key%capacity计算哈希值,如果把整型换成字符串是无法进行计算的。如下图所示。

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

//特化
template<>
struct _Hash<string>
{size_t operator()(const string& key){size_t hash = 0;for (size_t i = 0; i < key.size(); ++i){hash += key[i];}return hash;}
};

利用以上模板函数,sort、left、right三个字符串计算出的值分别为456、427、542,可以看出假如字符串比较短或者字符串较为接近,字符串的每个字符的ASCII码值加起来的和比较接近,或者可能是相同的值,这会导致哈希冲突。因此,单纯的累加每个字符的ASCII码值显得不够专业。有人专门对字符串进行了研究,搞出了各种各样的重复率较底的字符串哈希算法。具体可以参考该文章,字符串哈希算法。
在众多字符串哈希算法中,BKDRHash在各方面都很优秀。因此,本文选择BKDRHash算法作为计算字符串值的函数。

BKDRHash的核心就是在原来值的基础上*131,再加上字符的ASCII码值。这样计算出来的值更为分散,更加符合我们的需求。

//特化
template<>
struct _Hash<string>
{size_t operator()(const string& key){size_t hash = 0;for (size_t i = 0; i < key.size(); ++i){hash *= 131;hash += key[i];}return hash;}
};

1.2 优化:素数大小

使用除留余数法时,哈希表的大小最好是素数,这样能够减少哈希冲突产生的次数。SGI版STL中,哈希表在扩容时就使用了这一技巧。
简单来说,就是当我们进行扩容时,按照下一个素数值的大小进行扩容,这些素数都是近似2倍的大小关系,在确保不会频繁扩容的同时,尽可能减少哈希冲突,所以需要这样一个函数。

size_t GetNextPrime(size_t num)
{const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul,25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,1610612741ul, 3221225473ul, 4294967291ul};for (size_t i = 0; i < PRIMECOUNT; ++i){if (primeList[i] > num){return primeList[i];}}return primeList[PRIMECOUNT - 1];
}

同样的,需要对扩容的地方进行改造。改造之后,哈希表的初始大小变为53。

2 新增迭代器类

2.1 设计背景

哈希表的数据存储分散在多个桶(bucket)中,每个桶可能是一个链表,存储若干个节点。因此,迭代器的实现不仅需要遍历链表中的节点,还需要跳跃到下一个非空桶继续遍历。为了实现这一功能,需要一个特殊的迭代器。因为哈希桶是一个单链表,只能往前走访问链表,不能往回走。因此我们的迭代器要设计为单向迭代器(只支持++)。如下代码所示:

//前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct __HashTableIterator
{KeyOfT keyoft;typedef __HashTableIterator<K, T, KeyOfT, Hash> Self;typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;Node* _node;HT* _pHashTable;//指向哈希表的指针__HashTableIterator(Node* node, HT* pHashTable):_node(node), _pHashTable(pHashTable){}//基本功能
};

2.2 为什么需要存储哈希表指针

迭代器的移动逻辑是:如果当前所在的桶中还有数据,迭代器直接移动到下一个节点即可。如果已经移动到了当前桶中最后一个元素,此时迭代器还需要继续往后移动,迭代器就需要移动至当前哈希表中下一个有数据的桶中。这里显然需要用到哈希表,并且是同一个哈希表。解决办法:构造迭代器__HashTableIterator时,迭代器除了保存指向当前节点的指针_node,还需要保存一个指向哈希表对象的指针_pHashTable,主要原因是为了跨桶查找下一个节点。

那么迭代器如何在哈希表中移动呢?首要问题是确定当前迭代器位于哈希表中的哪个位置,可以通过代码size_t index = HashFunc(koft(cur->_data)) % newtales.size();求出该位置。确定位置后,迭代器往后移动,直到移动至一个不为空的位置,返回即可。

1、桶定位需要哈希表结构:
当遍历完当前桶后,operator++需要从当前桶的索引位置开始,查找下一个不为空的桶;这些桶存储在哈希表的_tables成员中,迭代器需要通过指针_pHashTable访问它。

2、简化设计:
(1)如果不存储哈希表的指针,迭代器__HashTableIterator无法跨桶查找下一个节点,导致实现复杂且效率低下。
(2)存储_pHashTable后,可以直接使用哈希表的结构信息(如桶的数量和内容)进行遍历。

3、提升代码的灵活性:
_pHashTable提供了迭代器与哈希表的关联性,使得__HashTableIterator能够独立于具体的哈希表实现,在其他类似结构中复用。

现在能通过_pHashTable访问一个哈希表。
一个细节:需要对哈希表进行前置声明才能正常使用;

2.3 operator++()的逻辑

operator++的核心任务是移动迭代器到下一个可访问的节点,分为两种情况。

1、当前桶的链表还剩余有节点:
如果当前节点有_next指针,直接移动到链表中的下一个节点。

2、当前桶已经被遍历完:
获取当前桶的索引:根据当前节点的数据,通过哈希函数计算出当前桶的索引。
查找下一个非空桶
(1)从当前桶的下一个位置开始遍历_tables。
(2)找到第一个不为空的桶后,将_node指向该桶的第一个节点。
(3)如果遍历到表尾仍未找到,将_node设置为nullptr,表示迭代结束。

再次理解为什么要存储哈希表指针,如下图代码所示,我们需要表的数据记录hashi以及_size。

Self operator++()
{//当前桶还未遍历结束if (_node->_next){_node = _node->_next;}else{//此时,当前的桶已经遍历完了,需要对下一个桶进行遍历//1、计算当前桶的哈希值size_t hashi = _pHashTable->HashFunc(keyoft(_node->_data)) % _pHashTable->_tables.size();++hashi;//++i进入到下一个哈希桶for (; hashi < _pHashTable->_tables.size(); ++hashi){Node* cur = _pHashTable->_tables[hashi];if (cur){_node = cur;return *this;}}_node = nullptr;}return *this;
}

在该重载函数中,访问了哈希表类中的私有成员_tables,这种操作是不行的,为了让其能够成功访问哈希表类中的私有成员_tables,可以把迭代器类设为哈希表类的友元类。

class HashTable
{typedef HashNode<T> Node;friend struct __HashTableIterator<K, T, KeyOfT, Hash>;
public:typedef __HashTableIterator<K, T, KeyOfT, Hash> iterator;//各类功能
}

再次理解为什么要存储哈希表指针,如下图代码所示,我们需要表的数据记录hashi和_size。

2.4 begin()和end()的实现

1、begin()

  • 从第一个桶开始查找,找到第一个非空桶,返回其第一个节点对应的迭代器。
  • 如果所有的桶都为空,返回迭代器iterator(nullptr,this)

2、end()

  • 返回一个空迭代器iterator(nullptr,this),用于标志迭代结束。
iterator begin()
{for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return iterator(_tables[i], this);}}return end();
}iterator end()
{return iterator(nullptr, this);
}

2.5 友元和前置声明的作用

1、前置声明:

  • __HashTableIterator的定义在HashTable之前,且依赖于HashTable的定义,HashTable的接口(如begin()、end())需要返回__HashTableIterator。
  • 前置声明解决了这种相互依赖的问题,确保了在定义HashTable时能够使用__HashTableIterator。

2、友元

  • __HashTableIterator需要访问HashTable的私有成员_tables和_tables.size(),这是其实现operator++所必须的。
  • 通过友元声明,使__HashTableIterator可以直接访问HashTable的私有成员,简化了代码设计。

2.6 迭代器和哈希表实现的完整代码

#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}
};//前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct __HashTableIterator
{KeyOfT keyoft;typedef __HashTableIterator<K, T, KeyOfT, Hash> Self;typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;Node* _node;HT* _pHashTable;//指向哈希表的指针__HashTableIterator(Node* node, HT* pHashTable):_node(node), _pHashTable(pHashTable){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self operator++(){//当前桶还未遍历结束if (_node->_next){_node = _node->_next;}else{//此时,当前的桶已经遍历完了,需要对下一个桶进行遍历//1、计算当前桶的哈希值size_t hashi = _pHashTable->HashFunc(keyoft(_node->_data)) % _pHashTable->_tables.size();++hashi;//++i进入到下一个哈希桶for (; hashi < _pHashTable->_tables.size(); ++hashi){Node* cur = _pHashTable->_tables[hashi];if (cur){_node = cur;return *this;}}_node = nullptr;}return *this;}bool operator!=(const Self& s){return _node != s._node;}
};//如果K是一个整型,则直接返回该数据
template<class K>
struct _Hash
{const K& operator()(const K& key){return key;}
};//特化
template<>
struct _Hash<string>
{size_t operator()(const string& key){size_t hash = 0;for (size_t i = 0; i < key.size(); ++i){hash *= 131;hash += key[i];}return hash;}
};template<class K,class T ,class KeyOfT,class Hash>
class HashTable
{typedef HashNode<T> Node;friend struct __HashTableIterator<K, T, KeyOfT, Hash>;
public:typedef __HashTableIterator<K, T, KeyOfT, Hash> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return iterator(_tables[i], this);}}return end();}iterator end(){return iterator(nullptr, this);}//析构函数~HashTable(){Clear();}void Clear(){for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}size_t HashFunc(const K& key){Hash hash;return hash(key);}size_t GetNextPrime(size_t num){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul,25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,1610612741ul, 3221225473ul, 4294967291ul};for (size_t i = 0; i < PRIMECOUNT; ++i){if (primeList[i] > num){return primeList[i];}}return primeList[PRIMECOUNT - 1];}bool Insert(const T& data){KeyOfT keyoft;if (Find(keyoft(data)) != nullptr)return false;if (_num == _tables.size()){//创建一个新表,并扩容vector<Node*> newtables;//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;size_t newsize = GetNextPrime(_tables.size());newtables.resize(newsize);//将旧表中的数据拷贝到新表中for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){size_t index = HashFunc(keyoft(data)) % newtables.size();Node* next = cur->_next;cur->_next = newtables[index];newtables[index] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}//正常先查看该数据在不在表中,如果在就返回false,代码前面已经查找过了,//这里就不再查询了。 //计算数据在表中映射的位置size_t index = HashFunc(keyoft(data)) % _tables.size();//将该数据头查到链表中Node* newnode = new Node(data);newnode->_next = _tables[index];_tables[index] = newnode;++_num;return false;}Node* Find(const K& key){if (_tables.size() == 0)return nullptr;KeyOfT keyoft;size_t index = HashFunc(key) % _tables.size();Node* cur = _tables[index];while (cur){if (keyoft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr;}bool Erase(const K& key){if (Find(key) == nullptr)return false;KeyOfT keyoft;size_t index = HashFunc(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[index];while (cur){if (keyoft(cur->_data) == key){if (prev == nullptr){//此时,说明要删除的节点就是链表的第一个节点_tables[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return nullptr;}private:vector<Node*> _tables;size_t _num;//记录哈希表中存储的数据的个数
};

3 迭代器map与set的复用

3.1 map的复用,数据pair<K,V>

#include"HashTable.h"template<class K, class V, class Hash=_Hash<K>>
class MyUnorderedMap
{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename HashTable<K, pair<K, V>, MapKeyOfT, Hash>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}bool Insert(const pair<K, V>& kv){return _ht.Insert(kv);}private:HashTable<K, pair<K, V>, MapKeyOfT, Hash> _ht;
};

3.2 Set的复用,数据Key

#include"HashTable.h"template<class K, class Hash = _Hash<K>>
class MyUnorderedSet
{struct MapKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename HashTable<K, K, MapKeyOfT, Hash>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}bool Insert(const K& key){return _ht.Insert(key);}private:HashTable<K, K, MapKeyOfT, Hash> _ht;
};

4.const迭代器--增加Ref与Ptr模板参数

const迭代器复用普通迭代器的代码,唯一不同的是operator*与operator->的返回值,const迭代器返回的是const T*与const T& ,因此加入两个模板参数Ref与Ptr,实例化时就可以获得两种模式的迭代器。

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

根据以上代码可以实例化出两种模式的迭代器:普通迭代器和const迭代器。

typedef __HashTableIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef __HashTableIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;iterator begin()
{for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return iterator(_tables[i], this);}}return end();
}iterator end()
{return iterator(nullptr, this);
}const_iterator begin() const
{for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return const_iterator(_tables[i], this);}}return end();
}const_iterator end() const
{return iterator(nullptr, this);
}

5.const迭代器权限放大的问题

接下来重载构造函数,使用const HashTable<K,T ,KeyOfT, Hash>*来接收参数。

此时,我们就可以不用提供非const版本的构造函数了。

6.解决值不能修改的问题

对于Set来说,key不可以修改,对于Map来说,pair<K,V>中的K不能修改,因此需要采取对应的措施。
对于Set来说:

普通迭代器与const迭代器都是const迭代器

对于Map来说:

Map成员中的pair修改为pair<const K,V>
迭代器正常走:

7.解决Insert返回值改为pair<iterator,bool>中存在的问题

接下来要做Insert返回值的修改,为重载operator[]做准备:
1、更改返回值。

2、find返回值也要对应修改

这里就Set代码存在问题,现在代码跑不过了。

对于Map来说:

对于Set来说:

因此我们需要提供const_iterator的构造函数,使用iterator构造const_iterator。

上图中涉及到函数返回值会创建临时对象的问题,具体可参考文章C++中产生临时对象的情况及其解决方案。

8.operator[]

对于Map,我们还需要重载operator[],就像数组一样使用,可以插入数据,也可以查找、修改Value,这里我们要借助Insert来实现。

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

9.HashTable的代码实现

#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}
};//前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class Ref,class Ptr, class KeyOfT, class Hash>
struct __HashTableIterator
{KeyOfT keyoft;typedef __HashTableIterator<K, T,Ref, Ptr, KeyOfT, Hash> Self;typedef __HashTableIterator<K, T, T&, T*, KeyOfT, Hash> iterator;typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;Node* _node;const HT* _pHashTable;//指向哈希表的指针__HashTableIterator(Node* node, const HT* pHashTable):_node(node), _pHashTable(pHashTable){}__HashTableIterator(const iterator& it):_node(it._node), _pHashTable(it._pHashTable){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self operator++(){//当前桶还未遍历结束if (_node->_next){_node = _node->_next;}else{//此时,当前的桶已经遍历完了,需要对下一个桶进行遍历//1、计算当前桶的哈希值size_t hashi = _pHashTable->HashFunc(keyoft(_node->_data)) % _pHashTable->_tables.size();++hashi;//++i进入到下一个哈希桶for (; hashi < _pHashTable->_tables.size(); ++hashi){Node* cur = _pHashTable->_tables[hashi];if (cur){_node = cur;return *this;}}_node = nullptr;}return *this;}bool operator!=(const Self& s){return _node != s._node;}
};//如果K是一个整型,则直接返回该数据
template<class K>
struct _Hash
{const K& operator()(const K& key){return key;}
};//特化
template<>
struct _Hash<string>
{size_t operator()(const string& key){size_t hash = 0;for (size_t i = 0; i < key.size(); ++i){hash *= 131;hash += key[i];}return hash;}
};template<class K,class T ,class KeyOfT,class Hash>
class HashTable
{typedef HashNode<T> Node;template<class K, class T, class Ref, class Ptr, class KeyOfT, class HashFunc>friend struct __HashTableIterator;
public:typedef __HashTableIterator<K, T, T&, T*, KeyOfT, Hash> iterator;typedef __HashTableIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return iterator(_tables[i], this);}}return end();}iterator end(){return iterator(nullptr, this);}const_iterator begin() const{for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return const_iterator(_tables[i], this);}}return end();}const_iterator end() const{return const_iterator(nullptr, this);}//析构函数~HashTable(){Clear();}void Clear(){for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}size_t HashFunc(const K& key) const{Hash hash;return hash(key);}size_t GetNextPrime(size_t num){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul,25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,1610612741ul, 3221225473ul, 4294967291ul};for (size_t i = 0; i < PRIMECOUNT; ++i){if (primeList[i] > num){return primeList[i];}}return primeList[PRIMECOUNT - 1];}pair<iterator,bool> Insert(const T& data){KeyOfT keyoft;iterator it = Find(keyoft(data));if (it != end())return make_pair(it,false);if (_num == _tables.size()){//创建一个新表,并扩容vector<Node*> newtables;//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;size_t newsize = GetNextPrime(_tables.size());newtables.resize(newsize);//将旧表中的数据拷贝到新表中for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){size_t index = HashFunc(keyoft(data)) % newtables.size();Node* next = cur->_next;cur->_next = newtables[index];newtables[index] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}//正常先查看该数据在不在表中,如果在就返回false,代码前面已经查找过了,//这里就不再查询了。 //计算数据在表中映射的位置size_t index = HashFunc(keyoft(data)) % _tables.size();//将该数据头查到链表中Node* newnode = new Node(data);newnode->_next = _tables[index];_tables[index] = newnode;++_num;return make_pair(iterator(newnode, this), true);}iterator Find(const K& key){if (_tables.size() == 0)return iterator(nullptr,this);KeyOfT keyoft;size_t index = HashFunc(key) % _tables.size();Node* cur = _tables[index];while (cur){if (keyoft(cur->_data) == key){return iterator(cur,this);}else{cur = cur->_next;}}return iterator(nullptr, this);}bool Erase(const K& key){if (Find(key) == nullptr)return false;KeyOfT keyoft;size_t index = HashFunc(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[index];while (cur){if (keyoft(cur->_data) == key){if (prev == nullptr){//此时,说明要删除的节点就是链表的第一个节点_tables[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return nullptr;}private:vector<Node*> _tables;size_t _num;//记录哈希表中存储的数据的个数
};

10.unordered_map与unordered_set的实现

1、unordered_map:

#include"HashTable.h"template<class K, class V, class Hash=_Hash<K>>
class MyUnorderedMap
{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, 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);}V& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}private:HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};

2、unordered_set:

#include"HashTable.h"template<class K, class Hash = _Hash<K>>
class MyUnorderedSet
{struct MapKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename HashTable<K, K, MapKeyOfT, Hash>::const_iterator iterator;typedef typename HashTable<K, K, MapKeyOfT, 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){//写法1://1、先用一个普通迭代器来接收/*pair<typename HashTable<K, K, MapKeyOfT, Hash>::iterator, bool>ret = _ht.Insert(key);//2、再用普通迭代器来构造const迭代器,前提是补充实现一个拷贝构造函数。return pair<iterator, bool>(ret.first, ret.second);*///写法2:该写法会将_ht.Insert(key)的返回值赋值给pair<iterator,bool>,该过程会自动调用拷贝构造函数return _ht.Insert(key);}private:HashTable<K, K, MapKeyOfT, Hash> _ht;
};

unordered_map的实现及测试代码可参考:unordered_map的实现。unordered_set的实现及测试代码可参考:unordered_set的实现。​​​​​​​

相关文章:

哈希表的完善及unordered_map/unordered_set封装

1.哈希表的完善 1.1 优化&#xff1a;哈希函数 在实际使用中&#xff0c;往往需要以字符串作为存储依据(键值)&#xff0c;比如姓名与快递信息、商品名称与价格、中文单词与英文释义等。 而在上一篇文章中&#xff0c;我们实现的哈希表只考虑了整型的存储情况&#xff0c;即直…...

“物联·数据·产融·场景”聚力垂直数智场景下的新质生产力破局

人工智能、物联网&#xff08;简称AIOT&#xff09;正在深刻改变世界的经济格局&#xff0c;对各行各业产生深厚的影响。12月6日&#xff0c;由深圳市物联网协会、华夏银行深圳分行、深圳市区块链技术应用协会、深圳市康复辅助器具智能技术应用协会联合主办的第五届AIOT生态大会…...

API接口的性能测试与优化策略

在现代软件开发中&#xff0c;API&#xff08;应用程序编程接口&#xff09;扮演着至关重要的角色&#xff0c;它们作为不同服务之间的桥梁&#xff0c;确保数据的顺畅流通与交互。然而&#xff0c;随着用户需求的不断增长和系统复杂性的提升&#xff0c;API接口的性能问题日益…...

【Vue】Part4 接口调用

接口调用方式 原生ajax基于jQuery的ajaxfetchaxios 异步 JavaScript的执行环境是「单线程」所谓单线程&#xff0c;是指JS引擎中负责解释和执行JavaScript代码的线程只有一个&#xff0c;也就是一次只能完成一项任务&#xff0c;这个任务执行完后才能执行下一个&#xff0c;…...

管理系统前端框架开发案例学习

一、 需求分析 本案例的主要目标是开发一个智能学习辅助系统的前端界面&#xff0c;涵盖以下功能模块&#xff1a; 首页&#xff1a;显示系统的总体概览和关键功能介绍。 班级学员管理&#xff1a;实现班级管理和学员管理。 系统信息管理&#xff1a;管理部门和员工信息。 …...

协程设计原理与实现

协程设计原理与汇编实现 同步与异步 对于任何一个事情&#xff0c;都可以划分为不同的步骤。所谓同步&#xff0c;就先做第一个事情&#xff0c;按照这件事的步骤完成这件事之后&#xff0c;再去做第二件事。再去做第三件事&#xff0c;以此类推。 异步就是&#xff0c;可以…...

c++广播通讯的实现

概念大家都很清楚&#xff0c;不赘述。 广播必然用UDP这套东西。 setsockopt() 函数及其在广播中的应用&#xff1a; 在 C 网络编程中&#xff0c;setsockopt() 函数用于设置套接字选项&#xff0c;这些选项可以控制套接字的各种行为。对于广播通信&#xff0c;我们特别关心…...

Leetcode 3377. Digit Operations to Make Two Integers Equal

Leetcode 3377. Digit Operations to Make Two Integers Equal 1. 解题思路2. 代码实现 题目链接&#xff1a;3377. Digit Operations to Make Two Integers Equal 1. 解题思路 这一题的核心思路属于路径遍历问题&#xff0c;我们使用一个堆排来控制最优路径的选择。 我们首…...

高项 - 项目管理原则与项目绩效域

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 博文更新参考时间点&#xff1a;2024-12 高项 - 章节与知识点汇总&#xff1a;点击跳转 文章目录 高项 - 项目管理原则与项目绩效域项目管理12条原则原则1&#xff1a;成为勤勉、尊重和关心他人的管家 (p202)原则…...

【开源】A065—基于SpringBoot的库存管理系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看项目链接获取⬇️&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600个选题ex…...

LeetCode—189. 轮转数组(中等)

题目描述&#xff1a; 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例1&#xff1a; 输入: nums [1,2,3,4,5,6,7], k 3输出:[5,6,7,1,2,3,4] 解释: 向右轮转 1 步:[7,1,2,3,4,5,6] 向右轮转 2 步:[6,7,1,2,3,4,5] 向…...

fastadmin框架同时使用 阿里云oss和阿里云点播

背景 项目的实际需求中既要用到阿里云oss产品又用到阿里云点播系统&#xff0c;实现完美的统一。设置两个地址downUrl&#xff0c;thirdCode。分别代表阿里云oss上传路径和阿里云点播系统vId。 实现 默认框架你已经集成好阿里云oss集成工作&#xff0c;前端html页面实现 <…...

MySQL-DML之数据表操作

文章目录 一. 插入表记录1. 向表中插入部分字段2. 向表中插入所有字段,字段的顺序为创建表时的顺序3. 一次添加多条数据信息 二. 更新表记录1. 更新所有记录的指定字段 更新符号条件记录的指定字段2. 更新符号条件记录的指定字段 三. 删除表记录1. 按条件删除记录2. 清空记录 四…...

Android 逆向/反编译/Hook修改应用行为 基础实现

前言&#xff1a;本文通过一个简单的情景案例实现安卓逆向的基本操作 一、情景描述 本文通过一个简单的情景案例来实现安卓逆向的基本操作。在这个案例中所使用的项目程序是我自己的Demo程序&#xff0c;不会造成任何的财产侵害&#xff0c;本文仅作为日常记录及案例分享。实…...

【前端】理解 JavaScript 对象属性访问的复杂性

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 &#x1f4af;前言&#x1f4af;理论基础&#xff1a;JavaScript 对象属性的访问模式1. 点符号访问&#xff08;Dot Notation&#xff09;2. 方括号访问&#xff08;Bracket Notation&#xff09;点符号…...

进入 Dystopia:第九周游戏指南

本指南将为大家详细说明在第八周的每个体验中可以获得的奖励。 在杂草丛生的反乌托邦废墟中生存&#xff0c;随着大自然重新开垦这片土地&#xff0c;文明已陷入绝望。穿越高耸入云、摇摇欲坠的摩天大楼&#xff0c;抵御末世社会的各种危险。适应这个文明与荒野之间的界限已经消…...

Helm安装Mysql8主从复制集群

目录 一、Helm安装 二、安装mysql 1、拉取镜像 2、修改配置文件 3、创建mysql-secret 4、安装 一、Helm安装 这里不再赘叙&#xff0c;具体安装请参考官网 Helm | 快速入门指南 二、安装mysql 1、拉取镜像 #添加仓库 helm repo add bitnami https://charts.bitnami.c…...

[小白系列]GPU-nvidia-smi指令

‌nvidia-smi&#xff08;NVIDIA System Management Interface&#xff09;是一种命令行实用程序&#xff0c;用于监控和管理NVIDIA GPU&#xff08;图形处理器&#xff09;的状态和性能‌。它提供了一种简单而强大的方式来获取有关GPU的实时信息&#xff0c;并且可以用于诊断、…...

Flutter 图片编辑板(一) 事件路由

一个图片编辑板&#xff0c;有两部分组成。编辑板和内容项。每一个内容项是被InteractiveViewer修饰的widget&#xff0c;具有缩放偏移的功能。 在图片编辑板上&#xff0c; 会有多个内容相&#xff0c;图片或文字&#xff08;添加文字目前还没做过&#xff09;。 当要编辑其中…...

Cherno C++学习笔记 P33 字符串的字面量

在这篇文章当中我们介绍一下有关于字符串更加深入的知识&#xff0c;也就是字符串的字面量。 首先什么是字面量&#xff1f;其实也很简单&#xff0c;就是双引号里面的那一坨&#xff0c;其实就是字面量&#xff0c;我们举一个最简单的例子&#xff1a; #include<iostream…...

数据结构 (36)各种排序方法的综合比较

一、常见排序方法分类 插入排序类 直接插入排序&#xff1a;通过构建有序序列&#xff0c;对于未排序数据&#xff0c;在已排序序列中从后向前扫描&#xff0c;找到相应位置并插入。希尔排序&#xff1a;是插入排序的一种改进版本&#xff0c;先将整个待排序的记录序列分割成为…...

Master EDI 项目需求分析

Master Electronics 通过其全球分销网络&#xff0c;支持多种采购需求&#xff0c;确保能够为客户提供可靠的元件供应链解决方案&#xff0c;同时为快速高效的与全球伙伴建立合作&#xff0c;Master 选择通过EDI来实现与交易伙伴间的数据传输。 EDI为交易伙伴之间建立了一个安…...

java+ssm+mysql计算机等级考试网

项目介绍&#xff1a; 使用javassmmysql开发的计算机等级考试信息网&#xff0c;系统包含前后台&#xff0c;包含超级管理员&#xff0c;系统管理员角色&#xff0c;功能如下&#xff1a; 前台&#xff1a;首页&#xff1b;考试动态&#xff1b;相关资源下载&#xff1b;考试…...

MitelMiCollab 身份绕过导致任意文件读取漏洞复现(CVE-2024-41713)

0x01 产品描述: Mitel MiCollab 是一个企业协作平台,它将各种通信工具整合到一个应用程序中,提供语音和视频通话、消息传递、状态信息、音频会议、移动支持和团队协作功能。0x02 漏洞描述: Mitel MiCollab 的 NuPoint 统一消息 (NPM) 组件中存在身份验证绕过漏洞,由于输入…...

【Python]深入Python日志管理:从logging到分布式日志追踪的完整指南

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 日志是软件开发中的核心部分,尤其在分布式系统中,日志对于调试和问题定位至关重要。本篇文章将从Python标准库的logging模块出发,逐步探讨日志管理的最佳实践,涵盖日志配置、日志分层、日志格式化等基…...

python rstrip 的迷惑行为

在项目中&#xff0c;我需要把字符串末尾的一部分去掉&#xff0c;类似截断&#xff0c;我用ide的随笔提示&#xff0c;发现了rstrip这个方法&#xff0c;然后我试了下&#xff0c;满足我的需求&#xff0c;但在测试过程中&#xff0c;我发现了rstrip的一些行为很让我迷惑。 开…...

SPI通信协议

SPI通信协议 简介通信原理通信原理SPI数据通信的流程可以分为以下几步&#xff1a;通信特性设备时钟时钟速率时钟极性跟相位SPI协议层通讯流程详解优点&#xff1a;缺点&#xff1a; DS1302 时钟实验控制寄存器日历、时钟寄存器寄存器说明 DS1302 读写时序软件功能实现 简介 SP…...

vue中如何实现商品多规格添加(后台商城管理系统)

在制作商城类的后台管理系统中会遇到多规格商品的添加&#xff0c;需要向固定的数组内添加&#xff0c;通过查看数据结构正确的向数组中添加数据。 如图&#xff1a; 功能需求&#xff1a;1.每次点击添加新规格时&#xff0c;批量设置会多出来一行表格和一个标题输入框。 最…...

智创 AI 新视界 -- AIGC 重塑广告行业的创新力量(16 - 7)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...

Runloop

假设你的项目中有关tableView&#xff0c;然后还有一个定时器timer在执行&#xff0c;定时器代码如下&#xff1a; var num 0override func viewDidLoad() {super.viewDidLoad()let timer Timer(timeInterval: 1,target: self,selector: #selector(self.run),userInfo: nil,r…...

代码随想录第40天

121. 买卖股票的最佳时机 class Solution:def maxProfit(self, prices: List[int]) -> int:cost, profit float(inf), 0for price in prices:cost min(cost, price)profit max(profit, price - cost)return profit122.买卖股票的最佳时机II class Solution:def maxPr…...

element plus table组件多选获取数据id

首先给table加上 selection-change"handleSelectionChange"事件 示例 <el-table selection-change"handleSelectionChange" stripe:data"data?.slice((currentPage3 - 1) * pageSize3, currentPage3 * pageSize3)" style"width: 100%…...

自动驾驶:百年演进

亲爱的小伙伴们&#x1f618;&#xff0c;在求知的漫漫旅途中&#xff0c;若你对深度学习的奥秘、JAVA 、PYTHON与SAP 的奇妙世界&#xff0c;亦或是读研论文的撰写攻略有所探寻&#x1f9d0;&#xff0c;那不妨给我一个小小的关注吧&#x1f970;。我会精心筹备&#xff0c;在…...

STM32 了解OLED

内容扩展 调试方式串口调试&#xff1a;通过串口调试&#xff0c;将调试信息发送到电脑端&#xff0c;电脑使用串口助手显示调试信息 显示屏调试&#xff1a;直接将显示屏连接到单片机&#xff0c;将调试信息打印到显示屏上 keil调试模式&#xff1a;借助Keil软件的调试模式&a…...

NanoLog起步笔记-7-log解压过程初探

nonolog起步笔记-6-log解压过程初探 再看解压过程建立调试工程修改makefile添加新的launch项 注&#xff1a;重新学习nanolog的README.mdPost-Execution Log Decompressor 下面我们尝试了解&#xff0c;解压的过程&#xff0c;是如何得到文件头部的meta信息的。 再看解压过程 …...

python连接redis详细步骤

1.下载并安装windows python Window 平台安装 Python: 以下为在 Window 平台上安装 Python 的简单步骤。 打开 WEB 浏览器访问 Python Releases for Windows | Python.org &#xff1a; 记得勾选 Add Python 3.6 to PATH。 在cmd 执行pip install redis 2.编辑python代码…...

使用redis 的stream 做消息中间件 多线程消费消息

1.redis stream 特点 1.支持消息持久化 2.消费者组模式 3.消息确认机制 4. 消息重试机制 5. 死信队列2. 消息生产者服务 2.1 如下代码Service Slf4j public class StreamMessageProducer {Autowiredprivate StringRedisTemplate redisTemplate;private static final String S…...

《操作系统 - 清华大学》6 -5:局部页面置换算法:最不常用置换算法 (LFU, Least Frequently Used)

文章目录 1. 最不常用算法的工作原理2.最不常用算法特征3. 示例 1. 最不常用算法的工作原理 最不常用算法&#xff1a;注意并不是表示算法本身不常用&#xff0c;而是采取最不常使用页面的策略&#xff0c;Least Frequently Used&#xff0c; LFU。LRU 是最久未被访问的页&…...

GWAS分析先做后学

大家好&#xff0c;我是邓飞。 GWAS分析是生物信息和统计学的交叉学科&#xff0c;上可以学习编程&#xff0c;下可以学习统计。对于Linux系统&#xff0c;R语言&#xff0c;作图&#xff0c;统计学&#xff0c;机器学习等方向&#xff0c;都是一个极好的入门项目。生物信息如…...

【threejs】创建FPS相机

原理说明 控制器是一个很麻烦的东西&#xff0c;需要创建更多的类来管理相机行为&#xff0c;并且可自定义性差&#xff0c;所以将部分控制器的功能绑定到相机上&#xff0c;可以解决这些问题&#xff0c;所以我以 FlyControls为例&#xff0c;将控制器功能绑定到相机上&#…...

路径规划之启发式算法之十一:布谷鸟搜索算法(Cuckoo Search,CS)

布谷鸟搜索算法&#xff08;Cuckoo Search&#xff0c;CS&#xff09;是一种新兴的自然启发式算法&#xff0c;由剑桥大学的杨新社教授和S.戴布&#xff08;Xin-She Yang和Suash Deb&#xff09;于2009年提出。该算法基于布谷鸟的寄生性育雏&#xff08;巢寄生&#xff09;行为…...

Mitel MiCollab企业协作平台存在任意文件读取漏洞(CVE-2024-41713)

免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…...

java+springboot+mysql党务(党员)管理系统

项目介绍&#xff1a; 使用javaspringbootmysql开发的党务管理系统&#xff0c;系统包含管理员、用户角色&#xff0c;功能如下&#xff1a; 管理员&#xff1a;用户管理&#xff1b;党支部管理&#xff1b;党员管理&#xff08;入党申请、积极分子、发展对象、预备党员、正式…...

gozero项目迁移与新服务器环境配置,包含服务器安装包括go版本,Nginx,项目配置包括Mysql,redis,rabbit,域名

迁移 **GoZero** 项目到新服务器并配置相关环境涉及多个步骤。以下是一个系统化的指南&#xff0c;涵盖服务器环境安装、数据库和缓存配置、项目部署以及域名绑定。 ### 步骤概述 1. **服务器环境配置** - 安装 Go 语言环境 - 安装 Nginx - 安装 MySQL 和 Redis -…...

使用WebDAV来上传和下载文件

WebDAV是什么 基于Web的分布式编写和版本控制&#xff08;WebDAV&#xff09;是超文本传输协议&#xff08;HTTP&#xff09;的扩展&#xff0c;有利于用户间协同编辑和管理存储在万维网服务器文档。WebDAV 由互联网工程任务组的工作组在 RFC 4918 中定义。许多现代操作系统为 …...

集成运算放大电路反馈判断

集成运算放大电路 一种具有很高放大倍数的多级直接耦合放大电路&#xff0c;因最初用于信号运算而得名&#xff0c;简称集成运放或运放 模拟集成电路中的典型组件&#xff0c;是发展最快、品种最多、应用最广的一种 反馈 将放大电路输出信号的一部分或全部通过某种电路引回到输…...

什么是绩效文化?

绩效文化是一种组织文化&#xff0c;它将绩效视为核心价值观&#xff0c;贯穿于组织的各个层面和活动之中。 一、绩效文化的内涵 目标导向 绩效文化强调组织成员都朝着共同的目标努力。这个目标通常是明确、可衡量的&#xff0c;如企业的年度利润目标、市场份额增长目标等。例…...

【力扣】409.最长回文串

问题描述 思路解析 因为同时包含大小写字母&#xff0c;直接创建个ASCII表大小的桶来标记又因为是要回文子串&#xff0c;所以偶数个数的一定可以那么同时&#xff0c;对于出现奇数次数的&#xff0c;我没需要他们的次数-1&#xff0c;变为偶数&#xff0c;并且可以标记出现过…...

android studio创建虚拟机注意事项

emulator 启动模拟器的时候&#xff0c;可以用 AVD 界面&#xff0c;也可以用命令行启动&#xff0c;但命令行启 动的时候要注意&#xff0c;系统有两个 emulator.exe &#xff0c;建议使用 emulator 目录下的那个&#xff01;&#xff01; 创建类型为google APIs的虚拟机可从…...

DP协议:缩略词

缩写代表的含义ACT分配更改触发&#xff08;Allocation Change Trigger&#xff09;API应用程序编程接口&#xff08;Application Programming Interface&#xff09;AUX辅助&#xff08;Auxiliary&#xff09;BER比特错误率&#xff08;Bit Error Rate&#xff09;bpc每色比特…...