c++STL——哈希表封装:实现高效unordered_map与unordered_set
文章目录
- 用哈希表封装unordered_map和unordered_set
- 改进底层框架
- 迭代器实现
- 实现思路
- 迭代器框架
- 迭代器重载operator++
- 哈希表中获取迭代器位置
- 哈希表的默认成员函数
- 修改后的哈希表的代码
- 封装至上层容器
用哈希表封装unordered_map和unordered_set
在前面我们已经学过如何实现哈希表了。这篇文章我们将着重于使用哈希表对unordered_map和unordered_set这两个容器进行封装。
我们已经有了使用红黑树对map和set封装的经验了,所以这一次使用哈希表封装就不会再过多阐述细节。将对重点部分进行讲解。
此外还需要提及一下的是,c++STL库的这两个容器,底层也是使用哈希桶进行实现的。本篇文章也将使用哈希桶进行实现。因为哈希桶能很大程度上解决哈希冲突的问题。提高效率。
改进底层框架
首先我们需要做一件事情,就是改进一下底层哈希表的框架。
之前为了方便理解原理,每个节点中的数据类型我们都设定为pair类。这样子是默认了哈希表支持的是unordered_map。如果还要实现unordered_set还得在写一份类似的。这里面临的问题和红黑树封装map和set是一样的。这肯定是不太好。所以需要改进一下。
所以第一步是先泛化数据类型,我们统一让数据类型的模板参数为T:
template<class T>
struct HashNode {T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}
};
然后就是对底层的哈希表框架进行改进了。
但是现在就面临着第一个问题,由于现在数据类型又被泛化,在写代码的时候我们也不知道当前传的是什么数据类型,也就是不知道是Key模式还是Key_Value模式。解决方案我们早已见识过,就是多一个模板参数KeyofT,这个模板参数是专门用来提取出传入数据的关键字的。
其实也就是实现哈希表文章中提到的这张图。无论什么数据类型,我们都需要提取出他的关键字Key(可能本身就是),然后再通过Hash这个仿函数转成整形。
相比于上节课哈希桶的实现的改进,也就是数据类型泛化:
template<class K, class T, class KeyOfT, class Hash>
class HashTable {
public:using Node = HashNode<T>;//质数表inline unsigned long __stl_next_prime(unsigned long n){// Note: assumes long is at least 32 bits.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};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list +__stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}//默认构造HashTable():_table(__stl_next_prime(0), nullptr),_NodeNum(0){}pair<Iterator, bool> Insert(const T& data) {Hash hash;KeyOfT kot;Iterator it = Find(kot(data));if (it._node != nullptr) {return {it, false};}size_t hashNum = hash(kot(data));//负载因子为1的时候就进行扩容if (_NodeNum == Capacity()) {vector<Node*> newtables(__stl_next_prime(Capacity() + 1), nullptr);for (size_t i = 0; i < Capacity(); i++) {Node* cur = _table[i];while (cur) {Node* next = cur->_next;// 旧表中节点,挪动新表重新映射的位置size_t New_hashi = hash(kot(cur->_data)) % newtables.size();// 头插到新表cur->_next = newtables[New_hashi];newtables[New_hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newtables);}size_t hashi = hashNum % Capacity();//头插Node* newnode = new Node(data);newnode->_next = _table[hashi];_table[hashi] = newnode;++_NodeNum;return { Iterator(newnode, this), true};}iterator find(const K& key) {return _ht.Find(key);}bool Erase(const K& key) {if (Find(key) == nullptr)return false;Hash hash;KeyOfT kot;size_t hashi = hash(key) % Capacity();Node* prev = nullptr;Node* cur = _table[hashi];while (cur) {if (kot(cur->_data) == key) {//删除操作if (prev == nullptr)//头删_table[hashi] = cur->_next;elseprev->_next = cur->_next;delete cur;--_NodeNum;return true;}prev = cur;cur = cur->_next;}return false;}size_t Capacity() const{return _table.size();}size_t Size() const{return _NodeNum;}private:vector<Node*> _table;size_t _NodeNum = 0;
};
当然,这里的版本是实现了迭代器的。但是并不要紧。对框架的初步改进其实就是改进一下处理数据类型泛化的逻辑。也就是使用仿函数对象kot取出数据中的关键字key而已。这些东西都已经在用红黑树封装的时候就介绍过了。所以这里就不再赘述。
前面也提到过,库里面在实现的时候其实还有一个模板参数Equal。用来支持关键字Key的相等比较的。但是这个我们以理解原理为主,默认所有的数据类型都有支持相等比较。所以这就得保证我们测试的时候传入的数据类型的Key是支持比较的,可能需要自行重载实现。
迭代器实现
这个部分是和红黑树封装最大的不同之处。对于哈希桶的迭代器遍历其实就是从表头的第一个位置的第一个节点开始,按照顺序往下遍历。如果这个桶没了就紧接下一个桶。
我们还是一样的,在哈希表这一层实现好迭代器。上层的容器的迭代器就是调用下层的接口。
当我们打开文档后会发现:unordered_map和unordered_set都是没有反向迭代器的。也就是说,它们的底层哈希表的迭代器是一个单项迭代器。这其实很好理解。我们实现的哈希桶挂的是单链表。单链表总表头走到表尾很简单,顺着指针链接顺序往后走就可以。但是单链表是很难往前走的。在迭代器的分类里,单链表的迭代器是属于单向迭代器。
很多人会说,这个好办,让哈希表底层的vector存储的是一个个的list容器不就好了。这样不就可以做到双向迭代器了吗?可以是可以,但是这样子会把事情复杂化。这样子迭代器就很不好实现了。除此之外,使用list效率也一般。这些就不多说了,感兴趣可以去查查。
况且标准库里面的实现都是用单链表的,所以就跟着标准走了。
实现思路
假设有一个这样的哈希表,怎么样通过迭代器走到下一个节点呢?
我们首先得理解迭代器的本质是什么?我们学过list的实现,实现过红黑树,迭代器其实都是一个指向节点的指针,只不过把它封装到一个类里面去。然后根据结构的不同,对迭代器的一些造作进行特殊的重载。这里也是一样的,迭代器的底层就是指向节点的指针。
然后我们再来看,应该怎么样通过迭代器遍历到下一个节点呢?
如果当前节点的下一个节点不为空,那直接走到紧跟着的下一个节点就好了。
但是如果下一个节点为空呢?那就要换一个桶进行遍历了。但是当前只有一个指向节点的指针,怎么样找到下一个桶呢?桶在哈希表里面。这怎么办呢?
我们来看看库里面是怎么玩的:
库里面在实现迭代器的时候,不仅仅有指向节点的指针,还有一个指向整个哈希表的指针。这就解决了前面那个难题,如何访问哈希表中其它的桶。当下一个节点为空的时候,直接往后找非空桶,如果后续全是空桶,就可以确定到结尾位置了。结尾该怎么处理呢?
还是一样的,让结尾位置的迭代器内部的指针置空就好了。
这个实现方式其实早已接触。早在实现红黑树的迭代器的时候,为了支持迭代器在结尾处还能往前走的逻辑,就需要传入整个树的根节点。要不然没有办法找到整个树对应的最右节点。
迭代器框架
//提前声名一下
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash, class Ref, class Ptr>
struct HT_Iterator {using Node = HashNode<T>;using Self = HT_Iterator<K, T, KeyOfT, Hash, Ref, Ptr>;Node* _node;const HashTable<K, T, KeyOfT, Hash>* _pht;HT_Iterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht):_node(node),_pht(pht){}Ref operator*() {return _node->_data;}Ptr operator->() {return &(_node->_data);}bool operator==(const Self& self) {return _node == self._node;}bool operator!=(const Self& self) {return !(_node == self._node);}
};
这里也是为了实现两种不同版本的迭代器,需要Ref和Ptr这两个模板参数来区分。因为普通迭代器和const迭代器的本质差别就在解引用的时候。这可以减少多写一份冗余的代码。
由于迭代器内部有一个哈希表指针,哈希表又是一个类模板,所以需要把哈希表的模板参数一起拿过来构成迭代器的模板参数。所以这就是为什么哈希表的迭代器的模板参数有这么多。
眼尖的也会发现,最前面竟然多了点东西,提前声名了哈希表和它的模板参数。这是为何?这是因为迭代器和哈希表这两个类互相包含了。你中有我我中有你的情况。c++的编译器是执行向上巡查的进行编译的。而把迭代器写在哈希表的上面,想上找肯定是找不到哈希表的结构的。所以编译器会不认识,所以需要提前声名一下。把迭代器写在哈希表下面也是不可行的。因为到时候哈希表内会通过迭代器实现接口,写在下面一样是找不到。
注意:
这里指向哈希表的指针加了const修饰,这是必要的。库里面是写了两份代码来区分两种不同版本的迭代器。而且库里面的const迭代器内哈希表指针也是const修饰的。
这里我们先不作解释,埋个伏笔,等到迭代器接口实现的时候再来讲解。
迭代器重载operator++
由于迭代器是单项迭代器,是不支持–操作的,也不支持随机迭代器的+/-操作。所以最重要的操作就是实现出++操作。本部分将重点讲解。
前面说到了,哈希表的迭代器就是对一个个节点进行遍历。如果下一个节点不为空,那就走到下一个。下一个为空,就得通过哈希表去找到下一个不为空的桶。反之返回结束位置。
第一种情况非常好办,第二种怎么操作呢?
这个也简单,把当前节点在哈希表vector的下标找出来就好了。然后往后找。况且迭代器这个类的模板参数中是有哈希表所需要的所有模板参数的。也就是说,迭代器内可以使用取出关键字的KeyOfT仿函数,也可以使用将关键字转整形的Hash仿函数,所以这不是很难的事情。
所以来直接看代码:
Self& operator++() {//下一个节点不为空,直接走到下一个节点if (_node->_next) _node = _node->_next;else {Hash hash; KeyOfT kot;//找到当前哈希表所处位置size_t hashi = hash(kot(_node->_data)) % _pht->_table.size();//因为下一个节点为空,得换一个桶,从当前桶的下一个位置开始查找++hashi;size_t i = 0;for (i = hashi; i < _pht->_table.size(); ++i) {Node* cur = _pht->_table[i];if (cur) break;}//这里必须要经过下面判断逻辑,因为不知道是什么方式退出循环的//如果下一个节点不存在(后续全为空桶)if (i == _pht->_table.size())_node = nullptr;else _node = _pht->_table[i]; }return *this;
}//后置++调用前置的逻辑就可以了
Self operator++(int) {Self s = *this;++(*this);return s;
}
所以哈希表迭代器的operator++操作也不是很难。跟着逻辑走就可以了。
哈希表中获取迭代器位置
这个时候就得在哈希表中来写获取迭代器位置的接口了:
using Iterator = HT_Iterator<K, T, KeyOfT, Hash, T&, T*>;
using ConstIterator = HT_Iterator<K, T, KeyOfT, Hash, const T&, const T*>;
在哈希表内部需要区分一下迭代器的版本,所以需要声名一下。当然这也是为了书写代码的方便。在哈希表内部进行两种迭代器的区分如上。
然后需要编写获取迭代器开始位置的接口:
Iterator Begin() {//找到第一个不为空的桶//如果全是空桶就返回End()if (_NodeNum == 0)return End();size_t i = 0;for (i = 0; i < Capacity(); ++i) {Node* cur = _table[i];if (cur) return Iterator(cur, this);}return End();}Iterator End() {return Iterator(nullptr, this);
}ConstIterator Begin() const {if (_NodeNum == 0)return End();size_t i = 0;for (i = 0; i < Capacity(); ++i) {Node* cur = _table[i];if (_table[i]) return ConstIterator(cur, this);}return End();
}ConstIterator End() const { return ConstIterator(nullptr, this);
}
这个时候来讲解一下为什么,在哈希表迭代器内的哈希表指针需要使用const进行修饰。
首先,获取迭代器接口的时候,会有两个版本,一个是普通迭代器,一个是const迭代器。返回值不一样。但是它们的函数名一样,这会构成重载。但是重载的条件是参数不同。返回值不同是没办法构成重载的。所以为了区分,在实现const迭代器的接口的时候,必须修饰成const成员函数。因为const修饰成员函数本质是将隐式参数this指针从类名 * const this变成const 类名 * const this。限制的是this指向的内容不能被修改。隐式参数也是参数,参数不一样,所以构成重载。所以必须对const迭代器的接口修饰成const成员函数。
其次,正是这个原因,导致必须要让迭代器内部的那个哈希表指针修饰为const HashTable*。因为要传哈希表的指针,就是传this指针。但是如果是const迭代器的接口内,由于this指针被const修饰,如果传给迭代器构造函数里面那个哈希表指针参数不加const修饰,就会引起权限放大的问题。之前说过,权限只可以平移或者缩小,不能放大。所以迭代器类里面的哈希表指针必须加const修饰。就算是普通迭代器也可以用,因为权限可以缩小。
这点是我们以前没有见识过的。因为以往的所有迭代器的实现中,都不需要传入迭代器指向的类。比如list,map,set。都是不需要的。而这里需要传入this指针,那就必然会有这个问题。
有人会提出一个疑问,就是哈希表指针修饰成const的了,也就是指向内容不能改变了。那怎么通过迭代器进行修改呢?这就是对const修饰指针理解错误了:
const放在指针*的左边确实是限制了指向内容不能被修改。但是并不是真的说指向的内容再也不能修改了。而是说不能通过被const修饰的那个指针进行修改,也不能将其权限放大给另一个指针修改。事实上,再另整一个与被const修饰的那个指针无关的指针就可以对数据修改了。举个例子看看就知道了:
还要注意的是,这里我使用接口Capacity()来代替_table.size()。是为了代码能写的清楚一点。我自己每次写_table.size()觉得有点累。这里必须实现成const成员函数(如果只实现一个Capacity()接口的情况下,当然一般实现两个版本的)。原因也是一样的,因为const迭代器接口里面调用了,调用接口是通过this指针调用。而经过const修饰的this指针只能调用到const成员函数。正常的this指针既可以调const成员函数,也可以调非const的成员函数。
这里有个问题:普通迭代器的Begin()接口和const版本的接口有点冗余。写的太重复了,能不能复用一下接口呢?
这里是不可以的。因为this指针的缘故,在const版本的迭代器中调用iterator it = Begin()接口,想通过普通迭代器来找。但是这个代码的本质是const 类名* const this ->Begin()。这个返回值是ConstIterator,it的类型是iterator,我们并没有支持这二者的转换。会报错。
哈希表的默认成员函数
unordered_map和unordered_set是适配器模式,底层适配了一个哈希表。此外再无其他变量和 资源。所以上层的一些成员函数就可以不用自行编写了,只需要调用一下下层的接口。因为本质操作的都是底层的哈希表。
//默认构造
HashTable():_table(__stl_next_prime(0), nullptr),_NodeNum(0)
{}//拷贝构造
//说明一下:这里的深拷贝会导致哈希桶的顺序反过来(但是仍能满足深拷贝的要求)
//如果不是特殊要求要保持原来数据这样子就可以了
//因为这样子效率会高的多
//并且哈希桶每个桶中的节点数量其实不会太多,反过来其实不会影响效率
HashTable(const HashTable<K, T, KeyOfT, Hash>& Htable) {//提前扩容 减少移动数据_table.resize(Htable._table.size());for (size_t i = 0; i < Htable.Capacity(); ++i) {Node* cur = Htable._table[i];while (cur) {Insert(cur->_data);cur = cur->_next;}}
}//赋值重载
//使用现代写法 赋值后也是桶反序(因为调用了拷贝构造)
HashTable<K, T, KeyOfT, Hash>& operator=(HashTable<K, T, KeyOfT, Hash> Htable) {_table.swap(Htable._table);std::swap(_NodeNum, Htable._NodeNum);return *this;
}//析构函数
~HashTable() {for (size_t i = 0; i < Capacity(); ++i) {Node* cur = _table[i];while (cur) {Node* curnext = cur->_next;delete cur;cur = curnext;}}
}
这里的拷贝构造就需要一个个的开节点进行映射了。只不过这里有一个问题,如果按照每个桶从上到下,表从左到右的顺序去深拷贝,会发现一个问题,就是每个桶的顺序倒过来了。这是必然的。自己去画图理解一下就知道了。但是这个不要紧。
因为这样子也算是深拷贝。因为每个桶的节点不多,对查找个别数据的影响并没有多大影响。如果想要保证桶的顺序一致,那么对新表的操作就是尾插。对于单链表而言,尾插效率不高。所以一般不是什么特殊的需求的情况下,这样子做就可以了。
赋值重载也是一样的,调用了拷贝构造(传参部分)。所以也是反序。
析构函数只需要一个个释放节点就好了。对于vector本身,因为它已经自行实现析构函数了。编译器会自行调用的。所以不需要去释放vector。
修改后的哈希表的代码
namespace myspace {template<class T>struct HashNode {T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};template<class K>struct HashFunc {size_t operator()(const K& key) {return (size_t)key;}};template<>struct HashFunc<string> {size_t operator()(const string& str) {size_t hash = 0;for (auto ch : str) {hash += ch;hash *= 131;}return hash;}};//提前声名一下template<class K, class T, class KeyOfT, class Hash>class HashTable;//迭代器实现template<class K, class T, class KeyOfT, class Hash, class Ref, class Ptr>struct HT_Iterator {using Node = HashNode<T>;using Self = HT_Iterator<K, T, KeyOfT, Hash, Ref, Ptr>;Node* _node; const HashTable<K, T, KeyOfT, Hash>* _pht;HT_Iterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht):_node(node),_pht(pht){}Self& operator++() {if (_node->_next) _node = _node->_next;else {Hash hash; KeyOfT kot;size_t hashi = hash(kot(_node->_data)) % _pht->_table.size();++hashi;size_t i = 0;for (i = hashi; i < _pht->_table.size(); ++i) {Node* cur = _pht->_table[i];if (cur) break;}//如果下一个节点不存在if (i == _pht->_table.size())_node = nullptr;else _node = _pht->_table[i]; }return *this;}Self operator++(int) {Self s = *this;++(*this);return s;}Ref operator*() {return _node->_data;}Ptr operator->() {return &(_node->_data);}bool operator==(const Self& self) {return _node == self._node;}bool operator!=(const Self& self) {return !(_node == self._node);}};//其实正常来讲还有一个支持key的相等比较的,因为不知道外界传入的key到底支不支持比较//但是这里默认是有的了,没有的话再加入一个仿函数从Equal就可以了template<class K, class T, class KeyOfT, class Hash>class HashTable {public:using Node = HashNode<T>;//友元声名template<class K, class T, class KeyOfT, class Hash, class Ref, class Ptr>friend struct HT_Iterator; //迭代器using Iterator = HT_Iterator<K, T, KeyOfT, Hash, T&, T*>;using ConstIterator = HT_Iterator<K, T, KeyOfT, Hash, const T&, const T*>;Iterator Begin() {//找到第一个不为空的桶//如果全是空桶就返回End()if (_NodeNum == 0)return End();size_t i = 0;for (i = 0; i < Capacity(); ++i) {Node* cur = _table[i];if (cur) return Iterator(cur, this);}return End();}Iterator End() {return Iterator(nullptr, this);}ConstIterator Begin() const {if (_NodeNum == 0)return End();size_t i = 0;for (i = 0; i < Capacity(); ++i) {Node* cur = _table[i];if (_table[i]) return ConstIterator(cur, this);}return End();}ConstIterator End() const { return ConstIterator(nullptr, this);}//质数表inline unsigned long __stl_next_prime(unsigned long n){// Note: assumes long is at least 32 bits.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};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list +__stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}//默认构造HashTable():_table(__stl_next_prime(0), nullptr),_NodeNum(0){}//拷贝构造//说明一下:这里的深拷贝会导致哈希桶的顺序反过来(但是仍能满足深拷贝的要求)//如果不是特殊要求要保持原来数据这样子就可以了//因为这样子效率会高的多//并且哈希桶每个桶中的节点数量其实不会太多,反过来其实不会影响效率HashTable(const HashTable<K, T, KeyOfT, Hash>& Htable) {//提前扩容 减少移动数据_table.resize(Htable._table.size());for (size_t i = 0; i < Htable.Capacity(); ++i) {Node* cur = Htable._table[i];while (cur) {Insert(cur->_data);cur = cur->_next;}}}//赋值重载//使用现代写法 赋值后也是桶反序(因为调用了拷贝构造)HashTable<K, T, KeyOfT, Hash>& operator=(HashTable<K, T, KeyOfT, Hash> Htable) {_table.swap(Htable._table);std::swap(_NodeNum, Htable._NodeNum);return *this;}//析构函数~HashTable() {for (size_t i = 0; i < Capacity(); ++i) {Node* cur = _table[i];while (cur) {Node* curnext = cur->_next;delete cur;cur = curnext;}}}pair<Iterator, bool> Insert(const T& data) {Hash hash;KeyOfT kot;Iterator it = Find(kot(data));if (it._node != nullptr) {return {it, false};}size_t hashNum = hash(kot(data));//负载因子为1的时候就进行扩容if (_NodeNum == Capacity()) {vector<Node*> newtables(__stl_next_prime(Capacity() + 1), nullptr);for (size_t i = 0; i < Capacity(); i++) {Node* cur = _table[i];while (cur) {Node* next = cur->_next;// 旧表中节点,挪动新表重新映射的位置size_t New_hashi = hash(kot(cur->_data)) %newtables.size();// 头插到新表cur->_next = newtables[New_hashi];newtables[New_hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newtables);}size_t hashi = hashNum % Capacity();//头插Node* newnode = new Node(data);newnode->_next = _table[hashi];_table[hashi] = newnode;++_NodeNum;return { Iterator(newnode, this), true};}Iterator Find(const K& key) {Hash hash;KeyOfT kot;size_t hashi = hash(key) % Capacity();Node* cur = _table[hashi];while (cur) {if (kot(cur->_data) == key)return Iterator(cur, this);cur = cur->_next;}return Iterator(nullptr, this);}ConstIterator Find(const K& key) const {Hash hash;KeyOfT kot;size_t hashi = hash(key) % Capacity();Node* cur = _table[hashi];while (cur) {if (kot(cur->_data) == key)return ConstIterator(cur, this);cur = cur->_next;}return ConstIterator(nullptr, this);}bool Erase(const K& key) {if (Find(key) == nullptr)return false;Hash hash;KeyOfT kot;size_t hashi = hash(key) % Capacity();Node* prev = nullptr;Node* cur = _table[hashi];while (cur) {if (kot(cur->_data) == key) {//删除操作if (prev == nullptr)//头删_table[hashi] = cur->_next;elseprev->_next = cur->_next;delete cur;--_NodeNum;return true;}prev = cur;cur = cur->_next;}return false;}size_t Capacity() const{return _table.size();}size_t Size() const{return _NodeNum;}private:vector<Node*> _table;size_t _NodeNum = 0;};}
这里Insert()接口的返回值改成了pair类,这是因为后续要支持unordered_map的operator[]重载。
封装至上层容器
接下来的封装就很简单了,就和红黑树封装map和set一样,就不再多赘述:
封装unordered_map:
namespace myspace {//把Hash转化整形函数的默认值给到上层,这样子上层可以使用默认值 要不然上层也得传template<class K, class V, class Hash = HashFunc<K>>class unordered_map {struct umKeyOfT {const K& operator()(const pair<K, V>& kv) {return kv.first;}};public:using Node = HashNode<pair<const K, V>>;typedef typename HashTable<K, pair<const K, V>, umKeyOfT, Hash>::Iterator iterator;typedef typename HashTable<K, pair<const K, V>, umKeyOfT, Hash>::ConstIterator const_iterator;iterator begin() {return _ht.Begin();}const_iterator begin() const {return _ht.Begin();}iterator end() {return _ht.End();}const_iterator end() const {return _ht.End();}pair<iterator, bool> insert(const pair<K, V>& kv) {return _ht.Insert(kv);}iterator find(const K& key) {return _ht.Find(key);}const_iterator find(const K& key) const {return _ht.Find(key);}V& operator[](const K& key) { pair<iterator, bool> ret = _ht.Insert({ key, V() });iterator it = ret.first; return it->second; }private:HashTable<K, pair<const K, V>, umKeyOfT, Hash> _ht;};}
这里的unordered_map支持operator[]的方式和map中的是一模一样的。但这知只是表面一样,因为封装导致的。但其实底层的实现差别很大。
封装unordered_set:
namespace myspace {//把Hash转化整形函数的默认值给到上层,这样子上层可以使用默认值 要不然上层也得传template<class K, class Hash = HashFunc<K>>class unordered_set {struct usKeyOfT {const K& operator()(const K& key) {return key;}};public:using Node = HashNode<const K>;typedef typename HashTable<K, const K, usKeyOfT, Hash>::Iterator iterator;typedef typename HashTable<K, const K, usKeyOfT, Hash>::ConstIterator const_iterator;iterator begin() {return _ht.Begin();}const_iterator begin() const {return _ht.Begin();}iterator end() {return _ht.End();}const_iterator end() const {return _ht.End();}pair<iterator, bool> insert(const K& key) {return _ht.Insert(key);}iterator find(const K& key) {return _ht.Find(key);}const_iterator find(const K& key) const {return _ht.Find(key);}private:HashTable<K, const K, usKeyOfT, Hash> _ht;};}
由此可见封装的好处。我们只需要实现好底层。上层只需要调用对应接口即可操作。
相关文章:
c++STL——哈希表封装:实现高效unordered_map与unordered_set
文章目录 用哈希表封装unordered_map和unordered_set改进底层框架迭代器实现实现思路迭代器框架迭代器重载operator哈希表中获取迭代器位置 哈希表的默认成员函数修改后的哈希表的代码封装至上层容器 用哈希表封装unordered_map和unordered_set 在前面我们已经学过如何实现哈希…...
通过迁移学习改进深度学习模型
在 ArcGIS Living Atlas of the World (Browse | ArcGIS Living Atlas of the World)中,可以下载能够分类或检测影像中要素的预训练深度学习模型。 深度学习模型在与用于训练模型的原始影像十分相似的影像上运行效果最好。 如果您所拥有的影像…...
SpringAI更新:废弃tools方法、正式支持DeepSeek!
AI 技术发展很快,同样 AI 配套的相关技术发展也很快。这不今天刚打开 Spring AI 的官网就发现它又又又又更新了,而这次更新距离上次更新 M7 版本才不过半个月的时间,那这次 Spring AI 给我们带来了哪些惊喜呢?一起来看。 重点升级…...
输入一个正整数,将其各位数字倒序输出(如输入123,输出321)
之前的解法: 这种方法仅支持三位数。 学了while之后,可以利用循环解决。 这种方法动态构建逆序数,支持任意长度的正整数。...
react+html2canvas+jspdf将页面导出pdf
主要使用html2canvasjspdf 1.将前端页面导出为pdf 2.处理导出后图表的截断问题 export default function AIReport() {const handleExport async () > {try {// 需要导出的内容idconst element document.querySelector(#AI-REPORT-CONTAINER);if (!element) {message.err…...
Spring Boot 自动装配技术方案书
Spring Boot 自动装配技术方案书(增强版) 一、Spring Boot 自动装配体系全景解析 1.1 核心设计理念 “约定优于配置”:通过合理的默认配置减少开发工作量“即插即用”:通过标准化扩展机制实现组件自动集成“分层解耦”:业务代码与基础设施分离,通过SPI机制实现扩展二、组…...
面试--HTML
1.src和href的区别 总结来说: <font style"color:rgb(238, 39, 70);background-color:rgb(249, 241, 219);">src</font>用于替换当前元素,指向的资源会嵌入到文档中,例如脚本、图像、框架等。<font style"co…...
(3)python开发经验
文章目录 1 sender返回对象找不到函数2 获取绝对路径3 指定翻译字符 更多精彩内容👉内容导航 👈👉Qt开发 👈👉python开发 👈 1 sender返回对象找不到函数 在PySide6中多个信号绑定一个槽函数,使…...
机密虚拟机的威胁模型
本文将介绍近年兴起的机密虚拟机(Confidential Virtual Machine)技术所旨在抵御的威胁模型,主要关注内存机密性(confidentiality)和内存完整性(integrity)两个方面。在解释该威胁可能造成的问题…...
LLM笔记(一)基本概念
LLMs from scratch Developing an LLM: Building, Training, Finetuning LLM 的基本概念与定义: LLM是深度神经网络模型,能够理解、生成和解释类似人类的语言。“大型”指的是模型参数数量巨大以及训练数据集的规模庞大。LLM通常基于Transformer架构,并通…...
嵌入式(c语言篇)Day9
嵌入式Day9 C语言字符串标准库函数笔记 一、概述 C语言提供了一系列字符串标准库函数用于处理字符串,使用这些函数需要包含头文件 <string.h>。主要函数包括求字符串长度、字符串复制、字符串拼接和字符串比较等。我们不仅要理解这些函数的行为,…...
006-nlohmann/json 结构转换-C++开源库108杰
绝大多数情况下,程序和外部交换的数据,都是结构化的数据。 1. 手工实现——必须掌握的基本功 在的业务类型的同一名字空间下,实现 from_json 和 to_json 两个自由函数(必要时,也可定义为类型的友元函数)&a…...
b站视频如何下载到电脑——Best Video下载器
你是不是也经常在B站刷到超赞的视频,想保存到电脑慢慢看,却发现下载不了?别急,今天教你一个超简单的方法,轻松下载B站视频到电脑,高清画质,随时随地想看就看! 为什么需要下载B站视频…...
【行为型之模板方法模式】游戏开发实战——Unity标准化流程与可扩展架构的核心实现
文章目录 🧩 模板方法模式(Template Method Pattern)深度解析一、模式本质与核心价值二、经典UML结构三、Unity实战代码(关卡流程系统)1. 定义抽象模板类2. 实现具体子类3. 客户端使用 四、模式进阶技巧1. 钩子方法&am…...
每日算法-250514
每日算法学习记录 (2024-05-14) 今天记录三道 LeetCode 算法题的解题思路和代码。 1. 两数之和 题目截图: 解题思路 这道题要求我们从一个整数数组中找出两个数,使它们的和等于一个给定的目标值 target,并返回这两个数的下标。 核心思路是使用 哈希…...
信息安全入门基础知识
信息安全是保护信息系统和数据免受未经授权的访问、使用、披露、中断、修改或破坏的实践。对于个人和组织来说,了解信息安全的基础知识至关重要。 1. CIA三元组 信息安全的三个主要目标,也称为CIA三元组: 机密性(Confidentiality): 确保信息不被未经授权的人访问或披露完整性…...
力扣-98.验证二叉搜索树
题目描述 给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下: 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。 class Solutio…...
Java 框架配置自动化:告别冗长的 XML 与 YAML 文件
在 Java 开发领域,框架的使用极大地提升了开发效率和系统的稳定性。然而,传统框架配置中冗长的 XML 与 YAML 文件,却成为开发者的一大困扰。这些配置文件不仅书写繁琐,容易出现语法错误,而且在项目规模扩大时ÿ…...
大疆无人机自主飞行解决方案局限性及增强解决方案-AIBOX:特色行业无人机巡检解决方案
大疆无人机自主飞行解决方案局限性及增强解决方案-AIBOX:特色行业无人机巡检解决方案 大疆无人机是低空行业无人机最具性价比的产品,尤其是大疆机场3的推出,以及持续自身产品升级迭代,包括司空2、大疆智图以及大疆智运等专业软件和…...
【机器人】复现 SG-Nav 具身导航 | 零样本对象导航的 在线3D场景图提示
SG-Nav提出了一种新的零样本物体导航框架,用三维场景图来表示观察到的场景。 并设计了一个分层的思路链提示,帮助LLM通过遍历节点和边,根据场景上下文推理目标位置。 本文分享SG-Nav复现和模型推理的过程~ 下面是一个查找椅子示…...
详细说说Spring的IOC机制
Spring 的 IOC(控制反转)是框架的核心机制,用于管理对象的创建和依赖注入,通过将控制权从应用程序代码转移到容器,实现组件间的解耦。以下是详细解析: 1. IOC 核心概念 控制反转(Inversion of C…...
Android Activity之间跳转的原理
一、Activity跳转核心流程 Android Activity跳转的底层实现涉及 系统服务交互、进程间通信(IPC) 和 生命周期管理,主要流程如下: startActivity() 触发请求 应用调用 startActivity() 时,通过 Inst…...
第二个五年计划!
下一阶段!5年后!33岁!体重维持在125斤内!腰围74! 健康目标: 体检指标正常,结节保持较小甚至变小! 工作目标: 每年至少在一次考评里拿A(最高S,A我理…...
交易所功能设计的核心架构与创新实践
交易所功能设计的核心架构与创新实践 ——从用户体验到安全合规的全维度解析 一、核心功能模块:构建交易生态的四大支柱 1. 用户账户管理 多因子身份验证:集成邮箱/手机注册、谷歌验证器(2FA)、活体检测(误识率<0…...
Windows10安装WSA
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、WSAOnWin10二、安装1.第一种方法2.第二种方法 总结 前言 有时候工作需要用到WSA,然而我们的电脑是Windows10的不能直接安装。接下来我就教你们…...
标签部件(lv_label)
一、如何创建标签部件以及设置文本? 知识点1:如何创建标签部件 lv_obj_t *label lv_label_create(parent); 知识点2:设置文本的3种方式 ①直接设置文本,存储文本的内存动态分配:lv_label_set_text(label,"he…...
Spring中的循环引用
循环依赖发生在两个或两个以上的bean互相持有对方,形成闭环。Spring框架允许循环依赖存在,并通过三级缓存解决大部分循环依赖问题: 一级缓存:单例池,缓存已完成初始化的bean对象。 二级缓存:缓存尚未完成生…...
技术选型不当,如何避免影响项目进展
建立选型评估机制、综合考虑业务与技术匹配度、引入技术决策审查流程、做好选型后的风险预案与替代方案准备 是避免因技术选型不当影响项目进展的关键措施。尤其要重视建立选型评估机制,通过全流程、数据化、多维度的评估体系,确保所选技术能在性能、可维…...
图表制作-基础饼图
首先登录自己的账号,没有账号的可以注册一个。 登录之后,在左侧菜单栏找到图表制作-统计图。 点击新建统计图,点击饼图-基础饼图。 初始会有一些演示数据,可以根据自己的需要进行修改。 如果嫌手动修改太麻烦,可以导入…...
Java 大视界 -- 基于 Java 的大数据分布式存储在工业互联网海量设备数据长期存储中的应用优化(248)
往期文章推荐: 《大数据新视界》和《 Java 大视界》专栏: Java 大视界 – Java 大数据在智能教育自适应学习路径动态调整中的应用与实践(247)(最新)Java 大视界 – Java 大数据在智能安防生物特征识别系统中的多模态…...
如何恢复被勒索软件加密的服务器文件(解密与备份策略)
针对勒索软件加密文件的恢复和解密策略,结合当前数据安全最佳实践,整理应对指南如下: 一、文件解密与修复方法 立即隔离设备 断开网络连接并禁用共享功能,防止病毒横向传播 通过文件后缀异常(如.locked、.wxx&…...
Java知识框架
一、Java 基础语法 1. 基础语法 数据类型 基本类型:int, double, boolean, char 等 引用类型:String, 数组, 对象 变量与常量 final 关键字 作用域(局部变量、成员变量) 运算符 算术、逻辑、位运算 三元运算符 ? : 控制…...
腾讯云-人脸核身+人脸识别教程
一。产品概述 慧眼人脸核身特惠活动 腾讯云慧眼人脸核身是一组对用户身份信息真实性进行验证审核的服务套件,提供人脸核身、身份信息核验、银行卡要素核验和运营商类要素核验等各类实名信息认证能力,以解决行业内大量对用户身份信息真实性核实的需求&a…...
102. 二叉树的层序遍历递归法:深度优先搜索的巧妙应用
二叉树的层序遍历是一种经典的遍历方式,它要求按层级逐层访问二叉树的节点。通常我们会使用队列来实现层序遍历,但递归法也是一种可行且有趣的思路。本文将深入探讨递归法解决二叉树层序遍历的核心难点,并结合代码和模拟过程进行详细讲解。 …...
电脑内存智能监控清理,优化性能的实用软件
软件介绍 Memory cleaner是一款内存清理软件。功能很强,效果很不错。 Memory cleaner会在内存用量超出80%时,自动执行“裁剪进程工作集”“清理系统缓存”以及“用全部可能的方法清理内存”等操作,以此来优化电脑性能。 同时,我…...
Chrome浏览器实验性API computePressure的隐私保护机制如何绕过?
一、computePressure API 设计原理与隐私保护机制 1.1 API 设计目标 computePressure是W3C提出的系统状态监控API,旨在: • 提供系统资源状态的抽象指标(非精确值) • 防止通过高精度时序攻击获取用户指纹 • 平衡开发者需求与用户隐私保护 1.2 隐私保护实现方式 // 典…...
开放传神创始人论道AI未来|“广发证券—国信中数人工智能赛道专家交流论坛“落幕
4月25日,“广发证券—国信中数人工智能赛道专家交流论坛”在广发证券大厦成功举办。本次论坛由广发证券股份有限公司与北京国信中数投资管理有限公司联合主办,汇聚了人工智能领域的50多位企业、行业专家、专业投资机构的精英代表,旨在搭建产学…...
MySQL八股(自用)
MySQL 定位慢查询 1.聚合查询 2.多表查询 3.表数据量过大查询 4.深度分页查询 MySQL自带慢日志 开启慢查询日志,配置文件(/etc/my.cnf) 开启慢日志,设置慢日志的时间 用EXPLAIN或者DESC命令获取MySQL如何执行SELECT语句的信…...
2025年6月一区SCI-不实野燕麦优化算法Animated Oat Optimization-附Matlab免费代码
引言 近年来,在合理框架内求解优化问题的元启发式算法的发展引起了全球科学界的极大关注。本期介绍一种新的元启发式算法——不实野燕麦优化算法Animated Oat Optimization algorithm,AOO。该算法模拟了不实野燕麦的3种独特行为,于2025年6月…...
如何开发一款 Chrome 浏览器插件
Chrome是由谷歌开发的网页浏览器,基于开源软件(包括WebKit和Mozilla)开发,任何人都可以根据自己需要使用、修改或增强它的功能。Chrome凭借着其优秀的性能、出色的兼容性以及丰富的扩展程序,赢得了广大用户的信任。市场…...
UniApp 微信小程序绑定动态样式 :style 避坑指南
在使用 UniApp 开发跨端应用时,绑定动态样式 :style 是非常常见的操作。然而,很多开发者在编译为 微信小程序 时会遇到一个奇怪的问题: 原本在 H5 中可以正常渲染的样式,在微信小程序中却不生效! 让我们通过一个示例来…...
基于OpenCV中的图像拼接方法详解
文章目录 引言一、图像拼接的基本流程二、代码实现详解1. 准备工作2. 特征检测与描述detectAndDescribe 函数详解(1)函数功能(2)代码解析(3)为什么需要这个函数?(4)输出数…...
【BUG】滴答定时器的时间片轮询与延时冲突
SysTick定时器实现延时与时间戳的深度分析与问题解决指南 1. SysTick基础原理 1.1 SysTick的功能与核心配置 SysTick是ARM Cortex-M内核的系统定时器,常用于以下场景: 时间戳:通过周期性中断记录系统运行时间(如tick_ms计数器&…...
基于EFISH-SCB-RK3576/SAIL-RK3576的智能快递分拣机技术方案
(国产化替代J1900的物流自动化解决方案) 一、硬件架构设计 高速视觉识别系统 多目立体成像: 双MIPI-CSI接入16K线阵相机(扫描速度5m/s),支持0.1mm级条形码破损识别NPU加速YOLOv7算法࿰…...
The 2022 ICPC Asia Xian Regional Contest(E,L)题解
E Find Maximum 题意: 首先,通过观察与打表,可以发现: 规律: 对于非负整数 x,函数 f(x) 的值等于: 将 xx 写成三进制后,各个位数的数字之和 该三进制数的位数。 例如,…...
Jmeter 安装包与界面汉化
Jmeter 安装包: 通过网盘分享的文件:CSDN-apache-jmeter-5.5 链接: https://pan.baidu.com/s/17gK98NxS19oKmkdRhGepBA?pwd1234 提取码: 1234 Jmeter界面汉化:...
《Python星球日记》 第70天:Seq2Seq 与Transformer Decoder
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、Seq2Seq模型基础1. 什么是Seq2Seq模型?2. Encoder-Decoder架构详解1️⃣编码器(Encoder)2️⃣解码器(Decoder)3. 传统Seq2Seq模型的局限性…...
【Linux】基础指令(Ⅱ)
目录 1. mv指令 2. cat指令 3.echo指令 补:输出重定向 4. more指令 5. less指令 6. head指令和tail指令 7.date指令 时间戳: 8. cal指令 9. alias指令 10.grep指令 1. mv指令 语法:mv [选项]... 源文件/目录 目标文件/目录 …...
【Python3教程】Python3基础篇之输入与输出
博主介绍:✌全网粉丝23W+,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物联网、机器学习等设计与开发。 感兴趣的可…...
mysql的一个缺点
最近再移植一个从oracle转mysql的项目,喜提一个报错: You cant specify target table A016 for update in FROM clause 对应的程序代码: public void setCurrent(String setId, String pk, String userId) throws SysException {String[]…...