【问题标题】:Graph implementations and initialization of adjacency matrices邻接矩阵的图实现和初始化
【发布时间】:2017-07-14 04:13:26
【问题描述】:

图通常使用邻接矩阵来表示。各种来源表明可以避免初始化成本为 |V^2| (V 是顶点的数量)但我可能还没有弄清楚如何。

在 Java 中,只需声明矩阵,例如boolean adj [][],运行时将自动使用 false 初始化数组,这将花费 O(V^2) 成本(数组的维度)。 我误解了吗?是否可以避免邻接矩阵初始化的二次成本,或者这只是依赖于实现语言的理论?

【问题讨论】:

    标签: java algorithm optimization data-structures graph


    【解决方案1】:

    这可以通过使用邻接矩阵的稀疏矩阵表示来实现,其中只分配“1”的位置,而不是矩阵的每个元素(可能包括大量的零)。你可能会发现this thread 也很有用

    【讨论】:

    • 邻接矩阵的稀疏表示只是一个邻接列表。
    【解决方案2】:

    矩阵值的默认初始化实际上是一个特性。如果不是默认初始化,难道你还需要自己初始化每个字段,以便知道它的值是什么?

    邻接矩阵有这个缺点:它们在内存效率方面很差(它们需要 O(n2) 个内存单元),并且正如您所说,它们的初始化速度较慢。然而,初始化从未被认为是最大的问题。相信我,内存分配要慢得多,所需的内存比初始化时间要多得多。


    在许多情况下,人们更喜欢使用邻接列表,而不是矩阵。这样的列表需要O(m) 内存,其中m 是图中的边数。这更有效,尤其是对于稀疏图。这个图表示比邻接矩阵差的唯一操作是查询is there edge between vertices i and j。矩阵在O(1) 时间内回答,列表肯定会慢很多。

    然而,许多典型的图算法(如Dijkstra、Bellman-Ford、Prim、Tarjan、BFS 和DFS)只需要迭代给定顶点的所有邻居。如果您使用邻接表而不是矩阵,所有这些算法都会受益匪浅。

    【讨论】:

    • 所以您是说没有某种“技巧”可以在小于二次的时间内初始化数组?即使在理论上?
    • @user384706 不,不存在,理论上也不存在。但是,有些函数使用了高度优化的操作,例如 c++ 的 memset 函数。
    • 嗯,它们只是在某些情况下的内存效率方面不好。我知道你想说什么,但重要的是要知道矩阵可以提高内存效率。请参阅this thread 了解更多信息
    • 如果算法存在的话,并行算法通常比邻接列表更容易并行化,对于简单的二维数组。请注意,您列表中列出的大多数算法都很难并行化工作效率。啊,所有的利弊。
    【解决方案3】:

    此线程中有很多混乱和错误信息。事实上,有一种方法可以避免邻接矩阵(以及一般的任何数组)的初始化成本。但是,无法将该方法与 Java 原语一起使用,因为它们在后台使用默认值进行了初始化。

    假设您可以创建一个未自动初始化的数组data[0..n]。首先,它充满了以前记忆中的垃圾。如果我们不想花费 O(n) 时间覆盖它,我们需要一些方法来区分我们添加的好数据和垃圾数据。

    “诀窍”是使用辅助堆栈来跟踪包含良好数据的单元格。第一次写入data[i] 时,我们将索引i 添加到堆栈中。由于堆栈只会随着我们添加而增长,因此它永远不会包含我们需要担心的任何垃圾。

    现在每当我们访问data[k] 时,我们都可以通过扫描堆栈中的k 来检查它是否为垃圾。但是每次读取都需要 O(n) 时间,首先要打败数组的点!

    为了解决这个问题,我们创建了另一个辅助数组stack_check[0..n],它也开始充满垃圾。该数组包含指向堆栈中元素的指针。现在,当我们第一次写入data[i] 时,我们将i 压入堆栈并设置stack_check[i] 指向新的堆栈元素。

    如果data[k] 是好的数据,那么stack_check[k] 指向一个包含k 的堆栈元素。如果data[k] 是垃圾,那么stack_check[k] 的垃圾值要么指向​​堆栈外,要么指向除k 之外的某个堆栈元素(因为从未将k 放入堆栈)。检查这个属性只需要 O(1) 时间,所以我们的数组访问仍然很快。

    综合起来,我们可以在 O(1) 时间内创建我们的数组和辅助结构,让它们充​​满垃圾。在每次读取和写入时,我们使用我们的帮助器在 O(1) 时间内检查给定单元格是否包含垃圾。如果我们正在写垃圾,我们会更新我们的辅助结构以将单元格标记为有效数据。如果我们阅读垃圾,我们可以用任何适合给定问题的方式来处理它。例如,我们可以返回一个像 0 这样的默认值(现在你甚至不能说我们没有初始化它!)或者可能抛出一个异常。

    【讨论】:

      【解决方案4】:

      我会详细说明 A_A 的答案。他推荐使用稀疏矩阵,这基本上意味着您要重新维护邻接列表。

      您有两个理由使用矩阵 - 如果您根本不关心性能并且喜欢它提供的简单代码,或者如果您确实关心性能但您的矩阵将相对完整(假设在为了这篇文章,至少 20% 已满)。

      您显然确实关心性能。如果您的矩阵将是相对空的,那么它的初始化开销可能是有意义的,并且您最好使用邻接列表。如果它会很满,初始化变得可以忽略不计 - 您需要填充矩阵中的正确单元格(这将花费更多时间而不是初始化它),并且您需要处理它们(再次,这将花费更多时间初始化它)。

      【讨论】:

        猜你喜欢
        • 2017-07-24
        • 2017-10-13
        • 2014-05-04
        • 1970-01-01
        • 2022-06-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多