对于C ++中的图形问题,邻接列表或邻接矩阵更好吗?每种都有哪些优点和缺点?
std::list
(或者更好std::vector
)。
std::deque
或std::set
。这取决于图形随时间变化的方式以及打算在其上运行的算法。
对于C ++中的图形问题,邻接列表或邻接矩阵更好吗?每种都有哪些优点和缺点?
std::list
(或者更好std::vector
)。
std::deque
或std::set
。这取决于图形随时间变化的方式以及打算在其上运行的算法。
Answers:
这取决于问题。
这个答案不仅仅针对C ++,因为所有提及的内容都是关于数据结构本身的,而与语言无关。而且,我的回答是假设您了解邻接表和矩阵的基本结构。
如果您最关注内存,则可以按照以下公式生成一个允许循环的简单图形:
邻接矩阵占据Ñ 2 /8个字节的空间(每个条目的一个比特)。
邻接表占用8e空间,其中e是边数(32位计算机)。
如果我们将图的密度定义为d = e / n 2 (边数除以最大边数),我们可以找到“断点”,其中列表占用的内存比矩阵多:
图8e>Ñ 2 /8 当 d> 1/64
因此,使用这些数字(仍为32位特定),断点降为1/64。如果密度(e / n 2)大于1/64,则要节省内存,最好使用矩阵。
你可以在阅读有关此维基百科(上邻接矩阵的文章)和许多其他的网站。
旁注:可以通过使用哈希表来提高邻接矩阵的空间效率,该哈希表的键是成对的顶点(仅无向)。
邻接表是仅表示现有边的紧凑方式。然而,这以可能缓慢寻找特定边缘为代价。由于每个列表的长度与顶点的程度一样长,因此如果列表无序,则检查特定边的最坏情况下的查找时间可能变为O(n)。但是,查找顶点的邻居变得微不足道,对于稀疏或小的图形,通过邻接表进行迭代的成本可能可以忽略不计。
另一方面,邻接矩阵使用更多空间以提供恒定的查找时间。由于存在所有可能的条目,因此您可以使用索引在恒定时间内检查边的存在。但是,邻居查找需要O(n),因为您需要检查所有可能的邻居。空间上的明显缺点是,对于稀疏图,添加了大量填充。有关更多信息,请参见上面的内存讨论。
如果您仍不确定使用什么:大多数实际问题会产生稀疏和/或大图,这更适合于邻接列表表示。它们似乎很难实现,但我向您保证不是,并且当您编写BFS或DFS并想获取节点的所有邻居时,它们只是一行代码。但是,请注意,我一般不会推广邻接表。
e = n / s
,其中s
指针大小。
好的,我已经在图上编译了基本操作的时间和空间复杂性。
下图应该是不言自明的。
注意,当我们期望图是稠密的时,邻接矩阵如何是可取的;而当我们期望图是稀疏的时,邻接表是如何可取的。
我做了一些假设。问我是否需要澄清复杂性(时间或空间)。(例如,对于一个稀疏图,我将En设为一个小常数,因为我假设添加一个新顶点将仅添加一些边,因为我们希望该图即使在添加后也会保持稀疏顶点。)
请告诉我是否有任何错误。
这取决于您要寻找的东西。
使用邻接矩阵,您可以快速回答有关两个顶点之间的特定边是否属于图形的问题,并且还可以快速插入和删除边。的缺点是,你必须使用过多的空间,尤其是对于有许多顶点,这是非常低效的,特别是如果你的图是稀疏图。
另一方面,对于邻接列表,检查给定边是否在图形中比较困难,因为您必须搜索适当的列表以找到边,但是它们的空间利用率更高。
通常,邻接表是大多数图形应用程序的正确数据结构。
假设我们有一个具有n个节点和m个边的图,
邻接矩阵: 我们正在创建一个具有n个行和列的矩阵,因此在内存中它将占用与n 2成正比的空间。检查两个名为u和v的节点之间是否有边将花费Θ(1)时间。例如,检查(1,2)是一条边将在代码中如下所示:
if(matrix[1][2] == 1)
如果要标识所有边缘,则必须在矩阵上进行迭代,这将需要两个嵌套循环,并且需要Θ(n 2)。(您可以只使用矩阵的上三角部分来确定所有边,但是它将再次为Θ(n 2))
邻接列表: 我们正在创建一个列表,每个节点还指向另一个列表。您的列表将包含n个元素,并且每个元素将指向一个列表,该列表的项目数等于此节点的邻居数(查看图像以获得更好的可视化效果)。因此,它将占用与n + m成正比的内存空间。检查(u,v)是否为边将花费O(deg(u))时间,其中deg(u)等于u的邻居数。因为最多只能遍历u指向的列表。识别所有边缘将取Θ(n + m)。
示例图的邻接表
最好用示例来回答。
例如,以弗洛伊德·沃歇尔(Floyd-Warshall)为例。我们必须使用邻接矩阵,否则算法将渐近变慢。
或者,如果它是30,000个顶点上的密集图,该怎么办?然后邻接矩阵可能有意义,因为您将每对顶点存储1位,而不是每个边沿存储16位(邻接列表所需的最小值):107 MB,而不是1.7 GB。
但是对于DFS,BFS(以及使用它的算法,例如Edmonds-Karp),优先级优先搜索(Dijkstra,Prim,A *)等算法,邻接表和矩阵一样好。好吧,当图形密集时,矩阵可能会有轻微的边缘,但是只有很小的常数因数。(多少钱?这是一个实验的问题。)
an adjacency list is as good as a matrix
在这种情况下,您为什么这么认为?
要添加到keyer5053有关内存使用情况的答案。
对于任何有向图,邻接矩阵(每个边沿1位)会占用n^2 * (1)
存储位。
对于完整的图,邻接列表(具有64位指针)会消耗n * (n * 64)
内存位,但不包括列表开销。
对于不完整的图,邻接表会消耗0
内存位,但不包括列表开销。
对于邻接表,可以使用以下公式确定e
邻接矩阵最适合存储之前的最大边数()。
edges = n^2 / s
确定最大边缘数,其中s
平台的指针大小在哪里。
如果您的图形是动态更新的,则可以使用(每个节点的)平均边缘计数来保持此效率n / s
。
一些带有64位指针和动态图的示例(动态图在更改后有效地更新了问题的解决方案,而不是每次更改后都从头开始重新计算它。)
对于有向图,其中n
为300,使用邻接表的每个节点的最佳边数为:
= 300 / 64
= 4
如果将其插入keyer5053的公式d = e / n^2
(e
总边数),则可以看到我们位于断点(1 / s
)下方:
d = (4 * 300) / (300 * 300)
d < 1/64
aka 0.0133 < 0.0156
但是,指针的64位可能会过大。如果改为使用16位整数作为指针偏移量,则在断点之前我们最多可以容纳18个边。
= 300 / 16
= 18
d = ((18 * 300) / (300^2))
d < 1/16
aka 0.06 < 0.0625
这些示例中的每个示例都忽略了邻接列表自身的开销(64*2
对于向量和64位指针)。
d = (4 * 300) / (300 * 300)
,不是d = 4 / (300 * 300)
吗?由于公式是d = e / n^2
。
我只是要克服常规邻接表表示的权衡问题,因为其他答案已经涵盖了其他方面。
通过利用Dictionary和HashSet数据结构,可以使用EdgeExists查询以摊销后的恒定时间在邻接表中表示图。这个想法是将顶点保存在字典中,并且对于每个顶点,我们保留一个哈希集,该哈希集引用其具有边的其他顶点。
此实现中的一个小折衷是,它将具有空间复杂度O(V + 2E)而不是像常规邻接表中那样的O(V + E),因为边在这里表示两次(因为每个顶点都有自己的哈希集)的边缘)。但是,使用此实现可以在摊销时间O(1)中完成诸如AddVertex,AddEdge,RemoveEdge之类的操作,除了RemoveVertex像邻接矩阵一样需要O(V)。这意味着邻接关系矩阵除了实现简单之外,没有任何特定的优势。在此邻接表实现中,我们可以在稀疏图上节省空间,而性能几乎相同。
有关详细信息,请查看下面的Github C#存储库中的实现。请注意,对于加权图,它使用嵌套字典而不是字典-哈希集组合,以适应权重值。类似地,对于有向图,对于输入和输出边缘也有单独的哈希集。
注意:我相信使用惰性删除我们可以进一步优化RemoveVertex操作以摊销O(1),即使我尚未测试过该想法。例如,删除后,只需将顶点标记为在字典中已删除,然后在其他操作期间懒惰地清除孤立边缘。