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.
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.
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.
Je pense qu'un HashSet
, LinkedList
ou Dictionary
vous fera beaucoup mieux.
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:
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);
Ou vous pouvez faire ceci:
List<int> listA;
List<int> listB;
...
List<int> resultingList = listA.Except(listB);
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();
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
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
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éatoiresAList<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).