如何从列表中快速删除项目

时间:2011-08-03 12:37:48

标签: c# linq list collections

我正在寻找一种快速从C#List<T>中删除项目的方法。文档指出List.Remove()List.RemoveAt()操作都是O(n)

这严重影响了我的申请。

我写了一些不同的删除方法,并在List<String>上测试了所有这些方法,包含500,000个项目。测试用例如下所示......


概述

我写了一个方法,它会生成一个字符串列表,其中只包含每个数字的字符串表示形式(“1”,“2”,“3”,...)。然后我尝试remove列表中的每第5项。以下是用于生成列表的方法:

private List<String> GetList(int size)
{
    List<String> myList = new List<String>();
    for (int i = 0; i < size; i++)
        myList.Add(i.ToString());
    return myList;
}

测试1:RemoveAt()

以下是我用来测试RemoveAt()方法的测试。

private void RemoveTest1(ref List<String> list)
{
     for (int i = 0; i < list.Count; i++)
         if (i % 5 == 0)
             list.RemoveAt(i);
}

测试2:删除()

以下是我用来测试Remove()方法的测试。

private void RemoveTest2(ref List<String> list)
{
     List<int> itemsToRemove = new List<int>();
     for (int i = 0; i < list.Count; i++)
        if (i % 5 == 0)
             list.Remove(list[i]);
}

测试3:设置为null,排序,然后设置RemoveRange

在此测试中,我循环浏览列表一次,并将要删除的项目设置为null。然后,我对列表进行了排序(因此null将位于顶部),并删除顶部设置为null的所有项目。 注意:这重新排序了我的列表,所以我可能不得不按正确的顺序重新安排它。

private void RemoveTest3(ref List<String> list)
{
    int numToRemove = 0;
    for (int i = 0; i < list.Count; i++)
    {
        if (i % 5 == 0)
        {
            list[i] = null;
            numToRemove++;
        }
    }
    list.Sort();
    list.RemoveRange(0, numToRemove);
    // Now they're out of order...
}

测试4:创建新列表,并将所有“好”值添加到新列表中

在此测试中,我创建了一个新列表,并将所有保留项添加到新列表中。然后,我将所有这些项目放入原始列表中。

private void RemoveTest4(ref List<String> list)
{
   List<String> newList = new List<String>();
   for (int i = 0; i < list.Count; i++)
   {
      if (i % 5 == 0)
         continue;
      else
         newList.Add(list[i]);
   }

   list.RemoveRange(0, list.Count);
   list.AddRange(newList);
}

测试5:设置为null,然后设置为FindAll()

在此测试中,我将所有待删除的项目设置为null,然后使用FindAll()功能查找非null

的所有项目
private void RemoveTest5(ref List<String> list)
{
    for (int i = 0; i < list.Count; i++)
       if (i % 5 == 0)
           list[i] = null;
    list = list.FindAll(x => x != null);
}

测试6:设置为null,然后设置为RemoveAll()

在此测试中,我将所有待删除的项目设置为null,然后使用RemoveAll()功能删除所有不是null的项目

private void RemoveTest6(ref List<String> list)
{
    for (int i = 0; i < list.Count; i++)
        if (i % 5 == 0)
            list[i] = null;
    list.RemoveAll(x => x == null);
}

客户端应用程序和输出

int numItems = 500000;
Stopwatch watch = new Stopwatch();

// List 1...
watch.Start();
List<String> list1 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());

watch.Reset(); watch.Start();
RemoveTest1(ref list1);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();

// List 2...
watch.Start();
List<String> list2 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());

watch.Reset(); watch.Start();
RemoveTest2(ref list2);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();

// List 3...
watch.Reset(); watch.Start();
List<String> list3 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());

watch.Reset(); watch.Start();
RemoveTest3(ref list3);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();

// List 4...
watch.Reset(); watch.Start();
List<String> list4 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());

watch.Reset(); watch.Start();
RemoveTest4(ref list4);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();

// List 5...
watch.Reset(); watch.Start();
List<String> list5 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());

watch.Reset(); watch.Start();
RemoveTest5(ref list5);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();

// List 6...
watch.Reset(); watch.Start();
List<String> list6 = GetList(numItems);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());

watch.Reset(); watch.Start();
RemoveTest6(ref list6);
watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
Console.WriteLine();

结果

00:00:00.1433089   // Create list
00:00:32.8031420   // RemoveAt()

00:00:32.9612512   // Forgot to reset stopwatch :(
00:04:40.3633045   // Remove()

00:00:00.2405003   // Create list
00:00:01.1054731   // Null, Sort(), RemoveRange()

00:00:00.1796988   // Create list
00:00:00.0166984   // Add good values to new list

