1. 程式人生 > 其它 >硬核來襲 | 2 萬字 + 10 圖帶你手撕 STL 關聯式容器原始碼

硬核來襲 | 2 萬字 + 10 圖帶你手撕 STL 關聯式容器原始碼

硬核來襲 | 2 萬字 + 10 圖帶你手撕 STL 關聯式容器原始碼

本篇已同步收錄 GitHub 倉庫,這裡有小賀的原始碼閱讀筆記:https://github.com/rongweihe/CPPNotes/tree/master/STL-source-code-notes

大家好,我是小賀。

鴿了好久的STL 原始碼系列,這周開始更新,還剩最後兩篇,分別是關聯式容器和 STL 基本演算法。

距離上篇原始碼剖析的文章好像在幾個月前?

咕咕咕,連我自己都看不下去了,怎麼能這麼懶呢?正好趁著這幾天休假,一鼓作氣的把該寫的文章補上吧。

前言

STL 原始碼剖析系列已經出了三篇:

5 千字長文+ 30 張圖解 | 陪你手撕 STL 空間配置器原始碼萬字長文炸裂!手撕 STL 迭代器原始碼與 traits 程式設計技法超硬核 | 2 萬字+20 圖帶你手撕 STL 序列式容器原始碼

上一篇,我們剖析了序列式容器,這一篇我們來學習下關聯式容器。

在 STL 程式設計中,容器是我們經常會用到的一種資料結構,容器分為序列式容器和關聯式容器。

兩者的本質區別在於:序列式容器是通過元素在容器中的位置順序儲存和訪問元素,而關聯容器則是通過鍵 (key) 儲存和讀取元素。

本篇著重剖析關聯式容器相關背後的知識點,來一張思維導圖。

容器分類

前面提到了,根據元素儲存方式的不同,容器可分為序列式和關聯式,那具體的又有哪些分類呢,這裡我畫了一張圖來看一下。

關聯式容器比序列式容器更好理解,從底層實現來分的話,可以分為 RB_tree 還是 hash_table,所有暴露給使用者使用的關聯式容器都繞不過底層這兩種實現。

不多 BB。我們先來分析其底層的兩種實現,後面在逐個一一剖析其外在形式,這樣對於新手還是老手,對於其背後核心的設計和奧祕,理解起來都會絲滑順暢。

RB-Tree介紹與應用

樹的適用場景還是為了增加程式碼的複用性,以及擴充套件性。

首先來介紹紅黑樹,RB Tree 全稱是 Red-Black Tree,又稱為“紅黑樹”,它一種特殊的二叉查詢樹。紅黑樹的每個節點上都有儲存位表示節點的顏色,可以是紅 (Red) 或黑 (Black)。

紅黑樹的特性:

  • 每個節點或者是黑色,或者是紅色。
  • 根節點是黑色。
  • 每個葉子節點(NIL)是黑色。[注意:這裡葉子節點,是指為空(NIL或NULL)的葉子節點!]
  • 如果一個節點是紅色的,則它的子節點必須是黑色的。
  • 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

注意:

  • 特性 (3)中的葉子節點,是隻為空(NIL或null)的節點。
  • 特性 (5)確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近衡的二叉樹。

紅黑樹示意圖如下:

紅黑樹保證了最壞情形下在 O(logn) 時間複雜度內完成查詢、插入及刪除操作;效率非常之高。

因此紅黑樹可用於很多場景,比如下圖。

好了,紅黑樹介紹到這裡差不多了,關於紅黑樹的分析在深入又是另一篇文章了,下面我們在簡單介紹一下紅黑樹的兩種資料操作方式。

RB-Tree的基本操作

紅黑樹的基本操作包括新增、刪除。

在對紅黑樹進行新增或刪除之後,都會用到旋轉方法。原因在於新增或刪除紅黑樹中的節點之後,紅黑樹就發生了變化,可能不滿足紅黑樹的 5 條性質,也就說不再是一顆紅黑樹了,而是一顆普通的樹。

而通過旋轉,可以使這顆樹重新成為紅黑樹。簡單點說,旋轉的目的是讓樹保持紅黑樹的特性。

在紅黑樹裡的旋轉包括兩種:左旋和右旋。

