有三种方法可以在内存中存储图形:
我知道如何写这三个,但我不确定我是否已经考虑过每个人的所有优点和缺点。
将这些图存储在内存中的每种方法有哪些优点和缺点?
答案 0 :(得分:47)
分析这些的一种方法是在内存和时间复杂度方面(这取决于您希望如何访问图表)。
将节点存储为具有彼此指针的对象
存储边权重矩阵
根据您在图表上运行的算法以及有多少个节点,您必须选择合适的表示。
答案 1 :(得分:10)
还有几件事需要考虑:
通过将权重存储在矩阵中,矩阵模型更容易使自身更适合具有加权边的图。对象/指针模型需要在并行数组中存储边权重,这需要与指针数组同步。
对象/指针模型使用有向图比无向图更好,因为指针需要成对维护,这可能会变得不同步。
答案 2 :(得分:7)
我认为你的第一个例子有点模棱两可 - 节点作为对象,边缘作为指针。您可以通过仅存储指向某个根节点的指针来跟踪这些情况,在这种情况下,访问给定节点可能效率低下(假设您想要节点4 - 如果未提供节点对象,则可能必须搜索它) 。在这种情况下,您还会丢失无法从根节点访问的图形部分。我认为这是f64彩虹假设的情况,他说访问给定节点的时间复杂度是O(n)。
否则,您还可以保持一个数组(或hashmap)充满指向每个节点的指针。这允许O(1)访问给定节点,但稍微增加了内存使用量。如果n是节点数,e是边数,则该方法的空间复杂度为O(n + e)。
矩阵方法的空间复杂度将沿着O(n ^ 2)的线(假设边是单向的)。如果图形稀疏,则矩阵中将包含大量空单元格。但是如果你的图形是完全连接的(e = n ^ 2),这与第一种方法相比是有利的。正如RG所说,如果你将矩阵分配为一块内存,你可能也会减少使用这种方法的缓存未命中,这可能会更快地跟随图形的大量边缘。
对于大多数情况来说,第三种方法可能是空间效率最高的 - O(e) - 但会使找到给定节点的所有边缘成为O(e)家务。我想不出一个非常有用的案例。
答案 3 :(得分:7)
对象和指针方法遇到了搜索困难,正如一些人所指出的那样,但是对于构建二元搜索树这样的事情来说非常自然,因为它有很多额外的结构。
我个人喜欢邻接矩阵,因为它们使用代数图论的工具使各种问题变得容易多了。 (邻接矩阵的k次方给出了从顶点i到顶点j的长度为k的路径数。例如,在取k次幂之前加上一个单位矩阵,得到长度为< = k的路径数。对拉普拉斯算子进行n-1次调整以获得生成树的数量......依此类推。)
但是每个人都认为邻接矩阵的内存很贵!它们只是右半边:当图形边缘很少时,你可以使用稀疏矩阵来解决这个问题。稀疏矩阵数据结构完全可以保留邻接列表,但仍然可以使用标准矩阵操作的全部范围,为您提供两全其美的优势。
答案 4 :(得分:4)
在维基百科上查看comparison table。它很好地理解了何时使用图的每个表示。
答案 5 :(得分:3)
好的,所以如果边没有权重,那么矩阵可以是一个二进制数组,并且使用二元运算符可以使事情变得非常非常快。
如果图形稀疏,则对象/指针方法看起来效率更高。将数据结构中的对象/指针专门用于将它们哄骗到一块内存中也可能是一个很好的计划,或者让它们保持在一起的任何其他方法。
邻接列表 - 简单地说是连接节点的列表 - 似乎是目前最有效的内存,但也可能是最慢的。
使用矩阵表示反转有向图是 easy ,使用邻接列表可以轻松反转,但对于对象/指针表示则不是很好。
答案 6 :(得分:2)
还有另一种选择:节点作为对象,边缘也作为对象,每个边缘同时存在于两个双向链接列表中:从同一节点出来的所有边缘的列表以及进入的所有边缘的列表同一节点。
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的边缘)。
有关此方法的更详细说明here。