Power up C++ with the Standard Template Library: Part I
作者:DmitryKorolev
TopCoder成员
原文地址:
http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=standardTemplateLibrary
可能你已经使用C++作为你解决TopCoder题目的主要编程语言了。这意味你已经以一种简单的方式在使用STL了,因为数组和字符串作为STL的对象传递给了你的函数。你可能也注意到了这点,但是,许许多多的coder(程序员)都设法去写出比你更高效更简洁的代码。
或者可能你不是一名C++程序员,但是想要成为其中之一,因为这门语言强大的功能以及它所带的库(或许是由于你已经在TopCoder的练习房间和比赛里阅读了很多非常简洁的解决方案)。
无论你来自哪里,这篇文章都将对你有所帮助。在文章中,我们将回顾标准模板库(STL)的一些强大功能——那是一个非常棒的工具,有时在算法竞赛中能节省你大量的时间。
从“container”(容器)开始讲起是最简单的熟悉STL的方式。
Containers(容器)
任何时候你需要操作众多的元素的时候,你需要一些容器。在天然的C(不是C++)中,仅仅有一种容器类型:数组。
问题并不在于数组有限制(虽然,例如,它可能在运行的时候去决定数组的大小),然而,主要问题是许多题目需要一个带有更强大功能的容器。
例如,我们需要进行一个或更多的下面的操作:
- 从一个容器中添加一个字符串。
- 从一个容器中移除一个字符串。
- 判断一个字符串是否在容器中。
- 从一个容器中返回一些不同类型的元素。
- 遍历一个容器,并以某种顺序得到一个字符串列表。
但是思考一下:这样一个容器的实现是不是取决于我们将要存储的元素类型?我们是否要重新实现这个模块使其功能性更强,例如,对于平面上的点而不是线上的点?
如果不是,我们能开发出一次这样的容器的接口,然后可以让任何类型的数据都使用。总之,那就是STL容器的概念。
在开始之前
当程序使用STL的时候,它应当#include(包含)起来标准头文件的联系。对于大多数容器阿狸说,标准头文件的标题与容器的名字相匹配,并且必须没有扩展名。例如,你将要使用stack(栈),仅需在你的程序开头加入下面这行。
#include <stack>
容器类型(并且算法,功能函数和STL也是如此)没有被定义在全局“namespace”(命名空间)中,而是定义在被称作“std”的特殊命名空间里。在你的包含关系之后并且在你的额代码开始之前添加下面这一行代码。
using namespace std;
另一个要记住的重要的事情就是一个容器的类型是模板参数。在代码中,使用尖括号‘<’/‘>’来指定模板参数。例如:
vector<int> N;
当使用嵌套类型的时候,要确保尖括号不会紧邻着下一个尖括号——在它们之间加一个空格。
vector< vector<int> > CorrectDefinition;
vector<vector<int>> WrongDefinition; // 错误:编译器将由于操作符“>>”导致错误
Vector
最简单的STL容器就是“vector”,vetcor 仅仅是一个带有扩展功能的数组。顺便说一句,vector是唯一一个逆向兼容天然的C代码的容器——这意味着vector确确实实是一个数组,但是却带有一些新增的功能。 vector<int> v(10);
for(int i = 0; i < 10; i++) {
v[i] = (i+1)*(i+1);
}
for(int i = 9; i > 0; i--) {
v[i] -= v[i-1];
}
事实上,当你敲下如下代码的时候
vector<int> v;
一个空的vector被创建。小心使用类似下面的构造:
vector<int> v[10];
这里我们声明了一个“V”作为一个10个vector<int>类型的数组,最初是空的。在大部分情况下,这不是我们想要的。注意在这里要使用圆括号替代尖括号。vector容器最频繁使用的功能就是报告它的大小了。
int elements_count = v.size();
两个备注:第一size()返回一个无符号整型,这在有些时候会导致一些问题。因此,我通常去定义宏,例如 sz(C)去返回一个作为有符号整型的C。第二点,如果你想要知道容器是否是空的,用 v.size()和0来比较,不是一个好做法。你最好使用empty()函数:
bool is_nonempty_notgood = (v.size() >= 0); // 尽量避免这样
bool is_nonempty_ok = !v.empty();
这是因为并非所有容器都能在
O(1)的时间复杂度内报告它的大小,你绝对不应该在一个双向链表中要求计算所有的元素的数量仅仅是为了确保它包含不止一个元素。
vector<int> v;
for(int i = 1; i < 1000000; i *= 2) {
v.push_back(i);
}
int elements_count = v.size();
不要担心内存分配——vector并不会每增加一个元素就分配一个内存。相反地,当使用push_back增加一个新元素的时候vector容器会比它实际需要的内存,分配更多的内存。你唯一应该担心的是内存使用,但是你在TopCoder上这可能并不是问题。(更多关于vector的内存说明稍后继续)
vector<int> v(20);
for(int i = 0; i < 20; i++) {
v[i] = i+1;
}
v.resize(25);
for(int i = 20; i < 25; i++) {
v[i] = i*2;
}
resize()函数将包含所需数量的元素。如果比vector已经包含的元素,你需要的更少,那么最后面的元素将被删除。如果你需要vector变大,它将扩充vector的大小,并用一系列的0去填充新创建的元素。
vector<int> v(20);
for(int i = 0; i < 20; i++) {
v[i] = i+1;
}
v.resize(25);
for(int i = 20; i < 25; i++) {
v.push_back(i*2); //元素写到了索引 [25..30)的位置,不是[20..25)!
}
要想清空vector容器,使用clear()函数。这个函数使vector包含0个元素。它不是使元素变成0——小心——它会擦除容器。
vector<int> v1;
// ...
vector<int> v2 = v1;
vector<int> v3(v1);
上例中v2和v3的初始化方式确实是一样的。
vector<int> Data(1000);
在上例中,在创建后Data将包含1000个0。记住去使用圆括号而不是尖括号。如果你想要vector初始化时带有其他的东西,写错这样的方式:
vector<string> names(20, “Unknown”);
牢记你能创建任何类型的vector容器哦。
vector< vector<int> > Matrix;
如何创建给定大小的二位数组的方法,对你来说应该已经明了了。
int N, N;
// ...
vector< vector<int> > Matrix(N, vector<int>(M, -1));
这里我们创建了一个名叫Matrix的N*N型二维数组,并用-1填充它。
void some_function(vector<int> v){//除非你确定你做什么否则不要做它
// ...
}
代替它,使用下面的构造方式:
void some_function(const vector<int>& v) { // OK
// ...
}
如果在这个函数中你要去改变vector的内容,就要省略“const”修饰符。
int modify_vector(vector<int>& v) { //正确
V[0]++;
}
Pairs
在我们开始讲iterators(迭代器)的时候,让我们先谈一谈"pairs"。pairs在STL中被广泛应用。简单的问题,例如TopCoder中SRM(周赛)250 里面的500分题目,经常需要像pairs这样合适的数据结构。std::pairs仅仅是一对元素。最简单的形式就是下面这样: template<typename T1, typename T2> struct pair {
T1 first;
T2 second;
};
一般来说,
pair<int,int>是一对整型值。在一个更复杂的使用水平上,pair<string, pair<int, int> >是一对字符串和两个整数。在第二种情况下,用法可能是这样的:
pair<string, pair<int,int> > P;
string s = P.first; // 提取出字符串
int x = P.second.first; // 提取出第一个整数
int y = P.second.second; // 提取出第二个整数
pairs最大的优势是他们有内置的操作去比较他们自己。pairs从第一个元素开始比较一直到第二个元素比较。如果第一个元素的比较后不相等,那么整体的比较结果将基于第一个元素的比较结果;只有当第一个元素相等时,才会比较第二个元素。通过STL的内部函数,pairs的数组(或者vector)很容易实现排序。
Iterators(迭代器)
void reverse_array_simple(int *A, int N) {
int first = 0, last = N-1; // 将被交换位置的第一个元素和最后一个元素的下标
While(first < last) { // 循环进行交换元素
swap(A[first], A[last]); // swap(a,b)是标准的STL函数
first++; // 向前移动第一个下标
last--; // 向后移动最后一个下标
}
}
(译者:我感觉此处有误,while里面应该是first<last/2)
void reverse_array(int *A, int N) {
int *first = A, *last = A+N-1;
while(first < last) {
Swap(*first, *last);
first++;
last--;
}
}
看看这段代码,在主循环中,对于指针“first”和“last”只使用了四个明显的操作:
- 比较指针(first<last)
- 通过指针(*first,*last)得到数值
- 指针自增,和
- 指针自减
- 得到迭代器的值,int x = *it;
- 迭代器的自增与自减运算 it1++,it2--;
- 使用“!=”和“<”来比较迭代器
- 直接增加迭代器元素 it+=20;向前移动20个元素;
- 得到两个迭代器的间距, int n=it2-it1;
template<typename T> void reverse_array(T *first, T *last) {
if(first != last) {
while(true) {
swap(*first, *last);
first++;
if(first == last) {
break;
}
last--;
if(first == last) {
break;
}
}
}
}
这段代码与先前的代码主要的不同点是,我们没有使用“<”去比较迭代器,而是仅仅使用了一个“==”。此外,不要恐慌如果你是惊讶于函数原型:template 仅仅是去声明一个函数,在任何合适的参数类型上都能执行的函数。这个行数应该能在指向任何对象类型的指针和迭代器上完美执行。
template<typename T> void reverse_array_stl_compliant(T *begin, T *end) {
// 我们首先应该缩减 'end'
// 但是仅仅当非空的时候
if(begin != end)
{
end--;
if(begin != end) {
while(true) {
swap(*begin, *end);
begin++;
If(begin == end) {
break;
}
end--;
if(begin == end) {
break;
}
}
}
}
}
注意这个函数和标准函数
std::reverse(T begin, T end) 做了同样地事,这个标准函数能在算法模块 (#include <algorithm>)中找到。
vector<int> v;
// ...
vector<int> v2(v);
vector<int> v3(v.begin(), v.end()); // v3 equals to v2
int data[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 };
vector<int> primes(data, data+(sizeof(data) / sizeof(data[0])));
最后一行展示了一个vector的构造,来源于C中顺序数组的构造。没有下标的data被视作数组起始的指针。“data+N”指向第N个元素,因此,当N是数组的大小的时候,“data+N”指向第一个不在数组中的元素,因此“data+length of data(数组长度)”可以看做是data数组的end迭代器。表达式“
sizeof(data)/sizeof(data[0])”返回数组data的大小,但是仅仅是在少数情况下,因此,除非是在这样的结构下否则不要使用它。(C程序员将赞同我!)
vector<int> v;
// ...
vector<int> v2(v.begin(), v.begin() + (v.size()/2));
它创建出的vector类型的v2是v的前半部分。
int data[10] = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 };
reverse(data+2, data+6); // the range { 5, 7, 9, 11 } is now { 11, 9, 7, 5 };
每个容器都有rbegin()或rend()函数,可以返回逆置的迭代器。迭代器逆置被应用于向后遍历容器。因此:
vector<int> v;
vector<int> v2(v.rbegin()+(v.size()/2), v.rend());
将用v的前半部分创建v2,顺序从后向前。
vector<int> v;
// ...
// 遍历整个容器,从 begin() 到 end()
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
*it++; //自减迭代器指向的值
}
我推荐你使用
'!='代替 '<', 'empty()'代替 'size() != 0' —对于一些容器类型,判定那种容器类型优于另一种类型是非常无效的。
vector<int> v;
for(int i = 1; i < 100; i++) {
v.push_back(i*i);
}
if(find(v.begin(), v.end(), 49) != v.end()) {
// ...
}
要得到被找到的元素的下标,我们应该用find()返回的迭代器区间去开始的迭代器:
int i = (find(v.begin(), v.end(), 49) - v.begin();
if(i < v.size()) {
// ...
}
记住当你使用STL算法的时候,在你的源文件里面加上
#include <algorithm>。
int data[5] = { 1, 5, 2, 4, 3 };
vector<int> X(data, data+5);
int v1 = *max_element(X.begin(), X.end()); // 返回 vector中最大的元素的值
int i1 = min_element(X.begin(), X.end()) – X.begin; // 返回 vector中最小元素的下标
int v2 = *max_element(data, data+5); //返回数组中最大元素的值
int i3 = min_element(data, data+5) – data; // 返回数组中最小元素的下标
现在你会发现这样的宏将是有用的:
#define all(c) c.begin(), c.end()
不要将这些宏的右边整个扩进括号里——那将是错误的!
vector<int> X;
// ...
sort(X.begin(), X.end()); // 以升序给数组排序
sort(all(X)); // 以升序给数组排序,使用我们的 宏#define
sort(X.rbegin(), X.rend()); // 使用逆置迭代器,给数组降序排序
编译 STL 程序
在这里一个值得指出的事情就是STL的错误信息。当STL分布在源程序中,对于编译器来说组建出有效的可执行文件变成必须的,STL有一个习惯是错误信息难以理解。void f(const vector<int>& v) {
for(
vector<int>::iterator it = v.begin(); // 嗯哼... 哪里错了?..
// ...
// ...
}
这的错误是你正在努力地从一个带有begin()成员函数的常量对象中,创建一个非常量的迭代器(尽管找到错误实际上比纠正它要困难)。正确的代码像这样:
void f(const vector<int>& v) {
int r = 0;
// 使用const_iterator遍历vector
for(vector<int>::const_iterator it = v.begin(); it != v.end(); it++) {
r += (*it)*(*it);
}
return r;
}
尽管如此,让我来说说GNU C++中叫做typeof的重要功能。这个操作符在编译期间被替换为表达式类型。思考下面的例子:
typeof(a+b) x = (a+b);
这条语句创建的变量x的类型,匹配成(a+b)表达式的类型。当心,对于STL中的任何容器类型,
typeof(v.size())都是无符号整型。但是在TopCoder上typeof最重要的应用是遍历一个容器。思考一下下面的宏:
#define tr(container, it) \
for(typeof(container.begin()) it = container.begin(); it != container.end(); it++)
通过使用这些宏,我们可以遍历每种容器,而不仅仅是vector。这将产生常量对象的
const_iterator 和非常量对象的常规迭代器,并且你从来不会在这里得到错误。
void f(const vector<int>& v) {
int r = 0;
tr(v, it) {
r += (*it)*(*it);
}
return r;
}
注意:为了提高它的可读性,我没有在
#define那行添加多余的圆括号。看看下面的文章,你会看到更多的正确的 #define语句。
在vector中的数据操作
vector<int> v;
// ...
v.insert(1, 42); // 在第一个元素后面插入值42
从第二个元素(下标是1)开始到最后一个元素的所有元素将向后移动一个元素,去为了新的元素留出一个空位。如果你计划添加许多元素,对于大量的移动来说并不好——你最后调用insert()一次。因此,insert()有一个区间的形式:
vector<int> v;
vector<int> v2;
// ..
// 移动从第二个到最后一个所有的元素到合适的位置
// 然后复制v2的内容到v中.
v.insert(1, all(v2));
vector也有一个成员函数erase,它有两种形式。猜猜他们是这样的:
erase(iterator);
erase(begin iterator, end iterator);
在第一中情况下,vector的单个元素被删除。在第二种情况下,这个区间,被两个迭代器分开的区间,从vector中被擦除。
String(字符串)
string s = "hello";
string
s1 = s.substr(0, 3), // "hel"
s2 = s.substr(1, 3), // "ell"
s3 = s.substr(0, s.length()-1), "hell"
s4 = s.substr(1); // "ello"
Set(集合)
- 添加一个元素,但是不允许创建副本
- 移除一个元素
- 统计元素个数(截然不同的元素)
- 检查元素现在是否在set中。
set<int> s;
for(int i = 1; i <= 100; i++) {
s.insert(i); // 插入100个元素 [1..100]
}
s.insert(42); // 什么都没做, 42 已经在set中了
for(int i = 2; i <= 100; i += 2) {
s.erase(i); // 擦除偶数
}
int n = int(s.size()); // n将是50
push_back()成员函数在set中不能被使用。这样的意义是:因为在set中元素的顺序无所谓,所以
push_back()在这里是不适用的。
// 计算set中元素的和
set<int> S;
// ...
int r = 0;
for(set<int>::const_iterator it = S.begin(); it != S.end(); it++) {
r += *it;
}
set< pair<string, pair< int, vector<int> > > SS;
int total = 0;
tr(SS, it) {
total += it->second.first;
}
注意“
it->second.first”的语法。因为它是迭代器,我们需要在操作之前从“it”中获取一个对象。因此,正确的语法将是“(*it).second.first”然而,写成 'something->' 比'(*something)'更容易。完整的解释将特别冗长——对于迭代器和必需的语法仅仅记住就可以了。
set<int> s;
// ...
if(s.find(42) != s.end()) {
// 42 在set中
}
else {
// 42 不在set中
}
调用另一个成员函数并且在O(logN)时间内工作的算法是count。一些人认为会是这样
if(s.count(42) != 0) {
// …
}
或者是这样
if(s.count(42)) {
// …
}
很容易写出来。就我而言,我认为不是不是这样。使用count()在set/map中是荒谬的:元素或者存在或者不存在。对我来说,我更喜欢使用下面两个宏:
#define present(container, element) (container.find(element) != container.end())
#define cpresent(container, element) (find(all(container),element) != container.end())
(记住
all(c) s代表“c.begin(), c.end()”)
set<int> s;
// …
s.insert(54);
s.erase(29);
erase()函数也有擦除区间的形式:
set<int> s;
// ..
set<int>::iterator it1, it2;
it1 = s.find(10);
it2 = s.find(100);
// 如果it1和it2是有效的迭代器,也就是说,值10和100存在set。
s.erase(it1, it2); // 注意10将被删除,但是100还在容器中
set有一个区间的构造:
int data[5] = { 5, 1, 4, 2, 3 };
set<int> S(data, data+5);
它提供给我们一个简单的方式去丢掉vector中的副本,并且给它排序。
Map
map<string, int> M;
M["Top"] = 1;
M["Coder"] = 2;
M["SRM"] = 10;
int x = M["Top"] + M["Coder"];
if(M.find("SRM") != M.end()) {
M.erase(M.find("SRM")); // 或者甚至 M.erase("SRM")
}
非常简单,不是吗?
map<string, int> M;
// …
int r = 0;
tr(M, it) {
r += it->second;
}
不要用迭代器去改变map的键,因为注意可能会破坏map内部的数据结构的完整性(向下看)。
void f(const map<string, int>& M) {
if(M["the meaning"] == 42) { // 错误! 不能使用 [] 在常量型map上!
}
if(M.find("the meaning") != M.end() && M.find("the meaning")->second == 42) { // 正确
cout << "Don't Panic!" << endl;
}
}
Map和Set的注意事项
在内部map和set几乎总是以红黑树的形式被存储。我们不需要担心内部的结构,要记住的事情就是当遍历map和set的时候,它们内部的元素总是以升序被排序。并且这就是为什么当遍历map或set的时候,强烈推荐你不要改变键值的原因。如果你做了破坏顺序的更改,它将导致容器算法的不适当的功能,至少如此。set<int> S;
// ...
set<int>::iterator it = S.find(42);
set<int>::iterator it1 = it, it2 = it;
it1--;
it2++;
int a = *it1, b = *it2;
在这里“a”将包含42左边第一个元素并且“b”包含42右边第一个元素。
更多关于算法的事
vector<int> v;
for(int i = 0; i < 10; i++) {
v.push_back(i);
}
do {
Solve(..., v);
} while(next_permutation(all(v));
不要忘了保证容器中元素是有序的在你第一次调用
next_permutation(...)时。它们初始化的状态应该形成第一个序列。另外,一些序列不能被检查。
String Streams
void f(const string& s) {
// Construct an object to parse strings
istringstream is(s);
// Vector to store data
vector<int> v;
// Read integer while possible and add it to the vector
int tmp;
while(is >> tmp) {
v.push_back(tmp);
}
}
ostringstream对象被用来格式输出。这是它的代码:
string f(const vector<int>& v) {
// Constucvt an object to do formatted output
ostringstream os;
// Copy all elements from vector<int> to string stream as text
tr(v, it) {
os << ' ' << *it;
}
// Get string from string stream
string s = os.str();
// Remove first space character
if(!s.empty()) { // Beware of empty string here
s = s.substr(1);
}
return s;
}
总结
typedef vector<int> vi;
typedef vector<vi> vvi;
typedef pair<int,int> ii;
#define sz(a) int((a).size())
#define pb push_back
#defile all(c) (c).begin(),(c).end()
#define tr(c,i) for(typeof((c).begin() i = (c).begin(); i != (c).end(); i++)
#define present(c,x) ((c).find(x) != (c).end())
#define cpresent(c,x) (find(all(c),x) != (c).end())
容器vector<int>在这里是因为它真的非常普及。事实上,我发现对于一些容器(特别是
vector<string>, vector<ii>, vector< pair<double, ii> >)来说有一个简短的别名是十分方便的。但是这个列表不仅仅包含需要理解下面文字的宏。