Perché iterare su un Elenco sarebbe più veloce che indicizzarlo?


125

Leggendo la documentazione Java per l'elenco ADT si dice:

L'interfaccia Elenco fornisce quattro metodi per l'accesso posizionale (indicizzato) agli elementi dell'elenco. Gli elenchi (come gli array Java) sono basati su zero. Si noti che queste operazioni possono essere eseguite in tempo proporzionale al valore dell'indice per alcune implementazioni (la classe LinkedList, ad esempio). Pertanto, l'iterazione sugli elementi in un elenco è in genere preferibile all'indicizzazione attraverso di esso se il chiamante non conosce l'implementazione.

Cosa significa esattamente? Non capisco le conclusioni tratte.


12
Un altro esempio che può aiutarti a capire il caso generale di questo è l'articolo di Joel Spolsky "Back to Basics" - cerca "l'algoritmo di Shlemiel il pittore".
un CVn

Risposte:


211

In un elenco collegato, ogni elemento ha un puntatore all'elemento successivo:

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

Per accedere item3, puoi vedere chiaramente che devi camminare dalla testa attraverso ogni nodo fino a raggiungere l'elemento 3, poiché non puoi saltare direttamente.

Pertanto, se volessi stampare il valore di ogni elemento, se scrivo questo:

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

quello che succede è questo:

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

Questo è orribilmente inefficiente perché ogni volta che si sta indicizzando si riavvia dall'inizio dell'elenco e passa attraverso ogni elemento. Ciò significa che la tua complessità è effettivamente O(N^2)solo per attraversare l'elenco!

Se invece ho fatto questo:

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

allora quello che succede è questo:

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

tutto in un unico attraversamento, che è O(N).

Ora, andare al altre applicazioni di Listcui è ArrayList, che uno è sostenuta da un semplice array. In quel caso entrambi i traversali di cui sopra sono equivalenti, poiché un array è contiguo, quindi consente salti casuali a posizioni arbitrarie.


29
Avviso minore: LinkedList cercherà dalla fine dell'elenco se l'indice si trova nella metà successiva dell'elenco, ma ciò non modifica realmente l'inefficienza fondamentale. Lo rende solo leggermente meno problematico.
Joachim Sauer,

8
Questo è orribilmente inefficiente . Per le LinkedList più grandi, sì, per le più piccole può funzionare più velocemente REVERSE_THRESHOLDè impostato su 18 java.util.Collections, è strano vedere una risposta così votata senza l'osservazione.
bestsss

1
@DanDiplo, se la struttura è collegata, sì, è vera. L'uso delle strutture LinkedS, tuttavia, è un piccolo mistero. Quasi sempre si comportano in modo molto peggiore di quelli supportati da array (footprint di memoria aggiuntivo, località scarsamente amichevole e terribile). L'elenco standard in C # è supportato da array.
bestsss

