一切随心

编程菜园
数据加载中……
Power up C++ with the Standard Template Library: Part I[翻译]

                      Power up C++ with the Standard Template Library: Part I
                     【原文见: http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=standardTemplateLibrary
                                                                                                                    作者 By DmitryKorolev
                                                                                                                               Topcoder 成员
                                                                                                                     翻译 农夫三拳@seu

 Containers
 Before we begin
 Vector
 Pairs
 Iterators
 Compiling STL Programs
 Data manipulation in Vector
 String
 Set
 Map
  Notice on Map and Set
  More on algorithms
  String Streams
  Summary


     也许你已经使用C++作为主要编程语言来解决Topcoder中的问题了,这就意味着你已经在简单的使用
STL了,因为数组和字符串都是以STL对象的方式传入到你的函数中的。也许你已经注意到了,有许多
程序员他们写代码要比你快并且代码也比你的简洁。

     也许你不是一个C++程序员,但是因为C++的强大功能以及它的库(或者因为你在Topcoder
practice 房间和比赛里面看到的简短的解决方案),你想成为一个这样的程序员。

    不管你来自哪里,这篇文章将会帮助你掌握它。 我们将会看到STL中一些强大的特性--一个强大的工具有的时候可以为你在算法比赛中节省很多时间。

    最简单的方式去熟悉STL就是从它的容器入手了。

Containers

    任何时候当你需要处理很多元素的时候你需要一些种类的容器。在C当中,这里只有一种这种类型的容器,那就是数组。

    现在的问题不是数组的功能有限(比如它不能够在运行时决定数组的大小,而是有许多问题需要一个具有更多功能的容器。

    比方说, 我们需要一下的一种或几种操作:

 在容器中添加一些字符串
 从容器中移除一些字符串
 查看指定字符串是否在容器中
 返回容器中一些不同元素
 遍历整个容器并获得一定顺序的被添加的字符串的列表

    当然,你可以在一个顺序的数组里面实现以上的功能。但是这些普通实现会变得非常低效。你也可以
创建树型或者散列表结构来更得到一个更快的解决方案, 但是请思考一点:这个容器的实现是否依赖于
它将存储的元素呢?例如当你需要存储一个平面上的点的时候,你是否需要重新实现这个模块来使得它工作呢?

    如果不想这样,我们可以为这样的容器创建一个接口,那样我们就可以在任何地方使用任何数据类型了。
其实,那正是STL 容器的概念。
 
Before we begin

当程序中使用STL的时候,需要包含相应的文件,对于大多数的容器,包含文件的名字
与容器的名字相同,并且不需要后缀。如果你将要使用stack,只要在你的程序的开头
加上下面的一句话就可以了:

#include <stack>

容器类型(算法,函数对象和所有的STL内容)并不是定义在全局的名字空间中的,而是在一个特定
的名叫“std”的名字空间里的。将下面的话添加在include之后和你的代码之前:

using namespace std;

 另外一个重要的事情你要记住的是容器的类型是模版参数。程序当中模版参数使用 '<' 和 '>'
标注的。例如:

vector<int> N;

当使用嵌套的构造时,确保括号不要紧随着另外一个--中间最好留个空格。 

vector< vector<int> > CorrectDefinition; 
vector
<vector<int>> WrongDefinition; // Wrong: compiler may be confused by 'operator >>'

Vector
最简单的STL容器是vector。vector实际上是一个有着扩展功能的数组。此外,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>的数组,并且被初始化为空。
在很多情形下,这并不是我们所想要的。这里应使用圆括号而不是方括号。

最常使用的vector的特征是得到它的大小

int elements_count = v.size(); 

两个注意点:
第一,size()是无符号的,这个也许有的时候会产生一些问题。
相应的,我通常定义一些宏,比如sz(C),返回代表C的大小的带符号的数。
第二,将v.size()和0比较不是一个好的习惯,如果你想要知道这个容器是否
为空。你最好使用empty()函数: 

bool is_nonempty_notgood = (v.size() >= 0); // Try to avoid this
 bool is_nonempty_ok = !v.empty(); 

这个是因为并不是所有的容器可以在O(1)的复杂度内得到它的大小,并且你
应当绝对的避免计算一个双端链表所有的元素个数而仅仅是为了知道它是不是为
空。

另外一个经常使用的函数是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不会为一个元素每次都分配。相反的,vector在使用push_buck增加元素的
时候会比它需要的分配的多。唯一你要担心的是内存使用,但是在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_buck(),它将会在新增加的大小后继续增加元素,而不是
填充。在上面的例子中,vector最后的大小是25。而当我们在第二个循环中使用push_buch,它
的大小将变为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); // Writes to elements with indices [25..30), not [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<string> names(20, “Unknown”); 