00:00:00.2115022   // Create list
00:00:00.0194616   // FindAll()

00:00:00.3064646   // Create list
00:00:00.0167236   // RemoveAll()

备注和评论

  • 前两个测试实际上并未从列表中删除每个第5个项目,因为每次删除后都会重新排序列表。事实上,在500,000件物品中,只有83,334件被移除(应该是100,000件)。我对此很好 - 显然,Remove()/ RemoveAt()方法无论如何都不是一个好主意。

  • 虽然我尝试从列表中删除第5项,但在 reality 中却没有这样的模式。要删除的条目是随机的。

  • 虽然我在这个例子中使用了List<String>,但情况并非总是如此。它可以是List<Anything>

  • 不打开列表中的项目是一个选项。

  • 其他方法(3 - 6)表现得更好,比较,但我有点担心 - 在3,5和6我被迫设置一个值到null,然后根据此哨兵删除所有项目。我不喜欢这种方法,因为我可以设想一个场景,其中列表中的一个项目可能是null,并且会无意中删除。

我的问题是:从List<T>快速删除多项内容的最佳方法是什么?我尝试过的大多数方法看起来都很丑陋,对我来说也很危险。 List数据结构是否错误?

现在,我倾向于创建一个新列表并将好的项目添加到新列表中,但似乎应该有更好的方法。

11 个答案:

答案 0 :(得分:35)

列表在删除时不是一种有效的数据结构。您最好使用双链表(LinkedList),因为删除只需要在相邻条目中进行参考更新。

答案 1 :(得分:17)

如果您对创建新列表感到满意,则无需将设置项设置为null。例如:

// This overload of Where provides the index as well as the value. Unless
// you need the index, use the simpler overload which just provides the value.
List<string> newList = oldList.Where((value, index) => index % 5 != 0)
                              .ToList();

但是,您可能希望查看其他数据结构,例如LinkedList<T>HashSet<T>。这实际上取决于您需要从数据结构中获得哪些功能。

答案 2 :(得分:12)

我觉得HashSetLinkedListDictionary会让你更好。

答案 3 :(得分:11)

如果顺序无关紧要,那么就有一个简单的O(1)List.Remove方法。

public static class ListExt
{
    // O(1) 
    public static void RemoveBySwap<T>(this List<T> list, int index)
    {
        list[index] = list[list.Count - 1];
        list.RemoveAt(list.Count - 1);
    }

    // O(n)
    public static void RemoveBySwap<T>(this List<T> list, T item)
    {
        int index = list.IndexOf(item);
        RemoveBySwap(list, index);
    }

    // O(n)
    public static void RemoveBySwap<T>(this List<T> list, Predicate<T> predicate)
    {
        int index = list.FindIndex(predicate);
        RemoveBySwap(list, index);
    }
}

此解决方案对内存遍历很友好,因此即使您需要首先找到索引,它也会非常快。

注意:

  • 查找项目的索引必须为O(n),因为列表必须未排序。
  • 链接列表遍历速度很慢,特别是对于具有较长生命周期的大型集合。

答案 4 :(得分:4)

您始终可以从列表末尾删除项目。当对最后一个元素执行时,列表移除是O(1),因为它所做的全部是递减计数。所涉及的下一个要素没有变化。 (这就是为什么列表删除通常是O(n))

for (int i = list.Count - 1; i >= 0; --i)
  list.RemoveAt(i);

答案 5 :(得分:3)

好的,试试RemoveAll就像这样使用

static void Main(string[] args)
{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    List<Int32> test = GetList(500000);
    watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
    watch.Reset(); watch.Start();
    test.RemoveAll( t=> t % 5 == 0);
    List<String> test2 = test.ConvertAll(delegate(int i) { return i.ToString(); });
    watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());

    Console.WriteLine((500000 - test.Count).ToString());
    Console.ReadLine();

}

static private List<Int32> GetList(int size)
{
    List<Int32> test = new List<Int32>();
    for (int i = 0; i < 500000; i++)
        test.Add(i);
    return test;
}

这只会循环两次并删除100个项目

此代码的输出:

00:00:00.0099495 
00:00:00.1945987 
1000000

更新以尝试HashSet

