Por que a iteração sobre uma lista seria mais rápida do que a indexação?

125

Lendo a documentação Java para a Lista ADT , diz:

A interface List fornece quatro métodos para acesso posicional (indexado) aos elementos da lista. As listas (como matrizes Java) são baseadas em zero. Observe que essas operações podem ser executadas no tempo proporcional ao valor do índice para algumas implementações (a classe LinkedList, por exemplo). Portanto, a iteração sobre os elementos em uma lista é geralmente preferível à indexação através dela, se o chamador não souber a implementação.

O que exatamente isso significa? Eu não entendo a conclusão que é desenhada.

nomel7
fonte
12
Outro exemplo que pode ajudar você a entender o caso geral disso é o artigo de Joel Spolsky "Back to Basics" - procure por "Shlemiel, o algoritmo do pintor".
um CVn

Respostas:

211

Em uma lista vinculada, cada elemento possui um ponteiro para o próximo elemento:

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

Para acessar item3, você pode ver claramente que precisa percorrer todos os nós da cabeça até chegar ao item3, pois não pode pular diretamente.

Assim, se eu quisesse imprimir o valor de cada elemento, se eu escrever isso:

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

o que acontece é o seguinte:

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

Isso é terrivelmente ineficiente porque toda vez que você está indexando, ele é reiniciado no início da lista e passa por todos os itens. Isso significa que sua complexidade é efetivamente O(N^2)apenas para percorrer a lista!

Se, em vez disso, eu fiz isso:

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

então o que acontece é o seguinte:

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

tudo em uma única travessia, o que é O(N).

Agora, indo para o outro implementação do Listque é ArrayList, que um é apoiado por uma matriz simples. Nesse caso, as duas travessias acima são equivalentes, uma vez que uma matriz é contígua e permite saltos aleatórios em posições arbitrárias.

tudor
fonte
29
Aviso menor: o LinkedList pesquisará no final da lista se o índice estiver na metade posterior da lista, mas isso realmente não altera a ineficiência fundamental. Isso o torna apenas um pouco menos problemático.
Joachim Sauer
8
Isso é terrivelmente ineficiente . Para linksList maiores, sim, para menores, ele pode funcionar mais rápido com REVERSE_THRESHOLD18 java.util.Collectionspolegadas, é estranho ver uma resposta tão votada sem a observação.
bestsss
1
@ DanDiplo, se a estrutura estiver vinculada, sim, ela será verdadeira. Usar estruturas do LinkedS, no entanto, é um pequeno mistério. Eles quase sempre apresentam desempenho muito pior do que os suportados por array (pegada de memória extra, hostilidade do GC e local terrível). A lista padrão em C # é suportada por matriz.
bestsss
3
Aviso Menor: Se você quiser verificar que tipo de iteração deve ser utilizado (indexado vs Iterator / foreach), você sempre pode testar se uma lista implementos RandomAccess (a interface de marcador):List l = unknownList(); if (l instanceof RandomAccess) /* do indexed loop */ else /* use iterator/foreach */
afk5min
1
@ KK_07k11A0585: Na verdade, o loop for aprimorado em seu primeiro exemplo é compilado em um iterador, como no segundo exemplo, para que sejam equivalentes.
Tudor
35

A resposta está implícita aqui:

Observe que essas operações podem ser executadas em tempo proporcional ao valor do índice para algumas implementações (a classe LinkedList, por exemplo)

Uma lista vinculada não tem um índice inerente; a chamada .get(x)exigirá que a implementação da lista encontre a primeira entrada e chame .next()x-1 vezes (para acesso O (n) ou tempo linear), onde uma lista baseada em matriz pode apenas indexar backingarray[x]em O (1) ou tempo constante.

Se você olhar para o JavaDocLinkedList , verá o comentário

Todas as operações têm o desempenho esperado para uma lista duplamente vinculada. As operações indexadas na lista percorrerão a lista desde o início ou o final, o que estiver mais próximo do índice especificado.

enquanto que JavaDoc paraArrayList tem o correspondente

