从IList <T>中删除多个项目的最有效方法

Gui*_*rez 9 c# generics ienumerable ilist

IList<T>对象中删除多个项目的最有效方法是什么.假设我有一个IEnumerable<T>我要删除的项目,其顺序与原始列表中的相同.

我唯一想到的方法是:

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

foreach (var x in itemsToDelete)
{
    items.Remove(x);
}
Run Code Online (Sandbox Code Playgroud)

但我想它效率不高,因为每次Remove调用该方法时它都必须从初始化列表开始.

Ere*_*mez 8

随着要删除的项目数量变大,您可能会发现遍历列表并根据"要删除的项目"的哈希集检查每个项目更有效.像这样的扩展方法可能会有所帮助:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我使用下面这个小程序进行基准测试.(请注意,如果IList<T>实际上是a ,则使用优化路径List<T>.)

在我的机器上(并使用我的测试数据),这个扩展方法执行需要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();
    }
}
Run Code Online (Sandbox Code Playgroud)

更新(对于@ KarmaEDV和Mark Sowul的评论如下):如果你需要使用自定义相等比较器,扩展方法可能会有一个带有这样一个比较器的重载:

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--;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


sup*_*cat 6

如果IList<T>引用碰巧引用的实例,则将其List<T>强制转换为该类型并使用RemoveAll会比不依赖于其实现细节的任何其他方法产生更好的性能。

否则,虽然最佳方法取决于要删除的项目的相对比例和的性质,但IList<T>我建议您最好的选择是将其复制IList<T>到新的List<T>,清除的内容中,然后有选择地重新添加项目。即使列表中的项目不利于进行有效的哈希处理,事实上,中的项目与IEnumerable<T>中的项目具有相同的顺序IList<T>也会使这一点变得无关紧要。首先从中读取项目IEnumerable<T>。然后将项目从数组复制到列表,直到找到该项目为止。然后从中读取下一项,IEnumerable<T>然后将其从数组复制到列表中,直到找到该列表为止,IEnumerable<T>依此类推。等用完后,将数组的余额复制到中List<T>

使用的许多实现,这种方法将很快IList<T>。但是,它有一个主要缺点:删除和重新添加每个项目这一事实可能会对可观察列表之类的东西产生有害的副作用。如果列表是可观察的,则可能必须使用慢得多的N ^ 2算法来确保正确性。[顺便说一句,让我感到讨厌的是,它IList<T>有一种Remove(T)方法,但缺少一种更有用的RemoveAll(Func<T,bool>)方法。Remove(T)使用IndexOfRemoveAt,这在很大程度上是多余的,而RemoveAll如果不允许删除和重新添加项目,则在没有O(N ^ 2)的情况下,可以实现O(N ^ 2)的许多操作的O(N)实现。