STL序列式容器
一,概述
所谓序列式容器,指容器中的数据都是可序的,即元素集合呈线性关系排列,但未必是有序的(符合某比较关系)。
C++语言本身提供了一个序列式容器array,但是它不可以动态扩展。
STL另外提供了vector,list,deque,stack,queue,priority-queue等序列式容器。
stack,queue,缺省由deque改装而成,被归为配接器。
二,标准容器
1,vector
1.1 vector概述
vector,可以理解为动态数组。它的数组安排和操作方式与array非常相似。array是静态空间,而vector是动态空间,随元素的加入,内部会自行扩充空间。
对于array,如果空间不够用了,一些复杂的工作,得我们自己来安排。首先,申请一块新空间,然后将数据一一搬过去,最后把原空间释放。
而对于vector,这些工作它自动就完成了,完成的工作也是这样的。
1.2 vector部分定义
template<class T, class Alloc = alloc>
class vector
{
public:
.......
.......
///一些类型定义,具体请参阅STL源码
protected:
iterator start; //目前使用空间的头部位置
iterator finish; //目前使用空间的尾部位置
iterator end_of_storage; //目前可用空间的尾部位置
public:
iterator begin();
iterator end();
size_type size();
size_type capacity();
bool empty() const;
reference operator[](size_type n);
vector();
vector(size_type n ,const T&value);
vector(int n,const T&value);
vector(long n,const T&value);
explicit vector(size_type n); //explicit 构造函数不能在隐式转换中使用
~vector();
reference front();
reference back();
void push_back(const T&x);
void pop_back();
iterator erase(iterator pos);
void resize(size_type newsize,const T& x);
void resize(size_type newsize);
void clear();
};
1.3 vector的迭代器
vector维护的是一个连续线性空间,所有普通指针可以作为vector的迭代器,并且满足所有必要条件。vector迭代器所需要的行为包括 operator*,operator->,operator++,operator--,operator+,operator-,operator+=,operator-=
所以,vector提供的迭代器是Random Access Iterators
迭代器失效:
在插入的过程中,可能遇到扩容的可能,扩容的时候会重新分配空间。而原来的迭代器还指在初始位置上,这样就会引起迭代器失效。
可以在源码中看到
template<class T, class Alloc = alloc>
class vector
{
public:
typedef T value_type;
typedef value_type* iterator; //即 T*,一个普通的指针
...
..
};
1.4 vector的数据结构
template<class T, class Alloc = alloc>
class vector
{
protected:
iterator start; //目前使用空间的头部位置
iterator finish; //目前使用空间的尾部位置
iterator end_of_storage; //目前可用空间的尾部位置
};
iterator begin(){return start;}
iterator end(){return finash;}
size_type size()const
{
return size_type(end()-begin());
}
size_type capacity()const
{
return end_of_storage-begin();
}
为了降低空间配置时的速度成本,vector实际配置的大小可能比用户需求更大一些,以备将来扩充。
2,list
2.1 list概述
相对于vector的连续线性空间,list就显得复杂太多,它的好处是每次插入或者删除就会配置或者释放一个元素空间。
它对于空间的运用绝对精准,一点也不多余。而且对于指定位置元素的插入或移除,list都是常数时间。
2.2 list节点
对于链表,list本身,和list节点时不同的结构,需要分开设计。
这是list的节点结构,很显然,它是一个双向链表
template<class T>
struct _list_node
{
typedef void* void_pointer;
void_pointer prev; //也可以设为 _list_node<T>*
void_pointer next;
T data;
};
2.3 list迭代器
由于list是双向链表,迭代器应为 BidirectionIterators
这样的话,必须定义一个类,重载++,--等等即可。
template <class T,class Ref,class Ptr>
struct _list_iterator
{
typedef _list_iterator<T,T&,T*> iterator;
typedef _list_iterator<T,Ref,Ptr> self;
typedef _list_node<T>* link_type;
//其他定义不列出
link_type node; //迭代器内部当然需要一个指针了,指向list节点
self& operator++()
{
node = (link_type)((*node).next);
return *this;
}
self operator ++(int)
{
self tmp = *this;
++ *this;
return tmp;
}
self& operator--()
{
node = (link_type)((*node).prev);
return *this;
}
self operator --(int)
{
self tmp = *this;
-- *this;
return tmp;
}
/其他操作不列出
};
2.4 list的数据结构
template <class T,class Alloc=alloc>
class list
{
protected:
typedef _list_node<T> list_node;
public:
typedef list_node* link_type;
protected:
link_type node; //list是一个环,一个指针就可表示整个链表
};
node指针指向刻意置于尾端的一个空白节点,这样整个list就符合ST对于“前闭后开”区间的要求。
此时,首尾迭代器就定义为。
iterator begin()
{
return (link_type)((*node).next);
}
iterator end()
{
return node ;
}
这样做的好处在于,仅有一个指针就标明了首尾。
当试图在尾端和首端插入时就是常量时间了。
3,slist
3.1,slist概述
list是一个双向链表,而slist则是一个单向链表。主要差别在于,前者的迭代器是双向的Bidirectional Iterator,后者则是Forward Iterator。
为此,slist的功能受到了一些限制,不过,单向链表所耗用的空间更小,某些操作更快。
根据STL习惯,插入操作会将新元素放在指定位置之前。而作为一个单向链表,slist没有任何方便的办法可以回到前一个位置,因此,必须从头开始找起。这显得很耗时。
为此slist特别提供了insert_after()和erase_after()供灵活使用。
3.2 slist的的节点
template <class T>
struct _slist_node_base
{
_slist_node_base* next;
};
template <class T>
struct _slist_node:public _slist_node_base
{
T data;
};
注意到,此时,只有一个next指针。
3.3,slist的迭代器
struct _slist_iterator_base
{
typedef forward_iterator_tag iterator_categort; //单向的
_slist_iterator_base *node;
void incr(){node = node->next};
...
...
};
template <class T, class Ref,class Ptr>
struct _slist_iterator : public _slist_iterator_base
{
...
...
typedef _slist_iterator<T,Ref,Ptr> self;
self& operator ++()
{
incr();
return *this;
}
self& operator ++(int)
{
self tmp = *this;
incr();
return tmp;
}
//没有实现operatpr --
//
};
在这里,slist迭代器,只需要实现operator ++ 即可。
3.4,slist的数据结构
template <class T,class Alloc = alloc>
class slist
{
private:
typedef _slist_node<T,T&,T*> iterator;
typedef _slist_node<T> slist_node;
typedef _slist_node_base slist_node_base;
...
list_node_base head; //这里是一个实物,不是指针。 没有数据域
iterator begin(){return iterator((slist_node*)head.next);}
iterator end(){return iterator(0);}
....
}
4,deque
4.1,deque概述
相较于vector是单向开口的连续线性空间,deque则是一个双向开口的线性空间。
虽然模型图如此,但实际上,STL deque的结构远比这复杂。
它是动态地以分段连续空间组合而成。随时可以增加一段新的空间并连接起来。
虽然deque也提供Random Access Iterator,但是,它的迭代器并不是普通指针,这都是由于deque特殊的结构造成的。
这影响了deque各个函数的性能。例如deque的排序操作,为了最高效率, 可将deque西安完成复制到一个vector身上,将vector排序后,再复制回来。
4.2,deque的中控器
deque是由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便要配置一段定量的连续空间,串接在整个deuqe的头部或者尾部。
deque的最大任务,便是在这些分段的定量连续空间上, 维护其整体连续的假象,并提供随机存取的接口。
为此,deque采用一段所谓的map(不是STL中的map)作为主控。这里所谓的map是一小块连续的空间,每个元素都是一个指针,指向另一段连续空间,称为缓冲区。
缓冲区才是存放数据的地方。
template <class T,class Alloc = alloc,size_t BufSize=0>
class deque
{
public:
typedef T value_type;
typedef T* pointer;
protected:
typedef pointer* map_pointer;
map_pointer map; //指向map,其内部每个元素都是一个指针。
size_type map_size; //mao 可容纳的指针
};
4.3,deque的迭代器
deque是分段连续空间。维持其“整体连续”假象的任务,就交由迭代器来实现。具体就在于operator++和operator--两个子运算上。
template <class T,clas Ref,class Ptr,size_t BufSize=0>
struct _deque_iterator
{
typedef _deque_iterator<T,T&,T*,BufSize> iterator;
...
typedef random_access_iterator_tag iterator_category;
...
T* cur; //此迭代器所指的缓冲区的当前元素
T* first;//此迭代器所指的缓冲区的头部元素
T* last; //此迭代器所指的缓冲区的尾部元素
map_pointer node ;//指向中控器
};
实现代码为:
void set_node(map_pointer new_node)
{
node = new_node;
first = *new_node;
last = first+ difference_type(buffer_size());
}
self& operator ++()
{
cur ++ ;
if(cur == last) //如果满了,就切换到下一缓冲区
{
set_node(node+1);
cur = first;
}
return *this;
}
self& operator--()
{
if(cur == first) //已经是缓冲区的头部
{
set_node(node-1);
cur = last;
}
--cur;
return *this;
}
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()) -1;
set_node(node + node_offset); //切换到正确的缓冲区
cur = first + (offset - node_offset*difference_type(buffer_size());
}
}
三,容器配接器
stack,queue,虽然不符合标准容器的配置,但也是一种非常常用的“容器”。
1,stack
1.1,stack概述
stack是一种先进先出的数据结构(FIFO)。它只有一个出口。
只允许从顶端插入,移除元素。不支持遍历行为。
1.2,stack实现
template <class T,class Sequence=deque<T>>
class stack
{
//__STL_NULL_TMPL_ARGS展开会变成<> 为了实现 bound friend templates
//也就是说,class template的某个具体实现与其friend function template的某个具体实现有一对一的关系
friend bool operator == __STL_NULL_TMPL_ARGS(const stack&,const stack&);
friend bool operator < __STL_NULL_TMPL_ARGS(const stack&,const 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(){return c.empty()};
size_type size()const{return c.size();}
reference top(){return c.back();}
void push(const value_type&x){c.push_back(x);}
void pop(){c.pop_back();}
};
bool operator == __STL_NULL_TMPL_ARGS(const stack<T,Sequence>&,const stack<T,Sequence>&)
{
return x.c == y.c;
}
bool operator < __STL_NULL_TMPL_ARGS(const stack<T,Sequence>&,const stack<T,Sequence>&)
{
return x.c < y .c;
}
可以看出,stack默认是使用deque作为底层容器。
某些时候,也是可以实用list作为底层容器。
stack<int ,list<int>> istack
2,queue
2.1,queue概述
queue是一种先进先出的数据结构。它有两个开口。
queue运行在尾部增加元素,在首部取出元素。
和stack一样,不支持随机访问,也不能遍历。
2.2,queue实现
template <class T,class Sequence=deque<T>>
class queue
{
//__STL_NULL_TMPL_ARGS展开会变成<> 为了实现 bound friend templates
//也就是说,class template的某个具体实现与其friend function template的某个具体实现有一对一的关系
friend bool operator == __STL_NULL_TMPL_ARGS(const queue&,const queue&);
friend bool operator < __STL_NULL_TMPL_ARGS(const queue&,const 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(){return c.empty()};
size_type size()const{return c.size();}
reference front(){return c.front();}
reference back(){return c.back();}
void push(const value_type&x){c.push_back(x);}
void pop(){c.pop_front();}
};
bool operator == __STL_NULL_TMPL_ARGS(const queue<T,Sequence>&,const queue<T,Sequence>&)
{
return x.c == y.c;
}
bool operator < __STL_NULL_TMPL_ARGS(const queue<T,Sequence>&,const queue<T,Sequence>&)
{
return x.c < y .c;
}
可以看出,queue默认是使用deque作为底层容器。
某些时候,也是可以实用list作为底层容器。
queue<int ,list<int>> iqueue
四,其他
1, heap
STL采用了一种非常聪明的方式来构造heap——完全二叉树。
完全二叉树整棵树内都没有任何节点漏洞,这带来了极大的好处。可以使用vector来存储节点。假如使用一个小技巧,将vector的0号元素保留,那么当树中某个节点位于i处时,其左子节点为2*i,右孩子为2*i+1,父节点为i/2.
通过这么简单的位置规则,vector可是轻易实现完全二叉树。
这种以vector/array来表示tree 的方法,称为隐式表述法。
这样,一个vector,再加上一组heap算法来插入、删除元素,取极值等,就完成了一个heap。
2,priority_queue
max_heap可满足priority_queue所需要的特性。
template <class T,class Sequence=vector<T>
class Compare = less<typename Sequence::value_type>>
class priority_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; //底层容器
Compare comp; //元素大小比较规则
public:
template <class InputIterator>
priority_queue(InputIterator first,InputIterator last,const Compare& x)
:c(first,last),comp(x)
{
make_heap(c.begin(),c.end(),comp);
}
template <class InputIterator>
priority_queue(InputIterator first,InputIterator last):c(first,last)
{
make_heap(c.begin(),c.end(),comp);
}
void push(const value_type& x)
{
__STL_TRY
{
c.push_back(x); //先将元素推入末端,再重排heap
push_heap(c.begin(),c.end(),comp);
}
__STL_UNWIND(c.clear());
}
void pop()
{
__STL_TRY
{
//重排heap,再使用pop_back取的弹出的元素
push_heap(c.begin(),c.end(),comp);
c.pop_back();
}
__STL_UNWIND(c.clear());
}
};