【问题标题】:Can I have a faster nested loop just lowering the algorithm complexity?我可以有一个更快的嵌套循环来降低算法复杂度吗?
【发布时间】:2020-05-16 16:42:26
【问题描述】:

我有一个新手问题。

假设我有以下简单的嵌套循环,其中mn 不一定相同,但确实是大数字

x = 0;

for (i=0; i<m; i++)
{
   for (j=0; j<n; j++)
   {
      delta = CalculateDelta(i,j);
      x = x + j + i + delta;
   }
}

现在我有了这个:

x = 0;

for (i=0; i<m; i++)
{
   for (j=0; j<n; j++)
   {
      delta = CalculateDelta(i,j);
      x = x + j + i + delta;

      j++;

      delta = CalculateDelta(i,j);
      x = x + j + i + delta;
   }
}

规则:我确实需要遍历循环的所有元素,因为这个 delta 计算。

我的问题是:

1) 第二种算法比第一种算法快,还是一样? 我有这个疑问,因为对我来说,第一个算法的复杂度为 O(m * n),而第二个算法的复杂度为 O(m * n/2)。还是不需要较低的复杂性使其更快?

2) 如果没有Parallel. For 之类的东西,还有其他方法可以加快速度吗?

3) 如果我使用Parallel. For,它真的会更快吗,因为我可能需要对x 变量进行同步锁定?

谢谢!

【问题讨论】:

  • 当然可以并行化。 x 可以计算为 i 和 j 以及所有增量之和的函数。可以与典型的“收集”模式并行执行求和。 (假设 CalculateDelta 是一个纯函数 - 仅取决于它的参数并且没有副作用。)而且,,展开可能对您几乎没有任何作用。事实上,当 n 为奇数时,您的做法只会导致错误。
  • 您并没有降低复杂性。它仍然与 n 成正比。即使您在循环中展开一百万次迭代。它访问循环的次数仍然与 n 成正比,因此复杂度没有改变。
  • 第二个与第一个不同,因为它可能会为等于nj 值计算增量。基本上,如果n 是一个奇数,比如 3,那么第一个循环执行 j=0 和 j=1,然后下一个循环执行 j=2 和 j=3,因为第一个循环会在 j=2 处停止,您可以“修复”在j++ 之后有一个if(j&gt;=n) break; 但这仍然是n 操作。

标签: c# .net time-complexity computation-theory code-complexity


【解决方案1】:

绝对不是,因为时间复杂度大概是由调用 CalculateDelta() 的次数决定的,所以无论您是内联调用、在单个循环中还是在任意数量的循环中进行调用都无关紧要嵌套循环,调用 m*n 次。

现在你有一个错误(这就是我决定在 @Peter-Duniho 已经非常全面地完成之后添加答案的原因)

如果n 是奇怪的,您进行的迭代次数比预期的要多 - 几乎可以肯定得到错误的答案或使您的程序崩溃...

【讨论】:

    【解决方案2】:

    在一篇文章中提出三个问题正在突破“过于宽泛”的界限。但就你而言,这些问题相当简单,所以……

    1) 第二种算法比第一种算法更快,还是一样?我有这个疑问,因为对我来说,第一个算法的复杂度为 O(m * n),而第二个算法的复杂度为 O(m * n/2)。还是不需要较低的复杂性使其更快?

    复杂性会忽略 1/2 等系数。所以O(m * n)O(m * n/2) 之间没有区别。后者应该已经缩减为O(m * n),显然和前者是一样的。

    无论如何,第二个并不是真正的O(m * n/2),因为您并没有真正删除工作。您只是部分展开了循环。这些无意义的变换是我们首先忽略大 O 表示法中的系数的原因之一。在不真正改变实际计算工作的情况下摆弄系数太容易了。

    2) 如果没有 Parallel.For 之类的东西,有没有其他方法可以加快速度?

    这绝对是一个太宽泛的问题。 “任何其他方式”?大概。但是您没有提供足够的上下文。

    我可以在您发布的代码中看到的唯一明显的潜在改进是您正在重复计算 j + i,而您可以观察到整个计算的该组件在每次迭代时增加 1,因此您可以保持一个单独的递增变量,而不是每次都添加 ij。但是a)尚不清楚做出改变是否会加速任何事情(是否会加速,很大程度上取决于CPU自己的优化逻辑中的细节),并且b)如果这样做可靠,那么一个好的优化JIT编译器可能会将为您进行代码转换。

    但除此之外,CalculateDelta() 方法在这里是完全未知的。它可能是编译器为您内联的简单联机,也可能是支配整个循环的一些巨大计算。

    我们中的任何人都无法告诉您是否有“任何其他方式”可以使循环更快。就此而言,甚至不清楚您所做的更改是否会使循环更快。也许是,也许不是。

    3) 如果我使用 Parallel.For,它真的会更快吗,因为我可能需要对 x 变量进行同步锁定?

    这至少取决于CalculateDelta() 在做什么。如果它足够昂贵,那么x 上的同步可能无关紧要。更大的问题是x 的每个计算都依赖于前一个。并行计算是不可能的,因为它本质上是串行计算。

    可以做的是并行计算所有增量,因为它们不依赖于x(至少,它们不在您发布的代码中)。和的另一个元素是常数(i + j 表示已知的 mn),所以最后它只是增量和常数的总和。同样,这是否值得做在某种程度上取决于CalculateDelta() 的成本。该方法的成本越低,您就越不可能通过并行执行看到任何改进。

    【讨论】:

      【解决方案3】:

      一个有利的变换是提取算术和使用双算术级数公式ij 项的贡献。这节省了相当多的工作,将那部分计算的复杂度从 O(m*n) 降低到 O(1)。

      x = 0;
      
      for (i=0; i<m; i++)
      {
         for (j=0; j<n; j++)
         {
            delta = CalculateDelta(i,j);
            x = x + j + i + delta;
         }
      }
      

      可以变成

      x = n * m * (n + m - 2) / 2;
      
      for (i=0; i<m; i++)
      {
         for (j=0; j<n; j++)
         {
            x += CalculateDelta(i,j);
         }
      }
      

      进一步优化完全取决于CalculateDelta 做了什么,你没有透露。如果它有副作用,那就是一个问题。但如果它是一个纯函数(其结果仅取决于输入 ij),那么它很有可能也可以直接计算。

      【讨论】:

      • 这假定CalculateDelta 无法直接访问x,这也未在问题中披露。
      【解决方案4】:

      第一个 for() 会将您发送到第二个 for() 第二个 for() 将循环到 jn) 和第二个 mn/2

      【讨论】:

      • @Zartch 这是答案部分。他根本不应该在这里问问题,但我认为他不是。
      • @Rob 我的坏 o_o。我以为我在复习一个问题
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-28
      相关资源
      最近更新 更多