std

C语言函数中,可以用qsort 来进行 结构体数组的排序,但是如果上升到C++中,对vector<NODE>的排序 qsort 就无能为力了。。。

不过C++ <algorithm> 函数库中的sort函数可以对 结构体vector进行排序。 前提是 要对 node  进行改造

例如 一个含有四项key 值的node 如下

 


struct node
{
    string name;
    int medal[3];

} ;

 

要对其以medal数组 降序排序,若 medal 数组完全相等,则以 name 的字典序排序

 要调用  sort 函数对 node 进行排序,则 将node 改写为


struct node
{
    string name;
    int medal[3];
    bool operator <(const node& other) const
    {
        if(medal[0] > other.medal[0]) return true;
        if(medal[0] < other.medal[0]) return false;
        if(medal[1] > other.medal[1]) return true;
        if(medal[1] < other.medal[1]) return false;
        if(medal[2] > other.medal[2]) return true;
        if(medal[2] < other.medal[2]) return false;
        if(name < other.name) return true;
        return false;
    }

} ;

 

例如在全局声明了一个vector<node> V;

则调用sort 如下 sort(V.begin(),V.end());

详细代码如下 (以TCO SRM 209 DIV2的500分题为例)

 
Problem Statement
   
The Olympic Games in Athens end tomorrow. Given the results of the olympic disciplines, generate and return the medal table.  The results of the disciplines are given as a vector <string> results, where each element is in the format "GGG SSS BBB". GGG, SSS and BBB are the 3-letter country codes (three capital letters from 'A' to 'Z') of the countries winning the gold, silver and bronze medal, respectively.  The medal table is a vector <string> with an element for each country appearing in results. Each element has to be in the format "CCO G S B" (quotes for clarity), where G, S and B are the number of gold, silver and bronze medals won by country CCO, e.g. "AUT 1 4 1". The numbers should not have any extra leading zeros. Sort the elements by the number of gold medals won in decreasing order. If several countries are tied, sort the tied countries by the number of silver medals won in decreasing order. If some countries are still tied, sort the tied countries by the number of bronze medals won in decreasing order. If a tie still remains, sort the tied countries by their 3-letter code in ascending alphabetical order.
Definition
   
Class:
MedalTable
Method:
generate
Parameters:
vector <string>
Returns:
vector <string>
Method signature:
vector <string> generate(vector <string> results)
(be sure your method is public)


#include <vector>
#include <list>
#include <map>
#include <set>
#include <deque>
#include <stack>
#include <bitset>
#include <algorithm>
#include <functional>
#include <numeric>
#include <utility>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <ctime>

using namespace std;

struct node
{
    string name;
    int medal[3];
    bool operator <(const node& other) const
    {
        if(medal[0] > other.medal[0]) return true;
        if(medal[0] < other.medal[0]) return false;
        if(medal[1] > other.medal[1]) return true;
        if(medal[1] < other.medal[1]) return false;
        if(medal[2] > other.medal[2]) return true;
        if(medal[2] < other.medal[2]) return false;
        if(name < other.name) return true;
        return false;
    }
} ;

class MedalTable {
public:
    set<string> Set;
    vector<node> V;
    vector <string> generate(vector <string>);
};

vector <string> MedalTable::generate(vector <string> results) {
    vector<string> res;
    for(int i = 0;i < results.size();i++)
    {
        string a,b,c;
        istringstream s(results[i]);
        s>> a>> b >> c;
        Set.insert(a);
        Set.insert(b);
        Set.insert(c);
    }
    for(set<string>::iterator it = Set.begin();it != Set.end();it++)
    {
        node Node;
        Node.name = *it;
        Node.medal[0] = Node.medal[1] = Node.medal[2] = 0;
        V.push_back(Node);
    }
    for(int i = 0;i < results.size();i++)
    {
        istringstream s(results[i]);
        string a[3];
        for(int j = 0;j < 3;j++)
        {
            s>>a[j];
            for(int k = 0;k < V.size();k++)
            {
                if(a[j] == V[k].name)
                    V[k].medal[j] ++;
            }
        }
    }
    sort(V.begin(),V.end());
    for(int i = 0;i < V.size();i++)
    {
        ostringstream outa;
        outa<< V[i].name <<' '<<V[i].medal[0]<<' '<<V[i].medal[1]<<' '<<V[i].medal[2];
        res.push_back(outa.str());
    }
    return res;
}

 

 

 

 

一些基础概念的定义

模板(Template)——类(以及结构等各种数据类型和函数)的宏(macro)。有时叫做甜饼切割机 (cookie cutter),正规的名称应叫做范型(generic)——一个类的模板叫做范型类(generic class),而一个函数的模板也自然而然地被叫做范型函数(generic function)。
STL——标准模板库,一些聪明人写的一些模板,现在已成为每个人所使用的标准C++语言中的一部分。
容器(Container)——可容纳一些数据的模板类。STL中有vector,set,map,multimap和deque等容器。
向量(Vector)——基本数组模板,这是一个容器。
游标(Iterator)——这是一个奇特的东西,它是一个指针,用来指向STL容器中的元素,也可以指向其它的元素。

Hello World程序

我愿意在我的黄金时间在这里写下我的程序:一个hello world程序。这个程序将一个字符串传送到一个字符向量中,然后每次显示向量中的一个字符。向量就像是盛放变长数组的花园,大约所有STL容器中有一半 是基于向量的,如果你掌握了这个程序,你便差不多掌握了整个STL的一半了。


//程序:vector演示一
//目的:理解STL中的向量

// #include "stdafx.h" -如果你使用预编译的头文件就包含这个头文件
#include <vector>  // STL向量的头文件。这里没有".h"。
#include <iostream>  // 包含cout对象的头文件。
using namespace std;  //保证在程序中可以使用std命名空间中的成员。

