容器种类
STL 具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。以前的 11 个容器类型分别是 deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset 和 bitset(本章不讨论 bitset,它是在比特级处理数据的容器);C++11 新增了 forward_list、unordered_map、unordered_multimap、unordered_set 和 unordered_multiset,且不将 bitset 视为容器,而将其视为一种独立的类别。因为概念对类型进行了分类,下面先讨论它们。
-
容器概念
没有与基本容器概念对应的类型,但概念描述了所有容器类都通用的元素。它是一个概念化的抽象基类——说它概念化,是因为容器类并不真正使用继承机制。换句话说,容器概念指定了所有STL容器类都必须满足的一系列要求。容器是存储其他对象的对象。被存储的对象必须是同一种类型的,它们可以是 OOP 意义上的对象,也可以是内置类型的值。存储在容器中的数据为容器所有,这意味着当容器过期时,存储在容器中的数据也将过期(然而,如果数据是指针的话,则它指向的数据并不一定过期)。
不是任何类型的对象都可以存储在容器中,具体地说,类型必须是可复制构造的和可赋值的。基本类型满足这些要求:只要类定义没有将复制构造函数和赋值运算符声明为私有或保护的,则也满足这种要求。C++11 改进了这些概念,添加了术语可复制插入(CopyInsetable)和可移动插入(MoveInsetable),但这里只进行简单的概述。
基本容器不能保证其元素都按特定的顺序存储,也不能保证元素的顺序不变,但对概念进行改进后,则可以增加这样的保证。所有的容器都提供某些特征和操作。下表对一些通用特征进行了总结。其中,X 表示容器类型,如 vector;T表示存储在容器中的对象类型;a 和 b 表示类型为 X 的值;r 表示类型为 X& 的值;u 表示类型为 X 的标识符(即如果X表示vector<int>,则u是一个 vector<int> 对象)。
一些基本的容器特征:
表达式 返回类型 说明 复杂度 X::iterator 指向T的迭代器类型 满足正向迭代器要求的任何迭代器 编译时间 X::value_type T T的类型 编译时间 X u; 创建一个名为 u 的空容器 固定 X(); 创建一个匿名的空容器 固定 X u(a); 调用复制构造函数后 u == a 线性 X u = a; 作用同 X u(a); 线性 r = a; X& 调用赋值运算符后 r ==a 线性 (&a)->~X() void 对容器中的每个元素应用析构函数 线性 a.begin() 迭代器 返回指向容器第一个元素的迭代器 固定 a.size() 无符号整型 返回元素个数,等价于 a.end() - a.begin() 固定 a.swap(b) void 交换 a 和 b 的内容 固定 a == b 可转换为 bool 如果a和b的长度相同,且a中每个元素都等于(== 为真)b中相应的元素,则为真 线性 a != b 可转换为 bool 返回 !(a==b) 线性
上表中的“复杂度”一列描述了执行操作所需的时间。这个表列出了3种可能性,从快到慢依次为:
- 编译时间
- 固定时间
- 线性时间
如果复杂度为编译时间,则操作将在编译时执行,执行时间为0。固定复杂度意味着操作发生在运行阶段,但独立于对象中的元素数目。线性复杂度意味着时间与元素数目成正比。即如果a和b都是容器,则 a==b 具有线性复杂度,因为 == 操作必须用于容器中的每个元素。实际上,这是最糟糕的情况。如果两个容器的长度不同,则不需要作任何的单独比较。
固定时间和线性时间复杂度
假设有一个装满大包裹的狭长盒子,包裹一字排开,而盒子只有一端是打开的。假设任务是从打开的一端取出一个包裹,则这将是一项固定时间任务。不管在打开的一端后面有10个还是1000个包裹,都没有区别。
现在假设任务是取出盒子中没有打开的一端的那个包裹,则这将是线性时间任务。如果盒子里有10个包裹,则必须取出10个包裹才能拿到封口端的那个包裹;如果有100个包裹,则必须取出100个包裹。假设是一个不知疲倦的工人来做,每次只能取出一个包裹,则需要取10次或更多。
现在假设任务是取出任意一个包裹,则可能取出第一个包裹。然而,通常必须移动的包裹数目仍旧与容器中包裹的数目成正比,所以这种任务依然是线性时间复杂度。
如果盒子各边都可打开,而不是狭长的,则这种任务的复杂度将是固定时间的,因为可以直接取出想要的包裹,而不用移动其他的包裹。
时间复杂度概念描述了描述了容器长度对执行时间的影响,而忽略了其他因素。如果超人从一端打开的盒子中取出包裹的速度比普通人快100倍,则他完成任务时,复杂度仍然是线性时间的。在这种情况下,他取出封闭盒子中包裹(一端打开,复杂度为线性时间)的速度将比普通人取出开放盒子中包裹(复杂度为固定时间)要快,条件是盒子里没有太多的包裹。
复杂度要求是 STL 特征,虽然实现细节可以隐藏,但性能规格应公开,以便程序员能够知道完成特定操作的计算成本。
-
C++11 新增的容器要求
下表列出了 C++11 新增的通用容器要求。在这个表中,rv 表示类型为 X 的非常量右值,如函数的返回值。另外,在下表中,要求 X::iterator 满足正向迭代器的要求,而以前只要求它不是输出迭代器。
C++11 新增的基本容器要求
表达式 返回类型 说明 复杂度 X u(rv); 调用移动构造函数后,u 的值与 rv 的原始值相同 线性 X u = rv; 作用同 Xu(rv); 线性 a = rv; X& 调用移动赋值运算符后,u 的值与 rv 的原始值相同 线性 a.cbegin() const_iterator 返回指向容器第一个元素的 const 迭代器 固定 a.cend() const_iterator 返回超尾值 const 迭代器 固定 复制构造和复制赋值以及移动构造和移动赋值之间的差别在于,复制操作保留源对象,而移动操作可修改源对象,还可能转让所有权,而不作任何复制。如果源对象是临时的,移动操作的效率将高于常规复制。第18章将更详细地介绍移动语义。
-
序列
可以通过添加要求来改进基本的容器概念。序列(sequence)是一种重要的‘改进,因为7种STL容器类型(deque,C++11新增的 forward_list、list、queue、priotiry_queue、stack 和 vector)都是序列(本书前面说过,队列让您能够在队尾添加元素,在队首删除元素。deque 表示的双端队列允许在两端添加和删除元素)。序列概念增加了迭代器至少是正向迭代器这样的要求,这保证了元素将按特定顺序排列,不会在两次迭代之间发生变化。array 也被归类到序列容器,虽然它并不满足序列的所有要求。序列还要求其元素按严格的线性顺序排列,即存在第一个元素、最后一个元素,除第一个元素和最后一个元素外,每个元素前后都分别有一个元素。数组和链表都是序列,但分支结构(其中每个节点都指向两个子节点)不是。
因为序列中的元素具有确定的顺序,因此可以执行诸如将值插入到特定位置、删除特定区间等操作。下表列出了这些操作以及序列必须完成的其他操作。该表格使用的表示法与之前的一个表相同,此外,t表示类型为T(存储在容器中的值的类型)的值,n表示整数,p、q、i 和 j 表示迭代器。
序列的要求:
表达式 返回类型 说明 X a(n,t); 声明一个名为 a 的由 n 个 t 值组成的序列 X(a,t) 创建一个由 n 个 t 值组成的匿名序列 X a(i, j) 声明一个名为 a 的序列,并将其初始化为区间 [i, j) 的内容 X(i, j) 创建一个匿名序列 a.insert(p,t) 迭代器 将 t 插入到 p 的前面 a.insert(p, n, t) void 将 n 个 t 插入到 p 的前面 a.insert(p, i, j ) void 将 n 个 t 插入到 p 的前面 a.erase(p) 迭代器 删除 p 指向的元素 a.erase(p,q) 迭代器 删除区间 [p,q) 中的元素 a.clear() void 等价于 erase(begin(), end() ) 因为模板类 deque、list、queue、priority_queue、stack 和 vector 都是序列概念的模型,所以它们都支持上表所示的运算符。除此之外,这 6 个模型中的一些还可使用其他操作。在允许的情况下,它们的复杂度为固定时间。下表列出了其他操作。
序列的可选要求:表达式 返回类型 含义 容器 a.front() T& *a.begin() vector、list、deque a.back() T& *–a.end() vector、list、deque a.push_front(t) void a.insert(a.begin(), t) list、deque a.push_back(t) void a.insert(a.end(),t) vector、list、deque a.pop_front(t) void a.erase(a.begin()) list、deque a.pop_back(t) void a.erase(–a.end()) vector、list、deque a[n] T & *(a.begin()+n) vector、deque a.at(n) T & *(a.begin() + n) vector、deque 上表有些需要说明的地方。首先 a[n] 和 a.at(n) 都返回一个指向容器中第 n 个元素(从0开始编号)的引用。它们之间的差别在于,如果 n 落在容器的有效区间外,则 a.at(n) 将执行边界检查,并引发 out_of_range 的异常。其次,可能有人会问,为何为 list 和 deque 定义了 push_front(),而没有为 vector 定义?假设要将一个新值插入到包含100个元素的矢量的最前面。要腾出空间,必须将第99个元素移到位置100,然后把第98个元素移动到位置99,依此类推。这种操作的复杂度为线性时间,因为移动100个元素所需的时间为移动单个元素的100倍。但上表的操作假设仅当复杂度为固定时间时才被实现。链表和双端队列的设计允许将元素添加到前端,而不用移动其他元素,所以它们可以以固定时间的复杂度来实现push_front()。
下面详细介绍这7种序列容器类型。
-
vector
前面介绍了多个使用 vector 模板的例子,该模板是在 vector 头文件中声明的。简单地说,vector 是数组的一种类表示,它提供了自动内存管理功能,可以动态地改变 vector 对象的长度,并随着元素的添加和删除而增大和缩小。它提供了对元素的随机访问。在尾部添加和删除元素的时间是固定的,但在头部或中间插入和删除元素的复杂度为线性时间。除序列外,vector 还是可反转容器(reversible container)概念的模型。这增加了两个类方法:rbegin() 和 rend(),前者返回一个指向反转序列的第一个元素的迭代器,后者返回反转序列的超尾迭代器。因此,如果 dice 是一个 vector<int> 容器,而 Show(int) 是显示一个整数的函数,则下面的代码将首先正向显示 dice 的内容,然后反向显示:
for_each(dice.begin(), dice.end(), Show); // display in order cout << endl; for_each(dice.rbegin(), dice.rend(), Show); // display in reversed order cout << endl;
这两种迭代器返回的迭代器都是类级类型 reverse_iterator。对这样的迭代器进行递增,将导致它反向遍历可反转容器。
vector 模板类是最简单的序列类型,除非其他类型的特殊优点能够更好地满足程序的要求,否则应默认使用这种类型。 -
deque
deque 模板类(在 deque 头文件中声明)表示双端队列(double-ended queue),通常被简称为 deque。在 STL 中,其实现类似于 vector 容器,支持随机访问。主要区别在于,从 deque 对象的开始位置插入和删除元素的时间是固定的,而不像 vector 中那样是线性时间的。所以,如果多数操作发生在序列的起始和结尾处,则应考虑使用 deque 数据结构。为实现在 deque 两端执行插入和删除操作的时间为固定的这一目的,deque 对象的设计比 vector 对象更为复杂。因此,尽管二者都提供对元素的随机访问和在序列中部执行线性时间的插入和删除操作,但 vector 容器执行这些操作时速度要快些。
-
list
list 模板类(在 list 头文件中声明)表示双向链表。除了第一个和最后一个元素外,每个元素都与前后的元素相链接,这意味着可以双向遍历链表。list 和 vector 之间关键的区别在于,list 在链表中任一位置进行插入和删除的时间都是固定的(vector 模板提供了除结尾处外的线性时间的插入和删除,在结尾处,它提供了固定时间的插入和删除)。因此,vector 强调的是通过随机访问进行快速访问,而 list 强调的是元素的快速插入和删除。与 vector 相似,list 也是可反转容器。与 vector 不同的是,list 不支持数组表示法和随机访问。与矢量迭代器不同,从容器中插入或删除元素之后,链表迭代器指向元素将不变。我们来解释一下这句话。例如,假设有一个指向 vector 容器第 5 个元素的迭代器,并在容器的起始处插入一个元素。此时,必须移动其他所有元素,以便腾出位置,因此插入后,第 5 个元素包含的值将是以前第4个元素的值。因此,迭代器指向的位置不变,但数据不同。然后,在链表中插入新元素并不会移动已有的元素,而只是修改链接信息。指向某个元素的迭代器仍然指向该元素,但它链接的元素可能与以前不同。
除序列和可反转容器的函数外,list 模板类还包含了链表专用的成员函数。表 16.9 列出了其中一些(有关 STL 方法和函数的完整列表,请参见附录G)。通常不必担心 Alloc 模板参数,因为它有默认值。
list 成员函数:
函数 说明 void merge(list<T, Alloc> & x) 将链表 x 与调用链表合并。两个链表必须已经排序。合并后的经过排序的链表保存在调用链表中,x 为空。这个函数的复杂度为线性时间 void remove(const T & val) 从链表中删除 val 的所有实例。这个函数的复杂度为线性时间 void sort() 使用<运算符对链表进行排序;N个元素的复杂度为 NlogN void splice(iterator pos, list<T, Alloc>x) 将链表x 的内容插入到 pos 的前面,x 将为空。这个函数的复杂度为固定时间 void unique() 将连续的相同元素压缩为单个元素。这个函数的复杂度为线性时间 下面的程序演示了这些方法和 insert() 方法(所有模拟序列的 STL 类都有这种方法)的用法。
// list.cpp -- using a list #include<iostream> #include<list> #include<iterator> #include<algorithm> void outint(int n) { std::cout << n << " "; } int main(){ using namespace std; list<int> one(5,2); //list of 5 2s int stuff[5] = {1, 2, 4, 8, 6}; list<int> two; two.insert(two.begin(), stuff, stuff+5); int more[6] = {6, 4, 2, 4, 6, 5}; list<int> three(two); three.insert(three.end(), more, more+6); cout << "List one: "; for_each(one.begin(), one.end(), outint); cout << endl << "List two: "; for_each(two.begin(), two.end(), outint); cout << endl << "List three: "; for_each(three.begin(), three.end(), outint); three.remove(2); cout << endl << "List three minus 2s: "; for_each(three.begin(), three.end(), outint); three.splice(three.begin(), one); cout << endl << "List three after splice: "; for_each(three.begin(), three.end(), outint); cout << endl << "List one: "; for_each(one.begin(), one.end(), outint); three.unique(); cout << endl << "List three after unique: "; for_each(three.begin(), three.end(), outint); three.sort(); three.unique(); cout << endl << "List three after sort & unique: "; for_each(three.begin(), three.end(), outint); two.sort(); three.merge(two); cout << endl << "Sorted two merged into three: "; for_each(three.begin(), three.end(), outint); cout << endl; return 0; }
下面是该程序的输出:
List one: 2 2 2 2 2 List two: 1 2 4 8 6 List three: 1 2 4 8 6 6 4 2 4 6 5 List three minus 2s: 1 4 8 6 6 4 4 6 5 List three after splice: 2 2 2 2 2 1 4 8 6 6 4 4 6 5 List one: List three after unique: 2 1 4 8 6 4 6 5 List three after sort & unique: 1 2 4 5 6 8 Sorted two merged into three: 1 1 2 2 4 4 5 6 6 8 8
-
程序说明
上面的程序使用了 for_each() 算法 和 outint() 函数来显示列表。在 C++11 中,也可使用基于范围的 for 循环:for (auto x : three ) cout << x << " ";
insert() 和 splice() 之间的主要区别在于:insert() 将原始区间的副本插入到目标地址,而 splice() 则将原始区间移到目标地址。因此,在 one 的内容与 three 合并后,one 为空。(splice() 方法还有其他原型,用于移动单个元素和元素区间)。splice() 方法执行后,迭代器仍有效。也就是说,如果将迭代器设置为指向 one 中的元素,则在 splice() 将它重新定位到元素 three 后,该迭代器仍然指向相同的元素。
注意:unique() 只能将相邻的相同值压缩为单个值。程序执行 three.unique() 后,three 中仍包含不相邻的两个4和两个6。但应用 sort() 后再应用 unique() 时,每个值将只占一个位置。
还有非成员 sort() 函数,但它需要随机访问迭代器。因为快速插入的代价时放弃随机访问功能,所以不能将非成员函数 sort() 用于链表。因此,这个类中包括了一个只能在类中使用的成员版本。
-
list 工具箱
list 方法组成了一个方便的工具箱。例如,假设有两个邮件列表要整理,则可以对每个列表进行排序,合并它们,然后使用 unique() 来删除重复的元素。
sort()、merge() 和 unique() 方法还各自拥有接受另一个参数的版本,该参数用于指定用来比较元素的函数。同样,remove() 方法也有一个接受另一个参数的版本,该参数用于指定用来确定是否删除元素的函数。这些参数都是谓词函数,将稍后介绍。 -
forward_list (C++11)
C++11 新增了容器类 forward_list,它实现了单链表。在这种链表中,每个节点都只链接到下一个节点,而没有链接到前一个节点。因此 forward_list 只需要正向迭代器,而不需要双向迭代器。因此,不同于 vector 和 list,forward_list 是不可反转的容器。相比于 list,forward_list 更简单、更紧凑,但功能也更少。 -
queue
queue 模板类(在头文件 queue(以前为 queue.h)中声明)是一个适配器类。由前所述,ostream_iterator 模板就是一个适配器,让输出流能够使用迭代器接口。同样,queue 模板让底层类(默认为 deque)展示典型的队列接口。queue 模板的限制比 deque 更多。它不仅不允许随机访问队列元素,甚至不允许遍历队列。它把使用限制在定义队列的基本操作上,可以将元素添加到队尾、从队首删除元素、查看队首和队尾的值、检查元素数目和测试队列是否为空。
下表列出了这些操作:
queue 的操作:方法 说明 bool empty() const 如果队列为空,则返回 true;否则返回 false size_type size() const 返回队列中元素的数目 T& front() 返回指向队首元素的引用 T& back() 返回指向队尾元素的引用 void push(const T& x) 在队尾插入 x void pop() 删除队首元素 注意:pop() 是一个删除数据的方法,而不是检索数据的方法。如果要使用队列中的值,应首先使用 front() 来检索这个值,然后使用 pop() 将它从队列中删除。
-
priority_queue
priority_queue 模板类(在 queue 头文件中声明)是另一个适配器类,它支持的操作与 queue 相同。两者之间的主要区别在于,在priority_queue 中,最大的元素被移到队首(生活不总是公平的,队列也一样)。内部区别在于,默认的底层类是 vector。可以修改用于确定哪个元素放到队首的比较方式,方法是提供一个可选的构造函数参数:priority_queue<int> pq1; // default version priority_queue<int> pq2(greater<int>); // use greater<int> to order
greater<>() 函数是一个预定义的函数对象,本章稍后将讨论它。
-
stack
与 queue 相似,stack(在头文件 stack—— 以前为 stack.h——中声明)也是一个适配器类,它给底层类(默认情况下为 vector)提供了典型的栈接口。
stack模板的限制比vector更多。它不仅不允许随机访问栈元素,甚至不允许遍历栈。它把使用限制在定义栈的基本操作上,即可以将压入推到栈顶、从栈顶弹出元素、查看栈顶的值、检查元素数目和测试栈是否为空。下表列出了这些操作。
stack的操作:方法 说明 bool empty() const 如果栈为空,则返回true;否则返回 false size_type size() const 返回栈中的元素数目 T& top() 返回指向栈顶元素的引用 void push(const T& x) 在栈顶不插入 x void pop() 删除栈顶元素 与 queue 相似,如果要使用栈中的值,必须首先使用 top() 来检索这个值,然后使用 pop() 将它从栈中删除。
-
array(C++11)
第 4 章介绍过,模板类 array 是否头文件 array 中定义的,它并非 STL 容器,因为其长度是固定的。因此,array 没有定义调整容器大小的操作,如 push_back() 和 insert(),但定义了对它来说有意义的成员函数如 operator[]() 和 at()。可将很多标准 STL 算法用于 array 对象,如 copy() 和 for_each()。
-