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

image-20260605210306593

引言:为什么你的 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 输入的底层隐患,并带你掌握算法竞赛中不可或缺的"黄金搭档"——BufferedReaderStringTokenizer


避坑指南:为什么 Scanner 和 split() 是性能杀手?

image-20260605210328302

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

为了解决上述问题,我们需要引入一套更轻量、更高性能的输入方案。

image-20260605210349868

1. BufferedReader:高效的数据搬运工

BufferedReader 的核心在于**“缓冲(Buffer)”**。它不会每次需要一个字符就去和操作系统交互,而是提前在内存中开辟一个大缓冲区(默认 8KB),一次性读入大量字符,后续的读取直接在内存缓冲区中完成,从而极大地减少了系统调用的开销。

2. StringTokenizer:轻量级的"指针切割器"

不同于 split 会生成庞大的对象数组,StringTokenizer 采用了类似于指针移动的轻量级实现:

  • 它直接在原字符串上定位,只在你需要时,通过 nextToken() 像剥糖果一样依次取出下一个词。
  • 它会自动过滤多余的空格、制表符(Tab)和换行符,容错率极高。
  • 它不需要频繁创建大量字符串垃圾,内存消耗极小。

实战演练:代码的进化史

image-20260605210410388

我们通过一个简单的场景来看看代码是如何一步步优化的。

场景:读入一个 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);
    }
}

总结与建议

在算法竞赛中,选择合适的工具往往能让你事半功倍:

  1. 数据量较小( < 10 4 < 10^4 <104:可以使用 Scanner,图个方便和快捷。
  2. 数据量中等( 10 4 ∼ 10 5 10^4 \sim 10^5 104105:推荐换用 BufferedReader 确保不会超时。
  3. 大数据量( > 10 5 > 10^5 >105)或多组测试数据:必须使用 BufferedReader + StringTokenizer(或使用上述封装好的 FastReader 模板),保障程序的稳定运行。

希望这篇博客能帮你在后续的刷题和比赛中避开 I/O 的深坑,顺利通过所有大测试点!如果你有任何关于 Java 优化的问题,欢迎在评论区一起讨论交流。

  1. 大数据量( > 10 5 > 10^5 >105)或多组测试数据:必须使用 BufferedReader + StringTokenizer(或使用上述封装好的 FastReader 模板),保障程序的稳定运行。

希望这篇博客能帮你在后续的刷题和比赛中避开 I/O 的深坑,顺利通过所有大测试点!如果你有任何关于 Java 优化的问题,欢迎在评论区一起讨论交流。

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