惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
T
Tor Project blog
U
Unit 42
G
Google Developers Blog
T
The Blog of Author Tim Ferriss
Recorded Future
Recorded Future
B
Blog
I
InfoQ
H
Help Net Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
小众软件
小众软件
Spread Privacy
Spread Privacy
T
Tenable Blog
C
Cybersecurity and Infrastructure Security Agency CISA
F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
I
Intezer
P
Proofpoint News Feed
A
About on SuperTechFans
S
Securelist
D
DataBreaches.Net
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
Know Your Adversary
Know Your Adversary
大猫的无限游戏
大猫的无限游戏
Cyberwarzone
Cyberwarzone
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta
MyScale Blog
MyScale Blog
V
Visual Studio Blog
A
Arctic Wolf
Hugging Face - Blog
Hugging Face - Blog
Project Zero
Project Zero
博客园 - 司徒正美
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
B
Blog RSS Feed
Vercel News
Vercel News
T
Threatpost
博客园 - Franky
有赞技术团队
有赞技术团队
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 热门话题

Kenvix's Blog

实现带有Nvidia GPU+Rootless Podman+Docker+Systemd+自动驱动注入支持的systemd nspawn容器 | Kenvix's Blog 手动保留常用端口,解决 Windows 端口被 Hyper-V / WinNAT 占用的问题 免Telent/TTL屏蔽运营商新版光猫的远控、TR069和RMS,获取动态随机超级管理员密码并固化权限 | Kenvix's Blog 利用Windows卷影副本(Volume Shadow)找回被覆盖和删除的数据 | Kenvix's Blog 在Windows下实现WireGuard动态DNS解析(DDNS)的正确方法:避免无意义的开销 | Kenvix's Blog OpenWRT/DNSMasq 配置DHCP静态路由主动推送 实现流量直达和旁路由流量零代价分载 | Kenvix's Blog 解决 Windows 打开视频/图片文件夹很慢的问题 | Kenvix's Blog AltA2DP - 向支持Sony LDAC协议的耳机提供Windows下蓝牙LDAC音频编码器支持 | Kenvix's Blog (2024更新)修复黑群晖 DSM7.0 + Btrfs 存储空间/磁盘损毁/堪用 的问题 校园网白嫖思路分享:局域网中转-不花钱、不认证、高速上网 | Kenvix's Blog 在 Windows 上配置网卡多个 VLAN、多个虚拟网卡、实现单线多拨网速叠加(无需驱动支持) | Kenvix's Blog 解决视频彩铃、语音通话自动转视频通话导致打电话自动挂断的问题 | Kenvix's Blog 超低成本廉价考研教程:如何用小于¥500甚至¥300的开销考个研 | Kenvix's Blog 在 Ubuntu 21.10 上启用蓝牙 LDAC/AAC/AptX 高质量音频编码支持 在 VMware Workstation 桥接模式的网卡上让虚拟机使用 VLAN 的正确方法 在 Windows 上设置 NAT 或网络共享的正确方法——避免Wi-Fi热点无法使用 自编译 红米 AC2100 OpenWRT R21.7.26 Linux 内核结构和子系统简介 | Kenvix's Blog 利用 ThreadLocal + Lambda,实现有状态变量的单例模式 | Kenvix's Blog 状态压缩的动态规划问题:骨牌完全覆盖棋盘问题 | Kenvix's Blog 我的 Windows 10 2004 新增 Bug 解决办法记录 解决 Android Studio 及 IDEA 中 Gradle 错误信息乱码的问题 Kotlin 的那些骚操作 | Kenvix's Blog 在普通的 Gradle Java/Kotlin 项目中使用 BuildConfig 修复国行 MIUI 打开 Google Play 始终提示 DF-DFERH-01 的问题 解决 VSCode 持续调用 WMIC 导致一个 CPU 核心完全被占满的问题 扔鸡蛋问题 | Kenvix's Blog 计算机幻觉从入门到入土 | Kenvix's Blog Java 注解预处理 Annotation Processing & 代码生成 Ubuntu 上通过以太网分享网络连接(NAT) | Kenvix's Blog Windows 选择指定的网卡来开承载网络型热点 | Kenvix's Blog 修复升级 Windows10 版本后所有内置应用闪退+第三方应用参数错误的问题 | Kenvix's Blog 配置用于 Gradle6.x + MySQL 8 的 jOOQ 3.14 代码自动生成 (已更新) 修复 Windows 环境下的程序访问 WSL 中的 MySQL 提示 Access Denied 的问题 修复 WSL 下 PHP+FastCGI 卡死的问题 使用任意磁盘或路径保存 Windows 文件历史记录 | Kenvix's Blog [1.12.2+Mod] MoeCraft :: 自由开放的科技向公益 Mod 服务器 Kenvix's Blog 禁用使用Intel核显的Windows笔记本自动调节亮度功能 | Kenvix's Blog 真正实现Minecraft高级登录(外置登录)的几种方案 | Kenvix's Blog 谈谈神舟的两艘贼船,Z7M-KP7S1 / Z7M-KP7SC USBCopyer: 插上U盘自动按需复制文件 | Kenvix's Blog USBCopyer 回调功能详细说明 | Kenvix's Blog C# 实现自定义"应用程序设置"的配置文件(user.config)存储路径 | Kenvix's Blog Win10 资源管理器为所有格式激活“编辑”按钮并修改文本文件“编辑”按钮的编辑器 | Kenvix's Blog 留言板 | Kenvix's Blog 又一次 Hello world | Kenvix's Blog Java 学习笔记 (仍在更新) | Kenvix's Blog 在Win10 Pro下挂载NFS(网络文件系统) | Kenvix's Blog Nginx 反向代理 Aria2 JSONRPC | Kenvix's Blog (Android6.0~9.0) 清除锁屏密码 | Kenvix's Blog WordPress 更换站点地址后批量修改文章/评论中的旧地址 | Kenvix's Blog 修复一加3/3T因固件过老导致刷入ROM时提示错误7的问题 | Kenvix's Blog 修复Android DM-Verity 警告 | Kenvix's Blog 贴吧云签到 资源索引(下载|文档|插件) | Kenvix's Blog 继续监控!使用树莓派+Motion实现实时视频监控并通过浏览器查看 | Kenvix's Blog 自动获取Pixiv每日排行榜第一张图片(600x600 | 可用于博客背景图) | Kenvix's Blog 使用树莓派实现定时拍照监控并发送邮件到邮箱 | Kenvix's Blog 好压 V2.7 Beta1 绿色版——功能强大,良心的压缩软件 | Kenvix's Blog 任意语言实现读取压缩包注释 | Kenvix's Blog 自己实现QQ群自定义分享(管理员开启了群交易?) | Kenvix's Blog MoeCDN - 加速Gravatar/GoogleAPIs等无法在国内访问的资源 | Kenvix's Blog [Minecraft] WebLogin-连接到你的服务器来检查玩家是否可以登录 | Kenvix's Blog Android卡刷包提示This package is for device: ... this device is ...的解决方案 Kenvix's Blog 给EMLOG评论框加上复选框[√]防止垃圾评论 | Kenvix's Blog 欢迎使用emlog | Kenvix's Blog
Java 快速读取文本 (算法竞赛适用) | Kenvix's Blog
2020-10-31 · via Kenvix's Blog

