有三种方法可以将图形存储在内存中:
- 节点作为对象,边缘作为指针
- 包含编号的节点x和节点y之间的所有边缘权重的矩阵
- 编号节点之间的边列表
我知道怎么写这三者,但是我不确定我是否已经想到了每一种的优点和缺点。
这些将图形存储在内存中的每种方式的优缺点是什么?
有三种方法可以将图形存储在内存中:
我知道怎么写这三者,但是我不确定我是否已经想到了每一种的优点和缺点。
这些将图形存储在内存中的每种方式的优缺点是什么?
Answers:
分析这些的一种方法是根据内存和时间复杂度(取决于您要如何访问图形)。
将节点存储为具有相互指针的对象
存储边缘权重矩阵
根据在图形上运行的算法以及节点数量,您必须选择合适的表示形式。
还有两件事要考虑:
通过将权重存储在矩阵中,矩阵模型更容易使其具有加权边缘图。对象/指针模型需要将边缘权重存储在并行数组中,这需要与指针数组同步。
与无向图相比,对象/指针模型在有向图上的工作效果更好,这是因为指针需要成对维护,这可能会变得不同步。
正如一些人指出的那样,对象指针方法存在搜索困难的问题,但是对于诸如构建二进制搜索树之类的事情却很自然,因为那里有很多额外的结构。
我个人喜欢邻接矩阵,因为它们使用代数图论的工具使各种问题变得更加容易。(例如,邻接矩阵的k次幂给出从顶点i到顶点j的长度为k的路径数。在求k次幂之前添加一个单位矩阵,以获得长度<= k的路径数。拉普拉斯算子的n-1个未成年人,以获取生成树的数量...依此类推。)
但是每个人都说邻接矩阵是昂贵的内存!它们只是右半边:当图形的边很少时,可以使用稀疏矩阵来解决此问题。稀疏矩阵数据结构可以完全完成仅保留邻接表的工作,但仍具有标准矩阵运算的全部范围,可为您提供两全其美的方法。
我认为您的第一个示例有点模棱两可-节点作为对象,边缘作为指针。您可以通过仅存储指向某个根节点的指针来跟踪这些情况,在这种情况下,访问给定节点可能效率不高(例如,您想要节点4 –如果未提供节点对象,则可能必须搜索它) 。在这种情况下,您还会丢失图表中从根节点无法访问的部分。我认为f64 Rainbow就是这种情况,他说访问给定节点的时间复杂度为O(n)。
否则,您还可以使数组(或哈希图)充满指向每个节点的指针。这允许O(1)访问给定节点,但会稍微增加内存使用量。如果n是节点数,e是边数,则此方法的空间复杂度将为O(n + e)。
矩阵方法的空间复杂度将沿着O(n ^ 2)的线(假设边是单向的)。如果图形稀疏,矩阵中将有很多空单元格。但是,如果您的图是完全连接的(e = n ^ 2),则与第一种方法相比是有利的。如RG所述,如果将矩阵分配为一个内存块,则使用此方法还可能减少缓存丢失,这可能会使跟踪图形周围的许多边变得更快。
第三种方法在大多数情况下可能是最节省空间的-O(e)-但会使查找给定节点的所有边缘成为O(e)琐事。我想不出这会很有用的情况。
还有另一种选择:节点作为对象,边也作为对象,每个边同时在两个双向链接的列表中:来自同一节点的所有边的列表和进入同一节点的所有边的列表。
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个指针),但是您得到了
该结构还可以表示一个相当普通的图:带有循环的面向多重图(即,您可以在相同的两个节点之间具有多个不同的边,包括多个不同的循环-边从x到x)。
有关此方法的详细说明,请参见此处。