STL原始碼剖析之map和set
之前分析二叉搜尋樹和平衡二叉樹時,真心感覺樹的實現真是難,特別是平衡二叉樹,不平衡之後需要調整,還要考慮各種情況,累感不愛.今天看到這個紅黑樹,發現比平衡二叉樹還難,但是紅黑樹比平衡二叉樹使用的場景更多,所以平常使用時,我們需要了解紅黑樹的實現原理,如果有能力,可以自己實現,但是如果實在做不出來,也沒關係,因為STL和linux核心都有現成的紅黑樹實現,拿來用即可,前提是瞭解紅黑樹原理.
紅黑樹原理
紅黑樹和平衡二叉樹一樣,本質上都是二叉搜尋樹,但是二者的搜尋效率都能實現平均效率logn,比二叉搜尋樹效能好.平衡二叉樹實現原理是判斷每個節點的左右子樹的高度差是否等於2,如果等於2,則要通過旋轉來實現樹的左右子樹高度差平衡.而紅黑樹實現原理是節點的顏色和旋轉來實現的,實現較複雜,先看下紅黑樹的滿足條件:
- 每個結點要麼是紅的,要麼是黑的。
- 根結點是黑的。
- 每個葉結點,即空結點(NIL)是黑的。
- 如果一個結點是紅的,那麼它的倆個兒子都是黑的。
- 對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。 如果某棵二叉搜尋樹滿足上述條件,則為紅黑樹.為什麼滿足上述條件可以實現平衡了?主要是第4條和第5條,由第5條可以得出,某個到所有葉子節點最長路徑是最短路徑的兩倍.即一條路徑為一黑一紅和一條路徑為全黑,大體上紅黑樹是平衡的,只是沒有AVL樹要求那麼嚴格.
我不推薦直接看STL原始碼剖析這本書中的紅黑樹,而是先到網上部落格先看看大家是怎麼寫,因為這本書STL實現的紅黑樹比較複雜,不好看懂.我推薦部落格
紅黑樹節點和迭代器
紅黑樹節點和迭代器的設計和slist原理一樣,將結構和資料分離.原理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct __rb_tree_node_base { typedef __rb_tree_color_type color_type; typedef __rb_tree_node_base* base_ptr; color_type color; // 紅黑樹的顏色 base_ptr parent; // 父節點 base_ptr left; // 指左節點 base_ptr right; // 指向右節點 } template <class Value> struct __rb_tree_node : public __rb_tree_node_base { typedef __rb_tree_node<Value>* link_type; Value value_field; // 儲存資料 }; |
紅黑樹的基礎迭代器為struct __rb_tree_base_iterator,主要成員就是一個__rb_tree_node_base節點,指向樹中某個節點,作為迭代器與樹的連線關係,還有兩個方法,用於將當前迭代器指向前一個節點decrement()和下一個節點increment().下面看下正式迭代器的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
template <class Value, class Ref, class Ptr> struct __rb_tree_iterator : public __rb_tree_base_iterator { typedef Value value_type; typedef Ref reference; typedef Ptr pointer; typedef __rb_tree_iterator<Value, Value&, Value*> iterator; typedef __rb_tree_iterator<Value, const Value&, const Value*> const_iterator; typedef __rb_tree_iterator<Value, Ref, Ptr> self; typedef __rb_tree_node<Value>* link_type; __rb_tree_iterator() {}//迭代器預設建構函式 __rb_tree_iterator(link_type x) { node = x; }//由一個節點來初始化迭代器 __rb_tree_iterator(const iterator& it) { node = it.node; }//迭代器複製建構函式 //迭代器解引用,即返回這個節點儲存的數值 reference operator*() const { return link_type(node)->value_field; } #ifndef __SGI_STL_NO_ARROW_OPERATOR //返回這個節點數值值域的指標 pointer operator->() const { return &(operator*()); } #endif /* __SGI_STL_NO_ARROW_OPERATOR */ //迭代器++運算 self& operator++() { increment(); return *this; } self operator++(int) { self tmp = *this; increment(); return tmp; } //迭代器--運算 self& operator--() { decrement(); return *this; } self operator--(int) { self tmp = *this; decrement(); return tmp; } }; inline bool operator==(const __rb_tree_base_iterator& x, const __rb_tree_base_iterator& y) { return x.node == y.node; // 兩個迭代器相等,指這兩個迭代器指向的節點相等 } inline bool operator!=(const __rb_tree_base_iterator& x, const __rb_tree_base_iterator& y) { return x.node != y.node; // 兩個節點不相等,指這兩個迭代器指向的節點不等 } |
迭代器的解引用運算,返回的是這個節點的值域.所以對於set來說,返回的就是set儲存的值,對於map來說,返回的就是pair<key,value>鍵值對.
紅黑樹
為了實現紅黑樹的插入和刪除平衡,STL樹實現了幾個旋轉以及平衡函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
inline void __rb_tree_rotate_left(__rb_tree_node_base* x, __rb_tree_node_base*& root) inline void __rb_tree_rotate_right(__rb_tree_node_base* x, __rb_tree_node_base*& root) inline void __rb_tree_rebalance(__rb_tree_node_base* x, __rb_tree_node_base*& root) inline __rb_tree_node_base* __rb_tree_rebalance_for_erase(__rb_tree_node_base* z, __rb_tree_node_base*& root, __rb_tree_node_base*& leftmost, __rb_tree_node_base*& rightmost) |
這幾個函式是實現紅黑樹平衡的重要操作,左旋轉,右旋轉,插入一個節點之後的平衡操作,刪除一個節點的平衡操作.這幾個函式較為複雜,我就不分析了,但是我覺得看網上的部落格會比較好理解.
下面看下rb_tree真正的定義.先是建立節點和銷燬節點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//分配節點儲存空間 link_type get_node() { return rb_tree_node_allocator::allocate(); } //回收一個節點的空間 void put_node(link_type p) { rb_tree_node_allocator::deallocate(p); } //建立一個節點 link_type create_node(const value_type& x) { link_type tmp = get_node(); // 配置空間 __STL_TRY { construct(&tmp->value_field, x); // 建造內容 } __STL_UNWIND(put_node(tmp)); return tmp; } link_type clone_node(link_type x) { // 複製一個節點(主要是是顏色) link_type tmp = create_node(x->value_field); tmp->color = x->color; tmp->left = 0; tmp->right = 0; return tmp; } void destroy_node(link_type p) { destroy(&p->value_field); // 解析內容 put_node(p); // 回收節點空間 } |
紅黑樹的成員主要有3個,
1 2 3 |
size_type node_count; // 記錄樹的大小(節點的個數) link_type header; Compare key_compare; // 節點間的比較器,應該是個函式物件 |
對於header,其實相當與連結串列中的頭節點,不儲存資料,可用於紅黑樹的入口.header的設計可以說的STL紅黑樹設計的一個亮點,header和root互為父節點,header的左節點指向最小的值,header的右節點指向最大的值,所以header也可以作為end()迭代器指向的值.圖示如下:;
接下來就是一些小函式,都是實現獲取節點成員變數的函式,有一個需要提下:
1 |
static const Key& key(link_type x) { return KeyOfValue()(value(x)); } |
KeyOfValue這是個函式物件,對於set和map的實現是不一樣的,主要功能就是從一個節點中獲取這個節點儲存的鍵值,對於set而言,這個函式物件為 identity<value_type>,這個函式物件返回的就是這個set儲存的值,而對於map而言,這個函式物件為 select1st<value_type>,map的值域為pair物件,所以select1st就是獲取這個pair的第一個成員.
接下就對重要函式做個簡單的介紹,因為太複雜了,而且這篇文章主要是講解set和map.
-
對於set和multiset,map和multimap而言,最大的區別就是是否允許鍵值重複,而反應在紅黑樹上,則為插入函式的不同.set和map用的是insert_unique,而multiset和multimap用的是insert_equal函式.具體原始碼,我也是一知半解,所以就不分析了,誤人子弟就不好了.
-
有插入就有刪除函式,紅黑樹提供的是erase函式,但是使用這個函式之後,可能導致紅黑樹不滿足那5個條件,所以要呼叫 __rb_tree_rebalance_for_erase來維持樹的平衡.
-
對於multiset和multimap而言,紅黑樹還提供了查詢與某個鍵值相等的節點迭代器範圍,
1
pair<iterator,iterator> equal_range(const key_type& x);
這個函式返回二叉樹中和鍵值x相等的迭代器範圍.而set和map都是不允許鍵值重複的,所以就不要用這個函式,直接用find函式即可.
- 紅黑樹還提供了查詢不小於和大於某個鍵值的函式:
1
2
3
4
5
6
//返回不小於k的第一個節點迭代器
typename rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator
rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::lower_bound(const Key& k)
//返回大於k的第一個節點迭代器
typename rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator
rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::upper_bound(const Key& k)
map實現
map底層用的是紅黑樹,所以map只是在紅黑樹上加了一層封裝,map中用的用的紅黑樹定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
typedef Key key_type; //鍵值型別 typedef T data_type; // 值型別 typedef T mapped_type; //值型別 typedef pair<const Key, T> value_type; // 值型別,map儲存pair鍵值對 typedef Compare key_compare; // 鍵值函式物件 class value_compare//值域比較器,其實也是比較鍵值 : public binary_function<value_type, value_type, bool> { friend class map<Key, T, Compare, Alloc>; protected : Compare comp; value_compare(Compare c) : comp(c) {} public: bool operator()(const value_type& x, const value_type& y) const { return comp(x.first, y.first); } }; //map中紅黑樹的定義 typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type; rep_type t; |
可以看到在map中,傳入紅黑樹的KeyOfValue函式物件是select1st<value_type>,而value_type為pair型別,這個函式物件就是獲取pair第一個鍵值,為了鍵值比較排序.
map建構函式
map建構函式有分為預設建構函式和帶比較函式物件的構造的建構函式,一個是用預設的比較物件less<key>,另一個是可以自己定義比較物件的建構函式
1 2 |
map() : t(Compare()) {} explicit map(const Compare& comp) : t(comp) {} |
map還支援用一對輸入迭代器來初始化,迭代器之間即為要插入map的資料,所以可以將vector首尾迭代器作為引數傳入map建構函式來初始化
1 2 3 4 5 6 7 |
template <class InputIterator> map(InputIterator first, InputIterator last) : t(Compare()) { t.insert_unique(first, last); } template <class InputIterator> map(InputIterator first, InputIterator last, const Compare& comp) : t(comp) { t.insert_unique(first, last); } |
map還支援值型別的陣列指標範圍來初始化map,例如pair<string,int> parr[10],然後用這個陣列的首尾指標來初始化map.
1 2 3 4 |
map(const value_type* first, const value_type* last) : t(Compare()) { t.insert_unique(first, last); } map(const value_type* first, const value_type* last, const Compare& comp) : t(comp) { t.insert_unique(first, last); } |
map插入操作
map插入分為單個值插入,在某個節點插入以及插入一對迭代器範圍內的元素.
1 2 3 4 5 6 7 8 9 10 |
pair<iterator,bool> insert(const value_type& x) { return t.insert_unique(x); } iterator insert(iterator position, const value_type& x) { return t.insert_unique(position, x); } void insert(const value_type* first, const value_type* last) { t.insert_unique(first, last); } void insert(const_iterator first, const_iterator last) { t.insert_unique(first, last); } |
對於第一個迭代器,返回的是一個pair<iterator,bool>,迭代器指向插入節點的位置,布林值為插入是否成功.其他都是簡單的呼叫紅黑樹的方法.
map刪除操作
map刪除操作提供刪除某個迭代器指向的節點,某個鍵指向的節點,以及一對迭代器指向的元素:
1 2 3 |
void erase(iterator position) { t.erase(position); } size_type erase(const key_type& x) { return t.erase(x); } void erase(iterator first, iterator last) { t.erase(first, last); } |
map查詢函式
map查詢函式有查詢某個鍵值的find函式,查詢某個鍵值個數的count函式,兩個限界函式,最後查詢範圍的函式
1 2 3 4 5 6 7 8 9 10 11 |
//查詢某個鍵值對應的節點迭代器 iterator find(const key_type& x) { return t.find(x); } //查詢某個鍵值的個數 size_type count(const key_type& x) const { return t.count(x); } //查詢不小於,即大於等於鍵值x的節點迭代器, iterator lower_bound(const key_type& x) {return t.lower_bound(x); } //查詢大於鍵值x的節點迭代器 iterator upper_bound(const key_type& x) {return t.upper_bound(x); } //返回lower_bound和upper_bound迭代器對. pair<iterator,iterator> equal_range(const key_type& x) { return t.equal_range(x); |
map一定要提到的函式是過載下標註運算子,這個函式也算是查詢某個鍵所對應的值,但是如果某個鍵不存在的話,則會插入pair對,鍵值為k,值為data_type型別的預設值.
1 2 3 |
T& operator[](const key_type& k) { return (*((insert(value_type(k, T()))).first)).second; } |
這個函式有點複雜,insert(value_type(k, T()))先返回pair<iterator,bool>第一個iterator,即這個鍵所對應節點的迭代器,然後取這個鍵所對應的值pair<key_type,value_type>,最後取出第二個元素即可.
所以要查詢某個元素不能用這個下標表示法,應該用find函式,因為前者會將不存在的鍵插入到map中.
set的實現
先來看下set中紅黑樹的定義:
1 2 3 4 5 6 7 8 |
typedef Key key_type; typedef Key value_type; // 鍵值和值域比較器是一樣的,因為set儲存的就是一個值,而不是鍵值對 typedef Compare key_compare; typedef Compare value_compare; typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type; rep_type t; |
這裡面最重要的就是identity<value_type>函式物件了.這個就是獲取set儲存的value的鍵值部分.也是標準模板庫的一部分.
看到這我才發現,set的介面和map的介面幾乎一模一樣...
轉載:http://luodw.cc/2015/11/19/STL-map/