背景: 远离 Scanner

今天无意翻阅 Scanner 类时,发现了一个很坑的地方:

// 摘抄自 Scanner JDK15 源码

public final class Scanner implements Iterator<String>, Closeable {
    ....

    // Internal matcher used for finding delimiters
    private Matcher matcher;
    // Pattern used to delimit tokens
    private Pattern delimPattern;
    // Pattern found in last hasNext operation
    private Pattern hasNextPattern;

    private Pattern integerPattern;
    private String digits = "0123456789abcdefghijklmnopqrstuvwxyz";
    private String non0Digit = "[\\p{javaDigit}&&[^0]]";
    private int SIMPLE_GROUP_INDEX = 5;
    private String buildIntegerPatternString() {
        String radixDigits = digits.substring(0, radix);
        // \\p{javaDigit} is not guaranteed to be appropriate
        // here but what can we do? The final authority will be
        // whatever parse method is invoked, so ultimately the
        // Scanner will do the right thing
        String digit = "((?i)["+radixDigits+"\\p{javaDigit}])";
        String groupedNumeral = "("+non0Digit+digit+"?"+digit+"?("+
                                groupSeparator+digit+digit+digit+")+)";
        // digit++ is the possessive form which is necessary for reducing
        // backtracking that would otherwise cause unacceptable performance
        String numeral = "(("+ digit+"++)|"+groupedNumeral+")";
        String javaStyleInteger = "([-+]?(" + numeral + "))";
        String negativeInteger = negativePrefix + numeral + negativeSuffix;
        String positiveInteger = positivePrefix + numeral + positiveSuffix;
        return "("+ javaStyleInteger + ")|(" +
            positiveInteger + ")|(" +
            negativeInteger + ")";
    }
    private Pattern integerPattern() {
        if (integerPattern == null) {
            integerPattern = patternCache.forName(buildIntegerPatternString());
        }
        return integerPattern;
    }

