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

由此可见封装的好处。我们只需要实现好底层。上层只需要调用对应接口即可操作。

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

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

相关文章

虹科应用 | 探索PCAN卡与医疗机器人的革命性结合

随着医疗技术的不断进步&#xff0c;医疗机器人在提高手术精度、减少感染风险以及提升患者护理质量方面发挥着越来越重要的作用。医疗机器人的精确操作依赖于稳定且高效的数据通信系统&#xff0c;虹科提供的PCAN四通道mini PCIe转CAN FD卡&#xff0c;正是为了满足这一需求而设…

Yolov8的详解与实战-深度学习目标检测

Yolov8的详解与实战- 文章目录 摘要 模型详解 C2F模块 Loss head部分 模型实战 训练COCO数据集 下载数据集 COCO转yolo格式数据集&#xff08;适用V4&#xff0c;V5&#xff0c;V6&#xff0c;V7&#xff0c;V8&#xff09; 配置yolov8环境 训练 测试 训练自定义数据集 Labelme…

scons user 3.1.2

前言 感谢您抽出时间阅读有关 SCons 的内容。SCons 是一款下一代软件构建工具&#xff0c;或者称为 make 工具&#xff0c;即一种用于构建软件&#xff08;或其他文件&#xff09;并在底层输入文件发生更改时使已构建的软件保持最新状态的软件实用程序。 SCons 最显著的特点是…

Java的多线程笔记

创建一个线程的方法有多种&#xff0c;比如可以继承Thread类或者实现Runnable接口&#xff0c;结论是实现Runnable接口比前者更加优越。 二者代码对比 Java 不支持多继承&#xff0c;如果你继承了 Thread 类&#xff0c;就不能再继承其他类&#xff0c;实现 Runnable 接口后&am…

PDF Base64格式字符串转换为PDF文件临时文件

需求描述&#xff1a; 在对接电子病历系统与河北CA&#xff0c;进行免密文件签章的时候&#xff0c;两者系统入参不同&#xff0c;前者是pdf文件&#xff0c;base64格式&#xff1b;后者要求File类型的PDF文件。 在业务中间层开发时&#xff0c;则需要接收EMR侧提供的base64格式…

代码随想录训练营第二十三天| 572.另一颗树的子树 104.二叉树的最大深度 559.N叉树的最大深度 111.二叉树的最小深度

572.另一颗树的子树&#xff1a; 状态&#xff1a;已做出 思路&#xff1a; 这道题目当时第一时间不是想到利用100.相同的树思路来解决&#xff0c;而是先想到了使用kmp&#xff0c;不过这个题目官方题解确实是有kmp解法的&#xff0c;我使用的暴力解法&#xff0c;kmp的大致思…

【RabbitMq C++】消息队列组件

RabbitMq 消息队列组件 1. RabbitMq介绍2. 安装RabbitMQ3. 安装 RabbitMQ 的 C客户端库4. AMQP-CPP 库的简单使用4.1 使用4.1.1 TCP 模式4.1.2 扩展模式 4.2 常用类与接口介绍4.2.1 Channel4.3.2 ev 5. RabbitMQ样例编写5.1 发布消息5.2 订阅消息 1. RabbitMq介绍 RabbitMq - …

鸿蒙NEXT开发动画案例8

1.创建空白项目 2.Page文件夹下面新建Spin.ets文件&#xff0c;代码如下&#xff1a; /*** SpinKit动画组件 (重构版)* author: CSDN-鸿蒙布道师* since: 2025/05/14*/interface AnimationGroup {indexes: number[];delay: number; }ComponentV2 export struct SpinEight {Re…

MySQL全局优化

目录 1 硬件层面优化 1.1 CPU优化 1.2 内存优化 1.3 存储优化 1.4 网络优化 2 系统配置优化 2.1 操作系统配置 2.2 MySQL服务配置 3 库表结构优化 4 SQL及索引优化 mysql可以从四个层面考虑优化&#xff0c;分别是 硬件系统配置库表结构SQL及索引 从成本和优化效果来看&#xf…

vue和springboot交互数据,使用axios【跨域问题】

vue和springboot交互数据&#xff0c;使用axios【跨域问题】 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是node.js和vue的使用。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性。【帮帮志系列文章】&…