char* szHW = "Hello World"; 
//这是一个字符数组,以”/0”结束。

int main(int argc, char* argv[])
{
  vector <char> vec;  //声明一个字符向量vector (STL中的数组)

  //为字符数组定义一个游标iterator。
  vector <char>::iterator vi;

  //初始化字符向量,对整个字符串进行循环,
  //用来把数据填放到字符向量中,直到遇到”/0”时结束。
  char* cptr = szHW;  // 将一个指针指向“Hello World”字符串
  while (*cptr != '/0')
  {  vec.push_back(*cptr);  cptr++;  }
  // push_back函数将数据放在向量的尾部。

  // 将向量中的字符一个个地显示在控制台
  for (vi=vec.begin(); vi!=vec.end(); vi++) 
  // 这是STL循环的规范化的开始——通常是 "!=" , 而不是 "<"
  // 因为"<" 在一些容器中没有定义。
  // begin()返回向量起始元素的游标(iterator),end()返回向量末尾元素的游标(iterator)。
  {  cout << *vi;  }  // 使用运算符 “*” 将数据从游标指针中提取出来。
  cout << endl;  // 换行

  return 0;
}

push_back是将数据放入vector(向量)或deque(双端队列)的标准函数。Insert是一个与之类似的函数,然而它在所有容器中 都可以使用,但是用法更加复杂。end()实际上是取末尾加一(取容器中末尾的前一个元素),以便让循环正确运行——它返回的指针指向最靠近数组界限的数 据。就像普通循环中的数组,比如for (i=0; i<6; i++) {ar[i] = i;} ——ar[6]是不存在的,在循环中不会达到这个元素,所以在循环中不会出现问题。

STL的烦恼之一——初始化

STL令人烦恼的地方是在它初始化的时候。STL中容器的初始化比C/C++数组初始化要麻烦的多。你只能一个元素一个元素地来,或者先初始化一个普通数组再通过转化填放到容器中。我认为人们通常可以这样做:


//程序:初始化演示
//目的:为了说明STL中的向量是怎样初始化的。

#include <cstring>  // <cstring>和<string.h>相同
#include <vector>
using namespace std;

int ar[10] = {  12, 45, 234, 64, 12, 35, 63, 23, 12, 55  };
char* str = "Hello World";

int main(int argc, char* argv[])
{
  vector <int> vec1(ar, ar+10);
  vector <char> vec2(str, str+strlen(str));
  return 0;
}

在编程中,有很多种方法来完成同样的工作。另一种填充向量的方法是用更加熟悉的方括号,比如下面的程序:

//程序:vector演示二
//目的:理解带有数组下标和方括号的STL向量

#include <cstring>
#include <vector>
#include <iostream>
using namespace std;

char* szHW = "Hello World";
int main(int argc, char* argv[])
{
  vector <char> vec(strlen(sHW)); //为向量分配内存空间
  int i, k = 0;
  char* cptr = szHW;
  while (*cptr != '/0')
  {  vec[k] = *cptr;  cptr++;  k++;  }
  for (i=0; i<vec.size(); i++)
  {  cout << vec[i];  }
  cout << endl;
  return 0;
}

这个例子更加清晰,但是对游标(iterator)的操作少了,并且定义了额外的整形数作为下标,而且,你必须清楚地在程序中说明为向量分配多少内存空间。

命名空间(Namespace)

与STL相关的概念是命名空间(namespace)。STL定义在std命名空间中。有3种方法声明使用的命名空间:

1.用using关键字使用这个命名空间,在文件的顶部,但在声明的头文件下面加入:
using namespace std;
这对单个工程来说是最简单也是最好的方法,这个方法可以把你的代码限定在std命名空间中。

2.使用每一个模板前对每一个要使用的对象进行声明(就像原形化):
using std::cout;
using std::endl;
using std::flush;
using std::set;
using std::inserter;
尽管这样写有些冗长,但可以对记忆使用的函数比较有利,并且你可以容易地声明并使用其他命名空间中的成员。

3.在每一次使用std命名空间中的模版时,使用std域标识符。比如:
typedef std::vector VEC_STR;
这种方法虽然写起来比较冗长,但是是在混合使用多个命名空间时的最好方法。一些STL的狂热者一直使用这种方法,并且把不使用这种方法的人视为异类。一些人会通过这种方法建立一些宏来简化问题。

除此之外,你可以把using namespace std加入到任何域中,比如可以加入到函数的头部或一个控制循环体中。

一些建议

为了避免在调试模式(debug mode)出现恼人的警告,使用下面的编译器命令:

#pragma warning(disable: 4786)

另一条需要注意的是,你必须确保在两个尖括号之间或尖括号和名字之间用空格隔开,因为是为了避免同“>>”移位运算符混淆。比如
vector <list<int>> veclis;
这样写会报错,而这样写:
vector <list <int> > veclis;
就可以避免错误。

 

 

 

 

set是关联容器。其键值就是实值,实值就是键值,不可以有重复,所以我们不能 通过set的迭代器来改变set的元素的值,set拥有和list相同的特性:当对他进行插入和删除操作的时候,操作之前的迭代器依然有效。当然删除了的 那个就没效了。set的底层结构是RB-tree,所以是有序的。

   stl中特别提供了一种针对set的操作的算法:交集set_intersection,并集set_union,差集set_difference。对称差集set_symeetric_difference,这些算法稍后会讲到。

一:set模板类的声明。

template <
   class Key,
   class Traits=less<Key>,
   class Allocator=allocator<Key>
>
class set。

其中个参数的意义如下:

key:要放入set里的数据类型,可以是任何类型的数据。

