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.pswp.cn/bicheng/80939.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/80939.shtml
英文地址,請注明出處:http://en.pswp.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;天陽科技董事長兼總裁歐陽建…