web-dev-qa-db-fra.com

ArrayList Vs LinkedList

Je suivais un post précédent sur ce qui dit:

Pour LinkedList

  • get est O (n)
  • ajouter est O (1)
  • enlever est O (n)
  • Iterator.remove is O (1)

Pour ArrayList

  • get est O (1)
  • add est O(1) amorti, mais O(n) dans le pire des cas puisque le tableau doit être redimensionné et copié
  • enlever est O (n)

Donc, en regardant cela, j'ai conclu que si je devais juste insérer une insertion séquentielle dans ma collection pour, disons, 5000000 éléments, LinkedList surclassera ArrayList.

Et si je dois juste récupérer les éléments de la collection en effectuant une itération, c’est-à-dire en ne saisissant pas l’élément au milieu, toujours LinkedList surclassera `ArrayList.

Maintenant, pour vérifier mes deux déclarations ci-dessus, j’ai écrit ci-dessous un exemple de programme… Mais je suis surpris que mes déclarations ci-dessus se soient révélées fausses.

ArrayList surclassé Linkedlist dans les deux cas. Il a fallu moins de temps que LinkedList pour les ajouter et les récupérer à partir de Collection. Y a-t-il quelque chose que je ne fais pas correctement ou les déclarations initiales à propos de LinkedList et ArrayList ne sont pas vraies pour des collections de taille 5000000?

J'ai mentionné la taille, car si je réduisais le nombre d'éléments à 50000, LinkedList fonctionnait mieux et les instructions initiales étaient vraies.

long nano1 = System.nanoTime();

List<Integer> arr = new ArrayList();
for(int i = 0; i < 5000000; ++i) {
    arr.add(i);
}
System.out.println( (System.nanoTime() - nano1) );

for(int j : arr) {
    ;
}
System.out.println( (System.nanoTime() - nano1) );

long nano2 = System.nanoTime();

List<Integer> arrL = new LinkedList();
for(int i = 0; i < 5000000; ++i) {
    arrL.add(i);
}
System.out.println( (System.nanoTime() - nano2) );

for(int j : arrL) {
    ;
}
System.out.println( (System.nanoTime() - nano2) );
63
Vicky

N'oubliez pas que la complexité big-O décrit un comportement asymptotique et peut ne pas refléter la vitesse réelle de mise en œuvre. Il décrit comment le coût de chaque opération augmente avec la taille de la liste et non la vitesse de chaque opération. Par exemple, l'implémentation suivante de add est O(1) mais n'est pas rapide:

public class MyList extends LinkedList {
    public void add(Object o) {
        Thread.sleep(10000);
        super.add(o);
    }
}

Je pense que dans votre cas, ArrayList fonctionne bien car il augmente sa taille de la mémoire tampon interne de manière assez agressive, de sorte qu'il n'y aura pas un grand nombre de réaffectations. Lorsque le tampon n'a pas besoin d'être redimensionné, ArrayList aura plus rapidement adds.

Vous devez également faire très attention lorsque vous effectuez ce type de profilage. Je vous suggèrerais de modifier votre code de profilage pour effectuer une phase de préchauffage (pour que le JIT ait la possibilité de procéder à une optimisation sans affecter vos résultats) et de faire la moyenne des résultats sur plusieurs analyses.

private final static int WARMUP = 1000;
private final static int TEST = 1000;
private final static int SIZE = 500000;

public void perfTest() {
    // Warmup
    for (int i = 0; i < WARMUP; ++i) {
        buildArrayList();
    }
    // Test
    long sum = 0;
    for (int i = 0; i < TEST; ++i) {
        sum += buildArrayList();
    }
    System.out.println("Average time to build array list: " + (sum / TEST));
}

public long buildArrayList() {
    long start = System.nanoTime();
    ArrayList a = new ArrayList();
    for (int i = 0; i < SIZE; ++i) {
        a.add(i);
    }
    long end = System.nanoTime();
    return end - start;
}

... same for buildLinkedList

(Notez que sum risque de déborder et qu'il vaudrait mieux utiliser System.currentTimeMillis()).

Il est également possible que le compilateur optimise vos boucles vides get. Assurez-vous que la boucle fait réellement quelque chose pour vous assurer que le bon code est appelé.

50
Cameron Skinner

C'est un mauvais point de repère OMI.

  • besoin de répéter en boucle plusieurs fois pour réchauffer jvm
  • besoin de faire quelque chose dans votre boucle itérative ou il peut être optimisé tableau
  • ArrayList redimensionne, ce qui est coûteux. Si vous aviez construit ArrayList comme new ArrayList(500000), vous construiriez d'un seul coup et toutes les allocations seraient alors relativement économiques (un tableau sauvegardé préalablement).
  • Vous ne spécifiez pas votre machine virtuelle Java à mémoire; elle doit être exécutée avec -xMs == -Xmx (tout préalloué) et suffisamment élevé pour qu'aucun CPG ne soit susceptible de se déclencher.
  • Ce test ne couvre pas l’aspect le plus désagréable de LinkedList - l’accès aléatoire. (un itérateur n'est pas nécessairement la même chose). Si vous alimentez par exemple 10% de la taille d'une grande collection en tant que sélection aléatoire de list.get, Vous constaterez que les listes de liens sont horribles pour la capture de tout autre élément que le premier ou le dernier.

Pour un artiste: le jdk get est ce que vous attendez:

public E get(int index) {
    RangeCheck(index);

    return elementData[index];
}

(fondamentalement, il suffit de retourner l'élément de tableau indexé

Pour une liste chaînée:

public E get(int index) {
    return entry(index).element;
}

semble similaire? Pas assez. entry est une méthode et non un tableau primitif, et regardez ce qu’elle doit faire:

private Entry<E> entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException("Index: "+index+
                                            ", Size: "+size);
    Entry<E> e = header;
    if (index < (size >> 1)) {
        for (int i = 0; i <= index; i++)
            e = e.next;
    } else {
        for (int i = size; i > index; i--)
            e = e.previous;
    }
    return e;
}

C'est vrai, si vous demandez à dire list.get(250000), il faut commencer par la tête et parcourir plusieurs fois l'élément suivant. 250000 accès ou plus (il y a une optimisation dans le code qui commence en tête ou en queue en fonction de ce qui serait moins d'accès.)

20
MJB

Une ArrayList est une structure de données plus simple qu'une LinkedList. Un ArrayList a un tableau unique de pointeurs dans des emplacements de mémoire contigus. Il ne doit être recréé que si le tableau est développé au-delà de sa taille allouée.

Une LinkedList consiste en une chaîne de nœuds; chaque nœud est alloué séparément et possède des pointeurs avant et arrière vers les autres nœuds.

Qu'est-ce que cela signifie? À moins que vous n'ayez besoin d'insérer au milieu, d'épissure, de supprimer au milieu, etc., une ArrayList sera généralement plus rapide. Il nécessite moins d’allocations de mémoire, a une localité de référence bien meilleure (ce qui est important pour la mise en cache du processeur), etc.

12
seand

Pour comprendre pourquoi les résultats que vous avez obtenus ne contredisent pas la caractérisation du "gros O". Nous devons revenir aux principes premiers. c'est-à-dire la définition .

Soit f(x) et g(x)) deux fonctions définies sur un sous-ensemble de nombres réels. On écrit

f(x) = O(g(x)) as x -> infinity

si et seulement si, pour des valeurs de x suffisamment grandes, f(x) est au plus une constante multipliée par g(x) en valeur absolue. Cela is f(x) = O(g(x)) si et seulement s'il existe un nombre réel positif M et un nombre réel x0 tel que

|f(x)| <= M |g(x)| for all x > x_0.

Dans de nombreux contextes, l’hypothèse selon laquelle nous nous intéressons au taux de croissance lorsque la variable x va à l’infini n’est pas précisée, et l’on écrit plus simplement que f(x) = O (g )).

Donc, l'instruction add1 is O(1) signifie que le coût en temps d'une opération add1 Sur une liste de taille N tend vers une constante Cadd1 comme N tend vers l'infini.

Et l’énoncé add2 is O(1) amortized over N operations, signifie que le coût en temps moyen d’une opération de la séquence de N add2 Tend vers une constante Cadd2 comme N tend vers l'infini.

Ce qui ne dit pas ce sont ces constantes Cadd1 et Cadd2 sont. En fait, la raison pour laquelle LinkedList est plus lente que ArrayList dans votre référence est que Cadd1 est plus grand que Cadd2.

La leçon à tirer est que la grande notation O ne prévoit pas une performance absolue ni même une performance relative. Tout ce qu'il prédit est la forme de la fonction de performance car la variable de contrôle devient très grande. C'est utile à savoir, mais cela ne vous dit pas tout ce que vous devez savoir.

6
Stephen C

1) Structure de données sous-jacente La première différence entre ArrayList et LinkedList réside dans le fait que ArrayList est sauvegardé par Array alors que LinkedList est sauvegardé par LinkedList. Cela entraînera d'autres différences de performances.

2) LinkedList implémente Deque Une autre différence entre ArrayList et LinkedList est que, mis à part l'interface List, LinkedList implémente également l'interface Deque, qui fournit les opérations premier entré premier sorti pour add (), poll () et plusieurs autres. Fonctions Deque. ) Ajout d'éléments dans ArrayList L'ajout d'élément dans ArrayList est une opération O(1) si elle ne déclenche pas la redimensionnement de Array, auquel cas il devient O (log ( n)), D'autre part, ajouter un élément dans LinkedList est une opération O(1), car elle ne nécessite aucune navigation.

4) Suppression d'un élément d'une position Pour supprimer un élément d'un index particulier, par ex. en appelant remove (index), ArrayList effectue une opération de copie qui le rapproche de O(n) tandis que LinkedList doit passer à ce point, ce qui le rend également O (n/2), car il peut le faire de chaque direction en fonction de la proximité.

5) Itération sur ArrayList ou LinkedList L'itération est l'opération O(n) pour LinkedList et ArrayList, où n est le numéro d'un élément.

6) Récupération d'un élément d'une position L'opération get (index) est O(1) dans ArrayList alors que son O(n/2) dans LinkedList, comme il faut traverser jusqu'à cette entrée. Cependant, dans la notation Big O, O(n/2) n'est que O(n) parce que nous ignorons les constantes.

7) Memory LinkedList utilise un objet wrapper, Entry, qui est une classe imbriquée statique pour le stockage de données et deux nœuds next et previous, tandis que ArrayList ne stocke que des données dans Array.

Par conséquent, les besoins en mémoire semblent moins, dans le cas de ArrayList, que dans celui de LinkedList, sauf dans le cas où Array effectue l'opération de redimensionnement lorsqu'il copie le contenu d'un tableau à un autre.

Si Array est suffisamment grand, cela peut prendre beaucoup de mémoire et déclencher un Garbage Collection, ce qui peut ralentir le temps de réponse.

Parmi toutes les différences entre ArrayList et LinkedList ci-dessus, il semble que ArrayList soit le meilleur choix que LinkedList dans presque tous les cas, sauf lorsque vous effectuez une opération add () fréquente plutôt que remove () ou get ().

Il est plus facile de modifier une liste chaînée que ArrayList, en particulier si vous ajoutez ou supprimez des éléments de début ou de fin, car la liste chaînée conserve en interne les références de ces positions et elles sont accessibles dans O(1).

En d'autres termes, vous n'avez pas besoin de parcourir la liste liée pour atteindre la position où vous souhaitez ajouter des éléments. Dans ce cas, l'addition devient l'opération O(n). Par exemple, insérer ou supprimer un élément au milieu d'une liste liée.

À mon avis, utilisez ArrayList sur LinkedList pour la plupart des applications pratiques en Java.

1
Anjali Suman

La notation big-O ne concerne pas les timings absolus, mais les timings relatifs et vous ne pouvez pas comparer les nombres d'un algorithme à un autre.

Vous obtenez uniquement des informations sur la réaction du même algorithme lorsque le nombre de n-uplets augmente ou diminue.

Un algorithme peut prendre une heure pour une opération, et 2h pour deux opérations, et est O (n), et un autre est O(n) aussi, et prend une milliseconde pour une opération et deux millisecondes. pour deux opérations.

Un autre problème en cas de mesure avec la machine virtuelle Java est l'optimisation du compilateur hotspot. Une boucle do-none peut être éliminée par le compilateur JIT.

Une troisième chose à considérer est le système d'exploitation et la machine virtuelle, utilisant des caches et exécutant le garbage collection.

1
user unknown

Il est difficile de trouver un bon cas d'utilisation pour LinkedList. Si vous devez uniquement utiliser l'interface Dequeu, vous devriez probablement utiliser ArrayDeque. Si vous avez vraiment besoin d'utiliser l'interface List, vous entendrez souvent la suggestion d'utiliser toujours ArrayList car LinkedList se comporte très mal lors de l'accès à un élément aléatoire.

Malheureusement, ArrayList rencontre également des problèmes de performances si des éléments au début ou au milieu de la liste doivent être supprimés ou insérés.

Il existe cependant une nouvelle implémentation de liste appelée GapList qui combine les forces de ArrayList et de LinkedList. Il a été conçu pour remplacer à la fois ArrayList et LinkedList et implémente donc les interfaces List et Deque. De plus, toutes les méthodes publiques fournies par ArrayList sont implémentées (EnsureCapacty, trimToSize).

L'implémentation de GapList garantit un accès aléatoire efficace aux éléments par index (comme le fait ArrayList), ainsi que l'ajout et la suppression efficaces d'éléments en début et en fin de liste (comme le fait LinkedList).

Vous trouverez plus d'informations sur GapList à l'adresse http://Java.dzone.com/articles/gaplist-%E2%80%93-lightning-fast-list .

1
Thomas Mauch

L'analyse en notation fournit des informations importantes, mais elle a ses limites. Par définition, l'analyse en notation O considère que l'exécution de chaque opération nécessite environ le même temps, ce qui n'est pas vrai. Comme @seand l'a souligné, les listes chaînées utilisent en interne une logique plus complexe pour insérer et récupérer des éléments (regardez le code source, vous pouvez ctrl + clic dans votre IDE). En interne, ArrayList n'a besoin que d'insérer des éléments dans un tableau et d'augmenter sa taille de temps en temps (l'opération étant même une opération o(n), elle peut être réalisée assez rapidement).

À votre santé

0
Anthony Accioly

Vous pouvez séparer ajouter ou supprimer en deux étapes.

LinkedList: Si vous ajoutez un élément à l'index n, vous pouvez déplacer le pointeur de 0 à n-1, vous pouvez alors effectuer ce que vous appelez O(1) Ajouter une opération. Supprimer l'opération est la même.


ArraryList: ArrayList implémente l'interface RandomAccess, ce qui signifie qu'il peut accéder à un élément dans O (1).
Si vous ajoutez un élément dans l’indice n, il peut aller à l’indice n-1 dans O (1), déplacer les éléments après n-1, ajouter définir l’élément dans le logement n.
L'opération de déplacement est effectuée par une méthode native appelée System.arraycopy, c'est assez rapide.

public static void main(String[] args) {

    List<Integer> arrayList = new ArrayList<Integer>();
    for (int i = 0; i < 100000; i++) {
        arrayList.add(i);
    }

    List<Integer> linkList = new LinkedList<Integer>();

    long start = 0;
    long end = 0;
    Random random = new Random();

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        linkList.add(random.nextInt(100000), 7);
    }
    end = System.currentTimeMillis();
    System.out.println("LinkedList add ,random index" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        arrayList.add(random.nextInt(100000), 7);
    }
    end = System.currentTimeMillis();
    System.out.println("ArrayList add ,random index" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        linkList.add(0, 7);
    }
    end = System.currentTimeMillis();
    System.out.println("LinkedList add ,index == 0" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        arrayList.add(0, 7);
    }
    end = System.currentTimeMillis();
    System.out.println("ArrayList add ,index == 0" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        linkList.add(i);
    }
    end = System.currentTimeMillis();
    System.out.println("LinkedList add ,index == size-1" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        arrayList.add(i);
    }
    end = System.currentTimeMillis();
    System.out.println("ArrayList add ,index == size-1" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        linkList.remove(Integer.valueOf(random.nextInt(100000)));
    }
    end = System.currentTimeMillis();
    System.out.println("LinkedList remove ,random index" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        arrayList.remove(Integer.valueOf(random.nextInt(100000)));
    }
    end = System.currentTimeMillis();
    System.out.println("ArrayList remove ,random index" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        linkList.remove(0);
    }
    end = System.currentTimeMillis();
    System.out.println("LinkedList remove ,index == 0" + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        arrayList.remove(0);
    }
    end = System.currentTimeMillis();
    System.out.println("ArrayList remove ,index == 0" + (end - start));

}
0
dingjsh