    public int nextInt(int radix) {
        // Check cached result
        if ((typeCache != null) && (typeCache instanceof Integer)
            && this.radix == radix) {
            int val = ((Integer)typeCache).intValue();
            useTypeCache();
            return val;
        }
        setRadix(radix);
        clearCaches();
        // Search for next int
        try {
            String s = next(integerPattern());
            if (matcher.group(SIMPLE_GROUP_INDEX) == null)
                s = processIntegerToken(s);
            return Integer.parseInt(s, radix);
        } catch (NumberFormatException nfe) {
            position = matcher.start(); // don't skip bad token
            throw new InputMismatchException(nfe.getMessage());
        }
    }

    ....
}

可见,Java 的 Scanner 是基于正则表达式实现的,这意味着 Scanner 的效率 相当低。如果在蓝桥杯之类的算法竞赛用 Scanner,会有很大一部分CPU时间会浪费在 Scanner 的正则解析上,就很可能跑出 TLE

/img/jin1.jpg

这篇文章不探讨如何快速读取大型一般二进制文件,关注重点在于在低 JDK 版本下,如何快速解析文本,而非 “快速“ 读取文件。快速读取一般二进制大文件请考虑:

认识 StringTokenizer

StringTokenizer 是一个自 Java 1.0 就被引入的类,用于以标识符分隔字符串,也就是解析文本

Scanner 之所以复杂,主要在于它假定所有输入数据都是任意的、类型是不确定的,同时 Scanner 还要兼顾处理输入流,所以不得不使用正则表达式去进行处理。

由于算法竞赛的给定数据的类型都是已知的,我们可以直接使用 StringTokenizer 去代替 Scanner

有报告显示,StringTokenizer 结合 BufferedReader 读入流的性能甚至高于 C 语言的 scanf()

构造方法

StringTokenizer 有三个常用构造方法

  1. 直接输入要解析的字符串,默认会把 “ \t\n\r\f” 当作分隔符。同时,解析返回结果不包含分隔符

Constructs a string tokenizer for the specified string. The tokenizer uses the default delimiter set, which is “ \t\n\r\f”: the space character, the tab character, the newline character, the carriage-return character, and the form-feed character. Delimiter characters themselves will not be treated as tokens.
Params: str – a string to be parsed.
Throws: NullPointerException – if str is null

public StringTokenizer(String str) {
    this(str, " \t\n\r\f", false);
}
  1. 输入要解析的字符串和分隔符列表。解析返回结果不包含分隔符

