Как быстро удалить элементы из списка

Я ищу способ быстрого удаления элементов из С# 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, sort, а затем RemoveRange

В этом тесте я зациклился на списке один раз и установил подлежащие удалению элементы в 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()

Примечания и комментарии

  • Первые два теста фактически не удаляют каждый пятый элемент из списка, потому что список переупорядочивается после каждого удаления. Фактически, из 500 000 предметов было удалено только 83 334 человека (должно быть 100 000). Я в порядке с этим - очевидно, что методы Remove()/RemoveAt() в любом случае не являются хорошей идеей.

  • Хотя я попытался удалить 5-й элемент из списка, на самом деле такого шаблона не будет. Записи, которые нужно удалить, будут случайными.

  • Хотя в этом примере я использовал List<String>, это не всегда так. Это может быть List<Anything>

  • Не ставить элементы в списке, начиная с не.

  • Другие методы (3-6) выполнялись намного лучше, сравнительно, но я немного заинтересован - в 3, 5 и 6 я был вынужден установить значение null, а затем удалите все предметы в соответствии с этим стражем. Мне не нравится этот подход, потому что я могу представить себе сценарий, в котором один из элементов в списке может быть null, и он будет удален из-за непреднамеренно.

Мой вопрос: какой лучший способ быстро удалить многие элементы из List<T>? Большинство подходов, которые я пробовал, выглядят действительно уродливыми и потенциально опасными для меня. Является ли List неправильной структурой данных?

Сейчас я склоняюсь к созданию нового списка и добавлению хороших элементов в новый список, но кажется, что должен быть лучший способ.

Ответ 1

Список не является эффективной структурой данных, когда дело доходит до удаления. Вам лучше использовать двойной связанный список (LinkedList), поскольку удаление просто требует обновлений ссылок в смежных записях.

Ответ 2

Если вы счастливы создавать новый список, вам не нужно проходить настройку элементов до нуля. Например:

// 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>. Это действительно зависит от того, какие функции вам нужны из вашей структуры данных.

Ответ 3

Я чувствую, что HashSet, LinkedList или Dictionary сделают вам намного лучше.

Ответ 4

Если порядок не имеет значения, тогда существует простой метод 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), поскольку список должен быть несортирован.
  • Связанные списки медленны на обход, особенно для больших коллекций с длительным сроком службы.

Ответ 5

Вы всегда можете удалить элементы из конца списка. Удаление списка - это O (1), когда выполняется на последнем элементе, поскольку все, что он делает, - это счетчик уменьшения. Не происходит смещения следующих элементов. (именно поэтому удаление списка - O (n) в целом)

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

Ответ 6

Выполните попытку 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

Ответ 7

Я обнаружил, что при работе с большими списками это часто бывает быстрее. Скорость удаления и поиск нужного элемента в словаре для удаления, более чем для создания словаря. Пара вещей, хотя исходный список должен иметь уникальные значения, и я не думаю, что заказ будет гарантирован, как только вы закончите.

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

Ответ 8

Или вы можете это сделать:

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

...

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

Ответ 9

Другие ответы (и сам вопрос) предлагают различные способы борьбы с этой "пробкой" (ошибкой медлительности) с использованием встроенных классов .NET Framework.

Но если вы захотите переключиться на стороннюю библиотеку, вы можете получить лучшую производительность, просто изменив структуру данных и оставив свой код без изменений, за исключением типа списка.

Библиотеки Loyc Core включают два типа, которые работают так же, как List<T>, но могут быстрее удалять элементы:

  • DList<T> - это простая структура данных, которая дает вам 2x ускорение поверх List<T> при удалении элементов из случайных местоположений
  • AList<T> - сложная структура данных, которая дает вам большое ускорение над List<T>, когда ваши списки очень длинные (но могут быть медленнее, когда список короткий).

Ответ 10

Списки быстрее, чем LinkedLists, пока n не станет действительно большим. Причина этого в том, что так называемые промахи кэшей происходят чаще, используя LinkedLists, чем списки. Память выглядит довольно дорого. Поскольку список реализован как массив, центральный процессор может загружать кучу данных одновременно, поскольку он знает, что необходимые данные хранятся рядом друг с другом. Однако связанный список не дает процессору никакого намека на то, какие данные требуются в следующем, что заставляет ЦП делать больше обращений к памяти. Кстати. С памятью термина я имею в виду ОЗУ.

Для получения дополнительной информации смотрите: https://jackmott.github.io/programming/2016/08/20/when-bigo-foolsya.html