本教程详细阐述了如何在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<Person> persons = new ArrayList<>(); private List<Activity> 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<Person> getPersons() { return persons; } public List<Activity> getActivities() { return activities; } }
注意事项:
- 文件路径:示例中的文件路径是简化的,实际应用中应根据您的文件存放位置进行调整。
- 错误处理:增加了对数据行完整性的基本检查 (personComponents.length >= 8),以避免潜在的 ArrayIndexOutOfBoundsException。
- 资源关闭:确保 Scanner 对象在读取完毕后被关闭,以释放系统资源。
图的表示与构建
有了加载并存储的 Person 对象列表,我们现在可以着手构建图了。对于非加权图,邻接列表(Adjacency List)是一种非常高效且常用的表示方法,尤其适用于稀疏图(即边相对较少的图)。
邻接列表表示法
我们将使用 Map<Person, Set<Person>> 来表示图。其中,Map的键是图中的一个 Person 节点,而值是一个 Set,包含与该键 Person 节点有“密切联系”的所有其他 Person 节点。使用 Set 可以自动处理重复的联系,确保每个联系人只出现一次。
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<Person, Set<Person>> adjacencyList; public RecommendationGraph() { this.adjacencyList = new HashMap<>(); } /** * 构建图,根据“密切联系人”规则和隐私设置建立边。 * 密切联系人定义:共享相同的社区、学校或雇主。 * 隐私规则:如果某人设置了隐私,则不向其发送推荐(即不建立指向TA的边)。 * @param persons 所有人员列表 */ public void buildGraph(List<Person> 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<Person> getRecommendationsFor(Person person) { return adjacencyList.getOrDefault(person, new HashSet<>()); } // 打印图结构(可选) public void printGraph() { for (Map.Entry<Person, Set<Person>> 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<Person> allPersons = reader.getPersons(); // List<Activity> 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<Person> 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<Person> 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<String, List<Person>> 来按社区、学校或雇主分组人员。这样,在寻找密切联系人时,只需比较同一组内的人员,而不是所有人员。
// 优化思路示例:按社区分组 Map<String, List<Person>> personsByCommunity = new HashMap<>(); for (Person p : persons) { if (p.getCommunity() != null && !p.getCommunity().isEmpty()) { personsByCommunity.computeIfAbsent(p.getCommunity().toLowerCase(), k -> new ArrayList<>()).add(p); } } // 然后只在这些分组内部寻找联系人
- 健壮性:
- 扩展性:
- 加权图: 如果“密切联系”有不同强度(例如,共享社区比共享学校更重要),可以引入边的权重,将 Set<Person> 改为 Map<Person, Integer> 或自定义边对象。
- 专业图库: 对于更复杂的图算法(如最短路径、社区发现、中心性分析等),可以考虑使用成熟的图处理库,例如 JGraphT 或 Apache TinkerPop。这些库提供了丰富的图数据结构和算法实现。
- 内存管理: 对于非常大的数据集,ArrayList 和 HashMap 可能会占用大量内存。在极端情况下,可能需要考虑使用外部存储或分布式图数据库。
总结
本教程详细展示了如何从零开始,在Java中构建一个用于推荐系统的非加权图。我们从定义数据模型、安全地加载和存储CSV数据开始,然后重点阐述了如何基于特定规则(共同特征和隐私设置)构建图结构。通过将人员及其关系抽象为图的节点和边,我们能够以清晰、高效的方式实现联系人推荐功能。理解图数据结构及其在实际问题中的应用,是开发复杂推荐系统和社交网络分析工具的关键一步。
评论(已关闭)
评论已关闭