从对象中移除多个项,最有效的方法是什么?假设我有一个包含我想要删除的所有项的IEnumerable<T>
,以与原始列表中出现的顺序相同。
我所知道的唯一方法是:
IList<T> items;
IEnumerable<T> itemsToDelete;
...
foreach (var x in itemsToDelete)
{
items.Remove(x);
}
但我猜这不是很有效,因为每次调用Remove
方法时都必须重新遍历列表。
从对象中移除多个项,最有效的方法是什么?假设我有一个包含我想要删除的所有项的IEnumerable<T>
,以与原始列表中出现的顺序相同。
我所知道的唯一方法是:
IList<T> items;
IEnumerable<T> itemsToDelete;
...
foreach (var x in itemsToDelete)
{
items.Remove(x);
}
但我猜这不是很有效,因为每次调用Remove
方法时都必须重新遍历列表。
当需要移除的项目数量变大时,您可能会发现遍历列表并将每个项目与“要删除的项目”的哈希集合进行检查更加高效。像这样的扩展方法可能会有所帮助:
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.RemoveAll(set.Contains);
}
else
{
int i = iList.Count - 1;
while (i > -1)
{
if (set.Contains(iList[i])) iList.RemoveAt(i);
else i--;
}
}
}
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)
在很大程度上与 IndexOf
和 RemoveAt
重复,而 RemoveAll
将允许 O(N) 的实现许多在其缺席时是 O(N^2) 的操作,如果不允许删除和重新添加项的话。]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 < itemsToDelete.Count()
而不是使用 double
。这样还可以避免编译器错误。 - ta.speot.isIList<T>
接口有一个可用的扩展方法RemoveAll
,那么这个问题将更容易解决。因此,这里提供一个:/// <summary>
/// Removes all the elements that match the conditions defined by the
/// specified predicate.
/// </summary>
public static int RemoveAll<T>(this IList<T> list, Func<T, int, bool> predicate)
{
ArgumentNullException.ThrowIfNull(list);
ArgumentNullException.ThrowIfNull(predicate);
int i = 0, j = 0;
try
{
for (; i < list.Count; i++)
{
if (predicate(list[i], i)) continue;
if (j < i) list[j] = list[i];
j++;
}
}
finally
{
if (j < i)
{
for (; i < list.Count; i++, j++)
list[j] = list[i];
while (list.Count > j)
list.RemoveAt(list.Count - 1);
}
}
return i - j;
}
这是一个修改过的自定义List<T>.RemoveAll
实现版本,可以在此答案中找到。由于IList<T>
接口中缺少RemoveRange
方法,因此通过重复删除最后一个元素来清除IList<T>
中右侧剩余的插槽。在大多数IList<T>
实现中,这应该是一个非常快速的操作。
现在,可以像这样高效地解决从IList<T>
中删除多个项的原始问题:
IList<T> items;
IEnumerable<T> itemsToDelete;
//...
HashSet<T> itemsToDeleteSet = new(itemsToDelete);
items.RemoveAll((x, _) => itemsToDeleteSet.Contains(x));
在线演示。