29图的表示:如何存储微博、微信等社交网络中的好友关系

首先,该文章来自于极客时间网站,王争的专栏——《数据结构与算法之美》,我这里只是做简单的解释、记录并添加自己的见解,只是作为个人笔记,若侵权,马上删除。最后建议直接去该网站上购买该课程看原作者的讲解,一来是支持作者,二来是作者写的确实不错。

如何存储微博、微信等这些社交网络的好友关系呢?这就需要用到接下来要说的图这种数据结构。

如何理解“图”?

跟树一样,图也是一种非线性表数据结构。不过它比树更加复杂。

树中元素称为节点,而图中元素称为顶点(vertex)。顶点与顶点之间可以建立连接关系,称为边(edge)。

img

社交网路如微信都是非常典型的图结构。我们可以把每个用户看作一个顶点。如果两个用户之间互加好友,那就在两者之间建立一条边。所以,整个微信的好友关系就可以用一张图来表示。其中,每个用户有多少个好友,对应到图中,就叫作顶点的度(degree),就是跟顶点相连接的边的条数。

为了实现微博单向关注这个功能,需要借助有向图,例如A关注了B,则画一条从A到B的带箭头的边。

img

  • 无向图中有“度”这个概念,表示一个顶点有多少条边。
  • 有向图中把度分为入度(In-degree)和出度(Out-degree)。入度,表示有多少条边指向这个顶点;顶点的出度,表示有多少条边是以这个顶点为起点指向其他顶点。

QQ的社交关系更加复杂,它还记录了用户之间的亲密度,此时就需要用到另外一种图结构——带权图(weighted graph)。它的每一条边上都有一个权重表示QQ好友间的亲密度,如下所示:

img

那么如何在内存中存储这种数据结构呢?

邻接矩阵存储方法

图最直观的一种存储方法就是,邻接矩阵(Adjacency Matrix)。它的底层依赖一个二维数组。

  • 对于无向图,如果顶点 i 与顶点 j 之间有边,我们就将 $A[i][j]$和 $A[j][i]$标记为 1;
  • 对于有向图,如果有一个箭头从顶点 i 指向顶点 j,则将$A[i][j]$标记为 1;
  • 对于加权图,数组中就存储相应的权重。

img

用邻接矩阵表示一个图,简单、直观,但是浪费存储空间。比如对于无向图,其邻接矩阵为对称矩阵,只需要存储其上三角或者下三角就可以了。另外,若存储的是稀疏图(Sparse Matrix),也就是顶点很多,顶点上的边不多,用邻接矩阵的存储方法更加浪费空间。

拿邻接矩阵去存储图结构,有如下优点:

  • 邻接矩阵的存储方式简单、直接,因为基于数组,所以在获取两个顶点的关系时,就非常高效;
  • 邻接矩阵的存储方式计算方便,可以将很多图的运算转换成矩阵之间的运算。

邻接表存储方法

针对邻接矩阵存储浪费存储的缺点,提出另外一种图的存储方法——邻接表(Adjacency List)。

如下图所示,有点类似散列表,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。

img

邻接表存储起来比较节省空间,但是使用起来就比较耗时间。邻接矩阵存储方式正好相反。这就是之前说的空间换时间的思路。比如,要确定有没有一条从顶点2到顶点4的边,要遍历顶点2对应的那条链表,看链表中是否存在顶点4。而且,链表的存储方法对缓存不友好。所以,比起邻接矩阵的存储方式,在邻接表中查询两个顶点之间的关系就没那么高效了。

在前面基于链表法解决冲突的散列表中,如果链过长,为了提高查找效率,我们可以将链表换成其他更加高效的数据结构,比如平衡二叉查找树等。

前面说过,邻接表长得很像散列。所以,我们也可以将邻接表同散列表一样进行“改进升级”

我们可以将邻接表中的链表改成平衡二叉查找树。实际开发中,我们可以选择用红黑树。这样,我们就可以更加快速地查找两个顶点之间是否存在边了。当然,这里的二叉查找树可以换成其他动态数据结构,比如跳表、散列表等。除此之外,我们还可以将链表改成有序动态数组,可以通过二分查找的方法来快速定位两个顶点之间否是存在边。

解答开篇

如何存储微博、微信等社交网络中的好友关系?微博、微信是两种“图”,前者是有向图,后者是无向图。解决思路类似,这里只拿微博讲解。

数据结构是为算法服务的,所以具体选择哪种存储方法,与期望支持的操作有关系。针对微博用户关系,假设我们需要支持下面这样几个操作:

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

存储一个图,主要有两种存储方法,邻接矩阵和邻接表。而社交网路是稀疏图,比较适合用邻接表来存储。

不过,用邻接表存储这种有向图是一个问题。去查找某个用户关注了哪些用户非常容易,但是如果要想知道某个用户都被哪些用户关注了,也就是用户的粉丝列表,是非常困难的。

为了解决这个问题,需要一个逆邻接表。邻接表中存储了用户的关注关系,逆邻接表中存储的是用户的被关注关系。

img

不过,这基础的邻接表不适合快速判断两个用户之间是否是关注与被关注的关系。可以将邻接表中的链表改为支持快速查找的动态数据结构。例如红黑树、跳表、有序动态数组还是散列表,究竟选择哪个呢?

因为我们需要按照用户名称的首字母排序,分页来获取用户的粉丝列表或者关注列表,用跳表这种结构再合适不过了。这是因为,跳表插入、删除、查找都非常高效,时间复杂度是 $O(logn)$,空间复杂度上稍高,是 $O(n)$。最重要的一点,跳表中存储的数据本来就是有序的了,分页获取粉丝列表或关注列表,就非常高效。

对于微博那种上亿的用户,数据规模太大,无法全部存储在内存中。此时可以借助哈希算法等数据分片方式,将邻接表存储在不同的机器上。如下图所示,我们在机器 1 上存储顶点 1,2,3 的邻接表,在机器 2 上,存储顶点 4,5 的邻接表。逆邻接表的处理方式也一样。当要查询顶点与顶点关系的时候,我们就利用同样的哈希算法,先定位顶点所在的机器,然后再在相应的机器上查找。

img

上面说的思路都是在RAM运行时的思路。除此之外,我们还可以借助外部存储(比如硬盘)的方法,因为外部存储的存储空间要比内存会宽裕很多。例如,数据集就是经常用来持久化存储关系数据的。

例如用下面这张表来存储这样一个图。为了高效地支持前面定义的操作,我们可以在表上建立多个索引,比如第一列、第二列,给这两列都建立索引。

img

内容小结

对于图这种非线性表数据结构,需要理解无向图、有向图、带权图、顶点、边、度、入度、出度这几个概念。除此之外,图还有两种存储方式:邻接矩阵和邻接表。

邻接矩阵存储方法的缺点是比较浪费空间,但是优点是查询效率高,而且方便矩阵运算。邻接表存储方法中每个顶点都对应一个链表,存储与其相连接的其他顶点。尽管邻接表的存储方式比较节省存储空间,但链表不方便查找,所以查询效率没有邻接矩阵存储方式高。针对这个问题,邻接表还有改进升级版,即将链表换成更加高效的动态数据结构,比如平衡二叉查找树、跳表、散列表等。

------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道