【问题标题】:How to iterate at elements from a sub list and then remove the sub list from the list? With great performance如何迭代子列表中的元素,然后从列表中删除子列表?以出色的表现
【发布时间】:2025-12-13 13:25:06
【问题描述】:

这是一个例子: originalList 是一个对象列表

var subList = (originalList.Where(x => x.number < 0)).ToList();
originalList.RemoveAll(x => x.number < 0);

稍后我将使用subList。 在此示例中,originaList 被迭代两次。这个函数被调用了数十亿次,originalList 是一个很大的List

有没有简单的方法来提高性能?


一件重要的事情:对象的数字值可以在两次调用此函数之间改变。

【问题讨论】:

  • 这感觉可能是XY problem - 你必须使用List吗?更好的数据结构可能更优化。
  • 例如,使用LinkedList 而不是List,我得到的RemoveAllAndReturn 方法比List 版本快50 到200 倍,具体取决于删除的频率.
  • 不,我不必使用列表。非常感谢,我现在就去测试。
  • 请注意,LinkedList 占用更多空间 - 如果这很关键,或者时间真的很关键,那么实现单链表可能会更有效。

标签: c# performance linq collections


【解决方案1】:

一个效率改进(尽管最终仍然是 O(n))是将移除批量处理在一起。我的测试表明,根据移除的频率,这可以是相同的速度或快 4 倍以上。这是作为扩展方法的函数:

public static List<T> RemoveAllAndReturn<T>(this List<T> input, Func<T, bool> condition) {
    List<T> result = new List<T>();
    var removeCt = 0;
    for (int i = input.Count - 1; i >= 0; --i) {
        if (condition(input[i])) {
            result.Add(input[i]);
            ++removeCt;
        }
        else if (removeCt > 0) {
            input.RemoveRange(i + 1, removeCt);
            removeCt = 0;
        }
    }
    if (removeCt > 0)
        input.RemoveRange(0, removeCt);
    return result;
}

【讨论】:

    【解决方案2】:

    此方法删除所有满足条件的元素并返回已删除元素的列表。它只迭代一次。

    public static List<T> RemoveAll<T>(List<T> input, Func<T,bool> condition)
    {
        List<T> removedEntries = new List<T>();
        int offset = 0;
        for(int i = 0; i < input.Count - offset; i++)
        {
          while(i < input.Count - offset && condition.Invoke(input[i + offset]))
          {
             removedEntries.Add(input[i + offset]);
             offset++; 
             Console.WriteLine("i="+i+", offset="+offset);
          }
        
          if(i < input.Count - offset)
          {
             input[i] = input[i+offset];
          }
        }
        input.RemoveRange(input.Count - offset, offset);
        return removedEntries;
    }
    

    我们遍历列表并检查元素是否匹配条件。如果条件匹配,则将该元素之后的元素复制到该位置。所以所有不满足条件的元素都在列表的开头,所有满足条件的元素都在列表的末尾。在最后一步中,列表末尾的元素被删除。

    removedEntries 列表提供初始容量可能是明智的。默认情况下,列表的容量为 4,每次超出时都会加倍。如果要删除 100 个元素,则容量必须扩展 5 倍。每次都是O(n) 操作。如果你可以估计你会删除大约 10% 的元素,你可能会写

    List<T> removedEntries = new List<T>(input.Count / 10);
    

    这可能会为您节省一些时间,但另一方面,如果您不需要列表的全部初始容量,则会浪费一些内存。

    在线演示:https://dotnetfiddle.net/dlthkH

    【讨论】:

    • List.RemoveAt 是一个O(n) 操作,其中n = 计数 - 索引。
    • OPs 代码因此可能更有效,具体取决于要删除的元素/数量。
    • 我认为线程安全集合不会有太大帮助,除非您同时从列表中添加/删除对象。所需的同步程度将取决于代码需要实现的保证,而这在 OP 中没有明确描述。
    • 你为什么用condition.Invoke(input[i])而不是condition(input[i])
    • 运行一些测试,当移除百分比为 13% 或以上时,这甚至比使用 LinkedList 还要快;去除率低于 13%,LinkedList 似乎更快。
    【解决方案3】:

    你可以考虑做这个 hack:

    var subList = new List<SomeType>();
    originalList.RemoveAll(x =>
    {
        bool shouldBeRemoved = x.Number < 0;
        if (shouldBeRemoved) subList.Add(x);
        return shouldBeRemoved;
    });
    

    传递给RemoveAllPredicate&lt;T&gt; 不是纯的:它具有在subList 中插入匹配元素的副作用。基于RemoveAll 方法的implementation,此hack 应该可以按预期工作。 documentation 虽然没有明确保证每个元素只会调用一次谓词:

    当前List&lt;T&gt;的元素被单独传递给Predicate&lt;T&gt;委托,符合条件的元素从List&lt;T&gt;中移除。

    因此,请自行判断使用此 hack 是否安全。


    编辑:您也可以将其设为扩展方法:

    public static int RemoveAll<T>(this List<T> source, Predicate<T> match,
        out List<T> removed)
    {
        var removedLocal = new List<T>();
        removed = removedLocal;
        int removedCount = source.RemoveAll(x =>
        {
            bool shouldBeRemoved = match(x);
            if (shouldBeRemoved) removedLocal.Add(x);
            return shouldBeRemoved;
        });
        Debug.Assert(removedCount == removed.Count);
        return removedCount;
    }
    

    使用示例:

    originalList.RemoveAll(x => x.number < 0, out var subList);
    

    【讨论】:

      最近更新 更多