modificar - listas c#
Cómo quitar rápidamente elementos de una lista (10)
Descubrí que cuando se trata de listas grandes, a menudo es más rápido. La velocidad de Eliminar y encontrar el elemento correcto en el diccionario para eliminar, más que compensa la creación del diccionario. Un par de cosas, sin embargo, la lista original tiene que tener valores únicos, y no creo que la orden esté garantizada una vez que haya terminado.
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();
Estoy buscando una manera de eliminar rápidamente elementos de una List<T>
C # List<T>
. La documentación indica que las List.Remove()
y List.RemoveAt()
son ambas O(n)
Esto está afectando seriamente mi aplicación.
Escribí algunos métodos de eliminación diferentes y los probé todos en una List<String>
con 500,000 elementos. Los casos de prueba se muestran a continuación ...
Visión de conjunto
Escribí un método que generaría una lista de cadenas que simplemente contiene representaciones de cadenas de cada número ("1", "2", "3", ...). Luego intenté remove
cada 5º elemento de la lista. Aquí está el método usado para generar la lista:
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;
}
Prueba 1: RemoveAt ()
Aquí está la prueba que usé para probar el método RemoveAt()
.
private void RemoveTest1(ref List<String> list)
{
for (int i = 0; i < list.Count; i++)
if (i % 5 == 0)
list.RemoveAt(i);
}
Prueba 2: Eliminar ()
Aquí está la prueba que usé para probar el método 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]);
}
Prueba 3: establecer en nulo, ordenar, luego EliminarRango
En esta prueba, hice un bucle en la lista una vez y establecí los elementos que se eliminarán en null
. Luego, ordené la lista (por lo que null estaría en la parte superior) y eliminé todos los elementos en la parte superior que estaban configurados como nulos. NOTA: Esto reordenó mi lista, por lo que debo volver a colocarla en el orden correcto.
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...
}
Prueba 4: cree una nueva lista y agregue todos los valores "buenos" a la nueva lista
En esta prueba, creé una nueva lista y agregué todos mis elementos de mantenimiento a la nueva lista. Luego, puse todos estos elementos en la lista original.
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);
}
Prueba 5: establecer en nulo y luego FindAll ()
En esta prueba, configuré null
todos los elementos que se eliminarán, luego usé la función FindAll()
para encontrar todos los elementos que no son 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);
}
Prueba 6: establecer en nulo y luego quitar todo ()
En esta prueba, configuré null
todos los elementos que se eliminarán, luego usé la función RemoveAll()
para eliminar todos los elementos que no son 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);
}
Aplicación de cliente y salidas
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();
Resultados
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()
Notas y comentarios
Las dos primeras pruebas en realidad no eliminan cada 5º elemento de la lista, porque la lista se está reordenando después de cada eliminación. De hecho, de 500,000 artículos, solo 83,334 fueron eliminados (deberían haber sido 100,000). Estoy de acuerdo con esto - claramente los métodos Remove () / RemoveAt () no son una buena idea de todos modos.
Aunque traté de eliminar el quinto elemento de la lista, en realidad no habrá tal patrón. Las entradas que se eliminarán serán aleatorias.
Aunque utilicé
List<String>
en este ejemplo, ese no siempre será el caso. Podría ser unaList<Anything>
No poner los elementos en la lista para empezar no es una opción.
Los otros métodos (3 - 6) funcionaron mucho mejor, comparativamente , sin embargo, estoy un poco preocupado: en 3, 5 y 6 me vi obligado a establecer un valor
null
, y luego eliminar todos los elementos de acuerdo con este centinela . No me gusta ese enfoque porque puedo imaginar un escenario en el que uno de los elementos de la lista podría sernull
y se eliminaría involuntariamente.
Mi pregunta es: ¿Cuál es la mejor manera de eliminar rápidamente muchos elementos de una List<T>
? La mayoría de los enfoques que he intentado son realmente feos y potencialmente peligrosos para mí. ¿Es una List
la estructura de datos incorrecta?
En este momento, me inclino por crear una nueva lista y agregar los buenos artículos a la nueva lista, pero parece que debería haber una mejor manera.
La lista no es una estructura de datos eficiente en lo que respecta a la eliminación. Sería mejor utilizar una lista de doble enlace (LinkedList) ya que la eliminación simplemente requiere actualizaciones de referencia en las entradas adyacentes.
Las listas son más rápidas que las LinkedLists hasta que n sea realmente grande. La razón de esto es porque los llamados fallos de caché se producen con bastante frecuencia utilizando LinkedLists que listas. Las búsquedas de memoria son bastante costosas. Como una lista se implementa como una matriz, la CPU puede cargar una gran cantidad de datos a la vez porque sabe que los datos necesarios se almacenan uno al lado del otro. Sin embargo, una lista vinculada no le da a la CPU ninguna pista acerca de qué datos se necesitan a continuación, lo que obliga a la CPU a realizar más búsquedas en la memoria. Por cierto. Con memoria de términos me refiero a RAM.
Para obtener más detalles, consulte: https://jackmott.github.io/programming/2016/08/20/when-bigo-foolsya.html
Las otras respuestas (y la pregunta en sí misma) ofrecen varias formas de lidiar con este "slug" (error de lentitud) utilizando las clases integradas de .NET Framework.
Pero si está dispuesto a cambiar a una biblioteca de terceros, puede obtener un mejor rendimiento simplemente cambiando la estructura de datos y dejando el código sin cambios, excepto para el tipo de lista.
Las bibliotecas de Loyc Core incluyen dos tipos que funcionan de la misma manera que List<T>
pero pueden eliminar elementos más rápidamente:
-
DList<T>
es una estructura de datos simple que le da una aceleración de 2 veces sobreList<T>
al eliminar elementos de ubicaciones aleatorias -
AList<T>
es una estructura de datos sofisticada que le da una gran aceleración sobreList<T>
cuando sus listas son muy largas (pero pueden ser más lentas cuando la lista es corta).
O podrías hacer esto:
List<int> listA;
List<int> listB;
...
List<int> resultingList = listA.Except(listB);
Ok prueba RemoveAll usado así
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;
}
esto solo se repite dos veces y elimina exactamente 100 000 elementos
Mi salida para este código:
00:00:00.0099495
00:00:00.1945987
1000000
Actualizado para probar 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;
}
Esto me da:
00:00:00.0131586
00:00:00.1454723
100000
00:00:00.3459420
00:00:00.2122574
100000
Si el orden no importa, entonces hay un método 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);
}
}
Esta solución es amigable para el recorrido de la memoria, por lo que incluso si necesita encontrar primero el índice, será muy rápido.
Notas:
- Encontrar el índice de un artículo debe ser O (n) ya que la lista debe estar desordenada.
- Las listas vinculadas son lentas en el cruce, especialmente para grandes colecciones con una larga vida útil.
Si está contento creando una nueva lista, no tiene que pasar por establecer los elementos a nulo. Por ejemplo:
// 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();
Sin embargo, es posible que desee ver estructuras de datos alternativas, como LinkedList<T>
o HashSet<T>
. Realmente depende de qué características necesita de su estructura de datos.
Siempre puedes eliminar los elementos del final de la lista. La eliminación de lista es O (1) cuando se realiza en el último elemento, ya que todo lo que hace es recuento de decrementos. No hay desplazamiento de los siguientes elementos involucrados. (que es el motivo por el cual la eliminación de la lista es O (n) en general)
for (int i = list.Count - 1; i >= 0; --i)
list.RemoveAt(i);
Siento que un HashSet
, LinkedList
o Dictionary
te hará mucho mejor.