【问题标题】:Most efficient way to remove multiple items from a IList<T>从 IList<T> 中删除多个项目的最有效方法
【发布时间】:2013-08-02 23:23:03
【问题描述】:

IList&lt;T&gt; 对象中删除多个项目的最有效方法是什么。假设我有一个IEnumerable&lt;T&gt;,包含我想要删除的所有项目,其出现顺序与原始列表中的出现顺序相同。

我想到的唯一方法是:

IList<T> items;
IEnumerable<T> itemsToDelete;
...

foreach (var x in itemsToDelete)
{
    items.Remove(x);
}

但我猜它效率不高,因为每次调用 Remove 方法时,它都必须从一开始就遍历列表。

【问题讨论】:

  • 你分析过你的代码吗?您需要提高多少性能?
  • 正如 John Skeet 所说:否决者,愿意发表评论吗?
  • 这是因为我处理了很多列表,而且我将不得不多次这样做。
  • 计算机通常可以非常快速地“多次”执行操作。您确定这里甚至存在性能问题吗?
  • items 合适吗? itemsToDelete 有顺序吗?您是否考虑过是否可以使用ISet 进行快速排除?您是否对创建新列表而不是使用var set = new HashSet&lt;T&gt;(itemsToDelete); var newList = items.Where(i =&gt; !set.Contains(i)).ToList(); 就地编辑列表进行了基准测试?你做过基准测试吗?

标签: c# generics ienumerable ilist


【解决方案1】:

随着要删除的项目数量越来越多,您可能会发现遍历列表并根据“要删除的项目”哈希集检查每个项目的效率更高。像这样的扩展方法可能会有所帮助:

static void RemoveAll<T>(this IList<T> iList, IEnumerable<T> itemsToRemove)
{
    var set = new HashSet<T>(itemsToRemove);

    var list = iList as List<T>;
    if (list == null)
    {
        int i = 0;
        while (i < iList.Count)
        {
            if (set.Contains(iList[i])) iList.RemoveAt(i);
            else i++;
        }
    }
    else
    {
        list.RemoveAll(set.Contains);
    }
}

我使用下面的这个小程序进行了基准测试。 (请注意,如果IList&lt;T&gt; 实际上是List&lt;T&gt;,则它使用优化路径。)

在我的机器上(并使用我的测试数据),这种扩展方法需要 1.5 秒 来执行,而您问题中的代码需要 17 秒。但是,我没有测试过不同大小的数据。我敢肯定,只删除几个项目RemoveAll2 会更快。

static class Program
{
    static void RemoveAll<T>(this IList<T> iList, IEnumerable<T> itemsToRemove)
    {
        var set = new HashSet<T>(itemsToRemove);

        var list = iList as List<T>;
        if (list == null)
        {
            int i = 0;
            while (i < iList.Count)
            {
                if (set.Contains(iList[i])) iList.RemoveAt(i);
                else i++;
            }
        }
        else
        {
            list.RemoveAll(set.Contains);
        }
    }

    static void RemoveAll2<T>(this IList<T> list, IEnumerable<T> itemsToRemove)
    {
        foreach (var item in itemsToRemove)
            list.Remove(item);
    }

    static void Main(string[] args)
    {
        var list = Enumerable.Range(0, 10000).ToList();
        var toRemove = new[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 
                              43,  47,  53,  59,  61,  67,  71,  73,  79,  83,  89,  97, 101,
                             103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167,
                             173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239,
                             241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313,
                             317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397,
                             401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467,
                             479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569,
                             571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643,
                             647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733,
                             739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823,
                             827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911,
                             919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997};
        list.RemoveAll(toRemove); // JIT 
        //list.RemoveAll2(toRemove); // JIT 

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < 10000; i++)
        {
            list.RemoveAll(toRemove);
            //list.RemoveAll2(toRemove);
        }
        sw.Stop();
        Console.WriteLine("Elapsed: {0} ms", sw.ElapsedMilliseconds);
        Console.ReadKey();
    }
}

更新(下面是@KarmaEDV 和 Mark Sowul 的 cmets): 如果您需要使用自定义相等比较器,则扩展方法可能具有采用此类比较器的重载:

