Risposte:
Si prega di leggere i commenti a questa risposta. Le persone affermano che non ho eseguito test adeguati. Sono d'accordo che questa non dovrebbe essere una risposta accettata. Mentre stavo imparando, ho fatto alcuni test e ho avuto voglia di condividerli.
Ho trovato risultati interessanti:
// 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;
Anche se essenzialmente accedi ai dati, è molto più lento !! Dico di non usare mai un Elenco collegato.
Ecco un altro confronto che esegue molti inserimenti (prevediamo di inserire un elemento in mezzo all'elenco)
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;
Quindi, solo se hai intenzione di inserire più elementi e hai anche da qualche parte il riferimento di dove intendi inserire l'elemento, usa un elenco collegato. Solo perché devi inserire molti elementi, non è più veloce perché la ricerca della posizione in cui desideri inserirli richiede tempo.
list.AddLast(a);
negli ultimi due esempi di LinkedList? Lo faccio una volta prima del ciclo, come list.AddLast(new Temp(1,1,1,1));
nel prossimo ultimo Elenco collegato, ma sembra (per me) come se tu stessi aggiungendo il doppio di oggetti Temp nei loop stessi. (E quando ricontrollo me stesso con un'app di test , abbastanza sicuro, il doppio nella LinkedList.)
I say never use a linkedList.
è imperfetto come rivela il tuo post successivo. Potresti volerlo modificare. 2) Cosa stai facendo? Istanziazione, aggiunta ed enumerazione in un unico passaggio? Per lo più, l'istanza e l'enumerazione non sono ciò di cui i ppl sono preoccupati, quelli sono passi di una volta. In particolare, il tempismo degli inserti e delle aggiunte darebbe un'idea migliore. 3) Soprattutto, stai aggiungendo più del necessario a un elenco di link. Questo è un confronto sbagliato. Diffonde un'idea sbagliata sull'elenco collegato.
Nella maggior parte dei casi, List<T>
è più utile. LinkedList<T>
avrà un costo inferiore quando si aggiungono / rimuovono gli elementi in mezzo alla lista, mentre List<T>
possono solo aggiungere / rimuovere a buon mercato alla fine della lista.
LinkedList<T>
è più efficace solo se si accede a dati sequenziali (in avanti o all'indietro) - l'accesso casuale è relativamente costoso poiché deve camminare ogni volta sulla catena (quindi perché non ha un indicizzatore). Tuttavia, poiché a List<T>
è essenzialmente solo un array (con un wrapper), l'accesso casuale va bene.
List<T>
offre anche un sacco di metodi di supporto - Find
, ToArray
, ecc; tuttavia, questi sono disponibili anche LinkedList<T>
con .NET 3.5 / C # 3.0 tramite metodi di estensione, quindi è un fattore meno importante.
List<T>
e T[]
fallirà per essere troppo grosso (tutta una lastra), LinkedList<T>
gemerà per essere troppo granulare (lastra per elemento).
Pensare a un elenco collegato come un elenco può essere un po 'fuorviante. È più come una catena. In effetti, in .NET, LinkedList<T>
non implementa nemmenoIList<T>
. Non esiste un vero concetto di indice in un elenco collegato, anche se può sembrare che esista. Certamente nessuno dei metodi forniti nella classe accetta gli indici.
Le liste collegate possono essere singolarmente collegate o doppiamente collegate. Questo si riferisce al fatto che ogni elemento nella catena abbia un collegamento solo al successivo (singolarmente collegato) o ad entrambi gli elementi precedenti / successivi (doppiamente collegati). LinkedList<T>
è doppiamente collegato.
Internamente, List<T>
è supportato da un array. Ciò fornisce una rappresentazione molto compatta in memoria. Al contrario, LinkedList<T>
comporta memoria aggiuntiva per memorizzare i collegamenti bidirezionali tra elementi successivi. Quindi l'impronta di memoria di a LinkedList<T>
sarà generalmente più grande di for List<T>
(con l'avvertenza che List<T>
può avere elementi di array interni inutilizzati per migliorare le prestazioni durante le operazioni di aggiunta).
Hanno anche diverse caratteristiche prestazionali:
LinkedList<T>.AddLast(item)
tempo costanteList<T>.Add(item)
tempo costante ammortizzato, caso peggiore lineareLinkedList<T>.AddFirst(item)
tempo costanteList<T>.Insert(0, item)
tempo lineareLinkedList<T>.AddBefore(node, item)
tempo costanteLinkedList<T>.AddAfter(node, item)
tempo costanteList<T>.Insert(index, item)
tempo lineareLinkedList<T>.Remove(item)
tempo lineareLinkedList<T>.Remove(node)
tempo costanteList<T>.Remove(item)
tempo lineareList<T>.RemoveAt(index)
tempo lineareLinkedList<T>.Count
tempo costanteList<T>.Count
tempo costanteLinkedList<T>.Contains(item)
tempo lineareList<T>.Contains(item)
tempo lineareLinkedList<T>.Clear()
tempo lineareList<T>.Clear()
tempo lineareCome puoi vedere, sono per lo più equivalenti. In pratica, l'API diLinkedList<T>
è più ingombrante da usare e i dettagli delle sue esigenze interne si riversano nel tuo codice.
Tuttavia, se è necessario eseguire molti inserimenti / rimozioni all'interno di un elenco, offre un tempo costante. List<T>
offre un tempo lineare, poiché gli elementi aggiuntivi nell'elenco devono essere rimescolati dopo l'inserimento / la rimozione.
Gli elenchi collegati consentono di inserire o eliminare molto rapidamente un membro dell'elenco. Ogni membro in un elenco collegato contiene un puntatore al membro successivo nell'elenco in modo da inserire un membro nella posizione i:
Lo svantaggio di un elenco collegato è che l'accesso casuale non è possibile. Per accedere a un membro è necessario attraversare l'elenco fino a quando non viene trovato il membro desiderato.
La mia risposta precedente non era abbastanza precisa. Come veramente è stato orribile: D Ma ora posso pubblicare una risposta molto più utile e corretta.
Ho fatto alcuni test aggiuntivi. Puoi trovarne l'origine tramite il seguente link e ricontrollarlo sul tuo ambiente da solo: https://github.com/ukushu/DataStructuresTestsAndOther.git
Brevi risultati:
La matrice deve usare:
L'elenco deve usare:
LinkedList deve usare:
Più dettagli:
LinkedList<T>
internamente non è un elenco in .NET. Non è nemmeno implementare IList<T>
. Ed è per questo che mancano indici e metodi relativi agli indici.
LinkedList<T>
è una raccolta basata su puntatore nodo. In .NET è in implementazione doppiamente collegata. Ciò significa che gli elementi precedenti / successivi hanno un collegamento all'elemento corrente. E i dati sono frammentati: diversi oggetti della lista possono trovarsi in diversi punti della RAM. Inoltre ci sarà più memoria utilizzata per LinkedList<T>
che per List<T>
o Array.
List<T>
in .Net è l'alternativa di Java di ArrayList<T>
. Ciò significa che si tratta di wrapper di array. Quindi è allocato in memoria come un blocco contiguo di dati. Se la dimensione dei dati allocati supera 85000 byte, verrà spostata nell'heap di oggetti di grandi dimensioni. A seconda delle dimensioni, ciò può portare alla frammentazione dell'heap (una forma lieve di perdita di memoria). Ma allo stesso tempo se la dimensione <85000 byte - questo fornisce una rappresentazione molto compatta e ad accesso rapido in memoria.
Il blocco singolo contiguo è preferito per le prestazioni di accesso casuale e il consumo di memoria, ma per le raccolte che devono cambiare regolarmente dimensione una struttura come una matrice deve generalmente essere copiata in una nuova posizione mentre un elenco collegato deve solo gestire la memoria per il nuovo inserito / nodi eliminati.
La differenza tra List e LinkedList risiede nella loro implementazione sottostante. L'elenco è una raccolta basata su array (ArrayList). LinkedList è una raccolta basata su puntatore nodo (LinkedListNode). A livello di API, entrambi sono praticamente uguali poiché entrambi implementano lo stesso set di interfacce come ICollection, IEnumerable, ecc.
La differenza fondamentale viene quando le prestazioni contano. Ad esempio, se si sta implementando l'elenco con un'operazione "INSERT" pesante, LinkedList supera l'elenco. Dato che LinkedList può farlo in O (1), potrebbe essere necessario che List aumenti le dimensioni dell'array sottostante. Per ulteriori informazioni / dettagli, è possibile leggere la differenza algoritmica tra LinkedList e le strutture di dati dell'array. http://it.wikipedia.org/wiki/Linked_list e Array
Spero che questo aiuto,
Add
è sempre alla fine dell'array esistente. List
è "abbastanza buono", anche se non O (1). Il grave problema si verifica se hai bisogno di molti messaggi Add
che non sono alla fine. Marc sta sottolineando che la necessità di spostare i dati esistenti ogni volta che si inserisce (non solo quando è necessario il ridimensionamento) è un costo delle prestazioni più sostanziale di List
.
Il vantaggio principale degli elenchi collegati rispetto agli array è che i collegamenti ci offrono la possibilità di riorganizzare gli elementi in modo efficiente. Sedgewick, p. 91
Una circostanza comune per usare LinkedList è la seguente:
Supponiamo di voler rimuovere molte determinate stringhe da un elenco di stringhe di grandi dimensioni, ad esempio 100.000. Le stringhe da rimuovere possono essere cercate in HashSet dic e si ritiene che l'elenco delle stringhe contenga tra le 30.000 e le 60.000 stringhe da rimuovere.
Allora qual è il miglior tipo di elenco per memorizzare le 100.000 stringhe? La risposta è LinkedList. Se vengono archiviati in un ArrayList, l'iterazione su di esso e la rimozione delle stringhe corrispondenti richiederebbe miliardi di operazioni, mentre impiega circa 100.000 operazioni utilizzando un iteratore e il metodo 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
per rimuovere gli elementi da a List
senza spostare molti oggetti in giro, oppure usare Where
da LINQ per creare un secondo elenco. L'uso di un LinkedList
qui finisce tuttavia per consumare una quantità di memoria notevolmente maggiore rispetto ad altri tipi di raccolte e la perdita della localizzazione della memoria significa che sarà notevolmente più lenta da iterare, rendendolo un po 'peggio di un List
.
RemoveAll
equivalente in Java.
RemoveAll
non è disponibile per List
, potresti fare un algoritmo di "compattazione", che assomiglierebbe al loop di Tom, ma con due indici e la necessità di spostare gli elementi da tenere uno alla volta nell'array interno dell'elenco. L'efficienza è O (n), uguale all'algoritmo di Tom per LinkedList
. In entrambe le versioni, domina il tempo per calcolare la chiave HashSet per le stringhe. Questo non è un buon esempio di quando usare LinkedList
.
Quando hai bisogno di accesso indicizzato, ordinamento (e dopo questa ricerca binaria) incorporato e metodo "ToArray ()", dovresti usare Elenco.
Essenzialmente, a List<>
in .NET è un wrapper su un array . A LinkedList<>
è un elenco collegato . Quindi la domanda si riduce a: qual è la differenza tra un array e un elenco collegato e quando dovrebbe essere usato un array invece di un elenco collegato. Probabilmente i due fattori più importanti nella tua decisione di quale utilizzare sarebbero dovuti a:
Questo è adattato da Tono Nam risposta accettata di correggendo alcune misurazioni errate al suo interno.
Il test:
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);
}
E il codice:
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();
}
}
}
Puoi vedere che i risultati sono conformi alle prestazioni teoriche che altri hanno documentato qui. Abbastanza chiaro: LinkedList<T>
guadagna molto tempo in caso di inserzioni. Non ho testato la rimozione dalla metà dell'elenco, ma il risultato dovrebbe essere lo stesso. Ovviamente List<T>
ha altre aree in cui si comporta in modo migliore come O (1) accesso casuale.
Utilizzare LinkedList<>
quando
Token Stream
,.Per tutto il resto, è meglio usare List<>
.
LinkedListNode<T>
oggetti nel codice. Se riesci a farlo, allora è molto meglio dell'uso List<T>
, specialmente per elenchi molto lunghi in cui sono frequenti inserimenti / rimozioni.
node.Value
quando vuoi l'elemento originale). Quindi riscrivi l'algoritmo per lavorare con i nodi, non con i valori non elaborati.
Sono d'accordo con la maggior parte del punto sopra esposto. E sono anche d'accordo sul fatto che Elenco sembri una scelta più ovvia nella maggior parte dei casi.
Ma voglio solo aggiungere che ci sono molti casi in cui LinkedList è una scelta di gran lunga migliore rispetto a List per una migliore efficienza.
Spero che qualcuno possa trovare utili questi commenti.
Così tante risposte medie qui ...
Alcune implementazioni di elenchi collegati utilizzano blocchi sottostanti di nodi pre allocati. In caso contrario, il tempo costante / tempo lineare è meno rilevante in quanto le prestazioni della memoria saranno scarse e le prestazioni della cache peggioreranno ulteriormente.
Utilizzare gli elenchi collegati quando
1) Volete la sicurezza del thread. Puoi creare algos thread thread migliori. I costi di blocco domineranno un elenco di stili simultanei.
2) Se si dispone di una grande coda come le strutture e si desidera rimuovere o aggiungere ovunque ma alla fine continuamente. > Esistono 100.000 elenchi ma non sono così comuni.
Ho posto una domanda simile relativa alle prestazioni della collezione LinkedList e ho scoperto che l'attrezzo C # di Deque di Steven Cleary era una soluzione. A differenza della collezione Queue, Deque consente di spostare gli oggetti avanti / indietro davanti e dietro. È simile all'elenco collegato, ma con prestazioni migliorate.
Deque
è "simile a lista collegata, ma con prestazioni migliorate" . Si prega di qualificare questa affermazione: Deque
è la prestazione migliore rispetto LinkedList
, per il vostro codice specifico . Seguendo il tuo link, vedo che due giorni dopo hai appreso da Ivan Stoev che questa non era un'inefficienza di LinkedList, ma un'inefficienza nel tuo codice. (E anche se fosse stata un'inefficienza di LinkedList, ciò non giustificherebbe un'affermazione generale secondo cui Deque è più efficiente; solo in casi specifici.)