对于C ++中的图形问题,邻接表或邻接矩阵有什么更好的选择?


129

对于C ++中的图形问题,邻接列表或邻接矩阵更好吗?每种都有哪些优点和缺点?


21
您使用的结构不取决于语言,而是取决于您要解决的问题。
avakar 2010年

1
我的意思是像djikstra算法一样用于一般用途,我问了这个问题,因为我不知道值得尝试的链表实现是因为邻接矩阵比矩阵更难编码。
magiix 2010年

C ++中的列表就像输入一样容易std::list(或者更好std::vector)。
avakar 2010年

1
@avakar:std::dequestd::set。这取决于图形随时间变化的方式以及打算在其上运行的算法。
Alexandre C.

Answers:


125

这取决于问题。

邻接矩阵

  • 使用O(n ^ 2)内存
  • 查找并检查
    任意两个节点O(1)之间是否存在特定边的速度很快
  • 遍历所有边缘的速度很慢
  • 添加/删除节点很慢;复数运算O(n ^ 2)
  • 快速添加新边O(1)

邻接表

  • 内存使用量取决于边的数量(而不是节点的数量),
    如果邻接矩阵稀疏,则可能会节省大量内存
  • 查找任意两个节点之间是否存在特定边
    比矩阵O(k)慢一些;其中k是邻居节点的数量
  • 遍历所有边缘的速度很快,因为您可以直接访问任何节点邻居
  • 快速添加/删除节点;比矩阵表示更容易
  • 快速添加新边O(1)

链表更难编码,您认为它们的实现值得花一些时间来学习吗?
magiix

11
@magiix:是的,我认为您应该了解如何在需要时编写链表代码,但是不要重新发明轮子也很重要:cplusplus.com/reference/stl/list
Mark Byers 2010年

谁能以链接列表格式提供带有清晰代码的链接,例如广度优先搜索?
magiix 2010年


78

这个答案不仅仅针对C ++,因为所有提及的内容都是关于数据结构本身的,而与语言无关。而且,我的回答是假设您了解邻接表和矩阵的基本结构。

记忆

如果您最关注内存,则可以按照以下公式生成一个允许循环的简单图形:

邻接矩阵占据Ñ 2 /8个字节的空间(每个条目的一个比特)。

邻接表占用8e空间,其中e是边数(32位计算机)。

如果我们将图的密度定义为d = e / n 2 (边数除以最大边数),我们可以找到“断点”,其中列表占用的内存比矩阵多:

图8e>Ñ 2 /8d> 1/64

因此,使用这些数字(仍为32位特定),断点降为1/64。如果密度(e / n 2)大于1/64,则要节省内存,最好使用矩阵

你可以在阅读有关此维基百科(上邻接矩阵的文章)和许多其他的网站。

旁注:可以通过使用哈希表来提高邻接矩阵的空间效率,该哈希表的键是成对的顶点(仅无向)。

迭代和查找

邻接表是仅表示现有边的紧凑方式。然而,这以可能缓慢寻找特定边缘为代价。由于每个列表的长度与顶点的程度一样长,因此如果列表无序,则检查特定边的最坏情况下的查找时间可能变为O(n)。但是,查找顶点的邻居变得微不足道,对于稀疏或小的图形,通过邻接表进行迭代的成本可能可以忽略不计。

另一方面,邻接矩阵使用更多空间以提供恒定的查找时间。由于存在所有可能的条目,因此您可以使用索引在恒定时间内检查边的存在。但是,邻居查找需要O(n),因为您需要检查所有可能的邻居。空间上的明显缺点是,对于稀疏图,添加了大量填充。有关更多信息,请参见上面的内存讨论。

如果您仍不确定使用什么:大多数实际问题会产生稀疏和/或大图,这更适合于邻接列表表示。它们似乎很难实现,但我向您保证不是,并且当您编写BFS或DFS并想获取节点的所有邻居时,它们只是一行代码。但是,请注意,我一般不会推广邻接表。


9
+1用于洞察,但是必须通过用于存储邻接表的实际数据结构来纠正。您可能希望将每个顶点的邻接列表存储为地图或向量,在这种情况下,必须更新公式中的实际数字。同样,可以使用类似的计算来评估收支平衡点的特定算法的时间复杂度。
Alexandre C.

3
是的,此公式适用于特定情况。如果您想得到一个大概的答案,请继续使用此公式,或者根据需要根据您的规范对其进行修改(例如,如今大多数人都拥有64位计算机:))
键入

1
对于那些感兴趣的人,断点的公式(n个节点的图形中平均边缘的最大数量)为e = n / s,其中s指针大小。
deceleratedcaviar

33

好的,我已经在图上编译了基本操作的时间和空间复杂性。
下图应该是不言自明的。
注意,当我们期望图是稠密的时,邻接矩阵如何是可取的;而当我们期望图是稀疏的时,邻接表是如何可取的。
我做了一些假设。问我是否需要澄清复杂性(时间或空间)。(例如,对于一个稀疏图,我将En设为一个小常数,因为我假设添加一个新顶点将仅添加一些边,因为我们希望该图即使在添加后也会保持稀疏顶点。)

请告诉我是否有任何错误。

在此处输入图片说明


如果不知道图是密集图还是稀疏图,可以说邻接表的空间复杂度为O(v + e)吗?

对于大多数实用算法,最重要的操作之一是遍历给定顶点的所有边。您可能需要将其添加到列表中-AL的O(度)和AM的O(V)。
最大

