存储及基本操作

图的存储必须要完整、准确的反映顶点集和边集的信息。主要的存储方式有两种: 邻接矩阵 和 邻接表。前者属于图的顺序存储结构,后者属于图的链式存储结构。

邻接矩阵

设图的顶点数量为 NN邻接矩阵(adjacency matrix使用一个 n×nn \times n大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 1 或 0 表示两个顶点之间是否存在边。

如下图所示,设邻接矩阵为 MM、顶点列表为 VV ,那么矩阵元素 M[i,j]=1M[i,j]=1 表示顶点 V[i]V[i] 到顶点 V[j]V[j] 之间存在边,反之 M[i,j]=0M[i,j]=0 表示两顶点之间无边。

邻接矩阵具有以下特性:

  • 顶点不能与自身相连,因此邻接矩阵主对角线元素没有意义

  • 无向图的邻接矩一定是对称矩阵并且唯一

  • 邻接矩阵的第i行(或第i列) 非零元素(或非∞元素)的个数正好是第 i 个顶点的

  • 将邻接矩阵的元素从 1 0 替换为权重,则可表示有权图。

使用邻接矩阵表示图时,我们可以直接访问矩阵元素以获取边,因此增删查改操作的效率很高,时间复杂度均为 O(1)O(1)。然而,矩阵的空间复杂度为 O(n2)O(n^2),内存占用较多。

邻接矩阵的基本操作:

  • 添加或删除边:直接在邻接矩阵中修改指定的边即可,使用 O(1)O(1) 时间。而由于是无向图,因此需要同时更新两个方向的边。

  • 添加顶点:在邻接矩阵的尾部添加一行一列,并全部填 0 即可,使用 O(n)O(n)时间。

  • 删除顶点:在邻接矩阵中删除一行一列。当删除首行首列时达到最差情况,需要将 (n1)2(n-1)^2 个元素“向左上移动”,从而使用 O(n2)O(n^2)时间。

  • 初始化:传入 nn 个顶点,初始化长度为 nn 的顶点列表 vertices ,使用 O(n)O(n) 时间;初始化 n×nn \times n 大小的邻接矩阵 adjMat ,使用 O(n2)O(n^2) 时间。

实现代码

邻接表

邻接表(adjacency list使用 nn 个链表来表示图,链表节点表示顶点。第 ii 个链表对应顶点 ii ,其中存储了该顶点的所有邻接顶点(与该顶点相连的顶点)。下图展示了一个使用邻接表存储的图的示例。

邻接表仅存储实际存在的边,而边的总数通常远小于 n2n^2,因此它更加节省空间。然而,在邻接表中需要通过遍历链表来查找边,因此其时间效率不如邻接矩阵。

观察上图 ,邻接表结构与哈希表中的“链式地址”非常相似,因此我们也可以采用类似的方法来优化效率。比如当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从 O(n)O(n) 优化至 O(logn)O(\log{n});还可以把链表转换为哈希表,从而将时间复杂度降至 O(1)O(1)

邻接表的基本操作:

  • 添加边:在顶点对应链表的末尾添加边即可,使用 O(1)O(1) 时间。因为是无向图,所以需要同时添加两个方向的边。

  • 删除边:在顶点对应链表中查找并删除指定边,使用 O(m)O(m) 时间。在无向图中,需要同时删除两个方向的边。

  • 添加顶点:在邻接表中添加一个链表,并将新增顶点作为链表头节点,使用 O(1)O(1) 时间。

  • 删除顶点:需遍历整个邻接表,删除包含指定顶点的所有边,使用 O(n+m)O(n+m)时间。

  • 初始化:在邻接表中创建 nn 个顶点和 2m2m 条边,使用 O(n+m)O(n+m) 时间。

以下是邻接表的代码实现。对比上图 ,实际代码有以下不同。

  • 为了方便添加与删除顶点,以及简化代码,我们使用列表(动态数组)来代替链表。

  • 使用哈希表来存储邻接表,key 为顶点实例,value 为该顶点的邻接顶点列表(链表)。

另外,我们在邻接表中使用 Vertex 类来表示顶点,这样做的原因是:如果与邻接矩阵一样,用列表索引来区分不同顶点,那么假设要删除索引为 ii 的顶点,则需遍历整个邻接表,将所有大于 ii 的索引全部减 1 ,效率很低。而如果每个顶点都是唯一的 Vertex 实例,删除某一顶点之后就无须改动其他顶点了。

代码实现

效率对比

图的常见应用

顶点
图计算问题

社交网络

用户

好友关系

潜在好友推荐

地铁线路

站点

站点间的连通性

最短路线推荐

太阳系

星体

星体间的万有引力作用

行星轨道计算

Citing

Hello算法-图

Last updated

Was this helpful?