将图存储在内存中的三种方式,优缺点

Posted

技术标签:

【中文标题】将图存储在内存中的三种方式,优缺点【英文标题】:Three ways to store a graph in memory, advantages and disadvantages 【发布时间】:2011-03-18 06:02:03 【问题描述】:

在内存中存储图形有三种方式:

    节点作为对象,边作为指针 包含编号节点 x 和节点 y 之间所有边权重的矩阵 编号节点之间的边列表

我知道如何写这三个,但我不确定我是否考虑过每个的所有优点和缺点。

这些将图形存储在内存中的每种方式的优缺点是什么?

【问题讨论】:

只有当图形连接非常紧密或非常小时,我才会考虑矩阵。对于稀疏连接的图,对象/指针或边列表方法都将提供更好的内存使用。我很好奇除了存储之外我还忽略了什么。 ;) 它们的时间复杂度也不同,矩阵为 O(1),其他表示可能会根据您要查找的内容而有很大差异。 我记得不久前读过一篇文章,描述了将图形实现为矩阵而不是指针列表的硬件优势。我不记得太多了,除了因为您正在处理一个连续的内存块,在任何给定时间,您的大部分工作集很可能都在 L2 缓存中。另一方面,节点/指针列表可能会通过内存进行射击,并且可能需要不命中缓存的获取。我不确定我是否同意,但这是一个有趣的想法。 @Dean J:只是一个关于“节点作为对象,边作为指针表示”的问题。您使用哪种数据结构在对象中存储指针?是列表吗? 常用名称有:(1)等价于邻接表,(2)邻接矩阵,(3)边表. 【参考方案1】:

分析这些的一种方法是根据内存和时间复杂度(这取决于您希望如何访问图表)。

将节点存储为具有指向彼此的指针的对象

此方法的内存复杂度为 O(n),因为您拥有的对象数量与拥有的节点数量一样多。所需的指针(指向节点)的数量最多为 O(n^2),因为每个节点对象可能包含最多 n 个节点的指针。 此数据结构的时间复杂度为 O(n) 访问任何给定节点。

存储边权重矩阵

这将是矩阵的 O(n^2) 内存复杂度。 这种数据结构的优点是访问任何给定节点的时间复杂度为 O(1)。

根据您在图上运行的算法以及有多少节点,您必须选择合适的表示。

【讨论】:

如果您还将节点存储在单独的数组中,我相信在对象/指针模型中搜索的时间复杂度仅为 O(n)。否则,您将需要遍历图形来搜索所需的节点,不是吗?遍历任意图中的每个节点(但不一定是每个边)不能在 O(n) 中完成,可以吗? @BarryFruitman 我很确定你是对的。 BFS 为 O(V+E)。此外,如果您正在搜索未连接到其他节点的节点,您将永远找不到它。【参考方案2】:

我认为您的第一个示例有点模棱两可——节点作为对象,边作为指针。您可以通过仅存储指向某个根节点的指针来跟踪这些,在这种情况下访问给定节点可能效率低下(假设您想要节点 4 - 如果未提供节点对象,您可能必须搜索它) .在这种情况下,您还会丢失从根节点无法访问的图形部分。我认为这是 f64 rainbow 所假设的情况,他说访问给定节点的时间复杂度是 O(n)。

否则,您还可以保留一个充满指向每个节点的指针的数组(或哈希图)。这允许 O(1) 访问给定节点,但会增加内存使用量。如果 n 是节点数,e 是边数,则这种方法的空间复杂度为 O(n + e)。

矩阵方法的空间复杂度将沿着 O(n^2) 的线(假设边是单向的)。如果您的图表是稀疏的,您的矩阵中会有很多空单元格。但是如果你的图是全连接的(e = n^2),这与第一种方法相比是有利的。正如 RG 所说,如果您将矩阵分配为一块内存,则使用这种方法可能还会减少缓存未命中,这可以更快地跟踪图周围的许多边。

在大多数情况下,第三种方法可能是最节省空间的 - O(e) - 但会使查找给定节点的所有边成为 O(e) 的苦差事。我想不出这会有什么用处。

【讨论】:

