一文讲明白ACM输入输出模式
在线练习链接:牛客网OJ在线编程常见输入输出练习场
ACM输入输出模式是指在竞赛编程(ACM/ICPC)中常用的一种输入输出方式,它与普通控制台的输入输出有所不同。
输入模式
选择合适的输入方式
- 判断输入的类型,是数字还是字符串,如果是数字,则可以使用
cin
标准输入流,如果是字符串,我们可以用getline()
函数; - 判断是否知道输入数据的数量:
- 如果数量已知,可以直接用
for
循环读取; - 如果数量未知,需要在循环输入中设置退出输入模式的条件;
- 如果数量已知,可以直接用
使用cin
还是getline
进行输入
getline()
函数:
getline()
是 C++ 标准库中的一个函数,用于从输入流中读取一整行字符串,直到遇到换行符(\n
)或文件结尾。它通常用于从 cin
(标准输入)或文件中读取一行字符串。
函数原型:
std::istream& getline (std::istream& is, std::string& str, char delim = '\n');
is
是输入流对象,可以是std::cin
或文件流对象。str
是一个字符串引用,用于存储从输入流中读取的字符串。delim
是一个可选参数,表示结束字符,默认为换行符 ‘\n
’。
(std::stringstream
也可以放在std::istream
位置,例如getline(stringsteam& ss, string& str, char c)
)
注意的是:
getline()
会保留读取的字符串中的所有字符,包括空格、制表符等空白字符。- 如果输入流为空或遇到文件结尾,
getline()
会返回 false,否则返回 true。(没必要退出循环时判断为空,如果为空则不会进入循环while(getline(cin, s))
) - 读取的字符串不包含结束字符(默认为换行符 ‘
\n
’)。
如果一行中数字的数量是固定的,则使用cin
和getline()
都可以,并且cin
可能更方便;
例如:A+B(1)中,计算输入的两个数字之和;
输入:
1 5
10 20
每一行规定了只有两个数据,则使用cin
和getline()
都可以;
如果一行中数字的数量是未知的,比如计算每一行数字的累加和中:
输入:
1 2 3 4 5
1 2 3
1 1
每一行的数据长度不固定,而且没有显式说明每一行数据的长度,此时使用cin明显变得复杂不可控,直接使用getline()
读取一行字符串,然后字符串切割;
如果显式告诉了每行的数据的个数,则cin
和getline()
都可以,比如:
输入:
2 1 1
3 1 2 3
其中每行第一个数字是该行要累加的数据个数;
getline()
更加全能,但是在getline()
之后涉及字符串的切割、转换字符串为整型等一系列操作,所以使用时也要斟酌。
输入模式的退出
输入和输出是两个独立的部分,只有输入模式退出之后,才能进行输出;
例如:A+B(1)中,计算输入的两个数字之和中;
输入:
1 5
10 20
可以看出我们并不知道要输入几行数据,所以需要加入输入结束的判断逻辑,如果是使用getline()
读取的字符串数据,则输入结束条件就是getline()
获取到的为空行时结束输入,开始输出结果;
而在A+B(2)中,输入的第一行包括一个数据组数t
,接下来每一行是两个正整数,计算两数之和;
输入:
2
1 5
10 20
此时就并不需要进行判断输入结束条件,而是直接使用for
循环读取输入:for (int i = 0; i < t; i++) {读取数据,使用cin读取}
;
也有一些题目会给出输入退出条件,比如当输入0时,退出输入,只需要在输入后加入判断语句即可;
简而言之,如果知道输入的组数,则可以使用for
循环控制,如果不知道输入的组数且没有显式的输入结束退出条件,只需要自己在循环中设置退出条件;
输入为字符串的处理
使用getline()
获取到一行的字符串时,就涉及到字符串的处理;
例如:
输入:
1 2 3 4
1 2 3
我们使用getline()
读取一行字符串,例如是"1 2 3 4"
,则需要将字符串按照空格进行切割成多个子串,这些子串还需要转换为整型进行计算;
字符串的处理方法多种多样,这里介绍4种常用的方法:
- 将字符串看成一个字符数组,然后遍历字符数组;
- 使用
std::stringstream
字符串流进行处理(推荐使用); - 使用**
find()
和substr()
函数**组合处理; - 使用正则表达式进行匹配;
将字符串看作字符数组处理
例如:(以A+B(1)为例)
for (int j = s.size() - 1; j >= 0; j--) {
if (s[j] == ' ') count = 1;
else {
res += (s[j] - '0') * count;
count *= 10;
}
}
注意事项:
- 字符串要从右向左遍历,因为数字的低位靠右;
- 字符串拼接直接使用加号运算符(重载后的运算符);
- 注意字符串转整型的逻辑(按位处理);
使用std::stringstream
进行处理
例如:(以A+B(1)为例)
#include <sstream>
// 方法2: 使用std::sstream;
stringstream ss(s);
int a, b;
ss >> a >> b;
res = a + b;
stringstream
是C++标准库中的一个类,它可以让你像使用输入/输出流一样来操作字符串。它继承自iostream
类,因此具有和cin、cout
类似的操作符>>
和<<
。
stringstram
对象ss
的使用方法和cin
类似;我们可以使用>>
操作符从ss
中提取整数,就像从cin
中读取一样。
函数原型:
stringstream::stringstream(const string& str);
stringstream
默认是按照空格进行切分字符串的,切分结果可以通过>>
操作符提取,可以提取整型,也可以提取字符串;
如果字符串不是按照空格进行切分的,则不能使用stringstream
进行切分;
比如题目字符串排序(3)中,数据用’,
'而不是空格进行隔开,则此时无法直接使用stringstream
获取到切割后的子串,而是联合getline()
进行使用:
stringstream ss(s); // 只能切割为整型?不是
vector<string> strline;
string str;
// while (ss >> str) strline.push_back(str);// 按照逗号切割而不是空格;
while (getline(ss, str, ',')) { // 按照逗号切割; 注意getline在cin中只会按照\n切割,
// cin是std::istream类型,而ss是std::stringsteam类型,所以是两个重载方法,参数不一致,不要混淆;
strline.push_back(str);
}
注意如果是字符串连接,则使用加号运算符;如果是在字符串后面添加一个字符,则可以使用push_back
,因为字符串本质就是一个vector<char>
;
使用find()
和substr()
进行处理
std::string::find
是C++标准库中的一个字符串查找函数,它返回指定子串在字符串中第一次出现的位置。如果没有找到,则返回string::npos
。
std::string::substr
是一个字符串截取函数,它返回一个新字符串,该字符串是原字符串的一个子串。
例如:(以A+B(1)为例)
// 方法2:使用find和substr(切割字符串为子串)
size_t pos = s.find(' '); // 找到字符串中第一次出现空格处的索引,注意使用size_t而不是int类型;
int a = stoi(s.substr(0, pos)); // 注意stoi即string to int,此外注意substring的左闭右开;
int b = stoi(s.substr(pos + 1)); // 余下子串不必写结束位置;
res = a + b;
std::stoi
函数是C++11新增的一个字符串转整数的函数,它的原型为:
int stoi(const string& str, size_t* idx = 0, int base = 10);
它将字符串str转换为整数,base
参数指定进制(默认为10进制)。如果转换失败,它会抛出一个std::invalid_argument
异常。idx
参数是一个可选的输出参数,用于存储转换停止的位置。
注意find
函数返回的数据类型是size_t
而不是int
,size_t
是 C++ 标准库中的一种无符号整数类型,用于表示大小或计数。它的确切大小取决于操作系统和编译器的实现,但至少有足够的范围来表示当前机器的最大可能对象大小。使用size_t
类型作为索引或者计数变量的好处:
- 避免下标越界;(无符号类型)
- 尺寸兼容性;(和使用的库函数、容器类进行尺寸匹配)
- 跨平台兼容性;
- 标准化;
使用正则表达式处理
例如:(以A+B(1)为例)
#include <regex>
// 方法3:使用正则表达式匹配;stl::regex;
regex re("\\s+"); // 正则表达式匹配一个或多个空白字符,\\是因为防止转义字符
sregex_token_iterator iter(s.begin(), s.end(), re, -1); // 创建分词迭代器
sregex_token_iterator end; // 结束迭代器
int num1 = std::stoi(*iter++); // 提取第一个单词,并转换为整数,迭代器自增到第二个单词位置
int num2 = std::stoi(*iter); // 提取第二个单词,并转换为整数
res = num1 + num2;
正则表达式(Regular Expression)是一种用于匹配字符串的模式(pattern)。它是由一些特殊字符和普通字符组成的字符串,可以用来描述、匹配或替换另一个字符串。
正则表达式常用的元字符:
.
匹配任意单个字符(除了换行符)\d
匹配任意数字字符,等价于[0-9]
\D
匹配任意非数字字符,等价于[^0-9]
\w
匹配任意字母、数字或下划线字符\W
匹配任意非字母、非数字和非下划线字符\s
匹配任意空白字符(空格、制表符、换行符等)\S
匹配任意非空白字符^
匹配字符串的开始位置$
匹配字符串的结束位置[]
匹配括号内的任意单个字符[^]
匹配不在括号内的任意单个字符|
匹配左右两个表达式中的任意一个()
用于分组
+
匹配前面的子表达式一次或多次
*
匹配前面的子表达式零次或多次?
匹配前面的子表达式零次或一次{n}
匹配前面的子表达式恰好 n 次{n,}
匹配前面的子表达式至少 n 次{n,m}
``匹配前面的子表达式至少 n 次,但不超过 m 次
正则表达式匹配实例
^\d{3}\s+\d{3,8}$
匹配连续的数字(区号)和数字(电话号码),例如"800 8888888"\b\w+\b
匹配单词边界(一个或多个字母、数字和下划线)\d{4}-\d{2}-\d{2}
匹配日期格式,例如"2023-05-28"[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+
匹配电子邮件地址
输出模式
输入使用cout
进行输出即可;一般情况下,输入数据经过处理的结果在数组中,只需要循环访问数组中元素然后依次cout
即可;
对字符串进行排序
在字符串排序(1)中,需要对输入的字符串进行排序,可以使用sort
函数;
输入:
5
c d a bb e
可以使用stringstream
轻易地将一行中切割好后的各个字符串保存在一个字符串向量中(也可以使用其他方法)
在得到一个字符串向量ss
之后,要对字符串向量进行排序:
#include <algorithm> // sort函数头文件;
// 对字符串数组ss进行排序;
sort(ss.begin(), ss.end(), compareStrings);
compareStrings
函数是一个自定义函数,返回布尔类型:
bool compareStrings (const string& a, const string& b) {
return a < b;
}
此时即可实现字符串排序;