Traits:这是一个仿函数(关于仿函数是什么,我后面的文章会讲到)。提供了具有比较功能的仿函数,来觉得元素在set里的排 列的顺序,这是一个可选的参数,默认的是std::less<key>,如果要自己提供这个参数,那么必须要遵循此规则:具有两个参数,返回 类型为bool。

Allocator:空间配置器,这个参数是可选的,默认的是std::allocator<key>.

二:set里的基本操作

我们可以通过下面的方法来实例化一个set对

std::set<int> s;那个s这个对象里面存贮的元素是从小到大排序的,(因为用std::less作为比较工具。)

如果要想在s里面插入数据,可以用inset函数(set没用重载[]操作,因为set本生的值和索引是相同的)

s.insert(3);s.insert(5).....

因为set是集合,那么集合本身就要求是唯一性,所以如果要像set里面插入数据和以前的数据有重合,那么插入不成功。

可以通过下面的方法来遍历set里面的元素

std::set<int>::iterator it = s.begin();
while(it!=s.end())
{
   cout<<*it++<<endl;//迭代器依次后移,直到末尾。
}

如果要查找一个元素用find函数,it = s.find(3);这样it是指向3的那个元素的。可以通过rbegin,rend来逆向遍历

std::set<int>::reverse_iterator it = s.rbegin();

while(it!=s.rend())

{cout<<*it++<<endl;}

还有其他的一些操作在这就不一一列出了。

三:与set相关的一组算法

set_intersection() :这个函数是求两个集合的交集。下面是stl里的源代码

template<class _InIt1,
class _InIt2,
class _OutIt> inline
_OutIt set_intersection(_InIt1 _First1, _InIt1 _Last1,
   _InIt2 _First2, _InIt2 _Last2, _OutIt _Dest)
{ // AND sets [_First1, _Last1) and [_First2, _Last2), using operator<
for (; _First1 != _Last1 && _First2 != _Last2; )
   if (*_First1 < *_First2)
    ++_First1;
   else if (*_First2 < *_First1)
    ++_First2;
   else
    *_Dest++ = *_First1++, ++_First2;
return (_Dest);
}

这是个模板函数,从上面的算法可以看出,传进去的两个容器必须是有序的。_Dest指向输出的容器,这个容器必须是预先分配好空间的,否则会出错的,返回值指向保存结果的容器的尾端的下一个位置。eg.

set_union() :求两个集合的并集,参数要求同上。

std::set_difference():差集

set_symmetric_difference():得到的结果是第一个迭代器相对于第二个的差集并上第二个相当于第一个的差集。代码:

struct compare
{
bool operator ()(string s1,string s2)
{
   return s1>s2;
}///自定义一个仿函数
};
int main()
{
typedef std::set<string,compare> _SET;
_SET s;
s.insert(string("sfdsfd"));
s.insert(string("apple"));
s.insert(string("english"));
s.insert(string("dstd"));
cout<<"s1:"<<endl;
std::set<string,compare>::iterator it = s.begin();
while(it!=s.end())
   cout<<*it++<<"   ";
cout<<endl<<"s2:"<<endl;
_SET s2;
s2.insert(string("abc"));
s2.insert(string("apple"));
s2.insert(string("english"));
it = s2.begin();
while(it!=s2.end())
   cout<<*it++<<"   ";
cout<<endl<<endl;

string str[10];
string *end = set_intersection(s.begin(),s.end(),s2.begin(),s2.end(),str,compare());//求交集,返回值指向str最后一个元素的尾端
cout<<"result of set_intersection s1,s2:"<<endl;
   string *first = str;
   while(first<end)
    cout <<*first++<<" ";
   cout<<endl<<endl<<"result of set_union of s1,s2"<<endl;
   end = std::set_union(s.begin(),s.end(),s2.begin(),s2.end(),str,compare());//并集
   first = str;
   while(first<end)
    cout <<*first++<<" ";
   cout<<endl<<endl<<"result of set_difference of s2 relative to s1"<<endl;
   first = str;
   end = std::set_difference(s.begin(),s.end(),s2.begin(),s2.end(),str,compare());//s2相对于s1的差集
   while(first<end)
    cout <<*first++<<" ";
   cout<<endl<<endl<<"result of set_difference of s1 relative to s2"<<endl;
   first = str;
   end = std::set_difference(s2.begin(),s2.end(),s.begin(),s.end(),str,compare());//s1相对于s2的差集

   while(first<end)
    cout <<*first++<<" ";
   cout<<endl<<endl;
   first = str;
end = std::set_symmetric_difference(s.begin(),s.end(),s2.begin(),s2.end(),str,compare());//上面两个差集的并集
   while(first<end)
    cout <<*first++<<" ";
   cout<<endl;
}

 

STL实践指南

分类: C/C++
2006.5.9 16:15 作者:abeln | 评论:0 | 阅读:1804

STL简介

STL (标准模版库,Standard Template Library)是当今每个从事C++编程的人需要掌握的一项不错的技术。我觉得每一个初学STL的人应该花费一段时间来熟悉它,比如,学习STL时会有 急剧升降的学习曲线,并且有一些命名是不太容易凭直觉就能够记住的(也许是好记的名字已经被用光了),然而如果一旦你掌握了STL,你就不会觉得头痛了。 和MFC相比,STL更加复杂和强大。
STL有以下的一些优点:

  • 可以方便容易地实现搜索数据或对数据排序等一系列的算法;
  • 调试程序时更加安全和方便;
  • 即使是人们用STL在UNIX平台下写的代码你也可以很容易地理解(因为STL是跨平台的)。


背景知识

写这一部分是让一些初学计算机的读者在富有挑战性的计算机科学领域有一个良好的开端,而不必费力地了解那无穷无尽的行话术语和沉闷的规则,在这里仅仅把那些行话和规则当作STLer们用于自娱的创造品吧。

