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


72

我正在寻找一种快速从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()

这是我用来测试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个项目,但实际上不会有这种模式。要删除的条目将是随机的。

  • 尽管List<String>在此示例中使用了a ,但情况并非总是如此。可能是List<Anything>

  • 不将项目不放在列表开头就不是一种选择。

  • 其他方法(3 - 6)所有的表现要好得多,比较,但是我很担心一点- 3,5,6我不得不设置一个值null,然后删除所有根据该定点的项目。我不喜欢这种方法,因为我可以设想一个场景,其中列表中的某一项可能null会被无意中删除。

我的问题是:从中快速删除许多项目的最佳方法是什么List<T>?我尝试过的大多数方法对我来说看起来都很丑陋,并且有潜在危险。是一个List错误的数据结构?

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


4
您必须实际使用List<T>吗?除非您需要随机访问,否则LinkedList<T>可能会更好。
乔恩·斯基特

如果测试为4,则可以将新列表分配给该列表。您无需费心进行删除和添加。
Will Calderwood

Answers:


35

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


谢谢。我将调查LinkedList。主要缺点是什么?
user807566 2011年

5
一旦找到所需位置,链接列表即可快速删除和快速插入。但是为了定位元素,有必要遍历列表(从任一端开始)。但是由于不必重定位数据,因此插入或删除数据仍比使用List快得多。但重要的是,LinkedList与List一样确实保留了顺序。
史蒂夫·摩根

1
有几种方法可以使链表建立索引(更多)。我认为这主要是涉及更多麻烦的问题。
Lodewijk 2014年

1
另一个缺点是,LinkedList对现代处理器缓存不友好。
StefanLundmark'8

遵循史蒂夫·摩根(Steve Morgan)的评论,这意味着LinkedList没有实现IList。另外,由于必须将集合传递到索引,因此绑定到ItemsSource时可能会遇到性能问题。
kjhf

19

如果您乐于创建新列表,则无需将项目设置为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>。这实际上取决于您需要从数据结构中获取哪些功能。


16

如果顺序无关紧要,则有一个简单的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)。
  • 链接列表的遍历速度很慢,特别是对于寿命长的大型集合。

1
在我的测试,你的方法是10%,快于平原list.remove
阿尔森Zahray

如果您正在使用(例如)100个条目,并按照OP要求删除每5个条目的要求,则希望删除20个条目,但是第85、90、95和100个条目将在它们可以移动之前移至列表中的较早位置被删除,因此将被跳过。那可能就是为什么它更快?
Zac Faragher

扎克(Zac),我提到的这种方法仅在订购无关紧要而您只需要一袋东西时才适用。如果要根据元素的位置专门删除元素,则不应使用此方法。
优素福O'9


4

您始终可以从列表末尾删除项目。在最后一个元素上执行时,列表删除为O(1),因为它所做的只是减少计数。接下来的要素没有转移。(这就是为什么列表删除通常为O(n)的原因)

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

这将需要进行预排序,以将要删除的项目放在列表末尾。List.Sort用途Array.SortO(nlogn)在最好的和O(n^2)最坏的情况。
BaltoStar

4

或者您可以这样做:

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

...

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

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,000个项目

此代码的输出:

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

这是一个好方法。与其他答案不同,它不会在内存中创建新列表。这比逐一删除项目要快得多。
卡拉

3

我发现处理大型列表时,这通常更快。Remove的速度以及在字典中找到要删除的正确项目的速度,不仅仅可以弥补创建字典的不足。但是,有两件事,原始列表必须具有唯一的值,并且我认为完成后并不能保证顺序。

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();

您可以使50k列表成为字典,然后遍历100k一次并签入50k。这样可以为您节省5万个字典。这仍然是一个非常丑陋的解决方案。
Lodewijk 2014年

支持尼尔·皮尔森。我们有使用两个列表和RemoveAll的代码。通过仅更改主列表以使用.ToDictionary(x => x)并更改removeall以使用字典包含的内容,我们从几分钟缩短到了不到一秒钟。
Choco Smith

....这可能是HashSet而不是字典,因为我们真的只在乎密钥
NeilPearson

“我认为一旦您完成订单就不能保证。” 字典中不能保证顺序。但是如果需要的话,可以使用OrderedDictionary。
Zac Faragher

2

在n变大之前,列表比LinkedList更快。这样做的原因是因为使用LinkedList而不是List,发生所谓的高速缓存未命中的频率更高。内存查找非常昂贵。当列表被实现为数组时,CPU可以一次加载一堆数据,因为它知道所需的数据彼此相邻存储。但是,链接列表不会给CPU任何提示,即接下来需要哪些数据,这将迫使CPU进行更多的内存查找。顺便说说。术语记忆是指RAM。

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


1

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

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

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

  • DList<T>是一个简单的数据结构,List<T>当您从随机位置删除项目时,速度提高了2倍
  • AList<T>是一种复杂的数据结构,可以List<T>在列表很长时(但在列表很短时可能会较慢)大大提高速度。

0

如果仍然希望将List用作基础结构,则可以使用以下扩展方法,该方法可以为您带来繁重的工作。

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;
        }
    }
}

一个简单的秒表测试可以在大约200毫秒内将结果删除。请记住,这不是真正的基准用法。

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个元素

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.