FFMPEG 与 mp4

1. FFmpeg 中的 start_time 与 time_base start_time 流的起始时间戳&#xff08;单位&#xff1a;time_base&#xff09;&#xff0c;表示第一帧的呈现时间&#xff08;Presentation Time&#xff09;。通常用于同步多个流&#xff08;如音频和视频&#xff09;。 time_base …

AI世界的崩塌:当人类思考枯竭引发数据生态链断裂

AI世界的崩塌&#xff1a;当人类思考枯竭引发数据生态链断裂 ——论过度依赖AI创作对技术进化的反噬 一、数据生态的恶性循环&#xff1a;AI的“自噬危机” 当前AI模型的训练依赖于人类创造的原始数据——书籍、论文、艺术作品、社交媒体动态等。据统计&#xff0c;2025年全球…

C++【STL】(2)string

C【STL】string用法扩展 1. assign&#xff1a;为字符串赋新值 用于替换字符串内容&#xff0c;支持多种参数形式。 常用形式&#xff1a; // 用另一个字符串赋值 str.assign("Hello World");// 用另一个字符串的子串&#xff08;从第6个字符开始&#xff0c;取5…

树莓派4基于Debian GNU/Linux 12 (Bookworm)开启VNC,使用MobaXterm连接VNC出现黑屏/灰屏问题

1. 开启树莓派的VNC服务 启用VNC服务&#xff1a;通过raspi-config开启 # 1. 通过 raspi-config 工具开启 sudo raspi-config选择 Interface Options → VNC → Yes退出时会自动启动服务 检查服务状态&#xff1a; sudo systemctl status vncserver-x11-serviced正常输出应显示…

MongoDB使用x.509证书认证

文章目录 自定义证书生成CA证书生成服务器之间的证书生成集群证书生成用户证书 MongoDB配置java使用x.509证书连接MongoDBMongoShell使用证书连接 8.0版本的mongodb开启复制集&#xff0c;配置证书认证 自定义证书 生成CA证书 生成ca私钥&#xff1a; openssl genrsa -out ca…

Python爬虫实战:研究js混淆加密

一、引言 在当今数字化时代,数据已成为推动各行业发展的核心驱动力。网络爬虫作为一种高效的数据采集工具,能够从互联网上自动获取大量有价值的信息。然而,随着互联网技术的不断发展,许多网站为了保护自身数据安全和知识产权,采用了 JavaScript 混淆加密技术来防止数据被…

Java项目层级介绍 java 层级 层次

java 层级 层次 实体层 控制器层 数据连接层 Service : 业务处理类 Repository &#xff1a;数据库访问类 Java项目层级介绍 https://blog.csdn.net/m0_67574906/article/details/145811846 在Java项目中&#xff0c;层级结构&#xff08;Layered Architecture&#xf…

网络安全顶会——SP 2025 论文清单与摘要

1、"Check-Before-you-Solve": Verifiable Time-lock Puzzles 时间锁谜题是一种密码学原语&#xff0c;它向生成者保证该谜题无法在少于T个顺序计算步骤内被破解。近年来&#xff0c;该技术已在公平合约签署和密封投标拍卖等场景中得到广泛应用。然而&#xff0c;求解…

《100天精通Python——基础篇 2025 第18天:正则表达式入门实战,解锁字符串处理的魔法力量》

目录 一、认识正则表达式二、正则表达式基本语法2.1 行界定符2.2 单词定界符2.3 字符类2.4 选择符2.5 范围符2.6 排除符2.7 限定符2.8 任意字符2.9 转义字符2.10 反斜杠2.11 小括号2.11.1 定义独立单元2.11.2 分组 2.12 反向引用2.13 特殊构造2.14 匹配模式 三、re模块3.1 comp…

思迈特软件携手天阳科技,打造ChatBI金融智能分析新标杆

5月10日&#xff0c;广州思迈特软件有限公司&#xff08;以下简称“思迈特软件”&#xff09;与天阳宏业科技股份有限公司&#xff08;以下简称“天阳科技”&#xff09;在北京正式签署战略合作协议。思迈特软件董事长吴华夫、CEO姚诗成&#xff0c;天阳科技董事长兼总裁欧阳建…