译者:cuichen(chentsuei@gmail.com)
可能你已经使用C++作为解决TopCoder题目的主力语言,这意味着你已经简单地使用过STL,因为数组和字符串都作为STL对象传递到你的函数中。你可能已经意识到,许多人写代码比你更快更简洁。
或者你不是一个C++程序员,但因为这个语言的强大库和功能而想成为一个C++程序员(也可能因为你在TopCode练习或比赛时看到非常简短的代码)。
不管你从何而来,这个文章总能帮到你。在这里,我们将学习STL(Standard Template Library,标准模板库)的一些强大的特性,这是一个有时候可以让你在算法竞赛中节省大量时间的强大工具。
熟悉STL的最简单方法就是先从容器(Containers)开始。
容器(Containers)
当你需要对很多元素进行操作的时候你就需要某种容器。在原始的C语言(不是C++)中只有一种容器:数组。
问题不在于数组有很多限制(尽管,比如不能在运行时决定数组的大小)。其实,主要问题是许多问题需要一个有强大功能的容器。
比如。我们可能需要以下的操作:
- 将一些字符串添加到一个容器中
- 从一个容器中删除一个字符串
- 判断一个字符串是否在这个容器中
- 返回一个容器中不同元素的数量
- 遍历一个容器并得到一个某种顺序的字符串列表
当然,我们可以用普通的数组实现这些功能,但是这些琐碎的实现是十分低效的。你可以创建某种树结构或者哈希结构去更快的解决它,但是想一想:这种容器的实现是否依赖于要存储的元素个数?我们为了让它功能更丰富就必须重新实现它么,比如从字符串变成了平面上的点?
如果不是,我们可以只对这种容器开发一个接口,然后运用到任意类型的数据上。简而言之,这就是STL容器的思想。
开始之前
当程序用到STL的时候,需要#include适当的标准头文件。对于大多数容器来说,标准头文件名称和容器的名称是一致的,不需要额外地添加什么东西。比如,如果你要用栈(stack),只需要在你程序的开头加上下面这一行:
#include <stack>
容器类型(以及算法,仿函数以及所有的STL)都不是定义在全局命名空间的,而是定义在一个叫做"std"的特殊命名空间。在你include的后面,正式代码的前面加上下面这一行:
using namespace std;
还需要注意的是容器的类型是模板参数。代码中模板参数用尖括号'<'/'>'指定。比如:
vector<int> N;
当使用嵌套结构的时候,要确保尖括号不是紧跟在一个尖括号的后面,在他们之间加一个空格。
vector< vector<int> > CorrectDefinition; vector<vector<int>> WrongDefinition; // 错误:编译器可能会和">>"混淆
向量(Vector)
最简单的STL容器就是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];
这里我们声明了一个包含10个vector<int>的数组v,初始是空的。在大多数情况下,这不是我们想要的,在这儿应该使用圆括号而不是方括号。vector最常用的特性就是它可以报告自身的大小。
int elements_count = v.size();
两点需要注意:第一,size()是unsigned,可能有些时候造成一些问题。因此,我常常自定义宏,比如定义sz(C)作为int类型返回C的大小。第二,将v.size()和0比较来判断容器是否为空不是一个好的实践,最好使用empty()函数:
bool is_nonempty_notgood = (v.size() >= 0); // 避免使用这种方法 bool is_nonempty_ok = !v.empty();
这是因为不是所有的容器都能够O(1)的报告它的大小,你肯定不应该仅仅为了判断一个双向链表是否存在至少有一个元素而去数所有的元素。
vector中另外一个很常用的函数是push_back。push_back将一个元素加到vector的末尾,大小增加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的大小,使用resize()函数:
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增长,vector的大小会变大,并将新创建的元素填充为0。
注意如果你在使用resize()之后使用push_back(),将会在新分配的大小的后面增加元素,而不是在里面。上面的那个例子中,所得vector的大小是25,然而如果我们在第二个循环中使用push_bakc(),大小会是30。
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的方法。你可能通过另外一个vector创建一个vector:
vector<int> v1; // ... vector<int> v2 = v1; vector<int> v3(v1);
上面例子中v2和v3的初始化是完全一样的。
如果你想创建指定大小的vector,使用下面的构造:
vector<int> Data(1000);
上面的例子中,Data在创建之后将包含1000个0。记住使用小括号而不是尖括号。如果你想要vector用其他东西初始化,用这样的方式来写:
vector<string> names(20, “Unknown”);
记住你可以创建任何类型的vector。
多维数组非常重要。创建一个二维数组最简单的方法就是创建一个vector类型的vector。
vector< vector<int> > Matrix;
你现在应该清楚如何创建一个给定大小的二维vector:
int N, M; // ... vector< vector<int> > Matrix(N, vector<int>(M, -1));
这里我们创建了一个N*M的矩阵并用-1填充。
向vector中添加数据的最简单方法就是使用push_back()。但如果我们想向非末尾的位置加入数据怎么办?为此目的有一个insert()成员函数。并且也有一个erase()成员函数来删除元素。但是首先我们想说一些迭代器相关的。
你应该记得一个非常重要的事情:当vector作为参数被传递进一些函数中时,一份vector的拷贝就实际被创建了。创建新的vector可能会花费大量的时间和空间,但并不是真的需要。实际上,很难找到一项当vector作为参数被传递时真的需要拷贝vector的任务。所以你永远不应该这样写:
void some_function(vector<int> v) { // 除非你知道自己在做什么,否则永远不要用 // ... }
而是使用下面这种构造:
void some_function(const vector<int>& v) { // 可以 // ... }
如果你想在函数里更改vector的内容,只需要省略"const"修饰符。
int modify_vector(vector<int>& v) { // 正确 V[0]++; }
Pairs
在我们开始讲迭代器之前,让我先讲一些关于pairs的东西。Pairs在STL中被广泛的使用。像TopCoder SRM 250和500-point之类的简单题目,通常都需要一些简单的数据结构,pair就非常合适。STL中std::pair就是一对元素,最简单的形式如下:
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; // 取出string int x = P.second.first; // 取出第一个int int y = P.second.second; // 取出第二个int
pairs最大的优势就在于它们有内置的比较操作。pair是从第一个元素到第二个元素进行比较的。如果第一个元素不相等,结果就只基于第一个元素的比较;如果第一个元素相等第二个元素才会被比较。pairs的数组(或vector)都能够用STL内置的函数轻松地排序。
例如,如果你想对整数点的数组进行排序,使得它们组成一个多边形,将它们放在一个vector< pair<double, pair<int,int> >,其中vector中的每一个元素都是{ polar angle, { x, y } }。一次调用STL的排序函数就会得到想要的点的顺序。
pairs也广泛的用在关联容器中,我们会在文章的后面讲到。
迭代器(Iterators)
什么是迭代器?在STL中迭代器是访问容器中数据最普遍的方法。考虑一个简单的问题:翻转N个int的数组A。让我们从一个C语言风格的方法开始:
void reverse_array_simple(int *A, int N) { int first = 0, last = N-1; // First and last indices of elements to be swapped While(first < last) { // Loop while there is something to swap swap(A[first], A[last]); // swap(a,b) is the standard STL function first++; // Move first index forward last--; // Move last index back } }
这份代码对你来说应该非常清晰。很容易用指针重写:
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),
- 指针增量,以及
- 指针减量
现在设想你面临第二个问题:翻转一个双向链表的部分或全部。第一份使用索引的代码肯定不行了。至少,它不能高效的运行,因为在双向链表中不可能用O(1)的时间按照索引得到一个元素,只能是O(N)的,所以整个算法的时间复杂度就是O(N^2)了。呃……
但是看一下:第二份代码对任意指针类的对象都有效。唯一的限制就是对象需要有下面描述的操作:取值(一元运算符 *),比较(<),以及增加/减少(++/-)。与容器联系起来的拥有这些性质的对象叫做迭代器。任何STL容器可能都可以用某种迭代器遍历。尽管迭代器对于vector来说不是经常需要,但是对于其他的容器类型来说还是很重要的。
所以我们有什么?一个语法非常像指针的对象。下面是迭代器定义的操作:
- 取得一个迭代器的值,int x = *it;
- 增加和减少迭代器 it1++, it2–;
- 用‘!=’和‘<’来比较迭代器
- 为迭代器加上一个增量 it += 20; <=> 向前移动20个元素
- 获得迭代器之间的距离,int n = it2-it1;
相对于指针,迭代器提供了更强大的功能。它们不只是能对任何容器进行操作,它们还能做很多事,比如范围检查和分析容器的使用。
迭代器最大的优点当然就是大大的增加了代码的可重用性:你自己的基于迭代器的算法,会对很大范围的容器都有效,并且你自己的提供迭代器的容器,也可能会被传递到很大范围的标准函数。
注意不是所有类型的迭代器都提供所有可能的功能。实际上,有所谓的“普通迭代器”和“随机访问迭代器”。简而言之,普通迭代器可以用“==”和“!=”比较,而且还可能增加或减少。普通迭代器可能不能被减掉或者加上一个值。基本的,不可能对于所有的容器类型都O(1)的实现上述操作。尽管如此,翻转数组的函数应该看起来像这样:
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; } } } }
这个代码和之前那个主要的区别是我们不使用“<”对迭代器进行比较,只是用“==”。再次,如果你被函数原型惊讶到了不要慌张:模板只是声明函数的一种方式,对任何合适的参数类型都有效。该函数应该完美地对任何对象类型的指针和所有正常的迭代器都有效。
让我们回到STL。 STL算法总是使用两个称为“begin”和“end”的迭代器,但是最后的迭代器不是指向最后一个对象,而是指向第一个无效对象,或者是紧接在最后一个之后的对象, 这通常很方便。
每个STL容器都有成员函数begin()和end(),分别返回该容器的开始和结束迭代器。
基于这些原则,当且仅当c为空时c.begin() == c.end(),c.end() - c.begin()将始终等于c.size()。 (第二条在迭代器可被减去的情况下才是有效的,即begin()和end()返回随机访问迭代器时,不是对于所有类型的容器都这样)。
STL兼容的反向函数应该像下面这样写:
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; } } } } }
注意这个函数与算法模块(#include <algorithm>)中可以找到的标准函数std::reverse(T begin,T end)相同。
此外,具有足够功能的任何对象可以作为迭代器传递给STL算法和函数。 模板的威力来了! 请参见以下示例:
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])));
最后一行从一个C枚举数组执行vector的构造。 不带索引的项“data”被视为指向数组开头的指针。 “data + N”这一项指向第N个元素,所以如果N是数组的大小,“data + N”指向不在数组中的第一个元素,则“data + data的长度”可以被视为数组“data”的结束迭代器。 表达式'sizeof(data)/sizeof(data[0])'返回数组data的大小,但仅在少数情况下使用,所以除了这样的结构之外,不要在其他地方使用它。 (C程序员会同意我的观点!)
此外,我们甚至可以使用以下结构:
vector<int> v; // ... vector<int> v2(v.begin(), v.begin() + (v.size()/2));
它创建了一个等于vector v的前半部分的vector v2。
这里有一个reverse()函数的例子:
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,并按从后往前的顺序。
要创建一个迭代器对象,我们必须指定它的类型。 迭代器的类型可以通过容器的类型附加“::iterator”,“::const_iterator”,“::reverse_iterator”或“::const_reverse_iterator”来构造。 因此,可以通过以下方式遍历vector:
vector<int> v; // ... // 遍历整个容器,从 begin() 到 end() for(vector<int>::iterator it = v.begin(); it != v.end(); it++) { *it++; // 增加迭代器指向的数值 }
我建议您使用'!='代替'<','empty()'代替'size() != 0' - 对于某些容器类型,确定哪个迭代器在另一个之前是非常低效的。
现在你知道了STL算法reverse()。许多STL算法以相同的方式声明:它们获得一对迭代器 - 表示范围的开始和结束 - 并返回一个迭代器。
find()算法在一个区间中查找适当的元素。 如果找到该元素,则返回指向元素第一次出现的位置的迭代器。 否则,返回值等于区间的结束。 见代码:
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>。
min_element和max_element算法返回一个指向相应的元素的迭代器。 要获得min/max元素的值,像在find()中一样,使用*min_element(…) 或者 *max_element(…)。要获得min/max元素的索引的话,减掉一个容器或范围的开始迭代器:
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()
不要把宏的整个右边 c.begin(), c.end() 放在括号中 - 这是错误的!
另一个好用的算法是sort()。 它很容易使用, 请考虑以下示例:
vector<int> X; // ... sort(X.begin(), X.end()); // 按升序排序数组 sort(all(X)); // 按升序排序数组,使用我们的 #define sort(X.rbegin(), X.rend()); // 使用反向迭代器按降序排序数组
编译STL程序
这里值得一提的是STL的错误信息。 由于STL分布在源代码中,编译器构建高效的可执行文件是很有必要的,但STL的特点之一就是错误消息不可读。
例如,如果您将vector<int>作为const引用参数(如果你需要的话)传递给某个函数:
void f(const vector<int>& v) { for( vector<int>::iterator it = v.begin(); // 嗯……错误在哪里?…… // ... // ... }
这里的错误是你试图使用begin()成员函数从一个const对象创建非const迭代器(尽管确定该错误比实际更正它更难)。 正确的代码如下所示:
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);
这将创建一个与(a + b)表达式类型相同的变量x。 请注意,typeof(v.size()) 对于任何STL容器类型都是unsigned的。 但是对于TopCoder,typeof最重要的应用就是遍历一个容器。 考虑以下宏:
#define tr(container, it) \ for(typeof(container.begin()) it = container.begin(); it != container.end(); it++)
通过使用这些宏,我们可以遍历各种容器,而不仅仅是vector。 这将为const对象生成const_iterator,为非const对象生成普通迭代器,这里永远不会有错误。
void f(const vector<int>& v) { int r = 0; tr(v, it) { r += (*it) * (*it); } return r; }
注意:为了提高可读性,我没有在#define的那一行加上额外的括号。 请参阅文章的后面以获得更准确的#define语句,你可以在练习室中做一些实验。
遍历宏对于vector来说并不是必需的,但对于更复杂的数据类型(不支持索引,迭代器是访问数据的唯一方式)来说,是非常方便的。我们稍后会在这篇文章中谈到。
vector中的数据操作
可以使用insert()函数将一个元素插入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中删除。
插入/删除技术是常见的,但不是对于所有STL容器都相同。
字符串(String)
有一个特殊的容器来处理字符串。 字符串容器与vector<char>有一些区别。 大多数差异都归结为字符串操作函数和内存管理策略。
String的substring函数没有迭代器,只有索引:
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"
注意对空字符串使用(s.length()-1),因为s.length()是unsigned的, unsigned(0) – 1绝对不是你想要的结果!
集合(Set)
总是难以决定先讲集合(set)还是映射(map)。 我的想法是,如果读者具有算法的基本知识,从“set”开始应该更容易理解。
考虑我们需要一个具有以下功能的容器:
- 添加一个元素,但不允许重复
- 删除元素
- 获取元素的数量(不同元素)
- 检查元素是否存在于集合中
这是一个很常见的任务。 STL为此提供了专用容器 - set。 set可以O(log N)的时间复杂度添加,删除和检查特定元素的存在,其中N是集合中对象的个数。向set添加元素时,重复的元素将被丢弃。返回集合中的元素的个数N是O(1)的。 稍后我们将讨论set和map的算法实现 - 现在,让我们来研究它们的接口:
set<int> s; for(int i = 1; i <= 100; i++) { s.insert(i); // 插入100个元素, [1..100] } s.insert(42); // 什么也不做, 42 已经存在于集合中 for(int i = 2; i <= 100; i += 2) { s.erase(i); // 删除偶数值 } int n = int(s.size()); // n是50
push_back()成员不能用于set。 这是有道理的:由于集合中的元素的顺序并不重要,因此push_back()在这里不适用。
由于set不是一个线性容器,所以不可能通过索引来获得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> > >。 如何遍历? 写下迭代器类型名称?哦,还是算了。我们使用遍历宏代替。
set< pair<string, pair< int, vector<int> > > SS; int total = 0; tr(SS, it) { total += it->second.first; }
注意‘it->second.first’ 的语法。由于'it'是一个迭代器,因此我们需要在操作之前从“it”获取一个对象。所以,正确的语法将是‘(*it).second.first’.。但是,写 ‘something->’ 比写‘(*something)’更容易。完整的解释是相当长的 - 只需要记住,对于迭代器,这两种语法都是允许的。
使用‘find()’成员函数确定一些元素是否存在于set中。不要混淆,尽管:STL中有几个‘find()’。有一个全局算法‘find()’,它需要两个迭代器以及元素,复杂度O(N)。可以使用它来搜索集合中的元素,但是为什么在存在O(log N)的算法的情况下使用O(N)的算法?在set和map(以及multiset/multimap,hash_map/hash_set等)中搜索时,不要使用全局查找 - 而是使用成员函数 ‘set::find()’。作为'顺序'查找,set::find将返回一个迭代器,或者是发现的元素,或者返回'end()'。因此,检查元素是否存在如下所示:
set<int> s; // ... if(s.find(42) != s.end()) { // 42 在集合中 } else { // 42 不在集合中 }
另外一个O(log N)的算法是一个叫做count的成员函数。有些人认为
if(s.count(42) != 0) { // … }
甚至
if(s.count(42)) { // … }
更便于书写。个人不这么认为。 在set/map中使用count()是没有道理的:元素存在或者不存在。 对于我来说,我更喜欢使用以下两个宏:
#define present(container, element) (container.find(element) != container.end()) #define cpresent(container, element) (find(all(container),element) != container.end())
(记住all(c)表示 “c.begin(), c.end()”)
在这里,‘present()’返回元素在有成员函数‘find()’ 的容器(即set / map等)中是否存在,而'cpresent'是用于vector的。
要从set中删除元素使用erase() 函数。
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存在于集合中 s.erase(it1, it2); // 注意10会被删除,而100还会留在集合中
set有一个区间构造器:
int data[5] = { 5, 1, 4, 2, 3 }; set<int> S(data, data+5);
这提供给我们一个简单的方法来去除vector中重复的元素,并排序:
vector<int> v; // … set<int> s(all(v)); vector<int> v2(all(s));
这里'v2'将包含与'v'相同的元素,但按升序排列并删除重复的元素。
任何可比较的元素都可以存储在set中。 这将在后面描述。
映射(Map)
有两个关于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和set非常相似,除了它包含的不是值而是pairs <key, value>。map保证一个特定的key最多只有一个pair存在。另一个非常令人愉快的事情是map已经定义了[]运算符。
使用我们的‘tr()’宏遍历map非常方便。 请注意,迭代器将是一个key和value的std::pair。 所以,要使用it->second获得值。 示例如下:
map<string, int> M; // … int r = 0; tr(M, it) { r += it->second; }
不要通过迭代器更改map元素的key,因为它可能会破坏map内部数据结构的完整性(见下文)。
map::find()和map::operator []之间有一个重要区别。 map::find()永远不会改变map的内容,而[]运算符在元素不存在的情况下会创建一个元素。在某些情况下可能非常方便,但是当你不想添加新元素时,在循环中多次使用[]运算符绝对不是个好主意。 这就是为什么如果将map作为const引用参数传递给某个函数时可能不用[]运算符:
void f(const map<string, int>& M) { if(M["the meaning"] == 42) { // 错误!不能对const的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时更改其键值:如果你的修改破坏了顺序,至少会导致容器算法的功能不正确。
map和set的元素总是有序的这个性质在解决TopCoder问题时经常可以实际使用。
另一个重要的是,map和set中的迭代器中定义了++和--运算符 。 因此,如果值42出现在集合中,并且不是第一个和最后一个元素,下面的代码就会生效:
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'是右边的第一个邻居。
更多算法
现在是更深入地谈论算法的时候了。大多数算法声明在#include <algorithm>标准头文件中。首先,STL提供三种非常简单的算法:min(a, b), max(a, b), swap(a, b)。这里min(a, b)和max(a, b) 返回两个元素的最小值和最大值,而swap(a, b)交换两个元素。
算法sort()也被广泛使用。调用sort(begin, end)按升序排列区间。注意sort()需要随机访问迭代器,因此它不能在所有容器上运行。然而你可能不会在已经有序的set上调用sort()。
你已经听说过算法find()。调用find(begin, end, element) 返回元素第一次出现的迭代器,如果没有找到该元素,则返回end()。除了find(…),count(begin, end, element) 返回一个元素在容器或容器的一部分中的出现次数。记住set和map具有成员函数find()和count(),它们是O(log N)的,而std::find()和std::count()是O(N)的。
另外两个有用的算法是next_permutation()和prev_permutation()。我们来谈谈next_permutation。调用next_permutation(begin, end)产生[begin, end)相同元素的下一个排列,如果当前是最后一个排列则返回false。因此,next_permutation使很多任务变得相当容易。如果你想检查所有的排列,只需要写:
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)
你经常需要做一些字符串处理/输入/输出。 C++为它提供了两个非常有趣的对象:'istringstream'和'ostringstream'。 它们都声明在#include <sstream>。
istringstream对象允许你像从标准输入一样读取字符串。 看源代码更容易理解:
void f(const string& s) { // 构造一个对象来解析字符串 istringstream is(s); // 存放数据的vector vector<int> v; // 如果可能读入字符串,加入vector中 int tmp; while(is >> tmp) { v.push_back(tmp); } }
ostringstream对象用来格式化输出。这里是代码:
string f(const vector<int>& v) { // 创建一个对象以格式化输出 ostringstream os; // 将vector<int>中的所有元素作为文本复制到string stream tr(v, it) { os << ' ' << *it; } // 从string stream中获得string string s = os.str(); // 删除第一个空格字符 if(!s.empty()) { // 这里注意空字符串 s = s.substr(1); } return s; }
总结
要继续使用STL,我总结要用到的模板列表。 这将简化代码示例的阅读,并且希望能提高您的TopCoder技能。 模板和宏的简短列表如下:
typedef vector<int> vi; typedef vector<vi> vvi; typedef pair<int,int> ii; #define sz(a) int((a).size()) #define pb push_back #define 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> >)起一个简短的别名是非常方便的,但是这个列表只包含理解下列文本所需的宏。
另一个注意事项是:当#define左侧的token出现在右侧时,应该放在大括号中,以避免许多非常不寻常的问题。