
本教程详细阐述了如何在java中为推荐系统构建一个非加权图。内容涵盖了从csv文件加载人员和活动数据,将数据存储为对象列表,以及基于共同社区、学校或雇主来定义“密切联系人”关系并构建图结构。同时,文章强调了隐私设置在推荐逻辑中的应用,并提供了具体的代码示例和实现建议,旨在帮助开发者有效组织和处理复杂的人际关系数据。
引言:图数据结构在推荐系统中的应用
在构建基于人际关系的推荐系统时,图数据结构是一种极其强大且直观的工具。它能够有效地表示实体(如人)及其之间的复杂关系。例如,在一个联系人推荐系统中,我们可以将每个人视为图中的一个“节点”(Vertex),而他们之间的“密切联系”则可以表示为连接这些节点的“边”(Edge)。本教程将指导您如何从原始数据开始,逐步构建一个非加权图,以实现基于共同社区、学校或雇主以及隐私设置的联系人推荐功能。
数据模型设计
在处理数据之前,首先需要定义相应的数据模型来封装人员和活动信息。这将有助于我们以面向对象的方式组织数据。
Person 类
Person 类应包含所有与个人相关的信息,包括用于判断“密切联系人”的属性以及隐私设置。
public class Person {
private String firstname;
private String lastname;
private String phone;
private String email;
private String community;
private String school;
private String employer;
private String privacy; // "N" for no privacy, "Y" for privacy
// 构造函数
public Person(String firstname, String lastname, String phone, String email,
String community, String school, String employer, String privacy) {
this.firstname = firstname;
this.lastname = lastname;
this.phone = phone;
this.email = email;
this.community = community;
this.school = school;
this.employer = employer;
this.privacy = privacy;
}
// Getters
public String getFirstname() { return firstname; }
public String getLastname() { return lastname; }
public String getCommunity() { return community; }
public String getSchool() { return school; }
public String getEmployer() { return employer; }
public String getPrivacy() { return privacy; }
public boolean hasPrivacyEnabled() { return "Y".equalsIgnoreCase(privacy); }
// 为了在Map或Set中正确使用Person对象,需要重写equals和hashCode方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
// 假设firstname和lastname的组合唯一标识一个人
return firstname.equalsIgnoreCase(person.firstname) &&
lastname.equalsIgnoreCase(person.lastname);
}
@Override
public int hashCode() {
return (firstname.toLowerCase() + lastname.toLowerCase()).hashCode();
}
@Override
public String toString() {
return firstname + " " + lastname + " (Community: " + community + ", School: " + school + ", Employer: " + employer + ", Privacy: " + privacy + ")";
}
// Setters (根据需要添加)
}Activity 类
Activity 类用于存储个人的活动信息。
立即学习“Java免费学习笔记(深入)”;
public class Activity {
private String firstname;
private String lastname;
private String activity;
// 构造函数
public Activity(String firstname, String lastname, String activity) {
this.firstname = firstname;
this.lastname = lastname;
this.activity = activity;
}
// Getters
public String getFirstname() { return firstname; }
public String getLastname() { return lastname; }
public String getActivity() { return activity; }
@Override
public String toString() {
return firstname + " " + lastname + ": " + activity;
}
// Setters (根据需要添加)
}数据加载与持久化
从CSV文件读取数据是构建系统的第一步。关键在于将读取到的每一行数据解析成对应的对象,并将其存储在一个集合中,以便后续处理。
InfoReader 类改进
原始代码中读取数据后并未存储对象,导致数据丢失。以下是改进后的 InfoReader 类,它将解析后的 Person 和 Activity 对象分别存储到 ArrayList 中。
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class InfoReader {
private List persons = new ArrayList<>();
private List activities = new ArrayList<>();
public void readInfo() {
// 读取人员数据
try {
String fileLocation = "SamplefilePersons2022Oct31text.csv"; // 假设文件在项目根目录或指定路径
File personListFile = new File(fileLocation);
Scanner personScanner = new Scanner(personListFile);
while (personScanner.hasNextLine()) {
String nextLine = personScanner.nextLine();
String[] personComponents = nextLine.split(",");
// 确保数据完整性,防止数组越界
if (personComponents.length >= 8) {
String firstname = personComponents[0].trim();
String lastname = personComponents[1].trim();
String phone = personComponents[2].trim();
String email = personComponents[3].trim();
String community = personComponents[4].trim();
String school = personComponents[5].trim();
String employer = personComponents[6].trim();
String privacy = personComponents[7].trim();
Person newPerson = new Person(firstname, lastname, phone, email,
community, school, employer, privacy);
persons.add(newPerson); // 存储Person对象
} else {
System.err.println("Warning: Malformed person data line skipped: " + nextLine);
}
}
personScanner.close();
} catch (FileNotFoundException e) {
System.err.println("Person file not found: " + e.getMessage());
throw new RuntimeException("Error reading person data.", e);
}
// 读取活动数据
try {
String fileLocation = "SamplefileActivities2022Oct31text.csv"; // 假设文件在项目根目录或指定路径
File activityListFile = new File(fileLocation);
Scanner activityScanner = new Scanner(activityListFile);
while (activityScanner.hasNextLine()) {
String nextLine = activityScanner.nextLine();
String[] activityComponents = nextLine.split(",");
// 确保数据完整性
if (activityComponents.length >= 3) {
String firstname = activityComponents[0].trim();
String lastname = activityComponents[1].trim();
String activity = activityComponents[2].trim();
Activity newActivity = new Activity(firstname, lastname, activity);
activities.add(newActivity); // 存储Activity对象
} else {
System.err.println("Warning: Malformed activity data line skipped: " + nextLine);
}
}
activityScanner.close();
} catch (FileNotFoundException e) {
System.err.println("Activity file not found: " + e.getMessage());
throw new RuntimeException("Error reading activity data.", e);
}
}
public List getPersons() {
return persons;
}
public List getActivities() {
return activities;
}
} 注意事项:
- 文件路径:示例中的文件路径是简化的,实际应用中应根据您的文件存放位置进行调整。
- 错误处理:增加了对数据行完整性的基本检查 (personComponents.length >= 8),以避免潜在的 ArrayIndexOutOfBoundsException。
- 资源关闭:确保 Scanner 对象在读取完毕后被关闭,以释放系统资源。
图的表示与构建
有了加载并存储的 Person 对象列表,我们现在可以着手构建图了。对于非加权图,邻接列表(Adjacency List)是一种非常高效且常用的表示方法,尤其适用于稀疏图(即边相对较少的图)。
邻接列表表示法
我们将使用 Map
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RecommendationGraph {
private Map> adjacencyList;
public RecommendationGraph() {
this.adjacencyList = new HashMap<>();
}
/**
* 构建图,根据“密切联系人”规则和隐私设置建立边。
* 密切联系人定义:共享相同的社区、学校或雇主。
* 隐私规则:如果某人设置了隐私,则不向其发送推荐(即不建立指向TA的边)。
* @param persons 所有人员列表
*/
public void buildGraph(List persons) {
// 确保所有人都作为节点存在于图中,即使他们目前没有联系人
for (Person person : persons) {
adjacencyList.putIfAbsent(person, new HashSet<>());
}
// 遍历所有人员,寻找密切联系人
for (int i = 0; i < persons.size(); i++) {
Person personA = persons.get(i);
for (int j = i + 1; j < persons.size(); j++) { // 避免重复比较和自连接
Person personB = persons.get(j);
// 检查是否为密切联系人
boolean isCloseContact =
(personA.getCommunity() != null && !personA.getCommunity().isEmpty() &&
personA.getCommunity().equalsIgnoreCase(personB.getCommunity())) ||
(personA.getSchool() != null && !personA.getSchool().isEmpty() &&
personA.getSchool().equalsIgnoreCase(personB.getSchool())) ||
(personA.getEmployer() != null && !personA.getEmployer().isEmpty() &&
personA.getEmployer().equalsIgnoreCase(personB.getEmployer()));
if (isCloseContact) {
// 如果PersonB没有启用隐私,则PersonA可以向PersonB推荐
if (!personB.hasPrivacyEnabled()) {
adjacencyList.get(personA).add(personB);
}
// 如果PersonA没有启用隐私,则PersonB可以向PersonA推荐
if (!personA.hasPrivacyEnabled()) {
adjacencyList.get(personB).add(personA);
}
}
}
}
}
/**
* 获取指定人员的推荐联系人列表。
* @param person 要获取推荐的人员
* @return 该人员的密切联系人集合
*/
public Set getRecommendationsFor(Person person) {
return adjacencyList.getOrDefault(person, new HashSet<>());
}
// 打印图结构(可选)
public void printGraph() {
for (Map.Entry> entry : adjacencyList.entrySet()) {
System.out.println(entry.getKey().getFirstname() + " " + entry.getKey().getLastname() + " -> " + entry.getValue().size() + " contacts:");
for (Person contact : entry.getValue()) {
System.out.println(" - " + contact.getFirstname() + " " + contact.getLastname());
}
}
}
} 图构建逻辑详解
- 初始化节点: 首先,遍历所有 Person 对象,将每个人都作为图中的一个节点添加到邻接列表中。即使他们目前没有联系人,也确保他们在图中占有一席之地。
-
寻找密切联系人: 使用嵌套循环比较每对不同的人 (personA 和 personB)。
- 密切联系条件: 如果 personA 和 personB 共享相同的社区、学校或雇主,则认为他们是“密切联系人”。这里使用了 equalsIgnoreCase 和非空检查,以提高健壮性。
- 隐私处理: 推荐系统的隐私规则是“如果一个人请求了隐私,则不向该人发送推荐”。这意味着,如果 personB 启用了隐私,那么即使 personA 和 personB 是密切联系人,personA 也不能向 personB 推荐(即不能在 personA 的邻接列表中添加 personB)。反之亦然。因此,在建立边时,需要分别检查两个方向的隐私设置。如果 personA 没有隐私,personB 可以向 personA 推荐;如果 personB 没有隐私,personA 可以向 personB 推荐。
推荐系统逻辑示例
一旦图构建完成,获取某个人的推荐联系人就变得非常简单,只需查询其在邻接列表中的邻居即可。
public class RecommendationSystemApp {
public static void main(String[] args) {
// 1. 读取数据
InfoReader reader = new InfoReader();
try {
reader.readInfo();
} catch (RuntimeException e) {
System.err.println("Failed to read data: " + e.getMessage());
return;
}
List allPersons = reader.getPersons();
// List allActivities = reader.getActivities(); // 活动数据当前未用于图构建,但可以用于其他推荐维度
// 2. 构建推荐图
RecommendationGraph graph = new RecommendationGraph();
graph.buildGraph(allPersons);
// 3. 获取并打印推荐
System.out.println("--- Recommendation Results ---");
for (Person person : allPersons) {
System.out.println("\nRecommendations for " + person.getFirstname() + " " + person.getLastname() + ":");
Set recommendations = graph.getRecommendationsFor(person);
if (recommendations.isEmpty()) {
System.out.println(" No close contacts found or all contacts have privacy enabled for this person.");
} else {
for (Person recommendedPerson : recommendations) {
System.out.println(" - " + recommendedPerson.getFirstname() + " " + recommendedPerson.getLastname());
}
}
}
// 示例:查找特定人物的推荐
// 假设我们要找 "Rajay Mccalla" 的推荐
Person targetPerson = null;
for(Person p : allPersons) {
if ("Rajay".equalsIgnoreCase(p.getFirstname()) && "Mccalla".equalsIgnoreCase(p.getLastname())) {
targetPerson = p;
break;
}
}
if (targetPerson != null) {
System.out.println("\n--- Specific Recommendation for " + targetPerson.getFirstname() + " " + targetPerson.getLastname() + " ---");
Set specificRecs = graph.getRecommendationsFor(targetPerson);
if (specificRecs.isEmpty()) {
System.out.println(" No specific recommendations found.");
} else {
for (Person rec : specificRecs) {
System.out.println(" - " + rec.getFirstname() + " " + rec.getLastname());
}
}
} else {
System.out.println("\nTarget person 'Rajay Mccalla' not found.");
}
}
} 注意事项与优化
-
性能考量:
- 图构建效率: 当前的图构建方法使用了嵌套循环(O(N^2)),对于小规模数据集是可接受的。但如果人员数量 N 非常大(例如,数百万),这种方法会变得非常慢。
-
优化方法: 可以通过预处理数据来优化。例如,创建 Map
> 来按社区、学校或雇主分组人员。这样,在寻找密切联系人时,只需比较同一组内的人员,而不是所有人员。 // 优化思路示例:按社区分组 Map
> personsByCommunity = new HashMap<>(); for (Person p : persons) { if (p.getCommunity() != null && !p.getCommunity().isEmpty()) { personsByCommunity.computeIfAbsent(p.getCommunity().toLowerCase(), k -> new ArrayList<>()).add(p); } } // 然后只在这些分组内部寻找联系人
-
健壮性:
- 文件路径: 确保文件路径是正确的,并且文件可读。
- 数据清洗: CSV文件中的数据可能不规范,例如有额外的空格、缺失值或不一致的大小写。在解析时使用 .trim() 和 equalsIgnoreCase() 是良好的实践。对于缺失值,应有明确的处理策略(例如,跳过该属性的比较,或者将缺失值视为不匹配)。
- 异常处理: 对 FileNotFoundException 和 ArrayIndexOutOfBoundsException 等进行适当处理。
-
扩展性:
-
加权图: 如果“密切联系”有不同强度(例如,共享社区比共享学校更重要),可以引入边的权重,将 Set
改为 Map 或自定义边对象。 - 专业图库: 对于更复杂的图算法(如最短路径、社区发现、中心性分析等),可以考虑使用成熟的图处理库,例如 JGraphT 或 Apache TinkerPop。这些库提供了丰富的图数据结构和算法实现。
-
加权图: 如果“密切联系”有不同强度(例如,共享社区比共享学校更重要),可以引入边的权重,将 Set
- 内存管理: 对于非常大的数据集,ArrayList 和 HashMap 可能会占用大量内存。在极端情况下,可能需要考虑使用外部存储或分布式图数据库。
总结
本教程详细展示了如何从零开始,在Java中构建一个用于推荐系统的非加权图。我们从定义数据模型、安全地加载和存储CSV数据开始,然后重点阐述了如何基于特定规则(共同特征和隐私设置)构建图结构。通过将人员及其关系抽象为图的节点和边,我们能够以清晰、高效的方式实现联系人推荐功能。理解图数据结构及其在实际问题中的应用,是开发复杂推荐系统和社交网络分析工具的关键一步。










