用STL给C++充电:第一部分

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++)中,仅仅有一种容器类型:数组。

问题并不在于数组有限制(虽然,例如,它可能在运行的时候去决定数组的大小),然而,主要问题是许多题目需要一个带有更强大功能的容器。

例如,我们需要进行一个或更多的下面的操作:

 

  • 从一个容器中添加一个字符串。
  • 从一个容器中移除一个字符串。
  • 判断一个字符串是否在容器中。
  • 从一个容器中返回一些不同类型的元素。
  • 遍历一个容器,并以某种顺序得到一个字符串列表。
当然,用普通的数组也能实现这种功能,但是这些琐碎的实现将是非常低效的。你可以创建一个“ tree- of hash- structure(树的散列结构)”去以一种更快的方式解决它。

但是思考一下:这样一个容器的实现是不是取决于我们将要存储的元素类型?我们是否要重新实现这个模块使其功能性更强,例如,对于平面上的点而不是线上的点?

如果不是,我们能开发出一次这样的容器的接口,然后可以让任何类型的数据都使用。总之,那就是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容器中非常流行使用的函数是push_back。push_back在vector容器的末尾添加一个元素,增加一个单位的容器大小。思考一下下面的例子:
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的大小,并用一系列的0去填充新创建的元素。
注意如果你在resize()后面使用push_back(),它将在重新分配的大小后边添加新元素,但新元素不是插在它里面。在上面的例子中,使vector的大小变成25,然而如果我们使在第二个循环中使用push_back(),它将变成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< vector<int> > Matrix;
如何创建给定大小的二位数组的方法,对你来说应该已经明了了。
int N, N; 
 // ... 
 vector< vector<int> > Matrix(N, vector<int>(M, -1)); 
这里我们创建了一个名叫Matrix的N*N型二维数组,并用-1填充它。
给vector添加元素的最简单方式是push_back()。但是如果我们想在除了末尾的其他位置添加元素该怎么办呢?为此,有一个insert()成员函数。并且同样地,也有一个erase()成员函数去擦除元素。但是首先我们需要说几句关于iterator(迭代器)的事。
你应该记得一个非常重要的事:当vector作为参数被传入某个函数的时候,这个vector的拷贝确实被创建了。当它们不是真的必要的时候创建一个新的vector容器可能花费大量的时间的内存。事实上,当它被作为参数传递的时候,很难找到一个拷贝vector这个事情真的那么必要的任务。因此,你从来都不应该这样写:
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)很容易实现排序。
例如,如果你想要给一个整型数组进行排序使它们形成一个多边形,那么把他们放进 vector< pair<double, pair<int,int> >之中将是一个好主意,在这里每个vector类型的元素是{ polar angle, { x, y } }(注:极坐标角度,(x,y)表示坐标)调用STL排序函数将得到你所渴望得到的点的顺序。
pairs也被广泛应用于关联容器,这点我们将在下面的文章中谈到。

Iterators(迭代器)
iterators是什么?在STL中,访问容器中的数据的时候最普遍使用的方式是iterators(迭代器),思考一个简单的问题:逆置含有N个整型元素的数组A。让我们先从一个C语言风格的解决方案开始:
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)得到数值
  • 指针自增,和
  • 指针自减
现在想象一下你正面对第二个问题:逆置双向链表中的内容,或者其中的一部分。第一段代码,使用下标的方法,肯定不能起作用。至少,他不能即使解决这个问题,因为仅仅在O(1)或O(N)的时间复杂度内,靠使用下标的方法在双向链表中不能起作用,因此,整个算法将在O(N^2)的时间复杂度内运行。-_-|||
但是看一下:第二段代码能杜宇任何类指针对象起作用。唯一的限制就是对象要能够进行上述的操作:取值(单目运算符*),比较(<),和自增运算/i自减运算(++/--)。带有这些性能的对象都能和一种被称为iterators(迭代器)的容器联系起来。STL中的任何容器都可以通过迭代器的方式进行遍历。虽然对于vector类型并不经常使用迭代器来遍历,但是对于其他类型的容器来说它还是很重要的。
因此,我们有什么?一个拥有类似指针语法的对象。下面的操作被定义为迭代器: get value of an iterator, int x = *it;
  • 得到迭代器的值,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; 
                } 
           } 
      } 
 } 
