通用容器

来源:互联网 发布:阿里云rds 编辑:程序博客网 时间:2024/04/28 01:53

 

通用容器
STL是一个容器集,容器又是对象的集合(它里面持有对象)
1.      容器和迭代器
容器可以根据它里面对象的需要自行扩展,我们在使用容器时,不需要知道它里面要放多少个对象,也不需要知道容器里的处理细节,只需要定义一个容器对象,然后由容器来处理全部细节。不同的容器它们的接口类型和外部行为有所不同,它们处理相同操作所耗费的效率也不同。譬如:vector遍历元素或访问元素很方便,但是要在其中插入元素就很耗效率;而使用list要插入一个元素就很方便,但随机访问一个元素却比vector效率低。这些操作依赖于这些序列的底层结构。不同容器的关键不同之处在于它们在内存中存储对象的方式和向用户提供的操作。当我们使用时可以根据不同的需进行选择。
为了灵活的访问容器中的元素,引入迭代器(iterator)。迭代器是为了实现通用而做的抽象。使用迭代器可以返回迭代器所指的元素给迭代器的使用者。它可以灵活的遍历容器中的每一个元素。也容许同时存在多重迭代器。通过迭代器,容器可以被看作一个序列。迭代器操作与容器的操作是分离开的,任何一方的变动都不会影响到另一方。通过迭代器可以读取元素,也可以给元素赋值(改变元素值)。
2.      概述
容器分为三大类,每一类有分为几个类型:

分类
容器
序列容器
vector, list, deque
适配器容器
queue, stack, priority_queue
关联式容器
set, map, multiset, multimap