3
Avviso secondario: se si desidera verificare quale tipo di iterazione deve essere utilizzato (indicizzato rispetto a Iteratore / foreach), è sempre possibile verificare se un elenco implementa RandomAccess (un'interfaccia marker):List l = unknownList(); if (l instanceof RandomAccess) /* do indexed loop */ else /* use iterator/foreach */
afk5min

1
@ KK_07k11A0585: In realtà il ciclo for avanzato nel tuo primo esempio viene compilato in un iteratore come nel secondo esempio, quindi sono equivalenti.
Tudor,

35

La risposta è implicita qui:

Si noti che queste operazioni possono essere eseguite in tempo proporzionale al valore dell'indice per alcune implementazioni (la classe LinkedList, ad esempio)

Un elenco collegato non ha un indice intrinseco; la chiamata .get(x)richiederà l'implementazione dell'elenco per trovare la prima voce e chiamare .next()x-1 volte (per un O (n) o un accesso temporale lineare), in cui un elenco supportato da array può semplicemente indicizzare in backingarray[x]O (1) o tempo costante.

Se guardi JavaDoc perLinkedList , vedrai il commento

Tutte le operazioni vengono eseguite come previsto per un elenco doppiamente collegato. Le operazioni che indicizzano nell'elenco attraverseranno l'elenco dall'inizio o dalla fine, a seconda di quale sia il più vicino all'indice specificato.

mentre JavaDoc perArrayList ha il corrispondente

Implementazione di array ridimensionabili dell'interfaccia Elenco. Implementa tutte le operazioni dell'elenco facoltativo e consente tutti gli elementi, incluso null. Oltre all'implementazione dell'interfaccia Elenco, questa classe fornisce metodi per manipolare le dimensioni dell'array utilizzato internamente per memorizzare l'elenco. (Questa classe è approssimativamente equivalente a Vector, tranne per il fatto che non è sincronizzata.)

La size, isEmpty, get, set, iterator, e listIteratoroperazioni eseguire in tempo costante. L'operazione add viene eseguita in un tempo costante ammortizzato, ovvero l'aggiunta di n elementi richiede O (n) tempo. Tutte le altre operazioni vengono eseguite in un tempo lineare (approssimativamente parlando). Il fattore costante è basso rispetto a quello per l' LinkedListimplementazione.

Una domanda correlata intitolata "Riepilogo Big-O per Java Collections Framework" ha una risposta che punta a questa risorsa, "Java Collections JDK6" che potresti trovare utile.


7

Mentre la risposta accettata è sicuramente corretta, potrei sottolineare un piccolo difetto. Citando Tudor:

Ora, passando all'altra implementazione di List che è ArrayList, quella è supportata da un semplice array. In quel caso entrambi i traversali di cui sopra sono equivalenti , poiché un array è contiguo, quindi consente salti casuali a posizioni arbitrarie.

Questo non è del tutto vero. La verità è che

Con una ArrayList, un ciclo contato scritto a mano è circa 3 volte più veloce

fonte: Designing for Performance, il documento Android di Google

Si noti che il ciclo scritto a mano si riferisce all'iterazione indicizzata. Ho il sospetto che sia a causa dell'iteratore che viene utilizzato con i loop migliorati. Produce una prestazione minore in penalità in una struttura che è supportata da un array contiguo. Ho anche il sospetto che questo potrebbe essere vero per la classe Vector.

La mia regola è, quando possibile, utilizzare il ciclo avanzato per, e se ti interessa davvero le prestazioni, usa l'iterazione indicizzata solo per ArrayList o Vettori. Nella maggior parte dei casi, puoi persino ignorarlo: il compilatore potrebbe ottimizzarlo in background.

Voglio solo sottolineare che nel contesto dello sviluppo in Android, entrambi i passaggi di ArrayLists non sono necessariamente equivalenti . Solo spunti di riflessione.


La tua fonte è solo Anndroid. Questo vale anche per altre JVM?
Matsemann,

Nella maggior parte dei casi, non è completamente sicuro tbh, ma, di nuovo, l'utilizzo di Enhanced for loop dovrebbe essere l'impostazione predefinita.
Dhruv Gairola,

Per me ha senso, sbarazzarsi di tutta la logica dell'iteratore quando si accede a una struttura di dati che utilizza un array funziona più velocemente. Non so se 3 volte più veloce, ma sicuramente più veloce.
Setzer22

7

Scorrere su un elenco con un offset per la ricerca, come ad esempio i, è analogo all'algoritmo di Shlemiel del pittore .

Shlemiel ottiene un lavoro come pittore di strada, dipingendo le linee tratteggiate in mezzo alla strada. Il primo giorno prende una lattina di vernice sulla strada e termina a 300 iarde della strada. "È piuttosto buono!" dice il suo capo, "sei un lavoratore veloce!" e gli paga un copeco.

Il giorno successivo Shlemiel ottiene solo 150 iarde. "Beh, non è così buono come ieri, ma sei ancora un lavoratore veloce. 150 metri è rispettabile", e gli paga un copione.

Il giorno successivo Shlemiel dipinge 30 metri di strada. "Solo 30!" grida il suo capo. "È inaccettabile! Il primo giorno hai fatto dieci volte tanto lavoro! Cosa sta succedendo?"

"Non posso farci niente", dice Shlemiel. "Ogni giorno mi allontano sempre di più dal barattolo di vernice!"

Fonte .

Questa piccola storia può rendere più facile capire cosa sta succedendo internamente e perché è così inefficiente.


4

Per trovare l'i-esimo elemento di LinkedListun'implementazione passa attraverso tutti gli elementi fino all'i-esimo.

Così

for(int i = 0; i < list.length ; i++ ) {
    Object something = list.get(i); //Slow for LinkedList
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.