在前几个章节中我们已经使用了诸如队列、堆、堆栈、vector 等标准模板库中的模板,切身感受到了它给我们带来的极大便利。在本节中,我们还要介绍两种标准模板一一string和map,了解他们又会给我们带来怎样的便利。
string对象,顾名思义即用来保存和处理字符串的标准模板。我们介绍其相关的操作。
在使用它之前我们声明包括string 模板
#include //注意区别于string. h
并使用标准命名空间
using namespace std;
利用语句
string s;
定义string对象s。我们可以使用cin对其进行输入
cin>>s
也可以使用已经保存在字符数组里的字符串直接对其赋值
char str[]="test";
s=str;
对已经存在的string对象s,我们可以在其最后添加一个字符
s +='c';
添加一个字符串
s += "string";
甚至添加一个string对象
string b = "class";
s+=b;
可以这样操作的原因是标准模板库中已经帮我们重载了例如+、+=等运算符的行为,所以我们可以像使用基本类型一样直接调用它。
其它常用运算符有:
判断两个字符串是否相同
string b="class" ;
string a="Two";
if(a==b){
cout << a;
}
判断两个字符串间的大小关系
string b="cIass";
string a="Two";
if(a<=b){
cout << a;
}
同样的,与其对应的小于运算、大于运算、大于等于运算均可调用。
要输出一个string对象保存的字符串,我们可以使用C++风格的输出
string c="cout";
cout<<c<<endl;
也可以使用C风格的输出
string c="cout";
printf("%s\b",c.c_str());
若要对string对象s中的每一个字符中进行遍历,需要以下循环
for (int i=0;i < s.size();i ++) { //注意循环终止条件
char c=s[i];
}
除了以上基本操作以外,string 还包括以下常用的内置函数
s. erase(10, 8);
从string对象str中删除从s[10]到s[17]的字符,即从s[10]开始的8个字符。
string a="asdfsdfadfafd";
string b="fadf";
int startPos =0;
int pos= a.find(b,startPos);
在string中下标startPos位置开始查找b字符串,若能够找到b字符串则返回其第一次出现的下标;否则,返回一个常数string: :npos.其中string对象b也可以为字符数组。
string a="AAAA";
string b="BBB";
a.insert(2,b);
cout<<a<<endl;
在a中下标为2的字符前插入b字符串。其中string对象b也可以为字符数组。
若我们使用字符数组手动实现其所要求的查找删除操作,那么需要耗费大量的编码时间不说,即使是程序的正确性我们也很难保证。但是使用标准对象string,情况就大不相同了。这里我们特别注意,题面中要求的"大小写不区分",为了达到这一目的,我们将字符串全部改写成小写后进行匹配。
#include
#include
#include
#include
using namespace std;
int main(){
char str[101];
gets(str); //输入短字符串
string a=str; //将其保存在a中
for (int i=0;i < a.size();i ++) {
a[i]=tolower(a[i]);
} //将a中的字符全部改成小写
while (gets(str)) { //输入长字符串
string b=str,c=b; //将字符串保存至b, c
for (int i=0;i < b.size();i ++) {
b[i]=tolower(b[i]);
} //将b中字符全部改成小写,以便匹配
int t = b.find(a,0); //在b中查找a的位置
while(t!=string::npos) { //若查找成功,则重复循环
c.erase(t,a.size()); //删除c中相应位置字符c为原串
b.erase(t,a.size()); //删除b中相应位置字符,b为改为小写字符的串
t = b.find(a,t); //继续查找b中下一个出现字符串a的位置
}
t=c.find(' ',0); //查找c中空格
while(t !=string::npos) {
c.erase(t,1);
t=c.find(' ',0);
} //删除c中所有空格
cout << c << endl;
}
return 0;
}
可见,在使用了string对象后,关于字符串处理的问题将得到大大简化。这里,还要提醒大家注意另一个非常重要的地方一一关于gets()函数的使用。如代码中语句gets(str),我们使用该语句读入输入中一整行的数据保存在str中。但其在与scanf()函数合用时,我们必须小心翼翼的处理一些情况。作为反例,若将上例的解题代码中输入不包含空格的短字符,改为scanf ("%s" ,str)的输入方法,其它地方不变,程序将会出现错误。
要回答其原因,先来了解gets的输入特点,当程序运行至gets语句后, 它将依次读入遗留在输入缓冲中的数据直到出现换行符,并将除换行符外的所有已读字符保存在字符数组中,同时从输入缓冲中去除该换行符。
假设输入数据格式为,第一行为两个整数,第二行为一个字符串,如:
23 (换行)
Test (换行)
为了读入输入数据,我们使用语句
scanf("%d%d",&a,&b);
gets(str);
我们来分析其运行过程,当程序运行至scanf时, 程序读入输入缓冲中的数据2 3,并将数字2、3分别保存至变量a、b中,此时输入缓冲中遗留的数据为第一行一个换行符,第二行一个字符串,即
(换行)
Test (换行)
程序继续执行至gets语句,程序在输入缓冲中第一个读到的字符即为换行符,gets运行结束,它去除输入缓冲中换行的同时并没有读到任何字符,即str为空字符串,而原本将要输入的字符串却留在了输入缓冲中。这样,便造成了程序不能正确读入接下来的数据,这也就是为什么我们随意使用gets语句时会出现错误的原因。与其对应,scanf ("%s" ,str)函数读取输入缓冲的字符直到出现空格、换行字符,它将读到的字符保存至字符数组str中,但并不删除缓冲中紧接的空格与换行,上例中,若使用scanf("%s")读入短字符串后,其后换行符依然保留在缓冲中,从而导致后续gets函数不能正常使用。
所以,在使用gets时,我们对之前输入遗留在输入缓冲的换行符要特别的关注,确定其是否会对gets造成危险,如上的输入正确的处理方式为
scanf("%d%d",&a,&b);
getchar();
gets(str);
即在scanf后使用一个getchar去消除输入缓冲的换行符,使程序正常运行。
基于如上原因,我们应尽可能的避免使用gets,除非输入要求输入“ 包括空格”的一整行,否则我们尽可能的使用scanf("%s",str)去代替其完成功能。
上面我们主要讨论了string在机试中的用途,接下去我们还要介绍标准模板库中另一个十分实用的标准对象一一map。 .
仔细阅读前面章节的读者应该对这样的例题并不感到陌生,这便是我们在图论中所讨论过的拓扑排序问题。将选手对应结点,胜负关系对应为结点之间的有向边,可以产生冠军的情况即为全图中入度为零的点唯一。
与普通的拓扑排序问题不同,这里我们需要将输入的选手姓名映射为结点编号,这就需要标准对象map。
#include
#include
#include //要使用map, 必须包含此头文件.
#include
#include
using namespace std; //声明使用标准命名空间
map<string,int> M; //定义一个完成从string到int映射的map
int in[2002];
int main(){
int n;
while (scanf("%d",&n)!=EOF && n!=0) {
for(int i=0;i<2*n;i++){//n组胜负关系,至多存在n个队伍
in[i]=0; //初始化入度
}
M.clear(); //对map中的映 射关系清空
int idx=0; //下一个被映射的数字
for(int i=0;i<n;i++){
char str1[50],str2[50];
scanf("%s%s",str1,str2); //输入两个选手名称
string a=str1, b=str2; //将字符串保存至string中
int idxa,idxb;
if (M.find(a)==M.end()) { //若map中尚无对该a的映射
idxa=idx;
M[a]=idx++; //设定其映射为idx,并递增idx
}
else idxa=M[a]; //否则,直接读出该映射
if (M.find(b)==M.end()) {
idxb=idx;
M[b]=idx++;
}
else idxb=M[b]; //确定b的映射,方法与a相同
in[idxb]++; //b的入度递增
}
int cnt=0;
for (int i=0;i<idx;i++) { //确定所有映射数字的入度,统计入度为0的个数
if (in[i]==0)
cnt++;
}
puts(cnt== 1? "Yes" : "No");//若入度为0输出Yes,否则输出No.
}
return 0;
}
如例所示,map很好的完成了从string到int的映射,即完成了选手姓名到结点编号的映射。
下面回顾它的用法:
map<string,int> M;//定义一个完成从string到int映射的map
M.clear(); //清空一个map
M.find(b); //确定map中是否保存string对象b的映射,若没有函数返回M.end()
M[b]=idx; //若map中不存在string对象b的映射,则定义其映射为b映射为idx
idxb=M[b]; //若map中存在string对象b的映射,则读出该映射
顺便一提的是,map的内部实现是一棵红黑树。
假设有如下状态转移方程:
按照该状态转移方程,我们可以用二维数组保存其状态值,通过如下代码片段完成其状态的转移(这里仅作说明,不考虑边界情况):
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i][j]=max(dp[i-1][j+1],dp[i-1][j-1]);
}
}
int ans=dp[n][m];
考虑到每次状态的转移仅与上一行有关,我们可以将二维数组优化到使一维数组保存。如下:
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
buf[j]=max(dp[j+1],dp[j-1]);
for(int j=1;j<=m;j++){
dp[j]=buf[j];
}
}
int ans=dp[m];
如该代码片段所示,我们将原本二维的状态空间优化到了一维,对应的我们需要在每次状态转移过后进行一次循环次数为m的赋值操作。该操作不仅增加了代码量,还增加了程序的耗时。于是我们使用滚动数组,对其再次进行优化:
定义大小为2*m的数组为其状态空间:
int dp[2][M];
初始状态保存在dp[0][i]中。
设定两个int类型指针
int *src; //源指针
int *des; //目的指针
由于初始状态保存在dp数组的第0行中,初始时
src=dp[1];
des=dp[0];
按照状态转移方程进行状态转移
for(int i=1;i<=n;i++){
swap(src,des); //交换源和目的指针
for(int j=1;j<=m;j++){
des[j]=max(src[j+1],src[j-1]);
}
}
int ans=des[m];
如代码所示,我们在每次循环进行状态转移之前交换源数组和目的数组的指针,使程序能够正确的从源数组中转移状态到目的数组中。当状态转移完成时,新得到状态保存于目的数组中,但它在下一次循环的状态转移中又将变为源数组,于是我们在下次状态转移开始前再次交换源数组和目的数组指针,这就是滚动数组的工作原理。
滚动数组这个技巧不仅优化了原始的状态空间,还减少了循环次数节约了程序运行时间,同时对代码量的缩减也有很好的效果,是一个我们值得学习的小技巧。