Implementação de redimensionável array da interface da lista. Implementa todas as operações de lista opcionais e permite todos os elementos, incluindo nulo. Além de implementar a interface List, essa classe fornece métodos para manipular o tamanho da matriz usada internamente para armazenar a lista. (Essa classe é aproximadamente equivalente a Vector, exceto que não é sincronizada.)

A size, isEmpty, get, set,iterator , e listIteratoroperações executados em tempo constante. A operação de adição é executada em tempo constante amortizado, ou seja, adicionar n elementos requer O (n) tempo. Todas as outras operações são executadas em tempo linear (grosso modo). O fator constante é baixo comparado ao da LinkedListimplementação.

Uma pergunta relacionada intitulada "Resumo do Big-O para Java Collections Framework" possui uma resposta apontando para este recurso, "Java Collections JDK6", que você pode achar útil.

andersoj
fonte
7

Embora a resposta aceita esteja certamente correta, posso apontar uma falha menor. Citando Tudor:

Agora, indo para a outra implementação da List que é ArrayList, essa é suportada por uma matriz simples. Nesse caso, as duas travessias acima são equivalentes , uma vez que uma matriz é contígua e permite saltos aleatórios em posições arbitrárias.

Isto não é completamente verdadeiro. A verdade é aquilo

Com um ArrayList, um loop contado manuscrito é cerca de 3x mais rápido

fonte: Designing for Performance, documento Android do Google

Observe que o loop manuscrito se refere à iteração indexada. Eu suspeito que é por causa do iterador que é usado com aprimorada para loops. Produz um desempenho menor na penalidade em uma estrutura que é apoiada por uma matriz contígua. Eu também suspeito que isso possa ser verdade para a classe Vector.

Minha regra é, use o loop for aprimorado sempre que possível e, se você realmente se importa com o desempenho, use a iteração indexada apenas para ArrayLists ou Vectors. Na maioria dos casos, você pode até ignorar isso - o compilador pode estar otimizando isso em segundo plano.

Quero apenas ressaltar que, no contexto do desenvolvimento no Android, ambas as travessias de ArrayLists não são necessariamente equivalentes . Apenas comida para pensar.

Dhruv Gairola
fonte
Sua fonte é apenas o Anndroid. Isso vale para outras JVMs também?
Matsemann 10/10/12
Não tenho certeza absoluta tbh, mas novamente, o uso de aprimorados para loops deve ser o padrão na maioria dos casos.
Dhruv Gairola
Faz sentido para mim, livrar-se de toda a lógica do iterador ao acessar uma estrutura de dados que usa uma matriz funciona mais rápido. Não sei se é 3x mais rápido, mas certamente mais rápido.
precisa saber é o seguinte
7

A iteração sobre uma lista com um deslocamento para a pesquisa, como i, é análogo ao algoritmo do pintor Shlemiel .

Shlemiel consegue um emprego como pintor de rua, pintando as linhas pontilhadas no meio da estrada. No primeiro dia, ele leva uma lata de tinta para a estrada e termina a 300 jardas da estrada. "Isso é muito bom!" diz seu chefe, "você é um trabalhador rápido!" e paga-lhe um copeque.

No dia seguinte, Shlemiel faz apenas 150 jardas. "Bem, isso não é tão bom quanto ontem, mas você ainda é um trabalhador rápido. 150 jardas são respeitáveis", e lhe paga um copeque.

No dia seguinte, Shlemiel pinta 30 jardas da estrada. "Apenas 30!" grita seu chefe. "Isso é inaceitável! No primeiro dia você trabalhou dez vezes mais! O que está acontecendo?"

"Não posso evitar", diz Shlemiel. "Todo dia eu fico cada vez mais longe da lata de tinta!"

Fonte .

Essa pequena história pode facilitar a compreensão do que está acontecendo internamente e por que é tão ineficiente.

alex
fonte
4

Para encontrar o i-ésimo elemento de uma LinkedListimplementação, percorre todos os elementos até o i-ésimo.

assim

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