笔试编程必备技巧——fstream、sstream处理复杂输入,不定输入

文章目录

    • 复杂输入
    • 文件输入
      • 文件输入使用流程
    • 不定输入
      • 每行输入不定
      • 输入行数不定
      • 使用 getline 的注意事项

复杂输入

输入输出是笔试编程题目的一个考点,题目中经常会出现比较复杂的输入情况,例如最短路径问题,其输入描述如下:

输入n,m,点的编号是1~n,然后是m行,每行4个数 a,b,d,p,表示a和b之间有一条边,且其长度为d,花费为p。最后一行是两个数 s,t;起点s,终点t。n和m为0时输入结束。
(1

输入示例:

3 2
1 2 5 6
2 3 4 5
1 3
0 0

对于此类包含多行数据的复杂输入,在调试代码时如何提升输入效率是个值得重视的问题。手动输入效率太低,复制粘贴是一种可行的办法,但是每次运行都需要操作一次,而且遇到不定输入的情况,还需要手动按ctrl + c终止,很不方便,这种情况有一种很好的解决方法:文件输入

文件输入

由于C++代码中要求#include,即使用标准输入输出流,输入输出都需要通过windows的控制台窗口进行,因此当输入复杂时会带来不便。

接下来,本文将以C++语言为例,讲解如何有效应用文件输入解决复杂输入问题。

使用文件输入需要包含头文件,其基本代码如下:

#include 
#include 
using namespace std;

int main()
{
	ifstream cin("./in.txt");

	cin.close();
	return 0;
}

我们构造一个与标准输入同名的文件输入流ifstream变量cin,它默认以读的方式打开当前目录下的in.txt文件(./代表当前目录,即当前正在编辑的代码文件所在文件夹),之后题目的nm等所有输入都可以从文件输入流cin中获取,如:

#include 
#include 
using namespace std;

int main()
{
	ifstream cin("./in.txt");
	int n, m;
	cin >> n >> m;
	...
	cin.close();
	return 0;
}

将题目输入数据复制到in.txt中,直接运行代码即可得到输出结果。因为之前将ifstream变量也命名为cin,因此在提交代码时,将ifstream cin("./in.txt")cin.close()两行代码注释或删除掉,此时cin又恢复成默认的标准输入流了,直接提交代码即可。

文件输入使用流程

接下来以Visual Studio 2017(Enterprise)为例,介绍使用文件输入的流程。

定义文件输入代码模板
事先定义一个使用文件输入的代码模板,在包含常用头文件的基础上,包含前述文件输入代码,例如:

#include "pch.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
	ifstream cin("./in.txt");


	cin.close();
	return 0;
}

代码模板可以由代码片段功能实现,若遇到输入较复杂的题目,使用快捷键展开为文件输入模板。

创建 in.txt 文件
右键当前正在编辑的cpp文件,打开文件所在的文件夹,在弹出的资源管理器窗口中新建in.txt文件,将题目输入全部复制到该文件中,保存退出。

编辑代码并运行
正常编辑代码,运行即可,此时不再需要手动输入数据,可以直接得到运行结果。

提交代码
代码调试完毕,将以下三行注释或删除掉:

#include “pch.h”
ifstream cin("./in.txt");
cin.close();

其余代码即可正常提交,关于pch.h头文件的说明见代码片段这篇文章。

最短路径问题虽然输入较为复杂,但输入内容是固定的,因此在编写输入代码时难度不大,只是文件输入可以“一劳永逸”,效率更高而已,但是输入不定的情况下,其优势非常明显。

不定输入

不定输入在笔试中是一个令人头疼的问题,它通常分为两种情况:输入行数不定,每行输入内容不定,例如流水线问题:

题目有m行输入,代表有m条流水线,m的数值未定
其中每行有若干个整数,代表当前流水线上若干个物品的编号,每两个整数之间用空格隔开
请将所有流水线上的物品按照编号升序排列并输出。

上述输入就包含了所述的两种情况,下面我们分别对其进行处理。

每行输入不定

对于每行输入不定的情况,基本只有一种解决方法,使用getline将整行作为字符串读入,然后按照题目输入规则进行拆分。在拆分时,可以自己实现split函数,这里要介绍的是使用字符串流sstream,其代码实现如下:

#include // 包含头文件
	...
	string line;
	getline(cin, line); // 使用 getline 读入当前整行
	istringstream is(line); // 用 line 构造输入字符串流 is
	int tmp;
	vector<int> curLine;
	while (is >> tmp) // 用 is 作为输入流,输入到 tmp
	{
		curLine.push_back(tmp);
	}

