对于 C++ 中的图问题,邻接列表或邻接矩阵哪个更好?
Posted
技术标签:
【中文标题】对于 C++ 中的图问题,邻接列表或邻接矩阵哪个更好?【英文标题】:What is better, adjacency lists or adjacency matrices for graph problems in C++? 【发布时间】:2011-01-14 04:04:10 【问题描述】:各自的优点和缺点是什么?
【问题讨论】:
您使用的结构不取决于语言,而是取决于您要解决的问题。 我的意思是像 djikstra 算法这样的一般用途,我问这个问题是因为我不知道链表实现是否值得尝试,因为它比邻接矩阵更难编码。 C++ 中的列表就像输入std::list
(或者更好的是,std::vector
)一样简单。
@avakar: 或std::deque
或std::set
。这取决于图表随时间变化的方式以及您打算在其上运行的算法。
阅读详情来自khan academy
【参考方案1】:
这取决于问题。
Adjacency Matrix
使用 O(n^2) 内存 查找和检查特定边是否存在的速度很快 任意两个节点之间 O(1) 遍历所有边很慢 添加/删除节点很慢;复杂运算 O(n^2) 添加新边 O(1) 速度很快Adjacency List
内存使用更多地取决于边数(更少取决于节点数), 如果邻接矩阵稀疏,这可能会节省大量内存 查找任意两个节点之间是否存在特定边 比使用矩阵 O(k) 稍慢;其中 k 是邻居节点的数量 迭代所有边的速度很快,因为您可以直接访问任何节点邻居 添加/删除节点速度快;比矩阵表示更容易 添加新边 O(1) 速度很快【讨论】:
链表比较难编码,你觉得它们的实现值得花点时间学习吗? @magiix:是的,我认为您应该了解如何在需要时对链表进行编码,但不要重新发明***也很重要:cplusplus.com/reference/stl/list 谁能提供一个干净的代码链接,以链表格式说广度优先搜索?? 使用 std::list geeksforgeeks.org/breadth-first-traversal-for-a-graph【参考方案2】:这个答案不仅适用于 C++,因为所提到的一切都是关于数据结构本身的,而与语言无关。而且,我的回答是假设您知道邻接表和矩阵的基本结构。
内存
如果内存是您最关心的问题,您可以按照以下公式制作允许循环的简单图表:
邻接矩阵占用 n2/8 字节空间(每个条目一位)。
一个邻接表占用8e空间,其中e是边数(32位计算机)。
如果我们将图的密度定义为 d = e/n2(边数除以最大边数),我们可以找到列表占用的“断点”比矩阵更多的内存:
8e > n2/8 当d > 1/64
因此,对于这些数字(仍然是 32 位特定的),断点位于 1/64。 如果密度 (e/n2) 大于 1/64,那么如果要节省内存,最好使用 矩阵。
您可以在 wikipedia(关于邻接矩阵的文章)和许多其他网站上阅读有关此内容的信息。
旁注:可以通过使用哈希表来提高邻接矩阵的空间效率,其中键是一对顶点(仅限无向)。
迭代和查找
邻接表是一种仅表示现有边的紧凑方式。然而,这是以查找特定边的速度可能缓慢为代价的。由于每个列表与顶点的度数一样长,如果列表无序,则检查特定边的最坏情况查找时间可能变为 O(n)。 但是,查找顶点的邻居变得微不足道,对于稀疏或小型图,遍历邻接列表的成本可能可以忽略不计。
另一方面,邻接矩阵使用更多空间来提供恒定的查找时间。由于存在每个可能的条目,因此您可以使用索引在恒定时间内检查是否存在边。但是,邻居查找需要 O(n),因为您需要检查所有可能的邻居。 明显的空间缺点是,对于稀疏图,添加了很多填充。有关这方面的更多信息,请参阅上面的内存讨论。
如果您仍然不确定要使用什么:大多数实际问题都会产生稀疏和/或大图,这更适合邻接表表示。它们可能看起来更难实现,但我向您保证,它们不是,当您编写 BFS 或 DFS 并想要获取节点的所有邻居时,它们只需一行代码即可。但是请注意,我一般不会宣传邻接列表。
【讨论】:
+1 以获得洞察力,但这必须通过用于存储邻接列表的实际数据结构来纠正。您可能希望将每个顶点的邻接列表存储为映射或向量,在这种情况下,必须更新公式中的实际数字。此外,类似的计算可用于评估特定算法的时间复杂度的收支平衡点。 是的,这个公式是针对特定场景的。如果您想要一个粗略的答案,请继续使用此公式,或者根据需要根据您的规格进行修改(例如,现在大多数人都有一台 64 位计算机:)) 对于那些感兴趣的人,断点的公式(n个节点的图中平均边的最大数量)是e = n / s
,其中s
是指针大小。【参考方案3】:
好的,我已经编译了图上基本操作的时间和空间复杂性。 下面的图片应该是不言自明的。 请注意,当我们期望图是密集的时,邻接矩阵是如何更可取的,以及当我们期望图是稀疏的时,邻接表是如何更可取的。 我做了一些假设。问我是否需要澄清复杂性(时间或空间)。 (例如,对于稀疏图,我将 En 作为一个小常数,因为我假设添加一个新顶点只会添加几条边,因为我们希望图在添加之后仍然保持稀疏顶点。)
如果有错误请告诉我。
【讨论】:
如果不知道图是密集图还是稀疏图,是否可以说邻接表的空间复杂度为 O(v+e) ?跨度> 对于大多数实用算法,最重要的操作之一是遍历所有从给定顶点出来的边。您可能希望将其添加到您的列表中 - AL 为 O(degree),AM 为 O(V)。 @johnred 说为 AL 添加顶点(时间)是 O(1) 不是更好,因为不是 O(en),因为我们在添加顶点时并没有真正添加边.添加边可以作为单独的操作处理。对于 AM 来说,考虑是有意义的,但即使在那里,我们也只需要将新顶点的相关行和列初始化为零。甚至对于 AM 添加的边也可以单独考虑。 如何向 AL O(V) 添加顶点?我们必须创建一个新矩阵,将以前的值复制到其中。它应该是 O(v^2)。 @Alex_ban 通常是的,但实际上,这取决于语言及其实现方式(您可以做很多优化,例如使用动态数组)。【参考方案4】:这取决于您要查找的内容。
使用邻接矩阵,您可以快速回答有关两个顶点之间的特定边是否属于图形的问题,您还可以快速插入和删除边. 缺点是你必须使用过多的空间,特别是对于有很多顶点的图,这是非常低效的,特别是如果你的图是稀疏的。
另一方面,使用 邻接列表 更难检查给定边是否在图中,因为您必须搜索适当的列表才能找到边缘,但它们更节省空间。
一般来说,邻接表是大多数图应用的正确数据结构。
【讨论】:
如果您使用字典来存储邻接列表,那将在 O(1) 摊销时间内为您提供一条边。【参考方案5】:假设我们有一个图,它有 n 个节点和 m 个边,
示例图
邻接矩阵: 我们正在创建一个具有 n 行和列数的矩阵,因此在内存中它将占用与 n2 成比例的空间。检查名为 u 和 v 的两个节点之间是否有边将花费 Θ(1) 时间。例如,检查 (1, 2) 是否是一条边在代码中如下所示:
if(matrix[1][2] == 1)
如果要识别所有边,则必须遍历矩阵,这将需要两个嵌套循环,并且需要 Θ(n2)。 (您可以只使用矩阵的上三角部分来确定所有边,但它将再次是 Θ(n2))
邻接列表: 我们正在创建一个列表,每个节点也指向另一个列表。您的列表将有 n 个元素,每个元素将指向一个列表,该列表的项目数等于该节点的邻居数(查看图像以获得更好的可视化效果)。因此它将占用与 n+m 成正比的内存空间。检查 (u, v) 是否是一条边将花费 O(deg(u)) 时间,其中 deg(u) 等于 u 的邻居数。因为至多,您必须遍历 u 指向的列表。识别所有边需要 Θ(n+m)。
示例图的邻接列表
您应该根据自己的需要做出选择。 由于我的声誉,我无法放置矩阵的图像,抱歉
【讨论】:
图表中 2 和 4 之间的橙色边是什么?为什么你的图片中没有2 -> 4
或4 -> 2
?
边在第二张图中表示为红色块。第二张图表示 2 和 4 之间的关系,2 的列表中有 (1, 3, 4, 5),4 的列表中有 (2, 5)。第二个图表示该节点所连接的节点的链表。
非常感谢!从 SQL 来到这里,并没有得到链表的东西。【参考方案6】:
如果您正在研究 C++ 中的图形分析,可能首先要开始的是 boost graph library,它实现了包括 BFS 在内的多种算法。
Boost Graph Library Docs编辑
上一个关于 SO 的问题可能会有所帮助:
how-to-create-a-c-boost-undirected-graph-and-traverse-it-in-depth-first-search
【讨论】:
谢谢你我会检查这个库 +1 用于提升图。这是要走的路(当然,如果是出于教育目的,则除外)【参考方案7】:最好用例子来回答。
以Floyd-Warshall 为例。我们必须使用邻接矩阵,否则算法会越来越慢。
或者如果它是一个有 30,000 个顶点的密集图呢?然后邻接矩阵可能有意义,因为您将存储每对顶点 1 位,而不是每条边 16 位(邻接列表所需的最小值):即 107 MB,而不是 1.7 GB。
但对于 DFS、BFS(以及使用它的算法,如 Edmonds-Karp)、优先级优先搜索(Dijkstra、Prim、A*)等算法,邻接表与矩阵一样好。好吧,当图形密集时,矩阵可能有轻微的边缘,但只有一个不起眼的常数因子。 (多少?这是实验的问题。)
【讨论】:
对于 DFS 和 BFS 等算法,如果使用矩阵,则每次要查找相邻节点时都需要检查整行,而相邻列表中已经有相邻节点。在这些情况下,您为什么认为an adjacency list is as good as a matrix
?
@realUser404 确切地说,扫描整个矩阵行是一个 O(n) 操作。当您需要遍历所有传出边时,邻接表更适合稀疏图,它们可以在 O(d) 中完成(d:节点的度数)。但是,由于顺序访问,矩阵比邻接列表具有更好的缓存性能,因此对于有些密集的图,扫描矩阵可能更有意义。【参考方案8】:
添加到keyser5053关于内存使用的答案。
对于任何有向图,邻接矩阵(每条边 1 位)消耗 n^2 * (1)
位内存。
对于complete graph,邻接列表(带有 64 位指针)会消耗n * (n * 64)
位内存,不包括列表开销。
对于不完整的图,邻接表会消耗0
位内存,不包括列表开销。
对于邻接列表,您可以使用以下公式确定在邻接矩阵最适合内存之前的最大边数 (e
)。
edges = n^2 / s
确定最大边数,其中s
是平台的指针大小。
如果您的图形是动态更新的,您可以通过平均边数(每个节点)n / s
来保持这种效率。
一些带有 64 位指针和动态图的示例(动态图在更改后有效地更新问题的解决方案,而不是每次更改后都从头开始重新计算。)
对于有向图,其中n
为 300,使用邻接表的每个节点的最佳边数为:
= 300 / 64
= 4
如果我们将其代入 keyser5053 的公式 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
。【参考方案9】:
根据邻接矩阵的实现,应该更早地知道图形的“n”以实现有效的实现。如果图形过于动态,需要时不时地扩展矩阵,这也可以算作不利因素?
【讨论】:
【参考方案10】:如果您使用哈希表而不是邻接矩阵或列表,您将获得更好或相同的 big-O 运行时间和所有操作空间(检查边是 O(1)
,获取所有相邻边是O(degree)
等)。
在运行时和空间方面都有一些常数因子开销(哈希表不如链表或数组查找快,并且需要相当多的额外空间来减少冲突)。
【讨论】:
【参考方案11】:我只是要谈谈克服常规邻接表表示的权衡,因为其他答案已经涵盖了这些方面。
利用 Dictionary 和 HashSet 数据,可以在摊销常数时间内用 EdgeExists 查询表示邻接表中的图结构。这个想法是将顶点保存在字典中,并且对于每个顶点,我们保留一个哈希集,以引用与它有边的其他顶点。
这个实现中的一个小折衷是它将具有 O(V + 2E) 的空间复杂度,而不是像常规邻接表中那样的 O(V + E),因为这里边表示两次(因为每个顶点都有它的自己的哈希边集)。但是像 AddVertex、AddEdge、RemoveEdge 这样的操作可以用这个实现在 O(1) 的摊销时间内完成,除了 RemoveVertex ,这将像在具有数组索引查找字典的邻接矩阵中一样被 O(V) 摊销。这意味着除了实现简单之外,邻接矩阵没有任何特定优势。我们可以在这个邻接表实现中以几乎相同的性能在稀疏图上节省空间。
请查看 Github C# 存储库中的以下实现以了解详细信息。请注意,对于加权图,它使用嵌套字典而不是字典哈希集组合,以适应权重值。同样,对于有向图,输入和输出边也有单独的哈希集。
Advanced-Algorithms
注意:我相信使用延迟删除我们可以将 RemoveVertex 操作进一步优化到 O(1) 摊销,即使我没有测试过这个想法。例如,删除时只需在字典中将顶点标记为已删除,然后在其他操作中懒惰地清除孤立的边。
【讨论】:
对于邻接矩阵,删除顶点需要 O(V^2) 而不是 O(V) 是的。但是如果您使用字典来跟踪数组索引,那么它将下降到 O(V)。看看这个RemoveVertex 实现。以上是关于对于 C++ 中的图问题,邻接列表或邻接矩阵哪个更好?的主要内容,如果未能解决你的问题,请参考以下文章