从 TLE 到极速通过:Java 算法竞赛大数据量输入优化(BufferedReader + StringTokenizer 详解)
从 TLE 到极速通过:Java 算法竞赛大数据量输入优化(BufferedReader + StringTokenizer 详解)

引言:为什么你的 Java 算法逻辑对了,却依然超时?
很多刚从 LeetCode 转战到蓝桥杯、洛谷、ACM(Codeforces/Nowcoder)等平台的 Java 选手,经常会遇到一个令人沮丧的问题:
“我的算法时间复杂度明明是 O ( N ) O(N) O(N),逻辑也完全没问题,为什么一提交就显示 TLE(超时),甚至 MLE(内存超限)?”
其实,问题往往不在你的算法逻辑上,而是在 Java 的输入输出(I/O)性能上。当题目给出的输入数据量达到 10 5 10^5 105 到 10 6 10^6 106 级别时,传统的输入方式往往会成为程序的性能瓶颈。
本文将深度拆解 Java 输入的底层隐患,并带你掌握算法竞赛中不可或缺的"黄金搭档"——BufferedReader 与 StringTokenizer。
避坑指南:为什么 Scanner 和 split() 是性能杀手?

1. Scanner:便利背后的"沉重代价"
在平时练习中,我们最习惯使用 Scanner sc = new Scanner(System.in);。
然而,Scanner 的底层实现非常复杂:
- 它内部使用了大量的正则表达式来匹配不同类型的数据(如
nextInt()、nextDouble())。 - 正则表达式的解析和匹配是非常消耗 CPU 资源的,导致其运行速度极慢。在大数据量(超过 10 5 10^5 105)的情况下,光是读入数据就可能耗尽题目的时间限制。
2. split(" “):隐形的"内存刺客”
为了加速,有些同学换成了 BufferedReader,但习惯用 split 来切割字符串:
// 这是一个常见的性能陷阱
String[] arr = br.readLine().split(" ");
int a = Integer.parseInt(arr[0]);
int b = Integer.parseInt(arr[1]);
这样做同样非常危险:
split方法会在底层创建大量的String[]数组和全新的String子对象。- 如果在多组测试数据或循环中使用
split,会在短时间内产生海量的垃圾对象,频繁触发 Java 的 GC(垃圾回收机制)。这不仅会造成程序卡顿导致 TLE,甚至可能直接因为内存占满而导致 MLE。 - 如果输入数据中包含连续的空格,
split还会切出空字符串"",从而导致Integer.parseInt抛出异常。
黄金搭档:BufferedReader + StringTokenizer
为了解决上述问题,我们需要引入一套更轻量、更高性能的输入方案。

1. BufferedReader:高效的数据搬运工
BufferedReader 的核心在于**“缓冲(Buffer)”**。它不会每次需要一个字符就去和操作系统交互,而是提前在内存中开辟一个大缓冲区(默认 8KB),一次性读入大量字符,后续的读取直接在内存缓冲区中完成,从而极大地减少了系统调用的开销。
2. StringTokenizer:轻量级的"指针切割器"
不同于 split 会生成庞大的对象数组,StringTokenizer 采用了类似于指针移动的轻量级实现:
- 它直接在原字符串上定位,只在你需要时,通过
nextToken()像剥糖果一样依次取出下一个词。 - 它会自动过滤多余的空格、制表符(Tab)和换行符,容错率极高。
- 它不需要频繁创建大量字符串垃圾,内存消耗极小。
实战演练:代码的进化史

我们通过一个简单的场景来看看代码是如何一步步优化的。
场景:读入一个 N × M N \times M N×M 的矩阵,并进行数据处理。
进化前:Scanner 方案(最慢,易超时)
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[][] matrix = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
matrix[i][j] = sc.nextInt();
}
}
进化中:BufferedReader + split 方案(有安全隐患,易 MLE)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] firstLine = br.readLine().split(" ");
int n = Integer.parseInt(firstLine[0]);
int m = Integer.parseInt(firstLine[1]);
int[][] matrix = new int[n][m];
for (int i = 0; i < n; i++) {
String[] row = br.readLine().split(" ");
for (int j = 0; j < m; j++) {
matrix[i][j] = Integer.parseInt(row[j]); // 频繁创建数组,GC压力大
}
}
终极形态:BufferedReader + StringTokenizer(极速且安全)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
int[][] matrix = new int[n][m];
for (int i = 0; i < n; i++) {
// 每一行重新初始化一个 Tokenizer,读取速度极快
st = new StringTokenizer(br.readLine());
for (int j = 0; j < m; j++) {
matrix[i][j] = Integer.parseInt(st.nextToken());
}
}
算法竞赛专属:Java 极速输入输出(Fast I/O)标准模板
在比赛中,为了避免每次都手写 StringTokenizer 的判断,我们可以将其封装成一个类似 Scanner 用法的工具类 FastReader(或直接命名为 Read)。
建议将以下代码保存为你的个人常用模板,在遇到大数据量题目时直接套用:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
// ============================================
// 核心:FastReader 快速读入模板
// ============================================
static class FastReader {
BufferedReader br;
StringTokenizer st;
public FastReader() {
br = new BufferedReader(new InputStreamReader(System.in));
}
// 核心读取方法:当前行读完时,自动读取下一行
String next() {
while (st == null || !st.hasMoreTokens()) {
try {
String line = br.readLine();
if (line == null) {
return null; // 读到文件末尾
}
st = new StringTokenizer(line);
} catch (IOException e) {
e.printStackTrace();
}
}
return st.nextToken();
}
// 快捷读取整数
int nextInt() {
return Integer.parseInt(next());
}
// 快捷读取长整数
long nextLong() {
return Long.parseLong(next());
}
// 快捷读取浮点数
double nextDouble() {
return Double.parseDouble(next());
}
}
public static void main(String[] args) {
// 实例化快读类
FastReader sc = new FastReader();
// 像使用 Scanner 一样调用,但速度提升数十倍
int n = sc.nextInt();
long m = sc.nextLong();
System.out.println("读入成功:" + n + ", " + m);
}
}
总结与建议
在算法竞赛中,选择合适的工具往往能让你事半功倍:
- 数据量较小( < 10 4 < 10^4 <104):可以使用
Scanner,图个方便和快捷。 - 数据量中等( 10 4 ∼ 10 5 10^4 \sim 10^5 104∼105):推荐换用
BufferedReader确保不会超时。 - 大数据量( > 10 5 > 10^5 >105)或多组测试数据:必须使用
BufferedReader+StringTokenizer(或使用上述封装好的FastReader模板),保障程序的稳定运行。
希望这篇博客能帮你在后续的刷题和比赛中避开 I/O 的深坑,顺利通过所有大测试点!如果你有任何关于 Java 优化的问题,欢迎在评论区一起讨论交流。
- 大数据量( > 10 5 > 10^5 >105)或多组测试数据:必须使用
BufferedReader+StringTokenizer(或使用上述封装好的FastReader模板),保障程序的稳定运行。
希望这篇博客能帮你在后续的刷题和比赛中避开 I/O 的深坑,顺利通过所有大测试点!如果你有任何关于 Java 优化的问题,欢迎在评论区一起讨论交流。
更多推荐


所有评论(0)