以上代码实现的功能为:将当前行若干个整数依次添加到curLine中,这里需要注意的有两点:

  1. is是以字符串line构造的输入字符串流,那么之后它的作用类似于标准输入流cin,可以对intstring等变量进行输入赋值,其风格与cin相同,单次输入遇到空格回车停止,不读入空格与回车,下次读入时跳过空格与回车,直到有效输入开始,并一直读到下一个空格或回车时结束,该过程持续到读到字符串末尾后结束。由于当前字符串不包含回车,因此该代码实现的功能为:按空格拆分字符串
  2. 输入代码is >> tmp若输入成功,会有非零返回值(cin同样),因此可以作为while循环的判断条件,当到达字符串末尾后输入失败,返回值为0,循环结束,以此来处理输入不定的情况。

由此可以总结,对每行输入不定情况的处理方法即为:将当前行整个读入到字符串中,然后按照指定规则拆分字符串,其中我们借助到了输入字符串流istringstream,它可以实现类似于cin风格的赋值操作,并借助输入成功有非零返回值这一特性,使用while循环来控制结束。

另外,有些题目每行内容并非是按照空格分隔,如将题目改为:

每两个整数之间用','隔开,如1,2,3

那么又该如何处理呢?一种可行的办法是读入字符串后,将其中所有的 ',' 改为空格,但这种方法效率不高,我们介绍一种使用getline按照指定字符拆分字符串的方法:

	string line; 
	getline(cin, line); //同上
	istringstream is(line); // 同上
	vector<int> curLine;
	string tmp; // 注意,这里必须将 tmp 定义为 string 变量
	while (getline(is, tmp, ',')) // 按照 ',' 拆分字符串
	{
		curLine.push_back(stoi(tmp));
	}

以上代码实现的功能为,使用getlineis按照','拆分依次赋值给tmp,这里要求tmp必须为string类型。有些情况下,使用stoi函数会出现一些问题,这里建议统一使用atoi函数:

curLine.push_back(atoi(tmp.c_str()));

输入行数不定

处理输入行数不定的情况,其核心在于利用流输入的返回值,使用while循环进行控制。

每行内容固定
虽然行数不定,但每行内容固定的情况处理时较为简单,例如:

有若干行输入,每行有3个整数,分别为 x, y, z

那么这种情况可以直接用如下方式读入:

	int x, y, z;
	while(cin >> x)
	{
		cin >> y >> z;
		...
	}

每行内容不定
如流水线问题,输入行数不定,每行内容也不定,这种情况下需要使用getline每次读入一行,并用while循环控制,其代码如下:

	string line;
	while(getline(cin, line))
	{
		...
	}

读入之后再对每行单独处理即可。

完整代码
之前提到,对于不定行输入的情况下,手动输入需要按ctrl + c手动停止,而使用文件输入的话,当读到文件末尾时while循环会自动停止,因此输入代码建议结合文件输入使用。

接下来,我们给出流水线问题的完整输入代码以供参考:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
	ifstream cin("./in.txt");

	string line;
	vector<vector<int> > input;
	while (getline(cin, line))
	{
		istringstream is(line);
		vector<int> curLine;
		int tmp;
		while (is >> tmp)
		{
			curLine.push_back(tmp);
		}
		input.push_back(curLine);
	}
	... // 解题代码
	cin.close();
	return 0;
}

若题目说明为用','分隔,则将处理代码对应修改即可。

需要注意的是,若将vector curLine定义在循环外部,则循环内需要在最后添加代码curLine.clear();,切记!

使用 getline 的注意事项

假如有这样一道航班信息的题目,输入如下:

第一行为两个整数 n, m, 代表有 n 个城市,m 批次开放的航班信息
接下来有 m 行
每一行包含该批次开放的若干个航班信息,每个航班信息用 "a,b,w"的形式表示,表示从城市 a 飞到城市 b 需要价格为 w,每两个航班信息之间用空格隔开。

输入案例如下

9 2
1,2,5 2,4,6
1,3,6 3,7,9 7,8,9

