前言
- 本博客文章只有代码块,一些输出和
try...catch
等内容没有写在这里。 - 本博客文章的目的是熟悉
FileReader
和FileWriter
这两个类库的操作,Java新手,如有不妥之处请指正!
生成一个80亿长的字符串
- 80亿个ASCII码字符占用的空间大约为8G,所以只能分步写入硬盘。由于硬盘的速度较慢,所以一次写几个字节至文件是不太划算的,所以我们需要建立一个缓冲区,一次将缓冲区内所有的数据都写入硬盘,这样效率比较高。
建立缓冲区并随机设置数据
final int BUFFER_LEN = 8 * 1000 * 1000; char[] buffer = new char[BUFFER_LEN]; // 缓冲区大小为16M for (int i = 0; i < BUFFER_LEN; ++i) { buffer[i] = (char) (Math.random() * 10 + '0'); // 随机设为'0'到'9'中的值 }
用
FileWriter
开始写入文件。注意!上一步设置数据时我设置的都是标准ASCII字符,而FileWriter
在我的平台(win10x64)上写入文件用的是utf-8
编码,一个标准ASCII字符输出到文件只会占用一个字节。所以我缓冲区的数据写入到文件中就只会占用8M的空间。fw = new FileWriter("big.txt"); for (int i = 0; i < 1000; ++i) { fw.write(buffer); }
最后别忘了关闭文件
fw.flush(); fw.close();
在我的固态硬盘上总共用了25秒,速度还可以。
插入多个子字符串
上一步我生成的字符都为数字,所以这次插入的子串只要为英文字母就可以避免冲突。这里插入100个随机字符串,长度为10~100随机。
生成随机子串。考虑到调试问题,这里的随机子串是固定的。
final int subNum = 100; char[][] subStrBuf = new char[subNum][]; Random r = new Random(11111111); for (int i = 0; i < subNum; ++i) { int len = r.nextInt(90); subStrBuf[i] = new char[len]; for (int j = 0; j < len; ++j) { subStrBuf[i][j] = (char) (r.nextInt(26) + 'a'); } }
从原文件读取,读9次缓冲区就按顺序写入上一步创建的随机子串,同时写入目标文件。因为我没办法获取原文件的字符数量,只好这样。总共需要读取1000次缓冲区,所以所有的子串都能写入目标文件。
FileReader fr = new FileReader("big.txt"); FileWriter fw = new FileWriter("big_sub.txt"); // 建立缓冲区和计数器 final int bufLen = 8 * 1000 * 1000 + 1; char[] cbuf = new char[bufLen]; int n = 0; // 开始读取 int len; while ((len = fr.read(cbuf)) != -1) { fw.write(cbuf, 0, len); // 读9个块写入一次子串 if (n % 9 == 0 && n / 9 < subNum) { fw.write(subStrBuf[n / 9]); } n += 1;
关闭文件
fr.close(); fw.flush(); fw.close();
这次需要读取+写入,所以耗时会比第一部慢,耗时71秒。
从文件中查找子串
思路是分块从文件中读取内容到缓冲区,然后再缓冲区内进行子串的查找。考虑到子串可能跨缓冲区,所以从第二次及以后的读入操作,都要保留上一次缓冲区的末尾内容,这个末尾的长度要高于子串的长度。
准备工作。设置一个数组来存放查找位置,找到多个子串的最大长度来确定缓冲区末尾的长度,以及打开文件。这里的
subStrBuf
是一个二维数组(char[][]),存放着多个随机子字符串,具体定义可以看上一步。long[] indexs = new long[subStrBuf.length]; for (int i = 0; i < subStrBuf.length; ++i) { indexs[i] = -1; } int maxLen = 0; for (int i = 0; i < subStrBuf.length; ++i) { if (subStrBuf[i].length > maxLen) { maxLen = subStrBuf.length; } } FileReader fr = new FileReader(fileName);
定义缓冲区
final int bufLen = 8 * 1000 * 1000; char[] cbuf = new char[bufLen];
读取内容到缓冲区并获取子串的位置。
long n = 0; // n存放当前的文件的绝对位置 while (true) { int len; // len存放读取到的内容的长度,为-1时代表文件结束 if (n == 0) { len = fr.read(cbuf); } else { // 保留上一个缓冲区的末尾作为这次缓冲区的头部。 for (int i = 0; i < maxLen; ++i) { cbuf[i] = cbuf[bufLen - maxLen + i]; } len = fr.read(cbuf, maxLen, bufLen - maxLen); } if (len == -1) break; // 文件结束 // 遍历子字符串 for (int i = 0; i < subStrBuf.length; ++i) { // 如果位置值不是-1就代表查找到了,不用再查 if (indexs[i] == -1) { // 自己写的Find函数,获取子串在cbuf中的位置 int tempIndex = Find(cbuf, subStrBuf[i]); if (tempIndex != -1) { // n为cbuf在整个文件中的位置 indexs[i] = n + tempIndex; } } } n += len; } // 文件结束
Find函数的定义。
private static int Find(char[] buf, char[] subStr) { // i为母串的位置 for (int i = 0; i < buf.length; ++i) { // j为子串的位置 int j; for (j = 0; j < subStr.length; ++j) { // 判断越界 if (i + j >= buf.length) return -1; // 判断是否是子串 if (buf[i + j] != subStr[j]) break; } // 循环正常退出就说明是子串,返回位置 if (j == subStr.length) return i; } return -1; }
因为这个原始查找算法的时间复杂度很高(O(n^2)),所以程序运行的时间为11分钟。
结果感言
- 算法很重要啊。如果查找算法足够优秀,那从文件中查找子串的时间会大大缩小。
- 我的硬盘是固态硬盘(读500M/s,写400M/s),机械硬盘(一般读150M,写100M)的运行时间应该会慢很多。
- 刚开始的时候完全没必要将文件大小设为8G,这样调试什么的很不方便。
fr.read(buf, offset, length);
这个函数我一直以为是fr.read(buf, start, end);
,结果一运行就报下标越界,找了许久才发现是用错了。IDE给参数提示的时候一定要认真啊。- 这整个程序用的是C语言的贴近底层的风格,完全没有体现出Java这种高级面向对象语言的优点,失败啊。