web-dev-qa-db-fra.com

Pourquoi l'itération sur une liste serait-elle plus rapide que son indexation?

Lire la documentation Java pour la liste ADT il dit:

L'interface List propose quatre méthodes d'accès positionnel (indexé) aux éléments de la liste. Les listes (comme Java) sont basées sur zéro. Notez que ces opérations peuvent s'exécuter dans le temps proportionnellement à la valeur d'index pour certaines implémentations (la classe LinkedList, par exemple). Ainsi, l'itération sur les éléments dans une liste est généralement préférable à l'indexation à travers elle si l'appelant ne connaît pas l'implémentation.

Qu'est-ce que cela signifie exactement? Je ne comprends pas la conclusion qui est tirée.

123
nomel7

Dans une liste chaînée, chaque élément a un pointeur sur l'élément suivant:

head -> item1 -> item2 -> item3 -> etc.

Pour accéder à item3, Vous pouvez voir clairement que vous devez marcher de la tête à travers chaque nœud jusqu'à ce que vous atteigniez l'élément3, car vous ne pouvez pas sauter directement.

Ainsi, si je voulais imprimer la valeur de chaque élément, si j'écris ceci:

for(int i = 0; i < 4; i++) {
    System.out.println(list.get(i));
}

ce qui se passe est ceci:

head -> print head
head -> item1 -> print item1
head -> item1 -> item2 -> print item2
head -> item1 -> item2 -> item3 print item3

C'est horriblement inefficace parce que chaque fois que vous indexez, il redémarre depuis le début de la liste et passe en revue chaque élément. Cela signifie que votre complexité est effectivement O(N^2) juste pour parcourir la liste!

Si au lieu de cela je faisais ceci:

for(String s: list) {
    System.out.println(s);
}

alors ce qui se passe est le suivant:

head -> print head -> item1 -> print item1 -> item2 -> print item2 etc.

le tout dans une seule traversée, qui est O(N).

Passons maintenant à l'autre implémentation de List qui est ArrayList, celle-ci est soutenue par un simple tableau. Dans ce cas, les deux traversées ci-dessus sont équivalentes, puisqu'un tableau est contigu, il permet donc des sauts aléatoires à des positions arbitraires.

210
Tudor

La réponse est implicite ici:

Notez que ces opérations peuvent s'exécuter dans le temps proportionnellement à la valeur d'index pour certaines implémentations (la classe LinkedList, par exemple)

Une liste chaînée n'a pas d'index inhérent; appeler .get(x) nécessitera l'implémentation de la liste pour trouver la première entrée et appeler .next() x-1 fois (pour un O(n) ou temps linéaire access), où une liste basée sur un tableau peut simplement indexer dans backingarray[x] in O(1) ou temps constant.

Si vous regardez le JavaDoc pour LinkedList , vous verrez le commentaire

Toutes les opérations fonctionnent comme on pouvait s'y attendre pour une liste à double liaison. Les opérations qui indexent dans la liste traverseront la liste depuis le début ou la fin, selon la valeur la plus proche de l'index spécifié.

alors que JavaDoc pour ArrayList a le correspondant

Implémentation d'un tableau redimensionnable de l'interface List. Implémente toutes les opérations de liste facultatives et autorise tous les éléments, y compris null. En plus d'implémenter l'interface List, cette classe fournit des méthodes pour manipuler la taille du tableau utilisé en interne pour stocker la liste. (Cette classe est à peu près équivalente à Vector, sauf qu'elle n'est pas synchronisée.)

Les opérations size, isEmpty, get, set, iterator et listIterator s'exécutent en temps constant. L'opération d'ajout s'exécute en temps constant amorti, c'est-à-dire que l'ajout de n éléments nécessite O(n) temps. Toutes les autres opérations s'exécutent en temps linéaire (grosso modo). Le facteur constant est faible par rapport à celle de l'implémentation LinkedList.

A ne question connexe intitulée "Big-O Summary for Java Collections Framework" a une réponse pointant vers cette ressource, "Java Collections JDK6" que vous pourriez trouver utile.

35
andersoj

Itérer sur une liste avec un décalage pour la recherche, comme i, est similaire à Shlemiel l'algorithme du peintre .

Shlemiel obtient un emploi de peintre de rue, peignant les lignes pointillées au milieu de la route. Le premier jour, il prend une boîte de peinture sur la route et termine à 300 mètres de la route. "C'est plutôt bien!" dit son patron, "vous êtes un travailleur rapide!" et lui paie un kopeck.

Le lendemain, Shlemiel ne fait que 150 mètres. "Eh bien, ce n'est pas aussi bon qu'hier, mais vous êtes toujours un travailleur rapide. 150 mètres est respectable", et lui paie un kopeck.

Le lendemain, Shlemiel peint 30 mètres de la route. "Seulement 30!" crie son patron. "C'est inacceptable! Le premier jour, tu as fait dix fois plus de travail! Que se passe-t-il?"

"Je ne peux pas m'en empêcher", explique Shlemiel. "Chaque jour, je m'éloigne de plus en plus du pot de peinture!"

Source .

Cette petite histoire peut faciliter la compréhension de ce qui se passe en interne et pourquoi elle est si inefficace.

7
alex

Bien que la réponse acceptée soit très certainement correcte, je voudrais signaler un défaut mineur. Citant Tudor:

Passons maintenant à l'autre implémentation de List qui est ArrayList, celle-ci est soutenue par un simple tableau. Dans ce cas, les deux traversées ci-dessus sont équivalentes , car un tableau est contigu et permet donc des sauts aléatoires à des positions arbitraires.

Ce n'est pas tout à fait vrai. La vérité c'est que

Avec une ArrayList, une boucle comptée manuscrite est environ 3 fois plus rapide

source: Designing for Performance, Google Android doc

Notez que la boucle manuscrite fait référence à l'itération indexée. Je soupçonne que c'est à cause de l'itérateur qui est utilisé avec des boucles améliorées pour. Il produit une performance mineure en pénalité dans une structure qui est soutenue par un tableau contigu. Je soupçonne également que cela pourrait être vrai pour la classe Vector.

Ma règle est, utilisez la boucle améliorée pour autant que possible, et si vous vous souciez vraiment des performances, utilisez l'itération indexée uniquement pour les listes de tableaux ou les vecteurs. Dans la plupart des cas, vous pouvez même ignorer cela - le compilateur peut l'optimiser en arrière-plan.

Je veux simplement souligner que dans le contexte du développement sous Android, les traversées des ArrayLists ne sont pas pas nécessairement équivalentes . Juste matière à réflexion.

7
Dhruv Gairola

Pour trouver le i-ème élément d'un LinkedList, l'implémentation passe par tous les éléments jusqu'au i-ème.

Donc

for(int i = 0; i < list.length ; i++ ) {
    Object something = list.get(i); //Slow for LinkedList
}
4
esej