在Java中,从System.in读取数据的最快方法是什么?

68

我正在使用 Scanner(System.in) 从标准输入中读取一系列以空格或换行符分隔的整数。

在Java中是否有更快的方法来完成这个任务?


可能是在Java中从文件读取大量数据的重复问题。 - Crozin
你需要每秒读取多少百万个整数?如果你只有不到几百万个,那就不用太担心了。 - Peter Lawrey
我在编程比赛中遇到了这个问题。通常情况下,你会得到成千上万个问题实例,每个实例都有数千个数字(为了确保你不能用复杂度较差的解决方案)。 - aioobe
1
是的,这是为编程竞赛而设计的,我需要读取数千行代码。我注意到在C++中,cin比Java中的Scanner快得多,因此想知道是否存在其他替代方案。 - pathikrit
未来的搜索者也应该查看这个线程:https://dev59.com/FXRB5IYBdhLWcg3wNk53 - Jon
9个回答

98

有没有更快的Java实现方式?

有。根据我的经验,Scanner 的速度相当慢。

如果您不需要验证输入,建议将流包装在 BufferedInputStream 中,并使用类似于 String.split / Integer.parseInt 这样的方法。


下面是一个小比较:

使用此代码读取17兆字节(4233600个数字)

Scanner scanner = new Scanner(System.in);
while (scanner.hasNext())
    sum += scanner.nextInt();

这段代码在我的电脑上执行共花费3.3秒。而这段代码如下:

BufferedReader bi = new BufferedReader(new InputStreamReader(System.in));
String line;
while ((line = bi.readLine()) != null)
    for (String numStr: line.split("\\s"))
        sum += Integer.parseInt(numStr);

花费0.7秒

通过进一步混淆代码(使用String.indexOf / String.substring 迭代line),您可以轻松将其缩短到约0.1秒,但我想我已经回答了您的问题,而且我不想把它变成一些代码高尔夫比赛。


然而,你最好有一个很好的理由来添加这样的混乱到你的代码中。 - Ryan Stewart
26
String.split不如StringTokenizer快。为了得到最高效的代码,请使用StringTokenizer。 - kullalok

4

我创建了一个小 InputReader 类,它的工作方式类似于Java的Scanner,但速度比它快得多,事实上,它甚至比BufferedReader也快。这里有一个条形图,显示了我创建的InputReader类从标准输入读取不同类型数据的性能:

enter image description here

以下是使用InputReader类来计算System.in中所有数字总和的两种不同方法:

int sum = 0;
InputReader in = new InputReader(System.in);

// Approach #1
try {

    // Read all strings and then parse them to integers (this is much slower than the next method).
    String strNum = null;
    while( (strNum = in.nextString()) != null )
        sum += Integer.parseInt(strNum);

} catch (IOException e) { }

// Approach #2
try {

    // Read all the integers in the stream and stop once an IOException is thrown
    while( true ) sum += in.nextInt();

} catch (IOException e) { }

1
不幸的是,在OpenJKD8中,InputReader.readLine()BufferedReader.readLine()相比并没有明显的速度提升。缓冲区大小为2048,流长度为2.5 MB。更糟糕的是:该代码会破坏(UTF-8)字符编码。 - try-catch-finally

3

如果从竞技编程的角度来看,如果提交速度不够快就会得到TLE(时间超限)
然后您可以查看以下方法从System.in中检索字符串。 这是我从java(竞技网站)最优秀的程序员那里学到的。