@johnred最好说为AL添加一个顶点(时间)是O(1),因为不是O(en),因为我们并没有真正在添加顶点时添加边。添加边缘可以作为单独的操作处理。对于AM来说,考虑是有道理的,但是即使在那儿,我们也只需要将新顶点的相关行和列初始化为零即可。即使对于AM,边缘的增加也可以单独考虑。
Usman'1

如何为AL O(V)添加一个顶点?我们必须创建一个新的矩阵,将以前的值复制到其中。它应该是O(v ^ 2)。
Alex_ban

19

这取决于您要寻找的东西。

使用邻接矩阵,您可以快速回答有关两个顶点之间的特定边是否属于图形的问题,并且还可以快速插入和删除边。的缺点是,你必须使用过多的空间,尤其是对于有许多顶点,这是非常低效的,特别是如果你的图是稀疏图。

另一方面,对于邻接列表,检查给定边是否在图形中比较困难,因为您必须搜索适当的列表以找到边,但是它们的空间利用率更高。

通常,邻接表是大多数图形应用程序的正确数据结构。


如果使用字典存储邻接表怎么办,那将使您在O(1)摊销时间内存在边。
Rohith Yeravothula

10

假设我们有一个具有n个节点和m个边的图,

示例图
在此处输入图片说明

邻接矩阵: 我们正在创建一个具有n个行和列的矩阵,因此在内存中它将占用与n 2成正比的空间。检查两个名为uv的节点之间是否有边将花费Θ(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)。

示例图的邻接表

在此处输入图片说明
您应该根据需要进行选择。 由于我的声誉,我无法放入矩阵图像,对此感到抱歉



5

最好用示例来回答。

例如,以弗洛伊德·沃歇尔Floyd-Warshall)为例。我们必须使用邻接矩阵,否则算法将渐近变慢。

或者,如果它是30,000个顶点上的密集图,该怎么办?然后邻接矩阵可能有意义,因为您将每对顶点存储1位,而不是每个边沿存储16位(邻接列表所需的最小值):107 MB,而不是1.7 GB。

但是对于DFS,BFS(以及使用它的算法,例如Edmonds-Karp),优先级优先搜索(Dijkstra,Prim,A *)等算法,邻接表和矩阵一样好。好吧,当图形密集时,矩阵可能会有轻微的边缘,但是只有很小的常数因数。(多少钱?这是一个实验的问题。)


2
对于DFS和BFS之类的算法,如果使用矩阵,则每次要查找相邻节点时都需要检查整行,而相邻列表中已经有相邻节点。an adjacency list is as good as a matrix在这种情况下,您为什么这么认为?
realUser404

@ realUser404确实,扫描整个矩阵行是O(n)操作。当您需要遍历所有传出边时,邻接表更适合于稀疏图,它们可以在O(d)(d:节点的度数)中实现。但是,由于顺序访问,矩阵比邻接列表具有更好的缓存性能,因此对于密度较高的图形,扫描矩阵更有意义。
Jochem Kuijpers,

3

要添加到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^2e总边数),则可以看到我们位于断点(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
Saurabh,

2

根据邻接矩阵的实现,图形的“ n”应该更早知道,以实现高效实现。如果图表过于动态,并且需要不时扩展矩阵,那还可以算作不利因素吗?


1

如果您使用哈希表而不是邻接矩阵或列表,则对于所有操作,您将获得更好或相同的big-O运行时和空间(检查边为O(1),获取所有相邻边为O(degree),等等)。

尽管在运行时和空间上都存在一些恒定的因素开销(哈希表的速度不及链表或数组查找的速度,并且需要大量的额外空间来减少冲突)。


1

我只是要克服常规邻接表表示的权衡问题,因为其他答案已经涵盖了其他方面。

通过利用DictionaryHashSet数据结构,可以使用EdgeExists查询以摊销后的恒定时间在邻接表中表示图。这个想法是将顶点保存在字典中,并且对于每个顶点,我们保留一个哈希集,该哈希集引用其具有边的其他顶点。

此实现中的一个小折衷是,它将具有空间复杂度O(V + 2E)而不是像常规邻接表中那样的O(V + E),因为边在这里表示两次(因为每个顶点都有自己的哈希集)的边缘)。但是,使用此实现可以在摊销时间O(1)中完成诸如AddVertexAddEdgeRemoveEdge之类的操作,除了RemoveVertex像邻接矩阵一样需要O(V)。这意味着邻接关系矩阵除了实现简单之外,没有任何特定的优势。在此邻接表实现中,我们可以在稀疏图上节省空间,而性能几乎相同。

有关详细信息,请查看下面的Github C#存储库中的实现。请注意,对于加权图,它使用嵌套字典而不是字典-哈希集组合,以适应权重值。类似地,对于有向图,对于输入和输出边缘也有单独的哈希集。

高级算法

注意:我相信使用惰性删除我们可以进一步优化RemoveVertex操作以摊销O(1),即使我尚未测试过该想法。例如,删除后,只需将顶点标记为在字典中已删除,然后在其他操作期间懒惰地清除孤立边缘。


对于邻接矩阵,删除顶点需要O(V ^ 2)而不是O(V)
Saurabh

是。但是,如果您使用字典来跟踪数组索引,则它将降至O(V)。看一下这个RemoveVertex实现。
justcoding121
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.