本文主要讨论C++标准库中的顺序容器及相应的容器适配器,这些内容主要涉及顺序容器类型:vector、list、deque,顺序容器适配器类型:stack、queue、priority_queue。
如果文中有错误或遗漏之处,敬请指出,谢谢!
概述
标准库中的容器分为顺序容器和关联容器。顺序容器(sequential container)内的元素按其位置存储和访问,顾名思义,这些内部元素是顺序存放的;顺序容器内的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。而关联容器的元素按键(key)排序。
容器类共享部分公共接口。标准库定义的三种顺序容器类型:vector、list、deque(double-ended queue的缩写,发音为“deck”),它们的差别仅在访问元素的方式,以及添加或删除元素相关操作的代价。顺序容器适配器包括:stack、queue和priority_queue。容器只定义了少量操作,大多数操作由算法库提供。如果两个容器提供了相同的操作,则它们的接口(函数名和参数个数)应该相同。
vector | 容器,支持快速随机访问(连续存储) |
list | 链表,支持快速插入/删除 |
deque | 双端队列,支持随机访问(连续存储),两端能快速插入和删除 |
stack | 栈 |
queue | 队列 |
priority_queue | 优先级队列 |
顺序容器的定义
顺序容器的构造和初始化
下面的顺序容器C可以为vector、list和deque类型。
C<T> c; | 创建一个空容器;适用于所有容器。 |
C<T> c2(c); | 创建容器c的副本;适用于所有容器。 |
C<T> c(b, e); | 用迭代器[b, e)范围内的元素构造容器;适用于所有容器。 |
C<T> c(n); | 创建有n个值为默认的元素的容器;仅适用于顺序容器。 |
C<T> c(n, t); | 创建有n个值为t的元素的容器;仅适用于顺序容器。 |
注意:在将一个容器复制到另一个容器时,类型必须匹配,容器类型和元素类型必须相同。
容器元素的类型约束
对于所有容器,容器元素类型必须满足以下约束条件:
1)元素类型必须支持赋值运算;
2)元素类型的对象必须可以复制。
由此知道,所有内置或复合类型都可用做元素类型。引用不支持一般意义的赋值运算,因此没有元素是引用类型的容器;除了标准库中的输入输出流类和智能指针(auto_ptr类型)之外,所有其他标准库类型都是有效的容器元素类型。
如果使用上面顺序容器类型中的第四个构造函数,即构造n个有默认值的元素的容器,此时,若元素类型是类类型,则元素类型必须提供默认构造函数。
迭代器及其范围
下表为迭代器为所有容器类型所提供的运算:
*iter | 返回类型iter所指向的元素的引用 |
iter->mem | 对iter进行解引用,并取得指定成员 |
++iter | 给iter加1,使其指向容器中下一个元素 |
iter++ | |
--iter | 给iter减1,使其指向容器中前一个元素 |
iter-- | |
iter1 == iter2 | 当两个迭代器指向同一个容器中的同一元素,或者当它们都指向 |
iter1 != iter2 | 同一个容器的超出末端的下一个位置时,两个迭代器相等。 |
vector和deque容器的迭代器提供了额外的运算:迭代器的算术运算和另一些关系运算,如下表所示:
iter + n | 在迭代器上加(减)整数值,将产生指向容器中前面(后面)第n个元素的迭代器; |
iter - n | 新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置。 |
iter1 += iter2 | 复合运算:先加(减),再赋值 |
iter1 -= iter2 | |
iter1 - iter2 | 只适用于vector和deque |
>, >=, <, <= | 比较迭代器的位置关系;只适用于vector和deque |
关系操作符只适用于vector和deque容器,这是因为只有这两种容器为其元素提供快速、随机的访问。它们确保可根据元素位置直接有效地访问指定的容器元素。这两种容器都支持通过元素位置实现的随机访问,因此它们的迭代器可以有效地实现算术和关系运算。
迭代器范围:[first, last)是一个左闭合区间,表示范围从first开始,到last结束,但不包括last。注意:如果first不等于last,则对first反复做自增运算必须能够到达last;否则,即last位于first之前,则将发生未定义行为。
迭代器范围使用左闭合的意义:因为这样可以统一表示空集,就无需特别处理。
另外,使用迭代器时,要特别留意迭代器的可能的失效问题。
顺序容器的操作
容器内定义的类型别名
所有容器内部都提供了下列类型别名:
size_type | 无符号型型,足以存储此容器类型的最大可能容器长度 |
iterator | 此容器类型的迭代器类型 |
const_iterator | 元素的只读迭代类型 |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 元素的只读逆序迭代器 |
difference_type | 足够存储两个迭代器差值的有符号整型,可为负数 |
value_type | 元素类型 |
reference | 元素的左值类型,是value_type&的同义词 |
const_reference | 元素的常量左值类型,等效于const value_type& |
begin和end成员
begin() | 返回指向容器中第一个元素的迭代器 |
end() | 返回指向容器中最后一个元素的下一个位置的迭代器 |
rbegin() | 返回指向容器中最后一个元素的逆序迭代器 |
rend() | 返回指向容器中第一个元素前面的位置的逆序迭代器 |
上面的每个操作都有两个版本:const成员和非const成员。
添加元素
push_back(t) | 在容器的尾部添加值为t的元素,返回void类型 |
push_front(t) | 在容器的前端添加值为t的元素,返回void类型 |
insert(p, t) | 在迭代器p所指向的元素前面插入值为t的新元素,返回指向新元素的迭代器 |
insert(p, n, t) | 在迭代器p所指向的元素前面插入n个值为t的新元素,返回void类型 |
insert(p, b, e) | 在迭代器p所指向的元素前面插入迭代器范围[b, e)内的元素,返回void类型 |
注意:往容器中添加元素时,是添加元素的副本。 |
为什么是在插入点之前而不是之后插入元素呢?原因在于,对于指向容器的最后一个元素的下一个位置的迭代器(即end()),这也是一个合法的迭代器,也应该能够用它作为插入点。如果在插入点之后插入元素,那么这个迭代器位置就不能执行插入操作,也就是这个迭代器不能作为插入点。而如果在插入点之前插入元素,那么所有合法的迭代器都可以作为插入点。所以,选择了在插入点之前插入元素。
容器对象的比较
所有的容器类型都支持用关系运算符来实现两个容器的比较。容器的比较是基于容器内元素的比较:
1)如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器相等;否则,它们就不相等。
2)如果两个容器的长度不相同,但较短的容器中所有元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
3)如果两个容器都不是对方的子序列,则它们的比较结果取决于所比较的第一个不相等的元素。
容器大小
所有容器都提供了四种与容器大小相关的操作:
size() | 返回容器中的元素个数,返回类型为C::size_type |
max_size() | 返回容器可容纳的最多元素个数,返回类型为C::size_type |
empty() | 如果容器为空,则返回true,否则返回false |
resize(n) | 调整容器的长度大小,使其能容纳n个元素,新增元素采用值初始化 |
resize(n, t) | 调整容器的长度大小,使其能容纳n个元素,所有新增元素值为t |
另外,vector容器还提供了capacity()和reserve(n)两个函数,以供程序员与vector容器内存分配的实现部分交互工作。
访问元素
back() | 返回容器的最后一个元素的引用。如果容器为空,则该操作未定义 |
front() | 返回容器的第一个元素的引用。如果容器为空,则该操作未定义 |
c[n] | 返回下标为n的元素的引用;如果n<0 or n>=size(),则该操作未定义 |
at[n] | 返回下标为n的元素的引用;如果下标无效,则抛出异常out_of_range异常 (注:只适用于vector和deque容器) |
删除元素
erase(p) | 删除迭代器p所指向的元素。返回一个迭代器,它指向被删除的元素后面的元素。如果p指向容器内最后一个元素,则返回的迭代器指向容器的超出末端的下一个位置;如果p本身就是指向超出末端的下一个位置的迭代器,则该函数未定义 |
erase(b, e) | 删除[b, e)内的所有元素。返回一个迭代器,它指向被删除元素段后面的元素。如果e本身就是指向超出末端的下一个位置的迭代器,则返回的迭代器也指向超出末端的下一个位置。 |
clear() | 删除容器内的所有元素,返回void |
pop_back() | 删除容器内的最后一个元素,返回void。如果容器为空,则该操作未定义。 |
pop_front() | 删除容器内的第一个元素,返回void。如果c为空容器,则该操作未定义 (注:只适用于list和deque容器) |
赋值与swap
c1 = c2 | 删除容器c1的所有元素,然后将c2的元素复制给c1。c1和c2的类型必须相同。 |
c1.swap(c2) | 交换内容:调用该函数后,c1中存放的是c2原来的元素,c2中存放的是c1原来的元素。c1和c2的类型必须相同。该函数的执行速度通常要比将c2的元素复制到c1的操作快。 |
c.assign(b, e) | 重新设置c的元素:将迭代器b和e标记的范围内所有的元素复制到c中。b和e必须不是指向c中元素的迭代器。 |
c.assign(n, t) | 将容器c重新设置为存储n个值为t的元素。 |
注意:assign操作首先删除容器内所有的元素,再将参数所指定的新元素插入到容器中。
swap操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效。但要注意这些迭代器指向了另一个容器中的元素。
容器的选用
vector和deque容器提供了对元素的快速访问,但付出的代价是,在容器的任意位置插入或删除元素,比在容器尾部插入和删除的开销更大,因为要保证其连续存储,需要移动元素;list类型在任何位置都能快速插入和删除,因为不需要保证连续存储,但付出的代价是元素的随机访问开销较大。特征如下:
1)与vector容器一样,在deque容器的中间insert或erase元素效率比较低;
2)不同于vector容器,deque容器提供高效地在其首部实现insert和erase的操作,就像在尾部一样;
3)与vector容器一样而不同于list容器的是,deque容器支持对所有元素的随机访问。
4)在deque容器首部或尾部删除元素则只会使指向被删除元素的迭代器失效。在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器都失效。
一些容器选用法则:
1)如果程序要求随机访问元素,则应使用vector或deque容器;
2)如果程序必须在容器的中间位置插入或删除元素,则应采用list容器;
3)如果程序不是在容器的中间位置,而是在容器首部或尾部插入或删除元素,则应采用deque容器;
4)如果只需要在读取输入时在容器的中间位置插入元素,然后需要随机访问元素,则可以在输入时将元素读入到一个list容器中,然后对容器排序,再将排序后的list容器复制到vector容器中。
5)如果程序既需要随机访问,又需要在容器的中间位置插入或删除元素,此时应当权衡哪种操作的影响较大,从而决定选择list容器还是vector或deque容器。注:此时若选择使用vector或deque容器,可以考虑只使用它们和list容器所共有的操作,比如使用迭代器而不是下标,避免随机访问元素等,这样在必要时,可以很方便地将程序改写为使用list容器。
容器适配器
适配器(adaptor)是标准库中通用的概念,包括容器适配器、迭代器适配器和函数适配器。本质上,适配器是使一事物的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现,只是发生了接口转换而已。
标准库提供了三种顺序容器适配器:queue, priority_queue和stack。
所有适配器都定义了两个构造函数:默认构造函数用于创建空对象,而带一个容器参数的构造函数将参数容器的副本作为其基础值。
默认的stack和queue都基于deque容器实现,而priority_queue则在vector容器上实现。在创建适配器时,通过将一个顺序容器指定为适配器的第二个类型参数,可覆盖其关联的基础容器类型。例如:
stack<int, vector<int> > int_stack; // 此时,int-stack栈是基于vector实现
对于给定的适配器,其关联的容器必须满足一定的约束条件。stack适配器所关联的基本容器可以是任意一种顺序容器类型,因为这些容器类型都提供了push_back、pop_back和back操作;queue适配器要求其关联的基础容器必须提供pop_front操作,因此其不能建立在vector容器上;priority_queue适配器要求提供随机访问功能,因此不能建立在list容器上。
两个相同类型的适配器可以做==, !=, <, >, <=, >=这些关系运算,只要其基本元素类型支持==和<两个操作即可。这与容器大小比较原则一致。
栈
s.empty() | 如果栈为这人,则true;否则返回false |
s.size() | 返回栈中元素的个数 |
s.pop() | 删除栈顶元素,但不返回其值 |
s.top() | 返回栈顶元素的值,但不删除该元素 |
s.push(item) | 在栈项压入新元素 |
队列和优先级队列
标准库队列使用了先进先出(FIFO)的存储和检索策略,进入队列的元素被放置在尾部,下一个被取出的元素则取自队列的首部。
priority_queue默认使用元素类型的 < 操作符来确定它们之间的优先级关系,用户也可以定义自己的优先级关系。在优先级队列中,新元素被放置在比它优先级低的元素的前面。
q.empty() | 如果队列为空,则返回true;否则返回false |
q.size() | 返回队列中元素的个数 |
q.pop() | 删除队首元素,但不返回其值 |
q.front() | 返回队首元素的值,但不删除该元素 (注:该操作只适用于队列) |
q.back() | 返回队尾元素的值,但不删除该元素 (注:该操作只适用于队列) |
q.top() | 返回具有最高优先级的元素值,但不删除该元素 |
q.push(item) | 对于queue,在队尾压入一个新元素; 对于priority_queue,在基于优先级的适当位置插入新元素 |
关联容器:
关联容器(Associative Container)与顺序容器(Sequential Container)的本质区别在于:关联容器是通过键(key)存储和读取元素的,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。
关联容器支持通过键来高效地查找和读取元素,两个基本的关联容器是map和set。map的元素是“键-值”对的二元组形式:键用作元素在map中的索引,而值则表示所存储和读取的数据。set仅包含一个键,并有效地支持关于某个键是否存在的查询。set和map类型的对象所包含的元素都具有不同的键。如果需要一个键对应多个实例,则需要使用multimap或multiset类型。这两种类型允许多个元素拥有相同的键。
map | 关联数组:元素通过键来存储和读取 |
set | 大小可变的集合,支持通过键实现的快速读取 |
multimap | 支持同一个键多次出现的map类型 |
multiset | 支持同一个键多次出现的set类型 |
pair类型
pair模板类用来绑定两个对象为一个新的对象,该类型在<utility>头文件中定义。pair类型提供的操作如下表:
pair<T1, T2> p1; | 创建一个空的pair对象,它的两个元素分别是T1和T2类型,采用值初始化 |
pair<T1, T2> p1(v1, v2); | 创建一个pair对象,它的两个元素分别是T1和T2类型,其中first成员初始化为v1,second成员初始化为v2 |
make_pair(v1, v2) | 以v1和v2值创建一个新的pair对象,其元素类型分别是v1和v2的类型 |
p1 < p2 | 字典次序:如果p1.first<p2.first或者!(p2.first < p1.first)&& p1.second<p2.second,则返回true |
p1 == p2 | 如果两个pair对象的first和second成员依次相等,则这两个对象相等。 |
p.first | 返回p中名为first的(公有)数据成员 |
p.second | 返回p中名为second的(公有)数据成员 |
关联容器
关联容器共享大部分顺序容器的操作,但不提供front, push_front, back, push_back以及pop_back操作。
具体而言,有顺序容器中的:前三种构造函数;关系运算;begin, end, rbegin和rend操作;类型别名;swap和赋值操作,但关联容器不提供assign函数;clear和erase函数,但erase函数返回void类型;关于容器大小的操作,但resize函数不能用于关联容器。
map类型
map类型定义在头文件<map>中。map是键-值对的集合,通常看作关联数组:可使用键作为下标来获取一个值。map类定义内部定义的类型有key_type, mapped_type, value_type,如下表所示:
map<K, V>::key_type | 在map容器内,用做索引的键的类型 |
map<K, V>::mapped_type | 在map容器中,键所关联的值的类型 |
map<K, V>::value_type | map的值类型:一个pair类型,它的first元素具有 const map<K, V>::key_type类型,而second元素 则为map<K, V>::mapped_type类型 |
注意:map的元素类型为pair类型,且键成员不可修改。其它类型别名与顺序容器一样。
map对象的定义
map<K, V> m; | 创建一个名为m的空map对象,其键和值的类型分别为K和V |
map<K, V> m(m2); | 创建m2的副本m,m与m2必须有相同的键类型和值类型 |
map<k, V> m(b, e); | 创建map类型的对象m,存储迭代器b和e标记的范围内所有元素的副本。元素的类型必须能转换为pair<const k, v> |
键类型的约束
在使用关联容器时,它的键不但有一个类型,而且还有一个相关的比较函数。默认情况下,标准库使用键类型定义的 < 操作符来实现键的比较。这个比较函数必须满足:当一个键和自身比较时,结果必定是false;当两个键之间都不存在“小于”关系时,则容器将之视为相同的键。也就是说,map内的元素按键值升序排列。
operator[]
A::reference operator[](const Key& key); |
[]操作符返回键key所关联的值的引用;如果该键key不存在,则向map对象添加一个新的元素,元素的键为key,所关联的值采用值初始化。(要特别留意这个副作用) |
注:map下标操作符返回的类型(mapped_type&)与对map迭代器进行解引用获得的类型(value_type)不相同。
例如:
map <string, int> wordCount; // empty map
word_count["Hello"] = 1;
上面的代码首先创建一个空的map对象,然后执行下列步骤:
1)在wordCount中查找键为“Hello”的元素,没有找到;
2)将一个新的键-值对插入到wordCount中,其中,键为“Hello”,值为0
3)读取新插入的键-值对的值,并将它的值赋为1。
应用实例,下面的程序用来统计一篇英文文章中单词出现的频率:
|
map::insert
m.insert(e) | e是一个用在m上的value_type类型的值,如果键(e.first)不在m中,则插入e到m中;如果键已经在m中存在,则保持m不变。 该函数返回一个pair类型对象,如果发生了插入动作,则返回pair(it, true);否则返回pair(it, false)。其中,it是指向键为e.first那个元素的迭代器。 |
m.insert(beg, end) | beg和end是标记元素范围的迭代器,其中的元素必须为value_type类型的键-值对。对于该范围内的所有元素,如果它的键在m中不存在,则将该键及其关联的值插入到m。返回void类型。 |
m.insert(iter, e) | insert(e),并以iter为起点搜索新元素的位置。返回一个迭代器,指向m中键为e.first的元素。 |
注:当需要插入一个map元素时,一是可以用map::value_type来构造一个pair对象,另外,也可以用make_pair来构造这个对象。
查找元素
m.count(k) | 返回m中k的出现次数(0或1) |
m.find(k) | 如果容器中存在键为k的元素,则返回指向该元素的迭代器。 如果不存在,则返回end()值。 |
删除元素
m.erase(k) | 删除m中键为k的元素,返回size_type类型的值,表示删除的元素个数(0或1) |
m.erase(p) | 从m中删除迭代器p所指向的元素。p必须指向m中确实存在的元素,而且不能等于e.end()。返回void类型 |
m.erase(b, e) | 从m中删除[b, e)范围内的元素,返回void类型 |
set类型
set类型定义于<set>头文件中。set容器支持大部分map容器的操作,如:构造函数;insert操作;count和find操作;erase操作。两个例外情况是:set不支持下标操作符,而且没有定义mapped_type类型。与map一样,set容器存储的键也必须是唯一的,而且不能修改。
multimap和multiset类型
map和set容器中,一个键只能对应一个实例。而multiset和multimap类型则允许一个键对应多个实例。
multimap和multiset所支持的操作分别与map和set的操作相同,只有一个例外:multimap不支持下标运算。为了顺序一个键可以对应多个值这一特性,map和mulitmap,或set和multiset中相同的操作都以不同的方式做出了一定的修改。
元素的添加和删除
map和set容器中的insert和erase操作同样适用于multimap和multiset容器,实现元素的添加和删除。
由于键不要求是唯一的,因此每次调用insert总会添加一个元素。
而带有一个键参数的erase将删除拥有该键的所有元素,并返回删除元素的个数;而带有一个或一对迭代器参数的erase版本只删除指定的元素,并返回void类型。
查找元素
在map和set容器中,元素是有序存储的(升序),同样multimap和multiset也一样。因此,在multimap和multiset容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放,即迭代遍历时,可保证依次返回特定键所关联的所有元素。
要查找特定键所有相关联的值,可以有下面三种方法:
1)配合使用find和count来查找:count函数求出某键出现的次数,而find操作返回指向第一个键的实例的迭代器。
2)使用lower_bound和upper_bound函数:这两个函数常用于multimap和multiset,但也可以用于map和set容器。所有这些操作都需要传递一个键,并返回一个迭代器。
m.lower_bound(k) | 返回一个迭代器,指向键不小于k的第一个元素 |
m.upper_bound(k) | 返回一个迭代器,指向键大于k的第一个元素 |
m.equal_range(k) | 返回一个迭代器的pair对象;它的first成员等价于 m.lower_bound(k),而second成员则等价于 m.upper_bound(k) |
注意:形成的有效区间是[lower_bound(k), upper_bound(i)),是个半开半闭区间。
lower_bound返回的迭代器不一定指向拥有特定键的元素。如果该键不在容器中,则lower_bound返回在保持容器元素顺序的前提下该键应被插入的第一个位置。
若键不存在,返回的迭代器相同。
3)使用equal_range,其实质跟法2)相同。