1. 程式人生 > >Java端ACM輸入解析器(高效)

Java端ACM輸入解析器(高效)

程序 接收 ann 下一個 高效 英文單詞 空間 times 緩存機制

最近註冊了一個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 = 0; 142 boolean positive = true; 143 int c = nextByte(); 144 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 145 positive = c == ‘+‘; 146 } else { 147 value = ‘0‘ - c; 148 } 149 c = nextByte(); 150 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 151 value = (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 = 0; 215 boolean positive = true; 216 int c = nextByte(); 217 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 218 positive = c == ‘+‘; 219 } else { 220 value = ‘0‘ - c; 221 } 222 c = nextByte(); 223 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 224 value = (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 = 0; 250 boolean positive = true; 251 int c = nextByte(); 252 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 253 positive = c == ‘+‘; 254 } else { 255 value = c - ‘0‘; 256 } 257 c = nextByte(); 258 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 259 value = 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輸入解析器(高效)