图的表示:如何存储微博、微信等社交网络中的好友关系? 什么是“图”?(Graph) 存储 如何存储微博、微信等社交网络中的好友关系?

x博中,两个人可以互相关注,互加好友,那如何存储这些社交网络的好友关系呢?

这就要用到:图。

什么是“图”?(Graph)

和树比起来,这是一种更加复杂的非线性表结构。

树的元素称为节点,图中元素叫作顶点(vertex)。图中的一个顶点可以与任意其他顶点建立连接关系,这种建立的关系叫作边(edge)。


社交网络就是典型的图结构。

把每个用户看作一个顶点。如果两个用户之间互加好友,就在两者之间建立一条边。
所以,整个微信的好友关系就可用一张图表示。
每个用户有多少个好友,对应到图中就叫作顶点的度(degree),即跟顶点相连接的边的条数。

不过微博的社交关系跟微信还有点不同,更复杂一点。微博允许单向关注,即用户A关注用户B,但B可不关注A。

如何用图表示这种单向社交关系呢?

这就引入边的“方向”。

A关注B,就在图中画一条从A到B的带箭头的边,表示边的方向。A、B互关,就画一条从A指向B的边,再画一条从B指向A的边,这种边有方向的图叫作“有向图”。边没有方向的图也就叫“无向图”。



无向图中有“度”:一个顶点有多少条边。
有向图中,把度分为:

  • 入度(In-degree)
    有多少条边指向这个顶点,即有多少粉丝
  • 出度(Out-degree)
    有多少条边是以这个顶点为起点指向其他顶点。对应到微博的例子,即关注了多少人

QQ社交关系更复杂,不仅记录用户之间的好友关系,还记录了两个用户之间的亲密度,如何在图中记录这种好友关系亲密度呢?
这就要用到带权图(weighted graph),每条边都有个权重(weight),可以通过这个权重来表示QQ好友间的亲密度。


存储

邻接矩阵存储方法

最直观的一种存储方法,邻接矩阵(Adjacency Matrix)。

依赖一个二维数组:

  • 无向图
    如果顶点i与顶点j之间有边,就将A[i][j]和A[j][i]标记为1
  • 有向图
    如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,那我们就将A[i][j]标记为1
    如果有一条箭头从顶点j指向顶点i的边,我们就将A[j][i]标记为1
  • 带权图,数组中就存储相应的权重


简单、直观,但比较浪费存储空间!

无向图,若A[i][j]==1,则A[j][i]==1。实际上,只需存储一个即可。即无向图的二维数组,如果将其用对角线划分为上下两部分,则只需利用上或下面这样一半空间就够了,另外一半其实完全浪费。
如果存储的是稀疏图(Sparse Matrix),即顶点很多,但每个顶点的边并不多,则更浪费空间。
如微信有好几亿用户,对应到图就是好几亿顶点。但每个用户好友并不很多,一般也就三五百个而已。如果我们用邻接矩阵来存储,那绝大部分的存储空间都被浪费了。

但这也并不是说,邻接矩阵的存储方法就完全没有优点。首先,邻接矩阵的存储方式简单、直接,因为基于数组,所以在获取两个顶点的关系时,就非常高效。其次,用邻接矩阵存储图的另外一个好处是方便计算。这是因为,用邻接矩阵的方式存储图,可以将很多图的运算转换成矩阵之间的运算。比如求解最短路径问题时会提到一个Floyd-Warshall算法,就是利用矩阵循环相乘若干次得到结果。

邻接表存储方法

针对上面邻接矩阵比较浪费内存空间,另外一种图存储,邻接表(Adjacency List)。

有点像散列表?每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点。对于无向图来说,也是类似的,不过,每个顶点的链表中存储的,是跟这个顶点有边相连的顶点,你可以自己画下。


  • 邻接矩阵存储较浪费空间,但更省时
  • 邻接表较节省存储空间,但较耗时

如上图示例,若要确定是否存在一条从顶点2到顶点4的边,就要遍历顶点2的链表,看其中是否存在顶点4,而链表存储对缓存不友好。所以邻接表查询两个顶点之间的关系较为低效。

基于链表法解决冲突的散列表中,若链过长,为提高查找效率,可将链表换成其他更高效数据结构,如平衡二叉查找树。
邻接表长得很像散列。所以,也可将邻接表同散列表一样进行“优化”。

可将邻接表中的链表改成平衡二叉查找树。实际可选用红黑树。即可更快速查找两个顶点之间是否存在边。
这里的二叉查找树也可换成其他动态数据结构,如跳表、散列表。
还可将链表改成有序动态数组,通过二分查找快速定位两个顶点之间是否存在边。

如何存储微博、微信等社交网络中的好友关系?

虽然微博有向图,微信是无向图,但对该问题,二者思路类似,以微博为例。

数据结构服务于算法,选择哪种存储方法和需支持的操作有关。
对于微博用户关系,需支持如下操作:

  • 判断用户A是否关注了用户B
  • 判断用户A是否是用户B的粉丝
  • 用户A关注用户B
  • 用户A取消关注用户B
  • 根据用户名称的首字母排序,分页获取用户的粉丝列表
  • 根据用户名称的首字母排序,分页获取用户的关注列表

因为社交网络是一张稀疏图,使用邻接矩阵存储比较浪费存储空间。所以,这里采用邻接表。

但一个邻接表存储这种有向图也是不够的。查找某用户关注了哪些用户很容易,但若想知道某用户都被哪些用户关注了,即粉丝列表就没法了。

因此,还需一个逆邻接表,存储用户的被关注关系:

  • 邻接表,每个顶点的链表中,存储的就是该顶点指向的顶点
    查找某个用户关注了哪些用户
  • 逆邻接表,每个顶点的链表中,存储的是指向该顶点的顶点
    查找某个用户被哪些用户关注


基础的邻接表不适合快速判断两个用户是否为关注与被关注关系,所以进行优化,将邻接表的链表改为支持快速查找的动态数据结构。

那是红黑树、跳表、有序动态数组还是散列表呢?

因需按照用户名称首字母排序,分页获取用户的粉丝列表或关注列表,跳表最合适:插入、删除、查找都非常高效,时间复杂度O(logn),空间复杂度稍高,是O(n)
跳表存储数据先天有序,分页获取粉丝列表或关注列表,非常高效。

对小规模数据,如社交网络中只有几万、几十万个用户,可将整个社交关系存储在内存,该解决方案没问题。

但像微博上亿用户,数据量太大,无法全部存储在内存,何解?

可通过哈希算法等数据分片方案,将邻接表存储在不同机器:
如下图,在机器1上存储顶点1,2,3的邻接表,在机器2上,存储顶点4,5的邻接表。逆邻接表的处理方式也一样。当要查询顶点与顶点关系的时候,我们就利用同样的哈希算法,先定位顶点所在的机器,然后再在相应的机器上查找。

还能借助外部存储(比如硬盘),因为外部存储的存储空间比内存多很多:
如用下表存储这样一个图。为高效支持前面定义的操作,可建多个索引,比如第一列、第二列,给这两列都建立索引。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章