序列容器只是将它们的元素线性地组织起来,是最基本的容器类型。
适配器容器可以在序列容器需要的时候为它们附上某些特殊的属性(如:队列或栈的抽象建立模型)。
关联式容器则是基于关键字来组织它们的数据,并允许快速的检索那些数据。
vectorlistdeque的区别:
1)         vector是一个允许快速随机访问其中元素的线性序列,它随机访问元素的速度快,但是向其中插入元素的速度慢,不支持push_front()操作;
list
是一个双向链表,要移动它的元素所需花费的代价很高,但是很容易向其中任何地方插入元素;
deque
是一个双端队列,也可以用几乎和vector一样快(但是会比vector稍慢)的速度随机访问其中的元素,但是它扩展资源的速度要快很多,而且可以很容易的在它两端加入新元素。
2)         vectordeque允许使用检索操作符[ ],但list不允许。
3)         它们都提供了相应的类型的迭代器来访问它的元素。
set里面存入的是不重复的元素,且放入这些元素时会自动对它们排序。
容器中所持有的是存入对象的拷贝,并根据需要扩展它们的资源。所以这些对象必须有可访问的拷贝构造函数和可访问的赋值操作符。对于在容器里所存对象堆空间的分配管理需要用户自己做。譬如:当容器所持有的对象被销毁之后,容器并不会自动destroy它所持有的相应指,而需用户自己去做。对于解决这种问题,最容易和最安全的方法就是使用智能指针(smart pointer)
同一个对象,可以有不至一个容器里的指针指向它。
1) 字符串容器
2) STL容器继承
继承自容器的类的对象 也具有那个容器的属性和行为。
3.      更多迭代器
<ContainerType>::iterator;
<ContainerType>::const_iterator
如果一个容器是const(常)容器,那么它的迭代器也是const()迭代器,也就是不允许更换这些迭代器所指向的元素(因为相应的运算符都是const的)。
所有迭代器都++可以前向移动,也可以使用 != == 对迭代器进行比较。
可以通过使用解析运算符(operator *)来取得迭代器当前所指向的容器元素。
如果it是一个可遍历容器的迭代器,且f()是该容器所持有的对象的成员函数,那么可以用(*it).f()it->f() 访问((容器所包含的类型的)对象的)这个成员函数。但是要注意如下用法:
template<class Cont, class PtrMemFun>
void apply(Cont& c, PtrMemFun f) {
 typename Cont::iterator it = c.begin();
 while(it != c.end())
 {
  //(it->*f)();//将当成it使用操作符->* ,但是在迭代器类中并没有提供->* 这个操作符。
  //(it->(*f))();//error C2039: 'it' : is not a member of 'Z'
      ((*it).*f)(); // Alternate form
  
      ++it;
 }
}
1) 可逆容器中的迭代器
可逆容器的迭代器可用来反向遍历容器,有成员函数rbegin()用于产生一个选择了容器末尾的迭代器reverse_iteratorrend()用于产生一个选择了容器超越起始的迭代器reverse_iterator。对所有的容器调用rbegin()rend()可得到reverse_iterator对象。如果容器是const的,那么rbegin()rend()产生的迭代器是const_reverse_iterator
2) 迭代器的种类
为什么要对迭代器划分种类?因为:一使用某些内置的迭代器类型或创建自己的迭代器时,迭代器的种类就很重要;二使用STL算法时,每种算法对其迭代器都有使用场合的要求。在创建用户自己的可重用的算法模板时,这些种类知识尤其重要,因为自定义的算法所需要的迭代器种类决定了该算法的灵活性。如果只要求最基本的迭代器类型(输入或输出迭代器),则这种算法可以适用于任何场合(如copy())。
一个迭代器类的种类由一个迭代器的层次结构标记类进行标识,类名和迭代器的种类相符合,且它们之前的派生层次结构反映了它们之间的关系:
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag{} : public input_iterator_tag{};
struct bidirectional _iterator_tag{} : public forward_iterator_tag{};
struct random_access_iterator_tag{} : struct bidirectional_iterator_tag{};
(注:因为前向迭代器forward_iterator需要使用超越末尾的迭代器值,而输出迭代器output_iterator总是假定它总是可以解析的,可是保证不把一个超越末尾的迭代器值传给一个需要使输出迭代器的算法中是很重要的,所以,forward_iterator只从input_iterator继承而不继承output_iterator.)
为了提高效率,有些算法为不同的迭代器提代不同的实现。
下面按行为功能由严格到最强大的顺序来分别讨论看看具体划分为哪些种类以及它们分别有什么性能:
输入迭代器:只读,一次传递
可以对读操作的结果进行解析(一个值只解析一次),然后前向移动,可以用与超越末尾的值比较判断是否结束。为输入迭代器的预定义实现只有istream_iteratoristreambuf_iterator,用于从一个输入流istream中读取。
输出迭代器:只写,一次传递
可以对写操作的结果进行解析(一个值只解析一次),然后前向移动,没有使用超越末尾来的值结束的概念。为输出迭代器的预定义实现只有ostream_iteratorostreambuf_iterator,用于从一个输出流ostream中读取。
前向迭代器:多次读/
前向迭代器包括输入/输出迭代器的所有功能,不同的是它可以对一个迭代器所指定位置多次解析(也就是可以对迭代器所指向的值多次读写)。它也是只能前向移动。没有专为前向迭代器预定义的迭代器。
双向迭代器:operator--
它具有前向迭代器的所有功能,另外还具有后向移动的功能。由list返回的迭代器都是双向的。
随机访问迭代器:类似于一个指针
它具有双向迭代器的所有工轻盈,再加上一个指针所具有的功能,除了不具有一种空(null)迭代器和空指针对应。它可以像一个指针一样进行任何操作,可以使用operator[ ]进行索引,可以加若干值指向另一位置(向前/后移动),可以运用比较操作符在迭代器之间进行比较。
3) 预定义迭代器
front_inserter()用一个容器对象作参数,产生一个调用push_front进行赋值的迭代器
back_inserter()用一个容器对象作参数,产生一个调用push_back进行赋值的迭代器.
inserter()是插入而不是压入一个元素,再次替代operator=,它在插入之前需要知道要插入位置的迭代器
如:int a[] = { 1, 3, 5, 7, 11, 13, 17, 19, 23 };
deque<int> di;
vector<int> vi;//不支持push_frontpush_back.
copy(a, a + sizeof(a)/sizeof(deque), front_inserter (di)); //di{ 23, 19,17, 13, 11, 7, 5, 3, 1}
di.clear();//清空di
copy(a, a + sizeof(a)/sizeof(deque),back_inserter(di));   //di{1, 3, 5, 7, 11, 13, 17, 19, 23 }
copy(a,a + sizeof(a)/(sizeof(Cont::value_type)*2),inserter(ci,it)); //di{1,3,5,1,3,5,7,7,11,13,17,19,23}
l     更多的流迭代器
输入流迭代器istream_iterator有两个构造函数,一个获得输入流istream并且产生一个实际读取的迭代器对象,另一个是默认构造函数,用于产生一个作为超越末尾标记的迭代器对象。
输入流迭代器会丢失一些空格、回车、tab之类的空白字符,所以平时使用更多的是输入输出流缓冲迭代器,除非你不用考虑空白字符是否会丢失的问题。另外,istream::operator>>每次操作会增加较多开销。
l     操作从未初始化的存储区
raw_storage_iterator <tn1, tn2>是一个输出迭代器,可以用来操作操作未初始化的存储区。
它有两个模板参数:tn1是输出迭代器类型,tn2是所存储对象类型,输出迭代器指向这块未初始化的存储区。它提供的算法使结果存放到未经初始化的内存区。
接口:它的构造函数有一个指向原始(某未初始化)内存储区的迭代器(典型的指针),operator=将一个对象分配给那个未初始化的原始内存。
注意:原始内存区类型必须与所创建的对象类型相同(可以在创建时进行强制类型转换)。必须显示调用析构函数进行清理工作,也允许在操作容器期间每次删除一个对象。Delete表达式中的静态指针类型必须与new表达式中分配的类型相同。
4.      基本序列容器:vector、list和deque
所有基本序列容器都是完全按照存入去时的顺序持有对象。但是,不同的容器进行某些操作时的效率是不同,所以,我们在使用时要根据不同的操作需要选择合适的容器。
1) 基本序列容器的操作概述
都是可逆容器。
有多个构造函数,用默认构造函数创建的容器对象是空的;
可以使用赋值成员函数operator=和两种类型的assign()assign(it1,it2); assign(value_count,value);)对容器赋值;
可以用resize()重置容器大小;
可以用insert()在容器中插入一个或一组元素;
可以使用resverse_iterator反向读取容器中的元素;
可以用erase()的两个不同版本来清除序更中间的一个元素或一组元素;
可以用clear()来清空容器。
可以用++或——把iterator前向或后向移动一个位置,因为vectordeque可以产生随机访问迭代器,所以它们还可以使用operator+operator-来一次移多个位置,但list就只能一次移一个位置。
它们三个都支持push_back()pop_back()listdeque还支持push_front()pop_front(),但vector不支持push_front()pop_front()
成员函数swap()可以相互交换两个(持有相同类型对象的)容器的所有东西,也就高效的交换了容器本身。还有可用于交换两元素的非成员函数swap(),还有用于通于迭代器交换同一容器内两个元素的iter_swap算法。
下面将分别具体讨各类型序列容器的特点。
2) 向量(vector)
vector模板有点像数组,具有数组风格的快速索引,还可以动态地扩展。它可以快速索引和迭代,也可以在最后一个元素之后新增加元素,但不能在中间或前面插入元素。
l     已分配存储区溢出引起的问题
当新增元素而存储空间不够时,就会(1)在其它位置重新分配更大的新存储空间,(2)用拷贝构造函数把原位置的所有元素都拷贝到新的存储空间中,(3)调用析构函数把原位置的元素销毁,(4)释放原位置的内存。这样会引起一些副作用。
可以用reserve()vector预先分配足够大的空间,但是它与用vector构造函数的第一个参数来指定分配空间大小的做法是有区别的,后者是使用元素类型的默认构造函数来初始化元素,reserve()只是预分配空间,但并没调用构造函数初始化,且用size()取得值为0,也就是仍是空容器。有些操作会引起迭代器无效,譬如resize()。在进行一些迭代器无效的操作之后,如果要使用迭代器,需重新置迭代器。
由此可见,选择使用 “vector”的最安全的方法是:一次性填入所有元素,然后在程序的另一处只使用它而不再加入新元素。
l     插入和删除元素
使用vector最有效的条件是:
1) 在开始时用reserver()分配了正确数量的存储构,vector绝不再重新分配存储区;
2) 仅仅在序列的后端添加或者删除元素。
3) 双端队列(deque
dequevector的区别就在于:
vector的存储区必须是连续的,而deque的存储区不必是连续的存储块;
deque也有vector的所有操作,比vector多了push_frontpop_front操作;
在随机访问元素时dequevector稍慢,但是在插入元素需要重新分配存储区时deque不需要复制并销毁原有元素,插入和删除元素deque都比vector有效率的多。
 
已配置存储区溢出的引起的问题
vector不同,当deque所用存储区不够用时,它会根据当前缺少就只分配多少的一个新存储区,原有的存储区和元素都不变,也就不会有额外的拷贝构造和析构发生,不过它用于保存数据块索引信息的存储区有可能需要重新分配。在deque中间插入元素比vector更麻烦,但代价不大。
 
因为deque的存储管理方式,在deque两端添加元素,不会引起现有迭代器失效。
再次说vector用于预先知道容器中要存入的元素数目的情况比较合适。
当不确定将被存入容器中的元素数目时,用deque优于vector
只需很小改动即可以把vector转用deque
总之,在以下情况下使用deque是最好的:
1) 从序列的两端插入或删除元素;
2) 合理的快速遍历容器元素;
3) 使用operator[ ]相当快速的随机访问。
4) 序列容器间的转换
序列容器都有一个由两个迭代器作参数的双参数构造函数和一个将数据读入现存容器中的assign()成员函数,用它们来从一个序列容器转换为另一个序列容器是很容易的。它只是把那些对象拷贝构造到新容器中。
5) 被检查的随机访问
vectordeque都提供了两个随机访问函数:索引操作符operator[ ]和判定是否到了容器边界的的函数at(),是边界则返回true,否则返回false,超出边界则抛出一个异常。但是用at()的代价比operator[ ]的代价大。
6) 链表(list)
list是一个以双向链表数据结构实现的序列容器。
如果有较大、复杂的对象,就首选list,特别是如果构造、拷贝、赋值、析构操作的代价大,如果进行大量的譬如对象排序或以别的方式进行重新排列操作是更是这样。
list可以在序列的任何地方快速的插入或删除元素,但是代价较高,所以适插入或删除较大对象的元素;没有operator[ ],所以它随机访问元素非常慢,适合的情况就是从头到尾或从尾到头顺序遍历元素而不是随机访问一个中间元素,但还是很慢。除非已知要访问元素附近的一个迭代器,否则它的遍历都是从头或尾开始。
list容器中的元素在创建以后绝对不会移动,即使插入或删除一个元素(这时只会改变它的链接关系),也不会拷贝构造或向某个实际对象赋值,所以向list中添加元素,已有迭代器不会失效。
对于只需要改变链接而不移动对象的操作(如逆转、排序),不会进行拷贝对象。Swap就是通过拷贝来进行两个元素的交换。
一般来说如果系统提供了一个算法的成员版本就采用它的成员版本而不用其等价的通用的算法。通用的sort()reverse()算法只适用于数组、vectordeque
l     特殊的list操作
list1.splice(it,list2);//把整个list2插入到list1it位置处,并删除list2源链表中所有对象元素
list1.splice(it1,list2,it2);//list2it2处的元素插入到list1it,同时删除源链表中该元素的值
list1.splice(it1,list2,it2,it3);//list2中的it2it3之间([it2,it3])的元素插入到list1it1
remove(it)        //删除list中所有与it处元素等值的元素,删除操作链表不必排序
list.merge(list1);//listlist1合并并排序,合并后将list1源链表已被删除(因为它们已移到新链表中),合并前要先分别对它们用sort()排序
unique()   //删除list中相邻的重复的元素,须先对listsort排序然后再unique()才有效
l     链表与集合
执行listunique操作之后,list变成了一个set
也就是listunique()set()都可以保证容器中的元素不重复,但set()更高效一些
7) 交换序列
成员函数swap()用于同类型序列的相互交换。它是高效率的。它的执行不需进行拷贝和赋值,不论交换的两同类型的序列长度是否相等。实际上它们执行的时它们两个资源的相互交换。STL也包括一个swap()算法,它用在两相同类型的容器交换时,它具有很快速的性能。所以,对容器的容器用sort算法排序,也是很快速的。
5.      集合set
set只接受每个元素的一个副本。set中的元素是用operator<进行排序的。想创建一个容器放入元素时就自动排序,用set是很好的选择。Setfind()也很快速,比通用的find()算法要快很多,因为对已排好序的序列容器在查找元素时,用equal_range()就可以得到对数级的算法复杂性。
6.      堆栈stack
堆栈stackqueuepriority_queue一起被归类为适配器容器。它们通过调整一个基本序列容器以存储自己的数据。
Stack类的pop()并不返回栈顶元素,而是返回一个void值。如果要取得栈顶元素,可以用top()来得一个它的引用。Stack没有提供迭代器,也没有初始化形式,只提供了一个简单的接口。
7.      队列 queue
队列也是一个适配器容器,也是建立在一个基本序列容器之上,它的默认模板参数是dequedequequeue的理想的实现。它是受限的deque,它只能在一端插入元素,在另一端删除元素。在需要使用queue的任何地方使用deque,那样也可以使用deque的附加功能。当强调只使用queue相似的行为的时候,使用queue而不用deque
8.      优先队列 priority_queue
优先队列也是一个适配器容器,也是基于基本序列容器进行构建的适配器,默认的序列容器是vector
优先队列拥有与stack几乎相同的接口,但是这的表现不同。优先队列的栈顶元素是具有优先级最高的元素。
不能在一个priority_queue上从头到尾迭代,但可以用一个vector来模拟priority_queue的行为,因此允许访问那个vector
它使用的函数有:make_heap()push_heap()pop_heap()
可以说,堆就是一个优先队列,priority_queue是对堆的一个封装。
make_heap() & sort_heap()
9.      持有二进制位
表示二进制的两个类:bitsetvector<bool>。它们都不是传统的“STL容器”。
bitsetvector<bool>的区别:
1) bitset是持有固定数目的二进制位;vector<bool>持有的二进制位数目可以扩展。
2) bitset模板是为了在操纵二进制位时提高性能而设计的,它不是正常的STL容器,没有迭代器,它允许底层的整型数组存储在运行时的栈上,它有一个面向二进制位层次的操作接口,绝不与前面所讨论的STL容器相似。vector<bool>vector容器的一个特化,它的设计是用来提高bool数据的空间使用率,有普通vector的所有操作。
1) bitset<n>
一些相关操作:     
      to_ulong把二进制位转化成usigned long数字
      cbits(string)
      cbits(string,int)
      cbits(string,int,int)
      test(i)测试第i位是否为1
      set()置所有位为1,set(i)置第i位为1
      reset()置0,reset(i)将第i位置0
      flip()将所有位取反,flip(i)将第i位取反
      count()有多少个1
      any()(true)没有(false)1
      none()(true)(false)全部为0
      可以使用索引operator[ ]