static void Main(string[] args)
    {
        Stopwatch watch = new Stopwatch();
        do
        {
            // Test with list
            watch.Reset(); watch.Start();
            List<Int32> test = GetList(500000);
            watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
            watch.Reset(); watch.Start();
            List<String> myList = RemoveTest(test);
            watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
            Console.WriteLine((500000 - test.Count).ToString());
            Console.WriteLine();

            // Test with HashSet
            watch.Reset(); watch.Start();
            HashSet<String> test2 = GetStringList(500000);
            watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
            watch.Reset(); watch.Start();
            HashSet<String> myList2 = RemoveTest(test2);
            watch.Stop(); Console.WriteLine(watch.Elapsed.ToString());
            Console.WriteLine((500000 - test.Count).ToString());
            Console.WriteLine();
        } while (Console.ReadKey().Key != ConsoleKey.Escape);

    }

    static private List<Int32> GetList(int size)
    {
        List<Int32> test = new List<Int32>();
        for (int i = 0; i < 500000; i++)
            test.Add(i);
        return test;
    }

    static private HashSet<String> GetStringList(int size)
    {
        HashSet<String> test = new HashSet<String>();
        for (int i = 0; i < 500000; i++)
            test.Add(i.ToString());
        return test;
    }

    static private List<String> RemoveTest(List<Int32> list)
    {
        list.RemoveAll(t => t % 5 == 0);
        return list.ConvertAll(delegate(int i) { return i.ToString(); });
    }

    static private HashSet<String> RemoveTest(HashSet<String> list)
    {
        list.RemoveWhere(t => Convert.ToInt32(t) % 5 == 0);
        return list;
    }

这给了我:

00:00:00.0131586
00:00:00.1454723
100000

00:00:00.3459420
00:00:00.2122574
100000

答案 6 :(得分:3)

我在处理大型列表时发现,这通常更快。删除和找到要删除的字典中的正确项目的速度,以弥补创建字典。但有几件事情,原始列表必须具有唯一的值,我认为一旦完成,我认为订单不会保证。

List<long> hundredThousandItemsInOrignalList;
List<long> fiftyThousandItemsToRemove;

// populate lists...

Dictionary<long, long> originalItems = hundredThousandItemsInOrignalList.ToDictionary(i => i);

foreach (long i in fiftyThousandItemsToRemove)
{
    originalItems.Remove(i);
}

List<long> newList = originalItems.Select(i => i.Key).ToList();

答案 7 :(得分:2)

或者你可以这样做:

List<int> listA;
List<int> listB;

...

List<int> resultingList = listA.Except(listB);

答案 8 :(得分:2)

列表比LinkedLists更快,直到n变得非常大。这样做的原因是因为使用LinkedLists比列表更频繁地发生所谓的缓存未命中。记忆查看非常昂贵。由于列表是作为一个数组实现的,CPU可以一次加载一堆数据,因为它知道所需的数据是彼此相邻存储的。但是,链接列表不会给CPU提供任何暗示接下来需要哪些数据的提示,这会强制CPU执行更多的内存查找。顺便说说。术语记忆是指RAM。

有关详细信息,请查看:https://jackmott.github.io/programming/2016/08/20/when-bigo-foolsya.html

答案 9 :(得分:1)

其他答案(以及问题本身)提供了使用内置.NET Framework类处理此“slug”(慢速错误)的各种方法。

但是如果您愿意切换到第三方库,只需更改数据结构就可以获得更好的性能,并保持代码不变,除了列表类型。

Loyc Core库包含两种类型,与List<T>的工作方式相同,但可以更快地删除项目:

  • DList<T>是一种简单的数据结构,当您从随机位置移除项目时,它会比List<T>提供2倍的加速
  • AList<T>是一种复杂的数据结构,当您的列表很长时(在列表很短时可能会更慢),您可以在List<T>上获得大的加速。

答案 10 :(得分:0)

如果您仍想使用列表作为基础结构,则可以使用以下扩展方法,该方法可以为您带来繁重的工作。

using System.Collections.Generic;
using System.Linq;

namespace Library.Extensions
{
    public static class ListExtensions
    {
        public static IEnumerable<T> RemoveRange<T>(this List<T> list, IEnumerable<T> range)
        {
            var removed = list.Intersect(range).ToArray();
            if (!removed.Any())
            {
                return Enumerable.Empty<T>();
            }

            var remaining = list.Except(removed).ToArray();
            list.Clear();
            list.AddRange(remaining);

            return removed;
        }
    }
}

简单的秒表测试可在约200ms的时间内得出结果。请记住,这不是真正的基准用法。

public class Program
    {
        static void Main(string[] args)
        {
            var list = Enumerable
                .Range(0, 500_000)
                .Select(x => x.ToString())
                .ToList();

            var allFifthItems = list.Where((_, index) => index % 5 == 0).ToArray();

            var sw = Stopwatch.StartNew();
            list.RemoveRange(allFifthItems);
            sw.Stop();

            var message = $"{allFifthItems.Length} elements removed in {sw.Elapsed}";
            Console.WriteLine(message);
        }
    }

输出:

在00:00:00.2291337中删除了100000个元素