Respuestas:
Por favor lea los comentarios a esta respuesta. La gente dice que no hice las pruebas adecuadas. Estoy de acuerdo en que esta no debería ser una respuesta aceptada. Mientras aprendía, hice algunas pruebas y sentí ganas de compartirlas.
Encontré resultados interesantes:
// Temporary class to show the example
class Temp
{
public decimal A, B, C, D;
public Temp(decimal a, decimal b, decimal c, decimal d)
{
A = a; B = b; C = c; D = d;
}
}
LinkedList<Temp> list = new LinkedList<Temp>();
for (var i = 0; i < 12345678; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
List<Temp> list = new List<Temp>(); // 2.4 seconds
for (var i = 0; i < 12345678; i++)
{
var a = new Temp(i, i, i, i);
list.Add(a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
¡Incluso si solo accedes a los datos esencialmente, es mucho más lento! Yo digo que nunca uses una lista enlazada.
Aquí hay otra comparación que realiza muchas inserciones (planeamos insertar un elemento en el medio de la lista)
LinkedList<Temp> list = new LinkedList<Temp>();
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
var curNode = list.First;
for (var k = 0; k < i/2; k++) // In order to insert a node at the middle of the list we need to find it
curNode = curNode.Next;
list.AddAfter(curNode, a); // Insert it after
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
List<Temp> list = new List<Temp>();
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.Insert(i / 2, a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
list.AddLast(new Temp(1,1,1,1));
var referenceNode = list.First;
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
list.AddBefore(referenceNode, a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
Entonces, solo si planea insertar varios elementos y también en algún lugar tiene la referencia de dónde planea insertar el elemento, use una lista vinculada. El hecho de que tenga que insertar muchos elementos no lo hace más rápido, ya que la búsqueda de la ubicación en la que desea insertarlo lleva tiempo.
list.AddLast(a);
en los últimos dos ejemplos de LinkedList? Lo hago una vez antes del bucle, como list.AddLast(new Temp(1,1,1,1));
en la penúltima LinkedList, pero (para mí) parece que estás agregando el doble de objetos Temp en los mismos bucles. (Y cuando vuelvo a verificar con una aplicación de prueba ,
I say never use a linkedList.
es defectuoso como revela su publicación posterior. Es posible que desee editarlo. 2) ¿Qué estás cronometrando? ¿Instanciación, suma y enumeración en un solo paso? Principalmente, la creación de instancias y la enumeración no son lo que preocupa a las personas, son pasos únicos. Específicamente el tiempo de las inserciones y adiciones daría una mejor idea. 3) Lo más importante es que está agregando más de lo requerido a una lista vinculada. Esta es una comparación incorrecta. Difunde una idea equivocada sobre la lista vinculada.
En la mayoría de los casos, List<T>
es más útil. LinkedList<T>
tendrá menos costo al agregar / eliminar elementos en el medio de la lista, mientras List<T>
que solo puede agregar / eliminar de forma económica al final de la lista.
LinkedList<T>
solo es más eficiente si está accediendo a datos secuenciales (ya sea hacia adelante o hacia atrás): el acceso aleatorio es relativamente costoso, ya que debe recorrer la cadena cada vez (por lo tanto, no tiene un indexador). Sin embargo, dado que a List<T>
es esencialmente solo una matriz (con un contenedor), el acceso aleatorio está bien.
List<T>
También ofrece una gran cantidad de métodos de apoyo - Find
, ToArray
, etc; sin embargo, estos también están disponibles para LinkedList<T>
.NET 3.5 / C # 3.0 a través de métodos de extensión, por lo que eso es menos importante.
List<T>
y T[]
fallará por ser demasiado grueso (una sola losa), LinkedList<T>
llorará por ser demasiado granular (losa por elemento).
Pensar en una lista vinculada como una lista puede ser un poco engañoso. Es más como una cadena. De hecho, en .NET, LinkedList<T>
ni siquiera se implementa IList<T>
. No existe un concepto real de índice en una lista vinculada, aunque parezca que sí. Ciertamente, ninguno de los métodos proporcionados en la clase acepta índices.
Las listas vinculadas pueden estar individualmente vinculadas o doblemente vinculadas. Esto se refiere a si cada elemento de la cadena tiene un enlace solo con el siguiente (vinculado individualmente) o con los elementos anteriores / siguientes (doblemente vinculados). LinkedList<T>
está doblemente vinculado
Internamente, List<T>
está respaldado por una matriz. Esto proporciona una representación muy compacta en la memoria. Por el contrario, LinkedList<T>
implica memoria adicional para almacenar los enlaces bidireccionales entre elementos sucesivos. Por lo tanto, la huella de memoria de a LinkedList<T>
generalmente será mayor que para List<T>
(con la advertencia de que List<T>
puede tener elementos de matriz internos no utilizados para mejorar el rendimiento durante las operaciones de adición).
También tienen diferentes características de rendimiento:
LinkedList<T>.AddLast(item)
tiempo constanteList<T>.Add(item)
tiempo constante amortizado, peor caso linealLinkedList<T>.AddFirst(item)
tiempo constanteList<T>.Insert(0, item)
tiempo linealLinkedList<T>.AddBefore(node, item)
tiempo constanteLinkedList<T>.AddAfter(node, item)
tiempo constanteList<T>.Insert(index, item)
tiempo linealLinkedList<T>.Remove(item)
tiempo linealLinkedList<T>.Remove(node)
tiempo constanteList<T>.Remove(item)
tiempo linealList<T>.RemoveAt(index)
tiempo linealLinkedList<T>.Count
tiempo constanteList<T>.Count
tiempo constanteLinkedList<T>.Contains(item)
tiempo linealList<T>.Contains(item)
tiempo linealLinkedList<T>.Clear()
tiempo linealList<T>.Clear()
tiempo linealComo puede ver, son en su mayoría equivalentes. En la práctica, la API de LinkedList<T>
es más engorrosa de usar y los detalles de sus necesidades internas se derraman en su código.
Sin embargo, si necesita hacer muchas inserciones / eliminaciones desde una lista, ofrece tiempo constante. List<T>
ofrece tiempo lineal, ya que los elementos adicionales en la lista se deben barajar después de la inserción / eliminación.
Las listas vinculadas proporcionan una inserción o eliminación muy rápida de un miembro de la lista. Cada miembro de una lista vinculada contiene un puntero al siguiente miembro de la lista para insertar un miembro en la posición i:
La desventaja de una lista vinculada es que el acceso aleatorio no es posible. Acceder a un miembro requiere recorrer la lista hasta que se encuentre el miembro deseado.
Mi respuesta anterior no fue lo suficientemente precisa. Como realmente fue horrible: D Pero ahora puedo publicar una respuesta mucho más útil y correcta.
Hice algunas pruebas adicionales. Puede encontrar su fuente en el siguiente enlace y volver a verificarlo en su entorno: https://github.com/ukushu/DataStructuresTestsAndOther.git
Resultados cortos:
Matriz necesita usar:
Lista necesita usar:
LinkedList necesita usar:
Más detalles:
LinkedList<T>
internamente no es una Lista en .NET. Incluso no se implementa IList<T>
. Y es por eso que hay índices y métodos ausentes relacionados con los índices.
LinkedList<T>
es una colección basada en puntero de nodo. En .NET está en implementación doblemente vinculada. Esto significa que los elementos anteriores / siguientes tienen un enlace al elemento actual. Y los datos están fragmentados: se pueden ubicar diferentes objetos de lista en diferentes lugares de RAM. También habrá más memoria utilizada para LinkedList<T>
que para List<T>
o Array.
List<T>
en .Net es la alternativa de Java de ArrayList<T>
. Esto significa que este es un contenedor de matriz. Por lo tanto, se asigna en la memoria como un bloque contiguo de datos. Si el tamaño de datos asignado supera los 85000 bytes, se moverá a Montón de objetos grandes. Dependiendo del tamaño, esto puede conducir a la fragmentación del montón (una forma leve de pérdida de memoria). Pero al mismo tiempo si el tamaño es <85000 bytes, esto proporciona una representación muy compacta y de acceso rápido en la memoria.
Se prefiere un bloque contiguo único para el rendimiento de acceso aleatorio y el consumo de memoria, pero para las colecciones que necesitan cambiar regularmente el tamaño, una estructura como una matriz generalmente debe copiarse en una nueva ubicación, mientras que una lista vinculada solo necesita administrar la memoria para el recién insertado / nodos eliminados.
La diferencia entre List y LinkedList radica en su implementación subyacente. La lista es una colección basada en matriz (ArrayList). LinkedList es una colección basada en puntero de nodo (LinkedListNode). En el uso de nivel API, ambos son prácticamente iguales ya que ambos implementan el mismo conjunto de interfaces como ICollection, IEnumerable, etc.
La diferencia clave viene cuando el rendimiento es importante. Por ejemplo, si está implementando la lista que tiene una operación pesada "INSERTAR", LinkedList supera a List. Dado que LinkedList puede hacerlo en tiempo O (1), pero List puede necesitar expandir el tamaño de la matriz subyacente. Para obtener más información / detalles, es posible que desee leer sobre la diferencia algorítmica entre LinkedList y las estructuras de datos de matriz. http://en.wikipedia.org/wiki/Linked_list and Array
Espero que esto ayude,
Add
que siempre está al final de la matriz existente. List
es "suficientemente bueno" en eso, incluso si no es O (1). El problema grave se produce si necesita muchos Add
correos electrónicos que no están al final. Marc señala que la necesidad de mover datos existentes cada vez que inserte (no solo cuando se necesita cambiar el tamaño) es un costo de rendimiento más sustancial List
.
La principal ventaja de las listas vinculadas sobre las matrices es que los enlaces nos brindan la capacidad de reorganizar los elementos de manera eficiente. Sedgewick, pág. 91 91
Una circunstancia común para usar LinkedList es así:
Suponga que desea eliminar muchas cadenas determinadas de una lista de cadenas con un tamaño grande, digamos 100,000. Las cadenas para eliminar se pueden buscar en HashSet dic, y se cree que la lista de cadenas contiene entre 30,000 y 60,000 cadenas para eliminar.
Entonces, ¿cuál es el mejor tipo de lista para almacenar las 100.000 cadenas? La respuesta es LinkedList. Si se almacenan en una ArrayList, la iteración sobre ella y la eliminación de cadenas coincidentes requeriría miles de millones de operaciones, mientras que solo se necesitan alrededor de 100,000 operaciones mediante el uso de un iterador y el método remove ().
LinkedList<String> strings = readStrings();
HashSet<String> dic = readDic();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
String string = iterator.next();
if (dic.contains(string))
iterator.remove();
}
RemoveAll
para eliminar los elementos de a List
sin mover muchos elementos, o usar Where
de LINQ para crear una segunda lista. LinkedList
Sin embargo, el uso de un aquí termina consumiendo dramáticamente más memoria que otros tipos de colecciones y la pérdida de la localidad de memoria significa que será notablemente más lento para iterar, lo que lo hace un poco peor que a List
.
RemoveAll
equivalente en Java.
RemoveAll
no está disponible para List
, podría hacer un algoritmo de "compactación", que se vería como el bucle de Tom, pero con dos índices y la necesidad de mover elementos para mantenerse uno a la vez en la matriz interna de la lista. La eficiencia es O (n), lo mismo que para el algoritmo de Tom LinkedList
. En ambas versiones, domina el tiempo para calcular la clave HashSet para las cadenas. Este no es un buen ejemplo de cuándo usarlo LinkedList
.
Cuando necesite acceso indexado incorporado, clasificación (y después de esta búsqueda binaria) y método "ToArray ()", debe usar List.
Esencialmente, un List<>
en .NET es un contenedor sobre una matriz . A LinkedList<>
es una lista vinculada . Entonces, la pregunta se reduce a cuál es la diferencia entre una matriz y una lista vinculada, y cuándo debe usarse una matriz en lugar de una lista vinculada. Probablemente los dos factores más importantes en su decisión de cuál usar se reducirían a:
Esto está adaptado de la respuesta aceptada de Tono Nam corrigiendo algunas mediciones incorrectas.
La prueba:
static void Main()
{
LinkedListPerformance.AddFirst_List(); // 12028 ms
LinkedListPerformance.AddFirst_LinkedList(); // 33 ms
LinkedListPerformance.AddLast_List(); // 33 ms
LinkedListPerformance.AddLast_LinkedList(); // 32 ms
LinkedListPerformance.Enumerate_List(); // 1.08 ms
LinkedListPerformance.Enumerate_LinkedList(); // 3.4 ms
//I tried below as fun exercise - not very meaningful, see code
//sort of equivalent to insertion when having the reference to middle node
LinkedListPerformance.AddMiddle_List(); // 5724 ms
LinkedListPerformance.AddMiddle_LinkedList1(); // 36 ms
LinkedListPerformance.AddMiddle_LinkedList2(); // 32 ms
LinkedListPerformance.AddMiddle_LinkedList3(); // 454 ms
Environment.Exit(-1);
}
Y el codigo:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace stackoverflow
{
static class LinkedListPerformance
{
class Temp
{
public decimal A, B, C, D;
public Temp(decimal a, decimal b, decimal c, decimal d)
{
A = a; B = b; C = c; D = d;
}
}
static readonly int start = 0;
static readonly int end = 123456;
static readonly IEnumerable<Temp> query = Enumerable.Range(start, end - start).Select(temp);
static Temp temp(int i)
{
return new Temp(i, i, i, i);
}
static void StopAndPrint(this Stopwatch watch)
{
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}
public static void AddFirst_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Insert(0, temp(i));
watch.StopAndPrint();
}
public static void AddFirst_LinkedList()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (int i = start; i < end; i++)
list.AddFirst(temp(i));
watch.StopAndPrint();
}
public static void AddLast_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Add(temp(i));
watch.StopAndPrint();
}
public static void AddLast_LinkedList()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (int i = start; i < end; i++)
list.AddLast(temp(i));
watch.StopAndPrint();
}
public static void Enumerate_List()
{
var list = new List<Temp>(query);
var watch = Stopwatch.StartNew();
foreach (var item in list)
{
}
watch.StopAndPrint();
}
public static void Enumerate_LinkedList()
{
var list = new LinkedList<Temp>(query);
var watch = Stopwatch.StartNew();
foreach (var item in list)
{
}
watch.StopAndPrint();
}
//for the fun of it, I tried to time inserting to the middle of
//linked list - this is by no means a realistic scenario! or may be
//these make sense if you assume you have the reference to middle node
//insertion to the middle of list
public static void AddMiddle_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Insert(list.Count / 2, temp(i));
watch.StopAndPrint();
}
//insertion in linked list in such a fashion that
//it has the same effect as inserting into the middle of list
public static void AddMiddle_LinkedList1()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
LinkedListNode<Temp> evenNode = null, oddNode = null;
for (int i = start; i < end; i++)
{
if (list.Count == 0)
oddNode = evenNode = list.AddLast(temp(i));
else
if (list.Count % 2 == 1)
oddNode = list.AddBefore(evenNode, temp(i));
else
evenNode = list.AddAfter(oddNode, temp(i));
}
watch.StopAndPrint();
}
//another hacky way
public static void AddMiddle_LinkedList2()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start + 1; i < end; i += 2)
list.AddLast(temp(i));
for (int i = end - 2; i >= 0; i -= 2)
list.AddLast(temp(i));
watch.StopAndPrint();
}
//OP's original more sensible approach, but I tried to filter out
//the intermediate iteration cost in finding the middle node.
public static void AddMiddle_LinkedList3()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
{
if (list.Count == 0)
list.AddLast(temp(i));
else
{
watch.Stop();
var curNode = list.First;
for (var j = 0; j < list.Count / 2; j++)
curNode = curNode.Next;
watch.Start();
list.AddBefore(curNode, temp(i));
}
}
watch.StopAndPrint();
}
}
}
Puede ver que los resultados están de acuerdo con el rendimiento teórico que otros han documentado aquí. Muy claro: LinkedList<T>
gana mucho tiempo en caso de inserciones. No he probado la eliminación del medio de la lista, pero el resultado debería ser el mismo. Por supuesto, List<T>
tiene otras áreas donde funciona mucho mejor como el acceso aleatorio O (1).
Usar LinkedList<>
cuando
Token Stream
.Para todo lo demás, es mejor usarlo List<>
.
LinkedListNode<T>
objetos en su código. Si puede hacer eso, entonces es mucho mejor que usar List<T>
, especialmente para listas muy largas donde las inserciones / extracciones son frecuentes.
node.Value
cuando quiere el elemento original). Entonces reescribe el algoritmo para trabajar con nodos, no con valores sin formato.
Estoy de acuerdo con la mayoría de los puntos mencionados anteriormente. Y también estoy de acuerdo en que List parece una opción más obvia en la mayoría de los casos.
Pero, solo quiero agregar que hay muchas instancias en las que LinkedList es una opción mucho mejor que List para una mejor eficiencia.
Espero que alguien encuentre útiles estos comentarios.
Tantas respuestas promedio aquí ...
Algunas implementaciones de listas vinculadas utilizan bloques subyacentes de nodos preasignados. Si no lo hacen, el tiempo constante / tiempo lineal es menos relevante ya que el rendimiento de la memoria será deficiente y el rendimiento de la memoria caché será aún peor.
Use listas vinculadas cuando
1) Desea seguridad del hilo. Puedes construir mejores algos seguros para hilos. Los costos de bloqueo dominarán una lista de estilos concurrentes.
2) Si tiene una cola grande como estructuras y desea eliminar o agregar en cualquier lugar excepto el final todo el tiempo. > Existen 100K listas pero no son tan comunes.
Hice una pregunta similar relacionada con el rendimiento de la colección LinkedList , y descubrí que el implemento C # de Deque de Steven Cleary era una solución. A diferencia de la colección Queue, Deque permite mover elementos hacia adelante y hacia atrás. Es similar a la lista vinculada, pero con un rendimiento mejorado.
Deque
es "similar a la lista vinculada, pero con un rendimiento mejorado" . Por favor calificar esa declaración: Deque
es un mejor rendimiento que LinkedList
, para su código específico . Siguiendo su enlace, veo que dos días después usted aprendió de Ivan Stoev que esto no era una ineficiencia de LinkedList, sino una ineficiencia en su código. (E incluso si hubiera sido una ineficiencia de LinkedList, eso no justificaría una declaración general de que Deque es más eficiente; solo en casos específicos.)