处理操作系统的标准输入输出在 Web 项目开发中并不常用,像 LeetCode
一些现代刷题平台也取消了输入输出的处理,让用户专注于算法的实现。但对于传统的
OJ
平台和算法竞赛,往往需要用户从标准输入中读取指定格式的数据,然后使用标准输出返回答案。如果平时没有经验的话,在比赛或笔试时可能会浪费不必要的时间。本文主要介绍
Go 语言的 fmt.Scan
和 Scanner.Scan
函数来应对常见的 OJ 输入。
两个函数
先考虑常规的 OJ 数据格式:使用空格分隔数据元素,使用换行符分隔不同组数据。
fmt.Scan
1 |
|
Scan scans text read from standard input, storing successive space-separated values into successive arguments. Newlines count as space. It returns the number of items successfully scanned. If that is less than the number of arguments, err will report why.
fmt.Scan
方法会从标准输入中连续读入由空格(或换行)分隔的变量到传入的参数。当读入的变量个数小于传入的参数个数时,err
会报告错误。也就是说,Scan
方法会一直读取变量直至读取完毕所有输入。
根据 fmt.Scan
方法的这个特性,可以方便地处理空格对数据的划分,但由于换行也视作空格,导致无法区分数据组。因此使用该方法读入数据时,我们需要提前知道每组数据的长度,根据这个长度来对数据组进行划分即可。具体见下面两个例子。
例 1. 两数之和
1 |
|
1 |
|
示例
1 |
|
1 |
|
解答
由于每个用例固定有两个数据,只需要两个两个输入读入,然后计算两数之和即可:
1 |
|
例 2. 数列之和
1 |
|
1 |
|
示例
1 |
|
1 |
|
解答
这一题输入虽然每组的变量数不固定,但通过读入的第一个变量可以预先知道每组的变量数,因此通过循环控制即可对数据正确分组。
1 |
|
fmt.Scan
小结
fmt.Scan
方法简单易用,无需提前知道用例的组数,可以一直从标准输入读取由空格以及换行分隔的数据直至结束。但需要能够(从题目规定或输入变量)提前获知每组数组的长度。因此当每组数据长度无法提前获知时,该方法使用起来就不方便了。
bufio.Scanner
首先查看 Scanner
的定义:
1 |
|
Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text. Successive calls to the Scan method will step through the 'tokens' of a file, skipping the bytes between the tokens. The specification of a token is defined by a split function of type SplitFunc; the default split function breaks the input into lines with line termination stripped. Split functions are defined in this package for scanning a file into lines, bytes, UTF-8-encoded runes, and space-delimited words. The client may instead provide a custom split function.
简言之,Scanner 提供了每次读入一行输入的接口。
然后查看 Scanner
的 Scan
方法
1 |
|
Scan advances the Scanner to the next token, which will then be available through the Bytes or Text method. It returns false when the scan stops, either by reaching the end of the input or an error. After Scan returns false, the Err method will return any error that occurred during scanning, except that if it was io.EOF, Err will return nil. Scan panics if the split function returns too many empty tokens without advancing the input. This is a common error mode for scanners.
Scanner
的 Scan
方法(默认情况下)将
Scanner
推进到下一行,读取的该行数据可以被 Text
方法使用。当扫描结束时,该方法返回 false
。
其中 Text
方法将读取的该行输入转换为字符串返回:
1 |
|
Text returns the most recent token generated by a call to Scan as a newly allocated string holding its bytes.
因此通过使用 bufio.Scanner
我们可以逐行读取输入为一个字符串变量,然后通过
strings.Split
方法将每行字符串转变为数组便成功读入了一组数据。
bufio.Scanner
在具体使用中需要注意一些细节,包括传入标准输入,字符串分隔和类型转换等,具体见下例:
例 4. 数列求和
1 |
|
1 |
|
示例
1 |
|
1 |
|
解答
该题和例 3 是同一个问题,但缺少对每组数据个数的规定,因此无法使用
fmt.Scan
方法读取,需要使用 bufio.Scanner
逐行读取数据:
1 |
|
总结
从上文以及例题中可以发现,bufio.Scanner
的应用场景是可以完全覆盖 fmt.Scan
的,但前者使用起来需要配合很多其他的库,比较繁琐。因此当每组用例的变量数是可知的情况下优先使用
fmt.Scan
方法即可。当 fmt.Scan
方法不适用时再考虑使用 bufio.Scanner
。
另外还有一点需要注意,fmt.Scan
只处理空格分隔的变量,因此当输入格式由其他字符分隔变量时,则考虑使用
bufio.Scanner
读入后再调用 strings.Split
进行相应的分隔即可。
读取 OJ 常见输入的方法有很多,我查阅相关文章后发现上述两种方法最简单方便。如果有其他更好的方法请留言讨论。