首页 > 代码库 > Java端ACM输入解析器(高效)
Java端ACM输入解析器(高效)
最近注册了一个codeforeces的帐号,想在那上面刷刷题目。但是发现它用的是控制台输入数据。因此为了能够接收到这些数据,就动手写了一个Acm的输入解析器。
先说说这个解析器的缺点:
- 这个解析器只能用于ascii码的输入,而由于acm一般为了照顾像c和c++这样的语言,都会选择使用ascii码作为输入,因此能完美符合这一限制。
- 代码比较长,总共有300行代码。
- 使用需要一定的缓存空间(约256字节)
再说说这个解析器的优点:
- 速度较Scanner快不少。
- 使用简单。
- 功能强大。(对于ACM来说足够了)
速度较快是通过测试得到的,先说一下测试环境,在预热完毕的情况下,读取同样1000组数据,并循环读取100次,最后计算得到总的时间:
Test on parse integer:
AcmInputReader parse integer for 36527739ns
Scanner parse integer for 181622212ns
Test on parse integer:
AcmInputReader parse long for 46030367ns
Scanner parse long for 162285415ns
Test on parse double:
AcmInputReader parse double for 62484605ns
Scanner parse double for 675957938ns
Test on parse decimal:
AcmInputReader parse decimal for 64672405ns
Scanner parse decimal for 531412758ns
以上就是测试结果,可以看出对于解析整数,AcmInputReader比Scanner快了3倍将近,而解析浮点数则快了10倍,读入BigDecimal的速度也快了将近10倍。说一下做的性能优化:
- 将整数x乘上10的运算以(x<<3)+(x<<1)代替。
- 利用缓存机制将EOF作为普通字符处理,这样就可以不必每次都检查是否抵达文件尾。
- 利用缓存机制分类ascii码字节代表的含义。
- 减少了浮点数除法运算的发生。
下面是实际的代码,拷贝到自己提交的ACM文件中即可:
1 /** 2 * @author dalt 3 * @since java1.7 4 * @see java.lang.AutoCloseable 5 */ 6 class AcmInputReader implements AutoCloseable{ 7 private PushbackInputStream in; 8 9 @Override 10 public void close() throws IOException { 11 in.close(); 12 } 13 14 private static class AsciiMarksLazyHolder { 15 public static final byte BLANK_MARK = 1; 16 public static final byte SIGN_MARK = 1 << 1; 17 public static final byte NUMERAL_MARK = 1 << 2; 18 public static final byte UPPERCASE_LETTER_MARK = 1 << 3; 19 public static final byte LOWERCASE_LETTER_MARK = 1 << 4; 20 public static final byte LETTER_MARK = UPPERCASE_LETTER_MARK | LOWERCASE_LETTER_MARK; 21 public static final byte EOF = 1 << 5; 22 public static byte[] asciiMarks = new byte[256]; 23 24 static { 25 for (int i = 0; i <= 32; i++) { 26 asciiMarks[i] = BLANK_MARK; 27 } 28 asciiMarks[‘+‘] = SIGN_MARK; 29 asciiMarks[‘-‘] = SIGN_MARK; 30 for (int i = ‘0‘; i <= ‘9‘; i++) { 31 asciiMarks[i] = NUMERAL_MARK; 32 } 33 for (int i = ‘a‘; i <= ‘z‘; i++) { 34 asciiMarks[i] = LOWERCASE_LETTER_MARK; 35 } 36 for (int i = ‘A‘; i <= ‘Z‘; i++) { 37 asciiMarks[i] = UPPERCASE_LETTER_MARK; 38 } 39 asciiMarks[0xff] = EOF; 40 } 41 } 42 43 /** 44 * 创建读取器 45 * 46 * @param input 输入流 47 */ 48 public AcmInputReader(InputStream input) { 49 in = new PushbackInputStream(new BufferedInputStream(input)); 50 } 51 52 private int nextByte() throws IOException { 53 return in.read() & 0xff; 54 } 55 56 /** 57 * 如果下一个字节为b,则跳过该字节 58 * 59 * @param b 被跳过的字节值 60 * @throws IOException if 输入流读取错误 61 */ 62 public void skipByte(int b) throws IOException { 63 int c; 64 if ((c = nextByte()) != b) { 65 in.unread(c); 66 } 67 } 68 69 /** 70 * 如果后续k个字节均为b,则跳过k个字节。这里{@literal k<times} 71 * 72 * @param b 被跳过的字节值 73 * @param times 跳过次数,-1表示无穷 74 * @throws IOException if 输入流读取错误 75 */ 76 public void skipByte(int b, int times) throws IOException { 77 int c; 78 while ((c = nextByte()) == b && times > 0) { 79 times--; 80 } 81 if (c != b) { 82 in.unread(c); 83 } 84 } 85 86 /** 87 * 类似于{@link #skipByte(int, int)}, 但是会跳过中间出现的空白字符。 88 * 89 * @param b 被跳过的字节值 90 * @param times 跳过次数,-1表示无穷 91 * @throws IOException if 输入流读取错误 92 */ 93 public void skipBlankAndByte(int b, int times) throws IOException { 94 int c; 95 skipBlank(); 96 while ((c = nextByte()) == b && times > 0) { 97 times--; 98 skipBlank(); 99 } 100 if (c != b) { 101 in.unread(c); 102 } 103 } 104 105 /** 106 * 读取下一块不含空白字符的字符块 107 * 108 * @return 下一块不含空白字符的字符块 109 * @throws IOException if 输入流读取错误 110 */ 111 public String nextBlock() throws IOException { 112 skipBlank(); 113 StringBuilder sb = new StringBuilder(); 114 int c = nextByte(); 115 while (AsciiMarksLazyHolder.asciiMarks[c = nextByte()] != AsciiMarksLazyHolder.BLANK_MARK) { 116 sb.append((char) c); 117 } 118 in.unread(c); 119 return sb.toString(); 120 } 121 122 /** 123 * 跳过输入流中后续空白字符 124 * 125 * @throws IOException if 输入流读取错误 126 */ 127 private void skipBlank() throws IOException { 128 int c; 129 while ((c = nextByte()) <= 32) ; 130 in.unread(c); 131 } 132 133 /** 134 * 读取下一个整数(可正可负),这里没有对溢出做判断 135 * 136 * @return 下一个整数值 137 * @throws IOException if 输入流读取错误 138 */ 139 public int nextInteger() throws IOException { 140 skipBlank(); 141 int value = http://www.mamicode.com/0; 142 boolean positive = true; 143 int c = nextByte(); 144 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 145 positive = c == ‘+‘; 146 } else { 147 value = http://www.mamicode.com/‘0‘ - c; 148 } 149 c = nextByte(); 150 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 151 value = http://www.mamicode.com/(value << 3) + (value << 1) + ‘0‘ - c; 152 c = nextByte(); 153 } 154 155 in.unread(c); 156 return positive ? -value : value; 157 } 158 159 /** 160 * 判断是否到了文件结尾 161 * 162 * @return true如果到了文件结尾,否则false 163 * @throws IOException if 输入流读取错误 164 */ 165 public boolean isMeetEOF() throws IOException { 166 int c = nextByte(); 167 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.EOF) { 168 return true; 169 } 170 in.unread(c); 171 return false; 172 } 173 174 /** 175 * 判断是否在跳过空白字符后抵达文件结尾 176 * 177 * @return true如果到了文件结尾,否则false 178 * @throws IOException if 输入流读取错误 179 */ 180 public boolean isMeetBlankAndEOF() throws IOException { 181 skipBlank(); 182 int c = nextByte(); 183 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.EOF) { 184 return true; 185 } 186 in.unread(c); 187 return false; 188 } 189 190 /** 191 * 获取下一个用英文字母组成的单词 192 * 193 * @return 下一个用英文字母组成的单词 194 */ 195 public String nextWord() throws IOException { 196 StringBuilder sb = new StringBuilder(16); 197 skipBlank(); 198 int c; 199 while ((AsciiMarksLazyHolder.asciiMarks[(c = nextByte())] & AsciiMarksLazyHolder.LETTER_MARK) != 0) { 200 sb.append((char) c); 201 } 202 in.unread(c); 203 return sb.toString(); 204 } 205 206 /** 207 * 读取下一个长整数(可正可负),这里没有对溢出做判断 208 * 209 * @return 下一个长整数值 210 * @throws IOException if 输入流读取错误 211 */ 212 public long nextLong() throws IOException { 213 skipBlank(); 214 long value = http://www.mamicode.com/0; 215 boolean positive = true; 216 int c = nextByte(); 217 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 218 positive = c == ‘+‘; 219 } else { 220 value = http://www.mamicode.com/‘0‘ - c; 221 } 222 c = nextByte(); 223 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 224 value = http://www.mamicode.com/(value << 3) + (value << 1) + ‘0‘ - c; 225 c = nextByte(); 226 } 227 in.unread(c); 228 return positive ? -value : value; 229 } 230 231 /** 232 * 读取下一个浮点数(可正可负),浮点数是近似值 233 * 234 * @return 下一个浮点数值 235 * @throws IOException if 输入流读取错误 236 */ 237 public float nextFloat() throws IOException { 238 return (float) nextDouble(); 239 } 240 241 /** 242 * 读取下一个浮点数(可正可负),浮点数是近似值 243 * 244 * @return 下一个浮点数值 245 * @throws IOException if 输入流读取错误 246 */ 247 public double nextDouble() throws IOException { 248 skipBlank(); 249 double value = http://www.mamicode.com/0; 250 boolean positive = true; 251 int c = nextByte(); 252 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 253 positive = c == ‘+‘; 254 } else { 255 value = http://www.mamicode.com/c - ‘0‘; 256 } 257 c = nextByte(); 258 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 259 value = http://www.mamicode.com/value * 10.0 + c - ‘0‘; 260 c = nextByte(); 261 } 262 263 if (c == ‘.‘) { 264 double littlePart = 0; 265 double base = 1; 266 c = nextByte(); 267 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 268 littlePart = littlePart * 10.0 + c - ‘0‘; 269 base *= 10.0; 270 c = nextByte(); 271 } 272 value += littlePart / base; 273 } 274 in.unread(c); 275 return positive ? value : -value; 276 } 277 278 /** 279 * 读取下一个高精度数值 280 * 281 * @return 下一个高精度数值 282 * @throws IOException if 输入流读取错误 283 */ 284 public BigDecimal nextDecimal() throws IOException { 285 skipBlank(); 286 StringBuilder sb = new StringBuilder(); 287 sb.append((char) nextByte()); 288 int c = nextByte(); 289 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 290 sb.append((char) c); 291 c = nextByte(); 292 } 293 if (c == ‘.‘) { 294 sb.append(‘.‘); 295 c = nextByte(); 296 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 297 sb.append((char) c); 298 c = nextByte(); 299 } 300 } 301 in.unread(c); 302 return new BigDecimal(sb.toString()); 303 } 304 }
我为了测试,在hdu上完成了题目1000(从控制台读入两个整数并输出其和,性能瓶颈为读入字符串和解析字符串),代码如下:
public static void main(String[] args) throws Exception { AcmInputReader reader = new AcmInputReader(System.in); while (!reader.isMeetBlankAndEOF()) { System.out.println(reader.nextInteger() + reader.nextInteger()); } }
执行结果为:
其中前面三行都是我所提交的java代码,可以发现比其他选手所提交的java代码无论是执行时间还是执行时内存都要低上不少。但是相对来说代码长度就变得长了很多。需要清除代码长度不会过分影响java程序性能(除非永久代溢出),而真正影响java程序性能的实际上是内存和执行效率。无疑一般用Scanner的效率和所耗费的内存都比我所提供的AcmInputReader差不少。
可能有同学要问不是说整数解析的效率是Scanner的三倍吗,为什么执行效率只有快两倍。这是由于AcmInputReader有一个初始化的动作(自动发生),这个动作只执行一次并且永久有效,但是执行一次将会拖慢运行时间。也就是说在输入量足够大的时候,性能就有可能拉到三倍,而在输入量不足的时候,初始化带来的费用无法非常好地平摊,故平均速度将被肉眼可见地拖慢。
最后再稍微说一下AcmInputReader的用法:
先说明如何从输入流读取数据,假设输入流中有下列数据:
-412958 1842119
-59.9836 13541.84141 haDaweEqrj 1241jhjndjiqHUhie1^^%%$**123
-14129501.32681261951
下面是执行代码:
1 AcmInputReader reader = new AcmInputReader(inputStream); //这里在绝大多数Acm的情况下inputStream均为System.in,即标准输入流。 2 3 reader.nextInteger(); //读取下一个32位整数,这里返回-412958 4 5 reader.nextLong(); //读取下一个64位整数,这里返回1842119 6 7 reader.nextFloat(); //读取下一个单精度浮点数,这里返回-59.9836 8 9 reader.nextDouble(); //读取下一个双精度浮点数,这里返回13541.84141 10 11 reader.nextWord(); //读取下一个英文单词(大小写字母组成),这里返回haDaweEqrj 12 13 reader.nextBlock(); //读取下一块字符串(不含空白字符),这里返回1241jhjndjiqHUhie1^^%%$**123 14 15 reader.nextDecimal(); //读取下一个高精度数值,这里返回-14129501.32681261951
继续说明如何判断是否抵达输入流结尾:
reader.isMeetBlankAndEOF(); //返回true表示抵达输入流结尾,否则返回false。
最后说明如何跳过一些分隔字符,可能有些ACM题目以特殊字符分隔数据,比如逗号。下面假设输入为1134 , 1416
1 int num1 = reader.readInteger(); //读取1134 2 reader.skipBlankAndByte(‘,‘, 1); //跳过后续的1个逗号字符,以及出现的所有空白字符。(这里跳过了" , ") 3 int num2 = reader.readInteger(); //读取1416
上面就是完整的说明了,希望大家能喜欢我所提供的这个ACM输入解析的工具,也希望大家能在java端完美的解决所有可以被其它平台优雅解决的问题。
Java端ACM输入解析器(高效)