序列式容器(sequence containers)
1.容器的概观与分类
- 研究数据的特定排列方式,以利于搜寻或排序或其它特殊目的,称为数据结构(DataStructures)。任何特定的数据 结构都是为了实现某种特定的算法。
- STL容器即是将运用最广的一些数据结构实现出来(如下图所示)。未来,在每五年召开一次的C++标准委员会的会议中,STL容器 的数董还有可能增加。
- 常用的数据结构不外乎array(数组)、list(链表)、tree(树)、stack (堆栈)、queue(队列)、hashtable (散列表)、set(集合)、map(映射表)…等等。
- 根据“数据在容器中的排列'特性,这些数据结构分为序列式(sequence)和关联式(associative)两种。本章探讨序列式容器,下一章探讨关联式容器。
- 容器是大多数人对STL的第一印象,这说明了容器的好用与受欢迎。容器也是许多人对STL的唯一印象,这说明了还有多少人利器(STL)在手而未能善用。
- 上图表示的SGI STL的各种容器(本图以内缩方式来表达基层与衍生层的关系。
- 这里所谓的衍生, 并非派生(inheritance)关系,而是内含(containment)关 系。
- 例如heap内含一个vector, priority-queue内含一个heap、stack和queue都含 一个deque , set/map/multiset/multimap都内含 一 个RB-tree , hast_x都内含 一个 hash tabl。
1.1 序列式容器(sequential containers)
所谓序列式容器,其中的元素都可序(ordered), 但未必有序(sorted)。C++语言本身提供了一个序列式容器array,STL另外再提供vector,list, deque, stack,queue,priority-queue等等序列式容器。其中stack和queue由于只是将deque改头换面而成,技术上被归类为一种配接器(adapter),。
2.vector
2.1 vector概述
vector的数据安排以及操作方式,与array非常相似。两者的唯一差别在于空间的运用的灵活性。
- array是静态空间,一旦配置了就不能改变:要换个大(或小)一点的房子,可以,一切琐细得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。
- vector是动态空间,随着 元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector的运用对 于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必因为害怕空间不足而 一开始就要求一个大块头array了,我们可以安心使用vector。
- vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦vector旧有空间满载,如果客户端每新增一个元素,vector内部只是扩 充一个元素的空间,实为不智,因为所谓扩充空间(不论多大),一如稍早所说,是"配置新空间/数据移动/释还旧空间”的大工程,时间成本很高,应该加人某种未雨绸缪的考虑。
2.2 vector定义摘要
下面是vector定义的源代码。虽然STL规定,欲使用vector者必须 先包括,但SGI STL将vector实现于更底层的<stl_vector.h>,
template<class T,class Alloc=alloc>
class vector{
public:
typedef T value_type;
typedef value_type* pointer;
typedef value_type* iterator;
typedef value_type& reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
protected:
typedef simple_alloc<value_type,Alloc>data_allocator;
iterator start;
iterator finish;
iterator end_of_storage;
void insert_aux(iterator position, const T& x);
void deallocate() {
if(start)
data_allocator::deallocate(start,end_of_storage-start);
}
void fill_initialize(size_type n, const T& value) {
start = allocate_and_fill(n,value);
finish = start+n;
end_of_storage = finish;
}
public:
iterator begin() {return start; }
iterator end() { return finish; }
size_type size() const { return size_type(end() - begin()); }
size_type capacity()const{
return size_type(end_of_storage - begin());}
bool empty() const { return begin() == end();}
reference operator[] (size_type n) { return *(begin() + n);}
vector() : start(O), finish(O), end_of_storage (0) {}
vector(size_typen, const T& value) {fill_initialize(n, value);}
vector(int n,const T& value) { fill_initialize(n, value); }
vector(long n,const T& value){fill_initialize(n,value);}
explicit vector(size_typen, n){fill_initialize(n,T());}
~vector(){
destroy(start,finish);
deallocate();
}
reference front(){return *begin();}
reference back(){return *(end()-1);}
void push_back(const T& x){
if(finish !=end_of_storage){
construct(finish,x);
++finish;
}
else
insert_aux(end(),x);
}
void pop_back(){
--finish();
destroy(finish);
}
iterator erase(iterator position){
if(position+ 1 != end()){
copy(position+1, finish, position);
--finish;
destroy(finish);
return position;
}
void resize(size_type new_size,const T& x){
if(new_size<size())
erase(begin()+new_size,end());
else
insert(end(),new_size - size(),x);
}
void resize(size_type new_size){resize(new_size,T());}
void clear(){erase(begin(),end());}
protected:
iterator alloc_and_fill(size_type n,const T& x){
iterator result = data_allocator::allocate(n);
uniniuialized_fill_n(result,n,x);
return result;
}
2.3 vector的迭代器
vector维护的是一个连续线性空间,所以不论其元素型别为何,普通指针都可以作为vector的迭代器而满足所有必要条件,因为vector迭代器所需要的操作行为,如operator*,operator->,operator++,operator–,operator+,operator-, operator+=,operator-=, 普通指针天生就具备。vector支持随机存取,而普通指 针正有着这样的能力。所以,vector提供的是RandomAccess Iterators。
template <class T,class Alloc=alloc>
class vector{
public:
typedef T value_type;
typedef value_type* iterator;
...
};
根据上述定义,如果客户端写出这样的代码:
vector<int>::iterator ivite;
vector<Shape>::iterator svite;
2.4 vector的数据结构
vector所采用的数据结构非常简单:线性连续空间。它以两个迭代器start和finish分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器 end_of_storage指向整块连续空间(含备用空间)的尾端:
template <class T,class Alloc=alloc>
class vector{
..
protected:
iterator start;
iterator finish;
iterator end_of_storag;
...
}
- 为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求量更大一些,以备将来可能的扩充。这便是容量(capacity)的观念。
2.换句话说, 一个vector的容量永远大于或等于其大小。一旦容量等于大小,便是满载,下次再有新增元素,整个vector就得另觅居所,如下图所下:
运用start,finish,end_of_storage三个迭代器,便可轻易地提供首尾 标示、大小、容量、空容器判断、注标([])运算子、最前端元素值、最后端元 素值…等机能:
template <class T,class Alloc = alloc>
class vector{
public:
iterator begin() {return start; }
iterator end () { return finish; }
size_type size() const { return size_type(end() - begin()); }
size_type capacity()const{
return size_type(end_of_storage - begin());}
bool empty () const { return begin () == end () ; }
reference operator[] (size_type n) { return *(begin() + n);
...
};
2.5 vector的构造与内存管理:constructor, push_back
千头万绪该如何说起?以客户端程序代码为引导,观察其所得结果并实证源代 码,是一个良好的学习路径。下面是一个小小的测试程序,我的观察重点在构造的 方式、元素的添加,以及大小、容量的变化:
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int i;
vector<int> iv(2,9);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
iv.push_back(1);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
iv.push_back(2);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
iv.push_back(3);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
iv.push_back(4);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
for(i=0;i<iv.size();++i)
cout<<iv[i]<<'';
cout<<endl;
iv.push_back(5);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
for(i=0;i<iv.size();++i)
cout<<iv[i]<<'';
cout<<endl;
iv.pop_back();
iv.pop_back();
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
iv.pop_back();
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
vector<int>::iterator ivite = find(iv.begin(),iv.end(),1);
if(ivite) iv.erase(ivite);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
for(i=0;i<iv.size();++i)
cout<<iv[i]<<'';
cout<<endl;
ite = find(ivec.begin(),ivec.end(),2);
if(ite)ivec.insert(ite,3,7);
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
for(i=0;i<iv.size();++i)
cout<<iv[i]<<'';
cout<<endl;
iv.clear();
cout<<"size="<<iv.size()<<endl;
cout<<"capacity="<<iv.capacity()<<endl;
}
vector缺省使用alloc(第二章)作为空间配置器,并据此另外定义了data_allocator, 为的是更方便以元素大小为配置单位:
template <class T,class Alloc=alloc>
class vector{
protected:
typedef simple_alloc<value_type,Alloc>data_allocator;
...
}
于是,data_allocator::allocate(n)表示配置n个元素空间。
vector提供许多construct,其中一个允许我们指定空间大小及初值:
vector{size_type n, const T& value) { fill_initialize{n, value); }
void fill initialize(size_type n, const T& value) {
start=allocate_and_fill(n,value);
finish= start+ n;
end_of_storage = finish;
}
iterator rallocate_and_fill(size_type n, const T& x) {
iterator result = data_allocator: :allocate(n);
uninitialized_fill_n(result, n, x);
return result;
}
uninitialized_fill_n()会根据第一参数的型别特性决定使用算法fill_n()或反复调用construet()来完成任务。当我们以push_back()将新元素插入于vector尾端时, 该函数首先检查是否还有备用空间, 如果有就直接在备用空间上构造元素,并调整迭代器tinish, 使 vector变大。如果没有备用空间了,就扩充空间(重新配置、移动数据、释放原空间):
void push_back(const T& x){
if(finish !=end_of_storage){
construct(finish,x);
++finish;
}
else
insert_aux(end(),x);
}
template <class T,class Alloc>
void vector<T,Alloc>::insert_aux(iterator,position,const T& x){
if(finish !=end_of_storage){
construct(finish, *(finish - 1));
++finish;
T x_copy = x;
copy_backward(position, finish - 2, finish - 1);
*position = x_copy;
}
else{
const size_type old_size = size();
const size_type len = old_size != 0? 2 * old_size : 1;
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_star;
try {
new_finish = uninitialized copy(start, position, new_start);
construct(new_finish,x);
++new_finish;
new_finish = uninitialized_copy (position, finish, new_finish);
}
catch(...){
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
destroy(begin() , end()) ;
deallocate () ;
start= new_start;
finish= new_finish;
end_of_storage = new_start+ len;
}
}
注意,所谓动态增加大小,并不是在原空间之后接续新空间(因为无法保证原 空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然 后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此, 对vector的任何操作, 一且引起空间重新配置,指向原vector的所有迭代器就都失效了。 这是程序员易犯的一个错误, 务需小心。
2.6 vector的元素操作: pop_back, erase, clear, insert
vector所提供的元素操作很多, 无法在有限篇幅中 一一讲解 :其实也没有这种必要。 为搭配先前对空间配置的讨论,挑选数个相关函数作为解说对象。这些函数也出现在先前的测试程序中。
void pop_back()
{
--finish;
destroy(finish);
}
iterator erase(iterator first,iterator last){
iterator i = copy(last,finish,first);
destroy(i,finish);
finish=finish-(last-first);
return first;
}
iterator erase(iterator position){
if(position+1 !=end())
copy(position + 1,finish,position);
--finish();
destroy(finish);
return position;
}
void clear(){erase(begin(),end());}
局部区间的清除操作:
template <class T, class Alloc>
void vector<T,Alloc>::insert(iterator position,size_Type n,const T& x)
{
if(n !=0){
if(size_type(end_of_storage-finish)>=0){
T x_copy = x;
const size_type elems_after = finish - position;
iterator old_finish = finish;
if(elems_after>0){
uninitialized_copy(finish-n,finish,finish);
finish+=n;
copy_backward(position,old_finish-n,old_finish);
fill(position,position+n,x_copy);
}
else{
uninitialized_fill_n(finish,n-elems_after,x_copy);
finish +=n-elems_after;
uninitialized_copy(position,old_finish,finish);
finish +=elems_after;
fill(position,old_finish,x_copy);
}
}
else{
const size_type old_size = size();
const size_type len = old_size + max(old_size,n);
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
__STL_TRY{
new_finish = uninitialized_copy(start,position,new_start);
new_finish = uninitialized_copy(new_finish,n,x);
new_finish = uninitialized_copy(position, finish, new_finish);
}
# idef __STL_USE_EXCEPTIONS
catch(...){
destroy(new_start,new_finish);
data_allocator::deallocate(new_start,len);
throw;
}
#endif
destroy(start,finish);
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
}
在插入完成后,新节点将位于哨兵迭代器(上面之position,标示出插入点)所指之节点前方-这是STL对于“插入操作”的标准规范,如下图展示insert(position,n,x)的操作:
3.list
3.1 list概述
- 相较于vector的连续线性空间,list就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释放一个元素空间。
- list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list永远是常数时间。
- list和vector是两个最常被使用的容器。
- 什么时机下最适合使用哪一种容 器,必须视元素的多寡、元素的构造复杂度(有无non-trivialcopy constructor, non-trivial copy assignmen operator)、元素存取行为的特性而定。
3.2 list的节点(node)
每一个设计过list的人都知道,list本身和list的节点是不同的结构,需要分开设计。以下是STLlist的节点(node)结构:
template<class T>
struct __lsit_node{
typedef void* void_pointer;
void_pointer prev;
void_pointer next;
T data;
}
显然这是双链表。 3.3 list的迭代器
-
list不再能够像vector一样以普通指针作为迭代器,因为其节点不保证在储存空间存在。 -
list迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取等操作。 -
所谓“list迭代器正确的递增、递减、取值、成员取用”操作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取得的是节点的数据值,成员取用时取用的是节点得的成员。 list的结点与list的迭代器:
由于STL list是一个双向链表(doublelinked-list) , 迭代器必须具备前移、 后移的能力,所以list提供的是Bidirectional Iterators。
lsit有个重要性质:插入操作(insert)和接合操作(splice)都不会造成原有的list迭代器失效。这在vector是不成立的,因为vector定位插入操作可能造成记忆体 重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作,也只有“指向被删除元素”的哪个迭代器失效,其它迭代器不受如何影响。
下面是lsit迭代器的设计:
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 bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T>* link_type;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
link_type node;
__list_iterator(link_type x):nodc(x.node){}
__list_iterator(){}
__list_iterator(const iterator& x):nodc(x.node){}
bool operator==(const self& x)const{return node == x.node;}
bool operator!=(const self& x)const{return node !=x.node;}
reference operator*()const{return (*node).data;}
pointer operator->()const {return &(operator *());}
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;
}
3.4 list的数据结构
SGI list不仅是一个双向链表,而且还是一个环状双向链表。所以它只需要 一个指针,便可以完整表现整个链表:
template <class T,class Alloc = alloc>
class list{
protected:
typedef list_node* link_type;
public:
typedef __list_node<T> list_node;
protected:
link_type node;
...
}
如果让指针node指向刻意置于尾端的一个空白节点,node便能符合STL 对千“前闭后开“区间的要求,成为last迭代器,如下图所示。这么一来, 以下几个函数便都可以轻易完成:
iterator begin() { return (link_type) ((*node) .next); }
iterator end() { return node; }
bool empty() const { return node->next == node;
size_type size() const {
size_type result = O;
distance(begin(), end(), result);
return result;
}
reference front() { return *begin () ; }
reference back() { return *(--end()); }
list示意图。是环状链表只需一个标记,即可完全表示整个链表。只要刻意在环状链表的尾端加上一个空白节点,便符合STL规范之"前闭后开“区间。
3.5 list的构造与内存管理:constructor,push_back,insert
以客户端程序代码为引导,观察其所得结果并实证源代码,是个良好的学习路径。下面是一个测试程序,我的观察重点在构造的方式以及大小的变化:
#include<list>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int i;
list<int> ilist;
cout<<"size="<<ilist.size()<<endl;
ilist.push_back(0);
ilist.push_back(1);
ilist.push_back(2);
ilist.push_back(3);
ilist.push_back(4);
cout<<"size="<<ilist.size()<<endl;
list<int>::iterator ite;
for(ite=ilist.begin();ite !=ilist.end();++ite)
cout<<*ite<<' ';
cout<<endl;
ite=find(ilist.begin(),ilist.end(),3);
if(ite!=0)
ilist.insert(ite,99);
cout<<"size="<<ilist.size()<<endl;
cout<<*ite<<endl;
for(ite = ilist.begin();ite !=ilist.end();++ite)
cout<<*ite<<'';
cout<<endl;
ite=find(ilist.begin(),ilist.end(),1);
if(ite!=0)
cout<<*(ilist.erase(ite))<<endl;
for(ite = ilist.begin();ite !=ilist.end();++ite)
cout<<*ite<<'';
cout<<endl;
}
list缺省使用alloc作为空间适配器,并据此另外定义一个list_node_allocator,为的是更方便地以节点大小为单位:
template<class T,class Alloc = alloc>
class list{
protected:
typedef __list_node<T> list_node;
typedef simple_alloc<list_node,Alloc>list_node_allocator;
...
}
于是,list_node_allocator(n)表示配置n个节点空间。以下四个函数,分别用来配置、释放、构造销毁一个节点:
protected:
link_type get_node () { return list_node_allocator:: allocate () ;
void put_node (link_type p) { list_node_allocator::deallocate(p); }
link_type create_node(const T& x) {
link_type p = get_node();
construct(&p->data, x);
return p;
}
void destroy_node(link_type p){
destroy(&->data);
put_node(p);
}
list提供有许多constructor,其中一个是default constructor,允许不指定任何参数做出一个空的list出来:
public:
list(){empty_initialize();}
protected:
void empty_initialize(){
node = get_node () ;
node->next = node;
node->prev = node;
}
当我们以push_back()将新元素插入于list尾端时, 此函数内部调用 insert():
void push_back(const T& x) { insert (end(), x); }
insert()是 一个重载函数,有多种形式,其中最简单的一种如下,符合以上所需。首先配置并构造一个节点, 然后在尾端进行适当的指针操作, 将新节点插入进去:
iterator insert(iterator position, const T& x) {
link_type tmp = create_node(x);
tmp->next=position.node;
tmp->prev = position.node->prev;
(link_type(position.node->prev))->next = tmp;
position.node->prev = tmp;
return tmp;
}
于是, 当先前测试程序连续插入了五个节点(其值为0 1 2 3 4)之后,list的状态3.4中图所示。 如果希望在list内的某处插入新节点,首先必须确定插入位 置,例如希望在数据值为3的节点处插人一个数据值为99的节点, 可以这么做:
ilite = find(il.begin(), il.end(), 3);
if (ilite!=O)
il.insert(ilite,99);
find()操作稍后再做说明。 插入之后的list状态如下图所示。 注意, 插 入完成后,新节点将位于哨兵迭代器(标示出插入点)所指之节点的前方一这是 STL对于"插入操作” 的标准规范。由于list不像vector那样有可能在空间不 足时做重新配置、数据移动的操作,所以插入前的所有迭代器在插入操作之后都仍然有效。 3.6 list的元素操作:push_front, push_back, erase, pop_front, pop_back, clear, remove, unique, splice, merge, reverse, sort
list所提供的元素操作很多,无法一一讲。为搭配先前对空间配置的讨论,了解数个相关函数作为解说对象。先前示例中出现有尾部插入操作(push_back), 现在我们来看看其它的插入操作和移除操作。
void push_front(const T& x) { insert(begin() , x) ; }
void push_back(const T& x) { insert(end(), x);}
iterator erase (iterator position){
link_type next_node = link_type(position.node->next);
link_type prev_node = link_type(position.node->pnv);
prev_node->next = next_node;
next_node->prev = prev_node;
destroy_node(position.node);
return iterator(next_node);
}
void pop_front(){erase(begin());}
void pop_back(){
iterator tmp = end();
erase(--tmp);
}
template<class T,class Alloc>
void list<T,Alloc>::clear()
{
link_type cur = (link_type) node->next;
while(cur !=node){
link_type tmp = cur;
cur = (link_type) cur->next;
destroy_node(tmp);
}
node->next = node;
node->prev = node;
}
template<class T,class Alloc>
void list<T,Alloc>::remove(const T& value){
iterator first = begin();
iterator last = end();
while(first !=last){
iterator next = first;
++next;
if(*first == value)erase(first);
first = next;
}
}
template<class T,class Alloc>
void list<T,Alloc>::unique(){
iterator first = begin();
iterator last = end();
if(first==last) return;
iterator last = first;
while(++next !=last){
if(*first == *next)
erase(next);
else
first = next;
next = first;
}
}
由于list是个双向环状链表,只要我们把边际处理好,那么,在头部或尾部插入元素(push_front和push_back),操作几乎一样,在头部或尾部移除元素(pop_front和pop_back), 操作也几乎一样。移除(erase)某个迭代器所指元素,只是进行一些指针移除操作而已,并不复杂,如果上图在以下搜寻移除的操作,状况如下图所示:
ite = find(ilist.begin(),ilist.end(),1);
if(ite !=0)
cout<<*(ilist.erase(ite))<<endl;
移除“元素为1”的节点: list内部提供一个所谓的迁移操作(transfer): 将某连续范围的元素迁移到某个特定位置之前技术上很简单,节点间的指针移动而已。这个操作为其它的复杂操作如splice,sort,merge等打下良好基础。下面是transfer的源代码:
protected:
void transfer (iterator position, iterator first, iterator last) (
if(position !=last){
(*(link_type((*last.node) .prev))) .next= position.node;
(*(link_type((*first.node).prev))) .next = last.node;
(* (link_type ((*position.node) .prev))) .next = first.node;
link_type tmp= link_type((*position.node) .prev);
(*position.node).prev = (*last.node) .prev;
(*last.node) .prev = (*first.node) .prev;
(*first.node) .prev = tmp;
}
}
list::transfer中七个操作,如下图所示: transfer所接受的[first,last)区间,是否可以在同一个lsit之中呢? 答案是可以。你只要想象上图所画的两条lists其实是同一个lsit的两个区 段,就不难得到答案了。
上述的transfer并非公开接口。list公开提供的是所谓的接合操作(splice) : 将某连续范围的元素从一个list移动到另一个(或同一个)list 的某个定点如果接续先前list-test.cpp程序的最后执行点,继续执行以下splice 操作:
int iv[5]= { 5,6,7,8,9 };
list<int> ilist2(iv, iv+5);
ite = find(ilist.begin(), ilist.end(), 99);
ilist.splice(ite,ilsit2);
ilist.reverse();
ilist.sort();
下图显示接合操作,节点间的指针移动而已,这些操作完全由transfer()做掉了。 list的接合(splice)操作: 为了提供各种接口弹性,list: : splice有许多版本:
public:
void splice(iterator position, list&x) {
if(!x.empty())
transfer(position,x.begin(), x.end());
}
void splice(iterator position, list&, iterator i) {
iterator j = i;
++j;
if(position == i || postion == j)return;
transfer(position,i,j);
}
void splice (iterator position,list&,iteratorfirst, iterator last) {
if(first !=last)
transfer(position,first,last);
}
以下是merge() , reverse () , sort()的源代码。 有了transfer ()在手,这些操作都不难完成。
template <class T, class Alloc>
void list <T, Alloc>: :merge(list <T,Alloc>& x) {
iterator first1 = begin();
iterator lastl = end();
iterator first2 = x.begin();
iterator last2 = x.end();
while (firstl != last1 && first2 != last2)
if(*first2<*first){
iterator next = first2;
transfer(first1, first2, ++next);
first2 = next;
}
else
++first1;
if(first2 != last2) transfer(last 1, first2, last2);
}
template <class T, class Alloc> void list═,Alloc>: :reverse() {
if (node->next == node || link_type(node->next)->next == node) return;
jterator first= begin();
++first;
while (first!= end()) {
iterator old= first;
++first;
transfer(begin(), old, first);
}
}
template <class T, class Alloc>
void list<T, Alloc>::sort () {
if (node->next == node || link_type(node->next)->next == node)
return;
list<T,Alloc> carry;
list<T, Alloc> counter[64];
int fill = O;
while (!empty()) {
carry.splice(carry.begin(), *this, begin());
inti= O;
while(i < fill && !rount.er[i].empt.y()) {
counter [ i].merge (carry) ; carry.swap(counter[i++]);
}
carry.swap(counter[i]);
if (i == fill) ++fill;
}
for (inti= l; i < fill; +1i)
counter[i].merge(counter[i-1]); swap(counter[fill-1]);
}
4.deque
4.1 deque概述
- vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。
- 所谓双向开口,意思是可以在头尾两端分别做元素的插人和删除操作,如下图所示。
- vector当然也可以在头尾两端进行操作(从技术观点),但是其头部操作 效率奇差,无法被接受。
deque和vector的最大差异:
- 一在于deque允许于常数时间内对起头端进 行元素的插人或移除操作。
- 二在于deque没有所谓容量(capacity)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。换句话说,像vector那样“因旧空间不足而重新配置一块更大空间,然后复制元 素,再释放旧空间”这样的事情在deque是不会发生的。也因此,deque没有必要提供所谓的空间保留(reserve)功能。
虽然deque也提供Ramdon Access Iterator,但它的迭代器并不是普通指针, 其复杂度和vector不可 以道里计(稍后看到源代码),这会影响各个运算层面。因此,除非必要,我们应尽可能选择使用vector而非deque。 对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector 身上,将vector排序后(利用STLsort算法),再复制回deque。
4.2 deque的控制器
deque是连续空间(至少逻辑上看来如此), 连续线性空间总令我们联想到 array或vector,array无法成长 , vector虽可成长 , 却只能向尾端成长, 而且其所谓成长原是个假象,事实上是
- 另觅更大空间;
- 将原数据复制过去;
- 释放原空间三部曲。
如果不是vector每次配置新空间时都有留下一些余裕, 其”成长“ 假象所带来的代价将是相当高昂。
-
deque系由一段 一段的定量连续空间构成。 -
一旦有必要在deque的前端或尾端增加新空间, 便配置 一段定量连续空间, 串接在整个deque的头端或尾端。 -
deque的最大任务, 便是在这些分段的定量连续空间上, 维护其整体连续的假象,并提供随机存取的接口。 -
避开了 “重新配置、 复制、 释放” 的轮回, 代价则是复杂的迭代器架构。 -
分段连续线性空间的字面影响,可能以为deque的实现复杂度和 vector相比虽不中亦不远矣, 其实不然。 -
主要因为,分段连续线性空间, 就 必须有中央控制,而为了维持整体连续的假象,数据结构的设计及迭代器前进后退等操作都颇为繁琐。 -
deque的实现代码分量远比vector或list都多得多。 -
deque采用一块所谓的map (注意, 不是STL的map容器)作为主控。 这 里所谓map是一小块连续空间 , 其中每个元素(此处称为一个节点,node)都是 指针, 指向另 一段(较大的)连续线性空间, 称为缓冲区。 缓冲区才是deque的 储存空间主体。 -
SGI STL允许我们指定缓冲区大小, 默认值0表示将使用512 bytes缓冲区。
template<class T,class Alloc=alloc,size_t BufSiz = 0>
class deque{
public:
typedef T value_type;
typedef value_type* pointer;
...
protected:
typedef pointer* map_pointer;
protected:
map_pointer map;
size_type map_size;
...
}
把各种型别定义(为了型别安全,那其实是有必要的)整理一下,可以发现map其实是个T**,也就是说它是一个指针,所指之物又是一个指针,指向型别为T的一块空间。如下图所示:
deque的结构设计中,map和node-buffer(节点-缓冲区)的关系:
4.3 deque的迭代器
deque是分段连续空间。维持其“整体连续“假象的任务,落在了迭代器的 operator++和operator–两个运算子身上。
deque迭代器应该具备什么结构:
- 首先, 它必须能够指出分段连续空间(亦即缓冲区)在哪里。
- 其次它必须能够判断自己是否已经处于其所 在缓冲区的边缘,如果是, 一旦前进或后退时就必须跳跃至下一个或上一个缓冲区。
- 为了能够正确跳跃, deque必须随时掌握管控中心(map)。
下面这种实现方式符合需求:
template <class T, class Ref, class Ptr, size_t BufSiz>
struct _deque_iterator {
typedef deque_iterator<T, T&, T*, BufSiz> iterator;
typedef _deque_iterator<T, canst T&, constT*,BufSiz> const_iterator;
static size_t buffer_size () {return _deque_buf_size (BufSiz, sizeof (T)); }
typedef random_access_1terator_tag iterator_category;
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;
T* cur;
T* first;
T* last;
map_pointer node;
...
}
其中用来决定缓冲区大小的函数buffer_size(),调用_deque_buf_size(),后者是一个全局函数,定义如下:
inline size_t __deque_buf_size(size_t n,size_t sz)
{
return n !=0 ? n : (sz<512?size_t(512/sz):size_t(1));
}
下图所示的是deque的中控器、 缓冲区、 迭代器的相互关系:
- 假设现在我们产生 个deque, 并令其缓冲区大小为32, 于是每个缓冲区可容纳32/sizeof(i平) =4个元素。
- 经过某些操作之后, deque拥有20个元 素, 那么其begin ()和 end()所传回的两个迭代器应该如下图所示。 这两个迭代器事实上 直保持在deque内, 名为start和finish。
- 20个元素需要20/8 = 3个缓冲区, 所以map之内运用了三个节点。
- 迭代器start内的cur指针当然指向缓冲区的第 一个元素。
- 迭代器finish内的cur 指针当然指向缓冲区的最后元素(的下一个位置)。
- 注意, 最后 一个缓冲区尚有备用空间。
如果有新元素要插人于尾端, 可直接拿此备用空间来使用。
上图中deque : : begin()传回迭代器start,deque::end()传回迭代器 finish。这两个迭代器都是deque的data members。图中所示的这个deque拥有 20个int元素,以3个缓冲区储存之。每个缓冲区32bytes, 可储存8个int元素。 map大小为8(起始值),目前用了3个节点。
下面是deque迭代器的几个关键行为。由于迭代器内对各种指针运算都进行了重载操作,所以各种指针运算如加、减、前进、后退都不能直观视之。其中最关键就是:一旦行进时遇到缓冲区边缘,要特别小心,视前进或后退而定,可能需要调用set_node()跳一个缓冲区:
void set_node(map_pointer new_node) {
node= new_node;
first= *new_node;
last= first+ difference_type(buffer_size());
}
reference operator*() const{return *cur; }
pointer operator->() const { return &(operator*()); }
difference_type operator-(const self& x) const{
return difference_type(buffer_size())* (node -x.node -1) +(cur -first) + (x.last -x.cur);
}
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){
seif tmp = *this;
--*this;
return tmp;
}
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> O? 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()));
}
return *this;
}
self operator+(difference_type n) const {
self tmp = *this;
return tmp +=n;
}
self& operator-=(difference_type n) { return *this+= -n; }
self operator-(difference_type n) const {
self trnp = *this;
return trnp -= n;
}
reference operator[] (difference_type n) const { return * (*this + n); }
bool operator==(const self& x) const { return cur == x.cur; }
bool operatorl=(const self& x) const{ return ! (*this == x) ; }
bool operator<(const self& x) const{
return (node == x.node) ? (cur < x.cur) : (node < x.node);
}
4.4 deque的数据结构
deque除了维护一个先前说过的指向map的指针外,也维护start,finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一元素(的下一个位置)。此外,它当然也必须记住目前的map大小。因为一旦map所提供的节点不足,就必须重新配置更大的一块map。
template<class T, class Alloc=alloc,size_t BufSiz=0>
class deque {
public://Base types
typedef T value_type;
typedef value_type* pointer;
typedef size_t size_type;
public:
typedef __deque_iterator<T.T&,T*,BufSize> iterator;
protected:
typedef pointer* map_pointer;
protected:
iterator start;
iterator finisb;
map_pointer map;
size_type map_size;
...
}
有了上述结构,以下数个机能便可轻易完成:
public://Base accessors
iterator begin() { return start; }
iterator end() { return finish; }
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;; }
size_type max_size() const{return size_type(-1); }
bool empty() const { return finish== start; }
4.5 deque的构造与内存管理 ctor, push_back, push_front
千头万绪该如何说起?以客户端程序代码为引导,观察其所得结果并实证源代,是个良好的学习路径。下面是个测试程序,我的观察重点在构造的方式以及大小的变化,以及容器最前端的插人功能:
#include<deque>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
deque<int,alloc,32>ideq(20,9);
cout<<"size="<<ideq.size()<<endl;
for(int i=0;i<ideq.size();++i)
ideq[i] = i;
for(int i=0;i<ideq.size();++i)
cout<<ideq[i]<<'';
cout<<endl;
for(int i=0;i<3;i++)
ideq.push_back(1);
for(int i=0;i<ideq.size();++i)
cout<<ideq[i]<<'';
cout<<endl;
cout<<"size="<<ideq.size()<<endl;
ideq.push_back(3);
for(int i=O; i<ideq.size();++i)
cout<<ideq[i]<<'';
cout<<endl;
cout<<endl;
cout<<"size="<<ideq.size()<<endl;
ideq.push_front(9,9);
for(int i=O; i<ideq.size(); ++i)
cout<<ideq[i]<<'';
cout<<endl;
cout<<"size="<<ideq.size()<<endl;
ideq.push_front(98);
ideq.push_front(97);
for(int i=O; i<ideq.size(); ++i)
cout<<ideq[i]<<'';
cout<<endl;
cout<<"size="<<ideq.size()<<endl;
deque<int,alloc,32>::iterator itr;
itr = find(ideq.begin(), ideq.end(), 99);
cout<< *itr << endl;
cout<< *(itr.cur) << endl;
}
deque的缓冲区扩充操作相当琐碎繁杂, 以下将以分解操作的方式进行图解说明,程序一开始声明一个deque:
deque<int,alloc,32>ideq(20,9);
- 其缓冲区大小为32 bytes, 并令其保留20个元素空间, 每个元素初值为9。
- 为了指定deque的第三个template参数(缓冲区大小),我们必须将前两个参数 都指明出来(这是C++语法规则), 因此必须明确指定alloc (第二章)为空间 配置器。
- deque的情况如上图 (该图并未显示每个元素的初值为9)所示。
deque自行定义了两个专属的空间配置器:
protected:
typedef simple_alloc<value_type,Alloc>data_allocator;
typedef simple_alloc<pointer, Alloc> map_allocator;
并提供有一个constructor如下:
deque(int n, const value_type& value) :start(),finish(),map(0),map_size(0)
{
fill_initialize(n,value);
}
其内所调用的fill_initialize()负责产生并安排好deque的结构,并将元素的初值设定妥当:
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc,Butsize>: :fill _initialize(size_type n,const value_type& value){
creat_map_and_nodes(n);
map_pointer cur;
__STL TRY{
for (cur= start.node;cur< finish.node; ++cur)
uninitialized_fill(*cur, *cur+ buffer_size(), value);
uninitialized_fill(finish.first, finish.cur, value);
}
catch(...){
...
}
}
其中create_map_and_nodes()负责产生并安排好deque的结构:
template <class T, class Alloc,size_t BufSize>
void deque<T, Alloc, BufS ize>:: create_map_and_nodes (size_type num_elements)
{
size_type num_nodes = num_elements / buffer_size() + 1;
map_aize = max(initial_map_size (), num_nodes + 2);
map= map_allocator::allocate(map_size);
map_pointer nstart =map+ (map_size -num_nodes)/2;
map_pointer nfinish = nstart + num_nodes -1;
map_pointer cur;
_STL_TRY {
for (cur= nstart; cur<= nfinish; ++cur)
*cur = allocate_node();
}
catch(...){
...
}
start.set_node(nstart);
finish. set_node (nfinish);
start.cur= start.first;
finish.cur= finish.first+num_elements % buffer—size ();
}
接下来范例程序以下标操作符为每个元素重新设值,然后在尾端插人三个新元素:
for(int i=O; i<ideq.size(); ++i)
ideq[i] = i;
for(int i=0;i<3;i++)
ideq.push_back(i);
由于此时最后一个缓冲区仍有4个备用元素空间,所以不会引起缓冲区的再配置,此时的deque状态如下图所示: 延续上图的状态,将每个元素重新设值,并在尾部新增3个元素: 下面是push_back()函数内容:
public:
void push_back(const value_type& t){
if(finish.cur !=finish.last - 1){
construct(finish.cur,t);
++finish.cur;
}
else
push_back_aux(t);
}
现在,如果再新增一个新元素于尾端:
ideq.push_back(3);
由于尾端只剩一个元素备用空间,于是push_back()调用push_back_aux(), 先配置一整块新的缓冲区,再设妥新元素内容,然后更改迭代器finish的状态:
template <class T, class Alloc, size_t BufSize>
void deque<T,Alloc,BufSize>::push_back_aux(const value_type& t){
value_type t_copy = t;
reserve_map_at_back();
*(finish.node +1) = allocate_node();
__STL_TRY{
construct(finish.cur,t_copy);
finish.set_node(finish.node+1);
finish.cur = finish.first;
}
__STL_UNWIND(deallocate_node(*(finish.node+1)));
}
现在,deque的状态如图所示:
延续上图的状态,在尾端再加一个元素,于是引发新缓冲区的配置,同时也导致迭代器finish的状态改变,map大小为8(初始值),目前用了4个节点。
下面在deque的前端插入一个新元素: ideq.push_front(99); push_front()函数操作如下:
public:
void push_front(const,value_type& t){
if(start.cur !=start.first){
if(start.cur!= start.first){
construct(strart.cur -1,t);
--start.cur;
}
else
push_front_aux(t);
}
由于目前状态下,第一缓冲区并无备用空间,所以调用push_front_aux():
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, Bufsize>: :push_front_aux(const value_type& t)
{
value_type t_copy = t;
reserve_map at_front();
*(start.node-1) = alloc_node();
__STL_TRY{
start.set_node(start.node - l);
start.cur = start.last-1;
construct(start. cur, t_copy) ;
}
catch(...){
start.set_node(start .node + 1);
start.cur = start.first;
deallocate_node(* (start.node - 1));
throw;
}
}
该函数一开始即调用reserve_map_at_front(), 后者用来判断是否需要扩 充map, 如有需要就付诸行动。目前不需要重新整治map, 所以后继流程便配置了 块新缓冲区,并直接将节点安置千现有的map上,然后设定新元素,改变迭代器Start的状态, 如下图所示: 延续上图的状态,在最前端加上一个元素。引发新缓冲区的配置,同时也导致迭代器start状态改变。map大小为8(初始值),目前用掉5个节点。 接下来范例程序又在deque的最前端插入两个新元素:
ideq.push_front(98);
ideq.push_front (97);
这一次,由千第一缓冲区有备用空间,push_front() 可以直接在备用空间上 构造新元素,如下图所示: 延续上图的状态,在最前端再加两个元素。由于第一缓冲区尚有备用 空间,因此,直接取用备用空间来构造新元素即可。 上面的图是连环图解,充分展示了deque容器的空间运用策略。这时,什么时候map需要重新整治?这个问题判断由reserve_map_at_back() 和reserve_map_at_front() 进行操作则由reallocate_map()执行:
void reserve_map_at_back(size_type nodes_to_add=1){
if(nodes_to_add+1>map_size-(finish.node-map))
reallocate_map(nodes_to_add,false);
}
void reserve_map_front(size_type nodes_to_add=1){
if(nodes_to_add>start.node-map)
reallocate_map(nodes_to_add,true);
}
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,bool add_at_front){
size_type old_num_nodes = finish.node -start.node+ 1;
size_type new_num_nodes = old_num_nodes + nodes_to_add;
map_pointer new_nstart;
if (map_aize > 2 * new_num_nodes) {
new_nstart =map + (map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0);
if (new_nstart < start.node)
copy(start.node, finish.node + 1, new_nstart);
else
copy_backward(start.node,finish.node+1,new_nstart+old_num_nodes);
}
else{
size_type new_map_size = map_size + max(map_size,node_to_add)+2;
map_pointer new_map = map_allocator::allocate(new_map_size);
new_nstart = new_map+(new_map_size-new_num_nodes)/2 +(add_at_front ? nodes_to_add:0);
copy(start.node,finish.node +1,new_size);
map_allocator::deallocate(map,map_size);
map = new_map;
map_size = new_map_size;
}
start.set_node(new_nstart);
finish.set_node(new_nstart +old_num_nodes - 1);
{
4.6 deque的元素操作 pop_back, pop_front, clear, erase, insert
deque所提供的元素操作很多,无法一一讲解,下面介绍能够member function。 前述测试程序曾经以泛型算法find()寻找deque的某个元素:
deque<int,alloc,32>::iterator itr;
itr = find(ideq.begin(),ideq.end(),99);
当find()操作完成时,迭代器itr 状态如下图所示。下面这两个操作输出相同的结果,印证我们对deque迭代器的认识。
cout<<*itr<<endl;
cout<<*(itr.cur)<<endl;
延续上图的状态,以find()寻找数值为99的元素。此函数将传回一个迭代器,指向第一个符合条件的元素。注意,该迭代器的四个字段都必须有正确的设定。 前面展示过push_back()和push_front()的实现内容,现在例举对应的pop_back()和pop_front()为例。所谓pop是将元素拿掉。无论从deque的最前端或最尾端取元素,都需要考虑某种条件下将缓冲区释放掉:
void pop_back()
{
if(finish.cur !=finish.first){
--finish.cur;
destroy(finish.cur);
}
else
pop_back_aux();
}
template <class T,class Alloc,size_t BufSize>
void deque<T,Alloc,BufSize>::pop_back_aux(){
deallocate_node(finish.first);
finish.set_node(finish.node-1);
finish.cur = finish.last-1;
destroy(finish.cur);
}
void pop_front()
{
if(start.cur !=start.last-1){
destroy(start.cur);
++start.cur;
}
else
pop_front_aux();
}
template <class T,class Alloc,size_t BufSize>
void deque<T,Alloc,BufSize>::pop_front_aux(){
destroy(start.cur);
deallocate_node(start.first);
start.set_node(start.node+1);
start.cur = start.first;
}
下面这个例子是clear(),用来清除整个deque。请注意,deque的最初状态(无任何元素时)保有一个缓冲区,因此,clear()完成之后回复初始状态,也一样要保留一个缓冲区:
template <class T,class Alloc,size_t BufSize>
void deque<T,Alloc,BufSize>::clear(){
for (map_pointer node = start.node + 1; node < finish.node; ++node) {
destroy(*node, *node+ buffer_size());
data_allocator::deallocate(*node, buffer_size());
}
if(start.node !=finish.node){
destroy(start.cur,start.last) ;
destroy{finish.first,finish.cur);
data_allocator::deallocate(finish.first, buffer_size());
}
else
destroy(start.cur,finish.cur);
finish = start;
}
下面这个例子是erase(),用来清除某个元素:
iterator erase(iterator pos){
iterator next = pos;
++next;
difference_type index = pos - start;
if(index < (size()>>1)){
copy_backward(start,pos,next);
pop_front();
}
else{
copy(next,finish,pos);
pop_back();
}
return start + index;
}
下面例子是erase,用来清除[first,last)区间内的所有元素:
template<class T, class Alloc, size_t BufSize>
deque<T, Alloc, BufSiZe>::iterator
deque<T, Alloc, BufSize>::erase(iterator first, iterator last) {
if(first == start && last == finish){
clear();
return finsh;
}
else
{
difference_typen = last-firs;
difference_type elems_before = first-tart;
if(elems_before<(size()-n)/2){
copy_backward(start,first,last);
iterator new_start = start + n;
destroy(start,new_start);
for(map_pointer cur= start.node;cur< new_start.node;++cur)
data_allocator::deallocate(*cur, buffer_size());
start=new_start;
}
else{
copy(last, finish, first);
iterator new_finish = finish -n;
destroy (new_finish, finish);
for (map_pointer cur= new_finish.node + l; cur<= finish.node; ++cur)
data_allocator::deallocate(*cur, buffer_size());
finish = new_finish;
}
return start +elem_before;
}
}
本节要说明的最后一个例子是inser。deque为这个功能提供了许多版本, 最基础最重要的是以下版本,允许在某个点(之前)插入个元素,并设定其 值。
iterator insert(iterator position,const value_type& x){
if(position.cur == start.cur){
push_front(x);
return start;
}
else if(position.cur == finish.cur){
push_back(x);
iterator tmp=finish;
--tmp;
return tmp;
}
else{
return insert_aux(position,x);
}
}
template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::insert aux(iterator pos, const value_type& x) {
difference_type index= pos-start;
value_type x_copy = x;
if (index < s1.ze() / 2) {
push_front(front());
iterator frontl = start;
++front1;
iterator front2 = frontl;
++front2;
pos = start+index;
iterator pos1 = pos;
++pos1;
copy(front2, pos1, front1);
}
else{
push_back(back(});
iteator back1 = finish;
--back1;
iterator back2 = back1;
--back2;
pos = start + index;
copy_backward(pos,back2,back1);
}
*pos = x_copy();
return pos;
}
5.stack
5.1 stack 概述
- stack是一种先进后出(FirstIn Last Out, FILO)的数据结构。它只有一个出口,形式如下图所示。
- stack允许新增元素、移除元素、取得最顶端元素。但除了最顶端外,没有任何其它方法可以存取stack的其它元素。换言之,stack 不允许有遍历行为。
- 将元素推入stack的操作称为push,将元素推出stack的操作称为pop。
5.2 stack定义完整列表
- 以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack,是容易的。
- deque是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便轻而易举地形成一个stack。
- 因此,SGL STL便以deuqe作为缺省情况下的stack底部结构,stack的实现因而非常简单。
由于stack系以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器),因此,STL stack往往 不被归类为container(容器),而被归类为container adapter。
template <class T,class Sequence = deque<T> >
class stack{
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()const{return c.empty();}
size_type size()const { return c.size();}
reference top(){ return c.back ();}
const_reference top() const { return c.back();}
void push(const value_type& x) { c.push_back(x);}
void pop() { c.pop_back(); }
};
template <class T, class Sequence>
bool operator== (const stack<T, Sequence>& x, const stack<T, Sequence>& y)
{
return x.c == y.c;
}
template <class T, class Sequence>
bool operator< (const stack<T, Sequence>& x, const stack<T, Sequence>& y)
{
return x.c < y.c;
}
5.3 stack没有迭代器
stack所有元素的进出都必须符合 “先进后出" 的条件, 只有stack顶端的 元素, 才有机会被外界取用。 stack不提供走访功能, 也不提供迭代器。
5.4 以list作为stack的底层容器
除了deque之外, list也是双向开口的数据结构。 上述stack源代码中使 用的底层容器的函数有empty, size, back, push_back, pop_back, 凡此种种, list都具备。 因此, 若以list为底部结构并封闭其头端开口, 一样能够轻易形成一个stack。 下面是做法示范。
#include<stack>
#include<list>
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
stack<int,list<int>>istack;
istack.push(1);
istack.push(3);
istack.push(S);
istack.push(7);
cout<<istack.size()<<endl;
cout<<istack.top()<<endl;
istack.pop(); cout << istack.top()<< endl;
istack.pop(); cout << istack.top() << endl;
istack.pop(); cout << istack.top() << endl;
cout<<istack.size() << endl;
}
6.queue
6.1 queue概述
- queue是一种先进先出(First In First Out, FIFO)的数据结构。它有两个出 口,形式如下图所示。
- queue允许新增元素、移除元素、从最底端加入元素、 取得最顶端元素。但除了最底端可以加入、最顶端可以取出外,没有任何其它方法可以存取queue的其它元素。换言之,queue不允许有遍历行为。
- 将元素推入queu的操作称为push,将元素推出queue的操作为pop。
6.2 queue定义完整列表
- 以某种既有容器为底部结构,将其接口改变,使其符合先进先出"的特性, 形成一个queue,是很容易做到的。
- deque是双向开口的数据结构,若以deque为 底部结构并封闭其底端的出口和前端的入口,便轻而易举地形成了一个queue。
- 因此,SGI STL便以deque作为缺省情况下的queue底部结构,queue的实现因 而非常简单。
- 由于queue系以底部容器完成其所有工作,而具有这种“修改某物接口,形 成另一种风貌”之性质者,称为adapter(配接器),因此,STLqueue往往不被归类为container(容器),而被归类为container adapter。
template<class T, class Sequence"'deque<T> >
class queue{
friend bool operator==_STL_NULL_TMPL_ARGS (const queue& x, const queue& y);
friend bool operator< _STL_NULL_TMPL_ARGS (const queue& x, const queue& y);
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 { returnbc.size(); }
reference front() { returnbc.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(x);}
void pop(){ c.pop_front(); }
};
template<class T, class Sequence>
bool operator== (const queue<T,Sequence>& x, cons t queue<T, Sequence>& y)
{
return x.c == y.c;
}
template <class T, class Sequence>
bool operator< (const queue<T, Sequence>& x, const queue<T, Sequence>& y)
{
return x.c<y.c;
}
6.3 queue没有迭代器
queue所有元素的进出都必须符合 “先进先出 " 的条件, 只有queue顶端的 元素, 才有机会被外界取用。 queue不提供遍历功能, 也不提供迭代器。
6.4 以list作为 queue的底层容器
除了deque之外,list也是双向开口的数据结构。上述queue源代码中使用 的底层容器的函数有empty, size, back, push_back, pop_back, 凡此种种, list都具备。 因此,若以list为底部结构并封闭其头端开口, 一样能够轻易形成一个queue。 下面是做法示范。
#include <queue>
#include <list>
#include <iostream>
#include <algorithm>
using namespace std;
int main(}
{
queue<int,list<int> >iqueue;
iqueue.push(1);
iqueue.push(3);
iqueue.push(5);
iqueue.push(7);
cout<< iqueue.size()<< endl;
cout << iqueue. front()<< endl;
iqueue.pop(); cout << iqueue.front() << endl;
iqueue.pop(); cout << iqueue.front()<< endl;
iqueue.pop(); cout << iqueue.front() << endl;
cout << iqueue.size() << endl;
}
7.heap(隐私表述,implicit representation)
7.1 heap概述
- heap并不归属于STL容器组件,扮演priority queue 的助手。
- priority queue允许用户以任何次序将任何元素推入容器内,但取出时一定是从优先权最高(也就是数值最高)的元素开始取。
- binary max heap正是具有这样的特性,适合作为priority queue的底层机制。
- 前面使用的list作为priority queue的底层机制,元素插入操作可享常数时间。但是要找到list中的极值,却需要对整个list 进行线性扫描。也可以改变做法,让元素插入前先经过排序这一关,使得list 的元素值总是由小到大(或由大到小),但这么一来,收之东隅却失之桑榆:虽然取得极值以及元素删除操作达到最高效率,可元素的插入却只有线性表现。
- 其它的做法是以binary search tree (如后面的RB-tree)作为priority queue的底层机制。这么一来,元素的插入和极值的取得就有O(logN)的表现。但小题大做,一来binary search tree的输人需要足够的随机性, 二来binaryvsearch tree并不容易实现。
- priority queue的复杂度,最好介于 queue和binar ysearch tree之间,才算适得其所。binary heap便是这种条件的选择。
- 所谓binar yheap就是一种complete binary tree (完全二叉树) 也就是说, 整棵binary tree除了最底层的叶节点(s)之外,是填满的,而最底层的叶节点(s)由左至右又不得有空隙。下图所示的是一个complete binary tree。
一个完全二叉数及其array表达式:
- complete binary tree整棵树内没有任何节点漏洞,这带来一个极大的好处:我们可以利用array来储存所有节点。
- 假设动用一个小技巧3,将array的#0元素保留(或设为无限大值或无限小值),那么当complete binary tree中的某个节点位于array的li处时,其左子节点必位于array的2i处,其右子节点必位于array的 2i+1处,其父节点必位于"i/2"处(此处的"!’'权且代表高斯符号,取其整数)。
- 通过这么简单的位置规则,array可以轻易实现出complete binary tree。这种以 array表述tree的方式,我们称为隐式表述法(implicit representation)。
这么一来,我们需要的工具很简单了:一个array和一组heap算法(用来插入元素、删除元素、取极值,将某一个整组数据排列成一个heap)。array的缺点是无法动态改变大小,而heap却需要这项功能,因此,以vector代替array是更好的选择。
- 根据元素排列方式,heap可分为max-heap和而min-heap两种。
- 前者每个节点的键值(key)都大于或等于其子节点键值,后者的每个节点键值(key)都小于或等千其子节点键值。
- max-heap的最大值在根节点,并总是位于底层array 或vector的起头处;
- min-heap的最小值在根节点,亦总是位于底层array或 vector的起头处。
- STL供应的是max-heap,因此,以下我说heap时指的是max-heap。
7.2 heap算法
①.push_heap算法
下图所示的是push_heap算法的实际操演情况。为了满足complete binary tree的条件,新加入的元素一定要放在最下一层作为叶节点,并填补在由左至右的第一个空格,也就是把新元素插入在底层vector的end()处。
新元素是否适合于其现有位置呢?为满足max-heap的条件(每个节点的键值都大于或等于其子节点键值),我们执行一个所谓的percolate up(上溯)程序:将新节点拿来与其父节点比较,如果其键值(key)比父节点大,就父子对换位置。如果一直上溯,直到不需要对换或根节点为止。
下面便是push_heap算法的实现细节。 该函数接受两个迭代器, 用来表现一 个heap底部容器(vector)的头尾, 并且新元素已经插入到底部容器的最尾端。 如果不符合这两个条件,push_heap的执行结果未可预期。
template <class RandomAccessIterator>
inline void push_heap(RandomAccessiterator first,RandomAccessiterator last){
_push_heap_aux(first, last, distance_type(first),
value_type(first));
}
template <class RandomAccessIterator, class Distance, class T> inline void __push_heap_aux(RandomAccessiterator first, RandomAccessiterator last, Distance*, T*) {
__push_heap(first, Distance((last - first)- 1), Distance(O),
T(*(last-1)));
}
template <class RandomAccessiterator, class Distance, class T> void __push_heap(RandomAccessiterator first, Distance holeindex,Distance topIndex,Tbvalue){
Distance parent = (holeindex - 1)/2;
while (holeindex >topindex && *(first + parent) < value) {
*(first+holeIndex) = *(first + parent);
holeIndex = parent;
parent = (holeIndex-1)/2;
}
*(first + holeindex) = value;
}
②.pop_heap算法
- 下图所示的是pop_heap算法的实际操演情况。既然身为max-heap,最大
值必然在根节点。 - pop操作取走根节点(其实是移至底部容器vector的最后一个元素)之后,为了满足complete binary tree的条件,必须将最下一层最右边的叶节点拿掉,现在我们的任务是为这个被拿掉的节点找个适当的位置。
- 为满足max-heap的条件(每个节点的键值都大于或等于其子节点键值),我们执行一个所谓的percolate down (下溯)程序:将根节点(最大值被取走后,形成一个“洞”)填入上述哪个失去生存空间的叶子节点值。再将它拿来和其两个子节点比较键值(key),并与较大节点对调位置。
- 如此一直下放,直到这个“洞”的键值大于左右两个子节点或直到下放至叶子节点为止。
下面便是pop_heap算法的实现细节。该函数接受两个迭代器,用来表现一 个heap底部容器(vector)的头尾。如果不符合这个条件,pop_heap的执行结 果未可预期。
template <class RandomAccessIterator>
inline void pop_heap(RandomAccessIterator first,RandomAccessIterator last) {
_pop_heap_aux(first, last, value_type(first));
}
tempalte<class RandomAccessIterator first,RandomAccessIterator last,T*){
__pop_heap(first,last-1,last-1,T(*(last-1)),distance_type(first));
}
template <class RandomAccessiterator, class T, class Ditance>
inline void __pop_heap(RandomAccessiterator first, RandomAccessiterator last,RandomAccessiterator result,T value,Distance*){
*result = *first;
_adjust_heap(first, Distance(O), Distance(last -first),value};
}
template<class RandomAccessiterator, class Distance, class T>
void __djust_heap(RandomAccessiterator first, Distance holeindex,Distance len,T value){
Distance topindex = holeindex;
Distance secondChild = 2 * holeindex + 2;
while (secondChild < len) {
if (*(first+ secondChild) <*(first+ (secondChild -1)))
secondChild--;
(first+ holeindex) =*(first+ secondChild);
holeindex = secondChild;
secondChild = 2*(secondChild+1);
}
if(secondChild == len){
*(first + holeIndex) = *(first +(seconedChild -1));
holeIndex = secondeChild - 1;
}
__push_heap(first,holeIndex,topIndex,value);
}
注意,pop_heap之后,最大元素只是被置放于底部容器的最尾端,尚未被取走。如果要取其值,可使用底部容器(vector)所提供的back()操作函数。如果 要移除它,可使用底部容器(vector)所提供的pop_back()操作函数。
③. sort_heap 算法
既然每次pop_heap可获得heap中键值最大的元素,如果持续对整个heap 做pop_heap操作,每次将操作范围从后向前缩减个元素(因为pop_heap会把键值最大的元素放在底部容器的最尾端),当整个程序执行完毕时,我们便有一个递增序列。下图所示的是sort_heap的实际操演情况。
下面是sort_heap算法的实现细节。该函数接受两个迭代器,用来表现个 heap底部容器(vector)的头尾。如果不符合这个条件,sort_heap的执行结果 未可预期,注意,排序过后,原来的heap就不再是一个合法的heap了。
template<class RandomAccessiterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last){
while(last-first>1)
pop_heap(first,last--);
}
sort_heap算法:不断对heap进行pop操作,便可达到排序效果
④. make_heap算法
这个算法用来将一段现有的数据转化为一个heap。 其主要依据就是前面提 到的complete binary tree的隐式表述(implicit representation) 。
template <class RandomAccessiterator>
inline void make_heap(RandomAccessiterator first,RandomAccessiterator last){
__make_heap(first, last, value_type(first), distance_type(first));
}
template <class RandomAccessiterator, class T, class Distance> void _make_heap(RandomAccessIterator first, RandomAccessIterator last,T*,Distance*){
if (last-first< 2) return;
Distance len = last - first;
Distance parent = (len - 2)/2;
while (true) {
__adjust_heap(first,parent,len,T(*(first+parent)));
if(parent == 0)return;
parent--;
}
}
7.3 heap没有迭代器 heap的所有元素都必须遵循特别的(complete binary tree)排列规则,所以heap不提供遍历功能,也不提供迭代器。
7.4 heap测试实例
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
{
int ia[9] = {0,1,2,3,4,8,9,3,5};
vector<int>ivec(ia,ia+9);
make_heap(ivec.begin(),ivec.end());
for(int i=0;i<ivec.size();++i)
cout<<ivec[i]<<'';
cout<<endl;
ivec.push_back(7);
push_heap(ivec.begin(),ivec.end());
for(int i=0;i<ivec.size();++i)
cout<<ivec[i]<<' ';
cout<<endl;
pop_heap(ivec.begin(),ivec.end());
cout<<ivec.back()<<endl;
ivec.pop_back();
for(int i=0;i<ivec.size();++i)
cout<<ivec[i]<<' ';
cout<<endl;
sort_heap(ivec.begin(),ivec.end());
for(int i=0;i<ivec.size();++i)
cout<<ivec[i]<<' ';
cout<<endl;
}
{
int ia[9] = {0,1,2,3,4,8,9,3,5};
make_heap(ia,ia+9);
sort_heap(ia,ia+9);
for(int i=0;i<9;++i)
cout<<ia[i]<<'';
cout<<endl;
make_heap(ia,ia+9);
pop_heap(ia,ia+9);
cout<<ia[8]<<endl;
}
{
int ia[6] = {4,1,7,6,2,5};
make_heap(ia,ia+6);
for(int i=0;i<6;++i)
cout<<ia[i]<<' ';
cout<<endl;
}
}
8.priority_queue
8.1 priority_queue概述
-
priority_queue 是一个拥有权值观念的 queue, 它允许加入新元素、 移除旧元素、 审视元素值等功能。 由于这是一个 queue, 所以只允许在底端加入元素, 并从顶端取出元素,除此之外别无其它存取元素的途径。 -
priority_queue带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。 权值最高者, 排在最前面。 -
缺省情况下 priority_queue 系利用 一个 max-heap 完成, 后者是一个以 vector 表现的 complete binary tree。 max-heap 可以满足 priority_queue 所需要的 "依权值高低自动递增排序” 的特性。
8.2 priority_queue定义完整列表
- 由于 priority_queue 完全以底部容器为根据再加上 heap 处理规则, 所
以其实现非常简单。缺省情况下是以 vector 为底部容器。 - queue 以底部容器完成其所有工作。 具有这种 “修改某物接口, 形成另一种风貌” 之性质者,称为 adapter (配接器)。
- 因此, STL priori ty_queue 往往不被归类为 container (容器), 而被归类为 container adapter。
template <class T, class Sequence= vector<T>,class Compare = less<typename Sequence::value_type> >
class priority_quene{
public:
typedef typename Sequence::value_type value_type;
typedef typename Sequence::size_typee size_type;
typedef typename Sequence::reference reference;
typedef typename Sequence::const_reference const_reference;
protected:
Sequence c;
Compare comp;
public:
priority_queue():c() {}
explicit priority_queue(const Compare& x) : c(),comp(x) {}
template <class Inputlterator>
priority_queue(Input Iterator 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, Inputlterator last):c(first,last){make_heap(c.begin(),c.end(),comp);}
bool empty()const{return c.empty();]
size_type size()const{return c.size();}
const_reference top() const{return c.front();}
void push(const value_type& x){
__STL_TRY{
c.push_back(x);
push_heap(c.begin(), c.end(), comp);
}
__STL_UNWID(c.clear());
}
void pop(){
__STL_TRY{
pop_heap(c.begin(), c.end(), comp);
c.pop_back();
}
__STL_UNWIND(c.clear());
}
};
8.3 priority_queue没有迭代器
priority_queue的所有元素,进出都有一定的规则,只有queue顶端的元素(权值最高者),才有机会被外界取用。priority_queue不提供遍历功能,也不提供迭代器。
8.4 priority_queue测试实例
#incude<queue>
#include<istream>
#include<algorithm>
using namespace std;
int main(){
{
int ia[9] = {0,1,2,3,4,8,9,3,5};
priority_queue<int>ipq(ia,ia+9);
cout<<"size="<<ipq.size()<<endl;
for(int i=0;i<ipq.size();++i)
cout<<ipq.top<<' ';
ipq.pop();
}
cout<<endl;
}
9.slist
9.1 slist概述
- STL list是个双向链表(double linked list)。SGI STL另提供了一个单向链表(single linked list) , 名为slist。 这个容器并不在标准规格之内, 不过多做些剖析, 多看多学些实现技巧也不错。
- slist和list的主要差别在于,前者的迭代器属于单向的Forward Iterator,
后者的迭代器属于双向的Bidirectional Iterator。 为此,slist的功能自然也就受到许多限制。不过,单向链表所耗用的空间更小,某些操作更快,不失为另一种选择。 - list和list共同具有的个相同特色是, 它们的插入(insert)、 移除 (erase)、 接合(splice)等操作并不会造成原有的迭代器失效(当然啦, 指向被 移除元素的那个迭代器, 在移除操作发生之后肯定是会失效的)。
- 根据STL的习惯,插入操作会将新元素插入于指定位置之前,而非之后。然而作为个单向链表,slist没有任何方便的办法可以回头定出前个置,因此它必须从头找起。换句话说,除了slist起点处附近的区域之外,在其它位置 上采用insert或erase操作函数,都属不智之举。这便 是slist相较于list之下的大缺点。
- 为此, slist特别提供了insert_after()和erase_after ()供灵活运用. 基于同样的(效率)考虑,slist不提供push_back(),只提供push_front()。因此slist的元素次序会和元素插入进来的次序相反。
9.2 slist的节点
slist节点和其迭代器的设计,架构上比lsit复杂许多,运用继承关系,因此在型别转换上有复杂的表现。这种设计方式在第5章RB-tree将再一次出现。下图概述了slist节点和其迭代器的设计结构:
struct __slist_node_base
{
__slist_node_base* next;
};
tenpalte<class T>
struct __slist_node:public __slist_node_base
{
T date;
};
inline __slist_node_base* __slist_make_link(
__slist_node_base* prev_node;
__slist_node_base* new_node;
{
new_node->next = prev_node->next;
prev_node->next = new_node;
return new_node;
}
inline size_t __slist_size(__slist_node_base* node){
size_t result = 0;
for(;node !=0;node = node->next)
++result;
return result;
}
9.3 slist的迭代器
slisst迭代器如下图:
//单向链表的迭代器结构:
struct __slist_iterator_base
{
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef forward_iterator_tag iterator_category;
__slist_node_base* node;
__slist_iterator_base(__slist_node_base* x):node(x){}
void incr(){node = node->next;}
bool operator==(const __slist_iterator_base& x)const{
return node==x.node;
}
bool operator !=(const __slist_iterator_base& x)const{
return node !=x.node;
}
};
template<class T,class Ref,class Ptr>
struct __slist_iterator:public __slist_iterator_base
{
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef __slist_node<T>list_node;
__slist iterator(list_node* x):slist_iterator_base(x) {}
__list_iterator() : _slist_iterator_base(O) {}
__list_iterator(const iterator x):__slist_iterator_base(0){}
reference operator*() const return ((list_node*) node)->data; } pointer operator->() const { return &(operator*()); }
self& operator++()
{
incr();
return *this;
}
self operator++(int)
{
incr();
return tmp;
}
};
注意,比较两个slist迭代器是否等同时(例如我们常在循环中比较某个迭 代器是否等同于slist.end()),由__slist_iterator并未对operator== 施重载,所以会调用__slist_iterator_base::operator==。根据其中之定义, 我们知道,两个slist迭代器是否等同,视其_slist_node_base*node是否 等同而定。
9.4 slist的数据结构
template<class T, class Alloc=alloc>
class slist
{
public:
typedef T value_type;
typedef value_type* pointer;
typedef const value_type*const__pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef size_t size_type;
typedef ptrdiff_tdifference_type;
typedef _slist_iterator<T, T&, T*> iterator;
typedef _slist_iterator<T,const T&, const T*> const_iterator;
private:
typedef _slist_node<T>list_node;
typedef_slist_node_base list_node_base;
typedef_slist_iterator_base iterator_base;
typedef simple_alloc<list_node, Alloc> list_node_allocator;
static list_node* create_node(constvalue_type&x) {
list_node* node = list_node_allocator::llocate();
__STL_TRY{
construct(&node->data,x);
node->next = 0;
}
_STL_UNWIND(list_node_allocator::deallocate(node));
return node;
}
static void
private:
list_node_base head;
public:
silst(){head.next = 0;}
~slist(){clear();}
public:
iterator begin() { return iterator((list_node*)head.next); }
iterator end() { return iterator(O); }
size type size() const { return _slist_size(head.next);}
bool empty() const { return head.next == O; }
void swap(slist& L)
{
list_node_base* tmp = head.next;
head.next = L.head.next;
L.head.next = tmp;
}
public:
reference front() { return ((list_node*) head.next}->data; }
void push_front (const value_type& x) {
__slist_make_link(&head,create_node(x));
}
void pop_front(){
list_node* node = (list_node*) head.next;
head.next = node->next;
destroy_node (node) ;
}
...
};
9.5 slist的元素操作
#include<istream>
#include<slist>
#include<algorithm>
using namespace std;
int main()
{
int i;
slist<int> islist;
cout<<"size="<<islist.size()<<endl;
islist.push_front(9);
islist.push_front(1);
islist.push_front(2);
islist.push_front(3);
islist.push_front(4);
cout<<"size="<<islist.size()<<endl;
slist<int>::iterator ite = islist.begin();
slist<int>::iterator ite2 = islist.end();
for(;ite !=ite2;++ite)
cout<<*ite<<'';
cout<<endl;
ite = find(islist.begin(),islist.end(),1);
if(ite!=0)
islist.insert(ite,99);
cout<<"size="<<islist.size()<<endl;
cout<<*ite<<endl;
ite=islist.begin();
ite2=islist.end();
for(;ite !=ite2;++ite)
cout<<*ite<<'';
cout<<endl;
ite = find(islist.begin(),islist.end(),3);
if(ite!=0)
cout<<*(islist.erase(ite))<<endl;
ite=islist.begin();
ite2=islist.endl;
for(;ite !=ite2;++ite)
cout<<*ite<<'';
cout<<endl;
}
首先依次序把元素9,1,2,3,4插入到slist,实际把结呈现如下图所示:
下面搜寻元素1,并将新元素99插入进去,如下图所示:注意,新元素被插入在插入点(元素1)的前面本身后面:
在下来搜寻元素3,并将元素移除,如下图所示: 如果你对于三图中的end()的画法很奇怪,说明一下,注意,练习程序中一再以循环遍历整个slist,并以迭代器是否相等于slist.end()作为循环结束条件,这其中有一些容易疏忽的地方,调用end()企图做出一个指向尾部(下一个位置)的迭代器时,STL源码这么进行:
iterator end() {return iterator(0);}
这会因为源代码中如何下的定义:
typedef __slist_iterator<T,T&,T*>iterator;
而形成这样的结果:
__slist_iterator<T,T&,T*>(0);
从而因为源代码中如下定义:
__slist_iterator(list_node* x) : __slist_iterator_base(x){}
而导致基础的构造:
__slist_iterator_base(0);
并因为源代码这样的定义:
struct __slist_iterator_base
{
__slist_node_base* node;
__slist_iterator_base(__slist_node_base* x) :node(x){}
...
};
导致:
node(0)
因此,上面三个图皆以下图左侧的方式表现end(),它和下图右侧的迭代器截然不同。
|