使用代码
本文使用的代码在STL实践中主要具有指导意义。

一些基础概念的定义

模板(Template)——类(以及结构等各种数据类型和函数)的宏(macro)。有时叫做甜饼切割机 (cookie cutter),正规的名称应叫做范型(generic)——一个类的模板叫做范型类(generic class),而一个函数的模板也自然而然地被叫做范型函数(generic function)。
STL——标准模板库,一些聪明人写的一些模板,现在已成为每个人所使用的标准C++语言中的一部分。
容器(Container)——可容纳一些数据的模板类。STL中有vector,set,map,multimap和deque等容器。
向量(Vector)——基本数组模板,这是一个容器。
游标(Iterator)——这是一个奇特的东西,它是一个指针,用来指向STL容器中的元素,也可以指向其它的元素。

Hello World程序

我愿意在我的黄金时间在这里写下我的程序:一个hello world程序。这个程序将一个字符串传送到一个字符向量中,然后每次显示向量中的一个字符。向量就像是盛放变长数组的花园,大约所有STL容器中有一半 是基于向量的,如果你掌握了这个程序,你便差不多掌握了整个STL的一半了。

 
//程序:vector演示一
//目的:理解STL中的向量
 
// #include "stdafx.h" -如果你使用预编译的头文件就包含这个头文件
#include <vector> // STL向量的头文件。这里没有".h"。
#include <iostream> // 包含cout对象的头文件。
using namespace std; //保证在程序中可以使用std命名空间中的成员。
 
char* szHW = "Hello World";
//这是一个字符数组,以”/0”结束。
 
int main(int argc, char* argv[])
{
vector <char> vec; //声明一个字符向量vector (STL中的数组)
 
//为字符数组定义一个游标iterator。
vector <char>::iterator vi;
 
//初始化字符向量,对整个字符串进行循环,
//用来把数据填放到字符向量中,直到遇到”/0”时结束。
char* cptr = szHW; // 将一个指针指向“Hello World”字符串
while (*cptr != '/0')
{
vec.push_back(*cptr);
cptr++;
}
// push_back函数将数据放在向量的尾部。
 
// 将向量中的字符一个个地显示在控制台
for (vi=vec.begin(); vi!=vec.end(); vi++)
// 这是STL循环的规范化的开始——通常是 "!=" , 而不是 "<"
// 因为"<" 在一些容器中没有定义。
// begin()返回向量起始元素的游标(iterator),end()返回向量末尾元素的游标(iterator)。
{
cout << *vi;
} // 使用运算符 “*” 将数据从游标指针中提取出来。
cout << endl; // 换行
 
return 0;
}
 

push_back是将数据放入vector(向量)或deque(双端队列)的标准函数。Insert是一个与之类似的函数,然而它在所有容器中 都可以使用,但是用法更加复杂。end()实际上是取末尾加一(取容器中末尾的前一个元素),以便让循环正确运行——它返回的指针指向最靠近数组界限的数 据。就像普通循环中的数组,比如for (i=0; i<6; i++) {ar[i] = i;} ——ar[6]是不存在的,在循环中不会达到这个元素,所以在循环中不会出现问题。

STL的烦恼之一——初始化

STL令人烦恼的地方是在它初始化的时候。STL中容器的初始化比C/C++数组初始化要麻烦的多。你只能一个元素一个元素地来,或者先初始化一个普通数组再通过转化填放到容器中。我认为人们通常可以这样做:

//程序:初始化演示
//目的:为了说明STL中的向量是怎样初始化的。
 
#include <cstring> // <cstring>和<string.h>相同
#include <vector>
using namespace std;
 
int ar[10] =
{
12, 45, 234, 64, 12, 35, 63, 23, 12, 55
};
char* str = "Hello World";
 
int main(int argc, char* argv[])
{
vector <int> vec1(ar, ar+10);
vector <char> vec2(str, str+strlen(str));
return 0;
}

在编程中,有很多种方法来完成同样的工作。另一种填充向量的方法是用更加熟悉的方括号,比如下面的程序:

//程序:vector演示二
//目的:理解带有数组下标和方括号的STL向量
 
#include <cstring>
#include <vector>
#include <iostream>
using namespace std;
 
char* szHW = "Hello World";
int main(int argc, char* argv[])
{
vector <char> vec(strlen(sHW)); //为向量分配内存空间
int i, k = 0;
char* cptr = szHW;
while (*cptr != '/0')
{
vec[k] = *cptr;
cptr++;
k++;
}
for (i=0; i<vec.size(); i++)
{
cout << vec[i];
}
cout << endl;
return 0;
}

这个例子更加清晰,但是对游标(iterator)的操作少了,并且定义了额外的整形数作为下标,而且,你必须清楚地在程序中说明为向量分配多少内存空间。

命名空间(Namespace)

与STL相关的概念是命名空间(namespace)。STL定义在std命名空间中。有3种方法声明使用的命名空间:

1.用using关键字使用这个命名空间,在文件的顶部,但在声明的头文件下面加入:

using namespace std;

这对单个工程来说是最简单也是最好的方法,这个方法可以把你的代码限定在std命名空间中。

2.使用每一个模板前对每一个要使用的对象进行声明(就像原形化):

using std::cout;
using std::endl;
using std::flush;
using std::set;
using std::inserter;

尽管这样写有些冗长,但可以对记忆使用的函数比较有利,并且你可以容易地声明并使用其他命名空间中的成员。

3.在每一次使用std命名空间中的模版时,使用std域标识符。比如:
typedef std::vector VEC_STR;

这种方法虽然写起来比较冗长,但是是在混合使用多个命名空间时的最好方法。一些STL的狂热者一直使用这种方法,并且把不使用这种方法的人视为异类。一些人会通过这种方法建立一些宏来简化问题。

