對于標準庫來說,容器是非常大的一塊內容,那么之前已經談過關于list、vector、array、forward_list(slist)的內容,還有很多的容器是沒有談到的,今天就把剩下的容器一網打盡,全部過一遍,看看他們背后到底藏著些什么秘密吧。
容器結構分類
- 序列式容器(Sequence Container)的衍生關系
- array
(C++2.0)連續空間
- vector
連續空間
- heap
以算法形式呈現(xxx_heap())
- priority_queue
- list
雙向鏈表
- slist
C++2.0中為forward_list,單向鏈表
- deque
分段連續空間
- stack
Container Adapter
- queue
Container Adapter
- stack
- array
- 關聯式容器(Associative Containers)的衍生關系(復合)
- rb_tree
紅黑樹,非公開
- set
- map
- multiset
- multimap
- hashtable
非公開
- hash_set
非標準,C++2.0為unordered_set
- hash_map
非標準,C++2.0為unordered_map
- hash_multiset
非標準,C++2.0為unordered_multiset
- hash_mulitmap
非標準,C++2.0為unordered_multimap
- hash_set
- rb_tree
容器 deque —— 雙向開口的一段“表面連續”空間
表面看deque的圖形是如下圖所hi的樣子,可以認為他是一段連續的內存空間,同時,deque可以雙向擴充。不但可以向前方增加內存空間,也可以向后方增加內存空間。
但是對于實際內存來說,雙向擴充是比較復雜的事情。因為,在內存的角度來看,內存是分配給所有的運行程序的,所以在我們的程序之前或之后的內存空間,說不定操作系統早就幫我們分配給別的程序了,如果,那些空間都是為我們程序預留起來的話,那么實在是浪費空間,并且,給你預留多少合適呢,你總還是需要向兩方面擴充的。所以,如果內存的設計方式和我們所感覺的一樣,其實,在實際的操作中是不好實現的。
那么就需要設計一套行之有效的方法來解決在這樣的內存空間中的雙向擴充問題了。
那么stl的實際的解決方案,內存示意圖,是如下圖所示的這樣的。
對于deque來說,實際是有一塊連續的內存空間的,他的實現底層是采用的vector來實現的,而這塊連續空間中,只維護這一些*** 指 針 ***,這些指針,每個都指向一塊連續的空間,也就是buffer。在每個buffer中,保存的都是實際的數據。
這樣做可以解決之前所提出的問題,如果擴充的時候,只需要考慮改變vector這塊連續空間中的指針數組就可以了,增加的空間只需要放在對應的buffer,而不需要多大程度來影響連續空間的問題。
那么既然實際的內存空間不連續,如何讓使用者感受不到實際設計就變得十分重要了。接下來,我們會仔細研究一下,為什么deque可以通過這樣的設計,來讓使用者覺得他是連續的。
這里面最關鍵的就在于上圖所示的部分了,也就是這個iterator的作用,讓這樣“連續是假象,分段是實事”的情況得以實現。在迭代器運行到邊界的時候,都需要檢測是否到邊界,然后通過回到控制buffer的那個vector來管理邊界的buffer了。在iterator中,cur、first、last和node分別指向了用戶使用時的當前的數據,first指向了buffer的第一塊空間,last指向了buffer之后那個不在buffer中的空間,而node指向了控制buffer的指針序列中的實際位置。
原理其實說的差不多了,但是實際代碼長啥樣呢,我們來看看吧。
//如果BufSiz不為0,則返回對應值,表示buffer size由開發者自己確定
//如果BufSiz為0,表示buffer size 由預設值決定。
template<class T, class Alloc = alloc, size_t BufSiz = 0>
//T:存儲的數據類型
//Alloc:分配器
//BufSiz:buffer的大小
class deque{
public:
typedef T value_type;
typedef __deque_iterator<T, T&, T*, BufSize> iterator; //buffer size 是指每個buffer容納的元素個數
//在接下來會給出__deque_iterator的源代碼
protected:
typedef pointer* map_pointer; //T**
protected:
iterator start;
iterator finish;
map_pointer map;
size_type map_size;
public:
iterator begin(){return start;}
iterator end() {return finish;}
size_type size() const {return finish - start; }
...................
//確定BufSiz的大小,如果sz(sizeof(value_type)) < 512, 返回 512/sz
//如果sz>= 512,返回1
inline size_t __deque_buf_size(size_t n, size_t sz){
return n!=0? (sz<512?size_t(512/sz):size_t(1));
}
}
//__deque_iterator的源代碼
template<class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator{
typedef random_access_iterator_tag iterator_datagory
typedef T value_type;
typedef Ptr Pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T** map_pointer;
typedef __deque_iterator self;
//迭代器的數據部分,也就是之前的cur、first、last和node
T* cur;
T* first;
T* last;
map_pointer node;
............
}
//deque的插入問題
//元素插入的時候,因為是按順序排列,如果插入在中間的位置,應該會改變其他元素的位置
//就相當于在在書架中插入一本書,肯定需要移動前后的書
//如果插入點,距離前端比較近,那么移動前端比較合適,效率較高
//如果插入點距離后端比較近,那么將插入點之后的元素向后移動比較快
//在postion處插入一個元素x
iterator insert(iterator postion, const value_type& x){
if(postion.cur == start.cur) //如果安插點是deque的最前端
{
push_front(x); //直接使用push_front
return start;
}
else if(postion.cur == finish.cur) //如果安插點是deque的最末位
{
push_back(x); //直接交給push_back
iterator tmp = finish;
--tmp;
return tmp;
}
else
{
return insert_aux(postion, x);
}
}
template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator_deque<T, Alloc, BufSIze>:: itert_aux(iterator pos, const value_type& x){
difference_type index = pos - start; //安插點之前的元素個數
value_type x_copy = x;
if(index < size() / 2){ //如果安插點之前的元素較少
push_front(front()); //在最前端加入第一個元素同值的元素
.......
copy(front2, pos1, front1); //元素搬移
}
else { //安插點之后的元素較少
push_back(back());//在尾端加入最末元素同值的元素
......
copy_backward(pos, back2, back1);//元素搬移
}
*pos = x_copy;//在安插點上設定新值
return pos;
}
deque如何模擬連續空間
- 主要功勞都是iterator的協調
reference operator[](size_type n)
{
return start[difference_type(n)];
}
reference front()
{
return *start;
}
reference back()
{
iterator tmp = finish;
--tmp;
return *tmp;
}
size_type size() const
{
return finish - start;
// 此處內存不連續,說明操作符- 進行了重載
}
bool empty() const
{
return finish == start;
}
reference operator* () const
{
return *cur;
}
pointer operator->() const
{
return &(operator*());
}
//兩個iterator之間的距離相當于
//1.兩個iterator之間的buffer的總長度
//2.加上itr至buffer末尾的長度
//3.加上x至buffer開頭的長度
difference_type
operator- (const self& x) const
{
return difference_type(buffer_size()) * (node - x.node - 1) + (cur - first) + (x.last - x.cur);
//buffer size * 首尾buffer之間的buffer之間的數量 + 末尾(當前)buffer的元素量 + 起始buffer的元素量
}
//-- 和++ 的操作符重載
self& operator++()
{
++cur; //切換至下一個元素
if(cur == last){ //如果抵達緩沖區的末尾
set_node(node + 1); //就跳至下一個節點(緩沖區)的起點
cur = first;
}
return *this;
}
self operator++(int)
{
self tmp = *this;
++*this;
return tmp;
}
self& operator--()
{
if(cur == first){
set_node(node - 1);
cur = last;
}
--cur;
return *this;
}
self operator--(int)
{
self tmp = *this;
--*this;
return tmp;
}
void set_node(map_pointer new_node)
{
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size));
}
self& operator+=(difference_type n ){
difference_type offset = n + (cur - first);
if(offset >= 0 && offset < difference_type(buffer_size())
//目標位置在同一級緩存區
cur += n;
else{
//目標位置不在同一級緩存區內
difference_type node_offset = offset > 0? offset / difference_type(buffer_size()): -difference_type((-offset - 1) / buffer_size;
//切換至正確的的緩存區
set_node(node + node_offset);
cur = first + (offset - node_offset * difference_type(buffser_size());
}
return *this;
}
operator+(difference_type n) const
{
self tmp = *this;
return tmp += n;
}
self& operator-=(difference_type n)
{
return *this += - n;
}
self operator-(difference_type n)
{
self tmp = *this;
return tmp -= n;
}
reference operator[] (difference_type n)const
{
return *(*this + n);
}
GNU4.9版本中的版本
容器 queue
內部維護一個deque,開放部分功能實現先進先出。
template <class T, class Sequence = deque<T>>
class queue
{
............
public:
typedef typename Sequence::value_type value_type
typedef typename Sequence::size_type size_type
typedef typename Sequence::reference reference;
typedef typename Sequence::const_reference const_reference;
protected:
Sequence c; //底層容器
public:
bool empty() const{return c.empty();}
size_type size() const{return c.size();}
reference front() const {return c.front();}
const_reference front() const{ return c.front();}
reference back(){return c.back(); }
const_reference back() const {return c.back();}
void push (const value_type& x){ c.push_back(); }
void pop(){c.pop.front();}
}
容器 stack
內部維護一個deque,開放部分功能實現先進先出。
template <class T, class Sequence = deque<T>>
class stack
{
............
public:
typedef typename Sequence::value_type value_type
typedef typename Sequence::size_type size_type
typedef typename Sequence::reference reference;
typedef typename Sequence::const_reference const_reference;
protected:
Sequence c; //底層容器
public:
bool empty() const{return c.empty();}
size_type size() const{return c.size();}
reference top() const {return c.back();}
const_reference top() const{ return c.back();}
void push (const value_type& x){ c.push_back(); }
void pop(){c.pop.back();}
}
- stack和queue都可以選擇list或deque作為底層結構
- queue不可以選擇vector作為底層結構(vector中沒有pop_front()函數,提供給pop()函數來調用)
- stack和queue都不允許遍歷,也不提供iterator
rb-tree(紅黑樹)
Red-Black tree(紅黑樹)是平衡二叉查找樹(balanced Binary search tree)的一種,平衡二叉搜索樹的特點:排列規律,方便查找和插入,并能夠保持適度的平衡,不會產生深度很深的子樹
rb_tree提供“遍歷”的操作和iterator,按正常規則(++ite)遍歷,便能夠獲得排序后的狀態。
我們不應該使用rb_tree的iterator來改變元素的值(因為元素尤其嚴謹的排列規則)。編程層面并沒有禁止這樣做,但是如果設計是正確的,因為rb_tree即將為set和map服務(作為其底部支持),而map允許元素的data改變,只有元素的key啊此時不可以改變的。
rb_tree提供兩種插入方式:insert_unique()和insert_equal()前者表示節點的key一定在整個tree中獨一無二,否則安插失敗;第二個表示節點key可以重復。
標準庫對rb_tree的實現
template <class Key,
class Value,
class KeyOfValue,
class Compare,
class Alloc = alloc>
class rb_tree{
protected:
typedef __rb_tree_node<Value> rb_tree<node;
.....
public:
typedef rb_tree_node* link_type;
......
protected:
//rb_tree只以三種數據表現自己
size_type node_count; //rb_tree的大小
link_type header; //一個rb_tree_node的指針
Compare key_compare; //key的大小比較,應該是function object
..........
};
GNU4.9之后的rb_tree的結構
容器set、multiset
set和multiset以rb_tree為底層結構,因此有“元素自動排序的功能”。特性:排序的依據是key,而set和multiset元素的key和value合一,value就是key;
set和multiset提供“遍歷”的操作以及iterator,按正常規則(++ite)遍歷,便能夠獲得排序后的狀態
我們無法使用set和multiset的iterator改變元素的值(因為,key尤其嚴謹的排列規則),set和multiset的iterator是底部的紅黑樹的const_iterator,就是為了禁止開發者對元素進行賦值操作的。
set的元素的key必須獨一無二,因此insert()使用的是紅黑樹的insert_unique()
multiset元素的key可以重復,因此使用insert()使用的是紅黑樹的insert_equal()
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set{
public:
//typedefs:
typedef Key key_type;
typedef Key value_type;
typedef Compare key_compare;
typedef Compare value_compare;
private:
typedef rb_tree<key_type, value_type, identity<value_type<. key_compare, Alloc> rep_type;
rep_type t;
public:
typedef typename rep_type::const_iterator iterator; //此處為rep_type::const_iterator,所以不能夠修改
..........
//set的所有操作,都調用底層rb_tree的函數,從這點看來,set實際應該為container adapter
}
multiset和set的原理基本一樣,調用插入部分有些區別
容器map和multimap
map和multimap以rb_tree為底層機構,因此,有“元素自動排列的特性”,排序的依據是key。
map和multimap提供遍歷的操作和iterator。按正常的++ite,就可以得到排序后的結果
我們無法使用map和multimap的iterator改變元素的key(因為key尤其嚴謹的排序規則),但可以用它來改變元素的data。因此map和multimap內部自動將開發者指定的key type設置為const,以此便能夠禁止開發者對元素的key進行賦值。
map元素的key必須為獨一無二的,因此insert()用的是紅黑樹的insert_unique()
multimap元素的key可以重復,因此insert()用的是紅黑樹的insert_equal()
template <class Key,class T, class Compare = less<Key>, class Alloc = alloc>
class map{
public:
//typedefs:
typedef Key key_type;
typedef T data_type;
typedef T mapped_type;
typedef pair<const Key, T> value_type;
typedef Compare value_compare;
private:
typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;
rep_type t;
public:
typedef typename rep_type::const_iterator iterator; //此處為rep_type::const_iterator,所以不能夠修改
..........
//set的所有操作,都調用底層rb_tree的函數,從這點看來,set實際應該為container adapter
}
map通過重載了操作符[]來實現通過key來查找元素,multimap的key可以重復,那么沒有這個方法
如果對于在空間中不存在的key使用[] 賦值,會在map中自動添加該元素
hashtable(哈希表)
為了方便管理元素,對于多個元素來說么可以除以可放元素的空間的大小,得到的余數來作為存放元素的編號。對于不同元素放在同一個位置,會產生碰撞的情況,為了避免這個問題,采用了鏈表來組織重復的元素,這樣就可以避免了一個空間存放多個元素的問題(separate chaining)。
如此管理原始,解決了空間上的問題,但是如果出現了鏈表過長的情況,遍歷鏈表的過程反而消耗了大量的時間,效率會變得低下,那么應該如何解決呢?
對于存放鏈表的空間,把他們成為籃子,那么,憑借經驗值得出來,如果當元素的個數多過籃子的個數時,就將籃子的數量進行調整,重新分配元素的位置(rehashing)
籃子的個數,一般都是采用質數來做籃子的數量,當擴充時,會選擇原有籃子數量的兩倍以上的質數,作為籃子的個數
template<class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc = alloc>
class hashtable{
public:
typedef HashFcn hasherl
typedef EqualKey key_equal;
typedef size_t size_type;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*, Alloc> buckets;
size_type num_elements;
public:
size_type bucket_count() const{return buckets.size();}
...........
};
template<class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc = alloc>
struct __hashtable_iterator{
.....
node* cur;
hashtable* ht;
};
template<class Value>
struct __hashtable_node{
__hashtable_node* next;
Value val;
};
hash Function的問題(HashFcn)
目的:希望根據元素值酸楚一個hash code(一個可進行modulus運算的值),使得元素經hash code映射之后能夠“夠亂夠隨機”的被置于hashtable中,越混亂,越不容易發生碰撞。
//泛化
template<class Key> struct hash{};
//特化
#define __STL_TEMPLATE_NULL template<>
__STL_TEMPLATE_NULL struct hash<char> {
size_t operator()(char x) const {return x;}
};
__STL_TEMPLATE_NULL struct hash<short> {
size_t operator()(short x) const {return x;}
};
__STL_TEMPLATE_NULL struct hash<unsigned short> {
size_t operator()(unsigned short x) const {return x;}
};
.......
//char* 的hash function的特化版本
//標準庫沒有提供hash<std:string>的版本
__STL_TEMPLATE_NULL struct hash<char *> {
size_t operator()(char* x) const {return __stl_hash_string(x);}
};
inline size_t __stl_hash_string(const char* s)
{
unsigned long h = 0;
for(; *s; ++s){
h = 5 * h + *s;
}
return size_t(h);
}
- c++字符串(string)使用hash table為底層的容器,需要重寫hash function
unordered_set(hash_set)和unordered_multiset(hash_multiset)、unordered_map(hash_map)和unordered_multimap(hash_multimap)
這些容器的底層都是hashtable,因此這些容器的籃子一定大于元素的個數