从IList<T>
对象中删除多个项目的最有效方法是什么?假设我有一个IEnumerable<T>
我要移除的所有项目,其顺序与原始列表中的相同。
我唯一想到的方法是:
IList<T> items;
IEnumerable<T> itemsToDelete;
...
foreach (var x in itemsToDelete)
{
items.Remove(x);
}
但是我猜它效率不高,因为每次调用方法Remove
时都必须从列表中查看。
答案 0 :(得分: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);
}
}
我使用下面这个小程序进行基准测试。 (请注意,如果IList<T>
实际上是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();
}
}
更新(适用于@ 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--;
}
}
}
答案 1 :(得分:4)
如果IList<T>
引用恰好引用List<T>
的实例,则转换为该类型并使用RemoveAll
比任何其他不依赖的方法更容易产生更好的性能关于其实施的细节。
否则,虽然最佳方法取决于要删除的项目的相对比例以及IList<T>
的性质,但我建议您最好的选择是复制{{1} }到新的IList<T>
,清除它,并有选择地重新添加项目。即使列表中的项目不利于有效散列,List<T>
中的项目与IEnumerable<T>
中的项目的顺序相同也会导致无关紧要。首先阅读IList<T>
中的项目。然后将数组中的项目复制到列表中,直到找到该项目。然后从IEnumerable<T>
中读取下一个项目并从数组复制到列表,直到找到该项目等。一旦IEnumerable<T>
用完,将数组的余额复制到{{1} }。
对于IEnumerable<T>
的许多实现,这种方法会很快。但它有一个主要的缺点:它删除并重新添加每个项目的事实可能会对可观察列表之类的东西产生不必要的副作用。如果列表可能是可观察的,则可能必须使用更慢的N ^ 2算法来确保正确性。 [顺便说一句,令我感到惊讶的是List<T>
有一个IList<T>
方法,但缺少更有用的IList<T>
方法。 Remove(T)
与RemoveAll(Func<T,bool>)
和Remove(T)
在很大程度上是多余的,而IndexOf
允许O(N)实现许多操作,如果没有O(N ^ 2),一个人不允许删除和重新添加项目。
答案 2 :(得分:1)
也许这有帮助。可以包括相同类型的其他想法。
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);
}
}
}