【问题标题】:What is the fastest way to remove a single item from List<T> with no duplicates where order matters?从 List<T> 中删除单个项目的最快方法是什么?
【发布时间】:2019-04-20 06:58:06
【问题描述】:

我有一个没有重复的项目列表,其中订单很重要。

我需要定期从该列表中删除一个项目。

一个实际的例子是列表绑定 UI 控件,例如允许删除的 DataGridView

用户希望以特定顺序查看项目,因此删除后剩余的项目应保留其顺序。

这里有一些示例代码似乎表明删除列表项是O(n),因为

(1) 必须迭代列表以通过匹配条件找到要删除的项目

(2)List.RemoveAt 方法必须将剩余的项打乱

public class Item
{
  public string Name {get; set;}
  public string Description {get; set;}
}

public List<Item> Items = new List<Item>();

public void AddItem(string name, string description)
{
  Items.Add(new Item {Name=name, Description=description});
}

public void RemoveItem(string name)
{
  for (int i=0; i<Items.Count; i++)
  {
    if (Items[i].Name == name)
    {
      Items.RemoveAt(i);
      break;
    }
  }
}

有没有更快的方法?

LinkedList&lt;T&gt; 不起作用,因为它不能被索引以进行滚动和分页。同样对于高项目计数LinkedList&lt;T&gt; 由于缓存未命中,项目删除实际上更慢。

【问题讨论】:

  • 你不能从你正在迭代的集合中删除项目,你会得到一个异常
  • 您是否需要能够使用Items[someNumber] 才能访问该列表?如果是这样,切换到 LinkedList&lt;T&gt; 可以让您在删除列表中的任意项目时获得更好的性能。
  • 您的删除意味着搜索和删除。数组的搜索复杂度(列表是某种)是 O(n)。如果您需要较低的复杂性,则需要另一种数据结构(例如,保留项目名称以快速访问项目的字典)。
  • 我需要保留原来的订单项目被添加到列表中。我想我可以使用SortedDictionary&lt;int,Item&gt;,但这似乎有点矫枉过正并且会增加开销。
  • 为什么LinkedList&lt;T&gt; 会提高性能?看来您仍然需要进行线性扫描,所以它是 O(n)

标签: c# list indexof


【解决方案1】:

RemoveAt 需要 O(n) 时间。因为它必须将项目向左移动,从索引i 开始到n (Items.Count)。 IndexOf 类似。

这是您的代码的渐近估计:

public void RemoveItem(string name)
{
  foreach (var item in Items) //n times
  {
    if (item.Name == name)
    {
      Items.RemoveAt(Items.IndexOf(position)); // O(n) + O(n) => O(n)
    }
  }

  // => O(n * n)
}

它在 O(n ^ 2) 时间内运行。

这段代码怎么样?

for (int i = Items.Count - 1; i >= 0; i--) // n times
{
    if (Items[i].Name == name)
    {
        Items.RemoveAt(i); //O(n)
    }
}

所以这也运行在 O(n ^ 2)

但是这段代码:

Items.RemoveAll(i => item.Name == name);

在 O(n) 时间内运行。您应该看到此方法的实现以供证明。 RemoveAll 对列表中的项目进行一次迭代,如果满足条件,它也会在迭代时开始移位。每一步的这种转变是一个单一的分配,即 O(1)。因此整个方法在 O(n) 时间内运行,这比您的实现要快得多。

更新: 该问题现在在删除行之后包含一个break;

在该代码中,查找项目将在找到后停止并开始删除,这将花费 O(n) + O(n) => O(n)。

所以有问题的代码和RemoveAll之间不会有任何渐近差异。

所以它不是更快也不能,但 RemoveAll 仍然是一个更干净的代码恕我直言。

【讨论】:

  • O(n) + O(n) 是搜索和删除不是 O(n*n)。仍然是 O(n)。
  • 是的。 n * O(n) 是 O(n ^ 2)。 n 是 for 循环,O(n) 是那一行 :)
  • 一旦找到元素,它只会被删除一次。那里没有n 删除程序。这就是为什么你搜索一次是 O(n) 而你删除一次又是 O(n),总共你有 O(n) + O(n)。
  • 您的困惑可能是因为您错过了原始问题中的 break 运算符。
  • 好吧 break; 删除后只添加了 2 分钟!。我已经复制并粘贴了原始问题:)
【解决方案2】:

您不需要循环或自定义方法。试试这个:

Items.RemoveAll(i => item.Name == name);

编辑
正如@Gian Paolo 评论的那样,对于List 数据结构,在最好的情况下,它的复杂度为 O(n)。如果您想要更好的性能,可以考虑检查其他数据结构,包括LinkedListDictionaryHashSet

【讨论】:

  • 是的,是的,因为这是有效的,而您的解决方案不起作用。 ;o)
  • 即使您的解决方案有效,它仍然会更快,因为您额外的IndexOfcall 会占用大量时间。
  • @BaltoStar 这是删除符合条件的项目的一种非常干净的方法。可能你可以更快地实现一些东西,但如果你真的需要一些明显的差异,这意味着你应该寻找不同的数据结构。 List 总是需要扫描其中的每个元素。没有任何算法能比这快得多,你总是会有 O(n) 的复杂度。因此,如果您决定使用 List,请选择最干净的代码
  • 同意清洁剂(我知道带有谓词的删除扩展方法),但我的问题的精神是性能
  • @BaltoStar, RemoveAll 实际上是List&lt;T&gt; 的一种方法,我认为是相当优化的。你可以查看source code
【解决方案3】:

一种方法是对您的项目使用传统的循环,如果名称匹配,则删除,

for (int i = Items.Count - 1; i >= 0; i--)
{
    if (Items[i].Name == name)
    {
        Items.RemoveAt(i);
        break;
    }
}

【讨论】:

  • 我想迭代的方向并不重要?找到匹配项的索引是随机的,因此从 0 或 count-1 开始,底层数组中的 avg 偏移应该相同
  • @BaltoStar,是的,count-1 仅用于迭代目的。不确定,但我认为你的列表会保留顺序:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-28
  • 2011-10-21
  • 1970-01-01
  • 2012-06-25
相关资源
最近更新 更多