2) vector<bool>
它的迭代器必须特殊定义,不能使用指向bool的指针
它的操作函数比bitset受更多的限制,它比普通的vector多一个操作flip()就是对所有位取反。
使用operator[ ]时返回一个vector<bool>::reference类型的对象,这个对象也有一个用于对个别位取反的成员函数flip()
// Convert to a bitset:
      将一个vector<bool>转换成bitset<>时,可以把vector<bool>转换成string,再转换成一个bitset<>
      ostringstream os;
      copy(vb.begin(), vb.end(),ostream_iterator<bool>(os, ""));
      bitset<10> bs(os.str());
10. 关联式容器
关联式容器有:setmapmultimap。因为它们将关键字和值关联在一起,至少mapmultimap是这样子的。Set可以看作是没有值的map,它只有关键字,multisetmutipmap的关系也是这样。它们的结构是相似的。
它们主要的操作就是把对象放入容器。当把对象放入容器时,set会检查是否集合中已有这个对象;map会检查是否已有这个关键字,如果有就把这个值关联给关键字。
isert()
cunt()是否有元素
fnd()返回这个元素第一次出现的iterator
只对multimapmultiset有意义:
lwer_bound()
uper_bound()
eual_range()
map中将索引设置为一个超出范围的值,意味着要创建一个新条目,如果使用operator[ ]来查找一个条目,当不存在这个条目时map会新创建一个新条目,所以一般用count()find()查找。
1) 用于关联式容器的发生器和填充器
在序列容器中用fill()fill_n()generate()generate_n()填充数据时,其实现是用赋值方式operator=将值放进序列,对关联式容器填充数据时,是用各自的insert()函数来实现。
2) 不可思议的映像
map的迭代器解析得到的是一个pair(也就是称为value_type的对象),可以用first得到它的键值,用second得到它的键值。用insert()插入时也是先构建一个pair,然后把这个pair插入map
3) 多重映像和重复的关键字
multimap里面会有重复值,这时可以用equal_range()成员函数,它返回这些重复值pair集的起始迭代器和结超出范围的迭代器
4) 多重集合
Mutipset里可以有重复的元素值,但是这些重复值是一定连续的排在一起。同multimap一样可以用equal_range()返回这些重复值的起始和超出范围的迭代器,还可以用distance()来得到这个范围有多大。
也可以取得有哪些值是唯一存在的。
如果想不受限制的存放一个字符串,可以使用vector/deque/list
11. 将STL器联合使用
有时单独使用任一种类容器都不合适,那么可以把几种容器联合起来使用。譬如可以创建vectormap,而vector又包含map
12. 清除容器的指针
容器中的指针不会自动清除,需要我们手动清除,但是,也要保证不要对多个容器中持有的指向同一对象的指针进行多次清除,也就是不要对一个对象多次清除。但是对一个容器中的指针清除后赋为0,即使对这些指针多次清除也不会有问题。
13. 创建自己的容器
有了STL作基础,用户就也可以创建自己的容器了。
14. 对STL的扩充
尽管STL已提供了需要的很多功能,但它们还不是完美的。譬如setmap的速度虽然很快但有时还是不能满足用户的需要。SGI STL增加很多扩充的容器,包括hash_maphash_sethash_multimaphash_multisetslist(单链表)rope(一个string的变种,对非常大型的字符串、字符串的快速连结、取子字符串等操作进行了优化)hash_map要比map的某些操作的性能好些。
maphash_mapfind()要比operator[ ]的操作稍快些。
15. 非STL容器
在标准库中有两种“非STL”容器:bitsetvalarry。因它们没有完全符合STL的要求而被称为非STL容器。它们都没有迭代器。bitset把二进制位打包成整数且不允许对其成员进行直接寻址。
valarry是一个类-vector的模板类,它对一些有效率的数值计算进行了优化。Valarry提供了一个构造函数,该构造函数接受一个目标数组类型的参数和数组中元素计数的参数来初始化一个新的valarry。成员函数shift()将元素左移一个位置,移走后该位置的空位置填元素类型的默认值,如果参数是负的表是向右移;cshift()是进行循环移动;二进位运算符要求valarry具有相同大小和类型的参数;成员函数apply()对每一个元素应用一个成员函数,但结果被收集到一个结果valarry中;关系运算符返回大小匹配的valarry<bool>实例,该实例显示了元素与元素逐个对比的结果。它的大多数操作返回一个数组,但也有个别的返回一个数值,如:min()max()sum()。对valarry可以引用其元素中的一个子集(这个子集也被称为切片slice)。某些运算也可以用切片来做它们的工作。一个切片接受三个参数:起始索引、要提取的元素合计数和“跨距”(也就是用户感兴趣的两个元素之间的间距)。切片可以作为一个现有valarry的索引,并返回一个包含被提取元素的新的valarry