Java实现80亿长字符串子串查找

来源:互联网 发布:软件开发服务外包 编辑:程序博客网 时间:2024/05/12 11:28

前言

  • 本博客文章只有代码块,一些输出和 try...catch 等内容没有写在这里。
  • 本博客文章的目的是熟悉 FileReaderFileWriter 这两个类库的操作,Java新手,如有不妥之处请指正!

生成一个80亿长的字符串

  • 80亿个ASCII码字符占用的空间大约为8G,所以只能分步写入硬盘。由于硬盘的速度较慢,所以一次写几个字节至文件是不太划算的,所以我们需要建立一个缓冲区,一次将缓冲区内所有的数据都写入硬盘,这样效率比较高。
  • 建立缓冲区并随机设置数据

    final int BUFFER_LEN = 8 * 1000 * 1000;char[] buffer = new char[BUFFER_LEN]; // 缓冲区大小为16Mfor (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这种高级面向对象语言的优点,失败啊。
0 0