边缘列表对于Kruskal's algorithm 来说是自然的(“对于每个边缘,在 union-find 中查找”)。此外,Skiena(第 2 版,第 157 页)在他的库 Combinatorica(这是一个包含许多算法的通用库)中谈到边列表作为图的基本数据结构。他确实提到,造成这种情况的原因之一是 Mathematica 的计算模型所施加的限制,这是 Combinatorica 所处的环境。【参考方案3】:

好的,所以如果边没有权重,矩阵可以是二元数组,在这种情况下使用二元运算符可以让事情变得非常非常快。

如果图是稀疏的,对象/指针方法似乎更有效。将对象/指针保存在数据结构中以专门将它们哄入一块内存也可能是一个不错的计划,或者任何其他让它们保持在一起的方法。

邻接列表 - 只是连接节点的列表 - 似乎是迄今为止内存效率最高的,但也可能是最慢的。

反转有向图容易用矩阵表示,用邻接表容易,但用对象/指针表示就不是那么好。

【讨论】:

【参考方案4】:

还有一些需要考虑的事情:

    通过将权重存储在矩阵中,矩阵模型更容易用于具有加权边的图。对象/指针模型需要将边权重存储在并行数组中,这需要与指针数组同步。

    对象/指针模型更适用于有向图而不是无向图,因为指针需要成对维护,这可能会变得不同步。

【讨论】:

您的意思是指针需要与无向图成对维护,对吗?如果它是有向的,你只需将一个顶点添加到特定顶点的邻接列表中,但如果它是无向的,你必须在两个顶点的邻接列表中添加一个? @FrostyStraw 是的,完全正确。【参考方案5】:

正如一些人所指出的,对象和指针方法存在搜索困难,但对于构​​建二叉搜索树等具有大量额外结构的事情来说是很自然的。

我个人喜欢邻接矩阵,因为它们使用代数图论中的工具使各种问题变得容易得多。 (例如,邻接矩阵的 k 次方给出从顶点 i 到顶点 j 的长度为 k 的路径数。在取 k 次方之前添加一个单位矩阵以获得长度

但是每个人都说邻接矩阵很昂贵!他们只说对了一半:当您的图形边缘很少时,您可以使用稀疏矩阵来解决这个问题。稀疏矩阵数据结构完全可以保持邻接列表的工作,但仍然具有可用的标准矩阵运算的全部范围,为您提供两全其美的效果。

【讨论】:

【参考方案6】:

还有另一种选择:节点作为对象,边也作为对象,每条边同时位于两个双向链表中:来自同一节点的所有边的列表和进入的所有边的列表同一个节点。

struct Node 
    ... node payload ...
    Edge *first_in;    // All incoming edges
    Edge *first_out;   // All outgoing edges
;

struct Edge 
    ... edge payload ...
    Node *from, *to;
    Edge *prev_in_from, *next_in_from; // dlist of same "from"
    Edge *prev_in_to, *next_in_to;     // dlist of same "to"
;

内存开销很大(每个节点 2 个指针,每个边 6 个指针)但是你得到了

O(1) 节点插入 O(1) 边缘插入(给定指向“from”和“to”节点的指针) O(1) 边删除(给定指针) O(deg(n)) 节点删除(给定指针) O(deg(n)) 查找节点的邻居

该结构还可以表示一个相当一般的图:带循环的定向多重图(即,您可以在相同的两个节点之间有多个不同的边,包括多个不同的循环 - 边从 x 到 x)。

here 提供了有关此方法的更详细说明。

【讨论】:

【参考方案7】:

看看***上的comparison table。它很好地理解了何时使用图表的每种表示。

【讨论】:

以上是关于将图存储在内存中的三种方式,优缺点的主要内容,如果未能解决你的问题,请参考以下文章

js的三种继承方式及其优缺点

HTML中使用JavaScript的三种方式及优缺点

设计模式:单例模式的三种创建方式及其各自的优缺点

单例模式的三种写法和优缺点

Redis如何实现持久化?详细讲解RDB的三种触发机制及其优缺点,带你快速掌握RDB

构造并发程序的三种基本方法和优缺点