【问题标题】:Resizable Array and Amortized Runtime可调整大小的数组和摊销运行时
【发布时间】:2017-04-30 12:39:34
【问题描述】:

“因此,插入 N 个元素总共需要 O(N) 的工作量。每个插入平均为 O(1),尽管在最坏的情况下某些插入需要 O(N) 时间。”这句话可以在破解编码采访中找到。我有点理解这个说法,尽管它的一些小事让我很恼火。在好的日子里,摊销的插入是 O(1)。这仅仅意味着当可调整大小的数组不需要调整大小时,插入一些东西只是 O(1)。这很清楚。但是,在糟糕的一天,当我们用完空间时,我们需要 O(N) 来插入那个额外的元素。但是,我不同意上面的说法,因为它说在最坏的情况下某些插入需要 O(N)。不应该说,在最坏的情况下,一次插入需要 O(N)。

为了更清楚地说明这一点,这是我所说的一个例子。

假设我们有一个可调整大小的数组,但我们的数组大小为 4。现在假设插入 5 个元素,我们将有 O(1)、O(1)、O(1)、O(1),但是然后一旦我们到达最后一个元素,我们将不得不将所有这些人复制到一个新数组中,这个过程会给我们 O(N) 的成本。

谁能帮我澄清一下。我不明白为什么这本书说当我们在旧数组中运行空间时只需要将所有元素复制到一个新数组中时,某些情况会花费 O(N)。

【问题讨论】:

  • 每次你需要调整数组的大小都是 O(N)。
  • Constant Amortized Time看到这个答案
  • 我不明白引用和你的理解有什么区别......这本书不是说完全相同的事情:在最坏的情况下插入可能是 O(N)
  • 这本书说在 average 的情况下插入 N 个元素是 O(N)。或者换句话说,每个元素是 O(1)平均。要插入 5 个元素,您需要 O(1)、O(1)、O(1)、O(1)、O(6),但这与每个元素都是 O(2) 相同。实际上,无论你插入多少,它的平均值总是小于 O(3),这与 O(1) 相同。 (Big-O-notation 不是这样工作的,但它可能有助于直觉)。

标签: c++ arrays algorithm


【解决方案1】:

考虑在一个可调整大小的数组中进行 N 次插入的成本为(我将在这里使用波浪符号):

  • N 次插入的成本 = 新元素插入的成本 + 调整大小的成本

新元素插入的成本

这只是在数组中插入新元素的成本乘以插入新元素的次数,即 N:

  • 新元素插入成本 = 1 * N

调整大小的成本

假设您有一个 64 单元格的数组。然后,这意味着该数组已被调整大小:

  • 数组大小 = 1 -> 2 -> 4 -> 8 -> 16 -> 32 -> 64
  • #resize 完成 = 6

64 元胞数组已调整大小 6 次,即调整大小发生 log2(64) 次。 总的来说,现在我们知道对于 N 次插入,我们会执行 log2(N) 的 resize 操作。

但是我们在每次调整大小时做了什么?我们将在新调整大小的数组中复制数组中已经存在的元素:在调整大小“i”时,我们将复制多少个元素? 2^i。与前面的例子:

  • 调整数字 1 = 1 -> 2:复制 1 个元素
  • 调整数字 2 = 2 -> 4:复制 2 个元素
  • 调整数字 3 = 4 -> 8:复制 4 个元素
  • ......
  • 调整数字 6 = 32 -> 64:复制 32 个元素

所以:

  • 调整大小的成本 = 总和(从 i=1 到 log2(N))2^i = 2(N-1)

结论

  • N 次插入的成本 = 新元素插入的成本 + 调整大小的成本 = N + 2(N-1) ~ 3N

【讨论】:

    【解决方案2】:

    让我们将所有插入拆分为耗时与元素数量成正比的“重”插入和只需要固定时间即可完成的“轻”插入。然后,如果您从一个空列表开始并继续追加和追加,您将主要进行少量插入,但有时您会进行大量插入。

    为简单起见,假设每次空间不足时将数组的大小加倍,并从大小为 4 的数组开始。然后第一次调整大小必须移动四个元素,第二次将移动 8,然后 16,然后 32,然后 64,然后 128,然后 256,等等。

    请注意,这不仅仅是一个需要很长时间的附加 - 粗略地说,如果你有 n 总插入,那么大约 log n 其中会很重(它们之间的时间不断增长),而另一个大约 n -其中 log n 会很轻。

    【讨论】:

      【解决方案3】:

      我想你最好这样理解上面的陈述。

      起初。数组大小仅为 1. 并将其插入一个元素。现在阵列已满!您必须将其大小调整为前一个的 2 倍。

      接下来,数组大小为 2。让这个过程继续进行。您可以很容易地注意到,您必须调整数组大小的时刻是 1, 2, 4, 8, 16, 32, ... , 2^r。

      我会给你问题。

      1. 您需要调整数组大小的时刻有多少次?
      2. 在 N(N>=0) 步之前的总成本是多少?

      第一个答案是 floor(lgN) 次。我想你可以很容易地弄清楚。如果你找到第一个答案,计算这N个步骤的总成本,即第二个答案是很容易的。(我不知道我如何表达数学符号:

      1 + 2 + 4 + 8 + 16 + ... + 2^(floor(lgN)) = 2^(floor(lgN)+1) - 1 => O(n)

      要得到每一步的平均成本,将总成本除以 N => O(1)

      我认为参考文献提到的最坏情况是需要调整数组的大小。这种重新调整的成本与数组中的元素数量成正比,O(N)

      【讨论】:

        【解决方案4】:

        std::vector 将在内存中将其所有元素彼此相邻以实现快速迭代 - 这就是它的特性,它就是这样做的,这就是为什么每个人都喜欢 std::vector。通常,它会保留比包含在其中的元素数量更多的空间,或者该内存恰好是空闲的,因此当您将新元素添加到 vector 的末尾时,vector 会很快推动你的新元素在那里。

        但是,当vector 没有空间展开时,它不能只将现有元素留在原处并在其他地方开始一个新列表——所有元素在内存中必须彼此相邻!所以它必须找到一块足够大的空闲内存块来容纳所有元素加上你的新元素,然后将所有现有元素复制到那里,然后将你的新元素添加到末尾。

        如果添加 1 个元素需要 1 个单位时间,广义上讲,移动 N 个元素需要 N 个单位时间。如果您添加一个新元素,那就是一个操作。如果添加一个新元素并且需要重新定位 1024 个现有元素,那就是 1025 次操作。所以重新分配花费的时间与向量的大小成正比,因此O(N)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-06-18
          • 2014-02-20
          • 1970-01-01
          • 2011-03-29
          • 2014-09-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多