web-dev-qa-db-fra.com

Comment supprimer rapidement des éléments d'une liste

Je cherche un moyen de supprimer rapidement des éléments d'un C # List<T>. La documentation indique que les opérations List.Remove() et List.RemoveAt() sont toutes deux O(n)

Ceci affecte gravement ma candidature.

J'ai écrit quelques méthodes de suppression différentes et les ai toutes testées sur un List<String> Avec 500 000 éléments. Les cas de test sont présentés ci-dessous ...


Vue d'ensemble

J'ai écrit une méthode qui générerait une liste de chaînes contenant simplement des représentations sous forme de chaîne de chaque nombre ("1", "2", "3", ...). J'ai ensuite essayé de remove tous les 5 éléments de la liste. Voici la méthode utilisée pour générer la liste:

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

Test 1: RemoveAt ()

Voici le test que j'ai utilisé pour tester la méthode RemoveAt().

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

Test 2: Supprimer ()

Voici le test que j'ai utilisé pour tester la méthode 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]);
}

Test 3: Définissez la valeur sur null, triez puis supprimez la plage

Lors de ce test, j’ai parcouru la liste une fois et réglé les éléments à supprimer à null. Ensuite, j'ai trié la liste (donc null serait en haut) et supprimé tous les éléments en haut qui étaient définis sur null. NOTE: Ceci a réorganisé ma liste, donc je devrais peut-être aller la remettre dans le bon ordre.

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

Test 4: créez une nouvelle liste et ajoutez toutes les "bonnes" valeurs à la nouvelle liste

Dans ce test, j'ai créé une nouvelle liste et ajouté tous mes articles de sauvegarde à la nouvelle liste. Ensuite, j'ai mis tous ces éléments dans la liste d'origine.

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

Test 5: Définissez la valeur sur null, puis sur FindAll ()

Dans ce test, j'ai défini tous les articles à supprimer sur null, puis j'ai utilisé la fonction FindAll() pour rechercher tous les articles qui ne sont pas 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);
}

Test 6: Définissez la valeur sur null, puis sur RemoveAll ()

Dans ce test, j'ai défini tous les éléments à supprimer, sur null, puis j'ai utilisé la fonction RemoveAll() pour supprimer tous les éléments qui ne sont pas 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);
}

Application cliente et sorties

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

Résultats

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

Notes et commentaires

  • Les deux premiers tests ne suppriment pas réellement tous les 5 éléments de la liste, car la liste est en cours de réorganisation après chaque suppression. En fait, sur 500 000 articles, seuls 83 334 ont été retirés (ils auraient dû être 100 000). Je suis d'accord avec cela - clairement les méthodes Remove ()/RemoveAt () ne sont de toute façon pas une bonne idée.

  • Bien que j'ai essayé de supprimer le cinquième élément de la liste, il ne se produira pas de motif semblable dans la réalité. Les entrées à supprimer seront aléatoires.

  • Bien que j'utilise un List<String> Dans cet exemple, ce ne sera pas toujours le cas. Ce pourrait être un List<Anything>

  • Ne pas mettre les éléments de la liste pour commencer n'est pas une option.

  • Les autres méthodes (3 - 6) ont toutes donné de bien meilleurs résultats, comparativement, mais je suis un peu inquiet - En 3, 5 et 6, j’ai été forcé de définir une valeur sur null, puis supprimez tous les éléments en fonction de cette sentinelle. Je n’aime pas cette approche car je peux envisager un scénario dans lequel l’un des éléments de la liste pourrait être null et qui serait supprimé par inadvertance.

Ma question est la suivante: quel est le meilleur moyen de supprimer rapidement de nombreux éléments d'un List<T>? La plupart des approches que j'ai essayées me paraissent vraiment laides et potentiellement dangereuses. Est-ce qu'un List est une mauvaise structure de données?

À l'heure actuelle, je suis enclin à créer une nouvelle liste et à ajouter les bons éléments à la nouvelle liste, mais il semble qu'il devrait y avoir un meilleur moyen.

68
user807566