Constructs a string tokenizer for the specified string. The characters in the delim argument are the delimiters for separating tokens. Delimiter characters themselves will not be treated as tokens.
Note that if delim is null, this constructor does not throw an exception. However, trying to invoke other methods on the resulting StringTokenizer may result in a NullPointerException.
Params: str – a string to be parsed.
delim – the delimiters.
Throws: NullPointerException – if str is null

public StringTokenizer(String str, String delim) {
    this(str, delim, false);
}
  1. 输入要解析的字符串和分隔符列表。你决定要不要包含分隔符,true 为包含。

用法

获取每一段字符串 nextToken()

public String nextToken()

返回每一段被分隔的字符串 (token),等效于 Scanner.next(), 返回结果是否包含分隔符取决于你的构造参数

如果没有更多 token,会抛出 java.util.NoSuchElementException

看看还有没有更多待分隔的字符串 hasMoreTokens()

public boolean hasMoreTokens()

true 表示有更多,可以安全地调用 nextToken()

增强 StringTokenizer

问题来了,StringTokenizer 好像并没有 ScannernextInt() nextLong() 之类的东西,怎么办?

现场写个类去解析他!

import java.util.StringTokenizer;

public class ExtendedStringTokenizer extends StringTokenizer {

    public ExtendedStringTokenizer(String str) {
        super(str);
    }

    public long nextLong() {
        return Long.parseLong(this.nextToken());
    }

    public double nextDouble() {
        return Double.parseDouble(this.nextToken());
    }

    public int nextInt() {
        return Integer.parseInt(nextToken());
    }
}

只需继承一下 StringTokenizer,寥寥几行代码便可搞定

注意:此处 nextInt() nextLong() 不允许输入为小数,否则会引发解析错误。如果确实需要处理小数,可以考虑在解析前使用 indexOf()substring() 截去小数。

文件,以及有 EOF 的流快读方案

现在知道如何解析字符串了,但是怎么从流中读入字符串呢?

假设现在我们有了一个 InputStream inputStream,这个 InputStream 可以是:

  • 标准输入 System.in
  • 文件 FileInputStream("FileName")

由于算法竞赛的 JDK 版本一般很低,无法使用 NIO

一次性全部读入

这是性能最好的,并且是典型的空间换时间的方案。

以蓝桥杯竞赛为例,通常提供高达 512 MB 的算法运行内存空间,这个时候可以直接把整个流全部读入后解析。

DataInputStream stream = new DataInputStream(inputStream);
ExtendedStringTokenizer tokenizer = new ExtendedStringTokenizer(new String(stream.readAllBytes()));

注: Java 1.7 以上可以使用 Files.readString(Path.of("filename")) 直接读入整个文件到字符串

为什么不直接使用 DataInputStream

有读者可能注意到了这里并没有直接使用 DataInputStream 提供的 readInt() readDouble() 等方法,这里有一点需要理解:

例如,下面代码:

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream("5 1234567 2".getBytes());
DataInputStream dataInputStream = new DataInputStream(byteArrayInputStream);
System.out.println(dataInputStream.readLong());
System.out.println(dataInputStream.readLong());
System.out.println(dataInputStream.readLong());

输出:

3828113774942106934
Exception in thread "main" java.io.EOFException

根本不是我们想要的。

上面的说法是为了告诉你不要用它,事实上,更准确的说,readInt() readDouble() 是把一串二进制字节当作一个整体读入,用于处理二进制文件。与我们今天要做的事情(读取文本)一点关系都没有。

逐行读入

Reader 是专用于处理字符流的类,相对的,Stream 处理的是字节流

逐行读入时,使用 BufferedReader 包装 InputStreamReaderInputStreamReader 包装 InputStream 可以有效提升性能

// 注:BufferedReader 可以提供第二个参数,类型为 int,表示缓冲区容量,提供一个较大的容量可以更快
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;

