筆者在使用Python進行平常開發時,最經常使用的數據結構就是list和dict,其中dict在Python底層是基於hash_table來實現的,咱們今天就介紹一下對標dict的STL關聯容器Map。html
在使用map時總讓我有種在使用NoSQL的感受,由於說到底都是key-value,感受STL中的map能夠對標LevelDB之類的普通NoSQL,倒更加的貼切。ios
結合C++11來講,目前STL支持的map類型包括:map、multimap、unordered_map。雖然都是map,可是在使用和底層實現上會有一些差別,所以深刻理解一下才能更好將map用於平常開發工做中。面試
map的全部元素都是pair,同時具有key和value,其中pair的第一個元素做爲key,第二個元素做爲value。map不容許相同key出現,而且全部pair根據key來自動排序,其中pair的定義在以下:數組
template <typename T1, typename T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair() : first(T1()), second(T2()) { }
pair(const T1& a, const T2& b) : first(a), second(b) { }
};複製代碼
從定義中看到pair使用模板化的struct來實現的,成員變量默認都是public類型。bash
map的key不能被修改,可是value能夠被修改,STL中的map是基於紅黑樹來實現的,所以能夠認爲map是基於紅黑樹封裝了一層map的接口,底層的操做都是藉助於RB-Tree的特性來實現的,再來進一步看下map的定義,以下(舒適提示:代碼主要體現map與RB-Tree在實現上的交互,大體看懂便可):微信
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:
typedef Key key_type; //key類型
typedef T data_type; //value類型
typedef T mapped_type;
typedef pair<const Key, T> value_type; //元素類型, const要保證key不被修改
typedef Compare key_compare; //用於key比較的函數
private:
//內部採用RBTree做爲底層容器
typedef rb_tree<key_type, value_type,
identity<value_type>, key_compare, Alloc> rep_type;
rep_type t; //t爲內部RBTree容器
public:
//iterator_traits相關
typedef typename rep_type::const_pointer pointer;
typedef typename rep_type::const_pointer const_pointer;
typedef typename rep_type::const_reference reference;
typedef typename rep_type::const_reference const_reference;
typedef typename rep_type::difference_type difference_type;
//迭代器相關
typedef typename rep_type::iterator iterator;
typedef typename rep_type::const_iterator const_iterator;
typedef typename rep_type::const_reverse_iterator reverse_iterator;
typedef typename rep_type::const_reverse_iterator const_reverse_iterator;
typedef typename rep_type::size_type size_type;
//迭代器函數
iterator begin() { return t.begin(); }
const_iterator begin() const { return t.begin(); }
iterator end() { return t.end(); }
const_iterator end() const { return t.end(); }
reverse_iterator rbegin() { return t.rbegin(); }
const_reverse_iterator rbegin() const { return t.rbegin(); }
reverse_iterator rend() { return t.rend(); }
const_reverse_iterator rend() const { return t.rend(); }
//容量函數
bool empty() const { return t.empty(); }
size_type size() const { return t.size(); }
size_type max_size() const { return t.max_size(); }
//key和value比較函數
key_compare key_comp() const { return t.key_comp(); }
value_compare value_comp() const { return value_compare(t.key_comp()); }
//運算符
T& operator[](const key_type& k)
{
return (*((insert(value_type(k, T()))).first)).second;
}
friend bool operator== __STL_NULL_TMPL_ARGS (const map&, const map&);
friend bool operator< __STL_NULL_TMPL_ARGS (const map&, const map&);
}複製代碼
map的成員函數(部分經常使用)能夠分爲幾類:數據結構
map的insert操做是很是常見的,而且插入操做不存在迭代器失效問題,其中根據insert的重載類型,能夠支持多種插入方式,在C++11中增長了第四種重載函數,本文暫列舉C++98的3種重載定義,先看下insert的幾種定義:app
//single element (1)
pair<iterator,bool> insert (const value_type& val);
//with hint (2)
iterator insert (iterator position, const value_type& val);
//range (3)
template <class InputIterator>
void insert (InputIterator first, InputIterator last);複製代碼
上述代碼給出了3種插入方式分別是單元素pair、指定位置、範圍區間,看下實際的例子:
less
//map::insert (C++98)
#include <iostream>
#include <map>
int main ()
{
std::map<char,int> mymap;
//first insert function version (single parameter):
//注意返回值 是兩個 迭代器和是否成功
mymap.insert ( std::pair<char,int>('a',100) );
mymap.insert ( std::pair<char,int>('z',200) );
std::pair<std::map<char,int>::iterator,bool> ret;
ret = mymap.insert ( std::pair<char,int>('z',500) );
if (ret.second==false) {
std::cout << "element 'z' already existed";
std::cout << " with a value of " << ret.first->second << '\n';
}
// second insert function version (with hint position):
//因爲map的key的有序性 插入位置對效率有必定的影響
std::map<char,int>::iterator it = mymap.begin();
mymap.insert (it, std::pair<char,int>('b',300)); // max efficiency inserting
mymap.insert (it, std::pair<char,int>('c',400)); // no max efficiency inserting
// third insert function version (range insertion):
//第三種重載 範圍區間
std::map<char,int> anothermap;
anothermap.insert(mymap.begin(),mymap.find('c'));
// showing contents:
//迭代器遍歷
std::cout << "mymap contains:\n";
for (it=mymap.begin(); it!=mymap.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
std::cout << "anothermap contains:\n";
for (it=anothermap.begin(); it!=anothermap.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
return 0;
}複製代碼
清除操做也是很是重要的操做,而且存在迭代器失效問題,刪除操做一樣在C++98中有3個重載函數,定義以下:ide
void erase (iterator position);
size_type erase (const key_type& k);
void erase (iterator first, iterator last);複製代碼
能夠看到這三個函數分別支持:迭代器位置刪除、指定key刪除、迭代器範圍刪除,看下實際的例子:
// erasing from map
#include <iostream>
#include <map>
int main ()
{
std::map<char,int> mymap;
std::map<char,int>::iterator it;
// insert some values:
mymap['a']=10;
mymap['b']=20;
mymap['c']=30;
mymap['d']=40;
mymap['e']=50;
mymap['f']=60;
it=mymap.find('b');
mymap.erase (it); // erasing by iterator
mymap.erase ('c'); // erasing by key
it=mymap.find ('e');
mymap.erase ( it, mymap.end() ); // erasing by range
// show content:
for (it=mymap.begin(); it!=mymap.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
return 0;
}複製代碼
交換操做能夠實現兩個相同類型的map的交換,即便map元素容量不一樣,這個操做看着很神奇而且效率很高,能夠想下是如何實現的,舉個使用栗子:
// swap maps
#include <iostream>
#include <map>
int main () {
std::map<char,int> foo,bar;
foo['x']=100;
foo['y']=200;
bar['a']=11;
bar['b']=22;
bar['c']=33;
foo.swap(bar);
std::cout << "foo contains:\n";
for (std::map<char,int>::iterator it=foo.begin(); it!=foo.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
std::cout << "bar contains:\n";
for (std::map<char,int>::iterator it=bar.begin(); it!=bar.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
return 0;
}複製代碼
代碼輸出:
foo contains:
a => 11
b => 22
c => 33
bar contains:
x => 100
y => 200複製代碼
前面說了一些定義,如今介紹今天的重點內容Map與紅黑樹。
從定義能夠看到map的定義中本質上是在內部實現了一棵紅黑樹,由於紅黑樹的增刪改查的全部操做均可以在有時間保證的前提下完成,然而這些操做也正是map所須要的。
換句話說map應該提供的接口功能,紅黑樹也都有,從而map的全部操做都是內部轉向調用RB-Tree來實現的。
提到紅黑樹感受很難很複雜而且離咱們平常開發很遠,其實否則,紅黑樹不只是做爲AVL的工程版本,在增長節點顏色、不嚴格平滑等特性實現了更高效的插入和刪除。
更重要的是RB-Tree做爲一種基礎的數據結構,常常被用於構建其餘對外的結構,咱們今天說的map以及STL的set底層都是基於紅黑樹的,因此要把RB-Tree當作是一種基礎構造類型的數據結構。
SGI STL並無直接只用RB-Tree,而是對其進行了模板化處理以及增長一些有益節點,從而來更加高效的爲STL中的容器服務,因此STL中使用的能夠認爲是變種的紅黑樹。
好比SGI STL針對RB-Tree採用了header技巧,header指向根節點的指針,與根節點互爲對方的父節點,而且left和right指向左子樹最小和右子樹最大,如圖所示:
這裏引入一個常見的問題:
面試官:stl的map是基於紅黑樹實現的,那麼爲何要基於紅黑樹?
這個問題也算是高頻問題,不少人上來就回答紅黑樹的各類好處,那確實也沒錯,可是這樣也最多算答上來一半。
其實也是如此,沒有對比就沒有發言權,總說A好,不和BCD對比一下怎麼知道?
map的場景本質上就是動態查找過程,所謂動態就是其中包含了插入和刪除,而且數據量會比較大並且元素的結構也比較隨意,而且都是基於內存來實現的,所以咱們就須要考慮效率和成本,既要節約內存又要提升調整效率和查找速度。
說到查找問題 必然有幾種備選的數據結構不乏:
固然還有一些其餘我可能不知道的,可是熱門的基本都在這裏了,那麼一一來看進行優劣勢分析:
在網上也並無這種爲何不用跳錶的對比,筆者試着想了一下:
對於這個對比必定要結合map的發明年代背景來講,就好像咱們如今問10年前的人們爲何不用微信、拼多多同樣,由於那時候壓根沒有或者剛出來暫時沒有推廣等諸多緣由。
SGI STL是大約1994年左右開發的,然而跳躍鏈表是1990年左右由William Pugh發明的,也就是能夠認爲兩者是同時期的產物,然而Redis是2005年以後的產物,再看看紅黑樹是1978年左右發明出現的,因此對發明STL的那幫大牛,最開始考慮跳躍鏈表的可能性很小。
在國外的網站上有求知者試着用跳錶實現map可是性能和紅黑樹有必定的差距,另外跳錶自己是機率平衡的,節點大小相比於紅黑樹更大,可是我以爲這並非致命的問題,或許過些年STL就會出現基於跳錶的實現版本,這個很難說,因此不能一棒子打死說必須是紅黑樹。
從上面我所知道的數據結構來看,選擇紅黑樹有不少歷史緣由和性能考慮、時代在進步,並不必定後續就不會出現基於Treap樹堆和B樹的map版本,或許如今就有公司或者大佬本身使用跳錶、Treap、B樹來實現map。
因此咱們不能教條地記憶stl使用紅黑樹的緣由,至於真正的緣由只有創造者知道,不要把推測當作結論,沒有最好只有更好。