左旋:對節點 X 進行左旋,也就說讓節點 X 成為左節點。

右旋:對節點 X 進行右旋,也就說讓節點 X 成為右節點。

說完了旋轉,我們再來看一下它的插入,有兩種插入方式:

//不允許鍵值重複插入
pair<iterator,bool>insert_unique(constvalue_type&x);

//允許鍵值重複插入
iteratorinsert_equal(constvalue_type&x);

RB-tree 裡面分兩種插入方式,一種是允許鍵值重複插入,一種不允許。可以簡單的理解,如果呼叫 insert_unique 插入重複的元素,在 RB-tree 裡面其實是無效的。

其實在 RB-tree 原始碼裡面,上面兩個函式走到最底層,呼叫的是同一個 __insert() 函式。

知道了資料的操作方式,我們再來看 RB-tree 的構造方式:內部呼叫 rb_tree_node_allocator ,每次恰恰配置一個節點,會呼叫 simple_alloc 空間配置器來配置節點。

並且分別呼叫四個節點函式來進行初始化和構造化。

get_node(), put_node(), create_node(), clone_node(), destroy_node();

RB-tree 的構造方式也有兩種:一種是以現有的 RB-tree 複製一個新的 RB-tree,另一種是產生一棵空的樹。

雜湊表(hashtable)介紹與應用

紅黑樹的介紹就到這裡了,下面我們來看一下雜湊表。

我們知道陣列的特點是:定址容易,插入和刪除困難;而連結串列的特點是:定址困難,插入和刪除容易。

那麼我們能不能綜合兩者的特性,做出一種定址容易,插入刪除也容易的資料結構?

答案是肯定的,這就是雜湊表。

雜湊表,也被稱為散列表,是一種常用的資料結構,這種結構在插入、刪除、查詢等操作上也具有”常數平均時間“的表現。

也可以視為一種字典結構。

在講具體的 hashtable 原始碼之前,我們先來認識兩個概念:

  • 雜湊函式:使用某種對映函式,將大數對映為小數。負責將某一個元素對映為一個”大小可接受內的索引“,這樣的函式稱為 hash function(雜湊函式)。
  • 使用雜湊函式可能會帶來問題:可能會有不同的元素被對映到相同的位置,這無法避免,因為元素個數有可能大於分配的 array 容量,這就是所謂的碰撞問題,解決碰撞問題一般有:線性探測、二次探測、開鏈等。

不同的方法有不同的效率差別,本文以 SGI STL 原始碼裡採用的開鏈法來進行 hashtable 的學習。

拉鍊法,可以理解為“連結串列的陣列”,其思路是:如果多個關鍵字對映到了雜湊表的同一個位置處,則將這些關鍵字記錄在同一個線性連結串列中,如果有重複的,就順序拉在這條連結串列的後面。


以開鏈法完成的 hash table

注意,bucket 維護的連結串列,並不採用 STL 的 list ,而是自己維護的 hash table node,至於 buckets 表格,則是以 vector 構造完成,以便具有動態擴充能力。

hash table 的定義:

//模板引數定義
/*
Value:節點的實值型別
Key:節點的鍵值型別
HashFcn:hash function的型別
ExtractKey:從節點中取出鍵值的方法(函式或仿函式)
EqualKey:判斷鍵值是否相同的方法(函式或仿函式)
Alloc:空間配置器
*/
//hashtable的線性表是用vector容器維護
template<class_Val,class_Key,class_HashFcn,
class_ExtractKey,class_EqualKey,class_Alloc>
classhashtable{
public:
typedef_Keykey_type;
typedef_Valvalue_type;
typedef_HashFcnhasher;
typedef_EqualKeykey_equal;

typedefsize_tsize_type;
typedefptrdiff_tdifference_type;
typedefvalue_type*pointer;
typedefconstvalue_type*const_pointer;
typedefvalue_type&reference;
typedefconstvalue_type&const_reference;

hasherhash_funct()const{return_M_hash;}
key_equalkey_eq()const{return_M_equals;}

private:
typedef_Hashtable_node<_Val>_Node;

這裡需要注意的是,hashtable 的迭代器是正向迭代器,且必須維持這整個 buckets vector 的關係,並記錄目前所指的節點。其前進操作是目前所指的節點,前進一個位置。

//以下是hashtable的成員變數
private:
hasher_M_hash;
key_equal_M_equals;
_ExtractKey_M_get_key;
vector<_Node*,_Alloc>_M_buckets;//用vector維護buckets
size_type_M_num_elements;//hashtable中list節點個數

public:
typedef_Hashtable_iterator<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,_Alloc>
iterator;
typedef_Hashtable_const_iterator<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,
_Alloc>
const_iterator;

public:
//建構函式
hashtable(size_type__n,
const_HashFcn&__hf,
const_EqualKey&__eql,
const_ExtractKey&__ext,
constallocator_type&__a=allocator_type())
:__HASH_ALLOC_INIT(__a)
_M_hash(__hf),
_M_equals(__eql),
_M_get_key(__ext),
_M_buckets(__a),
_M_num_elements(0)
{
_M_initialize_buckets(__n);//預留空間,並將其初始化為空0
//預留空間大小為大於n的最小素數
}

提供兩種插入元素的方法:insert_equal允許重複插入;insert_unique不允許重複插入。

//插入元素節點,不允許存在重複元素
pair<iterator,bool>insert_unique(constvalue_type&__obj){
//判斷容量是否夠用,否則就重新配置
resize(_M_num_elements+1);
//插入元素,不允許存在重複元素
returninsert_unique_noresize(__obj);
}
//插入元素節點,允許存在重複元素
iteratorinsert_equal(constvalue_type&__obj)
{//判斷容量是否夠用,否則就重新配置
resize(_M_num_elements+1);
//插入元素,允許存在重複元素
returninsert_equal_noresize(__obj);
}

hashtable 的基本操作

後面馬上要介紹的關聯容器 set、multiset、map 和 multimap 的底層機制都是基於 RB-Tree 紅黑樹,雖然能夠實現在插入、刪除和搜素操作能夠達到對數平均時間,可是要求輸入資料有足夠的隨機性。

而 hash table 不需要要求輸入資料具有隨機性,在插入、刪除和搜素操作都能達到常數平均時間。

SGI 中實現 hash table 的方式,是在每個 buckets 表格元素中維護一個連結串列, 然後在連結串列上執行元素的插入、搜尋、刪除等操作,該表格中的每個元素被稱為桶 (bucket)。

雖然開鏈法並不要求表格大小為質數,但 SGI STL 仍然已質數來設計表格大小,並且將 28 個質數計算好,以備隨時訪問。

//Note:assumeslongisatleast32bits.
//注意:假設long至少為32-bits, 可以根據自己需要修改
//定義28個素數用作hashtable的大小
enum{__stl_num_primes=28};

staticconstunsignedlong__stl_prime_list[__stl_num_primes]={
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
};

//返回大於n的最小素數
inlineunsignedlong__stl_next_prime(unsignedlong__n){
constunsignedlong*__first=__stl_prime_list;
constunsignedlong*__last=__stl_prime_list+(int)__stl_num_primes;
constunsignedlong*pos=lower_bound(__first,__last,__n);

hashtable的節點配置和釋放分別由 new_node 和 delete_node 來完成,並且插入操作和表格重整分別由 insert_unique 和 insert_equal ,resize 三個函式來完成。限於篇幅,這裡用一張圖來展示:

C++ STL 標準庫中,不僅是 unordered_xxx 容器,所有無序容器的底層實現都採用的是雜湊表儲存結構。更準確地說,是用“鏈地址法”(又稱“開鏈法”)解決資料儲存位置發生衝突的雜湊表,整個儲存結構如圖所示。

其中,Pi 表示儲存的各個鍵值對。

最左邊的綠色稱之為 bucket 桶,可以看到,當使用無序容器儲存鍵值對時,會先申請一整塊連續的儲存空間,但此空間並不用來直接儲存鍵值對,而是儲存各個連結串列的頭指標,各鍵值對真正的儲存位置是各個連結串列的節點。

在 C++ STL 標準庫中,將圖 1 中的各個連結串列稱為桶(bucket),每個桶都有自己的編號(從 0 開始)。當有新鍵值對儲存到無序容器中時,整個儲存過程分為如下幾步:

  • 將該鍵值對中鍵的值帶入設計好的雜湊函式,會得到一個雜湊值(一個整數,用 H 表示);
  • 將 H 和無序容器擁有桶的數量 n 做整除運算(即 H % n),該結果即表示應將此鍵值對儲存到的桶的編號;
  • 建立一個新節點儲存此鍵值對,同時將該節點連結到相應編號的桶上。

另外值得一提的是,雜湊表儲存結構還有一個重要的屬性,稱為負載因子(load factor)。

該屬性同樣適用於無序容器,用於衡量容器儲存鍵值對的空/滿程式,即負載因子越大,意味著容器越滿,即各連結串列中掛載著越多的鍵值對,

這無疑會降低容器查詢目標鍵值對的效率;反之,負載因子越小,容器肯定越空,但並不一定各個連結串列中掛載的鍵值對就越少。

舉個例子,如果設計的雜湊函式不合理,使得各個鍵值對的鍵帶入該函式得到的雜湊值始終相同(所有鍵值對始終儲存在同一連結串列上)。這種情況下,即便增加桶數是的負載因子減小,該容器的查詢效率依舊很差。

無序容器中,負載因子的計算方法為:

負載因子 = 容器儲存的總鍵值對 / 桶數

預設情況下,無序容器的最大負載因子為 1.0。如果操作無序容器過程中,使得最大複雜因子超過了預設值,則容器會自動增加桶數,並重新進行雜湊,以此來減小負載因子的值。

需要注意的是,此過程會導致容器迭代器失效,但指向單個鍵值對的引用或者指標仍然有效。

這也就解釋了,為什麼我們在操作無序容器過程中,鍵值對的儲存順序有時會“莫名”的發生變動。

C++ STL 標準庫為了方便使用者更好地管控無序容器底層使用的雜湊表儲存結構,各個無序容器的模板類中都提供表 所示的成員方法。

成員方法功能
bucket_count 返回當前容器底層儲存鍵值對時,使用桶的數量
max_bucket_count 返回當前系統中,unordered_xxx 容器底層最多可以使用多少個桶
bucket_size 返回第 n 個桶中儲存鍵值對的數量
bucket(key) 返回以 key 為鍵的鍵值對所在桶的編號
load_factor 返回 unordered_map 容器中當前的負載因子
max_load_factor 返回或者設定當前 unordered_map 容器的最大負載因子
rehash(n) 嘗試重新調整桶的數量為等於或大於 n 的值。如果 n 大於當前容器使用的桶數,則該方法會是容器重新雜湊,該容器新的桶數將等於或大於 n。反之,如果 n 的值小於當前容器使用的桶數,則呼叫此方法可能沒有任何作用。
reserve(n) 將容器使用的桶數(bucket_count() 方法的返回值)設定為最適合儲存 n 個元素的桶
hash_function 返回當前容器使用的雜湊函式物件

介紹到這裡,hashtable 的原始碼的大觀也差不多了,想深入研究原始碼等細節大家可以訪問開頭的 GitHub 連結。

下面開始講解具體的關聯式容器,這裡的分類比較多,有的讀者可能會有點分不清。

那麼小賀也給大家總結了一句話:只要是字首帶了unordered的就是無序,字尾帶了multi的就是允許鍵重複,插入採用 insert_equal 而不是 insert_unique。

set、multiset、unordered_set、unordered_multiset

有了前面的 RB_tree 做鋪墊,下面來學習 set/multiset 和 map/multimap 就容易多了。

先來看一下 set 的性質

  • set 以 RB-tree 作為其底層機制,所有元素都會根據元素的鍵值自動被排序。
  • set 的元素就是鍵值,set 不允許兩個元素有相同的鍵值。
  • 不允許通過 set 的迭代器來改變 set 的元素值,因為 set 的元素值就是鍵值,更改了元素值就會影響其排列規則,如果任意更改元素值,會嚴重破壞 set 組織,因此在定義 set 的迭代器時被定義成了 RB-tree 的 const_iterator。
  • 由於 set 不允許有兩個相同的鍵值,所以插入時採用的是 RB-tree 的 insert_unique 方式
  • 這裡的型別的定義要注意一點, 都是 const 型別, 因為 set 的主鍵定義後就不能被修改了, 所以這裡都是以const型別。

下面來看一下 set 的原始碼

set 的主要實現大都是呼叫 RB-tree 的介面,這裡的型別的定義要注意一點, 都是 const 型別, 因為 set 的主鍵定義後就不能被修改了,所以這裡都是以 const 型別。

#ifndef__STL_LIMITED_DEFAULT_TEMPLATES
template<classKey,classCompare=less<Key>,classAlloc=alloc>
#else
template<classKey,classCompare,classAlloc=alloc>
#endif
classset{
public:
//typedefs:
typedefKeykey_type;
typedefKeyvalue_type;
typedefComparekey_compare;
typedefComparevalue_compare;
private:
//一RB-tree為介面封裝
typedefrb_tree<key_type,value_type,identity<value_type>,key_compare,Alloc>rep_type;
rep_typet;//red-blacktreerepresentingset
public:
//定義的型別都是const型別,不能修改
typedeftypenamerep_type::const_pointerpointer;
typedeftypenamerep_type::const_pointerconst_pointer;
typedeftypenamerep_type::const_referencereference;
typedeftypenamerep_type::const_referenceconst_reference;
typedeftypenamerep_type::const_iteratoriterator;
typedeftypenamerep_type::const_iteratorconst_iterator;
typedeftypenamerep_type::const_reverse_iteratorreverse_iterator;
typedeftypenamerep_type::const_reverse_iteratorconst_reverse_iterator;
typedeftypenamerep_type::size_typesize_type;
typedeftypenamerep_type::difference_typedifference_type;
...
};

建構函式構造成員的時候呼叫的是 RB-tree 的 insert_unique。

classset{
public:
...
set():t(Compare()){}
explicitset(constCompare&comp):t(comp){}//不能隱式轉換

//接受兩個迭代器
//建構函式構造成員的時候呼叫的是RB-tree的insert_unique
template<classInputIterator>
set(InputIteratorfirst,InputIteratorlast)
:t(Compare()){t.insert_unique(first,last);}
template<classInputIterator>
set(InputIteratorfirst,InputIteratorlast,constCompare&comp)
:t(comp){t.insert_unique(first,last);}

set(constvalue_type*first,constvalue_type*last)
:t(Compare()){t.insert_unique(first,last);}
set(constvalue_type*first,constvalue_type*last,constCompare&comp)
:t(comp){t.insert_unique(first,last);}

set(const_iteratorfirst,const_iteratorlast)
:t(Compare()){t.insert_unique(first,last);}
set(const_iteratorfirst,const_iteratorlast,constCompare&comp)
:t(comp){t.insert_unique(first,last);}
...
};

成員屬性獲取

classset{
public:
...
//所有的操作都是通過呼叫RB-tree獲取的
key_comparekey_comp()const{returnt.key_comp();}
value_comparevalue_comp()const{returnt.key_comp();}
iteratorbegin()const{returnt.begin();}
iteratorend()const{returnt.end();}
reverse_iteratorrbegin()const{returnt.rbegin();}
reverse_iteratorrend()const{returnt.rend();}
boolempty()const{returnt.empty();}
size_typesize()const{returnt.size();}
size_typemax_size()const{returnt.max_size();}
//交換
voidswap(set<Key,Compare,Alloc>&x){t.swap(x.t);}
//其他的find,count等都是直接呼叫的RB-tree的介面
iteratorfind(constkey_type&x)const{returnt.find(x);}
size_typecount(constkey_type&x)const{returnt.count(x);}
iteratorlower_bound(constkey_type&x)const{
returnt.lower_bound(x);
}
iteratorupper_bound(constkey_type&x)const{
returnt.upper_bound(x);
}
pair<iterator,iterator>equal_range(constkey_type&x)const{
returnt.equal_range(x);
}
...
};

insert 操作原始碼

classset{
public:
...
//pair型別我們準備下一節分析,這裡是直接呼叫insert_unique,返回插入成功就是pair(,true),插入失敗則是(,false)
typedefpair<iterator,bool>pair_iterator_bool;
pair<iterator,bool>insert(constvalue_type&x){
pair<typenamerep_type::iterator,bool>p=t.insert_unique(x);
returnpair<iterator,bool>(p.first,p.second);
}
//指定位置的插入
iteratorinsert(iteratorposition,constvalue_type&x){
typedeftypenamerep_type::iteratorrep_iterator;
returnt.insert_unique((rep_iterator&)position,x);
}
//可接受範圍插入
template<classInputIterator>
voidinsert(InputIteratorfirst,InputIteratorlast){
t.insert_unique(first,last);
}
...
};

erase 的實現是通過呼叫 RB-tree 實現的 erase。

classset{
public:
...
//erase的實現是通過呼叫RB-tree實現的erase
voiderase(iteratorposition){
typedeftypenamerep_type::iteratorrep_iterator;
t.erase((rep_iterator&)position);
}
size_typeerase(constkey_type&x){
returnt.erase(x);
}
voiderase(iteratorfirst,iteratorlast){
typedeftypenamerep_type::iteratorrep_iterator;
t.erase((rep_iterator&)first,(rep_iterator&)last);
}
voidclear(){t.clear();}
...
};

最後剩下一個過載運算子,也是以 RB-tree 為介面呼叫。

到這裡,set 大部分的原始碼都已經過了一遍。multiset 與 set 特性完全相同,唯一差別在於它允許鍵值重複,因此插入操作採用的是底層機制 RB-tree 的 insert_equal() 而非 insert_unique()。

接下來我們來了解一下兩個新的資料結構:hash_set 與 unordered_set。

它們都屬於基於雜湊表(hash table)構建的資料結構,並且是關鍵字與鍵值相等的關聯容器。

那 hash_set 與 unordered_set 哪個更好呢?實際上 unordered_set 在C++11的時候被引入標準庫了,而 hash_set 並沒有,所以建議還是使用 unordered_set 比較好,這就好比一個是官方認證的,一個是民間流傳的。

在 SGI STL 原始碼剖析裡,是以 hash_set 剖析的。

hash_set 將雜湊表的介面在進行了一次封裝, 實現與 set 類似的功能.

#ifndef__STL_LIMITED_DEFAULT_TEMPLATES
template<classValue,classHashFcn=hash<Value>,
classEqualKey=equal_to<Value>,
classAlloc=alloc>
#else
template<classValue,classHashFcn,classEqualKey,classAlloc=alloc>
#endif
classhash_set{
private:
//定義hashtable
typedefhashtable<Value,Value,HashFcn,identity<Value>,EqualKey,Alloc>ht;
htrep;

public:
typedeftypenameht::key_typekey_type;
typedeftypenameht::value_typevalue_type;
typedeftypenameht::hasherhasher;
typedeftypenameht::key_equalkey_equal;

//定義為const型別,鍵值不允許修改
typedeftypenameht::size_typesize_type;
typedeftypenameht::difference_typedifference_type;
typedeftypenameht::const_pointerpointer;
typedeftypenameht::const_pointerconst_pointer;
typedeftypenameht::const_referencereference;
typedeftypenameht::const_referenceconst_reference;

//定義迭代器
typedeftypenameht::const_iteratoriterator;
typedeftypenameht::const_iteratorconst_iterator;
//仿函式
hasherhash_funct()const{returnrep.hash_funct();}
key_equalkey_eq()const{returnrep.key_eq();}
...
};

建構函式

classhash_set
{
...
public:
hash_set():rep(100,hasher(),key_equal()){}//預設建構函式,表大小預設為100最近的素數
explicithash_set(size_typen):rep(n,hasher(),key_equal()){}
hash_set(size_typen,consthasher&hf):rep(n,hf,key_equal()){}
hash_set(size_typen,consthasher&hf,constkey_equal&eql)
:rep(n,hf,eql){}

#ifdef__STL_MEMBER_TEMPLATES
template<classInputIterator>
hash_set(InputIteratorf,InputIteratorl)
:rep(100,hasher(),key_equal()){rep.insert_unique(f,l);}
template<classInputIterator>
hash_set(InputIteratorf,InputIteratorl,size_typen)
:rep(n,hasher(),key_equal()){rep.insert_unique(f,l);}
template<classInputIterator>
hash_set(InputIteratorf,InputIteratorl,size_typen,
consthasher&hf)
:rep(n,hf,key_equal()){rep.insert_unique(f,l);}
template<classInputIterator>
hash_set(InputIteratorf,InputIteratorl,size_typen,
consthasher&hf,constkey_equal&eql)
:rep(n,hf,eql){rep.insert_unique(f,l);}
...
};

插入刪除等操作

insert呼叫的是insert_unqiue函式

classhash_set
{
...
public:
//都是呼叫hashtable的介面,這裡insert_unqiue函式
pair<iterator,bool>insert(constvalue_type&obj)
{
pair<typenameht::iterator,bool>p=rep.insert_unique(obj);
returnpair<iterator,bool>(p.first,p.second);
}

set、multiset、unordered_set、unordered_multiset總結

map、multimap、unordered_map、unordered_multimap

在分析 map 相關容器之前,我們來分析一下 pair 這種結構。

pair 是一個有兩個變數的結構體, 即誰都可以直接呼叫它的變數, 畢竟 struct 預設許可權都是 public, 將兩個變數用 pair 繫結在一起, 這就為 map<T1, T2> 提供的儲存的基礎.

template<classT1,classT2>//兩個引數型別
structpair{
typedefT1first_type;
typedefT2second_type;

//定義的兩個變數
T1first;
T2second;

//建構函式
pair():first(T1()),second(T2()){}
pair(constT1&a,constT2&b):first(a),second(b){}
#ifdef__STL_MEMBER_TEMPLATES
template<classU1,classU2>
pair(constpair<U1,U2>&p):first(p.first),second(p.second){}
#endif
};

過載實現:

template<classT1,classT2>
inlinebooloperator==(constpair<T1,T2>&x,constpair<T1,T2>&y){
returnx.first==y.first&&x.second==y.second;
}
template<classT1,classT2>
inlinebooloperator<(constpair<T1,T2>&x,constpair<T1,T2>&y){
returnx.first<y.first||(!(y.first<x.first)&&x.second<y.second);
}

整體 pair 的功能與實現都是很簡單的,這都是為 map 的實現做準備的,接下來我們就來分析 map 的實現。

map 基本結構定義

#ifndef__STL_LIMITED_DEFAULT_TEMPLATES
template<classKey,classT,classCompare=less<Key>,classAlloc=alloc>
#else
template<classKey,classT,classCompare,classAlloc=alloc>
#endif
classmap{
public:
typedefKeykey_type;//定義鍵值
typedefTdata_type;//定義資料
typedefTmapped_type;
typedefpair<constKey,T>value_type;//這裡定義了map的資料型別為pair,且鍵值為const型別,不能修改
typedefComparekey_compare;

private:
typedefrb_tree<key_type,value_type,
select1st<value_type>,key_compare,Alloc>rep_type;//定義紅黑樹,map是以rb-tree結構為基礎的
rep_typet;//red-blacktreerepresentingmap
public:
...

建構函式:map 所有插入操作都是呼叫的 RB-tree 的 insert_unique,不允許出現重複的鍵。

classmap{
public:
...
public:
//allocation/deallocation
map():t(Compare()){}//預設建構函式
explicitmap(constCompare&comp):t(comp){}
#ifdef__STL_MEMBER_TEMPLATES
//接受兩個迭代器
template<classInputIterator>
map(InputIteratorfirst,InputIteratorlast)
:t(Compare()){t.insert_unique(first,last);}
template<classInputIterator>
map(InputIteratorfirst,InputIteratorlast,constCompare&comp)
:t(comp){t.insert_unique(first,last);}
...

基本屬性的獲取

classmap{
public:
...
public:
//實際呼叫的是RB-tree的key_comp函式
key_comparekey_comp()const{returnt.key_comp();}
//value_comp實際返回的是一個仿函式value_compare
value_comparevalue_comp()const{returnvalue_compare(t.key_comp());}
//以下的begin,end等操作都是呼叫的是RB-tree的介面
iteratorbegin(){returnt.begin();}
const_iteratorbegin()const{returnt.begin();}
iteratorend(){returnt.end();}
const_iteratorend()const{returnt.end();}
reverse_iteratorrbegin(){returnt.rbegin();}
const_reverse_iteratorrbegin()const{returnt.rbegin();}
reverse_iteratorrend(){returnt.rend();}
const_reverse_iteratorrend()const{returnt.rend();}
boolempty()const{returnt.empty();}
size_typesize()const{returnt.size();}
size_typemax_size()const{returnt.max_size();}
//交換,呼叫RB-tree的swap,實際只交換head和count
voidswap(map<Key,T,Compare,Alloc>&x){t.swap(x.t);}
...
};
template<classKey,classT,classCompare,classAlloc>
inlinevoidswap(map<Key,T,Compare,Alloc>&x,
map<Key,T,Compare,Alloc>&y){
x.swap(y);
}

過載的分析

classmap{
public:
...
public:
T&operator[](constkey_type&k){
return(*((insert(value_type(k,T()))).first)).second;
}
...
};
  • insert(value_type(k, T()) : 查詢是否存在該鍵值, 如果存在則返回該pair, 不存在這重新構造一該鍵值並且值為空
  • *((insert(value_type(k, T()))).first) : pair的第一個元素表示指向該元素的迭代器, 第二個元素指的是(false與true)是否存在, first 便是取出該迭代器而 * 取出pair.
  • (*((insert(value_type(k, T()))).first)).second : 取出pair結構中的second儲存的資料

這裡有坑,初學者容易掉進去,請注意:

過載 operator[],這一步返回是實值 value(即pair.second)的引用,假如原先沒有定義 map 物件,即你訪問的鍵值 key 不存在,則會自動新建一個 map 物件,鍵值 key 為你訪問的鍵值 key,實值 value 為空,看下面的例子就明白了。

我在自己的開發機上測試,int 型別預設 value 為 0,bool 型別預設 value 為 false,string 型別預設是空。

_Tp&operator[](constkey_type&__k){
iterator__i=lower_bound(__k);
//__i->firstisgreaterthanorequivalentto__k.
if(__i==end()||key_comp()(__k,(*__i).first))
__i=insert(__i,value_type(__k,_Tp()));
return(*__i).second;
//其實簡單的方式是直接返回
//return(*((insert(value_type(k,T()))).first)).second;
}

map 的其他 insert, erase, find 都是直接呼叫 RB-tree 的介面函式實現的, 這裡就不直接做分析了。

想深入閱讀原始碼學習的可以點選文章開頭的 GitHub 連結。

map、multimap、unordered_map、unordered_multimap總結

map 和 multimap 的共同點:

  • 兩者底層實現均為紅黑樹,不可以通過迭代器修改元素的鍵,但是可以修改元素的值;
  • 擁有和 list 某些相同的特性,進行元素的新增和刪除後,操做前的迭代器依然可用;

不同點:

  • map 鍵不能重複,支援 [] 運算子;
  • multimap 支援重複的鍵,不支援 [] 運算子;

map 並不像 set 一樣將 iterator 設為 RB-tree 的 const_iterator,因為它允許使用者通過其迭代器修改元素的實值。

map 和 unordered_map 共同點:

  • 兩者均不能有重複的建,均支援[]運算子

不同點:

  • map 底層實現為紅黑樹
  • unordered_map 底層實現為雜湊表

unordered_map 是不允許存在相同的鍵存在,底層呼叫的 insert_unique() 插入元素 unordered_multimap 可以允許存在多個相同的鍵,底層呼叫的 insert_equal() 插入元素。

map 並不像 set 一樣將 iterator 設為 RB-tree 的 const_iterator,因為它允許使用者通過其迭代器修改元素的實值。

思考

為什麼 std::set 不支援[]運算子?

對於 std::map 而言,我們看一個例子:

std::map<std::string,int> m = { {"a",1}, {"b", 2 } };

m["a"] 返回的是1所在單元的引用。

而如果對於 std::setstd::string s = { "a", "b" };而言 s["a"] 應該是個什麼型別呢?

我們用索引取一個容器的元素 a[key] = value 的前提是既有 key 又有 value,set 只有 key 沒有 value,加了[]會導致歧義。