记住你可以创建任何类型的vector

多维数组也很重要,最简单的方法来创建一个二维数组是通过声明一个元素为vector的vector。

vector< vector<int> > Matrix; 

下面很清楚的向你显示如何创建一个指定大小的二维数组: 

int N, N; 
 
//  
 vector< vector<int> > Matrix(N, vector<int>(M, -1)); 

这里我们创建了一个N*M的二维数组,并且初始化为-1

最简单的向一个vector中添加数据是使用push_back(),但是如果我们不想在尾部添加呢?这里我们利用insert()
函数来达到这个目的。并且这里也有erase()函数来清除元素。但是首先我们需要谈谈迭代器。

你需要记住一些非常重要的事情:当vector作为参数传递给一些函数的时候。一份拷贝也会被创建。创建一个
这样的vector是非常耗时间和内存的,并且我们其实也不需要。事实上,发现一个需要vector的拷贝作为参数
的情况是很少的,所以,你不应该写:

void some_function(vector<int> v) // Never do it unless you’re sure what you do! 
      
//  
 }
 

相反的,你应该写:

void some_function(const vector<int>& v) // OK 
      
//  
 }
 

如果你需要在函数中更改vector中的内容,忽略const修饰符就可以了。

int modify_vector(vector<int>& v) // Correct 
      V[0]++
 }
 


Pairs

在我们谈论迭代器之前,让我们先谈谈pairs,Pairs在STL中被广泛使用着,像TopCoder
SRM 250 中的500分的简单问题,通常需要一些有着一对元素的简单数据结构,而STL中的
std::pair正好是一对元素。最简单的形式如下:

template<typename T1, typename T2> struct pair 
      T1 first; 
      T2 second; 
 }

简单的有pair<int, int>是一对整数,复杂的 pair<string, pair<int,int> >是一对有着
字符串和两个整数的pair。在第二种情况下,用法如下:

pair<string, pair<int,int> > P; 
 
string s = P.first; // extract string 
 int x = P.second.first; // extract first int 
 int y = P.second.second; // extract second int 

pairs的最大好处在于他们内建了比较操作,pairs比较是从第一个比较到第二个。
如果第一个元素不相等,那么结果仅仅基于第一个元素的比较;第二个元素比较被
用到仅当第一个元素相等的情况。 数组(或者vector)能够轻易的被STL中内部函数
排序。

例如,如果你想对一个有着整数点的数组排序而使得它能够构成一个多边形,
把他们放到 vector< pair<double,pair<int, int> >是一个好主意,这里的元素
是{极坐标,{x, y}}。调用STL中的一个排序函数能够给你所需要的顺序。

此外,pairs也被广泛的用在联合容器中,我们稍后将会在这里谈到它。


Iterators

什么是迭代器呢?在STL中,迭代器是最常使用的访问容器中数据的方法。考虑这样一个简单的问题:
将一个有着N个int类型元素的数组倒序。让我们先看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指针上进行了4种不同的操作:

比较指针(first < last)
通过指针获取值 (*first, *last)
增加指针的值
减少指针的值

现在考虑处理第二个问题:将整个或者部分的双端链表进行倒序。第一段使用索引的代码将不在有效。
至少,它在时间方面是没有优势的,因为在双端链表中不可能在O(1)的复杂度内通过索引获取到一个元素,
只有在O(N)才行,所以整个算法将在O(N^2)内完成。厄。。。