除此之外,你可以把using namespace std加入到任何域中,比如可以加入到函数的头部或一个控制循环体中。

一些建议

为了避免在调试模式(debug mode)出现恼人的警告,使用下面的编译器命令:

#pragma warning(disable: 4786) 

另一条需要注意的是,你必须确保在两个尖括号之间或尖括号和名字之间用空格隔开,因为是为了避免同“>>”移位运算符混淆。比如

vector <list<int>> veclis;

这样写会报错,而这样写:

vector <list <int> > veclis;

就可以避免错误。

另一种容器——集合(set)

这是微软帮助文档中对集合(set)的解 释:“描述了一个控制变长元素序列的对象(注:set中的key和value是Key类型的,而map中的 key和value是一个pair结构中的两个分量)的模板类,每一个元素包含了一个排序键(sort key)和一个值(value)。对这个序列可以进行查找、插入、删除序列中的任意一个元素,而完成这些操作的时间同这个序列中元素个数的对数成比例关 系,并且当游标指向一个已删除的元素时,删除操作无效。”
而一个经过更正的和更加实际的定义应该是:一个集合(set)是一个容器,它其中所包含 的元素的值是唯一的。这在收集一个数据的具体值的时候是有用的。集合中的元素按一定的顺序排列,并被作为集合中的实例。如果你需要一个键/值对 (pair)来存储数据,map是一个更好的选择。一个集合通过一个链表来组织,在插入操作和删除操作上比向量(vector)快,但查找或添加末尾的元 素时会有些慢。
下面是一个例子:

//程序:set演示
//目的:理解STL中的集合(set)
 
#include <string>
#include <set>
#include <iostream>
using namespace std;
 
int main(int argc, char* argv[])
{
set <string>
strset;
set <string>
::iterator si;
strset.insert("cantaloupes");
strset.insert("apple");
strset.insert("orange");
strset.insert("banana");
strset.insert("grapes");
strset.insert("grapes");
for (si=strset.begin(); si!=strset.end(); si++)
{
cout << *si << " ";
}
cout << endl;
return 0;
}
 
// 输出: apple banana cantaloupes grapes orange
//注意:输出的集合中的元素是按字母大小顺序排列的,而且每个值都不重复。

如果你感兴趣的话,你可以将输出循环用下面的代码替换:

copy(strset.begin(), strset.end(), ostream_iterator<string>(cout, " "));

.集合(set)虽然更强大,但我个人认为它有些不清晰的地方而且更容易出错,如果你明白了这一点,你会知道用集合(set)可以做什么。

所有的STL容器

容器(Container)的概念的出现早于模板(template),它原本是一个计算机科学领域中的一个重要概念,但在这里,它的概念和STL混合在一起了。下面是在STL中出现的7种容器:

vector(向量)——STL中标准而安全的数组。只能在vector 的“前面”增加数据。
deque(双端队列double-ended queue)——在功能上和vector相似,但是可以在前后两端向其中添加数据。
list(列表)——游标一次只可以移动一步。如果你对链表已经很熟悉,那么STL中的list则是一个双向链表(每个节点有指向前驱和指向后继的两个指针)。
set(集合)——包含了经过排序了的数据,这些数据的值(value)必须是唯一的。
map(映射)——经 过排序了的二元组的集合,map中的每个元素都是由两个值组成,其中的key(键值,一个map中的键值必须是唯一的)是在排序或搜索时使用,它的值可以 在容器中重新获取;而另一个值是该元素关联的数值。比如,除了可以ar[43] = "overripe"这样找到一个数据,map还可以通过ar["banana"] = "overripe"这样的方法找到一个数据。如果你想获得其中的元素信息,通过输入元素的全名就可以轻松实现。
multiset(多重集)——和集合(set)相似,然而其中的值不要求必须是唯一的(即可以有重复)。
multimap(多重映射)——和映射(map)相似,然而其中的键值不要求必须是唯一的(即可以有重复)。
注意:如 果你阅读微软的帮助文档,你会遇到对每种容器的效率的陈述。比如:log(n*n)的插入时间。除非你要处理大量的数据,否则这些时间的影响是可以忽略 的。如果你发现你的程序有明显的滞后感或者需要处理时间攸关(time critical)的事情,你可以去了解更多有关各种容器运行效率的话题。

怎样在一个map中使用类?

Map是一个通过key(键)来获得value(值)的模板类。
另一个问题是你希望在map中使用自己的类而不是已有的数据类型,比如现在已经用过的int。建立一个“为模板准备的(template-ready)”类,你必须确保在该类中包含一些成员函数和重载操作符。下面的一些成员是必须的:

    • 缺省的构造函数(通常为空)
    • 拷贝构造函数
    • 重载的”=”运算符 

你应该重载尽可能多的运算符来满足特定模板的需要,比如,如果你想定义一个类作为 map中的键(key),你必须重载相关的运算符。但在这里不对重载运算符做过多讨论了。

//程序:映射自定义的类。
//目的:说明在map中怎样使用自定义的类。
 
#include <string>
#include <iostream>
#include <vector>
#include <map>
using namespace std;
 
class CStudent
{
public :
int nStudentID;
int nAge;
public :
//缺省构造函数——通常为空
CStudent()
{ }
// 完整的构造函数
CStudent(int nSID, int nA)
{
nStudentID=nSID;
nAge=nA;
}
//拷贝构造函数
CStudent(const CStudent& ob)
{
nStudentID=ob.nStudentID;
nAge=ob.nAge;
}
// 重载“=”
void operator = (const CStudent& ob)
{
nStudentID=ob.nStudentID;
nAge=ob.nAge;
}
};
 
