图的三角形计数
需求:输入一份社交图谱,每行是两个ID,中间用空格分开,代表这两个人有联系,组成一个无向图,计算这个无向图中三角形的个数。
首先我需要将输入文件转化成一个无向图,这个比较简单,使用map操作将两个ID提取出来,如果两个ID相同,说明自己指向了自己,那么舍弃掉这个输入,map的输出在传递给reduce前会将相同key的值排序组成一个列表,这样我们就有了表示无向图的邻接表。相应的map部分的代码如下所示:
static class GBMapper extends Mapper<LongWritable, Text, Text, Text> { @Override public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] strs = value.toString().split(" "); if (!strs[0].equals(strs[1])) context.write(new Text(strs[0]), new Text(strs[1])); } }
接下来我出现了一个错误,我以为不需要写reduce部分的代码了,因为传递给reduce时已经将相同key的值已经组合在一起了,那么默认的reduce会将输入原原本本地输出来。后来查看中看中间结果的时候,发现存储下来的还是每一行两个ID,为什么呢?(TODO)
因为,即使传递给reduce的键值对,相同key的值已经组合在一起了,但是每一次调用reduce时,将会遍历每一个value,并写出去,默认的Reducer中的reduce源码如下:
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context ) throws IOException, InterruptedException { for(VALUEIN value: values) { context.write((KEYOUT) key, (VALUEOUT) value); } }
所以我尝试自定义了一个reduce,就是将Iterable<Text>里面的值写入到输出中,代码如下:
static class GBReducer extends Reducer<Text, Text, Text, Text> { private StringBuilder newValue = new StringBuilder(); @Override public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { for (Text value : values) newValue.append(value.toString() + " "); context.write(key, new Text(newValue.toString())); newValue = new StringBuilder(); } }
在写上面代码的时候,又踩了一个坑。当时跑代码的时候,reduce消耗了好长时间,自己推算了一下不应该这么长时间,后来发现是因为每次append完之后没有将newValue还原,所以对于下一个输入,newValue会接着append,导致newValue越来越大,极大地减慢了运行速度,而且最后生成的文件将会占据很大的空间。其实应该将newValue定义为reduce的局部变量,而不是成员变量。
以上代码我们将输入文件转化成了图的邻接表,现在需要统计三角形的个数。思路是这样的:首先得到的邻接表中key是一个顶点,values是与这个顶点连在一起的点,那么可以与这个顶点构成的三角形中需要的另一条边将是values中两两组合得到的边,所以我们需要统计已经存在的边,以及需要的边,key就是边的两个点,value就是表示已经存在(“e”)或者被需要(”n“),最后统计某条边的值是否既有“e“又有”n“,这样”n“的个数代表这条边可以构成三角形的数目,最后将”n”的个数累加即可。代码如下:
static class CMapper extends Mapper<Text, Text, Text, Text> { static final Text exist = new Text("e"); static final Text need = new Text("n"); private String newKey; // Input is Sequence Text @Override public void map(Text key, Text value, Context context) throws IOException, InterruptedException { String keyStr = key.toString(); String[] values = value.toString().split(" "); if (values.length != 0) { for (int i = 0; i < values.length; ++i) { if (values[i].compareTo(keyStr) > 0) newKey = keyStr + values[i]; else newKey = values[i] + keyStr; context.write(new Text(newKey), exist); for (int j = i + 1; j < values.length; ++j) { if (values[i].compareTo(values[j]) > 0) newKey = values[j] + values[i]; else newKey = values[i] + values[j]; context.write(new Text(newKey), need); } } } } }
// Reduce number must be 1 static class CReducer extends Reducer<Text, Text, Text, Text> { private static long totalCount = 0; @Override public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { boolean existState = false; long needCount = 0; for (Text value : values) { if (!existState && value.toString().equals("e")) existState = true; if (value.toString().equals("n")) ++needCount; } totalCount += needCount; } @Override public void cleanup(Context context) throws IOException, InterruptedException { context.write(new Text("The total number of triangle:"), new Text(Long.toString(totalCount))); } }
上面的代码中,我们需要的reduce的数目必须为1,因为每个键值对得到的“n”的个数要累计到totalCount中。