C ++中的graphics问题有更好的邻接表或邻接matrix吗?

C ++中的graphics问题有哪些更好的邻接表或邻接matrix? 每个的优点和缺点是什么?

这取决于问题。

邻接matrix使用O(n * n)存储器。 它有快速查找来检查存在或不存在特定边缘,但在所有边缘上迭代都很慢。

邻接列表使用与数字边缘成比例的内存,如果邻接matrix稀疏,则可以节省大量内存。 遍历所有边是快速的,但是发现存在或缺失特定边比在matrix中稍慢。

这个答案不仅适用于C ++,因为所有提到的内容都是关于数据结构本身的,不pipe语言如何。 而且,我的答案是假定你知道邻接表和matrix的基本结构。

记忆

如果记忆是你最关心的问题,你可以按照这个公式来做一个简单的循环图:

邻接matrix占据n 2/8字节空间(每个条目一位)。

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

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

d> 1/64时, 8e> n 2/8

所以这些数字(仍然是32比特)的断点落在1/64 。 如果密度(e / n 2 )大于1/64,那么如果你想节省内存, matrix是最好的。

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

注意 :通过使用一个散列表,可以提高邻接matrix的空间效率,其中的键是顶点对(只有无向)。

迭代和查找

邻接表是仅表示现有边的紧凑方式。 然而,这是以可能慢速查找特定边缘为代价的。 由于每个列表只要顶点的程度,最坏情况下检查特定边的查找时间可以变成O(n),如果列表是无序的。 然而,查找顶点的邻居变得微不足道,对于稀疏或小图,遍历邻接列表的代价可能可以忽略不计。

另一方面,邻接matrix使用更多的空间来提供恒定的查找时间。 由于每个可能的条目都存在,所以可以使用索引在恒定时间内检查是否存在边。 但是,邻居查找需要O(n),因为您需要检查所有可能的邻居。 显而易见的空间缺点是稀疏graphics添加了很多填充。 有关这方面的更多信息,请参阅上面的内存讨论。

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

好的,我已经在图表上编制了基本操作的时空复杂性。
下面的图片应该是不言自明的。
注意当我们期望图是密集的时候,邻接matrix是可取的,当我们期望图是稀疏的时候,邻接matrix是如何是可取的。
我做了一些假设。 问我是否需要澄清复杂性(时间或空间)。 (例如,对于一个稀疏图,我已经把En看作是一个小常量,因为我认为添加一个新的顶点只会添加几条边,因为我们期望即使在添加之后图也保持稀疏顶点。)

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

在这里输入图像说明

这取决于你在找什么。

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

另一方面,用邻接表来检查一个给定的边是否在一个图中是比较困难的,因为你必须search适当的列表来find边,但是它们更节省空间。

一般而言,邻接表是图的大多数应用的正确的数据结构。

如果你正在用C ++来查看graphics分析的话,那么首先要做的就是boost图库 ,它实现了一些algorithm,包括BFS。

  • 提升图表库文档

编辑

上一个关于SO的问题可能会有所帮助:

如何创buildac-boost-undirected-graph-and-traverse-it-in-depth-first-searc h

这是最好的例子回答。

以弗洛伊德 – 沃沙尔为例。 我们必须使用一个邻接matrix,否则algorithm将会渐近地变慢。

或者,如果它是30,000个顶点上的密集图? 那么邻接matrix可能是有意义的,因为您将每对存储1位顶点,而不是每边16位(您需要用于邻接列表的最小值):这是107 MB,而不是1.7 GB。

但对于像DFS,BFS(以及像Edmonds-Karp那样使用它的algorithm),优先优先search(Dijkstra,Prim,A *)等algorithm,邻接列表和matrix一样好。 那么matrix可能有一个轻微的边缘时,graphics是密集的,但只有一个不起眼的常数因素。 (多less钱?这是一个试验的问题。)

要添加到keyser5053有关内存使用情况的答案。

对于任何有向图,邻接matrix(每边1位)消耗n^2 * (1)位的内存。

对于完整的图 ,邻接列表(具有64位指针)消耗n * (n * 64)位的存储器,不包括列表开销。

对于不完整的图,邻接列表消耗0位内存,不包括列表开销。


对于邻接列表,可以使用以下公式来确定邻接matrix对于内存最优之前的最大边数( e )。

edges = n^2 / s来确定最大边数,其中s是平台的指针大小。

如果graphics是dynamic更新的,则可以使用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位指针来说是64*2 )。

根据邻接matrix的实现情况,应该早些知道图的“n”以实现有效的实现。 如果图表过于dynamic,并且需要matrix的扩展,那么也可以将其视为一个缺点?

如果使用散列表而不是邻接matrix或列表,那么对于所有操作,您将获得更好或相同的大运行时间和空间(检查边是O(1) ,获得所有相邻边是O(degree)等)。

虽然在运行时和空间上都有一些固定的系数开销(散列表不像链表或数组查找那么快,并且需要额外的空间来减less冲突)。

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

示例图
在这里输入图像说明

邻接matrix:我们正在创build一个有n个行和列的matrix,所以在内存中它将占用与n 2成正比的空间。 检查名为uv的两个节点之间是否有边,将花费Θ(1)次。 例如检查(1,2)是否在代码中看起来如下所示:

 if(matrix[1][2] == 1) 

如果你想识别所有的边,你必须迭代matrix,这将需要两个嵌套循环,它将需要Θ(n 2 )。 (你可能只是使用matrix的上三angular部分来确定所有的边,但它会再次Θ(n 2 ))

邻接列表:我们正在创build一个列表,每个节点也指向另一个列表。 您的列表将包含n个元素,每个元素将指向一个列表,其中的项目数等于此节点的邻居数量(查看图像以获得更好的可视化效果)。 所以它会占用与n + m成正比的内存空间。 检查(u,v)是否是一个边将需要O(deg(u))时间,其中deg(u)等于u的邻居数。 因为最多只能遍历u所指向的列表。 识别所有边缘将需要Θ(n + m)。

示例图的邻接列表

在这里输入图像说明
你应该根据你的需要做出select。 因为我的名誉,我不能把matrix的形象,对不起

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

通过利用DictionaryHashSet数据结构,可以在分段的常量时间内用EdgeExists查询表示邻接列表中的图。 其思想是将顶点保存在一个字典中,并且对于每个顶点我们保持一个散列集引用其边缘的其他顶点。

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

有关详细信息,请参阅下面的C#实现。 请注意,对于加权图,我使用嵌套字典而不是字典 – 哈希集合组合,以适应加权值。 类似地,对于有向图,存在用于入边和出边的单独的散列集。

  • 图( 实施 | testing )
  • 加权图( 实施 | testing )
  • DiGraph( 实现 | testing )
  • 加权图( 实施 | testing )

注意:我相信使用懒惰删除我们可以进一步优化RemoveVertex操作为O(1) 分期付款 ,即使我没有testing过这个想法。 例如,在删除时只需将顶点标记为在字典中被删除,然后在其他操作中懒散地清除孤立的边缘。