int main(int argc, char* argv[])
{
map <string, CStudent> mapStudent;
 
mapStudent["Joe Lennon"] = CStudent(103547, 22);
mapStudent["Phil McCartney"] = CStudent(100723, 22);
mapStudent["Raoul Starr"] = CStudent(107350, 24);
mapStudent["Gordon Hamilton"] = CStudent(102330, 22);
 
// 通过姓名来访问Cstudent类中的成员
cout << "The Student number for Joe Lennon is " <<
(mapStudent["Joe Lennon"].nStudentID) << endl;
 
return 0;
}
 
 
TYPEDEF

如果你喜欢使用typedef关键字,下面是个例子:
typedef set <int> SET_INT;
typedef SET_INT::iterator SET_INT_ITER

typedef set <int> SET_INT;
typedef SET_INT::iterator SET_INT_ITER

编写代码的一个习惯就是使用大写字母和下划线来命名数据类型。

ANSI / ISO字符串

ANSI/ISO字符串在STL容器中使用得很普遍。这是标准的字符串类,并得到了广泛地提倡,然而在缺乏格式声明的情况下就会出问题。你必须使用“<<”和输入输出流(iostream)代码(如dec, width等)将字符串串联起来。
可在必要的时候使用c_str()来重新获得字符指针。

游标(Iterator)

我说过游标是指针,但不仅仅是指针。游标和指针很像,功能很像指针,但是实际上,游标是通过重载一元的”*”和”->”来从容器中间接地返回 一个值。将这些值存储在容器中并不是一个好主意,因为每当一个新值添加到容器中或者有一个值从容器中删除,这些值就会失效。在某种程度上,游标可以看作是 句柄(handle)。通常情况下游标(iterator)的类型可以有所变化,这样容器也会有几种不同方式的转变:
iterator——对 于除了vector以外的其他任何容器,你可以通过这种游标在一次操作中在容器中朝向前的方向走一步。这意味着对于这种游标你只能使用“++”操作符。而 不能使用“--”或“+=”操作符。而对于vector这一种容器,你可以使用“+=”、“—”、“++”、“-=”中的任何一种操作符和“<”、 “<=”、“>”、“>=”、“==”、“!=”等比较运算符。
reverse_iterator ——如 果你想用向后的方向而不是向前的方向的游标来遍历除vector之外的容器中的元素,你可以使用reverse_iterator 来反转遍历的方向,你还可以用rbegin()来代替begin(),用rend()代替end(),而此时的“++”操作符会朝向后的方向遍历。
const_iterator ——一个向前方向的游标,它返回一个常数值。你可以使用这种类型的游标来指向一个只读的值。
const_reverse_iterator ——一个朝反方向遍历的游标,它返回一个常数值。

Set和Map中的排序

除了类型和值外,模板含有其他的参数。你可以传递一个回调函数(通常所说的声明“predicate”——这是带有一个参数的函数返回一个布尔值)。例如,如果你想自动建立一个集合,集合中的元素按升序排列,你可以用简明的方法建立一个set类:

set <int, greater<int> > set1

greater 是另一个模板函数(范型函数),当值放置在容器中后,它用来为这些值排序。如果你想按降序排列这些值,你可以这样写:

set <int, less<int> > set1

在实现算法时,将声明(predicate)作为一个参数传递到一个STL模板类中时会遇到很多的其他情况,下面将会对这些情况进行详细描述。

STL 的烦恼之二——错误信息

这些模板的命名需要对编译器进行扩充,所以当编译器因某种原因发生故障时,它会列出一段很长的错误信息,并且这些错误信息晦涩难懂。我觉得处理这样 的难题没有什么好办法。但最好的方法是去查找并仔细研究错误信息指明代码段的尾端。还有一个烦恼就是:当你双击错误信息时,它会将错误指向模版库的内部代 码,而这些代码就更难读了。一般情况下,纠错的最好方法是重新检查一下你的代码,运行时忽略所有的警告信息。

算法(Algorithms)

算法是模板中使用的函数。这才真正开始体现STL的强大之处。你可以学习一些大多数模板容器中都会用到的一些算法函数,这样你可以通过最简便的方式 进行排序、查找、交换等操作。STL中包含着一系列实现算法的函数。比如:sort(vec.begin()+1, vec.end()-1)可以实现对除第一个和最后一个元素的其他元素的排序操作。
容器自身不能使用算法,但两个容器中的游标可以限定容器中使用 算法的元素。既然这样,算法不直接受到容器的限制,而是通过采用游标,算法才能够得到支持。此外,很多次你会遇到传递一个已经准备好了的函数(以前提到的 声明:predicate)作为参数,你也可以传递以前的旧值。
下面的例子演示了怎样使用算法:

//程序:测试分数统计
//目的:通过对向量中保存的分数的操作说明怎样使用算法
 
#include <algorithm> //如果要使用算法函数,你必须要包含这个头文件。
#include <numeric> // 包含accumulate(求和)函数的头文件
#include <vector>
#include <iostream>
using namespace std;
 
int testscore[] =
{
67, 56, 24, 78, 99, 87, 56
};
 
//判断一个成绩是否通过了考试
bool passed_test(int n)
{
return (n >= 60);
}
 
// 判断一个成绩是否不及格
bool failed_test(int n)
{
return (n < 60);
}
 
