【读书笔记】《王道论坛计算机考研机试指南》第八章

第八章

标准模板库(STL)

在前几个章节中我们已经使用了诸如队列、堆、堆栈、vector 等标准模板库中的模板,切身感受到了它给我们带来的极大便利。在本节中,我们还要介绍两种标准模板一一string和map,了解他们又会给我们带来怎样的便利。
string对象,顾名思义即用来保存和处理字符串的标准模板。我们介绍其相关的操作。
在使用它之前我们声明包括string 模板

#include <string> //注意区别于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 <stdio.h>
#include <string>
#include <iostream>
#include <ctype.h>
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 <stdio.h>
#include <vector>
#include <map>//要使用map, 必须包含此头文件.
#include <string>
#include <queue>
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];

如代码所示,我们在每次循环进行状态转移之前交换源数组和目的数组的指针,使程序能够正确的从源数组中转移状态到目的数组中。当状态转移完成时,新得到状态保存于目的数组中,但它在下一次循环的状态转移中又将变为源数组,于是我们在下次状态转移开始前再次交换源数组和目的数组指针,这就是滚动数组的工作原理。
滚动数组这个技巧不仅优化了原始的状态空间,还减少了循环次数节约了程序运行时间,同时对代码量的缩减也有很好的效果,是一个我们值得学习的小技巧。

猜你喜欢

转载自blog.csdn.net/weixin_44029550/article/details/105776219