从列表中彻底删除某个特定值的元素的最有效方法是什么?C#

4

编辑:本问题底部发布了不同技术的基准测试结果。

我有一个非常大的 List<int>,里面装满了整数。我想从这个 List<int> 中删除所有出现的“3”。使用哪种技术可以最有效地完成此操作?通常我会使用.Remove(3)方法,直到它返回false,但我担心每次调用.Remove(3)内部都会不必要地遍历整个List<int>

编辑:在评论中建议尝试以下代码:

TheList = TheList.Where(x => x != 3).ToList();

但是我需要在不实例化新列表的情况下删除元素。

var TheList = new List<int> { 5, 7, 8, 2, 8, 3, 1, 0, 6, 3, 9, 3, 5, 2, 7, 9, 3, 5, 5, 1, 0, 4, 5, 3, 5, 8, 2, 3 };

//technique 1
//this technique has the shortest amount of code,
//but I fear that every time the Remove() method is called,
//the entire list is internally looped over again starting at index 0

while (TheList.Remove(3)) { }

//technique 2
//this technique is an attempt to keep the keep the list from
//being looped over every time an element is removed

for (var i = 0; i < TheList.Count; i++)
{
    if (TheList[i] == 3)
    {
        TheList.RemoveAt(i);
        i--;
    }
}

有没有更好的方法?

基准测试

我测试了三种技术来从包含100,000个元素的数组中删除10,138个元素:上面展示的两种和Serg在回答中推荐的一种。以下是结果:

  1. 'while'循环:179.6808毫秒
  2. 'for'循环:65.5099毫秒
  3. 'RemoveAll'谓词:0.5982毫秒

enter image description here

基准测试代码:

var RNG = new Random();
//inclusive min and max random number
Func<int, int, int> RandomInt = delegate (int min, int max) { return RNG.Next(min - 1, max) + 1; };

var TheList = new List<int>();
var ThreeCount = 0;
for (var i = 0; i < 100000; i++)
{
    var TheInteger = RandomInt(0, 9);
    if (TheInteger == 3) { ThreeCount++; }
    TheList.Add(TheInteger);
}
var Technique1List = TheList.ToList();
var Technique2List = TheList.ToList();
var Technique3List = TheList.ToList();
<div style="background-color:aquamarine;color:#000000;">Time to remove @ThreeCount items</div>

//technique 1
var Technique1Stopwatch = Stopwatch.StartNew();
while (Technique1List.Remove(3)) { }
var Technique1Time = Technique1Stopwatch.Elapsed.TotalMilliseconds;
<div style="background-color:#ffffff;color:#000000;">Technique 1: @(Technique1Time)ms ('while' loop)</div>

//technique 2
var Technique2Stopwatch = Stopwatch.StartNew();
for (var i = 0; i < Technique2List.Count; i++)
{
    if (Technique2List[i] == 3)
    {
        Technique2List.RemoveAt(i);
        i--;
    }
}
var Technique2Time = Technique2Stopwatch.Elapsed.TotalMilliseconds;
<div style="background-color:#ffffff;color:#000000;">Technique 2: @(Technique2Time)ms ('for' loop)</div>

//technique 3
var Technique3Stopwatch = Stopwatch.StartNew();
var RemovedCount = Technique3List.RemoveAll(x => x == 3);
var Technique3Time = Technique3Stopwatch.Elapsed.TotalMilliseconds;
<div style="background-color:#ffffff;color:#000000;">Technique 3: @(Technique3Time)ms ('RemoveAll' predicate)</div>

1
这是一行代码:list = list.Where(x=>x!=3).ToList(); - Shiljo Paulson
@ShiljoPaulson 那是个好的单行代码,但是这不会通过调用ToList()实例化一个新的List吗?我需要在不实例化新的List的情况下删除元素。 - Pershing
2
请注意,如果您想要高效且频繁地执行此操作,则可能需要考虑改用链表。从 List<T> 中删除项目涉及复制列表的其余部分。 - Jon Skeet
尽管 RemoveAll() 的复杂度是 O(N),这已经是 List<T> 的最低要求了,但您仍然可以通过删除所有不必要的类型检查、使方法非泛型并远离 Func<T> 委托向原始类型检查转变来稍微提高性能。 - Fabjan
1个回答

5
您可以使用List<T>.RemoveAll方法,并传递您的谓词条件 - https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.removeall?view=net-6.0#System_Collections_Generic_List_1_RemoveAll_System_Predicate__0_。这保证了线性复杂度为O(list.Count)
TheList.RemoveAll(x=>x==3);

此外,RemoveAll 在内部执行一些特定于 GC 的操作,因此在某些情况下,与简单的手动循环实现相比,可能会提供一些额外的性能优势(但我对此不确定)。
如果你想亲自动手完成所有操作,可以在这里查看 RemoveAll 的实现。通常,它只是类似你问题中 while 循环的实现。
此外,从 GitHub 的实现中可以看出(并且 Jon Skeet 在评论中也提到了),删除操作会导致列表剩余部分(删除的第一个元素后的所有元素)在释放的空间上进行复制(移位)。因此,如果你有非常大的列表和/或经常需要删除某些内容,可以考虑切换到其他数据结构,例如链表。

1
这是最快的技术之一。如果您感兴趣,我已经在我的原始帖子中放置了基准测试结果。 - Pershing

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接