int main(int argc, char* argv[])
{
int total;
// 初始化向量,使之能够装入testscore数组中的元素
vector <int> vecTestScore(testscore,
testscore + sizeof(testscore) / sizeof(int));
vector <int>::iterator vi;
 
// 排序并显示向量中的数据
sort(vecTestScore.begin(), vecTestScore.end());
cout << "Sorted Test Scores:" << endl;
for (vi=vecTestScore.begin(); vi != vecTestScore.end(); vi++)
{
cout << *vi << ", ";
}
cout << endl;
 
// 显示统计信息
 
// min_element 返回一个 _iterator_ 类型的对象,该对象指向值最小的那个元素。
//“*”运算符提取元素中的值。
vi = min_element(vecTestScore.begin(), vecTestScore.end());
cout << "The lowest score was " << *vi << "." << endl;
 
//与min_element类似,max_element是选出最大值。
vi = max_element(vecTestScore.begin(), vecTestScore.end());
cout << "The highest score was " << *vi << "." << endl;
 
// 使用声明函数(predicate function,指vecTestScore.begin()和vecTestScore.end())来确定通过考试的人数。
cout << count_if(vecTestScore.begin(), vecTestScore.end(), passed_test) <<
" out of " << vecTestScore.size() <<
" students passed the test" << endl;
 
// 确定有多少人考试挂了
cout << count_if(vecTestScore.begin(),
vecTestScore.end(), failed_test) <<
" out of " << vecTestScore.size() <<
" students failed the test" << endl;
 
//计算成绩总和
total = accumulate(vecTestScore.begin(),
vecTestScore.end(), 0);
// 计算显示平均成绩
cout << "Average score was " <<
(total / (int)(vecTestScore.size())) << endl;
 
return 0;
}

Allocator(分配器)

Allocator用在模板的初始化阶段,是为对象和数组进行分配内存空间和释放空间操作的模板类。它在各种情况下扮演着很神秘的角色,它关心的是 高层内存的优化,而且对黑盒测试来说,使用Allocator是最好的选择。通常,我们不需要明确指明它,因为它们通常是作为不用添加的缺省的参数出现 的。如果在专业的测试工作中出现了Allocator,你最好搞清楚它是什么。

Embed Templates(嵌入式模版)和Derive Templates(基模板)

每当你使用一个普通的类的时候,你也可以在其中使用一个STL类。它是可以被嵌入的:

class CParam
{
string name;
string unit;
vector <double> vecData;
};

或者将它作为一个基类:

class CParam : public vector <double>
{
string name;
string unit;
};

STL模版类作为基类时需要谨慎。这需要你适应这种编程方式。

模版中的模版

为构建一个复杂的数据结构,你可以将一个模板植入另一个模板中(即“模版嵌套”)。一般最好的方法是在程序前面使用typedef关键字来定义一个在另一个模板中使用的模版类型。

// 程序:在向量中嵌入向量的演示。
//目的:说明怎样使用嵌套的STL容器。
 
#include <iostream>
#include <vector>
 
using namespace std;
 
typedef vector <int> VEC_INT;
 
int inp[2][2] =
{
{
1, 1
}
,
{
2, 0
}
};
// 要放入模板中的2x2的正则数组
 
int main(int argc, char* argv[])
{
int i, j;
vector <VEC_INT> vecvec;
// 如果你想用一句话实现这样的嵌套,你可以这样写:
// vector <vector <int> > vecvec;
 
// 将数组填入向量
VEC_INT v0(inp[0], inp[0]+2);
// 传递两个指针
// 将数组中的值拷贝到向量中
VEC_INT v1(inp[1], inp[1]+2);
 
vecvec.push_back(v0);
vecvec.push_back(v1);
 
for (i=0; i<2; i++)
{
for (j=0; j<2; j++)
{
cout << vecvec[i][j] << " ";
}
cout << endl;
}
return 0;
}
 
// 输出:
// 1 1
// 2 0

虽然在初始化时很麻烦,一旦你将数据填如向量中,你就实现了一个变长的可扩充的二维数组(大小可扩充直到使用完内存)。根据实际需要,可以使用各种容器的嵌套组合。

总结

STL是有用的,但是使用过程中的困难和麻烦是再所难免的。就像中国人所说的:“如果你掌握了它,便犹如虎添翼。”

 

 

俗话说得好:“光说不练是假把式。”学习C++也是这样,无论看再多的书,如果不自己动手练一练,是体会不到C++的真谛的。在这里,我给自己找了一个简单的练习题:

有一个文本文件,其中保存了100万条email地址的纪录,每一条记录为一行,要求对这个文件中的记录进行排序,并去除重复的项,结果写入另外一个文件。

经常逛CSDN的朋友对这个题目肯定不陌生,因为在CSDN上就曾经有一个讨论是C++更快还是Python更快的帖子,使用的测试题就是这样的,不过他们使用的记录只有78万条,我这里只是增加到了100万条而已。

现 代C++的观点更以前相比已经发生了转变,效率已经不是最重要的考虑因素了,最重要的是怎样更快更正确的编写程序,这一点通过《C++ Primer》第四版和第三版的比较就可以看出来。在第四版中,作者更加偏重于介绍STL中的vector和bitset,而不再是数组指针和位操作符; 更加偏重于std::string而不是char * ,虽然对于某些在效率方面的要求有些偏执狂的人来说,std::string的实现并不是最完美的。

因此,使用标准库来完成这个题目是很简单的,代码如下:

 1  #include  < iostream >
 2  #include  < fstream >
 3  #include  < vector >
 4  #include  < string >
 5  #include  < algorithm >
 6 
 7  int  main()
 8  {
 9       // 读取文件中的email地址到vector中
10      std::ifstream input_file( " emails100w.txt " );
11      std::string tmp;
12      std::vector < std::string >  emails;
13       while (input_file  >>  tmp)
14      {
15          emails.push_back(tmp);
16      }
17      
18       // 排序
19      std::sort(emails.begin(),emails.end());
20      
21       // 去除重复项
22      std::vector < std::string > ::iterator end_after_unique  =  std::unique(emails.begin(),emails.end());
23      
24       // 写入结果文件
25      std::ofstream output_file( " results.txt " );
26       for (std::vector < std::string > ::iterator it  =  emails.begin(); it  !=  end_after_unique; it ++ )
27      {
28          output_file  <<   * it  <<  std::endl;
29      }
30      
31       return   0 ;
32  }

