前几天在论坛里看到一个帖子说百度的一道面试题,两个文件里各约有两亿行数据,每行只有一个数字,问如何求两个文件中数据的交集。
?
最近对大数据的处理比较感兴趣,所有思考了一下这个问题,对于JVM来说,两亿数据是非常多的,直接用数组来处理,是行不通的,另外,两亿的数据,效率也是一个重要的考量度。本来可以借助Hash的方法来解决这个问题,但因为每行只有一个数据,也就是只有数字0~9, 那么可以采用一个简便的方法,读取文件中的数字,假如是1, 那么给一个int数组的第1位加1,其他的数字如此类推,假如是2,给这个int数组的第2位加1, 直到所有的数据读取完毕。另外一个文件也采用相同的方法,最后,比较这两个int数组的相同位置上的元素,取当中小的作为交集的个数,比如a[9] = 100, b[9]=80, 那么说明在这两个文件中,有80个数字9是有交集的。最终的交集结果,用这样的统计形式表示,
{0 -- 1000}, 数字0在两个文件中有交集,并且是1000次
{1 -- 999}?
....
{8 -- 2000}
{9 -- 80}
?
基本的思路有了,问题是如何读取两亿个数字呢?即使数据采用分行方式保存,用readLine方法读取,两个文件,总共要读取4亿次,效率肯定是很低的,必须要用buffer,分析buffer,得出数字。从题目本身来说,一行只有一个数字是很好分析buffer的,考量到一定的扩展,本程序假定一行是一个数据,给出一个通用的解析方法。
?
首先定义一个基本的类,很简单,定义数据量,buffer的大小,和数据的最大值,比如10,表示文件里的数据是从0~9, 如果是100, 则是0~99。1000, 则表示0~999,这个最大值也决定了读取数据到目标数组的最大长度。
?
public class Data { int counter = 200000000; int bs = 0x80000; int max = 10; }
?
DataFinder类,从两个文件输入流中读取数据并且统计到数组,并求交集。因为考虑到每次读取到buffer中的bytes不一定是0x0a结尾,需要考虑这个0x0a在下一次读取的buffer中的情况,需要对上次结尾的一些bytes和本次读取的头几个bytes进行合并。取到一组正确的表示一个数据的bytes后,先转换成String,在转换成integer,根据这个integer来给数组中的一个特定位增加计数器。后来证明,这个思路中,需要花精力去处理的是IO及分析数据部分。
package data.comparison; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class DataFinder extends Data { private int[] readData(InputStream is) throws IOException { int[] ds = new int[max]; byte[] buff = new byte[bs]; int bc = 0; byte[] delta = null; while ((bc = (is.read(buff))) != -1) { int p = 0; int i = 0; if (delta != null) { while (buff[i++] != 0x0a) { } int len = i - p - 1; byte[] bn = new byte[delta.length + len]; System.arraycopy(delta, 0, bn, 0, delta.length); System.arraycopy(buff, 0, bn, delta.length, len); p = i; int n = Integer.parseInt(new String(bn)); ds[n]++; delta = null; } while (i < bc & p < bc) { if (buff[i] == 0x0a || i == bc - 1) { int len = i == bc - 1 ? bc - 1 - p: i - p; byte[] bn = new byte[len]; if (len==0){ System.out.println(); } System.arraycopy(buff, p, bn, 0, len); int n = Integer.parseInt(new String(bn)); ds[n]++; p = i + 1; } i++; } if (i == bc && buff[i - 1] != 0x0a) { delta = new byte[i - p]; System.arraycopy(buff, p, delta, 0, i - p); } } return ds; } public void compare(InputStream in1, InputStream in2) throws IOException { long t = System.currentTimeMillis(); StringBuilder result = new StringBuilder(32); int[] ds1 = this.readData(in1); int[] ds2 = this.readData(in2); for (int i = 0; i < ds1.length & i < ds2.length; i++) { result.append(String.format("{number: %d, occur: %d}%n", i, ds1[i] < ds2[i] ? ds1[i] : ds2[i])); } System.out.println(result.toString()); System.out.println("--- done ---"); System.out.format("comparison costs %d ms%n", System.currentTimeMillis() - t); } public static void main(String args[]) throws IOException { InputStream in1 = new FileInputStream("file1"); InputStream in2 = new FileInputStream("file2"); new DataFinder().compare(in1, in2); } }?
最后,再写一个随即创造数据的类,
package data.comparison; import java.io.FileOutputStream; import java.io.IOException; import java.util.Random; public class DataCreator extends Data { public void random(String file) throws IOException { long t = System.currentTimeMillis(); FileOutputStream writer = new FileOutputStream(file); int c = 0; while (c < counter) { Random random = new Random(); StringBuilder builder = new StringBuilder(200000); for (int i = 0; i < bs & c < counter; i++, c++) { builder.append(random.nextInt(max)).append("\n"); } writer.write(builder.toString().getBytes()); } writer.flush(); writer.close(); System.out.format("cost %d ms%n", System.currentTimeMillis() - t); } /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { DataCreator dc = new DataCreator(); dc.random("file1"); dc.random("file2"); } }
?
在I5 2.3G/4G/5400转的硬盘/Mac OS 10.7/JDK6.0.29的软硬件环境上,使用buffer方式后,性能还是比较可以的,随机写两个文件,只需要10秒不到的时间,可以接受。
cost 9313 ms cost 8884 ms
?求两个文件的交集,共花了63秒
{number: 0, occur: 20001948} {number: 1, occur: 20000771} {number: 2, occur: 19996245} {number: 3, occur: 19996415} {number: 4, occur: 19997276} {number: 5, occur: 19992915} {number: 6, occur: 19997709} {number: 7, occur: 19997344} {number: 8, occur: 19995041} {number: 9, occur: 19998757} --- done --- comparison costs 63365 ms
?
这个方法把众多的数据转换成一个较小的数组,采用计数器的方法来求交集,算法上很简练,同时也避免了JVM heap开销过大,在读取数据时,另辟蹊径,采用buffer读文件,再从buffer中分析数据,从而避免了IO的问题。 那么,对于这个面试题,还有没有更好的思路呢?
?
[全文完]