【问题标题】:How is it possible that "RemoveAll" in LINQ is much faster than iteration?LINQ 中的“RemoveAll”怎么可能比迭代快得多?
【发布时间】:2015-09-02 11:08:06
【问题描述】:

以下代码:

List<Interval> intervals = new List<Interval>();
List<int> points = new List<int>();

//Initialization of the two lists
// [...]

foreach (var point in points)
{
    intervals.RemoveAll (x => x.Intersects (point));
}

当列表大小约为 10000 时,至少比这快 100 倍:

List<Interval> intervals = new List<Interval>();
List<int> points = new List<int>();

//Initialization of the two lists
// [...]

foreach (var point in points)
{
    for (int i = 0; i < intervals.Count;)
    {
        if (intervals[i].Intersects(point))
        {
            intervals.Remove(intervals[i]);
        }
        else
        {
            i++;
        }
    }
}

这怎么可能? “RemoveAll”在幕后执行了什么?根据MSDN,“RemoveAll”执行线性搜索,因此在 O(n) 中。所以我希望两者的性能相似。

当用“RemoveAt”替换“Remove”时,迭代速度要快得多,与“RemoveAll”相当。但是 both "Remove" 和 "RemoveAt" 都有 O(n) 复杂度,为什么它们之间的性能差异如此之大呢?难道仅仅是因为“Remove (item)”将列表元素与“item”进行比较,而“RemoveAt”没有进行任何比较?

【问题讨论】:

  • RemoveAll 不使用 LINQ,它是 List&lt;T&gt; 上的标准方法。这是由RemoveAll 修改集合就地这一事实注意到的——LINQ 不修改集合。
  • @Brainless,你可以加快第二个代码示例,如果使用intervals.RemoveAt(i); 而不是intervals.Remove (intervals[i]);,我想。
  • RemoveAllRemove 都是O(n),所以很容易相信有一个额外的for 循环的执行速度会慢n 倍。
  • @Brainless RemoveAt 不执行任何比较,它只是删除指定位置的项目。另一方面,删除必须搜索与其参数相等的项目。
  • @Brainless: imo 最好的方法(在可读性和性能方面)是RemoveAll 和 LINQ 的组合:intervals.RemoveAll(i =&gt; points.Any(p =&gt; i.Intersects(p)));

标签: c# performance linq


【解决方案1】:

如果您从List&lt;T&gt; 中删除一个项目,它后面的所有项目都将移回一个位置。所以如果你删除 n 个项目,很多项目将被移动 n 次。
RemoveAll 只会移动一次,你可以在 List&lt;T&gt; 的源代码中看到:source

另一件事是Remove(T item) 将在整个列表中搜索该项目,所以这是另一个 n 操作。

与您的问题无关,但我还是想指出:
如果您使用 for 循环从 List 中删除项目,则从末尾开始会容易得多:

for (int i = intervals.Count - 1; i >= 0; i--)
{
    if (intervals[i].Intersects(point))
    {
        intervals.RemoveAt(i);
    }
}

这样,你就不需要那个丑陋的 else 子句了

【讨论】:

  • @Brainless 因为如果从头开始,您不必在删除时“补偿”i,并去掉 else 子句,这使得代码更具可读性。顺便说一句,你原来的循环是错误的......它会在不删除(不是一个)时向前移动 2 个插槽:for 语句将增加一个,而您的 else 将增加另一个
  • @Brainless。如果您删除比如说 list[3],则索引 4、5 等处的所有项目都将移回一个位置。但那是你已经去过的领域。它不会与项目 0,1 和 2 混淆。
  • @Brainless 因为如果您删除项目,您最终不会跳过下一个元素(因为索引应该保持不变)。如您所见,向后迭代时,您不需要在循环体内单独执行i++/i--
  • @Brainless 因为从下到上移动不需要在删除项目时跳过一个点的计数器增加..
  • 它还具有显着提高速度的潜力。考虑从列表中删除所有元素的极端情况:如果从后面开始,则剩余元素没有“移动”,所以它是 O(n)。如果从前面开始,则为 O(n²),因为每次删除都需要移动列表的其余部分
【解决方案2】:

RemoveAll 可以在O(n) 中通过检查n 元素的条件并最多移动n 元素来完成。

您的循环是O(n^2),因为每个Remove 最多需要检查n 元素。而且即使你把它改成RemoveAt,它仍然需要向上移动到n元素。

这可能是最快的解决方案: intervals.RemoveAll(x =&gt; points.Any(x.Intersects));

【讨论】:

    【解决方案3】:

    List 是一个数组,从数组中删除一个元素需要将要删除的元素之后的所有元素移动到上一个索引,因此 a[i] 被移动到 a[i-1]

    重复执行此操作需要多次移动,即使更多元素符合删除条件。 RemoveAll 可以通过在遍历列表并找到更多符合删除条件的元素时一次将元素移动超过 1 个索引来优化这一点。

    【讨论】:

      【解决方案4】:

      不同的是Remove本身是一个O(n),所以你得到O(n^2)。

      for 替换为新集合和分配。

      items = items.Where(i => ...).ToList();
      

      此方法与 RemoveAll 具有相同的算法时间复杂度,但使用额外的 O(n) 内存。

      【讨论】:

      猜你喜欢
      • 2016-02-16
      • 2015-06-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-20
      相关资源
      最近更新 更多