加上注释和程序中的空行,也只需要32行代码。使用标准库的好处是显而易见的,整个程序的意义都非常清晰,而且不容易出错,使用STL真的是太方便了。那么,运行效率如何呢?我使用Linux中自带的time命令对程序的运行时间进行分析,如下:
$ time . / SortAndUnique

real 0m35.786s
user 0m26.613s
sys  0m9.437s

那么,STL中的容器还有别的可以完成这个任务吗?我想到了std::set,该容器在插入数据的时候,会自动抛弃重复的值,而且它里面的内容都是排好序的,这么看来,这个容器更加适合于我们的任务。那么,写个代码试一下:
 1  #include  < iostream >
 2  #include  < fstream >
 3  #include  < set >
 4  #include  < string >
 5  #include  < algorithm >
 6 
 7  int  main()
 8  {
 9       // 读取文件中的email地址到std::set中
10      std::ifstream input_file( " emails100w.txt " );
11      std::string tmp;
12      std::set < std::string >  emails;
13       while (input_file  >>  tmp)
14      {
15          emails.insert(tmp);
16      }
17      
18       // 写入结果文件
19      std::ofstream output_file( " results.txt " );
20       for (std::set < std::string > ::iterator it  =  emails.begin(); it  !=  emails.end(); it ++ )
21      {
22          output_file  <<   * it  <<  std::endl;
23      }
24      
25       return   0 ;
26  }

嗯,不错,这个代码的行数更少。那它的运行效率呢?比使用std::vector的那个版本是快些还是慢些呢?请看下面的测试数据:
$ time . / SortWithSet

real 0m21.544s
user 0m12.370s
sys 0m9.609s

哇塞,这个程序比前一个整整快了14秒多,其中sys的时间是差不多的,说明这两个版本在输入输出的操作上没多大区别,而排序和去除重复项的工作,使用std::set只有使用std::vector一半不到的时间。

为什么会这样?我认为主要有以下几个原因:

1、std::sort算法使用的排序方法我们不清楚,我们知道,排序有很多种方法,如简单排序、快速排序、堆排序等,简单排序是最慢的,它的时间复杂度为O(n2) ,而快速排序呢,它在最好情况下能达到O(n*log2n),而最坏情况下就只有O(n2)了,堆排序速度最快,时间复杂度为O(n*log2n)。 我不知道std::sort算法使用的是不是堆排序,但是我可以肯定它绝对不会使用简单排序,编写STL的人可不会那么笨。而std::set使用的是什 么数据结构呢?一般都是使用的红黑树(平衡二叉树、AVL树),使用该结构的特点是查找一个元素的时间复杂度绝对不会超过log2n+1,因此,使用std::set进行排序,它的时间复杂度肯定是O(n*log2n)了。另外,在C++ 0x标准中,会加入另外一些标准容器,如std::unordered_set,从名字上可以看出,它是一个没有排序的set,它使用的数据结构就是哈希表,虽然没有排序,但是它查找数据的时间复杂度却是一个常数。

2、 使用std::set容器减少了std::string的复制次数,我们知道STL的容器中保存的是我们的数据的副本,因此,将std::string对 象放到std::vector容器中的时侯,会发生一个复制操作,而在使用std::sort算法的时候,容器中的元素交换位置,又会发生很多次的复制操 作,再使用std::unique算法的时候,移动容器中的元素也要发生复制操作。使用std::set容器,它只在insert的时候复制一次而已。所 以,使用std::set的这个版本比较快那是理所当然的了。

当然,如果你不使用std::string而是用char *,不使用容器和算法而是自己实现平衡二叉树,当然可以写出更快的版本,不过要付出更多的调试代价。

最后,为了让大家都能够找个100w行记录的文本练练手,下面给出一个随机生成100w个email地址的小程序,写得不好,请不要见笑:
 1  #include  < iostream >
 2  #include  < fstream >
 3  #include  < cstdlib >
 4  #include  < string >
 5  #include  < vector >
 6  using namespace std;
 7 
 8  int  main()
 9  {
10       // 创建1000个用户名
11       char  letters[]  =   " abcdefghijklmnopqrstuvwxyz1234567890_ " ;
12      vector < string >  names;
13       for (unsigned  int  i = 0 ; i < 1000 ; i ++ )
14      {
15           // 获取一个30以内的随机数作为用户名的长度
16           int  length  =  rand() % 30   +   1 ;
17          string name;
18           for (unsigned  int  j = 0 ; j < length; j ++ ){
19               int  index  =  rand() % 37 ;
20              name.append( 1 ,letters[index]);
21          }
22          names.push_back(name);
23      }
24      
25       // 创建700个网站名
26      string domains[]  =  { " .com " , " .cn " , " .com.cn " , " .gov " , " .gov.cn " , " .net " , " .net.cn " };
27      vector < string >  sites;
28       for (unsigned  int  i = 0 ; i < 100 ; i ++ )
29      {
30           // 获取一个10以内的随机数作为网站名的长度
31           int  length  =  rand() % 10   +   1 ;
32          string name;
33           for (unsigned  int  j = 0 ; j < length; j ++ ){
34               int  index  =  rand() % 37 ;
35              name.append( 1 ,letters[index]);
36          }
37           for ( int  k = 0 ; k < 7 ; k ++ ){
38              name.append(domains[k]);
39              sites.push_back(name);
40          }
41      }
42 
43       // 构建100万个email地址
44      ofstream emails( " emails100w.txt " );
45       for ( int  i = 0 ; i < 1000000 ; i ++ ){
46          emails  <<  names[rand() % 1000 <<   " @ "   <<  sites[rand() % 700 <<  endl;
47      }
48      
49       return   0 ;
50  }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值