但是注意: 第二段代码可以在任何指针类型的对象中起作用。仅有的约束是对象只能够进行上述的操作:
取值(unary *),比较(<),和自增自减(++/--). 与容器相关并且具有上述属性的对象被喻为迭代器。任何
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". 这个end 迭代器
并不是指向最后一个元素,而是指向第一个非法的元素,也就是最后一个元素后面的一个。
通常使用它是非常方便的。

每一个STL容器都有begin()和end()成员函数返回那个容器的begin和end 迭代器。

基于这些原理,我们可以得到,c.begin() == c.end() 只有当c是空的情况下,并且 c.end() - c.begin()总是等于
c.size().(这个式子当迭代器可以进行减法操作时才是合法的。也就是说, begin()和end()返回了随机访问迭代器,而这并不是对所有的容器都适合。前面双端链表就是一个例子)

服从STL规则的倒置函数应该像下面这么写:

template<typename T> void reverse_array_stl_compliant(T *begin, T *end) 
      
// We should at first decrement 'end' 
      
// But only for non-empty range 
      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[] = 235711131719232931 }
 vector
<int> primes(data, data+(sizeof(data) / sizeof(data[0]))); 

最后一句话展示了从一个C的数组构造vector的例子。没有加索引的data被当成指向数组开始的指针,'data+N'指向第N个元素。当N是这个数组的长度时,'data+N'指向第一个不在数组的元素, 所以‘data + data的长度'可以被当成数组'data'的end迭代器。表达式 'sizeof(data)/sizeof(data[0])' 返回data数组的大小,但是这样的语句只能用在一小部分情况下,所以不要在任何地方使用它除了像以上的情况。(C程序员会同意我的说法的!)

更进一步的,我们可以甚至使用下面的构造: 

vector<int> v;
// 
vector<int> v2(v.begin(), v.begin() + (v.size()/2));

这样就创建的v2向量等同于v向量的前半部分。

下面是使用reverse函数的一个例子 

int data[10= 135791113151719 }
 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()); 

这样创建的v2向量将等于v的前半部分,顺序是从后向前。

要创建一个迭代器对象,我们通常要指定它的类型。迭代器的类型通常可以由该类型的容器加上
"::iterator","::const_iterator","::reverse_iterator" 或者"const_reverse_iterator"来
构造。因此vector可以使用下面的方式进行遍历: 

vector<int> v; 

 
//  

 
// Traverse all container, from begin() to end() 
 for(vector<int>::iterator it = v.begin(); it != v.end(); it++
      
*it++// Increment the value iterator is pointing to 
 }
 

建议你使用 '!=' 而不是 '<' 并且 '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(...)或者 *max_element(...)。要获得下标的话,减去
容器或者一定范围的开始迭代器就可以了: 

int data[5= 15243 }
 vector
<int> X(data, data+5); 
 
int v1 = *max_element(X.begin(), X.end()); // Returns value of max element in vector 
 int i1 = min_element(X.begin(), X.end()) – X.begin; // Returns index of min element in vector 

 
int v2 = *max_element(data, data+5); // Returns value of max element in array 
 int i3 = min_element(data, data+5) – data; // Returns index of min element in array 

现在你可以看到有用的宏了:

 #define all(c) c.begin(), c.end() 

 不要把右边的部分放到圆括号里面--那样是错的!

另外一个好用的算法是sort(), 它使用起来非常简单。 看下面的一个例子:

vector<int> X; 

 
//  

 sort(X.begin(), X.end()); 
// Sort array in ascending order 
 sort(all(X)); // Sort array in ascending order, use our #define 
 sort(X.rbegin(), X.rend()); // Sort array in descending order using with reverse iterators 

Compiling STL Programs

一个需要值得指出的事情就是STL的错误消息。由于STL已经被广泛的使用在源代码中了,所以有必要要求
编译器去创建高效的可执行文件,STL的一个习惯在于它的难读的错误消息.

例如,如果你传递一个vector<int>的常引用给一些函数的时候:

 void f(const vector<int>& v) 
      
for
           vector
<int>::iterator it = v.begin(); // hm where’s the error?.. 
           
//  
      
//  
 }
 

这里的错误是你正在从一个常量对象中创建一个非指向常量的迭代器(实际上找出错误比修改它要难).正确的代码如下:

void f(const vector<int>& v) 
      
int r </