La liste n'est pas une structure de données efficace en matière de suppression. Vous feriez mieux d'utiliser une liste double liée (LinkedList), car la suppression nécessite simplement des mises à jour de références dans les entrées adjacentes.

35
Steve Morgan

Si vous êtes heureux de créer une nouvelle liste, vous n'avez pas besoin de définir des éléments sur null. Par exemple:

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

Cependant, vous voudrez peut-être examiner d'autres structures de données, telles que LinkedList<T> ou HashSet<T>. Cela dépend vraiment des fonctionnalités dont vous avez besoin de votre structure de données.

17
Jon Skeet

Je pense qu'un HashSet, LinkedList ou Dictionary vous fera beaucoup mieux.

13
Daniel A. White

Si l'ordre n'a pas d'importance, il existe une méthode simple 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);
    }
}

Cette solution est conviviale pour la traversée de la mémoire, donc même si vous devez d'abord trouver l'index, il sera très rapide.

Remarques:

  • La recherche de l'index d'un élément doit être O(n) car la liste doit être non triée.
  • Les listes chaînées sont lentes lors du parcours, en particulier pour les grandes collections ayant une longue durée de vie.
11
Yosef O

Vous pouvez toujours supprimer les éléments de la fin de la liste. La suppression de la liste est O(1) lorsqu'elle est effectuée sur le dernier élément, car elle ne fait que décompter. Il n'y a pas de déplacement des éléments suivants impliqués. (c'est la raison pour laquelle la suppression de la liste est généralement O(n))

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

Ou vous pouvez faire ceci:

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

...

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

J'ai trouvé que lorsque je traitais de grandes listes, c'est souvent plus rapide. La vitesse de suppression et la recherche du bon élément à supprimer dans le dictionnaire font plus que compenser la création du dictionnaire. Quelques choses cependant, la liste originale doit avoir des valeurs uniques, et je ne pense pas que l'ordre soit garanti une fois que vous avez terminé.

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

Ok essayez RemoveAll utilisé comme ça

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

cela ne fait que deux boucles et élimine 100 000 objets

Ma sortie pour ce code:

00:00:00.0099495 
00:00:00.1945987 
1000000

Mis à jour pour essayer un 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;
    }

Cela me donne:

00:00:00.0131586
00:00:00.1454723
100000

00:00:00.3459420
00:00:00.2122574
100000
3
Alex L

Les listes sont plus rapides que les LinkedLists jusqu'à ce que n devienne vraiment gros. Cela s'explique par le fait que les soi-disant erreurs de cache se produisent beaucoup plus souvent avec LinkedLists qu'avec les Lists. Les recherches de mémoire sont assez chères. Lorsqu'une liste est implémentée sous forme de tableau, la CPU peut charger un groupe de données à la fois, car elle sait que les données requises sont stockées les unes à côté des autres. Cependant, une liste chaînée ne donne aucune indication au processeur quant aux données requises par la suite, ce qui l'oblige à effectuer beaucoup plus de recherches dans la mémoire. Au fait. Avec mémoire à terme, je veux dire RAM.

Pour plus de détails, consultez: https://jackmott.github.io/programming/2016/08/20/when-bigo-foolsya.html

2

Les autres réponses (et la question elle-même) offrent différents moyens de traiter ce "slug" (bogue de lenteur) à l'aide des classes .NET Framework intégrées.

Toutefois, si vous souhaitez passer à une bibliothèque tierce, vous pouvez obtenir de meilleures performances simplement en modifiant la structure de données et en laissant votre code inchangé, à l'exception du type de liste.

Les bibliothèques Loyc Core comprennent deux types qui fonctionnent de la même manière que List<T> mais peut supprimer des éléments plus rapidement:

  • DList<T> est une structure de données simple qui vous donne une accélération 2x par rapport à List<T> lors de la suppression d’articles de lieux aléatoires
  • AList<T> est une structure de données sophistiquée qui accélère considérablement List<T> lorsque vos listes sont très longues (mais peuvent être plus lentes si votre liste est courte).
1
Qwertie