1. 序列
7种STL容器类型(deque, list, queue, priority_queue, stack, vector和C++11新增的forward_list)都是序列。array也被归类到序列容器。
序列概念增加了迭代器至少是正向迭代器这样的要求,这保证元素将按特定顺序排列,不会在两次迭代之间发生变化。序列还要求其元素按严格的线性顺序排列。数组和链表都是序列。
表1-1列出了基本的容器特征。其中,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.end() | 迭代器 | 返回超尾值迭代器 | 固定 |
a.size() | 无符号整型 | 返回元素个数,等价于a.end()-a.begin() | 固定 |
a.swap(b) | void | 交换a和b的内容 | 固定 |
a == b | 可转换为bool | 如果a和b的长度相同,且a中每个元素都等于(==为真)b中相应的元素,则为真 | 线性 |
a != b | 可转换为bool | 返回 !(a == b) | 线性 |
表1-2列出了序列必须完成的操作。部分表示法与表1-1相同,此外,t表示类型为T(存储在容器中的值的类型)的值,n表示整数,p、q、i 和 j 表示迭代器。
表达式 | 返回类型 | 说明 |
---|---|---|
X a(n, t) | 声明一个名为a的由n个t值组成的序列 | |
X(n, t) | 创建一个由n个t值组成的匿名序列 | |
X a(i, j) | 声明一个名为a的序列,并将其初始化为区间 [i, j) 的内容 | |
X(i, j) | 创建一个匿名序列,并将其初始化为区间 [i, j) 的内容 | |
a.insert(p, t) | 迭代器 | 将t插入到p的前面 |
a.insert(p, n, t) | void | 将 n 个 t 插入到 p 的前面 |
a.insert(p, i, j) | void | 将区间 [i, j) 中的元素插入到p的前面 |
a.erase(p) | 迭代器 | 删除p指向的元素 |
a.erase(p, q) | 迭代器 | 删除区间 [p, q) 中的元素 |
a.clear() | void | 等价于erase(begin(), end()) |
模板类deque、list、queue、priority_queue、stack和vector都是序列概念的模型,所以它们都支持上表所示的运算符。表1-3列出了其他可能支持的操作。
表达式 | 返回类型 | 含义 | 容器 |
---|---|---|---|
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(t) | T& | *(a.begin()+n) | vector、deque |
注:a[n]和a.at(n)都返回一个指向容器中第n个元素(从0开始编号)的引用。它们之间的差别在于,如果n落在容器的有效区间外,则a.at(n)将执行边界检查,并引发out_of_range异常。
1.1 vector
(1)vector是数组的一种类表示,它提供了自动内存管理功能,可以动态地改变vector对象的长度,并随着元素的添加和删除而增大和缩小。
(2)它提供了对元素的随机访问。在尾部添加和删除元素的时间是固定的O(1),但在头部或中间插入和删除元素的时间复杂度为线性时间O(N)。
(3)除序列外,vector还是可反转容器的模型。增加两个方法:rbegin():返回一个指向反转序列的第一个元素的迭代器,rend():返回反转序列的超尾迭代器。两种方法返回的迭代器都是类级类型reverse_iterator。
1.2 deque
(1)deque模板类表示双端队列,在STL中其实现类似于vector容器,支持随机访问。区别在于:从deque对象的开始位置插入和删除元素的时间是固定的,而不像vector中那样是线性时间。因此,如果多数操作发生在序列的起始和结尾处,则应考虑使用deque数据结构。
(2)deque对象的设计比vector对象更为复杂,因此对元素的随机访问和在序列中部执行线性时间的插入和删除操作时vector容器执行的要快。
1.3 list
(1)list模板类表示双向链表。可以双向遍历链表。list和vector之间的关键区别在于,list在链表中任一位置进行插入和删除的时间都是固定的。因此vector强调的是通过随机访问进行快速访问,而list强调的是元素的快速插入和删除。
(2)list也是可反转容器,但不支持数组表示法和随机访问。
(3)与矢量迭代器不同,从容器中插入或删除元素之后,链表迭代器指向元素将不变。在链表中插入新元素并不会移动已有的元素,而只是修改链接信息。指向某个元素的迭代器仍然指向该元素,但它链接的元素可能与以前不同。
(4)除序列和可反转容器的函数外,list模板类还包含了链表专用的成员函数。表1-4列出了一些,通常不必担心Alloc模板参数,因为它有默认值。
函数 | 说明 |
---|---|
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() | 将连续的相同元素压缩为单个元素。这个函数的复杂度为线性时间。 |
程序示例1-1(list.cpp):
// list.cpp -- using a list -- 16.12
#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 5个2
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);
// insert()和splice()的区别在于:insert()将原始区间的副本插入到目标地址,而splice()将原始区间移到目标地址。
// 因此,在one的内容与three合并后,one为空。
// splice()方法执行后,迭代器仍有效。
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);
// 非成员函数sort()函数需要随机访问迭代器。因为快速插入的代价是放弃随机访问功能,因此不能将非成员函数sort()用于链表。
// 因此这个类中包括了一个只能在类中使用的成员版本。
two.sort();
three.merge(two);
cout << endl << "Sorted two merged into three: ";
for_each(three.begin(), three.end(), outint);
cout << endl;
return 0;
}
1.4 list工具箱
1.5 queue
(1)queue模板类是一个适配器类。queue模板类让底层类(默认为deque)展示典型的队列接口。
(2)queue的限制比deque更多。它不允许随机访问队列元素,甚至不允许遍历队列。它把使用限制在定义队列的基本操作上,可以将元素添加到队尾、从队首删除元素、查看队首和队尾的值、检查元素数目和测试队列是否为空。表1-5列出了这些操作。
方法 | 说明 |
---|---|
bool empty() const | 如果队列为空,则返回true;否则返回false |
size_type size() const | 返回队列中元素的数目 |
T& front() | 返回指向队首元素的引用 |
T& back() | 返回指向队尾元素的引用 |
void push(const T& x) | 在队尾插入x |
void pop() | 删除队首元素 |
1.6 priority_queue
(1)priority_queue模板类是另一个适配器类,它支持的操作与queue相同。区别在于:在priority_queue中,最大的元素被移到队首;内部区别在于,默认的底层类是vector。
(2)可以修改用于确定哪个元素放到队首的比较方式,方法是提供一个可选的构造函数参数:
priority_queue<int> pq1;
priority_queue<int> pq2(greater<int>);
greater<>()函数是一个预定义的函数对象。
1.7 stack
(1)stack也是一个适配器类,它给底层类(默认情况下为vector)提供了典型的栈接口。
(2)stack不允许随机访问元素,甚至不允许遍历栈。它把使用限制在定义栈的基本操作上,即可以将元素压入栈顶、从栈顶弹出元素、查看栈顶的值、检查元素数目和测试栈是否为空。表1-6列出了这些操作。
方法 | 说明 |
---|---|
bool empty() const | 如果栈为空,则返回true;否则返回false |
size_type size() const | 返回栈中的元素数目 |
T& top() | 返回指向栈顶元素的引用 |
void push(const T& x) | 在栈顶部插入x |
void pop() | 删除栈顶元素 |
1.8 array(C++11)
它并非STL容器,因为其长度是固定的。
2. 关联容器
关联容器将值与键关联在一起,并使用键来查找值。
其优点在于,提供了对元素的快速访问。与序列相似,关联容器也允许插入新元素,但不能指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便能够快速检索信息。通常是由某种树实现。
STL提供了4种关联容器:set、multiset、map和multimap。前两种在头文件set,后两种在头文件map中定义。
2.1 set
(1)set模拟了多个概念,它是关联集合,可反转、可排序、且键是唯一的,所以不能存储多个相同的值。
set也使用模板参数来指定要存储的值类型:
set<string> A;
第二个模板参数是可选的,可用于指示用来对键进行排序的比较函数或对象。默认情况下,将使用模板less<>。
set<string, less<string> > A;
(2)数学为集合定义了一些标准操作,如并集、交集。STL提供了支持这些操作的算法。所有set对象都自动满足使用这些算法的先决条件。
- set_union()函数接收5个迭代器参数。
前两个迭代器定义了第一个集合的区间,接下来的连个定义了第二个集合区间,最后一个迭代器是输出迭代器,指出将结果集合复制到什么位置。
例如,要显示集合A和集合B的并集:
set_union(A.begin(), A.end(), B.begin(), B.end(),
ostream_iterator<string, char> out(cout, " "));
如果要将结果放在集合C中,则最后一个参数应该是一个指向C的迭代器。显而易见的选择是C.begin(),但它不管用。原因:703
set_union(A.begin(), A.end(), B.begin(), B.end(),
insert_iterator<set<string> > (C, C.begin()));
- 函数set_intersection()和set_difference()分别查找交集和获得两个集合的差,它们的接口与set_union()相同。
- 方法lower_bound(),upper_bound():
将键作为参数,并返回一个迭代器,该迭代器指向集合中第一个不小于/大于键参数的成员。
2.2 multimap
与set相似,multimap也是可反转的、经过排序的关联容器,但键和值的类型不同,且同一个键可能与多个值相关联。
multimap<int, string> codes;
为将信息结合在一起,实际的值类型将键类型和数据类型结合为一对。为此,STL使用模板类pair<class T, class U>将这两种值存储到一个对象中。
法一:创建一个pair,再将它插入:
pair<const int, string> item(213, "Los Angeles");
codes.insert(item);
法二:使用一条语句创建匿名pair对象并将其插入:
codes.insert(pair<const int, string> (213, "Los Angeles"));
(1)成员函数count()接收键作为参数,并返回具有该键的元素数目。
(2)成员函数lower_bound()和upper_bound()将键作为参数,且工作原理与处理set时相同。
(3)成员函数equal_range()用键作为参数,且返回两个迭代器,它们表示的区间与该键匹配。
下面代码打印codes对象中区号为718的所有城市:
pair<multimap<KeyType, string>::iterator, multimap<KeyType, string>::iterator> range = codes.equal_range(718);
cout << "Cities with area code 718: \n";
std::multimap<KeyType, std::string>::iterator it;
for (it = range.first; it != range.second; ++it)
cout << (*it).second << endl;
3. 无序关联容器(C++11)
无序关联容器也将值与键关联起来,并使用键来查找值。但底层差别在于,关联容器是基于树结构的,而无序关联容器是基于数据结构哈希表的,这旨在提高添加和删除元素的速度以及提高查找算法的效率。