public static void RemoveAll<T>(this IList<T> iList, IEnumerable<T> itemsToRemove, IEqualityComparer<T> comparer = null)
{
    var set = new HashSet<T>(itemsToRemove, comparer ?? EqualityComparer<T>.Default);

    if (iList is List<T> list)
    {
        list.RemoveAll(set.Contains);
    }
    else
    {
        int i = iList.Count - 1;
        while (i > -1)
        {
            if (set.Contains(iList[i])) iList.RemoveAt(i);
            else i--;
        }
    }
}

【讨论】:

  • 我建议以相反的顺序遍历列表。如果从列表中删除第一项,则必须移动所有剩余的项。从后往前移除更有效。
  • 这个解决方案很棒,但是如果 List 覆盖了 Contains 方法,优化的 List-Path 将无法可靠地工作。
  • @KarmaEDV 你能澄清一下吗? List<T>.RemoveAll 不会调用 Contains,并且您不能覆盖 Contains,因为它不是 virtual
  • @ErenErsönmez 我不确定“不调用包含”是什么意思。是的,对不起,不是覆盖,而是超载。如果您需要自定义比较器,则 IEnumerable.Contains(item, customComparer) 是一个重载。如果您使用它,则需要稍微调整建议的解决方案。
  • @KarmaEDV 如果理解你,是的,如果你需要使用自定义比较器,你需要稍作修改。我在答案中发布了这样一个变体。
【解决方案2】:

如果IList&lt;T&gt; 引用碰巧引用了List&lt;T&gt; 的一个实例,则强制转换为该类型并使用RemoveAll 会比任何其他不依赖于其实现细节的方法产生更好的性能.

否则,虽然最佳方法将取决于要删除的项目的相对比例以及 IList&lt;T&gt; 的性质,但我建议您最好的选择是将 IList&lt;T&gt; 复制到新的List&lt;T&gt;,清除它,并有选择地重新添加项目。即使列表中的项目不利于高效散列,IEnumerable&lt;T&gt; 中的项目与IList&lt;T&gt; 中的项目的顺序相同这一事实也会使这无关紧要。从阅读IEnumerable&lt;T&gt; 中的项目开始。然后将数组中的项复制到列表中,直到找到该项。然后从IEnumerable&lt;T&gt; 中读取下一项并从数组复制到列表中,直到找到该项,等等。一旦IEnumerable&lt;T&gt; 用完,将数组的余额复制到List&lt;T&gt;

这种方法在IList&lt;T&gt; 的许多实现中会很快。但是,它有一个主要缺点:它删除和重新添加每个项目的事实可能会对诸如可观察列表之类的事物产生不必要的副作用。如果列表可能是可观察的,则可能必须使用慢得多的 N^2 算法来确保正确性。 [顺便说一句,IList&lt;T&gt; 有一个 Remove(T) 方法,但缺少一个更有用的 RemoveAll(Func&lt;T,bool&gt;) 方法,这让我很生气。 Remove(T) 在很大程度上与IndexOfRemoveAt 相比是多余的,而RemoveAll 将允许O(N) 实现许多O(N^2) 的操作,如果不允许删除和重新添加项目。

【讨论】:

    【解决方案3】:

    也许这会有所帮助。可以包括其他相同类型的想法。

    IList<T> items;
    
    IEnumerable<T> itemsToDelete;
    ...
    {
       if(items.Equals(itemsToDelete)) //Equal lists?
         {
          items.Clear(); 
          return true;
         }
    
    
       if(  (double) items.Count/itemsToDelete.Count < 1){
          /* It is faster to iterate the small list first. */ 
                  foreach (var x in items)
                  {
                    if(itemsToDelete.Contains(x)){/**/} 
    
                  }
        }
       else{
               foreach (var x in itemsToDelete)
                  {
                   items.Remove(x);
                  }
       }
    }
    

    【讨论】:

    • 您可能想尝试比较items.Count &lt; itemsToDelete.Count(),而不是使用doubles。它还避免了编译器错误。
    • 我没有解释:除以防万一你想要一个百分比关闭:你可以将 1 替换为 .9 为 90%。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-22
    • 2011-03-12
    相关资源
    最近更新 更多