web-dev-qa-db-fra.com

Quand devrais-je utiliser une liste par rapport à une liste liée

Quand est-il préférable d'utiliser un List vs un LinkedList ?

355
Jonathan Allen

Modifier

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.

Réponse originale ...

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;
    }
}

Liste liée (3,9 secondes)

        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;

Liste (2,4 secondes)

        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)

Liste liée (51 secondes)

        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;

Liste (7.26 secondes)

        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;

Liste chaînée ayant la référence du lieu où insérer (0,04 seconde)

        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.

98
Tono Nam

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.

254
Marc Gravell

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:

Ajouter

  • LinkedList<T>.AddLast(item)temps constant
  • List<T>.Add(item)temps constant amorti, pire cas linéaire

Préfini

  • LinkedList<T>.AddFirst(item)temps constant
  • List<T>.Insert(0, item)temps linéaire

Insertion

  • LinkedList<T>.AddBefore(node, item)temps constant
  • LinkedList<T>.AddAfter(node, item)temps constant
  • List<T>.Insert(index, item)temps linéaire

Suppression

  • LinkedList<T>.Remove(item)temps linéaire
  • LinkedList<T>.Remove(node)temps constant
  • List<T>.Remove(item)temps linéaire
  • List<T>.RemoveAt(index)temps linéaire

Compter

  • LinkedList<T>.Countheure constante
  • List<T>.Countheure constante

Contient

  • LinkedList<T>.Contains(item)temps linéaire
  • List<T>.Contains(item)temps linéaire

Clair

  • LinkedList<T>.Clear()temps linéaire
  • List<T>.Clear()temps linéaire

Comme 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.

197
Drew Noakes

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

  • mettre à jour le pointeur dans le membre i-1 pour qu'il pointe vers le nouveau membre
  • positionner le pointeur dans le nouveau membre sur le membre 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é.

113
b3.

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,

17
user23117

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:

    • Si souvent que possible. C'est rapide et prend la plus petite plage RAM pour une information identique.
    • Si vous connaissez le nombre exact de cellules nécessaires
    • Si les données sont enregistrées dans le tableau <85000 b
    • Si nécessaire grande vitesse d'accès aléatoire
  • Liste besoin d'utiliser:

    • Si nécessaire pour ajouter des cellules à la fin de la liste (souvent)
    • Si nécessaire pour ajouter des cellules au début/au milieu de la liste (PAS SOUVENT)
    • Si les données sont enregistrées dans le tableau <85000 b
    • Si nécessaire grande vitesse d'accès aléatoire
  • LinkedList doit utiliser:

    • Si nécessaire pour ajouter des cellules au début/au milieu/à la fin de la liste (souvent)
    • Si nécessaire, uniquement un accès séquentiel (avant/arrière)
    • Si vous devez enregistrer de gros articles, le nombre d’articles est faible.
    • Mieux vaut ne pas utiliser pour une grande quantité d’éléments, car il utilise de la mémoire supplémentaire pour les liens.

Plus de détails:

 введите сюда описание изображения Intéressant à savoir:

  1. 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.

  2. 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.

  3. 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. 

  4. 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. 

14
Andrew

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

11
Dr. Alrawi

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();
}
2
Tom

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.

2
Michael Damatov

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).

1
nawfal

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:

  • Les listes chaînées ont de bien meilleures performances d’insertion/suppression, tant que les insertions/suppressions ne figurent pas sur le dernier élément de la collection. En effet, un tableau doit déplacer tous les éléments restants après le point d'insertion/suppression. Toutefois, si l'insertion/suppression se situe à la fin de la liste, ce changement n'est pas nécessaire (bien que le tableau puisse devoir être redimensionné si sa capacité est dépassée).
  • Les tableaux ont de bien meilleures capacités d’accès. Les tableaux peuvent être indexés directement (en temps constant). Les listes chaînées doivent être parcourues (temps linéaire).
1
iliketocode

Utilisez LinkedList<> quand

  1. Vous ne savez pas combien d'objets passent par la porte d'inondation. Par exemple, Token Stream.
  2. Lorsque vous vouliez UNIQUEMENT supprimer\insérer aux extrémités.

Pour tout le reste, il vaut mieux utiliser List<>.

0
Antony Thomas

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é.

  1. Supposons que vous parcouriez les éléments et que vous souhaitiez effectuer de nombreuses insertions/suppressions; LinkedList le fait en temps linéaire O(n), alors que List le fait en temps quadratique O (n ^ 2).
  2. Supposons que vous souhaitiez accéder à de plus gros objets encore et encore, LinkedList deviendra très utile.
  3. Deque () et queue () sont mieux implémentés en utilisant LinkedList.
  4. Augmenter la taille de LinkedList est beaucoup plus facile et meilleur une fois que vous avez affaire à de nombreux objets plus gros.

J'espère que quelqu'un trouvera ces commentaires utiles. 

0