Quand est-il préférable d'utiliser un List vs un LinkedList ?
S'il vous plaît lire les commentaires à cette réponse. Les gens prétendent que je n'ai pas fait tests appropriés. Je conviens que cela ne devrait pas être une réponse acceptée. Comme j'étais En apprenant, j'ai fait des tests et j'ai eu envie de les partager.
J'ai trouvé des résultats intéressants:
// 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;
Même si vous n'accédez qu'aux données, l'essentiel est beaucoup plus lent !! Je dis ne jamais utiliser une liste liée.
Voici une autre comparaison effectuant beaucoup d'insertions (nous prévoyons d'insérer un élément au milieu de la liste)
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;
Donc, seulement si vous envisagez d'insérer plusieurs éléments et que vous aussi quelque part avez la référence de l'endroit où vous prévoyez d'insérer l'élément, puis utilisez une liste chaînée. Ce n’est pas parce que vous devez insérer beaucoup d’éléments que cela accélère, car il faut du temps pour rechercher l’endroit où vous souhaitez l’insérer.
Dans la plupart des cas, List<T>
est plus utile. LinkedList<T>
coûtera moins cher lors de l'ajout/de la suppression d'éléments au milieu de la liste, alors que List<T>
ne peut qu'insérer/supprimer à moindre coût au fin de la liste.
LinkedList<T>
n’est efficace que si vous accédez à des données séquentielles (en avant ou en arrière) - l’accès aléatoire est relativement coûteux, car il doit parcourir la chaîne à chaque fois (ce qui explique pourquoi il ne dispose pas d’un indexeur). Cependant, étant donné que List<T>
est essentiellement un tableau (avec un wrapper), l'accès aléatoire suffit.
List<T>
offre également de nombreuses méthodes de support - Find
, ToArray
, etc. Cependant, ceux-ci sont également disponibles pour LinkedList<T>
avec .NET 3.5/C # 3.0 via des méthodes d'extension - de sorte que le facteur est moins important.
Considérer une liste chaînée comme une liste peut être un peu trompeur. Cela ressemble plus à une chaîne. En fait, dans .NET, LinkedList<T>
n'implémente même pas IList<T>
. Il n'y a pas de vrai concept d'index dans une liste chaînée, même si cela peut sembler être le cas. Certes, aucune des méthodes fournies sur la classe n'accepte les index.
Les listes chaînées peuvent être liées individuellement ou doublement liées. Cela indique si chaque élément de la chaîne a un lien uniquement avec le suivant (lié individuellement) ou avec les deux éléments précédents/suivants (liés doublement). LinkedList<T>
est doublement lié.
En interne, List<T>
est soutenu par un tableau. Ceci fournit une représentation très compacte en mémoire. Inversement, LinkedList<T>
implique une mémoire supplémentaire pour stocker les liens bidirectionnels entre les éléments successifs. Ainsi, l'encombrement mémoire d'un LinkedList<T>
sera généralement plus important que pour List<T>
(avec l'avertissement que List<T>
peut avoir des éléments de tableau interne inutilisés pour améliorer les performances lors des opérations d'ajout.)
Ils ont également des caractéristiques de performance différentes:
LinkedList<T>.AddLast(item)
temps constantList<T>.Add(item)
temps constant amorti, pire cas linéaireLinkedList<T>.AddFirst(item)
temps constantList<T>.Insert(0, item)
temps linéaireLinkedList<T>.AddBefore(node, item)
temps constantLinkedList<T>.AddAfter(node, item)
temps constantList<T>.Insert(index, item)
temps linéaireLinkedList<T>.Remove(item)
temps linéaireLinkedList<T>.Remove(node)
temps constantList<T>.Remove(item)
temps linéaireList<T>.RemoveAt(index)
temps linéaireLinkedList<T>.Count
heure constanteList<T>.Count
heure constanteLinkedList<T>.Contains(item)
temps linéaireList<T>.Contains(item)
temps linéaireLinkedList<T>.Clear()
temps linéaireList<T>.Clear()
temps linéaireComme vous pouvez le constater, ils sont pour la plupart équivalents. En pratique, l’API de LinkedList<T>
est plus lourde à utiliser et les détails de ses besoins internes se répercutent dans votre code.
Toutefois, si vous devez effectuer plusieurs insertions/suppressions à partir d'une liste, la durée est constante. List<T>
offre une durée linéaire, les éléments supplémentaires de la liste devant être déplacés après l'insertion/le retrait.
Les listes chaînées permettent une insertion ou une suppression très rapide d'un membre de la liste. Chaque membre de la liste liée contient un pointeur sur le membre suivant de la liste pour insérer un membre à la position i
L'inconvénient d'une liste chaînée est qu'un accès aléatoire n'est pas possible. L'accès à un membre nécessite de parcourir la liste jusqu'à ce que le membre souhaité soit trouvé.
La différence entre List et LinkedList réside dans leur implémentation sous-jacente. La liste est une collection basée sur un tableau (ArrayList). LinkedList est une collection basée sur un nœud et un pointeur (LinkedListNode). Au niveau de l'utilisation de l'API, les deux sont à peu près les mêmes puisqu'ils implémentent le même ensemble d'interfaces, telles que ICollection, IEnumerable, etc.
La différence clé vient lorsque la performance compte. Par exemple, si vous implémentez la liste ayant une opération "INSERT" lourde, LinkedList surpasse la liste. Étant donné que LinkedList peut le faire en O(1), il est possible que List ait besoin d'étendre la taille du tableau sous-jacent. Pour plus d'informations/de détails, vous pouvez lire la différence algorithmique entre les structures de données LinkedList et array. http://en.wikipedia.org/wiki/Linked_list et Array
J'espère que cette aide,
Ma réponse précédente n’était pas assez précise ... C'était aussi horrible que ça: D Mais maintenant je peux poster une réponse beaucoup plus utile et correcte.
J'ai fait des tests supplémentaires. Vous pouvez trouver sa source via le lien suivant et la vérifier à nouveau sur votre environnement: https://github.com/ukushu/DataStructuresTestsAndOther.git
Résultats brefs:
Les tableaux doivent utiliser:
Liste besoin d'utiliser:
LinkedList doit utiliser:
Plus de détails:
LinkedList<T>
en interne n'est pas une liste dans .NET. Il n'implémente même pas IList<T>
. Et c’est la raison pour laquelle il existe des index et des méthodes absents liés aux index.
LinkedList<T>
est une collection basée sur un nœud et un pointeur. Dans .NET, son implémentation est doublement liée. Cela signifie que les éléments précédents/suivants ont un lien avec l'élément en cours. Et les données sont fragmentées - différents objets de liste peuvent être situés à différents endroits de la RAM. En outre, il y aura plus de mémoire utilisée pour LinkedList<T>
que pour List<T>
ou Array.
List<T>
en .Net est l'alternative Java de ArrayList<T>
. Cela signifie qu'il s'agit d'un wrapper de tableau. Donc, il est alloué en mémoire comme un bloc de données contigu. Si la taille des données allouées dépasse 85 000 octets, elles seront déplacées vers le segment d'objets volumineux. Selon la taille, cela peut entraîner une fragmentation du tas (une forme légère de fuite de mémoire). Mais dans le même temps, si la taille est <85 000 octets - ceci fournit une représentation très compacte et à accès rapide en mémoire.
Le bloc unique contigu est préférable pour les performances d'accès aléatoire et la consommation de mémoire, mais pour les collections qui doivent régulièrement changer de taille, une structure telle qu'un tableau doit généralement être copiée vers un nouvel emplacement, tandis qu'une liste chaînée n'a besoin que de gérer la mémoire du nouvel élément inséré./noeuds supprimés.
Le principal avantage des listes chaînées par rapport aux tableaux est que les liens nous permettent de réorganiser efficacement les éléments . Sedgewick, p. 91
Une situation courante pour utiliser LinkedList est la suivante:
Supposons que vous vouliez supprimer de nombreuses chaînes d'une liste de chaînes de grande taille, par exemple 100 000. Les chaînes à supprimer peuvent être recherchées dans HashSet dic, et la liste des chaînes contient entre 30 000 et 60 000 chaînes à supprimer.
Alors quel est le meilleur type de liste pour stocker les 100 000 chaînes? La réponse est LinkedList. Si elles sont stockées dans une ArrayList, puis itérer dessus et supprimer les chaînes correspondantes devrait prendre jusqu'à plusieurs milliards d'opérations, alors qu'il faut environ 100 000 opérations en utilisant un itérateur et la méthode 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();
}
Lorsque vous avez besoin d'un accès indexé intégré, du tri (et après cette recherche binaire), et de la méthode "ToArray ()", vous devez utiliser la liste.
Ceci est adapté de la réponse acceptée de Tono Nam qui corrige quelques fausses mesures.
Le 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);
}
Et le code:
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();
}
}
}
Vous pouvez voir que les résultats sont conformes aux performances théoriques documentées ici. Tout à fait clair - LinkedList<T>
gagne beaucoup de temps en cas d’insertion. Je n'ai pas testé le retrait de la liste au milieu de la liste, mais le résultat devrait être identique. Bien sûr, List<T>
a d’autres domaines où il fonctionne beaucoup mieux que l’accès aléatoire O(1).
Essentiellement, un List<>
dans .NET est une enveloppe sur un array . Un LinkedList<>
est une liste chaînée . La question se pose donc de savoir quelle est la différence entre un tableau et une liste chaînée et quand faut-il utiliser un tableau au lieu d’une liste chaînée. Les deux facteurs les plus importants dans votre décision d'utilisation seraient:
Utilisez LinkedList<>
quand
Token Stream
.Pour tout le reste, il vaut mieux utiliser List<>
.
Je suis d'accord avec la plupart des remarques faites ci-dessus. Et je conviens également que List semble être un choix plus évident dans la plupart des cas.
Mais, je veux juste ajouter qu'il existe de nombreux cas où LinkedList est un meilleur choix que List pour une meilleure efficacité.
J'espère que quelqu'un trouvera ces commentaires utiles.