这段代码与先前的代码主要的不同点是,我们没有使用“<”去比较迭代器,而是仅仅使用了一个“==”。此外,不要恐慌如果你是惊讶于函数原型:template 仅仅是去声明一个函数,在任何合适的参数类型上都能执行的函数。这个行数应该能在指向任何对象类型的指针和迭代器上完美执行。
让我们回到STL。STL算法一直使用两种迭代器,叫做“begin”和“end”。这个end迭代器不是指向最后一个对象,而是指向第一个无效对象,或者指向最后一个对象后面的对象。它通常非常方便。
每个STL容器都有成员函数begin()和end()来返回那个容器的begin和end迭代器。
基于这些原则,当且仅当c是空的时候c.begin() == c.end() ,并且 c.end() – c.begin() 总等于 c.size()。(最后一个语句有效的条件是迭代器能被减,也就是begin() and 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; 
                     } 
                } 
           } 
      } 
 } 
注意这个函数和标准函数 std::reverse(T begin, T end) 做了同样地事,这个标准函数能在算法模块 (#include <algorithm>)中找到。
另外,任何有足够功能的对象都能作为迭代器传递给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]))); 
最后一行展示了一个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的前半部分。
这里是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”或者“::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()算法在一个区间内寻找合适的元素。如果元素被找到,指向第一个被发现元素的迭代器被返回。否则,返回值等于区间的end。看看这段代码:
 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(最大元素)算法返回各自的元素。要得到最小或最大元素的值,就像find()方法一样,使用*min_element(...) o或者*max_element(...) ,减去容器或者一个值域(原文为range,可理解为是数组)的起始迭代器,去得到数组下标:
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() 
不要将这些宏的右边整个扩进括号里——那将是错误的!
另外一个算法是sort(),它很容易被使用。思考下面的例子:
vector<int> X; 

 // ... 

 sort(X.begin(), X.end()); // 以升序给数组排序 
 sort(all(X)); // 以升序给数组排序,使用我们的 宏#define 
 sort(X.rbegin(), X.rend()); // 使用逆置迭代器,给数组降序排序
编译 STL 程序
在这里一个值得指出的事情就是STL的错误信息。当STL分布在源程序中,对于编译器来说组建出有效的可执行文件变成必须的,STL有一个习惯是错误信息难以理解。
例如,如果你把一个vector<int>作为常量引用参数(当你应该会用的时候)传递给某个函数的时候:
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中的数据操作
我们可以使用inser()函数来插入一个元素到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中被擦除。
inset/erase 的手法非常普遍,但是对于所有STL容器来说并不是完全相同。
String(字符串)
有一种特殊的容器来进行字符串的操作。string容器和vector<char>有一点不同。
最大的不同之处归根结底是字符串的操作函数和内存管理的规则。
字符串有不用迭代器的字串函数,仅使用下标:
 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()-1)是无符号类型,并且(s.length()-1)没有被明确地定义成你期望的类型!

Set(集合)
set或map,先来描述哪一个容器一直一来都难以决定。我的观点是,如果读者拥有基本的算法知识,先从“set”开始,会更容易理解。
思考一下我们需要一个带有下列功能的容器:
  • 添加一个元素,但是不允许创建副本
  • 移除一个元素
  • 统计元素个数(截然不同的元素)
  • 检查元素现在是否在set中。