private String ns()
{
    int b = skip();
    StringBuilder sb = new StringBuilder();
    while(!(isSpaceChar(b))){ // when nextLine, (isSpaceChar(b) && b != ' ')
        sb.appendCodePoint(b);
        b = readByte();
    }
    return sb.toString();
}`

1

StringTokenizer 是一种更快的读取以标记分隔的字符串输入的方法。

请查看下面的示例,以读取由空格分隔的整数字符串并存储在数组列表中:

String str = input.readLine(); //read string of integers using BufferedReader e.g. "1 2 3 4"
List<Integer> list = new ArrayList<>();
StringTokenizer st = new StringTokenizer(str, " ");
while (st.hasMoreTokens()) {
    list.add(Integer.parseInt(st.nextToken()));
} 

1
你可以逐位从System.in读取数据。看一下这个答案:https://dev59.com/YnE85IYBdhLWcg3wnU0d#2698772
我在这里复制了代码(稍作修改)。基本上,它读取由任何非数字字符分隔的整数。(原作者获得荣誉。)
private static int readInt() throws IOException {
    int ret = 0;
    boolean dig = false;
    for (int c = 0; (c = System.in.read()) != -1; ) {
        if (c >= '0' && c <= '9') {
            dig = true;
            ret = ret * 10 + c - '0';
        } else if (dig) break;
    }
    return ret;
}

在我的问题中,这段代码大约比使用StringTokenizer快2倍,而String.split(" ")已经比它快了。 (该问题涉及读取每个最多为100万的1百万个整数。)

0
从编程角度来看,这个定制的扫描和打印类比Java内置的Scanner和BufferedReader类要好得多。
import java.io.InputStream;
import java.util.InputMismatchException;
import java.io.IOException;

public class Scan
{

private byte[] buf = new byte[1024];

private int total;
private int index;
private InputStream in;

public Scan()
{
    in = System.in;
}

public int scan() throws IOException
{

    if(total < 0)
        throw new InputMismatchException();

    if(index >= total)
    {
        index = 0;
        total = in.read(buf);
        if(total <= 0)
            return -1;
    }

    return buf[index++];
}


public int scanInt() throws IOException
{

    int integer = 0;

    int n = scan();

    while(isWhiteSpace(n))   /*  remove starting white spaces   */
        n = scan();

    int neg = 1;
    if(n == '-')
    {
        neg = -1;
        n = scan();
    }

    while(!isWhiteSpace(n))
    {

        if(n >= '0' && n <= '9')
        {
            integer *= 10;
            integer += n-'0';
            n = scan();
        }
        else
            throw new InputMismatchException();
    }

    return neg*integer;
}


public String scanString()throws IOException
{
    StringBuilder sb = new StringBuilder();

    int n = scan();

    while(isWhiteSpace(n))
        n = scan();

    while(!isWhiteSpace(n))
    {
        sb.append((char)n);
        n = scan();
    }

    return sb.toString();
}


public double scanDouble()throws IOException
{
    double doub=0;
    int n=scan();
    while(isWhiteSpace(n))
    n=scan();
    int neg=1;
    if(n=='-')
    {
        neg=-1;
        n=scan();
    }
    while(!isWhiteSpace(n)&& n != '.')
    {
        if(n>='0'&&n<='9')
        {
            doub*=10;
            doub+=n-'0';
            n=scan();
        }
        else throw new InputMismatchException();
    }
    if(n=='.')
    {
        n=scan();
        double temp=1;
        while(!isWhiteSpace(n))
        {
            if(n>='0'&&n<='9')
            {
                temp/=10;
                doub+=(n-'0')*temp;
                n=scan();
            }
            else throw new InputMismatchException();
        }
    }
    return doub*neg;
}

public boolean isWhiteSpace(int n)
{
    if(n == ' ' || n == '\n' || n == '\r' || n == '\t' || n == -1)
        return true;

    return false;
}

public void close()throws IOException
{
    in.close();
}
}

自定义的打印类可以如下所示

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class Print
{
private BufferedWriter bw;

public Print()
{
    this.bw = new BufferedWriter(new OutputStreamWriter(System.out));
}


public void print(Object object)throws IOException
{
    bw.append("" + object);
}

public void println(Object object)throws IOException
{
    print(object);
    bw.append("\n");
}


public void close()throws IOException
{
    bw.close();
}

}

0
你可以使用 BufferedReader 来读取数据。
BufferedReader inp = new BufferedReader(new InputStreamReader(System.in));
  int t = Integer.parseInt(inp.readLine());
  while(t-->0){
    int n = Integer.parseInt(inp.readLine());
    int[] arr = new int[n];
    String line = inp.readLine();
    String[] str = line.trim().split("\\s+");
    for(int i=0;i<n;i++){
      arr[i] = Integer.parseInt(str[i]);
    }

而对于打印,请使用StringBuffer。
    StringBuffer sb = new StringBuffer();
    for(int i=0;i<n;i++){
              sb.append(arr[i]+" "); 
            }
    System.out.println(sb);

0
从磁盘读取数据多次会使扫描器变慢。我喜欢使用BufferedReader和Scanner的组合,以兼顾二者的优点,即BufferedReader的速度和Scanner丰富易用的API。
Scanner scanner = new Scanner(new BufferedReader(new InputStreamReader(System.in)));

0

这是完整版本的快速读写器。我还使用了缓冲。

import java.io.*;
import java.util.*;


public class FastReader {
    private static StringTokenizer st;
    private static BufferedReader in;
    private static PrintWriter pw;


    public static void main(String[] args) throws IOException {

        in = new BufferedReader(new InputStreamReader(System.in));
        pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        st = new StringTokenizer("");

        pw.close();
    }

    private static int nextInt() throws IOException {
        return Integer.parseInt(next());
    }
    private static long nextLong() throws IOException {
        return Long.parseLong(next());
    }
    private static double nextDouble() throws IOException {
        return Double.parseDouble(next());
    }
    private static String next() throws IOException {
        while(!st.hasMoreElements() || st == null){
            st = new StringTokenizer(in.readLine());
        }
        return st.nextToken();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接