您将如何为 Facebook 或 Linkedln 等大型社交网络设计数据结构?描述你将如何设计一个算法来显示两个人之间的最短路径(例如,我->鲍勃->苏珊->杰森->你)。
提问:谷歌面试
解决这个问题的一个好方法是消除一些限制并首先针对这种情况解决它。
案例 1:简化问题(不考虑数百万人)
我们可以通过将每个人视为一个节点并让两个节点之间的边表示两个用户是朋友来构建图。如果我们想找到两个人之间的路径,我们从一个人开始,做一个简单的广度优先搜索。或者,我们可以进行双向广度优先搜索。这意味着进行两次广度优先搜索,一次来自源,一次来自目的地。当搜索发生冲突时,我们知道我们找到了一条路径。
为什么深度优先搜索效果不佳?首先,深度优先搜索只会找到一条路径。它不一定会找到最短路径。其次,即使我们只需要任何路径,它也会非常低效。两个用户可能只相隔一个程度,但在找到这个相对直接的连接之前,它可以搜索他们“子树”中的数百万个节点。
在实现中,我们将使用两个类来帮助我们。 BFSData 保存广度优先搜索所需的数据,例如 isVisited 哈希表和 toVisit 队列。 PathNode 代表我们搜索时的路径,在这个路径中存储我们访问过的每个 Person 和 previousNode。
Java的主要逻辑如下
Linkedlist findPathBiBFS(HashMap people,
int source, int destination)
{
BFSData sourceData = new BFSData(people.get(source));
BFSData destData = new BFSData(people.get(destination));
while (!sourceData.isFinished() && !destData.isFinished())
{
/* Search out from source. */
Person collision = searchlevel(people, sourceData, destData);
if (collision != null)
return mergePaths(sourceData, destData, collision.getID());
/* Search out from destination. */
collision = searchlevel(people, destData, sourceData);
if (collision != null)
return mergePaths(sourceData, destData, collision.getID());
}
return null;
}
/* Search one level and return collision, if any.*/
Person searchLevel(HashMap people,
BFSData primary, BFSData secondary)
{
/* We only want to search one level at a time. Count
how many nodes are currently
in the primary's level and only do that many nodes.
We continue to add nodes to the end. */
int count = primary.toVisit.size();
for (int i= 0; i < count; i++)
{
/* Pull out first node. */
PathNode pathNode = primary.toVisit.poll();
int personld = pathNode.getPerson().getID();
/* Check if it's already been visited. */
if (secondary.visited.containsKey(personid))
return pathNode.getPerson();
/* Add friends to queue. */
Person person = pathNode. getPerson();
Arraylist friends = person.getFriends();
for (int friendid : friends)
{
if (!primary.visited.containsKey(friendid))
{
Person friend= people.get(friendld);
PathNode next = new PathNode(friend, pathNode);
primary.visited.put(friendld, next);
primary.toVisit.add(next);
}
}
}
return null;
}
/* Merge paths where searches met at the connection. */
Linkedlist mergePaths(BFSData bfsl, BFSData bfs2,
int connection)
{
// endl -> source, end2 -> dest
PathNode endl = bfsl.visited.get(connection);
PathNode end2 = bfs2.visited.get(connection);
Linkedlist pathOne = endl.collapse(false);
Linkedlist pathTwo = end2.collapse(true);
pathTwo.removeFirst(); // remove connection
pathOne.addAll(pathTwo); // add second path
return pathOne;
}
class PathNode
{
private Person person = null;
private PathNode previousNode = null;
public PathNode(Person p, PathNode previous)
{
person = p;
previousNode = previous;
}
public Person getPerson()
{
return person;
}
public Linkedlist collapse(boolean startsWithRoot)
{
Linkedlist path= new Linkedlist();
PathNode node = this;
while (node != null)
{
if (startsWithRoot)
path.addlast(node.person);
else
path.addFirst(node.person);
node = node.previousNode;
}
return path;
}
}
class BFSData
{
public Queue toVisit = new Linkedlist();
public HashMap visited =
new HashMap();
public BFSData(Person root)
{
PathNode sourcePath = new PathNode(root, null);
toVisit.add(sourcePath);
visited.put(root.getID(), sourcePath);
}
public boolean isFinished()
{
return toVisit.isEmpty();
}
}
基于 BFS 的解决方案有多快?
假设每个人都有 k 个朋友,源 S 和目的地 D 有一个共同的朋友 C。
1. 从 S 到 D 的传统广度优先搜索:我们大致经过 k+k*k 个节点:每个 S 的 k 个朋友,然后他们的每个朋友。
2.双向广度优先搜索:我们遍历2k个节点:S的k个朋友中的每一个和D的k个朋友中的每一个。当然,2k远小于k+k*k。
3. 将其推广到长度为 q 的路径,我们有:
3.1 BFS:O(k q )
3.2 双向 BFS:0( k q/2 + k q/2 ),也就是 0( k q/2 )
如果我们想象一条像 A->B->C->D->E 这样的路径,每个人有 100 个朋友,这就会有很大的不同。 BFS 将需要查看 1 亿 (100 4 ) 个节点。双向 BFS 只需要查看 20,000 个节点 (2 x 100 2 )。
案例二:处理百万用户
对于这么多用户,我们不可能将所有数据保存在一台机器上。这意味着我们上面的简单 Person 数据结构不太适用——我们的朋友可能和我们生活在同一台机器上。相反,我们可以用他们的 ID 列表替换我们的朋友列表,并按如下方式遍历:
1:对于每个好友ID:int machine index = getMachineIDForUser(person_ID);
2:转到机器#machine_index
3:在那台机器上,做:Personfriend = getPersonWithID(person_ID);
下面的代码概述了这个过程。我们已经定义了一个类 Server,它包含所有机器的列表,以及一个类 Machine,它代表一台机器。这两个类都有哈希表来有效地查找数据。
Java的主要逻辑如下->
// A server that holds list of all machines
class Server
{
HashMap machines =
new HashMap();
HashMap personToMachineMap =
new HashMap();
public Machine getMachineWithid(int machineID)
{
return machines.get(machineID);
}
public int getMachineIDForUser(int personID)
{
Integer machineID = personToMachineMap.get(personID);
return machineID == null ? -1 : machineID;
}
public Person getPersonWithID(int personID)
{
Integer machineID = personToMachineMap.get(personID);
if (machineID == null) return null;
Machine machine = getMachineWithid(machineID);
if (machine == null) return null;
return machine.getPersonWithID(personID);
}
}
// A person on social network has id, friends and other info
class Person
{
private Arraylist friends =
new Arraylist();
private int personID;
private String info;
public Person(int id)
{
this.personID =id;
}
public String getinfo()
{
return info;
}
public void setinfo(String info)
{
this.info = info;
}
public Arraylist getFriends()
{
return friends;
}
public int getID()
{
return personID;
}
public void addFriend(int id)
{
friends.add(id);
}
}
以下是一些优化和后续问题。
优化:减少机器跳跃
从一台机器跳到另一台机器是昂贵的。与其和每个朋友随机从一台机器跳到另一台机器,不如尝试批量跳转——例如,如果我的五个朋友住在一台机器上,我应该一次查找它们。
优化:人机智能分工
人们更有可能与生活在同一个国家的人成为朋友。与其在机器上随机划分人,不如尝试按国家、城市、州等划分他们。这将减少跳跃次数。
问题:广度优先搜索通常需要将节点“标记”为已访问。在这种情况下你怎么做?
通常,在 BFS 中,我们通过在节点类中设置访问标志来将节点标记为已访问。在这里,我们不想这样做。可能有多个搜索同时进行,所以只编辑我们的数据是个坏主意。
相反,我们可以用哈希表模拟节点的标记来查找节点 id 并确定它是否被访问过。
其他后续问题:
1. 在现实世界中,服务器出现故障。这对你有什么影响?
2. 如何利用缓存?
3. 你搜索到图的末尾(无穷大)吗?你如何决定何时放弃?
4. 在现实生活中,有些人的朋友的朋友比其他人多,因此更有可能在你和其他人之间开辟道路。你如何使用这些数据来选择从哪里开始遍历?
本文参考
本文参考