这是一个十分频繁被用到的任务。STL对于它提供了特殊的容器——set。set能够在O(logN)的时间复杂度内添加,移除和检查一个特殊元素的存在情况,这里N是指set中对象的个数。当我们添加元素到set中,副本被丢弃。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 已经在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中使用下标来获取元素是不可能的。因此,遍历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).second.first”然而,写成 'something->' 比'(*something)'更容易。完整的解释将特别冗长——对于迭代器和必需的语法仅仅记住就可以了。
确定某个元素是否在set中使用find()成员函数。不要感到困惑,虽然:在STL中有几个find()了。有一个全局的算法find(),获取两个迭代器,元素并且在O(N)的时间内执行。使用它在set中搜索元素是可能的,但是当我们有一个O(logN)的算法存在的时候我们为什么嗨哟啊使用一个O(N)的算法呢?当我们在set和map(  multiset/multimap, hash_map/hash_set等等也是如此)中查找的时候不要使用全局的find——代替它,使用成员函数“set::find()”。当依次查找的时候,set::find将返回一个迭代器,或者是被发现元素的迭代器,或者是end()的迭代器。因此,元素的存在性检查就像这样:
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()”
这里,present()使用成员函数find()翻译是否元素在容器中(就是set/map等等)。而cpreset()是针对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存在set。 
 s.erase(it1, it2); // 注意10将被删除,但是100还在容器中
set有一个区间的构造:
int data[5] = { 5, 1, 4, 2, 3 }; 
set<int> S(data, data+5); 
它提供给我们一个简单的方式去丢掉vector中的副本,并且给它排序。
任何可以比较的元素都能存储在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有被定义operator[]。
遍历map和women“tr()”宏一样容易,注意迭代器将是一个key(键)和value(值)的std::pair类型。因此,要得到值使用it->second。下面是例子:
 map<string, int> M; 
 // … 
 int r = 0; 
 tr(M, it) { 
      r += it->second; 
 } 
不要用迭代器去改变map的键,因为注意可能会破坏map内部的数据结构的完整性(向下看)。
map::find() 和 map::operator []之间有一个重要的不同之处。虽然map::find() 从来不会改变map的内容,但是 map::operator []在元素不存在的时候会创建一个元素出来。在一些情况下,这可能非常方便,但是当我们不想添加新元素的时候,在一个循环中多次使用操作符[]确实是一个坏主意。这就是为什么当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的时候,强烈推荐你不要改变键值的原因。如果你做了破坏顺序的更改,它将导致容器算法的不适当的功能,至少如此。
但是事实上当解决TopCoder问题的时候,map和set中的元素一直保持有序是十分有用的。
另一个十分重要的事情是操作符++和--被定义在map和set的迭代器上。因此,如果值42在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右边第一个元素。
更多关于算法的事
是时候去讲一点更深的算法了。更多的算法被声明在#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(),因为set总是有序的。
你可能已经听说了算法find().调用find(begin,end,element)返回第一个被发现元素的迭代器,或者在没有元素被发现的时候返回end。与find(...)不同,count(begin,end,element)返回一个容器或一对容器中元素出现的次数。记住set和map有在O(logN)时间内执行的的成员函数find()和count(),然而std::find()和std::count()的效率是O(N)。
其他的算法有next_permutation() 和prev_permutation()。让我们说说next_permutation()。调用next_permutation(begin,end)使区间[begin,end)得到同样元素全排列中下一个序列,或者如果序列已经是全排列中的最后一个序列了将返回错误。因此,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) { 

      // 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; 
 } 
总结
继续讲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 
 #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> >)来说有一个简短的别名是十分方便的。但是这个列表不仅仅包含需要理解下面文字的宏。
另外一个注意事项要牢记:当一个#define左边的一个token出现在了右边,它应该被放置在括号里去避免需要重大的问题。

接下来:第二部分:C++STL高级使用(待续)


 

posted @ 2013-06-10 20:29  爱生活,爱编程  阅读(244)  评论(0编辑  收藏  举报