该题的输入方法非常复杂,我们需要对输入处理得到所有的航班信息,思路为沿用之前的读入框架,对每行的处理方法为首先按空格拆分为若干个航班信息,再对每个航班按照','拆分得到每个航班的信息,参考代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
	ifstream cin("./in.txt");

	int n, m;
	cin >> n >> m;
	string line;
	vector<vector<int> > flight; // 每一行为一个独立的航班信息
	vector<int> curFlight; // 单个独立的航班信息,大小为3,形式对应为 a b w
	for(int i = 0; i < m; ++i) // 读入 m 行
	{
		getline(cin, line); // 读入每一行
		istringstream is(line); // 用 line 创建输入字符串流
		string tmp; // 按空格拆分后的单元为 “a,b,w"形式,故应用string
		while (is >> tmp) // tmp 为单个航班信息字符串
		{
			istringstream istmp(tmp); // 使用 tmp 创建新的输入字符串流
			string tmpStr; // 使用 getline拆分,此处必须为 string
			while (getline(istmp, tmpStr, ',')) // 按 ',' 拆分
			{
				curFlight.push_back(atoi(tmpStr.c_str())); // 依次放入 curFlight 数组中
			}
			flight.push_back(curFlight); // 添加到 flight 中
			curFlight.clear(); // 因curFlight定义在循环外,这里要清空
		}	
	}
	//...
	cin.close();
	return 0;
}

上述代码处理读入的思路非常正确,但当我们编写完代码运行的时候,会发现结果却不对。读者不妨测试一下,新建一个cpp文件,复制上述代码,添加代码输出flight信息,新建in.txt文件并写入输入案例,运行或进行调试,会发现flight中只有两条航班信息:

1 2 5
2 4 6

对比输入案例发现,这两条是第一批次开放的航班信息,而第二批次的航班信息没有读入,这是为什么呢?

读入规则
之前介绍过,使用输入流直接对 string,int 等变量赋值时,是通过空格和回车来分隔的,具体如下

  1. 从第一个不为空格和回车的字符开始,一直读到下一个为空格或回车停止,中间部分为读入内容;
  2. 不读入空格和回车;
  3. 下次读入时,跳过空格和回车,重复步骤 1 与 2。

getline是一次读入一整行,其规则如下:

  1. 从当前位置开始,一直读入,直到遇到回车(换行符)停止;
  2. 读入内容包含回车。

假设有下面两行输入

abc 123
def

当我们输入时,按键过程如下

abc' '123'\n'def'\n'

其中' '表示实际的空格键,'\n'表示实际的回车键。

若我们使用输入流,其读入过程如下:

string str;
int num;
cin >> str; // 从读入字符'a'开始,读到'c'停止, str = abc
cin >> num; // 读入空格,跳过,从字符'1'开始,读到'3'停止,以 int 的形式读入,num = 123
cin >> str; // 读入回车,跳过,从字符'd'开始,读到'f'结束,str = def

即使 abc 和 123 之间有多个空格与回车,读入时都会依次跳过。

若结合使用getline,其读入过程如下:

string str;
cin >> str; // 从读入字符'a'开始,读到'c'停止, str = abc
getline(cin, str); // 从当前位置' '开始,一直读到'\n'结束,str = ' 'abc 123\n
getline(cin, str); // 从当前位置'd'开始,一直读到'\n'结束,str = def\n

似乎读入很正常,但是如果我们想要分别以stringint的类型读入abc123,而将def以整行读入,我们执行以下代码:

string str1, str2;
int num;
cin >> str1 >> num;
getline(cin, str2);

此时str1num都能正确读入,但str2的内容却不是预期的def。这是因为根据读入规则,读入num时在'3'处停止,而接下来调用getline函数时,会从当前位置开始,读到'\n'结束,由于123后面的换行符'\n'还留在输入流中,故此时str2 = "\n"

到这里,我们就发现了问题所在,在航班信息的输入中,我们先输入了nm两个整数,却并未读入m后面的换行符。接下来我们用for循环调用mgetline函数,在第一次的读入会发生line = "\n"。由于getline函数会将当前行的换行符一起读入,所以在这之后的每行数据都能正确读入。

综上,所写代码只能正确读入测试案例中m - 1行信息,最后一行信息会丢失。

解决方法
当使用getline函数,而之前有其它的变量使用流输入时需要注意该问题,首先将流中的换行符跳过,再开始使用getline函数。

一般情况下,可以使用cin.get()函数空读一个字符,但是有些题目为了增加输入难度,会在数字后面添加空格,再添加换行,这种情况使用cin.get()也会出现问题,因此,我们建议直接使用getline(cin, line)空读一次,根据其规则,line会一直将当前行读完,包括最后的换行符,之后就可以正常读取了。

修改代码如下:

	int n, m;
	cin >> n >> m;
	string line;
	getline(cin, line); // 将当前行剩余内容读完,包括换行符
	vector<vector<int> > flight;
	for(int i = 0; i < m; ++i)
	{
		getline(cin, line); // 正常读入
		//...
	}

再运行代码即可发现,flight包含了全部的5条航班信息。

你可能感兴趣的:(笔试求职)