1. 程式人生 > >STL原始碼剖析之map和set

STL原始碼剖析之map和set

之前分析二叉搜尋樹和平衡二叉樹時,真心感覺樹的實現真是難,特別是平衡二叉樹,不平衡之後需要調整,還要考慮各種情況,累感不愛.今天看到這個紅黑樹,發現比平衡二叉樹還難,但是紅黑樹比平衡二叉樹使用的場景更多,所以平常使用時,我們需要了解紅黑樹的實現原理,如果有能力,可以自己實現,但是如果實在做不出來,也沒關係,因為STL和linux核心都有現成的紅黑樹實現,拿來用即可,前提是瞭解紅黑樹原理.

紅黑樹原理


紅黑樹和平衡二叉樹一樣,本質上都是二叉搜尋樹,但是二者的搜尋效率都能實現平均效率logn,比二叉搜尋樹效能好.平衡二叉樹實現原理是判斷每個節點的左右子樹的高度差是否等於2,如果等於2,則要通過旋轉來實現樹的左右子樹高度差平衡.而紅黑樹實現原理是節點的顏色和旋轉來實現的,實現較複雜,先看下紅黑樹的滿足條件:

  1. 每個結點要麼是紅的,要麼是黑的。
  2. 根結點是黑的。
  3. 每個葉結點,即空結點(NIL)是黑的。
  4. 如果一個結點是紅的,那麼它的倆個兒子都是黑的。
  5. 對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。 如果某棵二叉搜尋樹滿足上述條件,則為紅黑樹.為什麼滿足上述條件可以實現平衡了?主要是第4條和第5條,由第5條可以得出,某個到所有葉子節點最長路徑是最短路徑的兩倍.即一條路徑為一黑一紅和一條路徑為全黑,大體上紅黑樹是平衡的,只是沒有AVL樹要求那麼嚴格.

我不推薦直接看STL原始碼剖析這本書中的紅黑樹,而是先到網上部落格先看看大家是怎麼寫,因為這本書STL實現的紅黑樹比較複雜,不好看懂.我推薦部落格

michael,因為這篇部落格一步一步講解很好,圖做的也很好理解.

紅黑樹節點和迭代器


紅黑樹節點和迭代器的設計和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()迭代器指向的值.圖示如下:header圖示;

接下來就是一些小函式,都是實現獲取節點成員變數的函式,有一個需要提下:

 

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.

  1. 對於set和multiset,map和multimap而言,最大的區別就是是否允許鍵值重複,而反應在紅黑樹上,則為插入函式的不同.set和map用的是insert_unique,而multiset和multimap用的是insert_equal函式.具體原始碼,我也是一知半解,所以就不分析了,誤人子弟就不好了.

  2. 有插入就有刪除函式,紅黑樹提供的是erase函式,但是使用這個函式之後,可能導致紅黑樹不滿足那5個條件,所以要呼叫 __rb_tree_rebalance_for_erase來維持樹的平衡.

  3. 對於multiset和multimap而言,紅黑樹還提供了查詢與某個鍵值相等的節點迭代器範圍,

     

    1

     

    pair<iterator,iterator> equal_range(const key_type& x);

     

這個函式返回二叉樹中和鍵值x相等的迭代器範圍.而set和map都是不允許鍵值重複的,所以就不要用這個函式,直接用find函式即可.

  1. 紅黑樹還提供了查詢不小於和大於某個鍵值的函式:
     

    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/