目录
二、实现思路(想看程序如何设计的,不看代码,看这个也行)
三、代码与详解(比较精髓,但仅限于个人理解)
一、项目需求
(1)需求简介
类似于QQ好友推荐,在现实生活中,如果你的好友A和B都有一个共同的好友C,那么C很可能是你的潜在好友。如果C在你的朋友的关系中出现的次数越多,这种概率越大。
(2)问题描述
给出以下数据,为每个人推荐好友,按照共同好友的多少排序(最多10个),数据代表3有后面的好友,也代表,后面的好友都有3这个好友,因此后面好友都算是互相的隐藏好友。
(3)需求理解
社交网站上的各个用户以及用户之间的相互关注可以抽象为一个图。以下图为例:
顶点A、B、C到I分别是社交网站的用户,两顶点之间的边表示两顶点代表的用户之间相互关注。那么如何根据用户之间相互关注所构成的图,来向每个用户推荐好友呢?
现在我们以上图为例,介绍下如何利用用户之间相互关注所构成的图,来向每个用户推荐好友。首先我们不得不假设的是如果两用户之间相互关注,那么我们认为他们认识或者说是现实中的好友,至少应该认识。假设我们现在需要向用户I推荐好友,我们发现用户I的好友有H、G、C。其中H的好友还有A,G的好友还有F,C的好友还有B、F。那么用户I、H、G、C、A、B、F极有可能是同一个圈子里的人。我们应该把用户A、B、F推荐给用户I认识。进一步的想,用户F跟两位I的好友C、G是好友,而用户A、B都分别只跟一位I的好友是好友,那么相对于A、B来说,F当然更应该推荐给用户I认识。
(4)相关数据
可自行设计,可以使用如下数据进行操作。
链接:https://pan.baidu.com/s/1YWwhdfviRt7Cncjy9YXZvg
提取码:p8i7
如何将数据导入,参考上一篇文章https://blog.csdn.net/qq_40304825/article/details/90900005
二、实现思路
(1)Map设计:
- 从单行数据(0 1,3,4,5,7)入手。
- 得出0和1是直接好友,1和3是间接好友。
- 依照直接或者间接关系,打上标签,true为直接好友,false为间接好友
- 如0号和1号是直接好友,我们建立键值对,打上标签为(0 : 1,true)
- 如1号和3号暂时是间接好友,我们建立键值对,打上标签为(1 : 3,false)
以此类推:
- 直接好友:首先对用户0号本身,将0与其好友打上标签。
结果为:(0: true,1) (0: true,3) (0: true,4) (0: true,5) (0: true,7) - 间接好友:然后对于好友集合:笛卡尔乘积,互相打上标签2。1,3,4,5,7 X 1,3,4,5,7。(1: false,3) (3: false,1) (1: false,4) (4: false,1) 等…….
然后对于好友集合:笛卡尔乘积
(2)Reduce设计
Reduce接收分三种情况:
- 第一种情况:(0,true,1),(0,true,3),(0,true,4)
对于这情况(标签为true)-直接好友
如:(0: true,1) 告诉了我们0用户和1用户肯定是直接好友,所以我们不用把1用户推荐给0用户。 - 第二种情况(标签为2)
说明是暂时的间接好友
如:(1: false,4) 告诉了我们1用户和4用户暂时是间接好友关系,可以考虑进行推荐,我们的reduce过程便可以进行记录 - 第三种情况:如果又来了一个(1: false,4)呢?这说明了什么?
说明1和4有两个共同好友 - 第四种情况:如果之后又出现(1: true,4)呢?
这说明1和4他们已经互相认识了,我们不再将4推荐给1了
实现:接收map传递的键值对,要求key为0 如 (0: false,3)
- 创建一个容器进行记录(用HashMap)(用户id:共同好友数),HashSet表(该用户的直接好友)
- 如果标签为true,则添加至直接好友中,
- 如果标签为2并且没在黑名单,便进行记录计数
-
如 map.put(3,1)表示3与之有一个共同好友,再来一个就是map.put(3,1+1)
三、代码与详解
如何创建工程也在上一篇博客上写到https://blog.csdn.net/qq_40304825/article/details/90900005
(1)map代码
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;//注意此处Mapper的引用
import java.io.IOException;
public class FriendCountMapper extends Mapper<LongWritable, Text,Text, FriendCountBean> {
/*注意Mapper的四个范型,分别代表了输入的键值对类型,输出的键值对类型,一共四个。
*其中LongWritable是apache自定的基本数据类型,相当于平时我们理解的Long。
*其中Text是apache自定的基本数据类型,相当于平时我们理解的String,但是在操作时候最好调用toString方法,再进行操作。
*第一个范型(LongWritable)代表了本行数据是第几行,因为map是按行进行运行的。
*第二个范型(Text)代表了该行的文本信息。
*第三个范型(Text)代表了输出key的类型,这样结合map的Context来看。
*第四个范型(FriendCountBean)首先,这是我们自定义的类,其次,这个代表map输出value的类型。
**/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
/*
*该函数有三个参数类型,分别代表了如下信息:
*第一个参数,对应上面的第一个范型,意义也相同
*第二个参数,对应上面的第二个范型,意义相同
*第三个参数,表示该Map最终的输出,在本程序的末尾我们可以看到我们输出的是一个键值对,其键值对的形式为<Text,FriendCountBean>,与上面最后两个参数相对应。
**/
String data = value.toString(); //将Text类型转换成我们熟悉的String,再操作
String[] userAndFriends = data.split("\t"); //获得用户和该用户的朋友们
if (userAndFriends.length != 2){
System.err.println("该行不成功");
return; //返回为空,就代表这个Map废弃掉了
}
//接下来的操作就根据实现思路进行设计
String user = userAndFriends[0];
String[] friends = userAndFriends[1].split(",");
for (String friend:friends) {
context.write(new Text(user), new FriendCountBean(friend, true));
}
for (int i = 0;i < friends.length; i++){
for (int j = i+1 ;j < friends.length; j++){
/*一个Map可多次调用context.write,即一个Map可写入多行数据给Reduce进行操作,Reduce也是按行操作的。*/
context.write(new Text(friends[i]), new FriendCountBean(friends[j], false));
context.write(new Text(friends[j]), new FriendCountBean(friends[i], false));
}
}
}
}
(2)Reduce代码
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.*;
public class FriendCountReducer extends Reducer<Text, FriendCountBean, Text, Text> {
/*与Map相同,四个参数都分别代表输入的键值对和输出的键值对。
*但是有一点需要注意的是,Reduce的前两个就是Map的后两个,这个也很容易理解,因为Reduce的输入是Map通过调用Context.write方法产生的。
**/
@Override
protected void reduce(Text key, Iterable<FriendCountBean> values, Context context) throws IOException, InterruptedException {
//与Map的参数类型相似,可同步理解
/*需注意,这里key不再是行数,而是Map传递过来的key
*values是一个迭代器,因为多个Map可能对同一个key进行了操作,因此他们将操作得到的结果放在一起,共享相同的key,因此,我们操作时,需要使用迭代的方法取出相应的值
**/
//接下来就按设计思路去进行代码编写
Map<String,Integer> recommendation = new HashMap<String, Integer>(); //用于记录这个朋友被几个人推荐过
Set<String> friendsList = new HashSet<String>(); //用于记录该用户(key)的好友列表
//统计出每个人的推荐度,格式为<用户名:推荐度>
for (FriendCountBean friend:values) {//遍历多个值
String name = friend.getName();
if (!recommendation.containsKey(friend.getName())){//如果不存在这个朋友的信息,则添加
recommendation.put(name,0);
}
if (friend.isFriend()){//如果是朋友,就把之前的推荐度清空,并加入朋友列表中
friendsList.add(name);
recommendation.put(name,0);
continue;
}else if (!friendsList.contains(name)){//如果不存在朋友列表中,就添加推荐度
int count = recommendation.get(name);
count++;
recommendation.put(name, count);
}
}
//根据推荐度对其进行排序
List<Map.Entry<String,Integer>> list = new ArrayList<Map.Entry<String,Integer>>(recommendation.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
return o2.getValue()-o1.getValue();
}
});
String message = "";
//对已进行排序的列表选取前十个打印
//如果推荐度为0,就从此以后的数据不打印
if (list.size() >= 10){
for(int i = 0;i<10;i++){
if (list.get(i).getValue() <= 0){
break;
}
message+="推荐:"+list.get(i).getKey()+",推荐度:"+list.get(i).getValue()+"; ";
}
}else {//如果不够10个元素,就遍历
for(int i = 0; i<list.size();i++){
if (list.get(i).getValue() <= 0){
break;
}
message+="推荐:"+list.get(i).getKey()+",推荐度:"+list.get(i).getValue()+"; ";
}
}
context.write(key, new Text(message));
}
}
(3)Bean代码
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/*
*这个类,主要存放用户名和是否是直接好友关系
*/
public class FriendCountBean implements Writable {//必须实现这个接口,并实现相应的方法
//更严谨一点的话,这里应该是private
String name;
boolean isFriend;
//构造函数一定要写一个无参构造函数,不然会报错
public FriendCountBean(String name, boolean isFriend) {
this.name = name;
this.isFriend = isFriend;
}
public FriendCountBean() {
}
public String getName() {
return name;
}
public boolean isFriend() {
return isFriend;
}
//必须要实现的两个方法,基本也是格式化的,就这么写就行了
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(name);
dataOutput.writeBoolean(isFriend);
}
public void readFields(DataInput dataInput) throws IOException {
this.name = dataInput.readUTF();
this.isFriend = dataInput.readBoolean();
}
}
(4)Job代码,也是程序的入口
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
//这个也是固定了的,只要把相关的参数修改成自己的就行
public class FriendCount {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration, "friend count");
job.setMapperClass(FriendCountMapper.class);
job.setReducerClass(FriendCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FriendCountBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean result = job.waitForCompletion(true);
if (!result){
System.out.println("friend count failed");
}
}
}
四、程序运行结果
如何运行程序,在上一篇文章中有做过相应的解释https://blog.csdn.net/qq_40304825/article/details/90900005
数据有点多,看部分结果