while ((line = reader.readLine()) != null) {
    ExtendedStringTokenizer tokenizer = new ExtendedStringTokenizer(line);
    // TODO: your code
}

如果读取目标是文件,可以用 new FileReader("filename") 代替 new InputStreamReader(inputStream)

由于 StringTokenizer 只是一个简单的小对象,所以循环反复创建的开销还是很小的。但还是比一次性读入慢了很多

不提供 EOF 的无限流快速方案

注意:System.in 标准输入流不一定不提供 EOF,若用户在终端上按下 Ctrl+D 或通过其他方式输入等效内容,则System.in 会将此视作一个 EOF。但若用户一直不按 Ctrl+D 那就是无限等待输入了。

由于这种流不提供 EOF,我们没法直接全部读入,直接全部读入会造成永久阻塞。

但是,这种流肯定会告诉我们输入数据有多少行,我们可以用行数作为依据判断是否还要继续读。

假设第一行告诉我们后续输入有多少行:

// 注:BufferedReader 可以提供第二个参数,类型为 int,表示缓冲区容量,提供一个较大的容量可以更快
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
ExtendedStringTokenizer initTokenizer = new ExtendedStringTokenizer(reader.readLine());
int lineNum = initTokenizer.nextInt();

for (int i = 0; i < lineNum; i++) {
    // TODO: your code
    ExtendedStringTokenizer tokenizer = new ExtendedStringTokenizer(reader.readLine());
}

如果读取目标是文件,可以用 new FileReader("filename") 代替 new InputStreamReader(inputStream)

Reader 逐字符读取

如果输入数据,一个字符(注意不是字节)就是一个整体,那么还可以逐字符读取:

所用方法:

public int read()

读一个字符,并返回它。如果读到了 EOF 则返回 -1

例如:读取一个 ASCII 编码的、读取目标为数字、有换行符的文件到一个二维数组,每一个数字都是一个整体,每次换行就读取到数组的下一行。

var reader = new BufferedReader(new FileReader(PATH), 1 << 16);

byte[][] maze = new byte[100][100]; //读取的结果位于 0~9 之间,byte省空间
int lineCode = 0; // 也是实际有效行数
int colCode = 0;  // 实际有效列数
int token;

while ((token = reader.read()) != -1) {
    if (token >= '0' && token <= '9') {
        maze[lineCode][colCode] = (byte) (token - '0');
        colCode++;
    } else if (token == '\n') {
        lineCode++;

        if (colCode == 0) {
            colNum = colCode;
        }

        colCode = 0;
    }
}

多个数字(字符)是一个整体

对于多个数字是一个整体的,当然也可以使用自行读取一组数据到缓冲区,然后自行解析。然而,编写解析代码所花费的时间可能远不如使用 StringTokenizer 划算,而且这样操作带来的性能提升实际上也非常有限。

所用 Reader 方法:

public int read()
public int read(char cbuf[], int off, int len) throws IOException

后者为:从流中读取指定长度(len)的字符流,并从偏移量(off)开始装入你的数组 cbuf[],返回实际读取的有效长度(实际长度小于等于 len)

当然也可以使用前文提到的 DataInputStream

后记:一些常见误区

遍历字符串,不要 toCharArray() 后遍历

就我个人在 LeetCode 的刷题经验,当字符串编码为 Latin 时,直接 String.charAt() 会比 toCharArray() 后遍历更快(事实上在 JDK 源代码也可以找到相关依据)。

若对此有疑问,请阅读 toCharArray() 的源代码,并与 String.charAt() 进行对比,可见:

  • toCharArray() 会创建一个新字符数组,然后逐字符拷贝,并与 0xFF 进行与运算,这比 System.arraycopy() 直接数组拷贝的性能低很多。
  • 尽管 charAt() 也会进行一次与运算,但却不需要逐字拷贝而是直接访问 String 内部的字符数组。