图:
如何存储微博、微信等这些社交网络的好友关系?
为什么产生?
特点?
使用场景?
树中的元素称为节点,图中的元素称为顶点。顶点之间的关系叫作边。顶点相连接的边的条数叫作定点的度。
微信为例:
一个用户就是一个顶点,好友关系就是边,一个用户总的好友个数就是度
微博:可以单向关注,其实现为有向图
边上增加方向的概念就是有向图,此时无向图中的度分为 入度 和 出度。
入度:多少条边指向这个定点 出度:多少条边是从这个定点指出
qq中的亲密度,经常交流,亲密度高。其实现为:带权图.
在带权图中,每条边都有一个权重(weight),我们可以通过这个权重来表示 QQ 好友间的亲密度
图的存储
邻接矩阵
无向图:顶点 i 与顶点 j 之间有边,我们就将 A[i][j] 和 A[j][i] 标记为 1;
有向图:如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,那将A[i][j]标记为1;同理,如果有一条箭头从顶点j指向顶点i的边,我们就将A[j][i]标记为1。
带权图:数组中就存储相应的权重
实例如图:
缺点:浪费空间。
例如无向图,一条边存储一个就可以,即可以利用对角线划分一半出来即可
如稀疏图,几亿个定点,但是每个用户仅有数百个用户
优点:简单直观,邻接矩阵方便计算
邻接表
如图:
类似散列表。图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点。
对于无向图来说,也是类似的,不过,每个顶点的链表中存储的,是跟这个顶点有边相连的顶点。
邻接矩阵与邻接表,邻接矩阵更加省时间,但耗空间;邻接表更加耗时,但省时间:
如果我们要确定,是否存在一条从顶点2到顶点4的边,那我们就要遍历顶点2 对应的那条链表,看链表中是否存在顶点 4。而且,我们前面也讲过,链表的存储方式对缓存不友好。
微博服务中的好友关系怎么存储?
微博好友操作:
判断A是否关注B
用户A取关B
根据用户名称的首字母排序,分页获取用户的粉丝列表
根据用户名称的首字母排序,分页获取用户的关注列表
因为用户关系表是稀疏图,所以采用邻接表的方式存储。即存储某用户关注了哪些用户。
但是没发获取某用户都有哪些粉丝?
所以需要再存储一个逆邻接表,存储的是用户有哪些粉丝。
如下图:
上面的邻接表不适合快速判断两个用户之间是否是关注与被关注的关系,怎么改进呢?
将邻接表中的链表改为支持快速查找的动态数据结构。红黑树、跳表、有序动态数组还是散列表呢?
因为需要按照用户名称的首字母排序,分页来获取用户的粉丝列表或者关注列表,用跳表这种结构再合适不过了。因为,跳表插入、删除、查找都非常高效,时间复杂度是 O(logn),空间复杂度上稍高,是 O(n)。最重要的一点,跳表中存储的数据本来就是有序的了,分页获取粉丝列表或关注列表,就非常高效。
如果是上亿的大规模数据,上面的内容无法存储于内存中了?
方案一:hash算法分片,此时可以通过hash算法分片的方式,存储到不同的机器。存取之前都进行hash,确保存取发生在同一机器。
方案二:持久化,如存储到磁盘中,如mysql + 索引。
微信无向图的处理:
加/删除好友
好友列表
邻接表:
冗余保存,,可以采取树形或链表结构,
a添加了b,a后面有b,同时b后面有a。
用户量特别大的场景
1)分片,对用户进行hash分片
2)持久化,如磁盘等
解决现实问题的时候当存储图有多种选择,例如:
1.用邻接表自己存
2.关系型库
3.图数据库
那么这三种方式每一种的适用场景,优缺点分别是什么呢?该如何取舍
作者回复: 1 内存中用临界表
2 要持久化存储就用数据库
2 超大图 并且涉及大量图计算。用专业的图数据库
如何获取社交网络中某用户的三度好友?
广度优先搜索
先查找离起始顶点最近的,然后是次近的,依次往外搜索。
基于临近表的代码实现
1)定义图
2)广度优先上搜索,一直向外延伸没有查找过的顶点
Queue<Integer> queue 待处理节点,值就是顶点
boolean[] visited 已访问节点,下标:顶点
prev 访问到当前节点(下标)的节点(值)
深度优先搜索
假设你在迷宫的某个岔路口,然后想找到出口。你随意选择一个岔路口来走,走着走着发现走不通的时候,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口。
练习
广度优先搜素在无向图对应的邻接表中的 代码-书写
深度优先搜索代码书写 visited prev
图数据库有哪些?
推荐使用Neo4j
他们的实验试图在一个社交网络里找到最大深度为5的朋友的朋友。他们的数据集包括100万人,每人约有50个朋友。实验结果如下:
深度 MySQL执行时间(s) Neo4J执行时间(s) 返回记录数
2 0.016 0.01 ~2500
3 30.267 0.168 ~110 000
4 1543.505 1.359 ~600 000
5 未完成 2.132 ~800 000
在深度为2时(即朋友的朋友),两种数据库性能相差不是很明显;深度为3时(即朋友的朋友的朋友),很明显,关系型数据库的响应时间30s,已经变得不可接受了;深度到4时,关系数据库需要近半个小时才能返回结果,使其无法应用于在线系统;深度到5时,关系型数据库已经无法完成查询。而对于图数据库Neo4J,深度从3到5,其响应时间均在3秒以内。
可以看出,对于图数据库来说,数据量越大,越复杂的关联查询,约有利于体现其优势。从深度为4/5的查询结果我们可以看出,图数据库返回了整个社交网络一半以上的人数。
public class GraphTest {
public static void main(String[] args) {
// int[] tt = {0,0,3,1,2};
//// System.out.println(printRoad(tt,1,4));
LinkedList<Integer>[] list = new LinkedList[7];
LinkedList list1 = new LinkedList<>();
list1.add(3);
list1.add(2);
LinkedList list2 = new LinkedList<>();
list2.add(1);
list2.add(4);
LinkedList list3 = new LinkedList<>();
list3.add(1);
list3.add(4);
list3.add(5);
LinkedList list4 = new LinkedList<>();
list4.add(2);
list4.add(3);
list4.add(6);
LinkedList list5 = new LinkedList<>();
list5.add(3);
list5.add(6);
LinkedList list6 = new LinkedList<>();
list6.add(4);
list6.add(5);
list[1]=list1;
list[2]=list2;
list[3]=list3;
list[4]=list4;
list[5]=list5;
list[6]=list6;
Graph graph = new Graph(7, list);
depthFirstSearch(1,6, graph);
depthFirstSearch(1,9, graph);
breadhFirstSearch(1,5, graph);
String a = "a";
if(a.contains("a")){
}
}
/**
* desc: 图的广度优先搜索
*/
public static void breadhFirstSearch(int start, int end, Graph graph){
int v = graph.getV();
LinkedList<Integer>[] list = graph.getList();
// 记录图中已经访问的节点,index:顶点 值;true,已访问,不再访问
boolean[] visited = new boolean[v];
// index:顶点 value:到顶点值为index的顶点
int[] prev = new int[v];
for(int i=0; i<prev.length; i++){
prev[i] = -1;
}
// 存储待访问的顶点
LinkedList<Integer> queue= new LinkedList();
queue.addFirst(start);
LinkedList<Integer> nodeConnect = null;
while(!queue.isEmpty()){
int node = queue.removeLast();
// 未访问过
if(!visited[node]){
nodeConnect = list[node];
visited[node] = true;
for(int i=0; i<nodeConnect.size(); i++){
if(!visited[nodeConnect.get(i)]){
prev[nodeConnect.get(i)]=node;
if(nodeConnect.get(i) == end){
// 递归获取路径
System.out.println(printRoad(prev, start, end));
break;
}else{
queue.addFirst(nodeConnect.get(i));
}
}
}
}
}
}
/**
* desc: 图的广度优先搜索
*/
public static void depthFirstSearch(int start, int end, Graph graph){
if(start == end){
return ;
}
int v = graph.getV();
// index:顶点 value:访问到该顶点的index的值
int[] pre = new int[v];
// 已访问顶点的个数
boolean[] visited = new boolean[v];
visited[start] = true;
int current = start;
LinkedList<Integer>[] list = graph.getList();
// current ==0 说明结束,因为0处没有节点
while(current != end && current !=0){
LinkedList<Integer> integers = list[current];
for(int i=0; i<integers.size(); i++){
// 已经访问过,直接跳过
if(visited[integers.get(i)]){
// 说明与该节点所有相邻的节点都已经访问,此时只有回退
if(i == integers.size()-1){
current = back(current, pre, visited);
break;
}
continue;
}else{
pre[integers.get(i)] = current;
current = integers.get(i);
visited[integers.get(i)] = true;
break;
}
}
}
if(current == end){
System.out.println(printRoad(pre, start, end));
}else{
System.out.println("no node:"+end);
}
}
/**
* desc: 回退一步
*/
private static int back(int current, int[] pre, boolean[] visited) {
return pre[current];
}
/**
* desc: 打印访问路径
*/
private static String printRoad(int[] prev, int start, int end) {
if(end == start){
return start +"";
}
return printRoad(prev, start, prev[end]) +","+ end;
}
// 邻接表表示的图
static class Graph{
private int v;
private LinkedList<Integer>[] list;
public Graph(int v, LinkedList<Integer>[] list){
this.v=v;
this.list = list;
}
public int getV() {
return v;
}
public void setV(int v) {
this.v = v;
}
public LinkedList<Integer>[] getList() {
return list;
}
public void setList(LinkedList[] list) {
this.list = list;
}
}
}
来源:CSDN
作者:深山猿
链接:https://blog.csdn.net/h2604396739/article/details/103465326